diff --git a/webapp/channels/package.json b/webapp/channels/package.json
index dfffbad49c58..ea4e7bdc5203 100644
--- a/webapp/channels/package.json
+++ b/webapp/channels/package.json
@@ -14,6 +14,7 @@
"@mattermost/client": "*",
"@mattermost/compass-components": "^0.2.12",
"@mattermost/compass-icons": "0.1.39",
+ "@mattermost/desktop-api": "5.7.0-1",
"@mattermost/types": "*",
"@mui/base": "5.0.0-alpha.127",
"@mui/material": "5.11.16",
diff --git a/webapp/channels/src/actions/notification_actions.jsx b/webapp/channels/src/actions/notification_actions.jsx
index 2728d7f9e284..0b3d184e729b 100644
--- a/webapp/channels/src/actions/notification_actions.jsx
+++ b/webapp/channels/src/actions/notification_actions.jsx
@@ -20,6 +20,7 @@ import {isThreadOpen} from 'selectors/views/threads';
import {getHistory} from 'utils/browser_history';
import Constants, {NotificationLevels, UserStatuses, IgnoreChannelMentions} from 'utils/constants';
+import DesktopApp from 'utils/desktop_api';
import {t} from 'utils/i18n';
import {stripMarkdown, formatWithRenderer} from 'utils/markdown';
import MentionableRenderer from 'utils/markdown/mentionable_renderer';
@@ -312,24 +313,7 @@ export function sendDesktopNotification(post, msgProps) {
export const notifyMe = (title, body, channel, teamId, silent, soundName, url) => (dispatch) => {
// handle notifications in desktop app
if (isDesktopApp()) {
- const msg = {
- title,
- body,
- channel,
- teamId,
- silent,
- };
- msg.data = {soundName};
- msg.url = url;
-
- // get the desktop app to trigger the notification
- window.postMessage(
- {
- type: 'dispatch-notification',
- message: msg,
- },
- window.location.origin,
- );
+ DesktopApp.dispatchNotification(title, body, channel.id, teamId, silent, soundName, url);
} else {
showNotification({
title,
diff --git a/webapp/channels/src/components/channel_layout/channel_controller.test.tsx b/webapp/channels/src/components/channel_layout/channel_controller.test.tsx
index baf16b2099f6..c5dd75e5d5db 100644
--- a/webapp/channels/src/components/channel_layout/channel_controller.test.tsx
+++ b/webapp/channels/src/components/channel_layout/channel_controller.test.tsx
@@ -9,7 +9,7 @@ jest.mock('components/reset_status_modal', () => () =>
);
jest.mock('components/sidebar', () => () => );
jest.mock('components/channel_layout/center_channel', () => () => );
jest.mock('components/loading_screen', () => () => );
-jest.mock('components/favicon_title_handler', () => () => );
+jest.mock('components/unreads_status_handler', () => () => );
jest.mock('components/product_notices_modal', () => () => );
jest.mock('plugins/pluggable', () => () => );
diff --git a/webapp/channels/src/components/channel_layout/channel_controller.tsx b/webapp/channels/src/components/channel_layout/channel_controller.tsx
index 9b8535c30759..1e3c807006dd 100644
--- a/webapp/channels/src/components/channel_layout/channel_controller.tsx
+++ b/webapp/channels/src/components/channel_layout/channel_controller.tsx
@@ -10,11 +10,11 @@ import type {DispatchFunc} from 'mattermost-redux/types/actions';
import {loadStatusesForChannelAndSidebar} from 'actions/status_actions';
import CenterChannel from 'components/channel_layout/center_channel';
-import FaviconTitleHandler from 'components/favicon_title_handler';
import LoadingScreen from 'components/loading_screen';
import ProductNoticesModal from 'components/product_notices_modal';
import ResetStatusModal from 'components/reset_status_modal';
import Sidebar from 'components/sidebar';
+import UnreadsStatusHandler from 'components/unreads_status_handler';
import Pluggable from 'plugins/pluggable';
import {Constants} from 'utils/constants';
@@ -57,7 +57,7 @@ export default function ChannelController(props: Props) {
className='channel-view'
data-testid='channel_view'
>
-
+
{props.shouldRenderCenterChannel ?
:
}
diff --git a/webapp/channels/src/components/desktop_auth_token.tsx b/webapp/channels/src/components/desktop_auth_token.tsx
index 5f9bd9136538..1093b11a82b9 100644
--- a/webapp/channels/src/components/desktop_auth_token.tsx
+++ b/webapp/channels/src/components/desktop_auth_token.tsx
@@ -14,19 +14,13 @@ import type {DispatchFunc} from 'mattermost-redux/types/actions';
import {loginWithDesktopToken} from 'actions/views/login';
+import DesktopApp from 'utils/desktop_api';
+
import './desktop_auth_token.scss';
const BOTTOM_MESSAGE_TIMEOUT = 10000;
const DESKTOP_AUTH_PREFIX = 'desktop_auth_client_token';
-declare global {
- interface Window {
- desktopAPI?: {
- isDev?: () => Promise;
- };
- }
-}
-
enum DesktopAuthStatus {
None,
WaitingForBrowser,
@@ -71,8 +65,7 @@ const DesktopAuthToken: React.FC = ({href, onLogin}: Props) => {
};
const openExternalLoginURL = async () => {
- const isDev = await window.desktopAPI?.isDev?.();
- const desktopToken = `${isDev ? 'dev-' : ''}${crypto.randomBytes(32).toString('hex')}`.slice(0, 64);
+ const desktopToken = `${DesktopApp.isDev() ? 'dev-' : ''}${crypto.randomBytes(32).toString('hex')}`.slice(0, 64);
sessionStorage.setItem(DESKTOP_AUTH_PREFIX, desktopToken);
const parsedURL = new URL(href);
diff --git a/webapp/channels/src/components/global_header/left_controls/history_buttons/history_buttons.tsx b/webapp/channels/src/components/global_header/left_controls/history_buttons/history_buttons.tsx
index 8e80862accdc..4caaf600cf2b 100644
--- a/webapp/channels/src/components/global_header/left_controls/history_buttons/history_buttons.tsx
+++ b/webapp/channels/src/components/global_header/left_controls/history_buttons/history_buttons.tsx
@@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
-import React, {useEffect, useState, useCallback} from 'react';
+import React, {useEffect, useState} from 'react';
import {useHistory} from 'react-router-dom';
import styled from 'styled-components';
@@ -18,6 +18,7 @@ import OverlayTrigger from 'components/overlay_trigger';
import Tooltip from 'components/tooltip';
import Constants from 'utils/constants';
+import DesktopApp from 'utils/desktop_api';
import * as Utils from 'utils/utils';
const HistoryButtonsContainer = styled.nav`
@@ -49,45 +50,29 @@ const HistoryButtons = (): JSX.Element => {
const goBack = () => {
trackEvent('ui', 'ui_history_back');
history.goBack();
- window.postMessage(
- {
- type: 'history-button',
- },
- window.location.origin,
- );
+ requestButtons();
};
const goForward = () => {
trackEvent('ui', 'ui_history_forward');
history.goForward();
- window.postMessage(
- {
- type: 'history-button',
- },
- window.location.origin,
- );
+ requestButtons();
};
- const handleButtonMessage = useCallback((message: {origin: string; data: {type: string; message: {enableBack: boolean; enableForward: boolean}}}) => {
- if (message.origin !== window.location.origin) {
- return;
- }
-
- switch (message.data.type) {
- case 'history-button-return': {
- setCanGoBack(message.data.message.enableBack);
- setCanGoForward(message.data.message.enableForward);
- break;
- }
- }
- }, []);
+ const requestButtons = async () => {
+ const {canGoBack, canGoForward} = await DesktopApp.getBrowserHistoryStatus();
+ updateButtons(canGoBack, canGoForward);
+ };
+
+ const updateButtons = (enableBack: boolean, enableForward: boolean) => {
+ setCanGoBack(enableBack);
+ setCanGoForward(enableForward);
+ };
useEffect(() => {
- window.addEventListener('message', handleButtonMessage);
- return () => {
- window.removeEventListener('message', handleButtonMessage);
- };
- }, [handleButtonMessage]);
+ const off = DesktopApp.onBrowserHistoryStatusUpdated(updateButtons);
+ return off;
+ }, []);
return (
diff --git a/webapp/channels/src/components/logged_in/index.ts b/webapp/channels/src/components/logged_in/index.ts
index 6c7e9103c4f1..2776b14f9c3a 100644
--- a/webapp/channels/src/components/logged_in/index.ts
+++ b/webapp/channels/src/components/logged_in/index.ts
@@ -5,11 +5,9 @@ import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import type {Dispatch} from 'redux';
-import type {Channel} from '@mattermost/types/channels';
-
import {markChannelAsViewedOnServer, updateApproximateViewTime} from 'mattermost-redux/actions/channels';
import {autoUpdateTimezone} from 'mattermost-redux/actions/timezone';
-import {getCurrentChannelId, isManuallyUnread} from 'mattermost-redux/selectors/entities/channels';
+import {getChannel, getCurrentChannelId, isManuallyUnread} from 'mattermost-redux/selectors/entities/channels';
import {getLicense, getConfig} from 'mattermost-redux/selectors/entities/general';
import {getCurrentUser, shouldShowTermsOfService} from 'mattermost-redux/selectors/entities/users';
import type {DispatchFunc, GenericAction} from 'mattermost-redux/types/actions';
@@ -46,13 +44,14 @@ function mapStateToProps(state: GlobalState, ownProps: Props) {
}
// NOTE: suggestions where to keep this welcomed
-const getChannelURLAction = (channel: Channel, teamId: string, url: string) => (dispatch: DispatchFunc, getState: () => GlobalState) => {
+const getChannelURLAction = (channelId: string, teamId: string, url: string) => (dispatch: DispatchFunc, getState: () => GlobalState) => {
const state = getState();
if (url && isPermalinkURL(url)) {
return getHistory().push(url);
}
+ const channel = getChannel(state, channelId);
return getHistory().push(getChannelURL(state, channel, teamId));
};
diff --git a/webapp/channels/src/components/logged_in/logged_in.tsx b/webapp/channels/src/components/logged_in/logged_in.tsx
index 98a6e18fef11..eb746344f8ef 100644
--- a/webapp/channels/src/components/logged_in/logged_in.tsx
+++ b/webapp/channels/src/components/logged_in/logged_in.tsx
@@ -3,9 +3,7 @@
import React from 'react';
import {Redirect} from 'react-router-dom';
-import semver from 'semver';
-import type {Channel} from '@mattermost/types/channels';
import type {UserProfile} from '@mattermost/types/users';
import * as GlobalActions from 'actions/global_actions';
@@ -16,6 +14,7 @@ import LoadingScreen from 'components/loading_screen';
import WebSocketClient from 'client/web_websocket_client';
import Constants from 'utils/constants';
+import DesktopApp from 'utils/desktop_api';
import {isKeyPressed} from 'utils/keyboard';
import {getBrowserTimezone} from 'utils/timezone';
import * as UserAgent from 'utils/user_agent';
@@ -36,7 +35,7 @@ export type Props = {
mfaRequired: boolean;
actions: {
autoUpdateTimezone: (deviceTimezone: string) => void;
- getChannelURLAction: (channel: Channel, teamId: string, url: string) => void;
+ getChannelURLAction: (channelId: string, teamId: string, url: string) => void;
markChannelAsViewedOnServer: (channelId: string) => void;
updateApproximateViewTime: (channelId: string) => void;
};
@@ -47,22 +46,9 @@ export type Props = {
};
}
-type DesktopMessage = {
- origin: string;
- data: {
- type: string;
- message: {
- version: string;
- userIsActive: boolean;
- manual: boolean;
- channel: Channel;
- teamId: string;
- url: string;
- };
- };
-}
-
export default class LoggedIn extends React.PureComponent {
+ private cleanupDesktopListeners?: () => void;
+
constructor(props: Props) {
super(props);
@@ -92,16 +78,13 @@ export default class LoggedIn extends React.PureComponent {
GlobalActions.emitBrowserFocus(false);
}
- // Listen for messages from the desktop app
- window.addEventListener('message', this.onDesktopMessageListener);
-
- // Tell the desktop app the webapp is ready
- window.postMessage(
- {
- type: 'webapp-ready',
- },
- window.location.origin,
- );
+ // Listen for user activity and notifications from the Desktop App (if applicable)
+ const offUserActivity = DesktopApp.onUserActivityUpdate(this.updateActiveStatus);
+ const offNotificationClicked = DesktopApp.onNotificationClicked(this.clickNotification);
+ this.cleanupDesktopListeners = () => {
+ offUserActivity();
+ offNotificationClicked();
+ };
// Device tracking setup
if (UserAgent.isIos()) {
@@ -133,7 +116,8 @@ export default class LoggedIn extends React.PureComponent {
window.removeEventListener('focus', this.onFocusListener);
window.removeEventListener('blur', this.onBlurListener);
- window.removeEventListener('message', this.onDesktopMessageListener);
+
+ this.cleanupDesktopListeners?.();
}
public render(): React.ReactNode {
@@ -164,44 +148,22 @@ export default class LoggedIn extends React.PureComponent {
GlobalActions.emitBrowserFocus(false);
}
- // listen for messages from the desktop app
- // TODO: This needs to be deprecated in favour of a more solid Desktop App API.
- private onDesktopMessageListener = (desktopMessage: DesktopMessage) => {
+ private updateActiveStatus = (userIsActive: boolean, idleTime: number, manual: boolean) => {
if (!this.props.currentUser) {
return;
}
- if (desktopMessage.origin !== window.location.origin) {
- return;
- }
- switch (desktopMessage.data.type) {
- case 'register-desktop': {
- // Currently used by calls
- const {version} = desktopMessage.data.message;
- if (!window.desktop) {
- window.desktop = {};
- }
- window.desktop.version = semver.valid(semver.coerce(version));
- break;
+ // update the server with the users current away status
+ if (userIsActive === true || userIsActive === false) {
+ WebSocketClient.userUpdateActiveStatus(userIsActive, manual);
}
- case 'user-activity-update': {
- const {userIsActive, manual} = desktopMessage.data.message;
+ };
- // update the server with the users current away status
- if (userIsActive === true || userIsActive === false) {
- WebSocketClient.userUpdateActiveStatus(userIsActive, manual);
- }
- break;
- }
- case 'notification-clicked': {
- const {channel, teamId, url} = desktopMessage.data.message;
- window.focus();
+ private clickNotification = (channelId: string, teamId: string, url: string) => {
+ window.focus();
- // navigate to the appropriate channel
- this.props.actions.getChannelURLAction(channel, teamId, url);
- break;
- }
- }
+ // navigate to the appropriate channel
+ this.props.actions.getChannelURLAction(channelId, teamId, url);
};
private handleBackSpace = (e: KeyboardEvent): void => {
diff --git a/webapp/channels/src/components/login/login.tsx b/webapp/channels/src/components/login/login.tsx
index 339e7db500ee..5034f05a3ec5 100644
--- a/webapp/channels/src/components/login/login.tsx
+++ b/webapp/channels/src/components/login/login.tsx
@@ -53,6 +53,7 @@ import Input, {SIZE} from 'components/widgets/inputs/input/input';
import PasswordInput from 'components/widgets/inputs/password_input/password_input';
import Constants from 'utils/constants';
+import DesktopApp from 'utils/desktop_api';
import {t} from 'utils/i18n';
import {showNotification} from 'utils/notifications';
import {isDesktopApp} from 'utils/user_agent';
@@ -239,6 +240,7 @@ const Login = ({onCustomizeHeader}: LoginProps) => {
const onDismissSessionExpired = useCallback(() => {
LocalStorageStore.setWasLoggedIn(false);
setSessionExpired(false);
+ DesktopApp.setSessionExpired(false);
dismissAlert();
}, []);
@@ -430,9 +432,12 @@ const Login = ({onCustomizeHeader}: LoginProps) => {
// our session after we use it to complete the sign in change.
LocalStorageStore.setWasLoggedIn(false);
} else {
+ setSessionExpired(true);
+ DesktopApp.setSessionExpired(true);
+
// Although the authority remains the local sessionExpired bit on the state, set this
// extra field in the querystring to signal the desktop app.
- setSessionExpired(true);
+ // This is legacy support for older Desktop Apps and can be removed eventually
const newSearchParam = new URLSearchParams(search);
newSearchParam.set('extra', Constants.SESSION_EXPIRED);
history.replace(`${pathname}?${newSearchParam}`);
@@ -455,6 +460,8 @@ const Login = ({onCustomizeHeader}: LoginProps) => {
window.removeEventListener('resize', onWindowResize);
window.removeEventListener('focus', onWindowFocus);
+
+ DesktopApp.setSessionExpired(false);
};
}, []);
diff --git a/webapp/channels/src/components/favicon_title_handler/index.ts b/webapp/channels/src/components/unreads_status_handler/index.ts
similarity index 91%
rename from webapp/channels/src/components/favicon_title_handler/index.ts
rename to webapp/channels/src/components/unreads_status_handler/index.ts
index 922f217e0621..8edb063e4022 100644
--- a/webapp/channels/src/components/favicon_title_handler/index.ts
+++ b/webapp/channels/src/components/unreads_status_handler/index.ts
@@ -15,11 +15,11 @@ import {getConfig} from 'mattermost-redux/selectors/entities/general';
import {getCurrentTeam} from 'mattermost-redux/selectors/entities/teams';
import type {GenericAction} from 'mattermost-redux/types/actions';
-import FaviconTitleHandler from './favicon_title_handler';
+import UnreadsStatusHandler from './unreads_status_handler';
type Props = RouteChildrenProps;
-function mapStateToProps(state: GlobalState, {location: {pathname}}: Props): ComponentProps {
+function mapStateToProps(state: GlobalState, {location: {pathname}}: Props): ComponentProps {
const config = getConfig(state);
const currentChannel = getCurrentChannel(state);
const currentTeammate = (currentChannel && currentChannel.teammate_id) ? currentChannel : null;
@@ -43,4 +43,4 @@ function mapDispatchToProps(dispatch: Dispatch) {
};
}
-export default withRouter(connect(mapStateToProps, mapDispatchToProps)(FaviconTitleHandler));
+export default withRouter(connect(mapStateToProps, mapDispatchToProps)(UnreadsStatusHandler));
diff --git a/webapp/channels/src/components/favicon_title_handler/favicon_title_handler.test.tsx b/webapp/channels/src/components/unreads_status_handler/unreads_status_handler.test.tsx
similarity index 85%
rename from webapp/channels/src/components/favicon_title_handler/favicon_title_handler.test.tsx
rename to webapp/channels/src/components/unreads_status_handler/unreads_status_handler.test.tsx
index f37d14f61139..25f5109a4a6f 100644
--- a/webapp/channels/src/components/favicon_title_handler/favicon_title_handler.test.tsx
+++ b/webapp/channels/src/components/unreads_status_handler/unreads_status_handler.test.tsx
@@ -8,15 +8,15 @@ import type {ComponentProps} from 'react';
import type {ChannelType} from '@mattermost/types/channels';
import type {TeamType} from '@mattermost/types/teams';
-import FaviconTitleHandler from 'components/favicon_title_handler/favicon_title_handler';
-import type {FaviconTitleHandlerClass} from 'components/favicon_title_handler/favicon_title_handler';
+import UnreadsStatusHandler from 'components/unreads_status_handler/unreads_status_handler';
+import type {UnreadsStatusHandlerClass} from 'components/unreads_status_handler/unreads_status_handler';
import {shallowWithIntl} from 'tests/helpers/intl-test-helper';
import {Constants} from 'utils/constants';
import {TestHelper} from 'utils/test_helper';
import {isChrome, isFirefox} from 'utils/user_agent';
-type Props = ComponentProps;
+type Props = ComponentProps;
jest.mock('utils/user_agent', () => {
const original = jest.requireActual('utils/user_agent');
@@ -27,7 +27,7 @@ jest.mock('utils/user_agent', () => {
};
});
-describe('components/FaviconTitleHandler', () => {
+describe('components/UnreadsStatusHandler', () => {
const defaultProps = {
unreadStatus: false,
siteName: 'Test site',
@@ -51,8 +51,8 @@ describe('components/FaviconTitleHandler', () => {
test('set correctly the title when needed', () => {
const wrapper = shallowWithIntl(
- ,
- ) as unknown as ShallowWrapper;
+ ,
+ ) as unknown as ShallowWrapper;
const instance = wrapper.instance();
instance.updateTitle();
instance.componentDidUpdate = jest.fn();
@@ -91,8 +91,8 @@ describe('components/FaviconTitleHandler', () => {
(isFirefox as jest.Mock).mockImplementation(() => false);
(isChrome as jest.Mock).mockImplementation(() => false);
const wrapper = shallowWithIntl(
- ,
- ) as unknown as ShallowWrapper;
+ ,
+ ) as unknown as ShallowWrapper;
const instance = wrapper.instance();
wrapper.setProps({
@@ -115,8 +115,8 @@ describe('components/FaviconTitleHandler', () => {
document.head.appendChild(link);
const wrapper = shallowWithIntl(
- ,
- ) as unknown as ShallowWrapper;
+ ,
+ ) as unknown as ShallowWrapper;
const instance = wrapper.instance();
instance.updateFavicon = jest.fn();
@@ -138,13 +138,13 @@ describe('components/FaviconTitleHandler', () => {
test('should display correct title when in drafts', () => {
const wrapper = shallowWithIntl(
- ,
- ) as unknown as ShallowWrapper;
+ ) as unknown as ShallowWrapper;
wrapper.instance().updateTitle();
expect(document.title).toBe('Drafts - Test team display name');
diff --git a/webapp/channels/src/components/favicon_title_handler/favicon_title_handler.tsx b/webapp/channels/src/components/unreads_status_handler/unreads_status_handler.tsx
similarity index 93%
rename from webapp/channels/src/components/favicon_title_handler/favicon_title_handler.tsx
rename to webapp/channels/src/components/unreads_status_handler/unreads_status_handler.tsx
index 2f49dbff948c..540a2c93886b 100644
--- a/webapp/channels/src/components/favicon_title_handler/favicon_title_handler.tsx
+++ b/webapp/channels/src/components/unreads_status_handler/unreads_status_handler.tsx
@@ -27,6 +27,7 @@ import faviconUnread32x32 from 'images/favicon/favicon-unread-32x32.png';
import faviconUnread64x64 from 'images/favicon/favicon-unread-64x64.png';
import faviconUnread96x96 from 'images/favicon/favicon-unread-96x96.png';
import {Constants} from 'utils/constants';
+import DesktopApp from 'utils/desktop_api';
import * as UserAgent from 'utils/user_agent';
enum BadgeStatus {
@@ -46,7 +47,7 @@ type Props = {
inDrafts: boolean;
};
-export class FaviconTitleHandlerClass extends React.PureComponent {
+export class UnreadsStatusHandlerClass extends React.PureComponent {
componentDidUpdate(prevProps: Props) {
this.updateTitle();
const oldBadgeStatus = this.getBadgeStatus(prevProps.unreadStatus);
@@ -55,6 +56,8 @@ export class FaviconTitleHandlerClass extends React.PureComponent {
if (oldBadgeStatus !== newBadgeStatus) {
this.updateFavicon(newBadgeStatus);
}
+
+ this.updateDesktopApp();
}
get isDynamicFaviconSupported() {
@@ -70,6 +73,13 @@ export class FaviconTitleHandlerClass extends React.PureComponent {
return BadgeStatus.None;
}
+ updateDesktopApp = () => {
+ const {unreadStatus} = this.props;
+ const {isUnread, unreadMentionCount} = basicUnreadMeta(unreadStatus);
+
+ DesktopApp.updateUnreadsAndMentions(isUnread, unreadMentionCount);
+ };
+
updateTitle = () => {
const {
siteName,
@@ -170,4 +180,4 @@ export class FaviconTitleHandlerClass extends React.PureComponent {
}
}
-export default injectIntl(FaviconTitleHandlerClass);
+export default injectIntl(UnreadsStatusHandlerClass);
diff --git a/webapp/channels/src/plugins/export.js b/webapp/channels/src/plugins/export.js
index a6e64f6d00cd..fb83d0444653 100644
--- a/webapp/channels/src/plugins/export.js
+++ b/webapp/channels/src/plugins/export.js
@@ -19,6 +19,7 @@ import Avatar from 'components/widgets/users/avatar';
import {getHistory} from 'utils/browser_history';
import {ModalIdentifiers} from 'utils/constants';
+import DesktopApp from 'utils/desktop_api';
import messageHtmlToComponent from 'utils/message_html_to_component';
import * as NotificationSounds from 'utils/notification_sounds';
import {formatText} from 'utils/text_formatting';
@@ -103,3 +104,6 @@ window.ProductApi = {
getRhsSelectedPostId: getSelectedPostId,
getIsRhsOpen,
};
+
+// Desktop App module containing the app info and a series of helpers to work with legacy code
+window.DesktopApp = DesktopApp;
diff --git a/webapp/channels/src/utils/browser_history.tsx b/webapp/channels/src/utils/browser_history.tsx
index 1ea8949634c1..61e26cb109b6 100644
--- a/webapp/channels/src/utils/browser_history.tsx
+++ b/webapp/channels/src/utils/browser_history.tsx
@@ -5,57 +5,27 @@ import {createBrowserHistory} from 'history';
import type {History} from 'history';
import {getModule} from 'module_registry';
+import DesktopApp from 'utils/desktop_api';
import {isServerVersionGreaterThanOrEqualTo} from 'utils/server_version';
import {isDesktopApp, getDesktopVersion} from 'utils/user_agent';
const b = createBrowserHistory({basename: window.basename});
const isDesktop = isDesktopApp() && isServerVersionGreaterThanOrEqualTo(getDesktopVersion(), '5.0.0');
-
-type Data = {
- type?: string;
- message?: Record;
-}
-
-type Params = {
- origin?: string;
- data?: Data;
-}
-
-window.addEventListener('message', ({origin, data: {type, message = {}} = {}}: Params = {}) => {
- if (origin !== window.location.origin) {
- return;
- }
-
- switch (type) {
- case 'browser-history-push-return': {
- if (message.pathName) {
- const {pathName} = message;
- b.push(pathName);
- }
- break;
- }
- }
-});
-
const browserHistory = {
...b,
push: (path: string | { pathname: string }, ...args: string[]) => {
if (isDesktop) {
- window.postMessage(
- {
- type: 'browser-history-push',
- message: {
- path: typeof path === 'object' ? path.pathname : path,
- },
- },
- window.location.origin,
- );
+ DesktopApp.doBrowserHistoryPush(typeof path === 'object' ? path.pathname : path);
} else {
b.push(path, ...args);
}
},
};
+if (isDesktop) {
+ DesktopApp.onBrowserHistoryPush((pathName) => b.push(pathName));
+}
+
/**
* Returns the current history object.
*
diff --git a/webapp/channels/src/utils/desktop_api.ts b/webapp/channels/src/utils/desktop_api.ts
new file mode 100644
index 000000000000..a05f05c6ea9c
--- /dev/null
+++ b/webapp/channels/src/utils/desktop_api.ts
@@ -0,0 +1,293 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import semver from 'semver';
+
+import type {DesktopAPI} from '@mattermost/desktop-api';
+
+import {isDesktopApp} from 'utils/user_agent';
+
+declare global {
+ interface Window {
+ desktopAPI?: Partial;
+ }
+}
+
+class DesktopAppAPI {
+ private name?: string;
+ private version?: string | null;
+ private dev?: boolean;
+
+ /**
+ * @deprecated
+ */
+ private postMessageListeners?: Map void>>;
+
+ constructor() {
+ // Check the user agent string first
+ if (!isDesktopApp()) {
+ return;
+ }
+
+ this.getDesktopAppInfo().then(({name, version}) => {
+ this.name = name;
+ this.version = semver.valid(semver.coerce(version));
+
+ // Legacy Desktop App version, used by some plugins
+ if (!window.desktop) {
+ window.desktop = {};
+ }
+ window.desktop.version = semver.valid(semver.coerce(version));
+ });
+ window.desktopAPI?.isDev?.().then((isDev) => {
+ this.dev = isDev;
+ });
+
+ // Legacy code - to be removed
+ this.postMessageListeners = new Map();
+ window.addEventListener('message', this.postMessageListener);
+ window.addEventListener('beforeunload', () => {
+ window.removeEventListener('message', this.postMessageListener);
+ });
+ }
+
+ /*******************************************************
+ * Getters/setters for Desktop App specific information
+ *******************************************************/
+
+ getAppName = () => {
+ return this.name;
+ };
+
+ getAppVersion = () => {
+ return this.version;
+ };
+
+ isDev = () => {
+ return this.dev;
+ };
+
+ private getDesktopAppInfo = () => {
+ if (window.desktopAPI?.getAppInfo) {
+ return window.desktopAPI.getAppInfo();
+ }
+
+ return this.invokeWithMessaging(
+ 'webapp-ready',
+ undefined,
+ 'register-desktop',
+ );
+ };
+
+ /**********************
+ * Exposed API methods
+ **********************/
+
+ /**
+ * Invokes
+ */
+
+ getBrowserHistoryStatus = async () => {
+ if (window.desktopAPI?.requestBrowserHistoryStatus) {
+ return window.desktopAPI.requestBrowserHistoryStatus();
+ }
+
+ const {enableBack, enableForward} = await this.invokeWithMessaging(
+ 'history-button',
+ undefined,
+ 'history-button-return',
+ );
+
+ return {
+ canGoBack: enableBack,
+ canGoForward: enableForward,
+ };
+ };
+
+ /**
+ * Listeners
+ */
+
+ onUserActivityUpdate = (listener: (userIsActive: boolean, idleTime: number, isSystemEvent: boolean) => void) => {
+ if (window.desktopAPI?.onUserActivityUpdate) {
+ return window.desktopAPI.onUserActivityUpdate(listener);
+ }
+
+ const legacyListener = ({userIsActive, manual}: {userIsActive: boolean; manual: boolean}) => listener(userIsActive, 0, manual);
+ this.addPostMessageListener('user-activity-update', legacyListener);
+
+ return () => this.removePostMessageListener('user-activity-update', legacyListener);
+ };
+
+ onNotificationClicked = (listener: (channelId: string, teamId: string, url: string) => void) => {
+ if (window.desktopAPI?.onNotificationClicked) {
+ return window.desktopAPI.onNotificationClicked(listener);
+ }
+
+ const legacyListener = ({channel, teamId, url}: {channel: {id: string}; teamId: string; url: string}) => listener(channel.id, teamId, url);
+ this.addPostMessageListener('notification-clicked', legacyListener);
+
+ return () => this.removePostMessageListener('notification-clicked', legacyListener);
+ };
+
+ onBrowserHistoryPush = (listener: (pathName: string) => void) => {
+ if (window.desktopAPI?.onBrowserHistoryPush) {
+ return window.desktopAPI.onBrowserHistoryPush(listener);
+ }
+
+ const legacyListener = ({pathName}: {pathName: string}) => listener(pathName);
+ this.addPostMessageListener('browser-history-push-return', legacyListener);
+
+ return () => this.removePostMessageListener('browser-history-push-return', legacyListener);
+ };
+
+ onBrowserHistoryStatusUpdated = (listener: (enableBack: boolean, enableForward: boolean) => void) => {
+ if (window.desktopAPI?.onBrowserHistoryStatusUpdated) {
+ return window.desktopAPI.onBrowserHistoryStatusUpdated(listener);
+ }
+
+ const legacyListener = ({enableBack, enableForward}: {enableBack: boolean; enableForward: boolean}) => listener(enableBack, enableForward);
+ this.addPostMessageListener('history-button-return', legacyListener);
+
+ return () => this.removePostMessageListener('history-button-return', legacyListener);
+ };
+
+ /**
+ * One-ways
+ */
+
+ dispatchNotification = (
+ title: string,
+ body: string,
+ channelId: string,
+ teamId: string,
+ silent: boolean,
+ soundName: string,
+ url: string,
+ ) => {
+ if (window.desktopAPI?.sendNotification) {
+ window.desktopAPI.sendNotification(title, body, channelId, teamId, url, silent, soundName);
+ return;
+ }
+
+ // get the desktop app to trigger the notification
+ window.postMessage(
+ {
+ type: 'dispatch-notification',
+ message: {
+ title,
+ body,
+ channel: {id: channelId},
+ teamId,
+ silent,
+ data: {soundName},
+ url,
+ },
+ },
+ window.location.origin,
+ );
+ };
+
+ doBrowserHistoryPush = (path: string) => {
+ if (window.desktopAPI?.sendBrowserHistoryPush) {
+ window.desktopAPI.sendBrowserHistoryPush(path);
+ return;
+ }
+
+ window.postMessage(
+ {
+ type: 'browser-history-push',
+ message: {path},
+ },
+ window.location.origin,
+ );
+ };
+
+ updateUnreadsAndMentions = (isUnread: boolean, mentionCount: number) =>
+ window.desktopAPI?.setUnreadsAndMentions && window.desktopAPI.setUnreadsAndMentions(isUnread, mentionCount);
+ setSessionExpired = (expired: boolean) => window.desktopAPI?.setSessionExpired && window.desktopAPI.setSessionExpired(expired);
+
+ /*********************************************************************
+ * Helper functions for legacy code
+ * Remove all of this once we have no use for message passing anymore
+ *********************************************************************/
+
+ /**
+ * @deprecated
+ */
+ private postMessageListener = ({origin, data: {type, message}}: {origin: string; data: {type: string; message: unknown}}) => {
+ if (origin !== window.location.origin) {
+ return;
+ }
+
+ const listeners = this.postMessageListeners?.get(type);
+ if (!listeners) {
+ return;
+ }
+
+ listeners.forEach((listener) => {
+ listener(message);
+ });
+ };
+
+ /**
+ * @deprecated
+ */
+ private addPostMessageListener = (channel: string, listener: (message: any) => void) => {
+ if (this.postMessageListeners?.has(channel)) {
+ this.postMessageListeners.set(channel, this.postMessageListeners.get(channel)!.add(listener));
+ } else {
+ this.postMessageListeners?.set(channel, new Set([listener]));
+ }
+ };
+
+ /**
+ * @deprecated
+ */
+ private removePostMessageListener = (channel: string, listener: (message: any) => void) => {
+ const set = this.postMessageListeners?.get(channel);
+ set?.delete(listener);
+ if (set?.size) {
+ this.postMessageListeners?.set(channel, set);
+ } else {
+ this.postMessageListeners?.delete(channel);
+ }
+ };
+
+ /**
+ * @deprecated
+ */
+ private invokeWithMessaging = (
+ sendChannel: string,
+ sendData?: T,
+ receiveChannel?: string,
+ ) => {
+ return new Promise((resolve) => {
+ /* create and register a temporary listener if necessary */
+ const response = ({origin, data: {type, message}}: {origin: string; data: {type: string; message: T2}}) => {
+ /* ignore messages from other frames */
+ if (origin !== window.location.origin) {
+ return;
+ }
+
+ /* ignore messages from other channels */
+ if (type !== (receiveChannel ?? sendChannel)) {
+ return;
+ }
+
+ /* clean up listener and resolve */
+ window.removeEventListener('message', response);
+ resolve(message);
+ };
+
+ window.addEventListener('message', response);
+ window.postMessage(
+ {type: sendChannel, message: sendData},
+ window.location.origin,
+ );
+ });
+ };
+}
+
+const DesktopApp = new DesktopAppAPI();
+export default DesktopApp;
diff --git a/webapp/package-lock.json b/webapp/package-lock.json
index d9c79bae6851..6b15cf024590 100644
--- a/webapp/package-lock.json
+++ b/webapp/package-lock.json
@@ -60,6 +60,7 @@
"@mattermost/client": "*",
"@mattermost/compass-components": "^0.2.12",
"@mattermost/compass-icons": "0.1.39",
+ "@mattermost/desktop-api": "5.7.0-1",
"@mattermost/types": "*",
"@mui/base": "5.0.0-alpha.127",
"@mui/material": "5.11.16",
@@ -4084,6 +4085,19 @@
"resolved": "platform/components",
"link": true
},
+ "node_modules/@mattermost/desktop-api": {
+ "version": "5.7.0-1",
+ "resolved": "https://registry.npmjs.org/@mattermost/desktop-api/-/desktop-api-5.7.0-1.tgz",
+ "integrity": "sha512-3VdmrdiGwqXpLGomRWiDt8L2LMlOyeAP+S9uKm82JpRt8HoVFeSe9fu39VV9pbnA8O6u6wfnwGUXFeasmmIIkQ==",
+ "peerDependencies": {
+ "typescript": "^4.3"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@mattermost/eslint-plugin": {
"resolved": "platform/eslint-plugin",
"link": true