Commit 87368c7a authored by kelvinp's avatar kelvinp Committed by Commit bot

Separate host desktop related functionality into remoting.HostDesktop

remoting.ClientPluginImpl has grown organically over the years and become
a dumping ground of all functionality related to the plugin.

This CL moves all  host desktop related implementation from
remoting.ClientPluginImpl into remoting.ClientPlugin.HostDesktopImpl.

BUG=457890
NOTRY=true
TEST=Unit test passed locally and manually verified ShrinkToFit, bumpScrolling and resizeToClient works

Review URL: https://codereview.chromium.org/929323003

Cr-Commit-Position: refs/heads/master@{#317187}
parent 7ee02bed
......@@ -119,6 +119,7 @@
'remoting_webapp_js_client_files': [
'webapp/crd/js/client_plugin.js',
'webapp/crd/js/client_plugin_impl.js',
'webapp/crd/js/client_plugin_host_desktop_impl.js',
# TODO(garykac) For client_screen:
# * Split out pin/access code stuff into separate file.
# * Move client logic into session_connector
......@@ -127,6 +128,7 @@
'webapp/crd/js/clipboard.js',
'webapp/crd/js/desktop_connected_view.js',
'webapp/crd/js/hangout_session.js',
'webapp/crd/js/host_desktop.js',
'webapp/crd/js/session_connector.js',
'webapp/crd/js/session_connector_impl.js',
'webapp/crd/js/smart_reconnector.js',
......
......@@ -22,11 +22,10 @@ remoting.MockClientPlugin = function(container) {
this.container_ = container;
this.element_ = document.createElement('div');
this.element_.style.backgroundImage = 'linear-gradient(45deg, blue, red)';
this.width_ = 640;
this.height_ = 480;
this.connectionStatusUpdateHandler_ = null;
this.desktopSizeUpdateHandler_ = null;
this.container_.appendChild(this.element_);
this.hostDesktop_ = new remoting.MockClientPlugin.HostDesktop();
};
remoting.MockClientPlugin.prototype.dispose = function() {
......@@ -35,20 +34,8 @@ remoting.MockClientPlugin.prototype.dispose = function() {
this.connectionStatusUpdateHandler_ = null;
};
remoting.MockClientPlugin.prototype.getDesktopWidth = function() {
return this.width_;
};
remoting.MockClientPlugin.prototype.getDesktopHeight = function() {
return this.height_;
};
remoting.MockClientPlugin.prototype.getDesktopXDpi = function() {
return 96;
};
remoting.MockClientPlugin.prototype.getDesktopYDpi = function() {
return 96;
remoting.MockClientPlugin.prototype.hostDesktop = function() {
return this.hostDesktop_;
};
remoting.MockClientPlugin.prototype.element = function() {
......@@ -79,15 +66,6 @@ remoting.MockClientPlugin.prototype.remapKey = function(from, to) {};
remoting.MockClientPlugin.prototype.releaseAllKeys = function() {};
remoting.MockClientPlugin.prototype.notifyClientResolution =
function(width, height, dpi) {
this.width_ = width;
this.height_ = height;
if (this.desktopSizeUpdateHandler_) {
window.setTimeout(this.desktopSizeUpdateHandler_, 0);
}
};
remoting.MockClientPlugin.prototype.onIncomingIq = function(iq) {};
remoting.MockClientPlugin.prototype.isSupportedVersion = function() {
......@@ -152,11 +130,6 @@ remoting.MockClientPlugin.prototype.setRouteChangedHandler =
remoting.MockClientPlugin.prototype.setConnectionReadyHandler =
function(handler) {};
remoting.MockClientPlugin.prototype.setDesktopSizeUpdateHandler =
function(handler) {
this.desktopSizeUpdateHandler_ = handler;
};
remoting.MockClientPlugin.prototype.setCapabilitiesHandler =
function(handler) {};
......@@ -175,6 +148,61 @@ remoting.MockClientPlugin.prototype.setFetchThirdPartyTokenHandler =
remoting.MockClientPlugin.prototype.setFetchPinHandler =
function(handler) {};
/**
* @constructor
* @implements {remoting.HostDesktop}
* @extends {base.EventSourceImpl}
*/
remoting.MockClientPlugin.HostDesktop = function() {
/** @private */
this.width_ = 0;
/** @private */
this.height_ = 0;
/** @private */
this.xDpi_ = 96;
/** @private */
this.yDpi_ = 96;
/** @private */
this.resizable_ = true;
this.defineEvents(base.values(remoting.HostDesktop.Events));
};
base.extend(remoting.MockClientPlugin.HostDesktop, base.EventSourceImpl);
/**
* @return {{width:number, height:number, xDpi:number, yDpi:number}}
* @override
*/
remoting.MockClientPlugin.HostDesktop.prototype.getDimensions = function() {
return {
width: this.width_,
height: this.height_,
xDpi: this.xDpi_,
yDpi: this.yDpi_
};
};
/**
* @return {boolean}
* @override
*/
remoting.MockClientPlugin.HostDesktop.prototype.isResizable = function() {
return this.resizable_;
};
/**
* @param {number} width
* @param {number} height
* @param {number} deviceScale
* @override
*/
remoting.MockClientPlugin.HostDesktop.prototype.resize =
function(width, height, deviceScale) {
this.width_ = width;
this.height_ = height;
this.xDpi_ = this.yDpi_ = Math.floor(deviceScale * 96);
this.raiseEvent(remoting.HostDesktop.Events.sizeChanged,
this.getDimensions());
};
/**
* @constructor
......
......@@ -19,24 +19,9 @@ var remoting = remoting || {};
remoting.ClientPlugin = function() {};
/**
* @return {number} The width of the remote desktop, in pixels.
* @return {remoting.HostDesktop}
*/
remoting.ClientPlugin.prototype.getDesktopWidth = function() {};
/**
* @return {number} The height of the remote desktop, in pixels.
*/
remoting.ClientPlugin.prototype.getDesktopHeight = function() {};
/**
* @return {number} The x-DPI of the remote desktop.
*/
remoting.ClientPlugin.prototype.getDesktopXDpi = function() {};
/**
* @return {number} The y-DPI of the remote desktop.
*/
remoting.ClientPlugin.prototype.getDesktopYDpi = function() {};
remoting.ClientPlugin.prototype.hostDesktop = function() {};
/**
* @return {HTMLElement} The DOM element representing the remote session.
......@@ -87,14 +72,6 @@ remoting.ClientPlugin.prototype.remapKey = function(from, to) {};
*/
remoting.ClientPlugin.prototype.releaseAllKeys = function() {};
/**
* @param {number} width
* @param {number} height
* @param {number} dpi
*/
remoting.ClientPlugin.prototype.notifyClientResolution =
function(width, height, dpi) {};
/**
* @param {string} iq
*/
......@@ -107,7 +84,7 @@ remoting.ClientPlugin.prototype.isSupportedVersion = function() {};
/**
* @param {remoting.ClientPlugin.Feature} feature
* @return {boolean} True if the plugin support the specified feature.
* @return {boolean} True if the plugin supports the specified feature.
*/
remoting.ClientPlugin.prototype.hasFeature = function(feature) {};
......@@ -215,20 +192,6 @@ remoting.ClientPlugin.prototype.setRouteChangedHandler = function(handler) {};
remoting.ClientPlugin.prototype.setConnectionReadyHandler =
function(handler) {};
/**
* @param {function():void} handler Callback for desktop size change
* notifications.
*/
remoting.ClientPlugin.prototype.setDesktopSizeUpdateHandler =
function(handler) {};
/**
* @param {function(Array<Array<number>>):void} handler Callback for desktop
* shape change notifications.
*/
remoting.ClientPlugin.prototype.setDesktopShapeUpdateHandler =
function(handler) {};
/**
* @param {function(!Array<string>):void} handler Callback to inform of
* capabilities negotiated between host and client.
......
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* @fileoverview
* Provides an interface to manage the Host Desktop of a remoting session.
*/
var remoting = remoting || {};
remoting.ClientPlugin = remoting.ClientPlugin || {};
(function() {
'use strict';
/**
* @param {remoting.ClientPluginImpl} plugin
* @param {function(Object):void} postMessageCallback Callback to post a message
* to the Client Plugin.
*
* @implements {remoting.HostDesktop}
* @extends {base.EventSourceImpl}
* @constructor
*/
remoting.ClientPlugin.HostDesktopImpl = function(plugin, postMessageCallback) {
/** @private */
this.plugin_ = plugin;
/** @private */
this.width_ = 0;
/** @private */
this.height_ = 0;
/** @private */
this.xDpi_ = 96;
/** @private */
this.yDpi_ = 96;
/** @private */
this.postMessageCallback_ = postMessageCallback;
this.defineEvents(base.values(remoting.HostDesktop.Events));
};
base.extend(remoting.ClientPlugin.HostDesktopImpl, base.EventSourceImpl);
/** @return {boolean} Whether the host supports desktop resizing. */
remoting.ClientPlugin.HostDesktopImpl.prototype.isResizable = function() {
return this.plugin_.hasFeature(
remoting.ClientPlugin.Feature.NOTIFY_CLIENT_RESOLUTION);
};
/** @return {{width:number, height:number, xDpi:number, yDpi:number}} */
remoting.ClientPlugin.HostDesktopImpl.prototype.getDimensions = function() {
return {
width: this.width_,
height: this.height_,
xDpi: this.xDpi_,
yDpi: this.yDpi_
};
};
/**
* @param {number} width
* @param {number} height
* @param {number} deviceScale
*/
remoting.ClientPlugin.HostDesktopImpl.prototype.resize = function(
width, height, deviceScale) {
if (this.isResizable()) {
var dpi = Math.floor(deviceScale * 96);
this.postMessageCallback_({
method: 'notifyClientResolution',
data: {
width: Math.floor(width * deviceScale),
height: Math.floor(height * deviceScale),
x_dpi: dpi,
y_dpi: dpi
}
});
}
};
/**
* This function is called by |this.plugin_| when the size of the host
* desktop is changed.
*
* @param {remoting.ClientPluginMessage} message
*/
remoting.ClientPlugin.HostDesktopImpl.prototype.onSizeUpdated = function(
message) {
this.width_ = getNumberAttr(message.data, 'width');
this.height_ = getNumberAttr(message.data, 'height');
this.xDpi_ = getNumberAttr(message.data, 'x_dpi', 96);
this.yDpi_ = getNumberAttr(message.data, 'y_dpi', 96);
this.raiseEvent(remoting.HostDesktop.Events.sizeChanged,
this.getDimensions());
};
/**
* This function is called by |this.plugin_| when the shape of the host
* desktop is changed.
*
* @param {remoting.ClientPluginMessage} message
* @return {Array<{left:number, top:number, width:number, height:number}>}
* rectangles of the desktop shape.
*/
remoting.ClientPlugin.HostDesktopImpl.prototype.onShapeUpdated =
function(message) {
var shapes = getArrayAttr(message.data, 'rects');
var rects = shapes.map(
/** @param {Array.<number>} shape */
function(shape) {
if (!Array.isArray(shape) || shape.length != 4) {
throw 'Received invalid onDesktopShape message';
}
var rect = {};
rect.left = shape[0];
rect.top = shape[1];
rect.width = shape[2];
rect.height = shape[3];
return rect;
});
this.raiseEvent(remoting.HostDesktop.Events.shapeChanged, rects);
return rects;
};
}());
......@@ -48,15 +48,6 @@ remoting.ClientPluginImpl = function(container, onExtensionMessage,
*/
this.requiredCapabilities_ = requiredCapabilities;
/** @private */
this.desktopWidth_ = 0;
/** @private */
this.desktopHeight_ = 0;
/** @private */
this.desktopXDpi_ = 96;
/** @private */
this.desktopYDpi_ = 96;
/**
* @param {string} iq The Iq stanza received from the host.
* @private
......@@ -95,13 +86,6 @@ remoting.ClientPluginImpl = function(container, onExtensionMessage,
*/
this.fetchThirdPartyTokenHandler_ = function(
tokenUrl, hostPublicKey, scope) {};
/** @private */
this.onDesktopSizeUpdateHandler_ = function () {};
/**
* @param {Array<Array<number>>} rects
* @private
*/
this.onDesktopShapeUpdateHandler_ = function (rects) {};
/**
* @param {!Array<string>} capabilities The negotiated capabilities.
* @private
......@@ -180,6 +164,9 @@ remoting.ClientPluginImpl = function(container, onExtensionMessage,
if (remoting.settings.CLIENT_PLUGIN_TYPE == 'native') {
window.setTimeout(this.showPluginForClickToPlay_.bind(this), 500);
}
this.hostDesktop_ = new remoting.ClientPlugin.HostDesktopImpl(
this, this.postMessage_.bind(this));
};
/**
......@@ -264,22 +251,6 @@ remoting.ClientPluginImpl.prototype.setConnectionReadyHandler =
this.onConnectionReadyHandler_ = handler;
};
/**
* @param {function():void} handler
*/
remoting.ClientPluginImpl.prototype.setDesktopSizeUpdateHandler =
function(handler) {
this.onDesktopSizeUpdateHandler_ = handler;
};
/**
* @param {function(Array<Array<number>>):void} handler
*/
remoting.ClientPluginImpl.prototype.setDesktopShapeUpdateHandler =
function(handler) {
this.onDesktopShapeUpdateHandler_ = handler;
};
/**
* @param {function(!Array<string>):void} handler
*/
......@@ -374,14 +345,6 @@ remoting.ClientPluginImpl.prototype.handleMessageMethod_ = function(message) {
tokenize(getStringAttr(message.data, 'apiFeatures'));
// Negotiate capabilities.
/** @type {!Array<string>} */
var requestedCapabilities = [];
if ('requestedCapabilities' in message.data) {
requestedCapabilities =
tokenize(getStringAttr(message.data, 'requestedCapabilities'));
}
/** @type {!Array<string>} */
var supportedCapabilities = [];
if ('supportedCapabilities' in message.data) {
......@@ -425,23 +388,9 @@ remoting.ClientPluginImpl.prototype.handleMessageMethod_ = function(message) {
this.onRouteChangedHandler_(channel, connectionType);
} else if (message.method == 'onDesktopSize') {
this.desktopWidth_ = getNumberAttr(message.data, 'width');
this.desktopHeight_ = getNumberAttr(message.data, 'height');
this.desktopXDpi_ = getNumberAttr(message.data, 'x_dpi', 96);
this.desktopYDpi_ = getNumberAttr(message.data, 'y_dpi', 96);
this.onDesktopSizeUpdateHandler_();
this.hostDesktop_.onSizeUpdated(message);
} else if (message.method == 'onDesktopShape') {
var rects = getArrayAttr(message.data, 'rects');
for (var i = 0; i < rects.length; ++i) {
/** @type {Array<number>} */
var rect = rects[i];
if (typeof rect != 'object' || rect.length != 4) {
throw 'Received invalid onDesktopShape message';
}
}
this.onDesktopShapeUpdateHandler_(rects);
this.hostDesktop_.onShapeUpdated(message);
} else if (message.method == 'onPerfStats') {
// Return value is ignored. These calls will throw an error if the value
// is not a number.
......@@ -758,14 +707,7 @@ remoting.ClientPluginImpl.prototype.sendClipboardItem =
*/
remoting.ClientPluginImpl.prototype.notifyClientResolution =
function(width, height, device_scale) {
if (this.hasFeature(remoting.ClientPlugin.Feature.NOTIFY_CLIENT_RESOLUTION)) {
var dpi = Math.floor(device_scale * 96);
this.plugin_.postMessage(JSON.stringify(
{ method: 'notifyClientResolution',
data: { width: Math.floor(width * device_scale),
height: Math.floor(height * device_scale),
x_dpi: dpi, y_dpi: dpi }}));
}
this.hostDesktop_.resize(width, height, device_scale);
};
/**
......@@ -907,21 +849,9 @@ remoting.ClientPluginImpl.prototype.sendClientMessage =
};
remoting.ClientPluginImpl.prototype.getDesktopWidth = function() {
return this.desktopWidth_;
}
remoting.ClientPluginImpl.prototype.getDesktopHeight = function() {
return this.desktopHeight_;
}
remoting.ClientPluginImpl.prototype.getDesktopXDpi = function() {
return this.desktopXDpi_;
}
remoting.ClientPluginImpl.prototype.getDesktopYDpi = function() {
return this.desktopYDpi_;
}
remoting.ClientPluginImpl.prototype.hostDesktop = function() {
return this.hostDesktop_;
};
/**
* If we haven't yet received a "hello" message from the plugin, change its
......@@ -956,6 +886,17 @@ remoting.ClientPluginImpl.prototype.hidePluginForClickToPlay_ = function() {
this.plugin_.style.position = '';
};
/**
* Callback passed to submodules to post a message to the plugin.
*
* @param {Object} message
* @private
*/
remoting.ClientPluginImpl.prototype.postMessage_ = function(message) {
if (this.plugin_ && this.plugin_.postMessage) {
this.plugin_.postMessage(JSON.stringify(message));
}
};
/**
* @constructor
......
......@@ -244,6 +244,8 @@ remoting.ClientSession.Capability = {
// Let the host know that we're interested in knowing whether or not it
// rate limits desktop-resize requests.
// TODO(kelvinp): This has been supported since M-29. Currently we only have
// <1000 users on M-29 or below. Remove this and the capability on the host.
RATE_LIMIT_RESIZE_REQUESTS: 'rateLimitResizeRequests',
// Indicates that host/client supports Google Drive integration, and that the
......
......@@ -114,6 +114,11 @@ remoting.DesktopConnectedView = function(session, container, hostDisplayName,
/** @type {remoting.VideoFrameRecorder} @private */
this.videoFrameRecorder_ = null;
/** @private {base.Disposables} */
this.eventHooks_ = null;
this.defineEvents(base.values(remoting.DesktopConnectedView.Events));
};
base.extend(remoting.DesktopConnectedView, base.EventSourceImpl);
......@@ -204,7 +209,7 @@ remoting.DesktopConnectedView.prototype.setPluginSizeForBumpScrollTesting =
*/
remoting.DesktopConnectedView.prototype.notifyClientResolution_ = function() {
var clientArea = this.getClientArea_();
this.plugin_.notifyClientResolution(clientArea.width * this.desktopScale_,
this.plugin_.hostDesktop().resize(clientArea.width * this.desktopScale_,
clientArea.height * this.desktopScale_,
window.devicePixelRatio);
};
......@@ -303,10 +308,16 @@ remoting.DesktopConnectedView.prototype.onPluginInitialized_ = function(
this.plugin_.allowMouseLock();
}
this.plugin_.setDesktopShapeUpdateHandler(
this.onDesktopShapeChanged_.bind(this));
this.plugin_.setDesktopSizeUpdateHandler(
this.onDesktopSizeChanged_.bind(this));
base.dispose(this.eventHooks_);
var hostDesktop = this.plugin_.hostDesktop();
this.eventHooks_ = new base.Disposables(
new base.EventHook(
hostDesktop, remoting.HostDesktop.Events.sizeChanged,
this.onDesktopSizeChanged_.bind(this)),
new base.EventHook(
hostDesktop, remoting.HostDesktop.Events.shapeChanged,
this.onDesktopShapeChanged_.bind(this)));
this.plugin_.setMouseCursorHandler(this.updateMouseCursorImage_.bind(this));
this.onInitialized_(remoting.Error.NONE, this.plugin_);
......@@ -320,11 +331,12 @@ remoting.DesktopConnectedView.prototype.onPluginInitialized_ = function(
* @private
*/
remoting.DesktopConnectedView.prototype.onDesktopSizeChanged_ = function() {
var desktop = this.plugin_.hostDesktop().getDimensions();
console.log('desktop size changed: ' +
this.plugin_.getDesktopWidth() + 'x' +
this.plugin_.getDesktopHeight() +' @ ' +
this.plugin_.getDesktopXDpi() + 'x' +
this.plugin_.getDesktopYDpi() + ' DPI');
desktop.width + 'x' +
desktop.height +' @ ' +
desktop.xDpi + 'x' +
desktop.yDpi + ' DPI');
this.updateDimensions();
this.updateScrollbarVisibility();
};
......@@ -370,11 +382,7 @@ remoting.DesktopConnectedView.prototype.onResize = function() {
// Defer notifying the host of the change until the window stops resizing, to
// avoid overloading the control channel with notifications.
if (this.resizeToClient_) {
var kResizeRateLimitMs = 1000;
if (this.session_.hasCapability(
remoting.ClientSession.Capability.RATE_LIMIT_RESIZE_REQUESTS)) {
kResizeRateLimitMs = 250;
}
var kResizeRateLimitMs = 250;
var clientArea = this.getClientArea_();
this.notifyClientResolutionTimer_ = window.setTimeout(
this.notifyClientResolution_.bind(this),
......@@ -411,6 +419,8 @@ remoting.DesktopConnectedView.prototype.onConnectionReady = function(ready) {
*/
remoting.DesktopConnectedView.prototype.removePlugin = function() {
if (this.plugin_) {
base.dispose(this.eventHooks_);
this.eventHooks_ = null;
this.plugin_.element().removeEventListener(
'focus', this.callPluginGotFocus_, false);
this.plugin_.element().removeEventListener(
......@@ -744,15 +754,15 @@ remoting.DesktopConnectedView.prototype.scroll_ = function(dx, dy) {
* @return {void} Nothing.
*/
remoting.DesktopConnectedView.prototype.updateDimensions = function() {
if (this.plugin_.getDesktopWidth() == 0 ||
this.plugin_.getDesktopHeight() == 0) {
var desktopSize = this.plugin_.hostDesktop().getDimensions();
if (desktopSize.width === 0 ||
desktopSize.height === 0) {
return;
}
var desktopSize = { width: this.plugin_.getDesktopWidth(),
height: this.plugin_.getDesktopHeight() };
var desktopDpi = { x: this.plugin_.getDesktopXDpi(),
y: this.plugin_.getDesktopYDpi() };
var desktopDpi = { x: desktopSize.xDpi,
y: desktopSize.yDpi };
var newSize = remoting.DesktopConnectedView.choosePluginSize(
this.getClientArea_(), window.devicePixelRatio,
desktopSize, desktopDpi, this.desktopScale_,
......@@ -920,15 +930,16 @@ remoting.DesktopConnectedView.prototype.updateScrollbarVisibility = function() {
// Determine whether or not horizontal or vertical scrollbars are
// required, taking into account their width.
var clientArea = this.getClientArea_();
needsVerticalScroll = clientArea.height < this.plugin_.getDesktopHeight();
needsHorizontalScroll = clientArea.width < this.plugin_.getDesktopWidth();
var desktopSize = this.plugin_.hostDesktop().getDimensions();
needsVerticalScroll = clientArea.height < desktopSize.height;
needsHorizontalScroll = clientArea.width < desktopSize.width;
var kScrollBarWidth = 16;
if (needsHorizontalScroll && !needsVerticalScroll) {
needsVerticalScroll =
clientArea.height - kScrollBarWidth < this.plugin_.getDesktopHeight();
clientArea.height - kScrollBarWidth < desktopSize.height;
} else if (!needsHorizontalScroll && needsVerticalScroll) {
needsHorizontalScroll =
clientArea.width - kScrollBarWidth < this.plugin_.getDesktopWidth();
clientArea.width - kScrollBarWidth < desktopSize.width;
}
}
......
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* @fileoverview
* Interface abstracting the functionality of the HostDesktop.
*/
var remoting = remoting || {};
(function() {
'use strict';
/**
* @interface
* @extends {base.EventSource}
*/
remoting.HostDesktop = function() {};
/** @return {boolean} Whether the host supports desktop resizing. */
remoting.HostDesktop.prototype.isResizable = function() {};
/** @enum {string} */
remoting.HostDesktop.Events = {
// Fired when the size of the host desktop changes with the desktop dimensions
// {{width:number, height:number, xDpi:number, yDpi:number}}
sizeChanged: 'sizeChanged',
// Fired when the shape of the host desktop changes with an array of
// rectangles of desktop shapes as the event data.
// Array<{left:number, top:number, width:number, height:number}>
shapeChanged: 'shapeChanged'
};
/**
* @return {{width:number, height:number, xDpi:number, yDpi:number}}
* The dimensions and DPI settings of the host desktop.
*/
remoting.HostDesktop.prototype.getDimensions = function() {};
/**
* Resize the desktop of the host to |width|, |height| and |deviceScale|.
*
* @param {number} width The width of the desktop in DIPs.
* @param {number} height The height of the desktop in DIPs.
* @param {number} deviceScale
*/
remoting.HostDesktop.prototype.resize = function(width, height, deviceScale) {};
})();
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment