Skip to content

feat(world): map canvas home, first-class activity URLs, and navigation re-root#6980

Closed
wcjord wants to merge 4 commits into
mainfrom
feat/world-v2-map-canvas
Closed

feat(world): map canvas home, first-class activity URLs, and navigation re-root#6980
wcjord wants to merge 4 commits into
mainfrom
feat/world-v2-map-canvas

Conversation

@wcjord

@wcjord wcjord commented Jun 12, 2026

Copy link
Copy Markdown

What

World v2 slice 1: the world map becomes the app's canvas (every section root renders it), activities get shareable first-class URLs with a map popup, sessions share into all matching course spaces, and Pangea sections move out of /rooms behind permanent redirect shims.

Why

Closes #6979. Addresses #5775.

Design doc: pangeachat/.githubvision/world_v2.md.

Testing

  • flutter analyze lib/ clean; full flutter test suite green (595/595), including new unit tests for LegacyRedirects and AppSection (15).
  • Manual (local stack, Chrome): world home at /, analytics/settings/courses roots with map canvas + correct left columns and rail selection; legacy-path redirects; cold activity-URL deep link → popup → expand → close; activity session creation shared into the matching course space; settings category highlight; camera persistence across section switches.
  • Adversarial multi-agent review over the diff (16 findings → 12 confirmed → all fixed): /courses auth gate restored, extra-carrying navigations migrated off the shim (go_router drops extra on top-level redirects), MatrixState.activeRoomId recognizes /courses chats (push suppression), shim re-encodes path segments, bounded resolver timeout inside the session-launch dialog.

Notable for reviewers:

  • lib/pangea/navigation/ is the new single source of truth: PRoutes (typed paths), AppSection (exact-segment section resolution — replaces fullPath.contains() hacks), LegacyRedirects (one router-level shim; old URLs work forever).
  • Fork-owned routes (/rooms/:roomid, archive, new-chat) intentionally stay on upstream paths — push notification and matrix.to handling untouched.
  • ~40 long-tail callsites still navigate via legacy strings; they work through the shim and can migrate to PRoutes incrementally. extra-carrying callsites were all migrated now (the shim cannot preserve extra).
  • Public course preview moved to /courses/preview/:id to avoid colliding with /courses/:spaceid.

Deploy Notes

None.

Summary by CodeRabbit

Release Notes

  • New Features

    • Interactive world map showing course activity locations with tappable pins
    • First-class activity detail pages with streamlined session launching
  • Improvements

    • Streamlined navigation structure with cleaner route paths across settings, courses, and analytics
    • Enhanced activity room creation workflows
    • Automatic handling of legacy navigation paths for backward compatibility

@coderabbitai

coderabbitai Bot commented Jun 12, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Too many files!

This PR contains 299 files, which is 149 over the limit of 150.

To get a review, narrow the scope:
• coderabbit review --type committed # exclude uncommitted changes
• coderabbit review --dir # limit to a subdirectory
• coderabbit review --base # compare against a closer base

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 9ff28e23-5b3e-48bc-85e1-813b11b0ff89

📥 Commits

Reviewing files that changed from the base of the PR and between 96fbb33 and 3e037ff.

📒 Files selected for processing (299)
  • integration_test/app_test.dart
  • integration_test/extensions/default_flows.dart
  • lib/config/routes.dart
  • lib/features/activity_sessions/activity_feedback_repo.dart
  • lib/features/activity_sessions/activity_feedback_request.dart
  • lib/features/activity_sessions/activity_feedback_response.dart
  • lib/features/activity_sessions/activity_media_enum.dart
  • lib/features/activity_sessions/activity_plan_model.dart
  • lib/features/activity_sessions/activity_plan_request.dart
  • lib/features/activity_sessions/activity_role_model.dart
  • lib/features/activity_sessions/activity_roles_model.dart
  • lib/features/activity_sessions/activity_roles_room_extension.dart
  • lib/features/activity_sessions/activity_room_extension.dart
  • lib/features/activity_sessions/activity_session_analytics_repo.dart
  • lib/features/activity_sessions/activity_session_constants.dart
  • lib/features/activity_sessions/activity_session_preview_client_extension.dart
  • lib/features/activity_sessions/activity_session_preview_repo.dart
  • lib/features/activity_sessions/activity_summary_analytics_model.dart
  • lib/features/activity_sessions/activity_summary_model.dart
  • lib/features/activity_sessions/activity_summary_repo.dart
  • lib/features/activity_sessions/activity_summary_request_model.dart
  • lib/features/activity_sessions/activity_summary_response_model.dart
  • lib/features/activity_sessions/activity_summary_room_extension.dart
  • lib/features/analytics/analytics_constants.dart
  • lib/features/analytics/client_analytics_extension.dart
  • lib/features/analytics/construct_form.dart
  • lib/features/analytics/construct_identifier.dart
  • lib/features/analytics/construct_level_enum.dart
  • lib/features/analytics/construct_type_enum.dart
  • lib/features/analytics/construct_use_model.dart
  • lib/features/analytics/construct_use_type_enum.dart
  • lib/features/analytics/constructs_event.dart
  • lib/features/analytics/constructs_model.dart
  • lib/features/analytics/room_analytics_extension.dart
  • lib/features/analytics/saved_analytics_extension.dart
  • lib/features/analytics_access/access_notice_extension.dart
  • lib/features/analytics_access/access_notice_model.dart
  • lib/features/analytics_access/course_settings_extension.dart
  • lib/features/analytics_access/course_settings_model.dart
  • lib/features/analytics_access/grant_analytics_access_extension.dart
  • lib/features/analytics_access/join_room_analytics_access_extension.dart
  • lib/features/analytics_access/join_room_analytics_consent_handler.dart
  • lib/features/analytics_data/analytics_data_service.dart
  • lib/features/analytics_data/analytics_database.dart
  • lib/features/analytics_data/analytics_database_builder.dart
  • lib/features/analytics_data/analytics_init_error_indicator.dart
  • lib/features/analytics_data/analytics_settings_extension.dart
  • lib/features/analytics_data/analytics_settings_model.dart
  • lib/features/analytics_data/analytics_status_room_extension.dart
  • lib/features/analytics_data/analytics_sync_controller.dart
  • lib/features/analytics_data/analytics_update_dispatcher.dart
  • lib/features/analytics_data/analytics_update_events.dart
  • lib/features/analytics_data/analytics_update_service.dart
  • lib/features/analytics_data/analytics_updater_mixin.dart
  • lib/features/analytics_data/construct_merge_table.dart
  • lib/features/analytics_data/derived_analytics_data_model.dart
  • lib/features/authentication/delete_account_action_enum.dart
  • lib/features/authentication/delete_account_exception.dart
  • lib/features/authentication/delete_account_extension.dart
  • lib/features/authentication/delete_account_response_model.dart
  • lib/features/authentication/p_logout.dart
  • lib/features/bot/bot_client_extension.dart
  • lib/features/bot/bot_constants.dart
  • lib/features/bot/bot_event_extension.dart
  • lib/features/bot/bot_mode.dart
  • lib/features/bot/bot_options_model.dart
  • lib/features/bot/bot_room_extension.dart
  • lib/features/bot/bot_target_event_name_enum.dart
  • lib/features/bot/utils/bot_name.dart
  • lib/features/bot/utils/bot_style.dart
  • lib/features/bot/widgets/bot_chat_settings_dialog.dart
  • lib/features/bot/widgets/bot_face_svg.dart
  • lib/features/bot/widgets/bot_settings_language_icon.dart
  • lib/features/course_plans/course_activities/course_activity_repo.dart
  • lib/features/course_plans/course_activities/course_activity_translation_request.dart
  • lib/features/course_plans/course_activities/course_activity_translation_response.dart
  • lib/features/course_plans/course_info_batch_request.dart
  • lib/features/course_plans/course_locations/course_location_media_repo.dart
  • lib/features/course_plans/course_locations/course_location_media_response.dart
  • lib/features/course_plans/course_locations/course_location_model.dart
  • lib/features/course_plans/course_locations/course_location_repo.dart
  • lib/features/course_plans/course_locations/course_location_response.dart
  • lib/features/course_plans/course_media/course_media_info.dart
  • lib/features/course_plans/course_media/course_media_repo.dart
  • lib/features/course_plans/course_media/course_media_response.dart
  • lib/features/course_plans/course_topics/course_topic_model.dart
  • lib/features/course_plans/course_topics/course_topic_repo.dart
  • lib/features/course_plans/course_topics/course_topic_translation_request.dart
  • lib/features/course_plans/course_topics/course_topic_translation_response.dart
  • lib/features/course_plans/courses/course_filter.dart
  • lib/features/course_plans/courses/course_plan_builder.dart
  • lib/features/course_plans/courses/course_plan_client_extension.dart
  • lib/features/course_plans/courses/course_plan_event.dart
  • lib/features/course_plans/courses/course_plan_model.dart
  • lib/features/course_plans/courses/course_plan_room_extension.dart
  • lib/features/course_plans/courses/course_plans_repo.dart
  • lib/features/course_plans/courses/get_localized_courses_request.dart
  • lib/features/course_plans/courses/get_localized_courses_response.dart
  • lib/features/course_plans/localization_error_result.dart
  • lib/features/course_plans/map_clipper.dart
  • lib/features/course_plans/new_course_page.dart
  • lib/features/course_plans/payload_client/image_sizes.dart
  • lib/features/course_plans/payload_client/join_field.dart
  • lib/features/course_plans/payload_client/models/course_plan/cms_course_plan_media.dart
  • lib/features/course_plans/payload_client/models/course_plan/cms_course_plan_topic_location.dart
  • lib/features/course_plans/payload_client/models/course_plan/cms_course_plan_topic_location_media.dart
  • lib/features/course_plans/payload_client/paginated_response.dart
  • lib/features/course_plans/payload_client/payload_client.dart
  • lib/features/course_plans/payload_client/polymorphic_relationship.dart
  • lib/features/download/download_dialog.dart
  • lib/features/download/download_file_util.dart
  • lib/features/download/download_room_extension.dart
  • lib/features/download/download_type_enum.dart
  • lib/features/instructions/instruction_settings.dart
  • lib/features/instructions/instructions_enum.dart
  • lib/features/instructions/instructions_inline_tooltip.dart
  • lib/features/instructions/instructions_toggle.dart
  • lib/features/join_codes/custom_join_rules_model.dart
  • lib/features/join_codes/join_code_constants.dart
  • lib/features/join_codes/join_rule_extension.dart
  • lib/features/join_codes/knock_notification_utils.dart
  • lib/features/join_codes/knock_with_code_extension.dart
  • lib/features/join_codes/knocked_rooms_extension.dart
  • lib/features/join_codes/knocked_rooms_model.dart
  • lib/features/join_codes/request_room_code_extension.dart
  • lib/features/join_codes/share_room_button.dart
  • lib/features/join_codes/share_room_code_util.dart
  • lib/features/join_codes/space_code_controller.dart
  • lib/features/join_codes/space_code_repo.dart
  • lib/features/join_codes/too_many_requests_dialog.dart
  • lib/features/languages/l2_support_enum.dart
  • lib/features/languages/language_arc_model.dart
  • lib/features/languages/language_constants.dart
  • lib/features/languages/language_display_name_postfix_widget.dart
  • lib/features/languages/language_display_name_prefix_widget.dart
  • lib/features/languages/language_model.dart
  • lib/features/languages/language_repo.dart
  • lib/features/languages/language_service.dart
  • lib/features/languages/locale_provider.dart
  • lib/features/languages/p_language_store.dart
  • lib/features/navigation/app_section.dart
  • lib/features/navigation/legacy_redirects.dart
  • lib/features/navigation/route_paths.dart
  • lib/features/notifications/enable_notifications_dialog.dart
  • lib/features/notifications/notification_tap_utils.dart
  • lib/features/notifications/notifications_client_extension.dart
  • lib/features/notifications/notifications_constants.dart
  • lib/features/notifications/notifications_request_repo.dart
  • lib/features/notifications/notifications_settings_model.dart
  • lib/features/notifications/suggest_mobile_dialog.dart
  • lib/features/room_summaries/activity_sessions_status_model.dart
  • lib/features/room_summaries/activity_summary_status_enum.dart
  • lib/features/room_summaries/room_summaries_model.dart
  • lib/features/room_summaries/room_summary_extension.dart
  • lib/features/subscription/controllers/subscription_controller.dart
  • lib/features/subscription/models/base_subscription_info.dart
  • lib/features/subscription/models/mobile_subscriptions.dart
  • lib/features/subscription/models/web_subscriptions.dart
  • lib/features/subscription/repo/subscription_management_repo.dart
  • lib/features/subscription/repo/subscription_repo.dart
  • lib/features/subscription/utils/subscription_app_id.dart
  • lib/features/subscription/widgets/paywall_card.dart
  • lib/features/subscription/widgets/subscription_options.dart
  • lib/features/subscription/widgets/subscription_paywall.dart
  • lib/features/subscription/widgets/subscription_snackbar.dart
  • lib/features/support/support_client_extension.dart
  • lib/features/tutorials/tutorial_constants.dart
  • lib/features/tutorials/tutorial_enum.dart
  • lib/features/tutorials/tutorial_model.dart
  • lib/features/tutorials/tutorial_overlay_controller.dart
  • lib/features/tutorials/tutorial_overlay_widget.dart
  • lib/features/tutorials/tutorial_sequences.dart
  • lib/features/tutorials/tutorial_state_transition_events.dart
  • lib/features/tutorials/tutorial_step_model.dart
  • lib/features/tutorials/tutorial_tooltip_container_widget.dart
  • lib/features/tutorials/tutorial_tooltip_widget.dart
  • lib/features/user/analytics_profile_model.dart
  • lib/features/user/pangea_push_rules_extension.dart
  • lib/features/user/public_profile_model.dart
  • lib/features/user/user_constants.dart
  • lib/features/user/user_controller.dart
  • lib/features/user/user_model.dart
  • lib/features/user/user_search_extension.dart
  • lib/main.dart
  • lib/pages/bootstrap/bootstrap_dialog.dart
  • lib/pages/chat_access_settings/chat_access_settings_page.dart
  • lib/pages/chat_details/chat_details_view.dart
  • lib/pages/chat_details/participant_list_item.dart
  • lib/pages/chat_encryption_settings/chat_encryption_settings.dart
  • lib/pages/chat_encryption_settings/chat_encryption_settings_view.dart
  • lib/pages/chat_list/chat_list_header.dart
  • lib/pages/chat_list/client_chooser_button.dart
  • lib/pages/chat_list/space_view.dart
  • lib/pages/chat_list/test.dart
  • lib/pages/chat_members/chat_members.dart
  • lib/pages/chat_members/chat_members_view.dart
  • lib/pages/dialer/dialer.dart
  • lib/pages/dialer/pip/dismiss_keyboard.dart
  • lib/pages/dialer/pip/pip_view.dart
  • lib/pages/homeserver_picker/homeserver_picker.dart
  • lib/pages/homeserver_picker/homeserver_picker_view.dart
  • lib/pages/invitation_selection/invitation_selection.dart
  • lib/pages/invitation_selection/invitation_selection_view.dart
  • lib/pages/login/login_view.dart
  • lib/pages/new_group/new_group.dart
  • lib/pages/new_group/new_group_view.dart
  • lib/pangea/analytics_misc/level_up/level_popup_progess_bar.dart
  • lib/pangea/common/constants/default_power_level.dart
  • lib/pangea/common/controllers/pangea_controller.dart
  • lib/pangea/common/models/base_request_model.dart
  • lib/pangea/common/models/llm_feedback_model.dart
  • lib/pangea/common/utils/firebase_analytics.dart
  • lib/pangea/common/utils/overlay.dart
  • lib/pangea/common/utils/p_vguard.dart
  • lib/pangea/common/widgets/card_error_widget.dart
  • lib/pangea/common/widgets/card_header.dart
  • lib/pangea/common/widgets/choice_array.dart
  • lib/pangea/common/widgets/feedback_dialog.dart
  • lib/pangea/common/widgets/feedback_response_dialog.dart
  • lib/pangea/common/widgets/word_audio_button.dart
  • lib/pangea/extensions/create_room_extension.dart
  • lib/pangea/extensions/pangea_room_extension.dart
  • lib/pangea/extensions/room_children_and_parents_extension.dart
  • lib/pangea/lemmas/lemma_info_request.dart
  • lib/pangea/lemmas/lemma_info_response.dart
  • lib/pangea/lemmas/lemma_meaning_builder.dart
  • lib/pangea/lemmas/user_lemma_info_extension.dart
  • lib/pangea/login/widgets/app_config_dialog.dart
  • lib/pangea/morphs/grammar_construct_example.dart
  • lib/pangea/morphs/grammar_constructs_provider.dart
  • lib/pangea/morphs/morph_meaning_widget.dart
  • lib/pangea/spaces/client_spaces_extension.dart
  • lib/pangea/spaces/load_participants_builder.dart
  • lib/pangea/spaces/public_course_extension.dart
  • lib/routes/analytics/activities/activity_archive.dart
  • lib/routes/analytics/analytics_navigation_util.dart
  • lib/routes/analytics/analytics_page_constants.dart
  • lib/routes/analytics/construct_analytics/analytics_details_popup.dart
  • lib/routes/analytics/construct_analytics/analytics_dowload_dialog.dart
  • lib/routes/analytics/construct_analytics/analytics_download_button.dart
  • lib/routes/analytics/construct_analytics/analytics_summary_enum.dart
  • lib/routes/analytics/construct_analytics/analytics_summary_model.dart
  • lib/routes/analytics/construct_analytics/construct_analytics_details/analytics_details_usage_content.dart
  • lib/routes/analytics/construct_analytics/construct_analytics_details/construct_xp_progress_bar.dart
  • lib/routes/analytics/construct_analytics/construct_analytics_details/learning_skills_enum.dart
  • lib/routes/analytics/construct_analytics/construct_analytics_details/lemma_usage_dots.dart
  • lib/routes/analytics/construct_analytics/construct_analytics_details/lemma_use_example_messages.dart
  • lib/routes/analytics/construct_analytics/construct_analytics_details/morph_details_view.dart
  • lib/routes/analytics/construct_analytics/construct_analytics_details/vocab_analytics_details_view.dart
  • lib/routes/analytics/construct_analytics/construct_analytics_details/word_text_with_audio_button.dart
  • lib/routes/analytics/construct_analytics/morph_analytics_list_view.dart
  • lib/routes/analytics/construct_analytics/practice/analytics_practice_analytics_controller.dart
  • lib/routes/analytics/construct_analytics/practice/analytics_practice_choice_card_widget.dart
  • lib/routes/analytics/construct_analytics/practice/analytics_practice_choices_widget.dart
  • lib/routes/analytics/construct_analytics/practice/analytics_practice_constants.dart
  • lib/routes/analytics/construct_analytics/practice/analytics_practice_content_widget.dart
  • lib/routes/analytics/construct_analytics/practice/analytics_practice_data_service.dart
  • lib/routes/analytics/construct_analytics/practice/analytics_practice_feedback_widget.dart
  • lib/routes/analytics/construct_analytics/practice/analytics_practice_hint_button_widget.dart
  • lib/routes/analytics/construct_analytics/practice/analytics_practice_hint_section_widget.dart
  • lib/routes/analytics/construct_analytics/practice/analytics_practice_hints_progress_widget.dart
  • lib/routes/analytics/construct_analytics/practice/analytics_practice_message_widget.dart
  • lib/routes/analytics/construct_analytics/practice/analytics_practice_page.dart
  • lib/routes/analytics/construct_analytics/practice/analytics_practice_session_controller.dart
  • lib/routes/analytics/construct_analytics/practice/analytics_practice_session_model.dart
  • lib/routes/analytics/construct_analytics/practice/analytics_practice_session_repo.dart
  • lib/routes/analytics/construct_analytics/practice/analytics_practice_ui_controller.dart
  • lib/routes/analytics/construct_analytics/practice/analytics_practice_view.dart
  • lib/routes/analytics/construct_analytics/practice/audio_analytics_practice_continue_button_widget.dart
  • lib/routes/analytics/construct_analytics/practice/choice_cards/audio_choice_card.dart
  • lib/routes/analytics/construct_analytics/practice/choice_cards/game_choice_card.dart
  • lib/routes/analytics/construct_analytics/practice/choice_cards/grammar_choice_card.dart
  • lib/routes/analytics/construct_analytics/practice/choice_cards/meaning_choice_card.dart
  • lib/routes/analytics/construct_analytics/practice/completed_analytics_practice_exercises_view.dart
  • lib/routes/analytics/construct_analytics/practice/construct_practice_extension.dart
  • lib/routes/analytics/construct_analytics/practice/example_message_util.dart
  • lib/routes/analytics/construct_analytics/practice/grammar_error_example_widget.dart
  • lib/routes/analytics/construct_analytics/practice/grammar_error_practice_generator.dart
  • lib/routes/analytics/construct_analytics/practice/grammar_error_target_generator.dart
  • lib/routes/analytics/construct_analytics/practice/grammar_match_target_generator.dart
  • lib/routes/analytics/construct_analytics/practice/insufficient_data_indicator.dart
  • lib/routes/analytics/construct_analytics/practice/morph_category_practice_exercise_generator.dart
  • lib/routes/analytics/construct_analytics/practice/ongoing_analytics_practice_session_view.dart
  • lib/routes/analytics/construct_analytics/practice/percent_marker_bar.dart
  • lib/routes/analytics/construct_analytics/practice/practice_tier_enum.dart
  • lib/routes/analytics/construct_analytics/practice/practice_timer_widget.dart
  • lib/routes/analytics/construct_analytics/practice/stat_card.dart
  • lib/routes/analytics/construct_analytics/practice/unsubscribed_practice_page.dart
  • lib/routes/analytics/construct_analytics/practice/vocab_audio_practice_exercise_generator.dart
  • lib/routes/analytics/construct_analytics/practice/vocab_audio_target_generator.dart
  • lib/routes/analytics/construct_analytics/practice/vocab_meaning_practice_exercise_generator.dart
  • lib/routes/analytics/construct_analytics/practice/vocab_meaning_target_generator.dart
  • lib/routes/analytics/construct_analytics/vocab_analytics_list_tile.dart
  • lib/routes/analytics/construct_analytics/vocab_analytics_list_view.dart
  • lib/routes/analytics/empty_analytics_page.dart
  • lib/routes/analytics/level/level_analytics_details_content.dart
  • lib/routes/archive/archive.dart
  • lib/routes/archive/archive_view.dart
  • lib/routes/chat/activity_sessions/activity_chat_controller.dart

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR implements the world_v2 navigation redesign: creates a route paths foundation (PRoutes), adds AppSection-based URI routing, introduces legacy /rooms redirect shims, builds a world map canvas with location and activity pins, adds first-class activity URLs, centralizes activity session creation, restructures the route tree with new /analytics, /courses, /settings, /profile roots, and wires new routing throughout the navigation UI and auth flows.

