LP-5613 #2

Open
dmitrii.pichenikin wants to merge 37 commits from LP-5613 into dev
13 changed files with 226 additions and 272 deletions
Showing only changes of commit ec89c1f115 - Show all commits

View File

@ -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 = {

View File

@ -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__';

View File

@ -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;

View File

@ -72,7 +72,7 @@ const AllBadgesRow: React.FC<Props> = ({badge, onClick}: Props) => {
/>
</span>
{' '}
{badge.description ? markdown(badge.description) : ''}
{badge.description ? markdown(badge.description) : '-'}
</div>
<div className='badge-meta'>
<FormattedMessage

View File

@ -9,7 +9,7 @@ import Client from '../../client/api';
import {RHSState} from '../../types/general';
import {RHS_STATE_MY, RHS_STATE_OTHER, RHS_STATE_TYPES} from '../../constants';
import BadgeImage from '../utils/badge_image';
import BadgeImage from '../badge_image/badge_image';
import {markdown} from 'utils/markdown';

View File

@ -5,7 +5,7 @@ import {FormattedMessage, useIntl} from 'react-intl';
import Client4 from 'mattermost-redux/client/client4';
import {UserBadge} from '../../types/badges';
import BadgeImage from '../utils/badge_image';
import BadgeImage from '../badge_image/badge_image';
import {markdown} from 'utils/markdown';
import Client from '../../client/api';
import ConfirmDialog from '../confirm_dialog/confirm_dialog';

View File

@ -1,256 +1,169 @@
import {UserProfile} from 'mattermost-redux/types/users';
import React from 'react';
import React, {useCallback, useEffect, useMemo, useState} from 'react';
import {FormattedMessage, injectIntl, IntlShape} from 'react-intl';
import {FormattedMessage, useIntl} from 'react-intl';
import {GlobalState} from 'mattermost-redux/types/store';
import {useDispatch, useSelector} from 'react-redux';
import {systemEmojis} from 'mattermost-redux/actions/emojis';
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/common';
import {BadgeID, UserBadge} from 'types/badges';
import {systemEmojis, getCustomEmojisByName} from 'mattermost-redux/actions/emojis';
import {UserBadge} from 'types/badges';
import Client from 'client/api';
import BadgeImage from '../utils/badge_image';
import TooltipWrapper from '../utils/tooltip_wrapper';
import {RHSState} from 'types/general';
import BadgeImage from '../badge_image/badge_image';
import {IMAGE_TYPE_EMOJI, RHS_STATE_DETAIL, RHS_STATE_MY, RHS_STATE_OTHER} from '../../constants';
import {setRHSView, setRHSBadge, setRHSUser, openGrant} from '../../actions/actions';
import {getShowRHS} from 'selectors';
import {groupBadges} from 'components/utils/badge_list_utils';
import BadgeTooltip from './badge_tooltip';
import TooltipWrapper from './tooltip_wrapper';
import './badge_list.scss';
type Props = {
intl: IntlShape;
debug: GlobalState;
user: UserProfile;
currentUserID: string;
openRHS: (() => void) | null;
hide: () => void;
status?: string;
actions: {
setRHSView: (view: RHSState) => Promise<void>;
setRHSBadge: (id: BadgeID | null) => Promise<void>;
setRHSUser: (id: string | null) => Promise<void>;
openGrant: (user?: string, badge?: string) => Promise<void>;
getCustomEmojisByName: (names: string[]) => Promise<unknown>;
};
}
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<Props, State> {
constructor(props: Props) {
super(props);
const BadgeList: React.FC<Props> = ({user, hide}) => {
const intl = useIntl();
const dispatch = useDispatch();
const currentUserID = useSelector(getCurrentUserId);
const openRHS = useSelector(getShowRHS);
const [badges, setBadges] = useState<UserBadge[]>();
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);
Review

client

client
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<BadgeID, BadgeGroup>();
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).
Review

как будто filter.map.filter можно заменить на reduce

как будто filter.map.filter можно заменить на reduce
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 = (
<TooltipWrapper tooltipContent={tooltipLines}>
<a onClick={() => this.onBadgeClick(badge)}>
<span className='badge-stacked'>
<BadgeImage
return (
<div id='badgePlugin'>
<div><b>
<FormattedMessage
id='badges.popover.title'
defaultMessage='Достижения'
/>
</b></div>
<div id='contentContainer'>
Review

немного поплыли стили

немного поплыли стили
{visibleGroups.map(({badge, count}) => (
<TooltipWrapper
key={badge.id}
tooltipContent={
<BadgeTooltip
badge={badge}
size={BADGE_SIZE}
count={count}
/>
{count > 1 && (
<span className='badge-stack-count'>
{'×'}{count}
</span>
)}
</span>
</a>
</TooltipWrapper>
);
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 = (
<TooltipWrapper tooltipContent={andMoreText}>
<button
id='showMoreButton'
onClick={this.onMoreClick}
}
>
<span className={'fa fa-angle-right'}/>
</button>
</TooltipWrapper>
);
}
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
<a onClick={() => handleBadgeClick(badge)}>
<span className='badge-stacked'>
<BadgeImage
badge={badge}
size={BADGE_SIZE}
/>
{count > 1 && (
<span className='badge-stack-count'>
{'×'}{count}
</span>
)}
</span>
</a>
</TooltipWrapper>
))}
{groups.length > MAX_BADGES && (
<TooltipWrapper
tooltipContent={intl.formatMessage(
{id: 'badges.and_more', defaultMessage: 'и ещё {count}. Нажмите, чтобы увидеть все.'},
{count: groups.length - MAX_BADGES},
)}
>
<button
id='showMoreButton'
onClick={handleMoreClick}
>
<span className={'fa fa-angle-right'}/>
</button>
</TooltipWrapper>
)}
</div>
{!loaded && (
<div style={{height: BADGE_SIZE, minWidth: 66, maxWidth}}>
<div className='spinner'/>
</div>
);
}
return (
<div id='badgePlugin'>
<div><b>
<FormattedMessage
id='badges.popover.title'
defaultMessage='Достижения'
/>
</b></div>
<div id='contentContainer' >
{content}
{andMore}
</div>
{loading}
<button
id='grantBadgeButton'
onClick={this.onGrantClick}
>
<span className={'fa fa-plus-circle'}/>
<FormattedMessage
id='badges.grant_badge'
defaultMessage='Выдать достижение'
/>
</button>
<hr className='divider divider--expanded'/>
</div>
);
}
}
)}
<button
id='grantBadgeButton'
onClick={handleGrantClick}
>
<span className={'fa fa-plus-circle'}/>
<FormattedMessage
id='badges.grant_badge'
defaultMessage='Выдать достижение'
/>
</button>
<hr className='divider divider--expanded'/>
</div>
);
};
export default injectIntl(BadgeList);
export default BadgeList;

