Skip to content

CCCT-2085 Unsubscribe From Messaging Channels#3547

Open
conroy-ricketts wants to merge 13 commits intomasterfrom
CCCT-2085-allow-unsubscribe-from-messaging-channels
Open

CCCT-2085 Unsubscribe From Messaging Channels#3547
conroy-ricketts wants to merge 13 commits intomasterfrom
CCCT-2085-allow-unsubscribe-from-messaging-channels

Conversation

@conroy-ricketts
Copy link
Contributor

@conroy-ricketts conroy-ricketts commented Feb 18, 2026

CCCT-2085

Product Description

I gave the the "unsubscribe" action real functionality and added some UI tweaks for the unsubscribed state for messaging channels. Here's a video showing the unsubscribe functionality successfully working and updating the UI state, along with what it looks like if there is an error:

Screen_Recording_20260218_140043_CommCare.Debug.mp4

Edit:

Here's an updated video where I reuse the already existing network loading dialog instead:

Screen_Recording_20260219_140931_CommCare.Debug.mp4

Technical Summary

I deleted the fromMessagePayload() function from ConnectMessagingMessageRecord as it's no longer being used anywhere. And, I removed a lot of code regarding the user consenting to messaging channels given that channels are now auto-consented - this includes the deletion of few string resources that are no longer being used.

Also, it was noted in the ticket comments that an error top bar is not in the scope of this ticket, so I just added a temporary toast error message for now (for errors when calling the API to unsubscribe).

Safety Assurance

Safety story

I've verified the following:

  • Both the unsubscribed and subscribed states for messaging channels show correctly in the UI.
  • The consent API is successfully called for unsubscribe.
  • Unsubscribed channels always show at the bottom of the channels list.
  • An error is shown in the case that the API call is not a success.

QA Plan

Verify all the same things listed under "Safety Story" above.

Removed unused function fromMessagePayload() from ConnectMessagingMessageRecord.
Gave the unsubscribe from messaging channel dialog real functionality.

Sorted the messaging channels list.

Removed unused string resources that were there for the messaging channel consent feature.

Deleted some unused code that was implemented as part of the messaging channel consent feature.

Added some UI tweaks and UI additions for the unsubscribed messaging channel state.
Removed code that was setting answeredConsent when unsubscribing from messaging channels.
…nto CCCT-2085-allow-unsubscribe-from-messaging-channels
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 18, 2026

📝 Walkthrough

Walkthrough

This PR removes the Connect messaging consent bottom sheet and its layout, deletes consent-related strings across multiple locales, and removes dialog/navigation entries referencing the bottom sheet. Consent handling is consolidated into ConnectMessageFragment (new subscribe/unsubscribe flows and UI state helpers). ChannelAdapter now partitions and displays consented vs unsubscribed channels with visual differences. Database preview generation was adjusted to only show previews for consented channels. Three new color resources and a drawable using one of them were added. ConnectMessagingActivity gained progress-dialog management for messaging network tasks.

Sequence Diagram

sequenceDiagram
    participant User as User
    participant CMF as ConnectMessageFragment
    participant MM as MessageManager
    participant DB as ConnectMessagingDatabaseHelper
    participant UI as UI Layer

    User->>CMF: Open channel screen
    CMF->>DB: fetch channel (id)
    DB-->>CMF: channel (with consent flag)
    alt consented
        CMF->>UI: setChannelSubscribedState()
        UI-->>User: input enabled, previews shown
    else unconsented
        CMF->>UI: setChannelUnsubscribedState()
        UI-->>User: input disabled, unsubscribed message shown
    end

    User->>CMF: Tap unsubscribe
    CMF->>UI: showProgressDialog (NETWORK_ACTIVITY_MESSAGING_CHANNEL_ID)
    CMF->>MM: updateChannelConsent(channelId, false)
    MM->>DB: persist consent=false
    DB-->>MM: confirm
    MM-->>CMF: success
    CMF->>UI: dismissProgressDialog
    CMF->>UI: setChannelUnsubscribedState()
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The PR title clearly and concisely describes the main change: implementing unsubscribe functionality for messaging channels.
Description check ✅ Passed The PR description includes all major required sections: Product Description (with video demonstrations), Technical Summary (deleted code and reasoning), and Safety Assurance (safety story and QA plan aligned with implementation).

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch CCCT-2085-allow-unsubscribe-from-messaging-channels

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
Contributor