Changes

Pangea World v2 Navigation, Map Canvas, and First-Class Activities

Layer / File(s) Summary
Route paths foundation and constants
lib/pangea/navigation/route_paths.dart
PRoutes abstract class centralizes canonical route constants (world, analytics, courses, settings, profile, rooms) and builders (course(), room(), worldObject()), plus UUID pattern detection for first-class activity routes.
AppSection enum for URI-based section detection
lib/pangea/navigation/app_section.dart
AppSection enum associates five sections with route paths and icons; fromUri() selects section from first path segment (mapping rooms and UUIDs to chats); activeSpaceId() extracts joined course space from /courses/:spaceid URIs.
Legacy redirects for backward compatibility
lib/pangea/navigation/legacy_redirects.dart
LegacyRedirects utility with resolve() rewriting /rooms/* legacy paths to world_v2 roots (/analytics, /courses, /settings, /profile) with course-flow special handling; handle() go_router adapter; worldHome getter for home route.
World map canvas widget and initialization
lib/pangea/world/widgets/world_map.dart
WorldMap stateful widget renders full-bleed FlutterMap with OpenStreetMap tiles, dual marker layers (locations and activities), optional camera control, and asynchronous location/activity data loading with repository-error tolerance.
Map camera persistence
lib/pangea/world/world_camera_state.dart
WorldCameraState abstraction with static lastCenter and lastZoom fields plus remember() method to persist and restore map viewport across navigation.
World map location data repository
lib/pangea/world/world_locations_repo.dart
WorldLocationsRepo with 10-minute in-memory caching; mappableLocations() queries CMS via PayloadClient, paginates up to 10 pages, filters to valid non-placeholder coordinates.
Activity pins and world activities repository
lib/pangea/world/world_activities_repo.dart
WorldActivityPin data class and WorldActivitiesRepo.activityPins() building pins from course plans, topic translations, and mappable locations with 10-minute caching and offset-ring placement for co-located activities.
Activity course space resolver
lib/pangea/world/activity_course_resolver.dart
ActivityCourseResolver.matchingCourseSpaces() filters joined spaces to course-plan spaces, loads localized plans for user's L1 language, builds topic-to-plan mapping with optional L2 region matching, returns rooms whose plans contain target activity.
First-class activity map detail page
lib/pangea/world/pages/activity_map_page.dart
ActivityMapPage stateful widget loads activity data, resolves matching course spaces, checks completion, renders dual-mode UI: expanded session-start overlay or map with activity popup showing details, location, completion check, expandable and close actions.
Custom join rules with multiple allow room IDs
lib/pangea/join_codes/join_rule_extension.dart
generateCustomJoinRules() extended with optional allowRoomIds list parameter; aggregates both allowRoomId and allowRoomIds into combined set; generates m.room_membership allow entries for all collected IDs or null when empty.
Space child management without removing parents
lib/pangea/extensions/room_children_and_parents_extension.dart
Room.addSpaceChildKeepingParents() extension method delegates to _trySetSpaceChild to add room as space child while preserving other parent space associations.
Centralized activity session room creation
lib/pangea/activity_sessions/launch_activity_session.dart
Client.launchActivitySession() extension with best-effort course space resolution (10s timeout), private room creation with activity state and optional avatar/role, join rules based on matched spaces, per-space child attachment isolating failures, and 10s sync wait returning room ID.
CoursePlanRoomExtension delegates to centralized launch
lib/pangea/course_plans/courses/course_plan_room_extension.dart
launchActivityRoom() refactored from 68-line local async implementation to expression-bodied delegate calling client.launchActivitySession() with primarySpace parameter.
Select role controller uses centralized launch
lib/pangea/activity_sessions/activity_session_start/select_role_session_controller.dart
Updated confirmRoleSelection to call client.launchActivitySession() instead of widget.course.launchActivityRoom(), preserving loading dialog and analytics tracking.
Activity session view fallback navigation
lib/pangea/activity_sessions/activity_session_start/activity_sessions_start_view.dart
Close button conditionally pops when stack allows or falls back to GoRouter.go(PRoutes.world), adding go_router and route_paths imports.
Route tree restructuring and world_v2 roots
lib/config/routes.dart
Removes legacy Matrix handling, adds world_v2 / route with EmptyPage/ChatList in column mode, rewires /:roomid with shareItems support and nested search/invite/details, adds /courses (space routing, activity sessions), /analytics (practice exit confirmation), /settings (nested pages, learning exit), /profile, and first-class /:activityId UUID route to ActivityMapPage.
EmptyPage switched to world map canvas
lib/widgets/layouts/empty_page.dart
Replaces logo-based empty state with const WorldMap() in Scaffold body; removes dart:math dependency and logo widget import.
Legacy redirects wired to router
lib/widgets/fluffy_chat_app.dart
Integrates LegacyRedirects into GoRouter via redirect: (context, state) => LegacyRedirects.handle(state.uri) callback for permanent /rooms/* path shims.
Navigation rail uses AppSection and PRoutes
lib/widgets/navigation_rail.dart
Replaces path-substring detection with AppSection.fromUri() URI parsing for active section state; updates all section navigation to PRoutes constants (profile, world, courses, settings); changes _onTapSpace to use PRoutes.course().
Space navigation column uses AppSection for content routing
lib/pangea/spaces/space_navigation_column.dart
Replaces fullPath substring checks with AppSection.fromUri() section detection; switches _MainView.build to return analytics sub-views, Settings widget, FindCoursePage (or space-scoped ChatList), or chat ChatList by section.
Matrix post-login and active room routing
lib/widgets/matrix.dart
Changes post-login destination from hardcoded '/rooms' to PRoutes.world in multiple flows; broadens activeRoomId validation to accept /rooms/, /courses/, or /analytics/activities/ paths (previously /rooms/ only).
Chat list space and ignore list navigation
lib/pages/chat_list/chat_list.dart
Replaces hardcoded /rooms paths with PRoutes.world and PRoutes.course() for space navigation; updates ignorelist route from /rooms/settings/security/ignorelist to /settings/security/ignorelist.
Settings view uses /settings routes
lib/pages/settings/settings_view.dart
Replaces all /rooms/settings/* paths with /settings/* for active-state detection and navigation across learning, theme, notifications, devices, chat, subscription, and security tiles.
Notification tap navigation uses PRoutes
lib/pangea/notifications/notification_tap_utils.dart
Replaces hardcoded /rooms/spaces and /rooms paths with PRoutes.course() and PRoutes.world for activity-session, parent-course, invite, and space navigation.
PAuthGuard home redirect uses PRoutes
lib/pangea/common/utils/p_vguard.dart
Replaces hardcoded '/rooms' strings with PRoutes.world for logged-in and post-L2 home redirects.
Course chats ignore list navigation
lib/pangea/course_chats/course_chats_page.dart
Updates invite-block action route to /settings/security/ignorelist instead of /rooms/settings/security/ignorelist.
Course creation post-space navigation
lib/pangea/course_creation/selected_course_page.dart
Updates launchCourse() to navigate to /courses/own/:courseId/invite instead of /rooms/course/own/:courseId/invite.
Chat context menu ignore list navigation
lib/pangea/chat_settings/widgets/chat_context_menu_action.dart
Updates block action route to /settings/security/ignorelist instead of /rooms/settings/security/ignorelist.
User dialog ignore list navigation
lib/widgets/adaptive_dialogs/user_dialog.dart
Updates block action route to /settings/security/ignorelist instead of /rooms/settings/security/ignorelist.
Navigation util uses PRoutes for space routing
lib/pangea/navigation/navigation_util.dart
Replaces /rooms/spaces/:spaceid branch detection with /courses/:spaceid matching; builds destinations using PRoutes.course() and PRoutes.world.
AppSection URI resolution tests
test/pangea/navigation/app_section_test.dart
Comprehensive tests for AppSection.fromUri() and activeSpaceId(): section selection by first path segment, rooms/UUID mapping to chats, all section roots/subpaths, exact-segment matching without substring leakage, and activeSpaceId detection of joined spaces.
Legacy redirects rewrite tests
test/pangea/navigation/legacy_redirects_test.dart
Test suite for LegacyRedirects.resolve() and handle(): validates /rooms-to-root rewrites, course-flow literal paths, course preview encoding, course space moves, encoded segment survival, query string preservation, null cases, and self-redirect suppression.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes


🐰 A flutter of routes and a map so bright,
World v2 emerges in shimmering light,
Activities shared, sections realigned,
Legacy paths left behind!
🌍✨🎯

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/world-v2-map-canvas

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 8

🧹 Nitpick comments (2)
lib/pangea/spaces/space_navigation_column.dart (1)

196-207: ⚡ Quick win

Consider tightening the 'addcourse' segment check for clarity.

The segments.contains('addcourse') check at line 201 matches the string in any position within the path segments array. While this provides flexibility, a more explicit positional check would improve clarity and prevent unintended matches.

♻️ Proposed refinement

If the intended route structure is /courses/addcourse/..., consider checking the specific position:

       case AppSection.courses:
         // Find/browse flows show the course list in the left column; a
         // joined course (`/courses/:spaceid`) shows the chat list scoped
         // to that space until the course-outline relayout lands.
         if (AppSection.activeSpaceId(uri) == null &&
-            !segments.contains('addcourse')) {
+            (segments.length < 2 || segments[1] != 'addcourse')) {
           return const FindCoursePage();
         }

Alternatively, if multiple creation-flow paths exist (e.g., own, preview, addcourse), consider extracting the logic into a helper method for clarity:

bool _isCourseBrowsing(List<String> segments, Uri uri) {
  if (AppSection.activeSpaceId(uri) != null) return false;
  if (segments.length < 2) return true;
  // Literal creation/preview flows return false
  return !const ['addcourse', 'own', 'preview'].contains(segments[1]);
}
🤖 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 `@lib/pangea/spaces/space_navigation_column.dart` around lines 196 - 207, The
segments.contains('addcourse') check in the AppSection.courses branch is too
broad; update the condition to a positional check (e.g., verify segments.length
> 1 and compare segments[1] to 'addcourse') or extract a helper (e.g.,
_isCourseBrowsing(List<String> segments, Uri uri)) and use
AppSection.activeSpaceId(uri) plus explicit positional checks for known
creation/preview tokens ('addcourse', 'own', 'preview') to avoid accidental
matches; modify the branch that returns FindCoursePage() to call the new
positional check or helper and return ChatList otherwise so behavior is
unchanged but more explicit.
lib/widgets/layouts/empty_page.dart (1)

1-46: 💤 Low value

Consider removing commented-out code.

The file contains extensive commented-out code (L2-3, L9-10, L14-15, L20-22, L32-43) from the previous logo-based implementation. If this code is no longer needed and won't be referenced for rollback or comparison, consider removing it to improve maintainability. If it serves as documentation or a fallback, the comments can remain.

🧹 Proposed cleanup to remove dead code
-// `#Pangea`
-// import 'dart:math';
-// Pangea#
-
 import 'package:flutter/material.dart';

-// `#Pangea`
+// `#Pangea`
 import 'package:fluffychat/pangea/world/widgets/world_map.dart';
-// import 'package:fluffychat/pangea/common/widgets/pangea_logo_svg.dart';
-// Pangea#
+// Pangea#

 class EmptyPage extends StatelessWidget {
-  // `#Pangea`
-  // static const double _width = 400;
-  // Pangea#
   const EmptyPage({super.key});
   `@override`
   Widget build(BuildContext context) {
-    // `#Pangea`
-    // final width = min(MediaQuery.sizeOf(context).width, EmptyPage._width) / 2;
-    // final theme = Theme.of(context);
-    // Pangea#
     return Scaffold(
       // Add invisible appbar to make status bar on Android tablets bright.
       appBar: AppBar(
         automaticallyImplyLeading: false,
         elevation: 0,
         backgroundColor: Colors.transparent,
       ),
       extendBodyBehindAppBar: true,
       // `#Pangea`
       body: const WorldMap(),
-      // body: Container(
-      //   alignment: Alignment.center,
-      //   child: Image.asset(
-      //     'assets/logo_transparent.png',
-      //     color: theme.colorScheme.surfaceContainerHigh,
-      //     width: width,
-      //     height: width,
-      //     filterQuality: FilterQuality.medium,
-      //   ),
-      // ),
       // Pangea#
     );
   }
 }
🤖 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 `@lib/widgets/layouts/empty_page.dart` around lines 1 - 46, This file contains
large blocks of dead commented code (old imports, constants, and the previous
logo-based body) around the EmptyPage widget; remove the commented-out lines
(the commented 'dart:math' import, the commented pangea imports for Pangea logo,
the commented static _width and width/theme calculations, and the commented
Container body) leaving a clean EmptyPage implementation that uses the const
WorldMap() body and the existing AppBar; ensure you only delete commented lines
and keep symbols EmptyPage and WorldMap intact so behavior is unchanged.
🤖 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 `@lib/pangea/activity_sessions/launch_activity_session.dart`:
- Around line 50-53: The map literal for the local variable spaces uses invalid
nullable-key syntax; change it to only insert the primarySpace entry when
primarySpace is non-null (i.e., add a conditional entry like "if (primarySpace
!= null) primarySpace.id: primarySpace") and keep the for-loop entry for
matching spaces; locate the variable named spaces and the primarySpace
identifier in launch_activity_session.dart and update the map literal
accordingly.

In `@lib/pangea/world/activity_course_resolver.dart`:
- Around line 59-64: The code currently force-unwraps
planIdByTopicId[topic.uuid] when building matchingPlanIds which can crash if
topic.uuid is absent; change the lookup to a nullable lookup and only add to
matchingPlanIds when the value is non-null (e.g., var planId =
planIdByTopicId[topic.uuid]; if (planId != null) matchingPlanIds.add(planId)).
Ensure this change is applied in the loop over topics.topics.values where
matchingPlanIds is constructed so missing topic.uuid entries are safely ignored.

In `@lib/pangea/world/pages/activity_map_page.dart`:
- Around line 307-309: Replace the hardcoded error string in the _ErrorPopup
widget with a localized string: add a new L10n key (e.g., activityNotFound) to
your ARB/localization files, run codegen, then update the Text call inside
_ErrorPopup to use L10n.of(context).activityNotFound (or the generated getter)
instead of the literal "Activity not found" so the popup uses the i18n resource.
- Around line 271-279: The SnackBar content is hardcoded; add a new localization
key (e.g., joinCourseToPlayActivity) to your L10n files and use it instead of
the literal string—call L10n.of(context).joinCourseToPlayActivity and pass
_activity!.title (or use interpolation pattern in the ARB/strings) when building
the SnackBar in the onPressed branch that checks _matchingSpaces and toggles
_expanded; update all localization resource files (and generated L10n classes)
so the key exists for all locales.

In `@lib/pangea/world/widgets/world_map.dart`:
- Around line 134-151: Wrap the tappable activity marker's GestureDetector in a
Semantics widget providing an explicit accessible name and button role: replace
the direct GestureDetector child with Semantics(label: /* use activity
title/name, e.g. activity.name or activity.title */ , button: true, child:
GestureDetector(...)); keep the existing onTap handler (_openActivity(activity))
and visual decoration unchanged, and ensure the label uses the activity's
human-readable property (fallback to a generic 'Activity' if null) so screen
readers and Playwright can identify the control.
- Around line 46-62: Update the two catch blocks in _loadLocations and
_loadActivities to capture error and stack (catch (e, s)) and call
ErrorHandler.logError(e, s) so failures from
WorldLocationsRepo.mappableLocations() and WorldActivitiesRepo.activityPins()
are reported to Sentry; keep the existing behavior of not rethrowing (map stays
usable) and only add the ErrorHandler.logError call (optionally gated by
production if desired).

