Merge branch 'develop' into kegan/reg-refactor
This commit is contained in:
commit
e800d3d477
9 changed files with 209 additions and 39 deletions
|
@ -24,7 +24,7 @@
|
||||||
"flux": "^2.0.3",
|
"flux": "^2.0.3",
|
||||||
"glob": "^5.0.14",
|
"glob": "^5.0.14",
|
||||||
"linkifyjs": "^2.0.0-beta.4",
|
"linkifyjs": "^2.0.0-beta.4",
|
||||||
"matrix-js-sdk": "^0.3.0",
|
"matrix-js-sdk": "https://github.com/matrix-org/matrix-js-sdk.git#develop",
|
||||||
"optimist": "^0.6.1",
|
"optimist": "^0.6.1",
|
||||||
"q": "^1.4.1",
|
"q": "^1.4.1",
|
||||||
"react": "^0.14.2",
|
"react": "^0.14.2",
|
||||||
|
|
|
@ -15,58 +15,54 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var MatrixClientPeg = require("./MatrixClientPeg");
|
var MatrixClientPeg = require("./MatrixClientPeg");
|
||||||
|
var dis = require("./dispatcher");
|
||||||
|
|
||||||
// Time in ms after that a user is considered as unavailable/away
|
// Time in ms after that a user is considered as unavailable/away
|
||||||
var UNAVAILABLE_TIME_MS = 3 * 60 * 1000; // 3 mins
|
var UNAVAILABLE_TIME_MS = 3 * 60 * 1000; // 3 mins
|
||||||
var PRESENCE_STATES = ["online", "offline", "unavailable"];
|
var PRESENCE_STATES = ["online", "offline", "unavailable"];
|
||||||
|
|
||||||
// The current presence state
|
class Presence {
|
||||||
var state, timer;
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start listening the user activity to evaluate his presence state.
|
* Start listening the user activity to evaluate his presence state.
|
||||||
* Any state change will be sent to the Home Server.
|
* Any state change will be sent to the Home Server.
|
||||||
*/
|
*/
|
||||||
start: function() {
|
start() {
|
||||||
var self = this;
|
|
||||||
this.running = true;
|
this.running = true;
|
||||||
if (undefined === state) {
|
if (undefined === this.state) {
|
||||||
// The user is online if they move the mouse or press a key
|
|
||||||
document.onmousemove = function() { self._resetTimer(); };
|
|
||||||
document.onkeypress = function() { self._resetTimer(); };
|
|
||||||
this._resetTimer();
|
this._resetTimer();
|
||||||
|
this.dispatcherRef = dis.register(this._onUserActivity.bind(this));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stop tracking user activity
|
* Stop tracking user activity
|
||||||
*/
|
*/
|
||||||
stop: function() {
|
stop() {
|
||||||
this.running = false;
|
this.running = false;
|
||||||
if (timer) {
|
if (this.timer) {
|
||||||
clearTimeout(timer);
|
clearInterval(this.timer);
|
||||||
timer = undefined;
|
this.timer = undefined;
|
||||||
|
dis.unregister(this.dispatcherRef);
|
||||||
|
}
|
||||||
|
this.state = undefined;
|
||||||
}
|
}
|
||||||
state = undefined;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the current presence state.
|
* Get the current presence state.
|
||||||
* @returns {string} the presence state (see PRESENCE enum)
|
* @returns {string} the presence state (see PRESENCE enum)
|
||||||
*/
|
*/
|
||||||
getState: function() {
|
getState() {
|
||||||
return state;
|
return this.state;
|
||||||
},
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the presence state.
|
* Set the presence state.
|
||||||
* If the state has changed, the Home Server will be notified.
|
* If the state has changed, the Home Server will be notified.
|
||||||
* @param {string} newState the new presence state (see PRESENCE enum)
|
* @param {string} newState the new presence state (see PRESENCE enum)
|
||||||
*/
|
*/
|
||||||
setState: function(newState) {
|
setState(newState) {
|
||||||
if (newState === state) {
|
if (newState === this.state) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (PRESENCE_STATES.indexOf(newState) === -1) {
|
if (PRESENCE_STATES.indexOf(newState) === -1) {
|
||||||
|
@ -75,33 +71,42 @@ module.exports = {
|
||||||
if (!this.running) {
|
if (!this.running) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
state = newState;
|
var old_state = this.state;
|
||||||
MatrixClientPeg.get().setPresence(state).done(function() {
|
this.state = newState;
|
||||||
|
var self = this;
|
||||||
|
MatrixClientPeg.get().setPresence(this.state).done(function() {
|
||||||
console.log("Presence: %s", newState);
|
console.log("Presence: %s", newState);
|
||||||
}, function(err) {
|
}, function(err) {
|
||||||
console.error("Failed to set presence: %s", err);
|
console.error("Failed to set presence: %s", err);
|
||||||
|
self.state = old_state;
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Callback called when the user made no action on the page for UNAVAILABLE_TIME ms.
|
* Callback called when the user made no action on the page for UNAVAILABLE_TIME ms.
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
_onUnavailableTimerFire: function() {
|
_onUnavailableTimerFire() {
|
||||||
this.setState("unavailable");
|
this.setState("unavailable");
|
||||||
},
|
}
|
||||||
|
|
||||||
|
_onUserActivity() {
|
||||||
|
this._resetTimer();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Callback called when the user made an action on the page
|
* Callback called when the user made an action on the page
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
_resetTimer: function() {
|
_resetTimer() {
|
||||||
var self = this;
|
var self = this;
|
||||||
this.setState("online");
|
this.setState("online");
|
||||||
// Re-arm the timer
|
// Re-arm the timer
|
||||||
clearTimeout(timer);
|
clearTimeout(this.timer);
|
||||||
timer = setTimeout(function() {
|
this.timer = setTimeout(function() {
|
||||||
self._onUnavailableTimerFire();
|
self._onUnavailableTimerFire();
|
||||||
}, UNAVAILABLE_TIME_MS);
|
}, UNAVAILABLE_TIME_MS);
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
|
module.exports = new Presence();
|
||||||
|
|
57
src/UserActivity.js
Normal file
57
src/UserActivity.js
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
/*
|
||||||
|
Copyright 2015 OpenMarket Ltd
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
var dis = require("./dispatcher");
|
||||||
|
|
||||||
|
var MIN_DISPATCH_INTERVAL = 1 * 1000;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class watches for user activity (moving the mouse or pressing a key)
|
||||||
|
* and dispatches the user_activity action at times when the user is interacting
|
||||||
|
* with the app (but at a much lower frequency than mouse move events)
|
||||||
|
*/
|
||||||
|
class UserActivity {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start listening to user activity
|
||||||
|
*/
|
||||||
|
start() {
|
||||||
|
document.onmousemove = this._onUserActivity.bind(this);
|
||||||
|
document.onkeypress = this._onUserActivity.bind(this);
|
||||||
|
this.lastActivityAtTs = new Date().getTime();
|
||||||
|
this.lastDispatchAtTs = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop tracking user activity
|
||||||
|
*/
|
||||||
|
stop() {
|
||||||
|
document.onmousemove = undefined;
|
||||||
|
document.onkeypress = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
_onUserActivity() {
|
||||||
|
this.lastActivityAtTs = (new Date).getTime();
|
||||||
|
if (this.lastDispatchAtTs < this.lastActivityAtTs - MIN_DISPATCH_INTERVAL) {
|
||||||
|
this.lastDispatchAtTs = this.lastActivityAtTs;
|
||||||
|
dis.dispatch({
|
||||||
|
action: 'user_activity'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = new UserActivity();
|
|
@ -35,6 +35,10 @@ module.exports = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
componentWillReceiveProps: function(nextProps) {
|
||||||
|
this.refreshUrl();
|
||||||
|
},
|
||||||
|
|
||||||
defaultAvatarUrl: function(member, width, height, resizeMethod) {
|
defaultAvatarUrl: function(member, width, height, resizeMethod) {
|
||||||
if (this.skinnedDefaultAvatarUrl) {
|
if (this.skinnedDefaultAvatarUrl) {
|
||||||
return this.skinnedDefaultAvatarUrl(member, width, height, resizeMethod);
|
return this.skinnedDefaultAvatarUrl(member, width, height, resizeMethod);
|
||||||
|
@ -52,7 +56,7 @@ module.exports = {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
getInitialState: function() {
|
_computeUrl: function() {
|
||||||
var url = this.props.member.getAvatarUrl(
|
var url = this.props.member.getAvatarUrl(
|
||||||
MatrixClientPeg.get().getHomeserverUrl(),
|
MatrixClientPeg.get().getHomeserverUrl(),
|
||||||
this.props.width,
|
this.props.width,
|
||||||
|
@ -68,8 +72,20 @@ module.exports = {
|
||||||
this.props.resizeMethod
|
this.props.resizeMethod
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
return url;
|
||||||
|
},
|
||||||
|
|
||||||
|
refreshUrl: function() {
|
||||||
|
var newUrl = this._computeUrl();
|
||||||
|
if (newUrl != this.currentUrl) {
|
||||||
|
this.currentUrl = newUrl;
|
||||||
|
this.setState({imageUrl: newUrl});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
getInitialState: function() {
|
||||||
return {
|
return {
|
||||||
imageUrl: url
|
imageUrl: this._computeUrl()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -41,10 +41,26 @@ module.exports = {
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillReceiveProps: function(nextProps) {
|
componentWillReceiveProps: function(nextProps) {
|
||||||
|
this.refreshImageUrl();
|
||||||
|
},
|
||||||
|
|
||||||
|
refreshImageUrl: function(nextProps) {
|
||||||
|
// If the list has changed, we start from scratch and re-check, but
|
||||||
|
// don't do so unless the list has changed or we'd re-try fetching
|
||||||
|
// images each time we re-rendered
|
||||||
|
var newList = this.getUrlList();
|
||||||
|
var differs = false;
|
||||||
|
for (var i = 0; i < newList.length && i < this.urlList.length; ++i) {
|
||||||
|
if (this.urlList[i] != newList[i]) differs = true;
|
||||||
|
}
|
||||||
|
if (this.urlList.length != newList.length) differs = true;
|
||||||
|
|
||||||
|
if (differs) {
|
||||||
this._update();
|
this._update();
|
||||||
this.setState({
|
this.setState({
|
||||||
imageUrl: this._nextUrl()
|
imageUrl: this._nextUrl()
|
||||||
});
|
});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
_update: function() {
|
_update: function() {
|
||||||
|
|
|
@ -38,6 +38,7 @@ module.exports = {
|
||||||
componentWillMount: function() {
|
componentWillMount: function() {
|
||||||
var cli = MatrixClientPeg.get();
|
var cli = MatrixClientPeg.get();
|
||||||
cli.on("RoomState.members", this.onRoomStateMember);
|
cli.on("RoomState.members", this.onRoomStateMember);
|
||||||
|
cli.on("RoomMember.name", this.onRoomMemberName);
|
||||||
cli.on("Room", this.onRoom); // invites
|
cli.on("Room", this.onRoom); // invites
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -45,6 +46,7 @@ module.exports = {
|
||||||
if (MatrixClientPeg.get()) {
|
if (MatrixClientPeg.get()) {
|
||||||
MatrixClientPeg.get().removeListener("Room", this.onRoom);
|
MatrixClientPeg.get().removeListener("Room", this.onRoom);
|
||||||
MatrixClientPeg.get().removeListener("RoomState.members", this.onRoomStateMember);
|
MatrixClientPeg.get().removeListener("RoomState.members", this.onRoomStateMember);
|
||||||
|
MatrixClientPeg.get().removeListener("RoomMember.name", this.onRoomMemberName);
|
||||||
MatrixClientPeg.get().removeListener("User.presence", this.userPresenceFn);
|
MatrixClientPeg.get().removeListener("User.presence", this.userPresenceFn);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -97,6 +99,10 @@ module.exports = {
|
||||||
this._updateList();
|
this._updateList();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onRoomMemberName: function(ev, member) {
|
||||||
|
this._updateList();
|
||||||
|
},
|
||||||
|
|
||||||
_updateList: function() {
|
_updateList: function() {
|
||||||
this.memberDict = this.getMemberDict();
|
this.memberDict = this.getMemberDict();
|
||||||
|
|
||||||
|
|
|
@ -29,6 +29,7 @@ module.exports = {
|
||||||
cli.on("Room.timeline", this.onRoomTimeline);
|
cli.on("Room.timeline", this.onRoomTimeline);
|
||||||
cli.on("Room.name", this.onRoomName);
|
cli.on("Room.name", this.onRoomName);
|
||||||
cli.on("RoomState.events", this.onRoomStateEvents);
|
cli.on("RoomState.events", this.onRoomStateEvents);
|
||||||
|
cli.on("RoomMember.name", this.onRoomMemberName);
|
||||||
|
|
||||||
var rooms = this.getRoomList();
|
var rooms = this.getRoomList();
|
||||||
this.setState({
|
this.setState({
|
||||||
|
@ -89,6 +90,10 @@ module.exports = {
|
||||||
this.refreshRoomList();
|
this.refreshRoomList();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onRoomMemberName: function(ev, member) {
|
||||||
|
this.refreshRoomList();
|
||||||
|
},
|
||||||
|
|
||||||
refreshRoomList: function() {
|
refreshRoomList: function() {
|
||||||
var rooms = this.getRoomList();
|
var rooms = this.getRoomList();
|
||||||
this.setState({
|
this.setState({
|
||||||
|
|
|
@ -43,6 +43,7 @@ module.exports = {
|
||||||
this.dispatcherRef = dis.register(this.onAction);
|
this.dispatcherRef = dis.register(this.onAction);
|
||||||
MatrixClientPeg.get().on("Room.timeline", this.onRoomTimeline);
|
MatrixClientPeg.get().on("Room.timeline", this.onRoomTimeline);
|
||||||
MatrixClientPeg.get().on("Room.name", this.onRoomName);
|
MatrixClientPeg.get().on("Room.name", this.onRoomName);
|
||||||
|
MatrixClientPeg.get().on("Room.receipt", this.onRoomReceipt);
|
||||||
MatrixClientPeg.get().on("RoomMember.typing", this.onRoomMemberTyping);
|
MatrixClientPeg.get().on("RoomMember.typing", this.onRoomMemberTyping);
|
||||||
this.atBottom = true;
|
this.atBottom = true;
|
||||||
},
|
},
|
||||||
|
@ -59,6 +60,7 @@ module.exports = {
|
||||||
if (MatrixClientPeg.get()) {
|
if (MatrixClientPeg.get()) {
|
||||||
MatrixClientPeg.get().removeListener("Room.timeline", this.onRoomTimeline);
|
MatrixClientPeg.get().removeListener("Room.timeline", this.onRoomTimeline);
|
||||||
MatrixClientPeg.get().removeListener("Room.name", this.onRoomName);
|
MatrixClientPeg.get().removeListener("Room.name", this.onRoomName);
|
||||||
|
MatrixClientPeg.get().removeListener("Room.receipt", this.onRoomReceipt);
|
||||||
MatrixClientPeg.get().removeListener("RoomMember.typing", this.onRoomMemberTyping);
|
MatrixClientPeg.get().removeListener("RoomMember.typing", this.onRoomMemberTyping);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -87,6 +89,9 @@ module.exports = {
|
||||||
messageWrapper.scrollTop = messageWrapper.scrollHeight;
|
messageWrapper.scrollTop = messageWrapper.scrollHeight;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case 'user_activity':
|
||||||
|
this.sendReadReceipt();
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -149,6 +154,12 @@ module.exports = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onRoomReceipt: function(receiptEvent, room) {
|
||||||
|
if (room.roomId == this.props.roomId) {
|
||||||
|
this.forceUpdate();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
onRoomMemberTyping: function(ev, member) {
|
onRoomMemberTyping: function(ev, member) {
|
||||||
this.forceUpdate();
|
this.forceUpdate();
|
||||||
},
|
},
|
||||||
|
@ -164,6 +175,8 @@ module.exports = {
|
||||||
|
|
||||||
messageWrapper.scrollTop = messageWrapper.scrollHeight;
|
messageWrapper.scrollTop = messageWrapper.scrollHeight;
|
||||||
|
|
||||||
|
this.sendReadReceipt();
|
||||||
|
|
||||||
this.fillSpace();
|
this.fillSpace();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -346,7 +359,7 @@ module.exports = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ret.unshift(
|
ret.unshift(
|
||||||
<li key={mxEv.getId()}><EventTile mxEvent={mxEv} continuation={continuation} last={last}/></li>
|
<li ref={this._collectEventNode.bind(this, mxEv.getId())} key={mxEv.getId()}><EventTile mxEvent={mxEv} continuation={continuation} last={last}/></li>
|
||||||
);
|
);
|
||||||
++count;
|
++count;
|
||||||
}
|
}
|
||||||
|
@ -438,5 +451,54 @@ module.exports = {
|
||||||
uploadingRoomSettings: false,
|
uploadingRoomSettings: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_collectEventNode: function(eventId, node) {
|
||||||
|
if (this.eventNodes == undefined) this.eventNodes = {};
|
||||||
|
this.eventNodes[eventId] = node;
|
||||||
|
},
|
||||||
|
|
||||||
|
_indexForEventId(evId) {
|
||||||
|
for (var i = 0; i < this.state.room.timeline.length; ++i) {
|
||||||
|
if (evId == this.state.room.timeline[i].getId()) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
|
||||||
|
sendReadReceipt: function() {
|
||||||
|
if (!this.state.room) return;
|
||||||
|
var currentReadUpToEventId = this.state.room.getEventReadUpTo(MatrixClientPeg.get().credentials.userId);
|
||||||
|
var currentReadUpToEventIndex = this._indexForEventId(currentReadUpToEventId);
|
||||||
|
|
||||||
|
var lastReadEventIndex = this._getLastDisplayedEventIndex();
|
||||||
|
if (lastReadEventIndex === null) return;
|
||||||
|
|
||||||
|
if (lastReadEventIndex > currentReadUpToEventIndex) {
|
||||||
|
MatrixClientPeg.get().sendReadReceipt(this.state.room.timeline[lastReadEventIndex]);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_getLastDisplayedEventIndex: function() {
|
||||||
|
if (this.eventNodes === undefined) return null;
|
||||||
|
|
||||||
|
var messageWrapper = this.refs.messageWrapper;
|
||||||
|
if (messageWrapper === undefined) return null;
|
||||||
|
var wrapperRect = messageWrapper.getDOMNode().getBoundingClientRect();
|
||||||
|
|
||||||
|
for (var i = this.state.room.timeline.length-1; i >= 0; --i) {
|
||||||
|
var ev = this.state.room.timeline[i];
|
||||||
|
var node = this.eventNodes[ev.getId()];
|
||||||
|
if (node === undefined) continue;
|
||||||
|
|
||||||
|
var domNode = node.getDOMNode();
|
||||||
|
var boundingRect = domNode.getBoundingClientRect();
|
||||||
|
|
||||||
|
if (boundingRect.bottom < wrapperRect.bottom) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -16,6 +16,7 @@ limitations under the License.
|
||||||
|
|
||||||
var MatrixClientPeg = require("../../MatrixClientPeg");
|
var MatrixClientPeg = require("../../MatrixClientPeg");
|
||||||
var RoomListSorter = require("../../RoomListSorter");
|
var RoomListSorter = require("../../RoomListSorter");
|
||||||
|
var UserActivity = require("../../UserActivity");
|
||||||
var Presence = require("../../Presence");
|
var Presence = require("../../Presence");
|
||||||
var dis = require("../../dispatcher");
|
var dis = require("../../dispatcher");
|
||||||
|
|
||||||
|
@ -104,6 +105,7 @@ module.exports = {
|
||||||
window.localStorage.clear();
|
window.localStorage.clear();
|
||||||
}
|
}
|
||||||
Notifier.stop();
|
Notifier.stop();
|
||||||
|
UserActivity.stop();
|
||||||
Presence.stop();
|
Presence.stop();
|
||||||
MatrixClientPeg.get().stopClient();
|
MatrixClientPeg.get().stopClient();
|
||||||
MatrixClientPeg.get().removeAllListeners();
|
MatrixClientPeg.get().removeAllListeners();
|
||||||
|
@ -362,6 +364,7 @@ module.exports = {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
Notifier.start();
|
Notifier.start();
|
||||||
|
UserActivity.start();
|
||||||
Presence.start();
|
Presence.start();
|
||||||
cli.startClient();
|
cli.startClient();
|
||||||
},
|
},
|
||||||
|
|
Loading…
Reference in a new issue