Skip to content

TF-1302 Allow selecting emojis while drafting mail#4140

Open
dab246 wants to merge 11 commits intomasterfrom
enhancement/tf-1302-allow-selecting-emojis-while-drafting-mail
Open

TF-1302 Allow selecting emojis while drafting mail#4140
dab246 wants to merge 11 commits intomasterfrom
enhancement/tf-1302-allow-selecting-emojis-while-drafting-mail

Conversation

@dab246
Copy link
Member

@dab246 dab246 commented Nov 4, 2025

Issue

#1302

Dependency

Resolved

Screen.Recording.2025-11-04.at.10.20.40.AM.mov

Summary by CodeRabbit

  • New Features

    • Emoji picker added to the composer with animated overlay, recent-emoji tracking, insert-into-editor support, and persistence for recent reactions.
    • New emoji assets, emoji font fallback, and localization key for "Emoji".
    • New label text style and subtle color additions for emoji UI.
  • Chores

    • CI/CD workflows improved with SSH support, host verification, caching, prebuild steps, and refined artifact handling.
    • Some dependencies switched to SSH-based sources.
  • Refactor

    • Asset preloading consolidated into a centralized asset manager.

@github-actions
Copy link

github-actions bot commented Nov 4, 2025

This PR has been deployed to https://linagora.github.io/tmail-flutter/4140.

@hoangdat
Copy link
Member

@dab246 dab246 force-pushed the enhancement/tf-1302-allow-selecting-emojis-while-drafting-mail branch from 305013c to 97f26e5 Compare November 14, 2025 03:33
@dab246
Copy link
Member Author

dab246 commented Nov 14, 2025

Updated & rebase done

@dab246 dab246 requested a review from hoangdat November 14, 2025 03:33
@dab246 dab246 force-pushed the enhancement/tf-1302-allow-selecting-emojis-while-drafting-mail branch from 97f26e5 to ec0300b Compare November 17, 2025 03:49
@dab246 dab246 force-pushed the enhancement/tf-1302-allow-selecting-emojis-while-drafting-mail branch from ec0300b to 7f1034b Compare November 18, 2025 03:25
@tddang-linagora
Copy link
Collaborator

Screenshot 2025-11-20 at 1 52 08 PM

Emoji in the picker is different from the emoji in the composer when picked.

@dab246 dab246 force-pushed the enhancement/tf-1302-allow-selecting-emojis-while-drafting-mail branch from 7f1034b to 4bc0a0a Compare February 27, 2026 05:01
@dab246
Copy link
Member Author

dab246 commented Feb 27, 2026

Emoji in the picker is different from the emoji in the composer when picked.

Fixed

Screen.Recording.2026-02-27.at.11.59.37.mov

@coderabbitai
Copy link

coderabbitai bot commented Feb 27, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4a2a479 and cfd2545.

📒 Files selected for processing (1)
  • lib/features/base/widget/emoji/emoji_button.dart
🚧 Files skipped from review as they are similar to previous changes (1)
  • lib/features/base/widget/emoji/emoji_button.dart

Walkthrough

Adds emoji support and a reactions feature across the app and updates CI/build tooling. Introduces EmojiButton widget, emoji picker integration in web composer, RichTextWebController.insertEmoji, AssetManager and emoji data loading, new emoji font and localization key, and image/font resource entries. Implements reactions domain: datasource, cache manager, repository, interactors, states, extensions, and GetX bindings with composer-scoped registration. Multiple GitHub Actions/workflow, Dockerfile, and pubspec changes enable SSH-based private git deps, prebuild steps across modules, platform-specific setup, and artifact handling. Also adds small UI constants and image path getters.

Possibly related PRs

Suggested reviewers

  • tddang-linagora
  • hoangdat
🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'TF-1302 Allow selecting emojis while drafting mail' directly and accurately summarizes the main objective of this PR: adding emoji selection functionality to the mail composer during draft editing.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch enhancement/tf-1302-allow-selecting-emojis-while-drafting-mail

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
.github/workflows/image.yaml (1)

16-71: ⚠️ Potential issue | 🔴 Critical

Add missing repository checkout step and tag event trigger.

Both the build-dev-image and build-release-image jobs are missing the actions/checkout@v4 step. The docker/build-push-action@v5 requires access to the Dockerfile and source code, but without checkout, the runner's working directory is empty and the build will fail. This pattern is consistent across all other workflows in the repository.

Additionally, the build-release-image job will never trigger because the workflow only listens to push: branches: - "master". To enable the release job on version tags, add push: tags: to the trigger configuration.

Proposed fixes
  1. Add checkout as the first step in both jobs:
    steps:
+     - name: Checkout repository
+       uses: actions/checkout@v4
+
      - name: Set up SSH for private Git dependencies
        uses: webfactory/ssh-agent@v0.9.0
  1. Update the workflow trigger to include tag events:
on:
  push:
    branches:
      - "master"
