mirror of
https://github.com/mattermost/mattermost.git
synced 2025-02-25 18:55:24 -06:00
MM-47064/MM-34345/MM-47072 Remove inheritance from Suggestion components and migrate to TS (#23455)
* MM-47064 Remove inheritance from Suggestion components * Address feedback * Fix users without DM channels not appearing in the channel switcher --------- Co-authored-by: Mattermost Build <build@mattermost.com>
This commit is contained in:
parent
68be3a6bcd
commit
19ece476ad
@ -89,6 +89,7 @@ exports[`components/AddUserToChannelModal should match snapshot 1`] = `
|
||||
"latestComplete": true,
|
||||
"latestPrefix": "",
|
||||
"requestStarted": false,
|
||||
"triggerCharacter": undefined,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
@ -8,7 +8,7 @@ import {FormattedMessage} from 'react-intl';
|
||||
import {getFullName} from 'mattermost-redux/utils/user_utils';
|
||||
import {ActionResult} from 'mattermost-redux/types/actions';
|
||||
|
||||
import SearchChannelWithPermissionsProvider from 'components/suggestion/search_channel_with_permissions_provider.jsx';
|
||||
import SearchChannelWithPermissionsProvider from 'components/suggestion/search_channel_with_permissions_provider';
|
||||
import SuggestionBox from 'components/suggestion/suggestion_box';
|
||||
import SuggestionBoxComponent from 'components/suggestion/suggestion_box/suggestion_box';
|
||||
import ModalSuggestionList from 'components/suggestion/modal_suggestion_list';
|
||||
@ -54,7 +54,7 @@ export type Props = {
|
||||
* SearchChannelWithPermissionsProvider class to fetch channels
|
||||
* based on a search term
|
||||
*/
|
||||
autocompleteChannelsForSearch: (teamId: string, term: string) => Promise<ActionResult>;
|
||||
autocompleteChannelsForSearch: (teamId: string, term: string) => Promise<ActionResult<Channel[]>>;
|
||||
};
|
||||
|
||||
}
|
||||
|
@ -249,7 +249,7 @@ function ForwardPostChannelSelect({onSelect, value, currentBodyHeight}: Props<Ch
|
||||
const getDefaultResults = () => {
|
||||
let options: GroupedOption[] = [];
|
||||
|
||||
const handleDefaultResults = (res: ProviderResult) => {
|
||||
const handleDefaultResults = (res: ProviderResult<any>) => {
|
||||
options = [
|
||||
{
|
||||
label: formatMessage({id: 'suggestion.mention.recent.channels', defaultMessage: 'Recent'}),
|
||||
@ -278,7 +278,7 @@ function ForwardPostChannelSelect({onSelect, value, currentBodyHeight}: Props<Ch
|
||||
*
|
||||
* @see {@link components/suggestion/switch_channel_provider.jsx}
|
||||
*/
|
||||
const handleResults = async (res: ProviderResult) => {
|
||||
const handleResults = async (res: ProviderResult<any>) => {
|
||||
callCount++;
|
||||
await res.items.filter((item) => item?.channel && isValidChannelType(item.channel) && !item.deactivated).forEach((item) => {
|
||||
const {channel} = item;
|
||||
|
@ -90,6 +90,7 @@ exports[`components/QuickSwitchModal should match snapshot 1`] = `
|
||||
"latestComplete": true,
|
||||
"latestPrefix": "",
|
||||
"requestStarted": false,
|
||||
"triggerCharacter": undefined,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
@ -19,7 +19,7 @@ import FormattedMarkdownMessage from 'components/formatted_markdown_message';
|
||||
import SuggestionBox from 'components/suggestion/suggestion_box';
|
||||
import SuggestionBoxComponent from 'components/suggestion/suggestion_box/suggestion_box';
|
||||
import SuggestionList from 'components/suggestion/suggestion_list.jsx';
|
||||
import SwitchChannelProvider from 'components/suggestion/switch_channel_provider.jsx';
|
||||
import SwitchChannelProvider from 'components/suggestion/switch_channel_provider';
|
||||
import NoResultsIndicator from 'components/no_results_indicator/no_results_indicator';
|
||||
|
||||
const CHANNEL_MODE = 'channel';
|
||||
|
@ -2,44 +2,7 @@
|
||||
|
||||
exports[`at mention suggestion Should display nick name of non signed in user 1`] = `
|
||||
<AtMentionSuggestion
|
||||
intl={
|
||||
Object {
|
||||
"$t": [Function],
|
||||
"defaultFormats": Object {},
|
||||
"defaultLocale": "en",
|
||||
"defaultRichTextElements": undefined,
|
||||
"fallbackOnEmptyString": true,
|
||||
"formatDate": [Function],
|
||||
"formatDateTimeRange": [Function],
|
||||
"formatDateToParts": [Function],
|
||||
"formatDisplayName": [Function],
|
||||
"formatList": [Function],
|
||||
"formatListToParts": [Function],
|
||||
"formatMessage": [Function],
|
||||
"formatNumber": [Function],
|
||||
"formatNumberToParts": [Function],
|
||||
"formatPlural": [Function],
|
||||
"formatRelativeTime": [Function],
|
||||
"formatTime": [Function],
|
||||
"formatTimeToParts": [Function],
|
||||
"formats": Object {},
|
||||
"formatters": Object {
|
||||
"getDateTimeFormat": [Function],
|
||||
"getDisplayNames": [Function],
|
||||
"getListFormat": [Function],
|
||||
"getMessageFormat": [Function],
|
||||
"getNumberFormat": [Function],
|
||||
"getPluralRules": [Function],
|
||||
"getRelativeTimeFormat": [Function],
|
||||
},
|
||||
"locale": "en",
|
||||
"messages": Object {},
|
||||
"onError": [Function],
|
||||
"onWarn": [Function],
|
||||
"textComponent": "span",
|
||||
"timeZone": "Etc/UTC",
|
||||
}
|
||||
}
|
||||
isSelection={false}
|
||||
item={
|
||||
Object {
|
||||
"first_name": "a",
|
||||
@ -50,108 +13,91 @@ exports[`at mention suggestion Should display nick name of non signed in user 1`
|
||||
}
|
||||
}
|
||||
matchedPretext="@"
|
||||
onClick={[MockFunction]}
|
||||
onMouseMove={[MockFunction]}
|
||||
term="@user"
|
||||
>
|
||||
<div
|
||||
className="suggestion-list__item"
|
||||
<SuggestionContainer
|
||||
data-testid="mentionSuggestion_user2"
|
||||
onClick={[Function]}
|
||||
onMouseMove={[Function]}
|
||||
role="button"
|
||||
tabIndex={-1}
|
||||
isSelection={false}
|
||||
item={
|
||||
Object {
|
||||
"first_name": "a",
|
||||
"id": "userid2",
|
||||
"last_name": "b",
|
||||
"nickname": "c",
|
||||
"username": "user2",
|
||||
}
|
||||
}
|
||||
matchedPretext="@"
|
||||
onClick={[MockFunction]}
|
||||
onMouseMove={[MockFunction]}
|
||||
term="@user"
|
||||
>
|
||||
<span
|
||||
className="status-wrapper style--none"
|
||||
<div
|
||||
className="suggestion-list__item"
|
||||
data-testid="mentionSuggestion_user2"
|
||||
onClick={[Function]}
|
||||
onMouseMove={[Function]}
|
||||
role="button"
|
||||
tabIndex={-1}
|
||||
>
|
||||
<span
|
||||
className="profile-icon"
|
||||
className="status-wrapper style--none"
|
||||
>
|
||||
<Memo(Avatar)
|
||||
size="sm"
|
||||
url="/api/v4/users/userid2/image?_=0"
|
||||
username="user2"
|
||||
<span
|
||||
className="profile-icon"
|
||||
>
|
||||
<img
|
||||
alt="user2 profile image"
|
||||
className="Avatar Avatar-sm"
|
||||
loading="lazy"
|
||||
onError={[Function]}
|
||||
src="/api/v4/users/userid2/image?_=0"
|
||||
tabIndex={0}
|
||||
/>
|
||||
</Memo(Avatar)>
|
||||
<Memo(Avatar)
|
||||
size="sm"
|
||||
url="/api/v4/users/userid2/image?_=0"
|
||||
username="user2"
|
||||
>
|
||||
<img
|
||||
alt="user2 profile image"
|
||||
className="Avatar Avatar-sm"
|
||||
loading="lazy"
|
||||
onError={[Function]}
|
||||
src="/api/v4/users/userid2/image?_=0"
|
||||
tabIndex={0}
|
||||
/>
|
||||
</Memo(Avatar)>
|
||||
</span>
|
||||
<StatusIcon
|
||||
button={false}
|
||||
className=""
|
||||
/>
|
||||
</span>
|
||||
<StatusIcon
|
||||
button={false}
|
||||
className=""
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
className="suggestion-list__ellipsis"
|
||||
>
|
||||
<span
|
||||
className="suggestion-list__main"
|
||||
className="suggestion-list__ellipsis"
|
||||
>
|
||||
@user2
|
||||
</span>
|
||||
a b (c)
|
||||
<Component
|
||||
emojiSize={15}
|
||||
emojiStyle={
|
||||
Object {
|
||||
"margin": "0 4px 4px",
|
||||
<span
|
||||
className="suggestion-list__main"
|
||||
>
|
||||
@user2
|
||||
</span>
|
||||
a b (c)
|
||||
<Component
|
||||
emojiSize={15}
|
||||
emojiStyle={
|
||||
Object {
|
||||
"margin": "0 4px 4px",
|
||||
}
|
||||
}
|
||||
}
|
||||
showTooltip={true}
|
||||
userID="userid2"
|
||||
>
|
||||
<div />
|
||||
</Component>
|
||||
</span>
|
||||
</div>
|
||||
showTooltip={true}
|
||||
userID="userid2"
|
||||
>
|
||||
<div />
|
||||
</Component>
|
||||
</span>
|
||||
</div>
|
||||
</SuggestionContainer>
|
||||
</AtMentionSuggestion>
|
||||
`;
|
||||
|
||||
exports[`at mention suggestion Should not display nick name of the signed in user 1`] = `
|
||||
<AtMentionSuggestion
|
||||
intl={
|
||||
Object {
|
||||
"$t": [Function],
|
||||
"defaultFormats": Object {},
|
||||
"defaultLocale": "en",
|
||||
"defaultRichTextElements": undefined,
|
||||
"fallbackOnEmptyString": true,
|
||||
"formatDate": [Function],
|
||||
"formatDateTimeRange": [Function],
|
||||
"formatDateToParts": [Function],
|
||||
"formatDisplayName": [Function],
|
||||
"formatList": [Function],
|
||||
"formatListToParts": [Function],
|
||||
"formatMessage": [Function],
|
||||
"formatNumber": [Function],
|
||||
"formatNumberToParts": [Function],
|
||||
"formatPlural": [Function],
|
||||
"formatRelativeTime": [Function],
|
||||
"formatTime": [Function],
|
||||
"formatTimeToParts": [Function],
|
||||
"formats": Object {},
|
||||
"formatters": Object {
|
||||
"getDateTimeFormat": [Function],
|
||||
"getDisplayNames": [Function],
|
||||
"getListFormat": [Function],
|
||||
"getMessageFormat": [Function],
|
||||
"getNumberFormat": [Function],
|
||||
"getPluralRules": [Function],
|
||||
"getRelativeTimeFormat": [Function],
|
||||
},
|
||||
"locale": "en",
|
||||
"messages": Object {},
|
||||
"onError": [Function],
|
||||
"onWarn": [Function],
|
||||
"textComponent": "span",
|
||||
"timeZone": "Etc/UTC",
|
||||
}
|
||||
}
|
||||
isSelection={false}
|
||||
item={
|
||||
Object {
|
||||
"first_name": "a",
|
||||
@ -163,72 +109,93 @@ exports[`at mention suggestion Should not display nick name of the signed in use
|
||||
}
|
||||
}
|
||||
matchedPretext="@"
|
||||
onClick={[MockFunction]}
|
||||
onMouseMove={[MockFunction]}
|
||||
term="@user"
|
||||
>
|
||||
<div
|
||||
className="suggestion-list__item"
|
||||
<SuggestionContainer
|
||||
data-testid="mentionSuggestion_user"
|
||||
onClick={[Function]}
|
||||
onMouseMove={[Function]}
|
||||
role="button"
|
||||
tabIndex={-1}
|
||||
isSelection={false}
|
||||
item={
|
||||
Object {
|
||||
"first_name": "a",
|
||||
"id": "userid1",
|
||||
"isCurrentUser": true,
|
||||
"last_name": "b",
|
||||
"nickname": "c",
|
||||
"username": "user",
|
||||
}
|
||||
}
|
||||
matchedPretext="@"
|
||||
onClick={[MockFunction]}
|
||||
onMouseMove={[MockFunction]}
|
||||
term="@user"
|
||||
>
|
||||
<span
|
||||
className="status-wrapper style--none"
|
||||
<div
|
||||
className="suggestion-list__item"
|
||||
data-testid="mentionSuggestion_user"
|
||||
onClick={[Function]}
|
||||
onMouseMove={[Function]}
|
||||
role="button"
|
||||
tabIndex={-1}
|
||||
>
|
||||
<span
|
||||
className="profile-icon"
|
||||
className="status-wrapper style--none"
|
||||
>
|
||||
<Memo(Avatar)
|
||||
size="sm"
|
||||
url="/api/v4/users/userid1/image?_=0"
|
||||
username="user"
|
||||
<span
|
||||
className="profile-icon"
|
||||
>
|
||||
<img
|
||||
alt="user profile image"
|
||||
className="Avatar Avatar-sm"
|
||||
loading="lazy"
|
||||
onError={[Function]}
|
||||
src="/api/v4/users/userid1/image?_=0"
|
||||
tabIndex={0}
|
||||
/>
|
||||
</Memo(Avatar)>
|
||||
</span>
|
||||
<StatusIcon
|
||||
button={false}
|
||||
className=""
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
className="suggestion-list__ellipsis"
|
||||
>
|
||||
<span
|
||||
className="suggestion-list__main"
|
||||
>
|
||||
@user
|
||||
</span>
|
||||
a b
|
||||
<FormattedMessage
|
||||
defaultMessage="(you)"
|
||||
id="suggestion.user.isCurrent"
|
||||
>
|
||||
<span>
|
||||
(you)
|
||||
<Memo(Avatar)
|
||||
size="sm"
|
||||
url="/api/v4/users/userid1/image?_=0"
|
||||
username="user"
|
||||
>
|
||||
<img
|
||||
alt="user profile image"
|
||||
className="Avatar Avatar-sm"
|
||||
loading="lazy"
|
||||
onError={[Function]}
|
||||
src="/api/v4/users/userid1/image?_=0"
|
||||
tabIndex={0}
|
||||
/>
|
||||
</Memo(Avatar)>
|
||||
</span>
|
||||
</FormattedMessage>
|
||||
<Component
|
||||
emojiSize={15}
|
||||
emojiStyle={
|
||||
Object {
|
||||
"margin": "0 4px 4px",
|
||||
}
|
||||
}
|
||||
showTooltip={true}
|
||||
userID="userid1"
|
||||
<StatusIcon
|
||||
button={false}
|
||||
className=""
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
className="suggestion-list__ellipsis"
|
||||
>
|
||||
<div />
|
||||
</Component>
|
||||
</span>
|
||||
</div>
|
||||
<span
|
||||
className="suggestion-list__main"
|
||||
>
|
||||
@user
|
||||
</span>
|
||||
a b
|
||||
<FormattedMessage
|
||||
defaultMessage="(you)"
|
||||
id="suggestion.user.isCurrent"
|
||||
>
|
||||
<span>
|
||||
(you)
|
||||
</span>
|
||||
</FormattedMessage>
|
||||
<Component
|
||||
emojiSize={15}
|
||||
emojiStyle={
|
||||
Object {
|
||||
"margin": "0 4px 4px",
|
||||
}
|
||||
}
|
||||
showTooltip={true}
|
||||
userID="userid1"
|
||||
>
|
||||
<div />
|
||||
</Component>
|
||||
</span>
|
||||
</div>
|
||||
</SuggestionContainer>
|
||||
</AtMentionSuggestion>
|
||||
`;
|
||||
|
@ -4,9 +4,10 @@
|
||||
import React from 'react';
|
||||
|
||||
import * as Utils from 'utils/utils';
|
||||
import AtMentionSuggestion from 'components/suggestion/at_mention_provider/at_mention_suggestion';
|
||||
import {mountWithIntl} from 'tests/helpers/intl-test-helper';
|
||||
|
||||
import AtMentionSuggestion, {Item} from './at_mention_suggestion';
|
||||
|
||||
jest.mock('components/custom_status/custom_status_emoji', () => () => <div/>);
|
||||
jest.spyOn(Utils, 'getFullName').mockReturnValue('a b');
|
||||
|
||||
@ -18,7 +19,7 @@ describe('at mention suggestion', () => {
|
||||
last_name: 'b',
|
||||
nickname: 'c',
|
||||
isCurrentUser: true,
|
||||
};
|
||||
} as Item;
|
||||
|
||||
const userid2 = {
|
||||
id: 'userid2',
|
||||
@ -26,14 +27,21 @@ describe('at mention suggestion', () => {
|
||||
first_name: 'a',
|
||||
last_name: 'b',
|
||||
nickname: 'c',
|
||||
} as Item;
|
||||
|
||||
const baseProps = {
|
||||
matchedPretext: '@',
|
||||
term: '@user',
|
||||
isSelection: false,
|
||||
onClick: jest.fn(),
|
||||
onMouseMove: jest.fn(),
|
||||
};
|
||||
|
||||
it('Should not display nick name of the signed in user', () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<AtMentionSuggestion
|
||||
{...baseProps}
|
||||
item={userid1}
|
||||
matchedPretext='@'
|
||||
term='@user'
|
||||
/>,
|
||||
);
|
||||
|
||||
@ -46,9 +54,8 @@ describe('at mention suggestion', () => {
|
||||
it('Should display nick name of non signed in user', () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<AtMentionSuggestion
|
||||
{...baseProps}
|
||||
item={userid2}
|
||||
matchedPretext='@'
|
||||
term='@user'
|
||||
/>,
|
||||
);
|
||||
|
||||
|
@ -2,9 +2,7 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {ReactNode} from 'react';
|
||||
import {FormattedMessage, injectIntl} from 'react-intl';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import {FormattedMessage, useIntl} from 'react-intl';
|
||||
|
||||
import {Constants} from 'utils/constants';
|
||||
import * as Utils from 'utils/utils';
|
||||
@ -18,11 +16,11 @@ import GuestTag from 'components/widgets/tag/guest_tag';
|
||||
import CustomStatusEmoji from 'components/custom_status/custom_status_emoji';
|
||||
import StatusIcon from 'components/status_icon';
|
||||
|
||||
import Suggestion from '../suggestion.jsx';
|
||||
import {SuggestionContainer, SuggestionProps} from '../suggestion';
|
||||
|
||||
import {UserProfile} from '@mattermost/types/users';
|
||||
|
||||
interface Item extends UserProfile {
|
||||
export interface Item extends UserProfile {
|
||||
display_name: string;
|
||||
name: string;
|
||||
isCurrentUser: boolean;
|
||||
@ -33,169 +31,167 @@ interface Group extends Item {
|
||||
member_count: number;
|
||||
}
|
||||
|
||||
class AtMentionSuggestion extends Suggestion {
|
||||
render() {
|
||||
const {intl} = this.props;
|
||||
const {isSelection, item} = this.props;
|
||||
const AtMentionSuggestion = React.forwardRef<HTMLDivElement, SuggestionProps<Item>>((props, ref) => {
|
||||
const {item} = props;
|
||||
|
||||
let itemname: string;
|
||||
let description: ReactNode;
|
||||
let icon: JSX.Element;
|
||||
let customStatus: ReactNode;
|
||||
if (item.username === 'all') {
|
||||
itemname = 'all';
|
||||
description = (
|
||||
<FormattedMessage
|
||||
id='suggestion.mention.all'
|
||||
defaultMessage='Notifies everyone in this channel'
|
||||
/>
|
||||
);
|
||||
icon = (
|
||||
<span className='suggestion-list__icon suggestion-list__icon--large'>
|
||||
<i
|
||||
className='icon icon-account-multiple-outline'
|
||||
title={intl.formatMessage({id: 'generic_icons.member', defaultMessage: 'Member Icon'})}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
} else if (item.username === 'channel') {
|
||||
itemname = 'channel';
|
||||
description = (
|
||||
<FormattedMessage
|
||||
id='suggestion.mention.channel'
|
||||
defaultMessage='Notifies everyone in this channel'
|
||||
/>
|
||||
);
|
||||
icon = (
|
||||
<span className='suggestion-list__icon suggestion-list__icon--large'>
|
||||
<i
|
||||
className='icon icon-account-multiple-outline'
|
||||
title={intl.formatMessage({id: 'generic_icons.member', defaultMessage: 'Member Icon'})}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
} else if (item.username === 'here') {
|
||||
itemname = 'here';
|
||||
description = (
|
||||
<FormattedMessage
|
||||
id='suggestion.mention.here'
|
||||
defaultMessage='Notifies everyone online in this channel'
|
||||
/>
|
||||
);
|
||||
icon = (
|
||||
<span className='suggestion-list__icon suggestion-list__icon--large'>
|
||||
<i
|
||||
className='icon icon-account-multiple-outline'
|
||||
title={intl.formatMessage({id: 'generic_icons.member', defaultMessage: 'Member Icon'})}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
} else if (item.type === Constants.MENTION_GROUPS) {
|
||||
itemname = item.name;
|
||||
description = (
|
||||
<span className='ml-1'>{'- '}{item.display_name}</span>
|
||||
);
|
||||
icon = (
|
||||
<span className='suggestion-list__icon suggestion-list__icon--large'>
|
||||
<i
|
||||
className='icon icon-account-multiple-outline'
|
||||
title={intl.formatMessage({id: 'generic_icons.member', defaultMessage: 'Member Icon'})}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
} else {
|
||||
itemname = item.username;
|
||||
const intl = useIntl();
|
||||
|
||||
if (item.isCurrentUser) {
|
||||
if (item.first_name || item.last_name) {
|
||||
description = Utils.getFullName(item);
|
||||
}
|
||||
} else if (item.first_name || item.last_name || item.nickname) {
|
||||
description = `${Utils.getFullName(item)} ${item.nickname ? `(${item.nickname})` : ''}`.trim();
|
||||
}
|
||||
|
||||
icon = (
|
||||
<span className='status-wrapper style--none'>
|
||||
<span className='profile-icon'>
|
||||
<Avatar
|
||||
username={item && item.username}
|
||||
size='sm'
|
||||
url={Utils.imageURLForUser(item.id, item.last_picture_update)}
|
||||
/>
|
||||
</span>
|
||||
<StatusIcon status={item && item.status}/>
|
||||
</span>
|
||||
);
|
||||
|
||||
customStatus = (
|
||||
<CustomStatusEmoji
|
||||
showTooltip={true}
|
||||
userID={item.id}
|
||||
emojiSize={15}
|
||||
emojiStyle={{
|
||||
margin: '0 4px 4px',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const youElement = item.isCurrentUser ? (
|
||||
let itemname: string;
|
||||
let description: ReactNode;
|
||||
let icon: JSX.Element;
|
||||
let customStatus: ReactNode;
|
||||
if (item.username === 'all') {
|
||||
itemname = 'all';
|
||||
description = (
|
||||
<FormattedMessage
|
||||
id='suggestion.user.isCurrent'
|
||||
defaultMessage='(you)'
|
||||
id='suggestion.mention.all'
|
||||
defaultMessage='Notifies everyone in this channel'
|
||||
/>
|
||||
) : null;
|
||||
|
||||
const sharedIcon = item.remote_id ? (
|
||||
<SharedUserIndicator
|
||||
className='shared-user-icon'
|
||||
withTooltip={true}
|
||||
);
|
||||
icon = (
|
||||
<span className='suggestion-list__icon suggestion-list__icon--large'>
|
||||
<i
|
||||
className='icon icon-account-multiple-outline'
|
||||
title={intl.formatMessage({id: 'generic_icons.member', defaultMessage: 'Member Icon'})}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
} else if (item.username === 'channel') {
|
||||
itemname = 'channel';
|
||||
description = (
|
||||
<FormattedMessage
|
||||
id='suggestion.mention.channel'
|
||||
defaultMessage='Notifies everyone in this channel'
|
||||
/>
|
||||
) : null;
|
||||
);
|
||||
icon = (
|
||||
<span className='suggestion-list__icon suggestion-list__icon--large'>
|
||||
<i
|
||||
className='icon icon-account-multiple-outline'
|
||||
title={intl.formatMessage({id: 'generic_icons.member', defaultMessage: 'Member Icon'})}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
} else if (item.username === 'here') {
|
||||
itemname = 'here';
|
||||
description = (
|
||||
<FormattedMessage
|
||||
id='suggestion.mention.here'
|
||||
defaultMessage='Notifies everyone online in this channel'
|
||||
/>
|
||||
);
|
||||
icon = (
|
||||
<span className='suggestion-list__icon suggestion-list__icon--large'>
|
||||
<i
|
||||
className='icon icon-account-multiple-outline'
|
||||
title={intl.formatMessage({id: 'generic_icons.member', defaultMessage: 'Member Icon'})}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
} else if (item.type === Constants.MENTION_GROUPS) {
|
||||
itemname = item.name;
|
||||
description = (
|
||||
<span className='ml-1'>{'- '}{item.display_name}</span>
|
||||
);
|
||||
icon = (
|
||||
<span className='suggestion-list__icon suggestion-list__icon--large'>
|
||||
<i
|
||||
className='icon icon-account-multiple-outline'
|
||||
title={intl.formatMessage({id: 'generic_icons.member', defaultMessage: 'Member Icon'})}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
} else {
|
||||
itemname = item.username;
|
||||
|
||||
let countBadge;
|
||||
if (item.type === Constants.MENTION_GROUPS) {
|
||||
countBadge = (
|
||||
<span className='suggestion-list__group-count'>
|
||||
<Tag
|
||||
text={
|
||||
<FormattedMessage
|
||||
id='suggestion.group.members'
|
||||
defaultMessage='{member_count} {member_count, plural, one {member} other {members}}'
|
||||
values={{
|
||||
member_count: (item as Group).member_count,
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
if (item.isCurrentUser) {
|
||||
if (item.first_name || item.last_name) {
|
||||
description = Utils.getFullName(item);
|
||||
}
|
||||
} else if (item.first_name || item.last_name || item.nickname) {
|
||||
description = `${Utils.getFullName(item)} ${item.nickname ? `(${item.nickname})` : ''}`.trim();
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames('suggestion-list__item', {'suggestion--selected': isSelection})}
|
||||
data-testid={`mentionSuggestion_${itemname}`}
|
||||
onClick={this.handleClick}
|
||||
onMouseMove={this.handleMouseMove}
|
||||
{...Suggestion.baseProps}
|
||||
>
|
||||
{icon}
|
||||
<span className='suggestion-list__ellipsis'>
|
||||
<span className='suggestion-list__main'>
|
||||
{'@' + itemname}
|
||||
</span>
|
||||
{item.is_bot && <BotTag/>}
|
||||
{description}
|
||||
{youElement}
|
||||
{customStatus}
|
||||
{sharedIcon}
|
||||
{isGuest(item.roles) && <GuestTag/>}
|
||||
icon = (
|
||||
<span className='status-wrapper style--none'>
|
||||
<span className='profile-icon'>
|
||||
<Avatar
|
||||
username={item && item.username}
|
||||
size='sm'
|
||||
url={Utils.imageURLForUser(item.id, item.last_picture_update)}
|
||||
/>
|
||||
</span>
|
||||
{countBadge}
|
||||
</div>
|
||||
<StatusIcon status={item && item.status}/>
|
||||
</span>
|
||||
);
|
||||
|
||||
customStatus = (
|
||||
<CustomStatusEmoji
|
||||
showTooltip={true}
|
||||
userID={item.id}
|
||||
emojiSize={15}
|
||||
emojiStyle={{
|
||||
margin: '0 4px 4px',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default injectIntl(AtMentionSuggestion);
|
||||
const youElement = item.isCurrentUser ? (
|
||||
<FormattedMessage
|
||||
id='suggestion.user.isCurrent'
|
||||
defaultMessage='(you)'
|
||||
/>
|
||||
) : null;
|
||||
|
||||
const sharedIcon = item.remote_id ? (
|
||||
<SharedUserIndicator
|
||||
className='shared-user-icon'
|
||||
withTooltip={true}
|
||||
/>
|
||||
) : null;
|
||||
|
||||
let countBadge;
|
||||
if (item.type === Constants.MENTION_GROUPS) {
|
||||
countBadge = (
|
||||
<span className='suggestion-list__group-count'>
|
||||
<Tag
|
||||
text={
|
||||
<FormattedMessage
|
||||
id='suggestion.group.members'
|
||||
defaultMessage='{member_count} {member_count, plural, one {member} other {members}}'
|
||||
values={{
|
||||
member_count: (item as Group).member_count,
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SuggestionContainer
|
||||
ref={ref}
|
||||
{...props}
|
||||
data-testid={`mentionSuggestion_${itemname}`}
|
||||
>
|
||||
{icon}
|
||||
<span className='suggestion-list__ellipsis'>
|
||||
<span className='suggestion-list__main'>
|
||||
{'@' + itemname}
|
||||
</span>
|
||||
{item.is_bot && <BotTag/>}
|
||||
{description}
|
||||
{youElement}
|
||||
{customStatus}
|
||||
{sharedIcon}
|
||||
{isGuest(item.roles) && <GuestTag/>}
|
||||
</span>
|
||||
{countBadge}
|
||||
</SuggestionContainer>
|
||||
);
|
||||
});
|
||||
|
||||
AtMentionSuggestion.displayName = 'AtMentionSuggestion';
|
||||
export default AtMentionSuggestion;
|
||||
|
@ -14,73 +14,55 @@ import store from 'stores/redux_store.jsx';
|
||||
|
||||
import {Constants} from 'utils/constants';
|
||||
|
||||
import Provider from './provider';
|
||||
import Suggestion from './suggestion.jsx';
|
||||
import Provider, {ResultsCallback} from './provider';
|
||||
import {SuggestionContainer, SuggestionProps} from './suggestion';
|
||||
|
||||
export const MIN_CHANNEL_LINK_LENGTH = 2;
|
||||
|
||||
export type Results = {
|
||||
matchedPretext: string;
|
||||
terms: string[];
|
||||
items: WrappedChannels[];
|
||||
component: React.ElementType;
|
||||
}
|
||||
|
||||
type ResultsCallback = (results: Results) => void;
|
||||
|
||||
type WrappedChannels = {
|
||||
type WrappedChannel = {
|
||||
type: string;
|
||||
channel?: Channel;
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
export class ChannelMentionSuggestion extends Suggestion {
|
||||
render() {
|
||||
const isSelection = this.props.isSelection;
|
||||
const item = this.props.item;
|
||||
const channelIsArchived = item.channel.delete_at && item.channel.delete_at !== 0;
|
||||
export const ChannelMentionSuggestion = React.forwardRef<HTMLDivElement, SuggestionProps<WrappedChannel>>((props, ref) => {
|
||||
const {item} = props;
|
||||
const channelIsArchived = item.channel && item.channel.delete_at && item.channel.delete_at !== 0;
|
||||
|
||||
const channelName = item.channel.display_name;
|
||||
let channelIcon;
|
||||
if (channelIsArchived) {
|
||||
channelIcon = (
|
||||
<span className='suggestion-list__icon suggestion-list__icon--large'>
|
||||
<i className='icon icon-archive-outline'/>
|
||||
</span>
|
||||
);
|
||||
} else {
|
||||
channelIcon = (
|
||||
<span className='suggestion-list__icon suggestion-list__icon--large'>
|
||||
<i className={`icon icon--no-spacing icon-${item.channel.type === Constants.OPEN_CHANNEL ? 'globe' : 'lock'}`}/>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
let className = 'suggestion-list__item';
|
||||
if (isSelection) {
|
||||
className += ' suggestion--selected';
|
||||
}
|
||||
|
||||
const description = '~' + item.channel.name;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={className}
|
||||
onClick={this.handleClick}
|
||||
onMouseMove={this.handleMouseMove}
|
||||
{...Suggestion.baseProps}
|
||||
>
|
||||
{channelIcon}
|
||||
<div className='suggestion-list__ellipsis'>
|
||||
<span className='suggestion-list__main'>
|
||||
{channelName}
|
||||
</span>
|
||||
{description}
|
||||
</div>
|
||||
</div>
|
||||
const channelName = item.channel?.display_name;
|
||||
let channelIcon;
|
||||
if (channelIsArchived) {
|
||||
channelIcon = (
|
||||
<span className='suggestion-list__icon suggestion-list__icon--large'>
|
||||
<i className='icon icon-archive-outline'/>
|
||||
</span>
|
||||
);
|
||||
} else {
|
||||
channelIcon = (
|
||||
<span className='suggestion-list__icon suggestion-list__icon--large'>
|
||||
<i className={`icon icon--no-spacing icon-${item.channel?.type === Constants.OPEN_CHANNEL ? 'globe' : 'lock'}`}/>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const description = '~' + item.channel?.name;
|
||||
|
||||
return (
|
||||
<SuggestionContainer
|
||||
ref={ref}
|
||||
{...props}
|
||||
>
|
||||
{channelIcon}
|
||||
<div className='suggestion-list__ellipsis'>
|
||||
<span className='suggestion-list__main'>
|
||||
{channelName}
|
||||
</span>
|
||||
{description}
|
||||
</div>
|
||||
</SuggestionContainer>
|
||||
);
|
||||
});
|
||||
ChannelMentionSuggestion.displayName = 'ChannelMentionSuggestion';
|
||||
|
||||
export default class ChannelMentionProvider extends Provider {
|
||||
private lastPrefixTrimmed: string;
|
||||
@ -106,7 +88,7 @@ export default class ChannelMentionProvider extends Provider {
|
||||
this.delayChannelAutocomplete = props.delayChannelAutocomplete;
|
||||
}
|
||||
|
||||
handlePretextChanged(pretext: string, resultCallback: ResultsCallback) {
|
||||
handlePretextChanged(pretext: string, resultCallback: ResultsCallback<WrappedChannel>) {
|
||||
this.resetRequest();
|
||||
|
||||
const captured = (/\B(~([^~\r\n]*))$/i).exec(pretext.toLowerCase());
|
||||
@ -151,7 +133,7 @@ export default class ChannelMentionProvider extends Provider {
|
||||
|
||||
const words = prefix.toLowerCase().split(/\s+/);
|
||||
const wrappedChannelIds: Record<string, boolean> = {};
|
||||
let wrappedChannels: WrappedChannels[] = [];
|
||||
let wrappedChannels: WrappedChannel[] = [];
|
||||
getMyChannels(store.getState()).forEach((item) => {
|
||||
if (item.type !== 'O' || item.delete_at > 0) {
|
||||
return;
|
||||
@ -212,7 +194,7 @@ export default class ChannelMentionProvider extends Provider {
|
||||
}
|
||||
|
||||
// Wrap channels in an outer object to avoid overwriting the 'type' property.
|
||||
const wrappedMoreChannels: WrappedChannels[] = [];
|
||||
const wrappedMoreChannels: WrappedChannel[] = [];
|
||||
channels.forEach((item) => {
|
||||
if (item.delete_at > 0 && !myMembers[item.id]) {
|
||||
return;
|
||||
|
@ -1,12 +1,21 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`CommandSuggestion should match snapshot 1`] = `
|
||||
<div
|
||||
className="slash-command suggestion--selected"
|
||||
onClick={[Function]}
|
||||
onMouseMove={[Function]}
|
||||
role="button"
|
||||
tabIndex={-1}
|
||||
<SuggestionContainer
|
||||
isSelection={true}
|
||||
item={
|
||||
Object {
|
||||
"Complete": "/invite",
|
||||
"Description": "Invite a user to a channel",
|
||||
"Hint": "@[username] ~[channel]",
|
||||
"IconData": "",
|
||||
"Suggestion": "/invite",
|
||||
}
|
||||
}
|
||||
matchedPretext=""
|
||||
onClick={[MockFunction]}
|
||||
onMouseMove={[MockFunction]}
|
||||
term="/"
|
||||
>
|
||||
<div
|
||||
className="slash-command__icon"
|
||||
@ -29,5 +38,5 @@ exports[`CommandSuggestion should match snapshot 1`] = `
|
||||
Invite a user to a channel
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SuggestionContainer>
|
||||
`;
|
||||
|
@ -7,7 +7,7 @@ import {Store} from 'redux';
|
||||
|
||||
import globalStore from 'stores/redux_store';
|
||||
|
||||
import Provider from '../provider';
|
||||
import Provider, {ResultsCallback} from '../provider';
|
||||
import {GlobalState} from 'types/store';
|
||||
|
||||
import {Constants} from 'utils/constants';
|
||||
@ -31,19 +31,11 @@ type Props = {
|
||||
rootId?: string;
|
||||
};
|
||||
|
||||
export type Results = {
|
||||
matchedPretext: string;
|
||||
terms: string[];
|
||||
items: Array<AutocompleteSuggestion | UserProfile | {channel: Channel}>;
|
||||
component?: React.ElementType;
|
||||
components?: React.ElementType[];
|
||||
}
|
||||
|
||||
type ResultsCallback = (results: Results) => void;
|
||||
type Item = AutocompleteSuggestion | UserProfile | {channel: Channel};
|
||||
|
||||
export default class AppCommandProvider extends Provider {
|
||||
private store: Store<GlobalState>;
|
||||
private triggerCharacter: string;
|
||||
public triggerCharacter: string;
|
||||
private appCommandParser: AppCommandParser;
|
||||
|
||||
constructor(props: Props) {
|
||||
@ -58,7 +50,7 @@ export default class AppCommandProvider extends Provider {
|
||||
this.appCommandParser.setChannelContext(props.channelId, props.teamId, props.rootId);
|
||||
}
|
||||
|
||||
handlePretextChanged(pretext: string, resultCallback: ResultsCallback) {
|
||||
handlePretextChanged(pretext: string, resultCallback: ResultsCallback<Item>) {
|
||||
if (!pretext.startsWith(this.triggerCharacter)) {
|
||||
return false;
|
||||
}
|
||||
|
@ -8,7 +8,7 @@ import {Client4} from 'mattermost-redux/client';
|
||||
|
||||
import {AutocompleteSuggestion} from '@mattermost/types/integrations';
|
||||
|
||||
import CommandProvider, {CommandSuggestion, Results} from './command_provider';
|
||||
import CommandProvider, {CommandSuggestion} from './command_provider';
|
||||
|
||||
describe('CommandSuggestion', () => {
|
||||
const suggestion: AutocompleteSuggestion = {
|
||||
@ -24,6 +24,8 @@ describe('CommandSuggestion', () => {
|
||||
isSelection: true,
|
||||
term: '/',
|
||||
matchedPretext: '',
|
||||
onClick: jest.fn(),
|
||||
onMouseMove: jest.fn(),
|
||||
};
|
||||
|
||||
test('should match snapshot', () => {
|
||||
@ -61,7 +63,7 @@ describe('CommandProvider', () => {
|
||||
provider.handlePretextChanged('/jira issue', callback);
|
||||
await mockFunc();
|
||||
|
||||
const expected: Results = {
|
||||
const expected = {
|
||||
matchedPretext: '/jira issue',
|
||||
terms: ['/jira issue'],
|
||||
items: [{
|
||||
@ -102,7 +104,7 @@ describe('CommandProvider', () => {
|
||||
provider.handlePretextChanged('/jira issue', callback);
|
||||
await mockFunc();
|
||||
|
||||
const expected: Results = {
|
||||
const expected = {
|
||||
matchedPretext: '/jira issue',
|
||||
terms: ['/jira issue'],
|
||||
items: [{
|
||||
|
@ -14,8 +14,8 @@ import globalStore from 'stores/redux_store';
|
||||
import * as UserAgent from 'utils/user_agent';
|
||||
import {Constants} from 'utils/constants';
|
||||
|
||||
import Suggestion from '../suggestion';
|
||||
import Provider from '../provider';
|
||||
import {SuggestionContainer, SuggestionProps} from '../suggestion';
|
||||
import Provider, {ResultsCallback} from '../provider';
|
||||
|
||||
import {GlobalState} from 'types/store';
|
||||
|
||||
@ -27,62 +27,55 @@ const EXECUTE_CURRENT_COMMAND_ITEM_ID = Constants.Integrations.EXECUTE_CURRENT_C
|
||||
const OPEN_COMMAND_IN_MODAL_ITEM_ID = Constants.Integrations.OPEN_COMMAND_IN_MODAL_ITEM_ID;
|
||||
const COMMAND_SUGGESTION_ERROR = Constants.Integrations.COMMAND_SUGGESTION_ERROR;
|
||||
|
||||
export class CommandSuggestion extends Suggestion {
|
||||
render() {
|
||||
const {isSelection} = this.props;
|
||||
const item = this.props.item as AutocompleteSuggestion;
|
||||
const CommandSuggestion = React.forwardRef<HTMLDivElement, SuggestionProps<AutocompleteSuggestion>>((props, ref) => {
|
||||
const {item} = props;
|
||||
|
||||
let className = 'slash-command';
|
||||
if (isSelection) {
|
||||
className += ' suggestion--selected';
|
||||
}
|
||||
let symbolSpan = <span>{'/'}</span>;
|
||||
switch (item.IconData) {
|
||||
case EXECUTE_CURRENT_COMMAND_ITEM_ID:
|
||||
symbolSpan = <span className='block mt-1'>{'↵'}</span>;
|
||||
break;
|
||||
case OPEN_COMMAND_IN_MODAL_ITEM_ID:
|
||||
symbolSpan = (
|
||||
<span className='block mt-1'>
|
||||
<DockWindowIcon size={28}/>
|
||||
</span>
|
||||
);
|
||||
break;
|
||||
case COMMAND_SUGGESTION_ERROR:
|
||||
symbolSpan = <span>{'!'}</span>;
|
||||
break;
|
||||
}
|
||||
let icon = <div className='slash-command__icon'>{symbolSpan}</div>;
|
||||
if (item.IconData && ![EXECUTE_CURRENT_COMMAND_ITEM_ID, COMMAND_SUGGESTION_ERROR, OPEN_COMMAND_IN_MODAL_ITEM_ID].includes(item.IconData)) {
|
||||
icon = (
|
||||
<div
|
||||
className='slash-command__icon'
|
||||
style={{backgroundColor: 'transparent'}}
|
||||
>
|
||||
<img src={item.IconData}/>
|
||||
</div>);
|
||||
}
|
||||
|
||||
return (
|
||||
let symbolSpan = <span>{'/'}</span>;
|
||||
switch (item.IconData) {
|
||||
case EXECUTE_CURRENT_COMMAND_ITEM_ID:
|
||||
symbolSpan = <span className='block mt-1'>{'↵'}</span>;
|
||||
break;
|
||||
case OPEN_COMMAND_IN_MODAL_ITEM_ID:
|
||||
symbolSpan = (
|
||||
<span className='block mt-1'>
|
||||
<DockWindowIcon size={28}/>
|
||||
</span>
|
||||
);
|
||||
break;
|
||||
case COMMAND_SUGGESTION_ERROR:
|
||||
symbolSpan = <span>{'!'}</span>;
|
||||
break;
|
||||
}
|
||||
let icon = <div className='slash-command__icon'>{symbolSpan}</div>;
|
||||
if (item.IconData && ![EXECUTE_CURRENT_COMMAND_ITEM_ID, COMMAND_SUGGESTION_ERROR, OPEN_COMMAND_IN_MODAL_ITEM_ID].includes(item.IconData)) {
|
||||
icon = (
|
||||
<div
|
||||
className={className}
|
||||
onClick={this.handleClick}
|
||||
onMouseMove={this.handleMouseMove}
|
||||
{...Suggestion.baseProps}
|
||||
className='slash-command__icon'
|
||||
style={{backgroundColor: 'transparent'}}
|
||||
>
|
||||
{icon}
|
||||
<div className='slash-command__info'>
|
||||
<div className='slash-command__title'>
|
||||
{item.Suggestion.substring(1) + ' ' + item.Hint}
|
||||
</div>
|
||||
<div className='slash-command__desc'>
|
||||
{item.Description}
|
||||
</div>
|
||||
<img src={item.IconData}/>
|
||||
</div>);
|
||||
}
|
||||
|
||||
return (
|
||||
<SuggestionContainer
|
||||
ref={ref}
|
||||
{...props}
|
||||
>
|
||||
{icon}
|
||||
<div className='slash-command__info'>
|
||||
<div className='slash-command__title'>
|
||||
{item.Suggestion.substring(1) + ' ' + item.Hint}
|
||||
</div>
|
||||
<div className='slash-command__desc'>
|
||||
{item.Description}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
</SuggestionContainer>
|
||||
);
|
||||
});
|
||||
CommandSuggestion.displayName = 'CommandSuggestion';
|
||||
export {CommandSuggestion};
|
||||
|
||||
type Props = {
|
||||
teamId: string;
|
||||
@ -90,19 +83,10 @@ type Props = {
|
||||
rootId?: string;
|
||||
};
|
||||
|
||||
export type Results = {
|
||||
matchedPretext: string;
|
||||
terms: string[];
|
||||
items: AutocompleteSuggestion[];
|
||||
component: React.ElementType;
|
||||
}
|
||||
|
||||
type ResultsCallback = (results: Results) => void;
|
||||
|
||||
export default class CommandProvider extends Provider {
|
||||
private props: Props;
|
||||
private store: Store<GlobalState>;
|
||||
private triggerCharacter: string;
|
||||
public triggerCharacter: string;
|
||||
private appCommandParser: AppCommandParser;
|
||||
|
||||
constructor(props: Props) {
|
||||
@ -119,7 +103,7 @@ export default class CommandProvider extends Provider {
|
||||
this.appCommandParser.setChannelContext(props.channelId, props.teamId, props.rootId);
|
||||
}
|
||||
|
||||
handlePretextChanged(pretext: string, resultCallback: ResultsCallback) {
|
||||
handlePretextChanged(pretext: string, resultCallback: ResultsCallback<AutocompleteSuggestion>) {
|
||||
if (!pretext.startsWith(this.triggerCharacter)) {
|
||||
return false;
|
||||
}
|
||||
@ -156,7 +140,7 @@ export default class CommandProvider extends Provider {
|
||||
callback(term + ' ');
|
||||
}
|
||||
|
||||
handleMobile(pretext: string, resultCallback: ResultsCallback) {
|
||||
handleMobile(pretext: string, resultCallback: ResultsCallback<AutocompleteSuggestion>) {
|
||||
const {teamId} = this.props;
|
||||
|
||||
const command = pretext.toLowerCase();
|
||||
@ -209,7 +193,7 @@ export default class CommandProvider extends Provider {
|
||||
);
|
||||
}
|
||||
|
||||
handleWebapp(pretext: string, resultCallback: ResultsCallback) {
|
||||
handleWebapp(pretext: string, resultCallback: ResultsCallback<AutocompleteSuggestion>) {
|
||||
const command = pretext.toLowerCase();
|
||||
|
||||
const {teamId, channelId, rootId} = this.props;
|
||||
|
@ -7,7 +7,7 @@ import {getEmojiMap, getRecentEmojisNames} from 'selectors/emojis';
|
||||
import EmoticonProvider, {
|
||||
MIN_EMOTICON_LENGTH,
|
||||
EMOJI_CATEGORY_SUGGESTION_BLOCKLIST,
|
||||
} from 'components/suggestion/emoticon_provider.jsx';
|
||||
} from './emoticon_provider';
|
||||
|
||||
jest.mock('selectors/emojis', () => ({
|
||||
getEmojiMap: jest.fn(),
|
||||
@ -57,9 +57,9 @@ describe('components/EmoticonProvider', () => {
|
||||
expect(results.map((item) => item.name)).toEqual([
|
||||
'thumbsup', // thumbsup is a special case where it always appears before thumbsdown
|
||||
'thumbsdown',
|
||||
'thunder_cloud_and_rain',
|
||||
'thumbsdown-custom',
|
||||
'thumbsup-custom',
|
||||
'thunder_cloud_and_rain',
|
||||
'lithuania',
|
||||
'lithuania-custom',
|
||||
]);
|
||||
@ -141,8 +141,8 @@ describe('components/EmoticonProvider', () => {
|
||||
'lithuania-custom',
|
||||
'thumbsup',
|
||||
'thumbsdown',
|
||||
'thumbsup-custom',
|
||||
'thunder_cloud_and_rain',
|
||||
'thumbsup-custom',
|
||||
'lithuania',
|
||||
]);
|
||||
});
|
||||
@ -162,8 +162,8 @@ describe('components/EmoticonProvider', () => {
|
||||
'thumbsdown',
|
||||
'thumbsdown-custom',
|
||||
'lithuania-custom',
|
||||
'thumbsup-custom',
|
||||
'thunder_cloud_and_rain',
|
||||
'thumbsup-custom',
|
||||
'lithuania',
|
||||
]);
|
||||
});
|
||||
|
@ -3,7 +3,7 @@
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import {Preferences} from 'utils/constants';
|
||||
import {Emoji} from '@mattermost/types/emojis';
|
||||
|
||||
import {autocompleteCustomEmojis} from 'mattermost-redux/actions/emojis';
|
||||
import {getEmojiImageUrl, isSystemEmoji} from 'mattermost-redux/utils/emoji_utils';
|
||||
@ -12,55 +12,55 @@ import {getEmojiMap, getRecentEmojisNames} from 'selectors/emojis';
|
||||
|
||||
import store from 'stores/redux_store.jsx';
|
||||
|
||||
import {Preferences} from 'utils/constants';
|
||||
import * as Emoticons from 'utils/emoticons';
|
||||
import {compareEmojis, emojiMatchesSkin} from 'utils/emoji_utils';
|
||||
|
||||
import Suggestion from './suggestion.jsx';
|
||||
import Provider from './provider';
|
||||
import {SuggestionContainer, SuggestionProps} from './suggestion';
|
||||
import Provider, {ResultsCallback} from './provider';
|
||||
|
||||
export const MIN_EMOTICON_LENGTH = 2;
|
||||
export const EMOJI_CATEGORY_SUGGESTION_BLOCKLIST = ['skintone'];
|
||||
|
||||
class EmoticonSuggestion extends Suggestion {
|
||||
render() {
|
||||
const text = this.props.term;
|
||||
const emoji = this.props.item.emoji;
|
||||
|
||||
let className = 'emoticon-suggestion';
|
||||
if (this.props.isSelection) {
|
||||
className += ' suggestion--selected';
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={className}
|
||||
onClick={this.handleClick}
|
||||
onMouseMove={this.handleMouseMove}
|
||||
{...Suggestion.baseProps}
|
||||
>
|
||||
<div className='pull-left'>
|
||||
<img
|
||||
alt={text}
|
||||
className='emoticon-suggestion__image'
|
||||
src={getEmojiImageUrl(emoji)}
|
||||
title={text}
|
||||
/>
|
||||
</div>
|
||||
<div className='pull-left'>
|
||||
{text}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
type EmojiItem = {
|
||||
name: string;
|
||||
emoji: Emoji;
|
||||
type: string;
|
||||
}
|
||||
|
||||
const EmoticonSuggestion = React.forwardRef<HTMLDivElement, SuggestionProps<EmojiItem>>((props, ref) => {
|
||||
const text = props.term;
|
||||
const emoji = props.item.emoji;
|
||||
|
||||
return (
|
||||
<SuggestionContainer
|
||||
ref={ref}
|
||||
{...props}
|
||||
>
|
||||
<div className='pull-left'>
|
||||
<img
|
||||
alt={text}
|
||||
className='emoticon-suggestion__image'
|
||||
src={getEmojiImageUrl(emoji)}
|
||||
title={text}
|
||||
/>
|
||||
</div>
|
||||
<div className='pull-left'>
|
||||
{text}
|
||||
</div>
|
||||
</SuggestionContainer>
|
||||
);
|
||||
});
|
||||
EmoticonSuggestion.displayName = 'EmoticonSuggestion';
|
||||
|
||||
export default class EmoticonProvider extends Provider {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.triggerCharacter = ':';
|
||||
}
|
||||
handlePretextChanged(pretext, resultsCallback) {
|
||||
|
||||
handlePretextChanged(pretext: string, resultsCallback: ResultsCallback<EmojiItem>) {
|
||||
// Look for the potential emoticons at the start of the text, after whitespace, and at the start of emoji reaction commands
|
||||
const captured = (/(^|\s|^\+|^-)(:([^:\s]*))$/g).exec(pretext.toLowerCase());
|
||||
if (!captured) {
|
||||
@ -94,7 +94,7 @@ export default class EmoticonProvider extends Provider {
|
||||
return true;
|
||||
}
|
||||
|
||||
formatEmojis(emojis) {
|
||||
formatEmojis(emojis: EmojiItem[]) {
|
||||
return emojis.map((item) => ':' + item.name + ':');
|
||||
}
|
||||
|
||||
@ -108,9 +108,9 @@ export default class EmoticonProvider extends Provider {
|
||||
//
|
||||
// For now, this behaviour and difference is by design.
|
||||
// See https://mattermost.atlassian.net/browse/MM-17320.
|
||||
findAndSuggestEmojis(text, partialName, resultsCallback) {
|
||||
const recentMatched = [];
|
||||
const matched = [];
|
||||
findAndSuggestEmojis(text: string, partialName: string, resultsCallback: ResultsCallback<EmojiItem>) {
|
||||
const recentMatched: EmojiItem[] = [];
|
||||
const matched: EmojiItem[] = [];
|
||||
const state = store.getState();
|
||||
const skintone = state.entities?.preferences?.myPreferences['emoji--emoji_skintone']?.value || 'default';
|
||||
const emojiMap = getEmojiMap(state);
|
||||
@ -148,8 +148,8 @@ export default class EmoticonProvider extends Provider {
|
||||
}
|
||||
}
|
||||
|
||||
const sortEmojisHelper = (a, b) => {
|
||||
return compareEmojis(a, b, partialName);
|
||||
const sortEmojisHelper = (a: EmojiItem, b: EmojiItem) => {
|
||||
return compareEmojis(a.emoji, b.emoji, partialName);
|
||||
};
|
||||
|
||||
recentMatched.sort(sortEmojisHelper);
|
@ -7,61 +7,44 @@ import {Channel} from '@mattermost/types/channels';
|
||||
import {ServerError} from '@mattermost/types/errors';
|
||||
import {ActionResult} from 'mattermost-redux/types/actions';
|
||||
|
||||
import Provider from './provider';
|
||||
import Suggestion from './suggestion.jsx';
|
||||
|
||||
export type Results = {
|
||||
matchedPretext: string;
|
||||
terms: string[];
|
||||
items: Channel[];
|
||||
component: React.ElementType;
|
||||
}
|
||||
|
||||
type ResultsCallback = (results: Results) => void;
|
||||
import Provider, {ResultsCallback} from './provider';
|
||||
import {SuggestionContainer, SuggestionProps} from './suggestion';
|
||||
|
||||
type ChannelSearchFunc = (term: string, success: (channels: Channel[]) => void, error?: (err: ServerError) => void) => (ActionResult | Promise<ActionResult | ActionResult[]>);
|
||||
|
||||
class ChannelSuggestion extends Suggestion {
|
||||
render() {
|
||||
const isSelection = this.props.isSelection;
|
||||
const item = this.props.item;
|
||||
const GenericChannelSuggestion = React.forwardRef<HTMLDivElement, SuggestionProps<Channel>>((props, ref) => {
|
||||
const {item} = props;
|
||||
|
||||
const channelName = item.display_name;
|
||||
const purpose = item.purpose;
|
||||
const channelName = item.display_name;
|
||||
const purpose = item.purpose;
|
||||
|
||||
const icon = (
|
||||
<span className='suggestion-list__icon suggestion-list__icon--large'>
|
||||
<i className='icon icon--standard icon--no-spacing icon-globe'/>
|
||||
</span>
|
||||
);
|
||||
let className = 'suggestion-list__item';
|
||||
if (isSelection) {
|
||||
className += ' suggestion--selected';
|
||||
}
|
||||
const icon = (
|
||||
<span className='suggestion-list__icon suggestion-list__icon--large'>
|
||||
<i className='icon icon--standard icon--no-spacing icon-globe'/>
|
||||
</span>
|
||||
);
|
||||
|
||||
const description = '(~' + item.name + ')';
|
||||
const description = '(~' + item.name + ')';
|
||||
|
||||
return (
|
||||
<div
|
||||
className={className}
|
||||
onClick={this.handleClick}
|
||||
onMouseMove={this.handleMouseMove}
|
||||
{...Suggestion.baseProps}
|
||||
>
|
||||
{icon}
|
||||
<div className='suggestion-list__ellipsis'>
|
||||
<span className='suggestion-list__main'>
|
||||
{channelName}
|
||||
</span>
|
||||
{description}
|
||||
{purpose}
|
||||
</div>
|
||||
return (
|
||||
<SuggestionContainer
|
||||
ref={ref}
|
||||
{...props}
|
||||
>
|
||||
{icon}
|
||||
<div className='suggestion-list__ellipsis'>
|
||||
<span className='suggestion-list__main'>
|
||||
{channelName}
|
||||
</span>
|
||||
{description}
|
||||
{purpose}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
</SuggestionContainer>
|
||||
);
|
||||
});
|
||||
GenericChannelSuggestion.displayName = 'GenericChannelSuggestion';
|
||||
|
||||
export default class ChannelProvider extends Provider {
|
||||
export default class GenericChannelProvider extends Provider {
|
||||
autocompleteChannels: ChannelSearchFunc;
|
||||
constructor(channelSearchFunc: ChannelSearchFunc) {
|
||||
super();
|
||||
@ -69,24 +52,22 @@ export default class ChannelProvider extends Provider {
|
||||
this.autocompleteChannels = channelSearchFunc;
|
||||
}
|
||||
|
||||
handlePretextChanged(pretext: string, resultsCallback: ResultsCallback) {
|
||||
handlePretextChanged(pretext: string, resultsCallback: ResultsCallback<Channel>) {
|
||||
const normalizedPretext = pretext.toLowerCase();
|
||||
this.startNewRequest(normalizedPretext);
|
||||
|
||||
this.autocompleteChannels(
|
||||
normalizedPretext,
|
||||
(data: Channel[]) => {
|
||||
(channels: Channel[]) => {
|
||||
if (this.shouldCancelDispatch(normalizedPretext)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const channels: Channel[] = Object.assign([], data);
|
||||
|
||||
resultsCallback({
|
||||
matchedPretext: normalizedPretext,
|
||||
terms: channels.map((channel: Channel) => channel.display_name),
|
||||
items: channels,
|
||||
component: ChannelSuggestion,
|
||||
component: GenericChannelSuggestion,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
@ -13,85 +13,72 @@ import {isGuest} from 'mattermost-redux/utils/user_utils';
|
||||
|
||||
import Avatar from 'components/widgets/users/avatar';
|
||||
|
||||
import Provider from './provider';
|
||||
import Suggestion from './suggestion.jsx';
|
||||
import Provider, {ResultsCallback} from './provider';
|
||||
import {SuggestionContainer, SuggestionProps} from './suggestion';
|
||||
import {UserAutocomplete, UserProfile} from './command_provider/app_command_parser/app_command_parser_dependencies.js';
|
||||
|
||||
export type ProviderResults = {
|
||||
matchedPretext: string;
|
||||
terms: string[];
|
||||
items: Array<Record<string, any>>;
|
||||
component?: React.ReactNode;
|
||||
}
|
||||
const GenericUserSuggestion = React.forwardRef<HTMLDivElement, SuggestionProps<UserProfile>>((props, ref) => {
|
||||
const {item} = props;
|
||||
|
||||
class UserSuggestion extends Suggestion {
|
||||
render() {
|
||||
const {item, isSelection} = this.props;
|
||||
const username = item.username;
|
||||
let description = '';
|
||||
|
||||
let className = 'suggestion-list__item';
|
||||
if (isSelection) {
|
||||
className += ' suggestion--selected';
|
||||
}
|
||||
|
||||
const username = item.username;
|
||||
let description = '';
|
||||
|
||||
if ((item.first_name || item.last_name) && item.nickname) {
|
||||
description = `- ${Utils.getFullName(item)} (${item.nickname})`;
|
||||
} else if (item.nickname) {
|
||||
description = `- (${item.nickname})`;
|
||||
} else if (item.first_name || item.last_name) {
|
||||
description = `- ${Utils.getFullName(item)}`;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={className}
|
||||
onClick={this.handleClick}
|
||||
onMouseMove={this.handleMouseMove}
|
||||
{...Suggestion.baseProps}
|
||||
>
|
||||
<Avatar
|
||||
size='xxs'
|
||||
username={username}
|
||||
url={Client4.getUsersRoute() + '/' + item.id + '/image?_=' + (item.last_picture_update || 0)}
|
||||
/>
|
||||
<div className='suggestion-list__ellipsis'>
|
||||
<span className='suggestion-list__main'>
|
||||
{'@' + username}
|
||||
</span>
|
||||
{description}
|
||||
</div>
|
||||
{item.is_bot && <BotTag/>}
|
||||
{isGuest(item.roles) && <GuestTag/>}
|
||||
</div>
|
||||
);
|
||||
if ((item.first_name || item.last_name) && item.nickname) {
|
||||
description = `- ${Utils.getFullName(item)} (${item.nickname})`;
|
||||
} else if (item.nickname) {
|
||||
description = `- (${item.nickname})`;
|
||||
} else if (item.first_name || item.last_name) {
|
||||
description = `- ${Utils.getFullName(item)}`;
|
||||
}
|
||||
}
|
||||
|
||||
export default class UserProvider extends Provider {
|
||||
return (
|
||||
<SuggestionContainer
|
||||
ref={ref}
|
||||
{...props}
|
||||
>
|
||||
<Avatar
|
||||
size='xxs'
|
||||
username={username}
|
||||
url={Client4.getUsersRoute() + '/' + item.id + '/image?_=' + (item.last_picture_update || 0)}
|
||||
/>
|
||||
<div className='suggestion-list__ellipsis'>
|
||||
<span className='suggestion-list__main'>
|
||||
{'@' + username}
|
||||
</span>
|
||||
{description}
|
||||
</div>
|
||||
{item.is_bot && <BotTag/>}
|
||||
{isGuest(item.roles) && <GuestTag/>}
|
||||
</SuggestionContainer>
|
||||
);
|
||||
});
|
||||
GenericUserSuggestion.displayName = 'GenericUserSuggestion';
|
||||
|
||||
export default class GenericUserProvider extends Provider {
|
||||
autocompleteUsers: (text: string) => Promise<UserAutocomplete>;
|
||||
|
||||
constructor(searchUsersFunc: (username: string) => Promise<UserAutocomplete>) {
|
||||
super();
|
||||
this.autocompleteUsers = searchUsersFunc;
|
||||
}
|
||||
async handlePretextChanged(pretext: string, resultsCallback: (res: ProviderResults) => void) {
|
||||
|
||||
handlePretextChanged(pretext: string, resultsCallback: ResultsCallback<UserProfile>) {
|
||||
const normalizedPretext = pretext.toLowerCase();
|
||||
this.startNewRequest(normalizedPretext);
|
||||
|
||||
const data = await this.autocompleteUsers(normalizedPretext);
|
||||
this.autocompleteUsers(normalizedPretext).then((data) => {
|
||||
if (this.shouldCancelDispatch(normalizedPretext)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.shouldCancelDispatch(normalizedPretext)) {
|
||||
return false;
|
||||
}
|
||||
const users = data.users;
|
||||
|
||||
const users = Object.assign([], data.users);
|
||||
|
||||
resultsCallback({
|
||||
matchedPretext: normalizedPretext,
|
||||
terms: users.map((user: UserProfile) => user.username),
|
||||
items: users,
|
||||
component: UserSuggestion,
|
||||
resultsCallback({
|
||||
matchedPretext: normalizedPretext,
|
||||
terms: users.map((user: UserProfile) => user.username),
|
||||
items: users,
|
||||
component: GenericUserSuggestion,
|
||||
});
|
||||
});
|
||||
|
||||
return true;
|
||||
|
@ -3,42 +3,37 @@
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import {ProviderResults} from './generic_user_provider';
|
||||
import Provider, {ResultsCallback} from './provider';
|
||||
import {SuggestionContainer, SuggestionProps} from './suggestion';
|
||||
|
||||
import Provider from './provider';
|
||||
import Suggestion from './suggestion.jsx';
|
||||
|
||||
class MenuActionSuggestion extends Suggestion {
|
||||
render() {
|
||||
const {item, isSelection} = this.props;
|
||||
|
||||
let className = 'suggestion-list__item';
|
||||
if (isSelection) {
|
||||
className += ' suggestion--selected';
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={className}
|
||||
onClick={this.handleClick}
|
||||
onMouseMove={this.handleMouseMove}
|
||||
{...Suggestion.baseProps}
|
||||
>
|
||||
{item.text}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
interface MenuAction {
|
||||
text: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export default class MenuActionProvider extends Provider {
|
||||
private options: Array<Record<string, any>>;
|
||||
const MenuActionSuggestion = React.forwardRef<HTMLDivElement, SuggestionProps<MenuAction>>((props, ref) => {
|
||||
const {item} = props;
|
||||
|
||||
constructor(options: Array<Record<string, any>>) {
|
||||
return (
|
||||
<SuggestionContainer
|
||||
ref={ref}
|
||||
{...props}
|
||||
>
|
||||
{item.text}
|
||||
</SuggestionContainer>
|
||||
);
|
||||
});
|
||||
MenuActionSuggestion.displayName = 'MenuActionSuggestion';
|
||||
|
||||
export default class MenuActionProvider extends Provider {
|
||||
private options: MenuAction[];
|
||||
|
||||
constructor(options: MenuAction[]) {
|
||||
super();
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
handlePretextChanged(prefix: string, resultsCallback: (res: ProviderResults) => void) {
|
||||
handlePretextChanged(prefix: string, resultsCallback: ResultsCallback<MenuAction>) {
|
||||
if (prefix.length === 0) {
|
||||
this.displayAllOptions(resultsCallback);
|
||||
return true;
|
||||
@ -52,7 +47,7 @@ export default class MenuActionProvider extends Provider {
|
||||
return false;
|
||||
}
|
||||
|
||||
async displayAllOptions(resultsCallback: (res: ProviderResults) => void) {
|
||||
async displayAllOptions(resultsCallback: ResultsCallback<MenuAction>) {
|
||||
const terms = this.options.map((option) => option.text);
|
||||
|
||||
resultsCallback({
|
||||
@ -63,7 +58,7 @@ export default class MenuActionProvider extends Provider {
|
||||
});
|
||||
}
|
||||
|
||||
async filterOptions(prefix: string, resultsCallback: (res: ProviderResults) => void) {
|
||||
async filterOptions(prefix: string, resultsCallback: ResultsCallback<MenuAction>) {
|
||||
const filteredOptions = this.options.filter((option) => option.text.toLowerCase().indexOf(prefix.toLowerCase()) >= 0);
|
||||
const terms = filteredOptions.map((option) => option.text);
|
||||
|
||||
|
@ -1,20 +1,33 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
export type ProviderResult = {
|
||||
import {RequireOnlyOne} from '@mattermost/types/utilities';
|
||||
|
||||
export type ProviderResult<Item> = {
|
||||
matchedPretext: string;
|
||||
terms: string[];
|
||||
items: Array<Record<string, any>>;
|
||||
component?: React.ReactNode;
|
||||
}
|
||||
items: Array<Item | Loading>;
|
||||
} & RequireOnlyOne<{
|
||||
component: React.ReactNode;
|
||||
components: React.ReactNode[];
|
||||
}>;
|
||||
|
||||
export default class Provider {
|
||||
export type Loading = {
|
||||
type: string;
|
||||
loading: boolean;
|
||||
};
|
||||
|
||||
export type ResultsCallback<Item> = (result: ProviderResult<Item>) => void;
|
||||
|
||||
export default abstract class Provider {
|
||||
latestPrefix: string;
|
||||
latestComplete: boolean;
|
||||
disableDispatches: boolean;
|
||||
requestStarted: boolean;
|
||||
forceDispatch: boolean;
|
||||
|
||||
triggerCharacter?: string;
|
||||
|
||||
constructor() {
|
||||
this.latestPrefix = '';
|
||||
this.latestComplete = true;
|
||||
@ -23,9 +36,7 @@ export default class Provider {
|
||||
this.forceDispatch = false;
|
||||
}
|
||||
|
||||
handlePretextChanged(pretext: string, callback: (res: ProviderResult) => void) {// eslint-disable-line @typescript-eslint/no-unused-vars
|
||||
// NO-OP for inherited classes to override
|
||||
}
|
||||
abstract handlePretextChanged(pretext: string, callback: (res: ProviderResult<unknown>) => void): boolean;
|
||||
|
||||
resetRequest() {
|
||||
this.requestStarted = false;
|
||||
|
@ -11,22 +11,13 @@ import store from 'stores/redux_store.jsx';
|
||||
import Constants from 'utils/constants';
|
||||
import {getCurrentLocale} from 'selectors/i18n';
|
||||
|
||||
import Provider from './provider';
|
||||
import Provider, {ResultsCallback} from './provider';
|
||||
import SearchChannelSuggestion from './search_channel_suggestion';
|
||||
|
||||
import {Channel} from './command_provider/app_command_parser/app_command_parser_dependencies.js';
|
||||
|
||||
const getState = store.getState;
|
||||
|
||||
export type Results = {
|
||||
matchedPretext: string;
|
||||
terms: string[];
|
||||
items: Channel[];
|
||||
component: React.ElementType;
|
||||
}
|
||||
|
||||
type ResultsCallback = (results: Results) => void;
|
||||
|
||||
function itemToTerm(isAtSearch: boolean, item: { type: string; display_name: string; name: string }) {
|
||||
const prefix = isAtSearch ? '' : '@';
|
||||
if (item.type === Constants.DM_CHANNEL) {
|
||||
@ -48,7 +39,7 @@ export default class SearchChannelProvider extends Provider {
|
||||
this.autocompleteChannelsForSearch = channelSearchFunc;
|
||||
}
|
||||
|
||||
handlePretextChanged(pretext: string, resultsCallback: ResultsCallback) {
|
||||
handlePretextChanged(pretext: string, resultsCallback: ResultsCallback<Channel>) {
|
||||
const captured = (/\b(?:in|channel):\s*(\S*)$/i).exec(pretext.toLowerCase());
|
||||
if (captured) {
|
||||
let channelPrefix = captured[1];
|
||||
|
@ -1,12 +1,33 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`components/suggestion/search_channel_suggestion should match snapshot 1`] = `
|
||||
<div
|
||||
className="suggestion-list__item"
|
||||
onClick={[Function]}
|
||||
onMouseMove={[Function]}
|
||||
role="button"
|
||||
tabIndex={-1}
|
||||
<SuggestionContainer
|
||||
currentUserId="userid1"
|
||||
isSelection={false}
|
||||
item={
|
||||
Object {
|
||||
"create_at": 0,
|
||||
"creator_id": "id",
|
||||
"delete_at": 0,
|
||||
"display_name": "name",
|
||||
"group_constrained": false,
|
||||
"header": "header",
|
||||
"id": "channel_id",
|
||||
"last_post_at": 0,
|
||||
"last_root_post_at": 0,
|
||||
"name": "DN",
|
||||
"purpose": "purpose",
|
||||
"scheme_id": "id",
|
||||
"team_id": "team_id",
|
||||
"type": "O",
|
||||
"update_at": 0,
|
||||
}
|
||||
}
|
||||
matchedPretext=""
|
||||
onClick={[MockFunction]}
|
||||
onMouseMove={[MockFunction]}
|
||||
teammateIsBot={false}
|
||||
term=""
|
||||
>
|
||||
<span
|
||||
className="suggestion-list__icon suggestion-list__icon--large"
|
||||
@ -25,16 +46,37 @@ exports[`components/suggestion/search_channel_suggestion should match snapshot 1
|
||||
</span>
|
||||
~DN
|
||||
</div>
|
||||
</div>
|
||||
</SuggestionContainer>
|
||||
`;
|
||||
|
||||
exports[`components/suggestion/search_channel_suggestion should match snapshot, channel type DM_CHANNEL 1`] = `
|
||||
<div
|
||||
className="suggestion-list__item suggestion--selected"
|
||||
onClick={[Function]}
|
||||
onMouseMove={[Function]}
|
||||
role="button"
|
||||
tabIndex={-1}
|
||||
<SuggestionContainer
|
||||
currentUserId="userid1"
|
||||
isSelection={true}
|
||||
item={
|
||||
Object {
|
||||
"create_at": 0,
|
||||
"creator_id": "id",
|
||||
"delete_at": 0,
|
||||
"display_name": "name",
|
||||
"group_constrained": false,
|
||||
"header": "header",
|
||||
"id": "channel_id",
|
||||
"last_post_at": 0,
|
||||
"last_root_post_at": 0,
|
||||
"name": "DN",
|
||||
"purpose": "purpose",
|
||||
"scheme_id": "id",
|
||||
"team_id": "team_id",
|
||||
"type": "D",
|
||||
"update_at": 0,
|
||||
}
|
||||
}
|
||||
matchedPretext=""
|
||||
onClick={[MockFunction]}
|
||||
onMouseMove={[MockFunction]}
|
||||
teammateIsBot={false}
|
||||
term=""
|
||||
>
|
||||
<Memo(Avatar)
|
||||
size="sm"
|
||||
@ -49,16 +91,37 @@ exports[`components/suggestion/search_channel_suggestion should match snapshot,
|
||||
@name
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</SuggestionContainer>
|
||||
`;
|
||||
|
||||
exports[`components/suggestion/search_channel_suggestion should match snapshot, channel type GM_CHANNEL 1`] = `
|
||||
<div
|
||||
className="suggestion-list__item suggestion--selected"
|
||||
onClick={[Function]}
|
||||
onMouseMove={[Function]}
|
||||
role="button"
|
||||
tabIndex={-1}
|
||||
<SuggestionContainer
|
||||
currentUserId="userid1"
|
||||
isSelection={true}
|
||||
item={
|
||||
Object {
|
||||
"create_at": 0,
|
||||
"creator_id": "id",
|
||||
"delete_at": 0,
|
||||
"display_name": "name",
|
||||
"group_constrained": false,
|
||||
"header": "header",
|
||||
"id": "channel_id",
|
||||
"last_post_at": 0,
|
||||
"last_root_post_at": 0,
|
||||
"name": "DN",
|
||||
"purpose": "purpose",
|
||||
"scheme_id": "id",
|
||||
"team_id": "team_id",
|
||||
"type": "G",
|
||||
"update_at": 0,
|
||||
}
|
||||
}
|
||||
matchedPretext=""
|
||||
onClick={[MockFunction]}
|
||||
onMouseMove={[MockFunction]}
|
||||
teammateIsBot={false}
|
||||
term=""
|
||||
>
|
||||
<span
|
||||
className="suggestion-list__icon suggestion-list__icon--large"
|
||||
@ -78,16 +141,37 @@ exports[`components/suggestion/search_channel_suggestion should match snapshot,
|
||||
@name
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</SuggestionContainer>
|
||||
`;
|
||||
|
||||
exports[`components/suggestion/search_channel_suggestion should match snapshot, channel type OPEN_CHANNEL 1`] = `
|
||||
<div
|
||||
className="suggestion-list__item suggestion--selected"
|
||||
onClick={[Function]}
|
||||
onMouseMove={[Function]}
|
||||
role="button"
|
||||
tabIndex={-1}
|
||||
<SuggestionContainer
|
||||
currentUserId="userid1"
|
||||
isSelection={true}
|
||||
item={
|
||||
Object {
|
||||
"create_at": 0,
|
||||
"creator_id": "id",
|
||||
"delete_at": 0,
|
||||
"display_name": "name",
|
||||
"group_constrained": false,
|
||||
"header": "header",
|
||||
"id": "channel_id",
|
||||
"last_post_at": 0,
|
||||
"last_root_post_at": 0,
|
||||
"name": "DN",
|
||||
"purpose": "purpose",
|
||||
"scheme_id": "id",
|
||||
"team_id": "team_id",
|
||||
"type": "O",
|
||||
"update_at": 0,
|
||||
}
|
||||
}
|
||||
matchedPretext=""
|
||||
onClick={[MockFunction]}
|
||||
onMouseMove={[MockFunction]}
|
||||
teammateIsBot={false}
|
||||
term=""
|
||||
>
|
||||
<span
|
||||
className="suggestion-list__icon suggestion-list__icon--large"
|
||||
@ -106,16 +190,37 @@ exports[`components/suggestion/search_channel_suggestion should match snapshot,
|
||||
</span>
|
||||
~DN
|
||||
</div>
|
||||
</div>
|
||||
</SuggestionContainer>
|
||||
`;
|
||||
|
||||
exports[`components/suggestion/search_channel_suggestion should match snapshot, channel type PRIVATE_CHANNEL 1`] = `
|
||||
<div
|
||||
className="suggestion-list__item suggestion--selected"
|
||||
onClick={[Function]}
|
||||
onMouseMove={[Function]}
|
||||
role="button"
|
||||
tabIndex={-1}
|
||||
<SuggestionContainer
|
||||
currentUserId="userid1"
|
||||
isSelection={true}
|
||||
item={
|
||||
Object {
|
||||
"create_at": 0,
|
||||
"creator_id": "id",
|
||||
"delete_at": 0,
|
||||
"display_name": "name",
|
||||
"group_constrained": false,
|
||||
"header": "header",
|
||||
"id": "channel_id",
|
||||
"last_post_at": 0,
|
||||
"last_root_post_at": 0,
|
||||
"name": "DN",
|
||||
"purpose": "purpose",
|
||||
"scheme_id": "id",
|
||||
"team_id": "team_id",
|
||||
"type": "P",
|
||||
"update_at": 0,
|
||||
}
|
||||
}
|
||||
matchedPretext=""
|
||||
onClick={[MockFunction]}
|
||||
onMouseMove={[MockFunction]}
|
||||
teammateIsBot={false}
|
||||
term=""
|
||||
>
|
||||
<span
|
||||
className="suggestion-list__icon suggestion-list__icon--large"
|
||||
@ -134,16 +239,37 @@ exports[`components/suggestion/search_channel_suggestion should match snapshot,
|
||||
</span>
|
||||
~DN
|
||||
</div>
|
||||
</div>
|
||||
</SuggestionContainer>
|
||||
`;
|
||||
|
||||
exports[`components/suggestion/search_channel_suggestion should match snapshot, isSelection is false 1`] = `
|
||||
<div
|
||||
className="suggestion-list__item"
|
||||
onClick={[Function]}
|
||||
onMouseMove={[Function]}
|
||||
role="button"
|
||||
tabIndex={-1}
|
||||
<SuggestionContainer
|
||||
currentUserId="userid1"
|
||||
isSelection={false}
|
||||
item={
|
||||
Object {
|
||||
"create_at": 0,
|
||||
"creator_id": "id",
|
||||
"delete_at": 0,
|
||||
"display_name": "name",
|
||||
"group_constrained": false,
|
||||
"header": "header",
|
||||
"id": "channel_id",
|
||||
"last_post_at": 0,
|
||||
"last_root_post_at": 0,
|
||||
"name": "DN",
|
||||
"purpose": "purpose",
|
||||
"scheme_id": "id",
|
||||
"team_id": "team_id",
|
||||
"type": "O",
|
||||
"update_at": 0,
|
||||
}
|
||||
}
|
||||
matchedPretext=""
|
||||
onClick={[MockFunction]}
|
||||
onMouseMove={[MockFunction]}
|
||||
teammateIsBot={false}
|
||||
term=""
|
||||
>
|
||||
<span
|
||||
className="suggestion-list__icon suggestion-list__icon--large"
|
||||
@ -162,16 +288,37 @@ exports[`components/suggestion/search_channel_suggestion should match snapshot,
|
||||
</span>
|
||||
~DN
|
||||
</div>
|
||||
</div>
|
||||
</SuggestionContainer>
|
||||
`;
|
||||
|
||||
exports[`components/suggestion/search_channel_suggestion should match snapshot, isSelection is true 1`] = `
|
||||
<div
|
||||
className="suggestion-list__item suggestion--selected"
|
||||
onClick={[Function]}
|
||||
onMouseMove={[Function]}
|
||||
role="button"
|
||||
tabIndex={-1}
|
||||
<SuggestionContainer
|
||||
currentUserId="userid1"
|
||||
isSelection={true}
|
||||
item={
|
||||
Object {
|
||||
"create_at": 0,
|
||||
"creator_id": "id",
|
||||
"delete_at": 0,
|
||||
"display_name": "name",
|
||||
"group_constrained": false,
|
||||
"header": "header",
|
||||
"id": "channel_id",
|
||||
"last_post_at": 0,
|
||||
"last_root_post_at": 0,
|
||||
"name": "DN",
|
||||
"purpose": "purpose",
|
||||
"scheme_id": "id",
|
||||
"team_id": "team_id",
|
||||
"type": "O",
|
||||
"update_at": 0,
|
||||
}
|
||||
}
|
||||
matchedPretext=""
|
||||
onClick={[MockFunction]}
|
||||
onMouseMove={[MockFunction]}
|
||||
teammateIsBot={false}
|
||||
term=""
|
||||
>
|
||||
<span
|
||||
className="suggestion-list__icon suggestion-list__icon--large"
|
||||
@ -190,5 +337,5 @@ exports[`components/suggestion/search_channel_suggestion should match snapshot,
|
||||
</span>
|
||||
~DN
|
||||
</div>
|
||||
</div>
|
||||
</SuggestionContainer>
|
||||
`;
|
||||
|
@ -16,9 +16,11 @@ type OwnProps = {
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: GlobalState, ownProps: OwnProps) => {
|
||||
const teammate = getDirectTeammate(state, ownProps.item.id);
|
||||
|
||||
return {
|
||||
teammate: getDirectTeammate(state, ownProps.item.id),
|
||||
currentUser: getCurrentUserId(state),
|
||||
teammateIsBot: Boolean(teammate && teammate.is_bot),
|
||||
currentUserId: getCurrentUserId(state),
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -10,15 +10,16 @@ import SearchChannelSuggestion from './search_channel_suggestion';
|
||||
|
||||
describe('components/suggestion/search_channel_suggestion', () => {
|
||||
const mockChannel = TestHelper.getChannelMock();
|
||||
const mockTeamMate = TestHelper.getUserMock();
|
||||
|
||||
const baseProps = {
|
||||
item: mockChannel,
|
||||
isSelection: false,
|
||||
teammate: mockTeamMate,
|
||||
currentUser: 'userid1',
|
||||
currentUserId: 'userid1',
|
||||
teammateIsBot: false,
|
||||
term: '',
|
||||
matchedPretext: '',
|
||||
onClick: jest.fn(),
|
||||
onMouseMove: jest.fn(),
|
||||
};
|
||||
|
||||
test('should match snapshot', () => {
|
||||
|
@ -9,15 +9,16 @@ import {getUserIdFromChannelName} from 'mattermost-redux/utils/channel_utils';
|
||||
import {imageURLForUser} from 'utils/utils';
|
||||
import Constants from 'utils/constants';
|
||||
import Avatar from 'components/widgets/users/avatar';
|
||||
import Suggestion from '../suggestion';
|
||||
|
||||
import {SuggestionContainer, SuggestionProps} from '../suggestion';
|
||||
|
||||
import {Channel} from '@mattermost/types/channels';
|
||||
|
||||
function itemToName(item: Channel, currentUser: string): {icon: React.ReactElement; name: string; description: string} | null {
|
||||
function itemToName(item: Channel, currentUserId: string): {icon: React.ReactElement; name: string; description: string} | null {
|
||||
if (item.type === Constants.DM_CHANNEL) {
|
||||
const profilePicture = (
|
||||
<Avatar
|
||||
url={imageURLForUser(getUserIdFromChannelName(currentUser, item.name))}
|
||||
url={imageURLForUser(getUserIdFromChannelName(currentUserId, item.name))}
|
||||
size='sm'
|
||||
/>
|
||||
);
|
||||
@ -68,40 +69,38 @@ function itemToName(item: Channel, currentUser: string): {icon: React.ReactEleme
|
||||
return null;
|
||||
}
|
||||
|
||||
export default class SearchChannelSuggestion extends Suggestion {
|
||||
render(): JSX.Element {
|
||||
const {item, isSelection, teammate, currentUser} = this.props;
|
||||
|
||||
let className = 'suggestion-list__item';
|
||||
if (isSelection) {
|
||||
className += ' suggestion--selected';
|
||||
}
|
||||
|
||||
const nameObject = itemToName(item, currentUser);
|
||||
if (!nameObject) {
|
||||
return (<></>);
|
||||
}
|
||||
|
||||
const {icon, name, description} = nameObject;
|
||||
|
||||
const tag = item.type === Constants.DM_CHANNEL && teammate && teammate.is_bot ? <BotTag/> : null;
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={this.handleClick}
|
||||
onMouseMove={this.handleMouseMove}
|
||||
className={className}
|
||||
{...Suggestion.baseProps}
|
||||
>
|
||||
{icon}
|
||||
<div className='suggestion-list__ellipsis'>
|
||||
<span className='suggestion-list__main'>
|
||||
{name}
|
||||
</span>
|
||||
{description}
|
||||
</div>
|
||||
{tag}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
type Props = SuggestionProps<Channel> & {
|
||||
currentUserId: string;
|
||||
teammateIsBot: boolean;
|
||||
}
|
||||
|
||||
const SearchChannelSuggestion = React.forwardRef<HTMLDivElement, Props>((props, ref) => {
|
||||
const {item, teammateIsBot, currentUserId} = props;
|
||||
|
||||
const nameObject = itemToName(item, currentUserId);
|
||||
if (!nameObject) {
|
||||
return (<></>);
|
||||
}
|
||||
|
||||
const {icon, name, description} = nameObject;
|
||||
|
||||
const tag = item.type === Constants.DM_CHANNEL && teammateIsBot ? <BotTag/> : null;
|
||||
|
||||
return (
|
||||
<SuggestionContainer
|
||||
ref={ref}
|
||||
{...props}
|
||||
>
|
||||
{icon}
|
||||
<div className='suggestion-list__ellipsis'>
|
||||
<span className='suggestion-list__main'>
|
||||
{name}
|
||||
</span>
|
||||
{description}
|
||||
</div>
|
||||
{tag}
|
||||
</SuggestionContainer>
|
||||
);
|
||||
});
|
||||
SearchChannelSuggestion.displayName = 'SearchChannelSuggestion';
|
||||
export default SearchChannelSuggestion;
|
||||
|
@ -5,7 +5,7 @@ import {getState} from 'stores/redux_store';
|
||||
|
||||
import mockStore from 'tests/test_store';
|
||||
|
||||
import SearchChannelWithPermissionsProvider from 'components/suggestion/search_channel_with_permissions_provider.jsx';
|
||||
import SearchChannelWithPermissionsProvider from './search_channel_with_permissions_provider';
|
||||
|
||||
jest.mock('stores/redux_store', () => ({
|
||||
dispatch: jest.fn(),
|
||||
|
@ -3,6 +3,8 @@
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import {Channel} from '@mattermost/types/channels';
|
||||
|
||||
import {
|
||||
getChannelsInCurrentTeam,
|
||||
} from 'mattermost-redux/selectors/entities/channels';
|
||||
@ -14,68 +16,57 @@ import {haveIChannelPermission} from 'mattermost-redux/selectors/entities/roles'
|
||||
import {Permissions} from 'mattermost-redux/constants';
|
||||
import {sortChannelsByTypeAndDisplayName} from 'mattermost-redux/utils/channel_utils';
|
||||
import {logError} from 'mattermost-redux/actions/errors';
|
||||
import {ActionResult} from 'mattermost-redux/types/actions';
|
||||
|
||||
import store from 'stores/redux_store.jsx';
|
||||
import {Constants} from 'utils/constants';
|
||||
|
||||
import Provider from './provider';
|
||||
import Suggestion from './suggestion.jsx';
|
||||
import Provider, {ResultsCallback} from './provider';
|
||||
import {SuggestionContainer, SuggestionProps} from './suggestion';
|
||||
|
||||
class SearchChannelWithPermissionsSuggestion extends Suggestion {
|
||||
static get propTypes() {
|
||||
return {
|
||||
...super.propTypes,
|
||||
};
|
||||
}
|
||||
type WrappedChannel = {
|
||||
channel: Channel;
|
||||
name: string;
|
||||
}
|
||||
|
||||
render() {
|
||||
const {item, isSelection} = this.props;
|
||||
const channel = item.channel;
|
||||
const channelIsArchived = channel.delete_at && channel.delete_at !== 0;
|
||||
const SearchChannelWithPermissionsSuggestion = React.forwardRef<HTMLDivElement, SuggestionProps<WrappedChannel>>((props, ref) => {
|
||||
const {item} = props;
|
||||
const channel = item.channel;
|
||||
const channelIsArchived = channel.delete_at && channel.delete_at !== 0;
|
||||
|
||||
let className = 'suggestion-list__item';
|
||||
if (isSelection) {
|
||||
className += ' suggestion--selected';
|
||||
}
|
||||
|
||||
const displayName = channel.display_name;
|
||||
let icon = null;
|
||||
if (channelIsArchived) {
|
||||
icon = (
|
||||
<i className='icon icon--no-spacing icon-archive-outline'/>
|
||||
);
|
||||
} else if (channel.type === Constants.OPEN_CHANNEL) {
|
||||
icon = (
|
||||
<i className='icon icon--no-spacing icon-globe'/>
|
||||
);
|
||||
} else if (channel.type === Constants.PRIVATE_CHANNEL) {
|
||||
icon = (
|
||||
<i className='icon icon--no-spacing icon-lock-outline'/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={this.handleClick}
|
||||
className={className}
|
||||
onMouseMove={this.handleMouseMove}
|
||||
ref={(node) => {
|
||||
this.node = node;
|
||||
}}
|
||||
{...Suggestion.baseProps}
|
||||
>
|
||||
<span className='suggestion-list__icon suggestion-list__icon--large'>{icon}</span>
|
||||
<div className='suggestion-list__ellipsis'>
|
||||
<span className='suggestion-list__main'>{displayName}</span>
|
||||
</div>
|
||||
</div>
|
||||
const displayName = channel.display_name;
|
||||
let icon = null;
|
||||
if (channelIsArchived) {
|
||||
icon = (
|
||||
<i className='icon icon--no-spacing icon-archive-outline'/>
|
||||
);
|
||||
} else if (channel.type === Constants.OPEN_CHANNEL) {
|
||||
icon = (
|
||||
<i className='icon icon--no-spacing icon-globe'/>
|
||||
);
|
||||
} else if (channel.type === Constants.PRIVATE_CHANNEL) {
|
||||
icon = (
|
||||
<i className='icon icon--no-spacing icon-lock-outline'/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<SuggestionContainer
|
||||
ref={ref}
|
||||
{...props}
|
||||
>
|
||||
<span className='suggestion-list__icon suggestion-list__icon--large'>{icon}</span>
|
||||
<div className='suggestion-list__ellipsis'>
|
||||
<span className='suggestion-list__main'>{displayName}</span>
|
||||
</div>
|
||||
</SuggestionContainer>
|
||||
);
|
||||
});
|
||||
SearchChannelWithPermissionsSuggestion.displayName = 'SearchChannelWithPermissionsSuggestion';
|
||||
|
||||
let prefix = '';
|
||||
|
||||
function channelSearchSorter(wrappedA, wrappedB) {
|
||||
function channelSearchSorter(wrappedA: WrappedChannel, wrappedB: WrappedChannel) {
|
||||
const aIsArchived = wrappedA.channel.delete_at ? wrappedA.channel.delete_at !== 0 : false;
|
||||
const bIsArchived = wrappedB.channel.delete_at ? wrappedB.channel.delete_at !== 0 : false;
|
||||
if (aIsArchived && !bIsArchived) {
|
||||
@ -106,15 +97,17 @@ function channelSearchSorter(wrappedA, wrappedB) {
|
||||
}
|
||||
|
||||
export default class SearchChannelWithPermissionsProvider extends Provider {
|
||||
constructor(channelSearchFunc) {
|
||||
autocompleteChannelsForSearch: (channelId: string, userId: string) => Promise<ActionResult<Channel[]>>;
|
||||
|
||||
constructor(channelSearchFunc: SearchChannelWithPermissionsProvider['autocompleteChannelsForSearch']) {
|
||||
super();
|
||||
this.autocompleteChannelsForSearch = channelSearchFunc;
|
||||
}
|
||||
|
||||
makeChannelSearchFilter(channelPrefix) {
|
||||
makeChannelSearchFilter(channelPrefix: string) {
|
||||
const channelPrefixLower = channelPrefix.toLowerCase();
|
||||
|
||||
return (channel) => {
|
||||
return (channel: Channel) => {
|
||||
const state = store.getState();
|
||||
const channelId = channel.id;
|
||||
const teamId = getCurrentTeamId(state);
|
||||
@ -133,7 +126,7 @@ export default class SearchChannelWithPermissionsProvider extends Provider {
|
||||
};
|
||||
}
|
||||
|
||||
handlePretextChanged(channelPrefix, resultsCallback) {
|
||||
handlePretextChanged(channelPrefix: string, resultsCallback: ResultsCallback<WrappedChannel>) {
|
||||
if (channelPrefix) {
|
||||
prefix = channelPrefix;
|
||||
this.startNewRequest(channelPrefix);
|
||||
@ -150,7 +143,7 @@ export default class SearchChannelWithPermissionsProvider extends Provider {
|
||||
return true;
|
||||
}
|
||||
|
||||
async fetchChannels(channelPrefix, resultsCallback) {
|
||||
async fetchChannels(channelPrefix: string, resultsCallback: ResultsCallback<WrappedChannel>) {
|
||||
const state = store.getState();
|
||||
const teamId = getCurrentTeamId(state);
|
||||
if (!teamId) {
|
||||
@ -159,10 +152,10 @@ export default class SearchChannelWithPermissionsProvider extends Provider {
|
||||
|
||||
const channelsAsync = this.autocompleteChannelsForSearch(teamId, channelPrefix);
|
||||
|
||||
let channelsFromServer = [];
|
||||
let channelsFromServer: Channel[] = [];
|
||||
try {
|
||||
const {data} = await channelsAsync;
|
||||
channelsFromServer = data;
|
||||
channelsFromServer = data ?? [];
|
||||
} catch (err) {
|
||||
store.dispatch(logError(err));
|
||||
}
|
||||
@ -175,7 +168,7 @@ export default class SearchChannelWithPermissionsProvider extends Provider {
|
||||
this.formatChannelsAndDispatch(channelPrefix, resultsCallback, channels);
|
||||
}
|
||||
|
||||
formatChannelsAndDispatch(channelPrefix, resultsCallback, allChannels) {
|
||||
formatChannelsAndDispatch(channelPrefix: string, resultsCallback: ResultsCallback<WrappedChannel>, allChannels: Channel[]) {
|
||||
const channels = [];
|
||||
|
||||
const state = store.getState();
|
||||
@ -186,15 +179,14 @@ export default class SearchChannelWithPermissionsProvider extends Provider {
|
||||
return;
|
||||
}
|
||||
|
||||
const completedChannels = {};
|
||||
const completedChannels: Record<string, boolean> = {};
|
||||
|
||||
const channelFilter = this.makeChannelSearchFilter(channelPrefix);
|
||||
|
||||
const config = getConfig(state);
|
||||
const viewArchivedChannels = config.ExperimentalViewArchivedChannels === 'true';
|
||||
|
||||
for (const id of Object.keys(allChannels)) {
|
||||
const channel = allChannels[id];
|
||||
for (const channel of allChannels) {
|
||||
if (!channel) {
|
||||
continue;
|
||||
}
|
||||
@ -207,16 +199,15 @@ export default class SearchChannelWithPermissionsProvider extends Provider {
|
||||
const newChannel = Object.assign({}, channel);
|
||||
const channelIsArchived = channel.delete_at !== 0;
|
||||
|
||||
const wrappedChannel = {channel: newChannel, name: newChannel.name, deactivated: false};
|
||||
const wrappedChannel = {
|
||||
channel: newChannel,
|
||||
name: newChannel.name,
|
||||
};
|
||||
if (!viewArchivedChannels && channelIsArchived) {
|
||||
continue;
|
||||
} else if (!members[channel.id]) {
|
||||
continue;
|
||||
} else if (channel.type === Constants.OPEN_CHANNEL) {
|
||||
wrappedChannel.type = Constants.OPEN_CHANNEL;
|
||||
} else if (channel.type === Constants.PRIVATE_CHANNEL) {
|
||||
wrappedChannel.type = Constants.PRIVATE_CHANNEL;
|
||||
} else {
|
||||
} else if (channel.type !== Constants.OPEN_CHANNEL && channel.type !== Constants.PRIVATE_CHANNEL) {
|
||||
continue;
|
||||
}
|
||||
completedChannels[channel.id] = true;
|
@ -1,15 +1,13 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import Provider from './provider';
|
||||
import Provider, {ResultsCallback} from './provider';
|
||||
import SearchDateSuggestion from './search_date_suggestion';
|
||||
|
||||
type DateItem = {label: string; date: string};
|
||||
|
||||
type ResultsCallback = (results: {matchedPretext: string; terms: string[]; items: DateItem[]; component: typeof SearchDateSuggestion}) => void;
|
||||
|
||||
export default class SearchDateProvider extends Provider {
|
||||
handlePretextChanged(pretext: string, resultsCallback: ResultsCallback) {
|
||||
handlePretextChanged(pretext: string, resultsCallback: ResultsCallback<DateItem>) {
|
||||
const captured = (/\b(?:on|before|after):\s*(\S*)$/i).exec(pretext.toLowerCase());
|
||||
if (captured) {
|
||||
const datePrefix = captured[1];
|
||||
|
@ -7,7 +7,7 @@ import {DayPicker} from 'react-day-picker';
|
||||
|
||||
import type {Locale} from 'date-fns';
|
||||
|
||||
import Suggestion from '../suggestion.jsx';
|
||||
import {SuggestionProps} from '../suggestion';
|
||||
|
||||
import * as Keyboard from 'utils/keyboard';
|
||||
import * as Utils from 'utils/utils';
|
||||
@ -15,7 +15,14 @@ import Constants from 'utils/constants';
|
||||
|
||||
import 'react-day-picker/dist/style.css';
|
||||
|
||||
export default class SearchDateSuggestion extends Suggestion {
|
||||
type Props = SuggestionProps<never> & {
|
||||
currentDate?: Date;
|
||||
handleEscape: () => void;
|
||||
locale: string;
|
||||
preventClose: () => void;
|
||||
}
|
||||
|
||||
export default class SearchDateSuggestion extends React.PureComponent<Props> {
|
||||
private loadedLocales: Record<string, Locale> = {};
|
||||
|
||||
state = {
|
||||
|
@ -12,68 +12,55 @@ import SharedUserIndicator from 'components/shared_user_indicator';
|
||||
import {UserProfile} from '@mattermost/types/users';
|
||||
import {UserAutocomplete} from '@mattermost/types/autocomplete';
|
||||
|
||||
import Provider from './provider';
|
||||
import Suggestion from './suggestion.jsx';
|
||||
import {ProviderResults} from './generic_user_provider';
|
||||
import Provider, {ResultsCallback} from './provider';
|
||||
import {SuggestionContainer, SuggestionProps} from './suggestion';
|
||||
|
||||
class SearchUserSuggestion extends Suggestion {
|
||||
private node?: HTMLDivElement | null;
|
||||
render() {
|
||||
const {item, isSelection} = this.props;
|
||||
const SearchUserSuggestion = React.forwardRef<HTMLDivElement, SuggestionProps<UserProfile>>((props, ref) => {
|
||||
const {item} = props;
|
||||
|
||||
let className = 'suggestion-list__item';
|
||||
if (isSelection) {
|
||||
className += ' suggestion--selected';
|
||||
}
|
||||
const username = item.username;
|
||||
let description = '';
|
||||
|
||||
const username = item.username;
|
||||
let description = '';
|
||||
if ((item.first_name || item.last_name) && item.nickname) {
|
||||
description = `${Utils.getFullName(item)} (${item.nickname})`;
|
||||
} else if (item.nickname) {
|
||||
description = `(${item.nickname})`;
|
||||
} else if (item.first_name || item.last_name) {
|
||||
description = `${Utils.getFullName(item)}`;
|
||||
}
|
||||
|
||||
if ((item.first_name || item.last_name) && item.nickname) {
|
||||
description = `${Utils.getFullName(item)} (${item.nickname})`;
|
||||
} else if (item.nickname) {
|
||||
description = `(${item.nickname})`;
|
||||
} else if (item.first_name || item.last_name) {
|
||||
description = `${Utils.getFullName(item)}`;
|
||||
}
|
||||
|
||||
let sharedIcon;
|
||||
if (item.remote_id) {
|
||||
sharedIcon = (
|
||||
<SharedUserIndicator
|
||||
className='mention__shared-user-icon'
|
||||
withTooltip={true}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={className}
|
||||
ref={(node) => {
|
||||
this.node = node;
|
||||
}}
|
||||
onClick={this.handleClick}
|
||||
onMouseMove={this.handleMouseMove}
|
||||
{...Suggestion.baseProps}
|
||||
>
|
||||
<Avatar
|
||||
size='sm'
|
||||
username={username}
|
||||
url={Utils.imageURLForUser(item.id, item.last_picture_update)}
|
||||
/>
|
||||
<div className='suggestion-list__ellipsis'>
|
||||
<span className='suggestion-list__main'>
|
||||
{'@'}{username}
|
||||
</span>
|
||||
{item.is_bot && <BotTag/>}
|
||||
{description}
|
||||
</div>
|
||||
{sharedIcon}
|
||||
</div>
|
||||
let sharedIcon;
|
||||
if (item.remote_id) {
|
||||
sharedIcon = (
|
||||
<SharedUserIndicator
|
||||
className='mention__shared-user-icon'
|
||||
withTooltip={true}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<SuggestionContainer
|
||||
ref={ref}
|
||||
{...props}
|
||||
>
|
||||
<Avatar
|
||||
size='sm'
|
||||
username={username}
|
||||
url={Utils.imageURLForUser(item.id, item.last_picture_update)}
|
||||
/>
|
||||
<div className='suggestion-list__ellipsis'>
|
||||
<span className='suggestion-list__main'>
|
||||
{'@'}{username}
|
||||
</span>
|
||||
{item.is_bot && <BotTag/>}
|
||||
{description}
|
||||
</div>
|
||||
{sharedIcon}
|
||||
</SuggestionContainer>
|
||||
);
|
||||
});
|
||||
SearchUserSuggestion.displayName = 'SearchUserSuggestion';
|
||||
|
||||
export default class SearchUserProvider extends Provider {
|
||||
private autocompleteUsersInTeam: (username: string) => Promise<UserAutocomplete>;
|
||||
@ -82,7 +69,7 @@ export default class SearchUserProvider extends Provider {
|
||||
this.autocompleteUsersInTeam = userSearchFunc;
|
||||
}
|
||||
|
||||
handlePretextChanged(pretext: string, resultsCallback: (res: ProviderResults) => void) {
|
||||
handlePretextChanged(pretext: string, resultsCallback: ResultsCallback<UserProfile>) {
|
||||
const captured = (/\bfrom:\s*(\S*)$/i).exec(pretext.toLowerCase());
|
||||
|
||||
this.doAutocomplete(captured, resultsCallback);
|
||||
@ -90,7 +77,7 @@ export default class SearchUserProvider extends Provider {
|
||||
return Boolean(captured);
|
||||
}
|
||||
|
||||
async doAutocomplete(captured: RegExpExecArray | null, resultsCallback: (res: ProviderResults) => void) {
|
||||
async doAutocomplete(captured: RegExpExecArray | null, resultsCallback: ResultsCallback<UserProfile>) {
|
||||
if (!captured) {
|
||||
return;
|
||||
}
|
||||
|
@ -1,38 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
|
||||
export default class Suggestion extends React.PureComponent {
|
||||
static get propTypes() {
|
||||
return {
|
||||
item: PropTypes.oneOfType([
|
||||
PropTypes.object,
|
||||
PropTypes.string,
|
||||
]).isRequired,
|
||||
term: PropTypes.string.isRequired,
|
||||
matchedPretext: PropTypes.string.isRequired,
|
||||
isSelection: PropTypes.bool,
|
||||
onClick: PropTypes.func,
|
||||
onMouseMove: PropTypes.func,
|
||||
};
|
||||
}
|
||||
|
||||
static baseProps = {
|
||||
role: 'button',
|
||||
tabIndex: -1,
|
||||
};
|
||||
|
||||
handleClick = (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
this.props.onClick(this.props.term, this.props.matchedPretext);
|
||||
};
|
||||
|
||||
handleMouseMove = (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
this.props.onMouseMove(this.props.term);
|
||||
};
|
||||
}
|
65
webapp/channels/src/components/suggestion/suggestion.tsx
Normal file
65
webapp/channels/src/components/suggestion/suggestion.tsx
Normal file
@ -0,0 +1,65 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import classNames from 'classnames';
|
||||
import React, {useCallback} from 'react';
|
||||
|
||||
export interface SuggestionProps<Item> extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onClick' | 'onMouseMove'> {
|
||||
// eslint-disable-next-line react/no-unused-prop-types
|
||||
item: Item;
|
||||
|
||||
term: string;
|
||||
matchedPretext: string;
|
||||
isSelection: boolean;
|
||||
|
||||
children?: React.ReactNode;
|
||||
onClick: (term: string, matchedPretext: string) => void;
|
||||
onMouseMove: (term: string) => void;
|
||||
}
|
||||
|
||||
const SuggestionContainer = React.forwardRef<HTMLDivElement, SuggestionProps<unknown>>((props, ref) => {
|
||||
const {
|
||||
children,
|
||||
term,
|
||||
matchedPretext,
|
||||
isSelection,
|
||||
|
||||
onClick,
|
||||
onMouseMove,
|
||||
|
||||
role = 'button',
|
||||
tabIndex = -1,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
Reflect.deleteProperty(otherProps, 'item');
|
||||
|
||||
const handleClick = useCallback((e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
onClick(term, matchedPretext);
|
||||
}, [onClick, term, matchedPretext]);
|
||||
|
||||
const handleMouseMove = useCallback((e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
onMouseMove(term);
|
||||
}, [onMouseMove, term]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={classNames('suggestion-list__item', {'suggestion--selected': isSelection})}
|
||||
onClick={handleClick}
|
||||
onMouseMove={handleMouseMove}
|
||||
role={role}
|
||||
tabIndex={tabIndex}
|
||||
{...otherProps}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
SuggestionContainer.displayName = 'SuggestionContainer';
|
||||
export {SuggestionContainer};
|
@ -24,7 +24,7 @@ interface Props {
|
||||
items: any[];
|
||||
terms: string[];
|
||||
selection: string;
|
||||
components: Array<React.FunctionComponent<any>>;
|
||||
components: Array<React.FunctionComponent<SuggestionProps<unknown>>>;
|
||||
wrapperHeight?: number;
|
||||
|
||||
// suggestionBoxAlgn is an optional object that can be passed to align the SuggestionList with the keyboard caret
|
||||
|
@ -5,9 +5,10 @@ import {getState} from 'stores/redux_store';
|
||||
|
||||
import mockStore from 'tests/test_store';
|
||||
|
||||
import SwitchChannelProvider from 'components/suggestion/switch_channel_provider.jsx';
|
||||
import {Preferences} from 'mattermost-redux/constants';
|
||||
|
||||
import SwitchChannelProvider from './switch_channel_provider';
|
||||
|
||||
const latestPost = {
|
||||
id: 'latest_post_id',
|
||||
user_id: 'current_user_id',
|
||||
|
@ -2,10 +2,15 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {connect} from 'react-redux';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import {Channel, ChannelMembership, ChannelType} from '@mattermost/types/channels';
|
||||
import {PreferenceType} from '@mattermost/types/preferences';
|
||||
import {Team} from '@mattermost/types/teams';
|
||||
import {UserProfile} from '@mattermost/types/users';
|
||||
import {RelationOneToOne} from '@mattermost/types/utilities';
|
||||
|
||||
import GuestTag from 'components/widgets/tag/guest_tag';
|
||||
import BotTag from 'components/widgets/tag/bot_tag';
|
||||
|
||||
@ -36,11 +41,11 @@ import {
|
||||
getUser,
|
||||
makeSearchProfilesMatchingWithTerm,
|
||||
getStatusForUserId,
|
||||
getUserByUsername,
|
||||
} from 'mattermost-redux/selectors/entities/users';
|
||||
import {fetchAllMyTeamsChannelsAndChannelMembersREST, searchAllChannels} from 'mattermost-redux/actions/channels';
|
||||
import {getThreadCountsInCurrentTeam} from 'mattermost-redux/selectors/entities/threads';
|
||||
import {logError} from 'mattermost-redux/actions/errors';
|
||||
import {ActionResult} from 'mattermost-redux/types/actions';
|
||||
import {sortChannelsByTypeAndDisplayName, isChannelMuted} from 'mattermost-redux/utils/channel_utils';
|
||||
import SharedChannelIndicator from 'components/shared_channel_indicator';
|
||||
import CustomStatusEmoji from 'components/custom_status/custom_status_emoji';
|
||||
@ -53,12 +58,14 @@ import {isGuest} from 'mattermost-redux/utils/user_utils';
|
||||
import {Preferences} from 'mattermost-redux/constants';
|
||||
import {getPreferenceKey} from 'mattermost-redux/utils/preference_utils';
|
||||
|
||||
import Provider from './provider';
|
||||
import Suggestion from './suggestion.jsx';
|
||||
import Provider, {ResultsCallback} from './provider';
|
||||
import {SuggestionContainer, SuggestionProps} from './suggestion';
|
||||
import {GlobalState} from 'types/store';
|
||||
|
||||
const getState = store.getState;
|
||||
const searchProfilesMatchingWithTerm = makeSearchProfilesMatchingWithTerm();
|
||||
const ThreadsChannel = {
|
||||
|
||||
const ThreadsChannel: FakeChannel = {
|
||||
id: 'threads',
|
||||
name: 'threads',
|
||||
display_name: 'Threads',
|
||||
@ -66,7 +73,7 @@ const ThreadsChannel = {
|
||||
delete_at: 0,
|
||||
};
|
||||
|
||||
const InsightsChannel = {
|
||||
const InsightsChannel: FakeChannel = {
|
||||
id: 'insights',
|
||||
name: 'activity-and-insights',
|
||||
display_name: 'Insights',
|
||||
@ -74,216 +81,244 @@ const InsightsChannel = {
|
||||
delete_at: 0,
|
||||
};
|
||||
|
||||
class SwitchChannelSuggestion extends Suggestion {
|
||||
static get propTypes() {
|
||||
return {
|
||||
...super.propTypes,
|
||||
channelMember: PropTypes.object,
|
||||
hasDraft: PropTypes.bool,
|
||||
userImageUrl: PropTypes.string,
|
||||
dmChannelTeammate: PropTypes.object,
|
||||
collapsedThreads: PropTypes.bool,
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
const {item, isSelection, userImageUrl, status, userItem, collapsedThreads, team, isPartOfOnlyOneTeam} = this.props;
|
||||
const channel = item.channel;
|
||||
const channelIsArchived = channel.delete_at && channel.delete_at !== 0;
|
||||
|
||||
const member = this.props.channelMember;
|
||||
const teammate = this.props.dmChannelTeammate;
|
||||
let badge = null;
|
||||
|
||||
if ((member && member.notify_props) || item.unread_mentions) {
|
||||
let unreadMentions;
|
||||
if (item.unread_mentions) {
|
||||
unreadMentions = item.unread_mentions;
|
||||
} else {
|
||||
unreadMentions = collapsedThreads ? member.mention_count_root : member.mention_count;
|
||||
}
|
||||
if (unreadMentions > 0 && !channelIsArchived) {
|
||||
badge = (
|
||||
<div className={classNames('suggestion-list_unread-mentions', (isPartOfOnlyOneTeam ? 'position-end' : ''))}>
|
||||
<span className='badge'>
|
||||
{unreadMentions}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let className = 'suggestion-list__item';
|
||||
if (isSelection) {
|
||||
className += ' suggestion--selected';
|
||||
}
|
||||
|
||||
let name = channel.display_name;
|
||||
let description = '~' + channel.name;
|
||||
let icon;
|
||||
if (channelIsArchived) {
|
||||
icon = (
|
||||
<span className='suggestion-list__icon suggestion-list__icon--large'>
|
||||
<i className='icon icon-archive-outline'/>
|
||||
</span>
|
||||
);
|
||||
} else if (this.props.hasDraft) {
|
||||
icon = (
|
||||
<span className='suggestion-list__icon suggestion-list__icon--large'>
|
||||
<i className='icon icon-pencil-outline'/>
|
||||
</span>
|
||||
);
|
||||
} else if (channel.type === Constants.OPEN_CHANNEL) {
|
||||
icon = (
|
||||
<span className='suggestion-list__icon suggestion-list__icon--large'>
|
||||
<i className='icon icon-globe'/>
|
||||
</span>
|
||||
);
|
||||
} else if (channel.type === Constants.PRIVATE_CHANNEL) {
|
||||
icon = (
|
||||
<span className='suggestion-list__icon suggestion-list__icon--large'>
|
||||
<i className='icon icon-lock-outline'/>
|
||||
</span>
|
||||
);
|
||||
} else if (channel.type === Constants.THREADS) {
|
||||
icon = (
|
||||
<span className='suggestion-list__icon suggestion-list__icon--large'>
|
||||
<i className='icon icon-message-text-outline'/>
|
||||
</span>
|
||||
);
|
||||
} else if (channel.type === Constants.INSIGHTS) {
|
||||
icon = (
|
||||
<span className='suggestion-list__icon suggestion-list__icon--large'>
|
||||
<i className='icon icon-chart-line'/>
|
||||
</span>
|
||||
);
|
||||
} else if (channel.type === Constants.GM_CHANNEL) {
|
||||
icon = (
|
||||
<span className='suggestion-list__icon suggestion-list__icon--large'>
|
||||
<div className='status status--group'>{'G'}</div>
|
||||
</span>
|
||||
);
|
||||
} else {
|
||||
icon = (
|
||||
<ProfilePicture
|
||||
src={userImageUrl}
|
||||
status={teammate && teammate.is_bot ? null : status}
|
||||
size='sm'
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
let tag = null;
|
||||
let customStatus = null;
|
||||
if (channel.type === Constants.DM_CHANNEL) {
|
||||
if (teammate && teammate.is_bot) {
|
||||
tag = <BotTag/>;
|
||||
} else if (isGuest(teammate ? teammate.roles : '')) {
|
||||
tag = <GuestTag/>;
|
||||
}
|
||||
|
||||
customStatus = (
|
||||
<CustomStatusEmoji
|
||||
showTooltip={true}
|
||||
userID={userItem.id}
|
||||
emojiStyle={{
|
||||
marginBottom: 2,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
let deactivated = '';
|
||||
if (userItem.delete_at) {
|
||||
deactivated = (' - ' + Utils.localizeMessage('channel_switch_modal.deactivated', 'Deactivated'));
|
||||
}
|
||||
|
||||
if (channel.display_name && !(teammate && teammate.is_bot)) {
|
||||
description = '@' + userItem.username + deactivated;
|
||||
} else {
|
||||
name = userItem.username;
|
||||
const currentUserId = getCurrentUserId(getState());
|
||||
if (userItem.id === currentUserId) {
|
||||
name += (' ' + Utils.localizeMessage('suggestion.user.isCurrent', '(you)'));
|
||||
}
|
||||
description = deactivated;
|
||||
}
|
||||
} else if (channel.type === Constants.GM_CHANNEL) {
|
||||
// remove the slug from the option
|
||||
name = channel.display_name;
|
||||
description = '';
|
||||
}
|
||||
|
||||
let sharedIcon = null;
|
||||
if (channel.shared) {
|
||||
sharedIcon = (
|
||||
<SharedChannelIndicator
|
||||
className='shared-channel-icon'
|
||||
channelType={channel.type}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
let teamName = null;
|
||||
if (channel.team_id && team) {
|
||||
teamName = (<span className='ml-2 suggestion-list__team-name'>{team.display_name}</span>);
|
||||
}
|
||||
const showSlug = (isPartOfOnlyOneTeam || channel.type === Constants.DM_CHANNEL) && channel.type !== Constants.THREADS;
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={this.handleClick}
|
||||
onMouseMove={this.handleMouseMove}
|
||||
className={className}
|
||||
role='listitem'
|
||||
ref={(node) => {
|
||||
this.node = node;
|
||||
}}
|
||||
id={`switchChannel_${channel.name}`}
|
||||
data-testid={channel.name}
|
||||
aria-label={name}
|
||||
{...Suggestion.baseProps}
|
||||
>
|
||||
{icon}
|
||||
<div className='suggestion-list__ellipsis suggestion-list__flex'>
|
||||
<span className='suggestion-list__main'>
|
||||
<span className={classNames({'suggestion-list__unread': item.unread && !channelIsArchived})}>{name}</span>
|
||||
{showSlug && description && <span className='ml-2 suggestion-list__desc'>{description}</span>}
|
||||
</span>
|
||||
{customStatus}
|
||||
{sharedIcon}
|
||||
{tag}
|
||||
{badge}
|
||||
{!isPartOfOnlyOneTeam && teamName}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
type FakeChannel = Pick<Channel, 'id' | 'name' | 'display_name' | 'delete_at'> & {
|
||||
type: string;
|
||||
}
|
||||
|
||||
function mapStateToPropsForSwitchChannelSuggestion(state, ownProps) {
|
||||
type FakeDirectChannel = FakeChannel & {
|
||||
userId: string;
|
||||
}
|
||||
|
||||
type ChannelItem = Channel | FakeChannel | FakeDirectChannel;
|
||||
|
||||
function isRealChannel(item?: ChannelItem): item is Channel {
|
||||
return Boolean(item) && !isFakeChannel(item) && !isFakeDirectChannel(item);
|
||||
}
|
||||
|
||||
function isFakeChannel(item?: ChannelItem): item is FakeChannel {
|
||||
return Boolean(item) && !('create_at' in item!);
|
||||
}
|
||||
|
||||
function isFakeDirectChannel(item?: ChannelItem): item is FakeDirectChannel {
|
||||
return Boolean(item && 'userId' in item);
|
||||
}
|
||||
|
||||
interface WrappedChannel {
|
||||
channel: ChannelItem;
|
||||
name: string;
|
||||
deactivated: boolean;
|
||||
last_viewed_at?: number;
|
||||
type?: string;
|
||||
unread?: boolean;
|
||||
unread_mentions?: number;
|
||||
}
|
||||
|
||||
type Props = SuggestionProps<WrappedChannel> & {
|
||||
channelMember: ChannelMembership;
|
||||
collapsedThreads: boolean;
|
||||
dmChannelTeammate?: UserProfile;
|
||||
hasDraft: boolean;
|
||||
isPartOfOnlyOneTeam: boolean;
|
||||
status?: string;
|
||||
team?: Team;
|
||||
}
|
||||
|
||||
const SwitchChannelSuggestion = React.forwardRef<HTMLDivElement, Props>((props, ref) => {
|
||||
const {item, status, collapsedThreads, team, isPartOfOnlyOneTeam} = props;
|
||||
const channel = item.channel;
|
||||
const channelIsArchived = channel.delete_at && channel.delete_at !== 0;
|
||||
|
||||
const member = props.channelMember;
|
||||
const teammate = props.dmChannelTeammate;
|
||||
let badge = null;
|
||||
|
||||
if ((member && member.notify_props) || item.unread_mentions) {
|
||||
let unreadMentions;
|
||||
if (item.unread_mentions) {
|
||||
unreadMentions = item.unread_mentions;
|
||||
} else {
|
||||
unreadMentions = collapsedThreads ? member.mention_count_root : member.mention_count;
|
||||
}
|
||||
if (unreadMentions > 0 && !channelIsArchived) {
|
||||
badge = (
|
||||
<div className={classNames('suggestion-list_unread-mentions', (isPartOfOnlyOneTeam ? 'position-end' : ''))}>
|
||||
<span className='badge'>
|
||||
{unreadMentions}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let name = channel.display_name;
|
||||
let description = '~' + channel.name;
|
||||
let icon;
|
||||
if (channelIsArchived) {
|
||||
icon = (
|
||||
<span className='suggestion-list__icon suggestion-list__icon--large'>
|
||||
<i className='icon icon-archive-outline'/>
|
||||
</span>
|
||||
);
|
||||
} else if (props.hasDraft) {
|
||||
icon = (
|
||||
<span className='suggestion-list__icon suggestion-list__icon--large'>
|
||||
<i className='icon icon-pencil-outline'/>
|
||||
</span>
|
||||
);
|
||||
} else if (channel.type === Constants.OPEN_CHANNEL) {
|
||||
icon = (
|
||||
<span className='suggestion-list__icon suggestion-list__icon--large'>
|
||||
<i className='icon icon-globe'/>
|
||||
</span>
|
||||
);
|
||||
} else if (channel.type === Constants.PRIVATE_CHANNEL) {
|
||||
icon = (
|
||||
<span className='suggestion-list__icon suggestion-list__icon--large'>
|
||||
<i className='icon icon-lock-outline'/>
|
||||
</span>
|
||||
);
|
||||
} else if (channel.type === Constants.THREADS) {
|
||||
icon = (
|
||||
<span className='suggestion-list__icon suggestion-list__icon--large'>
|
||||
<i className='icon icon-message-text-outline'/>
|
||||
</span>
|
||||
);
|
||||
} else if (channel.type === Constants.INSIGHTS) {
|
||||
icon = (
|
||||
<span className='suggestion-list__icon suggestion-list__icon--large'>
|
||||
<i className='icon icon-chart-line'/>
|
||||
</span>
|
||||
);
|
||||
} else if (channel.type === Constants.GM_CHANNEL) {
|
||||
icon = (
|
||||
<span className='suggestion-list__icon suggestion-list__icon--large'>
|
||||
<div className='status status--group'>{'G'}</div>
|
||||
</span>
|
||||
);
|
||||
} else if (teammate) {
|
||||
icon = (
|
||||
<ProfilePicture
|
||||
src={Utils.imageURLForUser(teammate.id, teammate.last_picture_update)}
|
||||
status={teammate.is_bot ? undefined : status}
|
||||
size='sm'
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
let tag = null;
|
||||
let customStatus = null;
|
||||
if (channel.type === Constants.DM_CHANNEL && teammate) {
|
||||
if (teammate && teammate.is_bot) {
|
||||
tag = <BotTag/>;
|
||||
} else if (isGuest(teammate ? teammate.roles : '')) {
|
||||
tag = <GuestTag/>;
|
||||
}
|
||||
|
||||
customStatus = (
|
||||
<CustomStatusEmoji
|
||||
showTooltip={true}
|
||||
userID={teammate.id}
|
||||
emojiStyle={{
|
||||
marginBottom: 2,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
let deactivated = '';
|
||||
if (teammate.delete_at) {
|
||||
deactivated = (' - ' + Utils.localizeMessage('channel_switch_modal.deactivated', 'Deactivated'));
|
||||
}
|
||||
|
||||
if (channel.display_name && !(teammate && teammate.is_bot)) {
|
||||
description = '@' + teammate.username + deactivated;
|
||||
} else {
|
||||
name = teammate.username;
|
||||
const currentUserId = getCurrentUserId(getState());
|
||||
if (teammate.id === currentUserId) {
|
||||
name += (' ' + Utils.localizeMessage('suggestion.user.isCurrent', '(you)'));
|
||||
}
|
||||
description = deactivated;
|
||||
}
|
||||
} else if (channel.type === Constants.GM_CHANNEL) {
|
||||
// remove the slug from the option
|
||||
name = channel.display_name;
|
||||
description = '';
|
||||
}
|
||||
|
||||
let sharedIcon = null;
|
||||
if (isRealChannel(channel) && channel.shared) {
|
||||
sharedIcon = (
|
||||
<SharedChannelIndicator
|
||||
className='shared-channel-icon'
|
||||
channelType={channel.type as ChannelType}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
let teamName = null;
|
||||
if (isRealChannel(channel) && channel.team_id && team) {
|
||||
teamName = (<span className='ml-2 suggestion-list__team-name'>{team.display_name}</span>);
|
||||
}
|
||||
const showSlug = (isPartOfOnlyOneTeam || channel.type === Constants.DM_CHANNEL) && channel.type !== Constants.THREADS;
|
||||
|
||||
return (
|
||||
<SuggestionContainer
|
||||
ref={ref}
|
||||
id={`switchChannel_${channel.name}`}
|
||||
data-testid={channel.name}
|
||||
role='listitem'
|
||||
{...props}
|
||||
>
|
||||
{icon}
|
||||
<div className='suggestion-list__ellipsis suggestion-list__flex'>
|
||||
<span className='suggestion-list__main'>
|
||||
<span className={classNames({'suggestion-list__unread': item.unread && !channelIsArchived})}>{name}</span>
|
||||
{showSlug && description && <span className='ml-2 suggestion-list__desc'>{description}</span>}
|
||||
</span>
|
||||
{customStatus}
|
||||
{sharedIcon}
|
||||
{tag}
|
||||
{badge}
|
||||
{!isPartOfOnlyOneTeam && teamName}
|
||||
</div>
|
||||
</SuggestionContainer>
|
||||
);
|
||||
});
|
||||
SwitchChannelSuggestion.displayName = 'SwitchChannelSuggestion';
|
||||
|
||||
type OwnProps = SuggestionProps<WrappedChannel>;
|
||||
|
||||
function mapStateToPropsForSwitchChannelSuggestion(state: GlobalState, ownProps: OwnProps) {
|
||||
const channel = ownProps.item && ownProps.item.channel;
|
||||
const channelId = channel ? channel.id : '';
|
||||
const draft = channelId ? getPostDraft(state, StoragePrefixes.DRAFT, channelId) : false;
|
||||
const user = channel && getUser(state, channel.userId);
|
||||
const userImageUrl = user && Utils.imageURLForUser(user.id, user.last_picture_update);
|
||||
let dmChannelTeammate = channel && channel.type === Constants.DM_CHANNEL && getDirectTeammate(state, channel.id);
|
||||
const userItem = getUserByUsername(state, channel.name);
|
||||
const status = getStatusForUserId(state, channel.userId);
|
||||
const collapsedThreads = isCollapsedThreadsEnabled(state);
|
||||
const team = getTeam(state, channel.team_id);
|
||||
const isPartOfOnlyOneTeam = getMyTeams(state).length === 1;
|
||||
|
||||
if (channel && !dmChannelTeammate) {
|
||||
let dmChannelTeammate;
|
||||
if (isRealChannel(channel) && channel.type === Constants.DM_CHANNEL) {
|
||||
dmChannelTeammate = getDirectTeammate(state, channel.id);
|
||||
} else if (isFakeDirectChannel(channel)) {
|
||||
dmChannelTeammate = getUser(state, channel.userId);
|
||||
}
|
||||
|
||||
let status;
|
||||
if (dmChannelTeammate) {
|
||||
status = getStatusForUserId(state, dmChannelTeammate.id);
|
||||
}
|
||||
|
||||
const collapsedThreads = isCollapsedThreadsEnabled(state);
|
||||
|
||||
let team;
|
||||
if (isRealChannel(channel)) {
|
||||
team = getTeam(state, channel.team_id);
|
||||
}
|
||||
|
||||
const isPartOfOnlyOneTeam = getMyTeams(state).length === 1;
|
||||
|
||||
return {
|
||||
channelMember: getMyChannelMemberships(state)[channelId],
|
||||
hasDraft: draft && Boolean(draft.message.trim() || draft.fileInfos.length || draft.uploadsInProgress.length),
|
||||
userImageUrl,
|
||||
dmChannelTeammate,
|
||||
status,
|
||||
userItem,
|
||||
collapsedThreads,
|
||||
team,
|
||||
isPartOfOnlyOneTeam,
|
||||
@ -294,7 +329,7 @@ const ConnectedSwitchChannelSuggestion = connect(mapStateToPropsForSwitchChannel
|
||||
|
||||
let prefix = '';
|
||||
|
||||
function sortChannelsByRecencyAndTypeAndDisplayName(wrappedA, wrappedB) {
|
||||
function sortChannelsByRecencyAndTypeAndDisplayName(wrappedA: WrappedChannel, wrappedB: WrappedChannel) {
|
||||
if (wrappedA.last_viewed_at && wrappedB.last_viewed_at) {
|
||||
return wrappedB.last_viewed_at - wrappedA.last_viewed_at;
|
||||
} else if (wrappedA.last_viewed_at) {
|
||||
@ -304,10 +339,10 @@ function sortChannelsByRecencyAndTypeAndDisplayName(wrappedA, wrappedB) {
|
||||
}
|
||||
|
||||
// MM-12677 When this is migrated this needs to be fixed to pull the user's locale
|
||||
return sortChannelsByTypeAndDisplayName('en', wrappedA.channel, wrappedB.channel);
|
||||
return sortChannelsByTypeAndDisplayName('en', wrappedA.channel as Channel, wrappedB.channel as Channel);
|
||||
}
|
||||
|
||||
export function quickSwitchSorter(wrappedA, wrappedB) {
|
||||
export function quickSwitchSorter(wrappedA: WrappedChannel, wrappedB: WrappedChannel) {
|
||||
const aIsArchived = wrappedA.channel.delete_at ? wrappedA.channel.delete_at !== 0 : false;
|
||||
const bIsArchived = wrappedB.channel.delete_at ? wrappedB.channel.delete_at !== 0 : false;
|
||||
|
||||
@ -356,14 +391,14 @@ export function quickSwitchSorter(wrappedA, wrappedB) {
|
||||
return sortChannelsByRecencyAndTypeAndDisplayName(wrappedA, wrappedB);
|
||||
}
|
||||
|
||||
function makeChannelSearchFilter(channelPrefix) {
|
||||
function makeChannelSearchFilter(channelPrefix: string) {
|
||||
const channelPrefixLower = channelPrefix.toLowerCase();
|
||||
const splitPrefixBySpace = channelPrefixLower.trim().split(/[ ,]+/);
|
||||
const curState = getState();
|
||||
const usersInChannels = getUserIdsInChannels(curState);
|
||||
const userSearchStrings = {};
|
||||
const userSearchStrings: RelationOneToOne<UserProfile, string> = {};
|
||||
|
||||
return (channel) => {
|
||||
return (channel: ChannelItem) => {
|
||||
let searchString = `${channel.display_name}${channel.name}`;
|
||||
if (channel.type === Constants.GM_CHANNEL || channel.type === Constants.DM_CHANNEL) {
|
||||
const usersInChannel = usersInChannels[channel.id] || new Set([]);
|
||||
@ -411,7 +446,7 @@ export default class SwitchChannelProvider extends Provider {
|
||||
*
|
||||
* @see {@link components/forward_post_modal/forward_post_channel_select.tsx}
|
||||
*/
|
||||
handlePretextChanged(channelPrefix, resultsCallback) {
|
||||
handlePretextChanged(channelPrefix: string, resultsCallback: ResultsCallback<WrappedChannel>) {
|
||||
if (channelPrefix) {
|
||||
prefix = channelPrefix;
|
||||
this.startNewRequest(channelPrefix);
|
||||
@ -421,7 +456,7 @@ export default class SwitchChannelProvider extends Provider {
|
||||
|
||||
// Dispatch suggestions for local data (filter out deleted and archived channels from local store data)
|
||||
const channels = getChannelsInAllTeams(getState()).concat(getDirectAndGroupChannels(getState())).filter((c) => c.delete_at === 0);
|
||||
const users = Object.assign([], searchProfilesMatchingWithTerm(getState(), channelPrefix, false));
|
||||
const users = searchProfilesMatchingWithTerm(getState(), channelPrefix, false);
|
||||
const formattedData = this.formatList(channelPrefix, [ThreadsChannel, InsightsChannel, ...channels], users, true, true);
|
||||
if (formattedData) {
|
||||
resultsCallback(formattedData);
|
||||
@ -436,7 +471,7 @@ export default class SwitchChannelProvider extends Provider {
|
||||
return true;
|
||||
}
|
||||
|
||||
async fetchUsersAndChannels(channelPrefix, resultsCallback) {
|
||||
async fetchUsersAndChannels(channelPrefix: string, resultsCallback: ResultsCallback<WrappedChannel>) {
|
||||
const state = getState();
|
||||
const teamId = getCurrentTeamId(state);
|
||||
|
||||
@ -454,15 +489,16 @@ export default class SwitchChannelProvider extends Provider {
|
||||
|
||||
const channelsAsync = searchAllChannels(channelPrefix, {nonAdminSearch: true})(store.dispatch, store.getState);
|
||||
|
||||
let usersFromServer = [];
|
||||
let channelsFromServer = [];
|
||||
let usersFromServer;
|
||||
let channelsFromServer;
|
||||
|
||||
try {
|
||||
usersFromServer = await usersAsync;
|
||||
const {data} = await channelsAsync;
|
||||
channelsFromServer = data;
|
||||
const channelsResponse = await channelsAsync;
|
||||
channelsFromServer = (channelsResponse as ActionResult).data;
|
||||
} catch (err) {
|
||||
store.dispatch(logError(err));
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.shouldCancelDispatch(channelPrefix)) {
|
||||
@ -473,10 +509,10 @@ export default class SwitchChannelProvider extends Provider {
|
||||
|
||||
// filter out deleted and archived channels from local store data
|
||||
const localChannelData = getChannelsInAllTeams(state).concat(getDirectAndGroupChannels(state)).filter((c) => c.delete_at === 0) || [];
|
||||
const localUserData = Object.assign([], searchProfilesMatchingWithTerm(state, channelPrefix, false)) || [];
|
||||
const localUserData = searchProfilesMatchingWithTerm(state, channelPrefix, false);
|
||||
const localFormattedData = this.formatList(channelPrefix, [ThreadsChannel, InsightsChannel, ...localChannelData], localUserData);
|
||||
const remoteChannelData = channelsFromServer.concat(getGroupChannels(state)) || [];
|
||||
const remoteUserData = Object.assign([], usersFromServer.users) || [];
|
||||
const remoteUserData = usersFromServer.users || [];
|
||||
const remoteFormattedData = this.formatList(channelPrefix, remoteChannelData, remoteUserData, false);
|
||||
|
||||
store.dispatch({
|
||||
@ -484,18 +520,16 @@ export default class SwitchChannelProvider extends Provider {
|
||||
data: [...localUserData.filter((user) => user.id !== currentUserId), ...remoteUserData.filter((user) => user.id !== currentUserId)],
|
||||
});
|
||||
const combinedTerms = [...localFormattedData.terms, ...remoteFormattedData.terms.filter((term) => !localFormattedData.terms.includes(term))];
|
||||
const combinedItems = [...localFormattedData.items, ...remoteFormattedData.items.filter((item) => !localFormattedData.terms.includes(item.channel.userId || item.channel.id))];
|
||||
const combinedItems = [...localFormattedData.items, ...remoteFormattedData.items.filter((item: any) => !localFormattedData.terms.includes((item.channel as FakeDirectChannel).userId || item.channel.id))];
|
||||
|
||||
resultsCallback({
|
||||
...localFormattedData,
|
||||
...{
|
||||
items: combinedItems,
|
||||
terms: combinedTerms,
|
||||
},
|
||||
items: combinedItems,
|
||||
terms: combinedTerms,
|
||||
});
|
||||
}
|
||||
|
||||
userWrappedChannel(user, channel) {
|
||||
userWrappedChannel(user: UserProfile, channel?: ChannelItem): WrappedChannel {
|
||||
let displayName = '';
|
||||
const currentUserId = getCurrentUserId(getState());
|
||||
|
||||
@ -523,21 +557,21 @@ export default class SwitchChannelProvider extends Provider {
|
||||
id: channel ? channel.id : user.id,
|
||||
userId: user.id,
|
||||
update_at: user.update_at,
|
||||
delete_at: 0,
|
||||
type: Constants.DM_CHANNEL,
|
||||
last_picture_update: user.last_picture_update || 0,
|
||||
},
|
||||
type: 'search.direct',
|
||||
name: user.username,
|
||||
deactivated: user.delete_at,
|
||||
deactivated: Boolean(user.delete_at),
|
||||
};
|
||||
}
|
||||
|
||||
formatList(channelPrefix, allChannels, users, skipNotMember = true, localData = false) {
|
||||
formatList(channelPrefix: string, allChannels: ChannelItem[], users: UserProfile[], skipNotMember = true, localData = false) {
|
||||
const channels = [];
|
||||
|
||||
const members = getMyChannelMemberships(getState());
|
||||
|
||||
const completedChannels = {};
|
||||
const completedChannels: RelationOneToOne<Channel, boolean> = {};
|
||||
|
||||
const channelFilter = makeChannelSearchFilter(channelPrefix);
|
||||
|
||||
@ -548,17 +582,15 @@ export default class SwitchChannelProvider extends Provider {
|
||||
const allUnreadChannelIdsSet = new Set(allUnreadChannelIds);
|
||||
const currentUserId = getCurrentUserId(state);
|
||||
|
||||
for (const id of Object.keys(allChannels)) {
|
||||
const channel = allChannels[id];
|
||||
|
||||
for (const channel of allChannels) {
|
||||
if (completedChannels[channel.id]) {
|
||||
continue;
|
||||
}
|
||||
if (channelFilter(channel)) {
|
||||
const newChannel = Object.assign({}, channel);
|
||||
const newChannel = {...channel};
|
||||
const channelIsArchived = channel.delete_at !== 0;
|
||||
|
||||
let wrappedChannel = {channel: newChannel, name: newChannel.name, deactivated: false};
|
||||
let wrappedChannel: WrappedChannel = {channel: newChannel, name: newChannel.name, deactivated: false};
|
||||
if (members[channel.id]) {
|
||||
wrappedChannel.last_viewed_at = members[channel.id].last_viewed_at;
|
||||
} else if (skipNotMember && (newChannel.type !== Constants.THREADS && newChannel.type !== Constants.INSIGHTS)) {
|
||||
@ -642,9 +674,11 @@ export default class SwitchChannelProvider extends Provider {
|
||||
continue;
|
||||
}
|
||||
|
||||
const unread = allUnreadChannelIdsSet.has(channel?.id) && !isChannelMuted(members[channel.id]);
|
||||
if (unread) {
|
||||
wrappedChannel.unread = true;
|
||||
if (channel) {
|
||||
const unread = allUnreadChannelIdsSet.has(channel.id) && !isChannelMuted(members[channel.id]);
|
||||
if (unread) {
|
||||
wrappedChannel.unread = true;
|
||||
}
|
||||
}
|
||||
|
||||
completedChannels[user.id] = true;
|
||||
@ -653,7 +687,13 @@ export default class SwitchChannelProvider extends Provider {
|
||||
|
||||
const channelNames = channels.
|
||||
sort(quickSwitchSorter).
|
||||
map((wrappedChannel) => wrappedChannel.channel.userId || wrappedChannel.channel.id);
|
||||
map((wrappedChannel) => {
|
||||
if (isFakeDirectChannel(wrappedChannel.channel) && wrappedChannel.channel.userId) {
|
||||
return wrappedChannel.channel.userId;
|
||||
}
|
||||
|
||||
return wrappedChannel.channel.id;
|
||||
});
|
||||
|
||||
if (localData && !channels.length) {
|
||||
channels.push({
|
||||
@ -670,7 +710,7 @@ export default class SwitchChannelProvider extends Provider {
|
||||
};
|
||||
}
|
||||
|
||||
fetchAndFormatRecentlyViewedChannels(resultsCallback) {
|
||||
fetchAndFormatRecentlyViewedChannels(resultsCallback: ResultsCallback<WrappedChannel>) {
|
||||
const state = getState();
|
||||
const recentChannels = getChannelsInAllTeams(state).concat(getDirectAndGroupChannels(state));
|
||||
const wrappedRecentChannels = this.wrapChannels(recentChannels, Constants.MENTION_RECENT_CHANNELS);
|
||||
@ -703,15 +743,16 @@ export default class SwitchChannelProvider extends Provider {
|
||||
});
|
||||
}
|
||||
|
||||
getThreadsItem(countType = 'total', itemType) {
|
||||
getThreadsItem(countType = 'total', itemType?: string) {
|
||||
const state = getState();
|
||||
const counts = getThreadCountsInCurrentTeam(state);
|
||||
const collapsedThreads = isCollapsedThreadsEnabled(state);
|
||||
|
||||
// adding last viewed at equal to Date.now() to push it to the top of the list
|
||||
let threadsItem = {
|
||||
let threadsItem: WrappedChannel = {
|
||||
channel: ThreadsChannel,
|
||||
name: ThreadsChannel.name,
|
||||
unread: Boolean(counts?.total_unread_threads),
|
||||
unread_mentions: counts?.total_unread_mentions || 0,
|
||||
deactivated: false,
|
||||
last_viewed_at: Date.now(),
|
||||
@ -719,9 +760,6 @@ export default class SwitchChannelProvider extends Provider {
|
||||
if (itemType) {
|
||||
threadsItem = {...threadsItem, type: itemType};
|
||||
}
|
||||
if (counts?.total_unread_threads) {
|
||||
threadsItem.unread = true;
|
||||
}
|
||||
if (collapsedThreads && ((countType === 'unread' && counts?.total_unread_threads) || (countType === 'total'))) {
|
||||
return threadsItem;
|
||||
}
|
||||
@ -737,6 +775,7 @@ export default class SwitchChannelProvider extends Provider {
|
||||
const insightsItem = {
|
||||
channel: InsightsChannel,
|
||||
name: InsightsChannel.name,
|
||||
unread: false,
|
||||
unread_mentions: 0,
|
||||
deactivated: false,
|
||||
last_viewed_at: Date.now(),
|
||||
@ -749,13 +788,13 @@ export default class SwitchChannelProvider extends Provider {
|
||||
return null;
|
||||
}
|
||||
|
||||
getTimestampFromPrefs(myPreferences, category, name) {
|
||||
getTimestampFromPrefs(myPreferences: Record<string, PreferenceType>, category: string, name: string) {
|
||||
const pref = myPreferences[getPreferenceKey(category, name)];
|
||||
const prefValue = pref ? pref.value : '0';
|
||||
return parseInt(prefValue, 10);
|
||||
return parseInt(prefValue ?? '', 10);
|
||||
}
|
||||
|
||||
getLastViewedAt(member, myPreferences, channel) {
|
||||
getLastViewedAt(member: ChannelMembership, myPreferences: Record<string, PreferenceType>, channel: Channel) {
|
||||
// The server only ever sets the last_viewed_at to the time of the last post in channel,
|
||||
// So thought of using preferences but it seems that also not keeping track.
|
||||
// TODO Update and remove comment once solution is finalized
|
||||
@ -766,12 +805,11 @@ export default class SwitchChannelProvider extends Provider {
|
||||
);
|
||||
}
|
||||
|
||||
wrapChannels(channels, channelType) {
|
||||
wrapChannels(channels: Channel[], channelType: string) {
|
||||
const state = getState();
|
||||
const currentChannel = getCurrentChannel(state);
|
||||
const myMembers = getMyChannelMemberships(state);
|
||||
const myPreferences = getMyPreferences(state);
|
||||
const collapsedThreads = isCollapsedThreadsEnabled(state);
|
||||
const allUnreadChannelIds = getAllTeamsUnreadChannelIds(state);
|
||||
const allUnreadChannelIdsSet = new Set(allUnreadChannelIds);
|
||||
|
||||
@ -781,14 +819,11 @@ export default class SwitchChannelProvider extends Provider {
|
||||
if (channel.id === currentChannel?.id) {
|
||||
continue;
|
||||
}
|
||||
let wrappedChannel = {channel, name: channel.name, deactivated: false};
|
||||
let wrappedChannel: WrappedChannel = {channel, name: channel.name, deactivated: false};
|
||||
const member = myMembers[channel.id];
|
||||
if (member) {
|
||||
wrappedChannel.last_viewed_at = this.getLastViewedAt(member, myPreferences, channel);
|
||||
}
|
||||
if (member && channelType === Constants.MENTION_UNREAD) {
|
||||
wrappedChannel.unreadMentions = collapsedThreads ? member.mention_count_root : member.mention_count;
|
||||
}
|
||||
if (channel.type === Constants.GM_CHANNEL) {
|
||||
wrappedChannel.name = channel.display_name;
|
||||
} else if (channel.type === Constants.DM_CHANNEL) {
|
||||
@ -814,20 +849,21 @@ export default class SwitchChannelProvider extends Provider {
|
||||
return channelList;
|
||||
}
|
||||
|
||||
async fetchChannels(resultsCallback) {
|
||||
async fetchChannels(resultsCallback: ResultsCallback<WrappedChannel>) {
|
||||
const state = getState();
|
||||
const teamId = getCurrentTeamId(state);
|
||||
if (!teamId) {
|
||||
return;
|
||||
}
|
||||
const channelsAsync = fetchAllMyTeamsChannelsAndChannelMembersREST()(store.dispatch, store.getState);
|
||||
const channelsAsync = store.dispatch(fetchAllMyTeamsChannelsAndChannelMembersREST());
|
||||
let channels;
|
||||
|
||||
try {
|
||||
const {data} = await channelsAsync;
|
||||
channels = data.channels;
|
||||
channels = data.channels as Channel[];
|
||||
} catch (err) {
|
||||
store.dispatch(logError(err));
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.latestPrefix !== '') {
|
@ -16,7 +16,7 @@ import AtMentionProvider from 'components/suggestion/at_mention_provider';
|
||||
import ChannelMentionProvider from 'components/suggestion/channel_mention_provider';
|
||||
import AppCommandProvider from 'components/suggestion/command_provider/app_provider';
|
||||
import CommandProvider from 'components/suggestion/command_provider/command_provider';
|
||||
import EmoticonProvider from 'components/suggestion/emoticon_provider.jsx';
|
||||
import EmoticonProvider from 'components/suggestion/emoticon_provider';
|
||||
import SuggestionBox from 'components/suggestion/suggestion_box';
|
||||
import SuggestionBoxComponent from 'components/suggestion/suggestion_box/suggestion_box';
|
||||
import SuggestionList from 'components/suggestion/suggestion_list.jsx';
|
||||
|
@ -349,7 +349,7 @@ export function makeGetChannelUnreadCount(): (state: GlobalState, channelId: str
|
||||
);
|
||||
}
|
||||
|
||||
export function getChannelByName(state: GlobalState, channelName: string): Channel | undefined | null {
|
||||
export function getChannelByName(state: GlobalState, channelName: string): Channel | undefined {
|
||||
return getChannelByNameHelper(getAllChannels(state), channelName);
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user