Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
5 changes: 4 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,10 @@ jobs:
full_scope=true
files=""
else
git fetch --quiet --depth=1 origin "$GITHUB_BASE_REF"
# Full base history (no --depth) so the three-dot
# origin/<base>...HEAD diff can resolve a merge base;
# a shallow depth-1 base fetch fails with "no merge base".
git fetch --quiet origin "$GITHUB_BASE_REF"
files="$(git diff --name-only "origin/$GITHUB_BASE_REF"...HEAD)"
echo "---changed files---"
echo "$files"
Expand Down
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' })
{ query: 'gen', roomType: 'all', size: 50 }
)
})

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