Adds a feature flag and the logic to hide plugin interactions on shared channels (#31185)
* Adds a feature flag and the logic to hide plugin interactions on shared channels * Address review comments * Fix CI * Address review comments * Address review comments --------- Co-authored-by: Miguel de la Cruz <miguel@ctrlz.es> Co-authored-by: Mattermost Build <build@mattermost.com>
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
58d6c71ed2
Коммит
c6c27e7752
@@ -22,6 +22,9 @@ type FeatureFlags struct {
|
|||||||
// Enable DMs and GMs for shared channels.
|
// Enable DMs and GMs for shared channels.
|
||||||
EnableSharedChannelsDMs bool
|
EnableSharedChannelsDMs bool
|
||||||
|
|
||||||
|
// Enable plugins in shared channels.
|
||||||
|
EnableSharedChannelsPlugins bool
|
||||||
|
|
||||||
// AppsEnabled toggles the Apps framework functionalities both in server and client side
|
// AppsEnabled toggles the Apps framework functionalities both in server and client side
|
||||||
AppsEnabled bool
|
AppsEnabled bool
|
||||||
|
|
||||||
@@ -66,6 +69,7 @@ func (f *FeatureFlags) SetDefaults() {
|
|||||||
f.TestBoolFeature = false
|
f.TestBoolFeature = false
|
||||||
f.EnableRemoteClusterService = false
|
f.EnableRemoteClusterService = false
|
||||||
f.EnableSharedChannelsDMs = false
|
f.EnableSharedChannelsDMs = false
|
||||||
|
f.EnableSharedChannelsPlugins = true
|
||||||
f.AppsEnabled = false
|
f.AppsEnabled = false
|
||||||
f.NormalizeLdapDNs = false
|
f.NormalizeLdapDNs = false
|
||||||
f.DeprecateCloudFree = false
|
f.DeprecateCloudFree = false
|
||||||
|
|||||||
@@ -298,7 +298,7 @@ export class ActionMenuClass extends React.PureComponent<Props, State> {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}) || [];
|
});
|
||||||
|
|
||||||
let appBindings = [] as JSX.Element[];
|
let appBindings = [] as JSX.Element[];
|
||||||
if (this.props.appsEnabled && this.state.appBindings) {
|
if (this.props.appsEnabled && this.state.appBindings) {
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ import type {Post} from '@mattermost/types/posts';
|
|||||||
import {Permissions} from 'mattermost-redux/constants';
|
import {Permissions} from 'mattermost-redux/constants';
|
||||||
import {AppBindingLocations} from 'mattermost-redux/constants/apps';
|
import {AppBindingLocations} from 'mattermost-redux/constants/apps';
|
||||||
import {appsEnabled} from 'mattermost-redux/selectors/entities/apps';
|
import {appsEnabled} from 'mattermost-redux/selectors/entities/apps';
|
||||||
import {isMarketplaceEnabled} from 'mattermost-redux/selectors/entities/general';
|
import {getChannel} from 'mattermost-redux/selectors/entities/channels';
|
||||||
|
import {isMarketplaceEnabled, getFeatureFlagValue} from 'mattermost-redux/selectors/entities/general';
|
||||||
import {haveICurrentTeamPermission} from 'mattermost-redux/selectors/entities/roles';
|
import {haveICurrentTeamPermission} from 'mattermost-redux/selectors/entities/roles';
|
||||||
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
|
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
|
||||||
import {getCurrentUser} from 'mattermost-redux/selectors/entities/users';
|
import {getCurrentUser} from 'mattermost-redux/selectors/entities/users';
|
||||||
@@ -47,6 +48,8 @@ function mapStateToProps(state: GlobalState, ownProps: Props) {
|
|||||||
const {post} = ownProps;
|
const {post} = ownProps;
|
||||||
|
|
||||||
const systemMessage = isSystemMessage(post);
|
const systemMessage = isSystemMessage(post);
|
||||||
|
const channel = getChannel(state, post.channel_id);
|
||||||
|
const sharedChannelsPluginsEnabled = getFeatureFlagValue(state, 'EnableSharedChannelsPlugins') === 'true';
|
||||||
|
|
||||||
const apps = appsEnabled(state);
|
const apps = appsEnabled(state);
|
||||||
const showBindings = apps && !systemMessage && !isCombinedUserActivityPost(post.id);
|
const showBindings = apps && !systemMessage && !isCombinedUserActivityPost(post.id);
|
||||||
@@ -57,12 +60,14 @@ function mapStateToProps(state: GlobalState, ownProps: Props) {
|
|||||||
const currentUser = getCurrentUser(state);
|
const currentUser = getCurrentUser(state);
|
||||||
const isSysAdmin = isSystemAdmin(currentUser.roles);
|
const isSysAdmin = isSystemAdmin(currentUser.roles);
|
||||||
|
|
||||||
|
const pluginItemsVisible = !channel?.shared || sharedChannelsPluginsEnabled;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
appBindings,
|
appBindings,
|
||||||
appsEnabled: apps,
|
appsEnabled: apps,
|
||||||
pluginMenuItemComponents: state.plugins.components.PostDropdownMenuItem,
|
pluginMenuItemComponents: pluginItemsVisible ? state.plugins.components.PostDropdownMenuItem : [],
|
||||||
isSysAdmin,
|
isSysAdmin,
|
||||||
pluginMenuItems: state.plugins.components.PostDropdownMenu,
|
pluginMenuItems: pluginItemsVisible ? state.plugins.components.PostDropdownMenu : [],
|
||||||
teamId: getCurrentTeamId(state),
|
teamId: getCurrentTeamId(state),
|
||||||
isMobileView: getIsMobileView(state),
|
isMobileView: getIsMobileView(state),
|
||||||
canOpenMarketplace: (
|
canOpenMarketplace: (
|
||||||
|
|||||||
@@ -307,7 +307,7 @@ const AdvancedTextEditor = ({
|
|||||||
}, [dispatch, currentUserId, getFormattingBarPreferenceName, isFormattingBarHidden]);
|
}, [dispatch, currentUserId, getFormattingBarPreferenceName, isFormattingBarHidden]);
|
||||||
|
|
||||||
useOrientationHandler(textboxRef, rootId);
|
useOrientationHandler(textboxRef, rootId);
|
||||||
const pluginItems = usePluginItems(draft, textboxRef, handleDraftChange);
|
const pluginItems = usePluginItems(draft, textboxRef, handleDraftChange, channelId);
|
||||||
const focusTextbox = useTextboxFocus(textboxRef, channelId, isRHS, canPost);
|
const focusTextbox = useTextboxFocus(textboxRef, channelId, isRHS, canPost);
|
||||||
const [attachmentPreview, fileUploadJSX] = useUploadFiles(
|
const [attachmentPreview, fileUploadJSX] = useUploadFiles(
|
||||||
draft,
|
draft,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
import React, {useCallback, useMemo} from 'react';
|
import React, {useCallback, useMemo} from 'react';
|
||||||
import {useSelector} from 'react-redux';
|
import {useSelector} from 'react-redux';
|
||||||
|
|
||||||
|
import {usePluginVisibilityInSharedChannel} from 'components/common/hooks/usePluginVisibilityInSharedChannel';
|
||||||
import type TextboxClass from 'components/textbox/textbox';
|
import type TextboxClass from 'components/textbox/textbox';
|
||||||
|
|
||||||
import type {GlobalState} from 'types/store';
|
import type {GlobalState} from 'types/store';
|
||||||
@@ -13,8 +14,10 @@ const usePluginItems = (
|
|||||||
draft: PostDraft,
|
draft: PostDraft,
|
||||||
textboxRef: React.RefObject<TextboxClass>,
|
textboxRef: React.RefObject<TextboxClass>,
|
||||||
handleDraftChange: (draft: PostDraft) => void,
|
handleDraftChange: (draft: PostDraft) => void,
|
||||||
|
channelId?: string,
|
||||||
) => {
|
) => {
|
||||||
const postEditorActions = useSelector((state: GlobalState) => state.plugins.components.PostEditorAction);
|
const postEditorActions = useSelector((state: GlobalState) => state.plugins.components.PostEditorAction);
|
||||||
|
const pluginItemsVisible = usePluginVisibilityInSharedChannel(channelId);
|
||||||
|
|
||||||
const getSelectedText = useCallback(() => {
|
const getSelectedText = useCallback(() => {
|
||||||
const input = textboxRef.current?.getInputBox();
|
const input = textboxRef.current?.getInputBox();
|
||||||
@@ -34,21 +37,27 @@ const usePluginItems = (
|
|||||||
// Missing setting the state eventually?
|
// Missing setting the state eventually?
|
||||||
}, [handleDraftChange, draft]);
|
}, [handleDraftChange, draft]);
|
||||||
|
|
||||||
const items = useMemo(() => postEditorActions?.map((item) => {
|
const items = useMemo(() => {
|
||||||
if (!item.component) {
|
if (!pluginItemsVisible) {
|
||||||
return null;
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const Component = item.component as any;
|
return postEditorActions?.map((item) => {
|
||||||
return (
|
if (!item.component) {
|
||||||
<Component
|
return null;
|
||||||
key={item.id}
|
}
|
||||||
draft={draft}
|
|
||||||
getSelectedText={getSelectedText}
|
const Component = item.component as any;
|
||||||
updateText={updateText}
|
return (
|
||||||
/>
|
<Component
|
||||||
);
|
key={item.id}
|
||||||
}), [postEditorActions, draft, getSelectedText, updateText]);
|
draft={draft}
|
||||||
|
getSelectedText={getSelectedText}
|
||||||
|
updateText={updateText}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}, [postEditorActions, draft, getSelectedText, updateText, pluginItemsVisible]);
|
||||||
|
|
||||||
return items;
|
return items;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2700,53 +2700,6 @@ exports[`components/ChannelHeader should render shared view 1`] = `
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Connect(injectIntl(ChannelHeaderPlug))
|
|
||||||
channel={
|
|
||||||
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": "Test",
|
|
||||||
"purpose": "purpose",
|
|
||||||
"scheme_id": "id",
|
|
||||||
"shared": true,
|
|
||||||
"team_id": "team_id",
|
|
||||||
"type": "O",
|
|
||||||
"update_at": 0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
channelMember={
|
|
||||||
Object {
|
|
||||||
"channel_id": "channel_id",
|
|
||||||
"last_update_at": 0,
|
|
||||||
"last_viewed_at": 0,
|
|
||||||
"mention_count": 0,
|
|
||||||
"mention_count_root": 0,
|
|
||||||
"msg_count": 0,
|
|
||||||
"msg_count_root": 0,
|
|
||||||
"notify_props": Object {
|
|
||||||
"channel_auto_follow_threads": "off",
|
|
||||||
"desktop": "default",
|
|
||||||
"email": "default",
|
|
||||||
"ignore_channel_mentions": "default",
|
|
||||||
"mark_unread": "all",
|
|
||||||
"push": "default",
|
|
||||||
},
|
|
||||||
"roles": "channel_user",
|
|
||||||
"scheme_admin": false,
|
|
||||||
"scheme_user": true,
|
|
||||||
"urgent_mention_count": 0,
|
|
||||||
"user_id": "user_id",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Connect(CallButton) />
|
|
||||||
<ChannelInfoButton
|
<ChannelInfoButton
|
||||||
channel={
|
channel={
|
||||||
Object {
|
Object {
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ describe('components/ChannelHeader', () => {
|
|||||||
'hour',
|
'hour',
|
||||||
],
|
],
|
||||||
hideGuestTags: false,
|
hideGuestTags: false,
|
||||||
|
sharedChannelsPluginsEnabled: false,
|
||||||
intl: {
|
intl: {
|
||||||
formatMessage: jest.fn(({id, defaultMessage}) => defaultMessage || id),
|
formatMessage: jest.fn(({id, defaultMessage}) => defaultMessage || id),
|
||||||
} as MockIntl,
|
} as MockIntl,
|
||||||
|
|||||||
@@ -370,11 +370,15 @@ class ChannelHeader extends React.PureComponent<Props> {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ChannelHeaderPlug
|
{(!channel.shared || this.props.sharedChannelsPluginsEnabled) && (
|
||||||
channel={channel}
|
<>
|
||||||
channelMember={channelMember}
|
<ChannelHeaderPlug
|
||||||
/>
|
channel={channel}
|
||||||
<CallButton/>
|
channelMember={channelMember}
|
||||||
|
/>
|
||||||
|
<CallButton/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<ChannelInfoButton channel={channel}/>
|
<ChannelInfoButton channel={channel}/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import {
|
|||||||
isCurrentChannelMuted,
|
isCurrentChannelMuted,
|
||||||
getCurrentChannelStats,
|
getCurrentChannelStats,
|
||||||
} from 'mattermost-redux/selectors/entities/channels';
|
} from 'mattermost-redux/selectors/entities/channels';
|
||||||
import {getConfig} from 'mattermost-redux/selectors/entities/general';
|
import {getConfig, getFeatureFlagValue} from 'mattermost-redux/selectors/entities/general';
|
||||||
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
|
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
|
||||||
import {
|
import {
|
||||||
displayLastActiveLabel,
|
displayLastActiveLabel,
|
||||||
@@ -54,6 +54,7 @@ function makeMapStateToProps() {
|
|||||||
const channel = getCurrentChannel(state);
|
const channel = getCurrentChannel(state);
|
||||||
const user = getCurrentUser(state);
|
const user = getCurrentUser(state);
|
||||||
const config = getConfig(state);
|
const config = getConfig(state);
|
||||||
|
const sharedChannelsPluginsEnabled = getFeatureFlagValue(state, 'EnableSharedChannelsPlugins') === 'true';
|
||||||
|
|
||||||
let dmUser;
|
let dmUser;
|
||||||
let gmMembers;
|
let gmMembers;
|
||||||
@@ -96,6 +97,7 @@ function makeMapStateToProps() {
|
|||||||
isLastActiveEnabled,
|
isLastActiveEnabled,
|
||||||
timestampUnits,
|
timestampUnits,
|
||||||
hideGuestTags: config.HideGuestTags === 'true',
|
hideGuestTags: config.HideGuestTags === 'true',
|
||||||
|
sharedChannelsPluginsEnabled,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ import ChannelPublicPrivateMenu from './channel_header_menu_items/channel_header
|
|||||||
|
|
||||||
import ChannelHeaderTitleDirect from '../channel_header/channel_header_title_direct';
|
import ChannelHeaderTitleDirect from '../channel_header/channel_header_title_direct';
|
||||||
import ChannelHeaderTitleGroup from '../channel_header/channel_header_title_group';
|
import ChannelHeaderTitleGroup from '../channel_header/channel_header_title_group';
|
||||||
|
import {usePluginVisibilityInSharedChannel} from '../common/hooks/usePluginVisibilityInSharedChannel';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
dmUser?: UserProfile;
|
dmUser?: UserProfile;
|
||||||
@@ -55,6 +56,7 @@ export default function ChannelHeaderMenu({dmUser, gmMembers, isMobile, archived
|
|||||||
const isLicensedForLDAPGroups = useSelector(getLicense).LDAPGroups === 'true';
|
const isLicensedForLDAPGroups = useSelector(getLicense).LDAPGroups === 'true';
|
||||||
const pluginMenuItems = useSelector(getChannelHeaderMenuPluginComponents);
|
const pluginMenuItems = useSelector(getChannelHeaderMenuPluginComponents);
|
||||||
const isChannelBookmarksEnabled = useSelector(getIsChannelBookmarksEnabled);
|
const isChannelBookmarksEnabled = useSelector(getIsChannelBookmarksEnabled);
|
||||||
|
const pluginItemsVisible = usePluginVisibilityInSharedChannel(channel?.id);
|
||||||
|
|
||||||
const isReadonly = false;
|
const isReadonly = false;
|
||||||
|
|
||||||
@@ -86,22 +88,26 @@ export default function ChannelHeaderMenu({dmUser, gmMembers, isMobile, archived
|
|||||||
channelTitle = <ChannelHeaderTitleGroup gmMembers={gmMembers}/>;
|
channelTitle = <ChannelHeaderTitleGroup gmMembers={gmMembers}/>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const pluginItems = pluginMenuItems.map((item) => {
|
let pluginItems: JSX.Element[] = [];
|
||||||
const handlePluginItemClick = () => {
|
|
||||||
if (item.action) {
|
|
||||||
item.action(channel.id);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
if (pluginItemsVisible) {
|
||||||
<Menu.Item
|
pluginItems = pluginMenuItems.map((item) => {
|
||||||
id={item.id + '_pluginmenuitem'}
|
const handlePluginItemClick = () => {
|
||||||
key={item.id + '_pluginmenuitem'}
|
if (item.action) {
|
||||||
onClick={handlePluginItemClick}
|
item.action(channel.id);
|
||||||
labels={<span>{item.text}</span>}
|
}
|
||||||
/>
|
};
|
||||||
);
|
|
||||||
});
|
return (
|
||||||
|
<Menu.Item
|
||||||
|
id={item.id + '_pluginmenuitem'}
|
||||||
|
key={item.id + '_pluginmenuitem'}
|
||||||
|
onClick={handlePluginItemClick}
|
||||||
|
labels={<span>{item.text}</span>}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Menu.Container
|
<Menu.Container
|
||||||
@@ -183,4 +189,3 @@ export default function ChannelHeaderMenu({dmUser, gmMembers, isMobile, archived
|
|||||||
</Menu.Container>
|
</Menu.Container>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
import React, {useCallback, useEffect, useState} from 'react';
|
import React, {useCallback, useEffect, useState} from 'react';
|
||||||
import {useSelector} from 'react-redux';
|
import {useSelector} from 'react-redux';
|
||||||
|
|
||||||
|
import {usePluginVisibilityInSharedChannel} from 'components/common/hooks/usePluginVisibilityInSharedChannel';
|
||||||
import CopyButton from 'components/copy_button';
|
import CopyButton from 'components/copy_button';
|
||||||
|
|
||||||
import * as SyntaxHighlighting from 'utils/syntax_highlighting';
|
import * as SyntaxHighlighting from 'utils/syntax_highlighting';
|
||||||
@@ -15,9 +16,10 @@ type Props = {
|
|||||||
code: string;
|
code: string;
|
||||||
language: string;
|
language: string;
|
||||||
searchedContent?: string;
|
searchedContent?: string;
|
||||||
|
channelId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const CodeBlock: React.FC<Props> = ({code, language, searchedContent}: Props) => {
|
const CodeBlock: React.FC<Props> = ({code, language, searchedContent, channelId}: Props) => {
|
||||||
const getUsedLanguage = useCallback(() => {
|
const getUsedLanguage = useCallback(() => {
|
||||||
let usedLanguage = language || '';
|
let usedLanguage = language || '';
|
||||||
usedLanguage = usedLanguage.toLowerCase();
|
usedLanguage = usedLanguage.toLowerCase();
|
||||||
@@ -82,7 +84,9 @@ const CodeBlock: React.FC<Props> = ({code, language, searchedContent}: Props) =>
|
|||||||
}
|
}
|
||||||
|
|
||||||
const codeBlockActions = useSelector((state: GlobalState) => state.plugins.components.CodeBlockAction);
|
const codeBlockActions = useSelector((state: GlobalState) => state.plugins.components.CodeBlockAction);
|
||||||
const pluginItems = codeBlockActions?.
|
const pluginItemsVisible = usePluginVisibilityInSharedChannel(channelId);
|
||||||
|
|
||||||
|
const pluginItems = pluginItemsVisible ? codeBlockActions?.
|
||||||
map((item) => {
|
map((item) => {
|
||||||
if (!item.component) {
|
if (!item.component) {
|
||||||
return null;
|
return null;
|
||||||
@@ -95,7 +99,7 @@ const CodeBlock: React.FC<Props> = ({code, language, searchedContent}: Props) =>
|
|||||||
code={code}
|
code={code}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
}) : [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={className}>
|
<div className={className}>
|
||||||
|
|||||||
@@ -0,0 +1,108 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import {renderHook} from '@testing-library/react-hooks';
|
||||||
|
import React from 'react';
|
||||||
|
import {Provider} from 'react-redux';
|
||||||
|
import configureStore from 'redux-mock-store';
|
||||||
|
|
||||||
|
import {getChannel} from 'mattermost-redux/selectors/entities/channels';
|
||||||
|
import {getFeatureFlagValue} from 'mattermost-redux/selectors/entities/general';
|
||||||
|
|
||||||
|
import {usePluginVisibilityInSharedChannel} from './usePluginVisibilityInSharedChannel';
|
||||||
|
|
||||||
|
jest.mock('mattermost-redux/selectors/entities/channels', () => ({
|
||||||
|
getChannel: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('mattermost-redux/selectors/entities/general', () => ({
|
||||||
|
getFeatureFlagValue: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('usePluginVisibilityInSharedChannel', () => {
|
||||||
|
const mockStore = configureStore();
|
||||||
|
const mockGetChannel = getChannel as jest.MockedFunction<typeof getChannel>;
|
||||||
|
const mockGetFeatureFlagValue = getFeatureFlagValue as jest.MockedFunction<typeof getFeatureFlagValue>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mockGetFeatureFlagValue.mockReturnValue('false');
|
||||||
|
mockGetChannel.mockReturnValue(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
const renderHookWithChannelId = (channelId: string | undefined) => {
|
||||||
|
const store = mockStore({});
|
||||||
|
const wrapper = ({children}: {children: React.ReactNode}) => (
|
||||||
|
<Provider store={store}>{children}</Provider>
|
||||||
|
);
|
||||||
|
|
||||||
|
return renderHook(() => usePluginVisibilityInSharedChannel(channelId), {wrapper});
|
||||||
|
};
|
||||||
|
|
||||||
|
test('should return true when channelId is undefined', () => {
|
||||||
|
const {result} = renderHookWithChannelId(undefined);
|
||||||
|
expect(result.current).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return true when channel is not found', () => {
|
||||||
|
mockGetChannel.mockReturnValue(undefined);
|
||||||
|
const {result} = renderHookWithChannelId('channel-id-1');
|
||||||
|
expect(result.current).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return true for non-shared channels regardless of feature flag', () => {
|
||||||
|
mockGetChannel.mockReturnValue({
|
||||||
|
id: 'channel-id-1',
|
||||||
|
shared: false,
|
||||||
|
} as any);
|
||||||
|
|
||||||
|
const {result} = renderHookWithChannelId('channel-id-1');
|
||||||
|
expect(result.current).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return false for shared channels when feature flag is disabled', () => {
|
||||||
|
mockGetChannel.mockReturnValue({
|
||||||
|
id: 'channel-id-1',
|
||||||
|
shared: true,
|
||||||
|
} as any);
|
||||||
|
|
||||||
|
const {result} = renderHookWithChannelId('channel-id-1');
|
||||||
|
expect(result.current).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return true for shared channels when feature flag is enabled', () => {
|
||||||
|
mockGetChannel.mockReturnValue({
|
||||||
|
id: 'channel-id-1',
|
||||||
|
shared: true,
|
||||||
|
} as any);
|
||||||
|
mockGetFeatureFlagValue.mockReturnValue('true');
|
||||||
|
|
||||||
|
const {result} = renderHookWithChannelId('channel-id-1');
|
||||||
|
expect(result.current).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return true for non-shared channels when feature flag is enabled', () => {
|
||||||
|
mockGetChannel.mockReturnValue({
|
||||||
|
id: 'channel-id-1',
|
||||||
|
shared: false,
|
||||||
|
} as any);
|
||||||
|
mockGetFeatureFlagValue.mockReturnValue('true');
|
||||||
|
|
||||||
|
const {result} = renderHookWithChannelId('channel-id-1');
|
||||||
|
expect(result.current).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle channels without shared property', () => {
|
||||||
|
mockGetChannel.mockReturnValue({
|
||||||
|
id: 'channel-id-1',
|
||||||
|
|
||||||
|
// no shared property - should default to false
|
||||||
|
} as any);
|
||||||
|
|
||||||
|
const {result} = renderHookWithChannelId('channel-id-1');
|
||||||
|
expect(result.current).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import {useSelector} from 'react-redux';
|
||||||
|
|
||||||
|
import {getChannel} from 'mattermost-redux/selectors/entities/channels';
|
||||||
|
import {getFeatureFlagValue} from 'mattermost-redux/selectors/entities/general';
|
||||||
|
|
||||||
|
import type {GlobalState} from 'types/store';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom hook to determine if plugin components should be visible in a channel.
|
||||||
|
*
|
||||||
|
* @param channelId - The ID of the channel to check (optional)
|
||||||
|
* @returns true if plugins should be visible, false otherwise
|
||||||
|
*
|
||||||
|
* Plugins are visible when:
|
||||||
|
* - The channel ID is undefined/null (defaults to visible), OR
|
||||||
|
* - The channel is not shared, OR
|
||||||
|
* - The channel is shared AND the EnableSharedChannelsPlugins feature flag is enabled
|
||||||
|
*/
|
||||||
|
export function usePluginVisibilityInSharedChannel(channelId: string | undefined): boolean {
|
||||||
|
const channel = useSelector((state: GlobalState) =>
|
||||||
|
(channelId ? getChannel(state, channelId) : undefined),
|
||||||
|
);
|
||||||
|
|
||||||
|
const sharedChannelsPluginsEnabled = useSelector((state: GlobalState) =>
|
||||||
|
getFeatureFlagValue(state, 'EnableSharedChannelsPlugins') === 'true',
|
||||||
|
);
|
||||||
|
|
||||||
|
// If no channel ID provided or channel not found, default to showing plugins
|
||||||
|
if (!channelId || !channel) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return !channel.shared || sharedChannelsPluginsEnabled;
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ import type {GlobalState} from '@mattermost/types/store';
|
|||||||
import type {DeepPartial} from '@mattermost/types/utilities';
|
import type {DeepPartial} from '@mattermost/types/utilities';
|
||||||
|
|
||||||
import {renderWithContext} from 'tests/react_testing_utils';
|
import {renderWithContext} from 'tests/react_testing_utils';
|
||||||
|
import {TestHelper} from 'utils/test_helper';
|
||||||
|
|
||||||
import FileAttachment from './file_attachment';
|
import FileAttachment from './file_attachment';
|
||||||
|
|
||||||
@@ -58,6 +59,7 @@ describe('FileAttachment', () => {
|
|||||||
enableSVGs: false,
|
enableSVGs: false,
|
||||||
enablePublicLink: false,
|
enablePublicLink: false,
|
||||||
pluginMenuItems: [],
|
pluginMenuItems: [],
|
||||||
|
currentChannel: TestHelper.getChannelMock(),
|
||||||
handleFileDropdownOpened: jest.fn(() => null),
|
handleFileDropdownOpened: jest.fn(() => null),
|
||||||
actions: {
|
actions: {
|
||||||
openModal: jest.fn(),
|
openModal: jest.fn(),
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import type {FileInfo} from '@mattermost/types/files';
|
|||||||
|
|
||||||
import {getFileThumbnailUrl, getFileUrl} from 'mattermost-redux/utils/file_utils';
|
import {getFileThumbnailUrl, getFileUrl} from 'mattermost-redux/utils/file_utils';
|
||||||
|
|
||||||
|
import {usePluginVisibilityInSharedChannel} from 'components/common/hooks/usePluginVisibilityInSharedChannel';
|
||||||
import GetPublicModal from 'components/get_public_link_modal';
|
import GetPublicModal from 'components/get_public_link_modal';
|
||||||
import Menu from 'components/widgets/menu/menu';
|
import Menu from 'components/widgets/menu/menu';
|
||||||
import MenuWrapper from 'components/widgets/menu/menu_wrapper';
|
import MenuWrapper from 'components/widgets/menu/menu_wrapper';
|
||||||
@@ -69,6 +70,8 @@ export default function FileAttachment(props: Props) {
|
|||||||
|
|
||||||
const buttonRef = useRef<HTMLButtonElement | null>(null);
|
const buttonRef = useRef<HTMLButtonElement | null>(null);
|
||||||
|
|
||||||
|
const pluginItemsVisible = usePluginVisibilityInSharedChannel(props.currentChannel?.id);
|
||||||
|
|
||||||
const handleImageLoaded = () => {
|
const handleImageLoaded = () => {
|
||||||
if (mounted.current) {
|
if (mounted.current) {
|
||||||
setLoaded(true);
|
setLoaded(true);
|
||||||
@@ -195,16 +198,19 @@ export default function FileAttachment(props: Props) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const pluginItems = pluginMenuItems?.filter((item) => item?.match(fileInfo)).map((item) => {
|
let pluginItems: JSX.Element[] = [];
|
||||||
return (
|
if (pluginItemsVisible) {
|
||||||
<Menu.ItemAction
|
pluginItems = pluginMenuItems?.filter((item) => item?.match(fileInfo)).map((item) => {
|
||||||
id={item.id + '_pluginmenuitem'}
|
return (
|
||||||
key={item.id + '_pluginmenuitem'}
|
<Menu.ItemAction
|
||||||
onClick={() => item?.action(fileInfo)}
|
id={item.id + '_pluginmenuitem'}
|
||||||
text={item.text}
|
key={item.id + '_pluginmenuitem'}
|
||||||
/>
|
onClick={() => item?.action(fileInfo)}
|
||||||
);
|
text={item.text}
|
||||||
});
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const isMenuVisible = defaultItems?.length || pluginItems?.length;
|
const isMenuVisible = defaultItems?.length || pluginItems?.length;
|
||||||
if (!isMenuVisible) {
|
if (!isMenuVisible) {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import type {ConnectedProps} from 'react-redux';
|
|||||||
import {bindActionCreators} from 'redux';
|
import {bindActionCreators} from 'redux';
|
||||||
import type {Dispatch} from 'redux';
|
import type {Dispatch} from 'redux';
|
||||||
|
|
||||||
|
import {getCurrentChannel} from 'mattermost-redux/selectors/entities/channels';
|
||||||
import {getConfig} from 'mattermost-redux/selectors/entities/general';
|
import {getConfig} from 'mattermost-redux/selectors/entities/general';
|
||||||
|
|
||||||
import {openModal} from 'actions/views/modals';
|
import {openModal} from 'actions/views/modals';
|
||||||
@@ -29,6 +30,7 @@ function mapStateToProps(state: GlobalState, ownProps: OwnProps) {
|
|||||||
enableSVGs: config.EnableSVGs === 'true',
|
enableSVGs: config.EnableSVGs === 'true',
|
||||||
enablePublicLink: config.EnablePublicLink === 'true',
|
enablePublicLink: config.EnablePublicLink === 'true',
|
||||||
pluginMenuItems: getFilesDropdownPluginMenuItems(state),
|
pluginMenuItems: getFilesDropdownPluginMenuItems(state),
|
||||||
|
currentChannel: getCurrentChannel(state),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ describe('components/file_search_result/FileSearchResultItem', () => {
|
|||||||
channelDisplayName: '',
|
channelDisplayName: '',
|
||||||
channelType: Constants.OPEN_CHANNEL as ChannelType,
|
channelType: Constants.OPEN_CHANNEL as ChannelType,
|
||||||
teamName: 'test-team-name',
|
teamName: 'test-team-name',
|
||||||
|
channel: TestHelper.getChannelMock(),
|
||||||
|
enableSharedChannelsPlugins: false,
|
||||||
onClick: jest.fn(),
|
onClick: jest.fn(),
|
||||||
actions: {
|
actions: {
|
||||||
openModal: jest.fn(),
|
openModal: jest.fn(),
|
||||||
|
|||||||
@@ -58,7 +58,13 @@ export default class FileSearchResultItem extends React.PureComponent<Props, Sta
|
|||||||
};
|
};
|
||||||
|
|
||||||
private renderPluginItems = () => {
|
private renderPluginItems = () => {
|
||||||
const {fileInfo} = this.props;
|
const {fileInfo, channel, enableSharedChannelsPlugins} = this.props;
|
||||||
|
const isSharedChannel = channel?.shared || false;
|
||||||
|
|
||||||
|
if (isSharedChannel && !enableSharedChannelsPlugins) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const pluginItems = this.props.pluginMenuItems?.filter((item) => item?.match(fileInfo)).map((item) => {
|
const pluginItems = this.props.pluginMenuItems?.filter((item) => item?.match(fileInfo)).map((item) => {
|
||||||
return (
|
return (
|
||||||
<Menu.ItemAction
|
<Menu.ItemAction
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import type {Dispatch} from 'redux';
|
|||||||
import type {FileInfo} from '@mattermost/types/files';
|
import type {FileInfo} from '@mattermost/types/files';
|
||||||
|
|
||||||
import {getChannel} from 'mattermost-redux/selectors/entities/channels';
|
import {getChannel} from 'mattermost-redux/selectors/entities/channels';
|
||||||
|
import {getFeatureFlagValue} from 'mattermost-redux/selectors/entities/general';
|
||||||
|
|
||||||
import {openModal} from 'actions/views/modals';
|
import {openModal} from 'actions/views/modals';
|
||||||
|
|
||||||
@@ -26,10 +27,13 @@ export type OwnProps = {
|
|||||||
|
|
||||||
function mapStateToProps(state: GlobalState, ownProps: OwnProps) {
|
function mapStateToProps(state: GlobalState, ownProps: OwnProps) {
|
||||||
const channel = getChannel(state, ownProps.channelId);
|
const channel = getChannel(state, ownProps.channelId);
|
||||||
|
const enableSharedChannelsPlugins = getFeatureFlagValue(state, 'EnableSharedChannelsPlugins') === 'true';
|
||||||
|
|
||||||
return {
|
return {
|
||||||
channelDisplayName: '',
|
channelDisplayName: '',
|
||||||
channelType: channel?.type,
|
channelType: channel?.type,
|
||||||
|
channel,
|
||||||
|
enableSharedChannelsPlugins,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -176,6 +176,7 @@ function makeMapStateToProps() {
|
|||||||
canReply,
|
canReply,
|
||||||
pluginPostTypes: state.plugins.postTypes,
|
pluginPostTypes: state.plugins.postTypes,
|
||||||
channelIsArchived: isArchivedChannel(channel),
|
channelIsArchived: isArchivedChannel(channel),
|
||||||
|
channelIsShared: channel?.shared,
|
||||||
isConsecutivePost: isConsecutivePost(state, ownProps),
|
isConsecutivePost: isConsecutivePost(state, ownProps),
|
||||||
previousPostIsComment,
|
previousPostIsComment,
|
||||||
isFlagged: isPostFlagged(state, post.id),
|
isFlagged: isPostFlagged(state, post.id),
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ export type Props = {
|
|||||||
isReadOnly?: boolean;
|
isReadOnly?: boolean;
|
||||||
pluginPostTypes?: {[postType: string]: PostPluginComponent};
|
pluginPostTypes?: {[postType: string]: PostPluginComponent};
|
||||||
channelIsArchived?: boolean;
|
channelIsArchived?: boolean;
|
||||||
|
channelIsShared?: boolean;
|
||||||
isConsecutivePost?: boolean;
|
isConsecutivePost?: boolean;
|
||||||
isLastPost?: boolean;
|
isLastPost?: boolean;
|
||||||
recentEmojis: Emoji[];
|
recentEmojis: Emoji[];
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {isPostEphemeral} from 'mattermost-redux/utils/post_utils';
|
|||||||
|
|
||||||
import ActionsMenu from 'components/actions_menu';
|
import ActionsMenu from 'components/actions_menu';
|
||||||
import CommentIcon from 'components/common/comment_icon';
|
import CommentIcon from 'components/common/comment_icon';
|
||||||
|
import {usePluginVisibilityInSharedChannel} from 'components/common/hooks/usePluginVisibilityInSharedChannel';
|
||||||
import DotMenu from 'components/dot_menu';
|
import DotMenu from 'components/dot_menu';
|
||||||
import PostFlagIcon from 'components/post_view/post_flag_icon';
|
import PostFlagIcon from 'components/post_view/post_flag_icon';
|
||||||
import PostReaction from 'components/post_view/post_reaction';
|
import PostReaction from 'components/post_view/post_reaction';
|
||||||
@@ -32,6 +33,7 @@ type Props = {
|
|||||||
enableEmojiPicker?: boolean;
|
enableEmojiPicker?: boolean;
|
||||||
isReadOnly?: boolean;
|
isReadOnly?: boolean;
|
||||||
channelIsArchived?: boolean;
|
channelIsArchived?: boolean;
|
||||||
|
channelIsShared?: boolean;
|
||||||
handleCommentClick?: (e: React.MouseEvent) => void;
|
handleCommentClick?: (e: React.MouseEvent) => void;
|
||||||
handleJumpClick?: (e: React.MouseEvent) => void;
|
handleJumpClick?: (e: React.MouseEvent) => void;
|
||||||
handleDropdownOpened?: (e: boolean) => void;
|
handleDropdownOpened?: (e: boolean) => void;
|
||||||
@@ -200,7 +202,9 @@ const PostOptions = (props: Props): JSX.Element => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
let pluginItems: ReactNode = null;
|
let pluginItems: ReactNode = null;
|
||||||
if ((!isEphemeral && !post.failed && !systemMessage) && hoverLocal) {
|
const pluginItemsVisible = usePluginVisibilityInSharedChannel(post.channel_id);
|
||||||
|
|
||||||
|
if ((!isEphemeral && !post.failed && !systemMessage) && hoverLocal && pluginItemsVisible) {
|
||||||
pluginItems = props.pluginActions?.
|
pluginItems = props.pluginActions?.
|
||||||
map((item) => {
|
map((item) => {
|
||||||
if (item.component) {
|
if (item.component) {
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import React from 'react';
|
|||||||
|
|
||||||
import type {Channel, ChannelMembership} from '@mattermost/types/channels';
|
import type {Channel, ChannelMembership} from '@mattermost/types/channels';
|
||||||
|
|
||||||
|
import {usePluginVisibilityInSharedChannel} from 'components/common/hooks/usePluginVisibilityInSharedChannel';
|
||||||
|
|
||||||
import type {ChannelIntroButtonAction} from 'types/store/plugins';
|
import type {ChannelIntroButtonAction} from 'types/store/plugins';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -19,6 +21,12 @@ const PluggableIntroButtons = React.memo(({
|
|||||||
channelMember,
|
channelMember,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const channelIsArchived = channel.delete_at !== 0;
|
const channelIsArchived = channel.delete_at !== 0;
|
||||||
|
const pluginItemsVisible = usePluginVisibilityInSharedChannel(channel.id);
|
||||||
|
|
||||||
|
if (!pluginItemsVisible) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
if (channelIsArchived || pluginButtons.length === 0 || !channelMember) {
|
if (channelIsArchived || pluginButtons.length === 0 || !channelMember) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
import {connect} from 'react-redux';
|
import {connect} from 'react-redux';
|
||||||
|
|
||||||
import {Preferences} from 'mattermost-redux/constants';
|
import {Preferences} from 'mattermost-redux/constants';
|
||||||
|
import {getFeatureFlagValue} from 'mattermost-redux/selectors/entities/general';
|
||||||
import {getTheme, getBool} from 'mattermost-redux/selectors/entities/preferences';
|
import {getTheme, getBool} from 'mattermost-redux/selectors/entities/preferences';
|
||||||
import {getCurrentRelativeTeamUrl} from 'mattermost-redux/selectors/entities/teams';
|
import {getCurrentRelativeTeamUrl} from 'mattermost-redux/selectors/entities/teams';
|
||||||
|
|
||||||
@@ -21,6 +22,7 @@ function mapStateToProps(state: GlobalState) {
|
|||||||
pluginPostTypes: state.plugins.postTypes,
|
pluginPostTypes: state.plugins.postTypes,
|
||||||
theme: getTheme(state),
|
theme: getTheme(state),
|
||||||
currentRelativeTeamUrl: getCurrentRelativeTeamUrl(state),
|
currentRelativeTeamUrl: getCurrentRelativeTeamUrl(state),
|
||||||
|
sharedChannelsPluginsEnabled: getFeatureFlagValue(state, 'EnableSharedChannelsPlugins') === 'true',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,9 +7,12 @@ import {FormattedMessage} from 'react-intl';
|
|||||||
import type {Post} from '@mattermost/types/posts';
|
import type {Post} from '@mattermost/types/posts';
|
||||||
|
|
||||||
import {Posts} from 'mattermost-redux/constants';
|
import {Posts} from 'mattermost-redux/constants';
|
||||||
|
import {getChannel} from 'mattermost-redux/selectors/entities/channels';
|
||||||
import type {Theme} from 'mattermost-redux/selectors/entities/preferences';
|
import type {Theme} from 'mattermost-redux/selectors/entities/preferences';
|
||||||
import {isPostEphemeral} from 'mattermost-redux/utils/post_utils';
|
import {isPostEphemeral} from 'mattermost-redux/utils/post_utils';
|
||||||
|
|
||||||
|
import store from 'stores/redux_store';
|
||||||
|
|
||||||
import PostMarkdown from 'components/post_markdown';
|
import PostMarkdown from 'components/post_markdown';
|
||||||
import ShowMore from 'components/post_view/show_more';
|
import ShowMore from 'components/post_view/show_more';
|
||||||
import type {AttachmentTextOverflowType} from 'components/post_view/show_more/show_more';
|
import type {AttachmentTextOverflowType} from 'components/post_view/show_more/show_more';
|
||||||
@@ -36,6 +39,7 @@ type Props = {
|
|||||||
overflowType?: AttachmentTextOverflowType;
|
overflowType?: AttachmentTextOverflowType;
|
||||||
maxHeight?: number; /* The max height used by the show more component */
|
maxHeight?: number; /* The max height used by the show more component */
|
||||||
showPostEditedIndicator?: boolean; /* Whether or not to render the post edited indicator */
|
showPostEditedIndicator?: boolean; /* Whether or not to render the post edited indicator */
|
||||||
|
sharedChannelsPluginsEnabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
type State = {
|
type State = {
|
||||||
@@ -142,6 +146,10 @@ export default class PostMessageView extends React.PureComponent<Props, State> {
|
|||||||
|
|
||||||
const id = isRHS ? `rhsPostMessageText_${post.id}` : `postMessageText_${post.id}`;
|
const id = isRHS ? `rhsPostMessageText_${post.id}` : `postMessageText_${post.id}`;
|
||||||
|
|
||||||
|
// Check if channel is shared
|
||||||
|
const channel = getChannel(store.getState(), post.channel_id);
|
||||||
|
const isSharedChannel = channel?.shared || false;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ShowMore
|
<ShowMore
|
||||||
checkOverflow={this.state.checkOverflow}
|
checkOverflow={this.state.checkOverflow}
|
||||||
@@ -164,11 +172,13 @@ export default class PostMessageView extends React.PureComponent<Props, State> {
|
|||||||
showPostEditedIndicator={this.props.showPostEditedIndicator}
|
showPostEditedIndicator={this.props.showPostEditedIndicator}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Pluggable
|
{(!isSharedChannel || this.props.sharedChannelsPluginsEnabled) && (
|
||||||
pluggableName='PostMessageAttachment'
|
<Pluggable
|
||||||
postId={post.id}
|
pluggableName='PostMessageAttachment'
|
||||||
onHeightChange={this.handleHeightReceived}
|
postId={post.id}
|
||||||
/>
|
onHeightChange={this.handleHeightReceived}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</ShowMore>
|
</ShowMore>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ import {getMembershipForEntities} from 'actions/views/profile_popover';
|
|||||||
import {getSelectedPost} from 'selectors/rhs';
|
import {getSelectedPost} from 'selectors/rhs';
|
||||||
import {getIsMobileView} from 'selectors/views/browser';
|
import {getIsMobileView} from 'selectors/views/browser';
|
||||||
|
|
||||||
|
import {usePluginVisibilityInSharedChannel} from 'components/common/hooks/usePluginVisibilityInSharedChannel';
|
||||||
|
|
||||||
import Pluggable from 'plugins/pluggable';
|
import Pluggable from 'plugins/pluggable';
|
||||||
import {getHistory} from 'utils/browser_history';
|
import {getHistory} from 'utils/browser_history';
|
||||||
import {A11yCustomEventTypes, UserStatuses} from 'utils/constants';
|
import {A11yCustomEventTypes, UserStatuses} from 'utils/constants';
|
||||||
@@ -73,6 +75,7 @@ const ProfilePopover = ({
|
|||||||
const user = useSelector((state: GlobalState) => getUser(state, userId));
|
const user = useSelector((state: GlobalState) => getUser(state, userId));
|
||||||
const currentTeamId = useSelector((state: GlobalState) => getCurrentTeamId(state));
|
const currentTeamId = useSelector((state: GlobalState) => getCurrentTeamId(state));
|
||||||
const channelId = useSelector((state: GlobalState) => (channelIdProp || getDefaultChannelId(state)));
|
const channelId = useSelector((state: GlobalState) => (channelIdProp || getDefaultChannelId(state)));
|
||||||
|
const pluginItemsVisible = usePluginVisibilityInSharedChannel(channelId);
|
||||||
const isMobileView = useSelector(getIsMobileView);
|
const isMobileView = useSelector(getIsMobileView);
|
||||||
const teamUrl = useSelector(getCurrentRelativeTeamUrl);
|
const teamUrl = useSelector(getCurrentRelativeTeamUrl);
|
||||||
const modals = useSelector((state: GlobalState) => state.views.modals);
|
const modals = useSelector((state: GlobalState) => state.views.modals);
|
||||||
@@ -182,15 +185,17 @@ const ProfilePopover = ({
|
|||||||
haveOverrideProp={haveOverrideProp}
|
haveOverrideProp={haveOverrideProp}
|
||||||
isBot={user.is_bot}
|
isBot={user.is_bot}
|
||||||
/>
|
/>
|
||||||
<div className='user-profile-popover-pluggables'>
|
{pluginItemsVisible && (
|
||||||
<Pluggable
|
<div className='user-profile-popover-pluggables'>
|
||||||
pluggableName={PLUGGABLE_COMPONENT_NAME_PROFILE_POPOVER}
|
<Pluggable
|
||||||
user={user}
|
pluggableName={PLUGGABLE_COMPONENT_NAME_PROFILE_POPOVER}
|
||||||
hide={hide}
|
user={user}
|
||||||
status={hideStatus ? null : status}
|
hide={hide}
|
||||||
fromWebhook={fromWebhook}
|
status={hideStatus ? null : status}
|
||||||
/>
|
fromWebhook={fromWebhook}
|
||||||
</div>
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{enableCustomProfileAttributes && !user.is_bot && (
|
{enableCustomProfileAttributes && !user.is_bot && (
|
||||||
<ProfilePopoverCustomAttributes
|
<ProfilePopoverCustomAttributes
|
||||||
@@ -238,12 +243,14 @@ const ProfilePopover = ({
|
|||||||
user={user}
|
user={user}
|
||||||
hide={hide}
|
hide={hide}
|
||||||
/>
|
/>
|
||||||
<Pluggable
|
{pluginItemsVisible && (
|
||||||
pluggableName='PopoverUserActions'
|
<Pluggable
|
||||||
user={user}
|
pluggableName='PopoverUserActions'
|
||||||
hide={hide}
|
user={user}
|
||||||
status={hideStatus ? null : status}
|
hide={hide}
|
||||||
/>
|
status={hideStatus ? null : status}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ export type Options = Partial<{
|
|||||||
images: boolean;
|
images: boolean;
|
||||||
atPlanMentions: boolean;
|
atPlanMentions: boolean;
|
||||||
channelId: string;
|
channelId: string;
|
||||||
|
channelIsShared: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether or not the AtMention component should attempt to fetch at-mentioned users if none can be found for
|
* Whether or not the AtMention component should attempt to fetch at-mentioned users if none can be found for
|
||||||
@@ -267,6 +268,7 @@ export default function messageHtmlToComponent(html: string, options: Options =
|
|||||||
code={node.attribs['data-codeblock-code']}
|
code={node.attribs['data-codeblock-code']}
|
||||||
language={node.attribs['data-codeblock-language']}
|
language={node.attribs['data-codeblock-language']}
|
||||||
searchedContent={node.attribs['data-codeblock-searchedcontent']}
|
searchedContent={node.attribs['data-codeblock-searchedcontent']}
|
||||||
|
channelId={options.channelId}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user