-
Notifications
You must be signed in to change notification settings - Fork 6
Develop #265
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
Develop #265
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,139 @@ | ||
| import { router } from 'expo-router'; | ||
| import { MessageCircle, MessagesSquare, Radio, ShieldCheck, Users } from 'lucide-react-native'; | ||
| import React, { useCallback } from 'react'; | ||
| import { useTranslation } from 'react-i18next'; | ||
|
|
||
| import { Box } from '@/components/ui/box'; | ||
| import { Heading } from '@/components/ui/heading'; | ||
| import { HStack } from '@/components/ui/hstack'; | ||
| import { Pressable } from '@/components/ui/pressable'; | ||
| import { Text } from '@/components/ui/text'; | ||
| import { VStack } from '@/components/ui/vstack'; | ||
| import { useDirectMessage } from '@/hooks/use-direct-message'; | ||
| import { type IncidentContactInfo, type ResourceIncidentView } from '@/models/v4/incidentCommand/resourceIncidentView'; | ||
|
|
||
| import { getIncidentRoleName } from './incident-role-names'; | ||
|
|
||
| interface IncidentChatSectionProps { | ||
| view: ResourceIncidentView; | ||
| testID?: string; | ||
| } | ||
|
|
||
| /** One tappable channel row. Rendered only when the server handed us an id we are allowed to open. */ | ||
| const ChannelRow: React.FC<{ label: string; hint?: string | null; icon: React.ElementType; channelId?: string | null; testID: string }> = ({ label, hint, icon: IconComponent, channelId, testID }) => { | ||
| const onPress = useCallback(() => { | ||
| if (channelId) { | ||
| router.push(`/chat/${channelId}`); | ||
|
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. Inline route path Kody rule violation: Centralize string constants Prompt for LLMTalk to Kody by mentioning @kody Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction. |
||
| } | ||
| }, [channelId]); | ||
|
|
||
| if (!channelId) { | ||
| return null; | ||
| } | ||
|
|
||
| return ( | ||
| <Pressable onPress={onPress} className="rounded-lg border border-neutral-200 p-3 dark:border-neutral-700" testID={testID}> | ||
| <HStack className="items-center" space="sm"> | ||
| <IconComponent size={18} color="#3b82f6" /> | ||
| <VStack className="min-w-0 flex-1"> | ||
| <Text className="font-medium">{label}</Text> | ||
| {hint ? <Text className="text-xs text-gray-500">{hint}</Text> : null} | ||
| </VStack> | ||
| </HStack> | ||
| </Pressable> | ||
| ); | ||
| }; | ||
|
|
||
| /** A person on the incident with a button to open a 1:1 with them. */ | ||
| const ContactRow: React.FC<{ label: string; contact?: IncidentContactInfo | null; onMessage: (userId?: string | null) => void; testID: string }> = ({ label, contact, onMessage, testID }) => { | ||
| const { t } = useTranslation(); | ||
| const handlePress = useCallback(() => onMessage(contact?.UserId), [onMessage, contact]); | ||
|
|
||
| if (!contact) { | ||
| return null; | ||
| } | ||
|
|
||
| return ( | ||
| <HStack className="items-center justify-between py-1" testID={testID}> | ||
| <VStack className="min-w-0 flex-1"> | ||
| <Text className="text-xs text-gray-500">{label}</Text> | ||
| <Text className="font-medium">{contact.Name}</Text> | ||
| </VStack> | ||
| {/* External contacts have a name and phone but no Resgrid account, so no 1:1 to open. */} | ||
| {contact.UserId ? ( | ||
| <Pressable onPress={handlePress} className="p-3" hitSlop={8} accessibilityLabel={t('incident_command.message_person', { name: contact.Name })} testID={`${testID}-message`}> | ||
|
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. Inline numeric literal Kody rule violation: Replace magic numbers with named constants Prompt for LLMTalk to Kody by mentioning @kody Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction. |
||
| <MessageCircle size={20} color="#3b82f6" /> | ||
| </Pressable> | ||
| ) : null} | ||
| </HStack> | ||
| ); | ||
| }; | ||
|
|
||
| /** | ||
| * Incident chat on the Command tab: the channels this responder can open, and the people on the | ||
| * incident they can reach 1:1. | ||
| * | ||
| * Access is decided entirely by the server — a channel id only appears in the payload when the | ||
| * caller is actually allowed in, and it disappears again when they are (for example) taken off the | ||
| * lane. Nothing here infers access on its own. | ||
| */ | ||
| export const IncidentChatSection: React.FC<IncidentChatSectionProps> = ({ view, testID = 'incident-command-chat' }) => { | ||
| const { t } = useTranslation(); | ||
| const { openDirectMessage } = useDirectMessage(); | ||
|
|
||
| const chat = view.Chat; | ||
| const assignment = view.MyAssignment; | ||
| const roles = view.Roles ?? []; | ||
|
|
||
| const handleMessage = useCallback((userId?: string | null) => void openDirectMessage(userId), [openDirectMessage]); | ||
|
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. Unhandled rejection risk: Kody rule violation: Handle async operations with proper error handling Prompt for LLMTalk to Kody by mentioning @kody Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction. |
||
|
|
||
| const hasChannels = !!(chat?.IncidentChannelId || chat?.LaneChannelId || chat?.CommandChannelId || chat?.LeadsChannelId || chat?.DispatchChannelId); | ||
| // Only ICS role holders live here — the commander and this resource's lane leads already have | ||
| // contact cards higher up the panel, and listing them twice just made the screen noisy. | ||
| const hasContacts = roles.length > 0; | ||
|
|
||
| if (!hasChannels && !hasContacts) { | ||
| return null; | ||
| } | ||
|
Comment on lines
+93
to
+97
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. Orphaned UI element: const hasContacts = roles.some((role) => !!role.Contact);
if (!hasChannels && !hasContacts) {
return null;
}Prompt for LLMTalk to Kody by mentioning @kody Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction. |
||
|
|
||
| return ( | ||
| <Box testID={testID}> | ||
| <Heading size="sm" className="mb-2"> | ||
| {t('incident_command.chat')} | ||
| </Heading> | ||
|
|
||
| {chat?.IsFrozen ? ( | ||
| <Box className="mb-2 rounded-lg bg-neutral-100 p-2 dark:bg-neutral-800" testID={`${testID}-frozen`}> | ||
| <Text className="text-xs text-gray-600 dark:text-gray-300">{t('incident_command.chat_frozen')}</Text> | ||
| </Box> | ||
| ) : null} | ||
|
|
||
| {hasChannels ? ( | ||
| <VStack space="sm"> | ||
| <ChannelRow label={t('incident_command.incident_channel')} icon={MessagesSquare} channelId={chat?.IncidentChannelId} testID={`${testID}-incident`} /> | ||
| <ChannelRow label={t('incident_command.lane_channel')} hint={assignment?.LaneName} icon={MessagesSquare} channelId={chat?.LaneChannelId} testID={`${testID}-lane`} /> | ||
| <ChannelRow label={t('incident_command.command_channel')} hint={t('incident_command.command_channel_hint')} icon={ShieldCheck} channelId={chat?.CommandChannelId} testID={`${testID}-command`} /> | ||
| <ChannelRow label={t('incident_command.leads_channel')} hint={t('incident_command.leads_channel_hint')} icon={Users} channelId={chat?.LeadsChannelId} testID={`${testID}-leads`} /> | ||
| <ChannelRow label={t('incident_command.dispatch_channel')} hint={t('incident_command.dispatch_channel_hint')} icon={Radio} channelId={chat?.DispatchChannelId} testID={`${testID}-dispatch`} /> | ||
| </VStack> | ||
| ) : null} | ||
|
|
||
| {hasContacts ? ( | ||
| <Box className="mt-3"> | ||
| <Text className="mb-1 text-xs font-semibold uppercase text-gray-500">{t('incident_command.ics_positions')}</Text> | ||
| {roles.map((role) => ( | ||
| <ContactRow | ||
| key={`${role.RoleType}-${role.Contact?.UserId ?? ''}`} | ||
| label={getIncidentRoleName(t, role.RoleType)} | ||
| contact={role.Contact} | ||
| onMessage={handleMessage} | ||
| testID={`${testID}-contact-role-${role.RoleType}`} | ||
| /> | ||
| ))} | ||
| </Box> | ||
| ) : null} | ||
| </Box> | ||
| ); | ||
| }; | ||
|
|
||
| export default IncidentChatSection; | ||
|
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. Default export reduces clarity and refactoring ergonomics. Remove the default export and import the named export Kody rule violation: Avoid default exports Prompt for LLMTalk to Kody by mentioning @kody Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction. |
||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -1,5 +1,5 @@ | ||||||
| import { format } from 'date-fns'; | ||||||
| import { MailIcon, PhoneIcon } from 'lucide-react-native'; | ||||||
| import { MailIcon, MessageCircle, PhoneIcon } from 'lucide-react-native'; | ||||||
| import { useColorScheme } from 'nativewind'; | ||||||
| import React, { useEffect } from 'react'; | ||||||
| import { useTranslation } from 'react-i18next'; | ||||||
|
|
@@ -13,10 +13,13 @@ import { Pressable } from '@/components/ui/pressable'; | |||||
| import { Spinner } from '@/components/ui/spinner'; | ||||||
| import { Text } from '@/components/ui/text'; | ||||||
| import { VStack } from '@/components/ui/vstack'; | ||||||
| import { useDirectMessage } from '@/hooks/use-direct-message'; | ||||||
| import { logger } from '@/lib/logging'; | ||||||
| import { type IncidentContactInfo, IncidentNeedStatus, type TacticalObjective, TacticalObjectiveStatus } from '@/models/v4/incidentCommand/resourceIncidentView'; | ||||||
| import { useIncidentCommandStore } from '@/stores/calls/incident-command-store'; | ||||||
|
|
||||||
| import { IncidentChatSection } from './incident-chat-section'; | ||||||
|
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. 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win Use the configured path alias. Line 21 adds a relative import for Proposed fix-import { IncidentChatSection } from './incident-chat-section';
+import { IncidentChatSection } from '`@/components/incident-command/incident-chat-section`';As per coding guidelines, source files must use path aliases from 📝 Committable suggestion
Suggested change
🤖 Prompt for AI AgentsSource: Coding guidelines |
||||||
|
|
||||||
| interface IncidentCommandTabPanelProps { | ||||||
| callId: string; | ||||||
| } | ||||||
|
|
@@ -102,6 +105,9 @@ interface ContactRowProps { | |||||
| } | ||||||
|
|
||||||
| const ContactRow: React.FC<ContactRowProps> = ({ label, contact, testID }) => { | ||||||
| const { t } = useTranslation(); | ||||||
| const { openDirectMessage } = useDirectMessage(); | ||||||
|
|
||||||
| return ( | ||||||
| <Box className="border-b border-outline-100 pb-2" testID={testID}> | ||||||
| <Text className="text-sm text-gray-500">{label}</Text> | ||||||
|
|
@@ -122,6 +128,15 @@ const ContactRow: React.FC<ContactRowProps> = ({ label, contact, testID }) => { | |||||
| </HStack> | ||||||
| </Pressable> | ||||||
| ) : null} | ||||||
| {/* External contacts carry a name and phone but no Resgrid account, so there is nobody to message. */} | ||||||
| {contact.UserId ? ( | ||||||
| <Pressable onPress={() => void openDirectMessage(contact.UserId)} testID={`${testID}-message`}> | ||||||
|
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. Inline arrow function in the Kody rule violation: Avoid using .bind() or arrow functions in JSX props Prompt for LLMTalk to Kody by mentioning @kody Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction. |
||||||
| <HStack className="mt-1 items-center"> | ||||||
| <MessageCircle size={14} color="#3B82F6" /> | ||||||
|
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. 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win Use a semantic theme color for the new icon. Line 135 introduces hardcoded As per coding guidelines, use semantic color tokens instead of hardcoded hex values and support both light and dark mode. 🤖 Prompt for AI AgentsSource: Coding guidelines |
||||||
| <Text className="ml-1 text-sm text-blue-500">{t('incident_command.send_message')}</Text> | ||||||
| </HStack> | ||||||
| </Pressable> | ||||||
| ) : null} | ||||||
| </Box> | ||||||
| ); | ||||||
| }; | ||||||
|
|
@@ -264,6 +279,9 @@ export const IncidentCommandTabPanel: React.FC<IncidentCommandTabPanelProps> = ( | |||||
| </VStack> | ||||||
| </Box> | ||||||
|
|
||||||
| {/* Incident chat: the channels this responder may open, plus who they can reach 1:1. */} | ||||||
| <IncidentChatSection view={view} /> | ||||||
|
|
||||||
| {/* Objectives */} | ||||||
| <Box className={`rounded-lg p-4 shadow-xs ${cardClass}`} testID="incident-command-objectives"> | ||||||
| <Heading size="sm">{t('incident_command.objectives')}</Heading> | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| import { type TFunction } from 'i18next'; | ||
|
|
||
| /** | ||
| * Standard NIMS/ICS position titles, keyed by the Core `IncidentRoleType` value. | ||
| * | ||
| * English titles rather than translation keys: these are the position names an IC is trained | ||
| * against, and the IC app treats them the same way. Translating them needs a subject-matter | ||
| * translator per locale, not a literal one. | ||
| */ | ||
| const ICS_ROLE_NAMES: Record<number, string> = { | ||
| 0: 'Incident Commander', | ||
| 1: 'Deputy Incident Commander', | ||
| 2: 'Unified Command Member', | ||
| 3: 'Operations Section Chief', | ||
| 4: 'Planning Section Chief', | ||
| 5: 'Logistics Section Chief', | ||
| 6: 'Finance/Admin Section Chief', | ||
| 7: 'Safety Officer', | ||
| 8: 'Liaison Officer', | ||
| 9: 'Public Information Officer', | ||
| 10: 'Staging Area Manager', | ||
| 11: 'Resources Unit Leader', | ||
| 12: 'Situation Unit Leader', | ||
| 13: 'Documentation Unit Leader', | ||
| 14: 'Communications Unit Leader', | ||
| 15: 'Division/Group Supervisor', | ||
| 16: 'Branch Director', | ||
| 17: 'Strike Team/Task Force Leader', | ||
| 18: 'Medical Unit Leader', | ||
| 19: 'Rehab Officer', | ||
| 20: 'Medical Branch Director', | ||
| 21: 'Triage Officer', | ||
| 22: 'Treatment Officer', | ||
| 23: 'Transport Officer', | ||
| 24: 'HazMat Group Supervisor', | ||
| 25: 'Decon Officer', | ||
| 26: 'Entry Team Leader', | ||
| 27: 'Search Group Supervisor', | ||
| 28: 'Air Operations Branch Director', | ||
| 29: 'Shelter/Mass Care Coordinator', | ||
| 30: 'Damage Assessment Lead', | ||
| }; | ||
|
|
||
| /** Display name for an ICS position; falls back to a generic label for a value we don't know yet. */ | ||
| export const getIncidentRoleName = (t: TFunction, roleType: number): string => ICS_ROLE_NAMES[roleType] ?? t('incident_command.role_generic', { role: roleType }); | ||
|
Comment on lines
+10
to
+45
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 | 🟡 Minor | ⚡ Quick win Localize the ICS role names.
As per coding guidelines, all user-visible text must be wrapped in 🤖 Prompt for AI AgentsSource: Coding guidelines |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| import { router } from 'expo-router'; | ||
| import { useCallback, useState } from 'react'; | ||
| import { useTranslation } from 'react-i18next'; | ||
|
|
||
| import { createDirectMessage } from '@/api/chat/chat'; | ||
| import { logger } from '@/lib/logging'; | ||
| import { useToastStore } from '@/stores/toast/store'; | ||
|
|
||
| /** | ||
| * Opens a 1:1 conversation with someone and navigates to it. | ||
| * | ||
| * The server dedups on a normalized participant key, so calling this repeatedly for the same person | ||
| * reuses the existing conversation rather than starting a new one — which is what makes it safe to | ||
| * hang a "message" button off every contact on an incident. | ||
| */ | ||
| export const useDirectMessage = () => { | ||
| const { t } = useTranslation(); | ||
| const [isOpening, setIsOpening] = useState(false); | ||
|
|
||
| const openDirectMessage = useCallback( | ||
| async (targetUserId?: string | null) => { | ||
|
Comment on lines
+20
to
+21
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. Missing JSDoc: async function Kody rule violation: Document async/Promise behavior and errors Prompt for LLMTalk to Kody by mentioning @kody Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction. |
||
| if (!targetUserId) { | ||
| // External lane leads have a name and phone but no Resgrid account to message. | ||
| useToastStore.getState().showToast('info', t('incident_command.dm_unavailable')); | ||
| return; | ||
| } | ||
|
|
||
| setIsOpening(true); | ||
| try { | ||
| const channel = await createDirectMessage({ TargetUserId: targetUserId }); | ||
|
Comment on lines
+12
to
+30
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. 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- candidate files ---'
fd -i 'use-direct-message|incident-command-tab-panel|incident-chat-section|chat' src | head -80
printf '%s\n' '--- package/react-query references ---'
rg -n --glob 'package.json' --glob 'src/**/*.{ts,tsx}' '`@tanstack/react-query`|useMutation|createDirectMessage|CreateDirectMessage|isOpening|openDirectMessage' .
printf '%s\n' '--- hook outline ---'
ast-grep outline src/hooks/use-direct-message.ts
printf '%s\n' '--- hook ---'
cat -n src/hooks/use-direct-message.ts
printf '%s\n' '--- caller sections ---'
cat -n src/components/incident-command/incident-command-tab-panel.tsx | sed -n '85,150p'
cat -n src/components/incident-command/incident-chat-section.tsx | sed -n '60,155p'
printf '%s\n' '--- chat API ---'
cat -n src/api/chat/chat.ts | sed -n '1,100p'
printf '%s\n' '--- direct-message-related files ---'
rg -n -i --glob '!node_modules' 'direct.?message|CreateDirectMessage|TargetUserId|participant' src app server api .github 2>/dev/null | head -200Repository: Resgrid/Unit Length of output: 41358 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- tracked endpoint/idempotency evidence ---'
git ls-files | xargs -r rg -n -i 'CreateDirectMessage|create.?direct.?message|normalized participant|participant.?key|dedup|idempot' -- 2>/dev/null | head -300
printf '%s\n' '--- component declarations and relevant caller context ---'
cat -n src/components/incident-command/incident-command-tab-panel.tsx | sed -n '1,145p'
cat -n src/components/incident-command/incident-chat-section.tsx | sed -n '1,90p'
printf '%s\n' '--- React Query usage ---'
rg -n --glob 'src/**/*.{ts,tsx}' 'useMutation|mutationFn|mutateAsync|isPending|QueryClient|useQuery' src | head -240
printf '%s\n' '--- tests and test setup related to these components ---'
rg -n --glob 'src/components/incident-command/**/*.{ts,tsx}' 'useDirectMessage|message|Pressable|disabled|CreateDirectMessage|router.push' src/components/incident-command
cat -n package.json | sed -n '65,90p'
printf '%s\n' '--- state timing model: two invocations before a React state commit ---'
node - <<'JS'
let isOpening = false;
let requests = 0;
const pendingState = [];
function setIsOpening(value) {
pendingState.push(value);
}
function openDirectMessage() {
if (!isOpening) {
setIsOpening(true);
requests += 1;
}
}
openDirectMessage();
openDirectMessage();
console.log(JSON.stringify({ requests, committedStateBeforeFlush: isOpening, queuedStateUpdates: pendingState }));
JSRepository: Resgrid/Unit Length of output: 19945 🌐 Web query:
💡 Result: The Resgrid API (v4) architecture utilizes standard REST endpoints and Swagger documentation [1][2]. There is no public, documented API endpoint named Citations:
Make direct-message opening single-flight.
🤖 Prompt for AI AgentsSource: Coding guidelines |
||
| const channelId = channel?.Data?.ChatChannelId; | ||
| if (!channelId) { | ||
| useToastStore.getState().showToast('error', t('incident_command.dm_failed')); | ||
| return; | ||
| } | ||
| router.push(`/chat/${channelId}`); | ||
| } catch (error) { | ||
| logger.error({ message: 'chat: failed to open direct message', context: { error, targetUserId } }); | ||
| useToastStore.getState().showToast('error', t('incident_command.dm_failed')); | ||
| } finally { | ||
| setIsOpening(false); | ||
| } | ||
| }, | ||
| [t] | ||
| ); | ||
|
|
||
| return { openDirectMessage, isOpening }; | ||
| }; | ||
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 | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
Repository: Resgrid/Unit
Length of output: 50368
🏁 Script executed:
Repository: Resgrid/Unit
Length of output: 50368
🏁 Script executed:
Repository: Resgrid/Unit
Length of output: 675
Add incident-chat branch coverage.
Add tests for server-provided channel visibility, the
IsFrozenbanner, and message-button rendering for contacts with and withoutUserId. Add the missingRadiomock before testing dispatch-channel visibility.🤖 Prompt for AI Agents
Source: Coding guidelines