In `@lib/pangea/world/world_activities_repo.dart`:
- Around line 47-141: The activityPins() method can throw from calls like
CoursePlansRepo.search, CourseTopicRepo.get, CourseActivityRepo.get, and
WorldLocationsRepo.mappableLocations and currently has no logging; wrap the main
work (everything after the cache validity check) in a try-catch, import and call
Sentry.captureException(error, stackTrace) (and optionally a local logger) in
the catch block to record the error, then rethrow the exception (or return
_setCache([]) if you prefer to swallow but still log); ensure you reference
activityPins, CoursePlansRepo.search, CourseTopicRepo.get,
CourseActivityRepo.get, WorldLocationsRepo.mappableLocations, and use
Sentry.captureException to maintain observability.

In `@lib/pangea/world/world_locations_repo.dart`:
- Around line 14-44: Wrap the body of mappableLocations() in a try-catch that
catches all exceptions thrown by PayloadClient.find (and any other calls
inside), log the error to Sentry using Sentry.captureException with the caught
exception and stack trace, and optionally emit a local log (e.g.,
processLogger.error) with context including the function name and page info;
after logging rethrow the exception (or return an empty list if you prefer
failing-safe) so callers keep current behavior. Ensure you import Sentry and
reference mappableLocations, PayloadClient.find, _cache, _lastFetched, and
_hasUsableCoordinates when updating the method.

