Skip to content

Commit

Permalink
[MM-55153] Consolidate Desktop App API, use new contextBridge endpoin…
Browse files Browse the repository at this point in the history
…ts when available (mattermost#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
  • Loading branch information
devinbinnie authored Dec 4, 2023
1 parent 8bf9e4c commit d62122b
Show file tree
Hide file tree
Showing 16 changed files with 402 additions and 180 deletions.
1 change: 1 addition & 0 deletions webapp/channels/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
20 changes: 2 additions & 18 deletions webapp/channels/src/actions/notification_actions.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ jest.mock('components/reset_status_modal', () => () => <div/>);
jest.mock('components/sidebar', () => () => <div/>);
jest.mock('components/channel_layout/center_channel', () => () => <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('plugins/pluggable', () => () => <div/>);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -57,7 +57,7 @@ export default function ChannelController(props: Props) {
className='channel-view'
data-testid='channel_view'
>
<FaviconTitleHandler/>
<UnreadsStatusHandler/>
<ProductNoticesModal/>
<div className={classNames('container-fluid channel-view-inner')}>
{props.shouldRenderCenterChannel ? <CenterChannel/> : <LoadingScreen centered={true}/>}
Expand Down
13 changes: 3 additions & 10 deletions webapp/channels/src/components/desktop_auth_token.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<boolean>;
};
}
}

enum DesktopAuthStatus {
None,
WaitingForBrowser,
Expand Down Expand Up @@ -71,8 +65,7 @@ const DesktopAuthToken: React.FC<Props> = ({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);

Expand Down
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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`
Expand Down Expand Up @@ -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 (
<HistoryButtonsContainer>
Expand Down
7 changes: 3 additions & 4 deletions webapp/channels/src/components/logged_in/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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));
};

Expand Down
82 changes: 22 additions & 60 deletions webapp/channels/src/components/logged_in/logged_in.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';
Expand All @@ -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;
};
Expand All @@ -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> {
private cleanupDesktopListeners?: () => void;

constructor(props: Props) {
super(props);

Expand Down Expand Up @@ -92,16 +78,13 @@ export default class LoggedIn extends React.PureComponent<Props> {
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()) {
Expand Down Expand Up @@ -133,7 +116,8 @@ export default class LoggedIn extends React.PureComponent<Props> {

window.removeEventListener('focus', this.onFocusListener);
window.removeEventListener('blur', this.onBlurListener);
window.removeEventListener('message', this.onDesktopMessageListener);

this.cleanupDesktopListeners?.();
}

public render(): React.ReactNode {
Expand Down Expand Up @@ -164,44 +148,22 @@ export default class LoggedIn extends React.PureComponent<Props> {
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 => {
Expand Down
9 changes: 8 additions & 1 deletion webapp/channels/src/components/login/login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -239,6 +240,7 @@ const Login = ({onCustomizeHeader}: LoginProps) => {
const onDismissSessionExpired = useCallback(() => {
LocalStorageStore.setWasLoggedIn(false);
setSessionExpired(false);
DesktopApp.setSessionExpired(false);
dismissAlert();
}, []);

Expand Down Expand Up @@ -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}`);
Expand All @@ -455,6 +460,8 @@ const Login = ({onCustomizeHeader}: LoginProps) => {

window.removeEventListener('resize', onWindowResize);
window.removeEventListener('focus', onWindowFocus);

DesktopApp.setSessionExpired(false);
};
}, []);

Expand Down
Loading

0 comments on commit d62122b

Please sign in to comment.