Skip to content

Conversation

@OrangeAndGreen
Copy link
Contributor

Summary

Simple change to support markdown in the messaging page
Jira ticket

cross-request: dimagi/commcare-core#1455

Feature Flag

ConnectID Messaging

Product Description

When messages contain markdown, it will be rendered appropriately in the messaging page.

PR Checklist

  • If I think the PR is high risk, "High Risk" label is set
  • I have confidence that this PR will not introduce a regression for the reasons below
  • Do we need to enhance manual QA test coverage ? If yes, "QA Note" label is set correctly
  • Does the PR introduce any major changes worth communicating ? If yes, "Release Note" label is set and a "Release Note" is specified in PR description.

Automated test coverage

No automated tests for Connect code yet.

Safety story

Very small change, simply changes how the text is displayed and relies on existing code to render the markdown.

FirebaseMessagingService now checks for active messaging fragments before notifying about messages.
When active, service issues a broadcast instead of showing a push notification.
@coderabbitai
Copy link

coderabbitai bot commented Jan 28, 2025

📝 Walkthrough

Walkthrough

This pull request introduces changes to multiple files in the CommCare messaging system, focusing on enhancing message handling and notification mechanisms. The modifications span across three key files: ConnectMessageAdapter.java, ConnectMessageChannelListFragment.java, ConnectMessageFragment.java, and CommCareFirebaseMessagingService.java.

The changes primarily involve implementing markdown support for chat messages, adding broadcast receivers to manage messaging updates, and refining the logic for displaying Firebase Cloud Messaging (FCM) notifications. New static variables and methods have been introduced to track active messaging states and handle real-time updates. The implementation allows for more dynamic message rendering and improves the way notifications are managed based on the current user context within the messaging interface.

Sequence Diagram

sequenceDiagram
    participant User
    participant Fragment
    participant BroadcastReceiver
    participant FirebaseMessagingService
    
    User->>Fragment: Enters Messaging Screen
    Fragment->>BroadcastReceiver: Register Receiver
    FirebaseMessagingService->>BroadcastReceiver: Send Update Broadcast
    BroadcastReceiver->>Fragment: Trigger UI Refresh
    Fragment->>Fragment: Update Message Display
Loading

Possibly related PRs

Suggested reviewers

  • pm-dimagi
✨ Finishing Touches
  • 📝 Generate Docstrings (Beta)

Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR. (Beta)
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

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: 3

🧹 Nitpick comments (6)
app/src/org/commcare/fragments/connectMessaging/ConnectMessageChannelListFragment.java (2)

30-30: Consider alternatives to static fragment state.

Using a static boolean to track fragment state can lead to memory leaks and issues during configuration changes (e.g., screen rotation). Consider using ViewModel or saved instance state to manage fragment lifecycle state.


90-95: Add error handling to the BroadcastReceiver.

The current implementation could be more robust with proper error handling:

  1. Validate the incoming intent
  2. Handle potential exceptions in refreshUi()
 private final BroadcastReceiver updateReceiver = new BroadcastReceiver() {
     @Override
     public void onReceive(Context context, Intent intent) {
-        refreshUi();
+        if (intent != null && 
+            CommCareFirebaseMessagingService.MESSAGING_UPDATE_BROADCAST.equals(intent.getAction())) {
+            try {
+                refreshUi();
+            } catch (Exception e) {
+                // Log the error and possibly show a user-friendly message
+                e.printStackTrace();
+            }
+        }
     }
 };
app/src/org/commcare/fragments/connectMessaging/ConnectMessageFragment.java (1)

90-95: Add error handling to BroadcastReceiver.

The current implementation lacks error handling. Consider adding try-catch block and intent validation.

 private final BroadcastReceiver updateReceiver = new BroadcastReceiver() {
     @Override
     public void onReceive(Context context, Intent intent) {
+        if (intent == null || !CommCareFirebaseMessagingService.MESSAGING_UPDATE_BROADCAST.equals(intent.getAction())) {
+            return;
+        }
+        try {
             refreshUi();
+        } catch (Exception e) {
+            Logger.log(LogTypes.TYPE_ERROR_SERVER_COMMS, "Error refreshing messaging UI: " + e.getMessage());
+        }
     }
 };
app/src/org/commcare/services/CommCareFirebaseMessagingService.java (1)

178-196: Review notification priority setting.

Setting all notifications to HIGH priority (PRIORITY_HIGH) might lead to notification fatigue. Consider implementing priority levels based on message type or importance.

-    .setPriority(NotificationCompat.PRIORITY_HIGH)
+    int priority = isUrgentMessage(payloadData) 
+        ? NotificationCompat.PRIORITY_HIGH 
+        : NotificationCompat.PRIORITY_DEFAULT;
+    .setPriority(priority)

Add this helper method:

private boolean isUrgentMessage(Map<String, String> payloadData) {
    // Determine urgency based on message type, content, or metadata
    return payloadData.containsKey("urgent") || 
           "payment".equals(payloadData.get("type"));
}
app/src/org/commcare/adapters/ConnectMessageAdapter.java (2)

