Skip to content

Conversation

@emil-wire
Copy link
Contributor

@emil-wire emil-wire commented Nov 14, 2025

TaskWPB-18861 [Web] add filter for unread messages, mentions and replies

user selectable conversation filters + tabs

Bildschirmfoto 2025-11-14 um 01 25 03 Bildschirmfoto 2025-11-14 um 01 25 10 Bildschirmfoto 2025-11-14 um 01 25 34

This PR is a rewrite of a previous PR that didn't work out so well. Using the feature flag FEATURE_ENABLE_ADVANCED_FILTERS you can turn this on and test it.

Checklist

  • mentions the JIRA issue in the PR name (Ex. [WPB-XXXX])
  • PR has been self reviewed by the author;
  • Hard-to-understand areas of the code have been commented;
  • If it is a core feature, unit tests have been added;

@codecov
Copy link

codecov bot commented Nov 14, 2025

Codecov Report

❌ Patch coverage is 49.32432% with 75 lines in your changes missing coverage. Please review.
✅ Project coverage is 43.44%. Comparing base (b95b233) to head (ecabb3e).

Additional details and impacted files
@@            Coverage Diff             @@
##              dev   #19769      +/-   ##
==========================================
- Coverage   43.45%   43.44%   -0.01%     
==========================================
  Files        1294     1294              
  Lines       32542    32618      +76     
  Branches     7229     7244      +15     
==========================================
+ Hits        14140    14172      +32     
- Misses      16689    16728      +39     
- Partials     1713     1718       +5     
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@github-actions
Copy link
Contributor

github-actions bot commented Nov 14, 2025

🔗 Download Full Report Artifact

🧪 Playwright Test Summary

  • Passed: 0
  • Failed: 14
  • Skipped: 0
  • 🔁 Flaky: 0
  • 📊 Total: 14
  • Total Runtime: 1628.3s (~ 27 min 8 sec)

Failed Tests:

❌ Account Management (tags: TC-8639, crit-flow-web)

Location: specs/CriticalFlow/accountManagement-TC-8639.spec.ts:37
Duration: 32700ms

Errors:

