deploy: current vibn theia state
Made-with: Cursor
This commit is contained in:
10
packages/messages/.eslintrc.js
Normal file
10
packages/messages/.eslintrc.js
Normal file
@@ -0,0 +1,10 @@
|
||||
/** @type {import('eslint').Linter.Config} */
|
||||
module.exports = {
|
||||
extends: [
|
||||
'../../configs/build.eslintrc.json'
|
||||
],
|
||||
parserOptions: {
|
||||
tsconfigRootDir: __dirname,
|
||||
project: 'tsconfig.json'
|
||||
}
|
||||
};
|
||||
31
packages/messages/README.md
Normal file
31
packages/messages/README.md
Normal file
@@ -0,0 +1,31 @@
|
||||
<div align='center'>
|
||||
|
||||
<br />
|
||||
|
||||
<img src='https://raw.githubusercontent.com/eclipse-theia/theia/master/logo/theia.svg?sanitize=true' alt='theia-ext-logo' width='100px' />
|
||||
|
||||
<h2>ECLIPSE THEIA - MESSAGES EXTENSION</h2>
|
||||
|
||||
<hr />
|
||||
|
||||
</div>
|
||||
|
||||
## Description
|
||||
|
||||
The `@theia/messages` extension provides the ability to display user notifications in the application.
|
||||
|
||||
## Additional Information
|
||||
|
||||
- [API documentation for `@theia/messages`](https://eclipse-theia.github.io/theia/docs/next/modules/_theia_messages.html)
|
||||
- [Theia - GitHub](https://github.com/eclipse-theia/theia)
|
||||
- [Theia - Website](https://theia-ide.org/)
|
||||
|
||||
## License
|
||||
|
||||
- [Eclipse Public License 2.0](http://www.eclipse.org/legal/epl-2.0/)
|
||||
- [一 (Secondary) GNU General Public License, version 2 with the GNU Classpath Exception](https://projects.eclipse.org/license/secondary-gpl-2.0-cp)
|
||||
|
||||
## Trademark
|
||||
|
||||
"Theia" is a trademark of the Eclipse Foundation
|
||||
<https://www.eclipse.org/theia>
|
||||
51
packages/messages/package.json
Normal file
51
packages/messages/package.json
Normal file
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"name": "@theia/messages",
|
||||
"version": "1.68.0",
|
||||
"description": "Theia - Messages Extension",
|
||||
"dependencies": {
|
||||
"@theia/core": "1.68.0",
|
||||
"react-perfect-scrollbar": "^1.5.3",
|
||||
"ts-md5": "^1.2.2",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"theiaExtensions": [
|
||||
{
|
||||
"frontend": "lib/browser/messages-frontend-module",
|
||||
"backend": "lib/node/messages-backend-module"
|
||||
}
|
||||
],
|
||||
"keywords": [
|
||||
"theia-extension"
|
||||
],
|
||||
"license": "EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/eclipse-theia/theia.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/eclipse-theia/theia/issues"
|
||||
},
|
||||
"homepage": "https://github.com/eclipse-theia/theia",
|
||||
"files": [
|
||||
"lib",
|
||||
"src"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "theiaext build",
|
||||
"clean": "theiaext clean",
|
||||
"compile": "theiaext compile",
|
||||
"lint": "theiaext lint",
|
||||
"test": "theiaext test",
|
||||
"watch": "theiaext watch"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@theia/ext-scripts": "1.68.0"
|
||||
},
|
||||
"nyc": {
|
||||
"extends": "../../configs/nyc.json"
|
||||
},
|
||||
"gitHead": "21358137e41342742707f660b8e222f940a27652"
|
||||
}
|
||||
43
packages/messages/src/browser/messages-frontend-module.ts
Normal file
43
packages/messages/src/browser/messages-frontend-module.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2017 TypeFox and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import '../../src/browser/style/index.css';
|
||||
|
||||
import { ContainerModule } from '@theia/core/shared/inversify';
|
||||
import { MessageClient } from '@theia/core/lib/common';
|
||||
import { NotificationManager } from './notifications-manager';
|
||||
import { bindNotificationPreferences } from '../common/notification-preferences';
|
||||
import { NotificationsRenderer } from './notifications-renderer';
|
||||
import { NotificationsContribution } from './notifications-contribution';
|
||||
import { FrontendApplicationContribution, KeybindingContribution, StylingParticipant } from '@theia/core/lib/browser';
|
||||
import { CommandContribution, MenuContribution } from '@theia/core';
|
||||
import { ColorContribution } from '@theia/core/lib/browser/color-application-contribution';
|
||||
import { NotificationContentRenderer } from './notification-content-renderer';
|
||||
|
||||
export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
bind(NotificationContentRenderer).toSelf().inSingletonScope();
|
||||
bind(NotificationsRenderer).toSelf().inSingletonScope();
|
||||
bind(NotificationsContribution).toSelf().inSingletonScope();
|
||||
bind(FrontendApplicationContribution).toService(NotificationsContribution);
|
||||
bind(CommandContribution).toService(NotificationsContribution);
|
||||
bind(KeybindingContribution).toService(NotificationsContribution);
|
||||
bind(ColorContribution).toService(NotificationsContribution);
|
||||
bind(StylingParticipant).toService(NotificationsContribution);
|
||||
bind(MenuContribution).toService(NotificationsContribution);
|
||||
bind(NotificationManager).toSelf().inSingletonScope();
|
||||
rebind(MessageClient).toService(NotificationManager);
|
||||
bindNotificationPreferences(bind);
|
||||
});
|
||||
@@ -0,0 +1,97 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2019 TypeFox and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import * as React from '@theia/core/shared/react';
|
||||
import { DisposableCollection } from '@theia/core';
|
||||
import { NotificationManager, NotificationUpdateEvent } from './notifications-manager';
|
||||
import { NotificationComponent } from './notification-component';
|
||||
import { codicon, ContextMenuRenderer } from '@theia/core/lib/browser';
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
|
||||
const PerfectScrollbar = require('react-perfect-scrollbar');
|
||||
|
||||
export interface NotificationCenterComponentProps {
|
||||
readonly manager: NotificationManager;
|
||||
readonly contextMenuRenderer: ContextMenuRenderer;
|
||||
}
|
||||
|
||||
type NotificationCenterComponentState = Pick<NotificationUpdateEvent, Exclude<keyof NotificationUpdateEvent, 'toasts'>>;
|
||||
|
||||
export class NotificationCenterComponent extends React.Component<NotificationCenterComponentProps, NotificationCenterComponentState> {
|
||||
|
||||
constructor(props: NotificationCenterComponentProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
notifications: [],
|
||||
visibilityState: 'hidden'
|
||||
};
|
||||
}
|
||||
|
||||
protected readonly toDisposeOnUnmount = new DisposableCollection();
|
||||
|
||||
override async componentDidMount(): Promise<void> {
|
||||
this.toDisposeOnUnmount.push(
|
||||
this.props.manager.onUpdated(({ notifications, visibilityState }) => {
|
||||
this.setState({
|
||||
notifications: notifications,
|
||||
visibilityState
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
override componentWillUnmount(): void {
|
||||
this.toDisposeOnUnmount.dispose();
|
||||
}
|
||||
|
||||
override render(): React.ReactNode {
|
||||
const empty = this.state.notifications.length === 0;
|
||||
const title = empty
|
||||
? nls.localizeByDefault('No New Notifications')
|
||||
: nls.localizeByDefault('Notifications');
|
||||
return (
|
||||
<div className={`theia-notifications-container theia-notification-center ${this.state.visibilityState === 'center' ? 'open' : 'closed'}`}>
|
||||
<div className='theia-notification-center-header'>
|
||||
<div className='theia-notification-center-header-title'>{title}</div>
|
||||
<div className='theia-notification-center-header-actions'>
|
||||
<ul className='theia-notification-actions'>
|
||||
<li className={codicon('clear-all', true)} title={nls.localizeByDefault('Clear All Notifications')}
|
||||
onClick={this.onClearAll} />
|
||||
<li className={codicon('chevron-down', true)} title={nls.localizeByDefault('Hide Notifications')}
|
||||
onClick={this.onHide} />
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<PerfectScrollbar className='theia-notification-list-scroll-container'>
|
||||
<div className='theia-notification-list'>
|
||||
{this.state.notifications.map(notification =>
|
||||
<NotificationComponent key={notification.messageId} notification={notification} manager={this.props.manager}
|
||||
contextMenuRenderer={this.props.contextMenuRenderer} />
|
||||
)}
|
||||
</div>
|
||||
</PerfectScrollbar>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
protected onHide = () => {
|
||||
this.props.manager.hideCenter();
|
||||
};
|
||||
|
||||
protected onClearAll = () => {
|
||||
this.props.manager.clearAll();
|
||||
};
|
||||
|
||||
}
|
||||
143
packages/messages/src/browser/notification-component.tsx
Normal file
143
packages/messages/src/browser/notification-component.tsx
Normal file
@@ -0,0 +1,143 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2019 TypeFox and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import * as React from '@theia/core/shared/react';
|
||||
import * as DOMPurify from '@theia/core/shared/dompurify';
|
||||
import { NotificationManager, Notification } from './notifications-manager';
|
||||
import { codicon, ContextMenuRenderer } from '@theia/core/lib/browser';
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
import { NOTIFICATION_CONTEXT_MENU } from './notifications-commands';
|
||||
|
||||
export interface NotificationComponentProps {
|
||||
readonly manager: NotificationManager;
|
||||
readonly notification: Notification;
|
||||
readonly contextMenuRenderer?: ContextMenuRenderer;
|
||||
}
|
||||
|
||||
export class NotificationComponent extends React.Component<NotificationComponentProps> {
|
||||
|
||||
constructor(props: NotificationComponentProps) {
|
||||
super(props);
|
||||
this.state = {};
|
||||
}
|
||||
|
||||
protected onClear = (event: React.MouseEvent) => {
|
||||
if (event.target instanceof HTMLElement) {
|
||||
const messageId = event.target.dataset.messageId;
|
||||
if (messageId) {
|
||||
this.props.manager.clear(messageId);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
protected onToggleExpansion = (event: React.MouseEvent) => {
|
||||
if (event.target instanceof HTMLElement) {
|
||||
const messageId = event.target.dataset.messageId;
|
||||
if (messageId) {
|
||||
this.props.manager.toggleExpansion(messageId);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
protected onAction = (event: React.MouseEvent) => {
|
||||
if (event.target instanceof HTMLElement) {
|
||||
const messageId = event.target.dataset.messageId;
|
||||
const action = event.target.dataset.action;
|
||||
if (messageId && action) {
|
||||
this.props.manager.accept(messageId, action);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
protected onMessageClick = (event: React.MouseEvent) => {
|
||||
if (event.target instanceof HTMLAnchorElement) {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
const link = event.target.href;
|
||||
this.props.manager.openLink(link);
|
||||
}
|
||||
};
|
||||
|
||||
protected onContextMenu = (event: React.MouseEvent<HTMLElement>) => {
|
||||
if (this.props.contextMenuRenderer) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
this.props.contextMenuRenderer.render({
|
||||
menuPath: NOTIFICATION_CONTEXT_MENU,
|
||||
anchor: { x: event.clientX, y: event.clientY },
|
||||
args: [this.props.notification],
|
||||
context: event.currentTarget
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
override render(): React.ReactNode {
|
||||
const { messageId, message, type, progress, collapsed, expandable, source, actions } = this.props.notification;
|
||||
const isProgress = type === 'progress' || typeof progress === 'number';
|
||||
const icon = type === 'progress' ? 'info' : type;
|
||||
return (<div key={messageId} className='theia-notification-list-item-container' onContextMenu={this.onContextMenu}>
|
||||
<div className='theia-notification-list-item' tabIndex={0}>
|
||||
<div className={`theia-notification-list-item-content ${collapsed ? 'collapsed' : ''}`}>
|
||||
<div className='theia-notification-list-item-content-main'>
|
||||
<div className={`theia-notification-icon ${codicon(icon)} ${icon}`} />
|
||||
<div className='theia-notification-message'>
|
||||
<span
|
||||
// eslint-disable-next-line react/no-danger
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: DOMPurify.sanitize(message, {
|
||||
ALLOW_UNKNOWN_PROTOCOLS: true // DOMPurify usually strips non http(s) links from hrefs
|
||||
})
|
||||
}}
|
||||
onClick={this.onMessageClick}
|
||||
/>
|
||||
</div>
|
||||
<ul className='theia-notification-actions'>
|
||||
{expandable && (
|
||||
<li className={codicon('chevron-down', true) + (collapsed ? ' expand' : ' collapse')} title={collapsed ? 'Expand' : 'Collapse'}
|
||||
data-message-id={messageId} onClick={this.onToggleExpansion} />
|
||||
)}
|
||||
{!isProgress && (<li className={codicon('close', true)} title={nls.localizeByDefault('Clear')} data-message-id={messageId}
|
||||
onClick={this.onClear} />)}
|
||||
</ul>
|
||||
</div>
|
||||
{(source || !!actions.length) && (
|
||||
<div className='theia-notification-list-item-content-bottom'>
|
||||
<div className='theia-notification-source'>
|
||||
{source && (<span>{source}</span>)}
|
||||
</div>
|
||||
<div className='theia-notification-buttons'>
|
||||
{actions && actions.map((action, index) => (
|
||||
<button key={messageId + `-action-${index}`} className='theia-button'
|
||||
data-message-id={messageId} data-action={action}
|
||||
onClick={this.onAction}>
|
||||
{action}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{isProgress && (
|
||||
<div className='theia-notification-item-progress'>
|
||||
<div className={`theia-notification-item-progressbar ${progress ? 'determinate' : 'indeterminate'}`}
|
||||
style={{ width: `${progress ?? '100'}%` }} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2020 TypeFox and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { expect } from 'chai';
|
||||
import { NotificationContentRenderer } from './notification-content-renderer';
|
||||
|
||||
describe('notification-content-renderer', () => {
|
||||
|
||||
const contentRenderer = new NotificationContentRenderer();
|
||||
|
||||
it('should remove new lines', () => {
|
||||
expectRenderedContent('foo\nbar', 'foo bar');
|
||||
expectRenderedContent('foo\n\n\nbar', 'foo bar');
|
||||
});
|
||||
|
||||
it('should render links', () => {
|
||||
expectRenderedContent(
|
||||
'Link to [theia](https://github.com/eclipse-theia/theia)!',
|
||||
'Link to <a href="https://github.com/eclipse-theia/theia">theia</a>!'
|
||||
);
|
||||
expectRenderedContent(
|
||||
'Link to [theia](https://github.com/eclipse-theia/theia "title on hover")!',
|
||||
'Link to <a href="https://github.com/eclipse-theia/theia" title="title on hover">theia</a>!'
|
||||
);
|
||||
expectRenderedContent(
|
||||
'Click [here](command:my-command-id) to open stuff!',
|
||||
'Click <a href="command:my-command-id">here</a> to open stuff!'
|
||||
);
|
||||
expectRenderedContent(
|
||||
'Click [here](javascript:window.alert();) to open stuff!',
|
||||
'Click [here](javascript:window.alert();) to open stuff!'
|
||||
);
|
||||
});
|
||||
|
||||
it('should render markdown', () => {
|
||||
expectRenderedContent(
|
||||
'*italic*',
|
||||
'<em>italic</em>'
|
||||
);
|
||||
expectRenderedContent(
|
||||
'**bold**',
|
||||
'<strong>bold</strong>'
|
||||
);
|
||||
});
|
||||
|
||||
it('should not render html', () => {
|
||||
expectRenderedContent(
|
||||
'<script>document.getElementById("demo").innerHTML = "Hello JavaScript!";</script>',
|
||||
'<script>document.getElementById("demo").innerHTML = "Hello JavaScript!";</script>'
|
||||
);
|
||||
expectRenderedContent(
|
||||
'<a href="javascript:window.alert();">foobar</a>',
|
||||
'<a href="javascript:window.alert();">foobar</a>'
|
||||
);
|
||||
});
|
||||
|
||||
const expectRenderedContent = (input: string, output: string) =>
|
||||
expect(contentRenderer.renderMessage(input)).to.be.equal(output);
|
||||
|
||||
});
|
||||
@@ -0,0 +1,31 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2020 TypeFox and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import * as markdownit from '@theia/core/shared/markdown-it';
|
||||
import { injectable } from '@theia/core/shared/inversify';
|
||||
|
||||
@injectable()
|
||||
export class NotificationContentRenderer {
|
||||
|
||||
protected readonly mdEngine = markdownit({ html: false });
|
||||
|
||||
renderMessage(content: string): string {
|
||||
// in alignment with vscode, new lines aren't supported
|
||||
const contentWithoutNewlines = content.replace(/((\r)?\n)+/gm, ' ');
|
||||
|
||||
return this.mdEngine.renderInline(contentWithoutNewlines);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2019 TypeFox and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import * as React from '@theia/core/shared/react';
|
||||
import { DisposableCollection } from '@theia/core';
|
||||
import { NotificationManager, NotificationUpdateEvent } from './notifications-manager';
|
||||
import { NotificationComponent } from './notification-component';
|
||||
import { CorePreferences } from '@theia/core/lib/common';
|
||||
import { ContextMenuRenderer } from '@theia/core/lib/browser';
|
||||
|
||||
export interface NotificationToastsComponentProps {
|
||||
readonly manager: NotificationManager;
|
||||
readonly corePreferences: CorePreferences;
|
||||
readonly contextMenuRenderer: ContextMenuRenderer;
|
||||
}
|
||||
|
||||
type NotificationToastsComponentState = Pick<NotificationUpdateEvent, Exclude<keyof NotificationUpdateEvent, 'notifications'>>;
|
||||
|
||||
export class NotificationToastsComponent extends React.Component<NotificationToastsComponentProps, NotificationToastsComponentState> {
|
||||
|
||||
constructor(props: NotificationToastsComponentProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
toasts: [],
|
||||
visibilityState: 'hidden'
|
||||
};
|
||||
}
|
||||
|
||||
protected readonly toDisposeOnUnmount = new DisposableCollection();
|
||||
|
||||
override async componentDidMount(): Promise<void> {
|
||||
this.toDisposeOnUnmount.push(
|
||||
this.props.manager.onUpdated(({ toasts, visibilityState }) => {
|
||||
visibilityState = this.props.corePreferences['workbench.silentNotifications'] ? 'hidden' : visibilityState;
|
||||
this.setState({
|
||||
toasts: toasts.slice(-3),
|
||||
visibilityState
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
override componentWillUnmount(): void {
|
||||
this.toDisposeOnUnmount.dispose();
|
||||
}
|
||||
|
||||
override render(): React.ReactNode {
|
||||
return (
|
||||
<div className={`theia-notifications-container theia-notification-toasts ${this.state.visibilityState === 'toasts' ? 'open' : 'closed'}`}>
|
||||
<div className='theia-notification-list'>
|
||||
{this.state.toasts.map(notification => <NotificationComponent key={notification.messageId} notification={notification} manager={this.props.manager}
|
||||
contextMenuRenderer={this.props.contextMenuRenderer} />)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
58
packages/messages/src/browser/notifications-commands.ts
Normal file
58
packages/messages/src/browser/notifications-commands.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2019 TypeFox and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { Command, nls } from '@theia/core';
|
||||
import { codicon } from '@theia/core/lib/browser';
|
||||
|
||||
export namespace NotificationsCommands {
|
||||
|
||||
const NOTIFICATIONS_CATEGORY = 'Notifications';
|
||||
const NOTIFICATIONS_CATEGORY_KEY = nls.getDefaultKey(NOTIFICATIONS_CATEGORY);
|
||||
|
||||
export const TOGGLE = Command.toLocalizedCommand({
|
||||
id: 'notifications.commands.toggle',
|
||||
category: NOTIFICATIONS_CATEGORY,
|
||||
iconClass: codicon('list-unordered'),
|
||||
label: 'Toggle Notifications'
|
||||
}, 'theia/messages/toggleNotifications', NOTIFICATIONS_CATEGORY_KEY);
|
||||
|
||||
export const SHOW = Command.toDefaultLocalizedCommand({
|
||||
id: 'notifications.commands.show',
|
||||
category: NOTIFICATIONS_CATEGORY,
|
||||
label: 'Show Notifications'
|
||||
});
|
||||
|
||||
export const HIDE = Command.toDefaultLocalizedCommand({
|
||||
id: 'notifications.commands.hide',
|
||||
category: NOTIFICATIONS_CATEGORY,
|
||||
label: 'Hide Notifications'
|
||||
});
|
||||
|
||||
export const CLEAR_ALL = Command.toDefaultLocalizedCommand({
|
||||
id: 'notifications.commands.clearAll',
|
||||
category: NOTIFICATIONS_CATEGORY,
|
||||
iconClass: codicon('clear-all'),
|
||||
label: 'Clear All Notifications'
|
||||
});
|
||||
|
||||
export const COPY_MESSAGE = Command.toDefaultLocalizedCommand({
|
||||
id: 'notifications.commands.copyMessage',
|
||||
category: NOTIFICATIONS_CATEGORY,
|
||||
label: 'Copy Message'
|
||||
});
|
||||
}
|
||||
|
||||
export const NOTIFICATION_CONTEXT_MENU: string[] = ['notification-context-menu'];
|
||||
250
packages/messages/src/browser/notifications-contribution.ts
Normal file
250
packages/messages/src/browser/notifications-contribution.ts
Normal file
@@ -0,0 +1,250 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2019 TypeFox and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { injectable, inject } from '@theia/core/shared/inversify';
|
||||
import {
|
||||
FrontendApplicationContribution, StatusBar, FrontendApplication, StatusBarAlignment,
|
||||
KeybindingContribution, KeybindingRegistry, StylingParticipant, ColorTheme, CssStyleCollector
|
||||
} from '@theia/core/lib/browser';
|
||||
import { ClipboardService } from '@theia/core/lib/browser/clipboard-service';
|
||||
import { NotificationsCommands, NOTIFICATION_CONTEXT_MENU } from './notifications-commands';
|
||||
import { CommandContribution, CommandRegistry, MenuContribution, MenuModelRegistry } from '@theia/core';
|
||||
import { Notification, NotificationManager } from './notifications-manager';
|
||||
import { NotificationsRenderer } from './notifications-renderer';
|
||||
import { ColorContribution } from '@theia/core/lib/browser/color-application-contribution';
|
||||
import { ColorRegistry } from '@theia/core/lib/browser/color-registry';
|
||||
import { Color } from '@theia/core/lib/common/color';
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
import { isHighContrast } from '@theia/core/lib/common/theme';
|
||||
|
||||
@injectable()
|
||||
export class NotificationsContribution implements FrontendApplicationContribution, CommandContribution,
|
||||
KeybindingContribution, ColorContribution, StylingParticipant, MenuContribution {
|
||||
|
||||
protected readonly id = 'theia-notification-center';
|
||||
|
||||
@inject(NotificationManager)
|
||||
protected readonly manager: NotificationManager;
|
||||
|
||||
@inject(NotificationsRenderer)
|
||||
protected readonly notificationsRenderer: NotificationsRenderer; // required for initialization
|
||||
|
||||
@inject(StatusBar)
|
||||
protected readonly statusBar: StatusBar;
|
||||
|
||||
@inject(ClipboardService)
|
||||
protected readonly clipboardService: ClipboardService;
|
||||
|
||||
onStart(_app: FrontendApplication): void {
|
||||
this.createStatusBarItem();
|
||||
}
|
||||
|
||||
protected createStatusBarItem(): void {
|
||||
this.updateStatusBarItem();
|
||||
this.manager.onUpdated(e => this.updateStatusBarItem(e.notifications.length));
|
||||
}
|
||||
protected updateStatusBarItem(count: number = 0): void {
|
||||
this.statusBar.setElement(this.id, {
|
||||
text: this.getStatusBarItemText(count),
|
||||
alignment: StatusBarAlignment.RIGHT,
|
||||
priority: -900,
|
||||
command: NotificationsCommands.TOGGLE.id,
|
||||
tooltip: this.getStatusBarItemTooltip(count),
|
||||
accessibilityInformation: {
|
||||
label: this.getStatusBarItemTooltip(count)
|
||||
}
|
||||
});
|
||||
}
|
||||
protected getStatusBarItemText(count: number): string {
|
||||
return `$(${count ? 'codicon-bell-dot' : 'codicon-bell'})${count ? ` ${count}` : ''}`;
|
||||
}
|
||||
protected getStatusBarItemTooltip(count: number): string {
|
||||
if (this.manager.centerVisible) {
|
||||
return nls.localizeByDefault('Hide Notifications');
|
||||
}
|
||||
return count === 0
|
||||
? nls.localizeByDefault('No Notifications')
|
||||
: count === 1
|
||||
? nls.localizeByDefault('1 New Notification')
|
||||
: nls.localizeByDefault('{0} New Notifications', count.toString());
|
||||
}
|
||||
|
||||
registerCommands(commands: CommandRegistry): void {
|
||||
commands.registerCommand(NotificationsCommands.TOGGLE, {
|
||||
isEnabled: () => true,
|
||||
execute: () => this.manager.toggleCenter()
|
||||
});
|
||||
commands.registerCommand(NotificationsCommands.SHOW, {
|
||||
isEnabled: () => true,
|
||||
execute: () => this.manager.showCenter()
|
||||
});
|
||||
commands.registerCommand(NotificationsCommands.HIDE, {
|
||||
execute: () => this.manager.hide()
|
||||
});
|
||||
commands.registerCommand(NotificationsCommands.CLEAR_ALL, {
|
||||
execute: () => this.manager.clearAll()
|
||||
});
|
||||
commands.registerCommand(NotificationsCommands.COPY_MESSAGE, {
|
||||
execute: (notification?: Notification) => {
|
||||
if (!notification || !notification.message) {
|
||||
return;
|
||||
}
|
||||
const plainText = this.notificationToPlainText(notification.message);
|
||||
if (plainText) {
|
||||
this.clipboardService.writeText(plainText);
|
||||
}
|
||||
},
|
||||
isEnabled: (notification?: Notification) => !!notification && !!notification.message
|
||||
});
|
||||
}
|
||||
|
||||
protected notificationToPlainText(html: string): string {
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(html, 'text/html');
|
||||
return doc.body.textContent || '';
|
||||
}
|
||||
|
||||
registerMenus(menus: MenuModelRegistry): void {
|
||||
menus.registerMenuAction(
|
||||
[...NOTIFICATION_CONTEXT_MENU, '_copy'],
|
||||
{
|
||||
commandId: NotificationsCommands.COPY_MESSAGE.id
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
registerKeybindings(keybindings: KeybindingRegistry): void {
|
||||
keybindings.registerKeybinding({
|
||||
command: NotificationsCommands.HIDE.id,
|
||||
when: 'notificationsVisible',
|
||||
keybinding: 'esc'
|
||||
});
|
||||
}
|
||||
|
||||
registerColors(colors: ColorRegistry): void {
|
||||
colors.register(
|
||||
{
|
||||
id: 'notificationCenter.border', defaults: {
|
||||
hcDark: 'contrastBorder',
|
||||
hcLight: 'contrastBorder'
|
||||
}, description: 'Notifications center border color. Notifications slide in from the bottom right of the window.'
|
||||
},
|
||||
{
|
||||
id: 'notificationToast.border', defaults: {
|
||||
hcDark: 'contrastBorder',
|
||||
hcLight: 'contrastBorder'
|
||||
}, description: 'Notification toast border color. Notifications slide in from the bottom right of the window.'
|
||||
},
|
||||
{
|
||||
id: 'notifications.foreground', defaults: {
|
||||
dark: 'editorWidget.foreground',
|
||||
light: 'editorWidget.foreground',
|
||||
hcDark: 'editorWidget.foreground',
|
||||
hcLight: 'editorWidget.foreground'
|
||||
}, description: 'Notifications foreground color. Notifications slide in from the bottom right of the window.'
|
||||
},
|
||||
{
|
||||
id: 'notifications.background', defaults: {
|
||||
dark: 'editorWidget.background',
|
||||
light: 'editorWidget.background',
|
||||
hcDark: 'editorWidget.background',
|
||||
hcLight: 'editorWidget.background'
|
||||
}, description: 'Notifications background color. Notifications slide in from the bottom right of the window.'
|
||||
},
|
||||
{
|
||||
id: 'notificationLink.foreground', defaults: {
|
||||
dark: 'textLink.foreground',
|
||||
light: 'textLink.foreground',
|
||||
hcDark: 'textLink.foreground',
|
||||
hcLight: 'textLink.foreground'
|
||||
}, description: 'Notification links foreground color. Notifications slide in from the bottom right of the window.'
|
||||
},
|
||||
{
|
||||
id: 'notificationCenterHeader.foreground',
|
||||
description: 'Notifications center header foreground color. Notifications slide in from the bottom right of the window.'
|
||||
},
|
||||
{
|
||||
id: 'notificationCenterHeader.background', defaults: {
|
||||
dark: Color.lighten('notifications.background', 0.3),
|
||||
light: Color.darken('notifications.background', 0.05),
|
||||
hcDark: 'notifications.background',
|
||||
hcLight: 'notifications.background'
|
||||
}, description: 'Notifications center header background color. Notifications slide in from the bottom right of the window.'
|
||||
},
|
||||
{
|
||||
id: 'notifications.border', defaults: {
|
||||
dark: 'notificationCenterHeader.background',
|
||||
light: 'notificationCenterHeader.background',
|
||||
hcDark: 'notificationCenterHeader.background',
|
||||
hcLight: 'notificationCenterHeader.background'
|
||||
// eslint-disable-next-line max-len
|
||||
}, description: 'Notifications border color separating from other notifications in the notifications center. Notifications slide in from the bottom right of the window.'
|
||||
},
|
||||
{
|
||||
id: 'notificationsErrorIcon.foreground', defaults: {
|
||||
dark: 'editorError.foreground',
|
||||
light: 'editorError.foreground',
|
||||
hcDark: 'editorError.foreground',
|
||||
hcLight: 'editorError.foreground'
|
||||
}, description: 'The color used for the icon of error notifications. Notifications slide in from the bottom right of the window.'
|
||||
},
|
||||
{
|
||||
id: 'notificationsWarningIcon.foreground', defaults: {
|
||||
dark: 'editorWarning.foreground',
|
||||
light: 'editorWarning.foreground',
|
||||
hcDark: 'editorWarning.foreground',
|
||||
hcLight: 'editorWarning.foreground'
|
||||
}, description: 'The color used for the icon of warning notifications. Notifications slide in from the bottom right of the window.'
|
||||
},
|
||||
{
|
||||
id: 'notificationsInfoIcon.foreground', defaults: {
|
||||
dark: 'editorInfo.foreground',
|
||||
light: 'editorInfo.foreground',
|
||||
hcDark: 'editorInfo.foreground',
|
||||
hcLight: 'editorInfo.foreground'
|
||||
}, description: 'The color used for the icon of info notifications. Notifications slide in from the bottom right of the window.'
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
registerThemeStyle(theme: ColorTheme, collector: CssStyleCollector): void {
|
||||
const notificationsBackground = theme.getColor('notifications.background');
|
||||
if (notificationsBackground) {
|
||||
collector.addRule(`
|
||||
.theia-notification-list-item-container {
|
||||
background-color: ${notificationsBackground};
|
||||
}
|
||||
`);
|
||||
}
|
||||
const notificationHover = theme.getColor('list.hoverBackground');
|
||||
if (notificationHover) {
|
||||
collector.addRule(`
|
||||
.theia-notification-list-item:hover:not(:focus) {
|
||||
background-color: ${notificationHover};
|
||||
}
|
||||
`);
|
||||
}
|
||||
const focusBorder = theme.getColor('focusBorder');
|
||||
if (focusBorder && isHighContrast(theme.type)) {
|
||||
collector.addRule(`
|
||||
.theia-notification-list-item:hover:not(:focus) {
|
||||
outline: 1px dashed ${focusBorder};
|
||||
outline-offset: -2px;
|
||||
}
|
||||
`);
|
||||
}
|
||||
}
|
||||
}
|
||||
112
packages/messages/src/browser/notifications-manager.spec.ts
Normal file
112
packages/messages/src/browser/notifications-manager.spec.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2026 STMicroelectronics and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { enableJSDOM } from '@theia/core/lib/browser/test/jsdom';
|
||||
let disableJSDOM = enableJSDOM();
|
||||
import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider';
|
||||
FrontendApplicationConfigProvider.set({});
|
||||
|
||||
import { expect } from 'chai';
|
||||
import { Message } from '@theia/core/lib/common';
|
||||
import { NotificationManager } from './notifications-manager';
|
||||
import { NotificationPreferences } from '../common/notification-preferences';
|
||||
import { NotificationContentRenderer } from './notification-content-renderer';
|
||||
|
||||
disableJSDOM();
|
||||
|
||||
describe('NotificationManager', () => {
|
||||
|
||||
const DEFAULT_TIMEOUT = 30000;
|
||||
|
||||
class TestableNotificationManager extends NotificationManager {
|
||||
public testGetTimeout(plainMessage: Message): number {
|
||||
return this.getTimeout(plainMessage);
|
||||
}
|
||||
}
|
||||
|
||||
function createNotificationManager(preferenceTimeout: number = DEFAULT_TIMEOUT): TestableNotificationManager {
|
||||
const manager = new TestableNotificationManager();
|
||||
(manager as unknown as { preferences: Partial<NotificationPreferences> }).preferences = {
|
||||
'notification.timeout': preferenceTimeout
|
||||
};
|
||||
(manager as unknown as { contentRenderer: NotificationContentRenderer }).contentRenderer = new NotificationContentRenderer();
|
||||
return manager;
|
||||
}
|
||||
|
||||
before(() => {
|
||||
disableJSDOM = enableJSDOM();
|
||||
});
|
||||
|
||||
after(() => {
|
||||
disableJSDOM();
|
||||
});
|
||||
|
||||
describe('getTimeout', () => {
|
||||
|
||||
let manager: TestableNotificationManager = createNotificationManager();
|
||||
|
||||
it('should return preference timeout when no options are provided', () => {
|
||||
const message: Message = { text: 'Test message' };
|
||||
expect(manager.testGetTimeout(message)).to.equal(DEFAULT_TIMEOUT);
|
||||
});
|
||||
|
||||
it('should return preference timeout when options are provided without timeout', () => {
|
||||
const message: Message = { text: 'Test message', options: {} };
|
||||
expect(manager.testGetTimeout(message)).to.equal(DEFAULT_TIMEOUT);
|
||||
});
|
||||
|
||||
it('should return explicit timeout when provided', () => {
|
||||
const message: Message = { text: 'Test message', options: { timeout: 5000 } };
|
||||
expect(manager.testGetTimeout(message)).to.equal(5000);
|
||||
});
|
||||
|
||||
it('should return 0 when timeout is explicitly set to 0', () => {
|
||||
const message: Message = { text: 'Test message', options: { timeout: 0 } };
|
||||
expect(manager.testGetTimeout(message)).to.equal(0);
|
||||
});
|
||||
|
||||
it('should return 0 when actions are present regardless of timeout', () => {
|
||||
const message: Message = { text: 'Test message', actions: ['OK'], options: { timeout: 5000 } };
|
||||
expect(manager.testGetTimeout(message)).to.equal(0);
|
||||
});
|
||||
|
||||
it('should return 0 when actions are present and no timeout is set', () => {
|
||||
const message: Message = { text: 'Test message', actions: ['OK', 'Cancel'] };
|
||||
expect(manager.testGetTimeout(message)).to.equal(0);
|
||||
});
|
||||
|
||||
it('should return negative timeout when explicitly set', () => {
|
||||
const message: Message = { text: 'Test message', options: { timeout: -1 } };
|
||||
expect(manager.testGetTimeout(message)).to.equal(-1);
|
||||
});
|
||||
|
||||
it('should return explicit timeout even if custom preference timeout is available', () => {
|
||||
const customTimeout = 60000;
|
||||
manager = createNotificationManager(customTimeout);
|
||||
const message: Message = { text: 'Test message', options: { timeout: 5000 } };
|
||||
expect(manager.testGetTimeout(message)).to.equal(5000);
|
||||
});
|
||||
|
||||
it('should return custom preference timeout if no timeout is set', () => {
|
||||
const customTimeout = 60000;
|
||||
manager = createNotificationManager(customTimeout);
|
||||
const message: Message = { text: 'Test message' };
|
||||
expect(manager.testGetTimeout(message)).to.equal(customTimeout);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
310
packages/messages/src/browser/notifications-manager.ts
Normal file
310
packages/messages/src/browser/notifications-manager.ts
Normal file
@@ -0,0 +1,310 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2019 TypeFox and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { injectable, inject, postConstruct } from '@theia/core/shared/inversify';
|
||||
import { MessageClient, MessageType, Message as PlainMessage, ProgressMessage, ProgressUpdate, CancellationToken } from '@theia/core/lib/common';
|
||||
import { deepClone } from '@theia/core/lib/common/objects';
|
||||
import { Emitter } from '@theia/core';
|
||||
import { Deferred } from '@theia/core/lib/common/promise-util';
|
||||
import { Md5 } from 'ts-md5';
|
||||
import throttle = require('@theia/core/shared/lodash.throttle');
|
||||
import { NotificationPreferences } from '../common/notification-preferences';
|
||||
import { ContextKeyService, ContextKey } from '@theia/core/lib/browser/context-key-service';
|
||||
import { OpenerService } from '@theia/core/lib/browser';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import { NotificationContentRenderer } from './notification-content-renderer';
|
||||
|
||||
export interface NotificationUpdateEvent {
|
||||
readonly notifications: Notification[];
|
||||
readonly toasts: Notification[];
|
||||
readonly visibilityState: Notification.Visibility;
|
||||
}
|
||||
|
||||
export interface Notification {
|
||||
messageId: string;
|
||||
message: string;
|
||||
source?: string;
|
||||
expandable: boolean;
|
||||
collapsed: boolean;
|
||||
type: Notification.Type;
|
||||
actions: string[];
|
||||
progress?: number;
|
||||
}
|
||||
export namespace Notification {
|
||||
export type Visibility = 'hidden' | 'toasts' | 'center';
|
||||
export type Type = 'info' | 'warning' | 'error' | 'progress';
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class NotificationManager extends MessageClient {
|
||||
|
||||
@inject(NotificationPreferences)
|
||||
protected readonly preferences: NotificationPreferences;
|
||||
|
||||
@inject(ContextKeyService)
|
||||
protected readonly contextKeyService: ContextKeyService;
|
||||
|
||||
@inject(OpenerService)
|
||||
protected readonly openerService: OpenerService;
|
||||
|
||||
@inject(NotificationContentRenderer)
|
||||
protected readonly contentRenderer: NotificationContentRenderer;
|
||||
|
||||
protected readonly onUpdatedEmitter = new Emitter<NotificationUpdateEvent>();
|
||||
readonly onUpdated = this.onUpdatedEmitter.event;
|
||||
protected readonly fireUpdatedEvent = throttle(() => {
|
||||
const notifications = deepClone(Array.from(this.notifications.values()).filter((notification: Notification) =>
|
||||
notification.message
|
||||
));
|
||||
const toasts = deepClone(Array.from(this.toasts.values()).filter((toast: Notification) =>
|
||||
toast.message
|
||||
));
|
||||
const visibilityState = this.visibilityState;
|
||||
this.onUpdatedEmitter.fire({ notifications, toasts, visibilityState });
|
||||
}, 250, { leading: true, trailing: true });
|
||||
|
||||
protected readonly deferredResults = new Map<string, Deferred<string | undefined>>();
|
||||
protected readonly notifications = new Map<string, Notification>();
|
||||
protected readonly toasts = new Map<string, Notification>();
|
||||
|
||||
protected notificationToastsVisibleKey: ContextKey<boolean>;
|
||||
protected notificationCenterVisibleKey: ContextKey<boolean>;
|
||||
protected notificationsVisible: ContextKey<boolean>;
|
||||
|
||||
@postConstruct()
|
||||
protected init(): void {
|
||||
this.doInit();
|
||||
}
|
||||
|
||||
protected async doInit(): Promise<void> {
|
||||
this.notificationToastsVisibleKey = this.contextKeyService.createKey<boolean>('notificationToastsVisible', false);
|
||||
this.notificationCenterVisibleKey = this.contextKeyService.createKey<boolean>('notificationCenterVisible', false);
|
||||
this.notificationsVisible = this.contextKeyService.createKey<boolean>('notificationsVisible', false);
|
||||
}
|
||||
|
||||
protected updateContextKeys(): void {
|
||||
this.notificationToastsVisibleKey.set(this.toastsVisible);
|
||||
this.notificationCenterVisibleKey.set(this.centerVisible);
|
||||
this.notificationsVisible.set(this.toastsVisible || this.centerVisible);
|
||||
}
|
||||
|
||||
get toastsVisible(): boolean {
|
||||
return this.visibilityState === 'toasts';
|
||||
}
|
||||
get centerVisible(): boolean {
|
||||
return this.visibilityState === 'center';
|
||||
}
|
||||
protected visibilityState: Notification.Visibility = 'hidden';
|
||||
protected setVisibilityState(newState: Notification.Visibility): void {
|
||||
const changed = this.visibilityState !== newState;
|
||||
this.visibilityState = newState;
|
||||
if (changed) {
|
||||
this.fireUpdatedEvent();
|
||||
this.updateContextKeys();
|
||||
}
|
||||
}
|
||||
|
||||
hideCenter(): void {
|
||||
this.setVisibilityState('hidden');
|
||||
}
|
||||
showCenter(): void {
|
||||
this.setVisibilityState('center');
|
||||
}
|
||||
toggleCenter(): void {
|
||||
this.setVisibilityState(this.centerVisible ? 'hidden' : 'center');
|
||||
}
|
||||
|
||||
accept(notification: Notification | string, action: string | undefined): void {
|
||||
const messageId = this.getId(notification);
|
||||
if (!messageId) {
|
||||
return;
|
||||
}
|
||||
this.notifications.delete(messageId);
|
||||
this.toasts.delete(messageId);
|
||||
const result = this.deferredResults.get(messageId);
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
this.deferredResults.delete(messageId);
|
||||
if ((this.centerVisible && !this.notifications.size) || (this.toastsVisible && !this.toasts.size)) {
|
||||
this.setVisibilityState('hidden');
|
||||
}
|
||||
result.resolve(action);
|
||||
this.fireUpdatedEvent();
|
||||
}
|
||||
protected find(notification: Notification | string): Notification | undefined {
|
||||
return typeof notification === 'string' ? this.notifications.get(notification) : notification;
|
||||
}
|
||||
protected getId(notification: Notification | string): string {
|
||||
return typeof notification === 'string' ? notification : notification.messageId;
|
||||
}
|
||||
|
||||
hide(): void {
|
||||
if (this.toastsVisible) {
|
||||
this.toasts.clear();
|
||||
}
|
||||
this.setVisibilityState('hidden');
|
||||
}
|
||||
|
||||
clearAll(): void {
|
||||
this.setVisibilityState('hidden');
|
||||
Array.from(this.notifications.values()).forEach(n => this.clear(n));
|
||||
}
|
||||
|
||||
clear(notification: Notification | string): void {
|
||||
this.accept(notification, undefined);
|
||||
}
|
||||
|
||||
toggleExpansion(notificationId: string): void {
|
||||
const notification = this.find(notificationId);
|
||||
if (!notification) {
|
||||
return;
|
||||
}
|
||||
notification.collapsed = !notification.collapsed;
|
||||
this.fireUpdatedEvent();
|
||||
}
|
||||
|
||||
override showMessage(plainMessage: PlainMessage): Promise<string | undefined> {
|
||||
const messageId = this.getMessageId(plainMessage);
|
||||
|
||||
this.toasts.delete(messageId);
|
||||
this.notifications.delete(messageId);
|
||||
const existingDeferred = this.deferredResults.get(messageId);
|
||||
if (existingDeferred) {
|
||||
this.deferredResults.delete(messageId);
|
||||
existingDeferred.resolve(undefined);
|
||||
}
|
||||
|
||||
const message = this.contentRenderer.renderMessage(plainMessage.text);
|
||||
const type = this.toNotificationType(plainMessage.type);
|
||||
const actions = Array.from(new Set(plainMessage.actions));
|
||||
const source = plainMessage.source;
|
||||
const expandable = this.isExpandable(message, source, actions);
|
||||
const collapsed = expandable;
|
||||
const notification = { messageId, message, type, actions, expandable, collapsed };
|
||||
this.notifications.set(messageId, notification);
|
||||
const result = new Deferred<string | undefined>();
|
||||
this.deferredResults.set(messageId, result);
|
||||
|
||||
if (!this.centerVisible) {
|
||||
this.toasts.delete(messageId);
|
||||
this.toasts.set(messageId, notification);
|
||||
this.startHideTimeout(messageId, this.getTimeout(plainMessage));
|
||||
this.setVisibilityState('toasts');
|
||||
}
|
||||
this.fireUpdatedEvent();
|
||||
return result.promise;
|
||||
}
|
||||
protected hideTimeouts = new Map<string, number>();
|
||||
protected startHideTimeout(messageId: string, timeout: number): void {
|
||||
if (timeout > 0) {
|
||||
this.hideTimeouts.set(messageId, window.setTimeout(() => {
|
||||
this.hideToast(messageId);
|
||||
}, timeout));
|
||||
}
|
||||
}
|
||||
protected hideToast(messageId: string): void {
|
||||
this.toasts.delete(messageId);
|
||||
if (this.toastsVisible && !this.toasts.size) {
|
||||
this.setVisibilityState('hidden');
|
||||
} else {
|
||||
this.fireUpdatedEvent();
|
||||
}
|
||||
}
|
||||
protected getTimeout(plainMessage: PlainMessage): number {
|
||||
if (plainMessage.actions && plainMessage.actions.length > 0) {
|
||||
// Ignore the timeout if at least one action is set, and we wait for user interaction.
|
||||
return 0;
|
||||
}
|
||||
return plainMessage.options?.timeout !== undefined ? plainMessage.options.timeout : this.preferences['notification.timeout'];
|
||||
}
|
||||
protected isExpandable(message: string, source: string | undefined, actions: string[]): boolean {
|
||||
if (!actions.length && source) {
|
||||
return true;
|
||||
}
|
||||
return message.length > 500;
|
||||
}
|
||||
protected toNotificationType(type?: MessageType): Notification.Type {
|
||||
switch (type) {
|
||||
case MessageType.Error:
|
||||
return 'error';
|
||||
case MessageType.Warning:
|
||||
return 'warning';
|
||||
case MessageType.Progress:
|
||||
return 'progress';
|
||||
default:
|
||||
return 'info';
|
||||
}
|
||||
}
|
||||
protected getMessageId(m: PlainMessage): string {
|
||||
return String(Md5.hashStr(`[${m.type}] ${m.text} : ${(m.actions || []).join(' | ')};`));
|
||||
}
|
||||
|
||||
override async showProgress(messageId: string, plainMessage: ProgressMessage, cancellationToken: CancellationToken): Promise<string | undefined> {
|
||||
let notification = this.notifications.get(messageId);
|
||||
if (!notification) {
|
||||
const message = this.contentRenderer.renderMessage(plainMessage.text);
|
||||
const type = this.toNotificationType(plainMessage.type);
|
||||
const actions = Array.from(new Set(plainMessage.actions));
|
||||
const source = plainMessage.source;
|
||||
const expandable = this.isExpandable(message, source, actions);
|
||||
const collapsed = expandable;
|
||||
notification = { messageId, message, type, actions, expandable, collapsed };
|
||||
this.notifications.set(messageId, notification);
|
||||
|
||||
notification.progress = 0;
|
||||
cancellationToken.onCancellationRequested(() => {
|
||||
this.accept(messageId, ProgressMessage.Cancel);
|
||||
});
|
||||
}
|
||||
const result = this.deferredResults.get(messageId) || new Deferred<string | undefined>();
|
||||
this.deferredResults.set(messageId, result);
|
||||
|
||||
if (!this.centerVisible) {
|
||||
this.toasts.set(messageId, notification);
|
||||
this.setVisibilityState('toasts');
|
||||
}
|
||||
this.fireUpdatedEvent();
|
||||
return result.promise;
|
||||
}
|
||||
|
||||
override async reportProgress(messageId: string, update: ProgressUpdate, originalMessage: ProgressMessage, cancellationToken: CancellationToken): Promise<void> {
|
||||
const notification = this.find(messageId);
|
||||
if (!notification) {
|
||||
return;
|
||||
}
|
||||
if (cancellationToken.isCancellationRequested) {
|
||||
this.clear(messageId);
|
||||
} else {
|
||||
const textMessage = originalMessage.text && update.message ? `${originalMessage.text}: ${update.message}` : originalMessage.text || update?.message;
|
||||
if (textMessage) {
|
||||
notification.message = this.contentRenderer.renderMessage(textMessage);
|
||||
}
|
||||
notification.progress = this.toPlainProgress(update) || notification.progress;
|
||||
}
|
||||
this.fireUpdatedEvent();
|
||||
}
|
||||
protected toPlainProgress(update: ProgressUpdate): number | undefined {
|
||||
return update.work && Math.min(update.work.done / update.work.total * 100, 100);
|
||||
}
|
||||
|
||||
async openLink(link: string): Promise<void> {
|
||||
const uri = new URI(link);
|
||||
const opener = await this.openerService.getOpener(uri);
|
||||
opener.open(uri);
|
||||
}
|
||||
|
||||
}
|
||||
66
packages/messages/src/browser/notifications-renderer.tsx
Normal file
66
packages/messages/src/browser/notifications-renderer.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2019 TypeFox and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import * as React from '@theia/core/shared/react';
|
||||
import { createRoot, Root } from '@theia/core/shared/react-dom/client';
|
||||
import { injectable, inject, postConstruct } from '@theia/core/shared/inversify';
|
||||
import { ApplicationShell, ContextMenuRenderer } from '@theia/core/lib/browser';
|
||||
import { NotificationManager } from './notifications-manager';
|
||||
import { NotificationCenterComponent } from './notification-center-component';
|
||||
import { NotificationToastsComponent } from './notification-toasts-component';
|
||||
import { CorePreferences } from '@theia/core';
|
||||
|
||||
@injectable()
|
||||
export class NotificationsRenderer {
|
||||
|
||||
@inject(ApplicationShell)
|
||||
protected readonly shell: ApplicationShell;
|
||||
|
||||
@inject(NotificationManager)
|
||||
protected readonly manager: NotificationManager;
|
||||
|
||||
@inject(CorePreferences)
|
||||
protected readonly corePreferences: CorePreferences;
|
||||
|
||||
@inject(ContextMenuRenderer)
|
||||
protected readonly contextMenuRenderer: ContextMenuRenderer;
|
||||
|
||||
protected containerRoot: Root;
|
||||
|
||||
@postConstruct()
|
||||
protected init(): void {
|
||||
this.createOverlayContainer();
|
||||
this.render();
|
||||
}
|
||||
|
||||
protected container: HTMLDivElement;
|
||||
protected createOverlayContainer(): void {
|
||||
this.container = window.document.createElement('div');
|
||||
this.container.className = 'theia-notifications-overlay';
|
||||
if (window.document.body) {
|
||||
window.document.body.appendChild(this.container);
|
||||
}
|
||||
this.containerRoot = createRoot(this.container);
|
||||
}
|
||||
|
||||
protected render(): void {
|
||||
this.containerRoot.render(<div>
|
||||
<NotificationToastsComponent manager={this.manager} corePreferences={this.corePreferences} contextMenuRenderer={this.contextMenuRenderer} />
|
||||
<NotificationCenterComponent manager={this.manager} contextMenuRenderer={this.contextMenuRenderer} />
|
||||
</div>);
|
||||
}
|
||||
|
||||
}
|
||||
17
packages/messages/src/browser/style/index.css
Normal file
17
packages/messages/src/browser/style/index.css
Normal file
@@ -0,0 +1,17 @@
|
||||
/********************************************************************************
|
||||
* Copyright (C) 2017 TypeFox and others.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License v. 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0.
|
||||
*
|
||||
* This Source Code may also be made available under the following Secondary
|
||||
* Licenses when the conditions for such availability set forth in the Eclipse
|
||||
* Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
* with the GNU Classpath Exception which is available at
|
||||
* https://www.gnu.org/software/classpath/license.html.
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
********************************************************************************/
|
||||
|
||||
@import "./notifications.css";
|
||||
283
packages/messages/src/browser/style/notifications.css
Normal file
283
packages/messages/src/browser/style/notifications.css
Normal file
@@ -0,0 +1,283 @@
|
||||
/********************************************************************************
|
||||
* Copyright (C) 2019 TypeFox and others.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License v. 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0.
|
||||
*
|
||||
* This Source Code may also be made available under the following Secondary
|
||||
* Licenses when the conditions for such availability set forth in the Eclipse
|
||||
* Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
* with the GNU Classpath Exception which is available at
|
||||
* https://www.gnu.org/software/classpath/license.html.
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
********************************************************************************/
|
||||
|
||||
/* Container */
|
||||
|
||||
.theia-notifications-overlay {
|
||||
height: 0px;
|
||||
}
|
||||
|
||||
.theia-notifications-container {
|
||||
position: absolute;
|
||||
bottom: 36px;
|
||||
right: 16px;
|
||||
width: 500px;
|
||||
user-select: none;
|
||||
z-index: 1111;
|
||||
}
|
||||
.theia-notifications-container.closed {
|
||||
display: none;
|
||||
}
|
||||
.theia-notifications-container > * {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Toasts */
|
||||
|
||||
.theia-notifications-container.theia-notification-toasts .theia-notification-list-item-container {
|
||||
border-radius: 4px;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.theia-notifications-container.theia-notification-toasts .theia-notification-list-item {
|
||||
box-shadow: 0px 0px 4px 0px var(--theia-widget-shadow);
|
||||
border: 1px solid var(--theia-notificationToast-border);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* Center */
|
||||
|
||||
.theia-notifications-container.theia-notification-center {
|
||||
background-color: var(--theia-notifications-background);
|
||||
border: 1px solid var(--theia-notificationCenter-border);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0px 0px 6px 0px var(--theia-widget-shadow);
|
||||
}
|
||||
|
||||
/* Center > Header */
|
||||
|
||||
.theia-notification-center-header {
|
||||
color: var(--theia-notificationCenterHeader-foreground);
|
||||
background-color: var(--theia-notificationCenterHeader-background);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
justify-content: flex-end;
|
||||
min-height: 30px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.theia-notification-center-header-title {
|
||||
font-size: calc(var(--theia-ui-font-size1) / 1.1);
|
||||
font-family: var(--theia-ui-font-family);
|
||||
margin: 8px;
|
||||
flex-grow: 2;
|
||||
}
|
||||
|
||||
.theia-notification-center-header-actions {
|
||||
margin: 8px;
|
||||
}
|
||||
|
||||
/* List */
|
||||
|
||||
.theia-notification-list-scroll-container {
|
||||
max-height: 300px;
|
||||
overflow: auto;
|
||||
border-bottom-left-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
}
|
||||
|
||||
.theia-notification-list {
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
/* List > Item */
|
||||
|
||||
.theia-notification-list-item {
|
||||
background-color: var(--theia-notifications-background);
|
||||
width: 100%;
|
||||
cursor: pointer;
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-wrap: nowrap;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.theia-notification-list-item:focus {
|
||||
border-color: var(--theia-focusBorder);
|
||||
}
|
||||
|
||||
.theia-notification-center .theia-notification-list-item:not(:last-child) {
|
||||
border-top: 1px var(--theia-notifications-border) solid;
|
||||
}
|
||||
|
||||
.theia-notification-list-item-content {
|
||||
margin: 6px;
|
||||
flex-grow: 3;
|
||||
}
|
||||
.theia-notification-list-item-content.collapsed
|
||||
.theia-notification-list-item-content-bottom {
|
||||
display: none;
|
||||
}
|
||||
.theia-notification-list-item-content.collapsed .theia-notification-message {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.theia-notification-list-item-content-main {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
justify-content: space-between;
|
||||
padding: 5px 4px 7px 4px;
|
||||
}
|
||||
|
||||
.theia-notification-message {
|
||||
font-size: var(--theia-ui-font-size1);
|
||||
font-family: var(--theia-ui-font-family);
|
||||
overflow-wrap: break-word;
|
||||
box-sizing: border-box;
|
||||
flex-basis: 0%;
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
user-select: text;
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
.theia-notification-message a {
|
||||
border: none;
|
||||
color: var(--theia-notificationLink-foreground);
|
||||
outline: 0;
|
||||
text-decoration: none;
|
||||
}
|
||||
.theia-notification-message a:focus {
|
||||
outline-color: var(--theia-focusBorder);
|
||||
}
|
||||
|
||||
.theia-notification-icon {
|
||||
margin-top: 2px;
|
||||
margin-right: var(--theia-ui-padding);
|
||||
}
|
||||
|
||||
.theia-notification-icon:before {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.theia-notification-icon.info:before {
|
||||
color: var(--theia-notificationsInfoIcon-foreground);
|
||||
}
|
||||
|
||||
.theia-notification-icon.warning:before {
|
||||
color: var(--theia-notificationsWarningIcon-foreground);
|
||||
}
|
||||
|
||||
.theia-notification-icon.error:before {
|
||||
color: var(--theia-notificationsErrorIcon-foreground);
|
||||
}
|
||||
|
||||
.theia-notification-actions {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
justify-content: space-between;
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
.theia-notification-actions > li {
|
||||
display: inline-block;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
cursor: pointer;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.theia-notification-actions > .expand {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.theia-notification-list-item-content-bottom {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.theia-notification-source {
|
||||
font-size: var(--theia-ui-font-size0);
|
||||
font-family: var(--theia-ui-font-family);
|
||||
overflow-wrap: break-word;
|
||||
box-sizing: border-box;
|
||||
flex-grow: 1;
|
||||
padding: 4px;
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.theia-notification-buttons {
|
||||
flex-grow: 2;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.theia-notification-buttons > button {
|
||||
margin: 4px;
|
||||
max-width: 160px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.theia-notification-item-progress {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.theia-notification-item-progressbar {
|
||||
height: 2px;
|
||||
background-color: var(--theia-progressBar-background);
|
||||
width: 66%;
|
||||
}
|
||||
|
||||
.theia-notification-item-progressbar.indeterminate {
|
||||
/* `progress-animation` is defined in `packages/core/src/browser/style/progress-bar.css` */
|
||||
animation: progress-animation 1.3s 0s infinite
|
||||
cubic-bezier(0.645, 0.045, 0.355, 1);
|
||||
}
|
||||
|
||||
/* Perfect scrollbar */
|
||||
|
||||
.theia-notification-list-scroll-container .ps__rail-y {
|
||||
width: var(--theia-scrollbar-rail-width);
|
||||
}
|
||||
|
||||
.theia-notification-list-scroll-container .ps__rail-y:hover > .ps__thumb-y,
|
||||
.theia-notification-list-scroll-container .ps__rail-y:focus > .ps__thumb-y,
|
||||
.theia-notification-list-scroll-container
|
||||
.ps__rail-y.ps--clicking
|
||||
.ps__thumb-y {
|
||||
right: calc(
|
||||
(var(--theia-scrollbar-rail-width) - var(--theia-scrollbar-width)) / 2
|
||||
);
|
||||
width: var(--theia-scrollbar-width);
|
||||
}
|
||||
|
||||
.theia-notification-list-scroll-container .ps__rail-y > .ps__thumb-y {
|
||||
width: var(--theia-scrollbar-width);
|
||||
right: calc(
|
||||
(var(--theia-scrollbar-rail-width) - var(--theia-scrollbar-width)) / 2
|
||||
);
|
||||
background: var(--theia-scrollbarSlider-background);
|
||||
border-radius: 0px;
|
||||
}
|
||||
57
packages/messages/src/common/notification-preferences.ts
Normal file
57
packages/messages/src/common/notification-preferences.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2018 Ericsson and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { interfaces } from '@theia/core/shared/inversify';
|
||||
import {
|
||||
createPreferenceProxy,
|
||||
PreferenceContribution,
|
||||
PreferenceProxy,
|
||||
PreferenceSchema,
|
||||
PreferenceService,
|
||||
} from '@theia/core/lib/common/preferences';
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
|
||||
export const NotificationConfigSchema: PreferenceSchema = {
|
||||
'properties': {
|
||||
'notification.timeout': {
|
||||
'type': 'number',
|
||||
'description': nls.localize('theia/messages/notificationTimeout', 'Informative notifications will be hidden after this timeout.'),
|
||||
'default': 30 * 1000 // `0` and negative values are treated as no timeout.
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export interface NotificationConfiguration {
|
||||
'notification.timeout': number
|
||||
}
|
||||
|
||||
export const NotificationPreferenceContribution = Symbol('NotificationPreferenceContribution');
|
||||
export const NotificationPreferences = Symbol('NotificationPreferences');
|
||||
export type NotificationPreferences = PreferenceProxy<NotificationConfiguration>;
|
||||
|
||||
export function createNotificationPreferences(preferences: PreferenceService, schema: PreferenceSchema = NotificationConfigSchema): NotificationPreferences {
|
||||
return createPreferenceProxy(preferences, schema);
|
||||
}
|
||||
|
||||
export function bindNotificationPreferences(bind: interfaces.Bind): void {
|
||||
bind(NotificationPreferences).toDynamicValue(ctx => {
|
||||
const preferences = ctx.container.get<PreferenceService>(PreferenceService);
|
||||
const contribution = ctx.container.get<PreferenceContribution>(NotificationPreferenceContribution);
|
||||
return createNotificationPreferences(preferences, contribution.schema);
|
||||
}).inSingletonScope();
|
||||
bind(NotificationPreferenceContribution).toConstantValue({ schema: NotificationConfigSchema });
|
||||
bind(PreferenceContribution).toService(NotificationPreferenceContribution);
|
||||
}
|
||||
22
packages/messages/src/node/messages-backend-module.ts
Normal file
22
packages/messages/src/node/messages-backend-module.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2025 STMicroelectronics and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { ContainerModule } from '@theia/core/shared/inversify';
|
||||
import { bindNotificationPreferences } from '../common/notification-preferences';
|
||||
|
||||
export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
bindNotificationPreferences(bind);
|
||||
});
|
||||
16
packages/messages/tsconfig.json
Normal file
16
packages/messages/tsconfig.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"extends": "../../configs/base.tsconfig",
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"rootDir": "src",
|
||||
"outDir": "lib"
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "../core"
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user