From 57c7e48bea897772bf8b13023d1dc153935364d5 Mon Sep 17 00:00:00 2001 From: Eugene Pankov Date: Mon, 7 Jun 2021 01:13:53 +0200 Subject: [PATCH] finished transfers menu --- terminus-core/src/api/index.ts | 2 +- terminus-core/src/api/platform.ts | 25 ++++++- .../src/components/appRoot.component.pug | 27 +++---- .../src/components/appRoot.component.scss | 22 ------ .../src/components/appRoot.component.ts | 9 ++- .../components/transfersMenu.component.pug | 13 ++++ .../components/transfersMenu.component.scss | 31 ++++++++ .../src/components/transfersMenu.component.ts | 38 ++++++++++ terminus-core/src/icons/download-solid.svg | 1 + terminus-core/src/icons/download.svg | 2 +- terminus-core/src/icons/upload.svg | 1 + terminus-core/src/index.ts | 2 + terminus-core/src/services/config.service.ts | 2 +- terminus-core/src/theme.scss | 9 ++- terminus-core/src/theme.vars.scss | 2 +- .../src/services/platform.service.ts | 75 ++++++++++++++----- terminus-terminal/src/features/debug.ts | 34 +++------ terminus-terminal/src/features/zmodem.ts | 34 ++++----- 18 files changed, 221 insertions(+), 108 deletions(-) create mode 100644 terminus-core/src/components/transfersMenu.component.pug create mode 100644 terminus-core/src/components/transfersMenu.component.scss create mode 100644 terminus-core/src/components/transfersMenu.component.ts create mode 100644 terminus-core/src/icons/download-solid.svg create mode 100644 terminus-core/src/icons/upload.svg diff --git a/terminus-core/src/api/index.ts b/terminus-core/src/api/index.ts index fedafd2f..6099d20f 100644 --- a/terminus-core/src/api/index.ts +++ b/terminus-core/src/api/index.ts @@ -10,7 +10,7 @@ export { Theme } from './theme' export { TabContextMenuItemProvider } from './tabContextMenuProvider' export { SelectorOption } from './selector' export { CLIHandler, CLIEvent } from './cli' -export { PlatformService, ClipboardContent, MessageBoxResult, MessageBoxOptions, FileDownload, FileUpload, FileTransfer } from './platform' +export { PlatformService, ClipboardContent, MessageBoxResult, MessageBoxOptions, FileDownload, FileUpload, FileTransfer, FileUploadOptions } from './platform' export { MenuItemOptions } from './menu' export { BootstrapData, BOOTSTRAP_DATA } from './mainProcess' export { HostWindowService } from './hostWindow' diff --git a/terminus-core/src/api/platform.ts b/terminus-core/src/api/platform.ts index 54aa648c..559f33b7 100644 --- a/terminus-core/src/api/platform.ts +++ b/terminus-core/src/api/platform.ts @@ -1,4 +1,5 @@ import { MenuItemOptions } from './menu' +import { Subject, Observable } from 'rxjs' /* eslint-disable @typescript-eslint/no-unused-vars */ export interface ClipboardContent { @@ -54,18 +55,38 @@ export abstract class FileDownload extends FileTransfer { export abstract class FileUpload extends FileTransfer { abstract read (): Promise + + async readAll (): Promise { + const buffers: Buffer[] = [] + while (true) { + const buf = await this.read() + if (!buf.length) { + break + } + buffers.push(Buffer.from(buf)) + } + return Buffer.concat(buffers) + } +} + +export interface FileUploadOptions { + multiple: boolean } export abstract class PlatformService { supportsWindowControls = false + get fileTransferStarted$ (): Observable { return this.fileTransferStarted } + + protected fileTransferStarted = new Subject() + abstract readClipboard (): string abstract setClipboard (content: ClipboardContent): void abstract loadConfig (): Promise abstract saveConfig (content: string): Promise - abstract startDownload (name: string, size: number): Promise - abstract startUpload (): Promise + abstract startDownload (name: string, size: number): Promise + abstract startUpload (options?: FileUploadOptions): Promise getConfigPath (): string|null { return null diff --git a/terminus-core/src/components/appRoot.component.pug b/terminus-core/src/components/appRoot.component.pug index f8c51828..c4fd36d3 100644 --- a/terminus-core/src/components/appRoot.component.pug +++ b/terminus-core/src/components/appRoot.component.pug @@ -57,6 +57,17 @@ title-bar( ) div([class.ml-3]='hasIcons(button.submenuItems)') {{item.title}} + .d-flex( + *ngIf='activeTransfers.length > 0', + ngbDropdown, + [(open)]='activeTransfersDropdownOpen' + ) + button.btn.btn-secondary.btn-tab-bar( + title='File transfers', + ngbDropdownToggle + ) !{require('../icons/download-solid.svg')} + transfers-menu(ngbDropdownMenu, [(transfers)]='activeTransfers') + .drag-space.background([class.persistent]='config.store.appearance.frame == "thin" && hostApp.platform != Platform.macOS') .btn-group.background @@ -83,22 +94,6 @@ title-bar( ) div([class.ml-3]='hasIcons(button.submenuItems)') {{item.title}} - .d-flex(ngbDropdown) - button.btn.btn-secondary.btn-tab-bar( - title='File transfers', - ngbDropdownToggle - ) !{require('../icons/download.svg')} - .transfers-dropdown-menu(ngbDropdownMenu) - .dropdown-header File transfers - .dropdown-item.transfer - .mr-3 !{require('../icons/download.svg')} - .main - label file.bin - .progress - .progress-bar.w-25 - small 25% - button.btn.btn-link !{require('../icons/times.svg')} - button.btn.btn-secondary.btn-tab-bar.btn-update( *ngIf='updatesAvailable', title='Update available - Click to install', diff --git a/terminus-core/src/components/appRoot.component.scss b/terminus-core/src/components/appRoot.component.scss index 73885b4e..f29fb77c 100644 --- a/terminus-core/src/components/appRoot.component.scss +++ b/terminus-core/src/components/appRoot.component.scss @@ -178,25 +178,3 @@ hotkey-hint { ::ng-deep .btn-update svg { fill: cyan; } - -.transfers-dropdown-menu { - min-width: 300px; - - .transfer { - display: flex; - align-items: center; - padding: 5px 0 5px 25px; - - .main { - margin-right: auto; - - label { - margin-bottom: 5px; - } - } - - > i { - margin-right: 10px; - } - } -} diff --git a/terminus-core/src/components/appRoot.component.ts b/terminus-core/src/components/appRoot.component.ts index c51e0f44..bdc2a7fd 100644 --- a/terminus-core/src/components/appRoot.component.ts +++ b/terminus-core/src/components/appRoot.component.ts @@ -12,7 +12,7 @@ import { UpdaterService } from '../services/updater.service' import { BaseTabComponent } from './baseTab.component' import { SafeModeModalComponent } from './safeModeModal.component' -import { AppService, HostWindowService, PlatformService, ToolbarButton, ToolbarButtonProvider } from '../api' +import { AppService, FileTransfer, HostWindowService, PlatformService, ToolbarButton, ToolbarButtonProvider } from '../api' /** @hidden */ @Component({ @@ -60,6 +60,8 @@ export class AppRootComponent { tabsDragging = false unsortedTabs: BaseTabComponent[] = [] updatesAvailable = false + activeTransfers: FileTransfer[] = [] + activeTransfersDropdownOpen = false private logger: Logger private constructor ( @@ -131,6 +133,11 @@ export class AppRootComponent { this.noTabs = app.tabs.length === 0 }) + platform.fileTransferStarted$.subscribe(transfer => { + this.activeTransfers.push(transfer) + this.activeTransfersDropdownOpen = true + }) + config.ready$.toPromise().then(() => { this.leftToolbarButtons = this.getToolbarButtons(false) this.rightToolbarButtons = this.getToolbarButtons(true) diff --git a/terminus-core/src/components/transfersMenu.component.pug b/terminus-core/src/components/transfersMenu.component.pug new file mode 100644 index 00000000..e460eccb --- /dev/null +++ b/terminus-core/src/components/transfersMenu.component.pug @@ -0,0 +1,13 @@ +.dropdown-header File transfers +.dropdown-item.transfer(*ngFor='let transfer of transfers', (click)='showTransfer(transfer)') + .icon(*ngIf='isDownload(transfer)') !{require('../icons/download.svg')} + .icon(*ngIf='!isDownload(transfer)') !{require('../icons/upload.svg')} + .main + label {{transfer.getName()}} + .status(*ngIf='transfer.isComplete()') + ngb-progressbar(type='success', [value]='100') + .status(*ngIf='transfer.isCancelled()') + ngb-progressbar(type='danger', [value]='100') + .status(*ngIf='!transfer.isComplete() && !transfer.isCancelled()') + ngb-progressbar(type='info', [value]='getProgress(transfer)') + button.btn.btn-link((click)='removeTransfer(transfer); $event.stopPropagation()') !{require('../icons/times.svg')} diff --git a/terminus-core/src/components/transfersMenu.component.scss b/terminus-core/src/components/transfersMenu.component.scss new file mode 100644 index 00000000..947a0d06 --- /dev/null +++ b/terminus-core/src/components/transfersMenu.component.scss @@ -0,0 +1,31 @@ +:host { + min-width: 300px; +} + +.transfer { + display: flex; + align-items: center; + padding: 5px 0 5px 25px; + + .icon { + padding: 4px 10px; + width: 36px; + height: 32px; + background: rgba(0,0,0,.25); + margin-right: 12px; + } + + .main { + width: 100%; + margin-right: auto; + margin-bottom: 7px; + + label { + margin: 0; + } + } + + > i { + margin-right: 10px; + } +} diff --git a/terminus-core/src/components/transfersMenu.component.ts b/terminus-core/src/components/transfersMenu.component.ts new file mode 100644 index 00000000..bb0426a6 --- /dev/null +++ b/terminus-core/src/components/transfersMenu.component.ts @@ -0,0 +1,38 @@ +import { Component, Input, Output, EventEmitter } from '@angular/core' +import { FileDownload, FileTransfer, PlatformService } from '../api/platform' + +/** @hidden */ +@Component({ + selector: 'transfers-menu', + template: require('./transfersMenu.component.pug'), + styles: [require('./transfersMenu.component.scss')], +}) +export class TransfersMenuComponent { + @Input() transfers: FileTransfer[] + @Output() transfersChange = new EventEmitter() + + constructor (private platform: PlatformService) { } + + isDownload (transfer: FileTransfer): boolean { + return transfer instanceof FileDownload + } + + getProgress (transfer: FileTransfer): number { + return Math.round(100 * transfer.getCompletedBytes() / transfer.getSize()) + } + + showTransfer (transfer: FileTransfer): void { + const fp = transfer['filePath'] + if (fp) { + this.platform.showItemInFolder(fp) + } + } + + removeTransfer (transfer: FileTransfer): void { + if (!transfer.isComplete()) { + transfer.cancel() + } + this.transfers = this.transfers.filter(x => x !== transfer) + this.transfersChange.emit(this.transfers) + } +} diff --git a/terminus-core/src/icons/download-solid.svg b/terminus-core/src/icons/download-solid.svg new file mode 100644 index 00000000..5170a03f --- /dev/null +++ b/terminus-core/src/icons/download-solid.svg @@ -0,0 +1 @@ + diff --git a/terminus-core/src/icons/download.svg b/terminus-core/src/icons/download.svg index 00a19b78..fec6e2a9 100644 --- a/terminus-core/src/icons/download.svg +++ b/terminus-core/src/icons/download.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/terminus-core/src/icons/upload.svg b/terminus-core/src/icons/upload.svg new file mode 100644 index 00000000..6b12ff4a --- /dev/null +++ b/terminus-core/src/icons/upload.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/terminus-core/src/index.ts b/terminus-core/src/index.ts index 99fbe30d..3aadcd58 100644 --- a/terminus-core/src/index.ts +++ b/terminus-core/src/index.ts @@ -21,6 +21,7 @@ import { SplitTabComponent, SplitTabRecoveryProvider } from './components/splitT import { SplitTabSpannerComponent } from './components/splitTabSpanner.component' import { UnlockVaultModalComponent } from './components/unlockVaultModal.component' import { WelcomeTabComponent } from './components/welcomeTab.component' +import { TransfersMenuComponent } from './components/transfersMenu.component' import { AutofocusDirective } from './directives/autofocus.directive' import { FastHtmlBindDirective } from './directives/fastHtmlBind.directive' @@ -81,6 +82,7 @@ const PROVIDERS = [ SplitTabSpannerComponent, UnlockVaultModalComponent, WelcomeTabComponent, + TransfersMenuComponent, ], entryComponents: [ RenameTabModalComponent, diff --git a/terminus-core/src/services/config.service.ts b/terminus-core/src/services/config.service.ts index 4c4bc4c6..c9135687 100644 --- a/terminus-core/src/services/config.service.ts +++ b/terminus-core/src/services/config.service.ts @@ -242,7 +242,7 @@ export class ConfigService { private migrate (config) { config.version ??= 0 if (config.version < 1) { - for (const connection of config.ssh?.connections) { + for (const connection of config.ssh?.connections ?? []) { if (connection.privateKey) { connection.privateKeys = [connection.privateKey] delete connection.privateKey diff --git a/terminus-core/src/theme.scss b/terminus-core/src/theme.scss index f958ad3a..4f9c025b 100644 --- a/terminus-core/src/theme.scss +++ b/terminus-core/src/theme.scss @@ -43,7 +43,10 @@ app-root { .btn-tab-bar { background: transparent; &:hover { background: rgba(0, 0, 0, .25) !important; } - &:active { background: rgba(0, 0, 0, .5) !important; } + &:active, &[aria-expanded-true] { background: rgba(0, 0, 0, .5) !important; } + &:focus { + box-shadow: none; + } &::after { display: none; @@ -386,3 +389,7 @@ search-panel { hr { border-color: $list-group-border-color; } + +.dropdown-menu { + box-shadow: $dropdown-box-shadow; +} diff --git a/terminus-core/src/theme.vars.scss b/terminus-core/src/theme.vars.scss index bb53b7ed..a7eb6177 100644 --- a/terminus-core/src/theme.vars.scss +++ b/terminus-core/src/theme.vars.scss @@ -191,5 +191,5 @@ $modal-footer-border-color: #222; $modal-footer-border-width: 1px; $modal-content-border-width: 0; -$progress-bar-bg: $table-bg; +$progress-bg: $table-bg; $progress-height: 3px; diff --git a/terminus-electron/src/services/platform.service.ts b/terminus-electron/src/services/platform.service.ts index bcd315df..486621d4 100644 --- a/terminus-electron/src/services/platform.service.ts +++ b/terminus-electron/src/services/platform.service.ts @@ -4,8 +4,8 @@ import * as fsSync from 'fs' import * as os from 'os' import promiseIpc from 'electron-promise-ipc' import { execFile } from 'mz/child_process' -import { Injectable } from '@angular/core' -import { PlatformService, ClipboardContent, HostAppService, Platform, ElectronService, MenuItemOptions, MessageBoxOptions, MessageBoxResult, FileUpload } from 'terminus-core' +import { Injectable, NgZone } from '@angular/core' +import { PlatformService, ClipboardContent, HostAppService, Platform, ElectronService, MenuItemOptions, MessageBoxOptions, MessageBoxResult, FileUpload, FileDownload, FileUploadOptions } from 'terminus-core' const fontManager = require('fontmanager-redux') // eslint-disable-line /* eslint-disable block-scoped-var */ @@ -26,6 +26,7 @@ export class ElectronPlatformService extends PlatformService { constructor ( private hostApp: HostAppService, private electron: ElectronService, + private zone: NgZone, ) { super() this.configPath = path.join(electron.app.getPath('userData'), 'config.yaml') @@ -159,24 +160,58 @@ export class ElectronPlatformService extends PlatformService { this.electron.app.exit(0) } - async startUpload (): Promise { + async startUpload (options?: FileUploadOptions): Promise { + options ??= { multiple: false } + + const properties: any[] = ['openFile', 'treatPackageAsDirectory'] + if (options.multiple) { + properties.push('multiSelections') + } + const result = await this.electron.dialog.showOpenDialog( this.hostApp.getWindow(), { buttonLabel: 'Select', - properties: ['multiSelections', 'openFile', 'treatPackageAsDirectory'], + properties, }, ) if (result.canceled) { return [] } - return Promise.all(result.filePaths.map(async path => { - const t = new ElectronFileUpload(path) - await t.open() - return t + return Promise.all(result.filePaths.map(async p => { + const transfer = new ElectronFileUpload(p) + await this.wrapPromise(transfer.open()) + this.fileTransferStarted.next(transfer) + return transfer })) } + + async startDownload (name: string, size: number): Promise { + const result = await this.electron.dialog.showSaveDialog( + this.hostApp.getWindow(), + { + defaultPath: name, + }, + ) + if (!result.filePath) { + return null + } + const transfer = new ElectronFileDownload(result.filePath, size) + await this.wrapPromise(transfer.open()) + this.fileTransferStarted.next(transfer) + return transfer + } + + private wrapPromise (promise: Promise): Promise { + return new Promise((resolve, reject) => { + promise.then(result => { + this.zone.run(() => resolve(result)) + }).catch(error => { + this.zone.run(() => reject(error)) + }) + }) + } } class ElectronFileUpload extends FileUpload { @@ -205,7 +240,6 @@ class ElectronFileUpload extends FileUpload { async read (): Promise { const result = await this.file.read(this.buffer, 0, this.buffer.length, null) this.increaseProgress(result.bytesRead) - console.log(result) return this.buffer.slice(0, result.bytesRead) } @@ -215,18 +249,17 @@ class ElectronFileUpload extends FileUpload { } class ElectronFileDownload extends FileDownload { - private size: number private file: fs.FileHandle - private buffer: Buffer - constructor (private filePath: string) { + constructor ( + private filePath: string, + private size: number, + ) { super() - this.buffer = Buffer.alloc(256 * 1024) } async open (): Promise { - this.size = (await fs.stat(this.filePath)).size - this.file = await fs.open(this.filePath, 'r') + this.file = await fs.open(this.filePath, 'w') } getName (): string { @@ -237,11 +270,13 @@ class ElectronFileDownload extends FileDownload { return this.size } - async read (): Promise { - const result = await this.file.read(this.buffer, 0, this.buffer.length, null) - this.increaseProgress(result.bytesRead) - console.log(result) - return this.buffer.slice(0, result.bytesRead) + async write (buffer: Buffer): Promise { + let pos = 0 + while (pos < buffer.length) { + const result = await this.file.write(buffer, pos, buffer.length - pos, null) + this.increaseProgress(result.bytesWritten) + pos += result.bytesWritten + } } close (): void { diff --git a/terminus-terminal/src/features/debug.ts b/terminus-terminal/src/features/debug.ts index e792d503..064d1d09 100644 --- a/terminus-terminal/src/features/debug.ts +++ b/terminus-terminal/src/features/debug.ts @@ -1,16 +1,13 @@ -import * as fs from 'fs' import { Injectable } from '@angular/core' import { TerminalDecorator } from '../api/decorator' import { BaseTerminalTabComponent } from '../api/baseTerminalTab.component' -import { ElectronService, HostAppService, PlatformService } from 'terminus-core' +import { PlatformService } from 'terminus-core' /** @hidden */ @Injectable() export class DebugDecorator extends TerminalDecorator { constructor ( - private electron: ElectronService, private platform: PlatformService, - private hostApp: HostAppService, ) { super() } @@ -63,28 +60,21 @@ export class DebugDecorator extends TerminalDecorator { } private async loadFile (): Promise { - const result = await this.electron.dialog.showOpenDialog( - this.hostApp.getWindow(), - { - buttonLabel: 'Load', - properties: ['openFile', 'treatPackageAsDirectory'], - }, - ) - if (result.filePaths.length) { - return fs.readFileSync(result.filePaths[0], { encoding: 'utf-8' }) + const transfer = await this.platform.startUpload() + if (!transfer.length) { + return null } - return null + const data = await transfer[0].readAll() + transfer[0].close() + return data.toString() } private async saveFile (content: string, name: string) { - const result = await this.electron.dialog.showSaveDialog( - this.hostApp.getWindow(), - { - defaultPath: name, - }, - ) - if (result.filePath) { - fs.writeFileSync(result.filePath, content) + const data = Buffer.from(content) + const transfer = await this.platform.startDownload(name, data.length) + if (transfer) { + transfer.write(data) + transfer.close() } } diff --git a/terminus-terminal/src/features/zmodem.ts b/terminus-terminal/src/features/zmodem.ts index 71ab368b..8df0c33e 100644 --- a/terminus-terminal/src/features/zmodem.ts +++ b/terminus-terminal/src/features/zmodem.ts @@ -1,12 +1,11 @@ import colors from 'ansi-colors' import * as ZModem from 'zmodem.js' -import * as fs from 'fs' import { Observable } from 'rxjs' -import { filter } from 'rxjs/operators' +import { filter, first } from 'rxjs/operators' import { Injectable } from '@angular/core' import { TerminalDecorator } from '../api/decorator' import { BaseTerminalTabComponent } from '../api/baseTerminalTab.component' -import { LogService, Logger, ElectronService, HostAppService, HotkeysService, PlatformService, FileUpload } from 'terminus-core' +import { LogService, Logger, HotkeysService, PlatformService, FileUpload } from 'terminus-core' const SPACER = ' ' @@ -20,8 +19,6 @@ export class ZModemDecorator extends TerminalDecorator { constructor ( log: LogService, hotkeys: HotkeysService, - private electron: ElectronService, - private hostApp: HostAppService, private platform: PlatformService, ) { super() @@ -87,7 +84,7 @@ export class ZModemDecorator extends TerminalDecorator { this.logger.info('new session', zsession) if (zsession.type === 'send') { - const transfers = await this.platform.startUpload() + const transfers = await this.platform.startUpload({ multiple: true }) let filesRemaining = transfers.length let sizeRemaining = transfers.reduce((a, b) => a + b.getSize(), 0) for (const transfer of transfers) { @@ -116,20 +113,14 @@ export class ZModemDecorator extends TerminalDecorator { } = xfer.get_details() this.showMessage(terminal, colors.bgYellow.black(' Offered ') + ' ' + details.name, true) this.logger.info('offered', xfer) - const result = await this.electron.dialog.showSaveDialog( - this.hostApp.getWindow(), - { - defaultPath: details.name, - }, - ) - if (!result.filePath) { + + const transfer = await this.platform.startDownload(details.name, details.size) + if (!transfer) { this.showMessage(terminal, colors.bgRed.black(' Rejected ') + ' ' + details.name) xfer.skip() return } - const stream = fs.createWriteStream(result.filePath) - let bytesSent = 0 let canceled = false const cancelSubscription = this.cancelEvent.subscribe(() => { if (terminal.hasFocus) { @@ -147,18 +138,19 @@ export class ZModemDecorator extends TerminalDecorator { if (canceled) { return } - stream.write(Buffer.from(chunk)) - bytesSent += chunk.length - this.showMessage(terminal, colors.bgYellow.black(' ' + Math.round(100 * bytesSent / details.size).toString().padStart(3, ' ') + '% ') + ' ' + details.name, true) + transfer.write(Buffer.from(chunk)) + this.showMessage(terminal, colors.bgYellow.black(' ' + Math.round(100 * transfer.getCompletedBytes() / details.size).toString().padStart(3, ' ') + '% ') + ' ' + details.name, true) }, }), - this.cancelEvent.toPromise(), + this.cancelEvent.pipe(first()).toPromise(), ]) // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (canceled) { + transfer.cancel() this.showMessage(terminal, colors.bgRed.black(' Canceled ') + ' ' + details.name) } else { + transfer.close() this.showMessage(terminal, colors.bgGreen.black(' Received ') + ' ' + details.name) } } catch { @@ -166,7 +158,6 @@ export class ZModemDecorator extends TerminalDecorator { } cancelSubscription.unsubscribe() - stream.end() } private async sendFile (terminal, zsession, transfer: FileUpload, filesRemaining, sizeRemaining) { @@ -191,6 +182,7 @@ export class ZModemDecorator extends TerminalDecorator { while (true) { const chunk = await transfer.read() + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (canceled || !chunk.length) { break } @@ -199,6 +191,7 @@ export class ZModemDecorator extends TerminalDecorator { this.showMessage(terminal, colors.bgYellow.black(' ' + Math.round(100 * transfer.getCompletedBytes() / offer.size).toString().padStart(3, ' ') + '% ') + offer.name, true) } + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (canceled) { transfer.cancel() } else { @@ -216,6 +209,7 @@ export class ZModemDecorator extends TerminalDecorator { cancelSubscription.unsubscribe() } else { + transfer.cancel() this.showMessage(terminal, colors.bgRed.black(' Rejected ') + ' ' + offer.name) this.logger.warn('rejected by the other side') } -- GitLab