Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
4 changes: 2 additions & 2 deletions src/api/chat/chatbot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ 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;
return response.data?.Data ?? null;
};

/**
Expand All @@ -19,7 +19,7 @@ export const sendChatbotMessage = async (text: string, clientMessageId: string)
Text: text,
ClientMessageId: clientMessageId,
});
return response.data;
return response.data?.Data ?? null;
};

/** Resets the chatbot conversational session (message history is retained). */
Expand Down
7 changes: 7 additions & 0 deletions src/app/(app)/chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,13 @@ export default function ChatScreen() {

const openChannel = useCallback(
(channelId: string) => {
// The assistant conversation always opens in its dedicated restricted screen
// (text only, no reactions/threads/deletes) instead of the generic conversation.
const channel = useChatStore.getState().channels.find((c) => c.ChatChannelId === channelId);
if (channel?.ChannelType === ChatChannelType.Chatbot) {
router.push('/chatbot' as Href);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kody code-review Kody Rules low

Hardcoded route string literal '/chatbot' (also in src/app/chat/[channelId].tsx:273). Centralize route paths as constants/enums, e.g. const Routes = { Chatbot: '/chatbot', Chat: '/chat' } as const, and reference Routes.Chatbot instead of the raw string.

Kody rule violation: Centralize string constants

Prompt for LLM

File src/app/(app)/chat.tsx:

Line 110:

Hardcoded route string literal `'/chatbot'` (also in `src/app/chat/[channelId].tsx:273`). Centralize route paths as constants/enums, e.g. `const Routes = { Chatbot: '/chatbot', Chat: '/chat' } as const`, and reference `Routes.Chatbot` instead of the raw string.

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

return;
}
router.push(`/chat/${channelId}` as Href);
},
[router]
Expand Down
64 changes: 63 additions & 1 deletion src/app/(app)/chatbot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,13 @@
import { useTranslation } from 'react-i18next';
import { FlatList, Platform } from 'react-native';

import { copyToClipboard } from '@/components/chat/chat-utils';
import { MessageActionsSheet } from '@/components/chat/message-actions-sheet';
import { MessageBubble } from '@/components/chat/message-bubble';
import { TypingDots } from '@/components/chat/typing-indicator';
import { Actionsheet, ActionsheetBackdrop, ActionsheetContent, ActionsheetDragIndicator, ActionsheetDragIndicatorWrapper } from '@/components/ui/actionsheet';
import { Box } from '@/components/ui/box';
import { Button, ButtonText } from '@/components/ui/button';
import { Center } from '@/components/ui/center';
import { FocusAwareStatusBar } from '@/components/ui/focus-aware-status-bar';
import { HStack } from '@/components/ui/hstack';
Expand All @@ -15,19 +19,26 @@
import { Pressable } from '@/components/ui/pressable';
import { Spinner } from '@/components/ui/spinner';
import { Text } from '@/components/ui/text';
import { Textarea, TextareaInput } from '@/components/ui/textarea';
import { VStack } from '@/components/ui/vstack';
import { type ChatMessageResultData } from '@/models/v4/chat';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'Locate chatbot.tsx files:\n'
fd -a 'chatbot\.tsx$' . || true

file="$(fd 'chatbot\.tsx$' . | head -n 1 || true)"
if [ -n "$file" ]; then
  printf '\nOutline for %s:\n' "$file"
  ast-grep outline "$file" --view compact || true
  printf '\nFirst 45 lines of %s:\n' "$file"
  sed -n '1,45p' "$file" | cat -n
fi

printf '\nSearch for ChatMessageResultData imports/usages:\n'
rg -n "import\s*(type\s*)?\{\s*ChatMessageResultData\s*\}|\bChatMessageResultData\b" -g '*.ts' -g '*.tsx' . || true

Repository: Resgrid/IC

Length of output: 10371


Use a type-only import for ChatMessageResultData.

Replace the inline type specifier on line 24 with import type { ChatMessageResultData } from '@/models/v4/chat';. Keep this type-only import with the other imports and after value imports.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/`(app)/chatbot.tsx at line 24, Update the ChatMessageResultData
import in the chatbot module from an inline type specifier to a type-only import
declaration, keeping it grouped with the other imports after value imports.

Source: Coding guidelines

import useAuthStore from '@/stores/auth/store';
import { useChatStore } from '@/stores/chat/store';
import { useChatSystemStatus } from '@/stores/feature-flags/store';
import { securityStore } from '@/stores/security/store';
import { useToastStore } from '@/stores/toast/store';

export default function ChatbotScreen() {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

fd -H 'tsconfig*.json' . -x sed -n '1,220p' {}
rg -n --glob '*.{ts,tsx}' 'import type \* as React|React\.FC' src

Repository: Resgrid/IC

Length of output: 20479


🏁 Script executed:

#!/bin/bash
set -euo pipefail

for f in \
  'src/app/(app)/chatbot.tsx' \
  'src/components/chat/message-actions-sheet.tsx' \
  'src/components/chat/message-composer.tsx'
do
  echo "===== $f ====="
  fd -a "$f" . | sed 's#^\./##'
  wc -l "$f"
  sed -n '1,80p' "$f" | cat -n
done

echo "===== React imports/usages in target files ====="
rg -n 'import (React|type React|.*React)|React\.FC' 'src/app/(app)/chatbot.tsx' 'src/components/chat/message-actions-sheet.tsx' 'src/components/chat/message-composer.tsx'

Repository: Resgrid/IC

Length of output: 12718


Type these React components with React.FC.

Use the configured component declaration form at each site.

  • src/app/(app)/chatbot.tsx#L31-L31: declare ChatbotScreen as React.FC.
  • src/components/chat/message-actions-sheet.tsx#L31-L31: declare MessageActionsSheet as React.FC<MessageActionsSheetProps>.
  • src/components/chat/message-composer.tsx#L32-L32: declare MessageComposer as React.FC<MessageComposerProps>.
📍 Affects 3 files
  • src/app/(app)/chatbot.tsx#L31-L31 (this comment)
  • src/components/chat/message-actions-sheet.tsx#L31-L31
  • src/components/chat/message-composer.tsx#L32-L32
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/`(app)/chatbot.tsx at line 31, Update the component declarations to
use the configured React.FC form: declare ChatbotScreen as React.FC in
src/app/(app)/chatbot.tsx (lines 31-31), MessageActionsSheet as
React.FC<MessageActionsSheetProps> in
src/components/chat/message-actions-sheet.tsx (lines 31-31), and MessageComposer
as React.FC<MessageComposerProps> in src/components/chat/message-composer.tsx
(lines 32-32).

Source: Coding guidelines

const { t } = useTranslation();
const currentUserId = useAuthStore((s) => s.userId);
const chatbotChannelId = useChatStore((s) => s.chatbotChannelId);
const chatbotTyping = useChatStore((s) => s.chatbotTyping);
const messages = useChatStore((s) => (chatbotChannelId ? s.messagesByChannel[chatbotChannelId] : undefined));
const isModerator = !!securityStore((s) => s.rights)?.IsAdmin;
const [text, setText] = useState('');
const [actionsMessage, setActionsMessage] = useState<ChatMessageResultData | null>(null);
const [editMessage, setEditMessage] = useState<ChatMessageResultData | null>(null);
const [editText, setEditText] = useState('');
const chatStatus = useChatSystemStatus();
const isChatEnabled = chatStatus === 'enabled';

Expand Down Expand Up @@ -61,7 +72,7 @@

const renderItem = useCallback(
({ item }: { item: ChatMessageResultData }) => (
<MessageBubble message={item} isOwn={!!item.SenderUserId && item.SenderUserId === currentUserId} showSender={false} currentUserId={currentUserId} onLongPress={() => undefined} onToggleReaction={() => undefined} />
<MessageBubble message={item} isOwn={!!item.SenderUserId && item.SenderUserId === currentUserId} showSender={false} currentUserId={currentUserId} onLongPress={setActionsMessage} onToggleReaction={() => undefined} />

Check warning on line 75 in src/app/(app)/chatbot.tsx

View workflow job for this annotation

GitHub Actions / test

Replace `·message={item}·isOwn={!!item.SenderUserId·&&·item.SenderUserId·===·currentUserId}·showSender={false}·currentUserId={currentUserId}·onLongPress={setActionsMessage}·onToggleReaction={()·=>·undefined}` with `⏎········message={item}⏎········isOwn={!!item.SenderUserId·&&·item.SenderUserId·===·currentUserId}⏎········showSender={false}⏎········currentUserId={currentUserId}⏎········onLongPress={setActionsMessage}⏎········onToggleReaction={()·=>·undefined}⏎·····`

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kody code-review Kody Rules high

Inline arrow functions and .bind() calls in JSX props create new function references on every render, impacting performance. Extract these handlers to stable useCallback definitions outside the render method.

Kody rule violation: Avoid using .bind() or arrow functions in JSX props

Prompt for LLM

File src/app/(app)/chatbot.tsx:

Line 75:

Inline arrow functions and `.bind()` calls in JSX props create new function references on every render, impacting performance. Extract these handlers to stable `useCallback` definitions outside the render method.

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

),
[currentUserId]
);
Expand Down Expand Up @@ -134,6 +145,57 @@
</Pressable>
</HStack>
</KeyboardAvoidingView>

{/* Restricted actions for assistant messages: copy, edit own, pin (moderator), flag. */}
<MessageActionsSheet
message={actionsMessage}
isOpen={actionsMessage !== null}
onClose={() => setActionsMessage(null)}
isOwn={!!actionsMessage?.SenderUserId && actionsMessage.SenderUserId === currentUserId}
isModerator={isModerator}
assistant
onReact={() => undefined}
onReply={() => undefined}
onCopy={async (m) => {
const ok = await copyToClipboard(m.Body ?? '');
useToastStore.getState().showToast(ok ? 'success' : 'info', ok ? t('chat.copied') : t('chat.copy_unavailable'));
Comment on lines +160 to +161

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kody code-review Kody Rules high

Unhandled promise rejection from await copyToClipboard(...) in the onCopy async handler (chatbot.tsx:189). Wrap the call in try/catch and surface a user-facing error toast on failure.

Kody rule violation: Handle async operations with proper error handling

Prompt for LLM

File src/app/(app)/chatbot.tsx:

Line 160 to 161:

Unhandled promise rejection from `await copyToClipboard(...)` in the `onCopy` async handler (chatbot.tsx:189). Wrap the call in try/catch and surface a user-facing error toast on failure.

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

Comment on lines +160 to +161

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kody code-review Kody Rules high

Missing error handling around external clipboard API call copyToClipboard. Wrap in try/catch with context, log/handle the failure with the relevant message id, and surface a user-facing error toast.

Kody rule violation: Add try-catch blocks for external calls

Prompt for LLM

File src/app/(app)/chatbot.tsx:

Line 160 to 161:

Missing error handling around external clipboard API call `copyToClipboard`. Wrap in try/catch with context, log/handle the failure with the relevant message id, and surface a user-facing error toast.

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

}}
onEdit={(m) => {
setEditMessage(m);
setEditText(m.Body ?? '');
}}
onDelete={() => undefined}
onFlag={(m, reason) => useChatStore.getState().flagMessage(m.ChatMessageId, reason)}
onTogglePin={(m, pinned) => chatbotChannelId && useChatStore.getState().togglePin(m.ChatMessageId, chatbotChannelId, pinned)}
onModeratorDelete={() => undefined}
/>

{/* Edit own message */}
<Actionsheet isOpen={editMessage !== null} onClose={() => setEditMessage(null)}>
<ActionsheetBackdrop />
<ActionsheetContent>
<ActionsheetDragIndicatorWrapper>
<ActionsheetDragIndicator />
</ActionsheetDragIndicatorWrapper>
<VStack className="w-full p-2" space="md">
<Text className="text-base font-semibold text-typography-900">{t('chat.edit_message')}</Text>
<Textarea>
<TextareaInput value={editText} onChangeText={setEditText} multiline />
</Textarea>
<Button
className="bg-primary-600"
onPress={() => {
if (editMessage && chatbotChannelId && editText.trim()) {
void useChatStore.getState().editMessage(editMessage.ChatMessageId, chatbotChannelId, editText.trim());
}
setEditMessage(null);
}}
>
<ButtonText>{t('chat.save')}</ButtonText>
</Button>
</VStack>
</ActionsheetContent>
</Actionsheet>
</Box>
);
}
45 changes: 39 additions & 6 deletions src/app/chat/[channelId].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,15 @@ export default function ChannelConversationScreen() {
const [editText, setEditText] = useState('');
const [imageUri, setImageUri] = useState<string | null>(null);
const [presenceIds, setPresenceIds] = useState<Set<string>>(new Set());
const [resolveAttempted, setResolveAttempted] = useState(false);
const unsubscribeRef = useRef<(() => void) | null>(null);

const isDm = channel?.ChannelType === ChatChannelType.DirectMessage;
const isChatbot = channel?.ChannelType === ChatChannelType.Chatbot;
// Deep links (push notifications, cold starts) can arrive before the channel
// list loads; the channel type is unknown until then. Treat a completed fetch
// with no match as resolved so unknown channels keep the generic screen.
const isResolved = !!channel || resolveAttempted;
const showSender = !isDm;
// IC delta: in command-type channels the user posts as the Incident Commander.
// The server validates the user actually holds command (CanSendAsIcAsync) and rejects otherwise.
Expand All @@ -72,11 +78,21 @@ export default function ChannelConversationScreen() {
// Newest-first for the inverted list.
const inverted = useMemo(() => (messages ? messages.slice().reverse() : []), [messages]);

// Mount: activate channel, join hub, load history and members.
// Resolve the channel identity for deep links before mounting the generic view.
useEffect(() => {
if (channel || resolveAttempted || !isChatEnabled) return;
void useChatStore
.getState()
.fetchChannels()
.finally(() => setResolveAttempted(true));
Comment on lines +84 to +87

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kody code-review Kody Rules high

Unhandled promise rejection occurs because the promise returned by fetchChannels() chains .finally() without a .catch() handler. Add a .catch() handler before .finally() to log the error, satisfying Rule [1] and preventing app crashes or resolveAttempted remaining false indefinitely.

Kody rule violation: Handle async operations with proper error handling

Prompt for LLM

File src/app/chat/[channelId].tsx:

Line 84 to 87:

Unhandled promise rejection occurs because the promise returned by `fetchChannels()` chains `.finally()` without a `.catch()` handler. Add a `.catch()` handler before `.finally()` to log the error, satisfying Rule [1] and preventing app crashes or `resolveAttempted` remaining `false` indefinitely.

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

}, [channel, resolveAttempted, isChatEnabled]);

// Mount: activate channel, join hub, load history and members. Assistant
// conversations are handled by the dedicated chatbot screen — never join or
// load them here, and wait for unresolved deep links to identify first.
useFocusEffect(
useCallback(() => {
if (!isChatEnabled) return;
if (!channelId) return;
if (!channelId || !isChatEnabled || !isResolved || isChatbot) return;
const store = useChatStore.getState();
store.setActiveChannel(channelId);
void store.joinChannel(channelId);
Expand All @@ -85,7 +101,7 @@ export default function ChannelConversationScreen() {
return () => {
useChatStore.getState().setActiveChannel(null);
};
}, [channelId, isChatEnabled])
}, [channelId, isChatEnabled, isResolved, isChatbot])
);

// Fetch presence for the channel members (for the header online dot).
Expand All @@ -107,10 +123,10 @@ export default function ChannelConversationScreen() {
// Mark read whenever the newest message changes while viewing.
useEffect(() => {
if (!isChatEnabled) return;
if (channelId && inverted.length > 0) {
if (channelId && isResolved && !isChatbot && inverted.length > 0) {
void useChatStore.getState().markChannelRead(channelId);
}
}, [channelId, inverted.length, isChatEnabled]);
}, [channelId, inverted.length, isChatEnabled, isResolved, isChatbot]);

const otherOnline = useMemo(() => {
if (!isDm) return false;
Expand Down Expand Up @@ -267,6 +283,23 @@ export default function ChannelConversationScreen() {
return <Redirect href="/" />;
}

// Deep link to a channel that isn't loaded yet: wait for the channel list so
// assistant conversations never mount the full-featured view.
if (!isResolved) {
return (
<Box className="size-full flex-1 items-center justify-center bg-background-0">
<Stack.Screen options={{ title, headerShown: true, headerBackTitle: '' }} />
<Spinner />
</Box>
);
}

// Assistant conversations always use the dedicated restricted screen (text only,
// no reactions/threads/deletes) — catch deep links and stale routes here.
if (isChatbot) {
return <Redirect href={'/chatbot' as Href} />;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

return (
<Box className="size-full flex-1 bg-background-0">
<Stack.Screen
Expand Down
2 changes: 1 addition & 1 deletion src/app/chat/thread/[messageId].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@

<FlatList data={inverted} inverted keyExtractor={(item: ChatMessageResultData) => item.ChatMessageId} renderItem={renderItem} contentContainerStyle={{ paddingVertical: 8 }} />

<MessageComposer onSendText={handleSendText} onSendImage={() => undefined} onSendLocation={handleSendLocation} onOpenGif={handleSendGif} onTyping={() => undefined} placeholder={t('chat.reply_placeholder')} />
<MessageComposer onSendText={handleSendText} onSendImage={() => undefined} onSendLocation={handleSendLocation} onOpenGif={handleSendGif} onTyping={() => undefined} placeholder={t('chat.reply_placeholder')} allowUrgent={false} />

Check warning on line 139 in src/app/chat/thread/[messageId].tsx

View workflow job for this annotation

GitHub Actions / test

Replace `·onSendText={handleSendText}·onSendImage={()·=>·undefined}·onSendLocation={handleSendLocation}·onOpenGif={handleSendGif}·onTyping={()·=>·undefined}·placeholder={t('chat.reply_placeholder')}·allowUrgent={false}` with `⏎··········onSendText={handleSendText}⏎··········onSendImage={()·=>·undefined}⏎··········onSendLocation={handleSendLocation}⏎··········onOpenGif={handleSendGif}⏎··········onTyping={()·=>·undefined}⏎··········placeholder={t('chat.reply_placeholder')}⏎··········allowUrgent={false}⏎·······`
</KeyboardAvoidingView>
</Box>
);
Expand Down
55 changes: 54 additions & 1 deletion src/components/chat/__tests__/chat-utils.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import * as Clipboard from 'expo-clipboard';
import { type TFunction } from 'i18next';

import { ChatChannelType, type ChatChannelResultData } from '@/models/v4/chat';

import { getChannelDisplayName, getImageMimeType, hasLink, linkifySegments } from '../chat-utils';
import { copyToClipboard, getChannelDisplayName, getImageMimeType, hasLink, linkifySegments } from '../chat-utils';

jest.mock('expo-clipboard', () => ({ setStringAsync: jest.fn() }));

const mockT = ((key: string) => key) as TFunction;

Expand Down Expand Up @@ -94,4 +97,54 @@ describe('chat-utils', () => {
expect(linkifySegments('')).toEqual([]);
});
});

describe('copyToClipboard', () => {
const globalWithNavigator = globalThis as unknown as { navigator?: { clipboard?: { writeText?: (value: string) => Promise<void> } } };
let originalNavigator: unknown;

beforeEach(() => {
originalNavigator = globalWithNavigator.navigator;
jest.mocked(Clipboard.setStringAsync).mockReset();
});

afterEach(() => {
if (originalNavigator === undefined) {
delete globalWithNavigator.navigator;
} else {
globalWithNavigator.navigator = originalNavigator as typeof globalWithNavigator.navigator;
}
});

it('uses the web clipboard API when available', async () => {
const writeText = jest.fn().mockResolvedValue(undefined);
globalWithNavigator.navigator = { clipboard: { writeText } };

await expect(copyToClipboard('hello')).resolves.toBe(true);
expect(writeText).toHaveBeenCalledWith('hello');
expect(Clipboard.setStringAsync).not.toHaveBeenCalled();
});

it('falls back to the native module when the web API is unavailable', async () => {
delete globalWithNavigator.navigator;
jest.mocked(Clipboard.setStringAsync).mockResolvedValue(true);

await expect(copyToClipboard('hello')).resolves.toBe(true);
expect(Clipboard.setStringAsync).toHaveBeenCalledWith('hello');
});

it('falls back to the native module when the web API write fails', async () => {
globalWithNavigator.navigator = { clipboard: { writeText: jest.fn().mockRejectedValue(new Error('denied')) } };
jest.mocked(Clipboard.setStringAsync).mockResolvedValue(true);

await expect(copyToClipboard('hello')).resolves.toBe(true);
expect(Clipboard.setStringAsync).toHaveBeenCalledWith('hello');
});

it('returns false when the native write fails', async () => {
delete globalWithNavigator.navigator;
jest.mocked(Clipboard.setStringAsync).mockRejectedValue(new Error('unavailable'));

await expect(copyToClipboard('hello')).resolves.toBe(false);
});
});
});
15 changes: 10 additions & 5 deletions src/components/chat/chat-utils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import * as Clipboard from 'expo-clipboard';
import { type TFunction } from 'i18next';

