-
Notifications
You must be signed in to change notification settings - Fork 7
RG-T117 Chat and Chatbot Entrypoints #260
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,255 @@ | ||||||||||||||||||||||||||||||||||||||||||||
| import { getBaseApiUrl } from '@/lib/storage/app'; | ||||||||||||||||||||||||||||||||||||||||||||
| import { | ||||||||||||||||||||||||||||||||||||||||||||
| type AddMembersInput, | ||||||||||||||||||||||||||||||||||||||||||||
| type AddReactionInput, | ||||||||||||||||||||||||||||||||||||||||||||
| type ChatAckResultData, | ||||||||||||||||||||||||||||||||||||||||||||
| type ChatActionResult, | ||||||||||||||||||||||||||||||||||||||||||||
| type ChatAttachmentUploadedResult, | ||||||||||||||||||||||||||||||||||||||||||||
| type ChatChannelResultData, | ||||||||||||||||||||||||||||||||||||||||||||
| type ChatMemberResultData, | ||||||||||||||||||||||||||||||||||||||||||||
| type ChatMessageResultData, | ||||||||||||||||||||||||||||||||||||||||||||
| type ChatV4Response, | ||||||||||||||||||||||||||||||||||||||||||||
| type CreateAdHocChannelInput, | ||||||||||||||||||||||||||||||||||||||||||||
| type CreateDirectMessageInput, | ||||||||||||||||||||||||||||||||||||||||||||
| type EditMessageInput, | ||||||||||||||||||||||||||||||||||||||||||||
| type FlagMessageInput, | ||||||||||||||||||||||||||||||||||||||||||||
| type GetChatPresenceResult, | ||||||||||||||||||||||||||||||||||||||||||||
| type GifResultData, | ||||||||||||||||||||||||||||||||||||||||||||
| type MarkReadInput, | ||||||||||||||||||||||||||||||||||||||||||||
| type SendChatMessageInput, | ||||||||||||||||||||||||||||||||||||||||||||
| type SetNotificationPreferenceInput, | ||||||||||||||||||||||||||||||||||||||||||||
| type UpdateChannelInput, | ||||||||||||||||||||||||||||||||||||||||||||
| } from '@/models/v4/chat'; | ||||||||||||||||||||||||||||||||||||||||||||
| import useAuthStore from '@/stores/auth/store'; | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| import { api } from '../common/client'; | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| const CHAT = '/Chat'; | ||||||||||||||||||||||||||||||||||||||||||||
| const MODERATION = '/ChatModeration'; | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| // --------------------------------------------------------------------------- | ||||||||||||||||||||||||||||||||||||||||||||
| // Channels | ||||||||||||||||||||||||||||||||||||||||||||
| // --------------------------------------------------------------------------- | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| export const getChannels = async (activeUnitId?: number, signal?: AbortSignal) => { | ||||||||||||||||||||||||||||||||||||||||||||
| const response = await api.get<ChatV4Response<ChatChannelResultData[]>>(`${CHAT}/GetChannels`, { | ||||||||||||||||||||||||||||||||||||||||||||
| params: activeUnitId != null ? { activeUnitId } : undefined, | ||||||||||||||||||||||||||||||||||||||||||||
| signal, | ||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||
| return response.data; | ||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| export const getChannel = async (channelId: string, signal?: AbortSignal) => { | ||||||||||||||||||||||||||||||||||||||||||||
| const response = await api.get<ChatV4Response<ChatChannelResultData>>(`${CHAT}/GetChannel`, { params: { channelId }, signal }); | ||||||||||||||||||||||||||||||||||||||||||||
| return response.data; | ||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| export const createDirectMessage = async (input: CreateDirectMessageInput) => { | ||||||||||||||||||||||||||||||||||||||||||||
| const response = await api.post<ChatV4Response<ChatChannelResultData>>(`${CHAT}/CreateDirectMessage`, input); | ||||||||||||||||||||||||||||||||||||||||||||
| return response.data; | ||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| export const createAdHocChannel = async (input: CreateAdHocChannelInput) => { | ||||||||||||||||||||||||||||||||||||||||||||
| const response = await api.post<ChatV4Response<ChatChannelResultData>>(`${CHAT}/CreateAdHocChannel`, input); | ||||||||||||||||||||||||||||||||||||||||||||
| return response.data; | ||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| export const updateChannel = async (channelId: string, input: UpdateChannelInput) => { | ||||||||||||||||||||||||||||||||||||||||||||
| const response = await api.put<ChatV4Response<ChatChannelResultData>>(`${CHAT}/UpdateChannel`, input, { params: { channelId } }); | ||||||||||||||||||||||||||||||||||||||||||||
| return response.data; | ||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| export const archiveChannel = async (channelId: string) => { | ||||||||||||||||||||||||||||||||||||||||||||
| const response = await api.delete<ChatActionResult>(`${CHAT}/ArchiveChannel`, { params: { channelId } }); | ||||||||||||||||||||||||||||||||||||||||||||
| return response.data; | ||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| // --------------------------------------------------------------------------- | ||||||||||||||||||||||||||||||||||||||||||||
| // Members | ||||||||||||||||||||||||||||||||||||||||||||
| // --------------------------------------------------------------------------- | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| export const getMembers = async (channelId: string, signal?: AbortSignal) => { | ||||||||||||||||||||||||||||||||||||||||||||
| const response = await api.get<ChatV4Response<ChatMemberResultData[]>>(`${CHAT}/GetMembers`, { params: { channelId }, signal }); | ||||||||||||||||||||||||||||||||||||||||||||
| return response.data; | ||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| export const addMembers = async (channelId: string, input: AddMembersInput) => { | ||||||||||||||||||||||||||||||||||||||||||||
| const response = await api.post<ChatV4Response<ChatMemberResultData[]>>(`${CHAT}/AddMembers`, input, { params: { channelId } }); | ||||||||||||||||||||||||||||||||||||||||||||
| return response.data; | ||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| export const removeMember = async (channelId: string, userId: string) => { | ||||||||||||||||||||||||||||||||||||||||||||
| const response = await api.delete<ChatActionResult>(`${CHAT}/RemoveMember`, { params: { channelId, userId } }); | ||||||||||||||||||||||||||||||||||||||||||||
| return response.data; | ||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| export const setNotificationPreference = async (channelId: string, input: SetNotificationPreferenceInput) => { | ||||||||||||||||||||||||||||||||||||||||||||
| const response = await api.put<ChatActionResult>(`${CHAT}/SetNotificationPreference`, input, { params: { channelId } }); | ||||||||||||||||||||||||||||||||||||||||||||
| return response.data; | ||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| // --------------------------------------------------------------------------- | ||||||||||||||||||||||||||||||||||||||||||||
| // Messages | ||||||||||||||||||||||||||||||||||||||||||||
| // --------------------------------------------------------------------------- | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| export const getMessages = async (channelId: string, beforeSeq?: number, limit = 50, signal?: AbortSignal) => { | ||||||||||||||||||||||||||||||||||||||||||||
| const response = await api.get<ChatV4Response<ChatMessageResultData[]>>(`${CHAT}/GetMessages`, { | ||||||||||||||||||||||||||||||||||||||||||||
| params: { channelId, beforeSeq, limit }, | ||||||||||||||||||||||||||||||||||||||||||||
| signal, | ||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||
| return response.data; | ||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| export const getMessagesAfter = async (channelId: string, afterSeq: number, limit = 50, signal?: AbortSignal) => { | ||||||||||||||||||||||||||||||||||||||||||||
| const response = await api.get<ChatV4Response<ChatMessageResultData[]>>(`${CHAT}/GetMessagesAfter`, { | ||||||||||||||||||||||||||||||||||||||||||||
| params: { channelId, afterSeq, limit }, | ||||||||||||||||||||||||||||||||||||||||||||
| signal, | ||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||
| return response.data; | ||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| export const getThread = async (messageId: string, beforeSeq?: number, limit = 50, signal?: AbortSignal) => { | ||||||||||||||||||||||||||||||||||||||||||||
| const response = await api.get<ChatV4Response<ChatMessageResultData[]>>(`${CHAT}/GetThread`, { | ||||||||||||||||||||||||||||||||||||||||||||
| params: { messageId, beforeSeq, limit }, | ||||||||||||||||||||||||||||||||||||||||||||
| signal, | ||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||
| return response.data; | ||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| export const sendMessage = async (channelId: string, input: SendChatMessageInput) => { | ||||||||||||||||||||||||||||||||||||||||||||
| const response = await api.post<ChatV4Response<ChatMessageResultData>>(`${CHAT}/SendMessage`, input, { params: { channelId } }); | ||||||||||||||||||||||||||||||||||||||||||||
| return response.data; | ||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| export const editMessage = async (messageId: string, input: EditMessageInput) => { | ||||||||||||||||||||||||||||||||||||||||||||
| const response = await api.put<ChatV4Response<ChatMessageResultData>>(`${CHAT}/EditMessage`, input, { params: { messageId } }); | ||||||||||||||||||||||||||||||||||||||||||||
| return response.data; | ||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| export const deleteMessage = async (messageId: string) => { | ||||||||||||||||||||||||||||||||||||||||||||
| const response = await api.delete<ChatActionResult>(`${CHAT}/DeleteMessage`, { params: { messageId } }); | ||||||||||||||||||||||||||||||||||||||||||||
| return response.data; | ||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| // --------------------------------------------------------------------------- | ||||||||||||||||||||||||||||||||||||||||||||
| // Reactions, acks, read pointers, pins | ||||||||||||||||||||||||||||||||||||||||||||
| // --------------------------------------------------------------------------- | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| export const addReaction = async (messageId: string, input: AddReactionInput) => { | ||||||||||||||||||||||||||||||||||||||||||||
| const response = await api.post<ChatActionResult>(`${CHAT}/AddReaction`, input, { params: { messageId } }); | ||||||||||||||||||||||||||||||||||||||||||||
| return response.data; | ||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| export const removeReaction = async (messageId: string, emoji: string) => { | ||||||||||||||||||||||||||||||||||||||||||||
| const response = await api.delete<ChatActionResult>(`${CHAT}/RemoveReaction`, { params: { messageId, emoji } }); | ||||||||||||||||||||||||||||||||||||||||||||
| return response.data; | ||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| export const ackMessage = async (messageId: string) => { | ||||||||||||||||||||||||||||||||||||||||||||
| const response = await api.post<ChatActionResult>(`${CHAT}/Ack`, {}, { params: { messageId } }); | ||||||||||||||||||||||||||||||||||||||||||||
| return response.data; | ||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| export const getMyPendingAcks = async (signal?: AbortSignal) => { | ||||||||||||||||||||||||||||||||||||||||||||
| const response = await api.get<ChatV4Response<ChatAckResultData[]>>(`${CHAT}/GetMyPendingAcks`, { signal }); | ||||||||||||||||||||||||||||||||||||||||||||
| return response.data; | ||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| export const markRead = async (channelId: string, input: MarkReadInput) => { | ||||||||||||||||||||||||||||||||||||||||||||
| const response = await api.put<ChatActionResult>(`${CHAT}/MarkRead`, input, { params: { channelId } }); | ||||||||||||||||||||||||||||||||||||||||||||
| return response.data; | ||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| export const pinMessage = async (messageId: string) => { | ||||||||||||||||||||||||||||||||||||||||||||
| const response = await api.post<ChatActionResult>(`${CHAT}/PinMessage`, {}, { params: { messageId } }); | ||||||||||||||||||||||||||||||||||||||||||||
| return response.data; | ||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| export const unpinMessage = async (messageId: string) => { | ||||||||||||||||||||||||||||||||||||||||||||
| const response = await api.delete<ChatActionResult>(`${CHAT}/UnpinMessage`, { params: { messageId } }); | ||||||||||||||||||||||||||||||||||||||||||||
| return response.data; | ||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| export const getPins = async (channelId: string, signal?: AbortSignal) => { | ||||||||||||||||||||||||||||||||||||||||||||
| const response = await api.get<ChatV4Response<ChatMessageResultData[]>>(`${CHAT}/GetPins`, { params: { channelId }, signal }); | ||||||||||||||||||||||||||||||||||||||||||||
| return response.data; | ||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| // --------------------------------------------------------------------------- | ||||||||||||||||||||||||||||||||||||||||||||
| // Attachments | ||||||||||||||||||||||||||||||||||||||||||||
| // --------------------------------------------------------------------------- | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| export interface ChatUploadFile { | ||||||||||||||||||||||||||||||||||||||||||||
| uri: string; | ||||||||||||||||||||||||||||||||||||||||||||
| name: string; | ||||||||||||||||||||||||||||||||||||||||||||
| type: string; | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| export const uploadAttachment = async (channelId: string, messageId: string, file: ChatUploadFile) => { | ||||||||||||||||||||||||||||||||||||||||||||
| const form = new FormData(); | ||||||||||||||||||||||||||||||||||||||||||||
| // React Native FormData accepts { uri, name, type } file objects. | ||||||||||||||||||||||||||||||||||||||||||||
| form.append('file', { uri: file.uri, name: file.name, type: file.type } as unknown as Blob); | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| const response = await api.post<ChatAttachmentUploadedResult>(`${CHAT}/UploadAttachment`, form, { | ||||||||||||||||||||||||||||||||||||||||||||
| params: { channelId, messageId }, | ||||||||||||||||||||||||||||||||||||||||||||
| headers: { 'Content-Type': 'multipart/form-data' }, | ||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||
| return response.data; | ||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+188
to
+198
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "Locate chat.ts and dependency files"
fd -a 'chat\.ts$|package\.json$|axios|react-native' . | sed 's#^\./##' | head -200
echo
echo "Inspect src/api/chat/chat.ts around uploadAttachment"
if [ -f src/api/chat/chat.ts ]; then
nl -ba src/api/chat/chat.ts | sed -n '1,240p'
fi
echo
echo "Search api client creation"
rg -n "createApiEndpoint|createCachedApiEndpoint|axios|headers|Content-Type|multipart/form-data" -S --glob '!node_modules/**' --glob '!dist/**' --glob '!build/**' . | head -300Repository: Resgrid/Unit Length of output: 1142 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "Package references"
python3 - <<'PY'
import json, pathlib
for p in pathlib.Path('.').rglob('package.json'):
if 'node_modules' in p.parts or p.parts[:2]==['__mocks__']:
continue
try:
data=json.loads(p.read_text())
except Exception as e:
continue
deps={}
for sec in ('dependencies','devDependencies','peerDependencies','optionalDependencies'):
deps.update(data.get(sec,{}))
hits={k,v for k,v in deps.items() if 'react-native' in k.lower() or k.lower() in ('axios','axios-cache-interceptor')}
if hits:
print(p)
for k,v in hits:
print(f" {k}: {v}")
PY
echo
echo "src/api/chat/chat.ts relevant sections"
python3 - <<'PY'
from pathlib import Path
p=Path('src/api/chat/chat.ts')
lines=p.read_text().splitlines()
for start,end in [(1,260)]:
print(f"---- {p} lines {start}-{end} ----")
for i in range(start,end+1):
if i <= len(lines):
print(f"{i:4d}\t{lines[i-1]}")
PY
echo
echo "Search api client creation and axios interceptors"
python3 - <<'PY'
from pathlib import Path
import re
pattern=re.compile(r'(axios|createApiEndpoint|createCachedApiEndpoint|Content-Type|multipart/form-data)')
for p in Path('.').rglob('*'):
if p.is_file():
tree=('/'.join(p.parts))
if 'node_modules' in tree or p.parts[:2]==['__mocks__'] or str(p).startswith('./.git'):
continue
try:
text=p.read_text(errors='ignore')
except Exception:
continue
mlines=[]
for n,line in enumerate(text.splitlines(),1):
if pattern.search(line):
mlines.append((n,line))
if mlines:
print(f"---- {p} hits ----")
for n,line in mlines[:80]:
print(f"{n:4d}\t{line}")
PYRepository: Resgrid/Unit Length of output: 361 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "Package references"
python3 - <<'PY'
import json, pathlib
for p in pathlib.Path('.').rglob('package.json'):
if 'node_modules' in p.parts or p.parts[:2]==('__mocks__',):
continue
try:
data=json.loads(p.read_text())
except Exception as e:
continue
deps={}
for sec in ('dependencies','devDependencies','peerDependencies','optionalDependencies'):
deps.update(data.get(sec,{}))
hits=[(k,v) for k,v in deps.items() if 'react-native' in k.lower() or k.lower() in ('axios','axios-cache-interceptor')]
if hits:
print(p)
for k,v in hits:
print(f" {k}: {v}")
PY
echo
echo "src/api/chat/chat.ts relevant sections"
python3 - <<'PY'
from pathlib import Path
p=Path('src/api/chat/chat.ts')
lines=p.read_text().splitlines()
for start,end in [(1,260)]:
print(f"---- {p} lines {start}-{end} ----")
for i in range(start,end+1):
if i <= len(lines):
print(f"{i:4d}\t{lines[i-1]}")
PY
echo
echo "Search api client creation and axios interceptors"
python3 - <<'PY'
import re, pathlib
pattern=re.compile(r'(axios|createApiEndpoint|createCachedApiEndpoint|Content-Type|multipart/form-data)')
for p in pathlib.Path('.').rglob('*'):
if p.is_file():
tree='/'.join(p.parts)
if 'node_modules' in tree or p.parts[:2]==('__mocks__',) or str(p).startswith('./.git'):
continue
try:
text=p.read_text(errors='ignore')
except Exception:
continue
mlines=[]
for n,line in enumerate(text.splitlines(),1):
if pattern.search(line):
mlines.append((n,line))
if mlines:
print(f"---- {p} hits ----")
for n,line in mlines[:80]:
print(f"{n:4d}\t{line}")
PYRepository: Resgrid/Unit Length of output: 50369 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "src/api/chat/chat.ts relevant sections"
python3 - <<'PY'
from pathlib import Path
p=Path('src/api/chat/chat.ts')
lines=p.read_text().splitlines()
for start,end in [(1,260)]:
for i in range(start,end+1):
if i <= len(lines):
print(f"{i:4d}\t{lines[i-1]}")
PY
echo
echo "Focused search in tracked source files"
python3 - <<'PY'
import subprocess, pathlib, re
files=[]
repo_files=subprocess.check_output(['git','ls-files']).decode().splitlines()
for raw in repo_files:
p=pathlib.Path(raw)
if p.is_file() and 'node_modules' not in p.parts and p.parts[:2]!=['__mocks__'] and not str(p).startswith('.git'):
files.append(p)
pattern=re.compile(r'(axios|createApiEndpoint|createCachedApiEndpoint|Content-Type|multipart/form-data|UploadAttachment|uploadAttachment)')
for p in files:
try:
text=p.read_text(errors='ignore')
except Exception:
continue
mlines=[]
for n,line in enumerate(text.splitlines(),1):
if pattern.search(line):
mlines.append((n,line))
if mlines:
print(f"---- {p} hits ----")
for n,line in mlines[:120]:
print(f"{n:4d}\t{line}")
PYRepository: Resgrid/Unit Length of output: 35135 🌐 Web query:
💡 Result: When using FormData with Axios in React Native, explicitly setting the Content-Type header to multipart/form-data often causes issues because the networking layer (especially on Android) expects the runtime to automatically generate the required boundary parameter [1][2]. If you manually set 'Content-Type': 'multipart/form-data', you may encounter errors where the server or the React Native networking module rejects the request because the boundary is missing or incorrect [1][3]. Best Practices and Solutions: 1. Do Not Set Content-Type Manually: In many cases, it is recommended to avoid manually setting the Content-Type header for FormData requests. React Native's underlying networking layer is designed to detect FormData and handle the Content-Type and boundary generation automatically [4]. 2. Use transformRequest: If Axios is incorrectly interfering with your FormData object or applying an unwanted default header (such as application/x-www-form-urlencoded), you can use the transformRequest option to ensure the raw FormData object is passed through [5][6]: axios.post(url, formData, { transformRequest: (data, headers) => { return data; } }); 3. Header Clearing Workaround: If you are forced to use a configuration where Axios is applying an incorrect header, you can set the header to false. This prevents Axios from appending a default value while allowing the underlying native networking stack to fill in the correct multipart/form-data header with the necessary boundary [1]: const config = { headers: { 'Content-Type': false, }, }; Recent Axios updates (as of mid-2026) have introduced fixes to better handle React Native environments by clearing default Content-Type headers when FormData is detected, aligning behavior more closely with standard web environments [7][2]. Always ensure you are using a recent version of Axios if you are experiencing these issues. Citations:
🌐 Web query:
💡 Result: In React Native, you should not manually set the Content-Type header when sending FormData with Axios [1]. React Native's underlying networking layer (OkHttp on Android) requires the Content-Type to be absent so that it can automatically generate the multipart boundary required for the request [2][3][4]. If you explicitly set 'Content-Type': 'multipart/form-data', Axios may send this incomplete header to the React Native networking module, which often leads to errors—particularly on Android—because it lacks the necessary boundary string [2][3]. To correctly send FormData in React Native: 1. Do not include Content-Type in your request headers [1]. 2. Pass the FormData object directly as the data payload [1]. If you are encountering issues where Axios is forcing an incorrect Content-Type (such as application/x-www-form-urlencoded), you can override this behavior by explicitly setting the header to false in your request configuration [2]: const response = await axios.post(url, formData, { headers: { 'Content-Type': false, }, }); Setting 'Content-Type': false signals Axios to drop the default header, allowing React Native to handle the request correctly and append the boundary automatically [2]. This approach is generally preferred over manual workarounds as it aligns with Axios' internal logic for handling FormData across different environments [2][4]. Citations:
🌐 Web query:
💡 Result: When working with FormData in React Native, you should not manually set the Content-Type header [1]. If you encounter issues where the request fails or is malformed, setting Content-Type to false is a common workaround to force the underlying network layer to handle the header correctly [2][3]. Here is a summary of the best practices and technical context: 1. Automatic Handling: React Native's networking layer (via the native FormData implementation) is designed to automatically generate the Content-Type header, including the necessary multipart boundary (e.g., boundary=----RNGeneratedBoundary...) [4]. 2. The Problem with Manual Headers: If you manually set Content-Type: multipart/form-data, you often omit the required boundary [4][5]. React Native's Android implementation, in particular, is sensitive to this and will reject requests that have a bare multipart/form-data header without a boundary, often resulting in a Network Error or a crash [2][4]. 3. The "Content-Type: false" Workaround: Axios automatically applies a default Content-Type (like application/x-www-form-urlencoded) to certain requests [3]. If you pass FormData, this default header can conflict with React Native's requirements [3]. Setting "Content-Type": false in your headers object tells Axios to omit the header entirely, allowing React Native to inject the correct Content-Type with the required boundary automatically [3]. 4. Recent Updates: As of May 2026, improvements have been made to Axios to treat React Native FormData similarly to browser FormData, meaning the library should now automatically clear the default Content-Type in most cases [6]. You should verify your library version to see if this manual override is still necessary for your specific project [6]. If you are using an older version of Axios and experiencing issues on Android, using { "Content-Type": false } is the recommended approach to resolve conflicts between Axios's default headers and React Native's native network module [2][3]. Citations:
Remove the manual
🐛 Proposed fix to let React Native set the multipart boundary const response = await api.post<ChatAttachmentUploadedResult>(`${CHAT}/UploadAttachment`, form, {
params: { channelId, messageId },
- headers: { 'Content-Type': 'multipart/form-data' },
});📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| /** Absolute URL for downloading an attachment's binary. */ | ||||||||||||||||||||||||||||||||||||||||||||
| export const getChatAttachmentUrl = (attachmentId: string): string => `${getBaseApiUrl()}${CHAT}/GetAttachment?attachmentId=${encodeURIComponent(attachmentId)}`; | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| /** Absolute URL for downloading an attachment's thumbnail. */ | ||||||||||||||||||||||||||||||||||||||||||||
| export const getChatAttachmentThumbnailUrl = (attachmentId: string): string => `${getBaseApiUrl()}${CHAT}/GetAttachmentThumbnail?attachmentId=${encodeURIComponent(attachmentId)}`; | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||
| * Image source (with bearer auth header) suitable for expo-image / RN Image | ||||||||||||||||||||||||||||||||||||||||||||
| * when rendering a chat attachment. | ||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||
| export const getChatAttachmentImageSource = (attachmentId: string) => { | ||||||||||||||||||||||||||||||||||||||||||||
| const token = useAuthStore.getState().accessToken; | ||||||||||||||||||||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||||||||||||||||||||
| uri: getChatAttachmentUrl(attachmentId), | ||||||||||||||||||||||||||||||||||||||||||||
| headers: token ? { Authorization: `Bearer ${token}` } : undefined, | ||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| // --------------------------------------------------------------------------- | ||||||||||||||||||||||||||||||||||||||||||||
| // Search, GIFs, presence, flags, moderation | ||||||||||||||||||||||||||||||||||||||||||||
| // --------------------------------------------------------------------------- | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| export const searchMessages = async (q: string, channelId?: string, page = 0, signal?: AbortSignal) => { | ||||||||||||||||||||||||||||||||||||||||||||
| const response = await api.get<ChatV4Response<ChatMessageResultData[]>>(`${CHAT}/Search`, { | ||||||||||||||||||||||||||||||||||||||||||||
| params: { q, channelId, page }, | ||||||||||||||||||||||||||||||||||||||||||||
| signal, | ||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||
| return response.data; | ||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| export const searchGifs = async (q?: string, limit = 25, offset = 0, signal?: AbortSignal) => { | ||||||||||||||||||||||||||||||||||||||||||||
| const response = await api.get<ChatV4Response<GifResultData[]>>(`${CHAT}/SearchGifs`, { | ||||||||||||||||||||||||||||||||||||||||||||
| params: { q, limit, offset }, | ||||||||||||||||||||||||||||||||||||||||||||
| signal, | ||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||
| return response.data; | ||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| export const getPresence = async (userIds: string[], signal?: AbortSignal) => { | ||||||||||||||||||||||||||||||||||||||||||||
| const response = await api.get<GetChatPresenceResult>(`${CHAT}/GetPresence`, { | ||||||||||||||||||||||||||||||||||||||||||||
| params: { userIds: userIds.join(',') }, | ||||||||||||||||||||||||||||||||||||||||||||
| signal, | ||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||
| return response.data; | ||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| export const flagMessage = async (messageId: string, input: FlagMessageInput) => { | ||||||||||||||||||||||||||||||||||||||||||||
| const response = await api.post<ChatActionResult>(`${CHAT}/FlagMessage`, input, { params: { messageId } }); | ||||||||||||||||||||||||||||||||||||||||||||
| return response.data; | ||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| /** Department-admin / moderator hard delete of a message. */ | ||||||||||||||||||||||||||||||||||||||||||||
| export const moderatorDeleteMessage = async (messageId: string, reason: string) => { | ||||||||||||||||||||||||||||||||||||||||||||
| const response = await api.post<ChatActionResult>(`${MODERATION}/DeleteMessage`, {}, { params: { messageId, reason } }); | ||||||||||||||||||||||||||||||||||||||||||||
| return response.data; | ||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| import { type ChatbotChannelResponse, type ChatbotSendResponse, type ChatbotSessionResponse } from '@/models/v4/chat'; | ||
|
|
||
| import { api } from '../common/client'; | ||
|
|
||
| const CHATBOT = '/Chatbot'; | ||
|
|
||
| /** Gets (creating if needed) the caller's chatbot conversation channel. */ | ||
| export const getChatbotChannel = async (signal?: AbortSignal) => { | ||
| const response = await api.get<ChatbotChannelResponse>(`${CHATBOT}/GetChatChannel`, { signal }); | ||
| return response.data; | ||
| }; | ||
|
|
||
| /** | ||
| * Sends a message to the chatbot. The reply arrives asynchronously in the same | ||
| * channel over SignalR (chatbotMessageReceived). Idempotent via clientMessageId. | ||
| */ | ||
| export const sendChatbotMessage = async (text: string, clientMessageId: string) => { | ||
| const response = await api.post<ChatbotSendResponse>(`${CHATBOT}/SendChatMessage`, { | ||
| Text: text, | ||
| ClientMessageId: clientMessageId, | ||
| }); | ||
| return response.data; | ||
| }; | ||
|
|
||
| /** Resets the chatbot conversational session (message history is retained). */ | ||
| export const newChatbotSession = async () => { | ||
| const response = await api.post<ChatbotSessionResponse>(`${CHATBOT}/NewChatSession`, {}); | ||
| return response.data; | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
Repository: Resgrid/Unit
Length of output: 39843
🏁 Script executed:
Repository: Resgrid/Unit
Length of output: 15524
🌐 Web query:
axios multipart/form-data React Native set Content-Type header FormData boundary💡 Result:
When using
FormDatawith Axios in React Native, you should not manually set theContent-Typeheader [1][2]. The React Native runtime environment, specifically its networking layer, is responsible for generating the multipart boundary string required for successful multipart/form-data requests [3][4]. If you manually set theContent-Typeheader tomultipart/form-data, you overwrite the internal boundary generation process, which often leads to errors—particularly on Android, where the native networking layer strictly requires the boundary to be present in the header [5][3][4]. Best Practices: 1. Do not set headers manually: Simply pass yourFormDataobject directly to thedataproperty of your Axios request [1][2]. 2. Ensure you are using a recent version of Axios: Updates (notably those around May 2026) have improved how Axios handlesFormDatain React Native by ensuring default headers likeapplication/x-www-form-urlencodedare properly cleared whenFormDatais detected, allowing the React Native runtime to set the correctContent-Typewith the boundary [6][5]. 3. Troubleshooting: If you encounter issues where the request is sent with an incorrect header (e.g.,application/x-www-form-urlencoded) despite usingFormData, ensure your environment'sFormDatais being correctly identified by Axios [5]. As a last-resort workaround, you can explicitly unset the header in your request config to allow the runtime to take over:javascript // Only use this if you are encountering issues with automatic header resolution axios.post(url, formData, { headers: { 'Content-Type': false // This prevents axios from applying a default type } });Setting the header tofalseinstructs Axios to omit theContent-Typeheader entirely, which forces the React NativeNetworkingModuleto handle the request correctly [5].Citations:
multipart/form-dataplaceholder header inpostForm/putForm/patchForm(breaks RN Android) axios/axios#10886🏁 Script executed:
Repository: Resgrid/Unit
Length of output: 12790
Use the endpoint factory for all chat APIs and invalidate chat caches after mutations.
createApiEndpoint/createCachedApiEndpointprovide the shared path wrapper used by othersrc/api/**modules, while directapi.get/api.post/api.put/api.deletecalls bypass that pattern. Cacheable reads such as channel/message lists should usecreateCachedApiEndpoint; create/update/delete chat operations should callcacheManager.remove()for the affected endpoints after a successful mutation.🤖 Prompt for AI Agents
Source: Coding guidelines