---

Nitpick comments:
In `@lib/pangea/spaces/space_navigation_column.dart`:
- Around line 196-207: The segments.contains('addcourse') check in the
AppSection.courses branch is too broad; update the condition to a positional
check (e.g., verify segments.length > 1 and compare segments[1] to 'addcourse')
or extract a helper (e.g., _isCourseBrowsing(List<String> segments, Uri uri))
and use AppSection.activeSpaceId(uri) plus explicit positional checks for known
creation/preview tokens ('addcourse', 'own', 'preview') to avoid accidental
matches; modify the branch that returns FindCoursePage() to call the new
positional check or helper and return ChatList otherwise so behavior is
unchanged but more explicit.

In `@lib/widgets/layouts/empty_page.dart`:
- Around line 1-46: This file contains large blocks of dead commented code (old
imports, constants, and the previous logo-based body) around the EmptyPage
widget; remove the commented-out lines (the commented 'dart:math' import, the
commented pangea imports for Pangea logo, the commented static _width and
width/theme calculations, and the commented Container body) leaving a clean
EmptyPage implementation that uses the const WorldMap() body and the existing
AppBar; ensure you only delete commented lines and keep symbols EmptyPage and
WorldMap intact so behavior is unchanged.
🪄 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 Plus

Run ID: 636ad527-c119-4a2a-8c12-7d3ae1d43abb

📥 Commits

Reviewing files that changed from the base of the PR and between e861a99 and 96fbb33.

📒 Files selected for processing (32)
  • lib/config/routes.dart
  • lib/pages/chat_list/chat_list.dart
  • lib/pages/settings/settings_view.dart
  • lib/pangea/activity_sessions/activity_session_start/activity_sessions_start_view.dart
  • lib/pangea/activity_sessions/activity_session_start/select_role_session_controller.dart
  • lib/pangea/activity_sessions/launch_activity_session.dart
  • lib/pangea/chat_settings/widgets/chat_context_menu_action.dart
  • lib/pangea/common/utils/p_vguard.dart
  • lib/pangea/course_chats/course_chats_page.dart
  • lib/pangea/course_creation/selected_course_page.dart
  • lib/pangea/course_plans/courses/course_plan_room_extension.dart
  • lib/pangea/extensions/room_children_and_parents_extension.dart
  • lib/pangea/join_codes/join_rule_extension.dart
  • lib/pangea/navigation/app_section.dart
  • lib/pangea/navigation/legacy_redirects.dart
  • lib/pangea/navigation/navigation_util.dart
  • lib/pangea/navigation/route_paths.dart
  • lib/pangea/notifications/notification_tap_utils.dart
  • lib/pangea/spaces/space_navigation_column.dart
  • lib/pangea/world/activity_course_resolver.dart
  • lib/pangea/world/pages/activity_map_page.dart
  • lib/pangea/world/widgets/world_map.dart
  • lib/pangea/world/world_activities_repo.dart
  • lib/pangea/world/world_camera_state.dart
  • lib/pangea/world/world_locations_repo.dart
  • lib/widgets/adaptive_dialogs/user_dialog.dart
  • lib/widgets/fluffy_chat_app.dart
  • lib/widgets/layouts/empty_page.dart
  • lib/widgets/matrix.dart
  • lib/widgets/navigation_rail.dart
  • test/pangea/navigation/app_section_test.dart
  • test/pangea/navigation/legacy_redirects_test.dart

Comment on lines +50 to +53
final spaces = <String, Room>{
?primarySpace?.id: ?primarySpace,
for (final space in matching) space.id: space,
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if the file compiles by running flutter analyze on this specific file
flutter analyze lib/pangea/activity_sessions/launch_activity_session.dart

Repository: pangeachat/client

Length of output: 109


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Tool availability =="
command -v flutter >/dev/null 2>&1 && echo "flutter: yes" || echo "flutter: no"
command -v dart >/dev/null 2>&1 && echo "dart: yes" || echo "dart: no"

echo "== File snippet =="
nl -ba lib/pangea/activity_sessions/launch_activity_session.dart | sed -n '35,80p'

echo "== Search for suspicious nullable-key map syntax =="
rg -n --hidden --no-ignore-vcs "\\?\\s*\\w+\\?\\." lib/pangea/activity_sessions/launch_activity_session.dart || true

Repository: pangeachat/client

Length of output: 186


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Tool availability =="
command -v python3 >/dev/null 2>&1 && echo "python3: yes" || echo "python3: no"
command -v dart >/dev/null 2>&1 && echo "dart: yes" || echo "dart: no"
command -v flutter >/dev/null 2>&1 && echo "flutter: yes" || echo "flutter: no"

echo "== Print lines 40-70 of target file (via python) =="
python3 - <<'PY'
from pathlib import Path
p = Path("lib/pangea/activity_sessions/launch_activity_session.dart")
txt = p.read_text(encoding="utf-8").splitlines()
start, end = 40, 70
for i in range(start-1, min(end, len(txt))):
    print(f"{i+1:4d}: {txt[i]}")
PY

echo "== Search for suspicious pattern in that file =="
rg -n "\\?\\s*primarySpace\\?\\.id\\s*:" lib/pangea/activity_sessions/launch_activity_session.dart || true
rg -n "<String,\\s*Room>\\{[\\s\\S]*" lib/pangea/activity_sessions/launch_activity_session.dart --context 2 || true

Repository: pangeachat/client

Length of output: 1585


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Find occurrences of primarySpace in the file =="
rg -n "primarySpace" lib/pangea/activity_sessions/launch_activity_session.dart

echo "== Show nearby context around each occurrence (first few) =="
python3 - <<'PY'
from pathlib import Path
p = Path("lib/pangea/activity_sessions/launch_activity_session.dart")
lines = p.read_text(encoding="utf-8").splitlines()
idxs = [i for i,l in enumerate(lines) if "primarySpace" in l]
for i in idxs[:10]:
    a=max(0,i-5); b=min(len(lines), i+8)
    print("\n--- around line", i+1, "---")
    for j in range(a,b):
        print(f"{j+1:4d}: {lines[j]}")
PY

echo "== Locate Room.id declaration =="
rg -n "class Room\\b" -S lib | head -n 20 || true
# Try to find `id` field inside Room
rg -n "class Room\\b|\\bid\\b" lib | rg -n "Room" | head -n 50 || true

Repository: pangeachat/client

Length of output: 7918


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Search for Room class definition in repo =="
rg -n --hidden --no-ignore-vcs "class Room\\b|abstract class Room\\b|mixin Room\\b|interface Room\\b" lib -S || true

echo "== Search for a getter/field named id on Room =="
rg -n --hidden --no-ignore-vcs "(class Room\\b[\\s\\S]{0,2000}?)\\b(id)\\b" lib -S || true

echo "== Find where launch_activity_session.dart imports Room =="
python3 - <<'PY'
from pathlib import Path
p=Path("lib/pangea/activity_sessions/launch_activity_session.dart")
txt=p.read_text(encoding="utf-8").splitlines()
for i,l in enumerate(txt[:80], start=1):
    if "import" in l:
        print(f"{i:4d}: {l}")
PY

echo "== Search for 'Room?' usage in same file =="
rg -n "\\bRoom\\?" lib/pangea/activity_sessions/launch_activity_session.dart

Repository: pangeachat/client

Length of output: 1364


Fix invalid nullable-key syntax in spaces map literal

?primarySpace?.id: ?primarySpace is not valid Dart syntax for map entries. Replace it with a conditional entry when primarySpace is non-null.

🐛 Proposed fix
-    final spaces = <String, Room>{
-      ?primarySpace?.id: ?primarySpace,
-      for (final space in matching) space.id: space,
-    };
+    final spaces = <String, Room>{
+      if (primarySpace != null) primarySpace.id: primarySpace,
+      for (final space in matching) space.id: space,
+    };
📝 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.

Suggested change
final spaces = <String, Room>{
?primarySpace?.id: ?primarySpace,
for (final space in matching) space.id: space,
};
final spaces = <String, Room>{
if (primarySpace != null) primarySpace.id: primarySpace,
for (final space in matching) space.id: space,
};
🤖 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 `@lib/pangea/activity_sessions/launch_activity_session.dart` around lines 50 -
53, The map literal for the local variable spaces uses invalid nullable-key
syntax; change it to only insert the primarySpace entry when primarySpace is
non-null (i.e., add a conditional entry like "if (primarySpace != null)
primarySpace.id: primarySpace") and keep the for-loop entry for matching spaces;
locate the variable named spaces and the primarySpace identifier in
launch_activity_session.dart and update the map literal accordingly.

Comment on lines +59 to +64
final matchingPlanIds = <String>{};
for (final topic in topics.topics.values) {
if (topic.activityIds.contains(activityId)) {
matchingPlanIds.add(planIdByTopicId[topic.uuid]!);
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Verify CourseTopicRepo.get contract: does it return only requested topics?

# Search for CourseTopicRepo.get implementation
ast-grep --pattern $'class CourseTopicRepo {
  $$$
  static $_ get($$$) {
    $$$
  }
  $$$
}'

# Also check the response model for any filtering or extra topics
rg -n -A10 'class.*TopicResponse|class.*TranslateTopicResponse' \
  lib/pangea/course_plans/course_topics/

Repository: pangeachat/client

Length of output: 1390


Guard against missing topic.uuid in planIdByTopicId lookup (planIdByTopicId[topic.uuid]!)

TranslateTopicResponse.fromJson populates topics directly from json['topics'] without filtering to the requested topicIds, so topics.topics.values may contain a topic.uuid that isn’t present in planIdByTopicId. That makes the non-null assertion (!) potentially crash at runtime; use a nullable lookup + null-check before adding to matchingPlanIds.

🤖 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 `@lib/pangea/world/activity_course_resolver.dart` around lines 59 - 64, The
code currently force-unwraps planIdByTopicId[topic.uuid] when building
matchingPlanIds which can crash if topic.uuid is absent; change the lookup to a
nullable lookup and only add to matchingPlanIds when the value is non-null
(e.g., var planId = planIdByTopicId[topic.uuid]; if (planId != null)
matchingPlanIds.add(planId)). Ensure this change is applied in the loop over
topics.topics.values where matchingPlanIds is constructed so missing topic.uuid
entries are safely ignored.

Comment on lines +271 to +279
onPressed: _matchingSpaces.isNotEmpty
? () => setState(() => _expanded = true)
: () => ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'Join the course for "${_activity!.title}" to play it!',
),
),
),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use L10n for user-facing text.

