diff --git a/apps/README.md b/apps/README.md
index 259e2b8..d27d648 100644
--- a/apps/README.md
+++ b/apps/README.md
@@ -4,7 +4,7 @@
|---|---|---|
| [`ui/`](ui/README.md) | shared Vue 3 + Tailwind design system: tokens, fonts, icons, components | in use by admin and web |
| [`admin/`](admin/README.md) | admin web panel, served by its own optional `warp-admin` binary | done for the current admin API |
-| [`web/`](web/README.md) | web client (Vue SPA on matrix-js-sdk), served by its own optional `warp-web` binary | rooms, timeline, sending text and files, invites; E2EE next |
+| [`web/`](web/README.md) | web client (Vue SPA on matrix-js-sdk), served by its own optional `warp-web` binary | rooms, timeline, files, replies, reactions, threads, spaces, E2EE with QR and emoji verification, calls |
| `desktop/` | Wails v3 shell reusing the web frontend | next: web has a timeline |
| [`mobile/`](mobile/README.md) | Flutter app on the famedly Matrix SDK, Android first | rooms, timeline, files, replies, reactions, threads, spaces, E2EE with QR and emoji verification, push, calls, sliding sync |
diff --git a/apps/ui/src/icons/index.ts b/apps/ui/src/icons/index.ts
index 7a108df..b929a1f 100644
--- a/apps/ui/src/icons/index.ts
+++ b/apps/ui/src/icons/index.ts
@@ -45,7 +45,9 @@ export type IconName =
| 'settings'
| 'shield-alert'
| 'shield-check'
+ | 'space'
| 'sun'
+ | 'thread'
| 'trash'
| 'user'
| 'users'
diff --git a/apps/ui/src/icons/space.svg b/apps/ui/src/icons/space.svg
new file mode 100644
index 0000000..b30c951
--- /dev/null
+++ b/apps/ui/src/icons/space.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/apps/ui/src/icons/thread.svg b/apps/ui/src/icons/thread.svg
new file mode 100644
index 0000000..1463971
--- /dev/null
+++ b/apps/ui/src/icons/thread.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/apps/web/README.md b/apps/web/README.md
index b81b1d1..77f166a 100644
--- a/apps/web/README.md
+++ b/apps/web/README.md
@@ -46,6 +46,29 @@ that one origin.
then *Read* when someone else has read it; a failed one says *Not sent*
with Retry and Delete.
- Read receipts, typing notifications, deleting messages.
+- Replies: *Reply* on a message puts it above the composer, and the
+ reply quotes it (sender and first line); the quote goes to the message.
+ Replies are sent without the old quoted fallback in their body
+ (Matrix 1.13), and the fallback in older ones is not shown.
+- Reactions: six quick ones, or any other emoji typed in, from a
+ message's *React* button; chips under the message with counts, yours
+ highlighted. A chip adds your reaction or takes it back, and says who
+ reacted.
+- Threads: *Reply in thread* opens the thread beside the room, its root
+ and replies, with a composer that writes into it. Replies stay out of
+ the room's timeline; the root shows "N replies", the last reply's
+ sender and time. *Threads* in the room header lists the room's threads
+ (`/threads`). Read receipts are per thread (MSC3771): the room is
+ marked read with `thread_id` "main", a thread with its root's ID, so
+ reading one leaves the others unread. Unread counts come per thread
+ from the homeserver (MSC3773) and threads with replies you haven't
+ read have a dot, on the root, in the list and on the *Threads* button.
+- Spaces: when you're in any, the top of the room list picks *All chats*
+ or one space, which narrows the list to its rooms and its subspaces'.
+ A space's page (`/hierarchy`) lists its rooms and subspaces with *Open*
+ or *Join*; its admins add rooms you're in, create new ones in it, and
+ take rooms out. *New space* in the + menu and in the picker. Invites to
+ spaces show with the others.
- End-to-end encryption, with the Rust crypto stack matrix-js-sdk loads
(`matrix-sdk-crypto-wasm`): reading and sending in encrypted rooms,
files included (encrypted in the browser before they are uploaded, with
@@ -90,7 +113,7 @@ that one origin.
- Settings: display name, avatar, theme, encryption.
- Phone layout: one pane at a time, the room list is the home screen.
-**Not yet:** replies, reactions, threads, spaces, notifications.
+**Not yet:** editing messages, notifications.
### Encryption keys in the browser
diff --git a/apps/web/src/components/CreateRoomDialog.vue b/apps/web/src/components/CreateRoomDialog.vue
index 74a8a66..adcae32 100644
--- a/apps/web/src/components/CreateRoomDialog.vue
+++ b/apps/web/src/components/CreateRoomDialog.vue
@@ -4,8 +4,16 @@ import { WBanner, WButton, WDialog, WIcon, WSwitch, WTextArea, WTextField } from
import { errorMessage } from '@/lib/format'
import { useChat } from '@/stores/chat'
+// A room, a room in a space (`spaceId`), or a space (`kind` "space").
+const props = withDefaults(defineProps<{ kind?: 'room' | 'space'; spaceId?: string; spaceName?: string }>(), {
+ kind: 'room',
+ spaceId: undefined,
+ spaceName: undefined,
+})
const open = defineModel('open', { default: false })
const emit = defineEmits<{ created: [roomId: string] }>()
+const isSpace = computed(() => props.kind === 'space')
+const noun = computed(() => (isSpace.value ? 'space' : 'room'))
const chat = useChat()
const name = ref('')
@@ -22,7 +30,8 @@ watch(open, (v) => {
topic.value = ''
isPublic.value = false
alias.value = ''
- encrypted.value = chat.canEncrypt
+ // A space holds no conversation to protect: never encrypted.
+ encrypted.value = chat.canEncrypt && !isSpace.value
error.value = ''
})
@@ -40,7 +49,7 @@ watch(name, (n, old) => {
})
const aliasError = computed(() =>
- isPublic.value && alias.value && !/^[a-z0-9._=-]+$/.test(alias.value) ? 'Use lowercase letters, digits and . _ = -' : undefined,
+ isPublic.value && !isSpace.value && alias.value && !/^[a-z0-9._=-]+$/.test(alias.value) ? 'Use lowercase letters, digits and . _ = -' : undefined,
)
async function submit() {
@@ -48,13 +57,16 @@ async function submit() {
busy.value = true
error.value = ''
try {
- const roomId = await chat.createRoom({
- name: name.value,
- topic: topic.value,
- isPublic: isPublic.value,
- alias: alias.value,
- encrypted: encrypted.value,
- })
+ const roomId = isSpace.value
+ ? await chat.createSpace({ name: name.value, topic: topic.value, isPublic: isPublic.value })
+ : await chat.createRoom({
+ name: name.value,
+ topic: topic.value,
+ isPublic: isPublic.value,
+ alias: alias.value,
+ encrypted: encrypted.value,
+ spaceId: props.spaceId,
+ })
open.value = false
emit('created', roomId)
} catch (e) {
@@ -66,19 +78,22 @@ async function submit() {
-
+
diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts
index 1804c63..f54a3dd 100644
--- a/apps/web/vite.config.ts
+++ b/apps/web/vite.config.ts
@@ -1,3 +1,6 @@
+import { existsSync } from 'node:fs'
+import { createRequire } from 'node:module'
+import { dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
import tailwindcss from '@tailwindcss/vite'
import vue from '@vitejs/plugin-vue'
@@ -8,10 +11,33 @@ import { defineConfig } from 'vite'
// know where the homeserver is. WARP_WEB_HOMESERVER points it elsewhere.
const homeserver = process.env.WARP_WEB_HOMESERVER ?? 'http://localhost:8008'
-export default defineConfig({
+// The Rust crypto loads its .wasm relative to its own module
+// (new URL(..., import.meta.url)). Vite's dependency pre-bundling moves the
+// module into node_modules/.vite/deps without the .wasm, so the dev server
+// answered with index.html and initRustCrypto failed. The dev server serves
+// the package as it is instead. It is only matrix-js-sdk's dependency, which
+// pnpm doesn't make reachable from the pre-bundled matrix-js-sdk, so it is
+// aliased to the very copy matrix-js-sdk resolves. A production build is
+// bundled by Rollup and needs neither.
+const cryptoWasm = '@matrix-org/matrix-sdk-crypto-wasm'
+
+function cryptoWasmDir(): string {
+ const sdk = createRequire(import.meta.url).resolve('matrix-js-sdk')
+ let dir = dirname(createRequire(sdk).resolve(cryptoWasm))
+ while (!existsSync(join(dir, 'package.json'))) dir = dirname(dir)
+ return dir
+}
+
+export default defineConfig(({ command }) => ({
plugins: [vue(), tailwindcss()],
resolve: {
- alias: { '@': fileURLToPath(new URL('./src', import.meta.url)) },
+ alias: {
+ '@': fileURLToPath(new URL('./src', import.meta.url)),
+ ...(command === 'serve' ? { [cryptoWasm]: cryptoWasmDir() } : {}),
+ },
+ },
+ optimizeDeps: {
+ exclude: [cryptoWasm],
},
build: {
// Embedded into the warp-web binary by client/ (and the desktop app).
@@ -30,4 +56,4 @@ export default defineConfig({
'/.well-known/matrix/client': { target: homeserver, changeOrigin: true },
},
},
-})
+}))
diff --git a/server/internal/setup/routes.go b/server/internal/setup/routes.go
index 25b5760..0f804b9 100644
--- a/server/internal/setup/routes.go
+++ b/server/internal/setup/routes.go
@@ -20,7 +20,16 @@ import (
// client to be used. Advertising v1.11 or later is also what makes a client
// use the authenticated media endpoints, the only download endpoints Warp
// serves by default.
-var SpecVersions = []string{"v1.13", "v1.14"}
+//
+// The earlier v1.x versions are listed too: each is a subset of the
+// later ones, and clients test for the exact version a feature came in.
+// matrix-js-sdk only turns on threads (v1.3), unread counts per thread
+// (v1.4), recursive relations (v1.10) and intentional mentions (v1.7)
+// when that version is in the list.
+var SpecVersions = []string{
+ "v1.1", "v1.2", "v1.3", "v1.4", "v1.5", "v1.6", "v1.7",
+ "v1.8", "v1.9", "v1.10", "v1.11", "v1.12", "v1.13", "v1.14",
+}
// UnstableFeatures advertises MSC support. Simplified Sliding Sync
// (MSC4186) is how Element X and the matrix-rust-sdk find out they can