Conversation
Summary of ChangesHello @dioo1461, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! 이 풀리퀘스트는 사용자별 그룹 색상 할당 기능을 도입하여 그룹 관리의 개인화를 강화합니다. 이를 위해 새로운 React 훅을 추가하고, 기존 그룹 관련 컴포넌트들을 리팩토링했습니다. 또한, 학습 시간 기록 로직의 안정성을 개선하고, 비동기 저장소 키 관리 방식을 업데이트하여 데이터의 정확성과 사용자 경험을 향상시켰습니다. Highlights
Changelog
Activity
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
| await storeChats(entities); | ||
| const recentChatId = fetchedChats[fetchedChats.length - 1].chatId; | ||
| asyncStorage.set(ASYNC_KEYS.groupChatCursor(chatRoomId), recentChatId); | ||
| asyncStorage.set(ASYNC_KEYS.groupChatCursor(myUserId, chatRoomId), recentChatId); |
| groupChatCursor: (userId: number, chatRoomId: string) => `groupChat:${userId}-${chatRoomId}`, | ||
| groupColor: (userId: number, groupId: number) => `groupColor:${userId}-${groupId}`, |
|
|
||
| await storeChats([chatObjToEntity(newChat, chatRoomId)]); | ||
| await asyncStorage.set(ASYNC_KEYS.groupChatCursor(chatRoomId), message.id); | ||
| await asyncStorage.set(ASYNC_KEYS.groupChatCursor(myUserId, chatRoomId), message.id); |
|
|
||
| const cursor = | ||
| (await asyncStorage.get(ASYNC_KEYS.groupChatCursor(chatRoomId))) ?? undefined; | ||
| (await asyncStorage.get(ASYNC_KEYS.groupChatCursor(myUserId, chatRoomId))) ?? undefined; |
|
|
||
| const GroupDetailScreen = () => { | ||
| const groupColor = GROUP_COLORS[theme.color.blue[300]]; | ||
| const groupColor = GROUP_COLORS[useStackRoute<'GroupDetail'>().params.groupColorKey]; |
| <StackableScreenLayout | ||
| backgroundColor={GROUP_COLORS[groupColorKey].weak} | ||
| headerElementColor={GROUP_COLORS[groupColorKey].medium} | ||
| headerElementColor={GROUP_COLORS[groupColorKey].strong} |
| header, | ||
| children, | ||
| safeAreas, | ||
| safeAreas = 'top', |
| backgroundColor={groupColor.weak} | ||
| headerElementColor={groupColor.strong} |
| if (diff < 0) { | ||
| // eslint-disable-next-line no-console | ||
| console.warn('Resulting time is negative. Returning zero time.'); | ||
| console.log('[Time] Resulting time is negative. Returning zero time.'); |
| } | ||
|
|
||
| const recordedStudyTime = todayRecord.time; | ||
| const recordedStudyTime = todayRecord?.time ?? Time.fromMilliseconds(0); |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 5ecdb44ee2
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| loadMoreChats, | ||
| storeChats, | ||
| setChats, | ||
| myUserId: profileData?.id ?? null, | ||
| myUserId: profileData?.id ?? -1, | ||
| sendReadCursor, |
There was a problem hiding this comment.
Avoid bootstrapping chats with placeholder user id
Using profileData?.id ?? -1 means useGroupChatBootstrap runs before the real profile id is loaded. In that first pass it fetches and appends chats, and stores the cursor under groupChat:-1-…. When the profile data arrives, the effect re-runs with the real id, reads an empty cursor (different key) and fetches the same history again, appending duplicates to chats. This happens any time the profile query resolves after the chat screen mounts. Consider keeping myUserId as null until the profile is ready, or guard the bootstrap hook so it only runs when a valid user id is available.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Pull request overview
This PR implements a feature to assign random colors to groups on a per-user basis, ensuring each user sees groups with consistent but personalized colors.
Changes:
- Added
useGroupColorKeyhook that stores and retrieves user-specific group colors from async storage - Updated async storage keys to include user ID for per-user data isolation
- Changed chat-related hooks to use
-1instead ofnullas a fallback for missing user IDs - Refactored error handling in study time tracking to use default values instead of logging errors
Reviewed changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| src/features/groups/hooks/useGroupColorKey.ts | New hook that generates and persists random group colors per user |
| src/utils/asyncStorage/index.ts | Added user ID to storage keys for groupChatCursor and groupColor |
| src/screens/bottomTab/GroupsScreen/components/JoinedGroupPanel/index.tsx | Refactored to use new useGroupColorKey hook with GroupListItem component |
| src/screens/group/groupDetail/GroupDetailScreen/index.tsx | Updated to use groupColorKey from route params and apply colors to layout |
| src/screens/group/GroupSettingScreen/index.tsx | Changed header element color from medium to strong |
| src/screens/group/GroupRankingScreen/index.tsx | Changed header element color from medium to strong |
| src/screens/group/GroupChatScreen/index.tsx | Changed userId fallback from null to -1 |
| src/screens/group/GroupChatScreen/hooks/useGroupChatRealtime.ts | Updated myUserId type from number | null to number |
| src/screens/group/GroupChatScreen/hooks/useGroupChatBootstrap.ts | Updated to use new groupChatCursor key with userId parameter |
| src/features/study/api/mutations.ts | Removed error logging and used default Time value when record is missing |
| src/StudyDataHydrator.tsx | Removed error logging and used default Time value when record is missing |
| src/layout/StackableScreenLayout/index.tsx | Added default value 'top' for safeAreas prop |
| src/utils/Time/index.ts | Changed console.warn to console.log for negative time warning |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| (async () => { | ||
| const stored = await asyncStorage.get(ASYNC_KEYS.groupColor(userId, groupId)); | ||
| if (stored && isGroupColorKey(stored)) { | ||
| if (!cancelled) setColorKey(stored); | ||
| return; | ||
| } | ||
| const newKey = pickRandomGroupColorKey(); | ||
| await asyncStorage.set(ASYNC_KEYS.groupColor(userId, groupId), newKey); | ||
| if (!cancelled) setColorKey(newKey); |
There was a problem hiding this comment.
The cleanup function sets cancelled = true after the async operation has started, but there's a potential race condition. If the component unmounts while the async operation is in progress (between lines 25-32), setColorKey could still be called after the component has unmounted if the check if (!cancelled) hasn't been reached yet. The cancellation flag should be checked before each state update, but line 32 only checks it once. Consider checking cancelled before line 27 as well to prevent state updates on unmounted components.
| const GroupListItem = ({ group }: { group: GetGroupsResponse['data'][number] }) => { | ||
| const groupColorKey = useGroupColorKey(group.groupId); | ||
| if (!groupColorKey) return null; |
There was a problem hiding this comment.
The hook returns null when groupColorKey is null, causing the GroupListItem component to return null and not render. This creates an inconsistent user experience where groups may temporarily not appear in the list while their color is being loaded from async storage. Consider providing a default color or loading state instead, or ensure that the color is loaded before rendering the group list.
| const groupColor = GROUP_COLORS[useStackRoute<'GroupDetail'>().params.groupColorKey]; | ||
|
|
||
| const { data: categories } = useCategoriesQuery(); | ||
| const { params } = useStackRoute<'GroupDetail'>(); |
There was a problem hiding this comment.
The useStackRoute hook is called twice on lines 19 and 22, which is redundant and violates the React principle of calling hooks in a consistent order. The first call on line 19 is only used to access params.groupColorKey, while the second call is destructured to get params. Consider consolidating these into a single call by storing the result in a variable and accessing both properties from it.
| const groupColor = GROUP_COLORS[useStackRoute<'GroupDetail'>().params.groupColorKey]; | |
| const { data: categories } = useCategoriesQuery(); | |
| const { params } = useStackRoute<'GroupDetail'>(); | |
| const { params } = useStackRoute<'GroupDetail'>(); | |
| const groupColor = GROUP_COLORS[params.groupColorKey]; | |
| const { data: categories } = useCategoriesQuery(); |
| @@ -1,4 +1,4 @@ | |||
| import { useFocusEffect } from '@react-navigation/native'; | |||
| import { useFocusEffect, useRoute } from '@react-navigation/native'; | |||
There was a problem hiding this comment.
The useRoute import is unused after the refactoring. This import should be removed.
| import { useFocusEffect, useRoute } from '@react-navigation/native'; | |
| import { useFocusEffect } from '@react-navigation/native'; |
| isConnected: boolean; | ||
| chatRoomId: string; | ||
| myUserId: number | null; | ||
| myUserId: number; |
There was a problem hiding this comment.
The type of myUserId was changed from number | null to number. However, this change may cause type inconsistencies with other parts of the system. The useChatDB hook still expects userId: number | null, and the calling code in GroupChatScreen passes profileData?.id ?? -1 as a fallback. It appears -1 is being used as a sentinel value for missing user IDs, but this is not documented. Consider using a consistent approach - either update all related types to use -1 as a sentinel value with proper documentation, or keep using null to represent missing user IDs.
| myUserId: number; | |
| myUserId: number | null; |
| export const ASYNC_KEYS = { | ||
| language: 'language', | ||
| groupChatCursor: (chatRoomId: string) => `groupChat:${chatRoomId}`, | ||
| groupChatCursor: (userId: number, chatRoomId: string) => `groupChat:${userId}-${chatRoomId}`, |
There was a problem hiding this comment.
The signature of groupChatCursor was changed to include userId as the first parameter, but existing stored keys from the previous version will not be accessible anymore (keys were previously groupChat:${chatRoomId}, now groupChat:${userId}-${chatRoomId}). This is a breaking change that will lose chat cursor data for existing users. Consider implementing a migration strategy or documenting this as an intentional data reset.
| if (diff < 0) { | ||
| // eslint-disable-next-line no-console | ||
| console.warn('Resulting time is negative. Returning zero time.'); | ||
| console.log('[Time] Resulting time is negative. Returning zero time.'); |
There was a problem hiding this comment.
Changing from console.warn to console.log reduces the visibility of this edge case (negative time differences). In the codebase, console.warn is consistently used for warning scenarios (see src/features/chat/sse/messageParsers.ts:23, src/features/chat/sse/messageParsers.ts:36, src/features/chat/sse/messageParsers.ts:45, src/hooks/useStompConnection/index.ts:15, src/utils/fetch/executeFetch.ts:62). Consider keeping console.warn for consistency and better visibility of this edge case.
5ecdb44 to
5bb0b2d
Compare
✨ 구현 기능 명세
✅ PR Point
😭 어려웠던 점