Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
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
15 changes: 11 additions & 4 deletions chat-frontend/src/api/searchMessages/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,27 +8,34 @@ export interface SearchMessagesArgs {
size: number
}

/** Mirrors pkg/model.SearchMessage (the search.messages reply projection). */
export interface SearchMessageHit {
messageId: string
roomId: string
siteId: string
userId: string
userAccount: string
content: string
createdAt: string
editedAt?: string
updatedAt?: string
threadParentMessageId?: string
threadParentMessageCreatedAt?: string
}

export interface SearchMessagesResponse {
messages: SearchMessageHit[]
total: number
results: SearchMessageHit[]
}

/** Full-text search across messages. Optionally scope to a room subset. */
export async function searchMessages(
{ user, request }: Nats,
args: SearchMessagesArgs,
{ searchText, roomIds, size }: SearchMessagesArgs,
): Promise<SearchMessagesResponse> {
return request<SearchMessagesResponse>(searchMessagesSubject(user.account), args)
const payload: { query: string; roomIds?: string[]; size: number } = {
query: searchText,
size,
}
if (roomIds) payload.roomIds = roomIds
return request<SearchMessagesResponse>(searchMessagesSubject(user.account), payload)
}
27 changes: 15 additions & 12 deletions chat-frontend/src/api/searchRooms/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,33 +3,36 @@ import type { Nats, RoomType } from '../types'

export interface SearchRoomsArgs {
searchText: string
/** Server-accepted scope values; mirrors search-service's query_rooms.go.
* `'all'` returns rooms anyone can see; the type-specific values narrow. */
scope: 'all' | 'channel' | 'dm' | 'app'
/** Server-accepted roomType filter; mirrors search-service's
* query_rooms.go. `'all'` (default) returns every accessible room;
* the type-specific values narrow. The server rejects any other value. */
roomType: 'all' | 'channel' | 'dm'
size: number
}

/** Mirrors pkg/model.SearchRoom (the search.rooms reply projection). */
export interface SearchRoomHit {
roomId: string
roomName: string
roomType: RoomType
name: string
roomType?: RoomType
siteId: string
userAccount: string
joinedAt: string
}

export interface SearchRoomsResponse {
total: number
results: SearchRoomHit[]
rooms: SearchRoomHit[]
}