TimeoutError: locator.click: Timeout 20000ms exceeded.
Call log:
  - waiting for locator('[data-uie-name="go-preferences"]')


   at pageManager/webapp/components/conversationSidebar.component.ts:60

  58 |
  59 |   async clickPreferencesButton() {
> 60 |     await this.preferencesButton.click();
     |                                  ^
  61 |   }
  62 |
  63 |   async clickAllConversationsButton() {
    at ConversationSidebar.clickPreferencesButton (/home/runner/actions-runner/_work/wire-webapp/wire-webapp/test/e2e_tests/pageManager/webapp/components/conversationSidebar.component.ts:60:34)
    at /home/runner/actions-runner/_work/wire-webapp/wire-webapp/test/e2e_tests/specs/CriticalFlow/accountManagement-TC-8639.spec.ts:69:44
    at /home/runner/actions-runner/_work/wire-webapp/wire-webapp/test/e2e_tests/specs/CriticalFlow/accountManagement-TC-8639.spec.ts:68:14
❌ Team owner adds whole team to an all team chat (tags: TC-8631, crit-flow-web)

Location: specs/CriticalFlow/addMembersToChat-TC-8631.spec.ts:43
Duration: 1007ms

Errors:

AxiosError: Request failed with status code 409

   at backend/userRepository.e2e.ts:35

  33 |
  34 |   public async setUniqueUsername(username: string, token: string) {
> 35 |     await this.axiosInstance.put(
     |     ^
  36 |       'self/handle',
  37 |       {handle: username},
  38 |       {
    at settle (/home/runner/actions-runner/_work/wire-webapp/wire-webapp/node_modules/axios/lib/core/settle.js:19:12)
    at Unzip.handleStreamEnd (/home/runner/actions-runner/_work/wire-webapp/wire-webapp/node_modules/axios/lib/adapters/http.js:599:11)
    at Axios.request (/home/runner/actions-runner/_work/wire-webapp/wire-webapp/node_modules/axios/lib/core/Axios.js:45:41)
    at UserRepositoryE2E.setUniqueUsername (/home/runner/actions-runner/_work/wire-webapp/wire-webapp/test/e2e_tests/backend/userRepository.e2e.ts:35:5)
    at ApiManagerE2E.createTeamOwner (/home/runner/actions-runner/_work/wire-webapp/wire-webapp/test/e2e_tests/backend/apiManager.e2e.ts:142:5)
    at /home/runner/actions-runner/_work/wire-webapp/wire-webapp/test/e2e_tests/specs/CriticalFlow/addMembersToChat-TC-8631.spec.ts:54:20
    at /home/runner/actions-runner/_work/wire-webapp/wire-webapp/test/e2e_tests/specs/CriticalFlow/addMembersToChat-TC-8631.spec.ts:53:5
Error: User must have an ID to be removed from createdTeams

   at utils/tearDown.util.ts:45

  43 | export const removeCreatedTeam = async (api: ApiManagerE2E, user: User) => {
  44 |   if (!user.id) {
> 45 |     throw new Error('User must have an ID to be removed from createdTeams');
     |           ^
  46 |   }
  47 |   const teamId = createdTeams.get(user);
  48 |   if (!teamId) {
    at removeCreatedTeam (/home/runner/actions-runner/_work/wire-webapp/wire-webapp/test/e2e_tests/utils/tearDown.util.ts:45:11)
    at /home/runner/actions-runner/_work/wire-webapp/wire-webapp/test/e2e_tests/specs/CriticalFlow/addMembersToChat-TC-8631.spec.ts:241:26
❌ Setting up new device with a backup (tags: TC-8634, crit-flow-web)

Location: specs/CriticalFlow/backupRestoration-TC-8634.spec.ts:35
Duration: 32098ms

Errors:

TimeoutError: locator.click: Timeout 20000ms exceeded.
Call log:
  - waiting for locator('[data-uie-name="item-conversation"][data-uie-value="Pearline Graham"]').first()


   at pageManager/webapp/pages/conversationList.page.ts:76

  74 |
  75 |   async openConversation(conversationName: string) {
> 76 |     await this.getConversationLocator(conversationName).first().click();
     |                                                                 ^
  77 |   }
  78 |
  79 |   async openPendingConnectionRequest() {
    at ConversationListPage.openConversation (/home/runner/actions-runner/_work/wire-webapp/wire-webapp/test/e2e_tests/pageManager/webapp/pages/conversationList.page.ts:76:65)
    at /home/runner/actions-runner/_work/wire-webapp/wire-webapp/test/e2e_tests/specs/CriticalFlow/backupRestoration-TC-8634.spec.ts:71:36
    at /home/runner/actions-runner/_work/wire-webapp/wire-webapp/test/e2e_tests/specs/CriticalFlow/backupRestoration-TC-8634.spec.ts:70:14
❌ Calls in channels with device switch and screenshare (tags: TC-8754, crit-flow-web)

Location: specs/CriticalFlow/channelsCall-TC-8755.spec.ts:39
Duration: 91803ms

Errors:

TimeoutError: locator.waitFor: Timeout 60000ms exceeded.
Call log:
  - waiting for locator('[data-uie-name="go-preferences"]') to be visible


   at pageManager/webapp/components/conversationSidebar.component.ts:72

  70 |
  71 |   async isPageLoaded() {
> 72 |     await this.preferencesButton.waitFor({state: 'visible', timeout: this.pageLoadingTimeout});
     |                                  ^
  73 |   }
  74 |
  75 |   async clickArchive() {
    at ConversationSidebar.isPageLoaded (/home/runner/actions-runner/_work/wire-webapp/wire-webapp/test/e2e_tests/pageManager/webapp/components/conversationSidebar.component.ts:72:34)
    at completeLogin (/home/runner/actions-runner/_work/wire-webapp/wire-webapp/test/e2e_tests/utils/setup.util.ts:50:42)
    at /home/runner/actions-runner/_work/wire-webapp/wire-webapp/test/e2e_tests/specs/CriticalFlow/channelsCall-TC-8755.spec.ts:70:7
    at /home/runner/actions-runner/_work/wire-webapp/wire-webapp/test/e2e_tests/specs/CriticalFlow/channelsCall-TC-8755.spec.ts:69:5
❌ Channels Management (tags: TC-8752, crit-flow-web)

Location: specs/CriticalFlow/channelsManagement-TC-8752.spec.ts:37
Duration: 89598ms

Errors:

TimeoutError: locator.waitFor: Timeout 60000ms exceeded.
Call log:
  - waiting for locator('[data-uie-name="go-preferences"]') to be visible


   at pageManager/webapp/components/conversationSidebar.component.ts:72

  70 |
  71 |   async isPageLoaded() {
> 72 |     await this.preferencesButton.waitFor({state: 'visible', timeout: this.pageLoadingTimeout});
     |                                  ^
  73 |   }
  74 |
  75 |   async clickArchive() {
    at ConversationSidebar.isPageLoaded (/home/runner/actions-runner/_work/wire-webapp/wire-webapp/test/e2e_tests/pageManager/webapp/components/conversationSidebar.component.ts:72:34)
    at completeLogin (/home/runner/actions-runner/_work/wire-webapp/wire-webapp/test/e2e_tests/utils/setup.util.ts:50:42)
    at /home/runner/actions-runner/_work/wire-webapp/wire-webapp/test/e2e_tests/specs/CriticalFlow/channelsManagement-TC-8752.spec.ts:57:5
    at /home/runner/actions-runner/_work/wire-webapp/wire-webapp/test/e2e_tests/specs/CriticalFlow/channelsManagement-TC-8752.spec.ts:56:3
❌ Conversation Management (tags: TC-8636, crit-flow-web)

Location: specs/CriticalFlow/conversationManagement-TC-8636.spec.ts:34
Duration: 85829ms

Errors:

TimeoutError: locator.waitFor: Timeout 60000ms exceeded.
Call log:
  - waiting for locator('[data-uie-name="go-preferences"]') to be visible


   at pageManager/webapp/components/conversationSidebar.component.ts:72

  70 |
  71 |   async isPageLoaded() {
> 72 |     await this.preferencesButton.waitFor({state: 'visible', timeout: this.pageLoadingTimeout});
     |                                  ^
  73 |   }
  74 |
  75 |   async clickArchive() {
    at ConversationSidebar.isPageLoaded (/home/runner/actions-runner/_work/wire-webapp/wire-webapp/test/e2e_tests/pageManager/webapp/components/conversationSidebar.component.ts:72:34)
    at completeLogin (/home/runner/actions-runner/_work/wire-webapp/wire-webapp/test/e2e_tests/utils/setup.util.ts:50:42)
    at /home/runner/actions-runner/_work/wire-webapp/wire-webapp/test/e2e_tests/specs/CriticalFlow/conversationManagement-TC-8636.spec.ts:46:5
    at /home/runner/actions-runner/_work/wire-webapp/wire-webapp/test/e2e_tests/specs/CriticalFlow/conversationManagement-TC-8636.spec.ts:45:3
❌ Planning group call with sending various messages during call (tags: TC-8632, crit-flow-web)

Location: specs/CriticalFlow/groupCalls-TC-8632.spec.ts:37
Duration: 86429ms

Errors:

TimeoutError: locator.waitFor: Timeout 60000ms exceeded.
Call log:
  - waiting for locator('[data-uie-name="go-preferences"]') to be visible


   at pageManager/webapp/components/conversationSidebar.component.ts:72

  70 |
  71 |   async isPageLoaded() {
> 72 |     await this.preferencesButton.waitFor({state: 'visible', timeout: this.pageLoadingTimeout});
     |                                  ^
  73 |   }
  74 |
  75 |   async clickArchive() {
    at ConversationSidebar.isPageLoaded (/home/runner/actions-runner/_work/wire-webapp/wire-webapp/test/e2e_tests/pageManager/webapp/components/conversationSidebar.component.ts:72:34)
    at completeLogin (/home/runner/actions-runner/_work/wire-webapp/wire-webapp/test/e2e_tests/utils/setup.util.ts:50:42)
    at /home/runner/actions-runner/_work/wire-webapp/wire-webapp/test/e2e_tests/specs/CriticalFlow/groupCalls-TC-8632.spec.ts:71:7
    at /home/runner/actions-runner/_work/wire-webapp/wire-webapp/test/e2e_tests/specs/CriticalFlow/groupCalls-TC-8632.spec.ts:70:5
❌ Group Video call (tags: TC-8637, crit-flow-web)

Location: specs/CriticalFlow/groupVideoCall-TC-8637.spec.ts:39
Duration: 35689ms

Errors:

TimeoutError: locator.click: Timeout 20000ms exceeded.
Call log:
  - waiting for locator('[data-uie-name="conversation-list-header"] [data-uie-name="go-create-group"]')


   at pageManager/webapp/pages/conversationList.page.ts:104

  102 |
  103 |   async clickCreateGroup() {
> 104 |     await this.createGroupButton.click();
      |                                  ^
  105 |   }
  106 |
  107 |   getConversationLocator(conversationName: string) {
    at ConversationListPage.clickCreateGroup (/home/runner/actions-runner/_work/wire-webapp/wire-webapp/test/e2e_tests/pageManager/webapp/pages/conversationList.page.ts:104:34)
    at /home/runner/actions-runner/_work/wire-webapp/wire-webapp/test/e2e_tests/specs/CriticalFlow/groupVideoCall-TC-8637.spec.ts:104:43
    at /home/runner/actions-runner/_work/wire-webapp/wire-webapp/test/e2e_tests/specs/CriticalFlow/groupVideoCall-TC-8637.spec.ts:103:16
❌ New person joins team and setups up device (tags: TC-8635, crit-flow-web)

Location: specs/CriticalFlow/joinTeam-TC-8635.spec.ts:38
Duration: 87271ms

Errors:

TimeoutError: locator.waitFor: Timeout 60000ms exceeded.
Call log:
  - waiting for locator('[data-uie-name="go-preferences"]') to be visible


   at pageManager/webapp/components/conversationSidebar.component.ts:72

  70 |
  71 |   async isPageLoaded() {
> 72 |     await this.preferencesButton.waitFor({state: 'visible', timeout: this.pageLoadingTimeout});
     |                                  ^
  73 |   }
  74 |
  75 |   async clickArchive() {
    at ConversationSidebar.isPageLoaded (/home/runner/actions-runner/_work/wire-webapp/wire-webapp/test/e2e_tests/pageManager/webapp/components/conversationSidebar.component.ts:72:34)
    at completeLogin (/home/runner/actions-runner/_work/wire-webapp/wire-webapp/test/e2e_tests/utils/setup.util.ts:50:42)
    at /home/runner/actions-runner/_work/wire-webapp/wire-webapp/test/e2e_tests/specs/CriticalFlow/joinTeam-TC-8635.spec.ts:91:7
    at /home/runner/actions-runner/_work/wire-webapp/wire-webapp/test/e2e_tests/specs/CriticalFlow/joinTeam-TC-8635.spec.ts:90:5
❌ Messages in 1:1 (tags: TC-8750, crit-flow-web)

Location: specs/CriticalFlow/messagesIn1On1-TC-8750.spec.ts:47
Duration: 44480ms

Errors:

TimeoutError: locator.click: Timeout 20000ms exceeded.
Call log:
  - waiting for locator('[data-uie-name="primary-modals-container"][aria-label=\'Wire can’t open this conversation.\']').locator('[data-uie-name="do-action"]')


   at pageManager/webapp/modals/unableToOpenConversation.modal.ts:49

  47 |   async clickAcknowledge() {
  48 |     await this.acknowledgeButton.isVisible();
> 49 |     await this.acknowledgeButton.click();
     |                                  ^
  50 |   }
  51 | }
  52 |
    at UnableToOpenConversationModal.clickAcknowledge (/home/runner/actions-runner/_work/wire-webapp/wire-webapp/test/e2e_tests/pageManager/webapp/modals/unableToOpenConversation.modal.ts:49:34)
    at /home/runner/actions-runner/_work/wire-webapp/wire-webapp/test/e2e_tests/specs/CriticalFlow/messagesIn1On1-TC-8750.spec.ts:101:5
    at /home/runner/actions-runner/_work/wire-webapp/wire-webapp/test/e2e_tests/specs/CriticalFlow/messagesIn1On1-TC-8750.spec.ts:91:3
❌ Messages in Channels (tags: TC-8753, crit-flow-web)

Location: specs/CriticalFlow/messagesInChannels-TC-8753.spec.ts:44
Duration: 36775ms

Errors:

TimeoutError: locator.click: Timeout 20000ms exceeded.
Call log:
  - waiting for locator('[data-uie-name="conversation-list-header"] [data-uie-name="go-create-group"]')


   at pageManager/webapp/pages/conversationList.page.ts:104

  102 |
  103 |   async clickCreateGroup() {
> 104 |     await this.createGroupButton.click();
      |                                  ^
  105 |   }
  106 |
  107 |   getConversationLocator(conversationName: string) {
    at ConversationListPage.clickCreateGroup (/home/runner/actions-runner/_work/wire-webapp/wire-webapp/test/e2e_tests/pageManager/webapp/pages/conversationList.page.ts:104:34)
    at setupOwner (/home/runner/actions-runner/_work/wire-webapp/wire-webapp/test/e2e_tests/specs/CriticalFlow/messagesInChannels-TC-8753.spec.ts:76:45)
    at /home/runner/actions-runner/_work/wire-webapp/wire-webapp/test/e2e_tests/specs/CriticalFlow/messagesInChannels-TC-8753.spec.ts:89:7
    at /home/runner/actions-runner/_work/wire-webapp/wire-webapp/test/e2e_tests/specs/CriticalFlow/messagesInChannels-TC-8753.spec.ts:71:5
❌ Messages in Groups (tags: TC-8751, crit-flow-web)

Location: specs/CriticalFlow/messagesInGroups-TC-8751.spec.ts:42
Duration: 32687ms

Errors:

TimeoutError: locator.click: Timeout 20000ms exceeded.
Call log:
  - waiting for locator('[data-uie-name="conversation-list-header"] [data-uie-name="go-create-group"]')


   at pageManager/webapp/pages/conversationList.page.ts:104

  102 |
  103 |   async clickCreateGroup() {
> 104 |     await this.createGroupButton.click();
      |                                  ^
  105 |   }
  106 |
  107 |   getConversationLocator(conversationName: string) {
    at ConversationListPage.clickCreateGroup (/home/runner/actions-runner/_work/wire-webapp/wire-webapp/test/e2e_tests/pageManager/webapp/pages/conversationList.page.ts:104:34)
    at setupUserA (/home/runner/actions-runner/_work/wire-webapp/wire-webapp/test/e2e_tests/specs/CriticalFlow/messagesInGroups-TC-8751.spec.ts:66:45)
    at /home/runner/actions-runner/_work/wire-webapp/wire-webapp/test/e2e_tests/specs/CriticalFlow/messagesInGroups-TC-8751.spec.ts:78:7
    at /home/runner/actions-runner/_work/wire-webapp/wire-webapp/test/e2e_tests/specs/CriticalFlow/messagesInGroups-TC-8751.spec.ts:61:5
❌ 1:1 Video call with device switch and screenshare (tags: TC-8754, crit-flow-web)

Location: specs/CriticalFlow/oneOnOneCall-TC-8754.spec.ts:34
Duration: 86159ms

Errors:

TimeoutError: locator.waitFor: Timeout 60000ms exceeded.
Call log:
  - waiting for locator('[data-uie-name="go-preferences"]') to be visible


   at pageManager/webapp/components/conversationSidebar.component.ts:72

  70 |
  71 |   async isPageLoaded() {
> 72 |     await this.preferencesButton.waitFor({state: 'visible', timeout: this.pageLoadingTimeout});
     |                                  ^
  73 |   }
  74 |
  75 |   async clickArchive() {
    at ConversationSidebar.isPageLoaded (/home/runner/actions-runner/_work/wire-webapp/wire-webapp/test/e2e_tests/pageManager/webapp/components/conversationSidebar.component.ts:72:34)
    at completeLogin (/home/runner/actions-runner/_work/wire-webapp/wire-webapp/test/e2e_tests/utils/setup.util.ts:50:42)
    at /home/runner/actions-runner/_work/wire-webapp/wire-webapp/test/e2e_tests/specs/CriticalFlow/oneOnOneCall-TC-8754.spec.ts:64:7
    at /home/runner/actions-runner/_work/wire-webapp/wire-webapp/test/e2e_tests/specs/CriticalFlow/oneOnOneCall-TC-8754.spec.ts:63:5
❌ Personal Account Lifecycle (tags: TC-8638, crit-flow-web)

Location: specs/CriticalFlow/personalAccountLifecycle-TC-8638.spec.ts:31
Duration: 42663ms

Errors:

TimeoutError: locator.textContent: Timeout 20000ms exceeded.
Call log:
  - waiting for locator('[data-uie-name="status-name"]')


   at pageManager/webapp/components/conversationSidebar.component.ts:52

  50 |
  51 |   async getPersonalStatusName() {
> 52 |     return (await this.personalStatusName.textContent()) ?? '';
     |                                           ^
  53 |   }
  54 |
  55 |   async getPersonalUserName() {
    at ConversationSidebar.getPersonalStatusName (/home/runner/actions-runner/_work/wire-webapp/wire-webapp/test/e2e_tests/pageManager/webapp/components/conversationSidebar.component.ts:52:43)
    at /home/runner/actions-runner/_work/wire-webapp/wire-webapp/test/e2e_tests/specs/CriticalFlow/personalAccountLifecycle-TC-8638.spec.ts:88:51
    at /home/runner/actions-runner/_work/wire-webapp/wire-webapp/test/e2e_tests/specs/CriticalFlow/personalAccountLifecycle-TC-8638.spec.ts:87:14

@emil-wire
Copy link
Contributor Author

Bildschirmfoto 2025-11-14 um 13 23 11 Design update

@sonarqubecloud
Copy link


const channelConversationsLength = channelConversations.filter(filterUnreadAndArchivedConversations).length;
const groupConversationsLength = groupConversations.filter(filterUnreadAndArchivedConversations).length;
const channelConversationsLength = useMemo(
Copy link
Collaborator

Choose a reason for hiding this comment

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

useMemo only helps when the calculation is expensive enough to worry about and its dependencies do not change on every render.
If those arrays are new on every render, then useMemo recomputes every time anyway. In that case, useMemo just adds React bookkeeping overhead with zero gain.

Even if the arrays are referentially stable, these are still cheap O(n) filters which is rarely huge on the UI level. So from a performance POV, most of these are premature micro-optimizations.

I would remove these, but maybe I'm missing some context - others can provide their opinion as well

type: SidebarTabs.UNREAD,
title: t('conversationLabelUnread'),
dataUieName: 'go-unread-view',
Icon: <Icon.MarkAsUnreadIcon />,
Copy link
Collaborator

Choose a reason for hiding this comment

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

@emil-wire MarkAsUnreadIcon is missing on Icon component and locally this breaks the app - popup shows and the only thing to do is reload, which again shows same error. Please fix

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR introduces user-customizable conversation filters and tabs for the left sidebar, allowing users to toggle visibility of specialized conversation views (Unread, Mentions, Replies, Drafts, Pings) via a new settings dropdown. The feature is gated behind the FEATURE_ENABLE_ADVANCED_FILTERS flag.

Key Changes:

  • Converts filter dropdown to tab-based navigation with user-customizable visibility
  • Adds new sidebar tabs: UNREAD, MENTIONS, REPLIES, DRAFTS, PINGS
  • Implements TabsFilterButton component for managing tab visibility preferences
  • Enhances draft detection with real-time updates via amplify events and visibility change listeners
  • Refactors conversation filtering logic from a single dropdown to dedicated tab views

Reviewed changes

Copilot reviewed 69 out of 70 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
src/types/i18n.d.ts Reorganizes and adds i18n keys for new tab labels and filter UI
src/script/util/useChannelsFeatureFlag.ts Adds shouldShowChannelTab flag to centralize channel visibility logic
src/script/page/LeftSidebar/panels/Conversations/utils/draftUtils.ts Updates draft detection to only check plainMessage content (excludes editorState)
src/script/page/LeftSidebar/panels/Conversations/useSidebarStore.ts Replaces ConversationFilter enum with visibleTabs array and toggleTabVisibility function; defines ALWAYS_VISIBLE_TABS constant
src/script/page/LeftSidebar/panels/Conversations/hooks/useDraftConversations.ts Enhances draft detection with amplify event subscription and visibility change monitoring
src/script/page/LeftSidebar/panels/Conversations/helpers.tsx Removes applyAdvancedFilter function; adds dedicated tab handlers for UNREAD, MENTIONS, REPLIES, DRAFTS, PINGS with conversationFilters utilities
src/script/page/LeftSidebar/panels/Conversations/TabsFilterButton/TabsFilterButton.tsx New component providing dropdown UI for toggling tab visibility with keyboard support
src/script/page/LeftSidebar/panels/Conversations/TabsFilterButton/TabsFilterButton.styles.ts Styles for filter button and dropdown with accessibility focus states
src/script/page/LeftSidebar/panels/Conversations/Helpers.test.tsx Removes conversationFilter from test cases
src/script/page/LeftSidebar/panels/Conversations/Conversations.tsx Fixes typo (hasNoVisbleConversations → hasNoVisibleConversations); adds missing dependency arrays to useEffect/useCallback hooks
src/script/page/LeftSidebar/panels/Conversations/ConversationTabs/ConversationTabs.tsx Adds new tab definitions with useMemo for badge counts; filters visible tabs using isTabVisible; replaces ConversationFilterButton with TabsFilterButton
src/script/page/LeftSidebar/panels/Conversations/ConversationSidebar/ConversationSidebar.tsx Passes conversations and draftConversations props to ConversationTabs
src/script/page/LeftSidebar/panels/Conversations/ConversationFilterButton/* Removes old ConversationFilterButton component and styles
src/script/components/InputBar/common/draftState/draftState.ts Publishes DRAFT_STATE_CHANGED_EVENT when drafts are saved
src/i18n/*.json Removes old conversationFilter* keys; adds new conversationLabel* and search*Conversations keys across all locales

Comment on lines +118 to +120
if (shouldShowChannelTab) {
availableTabs.splice(2, 0, {type: SidebarTabs.CHANNELS, label: t('conversationLabelChannels')});
}
Copy link

Copilot AI Nov 24, 2025

Choose a reason for hiding this comment

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

[Important] The channel tab insertion logic uses a hardcoded index 2 which is fragile. If tabs are reordered or removed from the availableTabs array, the channel tab will be inserted at the wrong position. This inconsistency with the display order in ConversationTabs (where it's inserted at index 7) could lead to confusion.

Consider using a position-based approach similar to the suggestion for ConversationTabs.tsx, or define the tab order in a single location to maintain consistency.

Copilot uses AI. Check for mistakes.
Comment on lines +93 to +111
toggleTabVisibility: (tab: SidebarTabs) => {
set(state => {
const isCurrentlyVisible = state.visibleTabs.includes(tab);
const isActiveTab = state.currentTab === tab;

if (isCurrentlyVisible && isActiveTab) {
return {
currentTab: SidebarTabs.RECENT,
visibleTabs: state.visibleTabs.filter(visibleTab => visibleTab !== tab),
};
}

const newVisibleTabs = isCurrentlyVisible
? state.visibleTabs.filter(visibleTab => visibleTab !== tab)
: [...state.visibleTabs, tab];

return {visibleTabs: newVisibleTabs};
});
},
Copy link

Copilot AI Nov 24, 2025

Choose a reason for hiding this comment

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

[Blocker] The toggleTabVisibility function doesn't check if a tab is in ALWAYS_VISIBLE_TABS before attempting to hide it. Users can currently hide the RECENT tab (which is defined as always visible), creating an inconsistent state.

Add a guard clause at the beginning:

toggleTabVisibility: (tab: SidebarTabs) => {
  if (ALWAYS_VISIBLE_TABS.includes(tab)) {
    return;
  }
  set(state => {
    // ... rest of logic
  });
},

Copilot uses AI. Check for mistakes.
Comment on lines +156 to +164
<div css={dropdownCheckboxItem} role="menuitemcheckbox" aria-checked={visibleTabs.includes(tab.type)}>
<Checkbox
wrapperCSS={roundCheckbox}
checked={visibleTabs.includes(tab.type)}
onChange={() => toggleTabVisibility(tab.type)}
>
<CheckboxLabel css={checkboxLabel}>{tab.label}</CheckboxLabel>
</Checkbox>
</div>
Copy link

Copilot AI Nov 24, 2025

Choose a reason for hiding this comment

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

[Important] The checkbox items in the dropdown menu lack keyboard navigation support. Users cannot navigate between checkboxes using arrow keys, which is expected behavior for role="menu" with role="menuitemcheckbox" items. Additionally, the checkboxes should be clickable via the Space or Enter keys when focused.

Consider implementing keyboard event handlers for Up/Down arrow keys to navigate between items, and ensure Space/Enter keys toggle the selected item.

Copilot uses AI. Check for mistakes.
Comment on lines +59 to +63
// Check if plainMessage has actual content (not just whitespace)
const plainMessage = draft.plainMessage || '';
const hasTextContent = plainMessage.trim().length > 0;

return hasTextContent;
Copy link

Copilot AI Nov 24, 2025

Choose a reason for hiding this comment

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

[Important] The draft detection logic only checks plainMessage but ignores editorState. In the previous version (visible in the diff context), drafts with only editorState (and no plainMessage) were correctly detected. The new implementation will incorrectly return false for drafts that have rich text content stored in editorState but no plain text version.

Consider checking both fields:

const hasTextContent = plainMessage.trim().length > 0;
const hasEditorState = Boolean(draft.editorState);
return hasTextContent || hasEditorState;

Copilot uses AI. Check for mistakes.
Comment on lines +130 to +138
const channelConversationsLength = useMemo(
() => channelConversations.filter(filterUnreadAndArchivedConversations).length,
[channelConversations],
);

const groupConversationsLength = useMemo(
() => groupConversations.filter(filterUnreadAndArchivedConversations).length,
[groupConversations],
);
Copy link

Copilot AI Nov 24, 2025

Choose a reason for hiding this comment

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

[Suggestion] The useMemo hooks for channelConversationsLength and groupConversationsLength (lines 130-138) don't include filterUnreadAndArchivedConversations in their dependency arrays. While this function is defined in the component and uses conversationFilters, it's technically stable. However, it references external functions which could theoretically change. Consider either:

  1. Moving filterUnreadAndArchivedConversations outside the component as a module-level function, or
  2. Wrapping it with useCallback and including it in the dependency arrays

This ensures the memoization is correctly invalidated if the filter logic ever changes.

Copilot uses AI. Check for mistakes.
Comment on lines +259 to 267
if (shouldShowChannelTab) {
conversationTabs.splice(7, 0, {
type: SidebarTabs.CHANNELS,
title: t('conversationLabelChannels'),
dataUieName: 'go-channels-view',
Icon: <ChannelIcon />,
unreadConversations: channelConversationsLength,
});
}
Copy link

Copilot AI Nov 24, 2025

Choose a reason for hiding this comment

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

[Important] The channel tab insertion logic uses a hardcoded index 7 which is fragile and error-prone. If the order or number of tabs in the conversationTabs array changes (e.g., if someone removes or reorders tabs), the channel tab will be inserted at the wrong position.

Consider refactoring to insert the channel tab relative to a specific tab (e.g., after GROUPS or DIRECTS) or add it to the array based on a logical position marker. Example:

const groupsIndex = conversationTabs.findIndex(tab => tab.type === SidebarTabs.GROUPS);
if (groupsIndex !== -1) {
  conversationTabs.splice(groupsIndex + 1, 0, { ... });
}

Copilot uses AI. Check for mistakes.
Comment on lines +105 to +116
const availableTabs = [
{type: SidebarTabs.FAVORITES, label: t('conversationLabelFavorites')},
{type: SidebarTabs.GROUPS, label: t('conversationLabelGroups')},
{type: SidebarTabs.DIRECTS, label: t('conversationLabelDirects')},
{type: SidebarTabs.FOLDER, label: t('folderViewTooltip')},
{type: SidebarTabs.ARCHIVES, label: t('conversationFooterArchive')},
{type: SidebarTabs.UNREAD, label: t('conversationLabelUnread')},
{type: SidebarTabs.MENTIONS, label: t('conversationLabelMentions')},
{type: SidebarTabs.REPLIES, label: t('conversationLabelReplies')},
{type: SidebarTabs.DRAFTS, label: t('conversationLabelDrafts')},
{type: SidebarTabs.PINGS, label: t('conversationLabelPings')},
];
Copy link

Copilot AI Nov 24, 2025

Choose a reason for hiding this comment

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

[Important] The availableTabs array doesn't exclude tabs from ALWAYS_VISIBLE_TABS (specifically SidebarTabs.RECENT). This means users will see the RECENT tab as an option in the filter dropdown, but toggling it will have no effect due to the isTabVisible function. This creates a confusing UX where the checkbox appears to work but the tab remains visible.

The RECENT tab should be excluded from the availableTabs array entirely since it cannot be hidden.

Copilot uses AI. Check for mistakes.
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