import { getAvatarUrl } from '@/lib/utils';
Expand Down Expand Up @@ -105,9 +106,9 @@ export function hasLink(body?: string | null): boolean {
}

/**
* Copies text to the clipboard. Works on web/Electron via the async Clipboard
* API; native returns false (no clipboard native module is installed) so callers
* can surface an appropriate message.
* Copies text to the clipboard. Uses the async Clipboard API on web/Electron
* and expo-clipboard on native; returns false only when both are unavailable
* or the write fails, so callers can surface an appropriate message.
*/
export async function copyToClipboard(text: string): Promise<boolean> {
try {
Expand All @@ -117,9 +118,13 @@ export async function copyToClipboard(text: string): Promise<boolean> {
return true;
}
} catch {
// ignore and fall through
// ignore and fall through to the native module
}
try {
return await Clipboard.setStringAsync(text);
} catch {
return false;
Comment on lines +125 to +126

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kody code-review Kody Rules high

Silent exception swallowing occurs in the catch block for the expo-clipboard call, violating Rule [28] by returning false without logging. Capture the exception variable and log it with context before returning to ensure explicit error handling.

Kody rule violation: Avoid empty catch blocks

Prompt for LLM

File src/components/chat/chat-utils.ts:

Line 125 to 126:

Silent exception swallowing occurs in the `catch` block for the `expo-clipboard` call, violating Rule [28] by returning `false` without logging. Capture the exception variable and log it with context before returning to ensure explicit error handling.

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

}
Comment on lines +123 to 127

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kody code-review Bug high

Returning await Clipboard.setStringAsync(text) resolves with undefined instead of true because setStringAsync returns Promise<void>. This causes both callers ([channelId].tsx:373 and chatbot.tsx:160) to falsely report 'copy unavailable'; explicitly return true after awaiting the call to resolve the issue.

try {
  await Clipboard.setStringAsync(text);
  return true;
} catch {
  return false;
}
Prompt for LLM

File src/components/chat/chat-utils.ts:

Line 123 to 127:

Returning `await Clipboard.setStringAsync(text)` resolves with `undefined` instead of `true` because `setStringAsync` returns `Promise<void>`. This causes both callers (`[channelId].tsx:373` and `chatbot.tsx:160`) to falsely report 'copy unavailable'; explicitly return `true` after awaiting the call to resolve the issue.

Suggested Code:

  try {
    await Clipboard.setStringAsync(text);
    return true;
  } catch {
    return false;
  }

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

return false;
}

const IMAGE_MIME_BY_EXTENSION: Record<string, string> = {
Expand Down
Loading
Loading