@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

🧹 Nitpick comments (3)
app/src/org/commcare/fragments/connectMessaging/ConnectMessageChannelListFragment.java (1)

127-131: Consider encapsulating notifyDataSetChanged() inside setChannels().

setChannels() mutates internal adapter state but relies on callers to remember to call notifyDataSetChanged(). This is fragile — any future caller of setChannels() that forgets the notify call will produce a stale UI.

♻️ Proposed refactor in ChannelAdapter.java
 public void setChannels(List<ConnectMessagingChannelRecord> incomingChannels) {
     // ... partitioning logic ...
     channels.clear();
     channels.addAll(subscribedChannels);
     channels.addAll(unsubscribedChannels);
+    notifyDataSetChanged();
 }

Then remove the explicit call in ConnectMessageChannelListFragment.refreshUi():

             channelAdapter.setChannels(channels);
-            channelAdapter.notifyDataSetChanged();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/org/commcare/fragments/connectMessaging/ConnectMessageChannelListFragment.java`
around lines 127 - 131, The adapter's setChannels(...) mutates internal state
but relies on callers to call notifyDataSetChanged(), which is brittle; update
ChannelAdapter.setChannels(List<ConnectMessagingChannelRecord>) to call
notifyDataSetChanged() after updating its internal list, remove the explicit
channelAdapter.notifyDataSetChanged() call from
ConnectMessageChannelListFragment.refreshUi(), and search for other callers of
setChannels(...) to remove redundant notifyDataSetChanged() calls so the adapter
itself is responsible for notifying the UI.
app/src/org/commcare/fragments/connectMessaging/ConnectMessageFragment.java (1)

97-113: Commented-out menu items leave the analytics mapping (lines 115-120) and dialog handling as dead code.

Since both menu items are commented out, the menuItemsAnalyticsParamsMapping, onMenuItemSelected logic for unsubscribe/resubscribe, showDialogForMenuItem, and the entire unsubscribe callback at lines 338-361 are unreachable. This is fine as scaffolding if the plan is to re-enable soon, but worth noting.

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

In `@app/src/org/commcare/fragments/connectMessaging/ConnectMessageFragment.java`
around lines 97 - 113, The commented-out menu creation for MENU_UNSUBSCRIBE and
MENU_RESUBSCRIBE in ConnectMessageFragment makes related code dead; either
re-enable the menu items or remove the unreachable helpers and mappings: restore
the conditional block that adds MENU_UNSUBSCRIBE/MENU_RESUBSCRIBE (using
channel.getConsented()) so menuItemsAnalyticsParamsMapping, onMenuItemSelected,
showDialogForMenuItem, and the unsubscribe callback become reachable, or if you
intend to keep resubscribe disabled for now, delete the unused
menuItemsAnalyticsParamsMapping entry and remove/clean up the
unsubscribe/resubscribe branches in onMenuItemSelected and the unsubscribe
callback to eliminate dead code and stale analytics mappings.
app/src/org/commcare/connect/database/ConnectMessagingDatabaseHelper.java (1)

73-74: Prefer isEmpty() over manual size/length comparisons.

Lines 73 and 107 both check collection/string emptiness via > 0 instead of the idiomatic !isEmpty().

♻️ Proposed refactor
-        if (channels.size() > 0) {
+        if (!channels.isEmpty()) {
-                    if (existing.getKey().length() > 0) {
+                    if (!existing.getKey().isEmpty()) {

Also applies to: 107-109

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

In `@app/src/org/commcare/connect/database/ConnectMessagingDatabaseHelper.java`
around lines 73 - 74, The code in ConnectMessagingDatabaseHelper uses size() > 0
(e.g., the channels check returning channels.get(0)) and a similar > 0
string/collection check later; replace those with the idiomatic !isEmpty() (for
collections like channels) or isEmpty() (for positive/negative checks on
strings) to improve readability and null-safety—locate the checks around the
channels variable and the second check referenced in the diff (lines near 73 and
107) and change them to use !channels.isEmpty() and the equivalent
!someString.isEmpty()/someCollection.isEmpty() checks.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/src/org/commcare/fragments/connectMessaging/ConnectMessageFragment.java`:
- Around line 339-361: The code sets channel.setConsented(false) before calling
MessageManager.updateChannelConsent, but on API failure the in-memory channel is
never reverted; update the failure branch of
MessageManager.updateChannelConsent's callback to reset the in-memory state
(call channel.setConsented(true)) before showing the Toast and hiding the
spinner so the object remains consistent; ensure this change is made alongside
existing UI updates (binding.pbLoadingSpinner.setVisibility and not calling
setChannelUnsubscribedState) and keep requireActivity().invalidateMenu() only on
success as now.

---

Nitpick comments:
In `@app/src/org/commcare/connect/database/ConnectMessagingDatabaseHelper.java`:
- Around line 73-74: The code in ConnectMessagingDatabaseHelper uses size() > 0
(e.g., the channels check returning channels.get(0)) and a similar > 0
string/collection check later; replace those with the idiomatic !isEmpty() (for
collections like channels) or isEmpty() (for positive/negative checks on
strings) to improve readability and null-safety—locate the checks around the
channels variable and the second check referenced in the diff (lines near 73 and
107) and change them to use !channels.isEmpty() and the equivalent
!someString.isEmpty()/someCollection.isEmpty() checks.

In
`@app/src/org/commcare/fragments/connectMessaging/ConnectMessageChannelListFragment.java`:
- Around line 127-131: The adapter's setChannels(...) mutates internal state but
relies on callers to call notifyDataSetChanged(), which is brittle; update
ChannelAdapter.setChannels(List<ConnectMessagingChannelRecord>) to call
notifyDataSetChanged() after updating its internal list, remove the explicit
channelAdapter.notifyDataSetChanged() call from
ConnectMessageChannelListFragment.refreshUi(), and search for other callers of
setChannels(...) to remove redundant notifyDataSetChanged() calls so the adapter
itself is responsible for notifying the UI.

In `@app/src/org/commcare/fragments/connectMessaging/ConnectMessageFragment.java`:
- Around line 97-113: The commented-out menu creation for MENU_UNSUBSCRIBE and
MENU_RESUBSCRIBE in ConnectMessageFragment makes related code dead; either
re-enable the menu items or remove the unreachable helpers and mappings: restore
the conditional block that adds MENU_UNSUBSCRIBE/MENU_RESUBSCRIBE (using
channel.getConsented()) so menuItemsAnalyticsParamsMapping, onMenuItemSelected,
showDialogForMenuItem, and the unsubscribe callback become reachable, or if you
intend to keep resubscribe disabled for now, delete the unused
menuItemsAnalyticsParamsMapping entry and remove/clean up the
unsubscribe/resubscribe branches in onMenuItemSelected and the unsubscribe
callback to eliminate dead code and stale analytics mappings.

Tweaked UI a bit for the unsubscribe state.
Moved notifyDataSetChanged() back to ChannelAdapter.
Replaced checks with isEmpty() where appropriate.
Prevented stale consented value on in-memory channel.
Comment on lines 72 to 81
<ProgressBar
android:id="@+id/pbLoadingSpinner"
style="@style/Base.Widget.AppCompat.ProgressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:visibility="gone"/>
Copy link
Contributor

Choose a reason for hiding this comment

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

@conroy-ricketts We should not introduce new progressbar in individual fragments but try to use available here

Copy link
Contributor

Choose a reason for hiding this comment

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks guys! Will update this soon

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@Jignesh-dimagi @OrangeAndGreen Let me know what you guys think! - e9799e1

Screen_Recording_20260219_140931_CommCare.Debug.mp4

// Menu.NONE,
// R.string.connect_messaging_channel_menu_item_resubscribe
// );
// if (channel.getConsented()) {
Copy link
Contributor

Choose a reason for hiding this comment

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

@conroy-ricketts It think it's ok to uncomment it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This code is uncommented in #3549. The reason for doing it like this is so that the master branch is always in a release-ready state. I wanted to avoid showing any new menu items until this feature is fully implemented when merging into master.

@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
if (s.length() > 0) {
if (s.length() > 0 && channel.getConsented()) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we need this, as the editbox is already disabled at the beginning when the fragment view is created?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, we need this check or else the send button will be visible in the unsubscribed state:

Let me know if I should refactor this area of the code to make this more intuitive

channel.setConsented(true);
Toast.makeText(
requireContext(),
errorText,
Copy link
Contributor

Choose a reason for hiding this comment

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

errorText is alway connect_messaging_channel_unsubscribe_error, will be shown if error in subscribe call also.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The error text will change slightly depending on the action

If there is an error during the "unsubscribe" action, then the error text should be "Failed to unsubscribe". If there is an error during the "resubscribe" action then the error text should be "Failed to resubscribe"

Copy link
Contributor

@OrangeAndGreen OrangeAndGreen left a comment

Choose a reason for hiding this comment

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

Nothing additional to add other than the requested change to progress/wait dialog

Comment on lines 72 to 81
<ProgressBar
android:id="@+id/pbLoadingSpinner"
style="@style/Base.Widget.AppCompat.ProgressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:visibility="gone"/>
Copy link
Contributor

Choose a reason for hiding this comment

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

Removed the new progress spinner in favor of reusing the already existing blocking loading dialog.
…nto CCCT-2085-allow-unsubscribe-from-messaging-channels
OrangeAndGreen
OrangeAndGreen previously approved these changes Feb 19, 2026
Refactored code to fetch channel from DB after an API call.
@conroy-ricketts
Copy link
Contributor Author

@coderabbitai full review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 20, 2026

✅ Actions performed

Full review triggered.

Copy link
Contributor

@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: 2

🧹 Nitpick comments (2)
app/src/org/commcare/adapters/ChannelAdapter.java (1)

98-120: Unread badge is still visible for unsubscribed channels — intentional?

Lines 98–104 check unread > 0 regardless of consent status. An unsubscribed channel with leftover unread messages would show the badge but no timestamp and no preview, which looks slightly inconsistent. If unsubscribed channels should appear fully muted, consider hiding the unread count as well.

♻️ Suggested change if unread badge should be hidden for unsubscribed channels
-            boolean showUnread = unread > 0;
+            boolean showUnread = unread > 0 && channel.getConsented();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/org/commcare/adapters/ChannelAdapter.java` around lines 98 - 120,
Unread badge is being shown regardless of consent; update the visibility logic
so the unread count (binding.tvUnreadCount) is only displayed when unread > 0
AND the channel is consented (channel.getConsented()). Concretely, change the
showUnread condition or add a surrounding consent check in the bind block that
sets tvUnreadCount text and visibility (referencing binding.tvUnreadCount and
channel.getConsented()) so that unsubscribed channels always hide the unread
badge.
app/src/org/commcare/fragments/connectMessaging/ConnectMessageFragment.java (1)

96-148: Menu items and analytics map wired but unreachable — intentional per TODO.

The menu items are commented out (lines 103–117) with clear TODO comments. Note that menuItemsAnalyticsParamsMapping (lines 119–124) is still initialized with mappings for these menu IDs. This is harmless but slightly wasteful until the menu items are re-enabled.

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

In `@app/src/org/commcare/fragments/connectMessaging/ConnectMessageFragment.java`
around lines 96 - 148, The menu ID-to-analytics mapping is created in
setupMenuItems() even though the actual MenuItem additions are commented out;
remove or defer that initialization so we don't create unreachable mappings.
Update setupMenuItems(): move the menuItemsAnalyticsParamsMapping assignment
into the same branch where menu.add(...) would be executed (inside
onCreateMenu), or simply delete the Map.of(...) until the
MENU_UNSUBSCRIBE/MENU_RESUBSCRIBE additions are re-enabled; ensure
onMenuItemSelected still guards use of menuItemsAnalyticsParamsMapping if it may
be null. Reference symbols: setupMenuItems, onCreateMenu,
menuItemsAnalyticsParamsMapping, onMenuItemSelected, MENU_UNSUBSCRIBE,
MENU_RESUBSCRIBE.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/src/org/commcare/connect/database/ConnectMessagingDatabaseHelper.java`:
- Around line 107-109: The code in ConnectMessagingDatabaseHelper calls
existing.getKey().isEmpty() which can NPE if getKey() returns null; change the
conditional around the existing->incoming key copy (the block referencing
existing.getKey() and incoming.setKey()) to first null-check existing.getKey()
(e.g., ensure existing.getKey() != null) before calling isEmpty(), and only call
incoming.setKey(existing.getKey()) when the key is non-null and non-empty so
nulls from database/SqlStorage are handled safely.

In `@app/src/org/commcare/fragments/connectMessaging/ConnectMessageFragment.java`:
- Around line 343-377: The async callback passed to
MessageManager.updateChannelConsent currently calls requireContext() to re-fetch
the channel via ConnectMessagingDatabaseHelper.getMessagingChannel before
checking isAdded(), which can throw if the fragment is detached; move the call
to ConnectMessagingDatabaseHelper.getMessagingChannel(requireContext(),
channelId) inside the if (isAdded()) block (and likewise any calls that use
requireContext(), e.g., Toast.makeText and requireActivity().invalidateMenu())
so that requireContext()/requireActivity() are only invoked after isAdded() has
returned true; keep calls that must run regardless (if any) outside the
isAdded() guard and ensure
activity.dismissProgressDialogForTask(ConnectConstants.NETWORK_ACTIVITY_MESSAGING_CHANNEL_ID)
is still called from the UI-thread-safe, isAdded-guarded section.

---

Nitpick comments:
In `@app/src/org/commcare/adapters/ChannelAdapter.java`:
- Around line 98-120: Unread badge is being shown regardless of consent; update
the visibility logic so the unread count (binding.tvUnreadCount) is only
displayed when unread > 0 AND the channel is consented (channel.getConsented()).
Concretely, change the showUnread condition or add a surrounding consent check
in the bind block that sets tvUnreadCount text and visibility (referencing
binding.tvUnreadCount and channel.getConsented()) so that unsubscribed channels
always hide the unread badge.

In `@app/src/org/commcare/fragments/connectMessaging/ConnectMessageFragment.java`:
- Around line 96-148: The menu ID-to-analytics mapping is created in
setupMenuItems() even though the actual MenuItem additions are commented out;
remove or defer that initialization so we don't create unreachable mappings.
Update setupMenuItems(): move the menuItemsAnalyticsParamsMapping assignment
into the same branch where menu.add(...) would be executed (inside
onCreateMenu), or simply delete the Map.of(...) until the
MENU_UNSUBSCRIBE/MENU_RESUBSCRIBE additions are re-enabled; ensure
onMenuItemSelected still guards use of menuItemsAnalyticsParamsMapping if it may
be null. Reference symbols: setupMenuItems, onCreateMenu,
menuItemsAnalyticsParamsMapping, onMenuItemSelected, MENU_UNSUBSCRIBE,
MENU_RESUBSCRIBE.

Hid unread count if channel is unsubscribed.
Moved DB fetch to isAdded() check
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.

4 participants

Comments