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>
Этот коммит содержится в:
Miguel de la Cruz
2025-06-02 14:59:19 +02:00
коммит произвёл GitHub
родитель 58d6c71ed2
Коммит c6c27e7752
27 изменённых файлов: 311 добавлений и 122 удалений

Просмотреть файл

@@ -22,6 +22,9 @@ type FeatureFlags struct {
// Enable DMs and GMs for shared channels.
EnableSharedChannelsDMs bool
// Enable plugins in shared channels.
EnableSharedChannelsPlugins bool
// AppsEnabled toggles the Apps framework functionalities both in server and client side
AppsEnabled bool
@@ -66,6 +69,7 @@ func (f *FeatureFlags) SetDefaults() {
f.TestBoolFeature = false
f.EnableRemoteClusterService = false
f.EnableSharedChannelsDMs = false
f.EnableSharedChannelsPlugins = true
f.AppsEnabled = false
f.NormalizeLdapDNs = false
f.DeprecateCloudFree = false

Просмотреть файл

@@ -298,7 +298,7 @@ export class ActionMenuClass extends React.PureComponent<Props, State> {
}}
/>
);
}) || [];
});
let appBindings = [] as JSX.Element[];
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 {AppBindingLocations} from 'mattermost-redux/constants/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 {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
import {getCurrentUser} from 'mattermost-redux/selectors/entities/users';
@@ -47,6 +48,8 @@ function mapStateToProps(state: GlobalState, ownProps: Props) {
const {post} = ownProps;
const systemMessage = isSystemMessage(post);
const channel = getChannel(state, post.channel_id);
const sharedChannelsPluginsEnabled = getFeatureFlagValue(state, 'EnableSharedChannelsPlugins') === 'true';
const apps = appsEnabled(state);
const showBindings = apps && !systemMessage && !isCombinedUserActivityPost(post.id);
@@ -57,12 +60,14 @@ function mapStateToProps(state: GlobalState, ownProps: Props) {
const currentUser = getCurrentUser(state);
const isSysAdmin = isSystemAdmin(currentUser.roles);
const pluginItemsVisible = !channel?.shared || sharedChannelsPluginsEnabled;
return {
appBindings,
appsEnabled: apps,
pluginMenuItemComponents: state.plugins.components.PostDropdownMenuItem,
pluginMenuItemComponents: pluginItemsVisible ? state.plugins.components.PostDropdownMenuItem : [],
isSysAdmin,
pluginMenuItems: state.plugins.components.PostDropdownMenu,
pluginMenuItems: pluginItemsVisible ? state.plugins.components.PostDropdownMenu : [],
teamId: getCurrentTeamId(state),
isMobileView: getIsMobileView(state),
canOpenMarketplace: (

Просмотреть файл

@@ -307,7 +307,7 @@ const AdvancedTextEditor = ({
}, [dispatch, currentUserId, getFormattingBarPreferenceName, isFormattingBarHidden]);
useOrientationHandler(textboxRef, rootId);
const pluginItems = usePluginItems(draft, textboxRef, handleDraftChange);
const pluginItems = usePluginItems(draft, textboxRef, handleDraftChange, channelId);
const focusTextbox = useTextboxFocus(textboxRef, channelId, isRHS, canPost);
const [attachmentPreview, fileUploadJSX] = useUploadFiles(
draft,

Просмотреть файл

@@ -4,6 +4,7 @@
import React, {useCallback, useMemo} from 'react';
import {useSelector} from 'react-redux';
import {usePluginVisibilityInSharedChannel} from 'components/common/hooks/usePluginVisibilityInSharedChannel';
import type TextboxClass from 'components/textbox/textbox';
import type {GlobalState} from 'types/store';
@@ -13,8 +14,10 @@ const usePluginItems = (
draft: PostDraft,
textboxRef: React.RefObject<TextboxClass>,
handleDraftChange: (draft: PostDraft) => void,
channelId?: string,
) => {
const postEditorActions = useSelector((state: GlobalState) => state.plugins.components.PostEditorAction);
const pluginItemsVisible = usePluginVisibilityInSharedChannel(channelId);
const getSelectedText = useCallback(() => {
const input = textboxRef.current?.getInputBox();
@@ -34,21 +37,27 @@ const usePluginItems = (
// Missing setting the state eventually?
}, [handleDraftChange, draft]);
const items = useMemo(() => postEditorActions?.map((item) => {
if (!item.component) {
return null;
const items = useMemo(() => {
if (!pluginItemsVisible) {
return [];
}
const Component = item.component as any;
return (
<Component
key={item.id}
draft={draft}
getSelectedText={getSelectedText}
updateText={updateText}
/>
);
}), [postEditorActions, draft, getSelectedText, updateText]);
return postEditorActions?.map((item) => {
if (!item.component) {
return null;
}
const Component = item.component as any;
return (
<Component
key={item.id}
draft={draft}
getSelectedText={getSelectedText}
updateText={updateText}
/>
);
});
}, [postEditorActions, draft, getSelectedText, updateText, pluginItemsVisible]);
return items;
};

Просмотреть файл

@@ -2700,53 +2700,6 @@ exports[`components/ChannelHeader should render shared view 1`] = `
</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
channel={
Object {

Просмотреть файл

@@ -49,6 +49,7 @@ describe('components/ChannelHeader', () => {
'hour',
],
hideGuestTags: false,
sharedChannelsPluginsEnabled: false,
intl: {
formatMessage: jest.fn(({id, defaultMessage}) => defaultMessage || id),
} as MockIntl,

Просмотреть файл

@@ -370,11 +370,15 @@ class ChannelHeader extends React.PureComponent<Props> {
</div>
</div>
</div>
<ChannelHeaderPlug
channel={channel}
channelMember={channelMember}
/>
<CallButton/>
{(!channel.shared || this.props.sharedChannelsPluginsEnabled) && (
<>
<ChannelHeaderPlug
channel={channel}
channelMember={channelMember}
/>
<CallButton/>
</>
)}
<ChannelInfoButton channel={channel}/>
</div>
</div>

Просмотреть файл

@@ -18,7 +18,7 @@ import {
isCurrentChannelMuted,
getCurrentChannelStats,
} 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 {
displayLastActiveLabel,
@@ -54,6 +54,7 @@ function makeMapStateToProps() {
const channel = getCurrentChannel(state);
const user = getCurrentUser(state);
const config = getConfig(state);
const sharedChannelsPluginsEnabled = getFeatureFlagValue(state, 'EnableSharedChannelsPlugins') === 'true';
let dmUser;
let gmMembers;
@@ -96,6 +97,7 @@ function makeMapStateToProps() {
isLastActiveEnabled,
timestampUnits,
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 ChannelHeaderTitleGroup from '../channel_header/channel_header_title_group';
import {usePluginVisibilityInSharedChannel} from '../common/hooks/usePluginVisibilityInSharedChannel';
type Props = {
dmUser?: UserProfile;
@@ -55,6 +56,7 @@ export default function ChannelHeaderMenu({dmUser, gmMembers, isMobile, archived
const isLicensedForLDAPGroups = useSelector(getLicense).LDAPGroups === 'true';
const pluginMenuItems = useSelector(getChannelHeaderMenuPluginComponents);
const isChannelBookmarksEnabled = useSelector(getIsChannelBookmarksEnabled);
const pluginItemsVisible = usePluginVisibilityInSharedChannel(channel?.id);
const isReadonly = false;
@@ -86,22 +88,26 @@ export default function ChannelHeaderMenu({dmUser, gmMembers, isMobile, archived
channelTitle = <ChannelHeaderTitleGroup gmMembers={gmMembers}/>;
}
const pluginItems = pluginMenuItems.map((item) => {
const handlePluginItemClick = () => {
if (item.action) {
item.action(channel.id);
}
};
let pluginItems: JSX.Element[] = [];
return (
<Menu.Item
id={item.id + '_pluginmenuitem'}
key={item.id + '_pluginmenuitem'}
onClick={handlePluginItemClick}
labels={<span>{item.text}</span>}
/>
);
});
if (pluginItemsVisible) {
pluginItems = pluginMenuItems.map((item) => {
const handlePluginItemClick = () => {
if (item.action) {
item.action(channel.id);
}
};
return (
<Menu.Item
id={item.id + '_pluginmenuitem'}
key={item.id + '_pluginmenuitem'}
onClick={handlePluginItemClick}
labels={<span>{item.text}</span>}
/>
);
});
}
return (
<Menu.Container
@@ -183,4 +189,3 @@ export default function ChannelHeaderMenu({dmUser, gmMembers, isMobile, archived
</Menu.Container>
);
}

Просмотреть файл

@@ -4,6 +4,7 @@
import React, {useCallback, useEffect, useState} from 'react';
import {useSelector} from 'react-redux';
import {usePluginVisibilityInSharedChannel} from 'components/common/hooks/usePluginVisibilityInSharedChannel';
import CopyButton from 'components/copy_button';
import * as SyntaxHighlighting from 'utils/syntax_highlighting';
@@ -15,9 +16,10 @@ type Props = {
code: string;
language: 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(() => {
let usedLanguage = language || '';
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 pluginItems = codeBlockActions?.
const pluginItemsVisible = usePluginVisibilityInSharedChannel(channelId);
const pluginItems = pluginItemsVisible ? codeBlockActions?.
map((item) => {
if (!item.component) {
return null;
@@ -95,7 +99,7 @@ const CodeBlock: React.FC<Props> = ({code, language, searchedContent}: Props) =>
code={code}
/>
);
});
}) : [];
return (
<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 {renderWithContext} from 'tests/react_testing_utils';
import {TestHelper} from 'utils/test_helper';
import FileAttachment from './file_attachment';
@@ -58,6 +59,7 @@ describe('FileAttachment', () => {
enableSVGs: false,
enablePublicLink: false,
pluginMenuItems: [],
currentChannel: TestHelper.getChannelMock(),
handleFileDropdownOpened: jest.fn(() => null),
actions: {
openModal: jest.fn(),

Просмотреть файл

@@ -10,6 +10,7 @@ import type {FileInfo} from '@mattermost/types/files';
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 Menu from 'components/widgets/menu/menu';
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 pluginItemsVisible = usePluginVisibilityInSharedChannel(props.currentChannel?.id);
const handleImageLoaded = () => {
if (mounted.current) {
setLoaded(true);
@@ -195,16 +198,19 @@ export default function FileAttachment(props: Props) {
);
}
const pluginItems = pluginMenuItems?.filter((item) => item?.match(fileInfo)).map((item) => {
return (
<Menu.ItemAction
id={item.id + '_pluginmenuitem'}
key={item.id + '_pluginmenuitem'}
onClick={() => item?.action(fileInfo)}
text={item.text}
/>
);
});
let pluginItems: JSX.Element[] = [];
if (pluginItemsVisible) {
pluginItems = pluginMenuItems?.filter((item) => item?.match(fileInfo)).map((item) => {
return (
<Menu.ItemAction
id={item.id + '_pluginmenuitem'}
key={item.id + '_pluginmenuitem'}
onClick={() => item?.action(fileInfo)}
text={item.text}
/>
);
});
}
const isMenuVisible = defaultItems?.length || pluginItems?.length;
if (!isMenuVisible) {

Просмотреть файл

@@ -6,6 +6,7 @@ import type {ConnectedProps} from 'react-redux';
import {bindActionCreators} from 'redux';
import type {Dispatch} from 'redux';
import {getCurrentChannel} from 'mattermost-redux/selectors/entities/channels';
import {getConfig} from 'mattermost-redux/selectors/entities/general';
import {openModal} from 'actions/views/modals';
@@ -29,6 +30,7 @@ function mapStateToProps(state: GlobalState, ownProps: OwnProps) {
enableSVGs: config.EnableSVGs === 'true',
enablePublicLink: config.EnablePublicLink === 'true',
pluginMenuItems: getFilesDropdownPluginMenuItems(state),
currentChannel: getCurrentChannel(state),
};
}

Просмотреть файл

@@ -19,6 +19,8 @@ describe('components/file_search_result/FileSearchResultItem', () => {
channelDisplayName: '',
channelType: Constants.OPEN_CHANNEL as ChannelType,
teamName: 'test-team-name',
channel: TestHelper.getChannelMock(),
enableSharedChannelsPlugins: false,
onClick: jest.fn(),
actions: {
openModal: jest.fn(),

Просмотреть файл

@@ -58,7 +58,13 @@ export default class FileSearchResultItem extends React.PureComponent<Props, Sta
};
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) => {
return (
<Menu.ItemAction

Просмотреть файл

@@ -9,6 +9,7 @@ import type {Dispatch} from 'redux';
import type {FileInfo} from '@mattermost/types/files';
import {getChannel} from 'mattermost-redux/selectors/entities/channels';
import {getFeatureFlagValue} from 'mattermost-redux/selectors/entities/general';
import {openModal} from 'actions/views/modals';
@@ -26,10 +27,13 @@ export type OwnProps = {
function mapStateToProps(state: GlobalState, ownProps: OwnProps) {
const channel = getChannel(state, ownProps.channelId);
const enableSharedChannelsPlugins = getFeatureFlagValue(state, 'EnableSharedChannelsPlugins') === 'true';
return {
channelDisplayName: '',
channelType: channel?.type,
channel,
enableSharedChannelsPlugins,
};
}

Просмотреть файл

@@ -176,6 +176,7 @@ function makeMapStateToProps() {
canReply,
pluginPostTypes: state.plugins.postTypes,
channelIsArchived: isArchivedChannel(channel),
channelIsShared: channel?.shared,
isConsecutivePost: isConsecutivePost(state, ownProps),
previousPostIsComment,
isFlagged: isPostFlagged(state, post.id),

Просмотреть файл

@@ -69,6 +69,7 @@ export type Props = {
isReadOnly?: boolean;
pluginPostTypes?: {[postType: string]: PostPluginComponent};
channelIsArchived?: boolean;
channelIsShared?: boolean;
isConsecutivePost?: boolean;
isLastPost?: boolean;
recentEmojis: Emoji[];

Просмотреть файл

@@ -14,6 +14,7 @@ import {isPostEphemeral} from 'mattermost-redux/utils/post_utils';
import ActionsMenu from 'components/actions_menu';
import CommentIcon from 'components/common/comment_icon';
import {usePluginVisibilityInSharedChannel} from 'components/common/hooks/usePluginVisibilityInSharedChannel';
import DotMenu from 'components/dot_menu';
import PostFlagIcon from 'components/post_view/post_flag_icon';
import PostReaction from 'components/post_view/post_reaction';
@@ -32,6 +33,7 @@ type Props = {
enableEmojiPicker?: boolean;
isReadOnly?: boolean;
channelIsArchived?: boolean;
channelIsShared?: boolean;
handleCommentClick?: (e: React.MouseEvent) => void;
handleJumpClick?: (e: React.MouseEvent) => void;
handleDropdownOpened?: (e: boolean) => void;
@@ -200,7 +202,9 @@ const PostOptions = (props: Props): JSX.Element => {
);
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?.
map((item) => {
if (item.component) {

Просмотреть файл

@@ -5,6 +5,8 @@ import React from 'react';
import type {Channel, ChannelMembership} from '@mattermost/types/channels';
import {usePluginVisibilityInSharedChannel} from 'components/common/hooks/usePluginVisibilityInSharedChannel';
import type {ChannelIntroButtonAction} from 'types/store/plugins';
type Props = {
@@ -19,6 +21,12 @@ const PluggableIntroButtons = React.memo(({
channelMember,
}: Props) => {
const channelIsArchived = channel.delete_at !== 0;
const pluginItemsVisible = usePluginVisibilityInSharedChannel(channel.id);
if (!pluginItemsVisible) {
return null;
}
if (channelIsArchived || pluginButtons.length === 0 || !channelMember) {
return null;
}

Просмотреть файл

@@ -4,6 +4,7 @@
import {connect} from 'react-redux';
import {Preferences} from 'mattermost-redux/constants';
import {getFeatureFlagValue} from 'mattermost-redux/selectors/entities/general';
import {getTheme, getBool} from 'mattermost-redux/selectors/entities/preferences';
import {getCurrentRelativeTeamUrl} from 'mattermost-redux/selectors/entities/teams';
@@ -21,6 +22,7 @@ function mapStateToProps(state: GlobalState) {
pluginPostTypes: state.plugins.postTypes,
theme: getTheme(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 {Posts} from 'mattermost-redux/constants';
import {getChannel} from 'mattermost-redux/selectors/entities/channels';
import type {Theme} from 'mattermost-redux/selectors/entities/preferences';
import {isPostEphemeral} from 'mattermost-redux/utils/post_utils';
import store from 'stores/redux_store';
import PostMarkdown from 'components/post_markdown';
import ShowMore from 'components/post_view/show_more';
import type {AttachmentTextOverflowType} from 'components/post_view/show_more/show_more';
@@ -36,6 +39,7 @@ type Props = {
overflowType?: AttachmentTextOverflowType;
maxHeight?: number; /* The max height used by the show more component */
showPostEditedIndicator?: boolean; /* Whether or not to render the post edited indicator */
sharedChannelsPluginsEnabled?: boolean;
}
type State = {
@@ -142,6 +146,10 @@ export default class PostMessageView extends React.PureComponent<Props, State> {
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 (
<ShowMore
checkOverflow={this.state.checkOverflow}
@@ -164,11 +172,13 @@ export default class PostMessageView extends React.PureComponent<Props, State> {
showPostEditedIndicator={this.props.showPostEditedIndicator}
/>
</div>
<Pluggable
pluggableName='PostMessageAttachment'
postId={post.id}
onHeightChange={this.handleHeightReceived}
/>
{(!isSharedChannel || this.props.sharedChannelsPluginsEnabled) && (
<Pluggable
pluggableName='PostMessageAttachment'
postId={post.id}
onHeightChange={this.handleHeightReceived}
/>
)}
</ShowMore>
);
}

Просмотреть файл

@@ -17,6 +17,8 @@ import {getMembershipForEntities} from 'actions/views/profile_popover';
import {getSelectedPost} from 'selectors/rhs';
import {getIsMobileView} from 'selectors/views/browser';
import {usePluginVisibilityInSharedChannel} from 'components/common/hooks/usePluginVisibilityInSharedChannel';
import Pluggable from 'plugins/pluggable';
import {getHistory} from 'utils/browser_history';
import {A11yCustomEventTypes, UserStatuses} from 'utils/constants';
@@ -73,6 +75,7 @@ const ProfilePopover = ({
const user = useSelector((state: GlobalState) => getUser(state, userId));
const currentTeamId = useSelector((state: GlobalState) => getCurrentTeamId(state));
const channelId = useSelector((state: GlobalState) => (channelIdProp || getDefaultChannelId(state)));
const pluginItemsVisible = usePluginVisibilityInSharedChannel(channelId);
const isMobileView = useSelector(getIsMobileView);
const teamUrl = useSelector(getCurrentRelativeTeamUrl);
const modals = useSelector((state: GlobalState) => state.views.modals);
@@ -182,15 +185,17 @@ const ProfilePopover = ({
haveOverrideProp={haveOverrideProp}
isBot={user.is_bot}
/>
<div className='user-profile-popover-pluggables'>
<Pluggable
pluggableName={PLUGGABLE_COMPONENT_NAME_PROFILE_POPOVER}
user={user}
hide={hide}
status={hideStatus ? null : status}
fromWebhook={fromWebhook}
/>
</div>
{pluginItemsVisible && (
<div className='user-profile-popover-pluggables'>
<Pluggable
pluggableName={PLUGGABLE_COMPONENT_NAME_PROFILE_POPOVER}
user={user}
hide={hide}
status={hideStatus ? null : status}
fromWebhook={fromWebhook}
/>
</div>
)}
{enableCustomProfileAttributes && !user.is_bot && (
<ProfilePopoverCustomAttributes
@@ -238,12 +243,14 @@ const ProfilePopover = ({
user={user}
hide={hide}
/>
<Pluggable
pluggableName='PopoverUserActions'
user={user}
hide={hide}
status={hideStatus ? null : status}
/>
{pluginItemsVisible && (
<Pluggable
pluggableName='PopoverUserActions'
user={user}
hide={hide}
status={hideStatus ? null : status}
/>
)}
</div>
</div>
);

Просмотреть файл

@@ -36,6 +36,7 @@ export type Options = Partial<{
images: boolean;
atPlanMentions: boolean;
channelId: string;
channelIsShared: boolean;
/**
* 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']}
language={node.attribs['data-codeblock-language']}
searchedContent={node.attribs['data-codeblock-searchedcontent']}
channelId={options.channelId}
/>
);
},