From ec89c1f1154d87ee72bfe297e2da0d14c18260b3 Mon Sep 17 00:00:00 2001 From: "dmitrii.pichenikin" Date: Thu, 12 Mar 2026 14:01:34 +0300 Subject: [PATCH] some refactoring, the BadgeList component has been remade into a functional one --- .../{utils => badge_image}/badge_image.tsx | 2 +- webapp/src/components/badge_modal/index.tsx | 6 +- .../src/components/{utils => emoji}/emoji.tsx | 0 webapp/src/components/grant_modal/index.tsx | 2 +- webapp/src/components/rhs/all_badges_row.tsx | 2 +- webapp/src/components/rhs/badge_details.tsx | 2 +- webapp/src/components/rhs/user_badge_row.tsx | 2 +- .../components/user_popover/badge_list.tsx | 345 +++++++----------- .../components/user_popover/badge_tooltip.tsx | 62 ++++ webapp/src/components/user_popover/index.ts | 50 +-- .../tooltip_wrapper.scss | 0 .../tooltip_wrapper.tsx | 0 .../src/components/utils/badge_list_utils.ts | 25 ++ 13 files changed, 226 insertions(+), 272 deletions(-) rename webapp/src/components/{utils => badge_image}/badge_image.tsx (94%) rename webapp/src/components/{utils => emoji}/emoji.tsx (100%) create mode 100644 webapp/src/components/user_popover/badge_tooltip.tsx rename webapp/src/components/{utils => user_popover}/tooltip_wrapper.scss (100%) rename webapp/src/components/{utils => user_popover}/tooltip_wrapper.tsx (100%) create mode 100644 webapp/src/components/utils/badge_list_utils.ts diff --git a/webapp/src/components/utils/badge_image.tsx b/webapp/src/components/badge_image/badge_image.tsx similarity index 94% rename from webapp/src/components/utils/badge_image.tsx rename to webapp/src/components/badge_image/badge_image.tsx index ff4773f..4818e54 100644 --- a/webapp/src/components/utils/badge_image.tsx +++ b/webapp/src/components/badge_image/badge_image.tsx @@ -1,7 +1,7 @@ import React from 'react'; import {Badge} from '../../types/badges'; -import RenderEmoji from '../utils/emoji'; +import RenderEmoji from '../emoji/emoji'; import {IMAGE_TYPE_ABSOLUTE_URL, IMAGE_TYPE_EMOJI} from '../../constants'; type Props = { diff --git a/webapp/src/components/badge_modal/index.tsx b/webapp/src/components/badge_modal/index.tsx index 4d638a3..64a872c 100644 --- a/webapp/src/components/badge_modal/index.tsx +++ b/webapp/src/components/badge_modal/index.tsx @@ -6,6 +6,8 @@ import {FormattedMessage, useIntl} from 'react-intl'; import {getCurrentChannelId} from 'mattermost-redux/selectors/entities/common'; import {GlobalState} from 'mattermost-redux/types/store'; +import RenderEmoji from 'components/emoji/emoji'; + import {isCreateBadgeModalVisible, getEditBadgeModalData} from 'selectors'; import {closeCreateBadgeModal, closeEditBadgeModal, setRHSView} from 'actions/actions'; import {RHS_STATE_ALL} from '../../constants'; @@ -14,14 +16,14 @@ import Client from 'client/api'; import {getServerErrorId} from 'utils/helpers'; import CloseIcon from 'components/icons/close_icon'; import EmojiIcon from 'components/icons/emoji_icon'; -import RenderEmoji from 'components/utils/emoji'; + +import ConfirmDialog from 'components/confirm_dialog/confirm_dialog'; import EmojiPickerOverlay from './emoji_picker'; import InlineTypeForm from './inline_type_form'; import TypeSelect from './type_select'; import './badge_modal.scss'; -import ConfirmDialog from 'components/confirm_dialog/confirm_dialog'; const NEW_TYPE_VALUE = '__new__'; diff --git a/webapp/src/components/utils/emoji.tsx b/webapp/src/components/emoji/emoji.tsx similarity index 100% rename from webapp/src/components/utils/emoji.tsx rename to webapp/src/components/emoji/emoji.tsx diff --git a/webapp/src/components/grant_modal/index.tsx b/webapp/src/components/grant_modal/index.tsx index cf21611..0f1524f 100644 --- a/webapp/src/components/grant_modal/index.tsx +++ b/webapp/src/components/grant_modal/index.tsx @@ -13,7 +13,7 @@ import {AllBadgesBadge} from 'types/badges'; import Client from 'client/api'; import {getServerErrorId, getUserDisplayName} from 'utils/helpers'; import CloseIcon from 'components/icons/close_icon'; -import RenderEmoji from 'components/utils/emoji'; +import RenderEmoji from 'components/emoji/emoji'; type GrantFormData = { badgeId: string; diff --git a/webapp/src/components/rhs/all_badges_row.tsx b/webapp/src/components/rhs/all_badges_row.tsx index 08ccafb..a8d56bc 100644 --- a/webapp/src/components/rhs/all_badges_row.tsx +++ b/webapp/src/components/rhs/all_badges_row.tsx @@ -72,7 +72,7 @@ const AllBadgesRow: React.FC = ({badge, onClick}: Props) => { /> {' '} - {badge.description ? markdown(badge.description) : '—'} + {badge.description ? markdown(badge.description) : '-'}
void) | null; hide: () => void; - status?: string; - actions: { - setRHSView: (view: RHSState) => Promise; - setRHSBadge: (id: BadgeID | null) => Promise; - setRHSUser: (id: string | null) => Promise; - openGrant: (user?: string, badge?: string) => Promise; - getCustomEmojisByName: (names: string[]) => Promise; - }; -} - -type BadgeGroup = { - badge: UserBadge; - count: number; -} - -type State = { - badges?: UserBadge[]; - loaded?: boolean; } const MAX_BADGES = 6; const BADGE_SIZE = 24; -class BadgeList extends React.PureComponent { - constructor(props: Props) { - super(props); +const BadgeList: React.FC = ({user, hide}) => { + const intl = useIntl(); + const dispatch = useDispatch(); + const currentUserID = useSelector(getCurrentUserId); + const openRHS = useSelector(getShowRHS); + const [badges, setBadges] = useState(); + const [loaded, setLoaded] = useState(false); - this.state = {}; - } - - componentDidMount() { + useEffect(() => { const c = new Client(); - c.getUserBadges(this.props.user.id).then((badges) => { - this.setState({badges, loaded: true}); + c.getUserBadges(user.id).then((result) => { + setBadges(result); + setLoaded(true); }); - } + }, [user.id]); - componentDidUpdate(prevProps: Props, prevState: State) { - if (this.state.badges !== prevState.badges && this.state.badges) { - const groups = this.groupBadges(this.state.badges); - const toShow = Math.min(groups.length, MAX_BADGES); - const names: string[] = []; - for (let i = 0; i < toShow; i++) { - const {badge} = groups[i]; - if (badge.image_type === IMAGE_TYPE_EMOJI) { - names.push(badge.image); - } - } - const toLoad = names.filter((v) => !systemEmojis.has(v)); - this.props.actions.getCustomEmojisByName(toLoad); - } - } + const groups = useMemo( + () => (badges ? groupBadges(badges) : []), + [badges], + ); - groupBadges = (badges: UserBadge[]): BadgeGroup[] => { - const map = new Map(); - for (const badge of badges) { - const existing = map.get(badge.id); - if (existing) { - existing.count++; - } else { - map.set(badge.id, {badge, count: 1}); - } - } - return Array.from(map.values()); - } - - onMoreClick = () => { - if (!this.props.openRHS) { + useEffect(() => { + if (!badges) { return; } + const toShow = groups.slice(0, MAX_BADGES); + const names = toShow. + filter(({badge}) => badge.image_type === IMAGE_TYPE_EMOJI). + map(({badge}) => badge.image). + filter((v) => !systemEmojis.has(v)); + if (names.length > 0) { + dispatch(getCustomEmojisByName(names)); + } + }, [badges, groups, dispatch]); - if (this.props.currentUserID === this.props.user.id) { - this.props.actions.setRHSView(RHS_STATE_MY); - this.props.openRHS(); - this.props.hide(); + const handleMoreClick = useCallback(() => { + if (!openRHS) { return; } + if (currentUserID === user.id) { + dispatch(setRHSView(RHS_STATE_MY)); + } else { + dispatch(setRHSUser(user.id)); + dispatch(setRHSView(RHS_STATE_OTHER)); + } + openRHS(); + hide(); + }, [openRHS, currentUserID, user.id, dispatch, hide]); - this.props.actions.setRHSUser(this.props.user.id); - this.props.actions.setRHSView(RHS_STATE_OTHER); - this.props.openRHS(); - this.props.hide(); - } - - onBadgeClick = (badge: UserBadge) => { - if (!this.props.openRHS) { + const handleBadgeClick = useCallback((badge: UserBadge) => { + if (!openRHS) { return; } + dispatch(setRHSBadge(badge.id)); + dispatch(setRHSView(RHS_STATE_DETAIL)); + openRHS(); + hide(); + }, [openRHS, dispatch, hide]); - this.props.actions.setRHSBadge(badge.id); - this.props.actions.setRHSView(RHS_STATE_DETAIL); - this.props.openRHS(); - this.props.hide(); - } + const handleGrantClick = useCallback(() => { + dispatch(openGrant(user.username)); + hide(); + }, [dispatch, user.username, hide]); - onGrantClick = () => { - this.props.actions.openGrant(this.props.user.username); - this.props.hide(); - } + const visibleGroups = groups.slice(0, MAX_BADGES); + const maxWidth = (MAX_BADGES * BADGE_SIZE) + 30; - render() { - const {intl} = this.props; - const groups = this.state.badges ? this.groupBadges(this.state.badges) : []; - const nGroups = groups.length; - const toShow = Math.min(nGroups, MAX_BADGES); - - const nameLabel = intl.formatMessage( - {id: 'badges.label.name', defaultMessage: 'Название:'}, - ); - const descLabel = intl.formatMessage( - {id: 'badges.label.description', defaultMessage: 'Описание:'}, - ); - - const content: React.ReactNode[] = []; - for (let i = 0; i < toShow; i++) { - const {badge, count} = groups[i]; - - let tooltipLines: string; - if (count > 1) { - const countLabel = intl.formatMessage( - {id: 'badges.label.count', defaultMessage: 'Количество: {count}'}, - {count}, - ); - tooltipLines = [ - nameLabel + ' ' + badge.name, - descLabel + ' ' + badge.description, - countLabel, - ].join('\n'); - } else { - const time = new Date(badge.time); - let reason: string | null = null; - if (badge.reason) { - reason = intl.formatMessage( - {id: 'badges.label.reason', defaultMessage: 'Причина: {reason}'}, - {reason: badge.reason}, - ); - } - const grantedBy = intl.formatMessage( - {id: 'badges.label.granted_by', defaultMessage: 'Выдал: {username}'}, - {username: badge.granted_by_name}, - ); - const grantedAt = intl.formatMessage( - {id: 'badges.label.granted_at', defaultMessage: 'Выдан: {date}'}, - {date: intl.formatDate(time, {day: '2-digit', month: '2-digit', year: 'numeric'})}, - ); - tooltipLines = [ - nameLabel + ' ' + badge.name, - descLabel + ' ' + badge.description, - reason, - grantedBy, - grantedAt, - ].filter(Boolean).join('\n'); - } - - const badgeComponent = ( - - this.onBadgeClick(badge)}> - - +
+ +
+
+ {visibleGroups.map(({badge, count}) => ( + - {count > 1 && ( - - {'×'}{count} - - )} - - - - ); - content.push(badgeComponent); - } - - let andMore: React.ReactNode = null; - if (nGroups > MAX_BADGES) { - const andMoreText = intl.formatMessage( - {id: 'badges.and_more', defaultMessage: 'и ещё {count}. Нажмите, чтобы увидеть все.'}, - {count: nGroups - MAX_BADGES}, - ); - andMore = ( - - - - ); - } - const maxWidth = (MAX_BADGES * BADGE_SIZE) + 30; - let loading: React.ReactNode = null; - if (!this.state.loaded) { - loading = ( - - // Reserve enough height one row of badges and the "and more" button + handleBadgeClick(badge)}> + + + {count > 1 && ( + + {'×'}{count} + + )} + + + + ))} + {groups.length > MAX_BADGES && ( + + + + )} +
+ {!loaded && (
- ); - } - return ( -
-
- -
-
- {content} - {andMore} -
- {loading} - -
-
- ); - } -} + )} + +
+
+ ); +}; -export default injectIntl(BadgeList); +export default BadgeList; diff --git a/webapp/src/components/user_popover/badge_tooltip.tsx b/webapp/src/components/user_popover/badge_tooltip.tsx new file mode 100644 index 0000000..538155f --- /dev/null +++ b/webapp/src/components/user_popover/badge_tooltip.tsx @@ -0,0 +1,62 @@ +import React from 'react'; + +import {useIntl} from 'react-intl'; + +import {UserBadge} from 'types/badges'; + +import {truncateText} from 'components/utils/badge_list_utils'; + +type Props = { + badge: UserBadge; + count: number; +} + +const BadgeTooltip: React.FC = ({badge, count}) => { + const intl = useIntl(); + const desc = badge.description ? truncateText(badge.description) : '—'; + + const nameRow = intl.formatMessage( + {id: 'badges.label.name', defaultMessage: 'Название:'}, + ) + ' ' + badge.name; + + const descRow = intl.formatMessage( + {id: 'badges.label.description', defaultMessage: 'Описание:'}, + ) + ' ' + desc; + + if (count > 1) { + const countRow = intl.formatMessage( + {id: 'badges.label.count', defaultMessage: 'Количество: {count}'}, + {count}, + ); + return <>{nameRow}{'\n'}{descRow}{'\n'}{countRow}; + } + + const time = new Date(badge.time); + const grantedBy = intl.formatMessage( + {id: 'badges.label.granted_by', defaultMessage: 'Выдал: {username}'}, + {username: badge.granted_by_name}, + ); + const grantedAt = intl.formatMessage( + {id: 'badges.label.granted_at', defaultMessage: 'Выдан: {date}'}, + {date: intl.formatDate(time, {day: '2-digit', month: '2-digit', year: 'numeric'})}, + ); + + return ( + <> + {nameRow}{'\n'} + {descRow}{'\n'} + {badge.reason && ( + <> + {intl.formatMessage( + {id: 'badges.label.reason', defaultMessage: 'Причина: {reason}'}, + {reason: badge.reason}, + )}{'\n'} + + )} + {grantedBy}{'\n'} + {grantedAt} + + ); +}; + +export default BadgeTooltip; diff --git a/webapp/src/components/user_popover/index.ts b/webapp/src/components/user_popover/index.ts index 622e0ec..ae393b4 100644 --- a/webapp/src/components/user_popover/index.ts +++ b/webapp/src/components/user_popover/index.ts @@ -1,49 +1 @@ -// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. -// See License for license information. - -import {connect} from 'react-redux'; -import {ActionCreatorsMapObject, bindActionCreators, Dispatch} from 'redux'; - -import {GlobalState} from 'mattermost-redux/types/store'; - -import {getCurrentUserId} from 'mattermost-redux/selectors/entities/common'; - -import {getCustomEmojisByName} from 'mattermost-redux/actions/emojis'; - -import {setRHSView, setRHSBadge, setRHSUser, openGrant} from '../../actions/actions'; - -import {getShowRHS} from 'selectors'; -import {RHSState} from 'types/general'; -import {BadgeID} from 'types/badges'; - -import BadgeList from './badge_list'; - -function mapStateToProps(state: GlobalState) { - return { - openRHS: getShowRHS(state), - currentUserID: getCurrentUserId(state), - debug: state, - }; -} - -type Actions = { - setRHSView: (view: RHSState) => Promise; - setRHSBadge: (id: BadgeID | null) => Promise; - setRHSUser: (id: string | null) => Promise; - openGrant: (user?: string, badge?: string) => Promise; - getCustomEmojisByName: (names: string[]) => Promise; -} - -function mapDispatchToProps(dispatch: Dispatch) { - return { - actions: bindActionCreators({ - setRHSView, - setRHSBadge, - setRHSUser, - openGrant, - getCustomEmojisByName, - }, dispatch), - }; -} - -export default connect(mapStateToProps, mapDispatchToProps)(BadgeList); +export {default} from './badge_list'; diff --git a/webapp/src/components/utils/tooltip_wrapper.scss b/webapp/src/components/user_popover/tooltip_wrapper.scss similarity index 100% rename from webapp/src/components/utils/tooltip_wrapper.scss rename to webapp/src/components/user_popover/tooltip_wrapper.scss diff --git a/webapp/src/components/utils/tooltip_wrapper.tsx b/webapp/src/components/user_popover/tooltip_wrapper.tsx similarity index 100% rename from webapp/src/components/utils/tooltip_wrapper.tsx rename to webapp/src/components/user_popover/tooltip_wrapper.tsx diff --git a/webapp/src/components/utils/badge_list_utils.ts b/webapp/src/components/utils/badge_list_utils.ts new file mode 100644 index 0000000..c8d97ab --- /dev/null +++ b/webapp/src/components/utils/badge_list_utils.ts @@ -0,0 +1,25 @@ +import {BadgeID, UserBadge} from 'types/badges'; + +export type BadgeGroup = { + badge: UserBadge; + count: number; +} + +const MAX_DESC_LENGTH = 40; + +export function groupBadges(badges: UserBadge[]): BadgeGroup[] { + const map = new Map(); + for (const badge of badges) { + const existing = map.get(badge.id); + if (existing) { + existing.count++; + } else { + map.set(badge.id, {badge, count: 1}); + } + } + return Array.from(map.values()); +} + +export function truncateText(text: string): string { + return text.length > MAX_DESC_LENGTH ? text.slice(0, MAX_DESC_LENGTH) + '...' : text; +}