42-44: Add defensive programming measures for message handling.

While the markdown implementation looks good, consider these improvements for robustness:

  1. Add null check for chat.getMessage()
  2. Add error handling for markdown parsing failures
  3. Consider reusing SpannableStringBuilder instances for better performance
 public void bind(ConnectMessageChatData chat) {
+    String message = chat.getMessage();
+    if (message == null) {
+        message = "";
+    }
+    try {
         SpannableStringBuilder builder = new SpannableStringBuilder();
-        builder.append(chat.getMessage());
+        builder.append(message);
         MarkupUtil.setMarkdown(binding.tvChatMessage, builder, new SpannableStringBuilder());
+    } catch (Exception e) {
+        // Fallback to plain text if markdown parsing fails
+        binding.tvChatMessage.setText(message);
     }

59-61: Consider extracting common message formatting logic.

The message formatting code is duplicated between LeftViewHolder and RightViewHolder. Consider extracting this into a shared utility method to improve maintainability.

+    private static void formatMessage(TextView messageView, String message) {
+        if (message == null) {
+            message = "";
+        }
+        try {
+            SpannableStringBuilder builder = new SpannableStringBuilder();
+            builder.append(message);
+            MarkupUtil.setMarkdown(messageView, builder, new SpannableStringBuilder());
+        } catch (Exception e) {
+            messageView.setText(message);
+        }
+    }

     public static class LeftViewHolder extends RecyclerView.ViewHolder {
         // ...
         public void bind(ConnectMessageChatData chat) {
-            SpannableStringBuilder builder = new SpannableStringBuilder();
-            builder.append(chat.getMessage());
-            MarkupUtil.setMarkdown(binding.tvChatMessage, builder, new SpannableStringBuilder());
+            formatMessage(binding.tvChatMessage, chat.getMessage());
             binding.tvUserName.setText(DateUtils.formatDateTime(chat.getTimestamp(),
                     DateUtils.FORMAT_HUMAN_READABLE_SHORT));
         }
     }

     public static class RightViewHolder extends RecyclerView.ViewHolder {
         // ...
         public void bind(ConnectMessageChatData chat) {
-            SpannableStringBuilder builder = new SpannableStringBuilder();
-            builder.append(chat.getMessage());
-            MarkupUtil.setMarkdown(binding.tvChatMessage, builder, new SpannableStringBuilder());
+            formatMessage(binding.tvChatMessage, chat.getMessage());
             binding.tvUserName.setText(DateUtils.formatDateTime(chat.getTimestamp(),
                     DateUtils.FORMAT_HUMAN_READABLE_SHORT));
         }
     }
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 178299b and 35e875f.

📒 Files selected for processing (4)
  • app/src/org/commcare/adapters/ConnectMessageAdapter.java (4 hunks)
  • app/src/org/commcare/fragments/connectMessaging/ConnectMessageChannelListFragment.java (2 hunks)
  • app/src/org/commcare/fragments/connectMessaging/ConnectMessageFragment.java (3 hunks)
  • app/src/org/commcare/services/CommCareFirebaseMessagingService.java (8 hunks)
🔇 Additional comments (6)
app/src/org/commcare/fragments/connectMessaging/ConnectMessageChannelListFragment.java (2)

83-88: LGTM! Proper cleanup in onPause.

The broadcast receiver is correctly unregistered and the fragment state is cleaned up.


Line range hint 1-120: Consider adding tests for the messaging update mechanism.

While the PR objectives mention that automated tests aren't available for Connect code, it would be valuable to add tests for this new messaging update mechanism to ensure reliable delivery of markdown-formatted messages.

Key test scenarios to consider:

  1. Broadcast receiver registration/unregistration
  2. UI updates in response to broadcasts
  3. Error handling scenarios
app/src/org/commcare/fragments/connectMessaging/ConnectMessageFragment.java (1)

70-74: LGTM! Proper lifecycle management.

The registration and unregistration of the BroadcastReceiver in onResume/onPause is correctly implemented, following Android best practices.

Also applies to: 82-85

app/src/org/commcare/services/CommCareFirebaseMessagingService.java (1)

10-10: LGTM! Well-defined constant and appropriate import.

The MESSAGING_UPDATE_BROADCAST constant follows naming conventions and LocalBroadcastManager is the appropriate choice for internal app communication.

Also applies to: 43-43

app/src/org/commcare/adapters/ConnectMessageAdapter.java (2)

3-3: LGTM! Required imports added for markdown support.

The new imports for SpannableStringBuilder and MarkupUtil are appropriate for implementing markdown rendering functionality.

Also applies to: 13-13


42-44: Verify markdown parsing security and edge cases.

Let's verify the implementation of MarkupUtil.setMarkdown to ensure it properly handles:

  1. Security concerns (e.g., XSS prevention)
  2. Edge cases in markdown syntax
  3. Performance with large messages

Also applies to: 59-61

✅ Verification successful

Implementation verified - markdown parsing is secure and robust

The markdown implementation uses the Markwon library with proper input sanitization and security measures:

  • Input validation and character encoding handling
  • Secure file handling with proper permissions
  • HTML content sanitization
  • Null-safety and edge case handling
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for MarkupUtil implementation and tests
echo "Searching for MarkupUtil implementation..."
rg -A 10 "class MarkupUtil" 

echo "Searching for MarkupUtil tests..."
rg -l "class.*MarkupUtil.*Test"

echo "Checking for security-related comments or annotations..."
rg -i "security|xss|sanitize|escape" "$(fd -t f "MarkupUtil.java")"

Length of output: 1291


Script:

#!/bin/bash
# Get full implementation of MarkupUtil
echo "Full MarkupUtil implementation:"
cat app/src/org/commcare/utils/MarkupUtil.java

echo -e "\nChecking for markdown library dependencies:"
rg -i "markdown" build.gradle

echo -e "\nChecking for input validation patterns:"
ast-grep --pattern 'setMarkdown($_,$_,$_) {
  $$$
}'