/**
* Search rooms the caller is a member of (or all rooms if scope='all').
* Search rooms the caller is a member of (or all rooms if roomType='all').
* Mirrors search-service's `search.rooms` handler — hits sorted by relevance.
*/
export async function searchRooms(
{ user, request }: Nats,
args: SearchRoomsArgs,
{ searchText, roomType, size }: SearchRoomsArgs,
): Promise<SearchRoomsResponse> {
return request<SearchRoomsResponse>(searchRoomsSubject(user.account), args)
return request<SearchRoomsResponse>(searchRoomsSubject(user.account), {
query: searchText,
roomType,
size,
})
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ export default function SearchBar({ onSelectRoom, onEnterSearch }) {

const fetcher = useCallback(
async (q) => {
const resp = await searchRooms(nats, { searchText: q, scope: 'all', size: 8 })
const resp = await searchRooms(nats, { searchText: q, roomType: 'all', size: 8 })
setActiveIdx(0)
return resp.results ?? []
return resp.rooms ?? []
},
[nats]
)
Expand Down Expand Up @@ -87,7 +87,7 @@ export default function SearchBar({ onSelectRoom, onEnterSearch }) {
<div className="result-type">
{searchRoomPrefix(hit.roomType)}
</div>
<div className="result-name">{hit.roomName}</div>
<div className="result-name">{hit.name}</div>
</div>
))}
<div className="search-footer">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@ describe('SearchBar', () => {

it('fetches rooms after 250ms debounce when query >= 2 chars', async () => {
const request = vi.fn().mockResolvedValue({
results: [
{ roomId: 'r1', roomName: 'general', roomType: 'c', siteId: 'site-A' },
rooms: [
{ roomId: 'r1', name: 'general', roomType: 'c', siteId: 'site-A' },
],
total: 1,
})
Expand All @@ -53,16 +53,16 @@ describe('SearchBar', () => {
await waitFor(() => {
expect(request).toHaveBeenCalledWith(
'chat.user.alice.request.search.rooms',
{ searchText: 'fro', scope: 'all', size: 8 }
{ query: 'fro', roomType: 'all', size: 8 }
)
})
})

it('shows results in dropdown', async () => {
const request = vi.fn().mockResolvedValue({
results: [
{ roomId: 'r1', roomName: 'frontend-team', roomType: 'c', siteId: 'site-A' },
{ roomId: 'r2', roomName: 'frontend-perf', roomType: 'c', siteId: 'site-A' },
rooms: [
{ roomId: 'r1', name: 'frontend-team', roomType: 'c', siteId: 'site-A' },
{ roomId: 'r2', name: 'frontend-perf', roomType: 'c', siteId: 'site-A' },
],
total: 2,
})
Expand All @@ -84,8 +84,8 @@ describe('SearchBar', () => {
it('clicking result calls onSelectRoom and clears input', async () => {
const onSelectRoom = vi.fn()
const request = vi.fn().mockResolvedValue({
results: [
{ roomId: 'r1', roomName: 'general', roomType: 'c', siteId: 'site-A' },
rooms: [
{ roomId: 'r1', name: 'general', roomType: 'c', siteId: 'site-A' },
],
total: 1,
})
Expand Down Expand Up @@ -115,7 +115,7 @@ describe('SearchBar', () => {
const onEnterSearch = vi.fn()
useNats.mockReturnValue({
user: { account: 'alice' },
request: vi.fn().mockResolvedValue({ results: [], total: 0 }),
request: vi.fn().mockResolvedValue({ rooms: [], total: 0 }),
})

render(<SearchBar onSelectRoom={vi.fn()} onEnterSearch={onEnterSearch} />)
Expand All @@ -131,7 +131,7 @@ describe('SearchBar', () => {
useNats.mockReturnValue({
user: { account: 'alice' },
request: vi.fn().mockResolvedValue({
results: [{ roomId: 'r1', roomName: 'general', roomType: 'c', siteId: 'site-A' }],
rooms: [{ roomId: 'r1', name: 'general', roomType: 'c', siteId: 'site-A' }],
total: 1,
}),
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export default function InRoomSearch({ roomId, onClose, onJumpToMessage }) {
setSubmitted(true)
try {
const resp = await searchMessages(nats, { searchText: q, roomIds: [roomId], size: 50 })
setResults(resp.results ?? [])
setResults(resp.messages ?? [])
} catch {
setResults([])
} finally {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ describe('InRoomSearch', () => {
})

it('does not fetch on typing; fetches on Enter scoped to roomIds: [roomId]', async () => {
const request = vi.fn().mockResolvedValue({ results: [], total: 0 })
const request = vi.fn().mockResolvedValue({ messages: [], total: 0 })
useNats.mockReturnValue({
user: { account: 'alice' },
request,
Expand All @@ -38,7 +38,7 @@ describe('InRoomSearch', () => {
await waitFor(() => {
expect(request).toHaveBeenCalledWith(
'chat.user.alice.request.search.messages',
{ searchText: 'hi', roomIds: ['r1'], size: 50 }
{ query: 'hi', roomIds: ['r1'], size: 50 }
)
})
})
Expand All @@ -47,7 +47,7 @@ describe('InRoomSearch', () => {
const onJumpToMessage = vi.fn()
const onClose = vi.fn()
const request = vi.fn().mockResolvedValue({
results: [
messages: [
{ messageId: 'msg-1', roomId: 'r1', content: 'hello world' },
],
total: 1,
Expand Down Expand Up @@ -83,7 +83,7 @@ describe('InRoomSearch', () => {
const onClose = vi.fn()
useNats.mockReturnValue({
user: { account: 'alice' },
request: vi.fn().mockResolvedValue({ results: [], total: 0 }),
request: vi.fn().mockResolvedValue({ messages: [], total: 0 }),
})

render(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ const identity = (e) => e

const renderChannelResult = (r) => (
<div className="picker-result-line">
<strong>{r.roomName}</strong>
<strong>{r.name}</strong>
<span className="picker-result-sub"> — {r.siteId}</span>
</div>
)
Expand Down Expand Up @@ -78,8 +78,8 @@ const MemberPicker = forwardRef(function MemberPicker(

const channelFetcher = useCallback(
async (q) => {
const resp = await searchRooms(nats, { searchText: q, scope: 'all', size: 8 })
return resp.results ?? []
const resp = await searchRooms(nats, { searchText: q, roomType: 'all', size: 8 })
return resp.rooms ?? []
},
[nats]
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ vi.mock('@/context/NatsContext', () => ({
import { useNats } from '@/context/NatsContext'

function setup(overrides = {}) {
const request = vi.fn().mockResolvedValue({ results: [] })
const request = vi.fn().mockResolvedValue({ rooms: [] })
useNats.mockReturnValue({
user: { account: 'alice', siteId: 'site-A' },
request,
Expand Down Expand Up @@ -206,7 +206,7 @@ describe('MemberPicker', () => {

it('debounces search.rooms (channels) and adds a ChannelRef when a result is clicked', async () => {
const request = vi.fn().mockResolvedValue({
results: [{ roomId: 'r-x', roomName: 'project-x', siteId: 'site-B', roomType: 'c' }],
rooms: [{ roomId: 'r-x', name: 'project-x', siteId: 'site-B', roomType: 'c' }],
})
const onChannelsChange = vi.fn()
useNats.mockReturnValue({ user: { account: 'alice', siteId: 'site-A' }, request })
Expand All @@ -225,7 +225,7 @@ describe('MemberPicker', () => {
await waitFor(() => {
expect(request).toHaveBeenCalledWith(
'chat.user.alice.request.search.rooms',
expect.objectContaining({ searchText: 'pro' })
expect.objectContaining({ query: 'pro' })
)
})
await waitFor(() => expect(screen.getByText('project-x')).toBeInTheDocument())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,12 @@ export default function SearchResultsPane({
setRoomsError(null)
setMsgsError(null)

searchRooms(nats, { searchText: query, scope: 'all', size: 50 })
searchRooms(nats, { searchText: query, roomType: 'all', size: 50 })
.then((resp) => {
if (cancelled) return
setRoomResults(resp.results ?? [])
setRoomTotal(resp.total ?? 0)
const rooms = resp.rooms ?? []
setRoomResults(rooms)
setRoomTotal(rooms.length)
})
.catch((err) => {
if (!cancelled) setRoomsError(err?.message || 'Search failed')
Expand All @@ -47,7 +48,7 @@ export default function SearchResultsPane({
searchMessages(nats, { searchText: query, size: 50 })
.then((resp) => {
if (cancelled) return
setMsgResults(resp.results ?? [])
setMsgResults(resp.messages ?? [])
setMsgTotal(resp.total ?? 0)
})
.catch((err) => {
Expand Down Expand Up @@ -120,7 +121,7 @@ export default function SearchResultsPane({
<span className="result-type">
{searchRoomPrefix(hit.roomType)}
</span>
<span className="result-name">{hit.roomName}</span>
<span className="result-name">{hit.name}</span>
</button>
))}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,9 @@ describe('SearchResultsPane', () => {

it('fetches and displays room results immediately', async () => {
const request = vi.fn().mockResolvedValue({
results: [
{ roomId: 'r1', roomName: 'general', roomType: 'c', siteId: 'site-A' },
rooms: [
{ roomId: 'r1', name: 'general', roomType: 'c', siteId: 'site-A' },
],
total: 1,
})
useNats.mockReturnValue({
user: { account: 'alice' },
Expand All @@ -40,21 +39,20 @@ describe('SearchResultsPane', () => {

expect(request).toHaveBeenCalledWith(
'chat.user.alice.request.search.rooms',
expect.objectContaining({ searchText: 'gen' })
expect.objectContaining({ query: 'gen' })
)
})

it('Rooms tab shows results, Messages tab fetches on click', async () => {
const request = vi.fn().mockImplementation((subject) => {
if (subject.includes('.search.rooms')) {
return Promise.resolve({
results: [{ roomId: 'r1', roomName: 'general', roomType: 'c', siteId: 'site-A' }],
total: 1,
rooms: [{ roomId: 'r1', name: 'general', roomType: 'c', siteId: 'site-A' }],
})
}
if (subject.includes('.search.messages')) {
return Promise.resolve({
results: [
messages: [
{ messageId: 'm1', roomId: 'r1', content: 'hello', createdAt: '2026-04-17T10:00:00Z', userAccount: 'bob' },
],
total: 1,
Expand Down Expand Up @@ -93,10 +91,9 @@ describe('SearchResultsPane', () => {
const onSelectRoom = vi.fn()
const onClose = vi.fn()
const request = vi.fn().mockResolvedValue({
results: [
{ roomId: 'r1', roomName: 'general', roomType: 'c', siteId: 'site-A' },
rooms: [
{ roomId: 'r1', name: 'general', roomType: 'c', siteId: 'site-A' },
],
total: 1,
})
useNats.mockReturnValue({
user: { account: 'alice' },
Expand Down Expand Up @@ -130,11 +127,11 @@ describe('SearchResultsPane', () => {
const onClose = vi.fn()
const request = vi.fn().mockImplementation((subject) => {
if (subject.includes('.search.rooms')) {
return Promise.resolve({ results: [], total: 0 })
return Promise.resolve({ rooms: [] })
}
if (subject.includes('.search.messages')) {
return Promise.resolve({
results: [
messages: [
{ messageId: 'm1', roomId: 'r1', content: 'hello world', createdAt: '2026-04-17T10:00:00Z', userAccount: 'bob' },
],
total: 1,
Expand Down
2 changes: 1 addition & 1 deletion chat-frontend/src/lib/roomFormat.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export function roomDisplayName(room) {
export function roomFromSearchHit(hit) {
return {
id: hit.roomId,
name: hit.roomName,
name: hit.name,
type: hit.roomType,
siteId: hit.siteId,
}
Expand Down
2 changes: 1 addition & 1 deletion chat-frontend/src/lib/roomFormat.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ describe('roomDisplayName', () => {

describe('roomFromSearchHit', () => {
it('maps search-hit field names onto the room shape', () => {
const hit = { roomId: 'r1', roomName: 'frontend', roomType: 'channel', siteId: 'site-A' }
const hit = { roomId: 'r1', name: 'frontend', roomType: 'channel', siteId: 'site-A' }
expect(roomFromSearchHit(hit)).toEqual({
id: 'r1',
name: 'frontend',
Expand Down
Loading
Loading