Merge branch 'kegan/slash-command-tab-complete' into matthew/roomsettings2

This commit is contained in:
Matthew Hodgson 2016-01-14 16:02:29 +00:00
commit ffaea371ac
10 changed files with 310 additions and 172 deletions

View file

@ -20,6 +20,31 @@ var dis = require("./dispatcher");
var encryption = require("./encryption");
var Tinter = require("./Tinter");
class Command {
constructor(name, paramArgs, runFn) {
this.name = name;
this.paramArgs = paramArgs;
this.runFn = runFn;
}
getCommand() {
return "/" + this.name;
}
getCommandWithArgs() {
return this.getCommand() + " " + this.paramArgs;
}
run(roomId, args) {
return this.runFn.bind(this)(roomId, args);
}
getUsage() {
return "Usage: " + this.getCommandWithArgs()
}
}
var reject = function(msg) {
return {
error: msg
@ -34,18 +59,17 @@ var success = function(promise) {
var commands = {
// Change your nickname
nick: function(room_id, args) {
nick: new Command("nick", "<display_name>", function(room_id, args) {
if (args) {
return success(
MatrixClientPeg.get().setDisplayName(args)
);
}
return reject("Usage: /nick <display_name>");
},
return reject(this.getUsage());
}),
// Changes the colorscheme of your current room
tint: function(room_id, args) {
tint: new Command("tint", "<primaryColor> [<secondaryColor>]", function(room_id, args) {
if (args) {
var matches = args.match(/^(#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}))( +(#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})))?$/);
if (matches) {
@ -62,10 +86,10 @@ var commands = {
);
}
}
return reject("Usage: /tint <primaryColor> [<secondaryColor>]");
},
return reject(this.getUsage());
}),
encrypt: function(room_id, args) {
encrypt: new Command("encrypt", "<on/off>", function(room_id, args) {
if (args == "on") {
var client = MatrixClientPeg.get();
var members = client.getRoom(room_id).currentState.members;
@ -81,21 +105,21 @@ var commands = {
);
}
return reject("Usage: encrypt <on/off>");
},
return reject(this.getUsage());
}),
// Change the room topic
topic: function(room_id, args) {
topic: new Command("topic", "<topic>", function(room_id, args) {
if (args) {
return success(
MatrixClientPeg.get().setRoomTopic(room_id, args)
);
}
return reject("Usage: /topic <topic>");
},
return reject(this.getUsage());
}),
// Invite a user
invite: function(room_id, args) {
invite: new Command("invite", "<userId>", function(room_id, args) {
if (args) {
var matches = args.match(/^(\S+)$/);
if (matches) {
@ -104,11 +128,11 @@ var commands = {
);
}
}
return reject("Usage: /invite <userId>");
},
return reject(this.getUsage());
}),
// Join a room
join: function(room_id, args) {
join: new Command("join", "<room_alias>", function(room_id, args) {
if (args) {
var matches = args.match(/^(\S+)$/);
if (matches) {
@ -150,17 +174,17 @@ var commands = {
);
}
}
return reject("Usage: /join <room_alias>");
},
return reject(this.getUsage());
}),
part: function(room_id, args) {
part: new Command("part", "[#alias:domain]", function(room_id, args) {
var targetRoomId;
if (args) {
var matches = args.match(/^(\S+)$/);
if (matches) {
var room_alias = matches[1];
if (room_alias[0] !== '#') {
return reject("Usage: /part [#alias:domain]");
return reject(this.getUsage());
}
if (!room_alias.match(/:/)) {
room_alias += ':' + MatrixClientPeg.get().getDomain();
@ -196,10 +220,10 @@ var commands = {
dis.dispatch({action: 'view_next_room'});
})
);
},
}),
// Kick a user from the room with an optional reason
kick: function(room_id, args) {
kick: new Command("kick", "<userId> [<reason>]", function(room_id, args) {
if (args) {
var matches = args.match(/^(\S+?)( +(.*))?$/);
if (matches) {
@ -208,11 +232,11 @@ var commands = {
);
}
}
return reject("Usage: /kick <userId> [<reason>]");
},
return reject(this.getUsage());
}),
// Ban a user from the room with an optional reason
ban: function(room_id, args) {
ban: new Command("ban", "<userId> [<reason>]", function(room_id, args) {
if (args) {
var matches = args.match(/^(\S+?)( +(.*))?$/);
if (matches) {
@ -221,11 +245,11 @@ var commands = {
);
}
}
return reject("Usage: /ban <userId> [<reason>]");
},
return reject(this.getUsage());
}),
// Unban a user from the room
unban: function(room_id, args) {
unban: new Command("unban", "<userId>", function(room_id, args) {
if (args) {
var matches = args.match(/^(\S+)$/);
if (matches) {
@ -235,11 +259,11 @@ var commands = {
);
}
}
return reject("Usage: /unban <userId>");
},
return reject(this.getUsage());
}),
// Define the power level of a user
op: function(room_id, args) {
op: new Command("op", "<userId> [<power level>]", function(room_id, args) {
if (args) {
var matches = args.match(/^(\S+?)( +(\d+))?$/);
var powerLevel = 50; // default power level for op
@ -264,11 +288,11 @@ var commands = {
}
}
}
return reject("Usage: /op <userId> [<power level>]");
},
return reject(this.getUsage());
}),
// Reset the power level of a user
deop: function(room_id, args) {
deop: new Command("deop", "<userId>", function(room_id, args) {
if (args) {
var matches = args.match(/^(\S+)$/);
if (matches) {
@ -287,8 +311,8 @@ var commands = {
);
}
}
return reject("Usage: /deop <userId>");
}
return reject(this.getUsage());
})
};
// helpful aliases
@ -313,7 +337,7 @@ module.exports = {
var args = bits[3];
if (cmd === "me") return null;
if (commands[cmd]) {
return commands[cmd](roomId, args);
return commands[cmd].run(roomId, args);
}
else {
return reject("Unrecognised command: " + input);
@ -323,8 +347,8 @@ module.exports = {
},
getCommandList: function() {
return Object.keys(commands).map(function(cmd) {
return "/" + cmd;
return Object.keys(commands).map(function(cmdKey) {
return commands[cmdKey];
});
}
};

View file

@ -32,8 +32,6 @@ const MATCH_REGEX = /(^|\s)(\S+)$/;
class TabComplete {
constructor(opts) {
opts.startingWordSuffix = opts.startingWordSuffix || "";
opts.wordSuffix = opts.wordSuffix || "";
opts.allowLooping = opts.allowLooping || false;
opts.autoEnterTabComplete = opts.autoEnterTabComplete || false;
opts.onClickCompletes = opts.onClickCompletes || false;
@ -96,7 +94,9 @@ class TabComplete {
* @param {Entry} entry The tab-complete entry to complete to.
*/
completeTo(entry) {
this.textArea.value = this._replaceWith(entry.getText(), true, entry.getOverrideSuffix());
this.textArea.value = this._replaceWith(
entry.getFillText(), true, entry.getSuffix(this.isFirstWord)
);
this.stopTabCompleting();
// keep focus on the text area
this.textArea.focus();
@ -222,9 +222,9 @@ class TabComplete {
if (!this.inPassiveMode) {
// set textarea to this new value
this.textArea.value = this._replaceWith(
this.matchedList[this.currentIndex].getText(),
this.matchedList[this.currentIndex].getFillText(),
this.currentIndex !== 0, // don't suffix the original text!
this.matchedList[this.currentIndex].getOverrideSuffix()
this.matchedList[this.currentIndex].getSuffix(this.isFirstWord)
);
}
@ -244,7 +244,7 @@ class TabComplete {
}
}
_replaceWith(newVal, includeSuffix, overrideSuffix) {
_replaceWith(newVal, includeSuffix, suffix) {
// The regex to replace the input matches a character of whitespace AND
// the partial word. If we just use string.replace() with the regex it will
// replace the partial word AND the character of whitespace. We want to
@ -259,14 +259,9 @@ class TabComplete {
boundaryChar = "";
}
var suffix = "";
if (includeSuffix) {
if (overrideSuffix) {
suffix = overrideSuffix;
}
else {
suffix = (this.isFirstWord ? this.opts.startingWordSuffix : this.opts.wordSuffix);
}
suffix = suffix || "";
if (!includeSuffix) {
suffix = "";
}
var replacementText = boundaryChar + newVal + suffix;

View file

@ -28,6 +28,14 @@ class Entry {
return this.text;
}
/**
* @return {string} The text to insert into the input box. Most of the time
* this is the same as getText().
*/
getFillText() {
return this.text;
}
/**
* @return {ReactClass} Raw JSX
*/
@ -43,10 +51,10 @@ class Entry {
}
/**
* @return {?string} The suffix to override whatever the default is, or null to
* @return {?string} The suffix to append to the tab-complete, or null to
* not do this.
*/
getOverrideSuffix() {
getSuffix(isFirstWord) {
return null;
}
@ -59,22 +67,27 @@ class Entry {
}
class CommandEntry extends Entry {
constructor(command) {
super(command);
constructor(cmd, cmdWithArgs) {
super(cmdWithArgs);
this.cmd = cmd;
}
getFillText() {
return this.cmd;
}
getKey() {
return this.getText();
return this.getFillText();
}
getOverrideSuffix() {
getSuffix(isFirstWord) {
return " "; // force a space after the command.
}
}
CommandEntry.fromStrings = function(commandArray) {
CommandEntry.fromCommands = function(commandArray) {
return commandArray.map(function(cmd) {
return new CommandEntry(cmd);
return new CommandEntry(cmd.getCommand(), cmd.getCommandWithArgs());
});
}
@ -94,6 +107,10 @@ class MemberEntry extends Entry {
getKey() {
return this.member.userId;
}
getSuffix(isFirstWord) {
return isFirstWord ? ": " : " ";
}
}
MemberEntry.fromMemberList = function(members) {

View file

@ -65,6 +65,7 @@ module.exports.components['views.rooms.MemberInfo'] = require('./components/view
module.exports.components['views.rooms.MemberList'] = require('./components/views/rooms/MemberList');
module.exports.components['views.rooms.MemberTile'] = require('./components/views/rooms/MemberTile');
module.exports.components['views.rooms.MessageComposer'] = require('./components/views/rooms/MessageComposer');
module.exports.components['views.rooms.PresenceLabel'] = require('./components/views/rooms/PresenceLabel');
module.exports.components['views.rooms.RoomHeader'] = require('./components/views/rooms/RoomHeader');
module.exports.components['views.rooms.RoomList'] = require('./components/views/rooms/RoomList');
module.exports.components['views.rooms.RoomPreviewBar'] = require('./components/views/rooms/RoomPreviewBar');

View file

@ -96,8 +96,6 @@ module.exports = React.createClass({
// xchat-style tab complete, add a colon if tab
// completing at the start of the text
this.tabComplete = new TabComplete({
startingWordSuffix: ": ",
wordSuffix: " ",
allowLooping: false,
autoEnterTabComplete: true,
onClickCompletes: true,
@ -419,7 +417,7 @@ module.exports = React.createClass({
}
this.tabComplete.setCompletionList(
MemberEntry.fromMemberList(room.getJoinedMembers()).concat(
CommandEntry.fromStrings(SlashCommands.getCommandList())
CommandEntry.fromCommands(SlashCommands.getCommandList())
)
);
},

View file

@ -24,10 +24,16 @@ module.exports = React.createClass({
displayName: 'MemberAvatar',
propTypes: {
member: React.PropTypes.object.isRequired,
member: React.PropTypes.object,
width: React.PropTypes.number,
height: React.PropTypes.number,
resizeMethod: React.PropTypes.string,
/**
* The custom display name to use for this member. This can serve as a
* drop in replacement for RoomMember objects, or as a clobber name on
* an existing RoomMember. Used for 3pid invites.
*/
customDisplayName: React.PropTypes.string
},
getDefaultProps: function() {
@ -38,64 +44,68 @@ module.exports = React.createClass({
}
},
getInitialState: function() {
var defaultImageUrl = Avatar.defaultAvatarUrlForString(
this.props.customDisplayName || this.props.member.userId
)
return {
imageUrl: this._getMemberImageUrl() || defaultImageUrl,
defaultImageUrl: defaultImageUrl
};
},
componentWillReceiveProps: function(nextProps) {
this.refreshUrl();
},
defaultAvatarUrl: function(member, width, height, resizeMethod) {
return Avatar.defaultAvatarUrlForString(member.userId);
},
onError: function(ev) {
// don't tightloop if the browser can't load a data url
if (ev.target.src == this.defaultAvatarUrl(this.props.member)) {
if (ev.target.src == this.state.defaultImageUrl) {
return;
}
this.setState({
imageUrl: this.defaultAvatarUrl(this.props.member)
imageUrl: this.state.defaultImageUrl
});
},
_computeUrl: function() {
_getMemberImageUrl: function() {
if (!this.props.member) { return null; }
return Avatar.avatarUrlForMember(this.props.member,
this.props.width,
this.props.height,
this.props.resizeMethod);
},
_getInitialLetter: function() {
var name = this.props.customDisplayName || this.props.member.name;
var initial = name[0];
if (initial === '@' && name[1]) {
initial = name[1];
}
return initial.toUpperCase();
},
refreshUrl: function() {
var newUrl = this._computeUrl();
var newUrl = this._getMemberImageUrl();
if (newUrl != this.currentUrl) {
this.currentUrl = newUrl;
this.setState({imageUrl: newUrl});
}
},
getInitialState: function() {
return {
imageUrl: this._computeUrl()
};
},
///////////////
render: function() {
// XXX: recalculates default avatar url constantly
if (this.state.imageUrl === this.defaultAvatarUrl(this.props.member)) {
var initial;
if (this.props.member.name[0])
initial = this.props.member.name[0].toUpperCase();
if (initial === '@' && this.props.member.name[1])
initial = this.props.member.name[1].toUpperCase();
var name = this.props.customDisplayName || this.props.member.name;
if (this.state.imageUrl === this.state.defaultImageUrl) {
var initialLetter = this._getInitialLetter();
return (
<span className="mx_MemberAvatar" {...this.props}>
<span className="mx_MemberAvatar_initial" aria-hidden="true"
style={{ fontSize: (this.props.width * 0.65) + "px",
width: this.props.width + "px",
lineHeight: this.props.height + "px" }}>{ initial }</span>
<img className="mx_MemberAvatar_image" src={this.state.imageUrl} title={this.props.member.name}
lineHeight: this.props.height + "px" }}>{ initialLetter }</span>
<img className="mx_MemberAvatar_image" src={this.state.imageUrl} title={name}
onError={this.onError} width={this.props.width} height={this.props.height} />
</span>
);
@ -104,9 +114,8 @@ module.exports = React.createClass({
<img className="mx_MemberAvatar mx_MemberAvatar_image" src={this.state.imageUrl}
onError={this.onError}
width={this.props.width} height={this.props.height}
title={this.props.member.name}
{...this.props}
/>
title={name}
{...this.props} />
);
}
});

View file

@ -15,6 +15,7 @@ limitations under the License.
*/
var React = require('react');
var classNames = require('classnames');
var Matrix = require("matrix-js-sdk");
var MatrixClientPeg = require("../../../MatrixClientPeg");
var Modal = require("../../../Modal");
var sdk = require('../../../index');
@ -229,7 +230,8 @@ module.exports = React.createClass({
var MemberTile = sdk.getComponent("rooms.MemberTile");
var self = this;
return self.state.members.filter(function(userId) {
var memberList = self.state.members.filter(function(userId) {
var m = self.memberDict[userId];
return m.membership == membership;
}).map(function(userId) {
@ -238,6 +240,31 @@ module.exports = React.createClass({
<MemberTile key={userId} member={m} ref={userId} />
);
});
if (membership === "invite") {
// include 3pid invites (m.room.third_party_invite) state events.
// The HS may have already converted these into m.room.member invites so
// we shouldn't add them if the 3pid invite state key (token) is in the
// member invite (content.third_party_invite.signed.token)
var room = MatrixClientPeg.get().getRoom(this.props.roomId);
if (room) {
room.currentState.getStateEvents("m.room.third_party_invite").forEach(
function(e) {
// discard all invites which have a m.room.member event since we've
// already added them.
var memberEvent = room.currentState.getInviteForThreePidToken(e.getStateKey());
if (memberEvent) {
return;
}
memberList.push(
<MemberTile key={e.getStateKey()} ref={e.getStateKey()}
customDisplayName={e.getContent().display_name} />
)
})
}
}
return memberList;
},
onPopulateInvite: function(e) {

View file

@ -26,20 +26,19 @@ var Modal = require("../../../Modal");
module.exports = React.createClass({
displayName: 'MemberTile',
propTypes: {
member: React.PropTypes.any, // RoomMember
onFinished: React.PropTypes.func,
customDisplayName: React.PropTypes.string // for 3pid invites
},
getInitialState: function() {
return {};
},
onLeaveClick: function() {
dis.dispatch({
action: 'leave_room',
room_id: this.props.member.roomId,
});
this.props.onFinished();
},
shouldComponentUpdate: function(nextProps, nextState) {
if (this.state.hover !== nextState.hover) return true;
if (!this.props.member) { return false; } // e.g. 3pid members
if (
this.member_last_modified_time === undefined ||
this.member_last_modified_time < nextProps.member.getLastModifiedTime()
@ -65,44 +64,25 @@ module.exports = React.createClass({
},
onClick: function(e) {
if (!this.props.member) { return; } // e.g. 3pid members
dis.dispatch({
action: 'view_user',
member: this.props.member,
});
},
getDuration: function(time) {
if (!time) return;
var t = parseInt(time / 1000);
var s = t % 60;
var m = parseInt(t / 60) % 60;
var h = parseInt(t / (60 * 60)) % 24;
var d = parseInt(t / (60 * 60 * 24));
if (t < 60) {
if (t < 0) {
return "0s";
}
return s + "s";
_getDisplayName: function() {
if (this.props.customDisplayName) {
return this.props.customDisplayName;
}
if (t < 60 * 60) {
return m + "m";
}
if (t < 24 * 60 * 60) {
return h + "h";
}
return d + "d ";
},
getPrettyPresence: function(user) {
if (!user) return "Unknown";
var presence = user.presence;
if (presence === "online") return "Online";
if (presence === "unavailable") return "Idle"; // XXX: is this actually right?
if (presence === "offline") return "Offline";
return "Unknown";
return this.props.member.name;
},
getPowerLabel: function() {
if (!this.props.member) {
return this._getDisplayName();
}
var label = this.props.member.userId;
if (this.state.isTargetMod) {
label += " - Mod (" + this.props.member.powerLevelNorm + "%)";
@ -111,71 +91,74 @@ module.exports = React.createClass({
},
render: function() {
this.member_last_modified_time = this.props.member.getLastModifiedTime();
if (this.props.member.user) {
this.user_last_modified_time = this.props.member.user.getLastModifiedTime();
}
var isMyUser = MatrixClientPeg.get().credentials.userId == this.props.member.userId;
var power;
// if (this.props.member && this.props.member.powerLevelNorm > 0) {
// var img = "img/p/p" + Math.floor(20 * this.props.member.powerLevelNorm / 100) + ".png";
// power = <img src={ img } className="mx_MemberTile_power" width="44" height="44" alt=""/>;
// }
var member = this.props.member;
var isMyUser = false;
var name = this._getDisplayName();
var active = -1;
var presenceClass = "mx_MemberTile_offline";
var mainClassName = "mx_MemberTile ";
if (this.props.member.user) {
if (this.props.member.user.presence === "online") {
presenceClass = "mx_MemberTile_online";
}
else if (this.props.member.user.presence === "unavailable") {
presenceClass = "mx_MemberTile_unavailable";
if (member) {
if (member.user) {
this.user_last_modified_time = member.user.getLastModifiedTime();
// FIXME: make presence data update whenever User.presence changes...
active = (
(Date.now() - (member.user.lastPresenceTs - member.user.lastActiveAgo)) || -1
);
if (member.user.presence === "online") {
presenceClass = "mx_MemberTile_online";
}
else if (member.user.presence === "unavailable") {
presenceClass = "mx_MemberTile_unavailable";
}
}
this.member_last_modified_time = member.getLastModifiedTime();
isMyUser = MatrixClientPeg.get().credentials.userId == member.userId;
// if (this.props.member && this.props.member.powerLevelNorm > 0) {
// var img = "img/p/p" + Math.floor(20 * this.props.member.powerLevelNorm / 100) + ".png";
// power = <img src={ img } className="mx_MemberTile_power" width="44" height="44" alt=""/>;
// }
}
var mainClassName = "mx_MemberTile ";
mainClassName += presenceClass;
if (this.state.hover) {
mainClassName += " mx_MemberTile_hover";
}
var name = this.props.member.name;
// if (isMyUser) name += " (me)"; // this does nothing other than introduce line wrapping and pain
//var leave = isMyUser ? <img className="mx_MemberTile_leave" src="img/delete.png" width="10" height="10" onClick={this.onLeaveClick}/> : null;
var nameEl;
if (this.state.hover) {
var presence;
// FIXME: make presence data update whenever User.presence changes...
var active = this.props.member.user ? ((Date.now() - (this.props.member.user.lastPresenceTs - this.props.member.user.lastActiveAgo)) || -1) : -1;
if (active >= 0) {
presence = <div className="mx_MemberTile_presence">{ this.getPrettyPresence(this.props.member.user) } { this.getDuration(active) } ago</div>;
}
else {
presence = <div className="mx_MemberTile_presence">{ this.getPrettyPresence(this.props.member.user) }</div>;
}
nameEl =
var presenceState = (member && member.user) ? member.user.presence : null;
var PresenceLabel = sdk.getComponent("rooms.PresenceLabel");
nameEl = (
<div className="mx_MemberTile_details">
<img className="mx_MemberTile_chevron" src="img/member_chevron.png" width="8" height="12"/>
<div className="mx_MemberTile_userId">{ name }</div>
{ presence }
<PresenceLabel activeAgo={active}
presenceState={presenceState} />
</div>
);
}
else {
nameEl =
nameEl = (
<div className="mx_MemberTile_name">
{ name }
</div>
);
}
var MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
return (
<div className={mainClassName} title={ this.getPowerLabel() }
onClick={ this.onClick } onMouseEnter={ this.mouseEnter }
onMouseLeave={ this.mouseLeave }>
<div className="mx_MemberTile_avatar">
<MemberAvatar member={this.props.member} width={36} height={36} />
{ power }
<MemberAvatar member={this.props.member} width={36} height={36}
customDisplayName={this.props.customDisplayName} />
</div>
{ nameEl }
</div>

View file

@ -341,7 +341,7 @@ module.exports = React.createClass({
MatrixClientPeg.get().sendTextMessage(this.props.room.roomId, contentText);
}
sendMessagePromise.then(function() {
sendMessagePromise.done(function() {
dis.dispatch({
action: 'message_sent'
});

View file

@ -0,0 +1,84 @@
/*
Copyright 2015, 2016 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.
*/
'use strict';
var React = require('react');
var MatrixClientPeg = require('../../../MatrixClientPeg');
var sdk = require('../../../index');
module.exports = React.createClass({
displayName: 'PresenceLabel',
propTypes: {
activeAgo: React.PropTypes.number,
presenceState: React.PropTypes.string
},
getDefaultProps: function() {
return {
ago: -1,
presenceState: null
};
},
getDuration: function(time) {
if (!time) return;
var t = parseInt(time / 1000);
var s = t % 60;
var m = parseInt(t / 60) % 60;
var h = parseInt(t / (60 * 60)) % 24;
var d = parseInt(t / (60 * 60 * 24));
if (t < 60) {
if (t < 0) {
return "0s";
}
return s + "s";
}
if (t < 60 * 60) {
return m + "m";
}
if (t < 24 * 60 * 60) {
return h + "h";
}
return d + "d ";
},
getPrettyPresence: function(presence) {
if (presence === "online") return "Online";
if (presence === "unavailable") return "Idle"; // XXX: is this actually right?
if (presence === "offline") return "Offline";
return "Unknown";
},
render: function() {
if (this.props.activeAgo >= 0) {
return (
<div className="mx_PresenceLabel">
{ this.getPrettyPresence(this.props.presenceState) } { this.getDuration(this.props.activeAgo) } ago
</div>
);
}
else {
return (
<div className="mx_PresenceLabel">
{ this.getPrettyPresence(this.props.presenceState) }
</div>
);
}
}
});