-
Notifications
You must be signed in to change notification settings - Fork 0
RG-T117 Chat feature flag #35
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
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,37 @@ | ||
| import { api } from '../common/client'; | ||
|
|
||
| const FEATURE_TOGGLES = '/FeatureToggles'; | ||
|
|
||
| // --------------------------------------------------------------------------- | ||
| // Feature toggle evaluation (department-scoped, any authenticated user). | ||
| // Backed by the v4 FeatureToggles API; keys live in Resgrid.Model.FeatureFlagKeys. | ||
| // --------------------------------------------------------------------------- | ||
|
|
||
| export interface FeatureToggleData { | ||
| Key: string; | ||
| Enabled: boolean; | ||
| Value?: string | null; | ||
| ValueType?: string | null; | ||
| Source?: string | null; | ||
| } | ||
|
|
||
| export interface FeatureTogglesResult { | ||
| Data?: FeatureToggleData[]; | ||
| StateHash?: string; | ||
| } | ||
|
|
||
| export interface FeatureToggleResult { | ||
| Data?: FeatureToggleData; | ||
| } | ||
|
|
||
| /** Evaluates every active flag for the caller's department. */ | ||
| export const getAllFeatureFlags = async (signal?: AbortSignal) => { | ||
| const response = await api.get<FeatureTogglesResult>(`${FEATURE_TOGGLES}/GetAll`, { signal }); | ||
|
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 promise rejection: the awaited 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. 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 exception: the external HTTP call at Kody rule violation: Add try-catch blocks for external calls Prompt for LLMTalk to Kody by mentioning @kody Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction. |
||
| return response.data; | ||
| }; | ||
|
|
||
| /** Lightweight enabled-only check for a single flag. */ | ||
| export const getFeatureFlagState = async (key: string, signal?: AbortSignal) => { | ||
| const response = await api.get<FeatureToggleResult>(`${FEATURE_TOGGLES}/GetState`, { params: { key }, signal }); | ||
| return response.data; | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,4 @@ | ||
| import { type Href, Stack, useFocusEffect, useRouter } from 'expo-router'; | ||
| import { type Href, Redirect, Stack, useFocusEffect, useRouter } from 'expo-router'; | ||
| import { Bot, MessageCircle, MessagesSquare, Network, Plus, Sparkles, Users } from 'lucide-react-native'; | ||
| import React, { useCallback, useState } from 'react'; | ||
| import { useTranslation } from 'react-i18next'; | ||
|
|
@@ -15,10 +15,12 @@ import { Fab, FabIcon } from '@/components/ui/fab'; | |
| import { FocusAwareStatusBar } from '@/components/ui/focus-aware-status-bar'; | ||
| import { HStack } from '@/components/ui/hstack'; | ||
| 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 { type ChatChannelResultData, ChatChannelType } from '@/models/v4/chat'; | ||
| import { useChatStore } from '@/stores/chat/store'; | ||
| import { useChatSystemStatus } from '@/stores/feature-flags/store'; | ||
|
|
||
| function ChannelRow({ channel, onPress }: { channel: ChatChannelResultData; onPress: () => void }) { | ||
| const { t } = useTranslation(); | ||
|
|
@@ -86,12 +88,15 @@ export default function ChatScreen() { | |
| const pendingAcks = useChatStore((s) => s.pendingAcks); | ||
| const [fabOpen, setFabOpen] = useState(false); | ||
| const [newMode, setNewMode] = useState<'dm' | 'group' | null>(null); | ||
| const chatStatus = useChatSystemStatus(); | ||
| const isChatEnabled = chatStatus === 'enabled'; | ||
|
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. Hardcoded string literal 'enabled' represents a finite set of chat system statuses and risks typos. Export named constants like Kody rule violation: Use enums instead of magic strings Prompt for LLMTalk to Kody by mentioning @kody Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction. |
||
|
|
||
| useFocusEffect( | ||
| useCallback(() => { | ||
| if (!isChatEnabled) return; | ||
| useChatStore.getState().fetchChannels(); | ||
| useChatStore.getState().fetchPendingAcks(); | ||
| }, []) | ||
| }, [isChatEnabled]) | ||
| ); | ||
|
|
||
| const grouped = groupChannels(channels); | ||
|
|
@@ -103,6 +108,22 @@ export default function ChatScreen() { | |
| [router] | ||
| ); | ||
|
|
||
| // Chat.System flag not yet resolved: wait instead of redirecting away from a valid route. | ||
| if (chatStatus === 'unknown') { | ||
| return ( | ||
| <Box className="size-full flex-1 items-center justify-center bg-background-0"> | ||
| <Stack.Screen options={{ headerShown: false }} /> | ||
| <FocusAwareStatusBar /> | ||
| <Spinner /> | ||
| </Box> | ||
| ); | ||
| } | ||
|
|
||
| // Chat.System feature flag off: no chat for this department. | ||
| if (chatStatus === 'disabled') { | ||
| return <Redirect href="/" />; | ||
| } | ||
|
|
||
| return ( | ||
| <Box className="size-full flex-1 bg-background-0"> | ||
| <Stack.Screen options={{ headerShown: false }} /> | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,4 @@ | ||
| import { Stack, useLocalSearchParams } from 'expo-router'; | ||
| import { Redirect, Stack, useLocalSearchParams } from 'expo-router'; | ||
| import React, { useCallback, useEffect, useMemo, useState } from 'react'; | ||
| import { useTranslation } from 'react-i18next'; | ||
| import { FlatList, Platform } from 'react-native'; | ||
|
|
@@ -9,12 +9,14 @@ import { MessageComposer } from '@/components/chat/message-composer'; | |
| import { Box } from '@/components/ui/box'; | ||
| import { Divider } from '@/components/ui/divider'; | ||
| import { KeyboardAvoidingView } from '@/components/ui/keyboard-avoiding-view'; | ||
| import { Spinner } from '@/components/ui/spinner'; | ||
| import { Text } from '@/components/ui/text'; | ||
| import { VStack } from '@/components/ui/vstack'; | ||
| import { logger } from '@/lib/logging'; | ||
| import { ChatChannelType, ChatMessagePriority, type ChatMessageResultData, ChatMessageType } from '@/models/v4/chat'; | ||
| import useAuthStore from '@/stores/auth/store'; | ||
| import { useChatStore } from '@/stores/chat/store'; | ||
| import { useChatSystemStatus } from '@/stores/feature-flags/store'; | ||
|
|
||
| export default function ThreadScreen() { | ||
| const { t } = useTranslation(); | ||
|
|
@@ -26,18 +28,21 @@ export default function ThreadScreen() { | |
| const channel = useChatStore((s) => s.channels.find((c) => c.ChatChannelId === channelId)); | ||
| const channelMessages = useChatStore((s) => (channelId ? s.messagesByChannel[channelId] : undefined)); | ||
| const [fetchedReplies, setFetchedReplies] = useState<ChatMessageResultData[]>([]); | ||
| const chatStatus = useChatSystemStatus(); | ||
| const isChatEnabled = chatStatus === 'enabled'; | ||
|
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. Hardcoded string literal 'enabled' checks chat system status inline, creating synchronization risks across components. Define a constant like 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. |
||
|
|
||
| // IC delta: thread replies in command-type channels also post as the Incident Commander. | ||
| const isCommandChannel = channel?.ChannelType === ChatChannelType.Incident || channel?.ChannelType === ChatChannelType.IncidentLane || channel?.ChannelType === ChatChannelType.IncidentCommand; | ||
|
|
||
| const root = useMemo(() => (channelMessages ?? []).find((m) => m.ChatMessageId === messageId), [channelMessages, messageId]); | ||
|
|
||
| useEffect(() => { | ||
| if (!isChatEnabled) return; | ||
| if (!messageId) return; | ||
| getThread(messageId, undefined, 50) | ||
| .then((response) => setFetchedReplies(response.Data ?? [])) | ||
| .catch((error) => logger.error({ message: 'chat: failed to load thread', context: { error, messageId } })); | ||
| }, [messageId]); | ||
| }, [messageId, isChatEnabled]); | ||
|
|
||
| // Merge fetched replies with any realtime/optimistic replies already in the channel cache. | ||
| const replies = useMemo(() => { | ||
|
|
@@ -100,6 +105,21 @@ export default function ThreadScreen() { | |
| [currentUserId, channelId] | ||
| ); | ||
|
|
||
| // Chat.System flag not yet resolved: wait instead of redirecting away from a valid deep link. | ||
| if (chatStatus === 'unknown') { | ||
| return ( | ||
| <Box className="size-full flex-1 items-center justify-center bg-background-0"> | ||
| <Stack.Screen options={{ title: t('chat.thread'), headerShown: true, headerBackTitle: '' }} /> | ||
| <Spinner /> | ||
| </Box> | ||
| ); | ||
| } | ||
|
|
||
| // Chat.System feature flag off: block deep links into threads. | ||
| if (chatStatus === 'disabled') { | ||
| return <Redirect href="/" />; | ||
| } | ||
|
|
||
| return ( | ||
| <Box className="size-full flex-1 bg-background-0"> | ||
| <Stack.Screen options={{ title: t('chat.thread'), headerShown: true, headerBackTitle: '' }} /> | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,6 +9,7 @@ import { Icon } from '@/components/ui/icon'; | |
| import { Pressable } from '@/components/ui/pressable'; | ||
| import { Text } from '@/components/ui/text'; | ||
| import { VStack } from '@/components/ui/vstack'; | ||
| import { useIsChatEnabled } from '@/stores/feature-flags/store'; | ||
|
|
||
| interface SidebarProps { | ||
| onClose?: () => void; | ||
|
|
@@ -35,6 +36,10 @@ const MENU_ITEMS: MenuItem[] = [ | |
| const Sidebar = ({ onClose }: SidebarProps) => { | ||
| const { t } = useTranslation(); | ||
| const router = useRouter(); | ||
| const isChatEnabled = useIsChatEnabled(); | ||
|
|
||
| // Chat.System feature flag off: hide the chat and assistant entries entirely. | ||
| const menuItems = MENU_ITEMS.filter((item) => (item.key === 'chat' || item.key === 'chatbot' ? isChatEnabled : true)); | ||
|
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. Magic strings: the literals 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. 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. Magic strings: the literals Kody rule violation: Use enums instead of magic strings Prompt for LLMTalk to Kody by mentioning @kody Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction. |
||
|
|
||
| const handleNavigate = (href: string) => { | ||
| onClose?.(); | ||
|
|
@@ -46,7 +51,7 @@ const Sidebar = ({ onClose }: SidebarProps) => { | |
| <VStack space="xs" className="w-full flex-1 p-2"> | ||
| <Text className="px-3 pb-2 text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">{t('sidebar.menu')}</Text> | ||
|
|
||
| {MENU_ITEMS.map((item) => ( | ||
| {menuItems.map((item) => ( | ||
| <Pressable key={item.key} testID={`sidebar-link-${item.key}`} className="flex-row items-center gap-3 rounded-lg px-3 py-3 active:bg-gray-100 dark:active:bg-gray-800" onPress={() => handleNavigate(item.href)}> | ||
| <Icon as={item.icon} size="lg" className="text-primary-500 dark:text-primary-400" /> | ||
| <Text className="flex-1 text-base font-medium text-gray-900 dark:text-white">{t(item.labelKey)}</Text> | ||
|
|
||
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.
Incomplete JSDoc:
getAllFeatureFlags(line 27) omits@returns {Promise<FeatureTogglesResult>}and rejection conditions, leaving callers without knowledge of the resolved type, rejection scenarios, or that it must be used withawait. Add@returns {Promise<FeatureTogglesResult>}and document rejection scenarios (e.g., network failure, auth error).Kody rule violation: Document async/Promise behavior and errors
Prompt for LLM
Talk to Kody by mentioning @kody
Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.