Feat/notifications - #13
Conversation
Выбор фото профиля через системную галерею (локальный URI). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
readDeviceJson/writeDeviceJson/removeDeviceValue поверх булевых флагов — slice'ы не дёргают AsyncStorage напрямую. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Аватар-плейсхолдер, мультивыбор интересов и выбор фото из галереи. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
readMyActivities/addMyActivity (дедуп по slug) + activityKeys.mine, useMyActivities, useTrackMyActivity. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
readMyResponses/addMyResponse (дедуп по slug) + participantKeys.mine, useMyResponses, useTrackMyResponse. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
types/schemas/utils/storage/hooks/mappers + unit-тесты. Профиль живёт в AsyncStorage, без серверного аккаунта. 4 соцсети (telegram, instagram, whatsapp, website), фото — локальный URI без URL-валидации. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…писки На успешном создании/отклике дописываем в device-list через useTrackMyActivity/useTrackMyResponse. Сбой записи не роняет операцию. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
useEditProfileForm (RHF + zodResolver + useSaveProfile, префилл через mapProfileToForm) и EditProfileForm с выбором фото и интересов. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Просмотр профиля (фото, статистика, соцсети наружу, «Мои ивенты»), экран редактирования, пустое состояние. Ссылка в шапку _layout. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
TESTS.md дополнен секциями profile (schemas/mappers/utils). В about_the_project уточнены открытое создание ивентов и модерация. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Форма всегда даёт status «going» (литерал в схеме), выбор статуса убран. participantStatusOptions больше не нужен. Статистика и список читают старые записи maybe/cant. Тесты маппера и TESTS.md — payload всегда going. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Убран Select статуса из формы отклика — остаются имя, телеграм, комментарий. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Строка статистики на странице ивента — «Идут: N» вместо трёх статусов. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- главная лента фильтрует серии по городу/тексту (index, series/api,hooks) - ближайшая встреча серии считается по правилу, прошедший старт не всплывает - запись/выход из серии обрабатывают ошибки, сбой хранилища не ломает join - ограничение числа встреч в схеме серии - защита от NaN и неположительного интервала в конструкторе повторения - дизейбл кнопок сохранения/переноса/отмены и лайка на время запроса - откат оптимистичного лайка при сбое записи + guard от двойного тапа - сериализация read-modify-write device-хранилища (updateDeviceJson) - MultiSelect.label сужен до string, дубль optionalText убран в shared - microphonePermission:false у expo-image-picker, русские describe в тестах
Добавлен entity reminder (планирование/отмена уведомлений, настройки) и подключение к откликам на активности/серии и отметке "иду".
📝 WalkthroughWalkthroughThis PR adds recurring series creation and management, profile editing, local activity and response tracking, reminders, sharing, likes, chat links, feed filtering, reusable validation/UI components, and native notification configuration. ChangesApplication and activity experience
Recurring series
Reminder and supporting documentation
Estimated code review effort: 5 (Critical) | ~120 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 20
🧹 Nitpick comments (5)
src/shared/lib/zodFields.ts (1)
3-5: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick winConsider trimming whitespace in
emptyToNull.
emptyToNullconverts""andundefinedtonull, but a whitespace-only string like" "passes through as a valid value. ForoptionalTextfields (e.g.,city,location_text, socials), this means whitespace-only input is stored rather than treated as empty. URL and capacity fields are unaffected since downstream validation rejects whitespace.♻️ Proposed refactor
-const emptyToNull = (v: unknown) => (v === "" || v === undefined ? null : v); +const emptyToNull = (v: unknown) => { + if (typeof v === "string") { + const trimmed = v.trim(); + return trimmed === "" ? null : trimmed; + } + return v === undefined ? null : v; +};🤖 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/shared/lib/zodFields.ts` around lines 3 - 5, Update emptyToNull to treat whitespace-only strings as empty by checking the trimmed string value before returning null, while preserving existing handling of undefined and non-empty strings. Keep optionalText using emptyToNull so affected text fields receive the normalized null value.src/shared/ui/Avatar.tsx (1)
8-23: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winAdd
onErrorfallback to theImageso broken photo URLs show the letter avatar.If
photoUrlis stale or unreachable, theImagerenders blank with no visual fallback. Swapping to the letter avatar on error is a small change that meaningfully improves resilience.♻️ Proposed refactor with onError fallback
export function Avatar({ photoUrl, name }: Props) { + const [imgFailed, setImgFailed] = useState(false); + if (photoUrl != null && !imgFailed) { return ( <Image source={{ uri: photoUrl }} className="h-20 w-20 border border-gray-400" + onError={() => setImgFailed(true)} /> ); }🤖 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/shared/ui/Avatar.tsx` around lines 8 - 23, Update the Image rendered by Avatar to track load failure via onError and switch to the existing letter-avatar View using firstLetter(name) when the photo URL is stale or unreachable. Preserve the current image rendering for successful loads and the existing photoUrl-null fallback.src/shared/lib/storage.ts (1)
30-51: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueОбъявите
writeQueuesдоupdateDeviceJsonдля читаемости.
writeQueues(строка 51) объявлена после функцииupdateDeviceJson(строки 32–49), которая её использует. Это безопасно во время выполнения — функция вызывается уже после инициализации модуля, — но нарушает естественный порядок «сначала объявление, потом использование».♻️ Предлагаемый рефакторинг
+const writeQueues = new Map<string, Promise<unknown>>(); + // Read-modify-write по ключу, сериализованный в очередь: параллельные апдейты // не затирают друг друга, а выполняются по одному. Возвращает новое значение. export async function updateDeviceJson<T>( @@ -48,8 +49,6 @@ return result; } - -const writeQueues = new Map<string, Promise<unknown>>();🤖 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/shared/lib/storage.ts` around lines 30 - 51, Переместите объявление константы writeQueues перед функцией updateDeviceJson, сохранив её тип, инициализацию и остальную логику очереди без изменений.src/features/mark-meeting/useMarkMeeting.ts (1)
30-31: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winSilent error swallowing hides reminder failures.
.catch(() => {})discards all errors frommoveReminderPastSkip. IfcancelRemindersucceeds butscheduleReminderfails, the user silently loses their reminder with no diagnostic trace. Add aconsole.warnto aid debugging without changing control flow.🔊 Proposed fix: log swallowed errors
- if (status === "skip") - await moveReminderPastSkip(meetingId).catch(() => {}); + if (status === "skip") + await moveReminderPastSkip(meetingId).catch((error) => { + console.warn("moveReminderPastSkip failed", error); + });🤖 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/features/mark-meeting/useMarkMeeting.ts` around lines 30 - 31, Update the moveReminderPastSkip error handler in the status === "skip" branch to emit a console.warn containing the caught error before swallowing it. Preserve the existing non-blocking control flow and do not change reminder handling behavior.src/entities/series/hooks.ts (1)
163-170: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueИспользуйте
seriesKeysвместо захардкоженного ключа.
useUpsertMarkинвалидирует["series", "marks"]напрямую, тогда как остальной код используетseriesKeys.*. ДобавьтеallMarksвseriesKeysдля консистентности.♻️ Предлагаемый рефакторинг
export const seriesKeys = { all: ["series"] as const, + allMarks: ["series", "marks"] as const, list: (filter: SeriesFilter) => ["series", "list", filter] as const, // ... };Then update
useUpsertMark:onSuccess: () => - queryClient.invalidateQueries({ queryKey: ["series", "marks"] }), + queryClient.invalidateQueries({ queryKey: seriesKeys.allMarks }),🤖 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/entities/series/hooks.ts` around lines 163 - 170, Добавьте ключ allMarks в объект seriesKeys и обновите onSuccess в useUpsertMark, чтобы invalidateQueries использовал seriesKeys.allMarks вместо захардкоженного ["series", "marks"], сохранив текущую инвалидизацию после успешного upsertMark.
🤖 Prompt for all review comments with 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.
Inline comments:
In `@app/index.tsx`:
- Around line 113-127: Update nextMeetingDate so its listOccurrences call does
not use the default horizon of 5; pass a sufficiently large dedicated horizon or
use an unbounded way to find the first future recurrence. Preserve the existing
active-meeting candidates, filtering, sorting, and null behavior when no future
date exists.
In `@src/entities/activity/utils.test.ts`:
- Line 8: Переведите название набора тестов в describe("activity/utils", ...) на
русский язык, сохранив область тестов и остальную структуру без изменений.
In `@src/entities/profile/schemas.test.ts`:
- Line 13: Переведите название набора тестов в describe("profile/schemas") на
русский язык, сохранив охватываемый набор тестов и остальную структуру без
изменений.
In `@src/entities/profile/utils.ts`:
- Around line 50-57: Update formatRegisteredAt to use getUTCDate(),
getUTCMonth(), and getUTCFullYear() when formatting valid ISO timestamps, while
preserving the existing invalid-date fallback and padding behavior.
In `@src/entities/reminder/schedule.ts`:
- Around line 21-44: Update scheduleReminder to cancel any existing reminder for
the same target.slug before scheduling a new notification, reusing the existing
reminder lookup and cancellation APIs. Perform this cancellation before
scheduleNotificationAt, while preserving the current success and early-return
behavior.
In `@src/entities/series/hooks.ts`:
- Around line 143-162: Update the onSuccess handlers in useJoinSeries and
useLeaveSeries to invalidate both seriesKeys.members(seriesId) and
seriesKeys.memberships, ensuring useMyMemberships refreshes after joining or
leaving while preserving the existing member-list invalidation.
In `@src/entities/series/utils.test.ts`:
- Line 20: Переведите названия наборов тестов в describe-блоках, включая наборы
вокруг series/utils и связанные блоки на строках 97 и 111, с английского на
русский язык; изменяйте только текст названий, сохранив тестовую логику без
изменений.
- Around line 39-41: Remove the fixed spacing(dates, 0) assertion from the
weekly test. Keep the dates.map(ourWeekday) validation so the test verifies the
expected weekdays without assuming a constant millisecond interval across DST
transitions.
In `@src/entities/series/utils.ts`:
- Around line 17-24: В функции, формирующей список дат вокруг occurrenceLimit,
добавьте ранний возврат пустого списка при нулевом лимите до запуска
occurrenceSequence и добавления первой даты. Сохраните текущую обработку
положительных лимитов, until и ограничения по количеству встреч.
- Around line 101-105: Update the recurrence loop around current and advance so
monthly generation does not roll dates 29–31 into the following month. Add a
dedicated month calculation that clamps the target day to the last day of the
target month, while preserving the existing advance behavior for other
recurrence units.
In `@src/features/create-series/RecurrenceFields.tsx`:
- Around line 41-45: Переведите латинскую метку «N встреч» в массиве endOptions
на русский язык, сохранив смысл варианта количества встреч, и замените
англоязычный термин `coerce` в комментарии возле соответствующего кода на
русский эквивалент. Не изменяйте поведение компонента.
In `@src/features/create-series/useCreateSeriesForm.ts`:
- Around line 65-69: Добавьте минимальное предупреждающее логирование в пустой
блок catch функции rememberCreated, сохранив подавление ошибки и основной
сценарий работы без изменений.
In `@src/features/edit-profile/useEditProfileForm.ts`:
- Around line 40-41: Переведите комментарий, описывающий передачу валидированной
формы напрямую в хранилище, на русский язык, сохранив его смысл и расположение
рядом с соответствующим payload.
In `@src/features/join-series/useJoinSeries.ts`:
- Around line 51-55: Переведите оставшийся англоязычный термин в комментариях
функции useJoinSeries, включая комментарий около строки 98, на русский язык; не
изменяйте поведение addMembership и planReminder.
- Around line 66-69: Update the exit flow containing leaveMutation.mutateAsync,
removeMembership, and setMembership so a failure from removeMembership does not
prevent clearing the local membership state. Preserve the completed server-side
leave and reminder cancellation behavior, and ensure setMembership(null) runs
even when local removal rejects.
- Around line 102-115: В `useEffect` вокруг `findMembership` сбрасывайте
`membership` в `null` сразу при каждом изменении `slug`, до проверки на пустое
значение и запуска запроса. Для пустого `slug` после сброса сразу возвращайтесь;
для непустого сохраняйте текущую защиту `active`, чтобы результат предыдущей
серии не применялся.
In `@src/features/respond-to-activity/useRespondToActivityForm.ts`:
- Around line 49-52: Update the cancellation branch in the useEffect to set
reminderScheduled to false when activity.status is "cancelled", alongside
cancelReminder(activity.slug). Keep the existing dependency list and reminder
cancellation behavior unchanged.
In `@src/shared/lib/publicLink.test.ts`:
- Line 9: Переведите человекочитаемое название набора тестов в describe для
shared/publicLink на русский язык, сохранив структуру тестов и идентификатор
модуля без изменений.
In `@src/shared/ui/ChatLinkButton.tsx`:
- Around line 9-11: Update openChat in ChatLinkButton so failures from
Linking.openURL(url) are no longer silently swallowed; provide user-visible
feedback when opening the chat fails, while preserving the existing successful
URL-opening behavior.
In `@src/shared/ui/PhotoPicker.tsx`:
- Around line 37-41: Инициализируйте восстановление отложенного результата через
ImagePicker.getPendingResultAsync() в компоненте PhotoPicker или общем
провайдере, чтобы результат после пересоздания MainActivity передавался в
onChange. Сохраните текущую обработку результата launchImageLibraryAsync и
применяйте восстановленный asset только при наличии результата и URI.
---
Nitpick comments:
In `@src/entities/series/hooks.ts`:
- Around line 163-170: Добавьте ключ allMarks в объект seriesKeys и обновите
onSuccess в useUpsertMark, чтобы invalidateQueries использовал
seriesKeys.allMarks вместо захардкоженного ["series", "marks"], сохранив текущую
инвалидизацию после успешного upsertMark.
In `@src/features/mark-meeting/useMarkMeeting.ts`:
- Around line 30-31: Update the moveReminderPastSkip error handler in the status
=== "skip" branch to emit a console.warn containing the caught error before
swallowing it. Preserve the existing non-blocking control flow and do not change
reminder handling behavior.
In `@src/shared/lib/storage.ts`:
- Around line 30-51: Переместите объявление константы writeQueues перед функцией
updateDeviceJson, сохранив её тип, инициализацию и остальную логику очереди без
изменений.
In `@src/shared/lib/zodFields.ts`:
- Around line 3-5: Update emptyToNull to treat whitespace-only strings as empty
by checking the trimmed string value before returning null, while preserving
existing handling of undefined and non-empty strings. Keep optionalText using
emptyToNull so affected text fields receive the normalized null value.
In `@src/shared/ui/Avatar.tsx`:
- Around line 8-23: Update the Image rendered by Avatar to track load failure
via onError and switch to the existing letter-avatar View using
firstLetter(name) when the photo URL is stale or unreachable. Preserve the
current image rendering for successful loads and the existing photoUrl-null
fallback.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: d21f2ce6-4305-42dc-99e8-8d3ce8fe87e2
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (95)
TESTS.mdapp.jsonapp/_layout.tsxapp/a/[slug].tsxapp/create.tsxapp/index.tsxapp/manage/[editToken].tsxapp/manage/series/[editToken].tsxapp/profile/edit.tsxapp/profile/index.tsxapp/s/[slug].tsxdocs/about_the_project.mddocs/todo.mdpackage.jsonsrc/entities/activity/api.tssrc/entities/activity/hooks.tssrc/entities/activity/local.test.tssrc/entities/activity/local.tssrc/entities/activity/mappers.test.tssrc/entities/activity/mappers.tssrc/entities/activity/schemas.test.tssrc/entities/activity/schemas.tssrc/entities/activity/types.tssrc/entities/activity/utils.test.tssrc/entities/activity/utils.tssrc/entities/participant/hooks.tssrc/entities/participant/local.test.tssrc/entities/participant/local.tssrc/entities/participant/mappers.test.tssrc/entities/participant/mappers.tssrc/entities/participant/schemas.test.tssrc/entities/participant/schemas.tssrc/entities/participant/types.tssrc/entities/participant/utils.tssrc/entities/profile/hooks.tssrc/entities/profile/mappers.test.tssrc/entities/profile/mappers.tssrc/entities/profile/schemas.test.tssrc/entities/profile/schemas.tssrc/entities/profile/storage.tssrc/entities/profile/types.tssrc/entities/profile/utils.test.tssrc/entities/profile/utils.tssrc/entities/reminder/hooks.tssrc/entities/reminder/local.test.tssrc/entities/reminder/local.tssrc/entities/reminder/schedule.tssrc/entities/reminder/settings.tssrc/entities/reminder/types.tssrc/entities/reminder/utils.test.tssrc/entities/reminder/utils.tssrc/entities/series/api.tssrc/entities/series/hooks.tssrc/entities/series/local.test.tssrc/entities/series/local.tssrc/entities/series/mappers.test.tssrc/entities/series/mappers.tssrc/entities/series/schemas.test.tssrc/entities/series/schemas.tssrc/entities/series/types.tssrc/entities/series/utils.test.tssrc/entities/series/utils.tssrc/features/create-activity/CreateActivityForm.tsxsrc/features/create-activity/useCreateActivityForm.tssrc/features/create-series/CreateSeriesForm.tsxsrc/features/create-series/RecurrenceFields.tsxsrc/features/create-series/useCreateSeriesForm.tssrc/features/edit-profile/EditProfileForm.tsxsrc/features/edit-profile/useEditProfileForm.tssrc/features/join-series/JoinSeriesForm.tsxsrc/features/join-series/useJoinSeries.tssrc/features/like-activity/useLikeActivity.tssrc/features/manage-activity/ManageActivityForm.tsxsrc/features/manage-activity/useManageActivityForm.tssrc/features/manage-series/ManageSeriesForm.tsxsrc/features/manage-series/useManageSeriesForm.tssrc/features/mark-meeting/useMarkMeeting.tssrc/features/respond-to-activity/ResponseForm.tsxsrc/features/respond-to-activity/useRespondToActivityForm.tssrc/shared/lib/datetime.test.tssrc/shared/lib/datetime.tssrc/shared/lib/notifications.tssrc/shared/lib/publicLink.test.tssrc/shared/lib/publicLink.tssrc/shared/lib/publicUrl.tssrc/shared/lib/shareActivity.test.tssrc/shared/lib/shareActivity.tssrc/shared/lib/storage.tssrc/shared/lib/zodFields.tssrc/shared/ui/Avatar.tsxsrc/shared/ui/ChatLinkButton.tsxsrc/shared/ui/MultiSelect.tsxsrc/shared/ui/ParticipantList.tsxsrc/shared/ui/PhotoPicker.tsxsrc/shared/ui/ShareButton.tsx
💤 Files with no reviewable changes (3)
- src/entities/participant/mappers.ts
- src/entities/participant/types.ts
- src/entities/participant/utils.ts
| // Ближайшая встреча в будущем: активный слот или следующая дата по правилу. | ||
| // Будущих встреч нет — возвращаем null, чтобы не поднимать серию прошедшим стартом. | ||
| function nextMeetingDate(series: SeriesWithMeetings): Date | null { | ||
| const now = Date.now(); | ||
| const fromMeetings = series.meetings | ||
| .filter((meeting) => meeting.status === "active") | ||
| .map((meeting) => new Date(meeting.starts_at).getTime()); | ||
| const fromRule = listOccurrences(series.recurrence, series.starts_at).map( | ||
| (iso) => new Date(iso).getTime(), | ||
| ); | ||
| const upcoming = [...fromMeetings, ...fromRule] | ||
| .filter((time) => time >= now) | ||
| .sort((a, b) => a - b); | ||
| return upcoming.length > 0 ? new Date(upcoming[0]) : null; | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Найти определения и использования listOccurrences, а также участок с nextMeetingDate.
git ls-files | rg '(^|/)(app/index\.tsx|.*recurrence.*|.*occurrence.*|.*meeting.*|.*series.*)\.(ts|tsx|js|jsx)$' || true
rg -n "function nextMeetingDate|const nextMeetingDate|listOccurrences\\(" app/index.tsx .Repository: linkerkz/project-e
Length of output: 2251
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Посмотреть сигнатуру listOccurrences и связанные значения horizon.
rg -n "horizon\\s*=|listOccurrences\\s*\\(" . --glob '!**/node_modules/**'Repository: linkerkz/project-e
Length of output: 1300
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Понять, как listOccurrences использует horizon и что именно покрывают тесты.
wc -l src/entities/series/utils.ts src/entities/series/utils.test.ts app/index.tsx
sed -n '1,220p' src/entities/series/utils.ts
printf '\n--- TESTS ---\n'
sed -n '1,220p' src/entities/series/utils.test.ts
printf '\n--- APP ---\n'
sed -n '100,145p' app/index.tsxRepository: linkerkz/project-e
Length of output: 10590
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Понять реализацию listOccurrences и поведение horizon в тестах.
wc -l src/entities/series/utils.ts src/entities/series/utils.test.ts app/index.tsx
sed -n '1,220p' src/entities/series/utils.ts
printf '\n--- TESTS ---\n'
sed -n '1,220p' src/entities/series/utils.test.ts
printf '\n--- APP ---\n'
sed -n '100,145p' app/index.tsxRepository: linkerkz/project-e
Length of output: 10590
nextMeetingDate не должен использовать дефолтный horizon = 5
Для длинных повторяющихся серий listOccurrences(...) обрежет будущие даты, и лента покажет «ближайших встреч нет», хотя они есть. Передайте здесь отдельный больший горизонт или ищите первую будущую дату без фиксированного лимита.
🤖 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 `@app/index.tsx` around lines 113 - 127, Update nextMeetingDate so its
listOccurrences call does not use the default horizon of 5; pass a sufficiently
large dedicated horizon or use an unbounded way to find the first future
recurrence. Preserve the existing active-meeting candidates, filtering, sorting,
and null behavior when no future date exists.
| telegramToChatUrl, | ||
| } from "./utils"; | ||
|
|
||
| describe("activity/utils", () => { |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Переведите название набора тестов.
Предлагаемое исправление
-describe("activity/utils", () => {
+describe("активность/утилиты", () => {As per coding guidelines, «All content, comments, and documentation must be in Russian language».
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| describe("activity/utils", () => { | |
| describe("активность/утилиты", () => { |
🤖 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/entities/activity/utils.test.ts` at line 8, Переведите название набора
тестов в describe("activity/utils", ...) на русский язык, сохранив область
тестов и остальную структуру без изменений.
Source: Coding guidelines
| website: "", | ||
| }; | ||
|
|
||
| describe("profile/schemas", () => { |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Переведите название набора тестов.
Предлагаемое исправление
-describe("profile/schemas", () => {
+describe("профиль/схемы", () => {As per coding guidelines, «All content, comments, and documentation must be in Russian language».
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| describe("profile/schemas", () => { | |
| describe("профиль/схемы", () => { |
🤖 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/entities/profile/schemas.test.ts` at line 13, Переведите название набора
тестов в describe("profile/schemas") на русский язык, сохранив охватываемый
набор тестов и остальную структуру без изменений.
Source: Coding guidelines
| export function formatRegisteredAt(value: string) { | ||
| const date = new Date(value); | ||
| if (Number.isNaN(date.getTime())) return value; | ||
|
|
||
| return `${padDatePart(date.getDate())}.${padDatePart( | ||
| date.getMonth() + 1, | ||
| )}.${date.getFullYear()}`; | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check how registeredAt is stored in profile storage
rg -n 'registeredAt' src/entities/profile/storage.ts src/entities/profile/mappers.tsRepository: linkerkz/project-e
Length of output: 417
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n## storage.ts\n'
cat -n src/entities/profile/storage.ts
printf '\n## utils.ts\n'
cat -n src/entities/profile/utils.ts
printf '\n## mappers.ts\n'
if [ -f src/entities/profile/mappers.ts ]; then cat -n src/entities/profile/mappers.ts; fiRepository: linkerkz/project-e
Length of output: 4309
🏁 Script executed:
#!/bin/bash
set -euo pipefail
python3 - <<'PY'
from datetime import datetime, timezone
import os, subprocess, textwrap, json, sys
# Probe JS date behavior for a UTC ISO string and a date-only string across timezones.
script = r"""
const value = '2024-01-01T00:00:00.000Z';
const d = new Date(value);
console.log(JSON.stringify({
iso: value,
valid: !Number.isNaN(d.getTime()),
local: `${d.getDate()}.${d.getMonth()+1}.${d.getFullYear()}`,
utc: `${d.getUTCDate()}.${d.getUTCMonth()+1}.${d.getUTCFullYear()}`
}));
"""
for tz in ['UTC', 'Pacific/Honolulu', 'America/Los_Angeles', 'Asia/Tokyo']:
env = os.environ.copy()
env['TZ'] = tz
out = subprocess.check_output(['node', '-e', script], env=env, text=True).strip()
print(tz, out)
PYRepository: linkerkz/project-e
Length of output: 548
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n## src/entities/profile/storage.ts\n'
cat -n src/entities/profile/storage.ts
printf '\n## src/entities/profile/utils.ts\n'
cat -n src/entities/profile/utils.tsRepository: linkerkz/project-e
Length of output: 3328
🏁 Script executed:
#!/bin/bash
set -euo pipefail
python3 - <<'PY'
import os, subprocess
script = r"""
const value = '2024-01-01T00:00:00.000Z';
const d = new Date(value);
console.log(JSON.stringify({
valid: !Number.isNaN(d.getTime()),
local: `${d.getDate()}.${d.getMonth()+1}.${d.getFullYear()}`,
utc: `${d.getUTCDate()}.${d.getUTCMonth()+1}.${d.getUTCFullYear()}`
}));
"""
for tz in ['UTC', 'Pacific/Honolulu', 'America/Los_Angeles', 'Asia/Tokyo']:
env = os.environ.copy()
env['TZ'] = tz
out = subprocess.check_output(['node', '-e', script], env=env, text=True).strip()
print(tz, out)
PYRepository: linkerkz/project-e
Length of output: 416
Используйте UTC-методы в formatRegisteredAt
registeredAt сохраняется как UTC ISO-строка через toISOString(), а getDate()/getMonth()/getFullYear() показывают локальный календарный день. В отрицательных часовых поясах дата может сдвигаться на предыдущие сутки — лучше форматировать через getUTCDate()/getUTCMonth()/getUTCFullYear().
🤖 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/entities/profile/utils.ts` around lines 50 - 57, Update
formatRegisteredAt to use getUTCDate(), getUTCMonth(), and getUTCFullYear() when
formatting valid ISO timestamps, while preserving the existing invalid-date
fallback and padding behavior.
| export async function scheduleReminder(target: Target): Promise<boolean> { | ||
| const { enabled, offsetHours } = await readReminderSettings(); | ||
| if (!enabled) return false; | ||
|
|
||
| const remindAt = getReminderDate(target.startsAt, offsetHours); | ||
| if (remindAt == null) return false; | ||
|
|
||
| const granted = await ensureNotificationPermission(); | ||
| if (!granted) return false; | ||
|
|
||
| const notificationId = await scheduleNotificationAt( | ||
| remindAt, | ||
| target.title, | ||
| `Начало через ${offsetHours} ${hoursWord(offsetHours)}`, | ||
| ); | ||
| if (notificationId == null) return false; | ||
|
|
||
| await addReminder({ | ||
| slug: target.slug, | ||
| notificationId, | ||
| startsAt: target.startsAt, | ||
| }); | ||
| return true; | ||
| } |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Description: Find all call sites of scheduleReminder to verify whether they cancel first.
rg -n -C3 '\bscheduleReminder\s*\(' --type ts --type tsx src/Repository: linkerkz/project-e
Length of output: 184
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== scheduleReminder call sites =="
rg -n -C3 '\bscheduleReminder\s*\(' src --glob '!**/*.tsx'
echo
echo "== reminder-related files =="
git ls-files 'src/**/reminder*' 'src/**/useMarkMeeting*' 'src/**/schedule*' | sed 's#^`#-` #'
echo
echo "== relevant definitions =="
rg -n -C3 '\b(addReminder|cancelReminder|scheduleReminder|readReminderSettings|scheduleNotificationAt|getReminderDate|ensureNotificationPermission)\b' src
echo
echo "== file sizes (for focused reading) =="
for f in \
src/entities/reminder/schedule.ts \
src/entities/reminder/*.ts \
src/features/**/useMarkMeeting*.ts \
src/**/useMarkMeeting*.ts
do
[ -f "$f" ] && wc -l "$f"
doneRepository: linkerkz/project-e
Length of output: 18395
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== src/entities/reminder/schedule.ts =="
cat -n src/entities/reminder/schedule.ts
echo
echo "== src/entities/reminder/local.ts =="
cat -n src/entities/reminder/local.ts
echo
echo "== src/features/join-series/useJoinSeries.ts (relevant section) =="
sed -n '1,130p' src/features/join-series/useJoinSeries.ts | cat -n
echo
echo "== src/features/respond-to-activity/useRespondToActivityForm.ts (relevant section) =="
sed -n '1,170p' src/features/respond-to-activity/useRespondToActivityForm.ts | cat -nRepository: linkerkz/project-e
Length of output: 12160
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== src/features/join-series/useJoinSeries.ts =="
sed -n '1,130p' src/features/join-series/useJoinSeries.ts | cat -n
echo
echo "== src/features/respond-to-activity/useRespondToActivityForm.ts =="
sed -n '1,170p' src/features/respond-to-activity/useRespondToActivityForm.ts | cat -n
echo
echo "== src/entities/reminder/schedule.ts =="
sed -n '1,120p' src/entities/reminder/schedule.ts | cat -nRepository: linkerkz/project-e
Length of output: 11008
Отменять предыдущее напоминание перед повторным планированием. scheduleReminder должен сам снимать уже запланированное уведомление для того же slug: сейчас addReminder лишь перезаписывает локальную запись, а старый notificationId теряется, так что повторный вызов оставляет дубликат в системе. Это уже возможно из useRespondToActivityForm, где перед новым планированием нет cancelReminder.
🤖 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/entities/reminder/schedule.ts` around lines 21 - 44, Update
scheduleReminder to cancel any existing reminder for the same target.slug before
scheduling a new notification, reusing the existing reminder lookup and
cancellation APIs. Perform this cancellation before scheduleNotificationAt,
while preserving the current success and early-return behavior.
| useEffect(() => { | ||
| if (slug === "") return; | ||
| let active = true; | ||
| findMembership(slug) | ||
| .then((found) => { | ||
| if (active) setMembership(found); | ||
| }) | ||
| .catch(() => { | ||
| if (active) setMembership(null); | ||
| }); | ||
| return () => { | ||
| active = false; | ||
| }; | ||
| }, [slug]); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Не используйте членство от предыдущей серии после смены slug.
При новом или пустом slug состояние сохраняет прежнюю запись до завершения эффекта. Тогда leave может передать memberId старой серии в mutation новой серии.
Предлагаемое исправление
function useMembership(slug: string) {
- const [membership, setMembership] = useState<Membership | null>(null);
+ const [storedMembership, setMembership] = useState<Membership | null>(null);
+ const membership =
+ storedMembership?.seriesSlug === slug ? storedMembership : null;
useEffect(() => {
- if (slug === "") return;
+ if (slug === "") {
+ setMembership(null);
+ return;
+ }🤖 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/features/join-series/useJoinSeries.ts` around lines 102 - 115, В
`useEffect` вокруг `findMembership` сбрасывайте `membership` в `null` сразу при
каждом изменении `slug`, до проверки на пустое значение и запуска запроса. Для
пустого `slug` после сброса сразу возвращайтесь; для непустого сохраняйте
текущую защиту `active`, чтобы результат предыдущей серии не применялся.
| useEffect(() => { | ||
| if (activity?.status !== "cancelled") return; | ||
| cancelReminder(activity.slug).catch(() => {}); | ||
| }, [activity?.status, activity?.slug]); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
reminderScheduled не сбрасывается при отмене активности.
useEffect отменяет напоминание через cancelReminder, но не сбрасывает reminderScheduled в false. В результате ActivityPage показывает «Напомним за 2 часа до начала.» одновременно с «ОТМЕНЕНО», что противоречит друг другу.
🔧 Предлагаемое исправление
useEffect(() => {
if (activity?.status !== "cancelled") return;
cancelReminder(activity.slug).catch(() => {});
+ setReminderScheduled(false);
}, [activity?.status, activity?.slug]);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| useEffect(() => { | |
| if (activity?.status !== "cancelled") return; | |
| cancelReminder(activity.slug).catch(() => {}); | |
| }, [activity?.status, activity?.slug]); | |
| useEffect(() => { | |
| if (activity?.status !== "cancelled") return; | |
| cancelReminder(activity.slug).catch(() => {}); | |
| setReminderScheduled(false); | |
| }, [activity?.status, activity?.slug]); |
🤖 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/features/respond-to-activity/useRespondToActivityForm.ts` around lines 49
- 52, Update the cancellation branch in the useEffect to set reminderScheduled
to false when activity.status is "cancelled", alongside
cancelReminder(activity.slug). Keep the existing dependency list and reminder
cancellation behavior unchanged.
|
|
||
| import { generateEditToken, generateSlug, slugifyTitle } from "./publicLink"; | ||
|
|
||
| describe("shared/publicLink", () => { |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Переведите название набора тестов.
"shared/publicLink" — человекочитаемый текст на английском.
Предлагаемое исправление
-describe("shared/publicLink", () => {
+describe("общая публичная ссылка", () => {As per coding guidelines, «All content, comments, and documentation must be in Russian language».
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| describe("shared/publicLink", () => { | |
| describe("общая публичная ссылка", () => { |
🤖 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/shared/lib/publicLink.test.ts` at line 9, Переведите человекочитаемое
название набора тестов в describe для shared/publicLink на русский язык,
сохранив структуру тестов и идентификатор модуля без изменений.
Source: Coding guidelines
| function openChat() { | ||
| Linking.openURL(url).catch(() => {}); | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Пользователь не получит обратной связи при ошибке открытия чата.
Linking.openURL(url).catch(() => {}) полностью подавляет любые ошибки. Если URL невалидный или приложение-обработчик недоступно, пользователь нажимает кнопку и ничего не происходит.
Предлагаемое исправление
import { Linking } from "react-native";
import { Button } from "./Button";
type Props = {
url: string;
};
export function ChatLinkButton({ url }: Props) {
function openChat() {
- Linking.openURL(url).catch(() => {});
+ Linking.openURL(url).catch(() => {
+ Alert.alert("Ошибка", "Не удалось открыть чат");
+ });
}
return <Button title="Перейти в чат" onPress={openChat} />;
}+import { Alert, Linking } from "react-native";📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| function openChat() { | |
| Linking.openURL(url).catch(() => {}); | |
| } | |
| import { Alert, Linking } from "react-native"; | |
| import { Button } from "./Button"; | |
| type Props = { | |
| url: string; | |
| }; | |
| export function ChatLinkButton({ url }: Props) { | |
| function openChat() { | |
| Linking.openURL(url).catch(() => { | |
| Alert.alert("Ошибка", "Не удалось открыть чат"); | |
| }); | |
| } | |
| return <Button title="Перейти в чат" onPress={openChat} />; | |
| } |
🤖 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/shared/ui/ChatLinkButton.tsx` around lines 9 - 11, Update openChat in
ChatLinkButton so failures from Linking.openURL(url) are no longer silently
swallowed; provide user-visible feedback when opening the chat fails, while
preserving the existing successful URL-opening behavior.
| const result = await ImagePicker.launchImageLibraryAsync({ | ||
| mediaTypes: ["images"], | ||
| quality: 1, | ||
| }); | ||
| if (!result.canceled) onChange(result.assets[0].uri); |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -eu
rg -n -C 3 'launchImageLibraryAsync|getPendingResultAsync' src/shared/ui/PhotoPicker.tsxRepository: linkerkz/project-e
Length of output: 431
🏁 Script executed:
#!/bin/bash
set -eu
echo '--- search pending-result usage ---'
rg -n -C 2 'getPendingResultAsync|launchImageLibraryAsync|launchCameraAsync' src . --glob '!**/node_modules/**' || true
echo
echo '--- inspect PhotoPicker.tsx ---'
sed -n '1,220p' src/shared/ui/PhotoPicker.tsx
echo
echo '--- locate PhotoPicker usages ---'
rg -n 'PhotoPicker' src --glob '!**/node_modules/**' || trueRepository: linkerkz/project-e
Length of output: 2556
🌐 Web query:
Expo SDK 56 imagepicker getPendingResultAsync destroyed Android Activity docs
💡 Result:
When using expo-image-picker on Android, the operating system may terminate the MainActivity due to low memory or when the "Don't keep activities" developer option is enabled [1][2]. When this happens, the original promise returned by launchCameraAsync or launchImageLibraryAsync is lost, and the application does not receive the expected result [3][1][4]. To handle this scenario, Expo provides the getPendingResultAsync method [1]. Key information regarding this method: - Purpose: It is designed to retrieve media data that was selected or captured but lost due to the activity being destroyed by the Android system [1][5]. - Implementation: When the activity is recreated, you should call getPendingResultAsync to check if a result is waiting in persistent storage [1]. If the picker finished successfully before the activity was killed, this method returns the result; otherwise, it returns an error result or null [1][6]. - Known Limitations: The mechanism has historically been subject to bugs, including instances where it returns an empty array or fails to capture the result depending on the specific Android version and device memory conditions [7][3][6][2]. Some community reports indicate that even with this method, low-memory scenarios can cause the process to be completely killed without successfully persisting the necessary data [3][4]. Official documentation advises that developers should always account for MainActivity destruction on Android by implementing this check [1]. You can test your implementation by enabling "Don't keep activities" in your Android device's Developer Options [1][2].
Citations:
- 1: https://docs.expo.dev/versions/latest/sdk/imagepicker
- 2: expo-image-picker crashes on Android after image is picked SDK 44 expo/expo#16749
- 3: [Android] launchCameraAsync() crashes / hangs on low-RAM devices when OS kills app during camera use (SDK 54 + 55) expo/expo#43913
- 4: On some low-end Android devices, when memory is low, using expo-image for image selection can cause the current ReactActivity to be reclaimed by the system, resulting in the failure to return the ImagePicker.ImagePickerResult. expo/expo#37064
- 5: https://github.com/expo/expo/blob/main/packages/expo-image-picker/android/src/main/java/expo/modules/imagepicker/ImagePickerModule.kt
- 6: [image-picker][android] fix
getPendingResultAsyncreturn type expo/expo#35791 - 7: [expo-image-picker] Issues with getPendingResultAsync() method expo/expo#11103
🏁 Script executed:
#!/bin/bash
set -eu
# Inspect current repository file tree around shared UI and relevant consumers
git ls-files 'src/shared/ui/*' 'src/**/PhotoPicker*' 'src/**' | sed -n '1,200p'Repository: linkerkz/project-e
Length of output: 3790
Добавьте ImagePicker.getPendingResultAsync() при инициализации экрана или общего провайдера: после уничтожения MainActivity результат launchImageLibraryAsync() может потеряться, и выбранное фото не восстановится.
🤖 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/shared/ui/PhotoPicker.tsx` around lines 37 - 41, Инициализируйте
восстановление отложенного результата через ImagePicker.getPendingResultAsync()
в компоненте PhotoPicker или общем провайдере, чтобы результат после
пересоздания MainActivity передавался в onChange. Сохраните текущую обработку
результата launchImageLibraryAsync и применяйте восстановленный asset только при
наличии результата и URI.
Summary by CodeRabbit