View File

@ -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<Props> = ({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'}
Review

такой конструкции я еще не видел, жестка

такой конструкции я еще не видел, жестка
Review

Что тут происходит?) Согласен с Владмиром

Что тут происходит?) Согласен с Владмиром
{descRow}{'\n'}
{badge.reason && (
<>
{intl.formatMessage(
{id: 'badges.label.reason', defaultMessage: 'Причина: {reason}'},
{reason: badge.reason},
)}{'\n'}
</>
)}
{grantedBy}{'\n'}
{grantedAt}
</>
);
};
export default BadgeTooltip;

View File

@ -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<void>;
setRHSBadge: (id: BadgeID | null) => Promise<void>;
setRHSUser: (id: string | null) => Promise<void>;
openGrant: (user?: string, badge?: string) => Promise<void>;
getCustomEmojisByName: (names: string[]) => Promise<unknown>;
}
function mapDispatchToProps(dispatch: Dispatch) {
return {
actions: bindActionCreators<ActionCreatorsMapObject, Actions>({
setRHSView,
setRHSBadge,
setRHSUser,
openGrant,
getCustomEmojisByName,
}, dispatch),
};
}
export default connect(mapStateToProps, mapDispatchToProps)(BadgeList);
export {default} from './badge_list';

View File

@ -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<BadgeID, BadgeGroup>();
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;
}