[MM-55153] Consolidate Desktop App API, use new contextBridge endpoints when available (#25438)
* Create Desktop API module, migrate message passing code * Changes to use new API * Use Desktop API to notify when mentions/unreads/expired changes * Expose Desktop App module to plugins * Fix lint * PR feedback * Fixed an issue where I forgot to check if the method exists first * Slight API changes * Fix package * Convert all to class, add comments, small reworks for PR feedback
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
8bf9e4c481
Коммит
d62122b884
@@ -14,6 +14,7 @@
|
|||||||
"@mattermost/client": "*",
|
"@mattermost/client": "*",
|
||||||
"@mattermost/compass-components": "^0.2.12",
|
"@mattermost/compass-components": "^0.2.12",
|
||||||
"@mattermost/compass-icons": "0.1.39",
|
"@mattermost/compass-icons": "0.1.39",
|
||||||
|
"@mattermost/desktop-api": "5.7.0-1",
|
||||||
"@mattermost/types": "*",
|
"@mattermost/types": "*",
|
||||||
"@mui/base": "5.0.0-alpha.127",
|
"@mui/base": "5.0.0-alpha.127",
|
||||||
"@mui/material": "5.11.16",
|
"@mui/material": "5.11.16",
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import {isThreadOpen} from 'selectors/views/threads';
|
|||||||
|
|
||||||
import {getHistory} from 'utils/browser_history';
|
import {getHistory} from 'utils/browser_history';
|
||||||
import Constants, {NotificationLevels, UserStatuses, IgnoreChannelMentions} from 'utils/constants';
|
import Constants, {NotificationLevels, UserStatuses, IgnoreChannelMentions} from 'utils/constants';
|
||||||
|
import DesktopApp from 'utils/desktop_api';
|
||||||
import {t} from 'utils/i18n';
|
import {t} from 'utils/i18n';
|
||||||
import {stripMarkdown, formatWithRenderer} from 'utils/markdown';
|
import {stripMarkdown, formatWithRenderer} from 'utils/markdown';
|
||||||
import MentionableRenderer from 'utils/markdown/mentionable_renderer';
|
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) => {
|
export const notifyMe = (title, body, channel, teamId, silent, soundName, url) => (dispatch) => {
|
||||||
// handle notifications in desktop app
|
// handle notifications in desktop app
|
||||||
if (isDesktopApp()) {
|
if (isDesktopApp()) {
|
||||||
const msg = {
|
DesktopApp.dispatchNotification(title, body, channel.id, teamId, silent, soundName, url);
|
||||||
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,
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
showNotification({
|
showNotification({
|
||||||
title,
|
title,
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ jest.mock('components/reset_status_modal', () => () => <div/>);
|
|||||||
jest.mock('components/sidebar', () => () => <div/>);
|
jest.mock('components/sidebar', () => () => <div/>);
|
||||||
jest.mock('components/channel_layout/center_channel', () => () => <div/>);
|
jest.mock('components/channel_layout/center_channel', () => () => <div/>);
|
||||||
jest.mock('components/loading_screen', () => () => <div/>);
|
jest.mock('components/loading_screen', () => () => <div/>);
|
||||||
jest.mock('components/favicon_title_handler', () => () => <div/>);
|
jest.mock('components/unreads_status_handler', () => () => <div/>);
|
||||||
jest.mock('components/product_notices_modal', () => () => <div/>);
|
jest.mock('components/product_notices_modal', () => () => <div/>);
|
||||||
jest.mock('plugins/pluggable', () => () => <div/>);
|
jest.mock('plugins/pluggable', () => () => <div/>);
|
||||||
|
|
||||||
|
|||||||
@@ -10,11 +10,11 @@ import type {DispatchFunc} from 'mattermost-redux/types/actions';
|
|||||||
import {loadStatusesForChannelAndSidebar} from 'actions/status_actions';
|
import {loadStatusesForChannelAndSidebar} from 'actions/status_actions';
|
||||||
|
|
||||||
import CenterChannel from 'components/channel_layout/center_channel';
|
import CenterChannel from 'components/channel_layout/center_channel';
|
||||||
import FaviconTitleHandler from 'components/favicon_title_handler';
|
|
||||||
import LoadingScreen from 'components/loading_screen';
|
import LoadingScreen from 'components/loading_screen';
|
||||||
import ProductNoticesModal from 'components/product_notices_modal';
|
import ProductNoticesModal from 'components/product_notices_modal';
|
||||||
import ResetStatusModal from 'components/reset_status_modal';
|
import ResetStatusModal from 'components/reset_status_modal';
|
||||||
import Sidebar from 'components/sidebar';
|
import Sidebar from 'components/sidebar';
|
||||||
|
import UnreadsStatusHandler from 'components/unreads_status_handler';
|
||||||
|
|
||||||
import Pluggable from 'plugins/pluggable';
|
import Pluggable from 'plugins/pluggable';
|
||||||
import {Constants} from 'utils/constants';
|
import {Constants} from 'utils/constants';
|
||||||
@@ -57,7 +57,7 @@ export default function ChannelController(props: Props) {
|
|||||||
className='channel-view'
|
className='channel-view'
|
||||||
data-testid='channel_view'
|
data-testid='channel_view'
|
||||||
>
|
>
|
||||||
<FaviconTitleHandler/>
|
<UnreadsStatusHandler/>
|
||||||
<ProductNoticesModal/>
|
<ProductNoticesModal/>
|
||||||
<div className={classNames('container-fluid channel-view-inner')}>
|
<div className={classNames('container-fluid channel-view-inner')}>
|
||||||
{props.shouldRenderCenterChannel ? <CenterChannel/> : <LoadingScreen centered={true}/>}
|
{props.shouldRenderCenterChannel ? <CenterChannel/> : <LoadingScreen centered={true}/>}
|
||||||
|
|||||||
@@ -14,19 +14,13 @@ import type {DispatchFunc} from 'mattermost-redux/types/actions';
|
|||||||
|
|
||||||
import {loginWithDesktopToken} from 'actions/views/login';
|
import {loginWithDesktopToken} from 'actions/views/login';
|
||||||
|
|
||||||
|
import DesktopApp from 'utils/desktop_api';
|
||||||
|
|
||||||
import './desktop_auth_token.scss';
|
import './desktop_auth_token.scss';
|
||||||
|
|
||||||
const BOTTOM_MESSAGE_TIMEOUT = 10000;
|
const BOTTOM_MESSAGE_TIMEOUT = 10000;
|
||||||
const DESKTOP_AUTH_PREFIX = 'desktop_auth_client_token';
|
const DESKTOP_AUTH_PREFIX = 'desktop_auth_client_token';
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface Window {
|
|
||||||
desktopAPI?: {
|
|
||||||
isDev?: () => Promise<boolean>;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
enum DesktopAuthStatus {
|
enum DesktopAuthStatus {
|
||||||
None,
|
None,
|
||||||
WaitingForBrowser,
|
WaitingForBrowser,
|
||||||
@@ -71,8 +65,7 @@ const DesktopAuthToken: React.FC<Props> = ({href, onLogin}: Props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const openExternalLoginURL = async () => {
|
const openExternalLoginURL = async () => {
|
||||||
const isDev = await window.desktopAPI?.isDev?.();
|
const desktopToken = `${DesktopApp.isDev() ? 'dev-' : ''}${crypto.randomBytes(32).toString('hex')}`.slice(0, 64);
|
||||||
const desktopToken = `${isDev ? 'dev-' : ''}${crypto.randomBytes(32).toString('hex')}`.slice(0, 64);
|
|
||||||
sessionStorage.setItem(DESKTOP_AUTH_PREFIX, desktopToken);
|
sessionStorage.setItem(DESKTOP_AUTH_PREFIX, desktopToken);
|
||||||
const parsedURL = new URL(href);
|
const parsedURL = new URL(href);
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// 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 {useHistory} from 'react-router-dom';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
@@ -18,6 +18,7 @@ import OverlayTrigger from 'components/overlay_trigger';
|
|||||||
import Tooltip from 'components/tooltip';
|
import Tooltip from 'components/tooltip';
|
||||||
|
|
||||||
import Constants from 'utils/constants';
|
import Constants from 'utils/constants';
|
||||||
|
import DesktopApp from 'utils/desktop_api';
|
||||||
import * as Utils from 'utils/utils';
|
import * as Utils from 'utils/utils';
|
||||||
|
|
||||||
const HistoryButtonsContainer = styled.nav`
|
const HistoryButtonsContainer = styled.nav`
|
||||||
@@ -49,45 +50,29 @@ const HistoryButtons = (): JSX.Element => {
|
|||||||
const goBack = () => {
|
const goBack = () => {
|
||||||
trackEvent('ui', 'ui_history_back');
|
trackEvent('ui', 'ui_history_back');
|
||||||
history.goBack();
|
history.goBack();
|
||||||
window.postMessage(
|
requestButtons();
|
||||||
{
|
|
||||||
type: 'history-button',
|
|
||||||
},
|
|
||||||
window.location.origin,
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const goForward = () => {
|
const goForward = () => {
|
||||||
trackEvent('ui', 'ui_history_forward');
|
trackEvent('ui', 'ui_history_forward');
|
||||||
history.goForward();
|
history.goForward();
|
||||||
window.postMessage(
|
requestButtons();
|
||||||
{
|
|
||||||
type: 'history-button',
|
|
||||||
},
|
|
||||||
window.location.origin,
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleButtonMessage = useCallback((message: {origin: string; data: {type: string; message: {enableBack: boolean; enableForward: boolean}}}) => {
|
const requestButtons = async () => {
|
||||||
if (message.origin !== window.location.origin) {
|
const {canGoBack, canGoForward} = await DesktopApp.getBrowserHistoryStatus();
|
||||||
return;
|
updateButtons(canGoBack, canGoForward);
|
||||||
}
|
};
|
||||||
|
|
||||||
switch (message.data.type) {
|
const updateButtons = (enableBack: boolean, enableForward: boolean) => {
|
||||||
case 'history-button-return': {
|
setCanGoBack(enableBack);
|
||||||
setCanGoBack(message.data.message.enableBack);
|
setCanGoForward(enableForward);
|
||||||
setCanGoForward(message.data.message.enableForward);
|
};
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
window.addEventListener('message', handleButtonMessage);
|
const off = DesktopApp.onBrowserHistoryStatusUpdated(updateButtons);
|
||||||
return () => {
|
return off;
|
||||||
window.removeEventListener('message', handleButtonMessage);
|
}, []);
|
||||||
};
|
|
||||||
}, [handleButtonMessage]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HistoryButtonsContainer>
|
<HistoryButtonsContainer>
|
||||||
|
|||||||
@@ -5,11 +5,9 @@ import {connect} from 'react-redux';
|
|||||||
import {bindActionCreators} from 'redux';
|
import {bindActionCreators} from 'redux';
|
||||||
import type {Dispatch} from 'redux';
|
import type {Dispatch} from 'redux';
|
||||||
|
|
||||||
import type {Channel} from '@mattermost/types/channels';
|
|
||||||
|
|
||||||
import {markChannelAsViewedOnServer, updateApproximateViewTime} from 'mattermost-redux/actions/channels';
|
import {markChannelAsViewedOnServer, updateApproximateViewTime} from 'mattermost-redux/actions/channels';
|
||||||
import {autoUpdateTimezone} from 'mattermost-redux/actions/timezone';
|
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 {getLicense, getConfig} from 'mattermost-redux/selectors/entities/general';
|
||||||
import {getCurrentUser, shouldShowTermsOfService} from 'mattermost-redux/selectors/entities/users';
|
import {getCurrentUser, shouldShowTermsOfService} from 'mattermost-redux/selectors/entities/users';
|
||||||
import type {DispatchFunc, GenericAction} from 'mattermost-redux/types/actions';
|
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
|
// 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();
|
const state = getState();
|
||||||
|
|
||||||
if (url && isPermalinkURL(url)) {
|
if (url && isPermalinkURL(url)) {
|
||||||
return getHistory().push(url);
|
return getHistory().push(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const channel = getChannel(state, channelId);
|
||||||
return getHistory().push(getChannelURL(state, channel, teamId));
|
return getHistory().push(getChannelURL(state, channel, teamId));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -3,9 +3,7 @@
|
|||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {Redirect} from 'react-router-dom';
|
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 type {UserProfile} from '@mattermost/types/users';
|
||||||
|
|
||||||
import * as GlobalActions from 'actions/global_actions';
|
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 WebSocketClient from 'client/web_websocket_client';
|
||||||
import Constants from 'utils/constants';
|
import Constants from 'utils/constants';
|
||||||
|
import DesktopApp from 'utils/desktop_api';
|
||||||
import {isKeyPressed} from 'utils/keyboard';
|
import {isKeyPressed} from 'utils/keyboard';
|
||||||
import {getBrowserTimezone} from 'utils/timezone';
|
import {getBrowserTimezone} from 'utils/timezone';
|
||||||
import * as UserAgent from 'utils/user_agent';
|
import * as UserAgent from 'utils/user_agent';
|
||||||
@@ -36,7 +35,7 @@ export type Props = {
|
|||||||
mfaRequired: boolean;
|
mfaRequired: boolean;
|
||||||
actions: {
|
actions: {
|
||||||
autoUpdateTimezone: (deviceTimezone: string) => void;
|
autoUpdateTimezone: (deviceTimezone: string) => void;
|
||||||
getChannelURLAction: (channel: Channel, teamId: string, url: string) => void;
|
getChannelURLAction: (channelId: string, teamId: string, url: string) => void;
|
||||||
markChannelAsViewedOnServer: (channelId: string) => void;
|
markChannelAsViewedOnServer: (channelId: string) => void;
|
||||||
updateApproximateViewTime: (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<Props> {
|
export default class LoggedIn extends React.PureComponent<Props> {
|
||||||
|
private cleanupDesktopListeners?: () => void;
|
||||||
|
|
||||||
constructor(props: Props) {
|
constructor(props: Props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
@@ -92,16 +78,13 @@ export default class LoggedIn extends React.PureComponent<Props> {
|
|||||||
GlobalActions.emitBrowserFocus(false);
|
GlobalActions.emitBrowserFocus(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Listen for messages from the desktop app
|
// Listen for user activity and notifications from the Desktop App (if applicable)
|
||||||
window.addEventListener('message', this.onDesktopMessageListener);
|
const offUserActivity = DesktopApp.onUserActivityUpdate(this.updateActiveStatus);
|
||||||
|
const offNotificationClicked = DesktopApp.onNotificationClicked(this.clickNotification);
|
||||||
// Tell the desktop app the webapp is ready
|
this.cleanupDesktopListeners = () => {
|
||||||
window.postMessage(
|
offUserActivity();
|
||||||
{
|
offNotificationClicked();
|
||||||
type: 'webapp-ready',
|
};
|
||||||
},
|
|
||||||
window.location.origin,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Device tracking setup
|
// Device tracking setup
|
||||||
if (UserAgent.isIos()) {
|
if (UserAgent.isIos()) {
|
||||||
@@ -133,7 +116,8 @@ export default class LoggedIn extends React.PureComponent<Props> {
|
|||||||
|
|
||||||
window.removeEventListener('focus', this.onFocusListener);
|
window.removeEventListener('focus', this.onFocusListener);
|
||||||
window.removeEventListener('blur', this.onBlurListener);
|
window.removeEventListener('blur', this.onBlurListener);
|
||||||
window.removeEventListener('message', this.onDesktopMessageListener);
|
|
||||||
|
this.cleanupDesktopListeners?.();
|
||||||
}
|
}
|
||||||
|
|
||||||
public render(): React.ReactNode {
|
public render(): React.ReactNode {
|
||||||
@@ -164,44 +148,22 @@ export default class LoggedIn extends React.PureComponent<Props> {
|
|||||||
GlobalActions.emitBrowserFocus(false);
|
GlobalActions.emitBrowserFocus(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
// listen for messages from the desktop app
|
private updateActiveStatus = (userIsActive: boolean, idleTime: number, manual: boolean) => {
|
||||||
// TODO: This needs to be deprecated in favour of a more solid Desktop App API.
|
|
||||||
private onDesktopMessageListener = (desktopMessage: DesktopMessage) => {
|
|
||||||
if (!this.props.currentUser) {
|
if (!this.props.currentUser) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (desktopMessage.origin !== window.location.origin) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (desktopMessage.data.type) {
|
// update the server with the users current away status
|
||||||
case 'register-desktop': {
|
if (userIsActive === true || userIsActive === false) {
|
||||||
// Currently used by calls
|
WebSocketClient.userUpdateActiveStatus(userIsActive, manual);
|
||||||
const {version} = desktopMessage.data.message;
|
|
||||||
if (!window.desktop) {
|
|
||||||
window.desktop = {};
|
|
||||||
}
|
|
||||||
window.desktop.version = semver.valid(semver.coerce(version));
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
case 'user-activity-update': {
|
};
|
||||||
const {userIsActive, manual} = desktopMessage.data.message;
|
|
||||||
|
|
||||||
// update the server with the users current away status
|
private clickNotification = (channelId: string, teamId: string, url: string) => {
|
||||||
if (userIsActive === true || userIsActive === false) {
|
window.focus();
|
||||||
WebSocketClient.userUpdateActiveStatus(userIsActive, manual);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'notification-clicked': {
|
|
||||||
const {channel, teamId, url} = desktopMessage.data.message;
|
|
||||||
window.focus();
|
|
||||||
|
|
||||||
// navigate to the appropriate channel
|
// navigate to the appropriate channel
|
||||||
this.props.actions.getChannelURLAction(channel, teamId, url);
|
this.props.actions.getChannelURLAction(channelId, teamId, url);
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
private handleBackSpace = (e: KeyboardEvent): void => {
|
private handleBackSpace = (e: KeyboardEvent): void => {
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ import Input, {SIZE} from 'components/widgets/inputs/input/input';
|
|||||||
import PasswordInput from 'components/widgets/inputs/password_input/password_input';
|
import PasswordInput from 'components/widgets/inputs/password_input/password_input';
|
||||||
|
|
||||||
import Constants from 'utils/constants';
|
import Constants from 'utils/constants';
|
||||||
|
import DesktopApp from 'utils/desktop_api';
|
||||||
import {t} from 'utils/i18n';
|
import {t} from 'utils/i18n';
|
||||||
import {showNotification} from 'utils/notifications';
|
import {showNotification} from 'utils/notifications';
|
||||||
import {isDesktopApp} from 'utils/user_agent';
|
import {isDesktopApp} from 'utils/user_agent';
|
||||||
@@ -239,6 +240,7 @@ const Login = ({onCustomizeHeader}: LoginProps) => {
|
|||||||
const onDismissSessionExpired = useCallback(() => {
|
const onDismissSessionExpired = useCallback(() => {
|
||||||
LocalStorageStore.setWasLoggedIn(false);
|
LocalStorageStore.setWasLoggedIn(false);
|
||||||
setSessionExpired(false);
|
setSessionExpired(false);
|
||||||
|
DesktopApp.setSessionExpired(false);
|
||||||
dismissAlert();
|
dismissAlert();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -430,9 +432,12 @@ const Login = ({onCustomizeHeader}: LoginProps) => {
|
|||||||
// our session after we use it to complete the sign in change.
|
// our session after we use it to complete the sign in change.
|
||||||
LocalStorageStore.setWasLoggedIn(false);
|
LocalStorageStore.setWasLoggedIn(false);
|
||||||
} else {
|
} else {
|
||||||
|
setSessionExpired(true);
|
||||||
|
DesktopApp.setSessionExpired(true);
|
||||||
|
|
||||||
// Although the authority remains the local sessionExpired bit on the state, set this
|
// Although the authority remains the local sessionExpired bit on the state, set this
|
||||||
// extra field in the querystring to signal the desktop app.
|
// 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);
|
const newSearchParam = new URLSearchParams(search);
|
||||||
newSearchParam.set('extra', Constants.SESSION_EXPIRED);
|
newSearchParam.set('extra', Constants.SESSION_EXPIRED);
|
||||||
history.replace(`${pathname}?${newSearchParam}`);
|
history.replace(`${pathname}?${newSearchParam}`);
|
||||||
@@ -455,6 +460,8 @@ const Login = ({onCustomizeHeader}: LoginProps) => {
|
|||||||
|
|
||||||
window.removeEventListener('resize', onWindowResize);
|
window.removeEventListener('resize', onWindowResize);
|
||||||
window.removeEventListener('focus', onWindowFocus);
|
window.removeEventListener('focus', onWindowFocus);
|
||||||
|
|
||||||
|
DesktopApp.setSessionExpired(false);
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|||||||
@@ -15,11 +15,11 @@ import {getConfig} from 'mattermost-redux/selectors/entities/general';
|
|||||||
import {getCurrentTeam} from 'mattermost-redux/selectors/entities/teams';
|
import {getCurrentTeam} from 'mattermost-redux/selectors/entities/teams';
|
||||||
import type {GenericAction} from 'mattermost-redux/types/actions';
|
import type {GenericAction} from 'mattermost-redux/types/actions';
|
||||||
|
|
||||||
import FaviconTitleHandler from './favicon_title_handler';
|
import UnreadsStatusHandler from './unreads_status_handler';
|
||||||
|
|
||||||
type Props = RouteChildrenProps;
|
type Props = RouteChildrenProps;
|
||||||
|
|
||||||
function mapStateToProps(state: GlobalState, {location: {pathname}}: Props): ComponentProps<typeof FaviconTitleHandler> {
|
function mapStateToProps(state: GlobalState, {location: {pathname}}: Props): ComponentProps<typeof UnreadsStatusHandler> {
|
||||||
const config = getConfig(state);
|
const config = getConfig(state);
|
||||||
const currentChannel = getCurrentChannel(state);
|
const currentChannel = getCurrentChannel(state);
|
||||||
const currentTeammate = (currentChannel && currentChannel.teammate_id) ? currentChannel : null;
|
const currentTeammate = (currentChannel && currentChannel.teammate_id) ? currentChannel : null;
|
||||||
@@ -43,4 +43,4 @@ function mapDispatchToProps(dispatch: Dispatch<GenericAction>) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(FaviconTitleHandler));
|
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(UnreadsStatusHandler));
|
||||||
@@ -8,15 +8,15 @@ import type {ComponentProps} from 'react';
|
|||||||
import type {ChannelType} from '@mattermost/types/channels';
|
import type {ChannelType} from '@mattermost/types/channels';
|
||||||
import type {TeamType} from '@mattermost/types/teams';
|
import type {TeamType} from '@mattermost/types/teams';
|
||||||
|
|
||||||
import FaviconTitleHandler from 'components/favicon_title_handler/favicon_title_handler';
|
import UnreadsStatusHandler from 'components/unreads_status_handler/unreads_status_handler';
|
||||||
import type {FaviconTitleHandlerClass} from 'components/favicon_title_handler/favicon_title_handler';
|
import type {UnreadsStatusHandlerClass} from 'components/unreads_status_handler/unreads_status_handler';
|
||||||
|
|
||||||
import {shallowWithIntl} from 'tests/helpers/intl-test-helper';
|
import {shallowWithIntl} from 'tests/helpers/intl-test-helper';
|
||||||
import {Constants} from 'utils/constants';
|
import {Constants} from 'utils/constants';
|
||||||
import {TestHelper} from 'utils/test_helper';
|
import {TestHelper} from 'utils/test_helper';
|
||||||
import {isChrome, isFirefox} from 'utils/user_agent';
|
import {isChrome, isFirefox} from 'utils/user_agent';
|
||||||
|
|
||||||
type Props = ComponentProps<typeof FaviconTitleHandlerClass>;
|
type Props = ComponentProps<typeof UnreadsStatusHandlerClass>;
|
||||||
|
|
||||||
jest.mock('utils/user_agent', () => {
|
jest.mock('utils/user_agent', () => {
|
||||||
const original = jest.requireActual('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 = {
|
const defaultProps = {
|
||||||
unreadStatus: false,
|
unreadStatus: false,
|
||||||
siteName: 'Test site',
|
siteName: 'Test site',
|
||||||
@@ -51,8 +51,8 @@ describe('components/FaviconTitleHandler', () => {
|
|||||||
|
|
||||||
test('set correctly the title when needed', () => {
|
test('set correctly the title when needed', () => {
|
||||||
const wrapper = shallowWithIntl(
|
const wrapper = shallowWithIntl(
|
||||||
<FaviconTitleHandler {...defaultProps}/>,
|
<UnreadsStatusHandler {...defaultProps}/>,
|
||||||
) as unknown as ShallowWrapper<Props, any, FaviconTitleHandlerClass>;
|
) as unknown as ShallowWrapper<Props, any, UnreadsStatusHandlerClass>;
|
||||||
const instance = wrapper.instance();
|
const instance = wrapper.instance();
|
||||||
instance.updateTitle();
|
instance.updateTitle();
|
||||||
instance.componentDidUpdate = jest.fn();
|
instance.componentDidUpdate = jest.fn();
|
||||||
@@ -91,8 +91,8 @@ describe('components/FaviconTitleHandler', () => {
|
|||||||
(isFirefox as jest.Mock).mockImplementation(() => false);
|
(isFirefox as jest.Mock).mockImplementation(() => false);
|
||||||
(isChrome as jest.Mock).mockImplementation(() => false);
|
(isChrome as jest.Mock).mockImplementation(() => false);
|
||||||
const wrapper = shallowWithIntl(
|
const wrapper = shallowWithIntl(
|
||||||
<FaviconTitleHandler {...defaultProps}/>,
|
<UnreadsStatusHandler {...defaultProps}/>,
|
||||||
) as unknown as ShallowWrapper<Props, any, FaviconTitleHandlerClass>;
|
) as unknown as ShallowWrapper<Props, any, UnreadsStatusHandlerClass>;
|
||||||
const instance = wrapper.instance();
|
const instance = wrapper.instance();
|
||||||
|
|
||||||
wrapper.setProps({
|
wrapper.setProps({
|
||||||
@@ -115,8 +115,8 @@ describe('components/FaviconTitleHandler', () => {
|
|||||||
document.head.appendChild(link);
|
document.head.appendChild(link);
|
||||||
|
|
||||||
const wrapper = shallowWithIntl(
|
const wrapper = shallowWithIntl(
|
||||||
<FaviconTitleHandler {...defaultProps}/>,
|
<UnreadsStatusHandler {...defaultProps}/>,
|
||||||
) as unknown as ShallowWrapper<Props, any, FaviconTitleHandlerClass>;
|
) as unknown as ShallowWrapper<Props, any, UnreadsStatusHandlerClass>;
|
||||||
const instance = wrapper.instance();
|
const instance = wrapper.instance();
|
||||||
instance.updateFavicon = jest.fn();
|
instance.updateFavicon = jest.fn();
|
||||||
|
|
||||||
@@ -138,13 +138,13 @@ describe('components/FaviconTitleHandler', () => {
|
|||||||
|
|
||||||
test('should display correct title when in drafts', () => {
|
test('should display correct title when in drafts', () => {
|
||||||
const wrapper = shallowWithIntl(
|
const wrapper = shallowWithIntl(
|
||||||
<FaviconTitleHandler
|
<UnreadsStatusHandler
|
||||||
{...defaultProps}
|
{...defaultProps}
|
||||||
inDrafts={true}
|
inDrafts={true}
|
||||||
currentChannel={undefined}
|
currentChannel={undefined}
|
||||||
siteName={undefined}
|
siteName={undefined}
|
||||||
/>,
|
/>,
|
||||||
) as unknown as ShallowWrapper<Props, any, FaviconTitleHandlerClass>;
|
) as unknown as ShallowWrapper<Props, any, UnreadsStatusHandlerClass>;
|
||||||
wrapper.instance().updateTitle();
|
wrapper.instance().updateTitle();
|
||||||
|
|
||||||
expect(document.title).toBe('Drafts - Test team display name');
|
expect(document.title).toBe('Drafts - Test team display name');
|
||||||
@@ -27,6 +27,7 @@ import faviconUnread32x32 from 'images/favicon/favicon-unread-32x32.png';
|
|||||||
import faviconUnread64x64 from 'images/favicon/favicon-unread-64x64.png';
|
import faviconUnread64x64 from 'images/favicon/favicon-unread-64x64.png';
|
||||||
import faviconUnread96x96 from 'images/favicon/favicon-unread-96x96.png';
|
import faviconUnread96x96 from 'images/favicon/favicon-unread-96x96.png';
|
||||||
import {Constants} from 'utils/constants';
|
import {Constants} from 'utils/constants';
|
||||||
|
import DesktopApp from 'utils/desktop_api';
|
||||||
import * as UserAgent from 'utils/user_agent';
|
import * as UserAgent from 'utils/user_agent';
|
||||||
|
|
||||||
enum BadgeStatus {
|
enum BadgeStatus {
|
||||||
@@ -46,7 +47,7 @@ type Props = {
|
|||||||
inDrafts: boolean;
|
inDrafts: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export class FaviconTitleHandlerClass extends React.PureComponent<Props> {
|
export class UnreadsStatusHandlerClass extends React.PureComponent<Props> {
|
||||||
componentDidUpdate(prevProps: Props) {
|
componentDidUpdate(prevProps: Props) {
|
||||||
this.updateTitle();
|
this.updateTitle();
|
||||||
const oldBadgeStatus = this.getBadgeStatus(prevProps.unreadStatus);
|
const oldBadgeStatus = this.getBadgeStatus(prevProps.unreadStatus);
|
||||||
@@ -55,6 +56,8 @@ export class FaviconTitleHandlerClass extends React.PureComponent<Props> {
|
|||||||
if (oldBadgeStatus !== newBadgeStatus) {
|
if (oldBadgeStatus !== newBadgeStatus) {
|
||||||
this.updateFavicon(newBadgeStatus);
|
this.updateFavicon(newBadgeStatus);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.updateDesktopApp();
|
||||||
}
|
}
|
||||||
|
|
||||||
get isDynamicFaviconSupported() {
|
get isDynamicFaviconSupported() {
|
||||||
@@ -70,6 +73,13 @@ export class FaviconTitleHandlerClass extends React.PureComponent<Props> {
|
|||||||
return BadgeStatus.None;
|
return BadgeStatus.None;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateDesktopApp = () => {
|
||||||
|
const {unreadStatus} = this.props;
|
||||||
|
const {isUnread, unreadMentionCount} = basicUnreadMeta(unreadStatus);
|
||||||
|
|
||||||
|
DesktopApp.updateUnreadsAndMentions(isUnread, unreadMentionCount);
|
||||||
|
};
|
||||||
|
|
||||||
updateTitle = () => {
|
updateTitle = () => {
|
||||||
const {
|
const {
|
||||||
siteName,
|
siteName,
|
||||||
@@ -170,4 +180,4 @@ export class FaviconTitleHandlerClass extends React.PureComponent<Props> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default injectIntl(FaviconTitleHandlerClass);
|
export default injectIntl(UnreadsStatusHandlerClass);
|
||||||
@@ -19,6 +19,7 @@ import Avatar from 'components/widgets/users/avatar';
|
|||||||
|
|
||||||
import {getHistory} from 'utils/browser_history';
|
import {getHistory} from 'utils/browser_history';
|
||||||
import {ModalIdentifiers} from 'utils/constants';
|
import {ModalIdentifiers} from 'utils/constants';
|
||||||
|
import DesktopApp from 'utils/desktop_api';
|
||||||
import messageHtmlToComponent from 'utils/message_html_to_component';
|
import messageHtmlToComponent from 'utils/message_html_to_component';
|
||||||
import * as NotificationSounds from 'utils/notification_sounds';
|
import * as NotificationSounds from 'utils/notification_sounds';
|
||||||
import {formatText} from 'utils/text_formatting';
|
import {formatText} from 'utils/text_formatting';
|
||||||
@@ -103,3 +104,6 @@ window.ProductApi = {
|
|||||||
getRhsSelectedPostId: getSelectedPostId,
|
getRhsSelectedPostId: getSelectedPostId,
|
||||||
getIsRhsOpen,
|
getIsRhsOpen,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Desktop App module containing the app info and a series of helpers to work with legacy code
|
||||||
|
window.DesktopApp = DesktopApp;
|
||||||
|
|||||||
@@ -5,57 +5,27 @@ import {createBrowserHistory} from 'history';
|
|||||||
import type {History} from 'history';
|
import type {History} from 'history';
|
||||||
|
|
||||||
import {getModule} from 'module_registry';
|
import {getModule} from 'module_registry';
|
||||||
|
import DesktopApp from 'utils/desktop_api';
|
||||||
import {isServerVersionGreaterThanOrEqualTo} from 'utils/server_version';
|
import {isServerVersionGreaterThanOrEqualTo} from 'utils/server_version';
|
||||||
import {isDesktopApp, getDesktopVersion} from 'utils/user_agent';
|
import {isDesktopApp, getDesktopVersion} from 'utils/user_agent';
|
||||||
|
|
||||||
const b = createBrowserHistory({basename: window.basename});
|
const b = createBrowserHistory({basename: window.basename});
|
||||||
const isDesktop = isDesktopApp() && isServerVersionGreaterThanOrEqualTo(getDesktopVersion(), '5.0.0');
|
const isDesktop = isDesktopApp() && isServerVersionGreaterThanOrEqualTo(getDesktopVersion(), '5.0.0');
|
||||||
|
|
||||||
type Data = {
|
|
||||||
type?: string;
|
|
||||||
message?: Record<string, string>;
|
|
||||||
}
|
|
||||||
|
|
||||||
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 = {
|
const browserHistory = {
|
||||||
...b,
|
...b,
|
||||||
push: (path: string | { pathname: string }, ...args: string[]) => {
|
push: (path: string | { pathname: string }, ...args: string[]) => {
|
||||||
if (isDesktop) {
|
if (isDesktop) {
|
||||||
window.postMessage(
|
DesktopApp.doBrowserHistoryPush(typeof path === 'object' ? path.pathname : path);
|
||||||
{
|
|
||||||
type: 'browser-history-push',
|
|
||||||
message: {
|
|
||||||
path: typeof path === 'object' ? path.pathname : path,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
window.location.origin,
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
b.push(path, ...args);
|
b.push(path, ...args);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (isDesktop) {
|
||||||
|
DesktopApp.onBrowserHistoryPush((pathName) => b.push(pathName));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the current history object.
|
* Returns the current history object.
|
||||||
*
|
*
|
||||||
|
|||||||
293
webapp/channels/src/utils/desktop_api.ts
Обычный файл
293
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<DesktopAPI>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DesktopAppAPI {
|
||||||
|
private name?: string;
|
||||||
|
private version?: string | null;
|
||||||
|
private dev?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated
|
||||||
|
*/
|
||||||
|
private postMessageListeners?: Map<string, Set<(message: unknown) => 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<void, {name: string; version: string}>(
|
||||||
|
'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<void, {enableBack: boolean; enableForward: boolean}>(
|
||||||
|
'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 = <T, T2>(
|
||||||
|
sendChannel: string,
|
||||||
|
sendData?: T,
|
||||||
|
receiveChannel?: string,
|
||||||
|
) => {
|
||||||
|
return new Promise<T2>((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;
|
||||||
14
webapp/package-lock.json
сгенерированный
14
webapp/package-lock.json
сгенерированный
@@ -60,6 +60,7 @@
|
|||||||
"@mattermost/client": "*",
|
"@mattermost/client": "*",
|
||||||
"@mattermost/compass-components": "^0.2.12",
|
"@mattermost/compass-components": "^0.2.12",
|
||||||
"@mattermost/compass-icons": "0.1.39",
|
"@mattermost/compass-icons": "0.1.39",
|
||||||
|
"@mattermost/desktop-api": "5.7.0-1",
|
||||||
"@mattermost/types": "*",
|
"@mattermost/types": "*",
|
||||||
"@mui/base": "5.0.0-alpha.127",
|
"@mui/base": "5.0.0-alpha.127",
|
||||||
"@mui/material": "5.11.16",
|
"@mui/material": "5.11.16",
|
||||||
@@ -4084,6 +4085,19 @@
|
|||||||
"resolved": "platform/components",
|
"resolved": "platform/components",
|
||||||
"link": true
|
"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": {
|
"node_modules/@mattermost/eslint-plugin": {
|
||||||
"resolved": "platform/eslint-plugin",
|
"resolved": "platform/eslint-plugin",
|
||||||
"link": true
|
"link": true
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user