feat: scaffold marmot chat backend

Pin marmot-ts 0.5.1, expose Marmot chat routes/configuration, and keep the web UI disabled until transport, storage, and signing adapters are implemented.
This commit is contained in:
2026-05-25 05:47:49 +02:00
parent 421fa01076
commit d257221dc8
7 changed files with 590 additions and 29 deletions
+464 -1
View File
@@ -6,7 +6,470 @@
"packages": {
"": {
"name": "aether-assets",
"version": "0.1.0"
"version": "0.1.0",
"dependencies": {
"@internet-privacy/marmot-ts": "0.5.1"
}
},
"node_modules/@hpke/common": {
"version": "1.10.1",
"resolved": "https://registry.npmjs.org/@hpke/common/-/common-1.10.1.tgz",
"integrity": "sha512-moJwhmtLtuxiUzzNp1jpfBfx8yefKoO9D/RCR9dmwrnc7qjJqId1rEtQz+lSlU5cabX8daToMSx/7HayXOiaFw==",
"license": "MIT",
"engines": {
"node": ">=16.0.0"
}
},
"node_modules/@hpke/core": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/@hpke/core/-/core-1.9.0.tgz",
"integrity": "sha512-pFxWl1nNJeQCSUFs7+GAblHvXBCjn9EPN65vdKlYQil2aURaRxfGMO6vBKGqm1YHTKwiAxJQNEI70PbSowMP9Q==",
"license": "MIT",
"dependencies": {
"@hpke/common": "^1.10.0"
},
"engines": {
"node": ">=16.0.0"
}
},
"node_modules/@internet-privacy/marmot-ts": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/@internet-privacy/marmot-ts/-/marmot-ts-0.5.1.tgz",
"integrity": "sha512-AZ9924Dz75CHbB+YJPmEGj09xYIYAHpyDCq1xn++ubQjP95sQuV4yNXX5CmPQNXcyEMd50LL5dhJOMuH8gmPsg==",
"license": "MIT",
"dependencies": {
"@hpke/core": "^1.9.0",
"@noble/ciphers": "^2.1.1",
"@noble/curves": "^2.0.1",
"@noble/hashes": "^2.0.1",
"@scure/base": "^2.0.0",
"applesauce-common": "^5.1.0",
"applesauce-core": "^5.1.0",
"debug": "^4.4.3",
"eventemitter3": "^5.0.4",
"ts-mls": "2.0.0-rc.10"
},
"engines": {
"bun": ">=1.1.0",
"deno": ">=2.0.0",
"node": ">=20.0.0"
}
},
"node_modules/@noble/ciphers": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-2.1.1.tgz",
"integrity": "sha512-bysYuiVfhxNJuldNXlFEitTVdNnYUc+XNJZd7Qm2a5j1vZHgY+fazadNFWFaMK/2vye0JVlxV3gHmC0WDfAOQw==",
"license": "MIT",
"engines": {
"node": ">= 20.19.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@noble/curves": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@noble/curves/-/curves-2.0.1.tgz",
"integrity": "sha512-vs1Az2OOTBiP4q0pwjW5aF0xp9n4MxVrmkFBxc6EKZc6ddYx5gaZiAsZoq0uRRXWbi3AT/sBqn05eRPtn1JCPw==",
"license": "MIT",
"dependencies": {
"@noble/hashes": "2.0.1"
},
"engines": {
"node": ">= 20.19.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@noble/curves/node_modules/@noble/hashes": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.0.1.tgz",
"integrity": "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==",
"license": "MIT",
"engines": {
"node": ">= 20.19.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@noble/hashes": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.2.0.tgz",
"integrity": "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg==",
"license": "MIT",
"engines": {
"node": ">= 20.19.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@scure/base": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@scure/base/-/base-2.2.0.tgz",
"integrity": "sha512-b8XEupJibegiXV+tDUseI8oLQc8ei3d/4Jkb2RpbHh3MfE054ov3uIz2dhFkB3FI8iwYkEh0gGCApkrYggkPNg==",
"license": "MIT",
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@scure/bip32": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.3.1.tgz",
"integrity": "sha512-osvveYtyzdEVbt3OfwwXFr4P2iVBL5u1Q3q4ONBfDY/UpOuXmOlbgwc1xECEboY8wIays8Yt6onaWMUdUbfl0A==",
"license": "MIT",
"dependencies": {
"@noble/curves": "~1.1.0",
"@noble/hashes": "~1.3.1",
"@scure/base": "~1.1.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@scure/bip32/node_modules/@noble/curves": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.1.0.tgz",
"integrity": "sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA==",
"license": "MIT",
"dependencies": {
"@noble/hashes": "1.3.1"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@scure/bip32/node_modules/@noble/curves/node_modules/@noble/hashes": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.1.tgz",
"integrity": "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==",
"license": "MIT",
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@scure/bip32/node_modules/@noble/hashes": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.3.tgz",
"integrity": "sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA==",
"license": "MIT",
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@scure/bip32/node_modules/@scure/base": {
"version": "1.1.9",
"resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.9.tgz",
"integrity": "sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg==",
"license": "MIT",
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@scure/bip39": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.2.1.tgz",
"integrity": "sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg==",
"license": "MIT",
"dependencies": {
"@noble/hashes": "~1.3.0",
"@scure/base": "~1.1.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@scure/bip39/node_modules/@noble/hashes": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.3.tgz",
"integrity": "sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA==",
"license": "MIT",
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@scure/bip39/node_modules/@scure/base": {
"version": "1.1.9",
"resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.9.tgz",
"integrity": "sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg==",
"license": "MIT",
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/applesauce-common": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/applesauce-common/-/applesauce-common-5.2.0.tgz",
"integrity": "sha512-6Natb0szkj65OBKK5LBHIszAmVd8ha9GkCZcavJnZbNeWBgoDTO0hfkgI4pk2L6L/OWNceo2XCajvDAx0AjlgQ==",
"license": "MIT",
"dependencies": {
"@scure/base": "^1.2.4",
"applesauce-core": "^5.2.0",
"hash-sum": "^2.0.0",
"light-bolt11-decoder": "^3.2.0",
"rxjs": "^7.8.1"
},
"funding": {
"type": "lightning",
"url": "lightning:nostrudel@geyser.fund"
}
},
"node_modules/applesauce-common/node_modules/@scure/base": {
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/@scure/base/-/base-1.2.6.tgz",
"integrity": "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==",
"license": "MIT",
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/applesauce-core": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/applesauce-core/-/applesauce-core-5.2.0.tgz",
"integrity": "sha512-aSuM6q6/Gs2FGUqytlHDjKZpSst2xKaT0vMXUQFWUctECNIxvwy6/hTDDInukMuI9mrQdjnO781ZJJgghI7RNw==",
"license": "MIT",
"dependencies": {
"debug": "^4.4.0",
"fast-deep-equal": "^3.1.3",
"hash-sum": "^2.0.0",
"nanoid": "^5.0.9",
"nostr-tools": "~2.19",
"rxjs": "^7.8.1"
},
"funding": {
"type": "lightning",
"url": "lightning:nostrudel@geyser.fund"
}
},
"node_modules/debug": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/eventemitter3": {
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz",
"integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==",
"license": "MIT"
},
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
"license": "MIT"
},
"node_modules/hash-sum": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/hash-sum/-/hash-sum-2.0.0.tgz",
"integrity": "sha512-WdZTbAByD+pHfl/g9QSsBIIwy8IT+EsPiKDs0KNX+zSHhdDLFKdZu0BQHljvO+0QI/BasbMSUa8wYNCZTvhslg==",
"license": "MIT"
},
"node_modules/light-bolt11-decoder": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/light-bolt11-decoder/-/light-bolt11-decoder-3.2.0.tgz",
"integrity": "sha512-3QEofgiBOP4Ehs9BI+RkZdXZNtSys0nsJ6fyGeSiAGCBsMwHGUDS/JQlY/sTnWs91A2Nh0S9XXfA8Sy9g6QpuQ==",
"license": "MIT",
"dependencies": {
"@scure/base": "1.1.1"
}
},
"node_modules/light-bolt11-decoder/node_modules/@scure/base": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.1.tgz",
"integrity": "sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA==",
"funding": [
{
"type": "individual",
"url": "https://paulmillr.com/funding/"
}
],
"license": "MIT"
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/nanoid": {
"version": "5.1.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.11.tgz",
"integrity": "sha512-v+KEsUv2ps74PaSKv0gHTxTCgMXOIfBEbaqa6w6ISIGC7ZsvHN4N9oJ8d4cmf0n5oTzQz2SLmThbQWhjd/8eKg==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"bin": {
"nanoid": "bin/nanoid.js"
},
"engines": {
"node": "^18 || >=20"
}
},
"node_modules/nostr-tools": {
"version": "2.19.4",
"resolved": "https://registry.npmjs.org/nostr-tools/-/nostr-tools-2.19.4.tgz",
"integrity": "sha512-qVLfoTpZegNYRJo5j+Oi6RPu0AwLP6jcvzcB3ySMnIT5DrAGNXfs5HNBspB/2HiGfH3GY+v6yXkTtcKSBQZwSg==",
"license": "Unlicense",
"dependencies": {
"@noble/ciphers": "^0.5.1",
"@noble/curves": "1.2.0",
"@noble/hashes": "1.3.1",
"@scure/base": "1.1.1",
"@scure/bip32": "1.3.1",
"@scure/bip39": "1.2.1",
"nostr-wasm": "0.1.0"
},
"peerDependencies": {
"typescript": ">=5.0.0"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/nostr-tools/node_modules/@noble/ciphers": {
"version": "0.5.3",
"resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-0.5.3.tgz",
"integrity": "sha512-B0+6IIHiqEs3BPMT0hcRmHvEj2QHOLu+uwt+tqDDeVd0oyVzh7BPrDcPjRnV1PV/5LaknXJJQvOuRGR0zQJz+w==",
"license": "MIT",
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/nostr-tools/node_modules/@noble/curves": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz",
"integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==",
"license": "MIT",
"dependencies": {
"@noble/hashes": "1.3.2"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/nostr-tools/node_modules/@noble/curves/node_modules/@noble/hashes": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz",
"integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==",
"license": "MIT",
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/nostr-tools/node_modules/@noble/hashes": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.1.tgz",
"integrity": "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==",
"license": "MIT",
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/nostr-tools/node_modules/@scure/base": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.1.tgz",
"integrity": "sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA==",
"funding": [
{
"type": "individual",
"url": "https://paulmillr.com/funding/"
}
],
"license": "MIT"
},
"node_modules/nostr-wasm": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/nostr-wasm/-/nostr-wasm-0.1.0.tgz",
"integrity": "sha512-78BTryCLcLYv96ONU8Ws3Q1JzjlAt+43pWQhIl86xZmWeegYCNLPml7yQ+gG3vR6V5h4XGj+TxO+SS5dsThQIA==",
"license": "MIT"
},
"node_modules/rxjs": {
"version": "7.8.2",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
"integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==",
"license": "Apache-2.0",
"dependencies": {
"tslib": "^2.1.0"
}
},
"node_modules/ts-mls": {
"version": "2.0.0-rc.10",
"resolved": "https://registry.npmjs.org/ts-mls/-/ts-mls-2.0.0-rc.10.tgz",
"integrity": "sha512-4FFbkysQkJlVaUv4fs7ZC4wuiwTOCgX/bEMo2fFGngy/QofC/lvWWgsScKuGxEIhEf4e2Q8APfJ7DknP0VRCoA==",
"license": "MIT",
"dependencies": {
"@hpke/core": "1.9.0"
},
"peerDependencies": {
"@hpke/chacha20poly1305": "1.8.0",
"@hpke/dhkem-x448": "1.8.0",
"@hpke/hybridkem-x-wing": "0.7.0",
"@hpke/ml-kem": "0.3.0",
"@noble/ciphers": "2.1.1",
"@noble/curves": "2.0.1",
"@noble/post-quantum": "0.5.2"
},
"peerDependenciesMeta": {
"@hpke/chacha20poly1305": {
"optional": true
},
"@hpke/dhkem-x448": {
"optional": true
},
"@hpke/hybridkem-x-wing": {
"optional": true
},
"@hpke/ml-kem": {
"optional": true
},
"@noble/curves": {
"optional": true
},
"@noble/post-quantum": {
"optional": true
}
}
},
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
}
}
}
+3
View File
@@ -4,5 +4,8 @@
"version": "0.1.0",
"scripts": {
"build": "mkdir -p ../priv/static && cp -r css/. ../priv/static && cp -r js/. ../priv/static"
},
"dependencies": {
"@internet-privacy/marmot-ts": "0.5.1"
}
}
+3
View File
@@ -41,6 +41,9 @@ defmodule Aether.Chat do
def embed_path(%Channel{slug: slug}), do: embed_path(slug)
def embed_path(slug) when is_binary(slug), do: "/aether/chat/embed/" <> slug
def marmot_path(%Channel{slug: slug}), do: marmot_path(slug)
def marmot_path(slug) when is_binary(slug), do: "/aether/chat/marmot/" <> slug
def ensure_context_channel(provider, type, id, attrs \\ %{}, opts \\ [])
when is_binary(provider) and is_binary(type) and is_binary(id) and is_map(attrs) do
attrs
+62 -27
View File
@@ -9,9 +9,9 @@ defmodule AetherWeb.ChatLive do
@impl true
def mount(_params, session, socket) do
{mode, slug} = channel_request(session)
%{mode: mode, slug: slug, backend: backend} = channel_request(session)
case Chat.ensure_channel(%{slug: slug, title: channel_title(slug)}) do
case Chat.ensure_channel(%{slug: slug, title: channel_title(slug), backend: backend}) do
{:ok, %Channel{} = channel} ->
messages = load_messages(channel)
message_ids = messages |> Enum.map(& &1.id) |> MapSet.new()
@@ -21,6 +21,7 @@ defmodule AetherWeb.ChatLive do
|> assign(:page_title, channel.title)
|> assign(:channel, channel)
|> assign(:embedded?, mode == :embedded)
|> assign(:backend, channel.backend)
|> assign(:message_ids, message_ids)
|> assign(:message_count, MapSet.size(message_ids))
|> stream_configure(:messages, dom_id: &("chat-message-" <> &1.id))
@@ -38,6 +39,7 @@ defmodule AetherWeb.ChatLive do
|> assign(:page_title, "Chat")
|> assign(:channel, nil)
|> assign(:embedded?, mode == :embedded)
|> assign(:backend, backend)
|> assign(:message_ids, MapSet.new())
|> assign(:message_count, 0)
|> assign(:chat_error, inspect(reason))
@@ -82,6 +84,7 @@ defmodule AetherWeb.ChatLive do
channel={@channel}
current_user={@current_user}
embedded?={@embedded?}
backend={@backend}
message_count={@message_count}
streams={@streams}
/>
@@ -116,6 +119,7 @@ defmodule AetherWeb.ChatLive do
channel={@channel}
current_user={@current_user}
embedded?={@embedded?}
backend={@backend}
message_count={@message_count}
streams={@streams}
/>
@@ -146,6 +150,14 @@ defmodule AetherWeb.ChatLive do
<span class="text-xs text-base-content/60">{@message_count}</span>
</div>
<div
:if={@backend == :marmot}
id="chat-marmot-notice"
class="border-b border-warning/30 bg-warning/10 px-4 py-3 text-sm text-warning-content"
>
Marmot backend scaffold is enabled for this channel. Browser transport, storage, and signing are not active yet.
</div>
<div
id="chat-messages"
phx-hook="AetherChatScroll"
@@ -185,26 +197,31 @@ defmodule AetherWeb.ChatLive do
</article>
</div>
<%= if @current_user do %>
<form id="chat-message-form" phx-submit="send_message" class="border-t border-base-300 bg-base-100 p-3 sm:p-4">
<div class="flex items-end gap-2 rounded-2xl border border-base-300 bg-base-200/60 p-2">
<label for="chat-message-body" class="sr-only">Message</label>
<textarea
id="chat-message-body"
name="message[body]"
rows="2"
class="textarea textarea-ghost min-h-12 flex-1 resize-none bg-transparent focus:outline-none"
placeholder={"Message #{if @channel, do: @channel.title, else: "chat"}"}
></textarea>
<button id="chat-send-button" type="submit" class="btn btn-primary rounded-full">
Send
</button>
<%= cond do %>
<% @backend == :marmot -> %>
<div id="chat-marmot-disabled" class="border-t border-base-300 bg-base-100 p-5 text-center text-sm text-base-content/70">
Marmot chat is not enabled in the web UI yet.
</div>
<% @current_user -> %>
<form id="chat-message-form" phx-submit="send_message" class="border-t border-base-300 bg-base-100 p-3 sm:p-4">
<div class="flex items-end gap-2 rounded-2xl border border-base-300 bg-base-200/60 p-2">
<label for="chat-message-body" class="sr-only">Message</label>
<textarea
id="chat-message-body"
name="message[body]"
rows="2"
class="textarea textarea-ghost min-h-12 flex-1 resize-none bg-transparent focus:outline-none"
placeholder={"Message #{if @channel, do: @channel.title, else: "chat"}"}
></textarea>
<button id="chat-send-button" type="submit" class="btn btn-primary rounded-full">
Send
</button>
</div>
</form>
<% true -> %>
<div id="chat-signed-out" class="border-t border-base-300 bg-base-100 p-5 text-center text-sm text-base-content/70">
Sign in to join the chat.
</div>
</form>
<% else %>
<div id="chat-signed-out" class="border-t border-base-300 bg-base-100 p-5 text-center text-sm text-base-content/70">
Sign in to join the chat.
</div>
<% end %>
</div>
"""
@@ -251,15 +268,33 @@ defmodule AetherWeb.ChatLive do
defp channel_request(%{"plugin_path" => path}) when is_binary(path) do
case String.split(path, "/", trim: true) do
["aether", "chat", "embed", slug | _rest] -> {:embedded, slug}
["plugins", "aether", "chat", "embed", slug | _rest] -> {:embedded, slug}
["aether", "chat", slug | _rest] -> {:standalone, slug}
["plugins", "aether", "chat", slug | _rest] -> {:standalone, slug}
_other -> {:standalone, Chat.default_channel_slug()}
["aether", "chat", "embed", slug | _rest] ->
request(:embedded, slug, :public_sync)
["plugins", "aether", "chat", "embed", slug | _rest] ->
request(:embedded, slug, :public_sync)
["aether", "chat", "marmot", slug | _rest] ->
request(:standalone, slug, :marmot)
["plugins", "aether", "chat", "marmot", slug | _rest] ->
request(:standalone, slug, :marmot)
["aether", "chat", slug | _rest] ->
request(:standalone, slug, :public_sync)
["plugins", "aether", "chat", slug | _rest] ->
request(:standalone, slug, :public_sync)
_other ->
request(:standalone, Chat.default_channel_slug(), :public_sync)
end
end
defp channel_request(_session), do: {:standalone, Chat.default_channel_slug()}
defp channel_request(_session),
do: request(:standalone, Chat.default_channel_slug(), :public_sync)
defp request(mode, slug, backend), do: %{mode: mode, slug: slug, backend: backend}
defp channel_title("general"), do: "General Chat"
+37 -1
View File
@@ -41,7 +41,43 @@ defmodule Tribes.Plugins.Aether.Plugin do
layout: nil
}
],
ash_domains: [Aether.Chat]
ash_domains: [Aether.Chat],
config_schema: %{
title: "Aether",
description: "Social feed and chat defaults.",
groups: [
%{
id: "chat",
label: "Chat",
description: "Group chat backend defaults.",
order: 10,
settings: [
%{
key: "chat.default_backend",
label: "Default chat backend",
description:
"New standalone channels use public synced chat until Marmot is explicitly selected.",
type: :enum,
default: "public_sync",
options: [
%{label: "Public synced", value: "public_sync"},
%{label: "Marmot scaffold", value: "marmot"}
],
order: 10
},
%{
key: "chat.marmot_enabled",
label: "Enable Marmot chat UI",
description:
"Reserved for the Marmot browser transport once storage, relay, and signing adapters are complete.",
type: :boolean,
default: false,
order: 20
}
]
}
]
}
})
end
end
+10
View File
@@ -37,6 +37,15 @@ defmodule Aether.ChatPageTest do
refute has_element?(view, "#aether-chat-page")
end
test "renders Marmot scaffold without public composer", %{signed_in_conn: conn} do
slug = "marmot-chat-#{System.unique_integer([:positive])}"
{:ok, view, _html} = live(conn, "/aether/chat/marmot/#{slug}")
assert has_element?(view, "#chat-marmot-notice")
assert has_element?(view, "#chat-marmot-disabled")
refute has_element?(view, "#chat-message-form")
end
test "ensures context group channels with stable slugs" do
attrs = %{
context_provider: "sender",
@@ -52,6 +61,7 @@ defmodule Aether.ChatPageTest do
assert first.slug == "sender-stream-stream-123"
assert first.conversation_kind == :context_group
assert Chat.embed_path(first) == "/aether/chat/embed/sender-stream-stream-123"
assert Chat.marmot_path(first) == "/aether/chat/marmot/sender-stream-stream-123"
end
test "ensures context group channels through provider helper" do
+11
View File
@@ -0,0 +1,11 @@
defmodule Aether.MarmotAssetsTest do
use ExUnit.Case, async: true
@package_path Path.expand("../../assets/package.json", __DIR__)
test "pins marmot-ts to the selected client version" do
package = @package_path |> File.read!() |> JSON.decode!()
assert get_in(package, ["dependencies", "@internet-privacy/marmot-ts"]) == "0.5.1"
end
end