The SnackBar message at L276 is hardcoded in English. Since the file already imports L10n and uses it elsewhere (L166, L270), this string should also be localized for consistency and i18n compliance.

🌐 Proposed fix to localize the message

Add a new L10n key (e.g., joinCourseToPlayActivity) to the localization files, then:

                      : () => ScaffoldMessenger.of(context).showSnackBar(
                          SnackBar(
                            content: Text(
-                             'Join the course for "${_activity!.title}" to play it!',
+                             L10n.of(context).joinCourseToPlayActivity(_activity!.title),
                            ),
                          ),
                        ),
🤖 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 `@lib/pangea/world/pages/activity_map_page.dart` around lines 271 - 279, The
SnackBar content is hardcoded; add a new localization key (e.g.,
joinCourseToPlayActivity) to your L10n files and use it instead of the literal
string—call L10n.of(context).joinCourseToPlayActivity and pass _activity!.title
(or use interpolation pattern in the ARB/strings) when building the SnackBar in
the onPressed branch that checks _matchingSpaces and toggles _expanded; update
all localization resource files (and generated L10n classes) so the key exists
for all locales.

Comment on lines +307 to +309
const Icon(Icons.error_outline, size: 36),
const SizedBox(height: 8),
Text('Activity not found', style: theme.textTheme.titleMedium),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use L10n for user-facing text.

The error message "Activity not found" at L309 is hardcoded in English. Since _ErrorPopup is part of the user-facing UI, this text should be localized using L10n.of(context) for consistency and i18n compliance.

🌐 Proposed fix to localize the error message

Add a new L10n key (e.g., activityNotFound) to the localization files, then:

            const Icon(Icons.error_outline, size: 36),
            const SizedBox(height: 8),
-           Text('Activity not found', style: theme.textTheme.titleMedium),
+           Text(L10n.of(context).activityNotFound, style: theme.textTheme.titleMedium),
            const SizedBox(height: 12),
🤖 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 `@lib/pangea/world/pages/activity_map_page.dart` around lines 307 - 309,
Replace the hardcoded error string in the _ErrorPopup widget with a localized
string: add a new L10n key (e.g., activityNotFound) to your ARB/localization
files, run codegen, then update the Text call inside _ErrorPopup to use
L10n.of(context).activityNotFound (or the generated getter) instead of the
literal "Activity not found" so the popup uses the i18n resource.

Comment on lines +46 to +62
Future<void> _loadLocations() async {
try {
final locations = await WorldLocationsRepo.mappableLocations();
if (mounted) setState(() => _locations = locations);
} catch (_) {
// Map stays usable without pins; Sentry catches repo errors.
}
}

