Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
4a4ebf0
feat(communities): open new members in a welcome channel and let admi…
matt2e Nov 5, 2026
050c674
fix(communities): check an unconfirmed welcome save, hold the reader …
matt2e Nov 5, 2026
b9c9a1f
fix(communities): re-record the reader after the welcome edge correct…
matt2e Nov 5, 2026
ec9a4dc
feat(communities): titled Markdown welcome and starter channels in Se…
matt2e Nov 6, 2026
91ad129
fix(communities): review follow-ups for the titled welcome and starte…
matt2e Nov 6, 2026
3b7cec2
feat(communities): searchable starter channels and a composer editor …
matt2e Nov 6, 2026
c54c962
fix(communities): announce starter changes and cap channel browsing i…
matt2e Nov 6, 2026
d2ede99
fix(communities): open the welcome channel on a fresh load and close …
matt2e Nov 6, 2026
a185989
fix(communities): let a later welcome channel replace a provisional d…
matt2e Nov 6, 2026
cde8a1a
fix(communities): switch to the welcome channel only on its own visit…
matt2e Nov 6, 2026
0a1eb4f
fix(communities): keep a provisional welcome choice through a route h…
matt2e Nov 6, 2026
6373e2b
fix(relay): show a confirmed starter flag in the cached directory
matt2e Nov 6, 2026
2bc034d
fix(communities): save starter changes with the welcome, drop the pre…
matt2e Nov 6, 2026
166f610
fix(channels): wait for the relay's welcome channel before opening an…
matt2e Nov 6, 2026
1ce2153
fix(communities): settle review findings on the welcome form and open…
matt2e Nov 6, 2026
60d3985
fix(communities): announce starter undos, keep the starter list acros…
matt2e Nov 6, 2026
18653e2
fix(channels): rename the welcome opening's list-loaded ref after rebase
matt2e Nov 6, 2026
4f6169f
fix(communities): style the welcome editor's link chips as messages do
matt2e Nov 6, 2026
0119931
feat(relay): admit one author-only read without kinds
matt2e Nov 6, 2026
24c0ae2
feat(communities): pin the welcome above the composer for new members
matt2e Nov 6, 2026
ee4c15e
refactor(messages): restore the timeline to main now that the welcome…
matt2e Nov 6, 2026
1f746f0
fix(communities): bind the welcome read to the viewer, forget it on l…
matt2e Nov 6, 2026
91d89ac
fix(communities): reference the global stylesheet from the welcome ca…
matt2e Nov 6, 2026
6389f83
fix(communities): format the welcome command check; let the policy re…
matt2e Nov 6, 2026
93bc40c
docs(communities): the welcome read is bound to the viewer; leaving c…
matt2e Nov 6, 2026
f9ac098
test(communities): fix the welcome tests on their first vitest run
matt2e Nov 6, 2026
6466ab8
test(fixtures): provide adminAccess to the channels plugin in the bro…
matt2e Nov 9, 2026
665ba45
test(communities): adapt the welcome tests to main's navigation and l…
matt2e Nov 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 88 additions & 1 deletion browser-host/relay-broker-api.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3397,6 +3397,54 @@ test("lifecycle uses dedicated shape-limited host routes, never the message writ
["p", transport.viewer],
],
},
{
...template,
kind: 9002,
tags: [
["h", id],
["landing", "false"],
["welcome", ""],
["welcome_title", ""],
],
},
{
...template,
kind: 9002,
tags: [
["h", id],
["landing", "true"],
["welcome", "a\rb"],
["welcome_title", ""],
],
},
{
...template,
kind: 9002,
tags: [
["h", id],
["landing", "true"],
["welcome", ""],
],
},
{
...template,
kind: 9002,
tags: [
["h", id],
["landing", "true"],
["welcome", ""],
["welcome_title", "two\nlines"],
],
},
{
...template,
kind: 9002,
tags: [
["h", id],
["landing", "true"],
["name", "rename"],
],
},
{ ...template, content: "extra" },
{
...template,
Expand Down Expand Up @@ -3463,7 +3511,23 @@ test("lifecycle uses dedicated shape-limited host routes, never the message writ
expect(flagged).toMatchObject(flag);
await transport.channelLifecycle.publish(flagged, signal);
}
expect(h.publications).toHaveLength(4);
const landing = {
...template,
kind: 9002,
tags: [
["h", id],
["landing", "true"],
["welcome", "# Say hi\n\n\t**here**"],
["welcome_title", "Hello"],
],
};
expect((await h.post("sign", landing)).status).toBe(400);
expect((await h.post("channel-details-sign", landing)).status).toBe(400);
const designated = await transport.channelLifecycle.sign(landing, signal);
expect(verifyEvent(designated)).toBe(true);
expect(designated).toMatchObject(landing);
await transport.channelLifecycle.publish(designated, signal);
expect(h.publications).toHaveLength(5);
} finally {
live?.dispose();
await h.close();
Expand Down Expand Up @@ -4151,6 +4215,29 @@ test.each([[], [1, 42], [1, 42, 43]])(
},
);

