snapshot of https://github.com/RocketChat/Rocket.Chat.git (develop, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 1s
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 1s
This commit is contained in:
39
apps/meteor/client/sidebar/hooks/useAvatarTemplate.tsx
Normal file
39
apps/meteor/client/sidebar/hooks/useAvatarTemplate.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import { RoomAvatar } from '@rocket.chat/ui-avatar';
|
||||
import type { SubscriptionWithRoom } from '@rocket.chat/ui-contexts';
|
||||
import { useUserPreference } from '@rocket.chat/ui-contexts';
|
||||
import type { ComponentType } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
export const useAvatarTemplate = (
|
||||
sidebarViewMode?: 'extended' | 'medium' | 'condensed',
|
||||
sidebarDisplayAvatar?: boolean,
|
||||
): null | ComponentType<SubscriptionWithRoom & { rid: string }> => {
|
||||
const sidebarViewModeFromSettings = useUserPreference<'extended' | 'medium' | 'condensed'>('sidebarViewMode');
|
||||
const sidebarDisplayAvatarFromSettings = useUserPreference('sidebarDisplayAvatar');
|
||||
|
||||
const viewMode = sidebarViewMode ?? sidebarViewModeFromSettings;
|
||||
const displayAvatar = sidebarDisplayAvatar ?? sidebarDisplayAvatarFromSettings;
|
||||
return useMemo(() => {
|
||||
if (!displayAvatar) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const size = ((): 'x36' | 'x28' | 'x20' => {
|
||||
switch (viewMode) {
|
||||
case 'extended':
|
||||
return 'x36';
|
||||
case 'medium':
|
||||
return 'x28';
|
||||
case 'condensed':
|
||||
default:
|
||||
return 'x20';
|
||||
}
|
||||
})();
|
||||
|
||||
const renderRoomAvatar: ComponentType<SubscriptionWithRoom & { rid: string }> = (room) => (
|
||||
<RoomAvatar size={size} room={{ ...room, _id: room.rid || room._id, type: room.t }} />
|
||||
);
|
||||
|
||||
return renderRoomAvatar;
|
||||
}, [displayAvatar, viewMode]);
|
||||
};
|
||||
125
apps/meteor/client/sidebar/hooks/useCategoryList.spec.ts
Normal file
125
apps/meteor/client/sidebar/hooks/useCategoryList.spec.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
import { useSetting, useUserPreference } from '@rocket.chat/ui-contexts';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
|
||||
import { SIDEBAR_DYNAMIC_GROUP_KEYS, mergeWithSectionsOrder, useCategoryList, withDynamicFirst } from './useCategoryList';
|
||||
import { useHasLicenseModule } from '../../hooks/useHasLicenseModule';
|
||||
|
||||
jest.mock('@rocket.chat/ui-contexts', () => ({
|
||||
useSetting: jest.fn(),
|
||||
useUserPreference: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../../hooks/useHasLicenseModule', () => ({
|
||||
useHasLicenseModule: jest.fn(),
|
||||
}));
|
||||
|
||||
const mockedUseSetting = jest.mocked(useSetting);
|
||||
const mockedUseUserPreference = jest.mocked(useUserPreference);
|
||||
const mockedUseHasLicenseModule = jest.mocked(useHasLicenseModule);
|
||||
|
||||
const STATIC_KEYS = ['Favorites', 'Teams', 'Discussions', 'Channels', 'Direct_Messages', 'Conversations'] as const;
|
||||
const DYNAMIC_KEYS = [...SIDEBAR_DYNAMIC_GROUP_KEYS];
|
||||
const ALL_KEYS = [...DYNAMIC_KEYS, ...STATIC_KEYS];
|
||||
|
||||
describe('mergeWithSectionsOrder', () => {
|
||||
it('inserts a missing key before its first successor already in the list', () => {
|
||||
const result = mergeWithSectionsOrder(['Favorites', 'Channels'], ALL_KEYS);
|
||||
expect(result.indexOf('Unread')).toBeLessThan(result.indexOf('Favorites'));
|
||||
});
|
||||
|
||||
it('does not move keys that are already present', () => {
|
||||
const result = mergeWithSectionsOrder(['Channels', 'Favorites'], ALL_KEYS);
|
||||
expect(result.indexOf('Channels')).toBeLessThan(result.indexOf('Favorites'));
|
||||
});
|
||||
|
||||
it('appends a key with no successor in the list to the end', () => {
|
||||
const result = mergeWithSectionsOrder(['Favorites'], ALL_KEYS);
|
||||
expect(result.at(-1)).toBe('Conversations');
|
||||
});
|
||||
|
||||
it('returns all sectionsOrder keys when explicitIds is empty', () => {
|
||||
const result = mergeWithSectionsOrder([], ALL_KEYS);
|
||||
expect(result).toEqual(ALL_KEYS);
|
||||
});
|
||||
});
|
||||
|
||||
describe('withDynamicFirst', () => {
|
||||
it('places all dynamic groups before any static group', () => {
|
||||
const result = withDynamicFirst(['Favorites', 'Channels'], ALL_KEYS);
|
||||
const lastDynamicIdx = Math.max(...DYNAMIC_KEYS.map((k) => result.indexOf(k)));
|
||||
const firstStaticIdx = Math.min(...STATIC_KEYS.map((k) => result.indexOf(k)));
|
||||
expect(lastDynamicIdx).toBeLessThan(firstStaticIdx);
|
||||
});
|
||||
|
||||
it('includes all dynamic keys from sectionsOrder even if absent from ids', () => {
|
||||
const result = withDynamicFirst(['Favorites'], ALL_KEYS);
|
||||
for (const dk of DYNAMIC_KEYS) {
|
||||
expect(result).toContain(dk);
|
||||
}
|
||||
});
|
||||
|
||||
it('places a new custom category first among static groups', () => {
|
||||
const customId = 'custom-xyz';
|
||||
const result = withDynamicFirst([customId, 'Favorites', 'Channels'], ALL_KEYS);
|
||||
for (const dk of DYNAMIC_KEYS) {
|
||||
expect(result.indexOf(dk)).toBeLessThan(result.indexOf(customId));
|
||||
}
|
||||
expect(result.indexOf(customId)).toBeLessThan(result.indexOf('Favorites'));
|
||||
expect(result.indexOf(customId)).toBeLessThan(result.indexOf('Channels'));
|
||||
});
|
||||
|
||||
it('preserves user-defined order among static groups', () => {
|
||||
const result = withDynamicFirst(['Channels', 'Favorites'], ALL_KEYS);
|
||||
expect(result.indexOf('Channels')).toBeLessThan(result.indexOf('Favorites'));
|
||||
});
|
||||
|
||||
it('corrects a dynamic group that was placed in the middle of statics', () => {
|
||||
const result = withDynamicFirst(['Favorites', 'Unread', 'Channels'], ALL_KEYS);
|
||||
expect(result.indexOf('Unread')).toBeLessThan(result.indexOf('Favorites'));
|
||||
expect(result.indexOf('Unread')).toBeLessThan(result.indexOf('Channels'));
|
||||
expect(result.indexOf('Favorites')).toBeLessThan(result.indexOf('Channels'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('useCategoryList', () => {
|
||||
const preferences: Record<string, unknown> = {};
|
||||
let hasLicenseModule = false;
|
||||
|
||||
beforeEach(() => {
|
||||
for (const key of Object.keys(preferences)) delete preferences[key];
|
||||
Object.assign(preferences, { sidebarGroupByType: false, sidebarShowFavorites: true, sidebarShowUnread: true });
|
||||
hasLicenseModule = false;
|
||||
|
||||
mockedUseUserPreference.mockImplementation((key: string, defaultValue?: unknown) => preferences[key] ?? defaultValue);
|
||||
mockedUseSetting.mockImplementation((_key: string, defaultValue?: unknown) => defaultValue);
|
||||
mockedUseHasLicenseModule.mockImplementation(() => ({ data: hasLicenseModule }) as any);
|
||||
});
|
||||
|
||||
const categoryList = () => renderHook(() => useCategoryList(false, false)).result.current;
|
||||
|
||||
it('ignores a section key of a system group that no longer exists', () => {
|
||||
preferences.sidebarSectionsOrder = ['Incoming_Calls', 'Unread', 'Drafts', 'Favorites', 'Conversations'];
|
||||
|
||||
expect(categoryList()).toEqual(['Incoming_Calls', 'Unread', 'Favorites', 'Conversations']);
|
||||
});
|
||||
|
||||
it('ignores a stale section key with the license module enabled', () => {
|
||||
hasLicenseModule = true;
|
||||
preferences.sidebarSectionsOrder = ['Incoming_Calls', 'Unread', 'Drafts', 'Favorites', 'Conversations'];
|
||||
|
||||
expect(categoryList()).not.toContain('Drafts');
|
||||
});
|
||||
|
||||
it('ignores a stale entry already persisted in sidebarCategories', () => {
|
||||
hasLicenseModule = true;
|
||||
preferences.sidebarSectionsOrder = ['Incoming_Calls', 'Unread', 'Favorites', 'Conversations'];
|
||||
preferences.sidebarCategories = [
|
||||
{ _id: 'Drafts', name: 'Drafts', default: true },
|
||||
{ _id: 'custom-xyz', name: 'Work' },
|
||||
];
|
||||
|
||||
const result = categoryList();
|
||||
expect(result).not.toContain('Drafts');
|
||||
expect(result).toContain('custom-xyz');
|
||||
});
|
||||
});
|
||||
218
apps/meteor/client/sidebar/hooks/useCategoryList.ts
Normal file
218
apps/meteor/client/sidebar/hooks/useCategoryList.ts
Normal file
@@ -0,0 +1,218 @@
|
||||
import type { SubscriptionWithRoom } from '@rocket.chat/ui-contexts';
|
||||
import { useSetting, useUserPreference } from '@rocket.chat/ui-contexts';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { useSidebarSectionsOrder } from './useSidebarSectionsOrder';
|
||||
import { useHasLicenseModule } from '../../hooks/useHasLicenseModule';
|
||||
import { useUserSidebarCategories } from '../categories/hooks/useUserSidebarCategories';
|
||||
|
||||
type FilterSystemCategoriesOptions = {
|
||||
showOmnichannel: boolean;
|
||||
inquiriesEnabled: boolean;
|
||||
sidebarGroupByType: boolean;
|
||||
favoritesEnabled: boolean;
|
||||
sidebarShowUnread: boolean;
|
||||
isDiscussionEnabled: boolean;
|
||||
};
|
||||
|
||||
const filterSystemCategories = (categories: readonly string[], options: FilterSystemCategoriesOptions) => {
|
||||
const { showOmnichannel, inquiriesEnabled, sidebarGroupByType, favoritesEnabled, sidebarShowUnread, isDiscussionEnabled } = options;
|
||||
return categories.filter((key) => {
|
||||
switch (key) {
|
||||
case 'Incoming_Livechats':
|
||||
return showOmnichannel && inquiriesEnabled;
|
||||
case 'On_Hold_Chats':
|
||||
case 'Open_Livechats':
|
||||
return showOmnichannel;
|
||||
case 'Discussions':
|
||||
return sidebarGroupByType && isDiscussionEnabled;
|
||||
case 'Teams':
|
||||
case 'Channels':
|
||||
case 'Direct_Messages':
|
||||
return sidebarGroupByType;
|
||||
case 'Conversations':
|
||||
return !sidebarGroupByType;
|
||||
case 'Unread':
|
||||
return sidebarShowUnread;
|
||||
case 'Favorites':
|
||||
return favoritesEnabled;
|
||||
case 'Incoming_Calls':
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const SIDEBAR_DYNAMIC_GROUP_KEYS: readonly string[] = [
|
||||
'Incoming_Calls',
|
||||
'Incoming_Livechats',
|
||||
'Open_Livechats',
|
||||
'On_Hold_Chats',
|
||||
'Unread',
|
||||
];
|
||||
|
||||
export const mergeWithSectionsOrder = (explicitIds: string[], sectionsOrder: readonly string[]): string[] => {
|
||||
const merged = [...explicitIds];
|
||||
for (const key of sectionsOrder) {
|
||||
if (merged.includes(key)) continue;
|
||||
const successorIdx = sectionsOrder
|
||||
.slice(sectionsOrder.indexOf(key) + 1)
|
||||
.map((k) => merged.indexOf(k))
|
||||
.find((pos) => pos !== -1);
|
||||
merged.splice(successorIdx ?? merged.length, 0, key);
|
||||
}
|
||||
return merged;
|
||||
};
|
||||
|
||||
export const withDynamicFirst = (ids: string[], sectionsOrder: readonly string[]): string[] => {
|
||||
const dynamicInOrder = sectionsOrder.filter((k) => SIDEBAR_DYNAMIC_GROUP_KEYS.includes(k));
|
||||
const staticIds = ids.filter((k) => !SIDEBAR_DYNAMIC_GROUP_KEYS.includes(k));
|
||||
const staticOrder = sectionsOrder.filter((k) => !SIDEBAR_DYNAMIC_GROUP_KEYS.includes(k));
|
||||
return [...dynamicInOrder, ...mergeWithSectionsOrder(staticIds, staticOrder)];
|
||||
};
|
||||
|
||||
export const filterGroupVisibility = <T>(
|
||||
groups: Map<string, Set<SubscriptionWithRoom>>,
|
||||
hasLicenseModule: boolean,
|
||||
makeGroup: (key: string, set: Set<SubscriptionWithRoom>) => T,
|
||||
): T[] => {
|
||||
const filteredGroups: T[] = [];
|
||||
groups.forEach((group, key) => {
|
||||
if (!group) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (key === 'Conversations') {
|
||||
filteredGroups.push(makeGroup(key, group));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!hasLicenseModule || SIDEBAR_DYNAMIC_GROUP_KEYS.includes(key)) {
|
||||
if (group.size > 0) {
|
||||
filteredGroups.push(makeGroup(key, group));
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
return filteredGroups.push(makeGroup(key, group));
|
||||
});
|
||||
|
||||
return filteredGroups;
|
||||
};
|
||||
|
||||
const isUnreadRoom = (room: SubscriptionWithRoom): boolean =>
|
||||
!room.hideUnreadStatus && Boolean(room.alert || room.unread || room.tunread?.length);
|
||||
|
||||
export const getRoomCategory = (
|
||||
room: SubscriptionWithRoom,
|
||||
{
|
||||
groups,
|
||||
hasIncomingCalls,
|
||||
}: {
|
||||
groups: Map<string, Set<SubscriptionWithRoom>>;
|
||||
hasIncomingCalls: (room: SubscriptionWithRoom['rid']) => boolean;
|
||||
},
|
||||
): string | false => {
|
||||
if (room.archived) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (hasIncomingCalls(room.rid)) {
|
||||
return 'Incoming_Calls';
|
||||
}
|
||||
|
||||
if (isUnreadRoom(room) && groups.has('Unread')) {
|
||||
return 'Unread';
|
||||
}
|
||||
|
||||
if (room.category && groups.has(room.category)) {
|
||||
return room.category;
|
||||
}
|
||||
|
||||
if (room.f && groups.has('Favorites')) {
|
||||
return 'Favorites';
|
||||
}
|
||||
|
||||
if (room.teamMain && groups.has('Teams')) {
|
||||
return 'Teams';
|
||||
}
|
||||
|
||||
if (room.prid && groups.has('Discussions')) {
|
||||
return 'Discussions';
|
||||
}
|
||||
|
||||
if (room.t === 'l' && room.onHold) {
|
||||
if (!groups.has('On_Hold_Chats')) {
|
||||
return false;
|
||||
}
|
||||
return 'On_Hold_Chats';
|
||||
}
|
||||
|
||||
if (room.t === 'l') {
|
||||
if (!groups.has('Open_Livechats')) {
|
||||
return false;
|
||||
}
|
||||
return 'Open_Livechats';
|
||||
}
|
||||
|
||||
if ((room.t === 'c' || room.t === 'p') && groups.has('Channels')) {
|
||||
return 'Channels';
|
||||
}
|
||||
|
||||
if (room.t === 'd' && groups.has('Direct_Messages')) {
|
||||
return 'Direct_Messages';
|
||||
}
|
||||
|
||||
return 'Conversations';
|
||||
};
|
||||
|
||||
export const useCategoryList = (showOmnichannel: boolean, inquiriesEnabled: boolean) => {
|
||||
const { data: hasLicenseModule = false } = useHasLicenseModule('experimental-enterprise-features');
|
||||
const { rawCategories: sidebarCategories } = useUserSidebarCategories();
|
||||
const sidebarSectionsOrder = useSidebarSectionsOrder();
|
||||
const sidebarGroupByType = useUserPreference<boolean>('sidebarGroupByType') ?? false;
|
||||
const favoritesEnabled = useUserPreference<boolean>('sidebarShowFavorites', true) ?? true;
|
||||
const isDiscussionEnabled = useSetting('Discussion_enabled', true) ?? true;
|
||||
const sidebarShowUnread = useUserPreference<boolean>('sidebarShowUnread', false) ?? false;
|
||||
|
||||
const categoryList = useMemo(() => {
|
||||
if (hasLicenseModule) {
|
||||
return filterSystemCategories(
|
||||
mergeWithSectionsOrder(
|
||||
sidebarCategories.map(({ _id }) => _id),
|
||||
sidebarSectionsOrder,
|
||||
),
|
||||
{
|
||||
showOmnichannel,
|
||||
inquiriesEnabled,
|
||||
sidebarGroupByType,
|
||||
favoritesEnabled,
|
||||
sidebarShowUnread,
|
||||
isDiscussionEnabled,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return filterSystemCategories(sidebarSectionsOrder, {
|
||||
showOmnichannel,
|
||||
inquiriesEnabled,
|
||||
sidebarGroupByType,
|
||||
favoritesEnabled,
|
||||
sidebarShowUnread,
|
||||
isDiscussionEnabled,
|
||||
});
|
||||
}, [
|
||||
sidebarCategories,
|
||||
sidebarSectionsOrder,
|
||||
hasLicenseModule,
|
||||
showOmnichannel,
|
||||
inquiriesEnabled,
|
||||
sidebarGroupByType,
|
||||
favoritesEnabled,
|
||||
sidebarShowUnread,
|
||||
isDiscussionEnabled,
|
||||
]);
|
||||
|
||||
return categoryList;
|
||||
};
|
||||
30
apps/meteor/client/sidebar/hooks/useCollapsedGroups.ts
Normal file
30
apps/meteor/client/sidebar/hooks/useCollapsedGroups.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { useLocalStorage } from '@rocket.chat/fuselage-hooks';
|
||||
import type { KeyboardEvent } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
export const useCollapsedGroups = () => {
|
||||
const [collapsedGroups, setCollapsedGroups] = useLocalStorage<string[]>('sidebarGroups', []);
|
||||
|
||||
const handleClick = useCallback(
|
||||
(group: string) => {
|
||||
if (collapsedGroups.includes(group)) {
|
||||
setCollapsedGroups(collapsedGroups.filter((item) => item !== group));
|
||||
} else {
|
||||
setCollapsedGroups([...collapsedGroups, group]);
|
||||
}
|
||||
},
|
||||
[collapsedGroups, setCollapsedGroups],
|
||||
);
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(event: KeyboardEvent, group: string) => {
|
||||
if (['Enter', 'Space'].includes(event.code)) {
|
||||
event.preventDefault();
|
||||
handleClick(group);
|
||||
}
|
||||
},
|
||||
[handleClick],
|
||||
);
|
||||
|
||||
return { collapsedGroups, handleClick, handleKeyDown };
|
||||
};
|
||||
21
apps/meteor/client/sidebar/hooks/usePreventDefault.ts
Normal file
21
apps/meteor/client/sidebar/hooks/usePreventDefault.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import type { RefObject } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
export const usePreventDefault = (ref: RefObject<Element | null>): { ref: RefObject<Element | null> } => {
|
||||
// Flowrouter uses an addEventListener on the document to capture any clink link, since the react synthetic event use an addEventListener on the document too,
|
||||
// it is impossible/hard to determine which one will happen before and prevent/stop propagation, so feel free to remove this effect after remove flow router :)
|
||||
|
||||
useEffect(() => {
|
||||
const { current } = ref;
|
||||
const stopPropagation: EventListener = (e) => {
|
||||
if ([(e.target as HTMLElement).nodeName, (e.target as HTMLElement).parentElement?.nodeName].includes('BUTTON')) {
|
||||
e.preventDefault();
|
||||
}
|
||||
};
|
||||
current?.addEventListener('click', stopPropagation);
|
||||
|
||||
return (): void => current?.removeEventListener('click', stopPropagation);
|
||||
}, [ref]);
|
||||
|
||||
return { ref };
|
||||
};
|
||||
391
apps/meteor/client/sidebar/hooks/useRoomList.spec.tsx
Normal file
391
apps/meteor/client/sidebar/hooks/useRoomList.spec.tsx
Normal file
@@ -0,0 +1,391 @@
|
||||
import { mockAppRoot } from '@rocket.chat/mock-providers';
|
||||
import type { SubscriptionWithRoom } from '@rocket.chat/ui-contexts';
|
||||
import { VideoConfContext } from '@rocket.chat/ui-video-conf';
|
||||
import { renderHook, waitFor } from '@testing-library/react';
|
||||
|
||||
import { useRoomList } from './useRoomList';
|
||||
import type { SidebarRoomListGroup } from './useRoomList';
|
||||
import { createFakeLicenseInfo, createFakeRoom, createFakeSubscription, createFakeUser } from '../../../tests/mocks/data';
|
||||
|
||||
let mockOpenedRoom: string | undefined;
|
||||
|
||||
jest.mock('../../lib/RoomManager', () => ({
|
||||
useOpenedRoom: () => mockOpenedRoom,
|
||||
}));
|
||||
|
||||
afterEach(() => {
|
||||
mockOpenedRoom = undefined;
|
||||
});
|
||||
|
||||
// The hook returns a rich `groups` array; these helpers reproduce the legacy flat views used by the assertions.
|
||||
const groupsListOf = (groups: SidebarRoomListGroup[]) => groups.map((group) => group.key);
|
||||
const roomListOf = (groups: SidebarRoomListGroup[]) => groups.flatMap((group) => group.rooms);
|
||||
|
||||
const user = createFakeUser({
|
||||
active: true,
|
||||
roles: ['admin'],
|
||||
type: 'user',
|
||||
});
|
||||
|
||||
const emptyUnread = {
|
||||
userMentions: 0,
|
||||
groupMentions: 0,
|
||||
unread: 0,
|
||||
tunread: undefined,
|
||||
tunreadUser: undefined,
|
||||
tunreadGroup: undefined,
|
||||
alert: false,
|
||||
};
|
||||
|
||||
const unreadChannels = [
|
||||
{ ...createFakeSubscription({ t: 'c', tunread: ['1'] }), ...createFakeRoom({ t: 'c' }) },
|
||||
{ ...createFakeSubscription({ t: 'c', tunread: ['1'] }), ...createFakeRoom({ t: 'c' }) },
|
||||
{ ...createFakeSubscription({ t: 'c', tunreadUser: ['1'] }), ...createFakeRoom({ t: 'c' }) },
|
||||
{ ...createFakeSubscription({ t: 'c', tunreadUser: ['1'] }), ...createFakeRoom({ t: 'c' }) },
|
||||
];
|
||||
|
||||
const favoriteRooms = [
|
||||
{ ...createFakeSubscription({ t: 'c', f: true, ...emptyUnread }), ...createFakeRoom({ t: 'c' }) },
|
||||
{ ...createFakeSubscription({ t: 'c', f: true, ...emptyUnread }), ...createFakeRoom({ t: 'c' }) },
|
||||
{ ...createFakeSubscription({ t: 'c', f: true, ...emptyUnread }), ...createFakeRoom({ t: 'c' }) },
|
||||
];
|
||||
|
||||
const teams = [
|
||||
{ ...createFakeSubscription({ ...emptyUnread }), ...createFakeRoom({ teamMain: true }) },
|
||||
{ ...createFakeSubscription({ ...emptyUnread }), ...createFakeRoom({ teamMain: true }) },
|
||||
{ ...createFakeSubscription({ ...emptyUnread }), ...createFakeRoom({ teamMain: true }) },
|
||||
{ ...createFakeSubscription({ ...emptyUnread }), ...createFakeRoom({ teamMain: true }) },
|
||||
{ ...createFakeSubscription({ ...emptyUnread }), ...createFakeRoom({ teamMain: true }) },
|
||||
];
|
||||
|
||||
const discussionRooms = [
|
||||
{ ...createFakeSubscription({ ...emptyUnread }), ...createFakeRoom({ prid: '123' }) },
|
||||
{ ...createFakeSubscription({ ...emptyUnread }), ...createFakeRoom({ prid: '124' }) },
|
||||
{ ...createFakeSubscription({ ...emptyUnread }), ...createFakeRoom({ prid: '125' }) },
|
||||
{ ...createFakeSubscription({ ...emptyUnread }), ...createFakeRoom({ prid: '126' }) },
|
||||
{ ...createFakeSubscription({ ...emptyUnread }), ...createFakeRoom({ prid: '127' }) },
|
||||
];
|
||||
|
||||
const directRooms = [
|
||||
{ ...createFakeSubscription({ t: 'd', ...emptyUnread }), ...createFakeRoom({ t: 'd' }) },
|
||||
{ ...createFakeSubscription({ t: 'd', ...emptyUnread }), ...createFakeRoom({ t: 'd' }) },
|
||||
{ ...createFakeSubscription({ t: 'd', ...emptyUnread }), ...createFakeRoom({ t: 'd' }) },
|
||||
{ ...createFakeSubscription({ t: 'd', ...emptyUnread }), ...createFakeRoom({ t: 'd' }) },
|
||||
];
|
||||
|
||||
const fakeRooms = [...unreadChannels, ...favoriteRooms, ...teams, ...discussionRooms, ...directRooms];
|
||||
|
||||
const emptyArr: any[] = [];
|
||||
|
||||
const getWrapperSettings = ({
|
||||
sidebarGroupByType = false,
|
||||
sidebarShowFavorites = true,
|
||||
sidebarShowUnread = false,
|
||||
isDiscussionEnabled = false,
|
||||
isEnterprise = false,
|
||||
fakeRoom = undefined,
|
||||
rooms = fakeRooms as unknown as SubscriptionWithRoom[],
|
||||
sidebarCategories = [],
|
||||
}: {
|
||||
sidebarGroupByType?: boolean;
|
||||
sidebarShowFavorites?: boolean;
|
||||
sidebarShowUnread?: boolean;
|
||||
isDiscussionEnabled?: boolean;
|
||||
isEnterprise?: boolean;
|
||||
fakeRoom?: SubscriptionWithRoom;
|
||||
rooms?: SubscriptionWithRoom[];
|
||||
sidebarCategories?: { _id: string; name?: string; default?: boolean; showUnreads?: boolean; keepUnreadsOnTop?: boolean }[];
|
||||
}) => {
|
||||
const root = mockAppRoot()
|
||||
.wrap((children) => (
|
||||
<VideoConfContext.Provider
|
||||
value={
|
||||
{
|
||||
queryIncomingCalls: () => [() => () => undefined, () => emptyArr],
|
||||
} as any
|
||||
}
|
||||
>
|
||||
{children}
|
||||
</VideoConfContext.Provider>
|
||||
))
|
||||
.withUser(user)
|
||||
.withSubscriptions([...rooms, fakeRoom && fakeRoom].filter(Boolean) as unknown as SubscriptionWithRoom[])
|
||||
.withUserPreference('sidebarGroupByType', sidebarGroupByType)
|
||||
.withUserPreference('sidebarShowFavorites', sidebarShowFavorites)
|
||||
.withUserPreference('sidebarShowUnread', sidebarShowUnread)
|
||||
.withUserPreference('sidebarCategories', sidebarCategories)
|
||||
.withSetting('Discussion_enabled', isDiscussionEnabled);
|
||||
|
||||
if (isEnterprise) {
|
||||
root.withEndpoint('GET', '/v1/licenses.info', () => ({
|
||||
license: createFakeLicenseInfo({ hasValidLicense: true, activeModules: ['experimental-enterprise-features'] }),
|
||||
}));
|
||||
}
|
||||
|
||||
return root;
|
||||
};
|
||||
|
||||
it('should return roomList, groupsCount and groupsList', async () => {
|
||||
const { result } = renderHook(() => useRoomList({ collapsedGroups: [] }), {
|
||||
wrapper: getWrapperSettings({}).build(),
|
||||
});
|
||||
|
||||
expect(roomListOf(result.current.groups)).toBeDefined();
|
||||
expect(groupsListOf(result.current.groups)).toBeDefined();
|
||||
expect(result.current.groupsCount).toBeDefined();
|
||||
});
|
||||
|
||||
it('should return groupsCount with the correct count', async () => {
|
||||
const { result } = renderHook(() => useRoomList({ collapsedGroups: [] }), {
|
||||
// Hide custom categories and Favorites so every room folds into a single "Conversations" group.
|
||||
wrapper: getWrapperSettings({ sidebarShowFavorites: false }).build(),
|
||||
});
|
||||
|
||||
const { groupsCount } = result.current;
|
||||
const roomList = roomListOf(result.current.groups);
|
||||
|
||||
expect(groupsCount).toContain(fakeRooms.length);
|
||||
expect(groupsCount).not.toContain(fakeRooms.length + 5);
|
||||
expect(groupsCount.reduce((a, b) => a + b, 0)).toBe(fakeRooms.length);
|
||||
expect(groupsCount.reduce((a, b) => a + b, 0)).toEqual(roomList.length);
|
||||
});
|
||||
|
||||
it('should return roomList with the subscribed rooms and the correct length', async () => {
|
||||
const { result } = renderHook(() => useRoomList({ collapsedGroups: [] }), {
|
||||
wrapper: getWrapperSettings({}).build(),
|
||||
});
|
||||
|
||||
const roomList = roomListOf(result.current.groups);
|
||||
expect(roomList).toContain(fakeRooms[0]);
|
||||
expect(roomList).toHaveLength(fakeRooms.length);
|
||||
});
|
||||
|
||||
it('should return groupsList with "Conversations" if preference sidebarGroupByType is not enabled', async () => {
|
||||
const { result } = renderHook(() => useRoomList({ collapsedGroups: [] }), {
|
||||
// Hide custom categories and Favorites so the only group is "Conversations".
|
||||
wrapper: getWrapperSettings({ sidebarShowFavorites: false }).build(),
|
||||
});
|
||||
|
||||
const groupsList = groupsListOf(result.current.groups);
|
||||
expect(groupsList).toContain('Conversations');
|
||||
expect(groupsList).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should return groupsList with "Teams" if sidebarGroupByType is enabled and roomList has teams', async () => {
|
||||
const { result } = renderHook(() => useRoomList({ collapsedGroups: [] }), {
|
||||
wrapper: getWrapperSettings({ sidebarGroupByType: true }).build(),
|
||||
});
|
||||
|
||||
const groupsList = groupsListOf(result.current.groups);
|
||||
const teamsIndex = groupsList.indexOf('Teams');
|
||||
expect(groupsList).toContain('Teams');
|
||||
expect(result.current.groupsCount[teamsIndex]).toEqual(teams.length);
|
||||
});
|
||||
|
||||
it('should group favorites into a "Favorites" group when sidebarShowFavorites is on', async () => {
|
||||
const { result } = renderHook(() => useRoomList({ collapsedGroups: [] }), {
|
||||
wrapper: getWrapperSettings({ sidebarShowFavorites: true, sidebarGroupByType: true }).build(),
|
||||
});
|
||||
|
||||
const groupsList = groupsListOf(result.current.groups);
|
||||
const favoritesIndex = groupsList.indexOf('Favorites');
|
||||
expect(groupsList).toContain('Favorites');
|
||||
expect(result.current.groupsCount[favoritesIndex]).toEqual(favoriteRooms.length);
|
||||
});
|
||||
|
||||
it('should not group favorites when sidebarShowFavorites is off', async () => {
|
||||
const { result } = renderHook(() => useRoomList({ collapsedGroups: [] }), {
|
||||
wrapper: getWrapperSettings({ sidebarShowFavorites: false, sidebarGroupByType: true }).build(),
|
||||
});
|
||||
expect(groupsListOf(result.current.groups)).not.toContain('Favorites');
|
||||
});
|
||||
|
||||
it('should group unread rooms into an "Unread" group when sidebarShowUnread is on', async () => {
|
||||
const { result } = renderHook(() => useRoomList({ collapsedGroups: [] }), {
|
||||
wrapper: getWrapperSettings({ sidebarShowUnread: true, sidebarGroupByType: true }).build(),
|
||||
});
|
||||
|
||||
const groupsList = groupsListOf(result.current.groups);
|
||||
const unreadIndex = groupsList.indexOf('Unread');
|
||||
expect(groupsList).toContain('Unread');
|
||||
// The four seeded unread channels are pulled into the Unread group.
|
||||
expect(result.current.groupsCount[unreadIndex]).toEqual(unreadChannels.length);
|
||||
});
|
||||
|
||||
it('should not group unread rooms when sidebarShowUnread is off', async () => {
|
||||
const { result } = renderHook(() => useRoomList({ collapsedGroups: [] }), {
|
||||
wrapper: getWrapperSettings({ sidebarShowUnread: false, sidebarGroupByType: true }).build(),
|
||||
});
|
||||
expect(groupsListOf(result.current.groups)).not.toContain('Unread');
|
||||
});
|
||||
|
||||
it('should return groupsList with "Discussions" if isDiscussionEnabled is enabled', async () => {
|
||||
const { result } = renderHook(() => useRoomList({ collapsedGroups: [] }), {
|
||||
wrapper: getWrapperSettings({ isDiscussionEnabled: true, sidebarGroupByType: true }).build(),
|
||||
});
|
||||
|
||||
const groupsList = groupsListOf(result.current.groups);
|
||||
const discussionIndex = groupsList.indexOf('Discussions');
|
||||
expect(groupsList).toContain('Discussions');
|
||||
expect(result.current.groupsCount[discussionIndex]).toEqual(discussionRooms.length);
|
||||
});
|
||||
|
||||
it('should return groupsList without "Discussions" if isDiscussionEnabled is disabled', async () => {
|
||||
const { result } = renderHook(() => useRoomList({ collapsedGroups: [] }), {
|
||||
wrapper: getWrapperSettings({ isDiscussionEnabled: false, sidebarGroupByType: true }).build(),
|
||||
});
|
||||
expect(groupsListOf(result.current.groups)).not.toContain('Discussions');
|
||||
});
|
||||
|
||||
it('should remove corresponding items from roomList and return groupCount 0 when group is collapsed and "Show unreads" is off', async () => {
|
||||
// "Show unreads" for Channels defaults to false — no preference entry needed.
|
||||
const { result } = renderHook(() => useRoomList({ collapsedGroups: ['Channels'] }), {
|
||||
wrapper: getWrapperSettings({ sidebarGroupByType: true, isEnterprise: true }).build(),
|
||||
});
|
||||
// hasLicenseModule resolves asynchronously from the mock endpoint.
|
||||
await waitFor(() => {
|
||||
const groupsList = groupsListOf(result.current.groups);
|
||||
const roomList = roomListOf(result.current.groups);
|
||||
const channelsIndex = groupsList.indexOf('Channels');
|
||||
expect(result.current.groupsCount[channelsIndex]).toEqual(0);
|
||||
expect(roomList.length).toEqual(result.current.groupsCount.reduce((a, b) => a + b, 0));
|
||||
});
|
||||
});
|
||||
|
||||
it('should hide all rooms and show a badge when a group is collapsed in CE ("Show unreads" is always off)', async () => {
|
||||
const { result } = renderHook(() => useRoomList({ collapsedGroups: ['Channels'] }), {
|
||||
wrapper: getWrapperSettings({ sidebarGroupByType: true }).build(),
|
||||
});
|
||||
const groupsList = groupsListOf(result.current.groups);
|
||||
const channelsIndex = groupsList.indexOf('Channels');
|
||||
// In CE showUnreads is always off — collapsing hides everything.
|
||||
expect(result.current.groupsCount[channelsIndex]).toEqual(0);
|
||||
// The header badge accumulates unread data from the hidden rooms.
|
||||
expect(result.current.groups[channelsIndex].unreadInfo.tunread.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should keep the opened room visible when its group is collapsed', async () => {
|
||||
// Every seeded DM is read, so without this behaviour collapsing "Direct_Messages" would hide all of them.
|
||||
mockOpenedRoom = directRooms[0].rid;
|
||||
const { result } = renderHook(() => useRoomList({ collapsedGroups: ['Direct_Messages'] }), {
|
||||
wrapper: getWrapperSettings({ sidebarGroupByType: true }).build(),
|
||||
});
|
||||
|
||||
const groupsList = groupsListOf(result.current.groups);
|
||||
const directIndex = groupsList.indexOf('Direct_Messages');
|
||||
expect(result.current.groupsCount[directIndex]).toEqual(1);
|
||||
expect(result.current.groups[directIndex].rooms).toEqual([directRooms[0]]);
|
||||
});
|
||||
|
||||
it('should not count the opened room in the header badge of its collapsed group', async () => {
|
||||
// The opened room stays visible with its own counters, so the collapsed header must not count it again.
|
||||
mockOpenedRoom = unreadChannels[0].rid;
|
||||
const { result } = renderHook(() => useRoomList({ collapsedGroups: ['Channels'] }), {
|
||||
wrapper: getWrapperSettings({ sidebarGroupByType: true }).build(),
|
||||
});
|
||||
|
||||
const groupsList = groupsListOf(result.current.groups);
|
||||
const channelsIndex = groupsList.indexOf('Channels');
|
||||
const hiddenChannels = unreadChannels.slice(1);
|
||||
const { unreadInfo } = result.current.groups[channelsIndex];
|
||||
|
||||
expect(result.current.groups[channelsIndex].rooms).toEqual([unreadChannels[0]]);
|
||||
expect(unreadInfo.tunread).toEqual(hiddenChannels.flatMap((room) => room.tunread || []));
|
||||
expect(unreadInfo.tunreadUser).toEqual(hiddenChannels.flatMap((room) => room.tunreadUser || []));
|
||||
expect(unreadInfo.unread).toEqual(hiddenChannels.reduce((acc, room) => acc + room.unread, 0));
|
||||
});
|
||||
|
||||
it('should not duplicate the opened room when a collapsed group already shows it as unread', async () => {
|
||||
mockOpenedRoom = unreadChannels[0].rid;
|
||||
const { result } = renderHook(() => useRoomList({ collapsedGroups: ['Channels'] }), {
|
||||
wrapper: getWrapperSettings({
|
||||
sidebarGroupByType: true,
|
||||
isEnterprise: true,
|
||||
sidebarCategories: [{ _id: 'Channels', name: 'Channels', default: true, showUnreads: true }],
|
||||
}).build(),
|
||||
});
|
||||
|
||||
// hasLicenseModule resolves asynchronously from the mock endpoint.
|
||||
await waitFor(() => {
|
||||
const groupsList = groupsListOf(result.current.groups);
|
||||
const channelsIndex = groupsList.indexOf('Channels');
|
||||
expect(result.current.groupsCount[channelsIndex]).toEqual(unreadChannels.length);
|
||||
});
|
||||
});
|
||||
|
||||
it('should keep unread rooms visible (and show no header badge) when a group is collapsed and "Show unreads" is on in EE', async () => {
|
||||
const { result } = renderHook(() => useRoomList({ collapsedGroups: ['Channels'] }), {
|
||||
wrapper: getWrapperSettings({
|
||||
sidebarGroupByType: true,
|
||||
isEnterprise: true,
|
||||
sidebarCategories: [{ _id: 'Channels', name: 'Channels', default: true, showUnreads: true }],
|
||||
}).build(),
|
||||
});
|
||||
// hasLicenseModule resolves asynchronously from the mock endpoint.
|
||||
await waitFor(() => {
|
||||
const groupsList = groupsListOf(result.current.groups);
|
||||
const channelsIndex = groupsList.indexOf('Channels');
|
||||
// All 4 seeded channels are unread, so they stay visible despite the group being collapsed.
|
||||
expect(result.current.groupsCount[channelsIndex]).toEqual(unreadChannels.length);
|
||||
// With unreads visible, the header total badge is suppressed.
|
||||
expect(result.current.groups[channelsIndex].unreadInfo.unread).toEqual(0);
|
||||
expect(result.current.groups[channelsIndex].unreadInfo.tunread).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
it('should always return groupsCount and groupsList with the same length', async () => {
|
||||
const { result } = renderHook(() => useRoomList({ collapsedGroups: [] }), {
|
||||
wrapper: getWrapperSettings({ sidebarGroupByType: true }).build(),
|
||||
});
|
||||
expect(result.current.groupsCount.length).toEqual(groupsListOf(result.current.groups).length);
|
||||
});
|
||||
|
||||
it('should keep empty system categories visible (dimmed)', async () => {
|
||||
// Only a single DM exists, so with group-by-type on "Channels"/"Teams" have no rooms — but must still render.
|
||||
// Empty system groups are an EE-only feature.
|
||||
const onlyDirect = [
|
||||
{ ...createFakeSubscription({ t: 'd', ...emptyUnread }), ...createFakeRoom({ t: 'd' }) },
|
||||
] as unknown as SubscriptionWithRoom[];
|
||||
const { result } = renderHook(() => useRoomList({ collapsedGroups: [] }), {
|
||||
wrapper: getWrapperSettings({ rooms: onlyDirect, sidebarGroupByType: true, isEnterprise: true }).build(),
|
||||
});
|
||||
await waitFor(() => expect(groupsListOf(result.current.groups)).toContain('Channels'));
|
||||
const groupsList = groupsListOf(result.current.groups);
|
||||
expect(groupsList).toContain('Direct_Messages');
|
||||
expect(result.current.groups[groupsList.indexOf('Channels')].empty).toBe(true);
|
||||
expect(result.current.groups[groupsList.indexOf('Direct_Messages')].empty).toBe(false);
|
||||
});
|
||||
|
||||
it('should keep the Favorites category visible when it is empty', async () => {
|
||||
// Empty Favorites group is an EE-only feature.
|
||||
const onlyDirect = [
|
||||
{ ...createFakeSubscription({ t: 'd', ...emptyUnread }), ...createFakeRoom({ t: 'd' }) },
|
||||
] as unknown as SubscriptionWithRoom[];
|
||||
const { result } = renderHook(() => useRoomList({ collapsedGroups: [] }), {
|
||||
wrapper: getWrapperSettings({ rooms: onlyDirect, sidebarGroupByType: true, sidebarShowFavorites: true, isEnterprise: true }).build(),
|
||||
});
|
||||
await waitFor(() => expect(groupsListOf(result.current.groups)).toContain('Favorites'));
|
||||
const groupsList = groupsListOf(result.current.groups);
|
||||
expect(result.current.groups[groupsList.indexOf('Favorites')].empty).toBe(true);
|
||||
});
|
||||
|
||||
it('should accumulate unread data into `groupedUnreadInfo` when group is collapsed and "Show unreads" is off', async () => {
|
||||
// The header total badge only accumulates when unreads are hidden, i.e. "Show unreads" off for the group.
|
||||
// Channels defaults to showUnreads=false — no preference entry needed.
|
||||
const { result } = renderHook(() => useRoomList({ collapsedGroups: ['Channels'] }), {
|
||||
wrapper: getWrapperSettings({ sidebarGroupByType: true, isEnterprise: true }).build(),
|
||||
});
|
||||
|
||||
// hasLicenseModule resolves asynchronously from the mock endpoint.
|
||||
await waitFor(() => {
|
||||
const groupsList = groupsListOf(result.current.groups);
|
||||
const channelsIndex = groupsList.indexOf('Channels');
|
||||
const { groupMentions, unread, userMentions, tunread, tunreadUser } = result.current.groups[channelsIndex].unreadInfo;
|
||||
|
||||
expect(groupMentions).toEqual(unreadChannels.reduce((acc, cv) => acc + cv.groupMentions, 0));
|
||||
expect(unread).toEqual(unreadChannels.reduce((acc, cv) => acc + cv.unread, 0));
|
||||
expect(userMentions).toEqual(unreadChannels.reduce((acc, cv) => acc + cv.userMentions, 0));
|
||||
expect(tunread).toEqual(unreadChannels.reduce((acc, cv) => [...acc, ...(cv.tunread || [])], [] as string[]));
|
||||
expect(tunreadUser).toEqual(unreadChannels.reduce((acc, cv) => [...acc, ...(cv.tunreadUser || [])], [] as string[]));
|
||||
});
|
||||
});
|
||||
195
apps/meteor/client/sidebar/hooks/useRoomList.ts
Normal file
195
apps/meteor/client/sidebar/hooks/useRoomList.ts
Normal file
@@ -0,0 +1,195 @@
|
||||
import type { ILivechatInquiryRecord, ISidebarCategory } from '@rocket.chat/core-typings';
|
||||
import { SIDEBAR_SYSTEM_GROUP_KEYS } from '@rocket.chat/core-typings';
|
||||
import { useDebouncedValue } from '@rocket.chat/fuselage-hooks';
|
||||
import type { SubscriptionWithRoom } from '@rocket.chat/ui-contexts';
|
||||
import { useUserSubscriptions } from '@rocket.chat/ui-contexts';
|
||||
import { useVideoConfIncomingCalls } from '@rocket.chat/ui-video-conf';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { filterGroupVisibility, getRoomCategory, useCategoryList } from './useCategoryList';
|
||||
import { useHasLicenseModule } from '../../hooks/useHasLicenseModule';
|
||||
import { useSortQueryOptions } from '../../hooks/useSortQueryOptions';
|
||||
import { useOpenedRoom } from '../../lib/RoomManager';
|
||||
import { useOmnichannelEnabled } from '../../views/omnichannel/hooks/useOmnichannelEnabled';
|
||||
import { useQueuedInquiries } from '../../views/omnichannel/hooks/useQueuedInquiries';
|
||||
import { useToggleUnreads } from '../categories/hooks/useToggleUnreads';
|
||||
import { useUserSidebarCategories } from '../categories/hooks/useUserSidebarCategories';
|
||||
|
||||
const query = { open: { $ne: false } };
|
||||
|
||||
const emptyQueue: ILivechatInquiryRecord[] = [];
|
||||
|
||||
type GroupUnreadInfo = {
|
||||
userMentions: number;
|
||||
groupMentions: number;
|
||||
tunread: string[];
|
||||
tunreadUser: string[];
|
||||
unread: number;
|
||||
};
|
||||
|
||||
export type SidebarRoomListGroup = {
|
||||
key: string;
|
||||
title: string;
|
||||
translateTitle: boolean;
|
||||
category?: ISidebarCategory;
|
||||
showUnreads: boolean;
|
||||
keepUnreadsOnTop: boolean;
|
||||
collapsed: boolean;
|
||||
rooms: SubscriptionWithRoom[];
|
||||
unreadInfo: GroupUnreadInfo;
|
||||
empty: boolean;
|
||||
};
|
||||
|
||||
type useRoomListReturnType = {
|
||||
groups: SidebarRoomListGroup[];
|
||||
groupsCount: number[];
|
||||
totalCount: number;
|
||||
};
|
||||
|
||||
export const isUnreadRoom = (room: SubscriptionWithRoom): boolean =>
|
||||
!room.hideUnreadStatus && Boolean(room.alert || room.unread || room.tunread?.length);
|
||||
|
||||
export const useRoomList = ({ collapsedGroups }: { collapsedGroups?: string[] }): useRoomListReturnType => {
|
||||
const showOmnichannel = useOmnichannelEnabled();
|
||||
|
||||
const { data: hasLicenseModule = false } = useHasLicenseModule('experimental-enterprise-features');
|
||||
|
||||
const { customCategories } = useUserSidebarCategories();
|
||||
const { isShowUnreads, isKeepUnreadsOnTop } = useToggleUnreads();
|
||||
|
||||
const options = useSortQueryOptions();
|
||||
|
||||
const rooms = useUserSubscriptions(query, options);
|
||||
|
||||
const inquiries = useQueuedInquiries();
|
||||
|
||||
const categoryList = useCategoryList(showOmnichannel, inquiries.enabled);
|
||||
|
||||
const incomingCalls = useVideoConfIncomingCalls();
|
||||
|
||||
const openedRoom = useOpenedRoom();
|
||||
|
||||
const queue = inquiries.enabled ? inquiries.queue : emptyQueue;
|
||||
|
||||
const groups = useDebouncedValue(
|
||||
useMemo(() => {
|
||||
const isCollapsed = (key: string) => collapsedGroups?.includes(key) ?? false;
|
||||
|
||||
const unfilteredGroups = new Map<string, Set<SubscriptionWithRoom>>();
|
||||
categoryList.forEach((category) => {
|
||||
unfilteredGroups.set(category, new Set<SubscriptionWithRoom>());
|
||||
});
|
||||
|
||||
rooms.forEach((room) => {
|
||||
const roomCategory = getRoomCategory(room, {
|
||||
groups: unfilteredGroups,
|
||||
hasIncomingCalls: (rid: SubscriptionWithRoom['rid']) => {
|
||||
return !!incomingCalls.find((call) => call.rid === rid);
|
||||
},
|
||||
});
|
||||
|
||||
if (!roomCategory) {
|
||||
return;
|
||||
}
|
||||
|
||||
const targetGroup = unfilteredGroups.get(roomCategory);
|
||||
|
||||
if (!targetGroup) {
|
||||
return;
|
||||
}
|
||||
|
||||
targetGroup.add(room);
|
||||
});
|
||||
|
||||
if (unfilteredGroups.has('Incoming_Livechats')) {
|
||||
unfilteredGroups.set('Incoming_Livechats', new Set(queue) as unknown as Set<SubscriptionWithRoom>);
|
||||
}
|
||||
|
||||
const emptyUnreadInfo = (): GroupUnreadInfo => ({ userMentions: 0, groupMentions: 0, tunread: [], tunreadUser: [], unread: 0 });
|
||||
|
||||
const buildUnreadInfo = (roomsToCount: SubscriptionWithRoom[]): GroupUnreadInfo =>
|
||||
roomsToCount.reduce<GroupUnreadInfo>((counter, room) => {
|
||||
if (room.hideUnreadStatus) {
|
||||
return counter;
|
||||
}
|
||||
|
||||
counter.userMentions += room.userMentions || 0;
|
||||
counter.groupMentions += room.groupMentions || 0;
|
||||
counter.tunread = [...counter.tunread, ...(room.tunread || [])];
|
||||
counter.tunreadUser = [...counter.tunreadUser, ...(room.tunreadUser || [])];
|
||||
counter.unread += room.unread || 0;
|
||||
|
||||
if (!room.unread && !room.tunread?.length && room.alert) {
|
||||
counter.unread += 1;
|
||||
}
|
||||
|
||||
return counter;
|
||||
}, emptyUnreadInfo());
|
||||
|
||||
const makeGroup = (key: string, set: Set<SubscriptionWithRoom>): SidebarRoomListGroup => {
|
||||
const category = customCategories.find(({ _id }) => _id === key);
|
||||
|
||||
const title = category ? category.name : key;
|
||||
const translateTitle = SIDEBAR_SYSTEM_GROUP_KEYS.includes(key as any);
|
||||
const collapsed = isCollapsed(key);
|
||||
const showUnreadsForGroup = hasLicenseModule ? isShowUnreads(key) : false;
|
||||
const showUnreads = category ? Boolean(category.showUnreads) : showUnreadsForGroup;
|
||||
const keepUnreadsOnTopForGroup = hasLicenseModule ? isKeepUnreadsOnTop(key) : false;
|
||||
const keepUnreadsOnTop = category ? Boolean(category.keepUnreadsOnTop) : keepUnreadsOnTopForGroup;
|
||||
const allRooms = [...set];
|
||||
// A collapsed group still shows the room currently open, so the user can locate themselves in the
|
||||
// sidebar, plus its unread rooms when "Show unreads" is enabled.
|
||||
const isVisibleWhileCollapsed = (room: SubscriptionWithRoom) => room.rid === openedRoom || (showUnreads && isUnreadRoom(room));
|
||||
let displayRooms = collapsed ? allRooms.filter(isVisibleWhileCollapsed) : allRooms;
|
||||
|
||||
// "Keep unreads on top": stable-partition so unread rooms come first, each partition keeping the
|
||||
// configured sort (activity / a-z) it already has from the subscription query.
|
||||
if (keepUnreadsOnTop) {
|
||||
displayRooms = [...displayRooms.filter(isUnreadRoom), ...displayRooms.filter((room) => !isUnreadRoom(room))];
|
||||
}
|
||||
|
||||
return {
|
||||
key,
|
||||
title,
|
||||
translateTitle,
|
||||
category,
|
||||
showUnreads,
|
||||
keepUnreadsOnTop,
|
||||
collapsed,
|
||||
rooms: displayRooms,
|
||||
// The header total badge only accounts for what the collapsed group hides. Rooms kept visible
|
||||
// while collapsed — the open one, and the unread ones when "Show unreads" is on — carry their
|
||||
// own counters, so counting them here as well would duplicate them.
|
||||
unreadInfo: collapsed ? buildUnreadInfo(allRooms.filter((room) => !isVisibleWhileCollapsed(room))) : emptyUnreadInfo(),
|
||||
empty: allRooms.length === 0,
|
||||
};
|
||||
};
|
||||
|
||||
const groups = filterGroupVisibility(unfilteredGroups, hasLicenseModule, makeGroup);
|
||||
|
||||
return groups;
|
||||
}, [
|
||||
categoryList,
|
||||
rooms,
|
||||
hasLicenseModule,
|
||||
collapsedGroups,
|
||||
incomingCalls,
|
||||
queue,
|
||||
customCategories,
|
||||
isShowUnreads,
|
||||
isKeepUnreadsOnTop,
|
||||
openedRoom,
|
||||
]),
|
||||
50,
|
||||
);
|
||||
|
||||
// Group ordering is applied AFTER the debounce so that "Move up / Move down"
|
||||
// takes effect immediately rather than waiting for the 50 ms settling period.
|
||||
const groupsCount = useMemo(() => groups.map((group) => (group.empty ? 0 : group.rooms.length)), [groups]);
|
||||
|
||||
return {
|
||||
groups,
|
||||
groupsCount,
|
||||
totalCount: useMemo(() => groupsCount.reduce((acc, count) => acc + count, 0), [groupsCount]),
|
||||
};
|
||||
};
|
||||
21
apps/meteor/client/sidebar/hooks/useShortcutOpenMenu.ts
Normal file
21
apps/meteor/client/sidebar/hooks/useShortcutOpenMenu.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import type { RefObject } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import tinykeys from 'tinykeys';
|
||||
|
||||
// used to open the menu option by keyboard
|
||||
export const useShortcutOpenMenu = (ref: RefObject<Element | null>): void => {
|
||||
useEffect(() => {
|
||||
const unsubscribe = tinykeys(ref.current as HTMLElement, {
|
||||
Alt: (event) => {
|
||||
if (!(event.target as HTMLElement).className.includes('rcx-sidebar-item')) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
(event.target as HTMLElement).querySelector('button')?.click();
|
||||
},
|
||||
});
|
||||
return (): void => {
|
||||
unsubscribe();
|
||||
};
|
||||
}, [ref]);
|
||||
};
|
||||
@@ -0,0 +1,51 @@
|
||||
import { SIDEBAR_SYSTEM_GROUP_KEYS } from '@rocket.chat/core-typings';
|
||||
import { useUserPreference } from '@rocket.chat/ui-contexts';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
|
||||
import { useSidebarSectionsOrder } from './useSidebarSectionsOrder';
|
||||
|
||||
jest.mock('@rocket.chat/ui-contexts', () => ({
|
||||
useUserPreference: jest.fn(),
|
||||
}));
|
||||
|
||||
const mockedUseUserPreference = jest.mocked(useUserPreference);
|
||||
|
||||
it('falls back to the system group keys when the preference is unset', () => {
|
||||
mockedUseUserPreference.mockReturnValue(undefined);
|
||||
|
||||
const { result } = renderHook(() => useSidebarSectionsOrder());
|
||||
|
||||
expect(result.current).toEqual(SIDEBAR_SYSTEM_GROUP_KEYS);
|
||||
});
|
||||
|
||||
it('drops keys of system groups that no longer exist', () => {
|
||||
mockedUseUserPreference.mockReturnValue(['Unread', 'Drafts', 'Favorites', 'Conversations']);
|
||||
|
||||
const { result } = renderHook(() => useSidebarSectionsOrder());
|
||||
|
||||
expect(result.current).toEqual(['Unread', 'Favorites', 'Conversations']);
|
||||
});
|
||||
|
||||
it('keeps the stored order of the remaining keys', () => {
|
||||
mockedUseUserPreference.mockReturnValue(['Conversations', 'Drafts', 'Favorites', 'Unread']);
|
||||
|
||||
const { result } = renderHook(() => useSidebarSectionsOrder());
|
||||
|
||||
expect(result.current).toEqual(['Conversations', 'Favorites', 'Unread']);
|
||||
});
|
||||
|
||||
it('does not treat custom category ids as section keys', () => {
|
||||
mockedUseUserPreference.mockReturnValue(['Favorites', 'custom-xyz']);
|
||||
|
||||
const { result } = renderHook(() => useSidebarSectionsOrder());
|
||||
|
||||
expect(result.current).toEqual(['Favorites']);
|
||||
});
|
||||
|
||||
it('returns an empty list when every stored key is stale', () => {
|
||||
mockedUseUserPreference.mockReturnValue(['Drafts']);
|
||||
|
||||
const { result } = renderHook(() => useSidebarSectionsOrder());
|
||||
|
||||
expect(result.current).toEqual([]);
|
||||
});
|
||||
@@ -0,0 +1,9 @@
|
||||
import { SIDEBAR_SYSTEM_GROUP_KEYS, isSidebarSystemGroupKey } from '@rocket.chat/core-typings';
|
||||
import { useUserPreference } from '@rocket.chat/ui-contexts';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
export const useSidebarSectionsOrder = (): readonly string[] => {
|
||||
const stored = useUserPreference<string[]>('sidebarSectionsOrder');
|
||||
|
||||
return useMemo(() => (stored ? stored.filter(isSidebarSystemGroupKey) : SIDEBAR_SYSTEM_GROUP_KEYS), [stored]);
|
||||
};
|
||||
21
apps/meteor/client/sidebar/hooks/useTemplateByViewMode.ts
Normal file
21
apps/meteor/client/sidebar/hooks/useTemplateByViewMode.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { useUserPreference } from '@rocket.chat/ui-contexts';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import Condensed from '../Item/Condensed';
|
||||
import Extended from '../Item/Extended';
|
||||
import Medium from '../Item/Medium';
|
||||
|
||||
export const useTemplateByViewMode = (): typeof Condensed | typeof Extended | typeof Medium => {
|
||||
const sidebarViewMode = useUserPreference('sidebarViewMode');
|
||||
return useMemo(() => {
|
||||
switch (sidebarViewMode) {
|
||||
case 'extended':
|
||||
return Extended;
|
||||
case 'medium':
|
||||
return Medium;
|
||||
case 'condensed':
|
||||
default:
|
||||
return Condensed;
|
||||
}
|
||||
}, [sidebarViewMode]);
|
||||
};
|
||||
224
apps/meteor/client/sidebar/hooks/useUnreadDisplay.spec.ts
Normal file
224
apps/meteor/client/sidebar/hooks/useUnreadDisplay.spec.ts
Normal file
@@ -0,0 +1,224 @@
|
||||
import { mockAppRoot } from '@rocket.chat/mock-providers';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
|
||||
import { useUnreadDisplay } from './useUnreadDisplay';
|
||||
import { createFakeSubscription } from '../../../tests/mocks/data';
|
||||
|
||||
const dmUnread = createFakeSubscription({
|
||||
t: 'd',
|
||||
unread: 3,
|
||||
userMentions: 0,
|
||||
groupMentions: 0,
|
||||
tunread: undefined,
|
||||
tunreadUser: undefined,
|
||||
});
|
||||
|
||||
const dmThread = createFakeSubscription({
|
||||
t: 'd',
|
||||
unread: 3,
|
||||
userMentions: 0,
|
||||
groupMentions: 0,
|
||||
tunread: ['1'],
|
||||
tunreadUser: undefined,
|
||||
});
|
||||
|
||||
const alert = createFakeSubscription({
|
||||
t: 'p',
|
||||
unread: 0,
|
||||
userMentions: 0,
|
||||
groupMentions: 0,
|
||||
tunread: undefined,
|
||||
tunreadUser: undefined,
|
||||
alert: true,
|
||||
});
|
||||
|
||||
const mentionAndGroupMention = createFakeSubscription({
|
||||
t: 'p',
|
||||
unread: 2,
|
||||
userMentions: 1,
|
||||
groupMentions: 1,
|
||||
tunread: undefined,
|
||||
tunreadUser: undefined,
|
||||
alert: true,
|
||||
});
|
||||
|
||||
const groupMention = createFakeSubscription({
|
||||
t: 'p',
|
||||
unread: 2,
|
||||
userMentions: 0,
|
||||
groupMentions: 2,
|
||||
tunread: undefined,
|
||||
tunreadUser: undefined,
|
||||
alert: true,
|
||||
});
|
||||
|
||||
const tunread = createFakeSubscription({
|
||||
t: 'p',
|
||||
unread: 0,
|
||||
userMentions: 0,
|
||||
groupMentions: 0,
|
||||
tunread: ['1'],
|
||||
tunreadUser: undefined,
|
||||
alert: true,
|
||||
});
|
||||
|
||||
const tunreadUser = createFakeSubscription({
|
||||
t: 'p',
|
||||
unread: 1,
|
||||
userMentions: 0,
|
||||
groupMentions: 0,
|
||||
tunread: ['1'],
|
||||
tunreadUser: ['1'],
|
||||
alert: true,
|
||||
});
|
||||
|
||||
const hideUnreadStatus = createFakeSubscription({
|
||||
t: 'p',
|
||||
hideUnreadStatus: true,
|
||||
});
|
||||
|
||||
const hideUnreadAndMention = createFakeSubscription({
|
||||
t: 'p',
|
||||
hideUnreadStatus: true,
|
||||
hideMentionStatus: true,
|
||||
});
|
||||
|
||||
const noUnread = createFakeSubscription({
|
||||
t: 'p',
|
||||
unread: 0,
|
||||
userMentions: 0,
|
||||
groupMentions: 0,
|
||||
tunread: undefined,
|
||||
tunreadUser: undefined,
|
||||
});
|
||||
|
||||
const wrapper = mockAppRoot()
|
||||
.withTranslations('en', 'core', {
|
||||
mentions_counter_one: '{{count}} mention',
|
||||
mentions_counter_other: '{{count}} mentions',
|
||||
threads_counter_one: '{{count}} unread threaded message',
|
||||
threads_counter_other: '{{count}} unread threaded messages',
|
||||
group_mentions_counter_one: '{{count}} group mention',
|
||||
group_mentions_counter_other: '{{count}} group mentions',
|
||||
unread_messages_counter_one: '{{count}} unread message',
|
||||
unread_messages_counter_other: '{{count}} unread messages',
|
||||
})
|
||||
.build();
|
||||
|
||||
it('should return correct unread data for [Direct message unread]', async () => {
|
||||
const { result } = renderHook(() => useUnreadDisplay(dmUnread), {
|
||||
wrapper,
|
||||
});
|
||||
expect(result.current.unreadVariant).toBe('secondary');
|
||||
expect(result.current.unreadTitle).toBe('3 unread messages');
|
||||
|
||||
expect(result.current.unreadCount).toHaveProperty('mentions', 0);
|
||||
expect(result.current.unreadCount).toHaveProperty('threads', 0);
|
||||
expect(result.current.unreadCount).toHaveProperty('groupMentions', 0);
|
||||
expect(result.current.unreadCount).toHaveProperty('total', 3);
|
||||
});
|
||||
|
||||
it('should return correct unread data for [Direct message with thread unread]', async () => {
|
||||
const { result } = renderHook(() => useUnreadDisplay(dmThread), {
|
||||
wrapper,
|
||||
});
|
||||
expect(result.current.unreadVariant).toBe('primary');
|
||||
expect(result.current.unreadTitle).toBe('1 unread threaded message, 3 unread messages');
|
||||
|
||||
expect(result.current.unreadCount).toHaveProperty('mentions', 0);
|
||||
expect(result.current.unreadCount).toHaveProperty('threads', 1);
|
||||
expect(result.current.unreadCount).toHaveProperty('groupMentions', 0);
|
||||
expect(result.current.unreadCount).toHaveProperty('total', 4);
|
||||
});
|
||||
|
||||
it('should return correct unread data for [Channel with unread messages alert only]', async () => {
|
||||
const { result } = renderHook(() => useUnreadDisplay(alert), {
|
||||
wrapper,
|
||||
});
|
||||
|
||||
expect(result.current.highlightUnread).toBe(true);
|
||||
expect(result.current.showUnread).toBe(false);
|
||||
|
||||
expect(result.current.unreadCount).toHaveProperty('mentions', 0);
|
||||
expect(result.current.unreadCount).toHaveProperty('threads', 0);
|
||||
expect(result.current.unreadCount).toHaveProperty('groupMentions', 0);
|
||||
expect(result.current.unreadCount).toHaveProperty('total', 0);
|
||||
});
|
||||
|
||||
it('should return correct unread data for [Mention and group mention]', async () => {
|
||||
const { result } = renderHook(() => useUnreadDisplay(mentionAndGroupMention), {
|
||||
wrapper,
|
||||
});
|
||||
expect(result.current.unreadVariant).toBe('danger');
|
||||
expect(result.current.unreadTitle).toBe('1 mention, 1 group mention');
|
||||
|
||||
expect(result.current.unreadCount).toHaveProperty('mentions', 1);
|
||||
expect(result.current.unreadCount).toHaveProperty('threads', 0);
|
||||
expect(result.current.unreadCount).toHaveProperty('groupMentions', 1);
|
||||
expect(result.current.unreadCount).toHaveProperty('total', 2);
|
||||
});
|
||||
|
||||
it('should return correct unread data for [Group mention]', async () => {
|
||||
const { result } = renderHook(() => useUnreadDisplay(groupMention), {
|
||||
wrapper,
|
||||
});
|
||||
expect(result.current.unreadVariant).toBe('warning');
|
||||
expect(result.current.unreadTitle).toBe('2 group mentions');
|
||||
|
||||
expect(result.current.unreadCount).toHaveProperty('mentions', 0);
|
||||
expect(result.current.unreadCount).toHaveProperty('threads', 0);
|
||||
expect(result.current.unreadCount).toHaveProperty('groupMentions', 2);
|
||||
expect(result.current.unreadCount).toHaveProperty('total', 2);
|
||||
});
|
||||
|
||||
it('should return correct unread data for [Thread unread]', async () => {
|
||||
const { result } = renderHook(() => useUnreadDisplay(tunread), {
|
||||
wrapper,
|
||||
});
|
||||
expect(result.current.unreadVariant).toBe('primary');
|
||||
expect(result.current.unreadTitle).toBe('1 unread threaded message');
|
||||
|
||||
expect(result.current.unreadCount).toHaveProperty('mentions', 0);
|
||||
expect(result.current.unreadCount).toHaveProperty('groupMentions', 0);
|
||||
expect(result.current.unreadCount).toHaveProperty('threads', 1);
|
||||
expect(result.current.unreadCount).toHaveProperty('total', 1);
|
||||
});
|
||||
|
||||
it('should return correct unread data for [Thread and thread user mention]', async () => {
|
||||
const { result } = renderHook(() => useUnreadDisplay(tunreadUser), {
|
||||
wrapper,
|
||||
});
|
||||
expect(result.current.unreadVariant).toBe('danger');
|
||||
expect(result.current.unreadTitle).toBe('1 mention, 1 unread threaded message');
|
||||
|
||||
expect(result.current.unreadCount).toHaveProperty('mentions', 1);
|
||||
expect(result.current.unreadCount).toHaveProperty('groupMentions', 0);
|
||||
expect(result.current.unreadCount).toHaveProperty('threads', 1);
|
||||
expect(result.current.unreadCount).toHaveProperty('total', 2);
|
||||
});
|
||||
|
||||
it('should not highlight unread if hideUnreadStatus is enabled', async () => {
|
||||
const { result } = renderHook(() => useUnreadDisplay(hideUnreadStatus));
|
||||
|
||||
expect(result.current.highlightUnread).toBe(false);
|
||||
expect(result.current.showUnread).toBe(true);
|
||||
});
|
||||
|
||||
it('should not show unread if hideUnreadStatus and hideMentionStatus is enabled', async () => {
|
||||
const { result } = renderHook(() => useUnreadDisplay(hideUnreadAndMention));
|
||||
|
||||
expect(result.current.highlightUnread).toBe(false);
|
||||
expect(result.current.showUnread).toBe(false);
|
||||
});
|
||||
|
||||
it("should not show unread if there isn't any unread message", async () => {
|
||||
const { result } = renderHook(() => useUnreadDisplay(noUnread));
|
||||
|
||||
expect(result.current.highlightUnread).toBe(false);
|
||||
expect(result.current.showUnread).toBe(false);
|
||||
|
||||
expect(result.current.unreadCount).toHaveProperty('mentions', 0);
|
||||
expect(result.current.unreadCount).toHaveProperty('groupMentions', 0);
|
||||
expect(result.current.unreadCount).toHaveProperty('threads', 0);
|
||||
expect(result.current.unreadCount).toHaveProperty('total', 0);
|
||||
});
|
||||
10
apps/meteor/client/sidebar/hooks/useUnreadDisplay.ts
Normal file
10
apps/meteor/client/sidebar/hooks/useUnreadDisplay.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import type { UnreadData } from '../../../lib/getSubscriptionUnreadData';
|
||||
import { getSubscriptionUnreadData } from '../../../lib/getSubscriptionUnreadData';
|
||||
|
||||
export const useUnreadDisplay = (unreadData: UnreadData) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return getSubscriptionUnreadData(unreadData, t);
|
||||
};
|
||||
Reference in New Issue
Block a user