diff --git a/server/public/model/feature_flags.go b/server/public/model/feature_flags.go index d66eda77ab..5fe97ec90c 100644 --- a/server/public/model/feature_flags.go +++ b/server/public/model/feature_flags.go @@ -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 diff --git a/webapp/channels/src/components/actions_menu/actions_menu.tsx b/webapp/channels/src/components/actions_menu/actions_menu.tsx index 13cb46f60a..1ca20cbd89 100644 --- a/webapp/channels/src/components/actions_menu/actions_menu.tsx +++ b/webapp/channels/src/components/actions_menu/actions_menu.tsx @@ -298,7 +298,7 @@ export class ActionMenuClass extends React.PureComponent { }} /> ); - }) || []; + }); let appBindings = [] as JSX.Element[]; if (this.props.appsEnabled && this.state.appBindings) { diff --git a/webapp/channels/src/components/actions_menu/index.ts b/webapp/channels/src/components/actions_menu/index.ts index d89685a056..7d58842923 100644 --- a/webapp/channels/src/components/actions_menu/index.ts +++ b/webapp/channels/src/components/actions_menu/index.ts @@ -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: ( diff --git a/webapp/channels/src/components/advanced_text_editor/advanced_text_editor.tsx b/webapp/channels/src/components/advanced_text_editor/advanced_text_editor.tsx index fc6d968a85..4bcfdd51d4 100644 --- a/webapp/channels/src/components/advanced_text_editor/advanced_text_editor.tsx +++ b/webapp/channels/src/components/advanced_text_editor/advanced_text_editor.tsx @@ -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, diff --git a/webapp/channels/src/components/advanced_text_editor/use_plugin_items.tsx b/webapp/channels/src/components/advanced_text_editor/use_plugin_items.tsx index c91d0ae265..1910791eb3 100644 --- a/webapp/channels/src/components/advanced_text_editor/use_plugin_items.tsx +++ b/webapp/channels/src/components/advanced_text_editor/use_plugin_items.tsx @@ -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, 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 ( - - ); - }), [postEditorActions, draft, getSelectedText, updateText]); + return postEditorActions?.map((item) => { + if (!item.component) { + return null; + } + + const Component = item.component as any; + return ( + + ); + }); + }, [postEditorActions, draft, getSelectedText, updateText, pluginItemsVisible]); return items; }; diff --git a/webapp/channels/src/components/channel_header/__snapshots__/channel_header.test.tsx.snap b/webapp/channels/src/components/channel_header/__snapshots__/channel_header.test.tsx.snap index b8974259ce..95efefab30 100644 --- a/webapp/channels/src/components/channel_header/__snapshots__/channel_header.test.tsx.snap +++ b/webapp/channels/src/components/channel_header/__snapshots__/channel_header.test.tsx.snap @@ -2700,53 +2700,6 @@ exports[`components/ChannelHeader should render shared view 1`] = ` - - { 'hour', ], hideGuestTags: false, + sharedChannelsPluginsEnabled: false, intl: { formatMessage: jest.fn(({id, defaultMessage}) => defaultMessage || id), } as MockIntl, diff --git a/webapp/channels/src/components/channel_header/channel_header.tsx b/webapp/channels/src/components/channel_header/channel_header.tsx index 136e094448..4637df98ef 100644 --- a/webapp/channels/src/components/channel_header/channel_header.tsx +++ b/webapp/channels/src/components/channel_header/channel_header.tsx @@ -370,11 +370,15 @@ class ChannelHeader extends React.PureComponent { - - + {(!channel.shared || this.props.sharedChannelsPluginsEnabled) && ( + <> + + + + )} diff --git a/webapp/channels/src/components/channel_header/index.ts b/webapp/channels/src/components/channel_header/index.ts index 9fdace4ecf..9e8ff2425a 100644 --- a/webapp/channels/src/components/channel_header/index.ts +++ b/webapp/channels/src/components/channel_header/index.ts @@ -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, }; }; } diff --git a/webapp/channels/src/components/channel_header_menu/channel_header_menu.tsx b/webapp/channels/src/components/channel_header_menu/channel_header_menu.tsx index ac795d1ec9..f76b082fef 100644 --- a/webapp/channels/src/components/channel_header_menu/channel_header_menu.tsx +++ b/webapp/channels/src/components/channel_header_menu/channel_header_menu.tsx @@ -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 = ; } - const pluginItems = pluginMenuItems.map((item) => { - const handlePluginItemClick = () => { - if (item.action) { - item.action(channel.id); - } - }; + let pluginItems: JSX.Element[] = []; - return ( - {item.text}} - /> - ); - }); + if (pluginItemsVisible) { + pluginItems = pluginMenuItems.map((item) => { + const handlePluginItemClick = () => { + if (item.action) { + item.action(channel.id); + } + }; + + return ( + {item.text}} + /> + ); + }); + } return ( ); } - diff --git a/webapp/channels/src/components/code_block/code_block.tsx b/webapp/channels/src/components/code_block/code_block.tsx index 73443f2428..185bfcd0c5 100644 --- a/webapp/channels/src/components/code_block/code_block.tsx +++ b/webapp/channels/src/components/code_block/code_block.tsx @@ -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 = ({code, language, searchedContent}: Props) => { +const CodeBlock: React.FC = ({code, language, searchedContent, channelId}: Props) => { const getUsedLanguage = useCallback(() => { let usedLanguage = language || ''; usedLanguage = usedLanguage.toLowerCase(); @@ -82,7 +84,9 @@ const CodeBlock: React.FC = ({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 = ({code, language, searchedContent}: Props) => code={code} /> ); - }); + }) : []; return (
diff --git a/webapp/channels/src/components/common/hooks/usePluginVisibilityInSharedChannel.test.tsx b/webapp/channels/src/components/common/hooks/usePluginVisibilityInSharedChannel.test.tsx new file mode 100644 index 0000000000..f08154eb5e --- /dev/null +++ b/webapp/channels/src/components/common/hooks/usePluginVisibilityInSharedChannel.test.tsx @@ -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; + const mockGetFeatureFlagValue = getFeatureFlagValue as jest.MockedFunction; + + beforeEach(() => { + mockGetFeatureFlagValue.mockReturnValue('false'); + mockGetChannel.mockReturnValue(undefined); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + const renderHookWithChannelId = (channelId: string | undefined) => { + const store = mockStore({}); + const wrapper = ({children}: {children: React.ReactNode}) => ( + {children} + ); + + 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); + }); +}); diff --git a/webapp/channels/src/components/common/hooks/usePluginVisibilityInSharedChannel.ts b/webapp/channels/src/components/common/hooks/usePluginVisibilityInSharedChannel.ts new file mode 100644 index 0000000000..b135ab3e01 --- /dev/null +++ b/webapp/channels/src/components/common/hooks/usePluginVisibilityInSharedChannel.ts @@ -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; +} diff --git a/webapp/channels/src/components/file_attachment/file_attachment.test.tsx b/webapp/channels/src/components/file_attachment/file_attachment.test.tsx index eecae09f26..d706013e4e 100644 --- a/webapp/channels/src/components/file_attachment/file_attachment.test.tsx +++ b/webapp/channels/src/components/file_attachment/file_attachment.test.tsx @@ -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(), diff --git a/webapp/channels/src/components/file_attachment/file_attachment.tsx b/webapp/channels/src/components/file_attachment/file_attachment.tsx index 21a58fa63e..b0e43f6754 100644 --- a/webapp/channels/src/components/file_attachment/file_attachment.tsx +++ b/webapp/channels/src/components/file_attachment/file_attachment.tsx @@ -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(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 ( - item?.action(fileInfo)} - text={item.text} - /> - ); - }); + let pluginItems: JSX.Element[] = []; + if (pluginItemsVisible) { + pluginItems = pluginMenuItems?.filter((item) => item?.match(fileInfo)).map((item) => { + return ( + item?.action(fileInfo)} + text={item.text} + /> + ); + }); + } const isMenuVisible = defaultItems?.length || pluginItems?.length; if (!isMenuVisible) { diff --git a/webapp/channels/src/components/file_attachment/index.ts b/webapp/channels/src/components/file_attachment/index.ts index 2ab1187e65..920edefc3c 100644 --- a/webapp/channels/src/components/file_attachment/index.ts +++ b/webapp/channels/src/components/file_attachment/index.ts @@ -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), }; } diff --git a/webapp/channels/src/components/file_search_results/file_search_result_item.test.tsx b/webapp/channels/src/components/file_search_results/file_search_result_item.test.tsx index 1720289382..943e0ccb37 100644 --- a/webapp/channels/src/components/file_search_results/file_search_result_item.test.tsx +++ b/webapp/channels/src/components/file_search_results/file_search_result_item.test.tsx @@ -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(), diff --git a/webapp/channels/src/components/file_search_results/file_search_result_item.tsx b/webapp/channels/src/components/file_search_results/file_search_result_item.tsx index 8885abd76a..7e79e2af21 100644 --- a/webapp/channels/src/components/file_search_results/file_search_result_item.tsx +++ b/webapp/channels/src/components/file_search_results/file_search_result_item.tsx @@ -58,7 +58,13 @@ export default class FileSearchResultItem extends React.PureComponent { - 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 ( 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) { diff --git a/webapp/channels/src/components/post_view/channel_intro_message/pluggable_intro_buttons/pluggable_intro_buttons.tsx b/webapp/channels/src/components/post_view/channel_intro_message/pluggable_intro_buttons/pluggable_intro_buttons.tsx index 62f087d58e..518c5e666f 100644 --- a/webapp/channels/src/components/post_view/channel_intro_message/pluggable_intro_buttons/pluggable_intro_buttons.tsx +++ b/webapp/channels/src/components/post_view/channel_intro_message/pluggable_intro_buttons/pluggable_intro_buttons.tsx @@ -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; } diff --git a/webapp/channels/src/components/post_view/post_message_view/index.ts b/webapp/channels/src/components/post_view/post_message_view/index.ts index f92ffc92fc..4e60503c86 100644 --- a/webapp/channels/src/components/post_view/post_message_view/index.ts +++ b/webapp/channels/src/components/post_view/post_message_view/index.ts @@ -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', }; } diff --git a/webapp/channels/src/components/post_view/post_message_view/post_message_view.tsx b/webapp/channels/src/components/post_view/post_message_view/post_message_view.tsx index b6e03be4a1..edaa89aaa6 100644 --- a/webapp/channels/src/components/post_view/post_message_view/post_message_view.tsx +++ b/webapp/channels/src/components/post_view/post_message_view/post_message_view.tsx @@ -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 { 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 ( { showPostEditedIndicator={this.props.showPostEditedIndicator} />
- + {(!isSharedChannel || this.props.sharedChannelsPluginsEnabled) && ( + + )} ); } diff --git a/webapp/channels/src/components/profile_popover/profile_popover.tsx b/webapp/channels/src/components/profile_popover/profile_popover.tsx index 319476858f..fbf184652b 100644 --- a/webapp/channels/src/components/profile_popover/profile_popover.tsx +++ b/webapp/channels/src/components/profile_popover/profile_popover.tsx @@ -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} /> -
- -
+ {pluginItemsVisible && ( +
+ +
+ )} {enableCustomProfileAttributes && !user.is_bot && ( - + {pluginItemsVisible && ( + + )} ); diff --git a/webapp/channels/src/utils/message_html_to_component.tsx b/webapp/channels/src/utils/message_html_to_component.tsx index abedbd0c82..555873a442 100644 --- a/webapp/channels/src/utils/message_html_to_component.tsx +++ b/webapp/channels/src/utils/message_html_to_component.tsx @@ -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} /> ); },