Skip to content

Commit 162322c

Browse files
authored
[MM-68797] MBE Phase 8c: registerChannelIconOverride follow-ups (mattermost#36576)
* phase 8c * merge errors * PR comments
1 parent 9f7fdad commit 162322c

6 files changed

Lines changed: 57 additions & 8 deletions

File tree

‎webapp/channels/src/components/channel_type_icon/channel_icon_override.test.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,3 +334,4 @@ describe('selectors/getChannelIconOverrideForChannel', () => {
334334
});
335335
});
336336
});
337+

‎webapp/channels/src/components/channel_type_icon/channel_icon_override.ts‎

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,10 @@ export function clearLoggedMatcherErrors(pluginId?: string): void {
2727
/**
2828
* Returns the IconGlyphTypes name of the first matching plugin override, or null.
2929
*
30-
* If `channel` is present, iterates `state.plugins.components.ChannelIconOverride` in array order
31-
* (sorted alphabetically by pluginId via the reducer). The first matcher returning true wins.
32-
* Matcher throws are caught, logged once per pluginId, and treated as no-match.
33-
*
34-
* Note: Do not use createSelector here — this is a per-row call over a short override list.
30+
* Iterates the registered matchers on every call. The framework does not memoize across
31+
* dispatches because the matcher contract takes full Redux state, so we cannot infer
32+
* which slices it reads. If a plugin's matcher is expensive, the plugin should memoize
33+
* inside its own predicate using `createSelector` keyed on the slices it consults.
3534
*/
3635
export function getChannelIconOverrideForChannel(
3736
state: GlobalState,
@@ -43,8 +42,7 @@ export function getChannelIconOverrideForChannel(
4342
const overrides = state.plugins.components.ChannelIconOverride ?? [];
4443
for (const entry of overrides) {
4544
try {
46-
const matched = entry.matcher(state, channel);
47-
if (matched === true) {
45+
if (entry.matcher(state, channel) === true) {
4846
return entry.iconName;
4947
}
5048
} catch (err) {

‎webapp/channels/src/components/channel_type_icon/channel_type_icon.tsx‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,15 @@ type Props = {
1212
channel?: Channel;
1313
} & HTMLAttributes<HTMLElement>;
1414

15+
/**
16+
* Renders a channel icon as a font glyph (`<i className="icon icon-…"/>`) honoring
17+
* any plugin override registered via `registerChannelIconOverride`. Use this for
18+
* font-icon sites.
19+
*
20+
* For SVG sites where you need a Compass SVG component directly, use the
21+
* `useChannelIconOverrideName(channel)` hook to get the override name, then
22+
* resolve via `compassIconForName(name)` and render the resulting component.
23+
*/
1524
const ChannelTypeIcon = ({channel, className, ...rest}: Props) => {
1625
const iconClassName = useChannelIconClassName(channel);
1726
return (

‎webapp/channels/src/components/channel_type_icon/useChannelIconOverrideName.ts‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,21 @@ import type {GlobalState} from 'types/store';
1010

1111
import {getChannelIconOverrideForChannel} from './channel_icon_override';
1212

13+
/**
14+
* Returns the override icon name (e.g., `'shield-outline'`) for the given channel
15+
* from the first matching plugin registration, or null if no override matches.
16+
*
17+
* For font-icon sites, prefer the `<ChannelTypeIcon channel={channel}/>` component,
18+
* which handles override resolution and rendering. Use this hook only when you
19+
* need the raw icon name for an SVG render path (resolve via
20+
* `compassIconForName(name)` and render the resulting component).
21+
*
22+
* Matcher cost: the resolver iterates every registered matcher on every call. The
23+
* framework does not memoize across dispatches because the matcher contract takes
24+
* full Redux state and we cannot know what slices it reads. Plugins with expensive
25+
* matchers should memoize internally using `createSelector` keyed on the slices
26+
* the matcher actually consults.
27+
*/
1328
export function useChannelIconOverrideName(channel?: Channel): IconGlyphTypes | null {
1429
return useSelector((state: GlobalState) => getChannelIconOverrideForChannel(state, channel));
1530
}

‎webapp/channels/src/utils/channel_utils.test.ts‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,32 @@ describe('Channel Utils', () => {
173173
});
174174
});
175175

176+
describe('isArchivedChannel', () => {
177+
test('returns false for undefined', () => {
178+
expect(Utils.isArchivedChannel(undefined)).toBe(false);
179+
});
180+
181+
test('returns false for channel object missing delete_at', () => {
182+
expect(Utils.isArchivedChannel({} as Channel)).toBe(false);
183+
});
184+
185+
test('returns false for channel with delete_at: 0', () => {
186+
expect(Utils.isArchivedChannel({delete_at: 0} as Channel)).toBe(false);
187+
});
188+
189+
test('returns true for channel with delete_at: 12345', () => {
190+
expect(Utils.isArchivedChannel({delete_at: 12345} as Channel)).toBe(true);
191+
});
192+
193+
test('returns true for channel with delete_at: -1', () => {
194+
expect(Utils.isArchivedChannel({delete_at: -1} as Channel)).toBe(true);
195+
});
196+
197+
test('getChannelIconClassName returns icon-globe for open channel missing delete_at', () => {
198+
expect(Utils.getChannelIconClassName({type: 'O'} as Channel)).toBe('icon-globe');
199+
});
200+
});
201+
176202
describe('getChannelRoutePathAndIdentifier', () => {
177203
test('should return channels path and channel name for open channels', () => {
178204
const channel = {type: Constants.OPEN_CHANNEL, name: 'town-square'} as Channel;

‎webapp/channels/src/utils/channel_utils.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ export function findNextUnreadChannelId(curChannelId: string, allChannelIds: str
6262
}
6363

6464
export function isArchivedChannel(channel?: Channel) {
65-
return Boolean(channel && channel.delete_at !== 0);
65+
return Boolean(channel?.delete_at);
6666
}
6767

6868
/**

0 commit comments

Comments
 (0)