Future<void> _loadActivities() async {
try {
final activities = await WorldActivitiesRepo.activityPins();
if (mounted) setState(() => _activities = activities);
} catch (_) {
// Map stays usable without activity pins.
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check if WorldLocationsRepo and WorldActivitiesRepo log errors to Sentry.

# Search for ErrorHandler.logError or Sentry logging in the repositories
rg -n -A3 'ErrorHandler\.logError|Sentry\.' \
  lib/pangea/world/world_locations_repo.dart \
  lib/pangea/world/world_activities_repo.dart

Repository: pangeachat/client

Length of output: 43


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Locate the repo implementations
echo "== WorldLocationsRepo implementation(s) =="
fd -e dart -g '*world*locations*repo*.dart' lib/pangea/world || true
rg -n --hidden -S "class\s+WorldLocationsRepo|WorldLocationsRepo" lib/pangea/world -g'*.dart'

echo
echo "== WorldActivitiesRepo implementation(s) =="
fd -e dart -g '*world*activities*repo*.dart' lib/pangea/world || true
rg -n --hidden -S "class\s+WorldActivitiesRepo|WorldActivitiesRepo" lib/pangea/world -g'*.dart'

echo
# 2) Show the current repository files (or key error handling parts)
echo "== Candidate file contents (error handling/logging related) =="
rg -n -S "try\s*\{|catch\s*\(|ErrorHandler|Sentry|captureException|logError|log.*error" \
  lib/pangea/world/world_locations_repo.dart \
  lib/pangea/world/world_activities_repo.dart || true

echo
# 3) Broader search across lib/pangea/world for sentry/error logging helpers
echo "== Broader search in lib/pangea/world for Sentry/ErrorHandler =="
rg -n -S "Sentry\.|ErrorHandler\.|captureException|logError|captureMessage" lib/pangea/world -g'*.dart' || true

Repository: pangeachat/client

Length of output: 1040


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find the relevant method bodies
echo "== mappableLocations() implementation =="
rg -n "mappableLocations\s*\(" lib/pangea/world/world_locations_repo.dart
echo
# Show the method (approx range around match)
line=$(rg -n "mappableLocations\s*\(" lib/pangea/world/world_locations_repo.dart | head -n1 | cut -d: -f1)
start=$((line-30)); end=$((line+120))
sed -n "${start},${end}p" lib/pangea/world/world_locations_repo.dart | cat -n

echo
echo "== activityPins() implementation =="
rg -n "activityPins\s*\(" lib/pangea/world/world_activities_repo.dart
echo
line=$(rg -n "activityPins\s*\(" lib/pangea/world/world_activities_repo.dart | head -n1 | cut -d: -f1)
start=$((line-30)); end=$((line+160))
sed -n "${start},${end}p" lib/pangea/world/world_activities_repo.dart | cat -n

echo
echo "== Any ErrorHandler.logError / Sentry usage in these repo files =="
rg -n -S "ErrorHandler\.logError|Sentry\.|captureException|captureMessage|logError" \
  lib/pangea/world/world_locations_repo.dart lib/pangea/world/world_activities_repo.dart || true

echo
echo "== Where ErrorHandler.logError is called (to see if UI catches/logs or repos do) =="
rg -n "ErrorHandler\.logError" lib/pangea/world -g'*.dart' || true

Repository: pangeachat/client

Length of output: 2025


🏁 Script executed:

#!/bin/bash
set -euo pipefail

file1="lib/pangea/world/world_locations_repo.dart"
file2="lib/pangea/world/world_activities_repo.dart"

echo "== Searching logging usage in repo files =="
rg -n -S "ErrorHandler\.logError|Sentry\.|captureException|captureMessage|logError" "$file1" "$file2" || true

echo
echo "== mappableLocations() context =="
line1=$(rg -n "mappableLocations\s*\(" "$file1" | head -n1 | cut -d: -f1)
start1=$((line1-30)); [ "$start1" -lt 1 ] && start1=1
end1=$((line1+140))
nl -ba "$file1" | sed -n "${start1},${end1}p"

echo
echo "== activityPins() context =="
line2=$(rg -n "activityPins\s*\(" "$file2" | head -n1 | cut -d: -f1)
start2=$((line2-30)); [ "$start2" -lt 1 ] && start2=1
end2=$((line2+220))
nl -ba "$file2" | sed -n "${start2},${end2}p"

Repository: pangeachat/client

Length of output: 202


🏁 Script executed:

#!/bin/bash
set -euo pipefail

f1="lib/pangea/world/world_locations_repo.dart"
f2="lib/pangea/world/world_activities_repo.dart"

echo "== Search for ErrorHandler/Sentry/logging inside the repo files =="
rg -n -S "ErrorHandler|Sentry\.|captureException|captureMessage|logError" "$f1" "$f2" || true

echo
echo "== mappableLocations() body (with nearby lines) =="
start=$(rg -n "static Future<.*>\s*mappableLocations\s*\(" "$f1" | head -n1 | cut -d: -f1)
# show a window (safe bounds)
win_pre=25; win_post=180
awk -v s="$((start-win_pre))" -v e="$((start+win_post))" '
  NR>=1 && NR>=s && NR<=e { printf "%5d:%s\n", NR, $0 }
' "$f1"

echo
echo "== activityPins() body (with nearby lines) =="
start2=$(rg -n "static Future<.*>\s*activityPins\s*\(" "$f2" | head -n1 | cut -d: -f1 || true)
if [ -z "${start2:-}" ]; then
  # fallback: match without "static"
  start2=$(rg -n "\bactivityPins\s*\(" "$f2" | head -n1 | cut -d: -f1)
fi
win_pre2=25; win_post2=240
awk -v s="$((start2-win_pre2))" -v e="$((start2+win_post2))" '
  NR>=1 && NR>=s && NR<=e { printf "%5d:%s\n", NR, $0 }
' "$f2"

Repository: pangeachat/client

Length of output: 7461


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Find ErrorHandler.logError definition/usages =="
rg -n "ErrorHandler\.logError" -S lib || true
rg -n "class\s+ErrorHandler|logError\s*\(" -S lib || true
rg -n -S "Sentry\." lib || true
rg -n -S "captureException|captureMessage|captureEvent" lib || true

echo
echo "== Inspect PayloadClient implementation (error handling / sentry) =="
payload_file=$(fd -e dart PayloadClient lib -a 2>/dev/null | head -n1 || true)
echo "PayloadClient file candidates: $(fd -e dart '*payload*client*.dart' lib -a 2>/dev/null | head -n5 | tr '\n' ' ')"
rg -n "class\s+PayloadClient" -S lib || true

# If we found a plausible file, show error-handling area around its request methods
rg -n "Future<.*find\s*\(|\bfind\s*\(" -S lib/pangea/payload_client lib || true

Repository: pangeachat/client

Length of output: 50373


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Inspect ErrorHandler implementation (to see if it logs to Sentry)
echo "== error_handler.dart =="
f="lib/pangea/common/utils/error_handler.dart"
rg -n "class\s+ErrorHandler|logError|Sentry|captureException|captureMessage" "$f" || true
start=$(rg -n "class\s+ErrorHandler" "$f" | head -n1 | cut -d: -f1)
# print a chunk covering logError + internals
end=$((start+140))
awk -v s="$start" -v e="$end" 'NR>=s && NR<=e {printf "%5d:%s\n", NR, $0}' "$f"

echo
# 2) Locate PayloadClient implementation(s)
echo "== Locate PayloadClient files =="
payload_files=$(fd -e dart -g '*payload*' lib | head -n50 || true)
echo "$payload_files" | head -n50

echo
echo "== Search PayloadClient for ErrorHandler/Sentry/logging =="
# Try to find the actual PayloadClient class file(s)
payload_class_files=$(rg -l "class\s+PayloadClient" lib -g'*.dart' || true)
echo "PayloadClient class files:"
echo "$payload_class_files"

for pf in $payload_class_files; do
  echo
  echo "== Inspect $pf =="
  rg -n "Future<.*\b(find|get)\b|ErrorHandler|Sentry\.|captureException|captureMessage" "$pf" || true
  # show likely request methods area if present
  loc_line=$(rg -n "\bFuture<.*\bfind\b|\bFuture<.*\bget\b|\bfind\s*\(" "$pf" | head -n1 | cut -d: -f1 || true)
  if [ -n "${loc_line:-}" ]; then
    s=$((loc_line-20)); [ "$s" -lt 1 ] && s=1
    e=$((loc_line+200))
    awk -v s="$s" -v e="$e" 'NR>=s && NR<=e {printf "%5d:%s\n", NR, $0}' "$pf"
  fi
done

Repository: pangeachat/client

Length of output: 9552


Add Sentry logging to world map location/activity load failures

  • WorldLocationsRepo.mappableLocations() and WorldActivitiesRepo.activityPins() contain no ErrorHandler.logError/Sentry calls and rely on PayloadClient which throws on HTTP errors without logging.
  • As a result, the catch (_) { ... } blocks in lib/pangea/world/widgets/world_map.dart (L46-62) swallow exceptions without reporting to Sentry; update them to log via ErrorHandler.logError (catch (e, s) and call it) in production.
🤖 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 `@lib/pangea/world/widgets/world_map.dart` around lines 46 - 62, Update the two
catch blocks in _loadLocations and _loadActivities to capture error and stack
(catch (e, s)) and call ErrorHandler.logError(e, s) so failures from
WorldLocationsRepo.mappableLocations() and WorldActivitiesRepo.activityPins()
are reported to Sentry; keep the existing behavior of not rethrowing (map stays
usable) and only add the ErrorHandler.logError call (optionally gated by
production if desired).

Comment on lines +134 to +151
child: GestureDetector(
onTap: () => _openActivity(activity),
child: Container(
decoration: BoxDecoration(
color: AppConfig.gold,
shape: BoxShape.circle,
border: Border.all(color: Colors.white, width: 2),
boxShadow: const [
BoxShadow(blurRadius: 4, color: Colors.black38),
],
),
child: const Icon(
Icons.star,
size: 20,
color: Colors.white,
),
),
),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

GestureDetector must be wrapped in Semantics for accessibility.

The activity marker's GestureDetector (L134-135) is used as a tappable button but lacks an accessible name for screen readers and Playwright. As per coding guidelines, GestureDetector and InkWell widgets used as buttons must be wrapped in Semantics(label: '...', button: true, child: ...).

♿ Proposed fix to wrap GestureDetector in Semantics
                  child: Tooltip(
                    message: '${activity.title}\n${activity.locationName}',
-                   child: GestureDetector(
-                     onTap: () => _openActivity(activity),
-                     child: Container(
+                   child: Semantics(
+                     label: 'Activity: ${activity.title} at ${activity.locationName}',
+                     button: true,
+                     child: GestureDetector(
+                       onTap: () => _openActivity(activity),
+                       child: Container(
                        decoration: BoxDecoration(
                          color: AppConfig.gold,
                          shape: BoxShape.circle,
                          border: Border.all(color: Colors.white, width: 2),
                          boxShadow: const [
                            BoxShadow(blurRadius: 4, color: Colors.black38),
                          ],
                        ),
                        child: const Icon(
                          Icons.star,
                          size: 20,
                          color: Colors.white,
                        ),
+                       ),
                      ),
                    ),
                  ),

As per coding guidelines: "GestureDetector and InkWell widgets used as buttons must be wrapped in Semantics(label: '...', button: true, child: ...) to surface an accessible name"

🤖 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 `@lib/pangea/world/widgets/world_map.dart` around lines 134 - 151, Wrap the
tappable activity marker's GestureDetector in a Semantics widget providing an
explicit accessible name and button role: replace the direct GestureDetector
child with Semantics(label: /* use activity title/name, e.g. activity.name or
activity.title */ , button: true, child: GestureDetector(...)); keep the
existing onTap handler (_openActivity(activity)) and visual decoration
unchanged, and ensure the label uses the activity's human-readable property
(fallback to a generic 'Activity' if null) so screen readers and Playwright can
identify the control.

Source: Coding guidelines

Comment on lines +47 to +141
static Future<List<WorldActivityPin>> activityPins() async {
if (_cache != null &&
_lastFetched != null &&
DateTime.now().difference(_lastFetched!) < _cacheDuration) {
return _cache!;
}

final l1 = MatrixState.pangeaController.userController.userL1Code ?? 'en';

// 1. All course plan ids (public CMS read).
final payload = PayloadClient(
baseUrl: Environment.cmsApi,
accessToken: MatrixState.pangeaController.userController.accessToken,
);
final planIdsResp = await payload.find(
'course-plans',
(json) => json['id'] as String,
limit: 100,
select: {'id': true},
);

// 2. Localized course plans -> topic ids.
final coursesResp = await CoursePlansRepo.search(
GetLocalizedCoursesRequest(coursePlanIds: planIdsResp.docs, l1: l1),
);
final planIdByTopicId = <String, String>{};
for (final entry in coursesResp.coursePlans.entries) {
for (final topicId in entry.value.topicIds) {
planIdByTopicId[topicId] = entry.key;
}
}
final topicIds = planIdByTopicId.keys.toList();
if (topicIds.isEmpty) return _setCache([]);

// 3. Topics -> activity ids + location ids.
final topicsResp = await CourseTopicRepo.get(
TranslateTopicRequest(topicIds: topicIds, l1: l1),
_worldBatchId,
);

// 4. Coordinates per location id (raw CMS read keeps coordinates).
final locations = await WorldLocationsRepo.mappableLocations();
final coordsById = {
for (final location in locations) location.id: location,
};

// 5. Activity details for topics that can be placed on the map.
final mappableTopics = topicsResp.topics.values.where(
(topic) => topic.locationIds.any(coordsById.containsKey),
);
final activityIds = mappableTopics
.expand((topic) => topic.activityIds)
.toSet()
.toList();
if (activityIds.isEmpty) return _setCache([]);
final activitiesResp = await CourseActivityRepo.get(
TranslateActivityRequest(activityIds: activityIds, l1: l1),
_worldBatchId,
);

// 6. Pins, fanned out around each topic's location.
const distance = Distance();
final pins = <WorldActivityPin>[];
for (final topic in mappableTopics) {
final coursePlanId = planIdByTopicId[topic.uuid];
if (coursePlanId == null) continue;
final location = topic.locationIds
.where(coordsById.containsKey)
.map((id) => coordsById[id]!)
.first;
final center = LatLng(
location.coordinates![1],
location.coordinates![0],
);
final placeable = topic.activityIds
.where(activitiesResp.plans.containsKey)
.toList();
for (var i = 0; i < placeable.length; i++) {
final plan = activitiesResp.plans[placeable[i]]!;
// Ring of ~25km around the location; single activities sit on it
// too so the location pin itself stays visible underneath.
final point = distance.offset(center, 25000, (360 / placeable.length) * i);
pins.add(
WorldActivityPin(
activityId: placeable[i],
title: plan.title,
locationName: location.name,
point: point,
coursePlanId: coursePlanId,
),
);
}
}
return _setCache(pins);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Add error logging to maintain observability.

The activityPins() method has no try-catch block and will propagate exceptions from repository calls (CoursePlansRepo.search, CourseTopicRepo.get, CourseActivityRepo.get, WorldLocationsRepo.mappableLocations) to the caller. As per coding guidelines, ensure all error handling includes logging to Sentry in production code to maintain error observability, especially for hotfixes. The caller (WorldMap._loadActivities) swallows errors without logging, relying on this repo to log them.

🛡️ Proposed fix to add Sentry error logging

Add import:

+import 'package:fluffychat/pangea/common/utils/error_handler.dart';

Wrap the method body (after cache check):

 static Future<List<WorldActivityPin>> activityPins() async {
   if (_cache != null &&
       _lastFetched != null &&
       DateTime.now().difference(_lastFetched!) < _cacheDuration) {
     return _cache!;
   }

+  try {
     final l1 = MatrixState.pangeaController.userController.userL1Code ?? 'en';

     // 1. All course plan ids (public CMS read).
     // ... rest of the method ...
     return _setCache(pins);
+  } catch (e, s) {
+    ErrorHandler.logError(e: e, s: s);
+    rethrow;
+  }
 }

As per coding guidelines: "Ensure all error handling includes logging to Sentry in production code to maintain error observability, especially for hotfixes"

📝 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.

Suggested change
static Future<List<WorldActivityPin>> activityPins() async {
if (_cache != null &&
_lastFetched != null &&
DateTime.now().difference(_lastFetched!) < _cacheDuration) {
return _cache!;
}
final l1 = MatrixState.pangeaController.userController.userL1Code ?? 'en';
// 1. All course plan ids (public CMS read).
final payload = PayloadClient(
baseUrl: Environment.cmsApi,
accessToken: MatrixState.pangeaController.userController.accessToken,
);
final planIdsResp = await payload.find(
'course-plans',
(json) => json['id'] as String,
limit: 100,
select: {'id': true},
);
// 2. Localized course plans -> topic ids.
final coursesResp = await CoursePlansRepo.search(
GetLocalizedCoursesRequest(coursePlanIds: planIdsResp.docs, l1: l1),
);
final planIdByTopicId = <String, String>{};
for (final entry in coursesResp.coursePlans.entries) {
for (final topicId in entry.value.topicIds) {
planIdByTopicId[topicId] = entry.key;
}
}
final topicIds = planIdByTopicId.keys.toList();
if (topicIds.isEmpty) return _setCache([]);
// 3. Topics -> activity ids + location ids.
final topicsResp = await CourseTopicRepo.get(
TranslateTopicRequest(topicIds: topicIds, l1: l1),
_worldBatchId,
);
// 4. Coordinates per location id (raw CMS read keeps coordinates).
final locations = await WorldLocationsRepo.mappableLocations();
final coordsById = {
for (final location in locations) location.id: location,
};
// 5. Activity details for topics that can be placed on the map.
final mappableTopics = topicsResp.topics.values.where(
(topic) => topic.locationIds.any(coordsById.containsKey),
);
final activityIds = mappableTopics
.expand((topic) => topic.activityIds)
.toSet()
.toList();
if (activityIds.isEmpty) return _setCache([]);
final activitiesResp = await CourseActivityRepo.get(
TranslateActivityRequest(activityIds: activityIds, l1: l1),
_worldBatchId,
);
// 6. Pins, fanned out around each topic's location.
const distance = Distance();
final pins = <WorldActivityPin>[];
for (final topic in mappableTopics) {
final coursePlanId = planIdByTopicId[topic.uuid];
if (coursePlanId == null) continue;
final location = topic.locationIds
.where(coordsById.containsKey)
.map((id) => coordsById[id]!)
.first;
final center = LatLng(
location.coordinates![1],
location.coordinates![0],
);
final placeable = topic.activityIds
.where(activitiesResp.plans.containsKey)
.toList();
for (var i = 0; i < placeable.length; i++) {
final plan = activitiesResp.plans[placeable[i]]!;
// Ring of ~25km around the location; single activities sit on it
// too so the location pin itself stays visible underneath.
final point = distance.offset(center, 25000, (360 / placeable.length) * i);
pins.add(
WorldActivityPin(
activityId: placeable[i],
title: plan.title,
locationName: location.name,
point: point,
coursePlanId: coursePlanId,
),
);
}
}
return _setCache(pins);
}
static Future<List<WorldActivityPin>> activityPins() async {
if (_cache != null &&
_lastFetched != null &&
DateTime.now().difference(_lastFetched!) < _cacheDuration) {
return _cache!;
}
try {
final l1 = MatrixState.pangeaController.userController.userL1Code ?? 'en';
// 1. All course plan ids (public CMS read).
final payload = PayloadClient(
baseUrl: Environment.cmsApi,
accessToken: MatrixState.pangeaController.userController.accessToken,
);
final planIdsResp = await payload.find(
'course-plans',
(json) => json['id'] as String,
limit: 100,
select: {'id': true},
);
// 2. Localized course plans -> topic ids.
final coursesResp = await CoursePlansRepo.search(
GetLocalizedCoursesRequest(coursePlanIds: planIdsResp.docs, l1: l1),
);
final planIdByTopicId = <String, String>{};
for (final entry in coursesResp.coursePlans.entries) {
for (final topicId in entry.value.topicIds) {
planIdByTopicId[topicId] = entry.key;
}
}
final topicIds = planIdByTopicId.keys.toList();
if (topicIds.isEmpty) return _setCache([]);
// 3. Topics -> activity ids + location ids.
final topicsResp = await CourseTopicRepo.get(
TranslateTopicRequest(topicIds: topicIds, l1: l1),
_worldBatchId,
);
// 4. Coordinates per location id (raw CMS read keeps coordinates).
final locations = await WorldLocationsRepo.mappableLocations();
final coordsById = {
for (final location in locations) location.id: location,
};
// 5. Activity details for topics that can be placed on the map.
final mappableTopics = topicsResp.topics.values.where(
(topic) => topic.locationIds.any(coordsById.containsKey),
);
final activityIds = mappableTopics
.expand((topic) => topic.activityIds)
.toSet()
.toList();
if (activityIds.isEmpty) return _setCache([]);
final activitiesResp = await CourseActivityRepo.get(
TranslateActivityRequest(activityIds: activityIds, l1: l1),
_worldBatchId,
);
// 6. Pins, fanned out around each topic's location.
const distance = Distance();
final pins = <WorldActivityPin>[];
for (final topic in mappableTopics) {
final coursePlanId = planIdByTopicId[topic.uuid];
if (coursePlanId == null) continue;
final location = topic.locationIds
.where(coordsById.containsKey)
.map((id) => coordsById[id]!)
.first;
final center = LatLng(
location.coordinates![1],
location.coordinates![0],
);
final placeable = topic.activityIds
.where(activitiesResp.plans.containsKey)
.toList();
for (var i = 0; i < placeable.length; i++) {
final plan = activitiesResp.plans[placeable[i]]!;
// Ring of ~25km around the location; single activities sit on it
// too so the location pin itself stays visible underneath.
final point = distance.offset(center, 25000, (360 / placeable.length) * i);
pins.add(
WorldActivityPin(
activityId: placeable[i],
title: plan.title,
locationName: location.name,
point: point,
coursePlanId: coursePlanId,
),
);
}
}
return _setCache(pins);
} catch (e, s) {
ErrorHandler.logError(e: e, s: s);
rethrow;
}
}
🤖 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 `@lib/pangea/world/world_activities_repo.dart` around lines 47 - 141, The
activityPins() method can throw from calls like CoursePlansRepo.search,
CourseTopicRepo.get, CourseActivityRepo.get, and
WorldLocationsRepo.mappableLocations and currently has no logging; wrap the main
work (everything after the cache validity check) in a try-catch, import and call
Sentry.captureException(error, stackTrace) (and optionally a local logger) in
the catch block to record the error, then rethrow the exception (or return
_setCache([]) if you prefer to swallow but still log); ensure you reference
activityPins, CoursePlansRepo.search, CourseTopicRepo.get,
CourseActivityRepo.get, WorldLocationsRepo.mappableLocations, and use
Sentry.captureException to maintain observability.

Source: Coding guidelines

Comment on lines +14 to +44
static Future<List<CmsCoursePlanTopicLocation>> mappableLocations() async {
if (_cache != null &&
_lastFetched != null &&
DateTime.now().difference(_lastFetched!) < _cacheDuration) {
return _cache!;
}

final payload = PayloadClient(
baseUrl: Environment.cmsApi,
accessToken: MatrixState.pangeaController.userController.accessToken,
);

final List<CmsCoursePlanTopicLocation> locations = [];
int page = 1;
bool hasNext = true;
while (hasNext && page <= 10) {
final resp = await payload.find(
CmsCoursePlanTopicLocation.slug,
CmsCoursePlanTopicLocation.fromJson,
page: page,
limit: 100,
);
locations.addAll(resp.docs);
hasNext = resp.hasNextPage;
page++;
}

_cache = locations.where(_hasUsableCoordinates).toList();
_lastFetched = DateTime.now();
return _cache!;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Add error logging to maintain observability.

The mappableLocations() method has no try-catch block and will propagate exceptions from PayloadClient.find to the caller. As per coding guidelines, ensure all error handling includes logging to Sentry in production code to maintain error observability, especially for hotfixes. The caller (WorldMap._loadLocations) swallows errors without logging, relying on this repo to log them.

🛡️ Proposed fix to add Sentry error logging

Add import:

+import 'package:fluffychat/pangea/common/utils/error_handler.dart';

Wrap the method body:

 static Future<List<CmsCoursePlanTopicLocation>> mappableLocations() async {
   if (_cache != null &&
       _lastFetched != null &&
       DateTime.now().difference(_lastFetched!) < _cacheDuration) {
     return _cache!;
   }

+  try {
     final payload = PayloadClient(
       baseUrl: Environment.cmsApi,
       accessToken: MatrixState.pangeaController.userController.accessToken,
     );

     final List<CmsCoursePlanTopicLocation> locations = [];
     int page = 1;
     bool hasNext = true;
     while (hasNext && page <= 10) {
       final resp = await payload.find(
         CmsCoursePlanTopicLocation.slug,
         CmsCoursePlanTopicLocation.fromJson,
         page: page,
         limit: 100,
       );
       locations.addAll(resp.docs);
       hasNext = resp.hasNextPage;
       page++;
     }

     _cache = locations.where(_hasUsableCoordinates).toList();
     _lastFetched = DateTime.now();
     return _cache!;
+  } catch (e, s) {
+    ErrorHandler.logError(e: e, s: s);
+    rethrow;
+  }
 }

As per coding guidelines: "Ensure all error handling includes logging to Sentry in production code to maintain error observability, especially for hotfixes"

🤖 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 `@lib/pangea/world/world_locations_repo.dart` around lines 14 - 44, Wrap the
body of mappableLocations() in a try-catch that catches all exceptions thrown by
PayloadClient.find (and any other calls inside), log the error to Sentry using
Sentry.captureException with the caught exception and stack trace, and
optionally emit a local log (e.g., processLogger.error) with context including
the function name and page info; after logging rethrow the exception (or return
an empty list if you prefer failing-safe) so callers keep current behavior.
Ensure you import Sentry and reference mappableLocations, PayloadClient.find,
_cache, _lastFetched, and _hasUsableCoordinates when updating the method.

Source: Coding guidelines

@ggurdin

ggurdin commented Jun 18, 2026

Copy link
Copy Markdown
Collaborator

Looks like this all got moved into #7028. Closing this.

@ggurdin ggurdin closed this Jun 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

World v2: map as the app canvas with first-class activity URLs

2 participants