test("join discovery projects welcome-channel support and designation", async () => {
const landing = "6f1c2a4e-0b7d-4c1e-9a52-2f0d8b3c7e11";
const h = await harness((call) =>
Response.json(
call.url === fixtureRelayUrl
? {
name: "Community",
supported_extensions: ["buzz-landing-channel"],
landing_channel: landing,
}
: { policy: null },
),
);
try {
expect(await (await h.get("info")).json()).toMatchObject({
landingChannels: true,
landingChannel: landing,
});
} finally {
await h.close();
}
});

test("Builderlab HTTP accepts full agent instructions and isolates the fixed native routes", async () => {
let openLogin;
const opened = new Promise((resolve) => {
Expand Down
10 changes: 7 additions & 3 deletions browser-host/relay-broker.mjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { isSidebarPreferencesBatch } from "../src/features/relay/sidebar-preferences.ts";
import { isEarlierActivityFilter } from "../src/features/relay/earlier-activity.ts";
import {
isCatalogKind,
MAX_EVENT_BYTES as CATALOG_EVENT_BYTES,
Expand Down Expand Up @@ -26,6 +27,7 @@ import {
} from "../src/features/repos/git.ts";
import { validateMemberAdministrationTemplate } from "../src/features/channel-members/administration-protocol.ts";
import { validateLifecycleTemplate } from "../src/features/relay/channel-lifecycle-protocol.ts";
import { landingInfo } from "../src/features/relay/landing.ts";
import { validateDetailsTemplate } from "../src/features/relay/channel-details-protocol.ts";
import { validateArchiveRequestTemplate } from "../src/features/relay/identity-archive-protocol.ts";
import {
Expand Down Expand Up @@ -749,7 +751,7 @@ export function validChannelActivityFilters(filters) {
)
);
}
export function validFilters(filters) {
export function validFilters(filters, viewer) {
return (
Array.isArray(filters) &&
filters.length >= 1 &&
Expand All @@ -767,7 +769,8 @@ export function validFilters(filters) {
)) ||
(filter.kinds === undefined &&
Array.isArray(filter.ids) &&
filter.ids.length > 0)) &&
filter.ids.length > 0) ||
isEarlierActivityFilter(filter, viewer)) &&
Number.isInteger(filter.limit) &&
filter.limit >= 1 &&
filter.limit <= MAX_LIMIT,
Expand Down Expand Up @@ -1319,6 +1322,7 @@ export function relayBrokerPlugin({
name: info.name,
icon: info.icon,
policy: policy ?? null,
...landingInfo(info),
...(gifSearchPath
? {
supported_extensions: ["buzz-gif"],
Expand Down Expand Up @@ -3146,7 +3150,7 @@ export function relayBrokerPlugin({
!readPublishing &&
!snapshot &&
!channelActivity &&
!validFilters(filters)
!validFilters(filters, viewer)
)
return json(res, 400, { error: "Read filter rejected" });
if (publishing || readPublishing) {
Expand Down
19 changes: 18 additions & 1 deletion browser-host/relay-broker.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ test("read broker accepts non-channel finite reads without relaxing filter budge
{ kinds: [9], "#e": ["root"], depth_limit: 10, limit: 80 },
])
assert.equal(validFilters([filter]), true);
// The welcome card's one question: has the viewer written anything yet?
const viewer = "a".repeat(64);
assert.equal(
validFilters([{ authors: [viewer], until: 1788810000, limit: 1 }], viewer),
true,
);
for (const filters of [
[],
Array(5).fill({ kinds: [0], limit: 1 }),
Expand All @@ -25,8 +31,19 @@ test("read broker accepts non-channel finite reads without relaxing filter budge
[{ kinds: [65536], limit: 1 }],
[{ kinds: [], limit: 1 }],
[{ limit: 1 }],
// Only that exact shape, about the broker's own identity, may omit kinds.
[{ authors: [viewer], limit: 1 }],
[{ authors: [viewer], until: 1788810000, limit: 2 }],
[{ authors: [viewer, "b".repeat(64)], until: 1, limit: 1 }],
[{ authors: [viewer], until: 1, limit: 1, "#h": ["channel"] }],
[{ authors: ["viewer"], until: 1, limit: 1 }],
[{ authors: ["b".repeat(64)], until: 1788810000, limit: 1 }],
])
assert.equal(validFilters(filters), false);
assert.equal(validFilters(filters, viewer), false);
assert.equal(
validFilters([{ authors: [viewer], until: 1788810000, limit: 1 }]),
false,
);
});

test("broker signing is limited to bounded channel messages and canonical direct and nested replies", () => {
Expand Down
32 changes: 32 additions & 0 deletions docs/buzz-help.md
Original file line number Diff line number Diff line change
Expand Up @@ -462,6 +462,38 @@ revokes all of them, and a pairing unused for 30 days expires.
- **You don't have access to this community**: when you're signed in through
the community's server, it names the account it refused and offers
**Switch account**. **Check again** once an admin has added you.
- **Welcome channel**: on relays that support it, a community can choose a
public channel that new members join and open first. There, a new member
sees a welcome card above the message box, showing the community's message
or a default one. Choose **Dismiss** to hide it on this computer; it stays
hidden even if the title or message changes, though a welcome moved to
another channel can show once more there. People who had already written in the
community never see it, though someone who has never posted may see it once
on each new device. The card isn't a message: it can't be replied to or
deleted. While
a new member's channels arrive, Messages says it is opening them, and waits a
few seconds for the welcome channel rather than open another conversation
first. If none arrive, **Try again** checks for them again; a welcome channel
that arrives later still opens. Owners and admins choose the channel and edit
the card's title and message in **Settings → Community admin → Welcome**. The
message editor has the composer's formatting toolbar and shortcuts. The
default title names the community when the relay or you have given it a
name, and is just "Welcome" otherwise. The message is saved as Markdown, up
to 4,000 characters,
and a published message opens as its Markdown source; identity links name
people without notifying them. Paste a channel's **Copy link** to link the
channel, or a person's **Copy npub** to link the person. People already in
the community keep their channels when the welcome channel changes.
- **Starter channels**: in the same **Welcome** settings, owners and admins
choose which other open channels new members join, even on relays without a
welcome channel. Only starter channels are listed, each with **Remove**;
**Add a channel** searches the others. Until you type a name, this picker and
the welcome channel picker list the first 50 channels. Adding and removing
change the draft; **Save** publishes them with the welcome, and **Cancel**
discards them. A saved change applies to people who join
afterwards, not to current members; in an open community it also adds
returning people who were never in that channel. Archiving a starter channel
or making it private pauses it until it is public again.
- **Finding channels**: search's **Channels** tab lists the community's open
channels you can join, with their member counts; typing also matches channels
you're in. Opening an unjoined channel shows a preview with **Join**. Private
Expand Down
Loading
Loading