+   tags:
+     - "v*"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/image.yaml around lines 16 - 71, The jobs build-dev-image
and build-release-image are missing the repository checkout so
docker/build-push-action@v5 has no Dockerfile/source to build; add an
actions/checkout@v4 step as the first step in each job (before Set up SSH and
Build and push image). Also update the workflow trigger to include push: tags:
(so build-release-image can run on version tags) alongside the existing push:
branches: - "master".
🧹 Nitpick comments (2)
.github/workflows/build.yaml (1)

103-110: Consider separating artifact paths by platform.

Both APK and IPA paths are listed, but only one will exist per matrix run. While upload-artifact@v4 handles missing files gracefully (it won't fail), you might see warnings in the logs. This is a minor observation and the current approach works.

💡 Optional: Platform-specific artifact paths
       - name: Upload artifacts
         uses: actions/upload-artifact@v4
         with:
           name: tmail-dev-${{ matrix.os }}
           path: |
-            build/app/outputs/flutter-apk/app-debug.apk
-            ios/Runner.ipa
+            ${{ matrix.os == 'android' && 'build/app/outputs/flutter-apk/app-debug.apk' || 'ios/Runner.ipa' }}

Or use conditional steps:

      - name: Upload Android artifact
        if: matrix.os == 'android'
        uses: actions/upload-artifact@v4
        with:
          name: tmail-dev-android
          path: build/app/outputs/flutter-apk/app-debug.apk

      - name: Upload iOS artifact
        if: matrix.os == 'ios'
        uses: actions/upload-artifact@v4
        with:
          name: tmail-dev-ios
          path: ios/Runner.ipa
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/build.yaml around lines 103 - 110, The current "Upload
artifacts" step lists both Android and iOS paths and can emit warnings when one
is missing; split this into two conditional upload steps: replace the single
"Upload artifacts" step with "Upload Android artifact" that runs only when
matrix.os == 'android' and uploads build/app/outputs/flutter-apk/app-debug.apk,
and "Upload iOS artifact" that runs only when matrix.os == 'ios' and uploads
ios/Runner.ipa, giving each a platform-specific artifact name (e.g.,
tmail-dev-android, tmail-dev-ios) so uploads only reference existing files and
avoid warnings.
lib/features/composer/presentation/extensions/handle_insert_emoji_to_editor_extension.dart (1)

24-35: Consider using Future<void> return type for async method.

The storeRecentReactions method is declared as async but returns void. While this works, it's a Dart best practice to return Future<void> for async methods to allow callers to await the operation if needed and to properly propagate any errors.

♻️ Suggested change
-  void storeRecentReactions(String emoji) async {
+  Future<void> storeRecentReactions(String emoji) async {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@lib/features/composer/presentation/extensions/handle_insert_emoji_to_editor_extension.dart`
around lines 24 - 35, The method storeRecentReactions is declared async but
returns void; change its signature to Future<void> storeRecentReactions(String
emoji) so callers can await it and errors propagate; update any
overridden/implemented declarations or typedefs that reference
storeRecentReactions, and keep the existing body that uses
AssetManager().emojiData, getBinding<StoreRecentReactionsInteractor>(tag:
composerId), and consumeState(interactor.execute(...)) unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/workflows/analyze-test.yaml:
- Around line 37-40: Replace mutable action tags with immutable commit SHAs:
locate usages of webfactory/ssh-agent@v0.9.0 and the other action referenced as
`@v1` in the workflow and update each to the corresponding full 40-character
commit SHA for that action (e.g., webfactory/ssh-agent@<40-char-sha>); ensure
you use the commit SHA from the action's repository (not a fork) and update both
occurrences so the CI uses pinned, immutable references.

In `@lib/features/base/widget/emoji/emoji_button.dart`:
- Around line 45-47: The fixed _dialogWidth causes the picker to render
off-screen on narrow viewports; replace usages of the static 400 width by
computing a responsive width at build time (e.g., clamp the width to
Math.min(MediaQuery.of(context).size.width - horizontalMargin, _dialogWidth) or
use LayoutBuilder constraints) and ensure computed start/left positions are
clamped to >= 0 so the picker never has a negative start; update all places
referencing _dialogWidth (including the build/positioning logic where start is
computed and the other occurrences around the blocks referenced) to use the
responsive computedWidth and keep _dialogHeight unchanged unless you also want
it responsive.

In `@lib/features/reactions/domain/extensions/list_reactions_extesion.dart`:
- Around line 1-16: Rename the file to fix the filename typo from
list_reactions_extesion.dart to list_reactions_extension.dart so imports and
searches match the extension declaration; update any import sites that reference
the old filename accordingly. The extension declaration (ListReactionsExtension)
and its member combineRecentReactions and constant maxRecentReactionsSize remain
unchanged—only the filename and any import paths should be updated to the
corrected spelling.

In `@lib/main/utils/asset_manager.dart`:
- Around line 44-47: The early return in loadEmojiData ignores the incoming
version once _isEmojiDataLoaded is true, causing stale emoji data to be reused;
update loadEmojiData to track the loaded emoji version (e.g., add a private
_emojiDataVersion) and compare it against the incoming version, and only return
early when _isEmojiDataLoaded is true AND the versions match—otherwise clear or
replace _emojiData and proceed to load the requested version so calls with a
different version reload correctly.

---

Outside diff comments:
In @.github/workflows/image.yaml:
- Around line 16-71: The jobs build-dev-image and build-release-image are
missing the repository checkout so docker/build-push-action@v5 has no
Dockerfile/source to build; add an actions/checkout@v4 step as the first step in
each job (before Set up SSH and Build and push image). Also update the workflow
trigger to include push: tags: (so build-release-image can run on version tags)
alongside the existing push: branches: - "master".

---

Nitpick comments:
In @.github/workflows/build.yaml:
- Around line 103-110: The current "Upload artifacts" step lists both Android
and iOS paths and can emit warnings when one is missing; split this into two
conditional upload steps: replace the single "Upload artifacts" step with
"Upload Android artifact" that runs only when matrix.os == 'android' and uploads
build/app/outputs/flutter-apk/app-debug.apk, and "Upload iOS artifact" that runs
only when matrix.os == 'ios' and uploads ios/Runner.ipa, giving each a
platform-specific artifact name (e.g., tmail-dev-android, tmail-dev-ios) so
uploads only reference existing files and avoid warnings.

In
`@lib/features/composer/presentation/extensions/handle_insert_emoji_to_editor_extension.dart`:
- Around line 24-35: The method storeRecentReactions is declared async but
returns void; change its signature to Future<void> storeRecentReactions(String
emoji) so callers can await it and errors propagate; update any
overridden/implemented declarations or typedefs that reference
storeRecentReactions, and keep the existing body that uses
AssetManager().emojiData, getBinding<StoreRecentReactionsInteractor>(tag:
composerId), and consumeState(interactor.execute(...)) unchanged.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ffb0035 and 4bc0a0a.

⛔ Files ignored due to path filters (5)
  • assets/fonts/fallback/NotoColorEmoji-Regular.ttf is excluded by !**/*.ttf
  • assets/images/ic_emoji.svg is excluded by !**/*.svg
  • assets/images/ic_search_emoji_empty.svg is excluded by !**/*.svg
  • cozy/pubspec.lock is excluded by !**/*.lock
  • pubspec.lock is excluded by !**/*.lock
📒 Files selected for processing (37)
  • .github/workflows/analyze-test.yaml
  • .github/workflows/build.yaml
  • .github/workflows/gh-pages.yaml
  • .github/workflows/image.yaml
  • .github/workflows/patrol-integration-test.yaml
  • .github/workflows/release.yaml
  • Dockerfile
  • core/lib/presentation/constants/constants_ui.dart
  • core/lib/presentation/extensions/color_extension.dart
  • core/lib/presentation/resources/image_paths.dart
  • core/lib/presentation/utils/theme_utils.dart
  • core/lib/utils/html/html_template.dart
  • cozy/pubspec.yaml
  • lib/features/base/widget/emoji/emoji_button.dart
  • lib/features/composer/presentation/composer_bindings.dart
  • lib/features/composer/presentation/composer_view_web.dart
  • lib/features/composer/presentation/controller/rich_text_web_controller.dart
  • lib/features/composer/presentation/extensions/handle_insert_emoji_to_editor_extension.dart
  • lib/features/composer/presentation/widgets/web/bottom_bar_composer_widget.dart
  • lib/features/reactions/data/datasource/reactions_datasource.dart
  • lib/features/reactions/data/datasource_impl/reactions_datasource_impl.dart
  • lib/features/reactions/data/local/reaction_cache_manager.dart
  • lib/features/reactions/data/repository/reactions_repository_impl.dart
  • lib/features/reactions/domain/extensions/list_reactions_extesion.dart
  • lib/features/reactions/domain/repository/reactions_repository.dart
  • lib/features/reactions/domain/state/get_recent_reactions_state.dart
  • lib/features/reactions/domain/state/store_recent_reactions_state.dart
  • lib/features/reactions/domain/usecase/get_recent_reactions_interactor.dart
  • lib/features/reactions/domain/usecase/store_recent_reactions_interactor.dart
  • lib/features/reactions/presentation/reactions_interactor_bindings.dart
  • lib/l10n/intl_messages.arb
  • lib/main/bindings/local/local_bindings.dart
  • lib/main/localizations/app_localizations.dart
  • lib/main/main_entry.dart
  • lib/main/utils/asset_manager.dart
  • lib/main/utils/asset_preloader.dart
  • pubspec.yaml
💤 Files with no reviewable changes (1)
  • lib/main/utils/asset_preloader.dart

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
.github/workflows/analyze-test.yaml (1)

2-3: ⚠️ Potential issue | 🟠 Major

Add job condition to skip secret-dependent setup on fork PRs.

This workflow runs on pull_request and unconditionally requires SSH_KEY (line 40) and FIREBASE_ENV (line 61) secrets. Forked PRs won't have access to these secrets, causing the SSH setup and Firebase configuration steps to fail.

Guard the job to run only on same-repository PRs:

Add job condition
jobs:
  analyze-test:
+   if: github.event.pull_request.head.repo.full_name == github.repository
    runs-on: ubuntu-latest

Also applies to: lines 36-40, 58-62

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/analyze-test.yaml around lines 2 - 3, The workflow
unconditionally uses secrets SSH_KEY and FIREBASE_ENV which fail on forked PRs;
add a job-level condition to each job that performs SSH setup (references
SSH_KEY) and Firebase setup (references FIREBASE_ENV) so they only run for
same-repository pull requests by checking the PR head repo equals the base repo
(e.g., compare github.event.pull_request.head.repo.full_name to
github.repository) and apply this condition to the jobs/steps that reference
SSH_KEY and FIREBASE_ENV.
🧹 Nitpick comments (5)
lib/features/base/widget/emoji/emoji_button.dart (1)

83-85: Call onPickerOpen only after the picker is actually inserted.

Right now, the callback fires even when preconditions fail and no dialog appears.

💡 Proposed fix
 Future<void> _openDialog() async {
-  widget.onPickerOpen();
-
   if (!mounted || _isDialogVisible) return;
@@
   final overlay = Overlay.maybeOf(context, rootOverlay: true);
   if (!mounted || overlay == null) return;
   overlay.insert(_overlayEntry!);
+  widget.onPickerOpen();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/features/base/widget/emoji/emoji_button.dart` around lines 83 - 85, The
callback widget.onPickerOpen is being invoked before dialog preconditions are
verified and before the picker is actually inserted; change _openDialog so you
only call widget.onPickerOpen after you have successfully invoked the picker
presentation (e.g. after calling showDialog/showModalBottomSheet or
Navigator.push that inserts the route) and only when those preconditions pass —
move the call from the top of _openDialog to immediately after the
showDialog/showModalBottomSheet invocation (but before awaiting its returned
Future) so it runs only when the picker is actually shown.
lib/features/reactions/domain/usecase/store_recent_reactions_interactor.dart (1)

19-20: Avoid the redundant list allocation before combining reactions.

combineRecentReactions already creates a mutable copy, so List<String>.from(reactions) is unnecessary here.

♻️ Proposed cleanup
-      final updatedReactions =
-          List<String>.from(reactions).combineRecentReactions(emojiId);
+      final updatedReactions = reactions.combineRecentReactions(emojiId);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/features/reactions/domain/usecase/store_recent_reactions_interactor.dart`
around lines 19 - 20, The code creates an unnecessary copy with
List<String>.from(reactions) before calling combineRecentReactions; remove the
redundant allocation and call reactions.combineRecentReactions(emojiId) directly
(ensure the variable passed to combineRecentReactions is already a List<String>
or compatible type). Update the assignment in the StoreRecentReactionsInteractor
(where updatedReactions is computed) to use
reactions.combineRecentReactions(emojiId) since combineRecentReactions itself
makes the mutable copy.
.github/workflows/build.yaml (1)

103-110: Split artifact upload by platform to avoid noisy missing-file cases.

The current matrix upload includes both Android and iOS paths in every run. This weakens failure clarity when artifacts are actually missing.

Suggested refactor
-      - name: Upload artifacts
-        uses: actions/upload-artifact@v4
-        with:
-          name: tmail-dev-${{ matrix.os }}
-          path: |
-            build/app/outputs/flutter-apk/app-debug.apk
-            ios/Runner.ipa
+      - name: Upload Android artifact
+        if: matrix.os == 'android'
+        uses: actions/upload-artifact@v4
+        with:
+          name: tmail-dev-android
+          path: build/app/outputs/flutter-apk/app-debug.apk
+          if-no-files-found: error
+
+      - name: Upload iOS artifact
+        if: matrix.os == 'ios'
+        uses: actions/upload-artifact@v4
+        with:
+          name: tmail-dev-ios
+          path: ios/Runner.ipa
+          if-no-files-found: error
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/build.yaml around lines 103 - 110, The "Upload artifacts"
step currently uploads both Android and iOS paths for every matrix.os, causing
noisy missing-file failures; update the actions/upload-artifact step (the step
named "Upload artifacts") to split into two conditional steps that run based on
matrix.os (e.g., one with if: matrix.os == 'ubuntu-latest' or similar that
uploads build/app/outputs/flutter-apk/app-debug.apk and names the artifact using
matrix.os, and another with if: matrix.os == 'macos' that uploads ios/Runner.ipa
and names it accordingly), so each runner only tries to upload the
platform-specific artifact.
.github/workflows/image.yaml (1)

1-4: Release job is unreachable with the current workflow trigger.

This workflow only triggers on master branch pushes, but build-release-image is tag-gated. If release builds are intended here, add tag triggers (or remove the dead job).

Suggested fix (if release job should run in this workflow)
 on:
   push:
     branches:
       - "master"
+    tags:
+      - "v*.*.*"

Also applies to: 76-80

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/image.yaml around lines 1 - 4, The workflow is only
triggered for pushes to branch "master" while the job build-release-image is
conditioned on tags and thus never runs; update the workflow trigger under the
on: section to include tag pushes (e.g., add a push: tags: entry for your
release tag pattern) or remove/disable the build-release-image job if releases
are not intended in this workflow; locate the on: block and the job named
build-release-image and either add the tag trigger to on: or remove the
tag-gated job so the workflow and job logic are consistent.
.github/workflows/gh-pages.yaml (1)

11-13: Consider canceling stale in-progress PR deployments.

For PR environments, setting cancel-in-progress: true usually improves turnaround and reduces wasted runner time.

Suggested update
 concurrency:
   group: ${{ github.workflow }}-${{ github.ref }}
+  cancel-in-progress: true
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/gh-pages.yaml around lines 11 - 13, Add the
cancel-in-progress flag under the existing concurrency block so GitHub cancels
stale in-progress runs for the same group; specifically update the concurrency
settings (the concurrency: block and its group: expression `${{ github.workflow
}}-${{ github.ref }}`) to include `cancel-in-progress: true` to enable automatic
cancellation of older PR deployment jobs.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/workflows/image-sentry.yaml:
- Around line 27-38: The Docker build step using docker/build-push-action@v5
must forward the SSH agent so RUN --mount=type=ssh in the Dockerfile can access
private deps; edit the docker/build-push-action@v5 steps (the
"docker/build-push-action@v5" uses in the workflow) and add the parameter ssh:
default under the step (e.g., ensure the step block that uses
"docker/build-push-action@v5" includes a top-level "with:" entry or top-level
"ssh: default" per action syntax), and apply the same addition to the second
occurrence of that action (the other docker/build-push-action@v5 step around
lines 72-93) so both builds forward the configured SSH socket.

In `@lib/features/base/widget/emoji/emoji_button.dart`:
- Around line 64-73: The _loadRecentEmoji method currently invokes
widget.onRecentEmojiSelected which can throw and break the picker open flow;
update _loadRecentEmoji to call widget.onRecentEmojiSelected inside a try/catch,
catching any exceptions and setting _recentEmoji = Future.value(null) on error
(and optionally logging the exception) so failures in onRecentEmojiSelected are
guarded and do not propagate.
- Around line 116-227: After awaiting _loadRecentEmoji(), the code must re-check
widget lifecycle to avoid operating on a disposed state; add an immediate guard
like if (!mounted) return; before constructing/assigning _overlayEntry and
before calling Overlay.maybeOf(...).insert(...),
_animationController.forward(...), and setState(() => _isDialogVisible = true);
ensure all uses of _overlayEntry, _animationController, and setState are only
executed when mounted to prevent animation/overlay calls on a disposed widget.

In
`@lib/features/composer/presentation/extensions/handle_insert_emoji_to_editor_extension.dart`:
- Around line 24-27: In storeRecentReactions, ensure emoji data is loaded before
calling AssetManager().emojiData.getIdByEmoji: if AssetManager().emojiData is
null, call and await the AssetManager.loadEmojiData() (or equivalent
initializer) and then re-check emojiData; if loading still fails, log an error
and return early so you don't silently skip or NPE. Update the method
(storeRecentReactions) to await the loader, re-resolve emojiId, and include a
clear log message when emoji data cannot be loaded.

In `@lib/features/reactions/domain/extensions/list_reactions_extension.dart`:
- Around line 7-8: The code currently calls result.remove(emojiId) which only
removes the first match, leaving duplicates; replace that call with a removal of
all occurrences (e.g., result.removeWhere((e) => e == emojiId)) before
prepending with result.insert(0, emojiId) so the list has a single leading
instance of emojiId; update the List extension in list_reactions_extension.dart
to use result.removeWhere instead of result.remove.

---

Outside diff comments:
In @.github/workflows/analyze-test.yaml:
- Around line 2-3: The workflow unconditionally uses secrets SSH_KEY and
FIREBASE_ENV which fail on forked PRs; add a job-level condition to each job
that performs SSH setup (references SSH_KEY) and Firebase setup (references
FIREBASE_ENV) so they only run for same-repository pull requests by checking the
PR head repo equals the base repo (e.g., compare
github.event.pull_request.head.repo.full_name to github.repository) and apply
this condition to the jobs/steps that reference SSH_KEY and FIREBASE_ENV.

---

Nitpick comments:
In @.github/workflows/build.yaml:
- Around line 103-110: The "Upload artifacts" step currently uploads both
Android and iOS paths for every matrix.os, causing noisy missing-file failures;
update the actions/upload-artifact step (the step named "Upload artifacts") to
split into two conditional steps that run based on matrix.os (e.g., one with if:
matrix.os == 'ubuntu-latest' or similar that uploads
build/app/outputs/flutter-apk/app-debug.apk and names the artifact using
matrix.os, and another with if: matrix.os == 'macos' that uploads ios/Runner.ipa
and names it accordingly), so each runner only tries to upload the
platform-specific artifact.

In @.github/workflows/gh-pages.yaml:
- Around line 11-13: Add the cancel-in-progress flag under the existing
concurrency block so GitHub cancels stale in-progress runs for the same group;
specifically update the concurrency settings (the concurrency: block and its
group: expression `${{ github.workflow }}-${{ github.ref }}`) to include
`cancel-in-progress: true` to enable automatic cancellation of older PR
deployment jobs.

In @.github/workflows/image.yaml:
- Around line 1-4: The workflow is only triggered for pushes to branch "master"
while the job build-release-image is conditioned on tags and thus never runs;
update the workflow trigger under the on: section to include tag pushes (e.g.,
add a push: tags: entry for your release tag pattern) or remove/disable the
build-release-image job if releases are not intended in this workflow; locate
the on: block and the job named build-release-image and either add the tag
trigger to on: or remove the tag-gated job so the workflow and job logic are
consistent.

In `@lib/features/base/widget/emoji/emoji_button.dart`:
- Around line 83-85: The callback widget.onPickerOpen is being invoked before
dialog preconditions are verified and before the picker is actually inserted;
change _openDialog so you only call widget.onPickerOpen after you have
successfully invoked the picker presentation (e.g. after calling
showDialog/showModalBottomSheet or Navigator.push that inserts the route) and
only when those preconditions pass — move the call from the top of _openDialog
to immediately after the showDialog/showModalBottomSheet invocation (but before
awaiting its returned Future) so it runs only when the picker is actually shown.

In
`@lib/features/reactions/domain/usecase/store_recent_reactions_interactor.dart`:
- Around line 19-20: The code creates an unnecessary copy with
List<String>.from(reactions) before calling combineRecentReactions; remove the
redundant allocation and call reactions.combineRecentReactions(emojiId) directly
(ensure the variable passed to combineRecentReactions is already a List<String>
or compatible type). Update the assignment in the StoreRecentReactionsInteractor
(where updatedReactions is computed) to use
reactions.combineRecentReactions(emojiId) since combineRecentReactions itself
makes the mutable copy.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4bc0a0a and cf2c98f.

📒 Files selected for processing (12)
  • .github/workflows/analyze-test.yaml
  • .github/workflows/build.yaml
  • .github/workflows/gh-pages.yaml
  • .github/workflows/image-sentry.yaml
  • .github/workflows/image.yaml
  • .github/workflows/patrol-integration-test.yaml
  • .github/workflows/release.yaml
  • lib/features/base/widget/emoji/emoji_button.dart
  • lib/features/composer/presentation/extensions/handle_insert_emoji_to_editor_extension.dart
  • lib/features/reactions/domain/extensions/list_reactions_extension.dart
  • lib/features/reactions/domain/usecase/store_recent_reactions_interactor.dart
  • lib/main/utils/asset_manager.dart

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🧹 Nitpick comments (1)
.github/workflows/gh-pages.yaml (1)

67-69: Consider the build time impact of full pub cache clean.

flutter pub cache clean removes all cached packages, requiring a full re-download of every dependency on each PR build. While this ensures no stale SSH clones persist, it may significantly increase build times.

If the goal is specifically to clear stale SSH-based dependencies, consider a more targeted approach or document that this trade-off is intentional.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/gh-pages.yaml around lines 67 - 69, The workflow step
named "Clean pub cache" currently runs the expensive global command `flutter pub
cache clean`; replace this with a targeted cleanup (e.g., remove only the
git/SSH clone cache under the pub cache or specific problematic repo directories
in .pub-cache/git) or gate the full clean behind an explicit env flag so PR
builds don't always re-download all deps; update the step (referencing the
"Clean pub cache" step and the `flutter pub cache clean` invocation) to either
perform `rm -rf $FLUTTER_HOME/.pub-cache/git/<problem-repo>`-style removals or
check an env var (e.g., CLEAN_PUB_CACHE=true) and document the trade-off in the
workflow comment.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/workflows/analyze-test.yaml:
- Around line 79-82: The "Upload test reports" step currently uses a conditional
of "success() || failure()" which won't run on cancellations; update the step's
if condition to "always()" so the artifacts are uploaded in all outcomes. Locate
the step with name "Upload test reports" and uses "actions/upload-artifact@v4"
and replace the conditional expression accordingly.
- Around line 14-17: Add an explicit permissions block for the analyze-test job
to enforce least-privilege GITHUB_TOKEN scopes: inside the job named
analyze-test, add a permissions mapping that grants only the minimal scopes
required (e.g., set GITHUB_TOKEN permissions such as contents: read and checks:
write or read as needed) and avoid leaving permissions unspecified; expand the
permissions only if a specific step later requires additional scopes.

In `@lib/features/base/widget/emoji/emoji_button.dart`:
- Around line 94-100: Clamp computed available and dialog dimensions to
non-negative values: ensure availableHeight = max(0, screenSize.height - 32) and
availableWidth = max(0, screenSize.width - 16) (or return early if either is <=
0), then compute dialogHeight = min(_dialogHeight, availableHeight) and
dialogWidth = min(_dialogWidth, availableWidth) so neither dialogHeight nor
dialogWidth can be negative; also ensure start (computed from buttonPosition,
buttonSize and dialogWidth) is adjusted after clamping (and apply the same
non-negative clamping/early-return fix for the other occurrence around lines
referencing dialogHeight/dialogWidth at 136-137).
- Around line 224-230: Wrap the widget.onPickerOpen() call in a try/catch so any
thrown error doesn't leave the overlay inserted but _isDialogVisible false;
after inserting _overlayEntry (overlay.insert(_overlayEntry!)) ensure either
_isDialogVisible is set to true in a finally block or update _closeDialog() to
guard on _overlayEntry != null instead of relying solely on _isDialogVisible.
Specifically, in the show/open flow around overlay.insert(_overlayEntry!),
widget.onPickerOpen(), and await _animationController.forward(from: 0) handle
exceptions from widget.onPickerOpen() (and similarly in the second block at
lines 233-239) and make close logic check _overlayEntry presence (or set
_isDialogVisible true before calling widget.onPickerOpen()) so the overlay can
always be closed even if the callback fails.

In
`@lib/features/composer/presentation/extensions/handle_insert_emoji_to_editor_extension.dart`:
- Around line 11-15: insertEmojiToEditor currently calls storeRecentReactions as
a silent fire‑and‑forget which can throw; modify storeRecentReactions to
internally catch and log any exceptions around its awaited work (wrap its
body/awaits in try/catch and emit a clear log via your logger), and make the
fire‑and‑forget in insertEmojiToEditor explicit (e.g., call it with your
project’s unawaited helper or add a clear comment) so failures are handled
inside storeRecentReactions and not dropped.

In
`@lib/features/reactions/domain/usecase/store_recent_reactions_interactor.dart`:
- Around line 17-22: The read→modify→write in StoreRecentReactionsInteractor
(calling _repository.getRecentReactions, combineRecentReactions(emojiId), then
_repository.storeRecentReactions) is racy; change it to an atomic operation in
the repository (e.g., add a method like appendRecentReaction/appendById that
takes emojiId and performs the read+merge+write under a single DB/transactional
operation) or protect the block with a per-composer mutex/queue helper (call it
from the interactor) so concurrent invocations cannot overwrite each other;
update the interactor to call the new atomic repository method (or
acquire/release the mutex around the existing sequence) and remove the
non-atomic get/transform/store sequence.

---

Nitpick comments:
In @.github/workflows/gh-pages.yaml:
- Around line 67-69: The workflow step named "Clean pub cache" currently runs
the expensive global command `flutter pub cache clean`; replace this with a
targeted cleanup (e.g., remove only the git/SSH clone cache under the pub cache
or specific problematic repo directories in .pub-cache/git) or gate the full
clean behind an explicit env flag so PR builds don't always re-download all
deps; update the step (referencing the "Clean pub cache" step and the `flutter
pub cache clean` invocation) to either perform `rm -rf
$FLUTTER_HOME/.pub-cache/git/<problem-repo>`-style removals or check an env var
(e.g., CLEAN_PUB_CACHE=true) and document the trade-off in the workflow comment.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cf2c98f and cf7008a.

📒 Files selected for processing (7)
  • .github/workflows/analyze-test.yaml
  • .github/workflows/gh-pages.yaml
  • .github/workflows/image-sentry.yaml
  • lib/features/base/widget/emoji/emoji_button.dart
  • lib/features/composer/presentation/extensions/handle_insert_emoji_to_editor_extension.dart
  • lib/features/reactions/domain/extensions/list_reactions_extension.dart
  • lib/features/reactions/domain/usecase/store_recent_reactions_interactor.dart
🚧 Files skipped from review as they are similar to previous changes (1)
  • lib/features/reactions/domain/extensions/list_reactions_extension.dart

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
lib/features/base/widget/emoji/emoji_button.dart (1)

94-97: ⚠️ Potential issue | 🟡 Minor

Clamp computed dialog dimensions to non-negative bounds.

On extremely small viewports, screenSize - margin can go negative, and that can propagate invalid width/height into the dialog container.

💡 Proposed fix
-    final double availableHeight = screenSize.height - 32;
+    final double availableHeight = math.max(0.0, screenSize.height - 32);
     final double dialogHeight = math.min(_dialogHeight, availableHeight);
-    final availableWidth = screenSize.width - 16;
+    final double availableWidth = math.max(0.0, screenSize.width - 16);
     final dialogWidth = math.min(_dialogWidth, availableWidth);
+    if (dialogWidth == 0 || dialogHeight == 0) return;

Also applies to: 134-135

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/features/base/widget/emoji/emoji_button.dart` around lines 94 - 97, Clamp
the computed sizes to non-negative values: ensure availableHeight and
availableWidth are max(screenSize.height - 32, 0) and max(screenSize.width - 16,
0), then compute dialogHeight = math.min(_dialogHeight, availableHeight) and
dialogWidth = math.min(_dialogWidth, availableWidth) (or clamp
dialogHeight/dialogWidth again to >= 0). Update the same logic at the other
occurrence (lines referencing _dialogHeight/_dialogWidth and
availableWidth/availableHeight around 134-135) so no negative dimensions are
passed into the dialog container.
🧹 Nitpick comments (2)
.github/workflows/analyze-test.yaml (2)

46-50: Consider hardcoding GitHub's SSH host key instead of runtime keyscan.

Using ssh-keyscan at runtime is susceptible to MITM attacks (though unlikely in GitHub Actions). For stronger supply-chain guarantees, hardcode GitHub's published host keys:

🛡️ Optional hardening
       - name: Add GitHub to known hosts
         run: |
           mkdir -p ~/.ssh
-          ssh-keyscan github.com >> ~/.ssh/known_hosts
+          # Hardcoded GitHub RSA host key (https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/githubs-ssh-key-fingerprints)
+          echo "github.com ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCj7ndNxQowgcQnjshcLrqPEiiphnt+VTTvDP6mHBL9j1aNUkY4Ue1gvwnGLVlOhGeYrnZaMgRK6+PKCUXaDbC7qtbW8gIkhL7aGCsOr/C56SJMy/BCZfxd1nWzAOxSDPgVsmerOBYfNqltV9/hWCqBywINIR+5dIg6JTJ72pcEpEjcYgXkE2YEFXV1JHnsKgbLWNlhScqb2UmyRkQyytRLtL+38TGxkxCflmO+5Z8CSSNY7GidjMIZ7Q4zMjA2n1nGrlTDkzwDCsw+wqFPGQA179cnfGWOWRVruj16z6XyvxvjJwbz0wQZ75XK5tKSb7FNyeIEs4TT4jk+S4dhPeAUC5y+bDYirYgM4GC7uEnztnZyaVWQ7B381AK4Qdrwt51ZqExKbQpTUNn+EjqoTwvqNj4kqx5QUCI0ThS/YkOxJCXmPUWZbhjpCg56i+2aB6CmK2JGhn57K5mj0MNdBXA4/WnwH6XoPWJzK5Nyu2zB3nAZp+S5hpQs+p1vN1/wsjk=" >> ~/.ssh/known_hosts
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/analyze-test.yaml around lines 46 - 50, Replace the
runtime ssh-keyscan step (the "Add GitHub to known hosts" step) with writing
GitHub's published SSH host key(s) directly into ~/.ssh/known_hosts: remove the
ssh-keyscan invocation and instead append the official hardcoded host key
strings (e.g., GitHub's rsa and ecdsa host keys) into the file, ensure ~/.ssh
exists and has correct permissions before writing, and update the step comment
to indicate keys are pinned to GitHub's published values; keep the step name
"Add GitHub to known hosts" so it's easy to find.

54-54: Pin subosito/flutter-action to a commit SHA for consistency.

This third-party action uses a mutable tag (@v2) while other third-party actions in this workflow (webfactory/ssh-agent, zgosalvez/github-actions-analyze-dart) are pinned to immutable commit SHAs. For consistent supply-chain security, pin this action to commit fd55f4c5af5b953cc57a2be44cb082c8f6635e8e.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/analyze-test.yaml at line 54, Replace the mutable tag used
for the GitHub Action reference "subosito/flutter-action@v2" with the provided
immutable commit SHA to ensure consistency and supply-chain security; update the
workflow line referencing subosito/flutter-action (the uses entry) to use
"subosito/flutter-action@fd55f4c5af5b953cc57a2be44cb082c8f6635e8e".
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@lib/features/base/widget/emoji/emoji_button.dart`:
- Around line 81-83: _openDialog() can be re-entered while the async preload
runs, allowing multiple overlays; add a guard to prevent concurrent opens by
using a new boolean (e.g. _isOpening) or checking _overlayEntry != null at the
start of the async path, set it true immediately before any await/preload and
set it false in finally (or clear _overlayEntry only after removal) so only one
overlay is created; apply the same guard logic to the other open paths
referenced (lines around 110-113, 218-223, 234-235) and ensure _isDialogVisible
is still set appropriately when the overlay is shown/hidden.

---

Duplicate comments:
In `@lib/features/base/widget/emoji/emoji_button.dart`:
- Around line 94-97: Clamp the computed sizes to non-negative values: ensure
availableHeight and availableWidth are max(screenSize.height - 32, 0) and
max(screenSize.width - 16, 0), then compute dialogHeight =
math.min(_dialogHeight, availableHeight) and dialogWidth =
math.min(_dialogWidth, availableWidth) (or clamp dialogHeight/dialogWidth again
to >= 0). Update the same logic at the other occurrence (lines referencing
_dialogHeight/_dialogWidth and availableWidth/availableHeight around 134-135) so
no negative dimensions are passed into the dialog container.

---

Nitpick comments:
In @.github/workflows/analyze-test.yaml:
- Around line 46-50: Replace the runtime ssh-keyscan step (the "Add GitHub to
known hosts" step) with writing GitHub's published SSH host key(s) directly into
~/.ssh/known_hosts: remove the ssh-keyscan invocation and instead append the
official hardcoded host key strings (e.g., GitHub's rsa and ecdsa host keys)
into the file, ensure ~/.ssh exists and has correct permissions before writing,
and update the step comment to indicate keys are pinned to GitHub's published
values; keep the step name "Add GitHub to known hosts" so it's easy to find.
- Line 54: Replace the mutable tag used for the GitHub Action reference
"subosito/flutter-action@v2" with the provided immutable commit SHA to ensure
consistency and supply-chain security; update the workflow line referencing
subosito/flutter-action (the uses entry) to use
"subosito/flutter-action@fd55f4c5af5b953cc57a2be44cb082c8f6635e8e".

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cf7008a and 4a2a479.

📒 Files selected for processing (3)
  • .github/workflows/analyze-test.yaml
  • lib/features/base/widget/emoji/emoji_button.dart
  • lib/features/composer/presentation/extensions/handle_insert_emoji_to_editor_extension.dart

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.

3 participants