Length of output: 9016

Comment on lines +73 to +77
isActive = true;

LocalBroadcastManager.getInstance(requireContext()).registerReceiver(updateReceiver,
new IntentFilter(CommCareFirebaseMessagingService.MESSAGING_UPDATE_BROADCAST));

Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Avoid duplicate message retrieval calls.

MessageManager.retrieveMessages() is called in both onCreateView and onResume. This could lead to unnecessary network calls and potential race conditions. Consider consolidating these calls or implementing a debounce mechanism.

 @Override
 public void onResume() {
     super.onResume();
     isActive = true;
     LocalBroadcastManager.getInstance(requireContext()).registerReceiver(updateReceiver,
             new IntentFilter(CommCareFirebaseMessagingService.MESSAGING_UPDATE_BROADCAST));
-    MessageManager.retrieveMessages(requireActivity(), success -> {
-        refreshUi();
-    });
 }
📝 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
isActive = true;
LocalBroadcastManager.getInstance(requireContext()).registerReceiver(updateReceiver,
new IntentFilter(CommCareFirebaseMessagingService.MESSAGING_UPDATE_BROADCAST));
isActive = true;
LocalBroadcastManager.getInstance(requireContext()).registerReceiver(updateReceiver,
new IntentFilter(CommCareFirebaseMessagingService.MESSAGING_UPDATE_BROADCAST));


public class ConnectMessageFragment extends Fragment {

public static String activeChannel;
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Consider alternative to static variable for tracking active channel.

Using a static variable to track the active channel state could lead to memory leaks and issues during configuration changes. Consider using a ViewModel or other lifecycle-aware component to manage this state.

Here's a suggested implementation using ViewModel:

class ConnectMessageViewModel : ViewModel() {
    private val _activeChannel = MutableLiveData<String?>()
    val activeChannel: LiveData<String?> = _activeChannel

    fun setActiveChannel(channelId: String?) {
        _activeChannel.value = channelId
    }
}

Usage in fragment:

-public static String activeChannel;
+private ConnectMessageViewModel viewModel;

@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
+   viewModel = new ViewModelProvider(this).get(ConnectMessageViewModel.class);
}

Comment on lines +153 to +158
if(ConnectMessageChannelListFragment.isActive ||
channelId.equals(ConnectMessageFragment.activeChannel)) {
//Notify active page to update instead of showing notification
Intent broadcastIntent = new Intent(MESSAGING_UPDATE_BROADCAST);
LocalBroadcastManager.getInstance(this).sendBroadcast(broadcastIntent);
} else {
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Include channel information in the broadcast intent.

The broadcast intent should include the channel ID to allow receivers to verify if they need to update their UI.

 if(ConnectMessageChannelListFragment.isActive ||
         channelId.equals(ConnectMessageFragment.activeChannel)) {
     //Notify active page to update instead of showing notification
     Intent broadcastIntent = new Intent(MESSAGING_UPDATE_BROADCAST);
+    broadcastIntent.putExtra("channel_id", channelId);
+    broadcastIntent.putExtra("update_type", isMessage ? "message" : "channel");
     LocalBroadcastManager.getInstance(this).sendBroadcast(broadcastIntent);
 }

Then update the receiver in ConnectMessageFragment:

 private final BroadcastReceiver updateReceiver = new BroadcastReceiver() {
     @Override
     public void onReceive(Context context, Intent intent) {
+        String updatedChannelId = intent.getStringExtra("channel_id");
+        if (updatedChannelId != null && updatedChannelId.equals(channelId)) {
             refreshUi();
+        }
     }
 };

Committable suggestion skipped: line range outside the PR's diff.

@OrangeAndGreen OrangeAndGreen merged commit bb2b292 into connect_qa Jan 28, 2025
1 of 2 checks passed
@OrangeAndGreen OrangeAndGreen deleted the dv/messaging_markdown branch January 28, 2025 18:54
@coderabbitai coderabbitai bot mentioned this pull request Jun 30, 2025
4 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants