feat(frontend): add createGroup API method and update AddDmMemberModal branching

- Add CreateGroupDmRequest to API client imports, type declaration, and implementation (POST /dm/group)
- Update AddDmMemberModal to branch on channel type: 1-on-1 DMs call createGroup and navigate to new channel, group DMs call addMember directly
- Remove legacy getApiForOrigin/channelOriginMap usage — S2S federation relay is now entirely server-side
This commit is contained in:
Jannis Braun
2026-03-26 22:32:30 +01:00
parent 4422d97ba7
commit be20f23500
2 changed files with 23 additions and 7 deletions
+3
View File
@@ -23,6 +23,7 @@ import type {
UpdateMemberRequest, UpdateMemberRequest,
LiveKitTokenResponse, LiveKitTokenResponse,
CreateDmRequest, CreateDmRequest,
CreateGroupDmRequest,
CreateDmMessageRequest, CreateDmMessageRequest,
Friend, Friend,
FriendRequest, FriendRequest,
@@ -135,6 +136,7 @@ export class BackspaceApiClient {
readonly dm: { readonly dm: {
list: () => Promise<DmChannel[]>; list: () => Promise<DmChannel[]>;
create: (data: CreateDmRequest) => Promise<DmChannel>; create: (data: CreateDmRequest) => Promise<DmChannel>;
createGroup: (data: CreateGroupDmRequest) => Promise<DmChannel>;
close: (id: string) => Promise<{ success: boolean }>; close: (id: string) => Promise<{ success: boolean }>;
messages: (id: string, before?: string, limit?: number) => Promise<DmMessageWithUser[]>; messages: (id: string, before?: string, limit?: number) => Promise<DmMessageWithUser[]>;
messagesAround: (id: string, messageId: string) => Promise<DmMessageWithUser[]>; messagesAround: (id: string, messageId: string) => Promise<DmMessageWithUser[]>;
@@ -480,6 +482,7 @@ export class BackspaceApiClient {
this.dm = { this.dm = {
list: () => request<DmChannel[]>('GET', '/dm'), list: () => request<DmChannel[]>('GET', '/dm'),
create: (data: CreateDmRequest) => request<DmChannel>('POST', '/dm', data), create: (data: CreateDmRequest) => request<DmChannel>('POST', '/dm', data),
createGroup: (data: CreateGroupDmRequest) => request<DmChannel>('POST', '/dm/group', data),
close: (id: string) => request<{ success: boolean }>('DELETE', `/dm/${id}`), close: (id: string) => request<{ success: boolean }>('DELETE', `/dm/${id}`),
messages: (id: string, before?: string, limit = 50) => { messages: (id: string, before?: string, limit = 50) => {
const params = new URLSearchParams(); const params = new URLSearchParams();
@@ -1,8 +1,10 @@
import React, { useState, useRef, useEffect } from 'react'; import React, { useState, useRef, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { Modal } from '../ui/Modal'; import { Modal } from '../ui/Modal';
import { Avatar } from '../ui/Avatar'; import { Avatar } from '../ui/Avatar';
import { useUIStore } from '../../stores/uiStore'; import { useUIStore } from '../../stores/uiStore';
import { useSpaceStore, getApiForOrigin } from '../../stores/spaceStore'; import { useSpaceStore } from '../../stores/spaceStore';
import { useAuthStore } from '../../stores/authStore';
import { api } from '../../api/client'; import { api } from '../../api/client';
import type { User } from '@backspace/shared'; import type { User } from '@backspace/shared';
@@ -16,7 +18,8 @@ export function AddDmMemberModal() {
const modalData = useUIStore((s) => s.modalData); const modalData = useUIStore((s) => s.modalData);
const closeModal = useUIStore((s) => s.closeModal); const closeModal = useUIStore((s) => s.closeModal);
const dmChannels = useSpaceStore((s) => s.dmChannels); const dmChannels = useSpaceStore((s) => s.dmChannels);
const channelOriginMap = useSpaceStore((s) => s.channelOriginMap); const navigate = useNavigate();
const myUserId = useAuthStore((s) => s.user?.id);
const inputRef = useRef<HTMLInputElement>(null); const inputRef = useRef<HTMLInputElement>(null);
const searchTimer = useRef<ReturnType<typeof setTimeout>>(); const searchTimer = useRef<ReturnType<typeof setTimeout>>();
@@ -64,14 +67,24 @@ export function AddDmMemberModal() {
}; };
const handleSelectUser = async (user: User) => { const handleSelectUser = async (user: User) => {
if (!dmChannelId || isAdding) return; if (!dmChannelId || !dmChannel || isAdding) return;
setError(''); setError('');
setIsAdding(true); setIsAdding(true);
try { try {
const origin = channelOriginMap.get(dmChannelId) || ''; if (!dmChannel.ownerId) {
const targetApi = getApiForOrigin(origin); // 1-on-1 DM → create a new group DM with all 3 users
await targetApi.dm.addMember(dmChannelId, { userId: user.id }); const otherMember = dmChannel.members.find(m => m.id !== myUserId);
if (!otherMember) return;
const newChannel = await api.dm.createGroup({ userIds: [otherMember.id, user.id] });
closeModal(); closeModal();
navigate(`/channels/@me/${newChannel.id}`);
} else {
// Group DM → add to existing group
// Always call the home instance — S2S federation handles relay.
// Only the owner can add members, and the owner is always on the home instance.
await api.dm.addMember(dmChannelId, { userId: user.id });
closeModal();
}
} catch (err) { } catch (err) {
setError((err as Error).message || 'Failed to add member'); setError((err as Error).message || 'Failed to add member');
} finally { } finally {