mirror of
https://github.com/mattermost/mattermost.git
synced 2026-05-28 04:35:04 -04:00
[MM-65627] Add Channel popout window (#35596)
* [MM-65627] Add Channel popout window * Merge'd * Merge'd * FIx e2e * Prettier * PR feedback --------- Co-authored-by: Mattermost Build <build@mattermost.com>
This commit is contained in:
parent
fb11968f87
commit
8f0f4239eb
36 changed files with 1305 additions and 60 deletions
|
|
@ -34,7 +34,7 @@ test('MM-65630-1 Search results should show popout button that opens results in
|
|||
await expect(page.locator('#searchContainer')).toBeVisible();
|
||||
await expect(page.locator('#searchContainer').getByText(uniqueText)).toBeVisible();
|
||||
|
||||
const popoutButton = page.locator('.PopoutButton');
|
||||
const popoutButton = page.locator('#searchContainer .PopoutButton');
|
||||
await expect(popoutButton).toBeVisible();
|
||||
|
||||
const [popoutPage] = await Promise.all([page.waitForEvent('popup'), popoutButton.click()]);
|
||||
|
|
@ -82,7 +82,7 @@ test('MM-65630-2 Recent mentions popout should open with the right results', asy
|
|||
await expect(page.locator('#searchContainer').getByRole('heading', {name: 'Recent Mentions'})).toBeVisible();
|
||||
await expect(page.locator('#searchContainer').getByText(mentionText)).toBeVisible();
|
||||
|
||||
const popoutButton = page.locator('.PopoutButton');
|
||||
const popoutButton = page.locator('#searchContainer .PopoutButton');
|
||||
await expect(popoutButton).toBeVisible();
|
||||
|
||||
const [popoutPage] = await Promise.all([page.waitForEvent('popup'), popoutButton.click()]);
|
||||
|
|
@ -138,7 +138,7 @@ test('MM-65630-3 Saved messages popout should open with the right results', asyn
|
|||
await expect(page.locator('#searchContainer').getByRole('heading', {name: 'Saved messages'})).toBeVisible();
|
||||
await expect(page.locator('#searchContainer').getByText(savedText)).toBeVisible();
|
||||
|
||||
const popoutButton = page.locator('.PopoutButton');
|
||||
const popoutButton = page.locator('#searchContainer .PopoutButton');
|
||||
await expect(popoutButton).toBeVisible();
|
||||
|
||||
const [popoutPage] = await Promise.all([page.waitForEvent('popup'), popoutButton.click()]);
|
||||
|
|
@ -185,7 +185,10 @@ test('MM-65630-4 Search popout should not show popout button in the popout windo
|
|||
|
||||
await expect(page.locator('#searchContainer')).toBeVisible();
|
||||
|
||||
const [popoutPage] = await Promise.all([page.waitForEvent('popup'), page.locator('.PopoutButton').click()]);
|
||||
const [popoutPage] = await Promise.all([
|
||||
page.waitForEvent('popup'),
|
||||
page.locator('#searchContainer .PopoutButton').click(),
|
||||
]);
|
||||
|
||||
await popoutPage.waitForLoadState('domcontentloaded');
|
||||
await expect(popoutPage.locator('#searchContainer')).toBeVisible({timeout: 10000});
|
||||
|
|
@ -224,7 +227,7 @@ test('MM-65630-5 Search popout should preserve search type (files) in the URL',
|
|||
const filesTab = page.locator('#searchContainer').getByRole('tab', {name: /Files/});
|
||||
await filesTab.click();
|
||||
|
||||
const popoutButton = page.locator('.PopoutButton');
|
||||
const popoutButton = page.locator('#searchContainer .PopoutButton');
|
||||
await expect(popoutButton).toBeVisible();
|
||||
|
||||
const [popoutPage] = await Promise.all([page.waitForEvent('popup'), popoutButton.click()]);
|
||||
|
|
|
|||
|
|
@ -60,6 +60,7 @@ import {getHistory} from 'utils/browser_history';
|
|||
import {isArchivedChannel} from 'utils/channel_utils';
|
||||
import {Constants, ActionTypes, EventTypes, PostRequestTypes} from 'utils/constants';
|
||||
import {stopTryNotificationRing} from 'utils/notification_sounds';
|
||||
import {isChannelPopoutWindow} from 'utils/popouts/popout_windows';
|
||||
|
||||
import type {ActionFuncAsync, ThunkActionFunc} from 'types/store';
|
||||
|
||||
|
|
@ -77,7 +78,11 @@ export function goToLastViewedChannel(): ActionFuncAsync {
|
|||
channelToSwitchTo = getChannelByName(channels, getRedirectChannelNameForTeam(state, getCurrentTeamId(state)));
|
||||
}
|
||||
|
||||
return dispatch(switchToChannel(channelToSwitchTo!));
|
||||
const result = await dispatch(switchToChannel(channelToSwitchTo!));
|
||||
if (isChannelPopoutWindow()) {
|
||||
window.close();
|
||||
}
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -191,9 +196,14 @@ export function leaveChannel(channelId: string): ActionFuncAsync {
|
|||
dispatch(selectTeam(''));
|
||||
dispatch({type: TeamTypes.LEAVE_TEAM, data: currentTeam});
|
||||
getHistory().push('/');
|
||||
if (isChannelPopoutWindow()) {
|
||||
window.close();
|
||||
}
|
||||
} else if (channelId === currentChannelId) {
|
||||
// We only need to leave the channel if we are in the channel
|
||||
getHistory().push(teamUrl);
|
||||
if (isChannelPopoutWindow()) {
|
||||
window.close();
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -156,6 +156,7 @@ import {loadPlugin, loadPluginsIfNecessary, removePlugin} from 'plugins';
|
|||
import {getHistory} from 'utils/browser_history';
|
||||
import {ActionTypes, Constants, AnnouncementBarMessages, SocketEvents, UserStatuses, ModalIdentifiers, PageLoadContext} from 'utils/constants';
|
||||
import {getIntl} from 'utils/i18n';
|
||||
import {isChannelPopoutWindow} from 'utils/popouts/popout_windows';
|
||||
import {getSiteURL} from 'utils/url';
|
||||
|
||||
import type {ActionFunc, ThunkActionFunc} from 'types/store';
|
||||
|
|
@ -754,7 +755,15 @@ export function handleChannelUpdatedEvent(msg: WebSocketMessages.ChannelUpdated)
|
|||
if (channel.id === getCurrentChannelId(state)) {
|
||||
// using channel's team_id to ensure we always redirect to current channel even if channel's team changes.
|
||||
const teamId = channel.team_id || getCurrentTeamId(state);
|
||||
getHistory().replace(`${getRelativeTeamUrl(state, teamId)}/channels/${channel.name}`);
|
||||
const teamUrl = getRelativeTeamUrl(state, teamId);
|
||||
let channelPath = `${teamUrl}/channels/${channel.name}`;
|
||||
|
||||
// For the popout we make an exception and redirect to the popout path instead of the channel path.
|
||||
// DM/GM names never change, so we only need to handle regular channels here.
|
||||
if (isChannelPopoutWindow() && channel.type !== General.DM_CHANNEL && channel.type !== General.GM_CHANNEL) {
|
||||
channelPath = `/_popout/channel${teamUrl}/channels/${channel.name}`;
|
||||
}
|
||||
getHistory().replace(channelPath);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -1093,6 +1102,9 @@ function handleDeleteTeamEvent(msg: WebSocketMessages.Team) {
|
|||
} else {
|
||||
getHistory().push('/');
|
||||
}
|
||||
if (isChannelPopoutWindow()) {
|
||||
window.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1206,7 +1218,10 @@ export function handleUserRemovedEvent(msg: WebSocketMessages.UserRemovedFromCha
|
|||
});
|
||||
|
||||
if (currentChannel && msg.data.channel_id === currentChannel.id) {
|
||||
redirectUserToDefaultTeam();
|
||||
const redirect = redirectUserToDefaultTeam();
|
||||
if (isChannelPopoutWindow()) {
|
||||
redirect.then(() => window.close());
|
||||
}
|
||||
}
|
||||
|
||||
if (isGuest(currentUser.roles)) {
|
||||
|
|
|
|||
|
|
@ -79,6 +79,9 @@ const intl = useIntl();
|
|||
- Prefer `userEvent` and accessible queries (`getByRole`) over implementation-specific selectors.
|
||||
- Avoid snapshots; assert visible behavior instead.
|
||||
|
||||
## Icons
|
||||
- **Menu items and components should use Compass icon components** from `@mattermost/compass-icons/components` (e.g., `<DockWindowIcon size={18}/>`), not raw `<i className="icon icon-..."/>` elements.
|
||||
|
||||
## Useful Examples
|
||||
- `channel_view/channel_view.tsx` – full-page component structure with co-located SCSS.
|
||||
- `post_view/post_list_virtualized/post_list_virtualized.tsx` – virtualization + hooks pattern.
|
||||
|
|
|
|||
|
|
@ -279,6 +279,10 @@ exports[`components/ChannelHeader should match snapshot with last active display
|
|||
}
|
||||
/>
|
||||
<Connect(CallButton) />
|
||||
<PopoutButton
|
||||
className="channel-header__icon"
|
||||
onClick={[Function]}
|
||||
/>
|
||||
<ChannelInfoButton
|
||||
channel={
|
||||
Object {
|
||||
|
|
@ -559,6 +563,10 @@ exports[`components/ChannelHeader should match snapshot with no last active disp
|
|||
}
|
||||
/>
|
||||
<Connect(CallButton) />
|
||||
<PopoutButton
|
||||
className="channel-header__icon"
|
||||
onClick={[Function]}
|
||||
/>
|
||||
<ChannelInfoButton
|
||||
channel={
|
||||
Object {
|
||||
|
|
@ -764,6 +772,10 @@ exports[`components/ChannelHeader should render active channel files 1`] = `
|
|||
}
|
||||
/>
|
||||
<Connect(CallButton) />
|
||||
<PopoutButton
|
||||
className="channel-header__icon"
|
||||
onClick={[Function]}
|
||||
/>
|
||||
<ChannelInfoButton
|
||||
channel={
|
||||
Object {
|
||||
|
|
@ -968,6 +980,10 @@ exports[`components/ChannelHeader should render active flagged posts 1`] = `
|
|||
}
|
||||
/>
|
||||
<Connect(CallButton) />
|
||||
<PopoutButton
|
||||
className="channel-header__icon"
|
||||
onClick={[Function]}
|
||||
/>
|
||||
<ChannelInfoButton
|
||||
channel={
|
||||
Object {
|
||||
|
|
@ -1172,6 +1188,10 @@ exports[`components/ChannelHeader should render active mentions posts 1`] = `
|
|||
}
|
||||
/>
|
||||
<Connect(CallButton) />
|
||||
<PopoutButton
|
||||
className="channel-header__icon"
|
||||
onClick={[Function]}
|
||||
/>
|
||||
<ChannelInfoButton
|
||||
channel={
|
||||
Object {
|
||||
|
|
@ -1376,6 +1396,10 @@ exports[`components/ChannelHeader should render active pinned posts 1`] = `
|
|||
}
|
||||
/>
|
||||
<Connect(CallButton) />
|
||||
<PopoutButton
|
||||
className="channel-header__icon"
|
||||
onClick={[Function]}
|
||||
/>
|
||||
<ChannelInfoButton
|
||||
channel={
|
||||
Object {
|
||||
|
|
@ -1580,6 +1604,10 @@ exports[`components/ChannelHeader should render archived view 1`] = `
|
|||
}
|
||||
/>
|
||||
<Connect(CallButton) />
|
||||
<PopoutButton
|
||||
className="channel-header__icon"
|
||||
onClick={[Function]}
|
||||
/>
|
||||
<ChannelInfoButton
|
||||
channel={
|
||||
Object {
|
||||
|
|
@ -1804,6 +1832,10 @@ exports[`components/ChannelHeader should render correct menu when muted 1`] = `
|
|||
}
|
||||
/>
|
||||
<Connect(CallButton) />
|
||||
<PopoutButton
|
||||
className="channel-header__icon"
|
||||
onClick={[Function]}
|
||||
/>
|
||||
<ChannelInfoButton
|
||||
channel={
|
||||
Object {
|
||||
|
|
@ -2008,6 +2040,10 @@ exports[`components/ChannelHeader should render not active channel files 1`] = `
|
|||
}
|
||||
/>
|
||||
<Connect(CallButton) />
|
||||
<PopoutButton
|
||||
className="channel-header__icon"
|
||||
onClick={[Function]}
|
||||
/>
|
||||
<ChannelInfoButton
|
||||
channel={
|
||||
Object {
|
||||
|
|
@ -2308,6 +2344,10 @@ exports[`components/ChannelHeader should render properly when custom status is e
|
|||
}
|
||||
/>
|
||||
<Connect(CallButton) />
|
||||
<PopoutButton
|
||||
className="channel-header__icon"
|
||||
onClick={[Function]}
|
||||
/>
|
||||
<ChannelInfoButton
|
||||
channel={
|
||||
Object {
|
||||
|
|
@ -2627,6 +2667,10 @@ exports[`components/ChannelHeader should render properly when custom status is s
|
|||
}
|
||||
/>
|
||||
<Connect(CallButton) />
|
||||
<PopoutButton
|
||||
className="channel-header__icon"
|
||||
onClick={[Function]}
|
||||
/>
|
||||
<ChannelInfoButton
|
||||
channel={
|
||||
Object {
|
||||
|
|
@ -2832,6 +2876,10 @@ exports[`components/ChannelHeader should render properly when empty 1`] = `
|
|||
}
|
||||
/>
|
||||
<Connect(CallButton) />
|
||||
<PopoutButton
|
||||
className="channel-header__icon"
|
||||
onClick={[Function]}
|
||||
/>
|
||||
<ChannelInfoButton
|
||||
channel={
|
||||
Object {
|
||||
|
|
@ -3036,6 +3084,10 @@ exports[`components/ChannelHeader should render properly when populated 1`] = `
|
|||
}
|
||||
/>
|
||||
<Connect(CallButton) />
|
||||
<PopoutButton
|
||||
className="channel-header__icon"
|
||||
onClick={[Function]}
|
||||
/>
|
||||
<ChannelInfoButton
|
||||
channel={
|
||||
Object {
|
||||
|
|
@ -3261,6 +3313,10 @@ exports[`components/ChannelHeader should render properly when populated with cha
|
|||
}
|
||||
/>
|
||||
<Connect(CallButton) />
|
||||
<PopoutButton
|
||||
className="channel-header__icon"
|
||||
onClick={[Function]}
|
||||
/>
|
||||
<ChannelInfoButton
|
||||
channel={
|
||||
Object {
|
||||
|
|
@ -3428,6 +3484,10 @@ exports[`components/ChannelHeader should render shared view 1`] = `
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<PopoutButton
|
||||
className="channel-header__icon"
|
||||
onClick={[Function]}
|
||||
/>
|
||||
<ChannelInfoButton
|
||||
channel={
|
||||
Object {
|
||||
|
|
@ -3650,6 +3710,10 @@ exports[`components/ChannelHeader should render the pinned icon with the pinned
|
|||
}
|
||||
/>
|
||||
<Connect(CallButton) />
|
||||
<PopoutButton
|
||||
className="channel-header__icon"
|
||||
onClick={[Function]}
|
||||
/>
|
||||
<ChannelInfoButton
|
||||
channel={
|
||||
Object {
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ describe('components/ChannelHeader', () => {
|
|||
showChannelMembers: jest.fn(),
|
||||
fetchChannelRemotes: jest.fn(),
|
||||
},
|
||||
teamId: 'team_id',
|
||||
team: TestHelper.getTeamMock({id: 'team_id'}),
|
||||
channel: TestHelper.getChannelMock({}),
|
||||
channelMember: TestHelper.getChannelMembershipMock({}),
|
||||
currentUser: TestHelper.getUserMock({}),
|
||||
|
|
|
|||
|
|
@ -7,8 +7,10 @@ import type {MouseEvent, ReactNode, RefObject} from 'react';
|
|||
import {FormattedMessage, injectIntl} from 'react-intl';
|
||||
import type {WrappedComponentProps} from 'react-intl';
|
||||
|
||||
import {getPopoutChannelTitle} from 'components/channel_popout/channel_popout';
|
||||
import CustomStatusEmoji from 'components/custom_status/custom_status_emoji';
|
||||
import CustomStatusText from 'components/custom_status/custom_status_text';
|
||||
import PopoutButton from 'components/popout_button';
|
||||
import Timestamp from 'components/timestamp';
|
||||
import Tag from 'components/widgets/tag/tag';
|
||||
import WithTooltip from 'components/with_tooltip';
|
||||
|
|
@ -16,11 +18,13 @@ import WithTooltip from 'components/with_tooltip';
|
|||
import CallButton from 'plugins/call_button';
|
||||
import ChannelHeaderPlug from 'plugins/channel_header_plug';
|
||||
import Pluggable from 'plugins/pluggable';
|
||||
import {getChannelRoutePathAndIdentifier} from 'utils/channel_utils';
|
||||
import {
|
||||
Constants,
|
||||
NotificationLevels,
|
||||
RHSStates,
|
||||
} from 'utils/constants';
|
||||
import {canPopout, isChannelPopoutWindow, popoutChannel} from 'utils/popouts/popout_windows';
|
||||
import {isEmptyObject} from 'utils/utils';
|
||||
|
||||
import ChannelHeaderText from './channel_header_text';
|
||||
|
|
@ -97,6 +101,14 @@ class ChannelHeader extends React.PureComponent<Props> {
|
|||
}
|
||||
};
|
||||
|
||||
popoutChannelView = () => {
|
||||
const {channel, team, dmUser, intl} = this.props;
|
||||
if (channel && team) {
|
||||
const {path, identifier} = getChannelRoutePathAndIdentifier(channel, dmUser?.username);
|
||||
popoutChannel(intl.formatMessage(getPopoutChannelTitle(channel.type)), team.name, path, identifier);
|
||||
}
|
||||
};
|
||||
|
||||
toggleChannelMembersRHS = () => {
|
||||
if (this.props.rhsState === RHSStates.CHANNEL_MEMBERS) {
|
||||
this.props.actions.closeRightHandSide();
|
||||
|
|
@ -132,7 +144,7 @@ class ChannelHeader extends React.PureComponent<Props> {
|
|||
|
||||
render() {
|
||||
const {
|
||||
teamId,
|
||||
team,
|
||||
currentUser,
|
||||
gmMembers,
|
||||
channel,
|
||||
|
|
@ -410,7 +422,7 @@ class ChannelHeader extends React.PureComponent<Props> {
|
|||
{hasGuestsText}
|
||||
{autotranslationMessage}
|
||||
<ChannelHeaderText
|
||||
teamId={teamId}
|
||||
teamId={team?.id}
|
||||
channel={channel}
|
||||
dmUser={dmUser}
|
||||
/>
|
||||
|
|
@ -427,6 +439,12 @@ class ChannelHeader extends React.PureComponent<Props> {
|
|||
<CallButton/>
|
||||
</>
|
||||
)}
|
||||
{canPopout() && !isChannelPopoutWindow() && (
|
||||
<PopoutButton
|
||||
className='channel-header__icon'
|
||||
onClick={this.popoutChannelView}
|
||||
/>
|
||||
)}
|
||||
<ChannelInfoButton channel={channel}/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ import {isChannelNamesMap} from 'utils/text_formatting';
|
|||
import {ChannelHeaderTextPopover} from './channel_header_text_popover';
|
||||
|
||||
interface Props {
|
||||
teamId: Team['id'];
|
||||
teamId?: Team['id'];
|
||||
channel: Channel;
|
||||
dmUser?: UserProfile;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ import {
|
|||
} from 'mattermost-redux/selectors/entities/channels';
|
||||
import {getConfig, getFeatureFlagValue} from 'mattermost-redux/selectors/entities/general';
|
||||
import {getRemoteNamesForChannel} from 'mattermost-redux/selectors/entities/shared_channels';
|
||||
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
|
||||
import {getCurrentTeam} from 'mattermost-redux/selectors/entities/teams';
|
||||
import {
|
||||
displayLastActiveLabel,
|
||||
getCurrentUser,
|
||||
|
|
@ -86,7 +86,7 @@ function makeMapStateToProps() {
|
|||
}
|
||||
|
||||
return {
|
||||
teamId: getCurrentTeamId(state),
|
||||
team: getCurrentTeam(state),
|
||||
channel,
|
||||
channelMember: getMyCurrentChannelMembership(state),
|
||||
memberCount: stats?.member_count || 0,
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ import ChannelDirectMenu from './channel_header_menu_items/channel_header_direct
|
|||
import ChannelGroupMenu from './channel_header_menu_items/channel_header_group_menu';
|
||||
import ChannelHeaderMobileMenu from './channel_header_menu_items/channel_header_mobile_menu';
|
||||
import ChannelPublicPrivateMenu from './channel_header_menu_items/channel_header_public_private_menu';
|
||||
import MenuItemOpenInNewWindow from './menu_items/open_in_new_window';
|
||||
|
||||
import ChannelHeaderTitleDirect from '../channel_header/channel_header_title_direct';
|
||||
import ChannelHeaderTitleGroup from '../channel_header/channel_header_title_group';
|
||||
|
|
@ -148,6 +149,7 @@ export default function ChannelHeaderMenu({dmUser, gmMembers, isMobile, archived
|
|||
horizontal: 'left',
|
||||
}}
|
||||
>
|
||||
<MenuItemOpenInNewWindow channel={channel}/>
|
||||
{isDirect && (
|
||||
<ChannelDirectMenu
|
||||
channel={channel}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,93 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import type {ChannelType} from '@mattermost/types/channels';
|
||||
|
||||
import {WithTestMenuContext} from 'components/menu/menu_context_test';
|
||||
|
||||
import {renderWithContext, screen, userEvent} from 'tests/react_testing_utils';
|
||||
import {isChannelPopoutWindow, popoutChannel} from 'utils/popouts/popout_windows';
|
||||
import {TestHelper} from 'utils/test_helper';
|
||||
|
||||
import MenuItemOpenInNewWindow from './open_in_new_window';
|
||||
|
||||
jest.mock('components/channel_popout/channel_popout', () => ({
|
||||
getPopoutChannelTitle: jest.fn(() => ({id: 'test.title', defaultMessage: 'Test Title'})),
|
||||
}));
|
||||
|
||||
jest.mock('utils/popouts/popout_windows', () => ({
|
||||
isChannelPopoutWindow: jest.fn(() => false),
|
||||
popoutChannel: jest.fn(),
|
||||
canPopout: jest.fn(() => true),
|
||||
}));
|
||||
|
||||
describe('MenuItemOpenInNewWindow', () => {
|
||||
const currentUser = TestHelper.getUserMock({id: 'current_user_id', username: 'currentuser'});
|
||||
const team = TestHelper.getTeamMock({id: 'team_id', name: 'test-team'});
|
||||
|
||||
const baseState = {
|
||||
entities: {
|
||||
channels: {
|
||||
currentChannelId: 'channel_id',
|
||||
channels: {},
|
||||
channelsInTeam: {},
|
||||
myMembers: {},
|
||||
},
|
||||
teams: {
|
||||
currentTeamId: team.id,
|
||||
teams: {[team.id]: team},
|
||||
myMembers: {},
|
||||
},
|
||||
users: {
|
||||
currentUserId: currentUser.id,
|
||||
profiles: {
|
||||
[currentUser.id]: currentUser,
|
||||
},
|
||||
},
|
||||
general: {config: {}},
|
||||
preferences: {myPreferences: {}},
|
||||
roles: {roles: {}},
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
jest.mocked(isChannelPopoutWindow).mockReturnValue(false);
|
||||
});
|
||||
|
||||
test('should render nothing when already in a channel popout', () => {
|
||||
jest.mocked(isChannelPopoutWindow).mockReturnValue(true);
|
||||
const channel = TestHelper.getChannelMock({type: 'O' as ChannelType, name: 'town-square'});
|
||||
|
||||
const {container} = renderWithContext(
|
||||
<WithTestMenuContext>
|
||||
<MenuItemOpenInNewWindow channel={channel}/>
|
||||
</WithTestMenuContext>,
|
||||
baseState,
|
||||
);
|
||||
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
});
|
||||
|
||||
test('should call popoutChannel when clicked', async () => {
|
||||
const channel = TestHelper.getChannelMock({type: 'O' as ChannelType, name: 'town-square'});
|
||||
|
||||
renderWithContext(
|
||||
<WithTestMenuContext>
|
||||
<MenuItemOpenInNewWindow channel={channel}/>
|
||||
</WithTestMenuContext>,
|
||||
baseState,
|
||||
);
|
||||
|
||||
await userEvent.click(screen.getByText('Open in new window'));
|
||||
|
||||
expect(jest.mocked(popoutChannel)).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
'test-team',
|
||||
'channels',
|
||||
'town-square',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {useSelector} from 'react-redux';
|
||||
|
||||
import type {Channel} from '@mattermost/types/channels';
|
||||
|
||||
import {getCurrentTeam} from 'mattermost-redux/selectors/entities/teams';
|
||||
import {getCurrentUserId, getUser} from 'mattermost-redux/selectors/entities/users';
|
||||
import {getUserIdFromChannelName} from 'mattermost-redux/utils/channel_utils';
|
||||
|
||||
import {getPopoutChannelTitle} from 'components/channel_popout/channel_popout';
|
||||
import * as Menu from 'components/menu';
|
||||
import PopoutMenuItem from 'components/popout_menu_item';
|
||||
|
||||
import {getChannelRoutePathAndIdentifier} from 'utils/channel_utils';
|
||||
import {Constants} from 'utils/constants';
|
||||
import {isChannelPopoutWindow, popoutChannel} from 'utils/popouts/popout_windows';
|
||||
|
||||
import type {GlobalState} from 'types/store';
|
||||
|
||||
interface Props {
|
||||
channel: Channel;
|
||||
}
|
||||
|
||||
const MenuItemOpenInNewWindow = ({channel}: Props) => {
|
||||
const intl = useIntl();
|
||||
const team = useSelector(getCurrentTeam);
|
||||
const currentUserId = useSelector(getCurrentUserId);
|
||||
const dmUser = useSelector((state: GlobalState) => {
|
||||
if (channel.type === Constants.DM_CHANNEL) {
|
||||
const dmUserId = getUserIdFromChannelName(currentUserId, channel.name);
|
||||
return getUser(state, dmUserId);
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
|
||||
if (isChannelPopoutWindow()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleClick = () => {
|
||||
if (!team) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {path, identifier} = getChannelRoutePathAndIdentifier(channel, dmUser?.username);
|
||||
popoutChannel(intl.formatMessage(getPopoutChannelTitle(channel.type)), team.name, path, identifier);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<PopoutMenuItem
|
||||
id='channelOpenInNewWindow'
|
||||
onClick={handleClick}
|
||||
/>
|
||||
<Menu.Separator/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default MenuItemOpenInNewWindow;
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
body.app__body.popout #root .main-wrapper.channel-popout {
|
||||
border: none;
|
||||
margin: 0;
|
||||
background-color: var(--center-channel-bg);
|
||||
grid-template: "center rhs";
|
||||
grid-template-columns: 1fr min-content;
|
||||
|
||||
#channel_view {
|
||||
overflow: hidden;
|
||||
grid-area: center;
|
||||
}
|
||||
|
||||
.sidebar--right--width-holder {
|
||||
grid-area: rhs;
|
||||
}
|
||||
|
||||
#sidebar-right {
|
||||
top: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.post-right__container .PopoutButton {
|
||||
display: initial;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
grid-template: "main";
|
||||
grid-template-columns: 1fr;
|
||||
|
||||
#channel_view,
|
||||
#sidebar-right {
|
||||
grid-area: main;
|
||||
}
|
||||
|
||||
.sidebar--right--width-holder {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&:not(.rhs-open) #channel-header {
|
||||
display: flex;
|
||||
|
||||
.flex-parent {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.channel-header .channel-header__icon {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.channel__wrap .app__content {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.search-bar__container {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.sidebar--right__close {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,191 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {MemoryRouter, Route} from 'react-router-dom';
|
||||
|
||||
import {fetchMyCategories} from 'mattermost-redux/actions/channel_categories';
|
||||
import {fetchChannelsAndMembers, getChannelStats} from 'mattermost-redux/actions/channels';
|
||||
import {fetchTeamScheduledPosts} from 'mattermost-redux/actions/scheduled_posts';
|
||||
import {selectTeam} from 'mattermost-redux/actions/teams';
|
||||
|
||||
import {useTeamByName} from 'components/common/hooks/use_team';
|
||||
|
||||
import {renderWithContext, screen, waitFor} from 'tests/react_testing_utils';
|
||||
import {TestHelper} from 'utils/test_helper';
|
||||
|
||||
import {getPopoutChannelTitle} from './channel_popout';
|
||||
|
||||
import ChannelPopout from './index';
|
||||
|
||||
const MOCK_ACTION = {type: 'MOCK'};
|
||||
|
||||
jest.mock('mattermost-redux/actions/channel_categories', () => ({
|
||||
fetchMyCategories: jest.fn(() => MOCK_ACTION),
|
||||
}));
|
||||
jest.mock('mattermost-redux/actions/channels', () => ({
|
||||
...jest.requireActual('mattermost-redux/actions/channels'),
|
||||
fetchChannelsAndMembers: jest.fn(() => MOCK_ACTION),
|
||||
getChannelStats: jest.fn(() => MOCK_ACTION),
|
||||
}));
|
||||
jest.mock('mattermost-redux/actions/scheduled_posts', () => ({
|
||||
fetchTeamScheduledPosts: jest.fn(() => MOCK_ACTION),
|
||||
}));
|
||||
jest.mock('mattermost-redux/actions/teams', () => ({
|
||||
selectTeam: jest.fn(() => MOCK_ACTION),
|
||||
}));
|
||||
jest.mock('components/common/hooks/use_team', () => ({
|
||||
useTeamByName: jest.fn(),
|
||||
}));
|
||||
jest.mock('utils/popouts/use_popout_title', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn(),
|
||||
}));
|
||||
jest.mock('components/channel_layout/channel_identifier_router', () => ({
|
||||
__esModule: true,
|
||||
default: () => <div data-testid='channel-identifier-router'>{'ChannelIdentifierRouter'}</div>,
|
||||
}));
|
||||
jest.mock('components/sidebar_right', () => ({
|
||||
__esModule: true,
|
||||
default: () => <div data-testid='sidebar-right'>{'SidebarRight'}</div>,
|
||||
}));
|
||||
jest.mock('components/unreads_status_handler', () => ({
|
||||
__esModule: true,
|
||||
default: () => <div data-testid='unreads-status-handler'>{'UnreadsStatusHandler'}</div>,
|
||||
}));
|
||||
jest.mock('components/loading_screen', () => ({
|
||||
__esModule: true,
|
||||
default: () => <div data-testid='loading-screen'>{'LoadingScreen'}</div>,
|
||||
}));
|
||||
|
||||
describe('ChannelPopout', () => {
|
||||
const team = TestHelper.getTeamMock({id: 'team_id', name: 'test-team'});
|
||||
const channel = TestHelper.getChannelMock({id: 'channel_id', name: 'town-square', type: 'O'});
|
||||
|
||||
const baseState = {
|
||||
entities: {
|
||||
channels: {
|
||||
currentChannelId: channel.id,
|
||||
channels: {[channel.id]: channel},
|
||||
channelsInTeam: {},
|
||||
myMembers: {},
|
||||
},
|
||||
teams: {
|
||||
currentTeamId: team.id,
|
||||
teams: {[team.id]: team},
|
||||
myMembers: {},
|
||||
},
|
||||
users: {currentUserId: 'user_id', profiles: {}},
|
||||
general: {config: {}},
|
||||
preferences: {myPreferences: {}},
|
||||
roles: {roles: {}},
|
||||
},
|
||||
views: {
|
||||
rhs: {isSidebarOpen: false},
|
||||
},
|
||||
};
|
||||
|
||||
function renderPopout(url: string) {
|
||||
return renderWithContext(
|
||||
<MemoryRouter initialEntries={[url]}>
|
||||
<Route path='/_popout/channel/:team/:path(channels|messages)/:identifier/:postid?'>
|
||||
<ChannelPopout/>
|
||||
</Route>
|
||||
</MemoryRouter>,
|
||||
baseState,
|
||||
);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('should show loading screen when team is not found', () => {
|
||||
jest.mocked(useTeamByName).mockReturnValue(undefined);
|
||||
renderPopout('/_popout/channel/test-team/channels/town-square');
|
||||
expect(screen.getByTestId('loading-screen')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should render ChannelIdentifierRouter and SidebarRight when team is resolved', () => {
|
||||
jest.mocked(useTeamByName).mockReturnValue(team);
|
||||
renderPopout('/_popout/channel/test-team/channels/town-square');
|
||||
|
||||
expect(screen.getByTestId('channel-identifier-router')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('sidebar-right')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should dispatch team bootstrapping actions when team is resolved', async () => {
|
||||
jest.mocked(useTeamByName).mockReturnValue(team);
|
||||
renderPopout('/_popout/channel/test-team/channels/town-square');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(jest.mocked(selectTeam)).toHaveBeenCalledWith('team_id');
|
||||
expect(jest.mocked(fetchChannelsAndMembers)).toHaveBeenCalledWith('team_id');
|
||||
expect(jest.mocked(fetchMyCategories)).toHaveBeenCalledWith('team_id');
|
||||
expect(jest.mocked(fetchTeamScheduledPosts)).toHaveBeenCalledWith('team_id', true);
|
||||
});
|
||||
});
|
||||
|
||||
test('should dispatch getChannelStats when channel is available', async () => {
|
||||
jest.mocked(useTeamByName).mockReturnValue(team);
|
||||
renderPopout('/_popout/channel/test-team/channels/town-square');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(jest.mocked(getChannelStats)).toHaveBeenCalledWith('channel_id');
|
||||
});
|
||||
});
|
||||
|
||||
test('should apply rhs-open class when RHS is open', () => {
|
||||
jest.mocked(useTeamByName).mockReturnValue(team);
|
||||
const stateWithRhsOpen = {
|
||||
...baseState,
|
||||
views: {rhs: {isSidebarOpen: true}},
|
||||
};
|
||||
|
||||
renderWithContext(
|
||||
<MemoryRouter initialEntries={['/_popout/channel/test-team/channels/town-square']}>
|
||||
<Route path='/_popout/channel/:team/:path(channels|messages)/:identifier/:postid?'>
|
||||
<ChannelPopout/>
|
||||
</Route>
|
||||
</MemoryRouter>,
|
||||
stateWithRhsOpen,
|
||||
);
|
||||
|
||||
const mainWrapper = screen.getByTestId('channel-identifier-router').closest('.main-wrapper');
|
||||
expect(mainWrapper).toHaveClass('rhs-open');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPopoutChannelTitle', () => {
|
||||
test('should return DM title format for DM channels', () => {
|
||||
const result = getPopoutChannelTitle('D');
|
||||
expect(result).toEqual({
|
||||
id: 'channel_popout.title.dm',
|
||||
defaultMessage: '{channelName} - {serverName}',
|
||||
});
|
||||
});
|
||||
|
||||
test('should return DM title format for GM channels', () => {
|
||||
const result = getPopoutChannelTitle('G');
|
||||
expect(result).toEqual({
|
||||
id: 'channel_popout.title.dm',
|
||||
defaultMessage: '{channelName} - {serverName}',
|
||||
});
|
||||
});
|
||||
|
||||
test('should return standard title format for regular channels', () => {
|
||||
const result = getPopoutChannelTitle('O');
|
||||
expect(result).toEqual({
|
||||
id: 'channel_popout.title',
|
||||
defaultMessage: '{channelName} - {teamName} - {serverName}',
|
||||
});
|
||||
});
|
||||
|
||||
test('should return standard title format when type is undefined', () => {
|
||||
const result = getPopoutChannelTitle(undefined);
|
||||
expect(result).toEqual({
|
||||
id: 'channel_popout.title',
|
||||
defaultMessage: '{channelName} - {teamName} - {serverName}',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import classNames from 'classnames';
|
||||
import React, {useEffect} from 'react';
|
||||
import {useDispatch, useSelector} from 'react-redux';
|
||||
import {useParams} from 'react-router-dom';
|
||||
|
||||
import type {ChannelType} from '@mattermost/types/channels';
|
||||
|
||||
import {fetchMyCategories} from 'mattermost-redux/actions/channel_categories';
|
||||
import {fetchChannelsAndMembers, getChannelStats} from 'mattermost-redux/actions/channels';
|
||||
import {fetchTeamScheduledPosts} from 'mattermost-redux/actions/scheduled_posts';
|
||||
import {selectTeam} from 'mattermost-redux/actions/teams';
|
||||
import {getCurrentChannel} from 'mattermost-redux/selectors/entities/channels';
|
||||
|
||||
import {getIsRhsOpen} from 'selectors/rhs';
|
||||
|
||||
import ChannelIdentifierRouter from 'components/channel_layout/channel_identifier_router';
|
||||
import {useTeamByName} from 'components/common/hooks/use_team';
|
||||
import LoadingScreen from 'components/loading_screen';
|
||||
import SidebarRight from 'components/sidebar_right';
|
||||
import UnreadsStatusHandler from 'components/unreads_status_handler';
|
||||
|
||||
import Constants from 'utils/constants';
|
||||
import usePopoutTitle from 'utils/popouts/use_popout_title';
|
||||
import {isDesktopApp} from 'utils/user_agent';
|
||||
|
||||
import './channel_popout.scss';
|
||||
|
||||
export function getPopoutChannelTitle(channelType?: ChannelType) {
|
||||
if (channelType === Constants.DM_CHANNEL || channelType === Constants.GM_CHANNEL) {
|
||||
return {id: 'channel_popout.title.dm', defaultMessage: '{channelName} - {serverName}'};
|
||||
}
|
||||
return {id: 'channel_popout.title', defaultMessage: '{channelName} - {teamName} - {serverName}'};
|
||||
}
|
||||
|
||||
export default function ChannelPopout() {
|
||||
const dispatch = useDispatch();
|
||||
const {team: teamName, postid} = useParams<{team: string; path: string; identifier: string; postid?: string}>();
|
||||
|
||||
const team = useTeamByName(teamName);
|
||||
const teamId = team?.id;
|
||||
|
||||
const channel = useSelector(getCurrentChannel);
|
||||
const channelId = channel?.id;
|
||||
|
||||
const rhsOpen = useSelector(getIsRhsOpen);
|
||||
|
||||
usePopoutTitle(getPopoutChannelTitle(channel?.type));
|
||||
|
||||
useEffect(() => {
|
||||
if (teamId) {
|
||||
dispatch(selectTeam(teamId));
|
||||
dispatch(fetchChannelsAndMembers(teamId));
|
||||
dispatch(fetchMyCategories(teamId));
|
||||
dispatch(fetchTeamScheduledPosts(teamId, true));
|
||||
}
|
||||
}, [dispatch, teamId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (channelId) {
|
||||
dispatch(getChannelStats(channelId));
|
||||
}
|
||||
}, [dispatch, channelId]);
|
||||
|
||||
if (!team) {
|
||||
return <LoadingScreen/>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{isDesktopApp() && <UnreadsStatusHandler/>}
|
||||
<div className={classNames('main-wrapper', 'channel-popout', {'rhs-open': rhsOpen})}>
|
||||
<div
|
||||
id='channel_view'
|
||||
className='channel-view'
|
||||
>
|
||||
<div className='container-fluid channel-view-inner'>
|
||||
<div className='inner-wrap channel__wrap'>
|
||||
<div className='row main'>
|
||||
<ChannelIdentifierRouter key={postid || 'channel'}/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<SidebarRight/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
4
webapp/channels/src/components/channel_popout/index.ts
Normal file
4
webapp/channels/src/components/channel_popout/index.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
export {default} from './channel_popout';
|
||||
|
|
@ -11,6 +11,7 @@ import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
|
|||
|
||||
import {loadStatusesByIds} from 'actions/status_actions';
|
||||
|
||||
import ChannelPopout from 'components/channel_popout';
|
||||
import HelpPopout from 'components/help_popout';
|
||||
import LoggedIn from 'components/logged_in';
|
||||
import ModalController from 'components/modal_controller';
|
||||
|
|
@ -19,7 +20,7 @@ import {useUserTheme} from 'components/theme_provider';
|
|||
import ThreadPopout from 'components/thread_popout';
|
||||
|
||||
import Pluggable from 'plugins/pluggable';
|
||||
import {TEAM_NAME_PATH_PATTERN, ID_PATH_PATTERN} from 'utils/path';
|
||||
import {TEAM_NAME_PATH_PATTERN, ID_PATH_PATTERN, IDENTIFIER_PATH_PATTERN} from 'utils/path';
|
||||
import {useBrowserPopout} from 'utils/popouts/use_browser_popout';
|
||||
|
||||
import './popout_controller.scss';
|
||||
|
|
@ -34,6 +35,10 @@ const PopoutController: React.FC<RouteComponentProps> = (routeProps) => {
|
|||
useEffect(() => {
|
||||
document.body.classList.add('app__body', 'popout');
|
||||
dispatch(getMe());
|
||||
|
||||
return () => {
|
||||
document.body.classList.remove('app__body', 'popout');
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -51,6 +56,10 @@ const PopoutController: React.FC<RouteComponentProps> = (routeProps) => {
|
|||
path={`/_popout/thread/:team(${TEAM_NAME_PATH_PATTERN})/:postId(${ID_PATH_PATTERN})`}
|
||||
component={ThreadPopout}
|
||||
/>
|
||||
<Route
|
||||
path={`/_popout/channel/:team(${TEAM_NAME_PATH_PATTERN})/:path(channels|messages)/:identifier(${IDENTIFIER_PATH_PATTERN})/:postid(${ID_PATH_PATTERN})?`}
|
||||
component={ChannelPopout}
|
||||
/>
|
||||
<Route
|
||||
path={`/_popout/rhs/:team(${TEAM_NAME_PATH_PATTERN})`}
|
||||
component={RhsPopout}
|
||||
|
|
|
|||
36
webapp/channels/src/components/popout_menu_item.tsx
Normal file
36
webapp/channels/src/components/popout_menu_item.tsx
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
||||
import {DockWindowIcon} from '@mattermost/compass-icons/components';
|
||||
|
||||
import * as Menu from 'components/menu';
|
||||
|
||||
import {canPopout} from 'utils/popouts/popout_windows';
|
||||
|
||||
type Props = {
|
||||
onClick: () => void;
|
||||
id?: string;
|
||||
};
|
||||
|
||||
export default function PopoutMenuItem({onClick, id = 'openInNewWindow'}: Props) {
|
||||
if (!canPopout()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Menu.Item
|
||||
id={id}
|
||||
leadingElement={<DockWindowIcon size={18}/>}
|
||||
labels={
|
||||
<FormattedMessage
|
||||
id='popout_menu_item.openInNewWindow'
|
||||
defaultMessage='Open in new window'
|
||||
/>
|
||||
}
|
||||
onClick={onClick}
|
||||
/>
|
||||
);
|
||||
}
|
||||
95
webapp/channels/src/components/post/actions.test.ts
Normal file
95
webapp/channels/src/components/post/actions.test.ts
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {loadPostsAround} from 'actions/views/channel';
|
||||
|
||||
import testConfigureStore from 'tests/test_store';
|
||||
import {getHistory} from 'utils/browser_history';
|
||||
import {ActionTypes} from 'utils/constants';
|
||||
import {TestHelper} from 'utils/test_helper';
|
||||
|
||||
import {highlightPostInChannelPopout} from './actions';
|
||||
|
||||
jest.mock('actions/views/channel', () => ({
|
||||
loadPostsAround: jest.fn(() => ({type: 'MOCK_LOAD_POSTS_AROUND'})),
|
||||
}));
|
||||
|
||||
jest.mock('utils/browser_history', () => ({
|
||||
getHistory: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('highlightPostInChannelPopout', () => {
|
||||
const team = TestHelper.getTeamMock({id: 'team_id', name: 'test-team'});
|
||||
const currentUser = TestHelper.getUserMock({id: 'current_user_id', username: 'currentuser'});
|
||||
const channel = TestHelper.getChannelMock({id: 'channel_id', name: 'town-square', type: 'O'});
|
||||
|
||||
const baseState = {
|
||||
entities: {
|
||||
channels: {
|
||||
currentChannelId: channel.id,
|
||||
channels: {[channel.id]: channel},
|
||||
channelsInTeam: {[team.id]: new Set([channel.id])},
|
||||
myMembers: {[channel.id]: {}},
|
||||
},
|
||||
teams: {
|
||||
currentTeamId: team.id,
|
||||
teams: {[team.id]: team},
|
||||
myMembers: {[team.id]: {}},
|
||||
},
|
||||
users: {
|
||||
currentUserId: currentUser.id,
|
||||
profiles: {[currentUser.id]: currentUser},
|
||||
statuses: {},
|
||||
profilesInChannel: {},
|
||||
},
|
||||
general: {config: {}},
|
||||
preferences: {myPreferences: {}},
|
||||
roles: {roles: {}},
|
||||
},
|
||||
};
|
||||
|
||||
let mockReplace: jest.Mock;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockReplace = jest.fn();
|
||||
jest.mocked(getHistory).mockReturnValue({replace: mockReplace} as any);
|
||||
});
|
||||
|
||||
test('should return false if team or channel is not available', async () => {
|
||||
const store = testConfigureStore({
|
||||
entities: {
|
||||
channels: {currentChannelId: '', channels: {}, channelsInTeam: {}, myMembers: {}},
|
||||
teams: {currentTeamId: '', teams: {}, myMembers: {}},
|
||||
users: {currentUserId: '', profiles: {}},
|
||||
general: {config: {}},
|
||||
preferences: {myPreferences: {}},
|
||||
roles: {roles: {}},
|
||||
},
|
||||
});
|
||||
|
||||
const result = await store.dispatch(highlightPostInChannelPopout('post_id'));
|
||||
expect(result).toEqual({data: false});
|
||||
expect(jest.mocked(loadPostsAround)).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should load posts around the target post', async () => {
|
||||
const store = testConfigureStore(baseState);
|
||||
await store.dispatch(highlightPostInChannelPopout('post_123'));
|
||||
expect(jest.mocked(loadPostsAround)).toHaveBeenCalledWith('channel_id', 'post_123');
|
||||
});
|
||||
|
||||
test('should dispatch RECEIVED_FOCUSED_POST and navigate to popout URL', async () => {
|
||||
const store = testConfigureStore(baseState);
|
||||
await store.dispatch(highlightPostInChannelPopout('post_123'));
|
||||
|
||||
const actions = store.getActions();
|
||||
expect(actions).toContainEqual({
|
||||
type: ActionTypes.RECEIVED_FOCUSED_POST,
|
||||
data: 'post_123',
|
||||
channelId: 'channel_id',
|
||||
});
|
||||
|
||||
expect(mockReplace).toHaveBeenCalledWith('/_popout/channel/test-team/channels/town-square/post_123');
|
||||
});
|
||||
});
|
||||
|
|
@ -3,15 +3,22 @@
|
|||
|
||||
import {removePost} from 'mattermost-redux/actions/posts';
|
||||
import type {ExtendedPost} from 'mattermost-redux/actions/posts';
|
||||
import {getCurrentChannel} from 'mattermost-redux/selectors/entities/channels';
|
||||
import {getCurrentTeam} from 'mattermost-redux/selectors/entities/teams';
|
||||
import {getCurrentUserId, getUser} from 'mattermost-redux/selectors/entities/users';
|
||||
import {getUserIdFromChannelName} from 'mattermost-redux/utils/channel_utils';
|
||||
|
||||
import {loadPostsAround} from 'actions/views/channel';
|
||||
import {removeDraft} from 'actions/views/drafts';
|
||||
import {closeRightHandSide} from 'actions/views/rhs';
|
||||
import {getGlobalItem} from 'selectors/storage';
|
||||
import {isThreadOpen} from 'selectors/views/threads';
|
||||
|
||||
import {StoragePrefixes} from 'utils/constants';
|
||||
import {getHistory} from 'utils/browser_history';
|
||||
import {getChannelRoutePathAndIdentifier} from 'utils/channel_utils';
|
||||
import {ActionTypes, StoragePrefixes} from 'utils/constants';
|
||||
|
||||
import type {ActionFunc} from 'types/store';
|
||||
import type {ActionFunc, ActionFuncAsync} from 'types/store';
|
||||
|
||||
/**
|
||||
* This action is called when the deleted post which is shown as 'deleted' in the RHS is then removed from the channel manually.
|
||||
|
|
@ -31,3 +38,35 @@ export function removePostCloseRHSDeleteDraft(post: ExtendedPost): ActionFunc<bo
|
|||
return dispatch(removePost(post));
|
||||
};
|
||||
}
|
||||
|
||||
export function highlightPostInChannelPopout(postId: string): ActionFuncAsync {
|
||||
return async (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const team = getCurrentTeam(state);
|
||||
const channel = getCurrentChannel(state);
|
||||
|
||||
if (!team || !channel) {
|
||||
return {data: false};
|
||||
}
|
||||
|
||||
const result = await dispatch(loadPostsAround(channel.id, postId));
|
||||
if ('error' in result) {
|
||||
return {data: false, error: result.error};
|
||||
}
|
||||
|
||||
dispatch({
|
||||
type: ActionTypes.RECEIVED_FOCUSED_POST,
|
||||
data: postId,
|
||||
channelId: channel.id,
|
||||
});
|
||||
|
||||
const currentUserId = getCurrentUserId(state);
|
||||
const dmUserId = getUserIdFromChannelName(currentUserId, channel.name);
|
||||
const dmUser = getUser(state, dmUserId);
|
||||
const {path, identifier} = getChannelRoutePathAndIdentifier(channel, dmUser?.username);
|
||||
|
||||
getHistory().replace(`/_popout/channel/${team.name}/${path}/${identifier}/${postId}`);
|
||||
|
||||
return {data: true};
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ import {getDisplayNameByUser} from 'utils/utils';
|
|||
|
||||
import type {GlobalState} from 'types/store';
|
||||
|
||||
import {removePostCloseRHSDeleteDraft} from './actions';
|
||||
import {highlightPostInChannelPopout, removePostCloseRHSDeleteDraft} from './actions';
|
||||
import PostComponent from './post_component';
|
||||
|
||||
type OwnProps = {
|
||||
|
|
@ -254,6 +254,7 @@ function mapDispatchToProps(dispatch: Dispatch) {
|
|||
actions: bindActionCreators({
|
||||
markPostAsUnread,
|
||||
emitShortcutReactToLastPostFrom,
|
||||
highlightPostInChannelPopout,
|
||||
selectPost,
|
||||
selectPostFromRightHandSideSearch,
|
||||
setRhsExpanded,
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ describe('PostComponent', () => {
|
|||
savePreferences: jest.fn(),
|
||||
openModal: jest.fn(),
|
||||
closeModal: jest.fn(),
|
||||
highlightPostInChannelPopout: jest.fn(),
|
||||
},
|
||||
isChannelAutotranslated: false,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ import {getArchiveIconComponent} from 'utils/channel_utils';
|
|||
import Constants, {A11yCustomEventTypes, AppEvents, Locations, PostTypes, ModalIdentifiers} from 'utils/constants';
|
||||
import type {A11yFocusEventDetail} from 'utils/constants';
|
||||
import {isKeyPressed} from 'utils/keyboard';
|
||||
import {isPopoutWindow} from 'utils/popouts/popout_windows';
|
||||
import {isChannelPopoutWindow, isPopoutWindow} from 'utils/popouts/popout_windows';
|
||||
import * as PostUtils from 'utils/post_utils';
|
||||
import {makeIsEligibleForClick} from 'utils/utils';
|
||||
|
||||
|
|
@ -110,6 +110,7 @@ export type Props = {
|
|||
savePreferences: (userId: string, preferences: Array<{category: string; user_id: string; name: string; value: string}>) => void;
|
||||
openModal: <P>(modalData: ModalData<P>) => void;
|
||||
closeModal: (modalId: string) => void;
|
||||
highlightPostInChannelPopout: (postId: string) => void;
|
||||
};
|
||||
timestampProps?: Partial<TimestampProps>;
|
||||
shouldHighlight?: boolean;
|
||||
|
|
@ -407,8 +408,14 @@ function PostComponent(props: Props) {
|
|||
}
|
||||
|
||||
props.actions.setRhsExpanded(false);
|
||||
|
||||
if (isChannelPopoutWindow() && props.isPinnedPosts) {
|
||||
props.actions.highlightPostInChannelPopout(post.id);
|
||||
return;
|
||||
}
|
||||
|
||||
getHistory().push(`/${props.teamName}/pl/${post.id}`);
|
||||
}, [props.isMobileView, props.actions, props.teamName, post?.id]);
|
||||
}, [props.isMobileView, props.actions, props.teamName, props.isPinnedPosts, post]);
|
||||
|
||||
const {selectPostFromRightHandSideSearch} = props.actions;
|
||||
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import ToastWrapper from 'components/toast_wrapper';
|
|||
import Pluggable from 'plugins/pluggable';
|
||||
import Constants, {PostListRowListIds, EventTypes, PostRequestTypes} from 'utils/constants';
|
||||
import DelayedAction from 'utils/delayed_action';
|
||||
import {isChannelPopoutWindow} from 'utils/popouts/popout_windows';
|
||||
import {getPreviousPostId, getLatestPostId} from 'utils/post_utils';
|
||||
import * as Utils from 'utils/utils';
|
||||
|
||||
|
|
@ -473,7 +474,7 @@ export default class PostList extends React.PureComponent<Props, State> {
|
|||
});
|
||||
}
|
||||
|
||||
if (!this.props.isMobileView && !this.state.isSearchHintDismissed) {
|
||||
if (!this.props.isMobileView && !this.state.isSearchHintDismissed && !isChannelPopoutWindow()) {
|
||||
this.setState({
|
||||
showSearchHint: offsetFromBottom > this.showSearchHintThreshold,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import SearchResultsHeader from './search_results_header';
|
|||
|
||||
jest.mock('utils/popouts/popout_windows', () => ({
|
||||
isPopoutWindow: jest.fn(),
|
||||
isChannelPopoutWindow: jest.fn(() => false),
|
||||
}));
|
||||
|
||||
jest.mock('components/popout_button', () => ({
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import PopoutButton from 'components/popout_button';
|
|||
import WithTooltip from 'components/with_tooltip';
|
||||
|
||||
import {RHSStates} from 'utils/constants';
|
||||
import {isPopoutWindow} from 'utils/popouts/popout_windows';
|
||||
import {isChannelPopoutWindow, isPopoutWindow} from 'utils/popouts/popout_windows';
|
||||
|
||||
import type {PropsFromRedux} from './index';
|
||||
|
||||
|
|
@ -95,7 +95,7 @@ function SearchResultsHeader(props: Props) {
|
|||
{props.newWindowHandler && (
|
||||
<PopoutButton onClick={props.newWindowHandler}/>
|
||||
)}
|
||||
{!isPopoutWindow() &&
|
||||
{(!isPopoutWindow() || isChannelPopoutWindow()) &&
|
||||
<WithTooltip
|
||||
title={
|
||||
<FormattedMessage
|
||||
|
|
|
|||
|
|
@ -108,10 +108,42 @@ exports[`components/sidebar/sidebar_channel/sidebar_channel_menu should match sn
|
|||
tabindex="-1"
|
||||
>
|
||||
<li
|
||||
class="MuiButtonBase-root-JDVeC cxEgXn MuiButtonBase-root MuiMenuItem-root MuiMenuItem-gutters Mui-focusVisible MuiMenuItem-root-dXjcMb iLWjgG MuiMenuItem-root MuiMenuItem-gutters sc-grYavY jmUrfe"
|
||||
class="MuiButtonBase-root-JDVeC cxEgXn MuiButtonBase-root MuiMenuItem-root MuiMenuItem-gutters MuiMenuItem-root-dXjcMb iLWjgG MuiMenuItem-root MuiMenuItem-gutters sc-grYavY jmUrfe"
|
||||
id="channelOpenInNewWindow"
|
||||
role="menuitem"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="leading-element"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="18"
|
||||
version="1.1"
|
||||
viewBox="0 0 24 24"
|
||||
width="18"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M18 18V20H4A2 2 0 0 1 2 18V8H4V18M22 6V14A2 2 0 0 1 20 16H8A2 2 0 0 1 6 14V6A2 2 0 0 1 8 4H20A2 2 0 0 1 22 6M20 6H8V14H20Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div
|
||||
class="label-elements"
|
||||
>
|
||||
Open in new window
|
||||
</div>
|
||||
</li>
|
||||
<hr
|
||||
aria-orientation="vertical"
|
||||
class="MuiDivider-root-ermPAM jKcMpH MuiDivider-root MuiDivider-fullWidth"
|
||||
/>
|
||||
<li
|
||||
class="MuiButtonBase-root-JDVeC cxEgXn MuiButtonBase-root MuiMenuItem-root MuiMenuItem-gutters MuiMenuItem-root-dXjcMb iLWjgG MuiMenuItem-root MuiMenuItem-gutters sc-grYavY jmUrfe"
|
||||
id="markAsUnread-channel_id"
|
||||
role="menuitem"
|
||||
tabindex="0"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="leading-element"
|
||||
|
|
@ -417,10 +449,42 @@ exports[`components/sidebar/sidebar_channel/sidebar_channel_menu should match sn
|
|||
tabindex="-1"
|
||||
>
|
||||
<li
|
||||
class="MuiButtonBase-root-JDVeC cxEgXn MuiButtonBase-root MuiMenuItem-root MuiMenuItem-gutters Mui-focusVisible MuiMenuItem-root-dXjcMb iLWjgG MuiMenuItem-root MuiMenuItem-gutters sc-grYavY jmUrfe"
|
||||
class="MuiButtonBase-root-JDVeC cxEgXn MuiButtonBase-root MuiMenuItem-root MuiMenuItem-gutters MuiMenuItem-root-dXjcMb iLWjgG MuiMenuItem-root MuiMenuItem-gutters sc-grYavY jmUrfe"
|
||||
id="channelOpenInNewWindow"
|
||||
role="menuitem"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="leading-element"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="18"
|
||||
version="1.1"
|
||||
viewBox="0 0 24 24"
|
||||
width="18"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M18 18V20H4A2 2 0 0 1 2 18V8H4V18M22 6V14A2 2 0 0 1 20 16H8A2 2 0 0 1 6 14V6A2 2 0 0 1 8 4H20A2 2 0 0 1 22 6M20 6H8V14H20Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div
|
||||
class="label-elements"
|
||||
>
|
||||
Open in new window
|
||||
</div>
|
||||
</li>
|
||||
<hr
|
||||
aria-orientation="vertical"
|
||||
class="MuiDivider-root-ermPAM jKcMpH MuiDivider-root MuiDivider-fullWidth"
|
||||
/>
|
||||
<li
|
||||
class="MuiButtonBase-root-JDVeC cxEgXn MuiButtonBase-root MuiMenuItem-root MuiMenuItem-gutters MuiMenuItem-root-dXjcMb iLWjgG MuiMenuItem-root MuiMenuItem-gutters sc-grYavY jmUrfe"
|
||||
id="markAsUnread-channel_id"
|
||||
role="menuitem"
|
||||
tabindex="0"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="leading-element"
|
||||
|
|
@ -726,10 +790,42 @@ exports[`components/sidebar/sidebar_channel/sidebar_channel_menu should match sn
|
|||
tabindex="-1"
|
||||
>
|
||||
<li
|
||||
class="MuiButtonBase-root-JDVeC cxEgXn MuiButtonBase-root MuiMenuItem-root MuiMenuItem-gutters Mui-focusVisible MuiMenuItem-root-dXjcMb iLWjgG MuiMenuItem-root MuiMenuItem-gutters sc-grYavY jmUrfe"
|
||||
class="MuiButtonBase-root-JDVeC cxEgXn MuiButtonBase-root MuiMenuItem-root MuiMenuItem-gutters MuiMenuItem-root-dXjcMb iLWjgG MuiMenuItem-root MuiMenuItem-gutters sc-grYavY jmUrfe"
|
||||
id="channelOpenInNewWindow"
|
||||
role="menuitem"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="leading-element"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="18"
|
||||
version="1.1"
|
||||
viewBox="0 0 24 24"
|
||||
width="18"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M18 18V20H4A2 2 0 0 1 2 18V8H4V18M22 6V14A2 2 0 0 1 20 16H8A2 2 0 0 1 6 14V6A2 2 0 0 1 8 4H20A2 2 0 0 1 22 6M20 6H8V14H20Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div
|
||||
class="label-elements"
|
||||
>
|
||||
Open in new window
|
||||
</div>
|
||||
</li>
|
||||
<hr
|
||||
aria-orientation="vertical"
|
||||
class="MuiDivider-root-ermPAM jKcMpH MuiDivider-root MuiDivider-fullWidth"
|
||||
/>
|
||||
<li
|
||||
class="MuiButtonBase-root-JDVeC cxEgXn MuiButtonBase-root MuiMenuItem-root MuiMenuItem-gutters MuiMenuItem-root-dXjcMb iLWjgG MuiMenuItem-root MuiMenuItem-gutters sc-grYavY jmUrfe"
|
||||
id="markAsUnread-channel_id"
|
||||
role="menuitem"
|
||||
tabindex="0"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="leading-element"
|
||||
|
|
@ -1035,10 +1131,42 @@ exports[`components/sidebar/sidebar_channel/sidebar_channel_menu should show cor
|
|||
tabindex="-1"
|
||||
>
|
||||
<li
|
||||
class="MuiButtonBase-root-JDVeC cxEgXn MuiButtonBase-root MuiMenuItem-root MuiMenuItem-gutters Mui-focusVisible MuiMenuItem-root-dXjcMb iLWjgG MuiMenuItem-root MuiMenuItem-gutters sc-grYavY jmUrfe"
|
||||
class="MuiButtonBase-root-JDVeC cxEgXn MuiButtonBase-root MuiMenuItem-root MuiMenuItem-gutters MuiMenuItem-root-dXjcMb iLWjgG MuiMenuItem-root MuiMenuItem-gutters sc-grYavY jmUrfe"
|
||||
id="channelOpenInNewWindow"
|
||||
role="menuitem"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="leading-element"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="18"
|
||||
version="1.1"
|
||||
viewBox="0 0 24 24"
|
||||
width="18"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M18 18V20H4A2 2 0 0 1 2 18V8H4V18M22 6V14A2 2 0 0 1 20 16H8A2 2 0 0 1 6 14V6A2 2 0 0 1 8 4H20A2 2 0 0 1 22 6M20 6H8V14H20Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div
|
||||
class="label-elements"
|
||||
>
|
||||
Open in new window
|
||||
</div>
|
||||
</li>
|
||||
<hr
|
||||
aria-orientation="vertical"
|
||||
class="MuiDivider-root-ermPAM jKcMpH MuiDivider-root MuiDivider-fullWidth"
|
||||
/>
|
||||
<li
|
||||
class="MuiButtonBase-root-JDVeC cxEgXn MuiButtonBase-root MuiMenuItem-root MuiMenuItem-gutters MuiMenuItem-root-dXjcMb iLWjgG MuiMenuItem-root MuiMenuItem-gutters sc-grYavY jmUrfe"
|
||||
id="markAsUnread-channel_id"
|
||||
role="menuitem"
|
||||
tabindex="0"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="leading-element"
|
||||
|
|
@ -1276,10 +1404,42 @@ exports[`components/sidebar/sidebar_channel/sidebar_channel_menu should show cor
|
|||
tabindex="-1"
|
||||
>
|
||||
<li
|
||||
class="MuiButtonBase-root-JDVeC cxEgXn MuiButtonBase-root MuiMenuItem-root MuiMenuItem-gutters Mui-focusVisible MuiMenuItem-root-dXjcMb iLWjgG MuiMenuItem-root MuiMenuItem-gutters sc-grYavY jmUrfe"
|
||||
class="MuiButtonBase-root-JDVeC cxEgXn MuiButtonBase-root MuiMenuItem-root MuiMenuItem-gutters MuiMenuItem-root-dXjcMb iLWjgG MuiMenuItem-root MuiMenuItem-gutters sc-grYavY jmUrfe"
|
||||
id="channelOpenInNewWindow"
|
||||
role="menuitem"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="leading-element"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="18"
|
||||
version="1.1"
|
||||
viewBox="0 0 24 24"
|
||||
width="18"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M18 18V20H4A2 2 0 0 1 2 18V8H4V18M22 6V14A2 2 0 0 1 20 16H8A2 2 0 0 1 6 14V6A2 2 0 0 1 8 4H20A2 2 0 0 1 22 6M20 6H8V14H20Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div
|
||||
class="label-elements"
|
||||
>
|
||||
Open in new window
|
||||
</div>
|
||||
</li>
|
||||
<hr
|
||||
aria-orientation="vertical"
|
||||
class="MuiDivider-root-ermPAM jKcMpH MuiDivider-root MuiDivider-fullWidth"
|
||||
/>
|
||||
<li
|
||||
class="MuiButtonBase-root-JDVeC cxEgXn MuiButtonBase-root MuiMenuItem-root MuiMenuItem-gutters MuiMenuItem-root-dXjcMb iLWjgG MuiMenuItem-root MuiMenuItem-gutters sc-grYavY jmUrfe"
|
||||
id="markAsUnread-channel_id"
|
||||
role="menuitem"
|
||||
tabindex="0"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="leading-element"
|
||||
|
|
@ -1553,10 +1713,42 @@ exports[`components/sidebar/sidebar_channel/sidebar_channel_menu should show cor
|
|||
tabindex="-1"
|
||||
>
|
||||
<li
|
||||
class="MuiButtonBase-root-JDVeC cxEgXn MuiButtonBase-root MuiMenuItem-root MuiMenuItem-gutters Mui-focusVisible MuiMenuItem-root-dXjcMb iLWjgG MuiMenuItem-root MuiMenuItem-gutters sc-grYavY jmUrfe"
|
||||
class="MuiButtonBase-root-JDVeC cxEgXn MuiButtonBase-root MuiMenuItem-root MuiMenuItem-gutters MuiMenuItem-root-dXjcMb iLWjgG MuiMenuItem-root MuiMenuItem-gutters sc-grYavY jmUrfe"
|
||||
id="channelOpenInNewWindow"
|
||||
role="menuitem"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="leading-element"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="18"
|
||||
version="1.1"
|
||||
viewBox="0 0 24 24"
|
||||
width="18"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M18 18V20H4A2 2 0 0 1 2 18V8H4V18M22 6V14A2 2 0 0 1 20 16H8A2 2 0 0 1 6 14V6A2 2 0 0 1 8 4H20A2 2 0 0 1 22 6M20 6H8V14H20Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div
|
||||
class="label-elements"
|
||||
>
|
||||
Open in new window
|
||||
</div>
|
||||
</li>
|
||||
<hr
|
||||
aria-orientation="vertical"
|
||||
class="MuiDivider-root-ermPAM jKcMpH MuiDivider-root MuiDivider-fullWidth"
|
||||
/>
|
||||
<li
|
||||
class="MuiButtonBase-root-JDVeC cxEgXn MuiButtonBase-root MuiMenuItem-root MuiMenuItem-gutters MuiMenuItem-root-dXjcMb iLWjgG MuiMenuItem-root MuiMenuItem-gutters sc-grYavY jmUrfe"
|
||||
id="markAsUnread-channel_id"
|
||||
role="menuitem"
|
||||
tabindex="0"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="leading-element"
|
||||
|
|
@ -1859,10 +2051,42 @@ exports[`components/sidebar/sidebar_channel/sidebar_channel_menu should show cor
|
|||
tabindex="-1"
|
||||
>
|
||||
<li
|
||||
class="MuiButtonBase-root-JDVeC cxEgXn MuiButtonBase-root MuiMenuItem-root MuiMenuItem-gutters Mui-focusVisible MuiMenuItem-root-dXjcMb iLWjgG MuiMenuItem-root MuiMenuItem-gutters sc-grYavY jmUrfe"
|
||||
class="MuiButtonBase-root-JDVeC cxEgXn MuiButtonBase-root MuiMenuItem-root MuiMenuItem-gutters MuiMenuItem-root-dXjcMb iLWjgG MuiMenuItem-root MuiMenuItem-gutters sc-grYavY jmUrfe"
|
||||
id="channelOpenInNewWindow"
|
||||
role="menuitem"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="leading-element"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="18"
|
||||
version="1.1"
|
||||
viewBox="0 0 24 24"
|
||||
width="18"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M18 18V20H4A2 2 0 0 1 2 18V8H4V18M22 6V14A2 2 0 0 1 20 16H8A2 2 0 0 1 6 14V6A2 2 0 0 1 8 4H20A2 2 0 0 1 22 6M20 6H8V14H20Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div
|
||||
class="label-elements"
|
||||
>
|
||||
Open in new window
|
||||
</div>
|
||||
</li>
|
||||
<hr
|
||||
aria-orientation="vertical"
|
||||
class="MuiDivider-root-ermPAM jKcMpH MuiDivider-root MuiDivider-fullWidth"
|
||||
/>
|
||||
<li
|
||||
class="MuiButtonBase-root-JDVeC cxEgXn MuiButtonBase-root MuiMenuItem-root MuiMenuItem-gutters MuiMenuItem-root-dXjcMb iLWjgG MuiMenuItem-root MuiMenuItem-gutters sc-grYavY jmUrfe"
|
||||
id="markAsUnread-channel_id"
|
||||
role="menuitem"
|
||||
tabindex="0"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="leading-element"
|
||||
|
|
@ -2168,10 +2392,42 @@ exports[`components/sidebar/sidebar_channel/sidebar_channel_menu should show cor
|
|||
tabindex="-1"
|
||||
>
|
||||
<li
|
||||
class="MuiButtonBase-root-JDVeC cxEgXn MuiButtonBase-root MuiMenuItem-root MuiMenuItem-gutters Mui-focusVisible MuiMenuItem-root-dXjcMb iLWjgG MuiMenuItem-root MuiMenuItem-gutters sc-grYavY jmUrfe"
|
||||
class="MuiButtonBase-root-JDVeC cxEgXn MuiButtonBase-root MuiMenuItem-root MuiMenuItem-gutters MuiMenuItem-root-dXjcMb iLWjgG MuiMenuItem-root MuiMenuItem-gutters sc-grYavY jmUrfe"
|
||||
id="channelOpenInNewWindow"
|
||||
role="menuitem"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="leading-element"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="18"
|
||||
version="1.1"
|
||||
viewBox="0 0 24 24"
|
||||
width="18"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M18 18V20H4A2 2 0 0 1 2 18V8H4V18M22 6V14A2 2 0 0 1 20 16H8A2 2 0 0 1 6 14V6A2 2 0 0 1 8 4H20A2 2 0 0 1 22 6M20 6H8V14H20Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div
|
||||
class="label-elements"
|
||||
>
|
||||
Open in new window
|
||||
</div>
|
||||
</li>
|
||||
<hr
|
||||
aria-orientation="vertical"
|
||||
class="MuiDivider-root-ermPAM jKcMpH MuiDivider-root MuiDivider-fullWidth"
|
||||
/>
|
||||
<li
|
||||
class="MuiButtonBase-root-JDVeC cxEgXn MuiButtonBase-root MuiMenuItem-root MuiMenuItem-gutters MuiMenuItem-root-dXjcMb iLWjgG MuiMenuItem-root MuiMenuItem-gutters sc-grYavY jmUrfe"
|
||||
id="markAsUnread-channel_id"
|
||||
role="menuitem"
|
||||
tabindex="0"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="leading-element"
|
||||
|
|
@ -2477,10 +2733,42 @@ exports[`components/sidebar/sidebar_channel/sidebar_channel_menu should show cor
|
|||
tabindex="-1"
|
||||
>
|
||||
<li
|
||||
class="MuiButtonBase-root-JDVeC cxEgXn MuiButtonBase-root MuiMenuItem-root MuiMenuItem-gutters Mui-focusVisible MuiMenuItem-root-dXjcMb iLWjgG MuiMenuItem-root MuiMenuItem-gutters sc-grYavY jmUrfe"
|
||||
class="MuiButtonBase-root-JDVeC cxEgXn MuiButtonBase-root MuiMenuItem-root MuiMenuItem-gutters MuiMenuItem-root-dXjcMb iLWjgG MuiMenuItem-root MuiMenuItem-gutters sc-grYavY jmUrfe"
|
||||
id="channelOpenInNewWindow"
|
||||
role="menuitem"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="leading-element"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="18"
|
||||
version="1.1"
|
||||
viewBox="0 0 24 24"
|
||||
width="18"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M18 18V20H4A2 2 0 0 1 2 18V8H4V18M22 6V14A2 2 0 0 1 20 16H8A2 2 0 0 1 6 14V6A2 2 0 0 1 8 4H20A2 2 0 0 1 22 6M20 6H8V14H20Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div
|
||||
class="label-elements"
|
||||
>
|
||||
Open in new window
|
||||
</div>
|
||||
</li>
|
||||
<hr
|
||||
aria-orientation="vertical"
|
||||
class="MuiDivider-root-ermPAM jKcMpH MuiDivider-root MuiDivider-fullWidth"
|
||||
/>
|
||||
<li
|
||||
class="MuiButtonBase-root-JDVeC cxEgXn MuiButtonBase-root MuiMenuItem-root MuiMenuItem-gutters MuiMenuItem-root-dXjcMb iLWjgG MuiMenuItem-root MuiMenuItem-gutters sc-grYavY jmUrfe"
|
||||
id="markAsUnread-channel_id"
|
||||
role="menuitem"
|
||||
tabindex="0"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="leading-element"
|
||||
|
|
@ -2786,10 +3074,42 @@ exports[`components/sidebar/sidebar_channel/sidebar_channel_menu should show cor
|
|||
tabindex="-1"
|
||||
>
|
||||
<li
|
||||
class="MuiButtonBase-root-JDVeC cxEgXn MuiButtonBase-root MuiMenuItem-root MuiMenuItem-gutters Mui-focusVisible MuiMenuItem-root-dXjcMb iLWjgG MuiMenuItem-root MuiMenuItem-gutters sc-grYavY jmUrfe"
|
||||
class="MuiButtonBase-root-JDVeC cxEgXn MuiButtonBase-root MuiMenuItem-root MuiMenuItem-gutters MuiMenuItem-root-dXjcMb iLWjgG MuiMenuItem-root MuiMenuItem-gutters sc-grYavY jmUrfe"
|
||||
id="channelOpenInNewWindow"
|
||||
role="menuitem"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="leading-element"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="18"
|
||||
version="1.1"
|
||||
viewBox="0 0 24 24"
|
||||
width="18"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M18 18V20H4A2 2 0 0 1 2 18V8H4V18M22 6V14A2 2 0 0 1 20 16H8A2 2 0 0 1 6 14V6A2 2 0 0 1 8 4H20A2 2 0 0 1 22 6M20 6H8V14H20Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div
|
||||
class="label-elements"
|
||||
>
|
||||
Open in new window
|
||||
</div>
|
||||
</li>
|
||||
<hr
|
||||
aria-orientation="vertical"
|
||||
class="MuiDivider-root-ermPAM jKcMpH MuiDivider-root MuiDivider-fullWidth"
|
||||
/>
|
||||
<li
|
||||
class="MuiButtonBase-root-JDVeC cxEgXn MuiButtonBase-root MuiMenuItem-root MuiMenuItem-gutters MuiMenuItem-root-dXjcMb iLWjgG MuiMenuItem-root MuiMenuItem-gutters sc-grYavY jmUrfe"
|
||||
id="markAsRead-channel_id"
|
||||
role="menuitem"
|
||||
tabindex="0"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="leading-element"
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import {
|
|||
ExitToAppIcon,
|
||||
} from '@mattermost/compass-icons/components';
|
||||
|
||||
import MenuItemOpenInNewWindow from 'components/channel_header_menu/menu_items/open_in_new_window';
|
||||
import ChannelInviteModal from 'components/channel_invite_modal';
|
||||
import ChannelMoveToSubmenu from 'components/channel_move_to_sub_menu';
|
||||
import * as Menu from 'components/menu';
|
||||
|
|
@ -298,6 +299,7 @@ const SidebarChannelMenu = ({
|
|||
onToggle: onMenuToggle,
|
||||
}}
|
||||
>
|
||||
<MenuItemOpenInNewWindow channel={channel}/>
|
||||
{markAsReadUnreadMenuItem}
|
||||
{favoriteUnfavoriteMenuItem}
|
||||
{muteUnmuteChannelMenuItem}
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ jest.mock('hooks/useReadout', () => ({
|
|||
}));
|
||||
jest.mock('utils/popouts/popout_windows', () => ({
|
||||
canPopout: jest.fn(() => true),
|
||||
isThreadPopoutWindow: jest.fn(() => false),
|
||||
popoutThread: jest.fn(),
|
||||
}));
|
||||
|
||||
|
|
|
|||
|
|
@ -20,10 +20,11 @@ import {manuallyMarkThreadAsUnread} from 'actions/views/threads';
|
|||
|
||||
import * as Menu from 'components/menu';
|
||||
import {focusPost} from 'components/permalink_view/actions';
|
||||
import PopoutMenuItem from 'components/popout_menu_item';
|
||||
import {getThreadPopoutTitle} from 'components/thread_popout/thread_popout';
|
||||
|
||||
import {useReadout} from 'hooks/useReadout';
|
||||
import {canPopout, popoutThread} from 'utils/popouts/popout_windows';
|
||||
import {isThreadPopoutWindow, popoutThread} from 'utils/popouts/popout_windows';
|
||||
import {getSiteURL} from 'utils/url';
|
||||
import {copyToClipboard} from 'utils/utils';
|
||||
|
||||
|
|
@ -121,18 +122,7 @@ function ThreadMenu({
|
|||
id: `${idPrefix}-dropdown-${threadId}`,
|
||||
}}
|
||||
>
|
||||
{canPopout() && (
|
||||
<Menu.Item
|
||||
labels={
|
||||
<FormattedMessage
|
||||
id='threading.threadMenu.openInNewWindow'
|
||||
defaultMessage='Open in new window'
|
||||
/>
|
||||
}
|
||||
onClick={popout}
|
||||
leadingElement={<i className='icon icon-dock-window'/>}
|
||||
/>
|
||||
)}
|
||||
{!isThreadPopoutWindow(team, threadId) && <PopoutMenuItem onClick={popout}/>}
|
||||
<Menu.Item
|
||||
labels={isFollowing ? (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import {getHistory} from 'utils/browser_history';
|
|||
import Constants from 'utils/constants';
|
||||
import {isToday} from 'utils/datetime';
|
||||
import {isKeyPressed} from 'utils/keyboard';
|
||||
import {isChannelPopoutWindow} from 'utils/popouts/popout_windows';
|
||||
import {isIdNotPost} from 'utils/post_utils';
|
||||
|
||||
import './toast__wrapper.scss';
|
||||
|
|
@ -29,7 +30,7 @@ const TOAST_REL_RANGES = [
|
|||
RelativeRanges.TODAY_YESTERDAY,
|
||||
];
|
||||
|
||||
export type Props = WrappedComponentProps & RouteComponentProps<{team: string}> & {
|
||||
export type Props = WrappedComponentProps & RouteComponentProps<{team: string; path?: string; identifier?: string}> & {
|
||||
channelMarkedAsUnread?: boolean;
|
||||
postListIds: string[];
|
||||
latestPostTimeStamp?: number;
|
||||
|
|
@ -341,6 +342,11 @@ export class ToastWrapperClass extends React.PureComponent<Props, State> {
|
|||
changeUrlToRemountChannelView = () => {
|
||||
const {match} = this.props;
|
||||
|
||||
if (isChannelPopoutWindow() && match.params.path && match.params.identifier) {
|
||||
getHistory().replace(`/_popout/channel/${match.params.team}/${match.params.path}/${match.params.identifier}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Inorder of mount the channel view we are redirecting to /team url to load the channel again
|
||||
// Todo: Can be changed to dispatch if we put focussedPostId in redux state.
|
||||
getHistory().replace(`/${match.params.team}`);
|
||||
|
|
|
|||
|
|
@ -5539,6 +5539,7 @@
|
|||
"pluggable.errorOccurred": "An error occurred in the {pluginId} plugin.",
|
||||
"pluggable.errorRefresh": "Refresh?",
|
||||
"pluginsMenu.more_actions": "More actions",
|
||||
"popout_menu_item.openInNewWindow": "Open in new window",
|
||||
"post_body.check_for_out_of_channel_groups_mentions.message": "did not get notified by this mention because they are not in the channel. They cannot be added to the channel because they are not a member of the linked groups. To add them to this channel, they must be added to the linked groups.",
|
||||
"post_body.check_for_out_of_channel_mentions.link.and": " and ",
|
||||
"post_body.check_for_out_of_channel_mentions.link.private": "add them to this private channel",
|
||||
|
|
@ -6382,7 +6383,6 @@
|
|||
"threading.threadMenu.markUnread": "Mark as unread",
|
||||
"threading.threadMenu.openInChannel": "Open in channel",
|
||||
"threading.threadMenu.openingChannel": "Opening channel",
|
||||
"threading.threadMenu.openInNewWindow": "Open in new window",
|
||||
"threading.threadMenu.save": "Save",
|
||||
"threading.threadMenu.saved": "Saved",
|
||||
"threading.threadMenu.unfollow": "Unfollow thread",
|
||||
|
|
|
|||
|
|
@ -172,4 +172,48 @@ describe('Channel Utils', () => {
|
|||
expect(icon).toBe(GlobeIcon);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getChannelRoutePathAndIdentifier', () => {
|
||||
test('should return channels path and channel name for open channels', () => {
|
||||
const channel = {type: Constants.OPEN_CHANNEL, name: 'town-square'} as Channel;
|
||||
const result = Utils.getChannelRoutePathAndIdentifier(channel);
|
||||
expect(result).toEqual({path: 'channels', identifier: 'town-square'});
|
||||
});
|
||||
|
||||
test('should return channels path and channel name for private channels', () => {
|
||||
const channel = {type: Constants.PRIVATE_CHANNEL, name: 'secret-ops'} as Channel;
|
||||
const result = Utils.getChannelRoutePathAndIdentifier(channel);
|
||||
expect(result).toEqual({path: 'channels', identifier: 'secret-ops'});
|
||||
});
|
||||
|
||||
test('should return messages path and @username for DM channels when dmUsername is provided', () => {
|
||||
const channel = {type: Constants.DM_CHANNEL, name: 'user1__user2'} as Channel;
|
||||
const result = Utils.getChannelRoutePathAndIdentifier(channel, 'johndoe');
|
||||
expect(result).toEqual({path: 'messages', identifier: '@johndoe'});
|
||||
});
|
||||
|
||||
test('should return messages path and channel name for DM channels when dmUsername is not provided', () => {
|
||||
const channel = {type: Constants.DM_CHANNEL, name: 'user1__user2'} as Channel;
|
||||
const result = Utils.getChannelRoutePathAndIdentifier(channel);
|
||||
expect(result).toEqual({path: 'messages', identifier: 'user1__user2'});
|
||||
});
|
||||
|
||||
test('should return messages path and group id for GM channels', () => {
|
||||
const channel = {type: Constants.GM_CHANNEL, name: 'abcdef1234567890abcdef1234567890abcdefgh'} as Channel;
|
||||
const result = Utils.getChannelRoutePathAndIdentifier(channel);
|
||||
expect(result).toEqual({path: 'messages', identifier: 'abcdef1234567890abcdef1234567890abcdefgh'});
|
||||
});
|
||||
|
||||
test('should ignore dmUsername for non-DM channels', () => {
|
||||
const channel = {type: Constants.OPEN_CHANNEL, name: 'town-square'} as Channel;
|
||||
const result = Utils.getChannelRoutePathAndIdentifier(channel, 'johndoe');
|
||||
expect(result).toEqual({path: 'channels', identifier: 'town-square'});
|
||||
});
|
||||
|
||||
test('should ignore dmUsername for GM channels', () => {
|
||||
const channel = {type: Constants.GM_CHANNEL, name: 'groupid1234'} as Channel;
|
||||
const result = Utils.getChannelRoutePathAndIdentifier(channel, 'johndoe');
|
||||
expect(result).toEqual({path: 'messages', identifier: 'groupid1234'});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -153,6 +153,19 @@ export function joinPrivateChannelPrompt(team: Team, channelDisplayName: string,
|
|||
};
|
||||
}
|
||||
|
||||
export function getChannelRoutePathAndIdentifier(channel: Pick<Channel, 'type' | 'name'>, dmUsername?: string): {path: string; identifier: string} {
|
||||
if (channel.type === Constants.DM_CHANNEL) {
|
||||
return {
|
||||
path: 'messages',
|
||||
identifier: dmUsername ? `@${dmUsername}` : channel.name,
|
||||
};
|
||||
}
|
||||
if (channel.type === Constants.GM_CHANNEL) {
|
||||
return {path: 'messages', identifier: channel.name};
|
||||
}
|
||||
return {path: 'channels', identifier: channel.name};
|
||||
}
|
||||
|
||||
export function makeNewEmptyChannel(displayName: string, teamId: string): Channel {
|
||||
return {
|
||||
team_id: teamId,
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import DesktopApp from 'utils/desktop_api';
|
|||
import {getBasePath} from 'utils/url';
|
||||
import {isDesktopApp} from 'utils/user_agent';
|
||||
|
||||
import {FOCUS_REPLY_POST, popoutRhsPlugin, popoutRhsSearch, popoutThread} from './popout_windows';
|
||||
import {FOCUS_REPLY_POST, popoutChannel, popoutRhsPlugin, popoutRhsSearch, popoutThread} from './popout_windows';
|
||||
|
||||
jest.mock('utils/desktop_api', () => ({
|
||||
__esModule: true,
|
||||
|
|
@ -177,6 +177,49 @@ describe('popout_windows', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('popoutChannel', () => {
|
||||
it('should include the path segment in the URL for regular channels', async () => {
|
||||
setupBrowser();
|
||||
|
||||
await popoutChannel('Title', 'test-team', 'channels', 'town-square');
|
||||
|
||||
expect(getMockSetupBrowserPopout()).toHaveBeenCalledWith(
|
||||
'/_popout/channel/test-team/channels/town-square',
|
||||
);
|
||||
});
|
||||
|
||||
it('should include the messages path for DM/GM channels', async () => {
|
||||
setupBrowser();
|
||||
|
||||
await popoutChannel('Title', 'test-team', 'messages', '@otheruser');
|
||||
|
||||
expect(getMockSetupBrowserPopout()).toHaveBeenCalledWith(
|
||||
'/_popout/channel/test-team/messages/@otheruser',
|
||||
);
|
||||
});
|
||||
|
||||
it('should include subpath in popout URL when basename is set', async () => {
|
||||
setupBrowser('/company/mattermost');
|
||||
|
||||
await popoutChannel('Title', 'test-team', 'channels', 'town-square');
|
||||
|
||||
expect(getMockSetupBrowserPopout()).toHaveBeenCalledWith(
|
||||
'/company/mattermost/_popout/channel/test-team/channels/town-square',
|
||||
);
|
||||
});
|
||||
|
||||
it('should call desktop popout with correct path for desktop app', async () => {
|
||||
setupDesktop();
|
||||
|
||||
await popoutChannel('Channel Title', 'test-team', 'channels', 'town-square');
|
||||
|
||||
expect(mockDesktopApp.setupDesktopPopout).toHaveBeenCalledWith(
|
||||
'/_popout/channel/test-team/channels/town-square',
|
||||
{titleTemplate: 'Channel Title'},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('popoutRhsSearch', () => {
|
||||
async function callDesktop(...args: Parameters<typeof popoutRhsSearch>) {
|
||||
setupDesktop();
|
||||
|
|
|
|||
|
|
@ -113,6 +113,17 @@ export async function popoutRhsSearch(
|
|||
},
|
||||
);
|
||||
}
|
||||
export async function popoutChannel(
|
||||
titleTemplate: string,
|
||||
teamName: string,
|
||||
path: string,
|
||||
channelIdentifier: string,
|
||||
) {
|
||||
return popout(
|
||||
`${getBasePath()}/_popout/channel/${teamName}/${path}/${channelIdentifier}`,
|
||||
{titleTemplate},
|
||||
);
|
||||
}
|
||||
|
||||
export async function popoutHelp() {
|
||||
return popout(
|
||||
|
|
@ -171,6 +182,10 @@ export function isPopoutWindow() {
|
|||
return window.location.href.startsWith(`${Client4.getUrl()}/_popout/`);
|
||||
}
|
||||
|
||||
export function isChannelPopoutWindow() {
|
||||
return window.location.href.startsWith(`${Client4.getUrl()}/_popout/channel/`);
|
||||
}
|
||||
|
||||
export function isThreadPopoutWindow(teamName: string, threadId: string) {
|
||||
return window.location.href.startsWith(`${Client4.getUrl()}/_popout/thread/${teamName}/${threadId}`);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue