feat(web)!: sign in before joining a space and drop the stranger option

Opening a space link while signed out shows the sign-in page first and keeps
the link in memory. The channel settings no longer offer the stranger switch,
and the client supports API contract major 8.
This commit is contained in:
justme committed 2026-10-16 06:46:15 +08:00
1 parent c858bd2a09
commit 8233369cd4
16 files changed
+27 -30

No files matched your search

+4 -3
View File
@@ -3,9 +3,9 @@ import { supportsApiContract } from './contract';
describe('API contract compatibility', () => {
it('accepts compatible SemVer releases in the supported major', () => {
expect(supportsApiContract('7.0.0')).toBe(true);
expect(supportsApiContract('7.8.2')).toBe(true);
expect(supportsApiContract('7.2.0-rc.1')).toBe(true);
expect(supportsApiContract('8.0.0')).toBe(true);
expect(supportsApiContract('8.8.2')).toBe(true);
expect(supportsApiContract('8.2.0-rc.1')).toBe(true);
});
it('rejects a breaking major and malformed versions', () => {
@@ -13,6 +13,7 @@ describe('API contract compatibility', () => {
expect(supportsApiContract('4.0.0')).toBe(false);
expect(supportsApiContract('5.0.0')).toBe(false);
expect(supportsApiContract('6.1.0')).toBe(false);
expect(supportsApiContract('7.0.0')).toBe(false);
expect(supportsApiContract('01.0.0')).toBe(false);
expect(supportsApiContract(undefined)).toBe(false);
});
+1 -1
View File
@@ -1,4 +1,4 @@
export const SUPPORTED_API_CONTRACT_MAJOR = 7;
export const SUPPORTED_API_CONTRACT_MAJOR = 8;
const semverPattern = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
+1 -1
View File
@@ -4,5 +4,5 @@ import type { CreateOptions } from './types';
export function creationOptions(defaults: DefaultsBody): CreateOptions {
const settings = defaults.channel;
return { mode: defaults.channel_mode, maxMembers: settings.max_members, ttlSeconds: settings.ttl_seconds, membersCanUpload: settings.members_can_upload,
fileMaxDownloads: settings.file_max_downloads, joinPolicy: settings.join_policy, allowGuests: settings.allow_guests };
fileMaxDownloads: settings.file_max_downloads, joinPolicy: settings.join_policy };
}
-3
View File
@@ -8,7 +8,6 @@ export interface ChannelInfo {
dataSource?: 'server' | 'cache';
creatorAccount?: string;
joinPolicy?: 'open' | 'invite';
allowGuests?: boolean;
passwordRequired?: boolean;
id: string;
/** 服务端 channel settings 中的共享名称,加入者可见。 */
@@ -33,7 +32,6 @@ export interface ChannelInfo {
/** 新建空间的选项(protocol §12 设置的子集) */
export interface CreateOptions {
joinPolicy?: 'open' | 'invite';
allowGuests?: boolean;
joinPassword?: string;
name?: string;
mode: ChannelMode;
@@ -51,7 +49,6 @@ export interface FileUploadOptions {
export interface ChannelSettingsPatch {
joinPolicy?: 'open' | 'invite';
allowGuests?: boolean;
joinPassword?: string;
name?: string;
maxMembers?: number;
@@ -34,7 +34,7 @@ export async function createChannel(options: CreateOptions, captchaToken?: strin
ttl_seconds: options.ttlSeconds,
members_can_upload: options.membersCanUpload,
file_max_downloads: options.fileMaxDownloads,
join_policy: options.joinPolicy ?? defaults.join_policy, allow_guests: options.allowGuests ?? defaults.allow_guests,
join_policy: options.joinPolicy ?? defaults.join_policy,
},
});
try {
@@ -157,7 +157,6 @@ export async function updateChannelSettings(resourceId: string, patch: ChannelSe
if (patch.ttlSeconds !== undefined) body.ttl_seconds = patch.ttlSeconds;
if (patch.fileMaxDownloads !== undefined) body.file_max_downloads = patch.fileMaxDownloads;
if (patch.joinPolicy !== undefined) body.join_policy = patch.joinPolicy;
if (patch.allowGuests !== undefined) body.allow_guests = patch.allowGuests;
if (patch.joinPassword !== undefined) body.join_password = patch.joinPassword;
const info = await apiFetch(`/channels/${resourceId}/settings`, {
method: 'PATCH', headers: { ...session.authHeaders('owner'), 'Content-Type': 'application/json' },
+2 -2
View File
@@ -8,7 +8,7 @@ describe('API contract preflight', () => {
});
it('blocks API requests when the server has a newer breaking major', async () => {
const fetchMock = vi.fn(async () => new Response(JSON.stringify({ contract_version: '8.0.0' }), { status: 200 }));
const fetchMock = vi.fn(async () => new Response(JSON.stringify({ contract_version: '9.0.0' }), { status: 200 }));
vi.stubGlobal('fetch', fetchMock);
setApiBase('http://api.test/api/v1');
@@ -19,7 +19,7 @@ describe('API contract preflight', () => {
it('checks the contract once and then sends the API request', async () => {
const fetchMock = vi.fn()
.mockResolvedValueOnce(new Response(JSON.stringify({ contract_version: '7.0.0' }), { status: 200 }))
.mockResolvedValueOnce(new Response(JSON.stringify({ contract_version: '8.0.0' }), { status: 200 }))
.mockResolvedValueOnce(new Response('{}', { status: 200 }))
.mockResolvedValueOnce(new Response('{}', { status: 200 }));
vi.stubGlobal('fetch', fetchMock);
+1 -1
View File
@@ -8,7 +8,7 @@ export function toChannelInfo(raw: ChannelWire, saved: SavedResource): ChannelIn
return {
id: raw.id,
creatorAccount: raw.creator_account ?? undefined,
joinPolicy, allowGuests: raw.settings.allow_guests, passwordRequired: raw.password_required,
joinPolicy, passwordRequired: raw.password_required,
name: raw.settings.name ?? undefined,
mode: raw.mode,
role: raw.can_manage_structure ? 'owner' : 'member',