Merge branch 'develop' into t3chguy/hide_left_chats_memberinfo
This commit is contained in:
commit
831a3f7e42
311 changed files with 28762 additions and 12120 deletions
|
@ -14,10 +14,8 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
var React = require('react');
|
||||
var AvatarLogic = require("../../../Avatar");
|
||||
import React from 'react';
|
||||
import AvatarLogic from '../../../Avatar';
|
||||
import sdk from '../../../index';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
|
||||
|
@ -34,7 +32,7 @@ module.exports = React.createClass({
|
|||
height: React.PropTypes.number,
|
||||
// XXX resizeMethod not actually used.
|
||||
resizeMethod: React.PropTypes.string,
|
||||
defaultToInitialLetter: React.PropTypes.bool // true to add default url
|
||||
defaultToInitialLetter: React.PropTypes.bool, // true to add default url
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
|
@ -42,7 +40,7 @@ module.exports = React.createClass({
|
|||
width: 40,
|
||||
height: 40,
|
||||
resizeMethod: 'crop',
|
||||
defaultToInitialLetter: true
|
||||
defaultToInitialLetter: true,
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -52,15 +50,14 @@ module.exports = React.createClass({
|
|||
|
||||
componentWillReceiveProps: function(nextProps) {
|
||||
// work out if we need to call setState (if the image URLs array has changed)
|
||||
var newState = this._getState(nextProps);
|
||||
var newImageUrls = newState.imageUrls;
|
||||
var oldImageUrls = this.state.imageUrls;
|
||||
const newState = this._getState(nextProps);
|
||||
const newImageUrls = newState.imageUrls;
|
||||
const oldImageUrls = this.state.imageUrls;
|
||||
if (newImageUrls.length !== oldImageUrls.length) {
|
||||
this.setState(newState); // detected a new entry
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
// check each one to see if they are the same
|
||||
for (var i = 0; i < newImageUrls.length; i++) {
|
||||
for (let i = 0; i < newImageUrls.length; i++) {
|
||||
if (oldImageUrls[i] !== newImageUrls[i]) {
|
||||
this.setState(newState); // detected a diff
|
||||
break;
|
||||
|
@ -73,31 +70,31 @@ module.exports = React.createClass({
|
|||
// work out the full set of urls to try to load. This is formed like so:
|
||||
// imageUrls: [ props.url, props.urls, default image ]
|
||||
|
||||
var urls = props.urls || [];
|
||||
const urls = props.urls || [];
|
||||
if (props.url) {
|
||||
urls.unshift(props.url); // put in urls[0]
|
||||
}
|
||||
|
||||
var defaultImageUrl = null;
|
||||
let defaultImageUrl = null;
|
||||
if (props.defaultToInitialLetter) {
|
||||
defaultImageUrl = AvatarLogic.defaultAvatarUrlForString(
|
||||
props.idName || props.name
|
||||
props.idName || props.name,
|
||||
);
|
||||
urls.push(defaultImageUrl); // lowest priority
|
||||
}
|
||||
return {
|
||||
imageUrls: urls,
|
||||
defaultImageUrl: defaultImageUrl,
|
||||
urlsIndex: 0
|
||||
urlsIndex: 0,
|
||||
};
|
||||
},
|
||||
|
||||
onError: function(ev) {
|
||||
var nextIndex = this.state.urlsIndex + 1;
|
||||
const nextIndex = this.state.urlsIndex + 1;
|
||||
if (nextIndex < this.state.imageUrls.length) {
|
||||
// try the next one
|
||||
this.setState({
|
||||
urlsIndex: nextIndex
|
||||
urlsIndex: nextIndex,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
@ -111,32 +108,32 @@ module.exports = React.createClass({
|
|||
return undefined;
|
||||
}
|
||||
|
||||
var idx = 0;
|
||||
var initial = name[0];
|
||||
if ((initial === '@' || initial === '#') && name[1]) {
|
||||
let idx = 0;
|
||||
const initial = name[0];
|
||||
if ((initial === '@' || initial === '#' || initial === '+') && name[1]) {
|
||||
idx++;
|
||||
}
|
||||
|
||||
// string.codePointAt(0) would do this, but that isn't supported by
|
||||
// some browsers (notably PhantomJS).
|
||||
var chars = 1;
|
||||
var first = name.charCodeAt(idx);
|
||||
let chars = 1;
|
||||
const first = name.charCodeAt(idx);
|
||||
|
||||
// check if it’s the start of a surrogate pair
|
||||
if (first >= 0xD800 && first <= 0xDBFF && name[idx+1]) {
|
||||
var second = name.charCodeAt(idx+1);
|
||||
const second = name.charCodeAt(idx+1);
|
||||
if (second >= 0xDC00 && second <= 0xDFFF) {
|
||||
chars++;
|
||||
}
|
||||
}
|
||||
|
||||
var firstChar = name.substring(idx, idx+chars);
|
||||
const firstChar = name.substring(idx, idx+chars);
|
||||
return firstChar.toUpperCase();
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const EmojiText = sdk.getComponent('elements.EmojiText');
|
||||
var imageUrl = this.state.imageUrls[this.state.urlsIndex];
|
||||
const imageUrl = this.state.imageUrls[this.state.urlsIndex];
|
||||
|
||||
const {
|
||||
name, idName, title, url, urls, width, height, resizeMethod,
|
||||
|
@ -152,7 +149,7 @@ module.exports = React.createClass({
|
|||
width: width + "px",
|
||||
lineHeight: height + "px" }}
|
||||
>
|
||||
{initialLetter}
|
||||
{ initialLetter }
|
||||
</EmojiText>
|
||||
);
|
||||
const imgNode = (
|
||||
|
@ -165,15 +162,15 @@ module.exports = React.createClass({
|
|||
<AccessibleButton element='span' className="mx_BaseAvatar"
|
||||
onClick={onClick} {...otherProps}
|
||||
>
|
||||
{textNode}
|
||||
{imgNode}
|
||||
{ textNode }
|
||||
{ imgNode }
|
||||
</AccessibleButton>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<span className="mx_BaseAvatar" {...otherProps}>
|
||||
{textNode}
|
||||
{imgNode}
|
||||
{ textNode }
|
||||
{ imgNode }
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
@ -198,5 +195,5 @@ module.exports = React.createClass({
|
|||
{...otherProps} />
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -24,10 +24,12 @@ export default React.createClass({
|
|||
|
||||
propTypes: {
|
||||
groupId: PropTypes.string,
|
||||
groupName: PropTypes.string,
|
||||
groupAvatarUrl: PropTypes.string,
|
||||
width: PropTypes.number,
|
||||
height: PropTypes.number,
|
||||
resizeMethod: PropTypes.string,
|
||||
onClick: PropTypes.func,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
|
@ -52,11 +54,11 @@ export default React.createClass({
|
|||
// extract the props we use from props so we can pass any others through
|
||||
// should consider adding this as a global rule in js-sdk?
|
||||
/*eslint no-unused-vars: ["error", { "ignoreRestSiblings": true }]*/
|
||||
const {groupId, groupAvatarUrl, ...otherProps} = this.props;
|
||||
const {groupId, groupAvatarUrl, groupName, ...otherProps} = this.props;
|
||||
|
||||
return (
|
||||
<BaseAvatar
|
||||
name={this.props.groupId[1]}
|
||||
name={groupName || this.props.groupId[1]}
|
||||
idName={this.props.groupId}
|
||||
url={this.getGroupAvatarUrl()}
|
||||
{...otherProps}
|
||||
|
|
|
@ -16,9 +16,9 @@ limitations under the License.
|
|||
|
||||
'use strict';
|
||||
|
||||
var React = require('react');
|
||||
var Avatar = require('../../../Avatar');
|
||||
var sdk = require("../../../index");
|
||||
const React = require('react');
|
||||
const Avatar = require('../../../Avatar');
|
||||
const sdk = require("../../../index");
|
||||
const dispatcher = require("../../../dispatcher");
|
||||
|
||||
module.exports = React.createClass({
|
||||
|
@ -63,14 +63,14 @@ module.exports = React.createClass({
|
|||
imageUrl: Avatar.avatarUrlForMember(props.member,
|
||||
props.width,
|
||||
props.height,
|
||||
props.resizeMethod)
|
||||
props.resizeMethod),
|
||||
};
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
|
||||
const BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
|
||||
|
||||
var {member, onClick, viewUserOnClick, ...otherProps} = this.props;
|
||||
let {member, onClick, viewUserOnClick, ...otherProps} = this.props;
|
||||
|
||||
if (viewUserOnClick) {
|
||||
onClick = () => {
|
||||
|
@ -83,7 +83,7 @@ module.exports = React.createClass({
|
|||
|
||||
return (
|
||||
<BaseAvatar {...otherProps} name={this.state.name} title={this.state.title}
|
||||
idName={member.userId} url={this.state.imageUrl} onClick={onClick}/>
|
||||
idName={member.userId} url={this.state.imageUrl} onClick={onClick} />
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
168
src/components/views/avatars/MemberPresenceAvatar.js
Normal file
168
src/components/views/avatars/MemberPresenceAvatar.js
Normal file
|
@ -0,0 +1,168 @@
|
|||
/*
|
||||
Copyright 2017 Travis Ralston
|
||||
|
||||
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';
|
||||
|
||||
import React from "react";
|
||||
import * as sdk from "../../../index";
|
||||
import MatrixClientPeg from "../../../MatrixClientPeg";
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import Presence from "../../../Presence";
|
||||
import dispatcher from "../../../dispatcher";
|
||||
import * as ContextualMenu from "../../structures/ContextualMenu";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
|
||||
// This is an avatar with presence information and controls on it.
|
||||
module.exports = React.createClass({
|
||||
displayName: 'MemberPresenceAvatar',
|
||||
|
||||
propTypes: {
|
||||
member: React.PropTypes.object.isRequired,
|
||||
width: React.PropTypes.number,
|
||||
height: React.PropTypes.number,
|
||||
resizeMethod: React.PropTypes.string,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
width: 40,
|
||||
height: 40,
|
||||
resizeMethod: 'crop',
|
||||
};
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
let presenceState = null;
|
||||
let presenceMessage = null;
|
||||
|
||||
// RoomMembers do not necessarily have a user.
|
||||
if (this.props.member.user) {
|
||||
presenceState = this.props.member.user.presence;
|
||||
presenceMessage = this.props.member.user.presenceStatusMsg;
|
||||
}
|
||||
|
||||
return {
|
||||
status: presenceState,
|
||||
message: presenceMessage,
|
||||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
MatrixClientPeg.get().on("User.presence", this.onUserPresence);
|
||||
this.dispatcherRef = dispatcher.register(this.onAction);
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
if (MatrixClientPeg.get()) {
|
||||
MatrixClientPeg.get().removeListener("User.presence", this.onUserPresence);
|
||||
}
|
||||
dispatcher.unregister(this.dispatcherRef);
|
||||
},
|
||||
|
||||
onAction: function(payload) {
|
||||
if (payload.action !== "self_presence_updated") return;
|
||||
if (this.props.member.userId !== MatrixClientPeg.get().getUserId()) return;
|
||||
this.setState({
|
||||
status: payload.statusInfo.presence,
|
||||
message: payload.statusInfo.status_msg,
|
||||
});
|
||||
},
|
||||
|
||||
onUserPresence: function(event, user) {
|
||||
if (user.userId !== MatrixClientPeg.get().getUserId()) return;
|
||||
this.setState({
|
||||
status: user.presence,
|
||||
message: user.presenceStatusMsg,
|
||||
});
|
||||
},
|
||||
|
||||
onStatusChange: function(newStatus) {
|
||||
Presence.stopMaintainingStatus();
|
||||
if (newStatus === "online") {
|
||||
Presence.setState(newStatus);
|
||||
} else Presence.setState(newStatus, null, true);
|
||||
},
|
||||
|
||||
onClick: function(e) {
|
||||
const PresenceContextMenu = sdk.getComponent('context_menus.PresenceContextMenu');
|
||||
const elementRect = e.target.getBoundingClientRect();
|
||||
|
||||
// The window X and Y offsets are to adjust position when zoomed in to page
|
||||
const x = (elementRect.left + window.pageXOffset) - (elementRect.width / 2) + 3;
|
||||
const chevronOffset = 12;
|
||||
let y = elementRect.top + (elementRect.height / 2) + window.pageYOffset;
|
||||
y = y - (chevronOffset + 4); // where 4 is 1/4 the height of the chevron
|
||||
|
||||
ContextualMenu.createMenu(PresenceContextMenu, {
|
||||
chevronOffset: chevronOffset,
|
||||
chevronFace: 'bottom',
|
||||
left: x,
|
||||
top: y,
|
||||
menuWidth: 125,
|
||||
currentStatus: this.state.status,
|
||||
onChange: this.onStatusChange,
|
||||
});
|
||||
|
||||
e.stopPropagation();
|
||||
|
||||
// XXX NB the following assumes that user is non-null, which is not valid
|
||||
// const presenceState = this.props.member.user.presence;
|
||||
// const presenceLastActiveAgo = this.props.member.user.lastActiveAgo;
|
||||
// const presenceLastTs = this.props.member.user.lastPresenceTs;
|
||||
// const presenceCurrentlyActive = this.props.member.user.currentlyActive;
|
||||
// const presenceMessage = this.props.member.user.presenceStatusMsg;
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const MemberAvatar = sdk.getComponent("avatars.MemberAvatar");
|
||||
|
||||
let onClickFn = null;
|
||||
if (this.props.member.userId === MatrixClientPeg.get().getUserId()) {
|
||||
onClickFn = this.onClick;
|
||||
}
|
||||
|
||||
const avatarNode = (
|
||||
<MemberAvatar member={this.props.member} width={this.props.width} height={this.props.height}
|
||||
resizeMethod={this.props.resizeMethod} />
|
||||
);
|
||||
let statusNode = (
|
||||
<span className={"mx_MemberPresenceAvatar_status mx_MemberPresenceAvatar_status_" + this.state.status} />
|
||||
);
|
||||
|
||||
// LABS: Disable presence management functions for now
|
||||
// Also disable the presence information if there's no status information
|
||||
if (!SettingsStore.isFeatureEnabled("feature_presence_management") || !this.state.status) {
|
||||
statusNode = null;
|
||||
onClickFn = null;
|
||||
}
|
||||
|
||||
let avatar = (
|
||||
<div className="mx_MemberPresenceAvatar">
|
||||
{ avatarNode }
|
||||
{ statusNode }
|
||||
</div>
|
||||
);
|
||||
if (onClickFn) {
|
||||
avatar = (
|
||||
<AccessibleButton onClick={onClickFn} className="mx_MemberPresenceAvatar" element="div">
|
||||
{ avatarNode }
|
||||
{ statusNode }
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
return avatar;
|
||||
},
|
||||
});
|
|
@ -13,11 +13,10 @@ 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 React = require('react');
|
||||
var ContentRepo = require("matrix-js-sdk").ContentRepo;
|
||||
var MatrixClientPeg = require('../../../MatrixClientPeg');
|
||||
var Avatar = require('../../../Avatar');
|
||||
var sdk = require("../../../index");
|
||||
import React from "react";
|
||||
import {ContentRepo} from "matrix-js-sdk";
|
||||
import MatrixClientPeg from "../../../MatrixClientPeg";
|
||||
import sdk from "../../../index";
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'RoomAvatar',
|
||||
|
@ -30,7 +29,7 @@ module.exports = React.createClass({
|
|||
oobData: React.PropTypes.object,
|
||||
width: React.PropTypes.number,
|
||||
height: React.PropTypes.number,
|
||||
resizeMethod: React.PropTypes.string
|
||||
resizeMethod: React.PropTypes.string,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
|
@ -44,13 +43,13 @@ module.exports = React.createClass({
|
|||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
urls: this.getImageUrls(this.props)
|
||||
urls: this.getImageUrls(this.props),
|
||||
};
|
||||
},
|
||||
|
||||
componentWillReceiveProps: function(newProps) {
|
||||
this.setState({
|
||||
urls: this.getImageUrls(newProps)
|
||||
urls: this.getImageUrls(newProps),
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -61,11 +60,10 @@ module.exports = React.createClass({
|
|||
props.oobData.avatarUrl,
|
||||
Math.floor(props.width * window.devicePixelRatio),
|
||||
Math.floor(props.height * window.devicePixelRatio),
|
||||
props.resizeMethod
|
||||
props.resizeMethod,
|
||||
), // highest priority
|
||||
this.getRoomAvatarUrl(props),
|
||||
this.getOneToOneAvatar(props),
|
||||
this.getFallbackAvatar(props) // lowest priority
|
||||
this.getOneToOneAvatar(props), // lowest priority
|
||||
].filter(function(url) {
|
||||
return (url != null && url != "");
|
||||
});
|
||||
|
@ -79,17 +77,17 @@ module.exports = React.createClass({
|
|||
Math.floor(props.width * window.devicePixelRatio),
|
||||
Math.floor(props.height * window.devicePixelRatio),
|
||||
props.resizeMethod,
|
||||
false
|
||||
false,
|
||||
);
|
||||
},
|
||||
|
||||
getOneToOneAvatar: function(props) {
|
||||
if (!props.room) return null;
|
||||
|
||||
var mlist = props.room.currentState.members;
|
||||
var userIds = [];
|
||||
const mlist = props.room.currentState.members;
|
||||
const userIds = [];
|
||||
// for .. in optimisation to return early if there are >2 keys
|
||||
for (var uid in mlist) {
|
||||
for (const uid in mlist) {
|
||||
if (mlist.hasOwnProperty(uid)) {
|
||||
userIds.push(uid);
|
||||
}
|
||||
|
@ -99,7 +97,7 @@ module.exports = React.createClass({
|
|||
}
|
||||
|
||||
if (userIds.length == 2) {
|
||||
var theOtherGuy = null;
|
||||
let theOtherGuy = null;
|
||||
if (mlist[userIds[0]].userId == MatrixClientPeg.get().credentials.userId) {
|
||||
theOtherGuy = mlist[userIds[1]];
|
||||
} else {
|
||||
|
@ -110,7 +108,7 @@ module.exports = React.createClass({
|
|||
Math.floor(props.width * window.devicePixelRatio),
|
||||
Math.floor(props.height * window.devicePixelRatio),
|
||||
props.resizeMethod,
|
||||
false
|
||||
false,
|
||||
);
|
||||
} else if (userIds.length == 1) {
|
||||
return mlist[userIds[0]].getAvatarUrl(
|
||||
|
@ -118,37 +116,24 @@ module.exports = React.createClass({
|
|||
Math.floor(props.width * window.devicePixelRatio),
|
||||
Math.floor(props.height * window.devicePixelRatio),
|
||||
props.resizeMethod,
|
||||
false
|
||||
false,
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
getFallbackAvatar: function(props) {
|
||||
let roomId = null;
|
||||
if (props.oobData && props.oobData.roomId) {
|
||||
roomId = this.props.oobData.roomId;
|
||||
} else if (props.room) {
|
||||
roomId = props.room.roomId;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Avatar.defaultAvatarUrlForString(roomId);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
|
||||
const BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
|
||||
|
||||
var {room, oobData, ...otherProps} = this.props;
|
||||
const {room, oobData, ...otherProps} = this.props;
|
||||
|
||||
var roomName = room ? room.name : oobData.name;
|
||||
const roomName = room ? room.name : oobData.name;
|
||||
|
||||
return (
|
||||
<BaseAvatar {...otherProps} name={roomName}
|
||||
idName={room ? room.roomId : null}
|
||||
urls={this.state.urls} />
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -36,7 +36,7 @@ module.exports = React.createClass({
|
|||
|
||||
render: function() {
|
||||
return (
|
||||
<button className="mx_CreateRoomButton" onClick={this.onClick}>{_t("Create Room")}</button>
|
||||
<button className="mx_CreateRoomButton" onClick={this.onClick}>{ _t("Create Room") }</button>
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -16,10 +16,10 @@ limitations under the License.
|
|||
|
||||
'use strict';
|
||||
|
||||
var React = require('react');
|
||||
const React = require('react');
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
var Presets = {
|
||||
const Presets = {
|
||||
PrivateChat: "private_chat",
|
||||
PublicChat: "public_chat",
|
||||
Custom: "custom",
|
||||
|
@ -29,7 +29,7 @@ module.exports = React.createClass({
|
|||
displayName: 'CreateRoomPresets',
|
||||
propTypes: {
|
||||
onChange: React.PropTypes.func,
|
||||
preset: React.PropTypes.string
|
||||
preset: React.PropTypes.string,
|
||||
},
|
||||
|
||||
Presets: Presets,
|
||||
|
@ -47,10 +47,10 @@ module.exports = React.createClass({
|
|||
render: function() {
|
||||
return (
|
||||
<select className="mx_Presets" onChange={this.onValueChanged} value={this.props.preset}>
|
||||
<option value={this.Presets.PrivateChat}>{_t("Private Chat")}</option>
|
||||
<option value={this.Presets.PublicChat}>{_t("Public Chat")}</option>
|
||||
<option value={this.Presets.Custom}>{_t("Custom")}</option>
|
||||
<option value={this.Presets.PrivateChat}>{ _t("Private Chat") }</option>
|
||||
<option value={this.Presets.PublicChat}>{ _t("Public Chat") }</option>
|
||||
<option value={this.Presets.Custom}>{ _t("Custom") }</option>
|
||||
</select>
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
var React = require('react');
|
||||
const React = require('react');
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
module.exports = React.createClass({
|
||||
|
@ -35,10 +35,10 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
getAliasLocalpart: function() {
|
||||
var room_alias = this.props.alias;
|
||||
let room_alias = this.props.alias;
|
||||
|
||||
if (room_alias && this.props.homeserver) {
|
||||
var suffix = ":" + this.props.homeserver;
|
||||
const suffix = ":" + this.props.homeserver;
|
||||
if (room_alias.startsWith("#") && room_alias.endsWith(suffix)) {
|
||||
room_alias = room_alias.slice(1, -suffix.length);
|
||||
}
|
||||
|
@ -52,22 +52,22 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
onFocus: function(ev) {
|
||||
var target = ev.target;
|
||||
var curr_val = ev.target.value;
|
||||
const target = ev.target;
|
||||
const curr_val = ev.target.value;
|
||||
|
||||
if (this.props.homeserver) {
|
||||
if (curr_val == "") {
|
||||
var self = this;
|
||||
const self = this;
|
||||
setTimeout(function() {
|
||||
target.value = "#:" + self.props.homeserver;
|
||||
target.setSelectionRange(1, 1);
|
||||
}, 0);
|
||||
} else {
|
||||
var suffix = ":" + this.props.homeserver;
|
||||
const suffix = ":" + this.props.homeserver;
|
||||
setTimeout(function() {
|
||||
target.setSelectionRange(
|
||||
curr_val.startsWith("#") ? 1 : 0,
|
||||
curr_val.endsWith(suffix) ? (target.value.length - suffix.length) : target.value.length
|
||||
curr_val.endsWith(suffix) ? (target.value.length - suffix.length) : target.value.length,
|
||||
);
|
||||
}, 0);
|
||||
}
|
||||
|
@ -75,7 +75,7 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
onBlur: function(ev) {
|
||||
var curr_val = ev.target.value;
|
||||
const curr_val = ev.target.value;
|
||||
|
||||
if (this.props.homeserver) {
|
||||
if (curr_val == "#:" + this.props.homeserver) {
|
||||
|
@ -84,8 +84,8 @@ module.exports = React.createClass({
|
|||
}
|
||||
|
||||
if (curr_val != "") {
|
||||
var new_val = ev.target.value;
|
||||
var suffix = ":" + this.props.homeserver;
|
||||
let new_val = ev.target.value;
|
||||
const suffix = ":" + this.props.homeserver;
|
||||
if (!curr_val.startsWith("#")) new_val = "#" + new_val;
|
||||
if (!curr_val.endsWith(suffix)) new_val = new_val + suffix;
|
||||
ev.target.value = new_val;
|
||||
|
@ -97,7 +97,7 @@ module.exports = React.createClass({
|
|||
return (
|
||||
<input type="text" className="mx_RoomAlias" placeholder={_t("Alias (optional)")}
|
||||
onChange={this.onValueChanged} onFocus={this.onFocus} onBlur={this.onBlur}
|
||||
value={this.props.alias}/>
|
||||
value={this.props.alias} />
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -23,16 +23,19 @@ import MatrixClientPeg from '../../../MatrixClientPeg';
|
|||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import Promise from 'bluebird';
|
||||
import { addressTypes, getAddressType } from '../../../UserAddress.js';
|
||||
import GroupStoreCache from '../../../stores/GroupStoreCache';
|
||||
|
||||
const TRUNCATE_QUERY_LIST = 40;
|
||||
const QUERY_USER_DIRECTORY_DEBOUNCE_MS = 200;
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: "UserPickerDialog",
|
||||
displayName: "AddressPickerDialog",
|
||||
|
||||
propTypes: {
|
||||
title: PropTypes.string.isRequired,
|
||||
description: PropTypes.node,
|
||||
// Extra node inserted after picker input, dropdown and errors
|
||||
extraNode: PropTypes.node,
|
||||
value: PropTypes.string,
|
||||
placeholder: PropTypes.string,
|
||||
roomId: PropTypes.string,
|
||||
|
@ -40,6 +43,12 @@ module.exports = React.createClass({
|
|||
focus: PropTypes.bool,
|
||||
validAddressTypes: PropTypes.arrayOf(PropTypes.oneOf(addressTypes)),
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
groupId: PropTypes.string,
|
||||
// The type of entity to search for. Default: 'user'.
|
||||
pickerType: PropTypes.oneOf(['user', 'room']),
|
||||
// Whether the current user should be included in the addresses returned. Only
|
||||
// applicable when pickerType is `user`. Default: false.
|
||||
includeSelf: PropTypes.bool,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
|
@ -47,6 +56,8 @@ module.exports = React.createClass({
|
|||
value: "",
|
||||
focus: true,
|
||||
validAddressTypes: addressTypes,
|
||||
pickerType: 'user',
|
||||
includeSelf: false,
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -140,10 +151,22 @@ module.exports = React.createClass({
|
|||
// Only do search if there is something to search
|
||||
if (query.length > 0 && query != '@' && query.length >= 2) {
|
||||
this.queryChangedDebouncer = setTimeout(() => {
|
||||
if (this.state.serverSupportsUserDirectory) {
|
||||
this._doUserDirectorySearch(query);
|
||||
if (this.props.pickerType === 'user') {
|
||||
if (this.props.groupId) {
|
||||
this._doNaiveGroupSearch(query);
|
||||
} else if (this.state.serverSupportsUserDirectory) {
|
||||
this._doUserDirectorySearch(query);
|
||||
} else {
|
||||
this._doLocalSearch(query);
|
||||
}
|
||||
} else if (this.props.pickerType === 'room') {
|
||||
if (this.props.groupId) {
|
||||
this._doNaiveGroupRoomSearch(query);
|
||||
} else {
|
||||
this._doRoomSearch(query);
|
||||
}
|
||||
} else {
|
||||
this._doLocalSearch(query);
|
||||
console.error('Unknown pickerType', this.props.pickerType);
|
||||
}
|
||||
}, QUERY_USER_DIRECTORY_DEBOUNCE_MS);
|
||||
} else {
|
||||
|
@ -185,6 +208,120 @@ module.exports = React.createClass({
|
|||
if (this._cancelThreepidLookup) this._cancelThreepidLookup();
|
||||
},
|
||||
|
||||
_doNaiveGroupSearch: function(query) {
|
||||
const lowerCaseQuery = query.toLowerCase();
|
||||
this.setState({
|
||||
busy: true,
|
||||
query,
|
||||
searchError: null,
|
||||
});
|
||||
MatrixClientPeg.get().getGroupUsers(this.props.groupId).then((resp) => {
|
||||
const results = [];
|
||||
resp.chunk.forEach((u) => {
|
||||
const userIdMatch = u.user_id.toLowerCase().includes(lowerCaseQuery);
|
||||
const displayNameMatch = (u.displayname || '').toLowerCase().includes(lowerCaseQuery);
|
||||
if (!(userIdMatch || displayNameMatch)) {
|
||||
return;
|
||||
}
|
||||
results.push({
|
||||
user_id: u.user_id,
|
||||
avatar_url: u.avatar_url,
|
||||
display_name: u.displayname,
|
||||
});
|
||||
});
|
||||
this._processResults(results, query);
|
||||
}).catch((err) => {
|
||||
console.error('Error whilst searching group rooms: ', err);
|
||||
this.setState({
|
||||
searchError: err.errcode ? err.message : _t('Something went wrong!'),
|
||||
});
|
||||
}).done(() => {
|
||||
this.setState({
|
||||
busy: false,
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
_doNaiveGroupRoomSearch: function(query) {
|
||||
const lowerCaseQuery = query.toLowerCase();
|
||||
const groupStore = GroupStoreCache.getGroupStore(this.props.groupId);
|
||||
const results = [];
|
||||
groupStore.getGroupRooms().forEach((r) => {
|
||||
const nameMatch = (r.name || '').toLowerCase().includes(lowerCaseQuery);
|
||||
const topicMatch = (r.topic || '').toLowerCase().includes(lowerCaseQuery);
|
||||
const aliasMatch = (r.canonical_alias || '').toLowerCase().includes(lowerCaseQuery);
|
||||
if (!(nameMatch || topicMatch || aliasMatch)) {
|
||||
return;
|
||||
}
|
||||
results.push({
|
||||
room_id: r.room_id,
|
||||
avatar_url: r.avatar_url,
|
||||
name: r.name || r.canonical_alias,
|
||||
});
|
||||
});
|
||||
this._processResults(results, query);
|
||||
this.setState({
|
||||
busy: false,
|
||||
});
|
||||
},
|
||||
|
||||
_doRoomSearch: function(query) {
|
||||
const lowerCaseQuery = query.toLowerCase();
|
||||
const rooms = MatrixClientPeg.get().getRooms();
|
||||
const results = [];
|
||||
rooms.forEach((room) => {
|
||||
let rank = Infinity;
|
||||
const nameEvent = room.currentState.getStateEvents('m.room.name', '');
|
||||
const name = nameEvent ? nameEvent.getContent().name : '';
|
||||
const canonicalAlias = room.getCanonicalAlias();
|
||||
const aliasEvents = room.currentState.getStateEvents('m.room.aliases');
|
||||
const aliases = aliasEvents.map((ev) => ev.getContent().aliases).reduce((a, b) => {
|
||||
return a.concat(b);
|
||||
}, []);
|
||||
|
||||
const nameMatch = (name || '').toLowerCase().includes(lowerCaseQuery);
|
||||
let aliasMatch = false;
|
||||
let shortestMatchingAliasLength = Infinity;
|
||||
aliases.forEach((alias) => {
|
||||
if ((alias || '').toLowerCase().includes(lowerCaseQuery)) {
|
||||
aliasMatch = true;
|
||||
if (shortestMatchingAliasLength > alias.length) {
|
||||
shortestMatchingAliasLength = alias.length;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!(nameMatch || aliasMatch)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (aliasMatch) {
|
||||
// A shorter matching alias will give a better rank
|
||||
rank = shortestMatchingAliasLength;
|
||||
}
|
||||
|
||||
const avatarEvent = room.currentState.getStateEvents('m.room.avatar', '');
|
||||
const avatarUrl = avatarEvent ? avatarEvent.getContent().url : undefined;
|
||||
|
||||
results.push({
|
||||
rank,
|
||||
room_id: room.roomId,
|
||||
avatar_url: avatarUrl,
|
||||
name: name || canonicalAlias || aliases[0] || _t('Unnamed Room'),
|
||||
});
|
||||
});
|
||||
|
||||
// Sort by rank ascending (a high rank being less relevant)
|
||||
const sortedResults = results.sort((a, b) => {
|
||||
return a.rank - b.rank;
|
||||
});
|
||||
|
||||
this._processResults(sortedResults, query);
|
||||
this.setState({
|
||||
busy: false,
|
||||
});
|
||||
},
|
||||
|
||||
_doUserDirectorySearch: function(query) {
|
||||
this.setState({
|
||||
busy: true,
|
||||
|
@ -245,17 +382,30 @@ module.exports = React.createClass({
|
|||
|
||||
_processResults: function(results, query) {
|
||||
const queryList = [];
|
||||
results.forEach((user) => {
|
||||
if (user.user_id === MatrixClientPeg.get().credentials.userId) {
|
||||
results.forEach((result) => {
|
||||
if (result.room_id) {
|
||||
queryList.push({
|
||||
addressType: 'mx-room-id',
|
||||
address: result.room_id,
|
||||
displayName: result.name,
|
||||
avatarMxc: result.avatar_url,
|
||||
isKnown: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!this.props.includeSelf &&
|
||||
result.user_id === MatrixClientPeg.get().credentials.userId
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Return objects, structure of which is defined
|
||||
// by UserAddressType
|
||||
queryList.push({
|
||||
addressType: 'mx',
|
||||
address: user.user_id,
|
||||
displayName: user.display_name,
|
||||
avatarMxc: user.avatar_url,
|
||||
addressType: 'mx-user-id',
|
||||
address: result.user_id,
|
||||
displayName: result.display_name,
|
||||
avatarMxc: result.avatar_url,
|
||||
isKnown: true,
|
||||
});
|
||||
});
|
||||
|
@ -291,16 +441,23 @@ module.exports = React.createClass({
|
|||
address: addressText,
|
||||
isKnown: false,
|
||||
};
|
||||
if (addrType == null) {
|
||||
if (!this.props.validAddressTypes.includes(addrType)) {
|
||||
this.setState({ error: true });
|
||||
return null;
|
||||
} else if (addrType == 'mx') {
|
||||
} else if (addrType == 'mx-user-id') {
|
||||
const user = MatrixClientPeg.get().getUser(addrObj.address);
|
||||
if (user) {
|
||||
addrObj.displayName = user.displayName;
|
||||
addrObj.avatarMxc = user.avatarUrl;
|
||||
addrObj.isKnown = true;
|
||||
}
|
||||
} else if (addrType == 'mx-room-id') {
|
||||
const room = MatrixClientPeg.get().getRoom(addrObj.address);
|
||||
if (room) {
|
||||
addrObj.displayName = room.name;
|
||||
addrObj.avatarMxc = room.avatarUrl;
|
||||
addrObj.isKnown = true;
|
||||
}
|
||||
}
|
||||
|
||||
const userList = this.state.userList.slice();
|
||||
|
@ -360,7 +517,12 @@ module.exports = React.createClass({
|
|||
const AddressTile = sdk.getComponent("elements.AddressTile");
|
||||
for (let i = 0; i < this.state.userList.length; i++) {
|
||||
query.push(
|
||||
<AddressTile key={i} address={this.state.userList[i]} canDismiss={true} onDismissed={ this.onDismissed(i) } />,
|
||||
<AddressTile
|
||||
key={i}
|
||||
address={this.state.userList[i]}
|
||||
canDismiss={true}
|
||||
onDismissed={this.onDismissed(i)}
|
||||
showAddress={this.props.pickerType === 'user'} />,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -382,23 +544,37 @@ module.exports = React.createClass({
|
|||
let error;
|
||||
let addressSelector;
|
||||
if (this.state.error) {
|
||||
let tryUsing = '';
|
||||
const validTypeDescriptions = this.props.validAddressTypes.map((t) => {
|
||||
return {
|
||||
'mx-user-id': _t("Matrix ID"),
|
||||
'mx-room-id': _t("Matrix Room ID"),
|
||||
'email': _t("email address"),
|
||||
}[t];
|
||||
});
|
||||
tryUsing = _t("Try using one of the following valid address types: %(validTypesList)s.", {
|
||||
validTypesList: validTypeDescriptions.join(", "),
|
||||
});
|
||||
error = <div className="mx_ChatInviteDialog_error">
|
||||
{_t("You have entered an invalid contact. Try using their Matrix ID or email address.")}
|
||||
{ _t("You have entered an invalid address.") }
|
||||
<br />
|
||||
{ tryUsing }
|
||||
</div>;
|
||||
} else if (this.state.searchError) {
|
||||
error = <div className="mx_ChatInviteDialog_error">{this.state.searchError}</div>;
|
||||
error = <div className="mx_ChatInviteDialog_error">{ this.state.searchError }</div>;
|
||||
} else if (
|
||||
this.state.query.length > 0 &&
|
||||
this.state.queryList.length === 0 &&
|
||||
!this.state.busy
|
||||
) {
|
||||
error = <div className="mx_ChatInviteDialog_error">{_t("No results")}</div>;
|
||||
error = <div className="mx_ChatInviteDialog_error">{ _t("No results") }</div>;
|
||||
} else {
|
||||
addressSelector = (
|
||||
<AddressSelector ref={(ref) => {this.addressSelector = ref;}}
|
||||
addressList={ this.state.queryList }
|
||||
onSelected={ this.onSelected }
|
||||
truncateAt={ TRUNCATE_QUERY_LIST }
|
||||
addressList={this.state.queryList}
|
||||
showAddress={this.props.pickerType === 'user'}
|
||||
onSelected={this.onSelected}
|
||||
truncateAt={TRUNCATE_QUERY_LIST}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -406,7 +582,7 @@ module.exports = React.createClass({
|
|||
return (
|
||||
<div className="mx_ChatInviteDialog" onKeyDown={this.onKeyDown}>
|
||||
<div className="mx_Dialog_title">
|
||||
{this.props.title}
|
||||
{ this.props.title }
|
||||
</div>
|
||||
<AccessibleButton className="mx_ChatInviteDialog_cancel"
|
||||
onClick={this.onCancel} >
|
||||
|
@ -419,10 +595,11 @@ module.exports = React.createClass({
|
|||
<div className="mx_ChatInviteDialog_inputContainer">{ query }</div>
|
||||
{ error }
|
||||
{ addressSelector }
|
||||
{ this.props.extraNode }
|
||||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button className="mx_Dialog_primary" onClick={this.onButtonClick}>
|
||||
{this.props.button}
|
||||
{ this.props.button }
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
|
@ -16,7 +16,7 @@ limitations under the License.
|
|||
|
||||
import React from 'react';
|
||||
|
||||
import * as KeyCode from '../../../KeyCode';
|
||||
import { KeyCode } from '../../../Keyboard';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import sdk from '../../../index';
|
||||
|
||||
|
|
|
@ -155,7 +155,7 @@ export default class ChatCreateOrReuseDialog extends React.Component {
|
|||
width={48} height={48}
|
||||
/>
|
||||
<div className="mx_ChatCreateOrReuseDialog_profile_name">
|
||||
{this.state.profile.displayName || this.props.userId}
|
||||
{ this.state.profile.displayName || this.props.userId }
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
@ -177,7 +177,7 @@ export default class ChatCreateOrReuseDialog extends React.Component {
|
|||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
return (
|
||||
<BaseDialog className='mx_ChatCreateOrReuseDialog'
|
||||
onFinished={ this.props.onFinished.bind(false) }
|
||||
onFinished={this.props.onFinished.bind(false)}
|
||||
title={title}
|
||||
>
|
||||
{ content }
|
||||
|
|
|
@ -16,7 +16,6 @@ limitations under the License.
|
|||
|
||||
import React from 'react';
|
||||
import sdk from '../../../index';
|
||||
import classnames from 'classnames';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
/*
|
||||
|
@ -24,51 +23,17 @@ import { _t } from '../../../languageHandler';
|
|||
*/
|
||||
export default React.createClass({
|
||||
displayName: 'ConfirmRedactDialog',
|
||||
propTypes: {
|
||||
onFinished: React.PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
defaultProps: {
|
||||
danger: false,
|
||||
},
|
||||
|
||||
onOk: function() {
|
||||
this.props.onFinished(true);
|
||||
},
|
||||
|
||||
onCancel: function() {
|
||||
this.props.onFinished(false);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
|
||||
const title = _t("Confirm Removal");
|
||||
|
||||
const confirmButtonClass = classnames({
|
||||
'mx_Dialog_primary': true,
|
||||
'danger': false,
|
||||
});
|
||||
|
||||
const QuestionDialog = sdk.getComponent('views.dialogs.QuestionDialog');
|
||||
return (
|
||||
<BaseDialog className="mx_ConfirmUserActionDialog" onFinished={this.props.onFinished}
|
||||
onEnterPressed={ this.onOk }
|
||||
title={title}
|
||||
>
|
||||
<div className="mx_Dialog_content">
|
||||
{_t("Are you sure you wish to remove (delete) this event? " +
|
||||
"Note that if you delete a room name or topic change, it could undo the change.")}
|
||||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button className={confirmButtonClass} onClick={this.onOk}>
|
||||
{_t("Remove")}
|
||||
</button>
|
||||
|
||||
<button onClick={this.onCancel}>
|
||||
{_t("Cancel")}
|
||||
</button>
|
||||
</div>
|
||||
</BaseDialog>
|
||||
<QuestionDialog onFinished={this.props.onFinished}
|
||||
title={_t("Confirm Removal")}
|
||||
description={
|
||||
_t("Are you sure you wish to remove (delete) this event? " +
|
||||
"Note that if you delete a room name or topic change, it could undo the change.")}
|
||||
button={_t("Remove")}>
|
||||
</QuestionDialog>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
|
|
@ -15,9 +15,11 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { MatrixClient } from 'matrix-js-sdk';
|
||||
import sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import classnames from 'classnames';
|
||||
import { GroupMemberType } from '../../../groups';
|
||||
|
||||
/*
|
||||
* A dialog for confirming an operation on another user.
|
||||
|
@ -30,8 +32,14 @@ import classnames from 'classnames';
|
|||
export default React.createClass({
|
||||
displayName: 'ConfirmUserActionDialog',
|
||||
propTypes: {
|
||||
member: React.PropTypes.object.isRequired, // matrix-js-sdk member object
|
||||
// matrix-js-sdk (room) member object. Supply either this or 'groupMember'
|
||||
member: React.PropTypes.object,
|
||||
// group member object. Supply either this or 'member'
|
||||
groupMember: GroupMemberType,
|
||||
// needed if a group member is specified
|
||||
matrixClient: React.PropTypes.instanceOf(MatrixClient),
|
||||
action: React.PropTypes.string.isRequired, // eg. 'Ban'
|
||||
title: React.PropTypes.string.isRequired, // eg. 'Ban this user?'
|
||||
|
||||
// Whether to display a text field for a reason
|
||||
// If true, the second argument to onFinished will
|
||||
|
@ -69,8 +77,8 @@ export default React.createClass({
|
|||
render: function() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const MemberAvatar = sdk.getComponent("views.avatars.MemberAvatar");
|
||||
const BaseAvatar = sdk.getComponent("views.avatars.BaseAvatar");
|
||||
|
||||
const title = _t("%(actionVerb)s this person?", { actionVerb: this.props.action});
|
||||
const confirmButtonClass = classnames({
|
||||
'mx_Dialog_primary': true,
|
||||
'danger': this.props.danger,
|
||||
|
@ -83,7 +91,7 @@ export default React.createClass({
|
|||
<form onSubmit={this.onOk}>
|
||||
<input className="mx_ConfirmUserActionDialog_reasonField"
|
||||
ref={this._collectReasonField}
|
||||
placeholder={ _t("Reason") }
|
||||
placeholder={_t("Reason")}
|
||||
autoFocus={true}
|
||||
/>
|
||||
</form>
|
||||
|
@ -91,24 +99,39 @@ export default React.createClass({
|
|||
);
|
||||
}
|
||||
|
||||
let avatar;
|
||||
let name;
|
||||
let userId;
|
||||
if (this.props.member) {
|
||||
avatar = <MemberAvatar member={this.props.member} width={48} height={48} />;
|
||||
name = this.props.member.name;
|
||||
userId = this.props.member.userId;
|
||||
} else {
|
||||
const httpAvatarUrl = this.props.groupMember.avatarUrl ?
|
||||
this.props.matrixClient.mxcUrlToHttp(this.props.groupMember.avatarUrl, 48, 48) : null;
|
||||
name = this.props.groupMember.displayname || this.props.groupMember.userId;
|
||||
userId = this.props.groupMember.userId;
|
||||
avatar = <BaseAvatar name={name} url={httpAvatarUrl} width={48} height={48} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<BaseDialog className="mx_ConfirmUserActionDialog" onFinished={this.props.onFinished}
|
||||
onEnterPressed={ this.onOk }
|
||||
title={title}
|
||||
onEnterPressed={this.onOk}
|
||||
title={this.props.title}
|
||||
>
|
||||
<div className="mx_Dialog_content">
|
||||
<div className="mx_ConfirmUserActionDialog_avatar">
|
||||
<MemberAvatar member={this.props.member} width={48} height={48} />
|
||||
{ avatar }
|
||||
</div>
|
||||
<div className="mx_ConfirmUserActionDialog_name">{this.props.member.name}</div>
|
||||
<div className="mx_ConfirmUserActionDialog_userId">{this.props.member.userId}</div>
|
||||
<div className="mx_ConfirmUserActionDialog_name">{ name }</div>
|
||||
<div className="mx_ConfirmUserActionDialog_userId">{ userId }</div>
|
||||
</div>
|
||||
{reasonBox}
|
||||
{ reasonBox }
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button className={confirmButtonClass}
|
||||
onClick={this.onOk} autoFocus={!this.props.askReason}
|
||||
>
|
||||
{this.props.action}
|
||||
{ this.props.action }
|
||||
</button>
|
||||
|
||||
<button onClick={this.onCancel}>
|
||||
|
|
|
@ -21,10 +21,6 @@ import dis from '../../../dispatcher';
|
|||
import { _t } from '../../../languageHandler';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
|
||||
// We match fairly liberally and leave it up to the server to reject if
|
||||
// there are invalid characters etc.
|
||||
const GROUP_REGEX = /^\+(.*?):(.*)$/;
|
||||
|
||||
export default React.createClass({
|
||||
displayName: 'CreateGroupDialog',
|
||||
propTypes: {
|
||||
|
@ -58,22 +54,9 @@ export default React.createClass({
|
|||
},
|
||||
|
||||
_checkGroupId: function(e) {
|
||||
const parsedGroupId = this._parseGroupId(this.state.groupId);
|
||||
let error = null;
|
||||
if (parsedGroupId === null) {
|
||||
error = _t(
|
||||
"Group IDs must be of the form +localpart:%(domain)s",
|
||||
{domain: MatrixClientPeg.get().getDomain()},
|
||||
);
|
||||
} else {
|
||||
const domain = parsedGroupId[1];
|
||||
if (domain !== MatrixClientPeg.get().getDomain()) {
|
||||
error = _t(
|
||||
"It is currently only possible to create groups on your own home server: "+
|
||||
"use a group ID ending with %(domain)s",
|
||||
{domain: MatrixClientPeg.get().getDomain()},
|
||||
);
|
||||
}
|
||||
if (!/^[a-z0-9=_\-\.\/]*$/.test(this.state.groupId)) {
|
||||
error = _t("Community IDs may only contain characters a-z, 0-9, or '=_-./'");
|
||||
}
|
||||
this.setState({
|
||||
groupIdError: error,
|
||||
|
@ -86,19 +69,19 @@ export default React.createClass({
|
|||
|
||||
if (this._checkGroupId()) return;
|
||||
|
||||
const parsedGroupId = this._parseGroupId(this.state.groupId);
|
||||
const profile = {};
|
||||
if (this.state.groupName !== '') {
|
||||
profile.name = this.state.groupName;
|
||||
}
|
||||
this.setState({creating: true});
|
||||
MatrixClientPeg.get().createGroup({
|
||||
localpart: parsedGroupId[0],
|
||||
localpart: this.state.groupId,
|
||||
profile: profile,
|
||||
}).then((result) => {
|
||||
dis.dispatch({
|
||||
action: 'view_group',
|
||||
group_id: result.group_id,
|
||||
group_is_new: true,
|
||||
});
|
||||
this.props.onFinished(true);
|
||||
}).catch((e) => {
|
||||
|
@ -112,22 +95,6 @@ export default React.createClass({
|
|||
this.props.onFinished(false);
|
||||
},
|
||||
|
||||
/**
|
||||
* Parse a string that may be a group ID
|
||||
* If the string is a valid group ID, return a list of [localpart, domain],
|
||||
* otherwise return null.
|
||||
*
|
||||
* @param {string} groupId The ID of the group
|
||||
* @return {string[]} array of localpart, domain
|
||||
*/
|
||||
_parseGroupId: function(groupId) {
|
||||
const matches = GROUP_REGEX.exec(this.state.groupId);
|
||||
if (!matches || matches.length < 3) {
|
||||
return null;
|
||||
}
|
||||
return [matches[1], matches[2]];
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const Spinner = sdk.getComponent('elements.Spinner');
|
||||
|
@ -142,21 +109,21 @@ export default React.createClass({
|
|||
// rather than displaying what the server gives us, but synapse doesn't give
|
||||
// any yet.
|
||||
createErrorNode = <div className="error">
|
||||
<div>{_t('Room creation failed')}</div>
|
||||
<div>{this.state.createError.message}</div>
|
||||
<div>{ _t('Something went wrong whilst creating your community') }</div>
|
||||
<div>{ this.state.createError.message }</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<BaseDialog className="mx_CreateGroupDialog" onFinished={this.props.onFinished}
|
||||
onEnterPressed={this._onFormSubmit}
|
||||
title={_t('Create Group')}
|
||||
title={_t('Create Community')}
|
||||
>
|
||||
<form onSubmit={this._onFormSubmit}>
|
||||
<div className="mx_Dialog_content">
|
||||
<div className="mx_CreateGroupDialog_inputRow">
|
||||
<div className="mx_CreateGroupDialog_label">
|
||||
<label htmlFor="groupname">{_t('Group Name')}</label>
|
||||
<label htmlFor="groupname">{ _t('Community Name') }</label>
|
||||
</div>
|
||||
<div>
|
||||
<input id="groupname" className="mx_CreateGroupDialog_input"
|
||||
|
@ -169,22 +136,27 @@ export default React.createClass({
|
|||
</div>
|
||||
<div className="mx_CreateGroupDialog_inputRow">
|
||||
<div className="mx_CreateGroupDialog_label">
|
||||
<label htmlFor="groupid">{_t('Group ID')}</label>
|
||||
<label htmlFor="groupid">{ _t('Community ID') }</label>
|
||||
</div>
|
||||
<div>
|
||||
<input id="groupid" className="mx_CreateGroupDialog_input"
|
||||
size="64"
|
||||
placeholder={_t('+example:%(domain)s', {domain: MatrixClientPeg.get().getDomain()})}
|
||||
<div className="mx_CreateGroupDialog_input_group">
|
||||
<span className="mx_CreateGroupDialog_prefix">+</span>
|
||||
<input id="groupid"
|
||||
className="mx_CreateGroupDialog_input mx_CreateGroupDialog_input_hasPrefixAndSuffix"
|
||||
size="32"
|
||||
placeholder={_t('example')}
|
||||
onChange={this._onGroupIdChange}
|
||||
onBlur={this._onGroupIdBlur}
|
||||
value={this.state.groupId}
|
||||
/>
|
||||
<span className="mx_CreateGroupDialog_suffix">
|
||||
:{ MatrixClientPeg.get().getDomain() }
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="error">
|
||||
{this.state.groupIdError}
|
||||
{ this.state.groupIdError }
|
||||
</div>
|
||||
{createErrorNode}
|
||||
{ createErrorNode }
|
||||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button onClick={this._onCancel}>
|
||||
|
|
81
src/components/views/dialogs/CreateRoomDialog.js
Normal file
81
src/components/views/dialogs/CreateRoomDialog.js
Normal file
|
@ -0,0 +1,81 @@
|
|||
/*
|
||||
Copyright 2017 Michael Telatynski <7t3chguy@gmail.com>
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import sdk from '../../../index';
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
export default React.createClass({
|
||||
displayName: 'CreateRoomDialog',
|
||||
propTypes: {
|
||||
onFinished: React.PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
const config = SdkConfig.get();
|
||||
// Dialog shows inverse of m.federate (noFederate) strict false check to skip undefined check (default = true)
|
||||
this.defaultNoFederate = config.default_federate === false;
|
||||
},
|
||||
|
||||
onOk: function() {
|
||||
this.props.onFinished(true, this.refs.textinput.value, this.refs.checkbox.checked);
|
||||
},
|
||||
|
||||
onCancel: function() {
|
||||
this.props.onFinished(false);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
return (
|
||||
<BaseDialog className="mx_CreateRoomDialog" onFinished={this.props.onFinished}
|
||||
onEnterPressed={this.onOk}
|
||||
title={_t('Create Room')}
|
||||
>
|
||||
<div className="mx_Dialog_content">
|
||||
<div className="mx_CreateRoomDialog_label">
|
||||
<label htmlFor="textinput"> { _t('Room name (optional)') } </label>
|
||||
</div>
|
||||
<div>
|
||||
<input id="textinput" ref="textinput" className="mx_CreateRoomDialog_input" autoFocus={true} size="64" />
|
||||
</div>
|
||||
<br />
|
||||
|
||||
<details className="mx_CreateRoomDialog_details">
|
||||
<summary className="mx_CreateRoomDialog_details_summary">{ _t('Advanced options') }</summary>
|
||||
<div>
|
||||
<input type="checkbox" id="checkbox" ref="checkbox" defaultChecked={this.defaultNoFederate} />
|
||||
<label htmlFor="checkbox">
|
||||
{ _t('Block users on other matrix homeservers from joining this room') }
|
||||
<br />
|
||||
({ _t('This setting cannot be changed later!') })
|
||||
</label>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button onClick={this.onCancel}>
|
||||
{ _t('Cancel') }
|
||||
</button>
|
||||
<button className="mx_Dialog_primary" onClick={this.onOk}>
|
||||
{ _t('Create Room') }
|
||||
</button>
|
||||
</div>
|
||||
</BaseDialog>
|
||||
);
|
||||
},
|
||||
});
|
|
@ -83,7 +83,7 @@ export default class DeactivateAccountDialog extends React.Component {
|
|||
let error = null;
|
||||
if (this.state.errStr) {
|
||||
error = <div className="error">
|
||||
{this.state.errStr}
|
||||
{ this.state.errStr }
|
||||
</div>;
|
||||
passwordBoxClass = 'error';
|
||||
}
|
||||
|
@ -94,30 +94,30 @@ export default class DeactivateAccountDialog extends React.Component {
|
|||
let cancelButton = null;
|
||||
if (!this.state.busy) {
|
||||
cancelButton = <button onClick={this._onCancel} autoFocus={true}>
|
||||
{_t("Cancel")}
|
||||
{ _t("Cancel") }
|
||||
</button>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx_DeactivateAccountDialog">
|
||||
<div className="mx_Dialog_title danger">
|
||||
{_t("Deactivate Account")}
|
||||
{ _t("Deactivate Account") }
|
||||
</div>
|
||||
<div className="mx_Dialog_content">
|
||||
<p>{_t("This will make your account permanently unusable. You will not be able to re-register the same user ID.")}</p>
|
||||
<p>{ _t("This will make your account permanently unusable. You will not be able to re-register the same user ID.") }</p>
|
||||
|
||||
<p>{_t("This action is irreversible.")}</p>
|
||||
<p>{ _t("This action is irreversible.") }</p>
|
||||
|
||||
<p>{_t("To continue, please enter your password.")}</p>
|
||||
<p>{ _t("To continue, please enter your password.") }</p>
|
||||
|
||||
<p>{_t("Password")}:</p>
|
||||
<p>{ _t("Password") }:</p>
|
||||
<input
|
||||
type="password"
|
||||
onChange={this._onPasswordFieldChange}
|
||||
ref={(e) => {this._passwordField = e;}}
|
||||
className={passwordBoxClass}
|
||||
/>
|
||||
{error}
|
||||
{ error }
|
||||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button
|
||||
|
@ -125,10 +125,10 @@ export default class DeactivateAccountDialog extends React.Component {
|
|||
onClick={this._onOk}
|
||||
disabled={!okEnabled}
|
||||
>
|
||||
{okLabel}
|
||||
{ okLabel }
|
||||
</button>
|
||||
|
||||
{cancelButton}
|
||||
{ cancelButton }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -28,25 +28,25 @@ export default function DeviceVerifyDialog(props) {
|
|||
const body = (
|
||||
<div>
|
||||
<p>
|
||||
{_t("To verify that this device can be trusted, please contact its " +
|
||||
{ _t("To verify that this device can be trusted, please contact its " +
|
||||
"owner using some other means (e.g. in person or a phone call) " +
|
||||
"and ask them whether the key they see in their User Settings " +
|
||||
"for this device matches the key below:")}
|
||||
"for this device matches the key below:") }
|
||||
</p>
|
||||
<div className="mx_UserSettings_cryptoSection">
|
||||
<ul>
|
||||
<li><label>{_t("Device name")}:</label> <span>{ props.device.getDisplayName() }</span></li>
|
||||
<li><label>{_t("Device ID")}:</label> <span><code>{ props.device.deviceId}</code></span></li>
|
||||
<li><label>{_t("Device key")}:</label> <span><code><b>{ key }</b></code></span></li>
|
||||
<li><label>{ _t("Device name") }:</label> <span>{ props.device.getDisplayName() }</span></li>
|
||||
<li><label>{ _t("Device ID") }:</label> <span><code>{ props.device.deviceId }</code></span></li>
|
||||
<li><label>{ _t("Device key") }:</label> <span><code><b>{ key }</b></code></span></li>
|
||||
</ul>
|
||||
</div>
|
||||
<p>
|
||||
{_t("If it matches, press the verify button below. " +
|
||||
{ _t("If it matches, press the verify button below. " +
|
||||
"If it doesn't, then someone else is intercepting this device " +
|
||||
"and you probably want to press the blacklist button instead.")}
|
||||
"and you probably want to press the blacklist button instead.") }
|
||||
</p>
|
||||
<p>
|
||||
{_t("In future this verification process will be more sophisticated.")}
|
||||
{ _t("In future this verification process will be more sophisticated.") }
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -63,11 +63,11 @@ export default React.createClass({
|
|||
<BaseDialog className="mx_ErrorDialog" onFinished={this.props.onFinished}
|
||||
title={this.props.title || _t('Error')}>
|
||||
<div className="mx_Dialog_content">
|
||||
{this.props.description || _t('An error has occurred.')}
|
||||
{ this.props.description || _t('An error has occurred.') }
|
||||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button ref="button" className="mx_Dialog_primary" onClick={this.props.onFinished}>
|
||||
{this.props.button || _t('OK')}
|
||||
{ this.props.button || _t('OK') }
|
||||
</button>
|
||||
</div>
|
||||
</BaseDialog>
|
||||
|
|
|
@ -48,7 +48,7 @@ export default React.createClass({
|
|||
getInitialState: function() {
|
||||
return {
|
||||
authError: null,
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
_onAuthFinished: function(success, result) {
|
||||
|
@ -73,12 +73,12 @@ export default React.createClass({
|
|||
if (this.state.authError) {
|
||||
content = (
|
||||
<div>
|
||||
<div>{this.state.authError.message || this.state.authError.toString()}</div>
|
||||
<div>{ this.state.authError.message || this.state.authError.toString() }</div>
|
||||
<br />
|
||||
<AccessibleButton onClick={this._onDismissClick}
|
||||
className="mx_UserSettings_button"
|
||||
>
|
||||
{_t("Dismiss")}
|
||||
{ _t("Dismiss") }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
);
|
||||
|
@ -100,7 +100,7 @@ export default React.createClass({
|
|||
onFinished={this.props.onFinished}
|
||||
title={this.state.authError ? 'Error' : (this.props.title || _t('Authentication'))}
|
||||
>
|
||||
{content}
|
||||
{ content }
|
||||
</BaseDialog>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -18,7 +18,7 @@ import Modal from '../../../Modal';
|
|||
import React from 'react';
|
||||
import sdk from '../../../index';
|
||||
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { _t, _td } from '../../../languageHandler';
|
||||
|
||||
/**
|
||||
* Dialog which asks the user whether they want to share their keys with
|
||||
|
@ -54,7 +54,7 @@ export default React.createClass({
|
|||
|
||||
const deviceInfo = r[userId][deviceId];
|
||||
|
||||
if(!deviceInfo) {
|
||||
if (!deviceInfo) {
|
||||
console.warn(`No details found for device ${userId}:${deviceId}`);
|
||||
|
||||
this.props.onFinished(false);
|
||||
|
@ -116,27 +116,27 @@ export default React.createClass({
|
|||
|
||||
let text;
|
||||
if (this.state.wasNewDevice) {
|
||||
text = "You added a new device '%(displayName)s', which is"
|
||||
+ " requesting encryption keys.";
|
||||
text = _td("You added a new device '%(displayName)s', which is"
|
||||
+ " requesting encryption keys.");
|
||||
} else {
|
||||
text = "Your unverified device '%(displayName)s' is requesting"
|
||||
+ " encryption keys.";
|
||||
text = _td("Your unverified device '%(displayName)s' is requesting"
|
||||
+ " encryption keys.");
|
||||
}
|
||||
text = _t(text, {displayName: displayName});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p>{text}</p>
|
||||
<p>{ text }</p>
|
||||
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button onClick={this._onVerifyClicked}>
|
||||
{_t('Start verification')}
|
||||
{ _t('Start verification') }
|
||||
</button>
|
||||
<button onClick={this._onShareClicked}>
|
||||
{_t('Share without verifying')}
|
||||
{ _t('Share without verifying') }
|
||||
</button>
|
||||
<button onClick={this._onIgnoreClicked}>
|
||||
{_t('Ignore request')}
|
||||
{ _t('Ignore request') }
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -154,7 +154,7 @@ export default React.createClass({
|
|||
} else {
|
||||
content = (
|
||||
<div>
|
||||
<p>{_t('Loading device info...')}</p>
|
||||
<p>{ _t('Loading device info...') }</p>
|
||||
<Spinner />
|
||||
</div>
|
||||
);
|
||||
|
@ -165,7 +165,7 @@ export default React.createClass({
|
|||
onFinished={this.props.onFinished}
|
||||
title={_t('Encryption key request')}
|
||||
>
|
||||
{content}
|
||||
{ content }
|
||||
</BaseDialog>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 New Vector Ltd.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -17,6 +18,7 @@ limitations under the License.
|
|||
import React from 'react';
|
||||
import sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import classnames from 'classnames';
|
||||
|
||||
export default React.createClass({
|
||||
displayName: 'QuestionDialog',
|
||||
|
@ -25,6 +27,7 @@ export default React.createClass({
|
|||
description: React.PropTypes.node,
|
||||
extraButtons: React.PropTypes.node,
|
||||
button: React.PropTypes.string,
|
||||
danger: React.PropTypes.bool,
|
||||
focus: React.PropTypes.bool,
|
||||
onFinished: React.PropTypes.func.isRequired,
|
||||
},
|
||||
|
@ -36,6 +39,7 @@ export default React.createClass({
|
|||
extraButtons: null,
|
||||
focus: true,
|
||||
hasCancelButton: true,
|
||||
danger: false,
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -51,23 +55,27 @@ export default React.createClass({
|
|||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const cancelButton = this.props.hasCancelButton ? (
|
||||
<button onClick={this.onCancel}>
|
||||
{_t("Cancel")}
|
||||
{ _t("Cancel") }
|
||||
</button>
|
||||
) : null;
|
||||
const buttonClasses = classnames({
|
||||
mx_Dialog_primary: true,
|
||||
danger: this.props.danger,
|
||||
});
|
||||
return (
|
||||
<BaseDialog className="mx_QuestionDialog" onFinished={this.props.onFinished}
|
||||
onEnterPressed={ this.onOk }
|
||||
onEnterPressed={this.onOk}
|
||||
title={this.props.title}
|
||||
>
|
||||
<div className="mx_Dialog_content">
|
||||
{this.props.description}
|
||||
{ this.props.description }
|
||||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button className="mx_Dialog_primary" onClick={this.onOk} autoFocus={this.props.focus}>
|
||||
{this.props.button || _t('OK')}
|
||||
<button className={buttonClasses} onClick={this.onOk} autoFocus={this.props.focus}>
|
||||
{ this.props.button || _t('OK') }
|
||||
</button>
|
||||
{this.props.extraButtons}
|
||||
{cancelButton}
|
||||
{ this.props.extraButtons }
|
||||
{ cancelButton }
|
||||
</div>
|
||||
</BaseDialog>
|
||||
);
|
||||
|
|
|
@ -18,7 +18,7 @@ import React from 'react';
|
|||
import sdk from '../../../index';
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
import Modal from '../../../Modal';
|
||||
import { _t, _tJsx } from '../../../languageHandler';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
|
||||
export default React.createClass({
|
||||
|
@ -45,10 +45,11 @@ export default React.createClass({
|
|||
if (SdkConfig.get().bug_report_endpoint_url) {
|
||||
bugreport = (
|
||||
<p>
|
||||
{_tJsx(
|
||||
{ _t(
|
||||
"Otherwise, <a>click here</a> to send a bug report.",
|
||||
/<a>(.*?)<\/a>/, (sub) => <a onClick={this._sendBugReport} key="bugreport" href='#'>{sub}</a>,
|
||||
)}
|
||||
{},
|
||||
{ 'a': (sub) => <a onClick={this._sendBugReport} key="bugreport" href='#'>{ sub }</a> },
|
||||
) }
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
@ -57,19 +58,19 @@ export default React.createClass({
|
|||
<BaseDialog className="mx_ErrorDialog" onFinished={this.props.onFinished}
|
||||
title={_t('Unable to restore session')}>
|
||||
<div className="mx_Dialog_content">
|
||||
<p>{_t("We encountered an error trying to restore your previous session. If " +
|
||||
<p>{ _t("We encountered an error trying to restore your previous session. If " +
|
||||
"you continue, you will need to log in again, and encrypted chat " +
|
||||
"history will be unreadable.")}</p>
|
||||
"history will be unreadable.") }</p>
|
||||
|
||||
<p>{_t("If you have previously used a more recent version of Riot, your session " +
|
||||
<p>{ _t("If you have previously used a more recent version of Riot, your session " +
|
||||
"may be incompatible with this version. Close this window and return " +
|
||||
"to the more recent version.")}</p>
|
||||
"to the more recent version.") }</p>
|
||||
|
||||
{bugreport}
|
||||
{ bugreport }
|
||||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button className="mx_Dialog_primary" onClick={this._continueClicked}>
|
||||
{_t("Continue anyway")}
|
||||
{ _t("Continue anyway") }
|
||||
</button>
|
||||
</div>
|
||||
</BaseDialog>
|
||||
|
|
|
@ -130,10 +130,10 @@ export default React.createClass({
|
|||
|
||||
const emailInput = this.state.emailBusy ? <Spinner /> : <EditableText
|
||||
className="mx_SetEmailDialog_email_input"
|
||||
placeholder={ _t("Email address") }
|
||||
placeholder={_t("Email address")}
|
||||
placeholderClassName="mx_SetEmailDialog_email_input_placeholder"
|
||||
blurToCancel={ false }
|
||||
onValueChanged={ this.onEmailAddressChanged } />;
|
||||
blurToCancel={false}
|
||||
onValueChanged={this.onEmailAddressChanged} />;
|
||||
|
||||
return (
|
||||
<BaseDialog className="mx_SetEmailDialog"
|
||||
|
|
|
@ -20,8 +20,8 @@ import React from 'react';
|
|||
import sdk from '../../../index';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import classnames from 'classnames';
|
||||
import KeyCode from '../../../KeyCode';
|
||||
import { _t, _tJsx } from '../../../languageHandler';
|
||||
import { KeyCode } from '../../../Keyboard';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
// The amount of time to wait for further changes to the input username before
|
||||
// sending a request to the server
|
||||
|
@ -226,7 +226,7 @@ export default React.createClass({
|
|||
let usernameIndicator = null;
|
||||
let usernameBusyIndicator = null;
|
||||
if (this.state.usernameBusy) {
|
||||
usernameBusyIndicator = <Spinner w="24" h="24"/>;
|
||||
usernameBusyIndicator = <Spinner w="24" h="24" />;
|
||||
} else {
|
||||
const usernameAvailable = this.state.username &&
|
||||
this.state.usernameCheckSupport && !this.state.usernameError;
|
||||
|
@ -267,25 +267,22 @@ export default React.createClass({
|
|||
</div>
|
||||
{ usernameIndicator }
|
||||
<p>
|
||||
{ _tJsx(
|
||||
{ _t(
|
||||
'This will be your account name on the <span></span> ' +
|
||||
'homeserver, or you can pick a <a>different server</a>.',
|
||||
[
|
||||
/<span><\/span>/,
|
||||
/<a>(.*?)<\/a>/,
|
||||
],
|
||||
[
|
||||
(sub) => <span>{this.props.homeserverUrl}</span>,
|
||||
(sub) => <a href="#" onClick={this.props.onDifferentServerClicked}>{sub}</a>,
|
||||
],
|
||||
)}
|
||||
{},
|
||||
{
|
||||
'span': <span>{ this.props.homeserverUrl }</span>,
|
||||
'a': (sub) => <a href="#" onClick={this.props.onDifferentServerClicked}>{ sub }</a>,
|
||||
},
|
||||
) }
|
||||
</p>
|
||||
<p>
|
||||
{ _tJsx(
|
||||
{ _t(
|
||||
'If you already have a Matrix account you can <a>log in</a> instead.',
|
||||
/<a>(.*?)<\/a>/,
|
||||
[(sub) => <a href="#" onClick={this.props.onLoginClick}>{sub}</a>],
|
||||
)}
|
||||
{},
|
||||
{ 'a': (sub) => <a href="#" onClick={this.props.onLoginClick}>{ sub }</a> },
|
||||
) }
|
||||
</p>
|
||||
{ auth }
|
||||
{ authErrorIndicator }
|
||||
|
|
|
@ -65,10 +65,10 @@ export default React.createClass({
|
|||
>
|
||||
<div className="mx_Dialog_content">
|
||||
<div className="mx_TextInputDialog_label">
|
||||
<label htmlFor="textinput"> {this.props.description} </label>
|
||||
<label htmlFor="textinput"> { this.props.description } </label>
|
||||
</div>
|
||||
<div>
|
||||
<input id="textinput" ref="textinput" className="mx_TextInputDialog_input" defaultValue={this.props.value} autoFocus={this.props.focus} size="64" onKeyDown={this.onKeyDown}/>
|
||||
<input id="textinput" ref="textinput" className="mx_TextInputDialog_input" defaultValue={this.props.value} autoFocus={this.props.focus} size="64" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
|
@ -76,7 +76,7 @@ export default React.createClass({
|
|||
{ _t("Cancel") }
|
||||
</button>
|
||||
<button className="mx_Dialog_primary" onClick={this.onOk}>
|
||||
{this.props.button}
|
||||
{ this.props.button }
|
||||
</button>
|
||||
</div>
|
||||
</BaseDialog>
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2017 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -15,11 +16,21 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import sdk from '../../../index';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import GeminiScrollbar from 'react-gemini-scrollbar';
|
||||
import Resend from '../../../Resend';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
|
||||
function markAllDevicesKnown(devices) {
|
||||
Object.keys(devices).forEach((userId) => {
|
||||
Object.keys(devices[userId]).map((deviceId) => {
|
||||
MatrixClientPeg.get().setDeviceKnown(userId, deviceId, true);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function DeviceListEntry(props) {
|
||||
const {userId, device} = props;
|
||||
|
@ -28,19 +39,19 @@ function DeviceListEntry(props) {
|
|||
|
||||
return (
|
||||
<li>
|
||||
<DeviceVerifyButtons device={ device } userId={ userId } />
|
||||
<DeviceVerifyButtons device={device} userId={userId} />
|
||||
{ device.deviceId }
|
||||
<br/>
|
||||
<br />
|
||||
{ device.getDisplayName() }
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
DeviceListEntry.propTypes = {
|
||||
userId: React.PropTypes.string.isRequired,
|
||||
userId: PropTypes.string.isRequired,
|
||||
|
||||
// deviceinfo
|
||||
device: React.PropTypes.object.isRequired,
|
||||
device: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
|
||||
|
@ -48,22 +59,22 @@ function UserUnknownDeviceList(props) {
|
|||
const {userId, userDevices} = props;
|
||||
|
||||
const deviceListEntries = Object.keys(userDevices).map((deviceId) =>
|
||||
<DeviceListEntry key={ deviceId } userId={ userId }
|
||||
device={ userDevices[deviceId] } />,
|
||||
<DeviceListEntry key={deviceId} userId={userId}
|
||||
device={userDevices[deviceId]} />,
|
||||
);
|
||||
|
||||
return (
|
||||
<ul className="mx_UnknownDeviceDialog_deviceList">
|
||||
{deviceListEntries}
|
||||
{ deviceListEntries }
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
UserUnknownDeviceList.propTypes = {
|
||||
userId: React.PropTypes.string.isRequired,
|
||||
userId: PropTypes.string.isRequired,
|
||||
|
||||
// map from deviceid -> deviceinfo
|
||||
userDevices: React.PropTypes.object.isRequired,
|
||||
userDevices: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
|
||||
|
@ -71,18 +82,18 @@ function UnknownDeviceList(props) {
|
|||
const {devices} = props;
|
||||
|
||||
const userListEntries = Object.keys(devices).map((userId) =>
|
||||
<li key={ userId }>
|
||||
<li key={userId}>
|
||||
<p>{ userId }:</p>
|
||||
<UserUnknownDeviceList userId={ userId } userDevices={ devices[userId] } />
|
||||
<UserUnknownDeviceList userId={userId} userDevices={devices[userId]} />
|
||||
</li>,
|
||||
);
|
||||
|
||||
return <ul>{userListEntries}</ul>;
|
||||
return <ul>{ userListEntries }</ul>;
|
||||
}
|
||||
|
||||
UnknownDeviceList.propTypes = {
|
||||
// map from userid -> deviceid -> deviceinfo
|
||||
devices: React.PropTypes.object.isRequired,
|
||||
devices: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
|
||||
|
@ -90,88 +101,124 @@ export default React.createClass({
|
|||
displayName: 'UnknownDeviceDialog',
|
||||
|
||||
propTypes: {
|
||||
room: React.PropTypes.object.isRequired,
|
||||
room: PropTypes.object.isRequired,
|
||||
|
||||
// map from userid -> deviceid -> deviceinfo
|
||||
devices: React.PropTypes.object.isRequired,
|
||||
onFinished: React.PropTypes.func.isRequired,
|
||||
// map from userid -> deviceid -> deviceinfo or null if devices are not yet loaded
|
||||
devices: PropTypes.object,
|
||||
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
|
||||
// Label for the button that marks all devices known and tries the send again
|
||||
sendAnywayLabel: PropTypes.string.isRequired,
|
||||
|
||||
// Label for the button that to send the event if you've verified all devices
|
||||
sendLabel: PropTypes.string.isRequired,
|
||||
|
||||
// function to retry the request once all devices are verified / known
|
||||
onSend: PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
// Given we've now shown the user the unknown device, it is no longer
|
||||
// unknown to them. Therefore mark it as 'known'.
|
||||
Object.keys(this.props.devices).forEach((userId) => {
|
||||
Object.keys(this.props.devices[userId]).map((deviceId) => {
|
||||
MatrixClientPeg.get().setDeviceKnown(userId, deviceId, true);
|
||||
});
|
||||
});
|
||||
componentWillMount: function() {
|
||||
MatrixClientPeg.get().on("deviceVerificationChanged", this._onDeviceVerificationChanged);
|
||||
},
|
||||
|
||||
// XXX: temporary logging to try to diagnose
|
||||
// https://github.com/vector-im/riot-web/issues/3148
|
||||
console.log('Opening UnknownDeviceDialog');
|
||||
componentWillUnmount: function() {
|
||||
if (MatrixClientPeg.get()) {
|
||||
MatrixClientPeg.get().removeListener("deviceVerificationChanged", this._onDeviceVerificationChanged);
|
||||
}
|
||||
},
|
||||
|
||||
_onDeviceVerificationChanged: function(userId, deviceId, deviceInfo) {
|
||||
if (this.props.devices[userId] && this.props.devices[userId][deviceId]) {
|
||||
// XXX: Mutating props :/
|
||||
this.props.devices[userId][deviceId] = deviceInfo;
|
||||
this.forceUpdate();
|
||||
}
|
||||
},
|
||||
|
||||
_onDismissClicked: function() {
|
||||
this.props.onFinished();
|
||||
},
|
||||
|
||||
_onSendAnywayClicked: function() {
|
||||
markAllDevicesKnown(this.props.devices);
|
||||
|
||||
this.props.onFinished();
|
||||
this.props.onSend();
|
||||
},
|
||||
|
||||
_onSendClicked: function() {
|
||||
this.props.onFinished();
|
||||
this.props.onSend();
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const client = MatrixClientPeg.get();
|
||||
const blacklistUnverified = client.getGlobalBlacklistUnverifiedDevices() ||
|
||||
this.props.room.getBlacklistUnverifiedDevices();
|
||||
if (this.props.devices === null) {
|
||||
const Spinner = sdk.getComponent("elements.Spinner");
|
||||
return <Spinner />;
|
||||
}
|
||||
|
||||
let warning;
|
||||
if (blacklistUnverified) {
|
||||
if (SettingsStore.getValue("blacklistUnverifiedDevices", this.props.room.roomId)) {
|
||||
warning = (
|
||||
<h4>
|
||||
{_t("You are currently blacklisting unverified devices; to send " +
|
||||
"messages to these devices you must verify them.")}
|
||||
{ _t("You are currently blacklisting unverified devices; to send " +
|
||||
"messages to these devices you must verify them.") }
|
||||
</h4>
|
||||
);
|
||||
} else {
|
||||
warning = (
|
||||
<div>
|
||||
<p>
|
||||
{_t("We recommend you go through the verification process " +
|
||||
{ _t("We recommend you go through the verification process " +
|
||||
"for each device to confirm they belong to their legitimate owner, " +
|
||||
"but you can resend the message without verifying if you prefer.")}
|
||||
"but you can resend the message without verifying if you prefer.") }
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
let haveUnknownDevices = false;
|
||||
Object.keys(this.props.devices).forEach((userId) => {
|
||||
Object.keys(this.props.devices[userId]).map((deviceId) => {
|
||||
const device = this.props.devices[userId][deviceId];
|
||||
if (device.isUnverified() && !device.isKnown()) {
|
||||
haveUnknownDevices = true;
|
||||
}
|
||||
});
|
||||
});
|
||||
let sendButton;
|
||||
if (haveUnknownDevices) {
|
||||
sendButton = <button onClick={this._onSendAnywayClicked}>
|
||||
{ this.props.sendAnywayLabel }
|
||||
</button>;
|
||||
} else {
|
||||
sendButton = <button onClick={this._onSendClicked}>
|
||||
{ this.props.sendLabel }
|
||||
</button>;
|
||||
}
|
||||
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
return (
|
||||
<BaseDialog className='mx_UnknownDeviceDialog'
|
||||
onFinished={() => {
|
||||
// XXX: temporary logging to try to diagnose
|
||||
// https://github.com/vector-im/riot-web/issues/3148
|
||||
console.log("UnknownDeviceDialog closed by escape");
|
||||
this.props.onFinished();
|
||||
}}
|
||||
onFinished={this.props.onFinished}
|
||||
title={_t('Room contains unknown devices')}
|
||||
>
|
||||
<GeminiScrollbar autoshow={false} className="mx_Dialog_content">
|
||||
<h4>
|
||||
{_t('"%(RoomName)s" contains devices that you haven\'t seen before.', {RoomName: this.props.room.name})}
|
||||
{ _t('"%(RoomName)s" contains devices that you haven\'t seen before.', {RoomName: this.props.room.name}) }
|
||||
</h4>
|
||||
{ warning }
|
||||
{_t("Unknown devices")}:
|
||||
{ _t("Unknown devices") }:
|
||||
|
||||
<UnknownDeviceList devices={this.props.devices} />
|
||||
</GeminiScrollbar>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button className="mx_Dialog_primary" autoFocus={ true }
|
||||
onClick={() => {
|
||||
this.props.onFinished();
|
||||
Resend.resendUnsentEvents(this.props.room);
|
||||
}}>
|
||||
{_t("Send anyway")}
|
||||
</button>
|
||||
<button className="mx_Dialog_primary" autoFocus={ true }
|
||||
onClick={() => {
|
||||
// XXX: temporary logging to try to diagnose
|
||||
// https://github.com/vector-im/riot-web/issues/3148
|
||||
console.log("UnknownDeviceDialog closed by OK");
|
||||
this.props.onFinished();
|
||||
}}>
|
||||
OK
|
||||
{sendButton}
|
||||
<button className="mx_Dialog_primary" autoFocus={true}
|
||||
onClick={this._onDismissClicked}
|
||||
>
|
||||
{_t("Dismiss")}
|
||||
</button>
|
||||
</div>
|
||||
</BaseDialog>
|
||||
|
|
|
@ -32,7 +32,7 @@ export default function AccessibleButton(props) {
|
|||
};
|
||||
restProps.tabIndex = restProps.tabIndex || "0";
|
||||
restProps.role = "button";
|
||||
restProps.className = (restProps.className ? restProps.className + " " : "") +
|
||||
restProps.className = (restProps.className ? restProps.className + " " : "") +
|
||||
"mx_AccessibleButton";
|
||||
return React.createElement(element, restProps, children);
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ import PropTypes from 'prop-types';
|
|||
import AccessibleButton from './AccessibleButton';
|
||||
import dis from '../../../dispatcher';
|
||||
import sdk from '../../../index';
|
||||
import Analytics from '../../../Analytics';
|
||||
|
||||
export default React.createClass({
|
||||
displayName: 'RoleButton',
|
||||
|
@ -47,6 +48,7 @@ export default React.createClass({
|
|||
|
||||
_onClick: function(ev) {
|
||||
ev.stopPropagation();
|
||||
Analytics.trackEvent('Action Button', 'click', this.props.action);
|
||||
dis.dispatch({action: this.props.action});
|
||||
},
|
||||
|
||||
|
@ -75,10 +77,11 @@ export default React.createClass({
|
|||
onClick={this._onClick}
|
||||
onMouseEnter={this._onMouseEnter}
|
||||
onMouseLeave={this._onMouseLeave}
|
||||
aria-label={this.props.label}
|
||||
>
|
||||
<TintableSvg src={this.props.iconPath} width={this.props.size} height={this.props.size} />
|
||||
{tooltip}
|
||||
{ tooltip }
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -30,6 +30,8 @@ export default React.createClass({
|
|||
|
||||
// List of the addresses to display
|
||||
addressList: React.PropTypes.arrayOf(UserAddressType).isRequired,
|
||||
// Whether to show the address on the address tiles
|
||||
showAddress: React.PropTypes.bool,
|
||||
truncateAt: React.PropTypes.number.isRequired,
|
||||
selected: React.PropTypes.number,
|
||||
|
||||
|
@ -46,8 +48,8 @@ export default React.createClass({
|
|||
|
||||
componentWillReceiveProps: function(props) {
|
||||
// Make sure the selected item isn't outside the list bounds
|
||||
var selected = this.state.selected;
|
||||
var maxSelected = this._maxSelected(props.addressList);
|
||||
const selected = this.state.selected;
|
||||
const maxSelected = this._maxSelected(props.addressList);
|
||||
if (selected > maxSelected) {
|
||||
this.setState({ selected: maxSelected });
|
||||
}
|
||||
|
@ -57,7 +59,7 @@ export default React.createClass({
|
|||
// As the user scrolls with the arrow keys keep the selected item
|
||||
// at the top of the window.
|
||||
if (this.scrollElement && this.props.addressList.length > 0 && !this.state.hover) {
|
||||
var elementHeight = this.addressListElement.getBoundingClientRect().height;
|
||||
const elementHeight = this.addressListElement.getBoundingClientRect().height;
|
||||
this.scrollElement.scrollTop = (this.state.selected * elementHeight) - elementHeight;
|
||||
}
|
||||
},
|
||||
|
@ -75,7 +77,7 @@ export default React.createClass({
|
|||
if (this.state.selected > 0) {
|
||||
this.setState({
|
||||
selected: this.state.selected - 1,
|
||||
hover : false,
|
||||
hover: false,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
@ -84,7 +86,7 @@ export default React.createClass({
|
|||
if (this.state.selected < this._maxSelected(this.props.addressList)) {
|
||||
this.setState({
|
||||
selected: this.state.selected + 1,
|
||||
hover : false,
|
||||
hover: false,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
@ -105,7 +107,7 @@ export default React.createClass({
|
|||
},
|
||||
|
||||
onMouseLeave: function() {
|
||||
this.setState({ hover : false });
|
||||
this.setState({ hover: false });
|
||||
},
|
||||
|
||||
selectAddress: function(index) {
|
||||
|
@ -117,15 +119,15 @@ export default React.createClass({
|
|||
},
|
||||
|
||||
createAddressListTiles: function() {
|
||||
var self = this;
|
||||
var AddressTile = sdk.getComponent("elements.AddressTile");
|
||||
var maxSelected = this._maxSelected(this.props.addressList);
|
||||
var addressList = [];
|
||||
const self = this;
|
||||
const AddressTile = sdk.getComponent("elements.AddressTile");
|
||||
const maxSelected = this._maxSelected(this.props.addressList);
|
||||
const addressList = [];
|
||||
|
||||
// Only create the address elements if there are address
|
||||
if (this.props.addressList.length > 0) {
|
||||
for (var i = 0; i <= maxSelected; i++) {
|
||||
var classes = classNames({
|
||||
for (let i = 0; i <= maxSelected; i++) {
|
||||
const classes = classNames({
|
||||
"mx_AddressSelector_addressListElement": true,
|
||||
"mx_AddressSelector_selected": this.state.selected === i,
|
||||
});
|
||||
|
@ -142,8 +144,14 @@ export default React.createClass({
|
|||
key={this.props.addressList[i].addressType + "/" + this.props.addressList[i].address}
|
||||
ref={(ref) => { this.addressListElement = ref; }}
|
||||
>
|
||||
<AddressTile address={this.props.addressList[i]} justified={true} networkName="vector" networkUrl="img/search-icon-vector.svg" />
|
||||
</div>
|
||||
<AddressTile
|
||||
address={this.props.addressList[i]}
|
||||
showAddress={this.props.showAddress}
|
||||
justified={true}
|
||||
networkName="vector"
|
||||
networkUrl="img/search-icon-vector.svg"
|
||||
/>
|
||||
</div>,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -151,13 +159,13 @@ export default React.createClass({
|
|||
},
|
||||
|
||||
_maxSelected: function(list) {
|
||||
var listSize = list.length === 0 ? 0 : list.length - 1;
|
||||
var maxSelected = listSize > (this.props.truncateAt - 1) ? (this.props.truncateAt - 1) : listSize;
|
||||
const listSize = list.length === 0 ? 0 : list.length - 1;
|
||||
const maxSelected = listSize > (this.props.truncateAt - 1) ? (this.props.truncateAt - 1) : listSize;
|
||||
return maxSelected;
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var classes = classNames({
|
||||
const classes = classNames({
|
||||
"mx_AddressSelector": true,
|
||||
"mx_AddressSelector_empty": this.props.addressList.length === 0,
|
||||
});
|
||||
|
@ -168,5 +176,5 @@ export default React.createClass({
|
|||
{ this.createAddressListTiles() }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -45,11 +45,12 @@ export default React.createClass({
|
|||
const address = this.props.address;
|
||||
const name = address.displayName || address.address;
|
||||
|
||||
let imgUrls = [];
|
||||
const imgUrls = [];
|
||||
const isMatrixAddress = ['mx-user-id', 'mx-room-id'].includes(address.addressType);
|
||||
|
||||
if (address.addressType === "mx" && address.avatarMxc) {
|
||||
if (isMatrixAddress && address.avatarMxc) {
|
||||
imgUrls.push(MatrixClientPeg.get().mxcUrlToHttp(
|
||||
address.avatarMxc, 25, 25, 'crop'
|
||||
address.avatarMxc, 25, 25, 'crop',
|
||||
));
|
||||
} else if (address.addressType === 'email') {
|
||||
imgUrls.push('img/icon-email-user.svg');
|
||||
|
@ -77,7 +78,7 @@ export default React.createClass({
|
|||
|
||||
let info;
|
||||
let error = false;
|
||||
if (address.addressType === "mx" && address.isKnown) {
|
||||
if (isMatrixAddress && address.isKnown) {
|
||||
const idClasses = classNames({
|
||||
"mx_AddressTile_id": true,
|
||||
"mx_AddressTile_justified": this.props.justified,
|
||||
|
@ -86,10 +87,13 @@ export default React.createClass({
|
|||
info = (
|
||||
<div className="mx_AddressTile_mx">
|
||||
<div className={nameClasses}>{ name }</div>
|
||||
<div className={idClasses}>{ address.address }</div>
|
||||
{ this.props.showAddress ?
|
||||
<div className={idClasses}>{ address.address }</div> :
|
||||
<div />
|
||||
}
|
||||
</div>
|
||||
);
|
||||
} else if (address.addressType === "mx") {
|
||||
} else if (isMatrixAddress) {
|
||||
const unknownMxClasses = classNames({
|
||||
"mx_AddressTile_unknownMx": true,
|
||||
"mx_AddressTile_justified": this.props.justified,
|
||||
|
@ -106,24 +110,24 @@ export default React.createClass({
|
|||
|
||||
let nameNode = null;
|
||||
if (address.displayName) {
|
||||
nameNode = <div className={nameClasses}>{ address.displayName }</div>
|
||||
nameNode = <div className={nameClasses}>{ address.displayName }</div>;
|
||||
}
|
||||
|
||||
info = (
|
||||
<div className="mx_AddressTile_mx">
|
||||
<div className={emailClasses}>{ address.address }</div>
|
||||
{nameNode}
|
||||
{ nameNode }
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
error = true;
|
||||
var unknownClasses = classNames({
|
||||
const unknownClasses = classNames({
|
||||
"mx_AddressTile_unknown": true,
|
||||
"mx_AddressTile_justified": this.props.justified,
|
||||
});
|
||||
|
||||
info = (
|
||||
<div className={unknownClasses}>{_t("Unknown Address")}</div>
|
||||
<div className={unknownClasses}>{ _t("Unknown Address") }</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -150,5 +154,5 @@ export default React.createClass({
|
|||
{ dismiss }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -19,9 +19,9 @@ export default class AppPermission extends React.Component {
|
|||
|
||||
const searchParams = new URLSearchParams(wurl.search);
|
||||
|
||||
if(this.isScalarWurl(wurl) && searchParams && searchParams.get('url')) {
|
||||
if (this.isScalarWurl(wurl) && searchParams && searchParams.get('url')) {
|
||||
curl = url.parse(searchParams.get('url'));
|
||||
if(curl) {
|
||||
if (curl) {
|
||||
curl.search = curl.query = "";
|
||||
curlString = curl.format();
|
||||
}
|
||||
|
@ -34,7 +34,7 @@ export default class AppPermission extends React.Component {
|
|||
}
|
||||
|
||||
isScalarWurl(wurl) {
|
||||
if(wurl && wurl.hostname && (
|
||||
if (wurl && wurl.hostname && (
|
||||
wurl.hostname === 'scalar.vector.im' ||
|
||||
wurl.hostname === 'scalar-staging.riot.im' ||
|
||||
wurl.hostname === 'scalar-develop.riot.im' ||
|
||||
|
@ -50,16 +50,16 @@ export default class AppPermission extends React.Component {
|
|||
let e2eWarningText;
|
||||
if (this.props.isRoomEncrypted) {
|
||||
e2eWarningText =
|
||||
<span className='mx_AppPermissionWarningTextLabel'>{_t('NOTE: Apps are not end-to-end encrypted')}</span>;
|
||||
<span className='mx_AppPermissionWarningTextLabel'>{ _t('NOTE: Apps are not end-to-end encrypted') }</span>;
|
||||
}
|
||||
return (
|
||||
<div className='mx_AppPermissionWarning'>
|
||||
<div className='mx_AppPermissionWarningImage'>
|
||||
<img src='img/warning.svg' alt={_t('Warning!')}/>
|
||||
<img src='img/warning.svg' alt={_t('Warning!')} />
|
||||
</div>
|
||||
<div className='mx_AppPermissionWarningText'>
|
||||
<span className='mx_AppPermissionWarningTextLabel'>{_t('Do you want to load widget from URL:')}</span> <span className='mx_AppPermissionWarningTextURL'>{this.state.curlBase}</span>
|
||||
{e2eWarningText}
|
||||
<span className='mx_AppPermissionWarningTextLabel'>{ _t('Do you want to load widget from URL:') }</span> <span className='mx_AppPermissionWarningTextURL'>{ this.state.curlBase }</span>
|
||||
{ e2eWarningText }
|
||||
</div>
|
||||
<input
|
||||
className='mx_AppPermissionButton'
|
||||
|
|
|
@ -17,12 +17,16 @@ limitations under the License.
|
|||
'use strict';
|
||||
|
||||
import url from 'url';
|
||||
import qs from 'querystring';
|
||||
import React from 'react';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import PlatformPeg from '../../../PlatformPeg';
|
||||
import ScalarAuthClient from '../../../ScalarAuthClient';
|
||||
import WidgetMessaging from '../../../WidgetMessaging';
|
||||
import TintableSvgButton from './TintableSvgButton';
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
import Modal from '../../../Modal';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { _t, _td } from '../../../languageHandler';
|
||||
import sdk from '../../../index';
|
||||
import AppPermission from './AppPermission';
|
||||
import AppWarning from './AppWarning';
|
||||
|
@ -48,35 +52,96 @@ export default React.createClass({
|
|||
userId: React.PropTypes.string.isRequired,
|
||||
// UserId of the entity that added / modified the widget
|
||||
creatorUserId: React.PropTypes.string,
|
||||
waitForIframeLoad: React.PropTypes.bool,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
getDefaultProps() {
|
||||
return {
|
||||
url: "",
|
||||
waitForIframeLoad: true,
|
||||
};
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
const widgetPermissionId = [this.props.room.roomId, encodeURIComponent(this.props.url)].join('_');
|
||||
/**
|
||||
* Set initial component state when the App wUrl (widget URL) is being updated.
|
||||
* Component props *must* be passed (rather than relying on this.props).
|
||||
* @param {Object} newProps The new properties of the component
|
||||
* @return {Object} Updated component state to be set with setState
|
||||
*/
|
||||
_getNewState(newProps) {
|
||||
const widgetPermissionId = [newProps.room.roomId, encodeURIComponent(newProps.url)].join('_');
|
||||
const hasPermissionToLoad = localStorage.getItem(widgetPermissionId);
|
||||
return {
|
||||
loading: false,
|
||||
widgetUrl: this.props.url,
|
||||
initialising: true, // True while we are mangling the widget URL
|
||||
loading: this.props.waitForIframeLoad, // True while the iframe content is loading
|
||||
widgetUrl: this._addWurlParams(newProps.url),
|
||||
widgetPermissionId: widgetPermissionId,
|
||||
// Assume that widget has permission to load if we are the user who added it to the room, or if explicitly granted by the user
|
||||
hasPermissionToLoad: hasPermissionToLoad === 'true' || this.props.userId === this.props.creatorUserId,
|
||||
// Assume that widget has permission to load if we are the user who
|
||||
// added it to the room, or if explicitly granted by the user
|
||||
hasPermissionToLoad: hasPermissionToLoad === 'true' || newProps.userId === newProps.creatorUserId,
|
||||
error: null,
|
||||
deleting: false,
|
||||
widgetPageTitle: newProps.widgetPageTitle,
|
||||
};
|
||||
},
|
||||
|
||||
// Returns true if props.url is a scalar URL, typically https://scalar.vector.im/api
|
||||
isScalarUrl: function() {
|
||||
const scalarUrl = SdkConfig.get().integrations_rest_url;
|
||||
return scalarUrl && this.props.url.startsWith(scalarUrl);
|
||||
/**
|
||||
* Add widget instance specific parameters to pass in wUrl
|
||||
* Properties passed to widget instance:
|
||||
* - widgetId
|
||||
* - origin / parent URL
|
||||
* @param {string} urlString Url string to modify
|
||||
* @return {string}
|
||||
* Url string with parameters appended.
|
||||
* If url can not be parsed, it is returned unmodified.
|
||||
*/
|
||||
_addWurlParams(urlString) {
|
||||
const u = url.parse(urlString);
|
||||
if (!u) {
|
||||
console.error("_addWurlParams", "Invalid URL", urlString);
|
||||
return url;
|
||||
}
|
||||
|
||||
const params = qs.parse(u.query);
|
||||
// Append widget ID to query parameters
|
||||
params.widgetId = this.props.id;
|
||||
// Append current / parent URL
|
||||
params.parentUrl = window.location.href;
|
||||
u.search = undefined;
|
||||
u.query = params;
|
||||
|
||||
return u.format();
|
||||
},
|
||||
|
||||
isMixedContent: function() {
|
||||
getInitialState() {
|
||||
return this._getNewState(this.props);
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns true if specified url is a scalar URL, typically https://scalar.vector.im/api
|
||||
* @param {[type]} url URL to check
|
||||
* @return {Boolean} True if specified URL is a scalar URL
|
||||
*/
|
||||
isScalarUrl(url) {
|
||||
if (!url) {
|
||||
console.error('Scalar URL check failed. No URL specified');
|
||||
return false;
|
||||
}
|
||||
|
||||
let scalarUrls = SdkConfig.get().integrations_widgets_urls;
|
||||
if (!scalarUrls || scalarUrls.length == 0) {
|
||||
scalarUrls = [SdkConfig.get().integrations_rest_url];
|
||||
}
|
||||
|
||||
for (let i = 0; i < scalarUrls.length; i++) {
|
||||
if (url.startsWith(scalarUrls[i])) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
isMixedContent() {
|
||||
const parentContentProtocol = window.location.protocol;
|
||||
const u = url.parse(this.props.url);
|
||||
const childContentProtocol = u.protocol;
|
||||
|
@ -88,43 +153,110 @@ export default React.createClass({
|
|||
return false;
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
if (!this.isScalarUrl()) {
|
||||
componentWillMount() {
|
||||
WidgetMessaging.startListening();
|
||||
WidgetMessaging.addEndpoint(this.props.id, this.props.url);
|
||||
window.addEventListener('message', this._onMessage, false);
|
||||
this.setScalarToken();
|
||||
},
|
||||
|
||||
/**
|
||||
* Adds a scalar token to the widget URL, if required
|
||||
* Component initialisation is only complete when this function has resolved
|
||||
*/
|
||||
setScalarToken() {
|
||||
this.setState({initialising: true});
|
||||
|
||||
if (!this.isScalarUrl(this.props.url)) {
|
||||
console.warn('Non-scalar widget, not setting scalar token!', url);
|
||||
this.setState({
|
||||
error: null,
|
||||
widgetUrl: this._addWurlParams(this.props.url),
|
||||
initialising: false,
|
||||
});
|
||||
return;
|
||||
}
|
||||
// Fetch the token before loading the iframe as we need to mangle the URL
|
||||
this.setState({
|
||||
loading: true,
|
||||
});
|
||||
this._scalarClient = new ScalarAuthClient();
|
||||
|
||||
// Fetch the token before loading the iframe as we need it to mangle the URL
|
||||
if (!this._scalarClient) {
|
||||
this._scalarClient = new ScalarAuthClient();
|
||||
}
|
||||
this._scalarClient.getScalarToken().done((token) => {
|
||||
// Append scalar_token as a query param
|
||||
// Append scalar_token as a query param if not already present
|
||||
this._scalarClient.scalarToken = token;
|
||||
const u = url.parse(this.props.url);
|
||||
if (!u.search) {
|
||||
u.search = "?scalar_token=" + encodeURIComponent(token);
|
||||
} else {
|
||||
u.search += "&scalar_token=" + encodeURIComponent(token);
|
||||
const u = url.parse(this._addWurlParams(this.props.url));
|
||||
const params = qs.parse(u.query);
|
||||
if (!params.scalar_token) {
|
||||
params.scalar_token = encodeURIComponent(token);
|
||||
// u.search must be set to undefined, so that u.format() uses query paramerters - https://nodejs.org/docs/latest/api/url.html#url_url_format_url_options
|
||||
u.search = undefined;
|
||||
u.query = params;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
error: null,
|
||||
widgetUrl: u.format(),
|
||||
loading: false,
|
||||
initialising: false,
|
||||
});
|
||||
|
||||
// Fetch page title from remote content if not already set
|
||||
if (!this.state.widgetPageTitle && params.url) {
|
||||
this._fetchWidgetTitle(params.url);
|
||||
}
|
||||
}, (err) => {
|
||||
console.error("Failed to get scalar_token", err);
|
||||
this.setState({
|
||||
error: err.message,
|
||||
loading: false,
|
||||
initialising: false,
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
_canUserModify: function() {
|
||||
componentWillUnmount() {
|
||||
WidgetMessaging.stopListening();
|
||||
WidgetMessaging.removeEndpoint(this.props.id, this.props.url);
|
||||
window.removeEventListener('message', this._onMessage);
|
||||
},
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (nextProps.url !== this.props.url) {
|
||||
this._getNewState(nextProps);
|
||||
this.setScalarToken();
|
||||
} else if (nextProps.show && !this.props.show && this.props.waitForIframeLoad) {
|
||||
this.setState({
|
||||
loading: true,
|
||||
});
|
||||
} else if (nextProps.widgetPageTitle !== this.props.widgetPageTitle) {
|
||||
this.setState({
|
||||
widgetPageTitle: nextProps.widgetPageTitle,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
_onMessage(event) {
|
||||
if (this.props.type !== 'jitsi') {
|
||||
return;
|
||||
}
|
||||
if (!event.origin) {
|
||||
event.origin = event.originalEvent.origin;
|
||||
}
|
||||
|
||||
if (!this.state.widgetUrl.startsWith(event.origin)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.data.widgetAction === 'jitsi_iframe_loaded') {
|
||||
const iframe = this.refs.appFrame.contentWindow
|
||||
.document.querySelector('iframe[id^="jitsiConferenceFrame"]');
|
||||
PlatformPeg.get().setupScreenSharingForIframe(iframe);
|
||||
}
|
||||
},
|
||||
|
||||
_canUserModify() {
|
||||
return WidgetUtils.canUserModifyWidgets(this.props.room.roomId);
|
||||
},
|
||||
|
||||
_onEditClick: function(e) {
|
||||
_onEditClick(e) {
|
||||
console.log("Edit widget ID ", this.props.id);
|
||||
const IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager");
|
||||
const src = this._scalarClient.getScalarInterfaceUrlForRoom(
|
||||
|
@ -134,22 +266,34 @@ export default React.createClass({
|
|||
}, "mx_IntegrationsManager");
|
||||
},
|
||||
|
||||
/* If user has permission to modify widgets, delete the widget, otherwise revoke access for the widget to load in the user's browser
|
||||
/* If user has permission to modify widgets, delete the widget,
|
||||
* otherwise revoke access for the widget to load in the user's browser
|
||||
*/
|
||||
_onDeleteClick: function() {
|
||||
_onDeleteClick() {
|
||||
if (this._canUserModify()) {
|
||||
console.log("Delete widget %s", this.props.id);
|
||||
this.setState({deleting: true});
|
||||
MatrixClientPeg.get().sendStateEvent(
|
||||
this.props.room.roomId,
|
||||
'im.vector.modular.widgets',
|
||||
{}, // empty content
|
||||
this.props.id,
|
||||
).then(() => {
|
||||
console.log('Deleted widget');
|
||||
}, (e) => {
|
||||
console.error('Failed to delete widget', e);
|
||||
this.setState({deleting: false});
|
||||
// Show delete confirmation dialog
|
||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
Modal.createTrackedDialog('Delete Widget', '', QuestionDialog, {
|
||||
title: _t("Delete Widget"),
|
||||
description: _t(
|
||||
"Deleting a widget removes it for all users in this room." +
|
||||
" Are you sure you want to delete this widget?"),
|
||||
button: _t("Delete widget"),
|
||||
onFinished: (confirmed) => {
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
this.setState({deleting: true});
|
||||
MatrixClientPeg.get().sendStateEvent(
|
||||
this.props.room.roomId,
|
||||
'im.vector.modular.widgets',
|
||||
{}, // empty content
|
||||
this.props.id,
|
||||
).catch((e) => {
|
||||
console.error('Failed to delete widget', e);
|
||||
this.setState({deleting: false});
|
||||
});
|
||||
},
|
||||
});
|
||||
} else {
|
||||
console.log("Revoke widget permissions - %s", this.props.id);
|
||||
|
@ -157,13 +301,34 @@ export default React.createClass({
|
|||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Called when widget iframe has finished loading
|
||||
*/
|
||||
_onLoaded() {
|
||||
this.setState({loading: false});
|
||||
},
|
||||
|
||||
/**
|
||||
* Set remote content title on AppTile
|
||||
* @param {string} url Url to check for title
|
||||
*/
|
||||
_fetchWidgetTitle(url) {
|
||||
this._scalarClient.getScalarPageTitle(url).then((widgetPageTitle) => {
|
||||
if (widgetPageTitle) {
|
||||
this.setState({widgetPageTitle: widgetPageTitle});
|
||||
}
|
||||
}, (err) =>{
|
||||
console.error("Failed to get page title", err);
|
||||
});
|
||||
},
|
||||
|
||||
// Widget labels to render, depending upon user permissions
|
||||
// These strings are translated at the point that they are inserted in to the DOM, in the render method
|
||||
_deleteWidgetLabel() {
|
||||
if (this._canUserModify()) {
|
||||
return 'Delete widget';
|
||||
return _td('Delete widget');
|
||||
}
|
||||
return 'Revoke widget access';
|
||||
return _td('Revoke widget access');
|
||||
},
|
||||
|
||||
/* TODO -- Store permission in account data so that it is persisted across multiple devices */
|
||||
|
@ -179,15 +344,15 @@ export default React.createClass({
|
|||
this.setState({hasPermissionToLoad: false});
|
||||
},
|
||||
|
||||
formatAppTileName: function() {
|
||||
formatAppTileName() {
|
||||
let appTileName = "No name";
|
||||
if(this.props.name && this.props.name.trim()) {
|
||||
if (this.props.name && this.props.name.trim()) {
|
||||
appTileName = this.props.name.trim();
|
||||
}
|
||||
return appTileName;
|
||||
},
|
||||
|
||||
onClickMenuBar: function(ev) {
|
||||
onClickMenuBar(ev) {
|
||||
ev.preventDefault();
|
||||
|
||||
// Ignore clicks on menu bar children
|
||||
|
@ -202,7 +367,16 @@ export default React.createClass({
|
|||
});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
_getSafeUrl() {
|
||||
const parsedWidgetUrl = url.parse(this.state.widgetUrl);
|
||||
let safeWidgetUrl = '';
|
||||
if (ALLOWED_APP_URL_SCHEMES.indexOf(parsedWidgetUrl.protocol) !== -1) {
|
||||
safeWidgetUrl = url.format(parsedWidgetUrl);
|
||||
}
|
||||
return safeWidgetUrl;
|
||||
},
|
||||
|
||||
render() {
|
||||
let appTileBody;
|
||||
|
||||
// Don't render widget if it is in the process of being deleted
|
||||
|
@ -217,36 +391,32 @@ export default React.createClass({
|
|||
// a link to it.
|
||||
const sandboxFlags = "allow-forms allow-popups allow-popups-to-escape-sandbox "+
|
||||
"allow-same-origin allow-scripts allow-presentation";
|
||||
const parsedWidgetUrl = url.parse(this.state.widgetUrl);
|
||||
let safeWidgetUrl = '';
|
||||
if (ALLOWED_APP_URL_SCHEMES.indexOf(parsedWidgetUrl.protocol) !== -1) {
|
||||
safeWidgetUrl = url.format(parsedWidgetUrl);
|
||||
}
|
||||
|
||||
if (this.props.show) {
|
||||
if (this.state.loading) {
|
||||
appTileBody = (
|
||||
<div className='mx_AppTileBody mx_AppLoading'>
|
||||
<MessageSpinner msg='Loading...'/>
|
||||
</div>
|
||||
);
|
||||
const loadingElement = (
|
||||
<div className='mx_AppTileBody mx_AppLoading'>
|
||||
<MessageSpinner msg='Loading...' />
|
||||
</div>
|
||||
);
|
||||
if (this.state.initialising) {
|
||||
appTileBody = loadingElement;
|
||||
} else if (this.state.hasPermissionToLoad == true) {
|
||||
if (this.isMixedContent()) {
|
||||
appTileBody = (
|
||||
<div className="mx_AppTileBody">
|
||||
<AppWarning
|
||||
errorMsg="Error - Mixed content"
|
||||
/>
|
||||
<AppWarning errorMsg="Error - Mixed content" />
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
appTileBody = (
|
||||
<div className="mx_AppTileBody">
|
||||
<div className={this.state.loading ? 'mx_AppTileBody mx_AppLoading' : 'mx_AppTileBody'}>
|
||||
{ this.state.loading && loadingElement }
|
||||
<iframe
|
||||
ref="appFrame"
|
||||
src={safeWidgetUrl}
|
||||
src={this._getSafeUrl()}
|
||||
allowFullScreen="true"
|
||||
sandbox={sandboxFlags}
|
||||
onLoad={this._onLoaded}
|
||||
></iframe>
|
||||
</div>
|
||||
);
|
||||
|
@ -268,39 +438,54 @@ export default React.createClass({
|
|||
// editing is done in scalar
|
||||
const showEditButton = Boolean(this._scalarClient && this._canUserModify());
|
||||
const deleteWidgetLabel = this._deleteWidgetLabel();
|
||||
let deleteIcon = 'img/cancel.svg';
|
||||
let deleteClasses = 'mx_filterFlipColor mx_AppTileMenuBarWidget';
|
||||
if(this._canUserModify()) {
|
||||
deleteIcon = 'img/cancel-red.svg';
|
||||
let deleteIcon = 'img/cancel_green.svg';
|
||||
let deleteClasses = 'mx_AppTileMenuBarWidget';
|
||||
if (this._canUserModify()) {
|
||||
deleteIcon = 'img/icon-delete-pink.svg';
|
||||
deleteClasses += ' mx_AppTileMenuBarWidgetDelete';
|
||||
}
|
||||
|
||||
const windowStateIcon = (this.props.show ? 'img/minimize.svg' : 'img/maximize.svg');
|
||||
|
||||
return (
|
||||
<div className={this.props.fullWidth ? "mx_AppTileFullWidth" : "mx_AppTile"} id={this.props.id}>
|
||||
<div ref="menu_bar" className="mx_AppTileMenuBar" onClick={this.onClickMenuBar}>
|
||||
{this.formatAppTileName()}
|
||||
<span className="mx_AppTileMenuBarTitle">
|
||||
<TintableSvgButton
|
||||
src={windowStateIcon}
|
||||
className="mx_AppTileMenuBarWidget mx_AppTileMenuBarWidgetPadding"
|
||||
title={_t('Minimize apps')}
|
||||
width="10"
|
||||
height="10"
|
||||
/>
|
||||
<b>{ this.formatAppTileName() }</b>
|
||||
{ this.state.widgetPageTitle && this.state.widgetPageTitle != this.formatAppTileName() && (
|
||||
<span> - { this.state.widgetPageTitle }</span>
|
||||
) }
|
||||
</span>
|
||||
<span className="mx_AppTileMenuBarWidgets">
|
||||
{/* Edit widget */}
|
||||
{showEditButton && <img
|
||||
src="img/edit.svg"
|
||||
className="mx_filterFlipColor mx_AppTileMenuBarWidget mx_AppTileMenuBarWidgetPadding"
|
||||
width="8" height="8"
|
||||
alt={_t('Edit')}
|
||||
{ /* Edit widget */ }
|
||||
{ showEditButton && <TintableSvgButton
|
||||
src="img/edit_green.svg"
|
||||
className="mx_AppTileMenuBarWidget mx_AppTileMenuBarWidgetPadding"
|
||||
title={_t('Edit')}
|
||||
onClick={this._onEditClick}
|
||||
/>}
|
||||
width="10"
|
||||
height="10"
|
||||
/> }
|
||||
|
||||
{/* Delete widget */}
|
||||
<img src={deleteIcon}
|
||||
className={deleteClasses}
|
||||
width="8" height="8"
|
||||
alt={_t(deleteWidgetLabel)}
|
||||
title={_t(deleteWidgetLabel)}
|
||||
onClick={this._onDeleteClick}
|
||||
{ /* Delete widget */ }
|
||||
<TintableSvgButton
|
||||
src={deleteIcon}
|
||||
className={deleteClasses}
|
||||
title={_t(deleteWidgetLabel)}
|
||||
onClick={this._onDeleteClick}
|
||||
width="10"
|
||||
height="10"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
{appTileBody}
|
||||
{ appTileBody }
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -6,10 +6,10 @@ const AppWarning = (props) => {
|
|||
return (
|
||||
<div className='mx_AppPermissionWarning'>
|
||||
<div className='mx_AppPermissionWarningImage'>
|
||||
<img src='img/warning.svg' alt={_t('Warning!')}/>
|
||||
<img src='img/warning.svg' alt={_t('Warning!')} />
|
||||
</div>
|
||||
<div className='mx_AppPermissionWarningText'>
|
||||
<span className='mx_AppPermissionWarningTextLabel'>{props.errorMsg}</span>
|
||||
<span className='mx_AppPermissionWarningTextLabel'>{ props.errorMsg }</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -24,7 +24,7 @@ const CreateRoomButton = function(props) {
|
|||
return (
|
||||
<ActionButton action="view_create_room"
|
||||
mouseOverAction={props.callout ? "callout_create_room" : null}
|
||||
label={ _t("Create new room") }
|
||||
label={_t("Create new room")}
|
||||
iconPath="img/icons-create-room.svg"
|
||||
size={props.size}
|
||||
tooltip={props.tooltip}
|
||||
|
|
85
src/components/views/elements/DNDTagTile.js
Normal file
85
src/components/views/elements/DNDTagTile.js
Normal file
|
@ -0,0 +1,85 @@
|
|||
/* eslint new-cap: "off" */
|
||||
/*
|
||||
Copyright 2017 New Vector 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.
|
||||
*/
|
||||
|
||||
import { DragSource, DropTarget } from 'react-dnd';
|
||||
|
||||
import TagTile from './TagTile';
|
||||
import dis from '../../../dispatcher';
|
||||
import { findDOMNode } from 'react-dom';
|
||||
|
||||
const tagTileSource = {
|
||||
canDrag: function(props, monitor) {
|
||||
return true;
|
||||
},
|
||||
|
||||
beginDrag: function(props) {
|
||||
// Return the data describing the dragged item
|
||||
return {
|
||||
tag: props.tag,
|
||||
};
|
||||
},
|
||||
|
||||
endDrag: function(props, monitor, component) {
|
||||
const dropResult = monitor.getDropResult();
|
||||
if (!monitor.didDrop() || !dropResult) {
|
||||
return;
|
||||
}
|
||||
props.onEndDrag();
|
||||
},
|
||||
};
|
||||
|
||||
const tagTileTarget = {
|
||||
canDrop(props, monitor) {
|
||||
return true;
|
||||
},
|
||||
|
||||
hover(props, monitor, component) {
|
||||
if (!monitor.canDrop()) return;
|
||||
const draggedY = monitor.getClientOffset().y;
|
||||
const {top, bottom} = findDOMNode(component).getBoundingClientRect();
|
||||
const targetY = (top + bottom) / 2;
|
||||
dis.dispatch({
|
||||
action: 'order_tag',
|
||||
tag: monitor.getItem().tag,
|
||||
targetTag: props.tag,
|
||||
// Note: we indicate that the tag should be after the target when
|
||||
// it's being dragged over the top half of the target.
|
||||
after: draggedY < targetY,
|
||||
});
|
||||
},
|
||||
|
||||
drop(props) {
|
||||
// Return the data to be returned by getDropResult
|
||||
return {
|
||||
tag: props.tag,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export default
|
||||
DropTarget('TagTile', tagTileTarget, (connect, monitor) => ({
|
||||
connectDropTarget: connect.dropTarget(),
|
||||
}))(DragSource('TagTile', tagTileSource, (connect, monitor) => ({
|
||||
connectDragSource: connect.dragSource(),
|
||||
}))((props) => {
|
||||
const { connectDropTarget, connectDragSource, ...otherProps } = props;
|
||||
return connectDropTarget(connectDragSource(
|
||||
<div>
|
||||
<TagTile {...otherProps} />
|
||||
</div>,
|
||||
));
|
||||
}));
|
|
@ -30,7 +30,7 @@ export default React.createClass({
|
|||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
device: this.props.device
|
||||
device: this.props.device,
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -60,37 +60,37 @@ export default React.createClass({
|
|||
|
||||
onUnverifyClick: function() {
|
||||
MatrixClientPeg.get().setDeviceVerified(
|
||||
this.props.userId, this.state.device.deviceId, false
|
||||
this.props.userId, this.state.device.deviceId, false,
|
||||
);
|
||||
},
|
||||
|
||||
onBlacklistClick: function() {
|
||||
MatrixClientPeg.get().setDeviceBlocked(
|
||||
this.props.userId, this.state.device.deviceId, true
|
||||
this.props.userId, this.state.device.deviceId, true,
|
||||
);
|
||||
},
|
||||
|
||||
onUnblacklistClick: function() {
|
||||
MatrixClientPeg.get().setDeviceBlocked(
|
||||
this.props.userId, this.state.device.deviceId, false
|
||||
this.props.userId, this.state.device.deviceId, false,
|
||||
);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var blacklistButton = null, verifyButton = null;
|
||||
let blacklistButton = null, verifyButton = null;
|
||||
|
||||
if (this.state.device.isBlocked()) {
|
||||
blacklistButton = (
|
||||
<button className="mx_MemberDeviceInfo_textButton mx_MemberDeviceInfo_unblacklist"
|
||||
onClick={this.onUnblacklistClick}>
|
||||
{_t("Unblacklist")}
|
||||
{ _t("Unblacklist") }
|
||||
</button>
|
||||
);
|
||||
} else {
|
||||
blacklistButton = (
|
||||
<button className="mx_MemberDeviceInfo_textButton mx_MemberDeviceInfo_blacklist"
|
||||
onClick={this.onBlacklistClick}>
|
||||
{_t("Blacklist")}
|
||||
{ _t("Blacklist") }
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
@ -99,14 +99,14 @@ export default React.createClass({
|
|||
verifyButton = (
|
||||
<button className="mx_MemberDeviceInfo_textButton mx_MemberDeviceInfo_unverify"
|
||||
onClick={this.onUnverifyClick}>
|
||||
{_t("Unverify")}
|
||||
{ _t("Unverify") }
|
||||
</button>
|
||||
);
|
||||
} else {
|
||||
verifyButton = (
|
||||
<button className="mx_MemberDeviceInfo_textButton mx_MemberDeviceInfo_verify"
|
||||
onClick={this.onVerifyClick}>
|
||||
{_t("Verify...")}
|
||||
{ _t("Verify...") }
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -95,7 +95,7 @@ export default class DirectorySearchBox extends React.Component {
|
|||
onChange={this._onChange} onKeyUp={this._onKeyUp}
|
||||
placeholder={this.props.placeholder} autoFocus
|
||||
/>
|
||||
{join_button}
|
||||
{ join_button }
|
||||
<span className="mx_DirectorySearchBox_clear_wrapper">
|
||||
<span className="mx_DirectorySearchBox_clear" onClick={this._onClearClick} />
|
||||
</span>
|
||||
|
|
|
@ -26,6 +26,10 @@ class MenuOption extends React.Component {
|
|||
this._onClick = this._onClick.bind(this);
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
disabled: false,
|
||||
};
|
||||
|
||||
_onMouseEnter() {
|
||||
this.props.onMouseEnter(this.props.dropdownKey);
|
||||
}
|
||||
|
@ -46,15 +50,15 @@ class MenuOption extends React.Component {
|
|||
onClick={this._onClick} onKeyPress={this._onKeyPress}
|
||||
onMouseEnter={this._onMouseEnter}
|
||||
>
|
||||
{this.props.children}
|
||||
</div>
|
||||
{ this.props.children }
|
||||
</div>;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
MenuOption.propTypes = {
|
||||
children: React.PropTypes.oneOfType([
|
||||
React.PropTypes.arrayOf(React.PropTypes.node),
|
||||
React.PropTypes.node
|
||||
React.PropTypes.node,
|
||||
]),
|
||||
highlighted: React.PropTypes.bool,
|
||||
dropdownKey: React.PropTypes.string,
|
||||
|
@ -153,6 +157,8 @@ export default class Dropdown extends React.Component {
|
|||
}
|
||||
|
||||
_onInputClick(ev) {
|
||||
if (this.props.disabled) return;
|
||||
|
||||
if (!this.state.expanded) {
|
||||
this.setState({
|
||||
expanded: true,
|
||||
|
@ -250,13 +256,13 @@ export default class Dropdown extends React.Component {
|
|||
onMouseEnter={this._setHighlightedOption}
|
||||
onClick={this._onMenuOptionClick}
|
||||
>
|
||||
{child}
|
||||
{ child }
|
||||
</MenuOption>
|
||||
);
|
||||
});
|
||||
if (options.length === 0) {
|
||||
return [<div key="0" className="mx_Dropdown_option">
|
||||
{_t("No results")}
|
||||
{ _t("No results") }
|
||||
</div>];
|
||||
}
|
||||
return options;
|
||||
|
@ -279,7 +285,7 @@ export default class Dropdown extends React.Component {
|
|||
/>;
|
||||
}
|
||||
menu = <div className="mx_Dropdown_menu" style={menuStyle}>
|
||||
{this._getMenuOptions()}
|
||||
{ this._getMenuOptions() }
|
||||
</div>;
|
||||
}
|
||||
|
||||
|
@ -288,12 +294,13 @@ export default class Dropdown extends React.Component {
|
|||
this.props.getShortOption(this.props.value) :
|
||||
this.childrenByKey[this.props.value];
|
||||
currentValue = <div className="mx_Dropdown_option">
|
||||
{selectedChild}
|
||||
</div>
|
||||
{ selectedChild }
|
||||
</div>;
|
||||
}
|
||||
|
||||
const dropdownClasses = {
|
||||
mx_Dropdown: true,
|
||||
mx_Dropdown_disabled: this.props.disabled,
|
||||
};
|
||||
if (this.props.className) {
|
||||
dropdownClasses[this.props.className] = true;
|
||||
|
@ -303,9 +310,9 @@ export default class Dropdown extends React.Component {
|
|||
// to the input, but overflows below it. The root contains both.
|
||||
return <div className={classnames(dropdownClasses)} ref={this._collectRoot}>
|
||||
<AccessibleButton className="mx_Dropdown_input" onClick={this._onInputClick}>
|
||||
{currentValue}
|
||||
{ currentValue }
|
||||
<span className="mx_Dropdown_arrow"></span>
|
||||
{menu}
|
||||
{ menu }
|
||||
</AccessibleButton>
|
||||
</div>;
|
||||
}
|
||||
|
@ -329,4 +336,6 @@ Dropdown.propTypes = {
|
|||
// in the dropped-down menu.
|
||||
getShortOption: React.PropTypes.func,
|
||||
value: React.PropTypes.string,
|
||||
}
|
||||
// negative for consistency with HTML
|
||||
disabled: React.PropTypes.bool,
|
||||
};
|
||||
|
|
153
src/components/views/elements/EditableItemList.js
Normal file
153
src/components/views/elements/EditableItemList.js
Normal file
|
@ -0,0 +1,153 @@
|
|||
/*
|
||||
Copyright 2017 New Vector 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.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import sdk from '../../../index';
|
||||
import {_t} from '../../../languageHandler.js';
|
||||
|
||||
const EditableItem = React.createClass({
|
||||
displayName: 'EditableItem',
|
||||
|
||||
propTypes: {
|
||||
initialValue: PropTypes.string,
|
||||
index: PropTypes.number,
|
||||
placeholder: PropTypes.string,
|
||||
|
||||
onChange: PropTypes.func,
|
||||
onRemove: PropTypes.func,
|
||||
onAdd: PropTypes.func,
|
||||
|
||||
addOnChange: PropTypes.bool,
|
||||
},
|
||||
|
||||
onChange: function(value) {
|
||||
this.setState({ value });
|
||||
if (this.props.onChange) this.props.onChange(value, this.props.index);
|
||||
if (this.props.addOnChange && this.props.onAdd) this.props.onAdd(value);
|
||||
},
|
||||
|
||||
onRemove: function() {
|
||||
if (this.props.onRemove) this.props.onRemove(this.props.index);
|
||||
},
|
||||
|
||||
onAdd: function() {
|
||||
if (this.props.onAdd) this.props.onAdd(this.state.value);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const EditableText = sdk.getComponent('elements.EditableText');
|
||||
return <div className="mx_EditableItem">
|
||||
<EditableText
|
||||
className="mx_EditableItem_editable"
|
||||
placeholderClassName="mx_EditableItem_editablePlaceholder"
|
||||
placeholder={this.props.placeholder}
|
||||
blurToCancel={false}
|
||||
editable={true}
|
||||
initialValue={this.props.initialValue}
|
||||
onValueChanged={this.onChange} />
|
||||
{ this.props.onAdd ?
|
||||
<div className="mx_EditableItem_addButton">
|
||||
<img className="mx_filterFlipColor"
|
||||
src="img/plus.svg" width="14" height="14"
|
||||
alt={_t("Add")} onClick={this.onAdd} />
|
||||
</div>
|
||||
:
|
||||
<div className="mx_EditableItem_removeButton">
|
||||
<img className="mx_filterFlipColor"
|
||||
src="img/cancel-small.svg" width="14" height="14"
|
||||
alt={_t("Delete")} onClick={this.onRemove} />
|
||||
</div>
|
||||
}
|
||||
</div>;
|
||||
},
|
||||
});
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'EditableItemList',
|
||||
|
||||
propTypes: {
|
||||
items: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
onNewItemChanged: PropTypes.func,
|
||||
onItemAdded: PropTypes.func,
|
||||
onItemEdited: PropTypes.func,
|
||||
onItemRemoved: PropTypes.func,
|
||||
|
||||
canEdit: PropTypes.bool,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
onItemAdded: () => {},
|
||||
onItemEdited: () => {},
|
||||
onItemRemoved: () => {},
|
||||
onNewItemChanged: () => {},
|
||||
};
|
||||
},
|
||||
|
||||
onItemAdded: function(value) {
|
||||
this.props.onItemAdded(value);
|
||||
},
|
||||
|
||||
onItemEdited: function(value, index) {
|
||||
if (value.length === 0) {
|
||||
this.onItemRemoved(index);
|
||||
} else {
|
||||
this.props.onItemEdited(value, index);
|
||||
}
|
||||
},
|
||||
|
||||
onItemRemoved: function(index) {
|
||||
this.props.onItemRemoved(index);
|
||||
},
|
||||
|
||||
onNewItemChanged: function(value) {
|
||||
this.props.onNewItemChanged(value);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const editableItems = this.props.items.map((item, index) => {
|
||||
return <EditableItem
|
||||
key={index}
|
||||
index={index}
|
||||
initialValue={item}
|
||||
onChange={this.onItemEdited}
|
||||
onRemove={this.onItemRemoved}
|
||||
placeholder={this.props.placeholder}
|
||||
/>;
|
||||
});
|
||||
|
||||
const label = this.props.items.length > 0 ?
|
||||
this.props.itemsLabel : this.props.noItemsLabel;
|
||||
|
||||
return (<div className="mx_EditableItemList">
|
||||
<div className="mx_EditableItemList_label">
|
||||
{ label }
|
||||
</div>
|
||||
{ editableItems }
|
||||
{ this.props.canEdit ?
|
||||
<EditableItem
|
||||
key={-1}
|
||||
initialValue={this.props.newItem}
|
||||
onAdd={this.onItemAdded}
|
||||
onChange={this.onNewItemChanged}
|
||||
addOnChange={true}
|
||||
placeholder={this.props.placeholder}
|
||||
/> : <div />
|
||||
}
|
||||
</div>);
|
||||
},
|
||||
});
|
|
@ -16,7 +16,7 @@ limitations under the License.
|
|||
|
||||
'use strict';
|
||||
|
||||
var React = require('react');
|
||||
const React = require('react');
|
||||
|
||||
const KEY_TAB = 9;
|
||||
const KEY_SHIFT = 16;
|
||||
|
@ -65,7 +65,9 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
componentWillReceiveProps: function(nextProps) {
|
||||
if (nextProps.initialValue !== this.props.initialValue) {
|
||||
if (nextProps.initialValue !== this.props.initialValue ||
|
||||
nextProps.initialValue !== this.value
|
||||
) {
|
||||
this.value = nextProps.initialValue;
|
||||
if (this.refs.editable_div) {
|
||||
this.showPlaceholder(!this.value);
|
||||
|
@ -93,8 +95,7 @@ module.exports = React.createClass({
|
|||
this.refs.editable_div.setAttribute("class", this.props.className + " " + this.props.placeholderClassName);
|
||||
this.placeholder = true;
|
||||
this.value = '';
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
this.refs.editable_div.textContent = this.value;
|
||||
this.refs.editable_div.setAttribute("class", this.props.className);
|
||||
this.placeholder = false;
|
||||
|
@ -150,8 +151,7 @@ module.exports = React.createClass({
|
|||
|
||||
if (!ev.target.textContent) {
|
||||
this.showPlaceholder(true);
|
||||
}
|
||||
else if (!this.placeholder) {
|
||||
} else if (!this.placeholder) {
|
||||
this.value = ev.target.textContent;
|
||||
}
|
||||
|
||||
|
@ -175,21 +175,21 @@ module.exports = React.createClass({
|
|||
onFocus: function(ev) {
|
||||
//ev.target.setSelectionRange(0, ev.target.textContent.length);
|
||||
|
||||
var node = ev.target.childNodes[0];
|
||||
const node = ev.target.childNodes[0];
|
||||
if (node) {
|
||||
var range = document.createRange();
|
||||
const range = document.createRange();
|
||||
range.setStart(node, 0);
|
||||
range.setEnd(node, node.length);
|
||||
|
||||
var sel = window.getSelection();
|
||||
const sel = window.getSelection();
|
||||
sel.removeAllRanges();
|
||||
sel.addRange(range);
|
||||
}
|
||||
},
|
||||
|
||||
onFinish: function(ev, shouldSubmit) {
|
||||
var self = this;
|
||||
var submit = (ev.key === "Enter") || shouldSubmit;
|
||||
const self = this;
|
||||
const submit = (ev.key === "Enter") || shouldSubmit;
|
||||
this.setState({
|
||||
phase: this.Phases.Display,
|
||||
}, function() {
|
||||
|
@ -200,19 +200,16 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
onBlur: function(ev) {
|
||||
var sel = window.getSelection();
|
||||
const sel = window.getSelection();
|
||||
sel.removeAllRanges();
|
||||
|
||||
if (this.props.blurToCancel)
|
||||
{this.cancelEdit();}
|
||||
else
|
||||
{this.onFinish(ev, this.props.blurToSubmit);}
|
||||
if (this.props.blurToCancel) {this.cancelEdit();} else {this.onFinish(ev, this.props.blurToSubmit);}
|
||||
|
||||
this.showPlaceholder(!this.value);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var editable_el;
|
||||
let editable_el;
|
||||
|
||||
if (!this.props.editable || (this.state.phase == this.Phases.Display && (this.props.label || this.props.labelClassName) && !this.value)) {
|
||||
// show the label
|
||||
|
@ -224,5 +221,5 @@ module.exports = React.createClass({
|
|||
}
|
||||
|
||||
return editable_el;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -64,7 +64,7 @@ export default class EditableTextContainer extends React.Component {
|
|||
errorString: error.toString(),
|
||||
busy: false,
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -96,22 +96,22 @@ export default class EditableTextContainer extends React.Component {
|
|||
errorString: error.toString(),
|
||||
busy: false,
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.busy) {
|
||||
var Loader = sdk.getComponent("elements.Spinner");
|
||||
const Loader = sdk.getComponent("elements.Spinner");
|
||||
return (
|
||||
<Loader />
|
||||
);
|
||||
} else if (this.state.errorString) {
|
||||
return (
|
||||
<div className="error">{this.state.errorString}</div>
|
||||
<div className="error">{ this.state.errorString }</div>
|
||||
);
|
||||
} else {
|
||||
var EditableText = sdk.getComponent('elements.EditableText');
|
||||
const EditableText = sdk.getComponent('elements.EditableText');
|
||||
return (
|
||||
<EditableText initialValue={this.state.value}
|
||||
placeholder={this.props.placeholder}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2016 Aviral Dasgupta
|
||||
Copyright 2017 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -15,12 +16,19 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import {emojifyText} from '../../../HtmlUtils';
|
||||
import {emojifyText, containsEmoji} from '../../../HtmlUtils';
|
||||
|
||||
export default function EmojiText(props) {
|
||||
const {element, children, ...restProps} = props;
|
||||
restProps.dangerouslySetInnerHTML = emojifyText(children);
|
||||
return React.createElement(element, restProps);
|
||||
|
||||
// fast path: simple regex to detect strings that don't contain
|
||||
// emoji and just return them
|
||||
if (containsEmoji(children)) {
|
||||
restProps.dangerouslySetInnerHTML = emojifyText(children);
|
||||
return React.createElement(element, restProps);
|
||||
} else {
|
||||
return React.createElement(element, restProps, children);
|
||||
}
|
||||
}
|
||||
|
||||
EmojiText.propTypes = {
|
||||
|
|
138
src/components/views/elements/Flair.js
Normal file
138
src/components/views/elements/Flair.js
Normal file
|
@ -0,0 +1,138 @@
|
|||
/*
|
||||
Copyright 2017 New Vector 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';
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {MatrixClient} from 'matrix-js-sdk';
|
||||
import FlairStore from '../../../stores/FlairStore';
|
||||
import dis from '../../../dispatcher';
|
||||
|
||||
|
||||
class FlairAvatar extends React.Component {
|
||||
constructor() {
|
||||
super();
|
||||
this.onClick = this.onClick.bind(this);
|
||||
}
|
||||
|
||||
onClick(ev) {
|
||||
ev.preventDefault();
|
||||
// Don't trigger onClick of parent element
|
||||
ev.stopPropagation();
|
||||
dis.dispatch({
|
||||
action: 'view_group',
|
||||
group_id: this.props.groupProfile.groupId,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const httpUrl = this.context.matrixClient.mxcUrlToHttp(
|
||||
this.props.groupProfile.avatarUrl, 16, 16, 'scale', false);
|
||||
const tooltip = this.props.groupProfile.name ?
|
||||
`${this.props.groupProfile.name} (${this.props.groupProfile.groupId})`:
|
||||
this.props.groupProfile.groupId;
|
||||
return <img
|
||||
src={httpUrl}
|
||||
width="16"
|
||||
height="16"
|
||||
onClick={this.onClick}
|
||||
title={tooltip} />;
|
||||
}
|
||||
}
|
||||
|
||||
FlairAvatar.propTypes = {
|
||||
groupProfile: PropTypes.shape({
|
||||
groupId: PropTypes.string.isRequired,
|
||||
name: PropTypes.string,
|
||||
avatarUrl: PropTypes.string.isRequired,
|
||||
}),
|
||||
};
|
||||
|
||||
FlairAvatar.contextTypes = {
|
||||
matrixClient: React.PropTypes.instanceOf(MatrixClient).isRequired,
|
||||
};
|
||||
|
||||
export default class Flair extends React.Component {
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {
|
||||
profiles: [],
|
||||
};
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this._unmounted = true;
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this._unmounted = false;
|
||||
this._generateAvatars(this.props.groups);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(newProps) {
|
||||
this._generateAvatars(newProps.groups);
|
||||
}
|
||||
|
||||
async _getGroupProfiles(groups) {
|
||||
const profiles = [];
|
||||
for (const groupId of groups) {
|
||||
let groupProfile = null;
|
||||
try {
|
||||
groupProfile = await FlairStore.getGroupProfileCached(this.context.matrixClient, groupId);
|
||||
} catch (err) {
|
||||
console.error('Could not get profile for group', groupId, err);
|
||||
}
|
||||
profiles.push(groupProfile);
|
||||
}
|
||||
return profiles.filter((p) => p !== null);
|
||||
}
|
||||
|
||||
async _generateAvatars(groups) {
|
||||
if (!groups || groups.length === 0) {
|
||||
return;
|
||||
}
|
||||
const profiles = await this._getGroupProfiles(groups);
|
||||
if (!this.unmounted) {
|
||||
this.setState({profiles: profiles.filter((profile) => {return profile.avatarUrl;})});
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.profiles.length === 0) {
|
||||
return <div />;
|
||||
}
|
||||
const avatars = this.state.profiles.map((profile, index) => {
|
||||
return <FlairAvatar key={index} groupProfile={profile} />;
|
||||
});
|
||||
return (
|
||||
<span className="mx_Flair">
|
||||
{ avatars }
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Flair.propTypes = {
|
||||
groups: PropTypes.arrayOf(PropTypes.string),
|
||||
};
|
||||
|
||||
// TODO: We've decided that all components should follow this pattern, which means removing withMatrixClient and using
|
||||
// this.context.matrixClient everywhere instead of this.props.matrixClient.
|
||||
// See https://github.com/vector-im/riot-web/issues/4951.
|
||||
Flair.contextTypes = {
|
||||
matrixClient: React.PropTypes.instanceOf(MatrixClient).isRequired,
|
||||
};
|
39
src/components/views/elements/GroupsButton.js
Normal file
39
src/components/views/elements/GroupsButton.js
Normal file
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
Copyright 2017 New Vector 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.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import sdk from '../../../index';
|
||||
import PropTypes from 'prop-types';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
const GroupsButton = function(props) {
|
||||
const ActionButton = sdk.getComponent('elements.ActionButton');
|
||||
return (
|
||||
<ActionButton action="view_my_groups"
|
||||
label={_t("Communities")}
|
||||
iconPath="img/icons-groups.svg"
|
||||
size={props.size}
|
||||
tooltip={props.tooltip}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
GroupsButton.propTypes = {
|
||||
size: PropTypes.string,
|
||||
tooltip: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default GroupsButton;
|
|
@ -23,7 +23,7 @@ const HomeButton = function(props) {
|
|||
const ActionButton = sdk.getComponent('elements.ActionButton');
|
||||
return (
|
||||
<ActionButton action="view_home_page"
|
||||
label={ _t("Home") }
|
||||
label={_t("Home")}
|
||||
iconPath="img/icons-home.svg"
|
||||
size={props.size}
|
||||
tooltip={props.tooltip}
|
||||
|
|
|
@ -18,8 +18,8 @@ limitations under the License.
|
|||
import React from 'react';
|
||||
|
||||
import sdk from '../../../index';
|
||||
import UserSettingsStore from '../../../UserSettingsStore';
|
||||
import * as languageHandler from '../../../languageHandler';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
|
||||
function languageMatchesSearchQuery(query, language) {
|
||||
if (language.label.toUpperCase().indexOf(query.toUpperCase()) == 0) return true;
|
||||
|
@ -35,14 +35,14 @@ export default class LanguageDropdown extends React.Component {
|
|||
this.state = {
|
||||
searchQuery: '',
|
||||
langs: null,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
languageHandler.getAllLanguagesFromJson().then((langs) => {
|
||||
langs.sort(function(a, b){
|
||||
if(a.label < b.label) return -1;
|
||||
if(a.label > b.label) return 1;
|
||||
langs.sort(function(a, b) {
|
||||
if (a.label < b.label) return -1;
|
||||
if (a.label > b.label) return 1;
|
||||
return 0;
|
||||
});
|
||||
this.setState({langs});
|
||||
|
@ -54,10 +54,10 @@ export default class LanguageDropdown extends React.Component {
|
|||
// If no value is given, we start with the first
|
||||
// country selected, but our parent component
|
||||
// doesn't know this, therefore we do this.
|
||||
const _localSettings = UserSettingsStore.getLocalSettings();
|
||||
if (_localSettings.hasOwnProperty('language')) {
|
||||
this.props.onOptionChange(_localSettings.language);
|
||||
}else {
|
||||
const language = SettingsStore.getValue("language", null, /*excludeDefault:*/true);
|
||||
if (language) {
|
||||
this.props.onOptionChange(language);
|
||||
} else {
|
||||
const language = languageHandler.normalizeLanguageKey(languageHandler.getLanguageFromBrowser());
|
||||
this.props.onOptionChange(language);
|
||||
}
|
||||
|
@ -89,18 +89,18 @@ export default class LanguageDropdown extends React.Component {
|
|||
|
||||
const options = displayedLanguages.map((language) => {
|
||||
return <div key={language.value}>
|
||||
{language.label}
|
||||
{ language.label }
|
||||
</div>;
|
||||
});
|
||||
|
||||
// default value here too, otherwise we need to handle null / undefined
|
||||
// values between mounting and the initial value propgating
|
||||
let language = SettingsStore.getValue("language", null, /*excludeDefault:*/true);
|
||||
let value = null;
|
||||
const _localSettings = UserSettingsStore.getLocalSettings();
|
||||
if (_localSettings.hasOwnProperty('language')) {
|
||||
value = this.props.value || _localSettings.language;
|
||||
if (language) {
|
||||
value = this.props.value || language;
|
||||
} else {
|
||||
const language = navigator.language || navigator.userLanguage;
|
||||
language = navigator.language || navigator.userLanguage;
|
||||
value = this.props.value || language;
|
||||
}
|
||||
|
||||
|
@ -108,8 +108,8 @@ export default class LanguageDropdown extends React.Component {
|
|||
onOptionChange={this.props.onOptionChange} onSearchChange={this._onSearchChange}
|
||||
searchEnabled={true} value={value}
|
||||
>
|
||||
{options}
|
||||
</Dropdown>
|
||||
{ options }
|
||||
</Dropdown>;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -17,6 +17,7 @@ limitations under the License.
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import sdk from '../../../index';
|
||||
import classNames from 'classnames';
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
import ScalarAuthClient from '../../../ScalarAuthClient';
|
||||
import ScalarMessaging from '../../../ScalarMessaging';
|
||||
|
@ -31,11 +32,9 @@ export default class ManageIntegsButton extends React.Component {
|
|||
|
||||
this.state = {
|
||||
scalarError: null,
|
||||
showIntegrationsError: false,
|
||||
};
|
||||
|
||||
this.onManageIntegrations = this.onManageIntegrations.bind(this);
|
||||
this.onShowIntegrationsError = this.onShowIntegrationsError.bind(this);
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
|
@ -48,7 +47,7 @@ export default class ManageIntegsButton extends React.Component {
|
|||
this.forceUpdate();
|
||||
}, (err) => {
|
||||
this.setState({ scalarError: err});
|
||||
console.error(err);
|
||||
console.error('Error whilst initialising scalarClient for ManageIntegsButton', err);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -59,6 +58,9 @@ export default class ManageIntegsButton extends React.Component {
|
|||
|
||||
onManageIntegrations(ev) {
|
||||
ev.preventDefault();
|
||||
if (this.state.scalarError && !this.scalarClient.hasCredentials()) {
|
||||
return;
|
||||
}
|
||||
const IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager");
|
||||
Modal.createDialog(IntegrationsManager, {
|
||||
src: (this.scalarClient !== null && this.scalarClient.hasCredentials()) ?
|
||||
|
@ -67,45 +69,33 @@ export default class ManageIntegsButton extends React.Component {
|
|||
}, "mx_IntegrationsManager");
|
||||
}
|
||||
|
||||
onShowIntegrationsError(ev) {
|
||||
ev.preventDefault();
|
||||
this.setState({
|
||||
showIntegrationsError: !this.state.showIntegrationsError,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
let integrationsButton = <div />;
|
||||
let integrationsError;
|
||||
let integrationsWarningTriangle = <div />;
|
||||
let integrationsErrorPopup = <div />;
|
||||
if (this.scalarClient !== null) {
|
||||
if (this.state.showIntegrationsError && this.state.scalarError) {
|
||||
integrationsError = (
|
||||
const integrationsButtonClasses = classNames({
|
||||
mx_RoomHeader_button: true,
|
||||
mx_RoomSettings_integrationsButton_error: !!this.state.scalarError,
|
||||
});
|
||||
|
||||
if (this.state.scalarError && !this.scalarClient.hasCredentials()) {
|
||||
integrationsWarningTriangle = <img src="img/warning.svg" title={_t('Integrations Error')} width="17" />;
|
||||
// Popup shown when hovering over integrationsButton_error (via CSS)
|
||||
integrationsErrorPopup = (
|
||||
<span className="mx_RoomSettings_integrationsButton_errorPopup">
|
||||
{ _t('Could not connect to the integration server') }
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
if (this.scalarClient.hasCredentials()) {
|
||||
integrationsButton = (
|
||||
<AccessibleButton className="mx_RoomHeader_button" onClick={this.onManageIntegrations} title={ _t('Manage Integrations') }>
|
||||
<TintableSvg src="img/icons-apps.svg" width="35" height="35"/>
|
||||
</AccessibleButton>
|
||||
);
|
||||
} else if (this.state.scalarError) {
|
||||
integrationsButton = (
|
||||
<div className="mx_RoomSettings_integrationsButton_error" onClick={ this.onShowIntegrationsError }>
|
||||
<img src="img/warning.svg" title={_t('Integrations Error')} width="17"/>
|
||||
{ integrationsError }
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
integrationsButton = (
|
||||
<AccessibleButton className="mx_RoomHeader_button" onClick={this.onManageIntegrations} title={ _t('Manage Integrations') }>
|
||||
<TintableSvg src="img/icons-apps.svg" width="35" height="35"/>
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
integrationsButton = (
|
||||
<AccessibleButton className={integrationsButtonClasses} onClick={this.onManageIntegrations} title={_t('Manage Integrations')}>
|
||||
<TintableSvg src="img/icons-apps.svg" width="35" height="35" />
|
||||
{ integrationsWarningTriangle }
|
||||
{ integrationsErrorPopup }
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
|
||||
return integrationsButton;
|
||||
|
|
|
@ -34,11 +34,13 @@ module.exports = React.createClass({
|
|||
threshold: React.PropTypes.number,
|
||||
// Called when the MELS expansion is toggled
|
||||
onToggle: React.PropTypes.func,
|
||||
// Whether or not to begin with state.expanded=true
|
||||
startExpanded: React.PropTypes.bool,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
expanded: false,
|
||||
expanded: Boolean(this.props.startExpanded),
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -84,7 +86,6 @@ module.exports = React.createClass({
|
|||
const summaries = orderedTransitionSequences.map((transitions) => {
|
||||
const userNames = eventAggregates[transitions];
|
||||
const nameList = this._renderNameList(userNames);
|
||||
const plural = userNames.length > 1;
|
||||
|
||||
const splitTransitions = transitions.split(',');
|
||||
|
||||
|
@ -94,18 +95,18 @@ module.exports = React.createClass({
|
|||
// Transform into consecutive repetitions of the same transition (like 5
|
||||
// consecutive 'joined_and_left's)
|
||||
const coalescedTransitions = this._coalesceRepeatedTransitions(
|
||||
canonicalTransitions
|
||||
canonicalTransitions,
|
||||
);
|
||||
|
||||
const descs = coalescedTransitions.map((t) => {
|
||||
return this._getDescriptionForTransition(
|
||||
t.transitionType, plural, t.repeats
|
||||
t.transitionType, userNames.length, t.repeats,
|
||||
);
|
||||
});
|
||||
|
||||
const desc = this._renderCommaSeparatedList(descs);
|
||||
|
||||
return nameList + " " + desc;
|
||||
return _t('%(nameList)s %(transitionList)s', { nameList: nameList, transitionList: desc });
|
||||
});
|
||||
|
||||
if (!summaries) {
|
||||
|
@ -117,7 +118,7 @@ module.exports = React.createClass({
|
|||
return (
|
||||
<span className="mx_TextualEvent mx_MemberEventListSummary_summary">
|
||||
<EmojiText>
|
||||
{summaries.join(", ")}
|
||||
{ summaries.join(", ") }
|
||||
</EmojiText>
|
||||
</span>
|
||||
);
|
||||
|
@ -206,148 +207,75 @@ module.exports = React.createClass({
|
|||
* For a certain transition, t, describe what happened to the users that
|
||||
* underwent the transition.
|
||||
* @param {string} t the transition type.
|
||||
* @param {boolean} plural whether there were multiple users undergoing the same
|
||||
* transition.
|
||||
* @param {integer} userCount number of usernames
|
||||
* @param {number} repeats the number of times the transition was repeated in a row.
|
||||
* @returns {string} the written Human Readable equivalent of the transition.
|
||||
*/
|
||||
_getDescriptionForTransition(t, plural, repeats) {
|
||||
_getDescriptionForTransition(t, userCount, repeats) {
|
||||
// The empty interpolations 'severalUsers' and 'oneUser'
|
||||
// are there only to show translators to non-English languages
|
||||
// that the verb is conjugated to plural or singular Subject.
|
||||
let res = null;
|
||||
switch(t) {
|
||||
switch (t) {
|
||||
case "joined":
|
||||
if (repeats > 1) {
|
||||
res = (plural)
|
||||
? _t("%(severalUsers)sjoined %(repeats)s times", { severalUsers: "", repeats: repeats })
|
||||
: _t("%(oneUser)sjoined %(repeats)s times", { oneUser: "", repeats: repeats });
|
||||
} else {
|
||||
res = (plural)
|
||||
? _t("%(severalUsers)sjoined", { severalUsers: "" })
|
||||
: _t("%(oneUser)sjoined", { oneUser: "" });
|
||||
}
|
||||
res = (userCount > 1)
|
||||
? _t("%(severalUsers)sjoined %(count)s times", { severalUsers: "", count: repeats })
|
||||
: _t("%(oneUser)sjoined %(count)s times", { oneUser: "", count: repeats });
|
||||
break;
|
||||
case "left":
|
||||
if (repeats > 1) {
|
||||
res = (plural)
|
||||
? _t("%(severalUsers)sleft %(repeats)s times", { severalUsers: "", repeats: repeats })
|
||||
: _t("%(oneUser)sleft %(repeats)s times", { oneUser: "", repeats: repeats });
|
||||
} else {
|
||||
res = (plural)
|
||||
? _t("%(severalUsers)sleft", { severalUsers: "" })
|
||||
: _t("%(oneUser)sleft", { oneUser: "" });
|
||||
}
|
||||
break;
|
||||
res = (userCount > 1)
|
||||
? _t("%(severalUsers)sleft %(count)s times", { severalUsers: "", count: repeats })
|
||||
: _t("%(oneUser)sleft %(count)s times", { oneUser: "", count: repeats });
|
||||
break;
|
||||
case "joined_and_left":
|
||||
if (repeats > 1) {
|
||||
res = (plural)
|
||||
? _t("%(severalUsers)sjoined and left %(repeats)s times", { severalUsers: "", repeats: repeats })
|
||||
: _t("%(oneUser)sjoined and left %(repeats)s times", { oneUser: "", repeats: repeats });
|
||||
} else {
|
||||
res = (plural)
|
||||
? _t("%(severalUsers)sjoined and left", { severalUsers: "" })
|
||||
: _t("%(oneUser)sjoined and left", { oneUser: "" });
|
||||
}
|
||||
res = (userCount > 1)
|
||||
? _t("%(severalUsers)sjoined and left %(count)s times", { severalUsers: "", count: repeats })
|
||||
: _t("%(oneUser)sjoined and left %(count)s times", { oneUser: "", count: repeats });
|
||||
break;
|
||||
case "left_and_joined":
|
||||
if (repeats > 1) {
|
||||
res = (plural)
|
||||
? _t("%(severalUsers)sleft and rejoined %(repeats)s times", { severalUsers: "", repeats: repeats })
|
||||
: _t("%(oneUser)sleft and rejoined %(repeats)s times", { oneUser: "", repeats: repeats });
|
||||
} else {
|
||||
res = (plural)
|
||||
? _t("%(severalUsers)sleft and rejoined", { severalUsers: "" })
|
||||
: _t("%(oneUser)sleft and rejoined", { oneUser: "" });
|
||||
}
|
||||
res = (userCount > 1)
|
||||
? _t("%(severalUsers)sleft and rejoined %(count)s times", { severalUsers: "", count: repeats })
|
||||
: _t("%(oneUser)sleft and rejoined %(count)s times", { oneUser: "", count: repeats });
|
||||
break;
|
||||
case "invite_reject":
|
||||
if (repeats > 1) {
|
||||
res = (plural)
|
||||
? _t("%(severalUsers)srejected their invitations %(repeats)s times", { severalUsers: "", repeats: repeats })
|
||||
: _t("%(oneUser)srejected their invitation %(repeats)s times", { oneUser: "", repeats: repeats });
|
||||
} else {
|
||||
res = (plural)
|
||||
? _t("%(severalUsers)srejected their invitations", { severalUsers: "" })
|
||||
: _t("%(oneUser)srejected their invitation", { oneUser: "" });
|
||||
}
|
||||
res = (userCount > 1)
|
||||
? _t("%(severalUsers)srejected their invitations %(count)s times", { severalUsers: "", count: repeats })
|
||||
: _t("%(oneUser)srejected their invitation %(count)s times", { oneUser: "", count: repeats });
|
||||
break;
|
||||
case "invite_withdrawal":
|
||||
if (repeats > 1) {
|
||||
res = (plural)
|
||||
? _t("%(severalUsers)shad their invitations withdrawn %(repeats)s times", { severalUsers: "", repeats: repeats })
|
||||
: _t("%(oneUser)shad their invitation withdrawn %(repeats)s times", { oneUser: "", repeats: repeats });
|
||||
} else {
|
||||
res = (plural)
|
||||
? _t("%(severalUsers)shad their invitations withdrawn", { severalUsers: "" })
|
||||
: _t("%(oneUser)shad their invitation withdrawn", { oneUser: "" });
|
||||
}
|
||||
res = (userCount > 1)
|
||||
? _t("%(severalUsers)shad their invitations withdrawn %(count)s times", { severalUsers: "", count: repeats })
|
||||
: _t("%(oneUser)shad their invitation withdrawn %(count)s times", { oneUser: "", count: repeats });
|
||||
break;
|
||||
case "invited":
|
||||
if (repeats > 1) {
|
||||
res = (plural)
|
||||
? _t("were invited %(repeats)s times", { repeats: repeats })
|
||||
: _t("was invited %(repeats)s times", { repeats: repeats });
|
||||
} else {
|
||||
res = (plural)
|
||||
? _t("were invited")
|
||||
: _t("was invited");
|
||||
}
|
||||
res = (userCount > 1)
|
||||
? _t("were invited %(count)s times", { count: repeats })
|
||||
: _t("was invited %(count)s times", { count: repeats });
|
||||
break;
|
||||
case "banned":
|
||||
if (repeats > 1) {
|
||||
res = (plural)
|
||||
? _t("were banned %(repeats)s times", { repeats: repeats })
|
||||
: _t("was banned %(repeats)s times", { repeats: repeats });
|
||||
} else {
|
||||
res = (plural)
|
||||
? _t("were banned")
|
||||
: _t("was banned");
|
||||
}
|
||||
res = (userCount > 1)
|
||||
? _t("were banned %(count)s times", { count: repeats })
|
||||
: _t("was banned %(count)s times", { count: repeats });
|
||||
break;
|
||||
case "unbanned":
|
||||
if (repeats > 1) {
|
||||
res = (plural)
|
||||
? _t("were unbanned %(repeats)s times", { repeats: repeats })
|
||||
: _t("was unbanned %(repeats)s times", { repeats: repeats });
|
||||
} else {
|
||||
res = (plural)
|
||||
? _t("were unbanned")
|
||||
: _t("was unbanned");
|
||||
}
|
||||
res = (userCount > 1)
|
||||
? _t("were unbanned %(count)s times", { count: repeats })
|
||||
: _t("was unbanned %(count)s times", { count: repeats });
|
||||
break;
|
||||
case "kicked":
|
||||
if (repeats > 1) {
|
||||
res = (plural)
|
||||
? _t("were kicked %(repeats)s times", { repeats: repeats })
|
||||
: _t("was kicked %(repeats)s times", { repeats: repeats });
|
||||
} else {
|
||||
res = (plural)
|
||||
? _t("were kicked")
|
||||
: _t("was kicked");
|
||||
}
|
||||
res = (userCount > 1)
|
||||
? _t("were kicked %(count)s times", { count: repeats })
|
||||
: _t("was kicked %(count)s times", { count: repeats });
|
||||
break;
|
||||
case "changed_name":
|
||||
if (repeats > 1) {
|
||||
res = (plural)
|
||||
? _t("%(severalUsers)schanged their name %(repeats)s times", { severalUsers: "", repeats: repeats })
|
||||
: _t("%(oneUser)schanged their name %(repeats)s times", { oneUser: "", repeats: repeats });
|
||||
} else {
|
||||
res = (plural)
|
||||
? _t("%(severalUsers)schanged their name", { severalUsers: "" })
|
||||
: _t("%(oneUser)schanged their name", { oneUser: "" });
|
||||
}
|
||||
res = (userCount > 1)
|
||||
? _t("%(severalUsers)schanged their name %(count)s times", { severalUsers: "", count: repeats })
|
||||
: _t("%(oneUser)schanged their name %(count)s times", { oneUser: "", count: repeats });
|
||||
break;
|
||||
case "changed_avatar":
|
||||
if (repeats > 1) {
|
||||
res = (plural)
|
||||
? _t("%(severalUsers)schanged their avatar %(repeats)s times", { severalUsers: "", repeats: repeats })
|
||||
: _t("%(oneUser)schanged their avatar %(repeats)s times", { oneUser: "", repeats: repeats });
|
||||
} else {
|
||||
res = (plural)
|
||||
? _t("%(severalUsers)schanged their avatar", { severalUsers: "" })
|
||||
: _t("%(oneUser)schanged their avatar", { oneUser: "" });
|
||||
}
|
||||
res = (userCount > 1)
|
||||
? _t("%(severalUsers)schanged their avatar %(count)s times", { severalUsers: "", count: repeats })
|
||||
: _t("%(oneUser)schanged their avatar %(count)s times", { oneUser: "", count: repeats });
|
||||
break;
|
||||
}
|
||||
|
||||
|
@ -368,17 +296,15 @@ module.exports = React.createClass({
|
|||
*/
|
||||
_renderCommaSeparatedList(items, itemLimit) {
|
||||
const remaining = itemLimit === undefined ? 0 : Math.max(
|
||||
items.length - itemLimit, 0
|
||||
items.length - itemLimit, 0,
|
||||
);
|
||||
if (items.length === 0) {
|
||||
return "";
|
||||
} else if (items.length === 1) {
|
||||
return items[0];
|
||||
} else if (remaining) {
|
||||
} else if (remaining > 0) {
|
||||
items = items.slice(0, itemLimit);
|
||||
return (remaining > 1)
|
||||
? _t("%(items)s and %(remaining)s others", { items: items.join(', '), remaining: remaining } )
|
||||
: _t("%(items)s and one other", { items: items.join(', ') });
|
||||
return _t("%(items)s and %(count)s others", { items: items.join(', '), count: remaining } );
|
||||
} else {
|
||||
const lastItem = items.pop();
|
||||
return _t("%(items)s and %(lastItem)s", { items: items.join(', '), lastItem: lastItem });
|
||||
|
@ -392,8 +318,8 @@ module.exports = React.createClass({
|
|||
);
|
||||
});
|
||||
return (
|
||||
<span className="mx_MemberEventListSummary_avatars" onClick={ this._toggleSummary }>
|
||||
{avatars}
|
||||
<span className="mx_MemberEventListSummary_avatars" onClick={this._toggleSummary}>
|
||||
{ avatars }
|
||||
</span>
|
||||
);
|
||||
},
|
||||
|
@ -417,19 +343,15 @@ module.exports = React.createClass({
|
|||
case 'join':
|
||||
if (e.mxEvent.getPrevContent().membership === 'join') {
|
||||
if (e.mxEvent.getContent().displayname !==
|
||||
e.mxEvent.getPrevContent().displayname)
|
||||
{
|
||||
e.mxEvent.getPrevContent().displayname) {
|
||||
return 'changed_name';
|
||||
}
|
||||
else if (e.mxEvent.getContent().avatar_url !==
|
||||
e.mxEvent.getPrevContent().avatar_url)
|
||||
{
|
||||
} else if (e.mxEvent.getContent().avatar_url !==
|
||||
e.mxEvent.getPrevContent().avatar_url) {
|
||||
return 'changed_avatar';
|
||||
}
|
||||
// console.log("MELS ignoring duplicate membership join event");
|
||||
return null;
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
return 'joined';
|
||||
}
|
||||
case 'leave':
|
||||
|
@ -481,7 +403,7 @@ module.exports = React.createClass({
|
|||
firstEvent.index < aggregateIndices[seq]) {
|
||||
aggregateIndices[seq] = firstEvent.index;
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
|
@ -492,7 +414,7 @@ module.exports = React.createClass({
|
|||
|
||||
render: function() {
|
||||
const eventsToRender = this.props.events;
|
||||
const eventIds = eventsToRender.map(e => e.getId()).join(',');
|
||||
const eventIds = eventsToRender.map((e) => e.getId()).join(',');
|
||||
const fewEvents = eventsToRender.length < this.props.threshold;
|
||||
const expanded = this.state.expanded || fewEvents;
|
||||
|
||||
|
@ -504,7 +426,7 @@ module.exports = React.createClass({
|
|||
if (fewEvents) {
|
||||
return (
|
||||
<div className="mx_MemberEventListSummary" data-scroll-tokens={eventIds}>
|
||||
{expandedEvents}
|
||||
{ expandedEvents }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -540,7 +462,7 @@ module.exports = React.createClass({
|
|||
|
||||
// Sort types by order of lowest event index within sequence
|
||||
const orderedTransitionSequences = Object.keys(aggregate.names).sort(
|
||||
(seq1, seq2) => aggregate.indices[seq1] > aggregate.indices[seq2]
|
||||
(seq1, seq2) => aggregate.indices[seq1] > aggregate.indices[seq2],
|
||||
);
|
||||
|
||||
let summaryContainer = null;
|
||||
|
@ -548,24 +470,24 @@ module.exports = React.createClass({
|
|||
summaryContainer = (
|
||||
<div className="mx_EventTile_line">
|
||||
<div className="mx_EventTile_info">
|
||||
{this._renderAvatars(avatarMembers)}
|
||||
{this._renderSummary(aggregate.names, orderedTransitionSequences)}
|
||||
{ this._renderAvatars(avatarMembers) }
|
||||
{ this._renderSummary(aggregate.names, orderedTransitionSequences) }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const toggleButton = (
|
||||
<div className={"mx_MemberEventListSummary_toggle"} onClick={this._toggleSummary}>
|
||||
{expanded ? 'collapse' : 'expand'}
|
||||
{ expanded ? _t('collapse') : _t('expand') }
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="mx_MemberEventListSummary" data-scroll-tokens={eventIds}>
|
||||
{toggleButton}
|
||||
{summaryContainer}
|
||||
{expanded ? <div className="mx_MemberEventListSummary_line"> </div> : null}
|
||||
{expandedEvents}
|
||||
{ toggleButton }
|
||||
{ summaryContainer }
|
||||
{ expanded ? <div className="mx_MemberEventListSummary_line"> </div> : null }
|
||||
{ expandedEvents }
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -26,8 +26,8 @@ module.exports = React.createClass({
|
|||
const msg = this.props.msg || "Loading...";
|
||||
return (
|
||||
<div className="mx_Spinner">
|
||||
<div className="mx_Spinner_Msg">{msg}</div>
|
||||
<img src="img/spinner.gif" width={w} height={h} className={imgClass}/>
|
||||
<div className="mx_Spinner_Msg">{ msg }</div>
|
||||
<img src="img/spinner.gif" width={w} height={h} className={imgClass} />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -37,11 +37,20 @@ const Pill = React.createClass({
|
|||
isMessagePillUrl: (url) => {
|
||||
return !!REGEX_LOCAL_MATRIXTO.exec(url);
|
||||
},
|
||||
roomNotifPos: (text) => {
|
||||
return text.indexOf("@room");
|
||||
},
|
||||
roomNotifLen: () => {
|
||||
return "@room".length;
|
||||
},
|
||||
TYPE_USER_MENTION: 'TYPE_USER_MENTION',
|
||||
TYPE_ROOM_MENTION: 'TYPE_ROOM_MENTION',
|
||||
TYPE_AT_ROOM_MENTION: 'TYPE_AT_ROOM_MENTION', // '@room' mention
|
||||
},
|
||||
|
||||
props: {
|
||||
// The Type of this Pill. If url is given, this is auto-detected.
|
||||
type: PropTypes.string,
|
||||
// The URL to pillify (no validation is done, see isPillUrl and isMessagePillUrl)
|
||||
url: PropTypes.string,
|
||||
// Whether the pill is in a message
|
||||
|
@ -72,14 +81,20 @@ const Pill = React.createClass({
|
|||
regex = REGEX_LOCAL_MATRIXTO;
|
||||
}
|
||||
|
||||
// Default to the empty array if no match for simplicity
|
||||
// resource and prefix will be undefined instead of throwing
|
||||
const matrixToMatch = regex.exec(nextProps.url) || [];
|
||||
let matrixToMatch;
|
||||
let resourceId;
|
||||
let prefix;
|
||||
|
||||
const resourceId = matrixToMatch[1]; // The room/user ID
|
||||
const prefix = matrixToMatch[2]; // The first character of prefix
|
||||
if (nextProps.url) {
|
||||
// Default to the empty array if no match for simplicity
|
||||
// resource and prefix will be undefined instead of throwing
|
||||
matrixToMatch = regex.exec(nextProps.url) || [];
|
||||
|
||||
const pillType = {
|
||||
resourceId = matrixToMatch[1]; // The room/user ID
|
||||
prefix = matrixToMatch[2]; // The first character of prefix
|
||||
}
|
||||
|
||||
const pillType = this.props.type || {
|
||||
'@': Pill.TYPE_USER_MENTION,
|
||||
'#': Pill.TYPE_ROOM_MENTION,
|
||||
'!': Pill.TYPE_ROOM_MENTION,
|
||||
|
@ -88,6 +103,10 @@ const Pill = React.createClass({
|
|||
let member;
|
||||
let room;
|
||||
switch (pillType) {
|
||||
case Pill.TYPE_AT_ROOM_MENTION: {
|
||||
room = nextProps.room;
|
||||
}
|
||||
break;
|
||||
case Pill.TYPE_USER_MENTION: {
|
||||
const localMember = nextProps.room.getMember(resourceId);
|
||||
member = localMember;
|
||||
|
@ -160,6 +179,17 @@ const Pill = React.createClass({
|
|||
let href = this.props.url;
|
||||
let onClick;
|
||||
switch (this.state.pillType) {
|
||||
case Pill.TYPE_AT_ROOM_MENTION: {
|
||||
const room = this.props.room;
|
||||
if (room) {
|
||||
linkText = "@room";
|
||||
if (this.props.shouldShowPillAvatar) {
|
||||
avatar = <RoomAvatar room={room} width={16} height={16} />;
|
||||
}
|
||||
pillClass = 'mx_AtRoomPill';
|
||||
}
|
||||
}
|
||||
break;
|
||||
case Pill.TYPE_USER_MENTION: {
|
||||
// If this user is not a member of this room, default to the empty member
|
||||
const member = this.state.member;
|
||||
|
@ -167,7 +197,7 @@ const Pill = React.createClass({
|
|||
userId = member.userId;
|
||||
linkText = member.rawDisplayName.replace(' (IRC)', ''); // FIXME when groups are done
|
||||
if (this.props.shouldShowPillAvatar) {
|
||||
avatar = <MemberAvatar member={member} width={16} height={16}/>;
|
||||
avatar = <MemberAvatar member={member} width={16} height={16} />;
|
||||
}
|
||||
pillClass = 'mx_UserPill';
|
||||
href = null;
|
||||
|
@ -180,7 +210,7 @@ const Pill = React.createClass({
|
|||
if (room) {
|
||||
linkText = (room ? getDisplayAliasForRoom(room) : null) || resource;
|
||||
if (this.props.shouldShowPillAvatar) {
|
||||
avatar = <RoomAvatar room={room} width={16} height={16}/>;
|
||||
avatar = <RoomAvatar room={room} width={16} height={16} />;
|
||||
}
|
||||
pillClass = 'mx_RoomPill';
|
||||
}
|
||||
|
@ -195,12 +225,12 @@ const Pill = React.createClass({
|
|||
if (this.state.pillType) {
|
||||
return this.props.inMessage ?
|
||||
<a className={classes} href={href} onClick={onClick} title={resource} data-offset-key={this.props.offsetKey}>
|
||||
{avatar}
|
||||
{linkText}
|
||||
{ avatar }
|
||||
{ linkText }
|
||||
</a> :
|
||||
<span className={classes} title={resource} data-offset-key={this.props.offsetKey}>
|
||||
{avatar}
|
||||
{linkText}
|
||||
{ avatar }
|
||||
{ linkText }
|
||||
</span>;
|
||||
} else {
|
||||
// Deliberately render nothing if the URL isn't recognised
|
||||
|
|
|
@ -20,14 +20,16 @@ import React from 'react';
|
|||
import * as Roles from '../../../Roles';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
var LEVEL_ROLE_MAP = {};
|
||||
var reverseRoles = {};
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'PowerSelector',
|
||||
|
||||
propTypes: {
|
||||
value: React.PropTypes.number.isRequired,
|
||||
// The maximum value that can be set with the power selector
|
||||
maxValue: React.PropTypes.number.isRequired,
|
||||
|
||||
// Default user power level for the room
|
||||
usersDefault: React.PropTypes.number.isRequired,
|
||||
|
||||
// if true, the <select/> should be a 'controlled' form element and updated by React
|
||||
// to reflect the current value, rather than left freeform.
|
||||
|
@ -43,90 +45,107 @@ module.exports = React.createClass({
|
|||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
custom: (LEVEL_ROLE_MAP[this.props.value] === undefined),
|
||||
levelRoleMap: {},
|
||||
// List of power levels to show in the drop-down
|
||||
options: [],
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
maxValue: Infinity,
|
||||
usersDefault: 0,
|
||||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
LEVEL_ROLE_MAP = Roles.levelRoleMap();
|
||||
Object.keys(LEVEL_ROLE_MAP).forEach(function(key) {
|
||||
reverseRoles[LEVEL_ROLE_MAP[key]] = key;
|
||||
});
|
||||
this._initStateFromProps(this.props);
|
||||
},
|
||||
|
||||
componentWillReceiveProps: function(newProps) {
|
||||
this._initStateFromProps(newProps);
|
||||
},
|
||||
|
||||
_initStateFromProps: function(newProps) {
|
||||
// This needs to be done now because levelRoleMap has translated strings
|
||||
const levelRoleMap = Roles.levelRoleMap(newProps.usersDefault);
|
||||
const options = Object.keys(levelRoleMap).filter((l) => {
|
||||
return l === undefined || l <= newProps.maxValue;
|
||||
});
|
||||
|
||||
this.setState({
|
||||
levelRoleMap,
|
||||
options,
|
||||
custom: levelRoleMap[newProps.value] === undefined,
|
||||
});
|
||||
},
|
||||
|
||||
onSelectChange: function(event) {
|
||||
this.setState({ custom: event.target.value === "Custom" });
|
||||
if (event.target.value !== "Custom") {
|
||||
this.props.onChange(this.getValue());
|
||||
this.setState({ custom: event.target.value === "SELECT_VALUE_CUSTOM" });
|
||||
if (event.target.value !== "SELECT_VALUE_CUSTOM") {
|
||||
this.props.onChange(event.target.value);
|
||||
}
|
||||
},
|
||||
|
||||
onCustomBlur: function(event) {
|
||||
this.props.onChange(this.getValue());
|
||||
this.props.onChange(parseInt(this.refs.custom.value));
|
||||
},
|
||||
|
||||
onCustomKeyDown: function(event) {
|
||||
if (event.key == "Enter") {
|
||||
this.props.onChange(this.getValue());
|
||||
this.props.onChange(parseInt(this.refs.custom.value));
|
||||
}
|
||||
},
|
||||
|
||||
getValue: function() {
|
||||
var value;
|
||||
if (this.refs.select) {
|
||||
value = reverseRoles[this.refs.select.value];
|
||||
if (this.refs.custom) {
|
||||
if (value === undefined) value = parseInt( this.refs.custom.value );
|
||||
}
|
||||
}
|
||||
return value;
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var customPicker;
|
||||
let customPicker;
|
||||
if (this.state.custom) {
|
||||
var input;
|
||||
if (this.props.disabled) {
|
||||
input = <span>{ this.props.value }</span>;
|
||||
customPicker = <span>{ _t(
|
||||
"Custom of %(powerLevel)s",
|
||||
{ powerLevel: this.props.value },
|
||||
) }</span>;
|
||||
} else {
|
||||
customPicker = <span> = <input
|
||||
ref="custom"
|
||||
type="text"
|
||||
size="3"
|
||||
defaultValue={this.props.value}
|
||||
onBlur={this.onCustomBlur}
|
||||
onKeyDown={this.onCustomKeyDown}
|
||||
/>
|
||||
</span>;
|
||||
}
|
||||
else {
|
||||
input = <input ref="custom" type="text" size="3" defaultValue={ this.props.value } onBlur={ this.onCustomBlur } onKeyDown={ this.onCustomKeyDown }/>;
|
||||
}
|
||||
customPicker = <span> of { input }</span>;
|
||||
}
|
||||
|
||||
var selectValue;
|
||||
let selectValue;
|
||||
if (this.state.custom) {
|
||||
selectValue = "Custom";
|
||||
selectValue = "SELECT_VALUE_CUSTOM";
|
||||
} else {
|
||||
selectValue = this.state.levelRoleMap[this.props.value] ?
|
||||
this.props.value : "SELECT_VALUE_CUSTOM";
|
||||
}
|
||||
else {
|
||||
selectValue = LEVEL_ROLE_MAP[this.props.value] || "Custom";
|
||||
}
|
||||
var select;
|
||||
let select;
|
||||
if (this.props.disabled) {
|
||||
select = <span>{ selectValue }</span>;
|
||||
}
|
||||
else {
|
||||
// Each level must have a definition in LEVEL_ROLE_MAP
|
||||
const levels = [0, 50, 100];
|
||||
let options = levels.map((level) => {
|
||||
select = <span>{ this.state.levelRoleMap[selectValue] }</span>;
|
||||
} else {
|
||||
// Each level must have a definition in this.state.levelRoleMap
|
||||
let options = this.state.options.map((level) => {
|
||||
return {
|
||||
value: LEVEL_ROLE_MAP[level],
|
||||
// Give a userDefault (users_default in the power event) of 0 but
|
||||
// because level !== undefined, this should never be used.
|
||||
text: Roles.textualPowerLevel(level, 0),
|
||||
}
|
||||
value: level,
|
||||
text: Roles.textualPowerLevel(level, this.props.usersDefault),
|
||||
};
|
||||
});
|
||||
options.push({ value: "Custom", text: _t("Custom level") });
|
||||
options.push({ value: "SELECT_VALUE_CUSTOM", text: _t("Custom level") });
|
||||
options = options.map((op) => {
|
||||
return <option value={op.value} key={op.value}>{op.text}</option>;
|
||||
return <option value={op.value} key={op.value}>{ op.text }</option>;
|
||||
});
|
||||
|
||||
select =
|
||||
<select ref="select"
|
||||
value={ this.props.controlled ? selectValue : undefined }
|
||||
defaultValue={ !this.props.controlled ? selectValue : undefined }
|
||||
onChange={ this.onSelectChange }>
|
||||
value={this.props.controlled ? selectValue : undefined}
|
||||
defaultValue={!this.props.controlled ? selectValue : undefined}
|
||||
onChange={this.onSelectChange}>
|
||||
{ options }
|
||||
</select>;
|
||||
}
|
||||
|
@ -137,5 +156,5 @@ module.exports = React.createClass({
|
|||
{ customPicker }
|
||||
</span>
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -16,23 +16,23 @@ limitations under the License.
|
|||
|
||||
'use strict';
|
||||
|
||||
var React = require('react');
|
||||
const React = require('react');
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'ProgressBar',
|
||||
propTypes: {
|
||||
value: React.PropTypes.number,
|
||||
max: React.PropTypes.number
|
||||
max: React.PropTypes.number,
|
||||
},
|
||||
|
||||
render: function() {
|
||||
// Would use an HTML5 progress tag but if that doesn't animate if you
|
||||
// use the HTML attributes rather than styles
|
||||
var progressStyle = {
|
||||
width: ((this.props.value / this.props.max) * 100)+"%"
|
||||
const progressStyle = {
|
||||
width: ((this.props.value / this.props.max) * 100)+"%",
|
||||
};
|
||||
return (
|
||||
<div className="mx_ProgressBar"><div className="mx_ProgressBar_fill" style={progressStyle}></div></div>
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -24,7 +24,7 @@ const RoomDirectoryButton = function(props) {
|
|||
return (
|
||||
<ActionButton action="view_room_directory"
|
||||
mouseOverAction={props.callout ? "callout_room_directory" : null}
|
||||
label={ _t("Room directory") }
|
||||
label={_t("Room directory")}
|
||||
iconPath="img/icons-directory.svg"
|
||||
size={props.size}
|
||||
tooltip={props.tooltip}
|
||||
|
|
|
@ -23,7 +23,7 @@ const SettingsButton = function(props) {
|
|||
const ActionButton = sdk.getComponent('elements.ActionButton');
|
||||
return (
|
||||
<ActionButton action="view_user_settings"
|
||||
label={ _t("Settings") }
|
||||
label={_t("Settings")}
|
||||
iconPath="img/icons-settings.svg"
|
||||
size={props.size}
|
||||
tooltip={props.tooltip}
|
||||
|
|
110
src/components/views/elements/SettingsFlag.js
Normal file
110
src/components/views/elements/SettingsFlag.js
Normal file
|
@ -0,0 +1,110 @@
|
|||
/*
|
||||
Copyright 2017 Travis Ralston
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'SettingsFlag',
|
||||
propTypes: {
|
||||
name: React.PropTypes.string.isRequired,
|
||||
level: React.PropTypes.string.isRequired,
|
||||
roomId: React.PropTypes.string, // for per-room settings
|
||||
label: React.PropTypes.string, // untranslated
|
||||
onChange: React.PropTypes.func,
|
||||
isExplicit: React.PropTypes.bool,
|
||||
manualSave: React.PropTypes.bool,
|
||||
|
||||
// If group is supplied, then this will create a radio button instead.
|
||||
group: React.PropTypes.string,
|
||||
value: React.PropTypes.any, // the value for the radio button
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
value: SettingsStore.getValueAt(
|
||||
this.props.level,
|
||||
this.props.name,
|
||||
this.props.roomId,
|
||||
this.props.isExplicit,
|
||||
),
|
||||
};
|
||||
},
|
||||
|
||||
onChange: function(e) {
|
||||
if (this.props.group && !e.target.checked) return;
|
||||
|
||||
const newState = this.props.group ? this.props.value : e.target.checked;
|
||||
if (!this.props.manualSave) this.save(newState);
|
||||
else this.setState({ value: newState });
|
||||
if (this.props.onChange) this.props.onChange(newState);
|
||||
},
|
||||
|
||||
save: function(val = undefined) {
|
||||
return SettingsStore.setValue(
|
||||
this.props.name,
|
||||
this.props.roomId,
|
||||
this.props.level,
|
||||
val !== undefined ? val : this.state.value,
|
||||
);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const value = this.props.manualSave ? this.state.value : SettingsStore.getValueAt(
|
||||
this.props.level,
|
||||
this.props.name,
|
||||
this.props.roomId,
|
||||
this.props.isExplicit,
|
||||
);
|
||||
|
||||
const canChange = SettingsStore.canSetValue(this.props.name, this.props.roomId, this.props.level);
|
||||
|
||||
let label = this.props.label;
|
||||
if (!label) label = SettingsStore.getDisplayName(this.props.name, this.props.level);
|
||||
else label = _t(label);
|
||||
|
||||
// We generate a relatively complex ID to avoid conflicts
|
||||
const id = this.props.name + "_" + this.props.group + "_" + this.props.value + "_" + this.props.level;
|
||||
let checkbox = (
|
||||
<input id={id}
|
||||
type="checkbox"
|
||||
defaultChecked={value}
|
||||
onChange={this.onChange}
|
||||
disabled={!canChange}
|
||||
/>
|
||||
);
|
||||
if (this.props.group) {
|
||||
checkbox = (
|
||||
<input id={id}
|
||||
type="radio"
|
||||
name={this.props.group}
|
||||
value={this.props.value}
|
||||
checked={value === this.props.value}
|
||||
onChange={this.onChange}
|
||||
disabled={!canChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<label>
|
||||
{ checkbox }
|
||||
{ label }
|
||||
</label>
|
||||
);
|
||||
},
|
||||
});
|
|
@ -24,7 +24,7 @@ const StartChatButton = function(props) {
|
|||
return (
|
||||
<ActionButton action="view_create_chat"
|
||||
mouseOverAction={props.callout ? "callout_start_chat" : null}
|
||||
label={ _t("Start chat") }
|
||||
label={_t("Start chat")}
|
||||
iconPath="img/icons-people.svg"
|
||||
size={props.size}
|
||||
tooltip={props.tooltip}
|
||||
|
|
119
src/components/views/elements/TagTile.js
Normal file
119
src/components/views/elements/TagTile.js
Normal file
|
@ -0,0 +1,119 @@
|
|||
/*
|
||||
Copyright 2017 New Vector 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.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import { MatrixClient } from 'matrix-js-sdk';
|
||||
import sdk from '../../../index';
|
||||
import dis from '../../../dispatcher';
|
||||
import { isOnlyCtrlOrCmdKeyEvent } from '../../../Keyboard';
|
||||
|
||||
import FlairStore from '../../../stores/FlairStore';
|
||||
|
||||
// A class for a child of TagPanel (possibly wrapped in a DNDTagTile) that represents
|
||||
// a thing to click on for the user to filter the visible rooms in the RoomList to:
|
||||
// - Rooms that are part of the group
|
||||
// - Direct messages with members of the group
|
||||
// with the intention that this could be expanded to arbitrary tags in future.
|
||||
export default React.createClass({
|
||||
displayName: 'TagTile',
|
||||
|
||||
propTypes: {
|
||||
// A string tag such as "m.favourite" or a group ID such as "+groupid:domain.bla"
|
||||
// For now, only group IDs are handled.
|
||||
tag: PropTypes.string,
|
||||
},
|
||||
|
||||
contextTypes: {
|
||||
matrixClient: React.PropTypes.instanceOf(MatrixClient).isRequired,
|
||||
},
|
||||
|
||||
getInitialState() {
|
||||
return {
|
||||
// Whether the mouse is over the tile
|
||||
hover: false,
|
||||
// The profile data of the group if this.props.tag is a group ID
|
||||
profile: null,
|
||||
};
|
||||
},
|
||||
|
||||
componentWillMount() {
|
||||
this.unmounted = false;
|
||||
if (this.props.tag[0] === '+') {
|
||||
FlairStore.getGroupProfileCached(
|
||||
this.context.matrixClient,
|
||||
this.props.tag,
|
||||
).then((profile) => {
|
||||
if (this.unmounted) return;
|
||||
this.setState({profile});
|
||||
}).catch((err) => {
|
||||
console.warn('Could not fetch group profile for ' + this.props.tag, err);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
componentWillUnmount() {
|
||||
this.unmounted = true;
|
||||
},
|
||||
|
||||
onClick: function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
dis.dispatch({
|
||||
action: 'select_tag',
|
||||
tag: this.props.tag,
|
||||
ctrlOrCmdKey: isOnlyCtrlOrCmdKeyEvent(e),
|
||||
shiftKey: e.shiftKey,
|
||||
});
|
||||
},
|
||||
|
||||
onMouseOver: function() {
|
||||
this.setState({hover: true});
|
||||
},
|
||||
|
||||
onMouseOut: function() {
|
||||
this.setState({hover: false});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
const RoomTooltip = sdk.getComponent('rooms.RoomTooltip');
|
||||
const profile = this.state.profile || {};
|
||||
const name = profile.name || this.props.tag;
|
||||
const avatarHeight = 35;
|
||||
|
||||
const httpUrl = profile.avatarUrl ? this.context.matrixClient.mxcUrlToHttp(
|
||||
profile.avatarUrl, avatarHeight, avatarHeight, "crop",
|
||||
) : null;
|
||||
|
||||
const className = classNames({
|
||||
mx_TagTile: true,
|
||||
mx_TagTile_selected: this.props.selected,
|
||||
});
|
||||
|
||||
const tip = this.state.hover ?
|
||||
<RoomTooltip className="mx_TagTile_tooltip" label={name} /> :
|
||||
<div />;
|
||||
return <AccessibleButton className={className} onClick={this.onClick}>
|
||||
<div className="mx_TagTile_avatar" onMouseOver={this.onMouseOver} onMouseOut={this.onMouseOut}>
|
||||
<BaseAvatar name={name} url={httpUrl} width={avatarHeight} height={avatarHeight} />
|
||||
{ tip }
|
||||
</div>
|
||||
</AccessibleButton>;
|
||||
},
|
||||
});
|
|
@ -16,9 +16,9 @@ limitations under the License.
|
|||
|
||||
'use strict';
|
||||
|
||||
var React = require('react');
|
||||
var ReactDOM = require("react-dom");
|
||||
var Tinter = require("../../../Tinter");
|
||||
const React = require('react');
|
||||
const ReactDOM = require("react-dom");
|
||||
const Tinter = require("../../../Tinter");
|
||||
|
||||
var TintableSvg = React.createClass({
|
||||
displayName: 'TintableSvg',
|
||||
|
@ -63,16 +63,16 @@ var TintableSvg = React.createClass({
|
|||
|
||||
render: function() {
|
||||
return (
|
||||
<object className={ "mx_TintableSvg " + (this.props.className ? this.props.className : "") }
|
||||
<object className={"mx_TintableSvg " + (this.props.className ? this.props.className : "")}
|
||||
type="image/svg+xml"
|
||||
data={ this.props.src }
|
||||
width={ this.props.width }
|
||||
height={ this.props.height }
|
||||
onLoad={ this.onLoad }
|
||||
data={this.props.src}
|
||||
width={this.props.width}
|
||||
height={this.props.height}
|
||||
onLoad={this.onLoad}
|
||||
tabIndex="-1"
|
||||
/>
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// Register with the Tinter so that we will be told if the tint changes
|
||||
|
|
61
src/components/views/elements/TintableSvgButton.js
Normal file
61
src/components/views/elements/TintableSvgButton.js
Normal file
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
Copyright 2017 New Vector 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.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import TintableSvg from './TintableSvg';
|
||||
|
||||
export default class TintableSvgButton extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
render() {
|
||||
let classes = "mx_TintableSvgButton";
|
||||
if (this.props.className) {
|
||||
classes += " " + this.props.className;
|
||||
}
|
||||
return (
|
||||
<span
|
||||
width={this.props.width}
|
||||
height={this.props.height}
|
||||
className={classes}>
|
||||
<TintableSvg
|
||||
src={this.props.src}
|
||||
width={this.props.width}
|
||||
height={this.props.height}
|
||||
></TintableSvg>
|
||||
<span
|
||||
title={this.props.title}
|
||||
onClick={this.props.onClick} />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
TintableSvgButton.propTypes = {
|
||||
src: PropTypes.string,
|
||||
title: PropTypes.string,
|
||||
className: PropTypes.string,
|
||||
width: PropTypes.string.isRequired,
|
||||
height: PropTypes.string.isRequired,
|
||||
onClick: PropTypes.func,
|
||||
};
|
||||
|
||||
TintableSvgButton.defaultProps = {
|
||||
onClick: function() {},
|
||||
};
|
55
src/components/views/elements/ToolTipButton.js
Normal file
55
src/components/views/elements/ToolTipButton.js
Normal file
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
Copyright 2017 New Vector 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.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import sdk from '../../../index';
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'ToolTipButton',
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
hover: false,
|
||||
};
|
||||
},
|
||||
|
||||
onMouseOver: function() {
|
||||
this.setState({
|
||||
hover: true,
|
||||
});
|
||||
},
|
||||
|
||||
onMouseOut: function() {
|
||||
this.setState({
|
||||
hover: false,
|
||||
});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const RoomTooltip = sdk.getComponent("rooms.RoomTooltip");
|
||||
const tip = this.state.hover ? <RoomTooltip
|
||||
className="mx_ToolTipButton_container"
|
||||
tooltipClassName="mx_ToolTipButton_helpText"
|
||||
label={this.props.helpText}
|
||||
/> : <div />;
|
||||
return (
|
||||
<div className="mx_ToolTipButton" onMouseOver={this.onMouseOver} onMouseOut={this.onMouseOut} >
|
||||
?
|
||||
{ tip }
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
Copyright 2017 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -13,7 +14,9 @@ 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 React = require('react');
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
module.exports = React.createClass({
|
||||
|
@ -21,12 +24,21 @@ module.exports = React.createClass({
|
|||
|
||||
propTypes: {
|
||||
// The number of elements to show before truncating. If negative, no truncation is done.
|
||||
truncateAt: React.PropTypes.number,
|
||||
truncateAt: PropTypes.number,
|
||||
// The className to apply to the wrapping div
|
||||
className: React.PropTypes.string,
|
||||
className: PropTypes.string,
|
||||
// A function that returns the children to be rendered into the element.
|
||||
// function getChildren(start: number, end: number): Array<React.Node>
|
||||
// The start element is included, the end is not (as in `slice`).
|
||||
// If omitted, the React child elements will be used. This parameter can be used
|
||||
// to avoid creating unnecessary React elements.
|
||||
getChildren: PropTypes.func,
|
||||
// A function that should return the total number of child element available.
|
||||
// Required if getChildren is supplied.
|
||||
getChildCount: PropTypes.func,
|
||||
// A function which will be invoked when an overflow element is required.
|
||||
// This will be inserted after the children.
|
||||
createOverflowElement: React.PropTypes.func
|
||||
createOverflowElement: PropTypes.func,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
|
@ -34,40 +46,56 @@ module.exports = React.createClass({
|
|||
truncateAt: 2,
|
||||
createOverflowElement: function(overflowCount, totalCount) {
|
||||
return (
|
||||
<div>{_t("And %(count)s more...", {count: overflowCount})}</div>
|
||||
<div>{ _t("And %(count)s more...", {count: overflowCount}) }</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
_getChildren: function(start, end) {
|
||||
if (this.props.getChildren && this.props.getChildCount) {
|
||||
return this.props.getChildren(start, end);
|
||||
} else {
|
||||
// XXX: I'm not sure why anything would pass null into this, it seems
|
||||
// like a bizzare case to handle, but I'm preserving the behaviour.
|
||||
// (see commit 38d5c7d5c5d5a34dc16ef5d46278315f5c57f542)
|
||||
return React.Children.toArray(this.props.children).filter((c) => {
|
||||
return c != null;
|
||||
}).slice(start, end);
|
||||
}
|
||||
},
|
||||
|
||||
_getChildCount: function() {
|
||||
if (this.props.getChildren && this.props.getChildCount) {
|
||||
return this.props.getChildCount();
|
||||
} else {
|
||||
return React.Children.toArray(this.props.children).filter((c) => {
|
||||
return c != null;
|
||||
}).length;
|
||||
}
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var childsJsx = this.props.children;
|
||||
var overflowJsx;
|
||||
var childArray = React.Children.toArray(this.props.children).filter((c) => {
|
||||
return c != null;
|
||||
});
|
||||
|
||||
var childCount = childArray.length;
|
||||
let overflowNode = null;
|
||||
|
||||
const totalChildren = this._getChildCount();
|
||||
let upperBound = totalChildren;
|
||||
if (this.props.truncateAt >= 0) {
|
||||
var overflowCount = childCount - this.props.truncateAt;
|
||||
|
||||
const overflowCount = totalChildren - this.props.truncateAt;
|
||||
if (overflowCount > 1) {
|
||||
overflowJsx = this.props.createOverflowElement(
|
||||
overflowCount, childCount
|
||||
overflowNode = this.props.createOverflowElement(
|
||||
overflowCount, totalChildren,
|
||||
);
|
||||
|
||||
// cut out the overflow elements
|
||||
childArray.splice(childCount - overflowCount, overflowCount);
|
||||
childsJsx = childArray; // use what is left
|
||||
upperBound = this.props.truncateAt;
|
||||
}
|
||||
}
|
||||
const childNodes = this._getChildren(0, upperBound);
|
||||
|
||||
return (
|
||||
<div className={this.props.className}>
|
||||
{childsJsx}
|
||||
{overflowJsx}
|
||||
{ childNodes }
|
||||
{ overflowNode }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -52,19 +52,19 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
render: function() {
|
||||
var self = this;
|
||||
const self = this;
|
||||
return (
|
||||
<div>
|
||||
<ul className="mx_UserSelector_UserIdList" ref="list">
|
||||
{this.props.selected_users.map(function(user_id, i) {
|
||||
return <li key={user_id}>{user_id} - <span onClick={function() {self.removeUser(user_id);}}>X</span></li>;
|
||||
})}
|
||||
{ this.props.selected_users.map(function(user_id, i) {
|
||||
return <li key={user_id}>{ user_id } - <span onClick={function() {self.removeUser(user_id);}}>X</span></li>;
|
||||
}) }
|
||||
</ul>
|
||||
<input type="text" ref="user_id_input" defaultValue="" className="mx_UserSelector_userIdInput" placeholder={_t("ex. @bob:example.com")}/>
|
||||
<input type="text" ref="user_id_input" defaultValue="" className="mx_UserSelector_userIdInput" placeholder={_t("ex. @bob:example.com")} />
|
||||
<button onClick={this.onAddUserId} className="mx_UserSelector_AddUserId">
|
||||
{_t("Add User")}
|
||||
{ _t("Add User") }
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
75
src/components/views/groups/GroupInviteTile.js
Normal file
75
src/components/views/groups/GroupInviteTile.js
Normal file
|
@ -0,0 +1,75 @@
|
|||
/*
|
||||
Copyright 2017 New Vector 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.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { MatrixClient } from 'matrix-js-sdk';
|
||||
import sdk from '../../../index';
|
||||
import dis from '../../../dispatcher';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
|
||||
export default React.createClass({
|
||||
displayName: 'GroupInviteTile',
|
||||
|
||||
propTypes: {
|
||||
group: PropTypes.object.isRequired,
|
||||
},
|
||||
|
||||
contextTypes: {
|
||||
matrixClient: PropTypes.instanceOf(MatrixClient),
|
||||
},
|
||||
|
||||
onClick: function(e) {
|
||||
dis.dispatch({
|
||||
action: 'view_group',
|
||||
group_id: this.props.group.groupId,
|
||||
});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
|
||||
const EmojiText = sdk.getComponent('elements.EmojiText');
|
||||
|
||||
const groupName = this.props.group.name || this.props.group.groupId;
|
||||
const httpAvatarUrl = this.props.group.avatarUrl ?
|
||||
this.context.matrixClient.mxcUrlToHttp(this.props.group.avatarUrl, 24, 24) : null;
|
||||
|
||||
const av = <BaseAvatar name={groupName} width={24} height={24} url={httpAvatarUrl} />;
|
||||
|
||||
const label = <EmojiText
|
||||
element="div"
|
||||
title={this.props.group.groupId}
|
||||
className="mx_RoomTile_name mx_RoomTile_badgeShown"
|
||||
dir="auto"
|
||||
>
|
||||
{ groupName }
|
||||
</EmojiText>;
|
||||
|
||||
const badge = <div className="mx_RoomSubList_badge mx_RoomSubList_badgeHighlight">!</div>;
|
||||
|
||||
return (
|
||||
<AccessibleButton className="mx_RoomTile mx_RoomTile_highlight" onClick={this.onClick}>
|
||||
<div className="mx_RoomTile_avatar">
|
||||
{ av }
|
||||
</div>
|
||||
<div className="mx_RoomTile_nameContainer">
|
||||
{ label }
|
||||
{ badge }
|
||||
</div>
|
||||
</AccessibleButton>
|
||||
);
|
||||
},
|
||||
});
|
204
src/components/views/groups/GroupMemberInfo.js
Normal file
204
src/components/views/groups/GroupMemberInfo.js
Normal file
|
@ -0,0 +1,204 @@
|
|||
/*
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2017 New Vector 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.
|
||||
*/
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { MatrixClient } from 'matrix-js-sdk';
|
||||
import dis from '../../../dispatcher';
|
||||
import Modal from '../../../Modal';
|
||||
import sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { GroupMemberType } from '../../../groups';
|
||||
import GroupStoreCache from '../../../stores/GroupStoreCache';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import GeminiScrollbar from 'react-gemini-scrollbar';
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'GroupMemberInfo',
|
||||
|
||||
contextTypes: {
|
||||
matrixClient: PropTypes.instanceOf(MatrixClient),
|
||||
},
|
||||
|
||||
propTypes: {
|
||||
groupId: PropTypes.string,
|
||||
groupMember: GroupMemberType,
|
||||
isInvited: PropTypes.bool,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
removingUser: false,
|
||||
isUserPrivilegedInGroup: null,
|
||||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
this._initGroupStore(this.props.groupId);
|
||||
},
|
||||
|
||||
componentWillReceiveProps(newProps) {
|
||||
if (newProps.groupId !== this.props.groupId) {
|
||||
this._unregisterGroupStore();
|
||||
this._initGroupStore(newProps.groupId);
|
||||
}
|
||||
},
|
||||
|
||||
_initGroupStore(groupId) {
|
||||
this._groupStore = GroupStoreCache.getGroupStore(this.props.groupId);
|
||||
this._groupStore.registerListener(this.onGroupStoreUpdated);
|
||||
},
|
||||
|
||||
_unregisterGroupStore() {
|
||||
if (this._groupStore) {
|
||||
this._groupStore.unregisterListener(this.onGroupStoreUpdated);
|
||||
}
|
||||
},
|
||||
|
||||
onGroupStoreUpdated: function() {
|
||||
this.setState({
|
||||
isUserInvited: this._groupStore.getGroupInvitedMembers().some(
|
||||
(m) => m.userId === this.props.groupMember.userId,
|
||||
),
|
||||
isUserPrivilegedInGroup: this._groupStore.isUserPrivileged(),
|
||||
});
|
||||
},
|
||||
|
||||
_onKick: function() {
|
||||
const ConfirmUserActionDialog = sdk.getComponent("dialogs.ConfirmUserActionDialog");
|
||||
Modal.createDialog(ConfirmUserActionDialog, {
|
||||
matrixClient: this.context.matrixClient,
|
||||
groupMember: this.props.groupMember,
|
||||
action: this.state.isUserInvited ? _t('Disinvite') : _t('Remove from community'),
|
||||
title: this.state.isUserInvited ? _t('Disinvite this user from community?')
|
||||
: _t('Remove this user from community?'),
|
||||
danger: true,
|
||||
onFinished: (proceed) => {
|
||||
if (!proceed) return;
|
||||
|
||||
this.setState({removingUser: true});
|
||||
this.context.matrixClient.removeUserFromGroup(
|
||||
this.props.groupId, this.props.groupMember.userId,
|
||||
).then(() => {
|
||||
// return to the user list
|
||||
dis.dispatch({
|
||||
action: "view_user",
|
||||
member: null,
|
||||
});
|
||||
}).catch((e) => {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createTrackedDialog('Failed to remove user from group', '', ErrorDialog, {
|
||||
title: _t('Error'),
|
||||
description: this.state.isUserInvited ?
|
||||
_t('Failed to withdraw invitation') :
|
||||
_t('Failed to remove user from community'),
|
||||
});
|
||||
}).finally(() => {
|
||||
this.setState({removingUser: false});
|
||||
});
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
_onCancel: function(e) {
|
||||
// Go back to the user list
|
||||
dis.dispatch({
|
||||
action: "view_user",
|
||||
member: null,
|
||||
});
|
||||
},
|
||||
|
||||
onRoomTileClick(roomId) {
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
room_id: roomId,
|
||||
});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
if (this.state.removingUser) {
|
||||
const Spinner = sdk.getComponent("elements.Spinner");
|
||||
return <Spinner />;
|
||||
}
|
||||
|
||||
let adminTools;
|
||||
if (this.state.isUserPrivilegedInGroup) {
|
||||
const kickButton = (
|
||||
<AccessibleButton className="mx_MemberInfo_field"
|
||||
onClick={this._onKick}>
|
||||
{ this.state.isUserInvited ? _t('Disinvite') : _t('Remove from community') }
|
||||
</AccessibleButton>
|
||||
);
|
||||
|
||||
// No make/revoke admin API yet
|
||||
/*const opLabel = this.state.isTargetMod ? _t("Revoke Moderator") : _t("Make Moderator");
|
||||
giveModButton = <AccessibleButton className="mx_MemberInfo_field" onClick={this.onModToggle}>
|
||||
{giveOpLabel}
|
||||
</AccessibleButton>;*/
|
||||
|
||||
if (kickButton) {
|
||||
adminTools =
|
||||
<div className="mx_MemberInfo_adminTools">
|
||||
<h3>{ _t("Admin Tools") }</h3>
|
||||
<div className="mx_MemberInfo_buttons">
|
||||
{ kickButton }
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
const avatarUrl = this.context.matrixClient.mxcUrlToHttp(
|
||||
this.props.groupMember.avatarUrl,
|
||||
36, 36, 'crop',
|
||||
);
|
||||
|
||||
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
|
||||
const avatar = (
|
||||
<BaseAvatar name={this.props.groupMember.userId} width={36} height={36}
|
||||
url={avatarUrl}
|
||||
/>
|
||||
);
|
||||
|
||||
const groupMemberName = (
|
||||
this.props.groupMember.displayname || this.props.groupMember.userId
|
||||
);
|
||||
|
||||
const EmojiText = sdk.getComponent('elements.EmojiText');
|
||||
return (
|
||||
<div className="mx_MemberInfo">
|
||||
<GeminiScrollbar autoshow={true}>
|
||||
<AccessibleButton className="mx_MemberInfo_cancel"onClick={this._onCancel}>
|
||||
<img src="img/cancel.svg" width="18" height="18" className="mx_filterFlipColor" />
|
||||
</AccessibleButton>
|
||||
<div className="mx_MemberInfo_avatar">
|
||||
{ avatar }
|
||||
</div>
|
||||
|
||||
<EmojiText element="h2">{ groupMemberName }</EmojiText>
|
||||
|
||||
<div className="mx_MemberInfo_profile">
|
||||
<div className="mx_MemberInfo_profileField">
|
||||
{ this.props.groupMember.userId }
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{ adminTools }
|
||||
</GeminiScrollbar>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
172
src/components/views/groups/GroupMemberList.js
Normal file
172
src/components/views/groups/GroupMemberList.js
Normal file
|
@ -0,0 +1,172 @@
|
|||
/*
|
||||
Copyright 2017 Vector Creations Ltd.
|
||||
Copyright 2017 New Vector 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.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import sdk from '../../../index';
|
||||
import GroupStoreCache from '../../../stores/GroupStoreCache';
|
||||
import GeminiScrollbar from 'react-gemini-scrollbar';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const INITIAL_LOAD_NUM_MEMBERS = 30;
|
||||
|
||||
export default React.createClass({
|
||||
displayName: 'GroupMemberList',
|
||||
|
||||
propTypes: {
|
||||
groupId: PropTypes.string.isRequired,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
members: null,
|
||||
invitedMembers: null,
|
||||
truncateAt: INITIAL_LOAD_NUM_MEMBERS,
|
||||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
this._unmounted = false;
|
||||
this._initGroupStore(this.props.groupId);
|
||||
},
|
||||
|
||||
_initGroupStore: function(groupId) {
|
||||
this._groupStore = GroupStoreCache.getGroupStore(groupId);
|
||||
this._groupStore.registerListener(() => {
|
||||
this._fetchMembers();
|
||||
});
|
||||
},
|
||||
|
||||
_fetchMembers: function() {
|
||||
if (this._unmounted) return;
|
||||
this.setState({
|
||||
members: this._groupStore.getGroupMembers(),
|
||||
invitedMembers: this._groupStore.getGroupInvitedMembers(),
|
||||
});
|
||||
},
|
||||
|
||||
_createOverflowTile: function(overflowCount, totalCount) {
|
||||
// For now we'll pretend this is any entity. It should probably be a separate tile.
|
||||
const EntityTile = sdk.getComponent("rooms.EntityTile");
|
||||
const BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
|
||||
const text = _t("and %(count)s others...", { count: overflowCount });
|
||||
return (
|
||||
<EntityTile className="mx_EntityTile_ellipsis" avatarJsx={
|
||||
<BaseAvatar url="img/ellipsis.svg" name="..." width={36} height={36} />
|
||||
} name={text} presenceState="online" suppressOnHover={true}
|
||||
onClick={this._showFullMemberList} />
|
||||
);
|
||||
},
|
||||
|
||||
_showFullMemberList: function() {
|
||||
this.setState({
|
||||
truncateAt: -1,
|
||||
});
|
||||
},
|
||||
|
||||
onSearchQueryChanged: function(ev) {
|
||||
this.setState({ searchQuery: ev.target.value });
|
||||
},
|
||||
|
||||
makeGroupMemberTiles: function(query, memberList) {
|
||||
const GroupMemberTile = sdk.getComponent("groups.GroupMemberTile");
|
||||
const TruncatedList = sdk.getComponent("elements.TruncatedList");
|
||||
query = (query || "").toLowerCase();
|
||||
if (query) {
|
||||
memberList = memberList.filter((m) => {
|
||||
const matchesName = (m.displayname || "").toLowerCase().includes(query);
|
||||
const matchesId = m.userId.toLowerCase().includes(query);
|
||||
|
||||
if (!matchesName && !matchesId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
const uniqueMembers = {};
|
||||
memberList.forEach((m) => {
|
||||
if (!uniqueMembers[m.userId]) uniqueMembers[m.userId] = m;
|
||||
});
|
||||
memberList = Object.keys(uniqueMembers).map((userId) => uniqueMembers[userId]);
|
||||
// Descending sort on isPrivileged = true = 1 to isPrivileged = false = 0
|
||||
memberList.sort((a, b) => {
|
||||
if (a.isPrivileged === b.isPrivileged) {
|
||||
const aName = a.displayname || a.userId;
|
||||
const bName = b.displayname || b.userId;
|
||||
if (aName < bName) {
|
||||
return -1;
|
||||
} else if (aName > bName) {
|
||||
return 1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
} else {
|
||||
return a.isPrivileged ? -1 : 1;
|
||||
}
|
||||
});
|
||||
|
||||
const memberTiles = memberList.map((m) => {
|
||||
return (
|
||||
<GroupMemberTile key={m.userId} groupId={this.props.groupId} member={m} />
|
||||
);
|
||||
});
|
||||
|
||||
return <TruncatedList className="mx_MemberList_wrapper" truncateAt={this.state.truncateAt}
|
||||
createOverflowElement={this._createOverflowTile}
|
||||
>
|
||||
{ memberTiles }
|
||||
</TruncatedList>;
|
||||
},
|
||||
|
||||
render: function() {
|
||||
if (this.state.fetching || this.state.fetchingInvitedMembers) {
|
||||
const Spinner = sdk.getComponent("elements.Spinner");
|
||||
return (<div className="mx_MemberList">
|
||||
<Spinner />
|
||||
</div>);
|
||||
}
|
||||
|
||||
const inputBox = (
|
||||
<form autoComplete="off">
|
||||
<input className="mx_GroupMemberList_query" id="mx_GroupMemberList_query" type="text"
|
||||
onChange={this.onSearchQueryChanged} value={this.state.searchQuery}
|
||||
placeholder={_t('Filter community members')} />
|
||||
</form>
|
||||
);
|
||||
|
||||
const joined = this.state.members ? <div className="mx_MemberList_joined">
|
||||
{ this.makeGroupMemberTiles(this.state.searchQuery, this.state.members) }
|
||||
</div> : <div />;
|
||||
|
||||
const invited = (this.state.invitedMembers && this.state.invitedMembers.length > 0) ?
|
||||
<div className="mx_MemberList_invited">
|
||||
<h2>{ _t("Invited") }</h2>
|
||||
{ this.makeGroupMemberTiles(this.state.searchQuery, this.state.invitedMembers) }
|
||||
</div> : <div />;
|
||||
|
||||
return (
|
||||
<div className="mx_MemberList">
|
||||
{ inputBox }
|
||||
<GeminiScrollbar autoshow={true} className="mx_MemberList_outerWrapper">
|
||||
{ joined }
|
||||
{ invited }
|
||||
</GeminiScrollbar>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
70
src/components/views/groups/GroupMemberTile.js
Normal file
70
src/components/views/groups/GroupMemberTile.js
Normal file
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2017 New Vector 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.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import sdk from '../../../index';
|
||||
import dis from '../../../dispatcher';
|
||||
import { GroupMemberType } from '../../../groups';
|
||||
import withMatrixClient from '../../../wrappers/withMatrixClient';
|
||||
|
||||
export default withMatrixClient(React.createClass({
|
||||
displayName: 'GroupMemberTile',
|
||||
|
||||
propTypes: {
|
||||
matrixClient: PropTypes.object,
|
||||
groupId: PropTypes.string.isRequired,
|
||||
member: GroupMemberType.isRequired,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {};
|
||||
},
|
||||
|
||||
onClick: function(e) {
|
||||
dis.dispatch({
|
||||
action: 'view_group_user',
|
||||
member: this.props.member,
|
||||
groupId: this.props.groupId,
|
||||
});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
|
||||
const EntityTile = sdk.getComponent('rooms.EntityTile');
|
||||
|
||||
const name = this.props.member.displayname || this.props.member.userId;
|
||||
const avatarUrl = this.props.matrixClient.mxcUrlToHttp(
|
||||
this.props.member.avatarUrl,
|
||||
36, 36, 'crop',
|
||||
);
|
||||
|
||||
const av = (
|
||||
<BaseAvatar name={this.props.member.userId}
|
||||
width={36} height={36}
|
||||
url={avatarUrl}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<EntityTile name={name} avatarJsx={av} onClick={this.onClick}
|
||||
suppressOnHover={true} presenceState="online"
|
||||
powerStatus={this.props.member.isPrivileged ? EntityTile.POWER_STATUS_ADMIN : null}
|
||||
/>
|
||||
);
|
||||
},
|
||||
}));
|
86
src/components/views/groups/GroupPublicityToggle.js
Normal file
86
src/components/views/groups/GroupPublicityToggle.js
Normal file
|
@ -0,0 +1,86 @@
|
|||
/*
|
||||
Copyright 2017 New Vector 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.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import sdk from '../../../index';
|
||||
import GroupStoreCache from '../../../stores/GroupStoreCache';
|
||||
import GroupStore from '../../../stores/GroupStore';
|
||||
import { _t } from '../../../languageHandler.js';
|
||||
|
||||
export default React.createClass({
|
||||
displayName: 'GroupPublicityToggle',
|
||||
|
||||
propTypes: {
|
||||
groupId: PropTypes.string.isRequired,
|
||||
},
|
||||
|
||||
getInitialState() {
|
||||
return {
|
||||
busy: false,
|
||||
ready: false,
|
||||
isGroupPublicised: null,
|
||||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
this._initGroupStore(this.props.groupId);
|
||||
},
|
||||
|
||||
_initGroupStore: function(groupId) {
|
||||
this._groupStore = GroupStoreCache.getGroupStore(groupId);
|
||||
this._groupStore.registerListener(() => {
|
||||
this.setState({
|
||||
isGroupPublicised: this._groupStore.getGroupPublicity(),
|
||||
ready: this._groupStore.isStateReady(GroupStore.STATE_KEY.Summary),
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
_onPublicityToggle: function(e) {
|
||||
e.stopPropagation();
|
||||
this.setState({
|
||||
busy: true,
|
||||
// Optimistic early update
|
||||
isGroupPublicised: !this.state.isGroupPublicised,
|
||||
});
|
||||
this._groupStore.setGroupPublicity(!this.state.isGroupPublicised).then(() => {
|
||||
this.setState({
|
||||
busy: false,
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
render() {
|
||||
const GroupTile = sdk.getComponent('groups.GroupTile');
|
||||
const input = <input type="checkbox"
|
||||
onClick={this._onPublicityToggle}
|
||||
checked={this.state.isGroupPublicised}
|
||||
/>;
|
||||
const labelText = !this.state.ready ? _t("Loading...") :
|
||||
(this.state.isGroupPublicised ?
|
||||
_t("Flair will appear if enabled in room settings") :
|
||||
_t("Flair will not appear")
|
||||
);
|
||||
return <div className="mx_GroupPublicity_toggle">
|
||||
<GroupTile groupId={this.props.groupId} showDescription={false} avatarHeight={40} />
|
||||
<label onClick={this._onPublicityToggle}>
|
||||
{ input }
|
||||
{ labelText }
|
||||
</label>
|
||||
</div>;
|
||||
},
|
||||
});
|
240
src/components/views/groups/GroupRoomInfo.js
Normal file
240
src/components/views/groups/GroupRoomInfo.js
Normal file
|
@ -0,0 +1,240 @@
|
|||
/*
|
||||
Copyright 2017 New Vector 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.
|
||||
*/
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { MatrixClient } from 'matrix-js-sdk';
|
||||
import dis from '../../../dispatcher';
|
||||
import Modal from '../../../Modal';
|
||||
import sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import GroupStoreCache from '../../../stores/GroupStoreCache';
|
||||
import GeminiScrollbar from 'react-gemini-scrollbar';
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'GroupRoomInfo',
|
||||
|
||||
contextTypes: {
|
||||
matrixClient: PropTypes.instanceOf(MatrixClient),
|
||||
},
|
||||
|
||||
propTypes: {
|
||||
groupId: PropTypes.string,
|
||||
groupRoomId: PropTypes.string,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
isUserPrivilegedInGroup: null,
|
||||
groupRoom: null,
|
||||
groupRoomPublicityLoading: false,
|
||||
groupRoomRemoveLoading: false,
|
||||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
this._initGroupStore(this.props.groupId);
|
||||
},
|
||||
|
||||
componentWillReceiveProps(newProps) {
|
||||
if (newProps.groupId !== this.props.groupId) {
|
||||
this._unregisterGroupStore();
|
||||
this._initGroupStore(newProps.groupId);
|
||||
}
|
||||
},
|
||||
|
||||
componentWillUnmount() {
|
||||
this._unregisterGroupStore();
|
||||
},
|
||||
|
||||
_initGroupStore(groupId) {
|
||||
this._groupStore = GroupStoreCache.getGroupStore(this.props.groupId);
|
||||
this._groupStore.registerListener(this.onGroupStoreUpdated);
|
||||
},
|
||||
|
||||
_unregisterGroupStore() {
|
||||
if (this._groupStore) {
|
||||
this._groupStore.unregisterListener(this.onGroupStoreUpdated);
|
||||
}
|
||||
},
|
||||
|
||||
_updateGroupRoom() {
|
||||
this.setState({
|
||||
groupRoom: this._groupStore.getGroupRooms().find(
|
||||
(r) => r.roomId === this.props.groupRoomId,
|
||||
),
|
||||
});
|
||||
},
|
||||
|
||||
onGroupStoreUpdated: function() {
|
||||
this.setState({
|
||||
isUserPrivilegedInGroup: this._groupStore.isUserPrivileged(),
|
||||
});
|
||||
this._updateGroupRoom();
|
||||
},
|
||||
|
||||
_onRemove: function(e) {
|
||||
const groupId = this.props.groupId;
|
||||
const roomName = this.state.groupRoom.displayname;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
Modal.createTrackedDialog('Confirm removal of group from room', '', QuestionDialog, {
|
||||
title: _t("Are you sure you want to remove '%(roomName)s' from %(groupId)s?", {roomName, groupId}),
|
||||
description: _t("Removing a room from the community will also remove it from the community page."),
|
||||
button: _t("Remove"),
|
||||
onFinished: (proceed) => {
|
||||
if (!proceed) return;
|
||||
this.setState({groupRoomRemoveLoading: true});
|
||||
const groupId = this.props.groupId;
|
||||
const roomId = this.props.groupRoomId;
|
||||
this._groupStore.removeRoomFromGroup(roomId).then(() => {
|
||||
dis.dispatch({
|
||||
action: "view_group_room_list",
|
||||
});
|
||||
}).catch((err) => {
|
||||
console.error(`Error whilst removing ${roomId} from ${groupId}`, err);
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createTrackedDialog('Failed to remove room from group', '', ErrorDialog, {
|
||||
title: _t("Failed to remove room from community"),
|
||||
description: _t(
|
||||
"Failed to remove '%(roomName)s' from %(groupId)s", {groupId, roomName},
|
||||
),
|
||||
});
|
||||
}).finally(() => {
|
||||
this.setState({groupRoomRemoveLoading: false});
|
||||
});
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
_onCancel: function(e) {
|
||||
dis.dispatch({
|
||||
action: "view_group_room_list",
|
||||
});
|
||||
},
|
||||
|
||||
_changeGroupRoomPublicity(e) {
|
||||
const isPublic = e.target.value === "public";
|
||||
this.setState({
|
||||
groupRoomPublicityLoading: true,
|
||||
});
|
||||
const groupId = this.props.groupId;
|
||||
const roomId = this.props.groupRoomId;
|
||||
const roomName = this.state.groupRoom.displayname;
|
||||
this._groupStore.updateGroupRoomVisibility(roomId, isPublic).catch((err) => {
|
||||
console.error(`Error whilst changing visibility of ${roomId} in ${groupId} to ${isPublic}`, err);
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createTrackedDialog('Failed to remove room from group', '', ErrorDialog, {
|
||||
title: _t("Something went wrong!"),
|
||||
description: _t(
|
||||
"The visibility of '%(roomName)s' in %(groupId)s could not be updated.",
|
||||
{roomName, groupId},
|
||||
),
|
||||
});
|
||||
}).finally(() => {
|
||||
this.setState({
|
||||
groupRoomPublicityLoading: false,
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
|
||||
const EmojiText = sdk.getComponent('elements.EmojiText');
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
const InlineSpinner = sdk.getComponent('elements.InlineSpinner');
|
||||
if (this.state.groupRoomRemoveLoading || !this.state.groupRoom) {
|
||||
const Spinner = sdk.getComponent("elements.Spinner");
|
||||
return <div className="mx_MemberInfo">
|
||||
<Spinner />
|
||||
</div>;
|
||||
}
|
||||
|
||||
let adminTools;
|
||||
if (this.state.isUserPrivilegedInGroup) {
|
||||
adminTools =
|
||||
<div className="mx_MemberInfo_adminTools">
|
||||
<h3>{ _t("Admin Tools") }</h3>
|
||||
<div className="mx_MemberInfo_buttons">
|
||||
<AccessibleButton className="mx_MemberInfo_field" onClick={this._onRemove}>
|
||||
{ _t('Remove from community') }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
<h3>
|
||||
{ _t('Visibility in Room List') }
|
||||
{ this.state.groupRoomPublicityLoading ?
|
||||
<InlineSpinner /> : <div />
|
||||
}
|
||||
</h3>
|
||||
<div>
|
||||
<label>
|
||||
<input type="radio"
|
||||
value="public"
|
||||
checked={this.state.groupRoom.isPublic}
|
||||
onClick={this._changeGroupRoomPublicity}
|
||||
/>
|
||||
<div className="mx_MemberInfo_label_text">
|
||||
{ _t('Visible to everyone') }
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label>
|
||||
<input type="radio"
|
||||
value="private"
|
||||
checked={!this.state.groupRoom.isPublic}
|
||||
onClick={this._changeGroupRoomPublicity}
|
||||
/>
|
||||
<div className="mx_MemberInfo_label_text">
|
||||
{ _t('Only visible to community members') }
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
const avatarUrl = this.context.matrixClient.mxcUrlToHttp(
|
||||
this.state.groupRoom.avatarUrl,
|
||||
36, 36, 'crop',
|
||||
);
|
||||
|
||||
const groupRoomName = this.state.groupRoom.displayname;
|
||||
const avatar = <BaseAvatar name={groupRoomName} width={36} height={36} url={avatarUrl} />;
|
||||
return (
|
||||
<div className="mx_MemberInfo">
|
||||
<GeminiScrollbar autoshow={true}>
|
||||
<AccessibleButton className="mx_MemberInfo_cancel" onClick={this._onCancel}>
|
||||
<img src="img/cancel.svg" width="18" height="18" className="mx_filterFlipColor" />
|
||||
</AccessibleButton>
|
||||
<div className="mx_MemberInfo_avatar">
|
||||
{ avatar }
|
||||
</div>
|
||||
|
||||
<EmojiText element="h2">{ groupRoomName }</EmojiText>
|
||||
|
||||
<div className="mx_MemberInfo_profile">
|
||||
<div className="mx_MemberInfo_profileField">
|
||||
{ this.state.groupRoom.canonical_alias }
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{ adminTools }
|
||||
</GeminiScrollbar>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
136
src/components/views/groups/GroupRoomList.js
Normal file
136
src/components/views/groups/GroupRoomList.js
Normal file
|
@ -0,0 +1,136 @@
|
|||
/*
|
||||
Copyright 2017 New Vector 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.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import sdk from '../../../index';
|
||||
import GroupStoreCache from '../../../stores/GroupStoreCache';
|
||||
import GeminiScrollbar from 'react-gemini-scrollbar';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const INITIAL_LOAD_NUM_ROOMS = 30;
|
||||
|
||||
export default React.createClass({
|
||||
propTypes: {
|
||||
groupId: PropTypes.string.isRequired,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
rooms: null,
|
||||
truncateAt: INITIAL_LOAD_NUM_ROOMS,
|
||||
searchQuery: "",
|
||||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
this._unmounted = false;
|
||||
this._initGroupStore(this.props.groupId);
|
||||
},
|
||||
|
||||
_initGroupStore: function(groupId) {
|
||||
this._groupStore = GroupStoreCache.getGroupStore(groupId);
|
||||
this._groupStore.registerListener(() => {
|
||||
this._fetchRooms();
|
||||
});
|
||||
this._groupStore.on('error', (err) => {
|
||||
this.setState({
|
||||
rooms: null,
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
_fetchRooms: function() {
|
||||
if (this._unmounted) return;
|
||||
this.setState({
|
||||
rooms: this._groupStore.getGroupRooms(),
|
||||
});
|
||||
},
|
||||
|
||||
_createOverflowTile: function(overflowCount, totalCount) {
|
||||
// For now we'll pretend this is any entity. It should probably be a separate tile.
|
||||
const EntityTile = sdk.getComponent("rooms.EntityTile");
|
||||
const BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
|
||||
const text = _t("and %(count)s others...", { count: overflowCount });
|
||||
return (
|
||||
<EntityTile className="mx_EntityTile_ellipsis" avatarJsx={
|
||||
<BaseAvatar url="img/ellipsis.svg" name="..." width={36} height={36} />
|
||||
} name={text} presenceState="online" suppressOnHover={true}
|
||||
onClick={this._showFullRoomList} />
|
||||
);
|
||||
},
|
||||
|
||||
_showFullRoomList: function() {
|
||||
this.setState({
|
||||
truncateAt: -1,
|
||||
});
|
||||
},
|
||||
|
||||
onSearchQueryChanged: function(ev) {
|
||||
this.setState({ searchQuery: ev.target.value });
|
||||
},
|
||||
|
||||
makeGroupRoomTiles: function(query) {
|
||||
const GroupRoomTile = sdk.getComponent("groups.GroupRoomTile");
|
||||
query = (query || "").toLowerCase();
|
||||
|
||||
let roomList = this.state.rooms;
|
||||
if (query) {
|
||||
roomList = roomList.filter((room) => {
|
||||
const matchesName = (room.name || "").toLowerCase().includes(query);
|
||||
const matchesAlias = (room.canonicalAlias || "").toLowerCase().includes(query);
|
||||
return matchesName || matchesAlias;
|
||||
});
|
||||
}
|
||||
|
||||
roomList = roomList.map((groupRoom, index) => {
|
||||
return (
|
||||
<GroupRoomTile
|
||||
key={index}
|
||||
groupId={this.props.groupId}
|
||||
groupRoom={groupRoom} />
|
||||
);
|
||||
});
|
||||
|
||||
return roomList;
|
||||
},
|
||||
|
||||
render: function() {
|
||||
if (this.state.rooms === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const inputBox = (
|
||||
<form autoComplete="off">
|
||||
<input className="mx_GroupRoomList_query" id="mx_GroupRoomList_query" type="text"
|
||||
onChange={this.onSearchQueryChanged} value={this.state.searchQuery}
|
||||
placeholder={_t('Filter community rooms')} />
|
||||
</form>
|
||||
);
|
||||
|
||||
const TruncatedList = sdk.getComponent("elements.TruncatedList");
|
||||
return (
|
||||
<div className="mx_GroupRoomList">
|
||||
{ inputBox }
|
||||
<GeminiScrollbar autoshow={true} className="mx_GroupRoomList_joined mx_GroupRoomList_outerWrapper">
|
||||
<TruncatedList className="mx_GroupRoomList_wrapper" truncateAt={this.state.truncateAt}
|
||||
createOverflowElement={this._createOverflowTile}>
|
||||
{ this.makeGroupRoomTiles(this.state.searchQuery) }
|
||||
</TruncatedList>
|
||||
</GeminiScrollbar>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
73
src/components/views/groups/GroupRoomTile.js
Normal file
73
src/components/views/groups/GroupRoomTile.js
Normal file
|
@ -0,0 +1,73 @@
|
|||
/*
|
||||
Copyright 2017 New Vector 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.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import {MatrixClient} from 'matrix-js-sdk';
|
||||
import PropTypes from 'prop-types';
|
||||
import sdk from '../../../index';
|
||||
import dis from '../../../dispatcher';
|
||||
import { GroupRoomType } from '../../../groups';
|
||||
|
||||
const GroupRoomTile = React.createClass({
|
||||
displayName: 'GroupRoomTile',
|
||||
|
||||
propTypes: {
|
||||
groupId: PropTypes.string.isRequired,
|
||||
groupRoom: GroupRoomType.isRequired,
|
||||
},
|
||||
|
||||
onClick: function(e) {
|
||||
dis.dispatch({
|
||||
action: 'view_group_room',
|
||||
groupId: this.props.groupId,
|
||||
groupRoomId: this.props.groupRoom.roomId,
|
||||
});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
const avatarUrl = this.context.matrixClient.mxcUrlToHttp(
|
||||
this.props.groupRoom.avatarUrl,
|
||||
36, 36, 'crop',
|
||||
);
|
||||
|
||||
const av = (
|
||||
<BaseAvatar name={this.props.groupRoom.displayname}
|
||||
width={36} height={36}
|
||||
url={avatarUrl}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<AccessibleButton className="mx_GroupRoomTile" onClick={this.onClick}>
|
||||
<div className="mx_GroupRoomTile_avatar">
|
||||
{ av }
|
||||
</div>
|
||||
<div className="mx_GroupRoomTile_name">
|
||||
{ this.props.groupRoom.displayname }
|
||||
</div>
|
||||
</AccessibleButton>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
GroupRoomTile.contextTypes = {
|
||||
matrixClient: React.PropTypes.instanceOf(MatrixClient).isRequired,
|
||||
};
|
||||
|
||||
|
||||
export default GroupRoomTile;
|
93
src/components/views/groups/GroupTile.js
Normal file
93
src/components/views/groups/GroupTile.js
Normal file
|
@ -0,0 +1,93 @@
|
|||
/*
|
||||
Copyright 2017 Vector Creations 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.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {MatrixClient} from 'matrix-js-sdk';
|
||||
import sdk from '../../../index';
|
||||
import dis from '../../../dispatcher';
|
||||
import FlairStore from '../../../stores/FlairStore';
|
||||
|
||||
const GroupTile = React.createClass({
|
||||
displayName: 'GroupTile',
|
||||
|
||||
propTypes: {
|
||||
groupId: PropTypes.string.isRequired,
|
||||
// Whether to show the short description of the group on the tile
|
||||
showDescription: PropTypes.bool,
|
||||
// Height of the group avatar in pixels
|
||||
avatarHeight: PropTypes.number,
|
||||
},
|
||||
|
||||
contextTypes: {
|
||||
matrixClient: React.PropTypes.instanceOf(MatrixClient).isRequired,
|
||||
},
|
||||
|
||||
getInitialState() {
|
||||
return {
|
||||
profile: null,
|
||||
};
|
||||
},
|
||||
|
||||
getDefaultProps() {
|
||||
return {
|
||||
showDescription: true,
|
||||
avatarHeight: 50,
|
||||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
FlairStore.getGroupProfileCached(this.context.matrixClient, this.props.groupId).then((profile) => {
|
||||
this.setState({profile});
|
||||
}).catch((err) => {
|
||||
console.error('Error whilst getting cached profile for GroupTile', err);
|
||||
});
|
||||
},
|
||||
|
||||
onClick: function(e) {
|
||||
e.preventDefault();
|
||||
dis.dispatch({
|
||||
action: 'view_group',
|
||||
group_id: this.props.groupId,
|
||||
});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
const profile = this.state.profile || {};
|
||||
const name = profile.name || this.props.groupId;
|
||||
const avatarHeight = this.props.avatarHeight;
|
||||
const descElement = this.props.showDescription ?
|
||||
<div className="mx_GroupTile_desc">{ profile.shortDescription }</div> :
|
||||
<div />;
|
||||
const httpUrl = profile.avatarUrl ? this.context.matrixClient.mxcUrlToHttp(
|
||||
profile.avatarUrl, avatarHeight, avatarHeight, "crop",
|
||||
) : null;
|
||||
return <AccessibleButton className="mx_GroupTile" onClick={this.onClick}>
|
||||
<div className="mx_GroupTile_avatar">
|
||||
<BaseAvatar name={name} url={httpUrl} width={avatarHeight} height={avatarHeight} />
|
||||
</div>
|
||||
<div className="mx_GroupTile_profile">
|
||||
<div className="mx_GroupTile_name">{ name }</div>
|
||||
{ descElement }
|
||||
<div className="mx_GroupTile_groupId">{ this.props.groupId }</div>
|
||||
</div>
|
||||
</AccessibleButton>;
|
||||
},
|
||||
});
|
||||
|
||||
export default GroupTile;
|
89
src/components/views/groups/GroupUserSettings.js
Normal file
89
src/components/views/groups/GroupUserSettings.js
Normal file
|
@ -0,0 +1,89 @@
|
|||
/*
|
||||
Copyright 2017 New Vector 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.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import GeminiScrollbar from 'react-gemini-scrollbar';
|
||||
import sdk from '../../../index';
|
||||
import { MatrixClient } from 'matrix-js-sdk';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
export default React.createClass({
|
||||
displayName: 'GroupUserSettings',
|
||||
|
||||
contextTypes: {
|
||||
matrixClient: PropTypes.instanceOf(MatrixClient),
|
||||
},
|
||||
|
||||
getInitialState() {
|
||||
return {
|
||||
error: null,
|
||||
groups: null,
|
||||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
this.context.matrixClient.getJoinedGroups().done((result) => {
|
||||
this.setState({groups: result.groups || [], error: null});
|
||||
}, (err) => {
|
||||
console.error(err);
|
||||
this.setState({groups: null, error: err});
|
||||
});
|
||||
},
|
||||
|
||||
_renderGroupPublicity() {
|
||||
let text = "";
|
||||
let scrollbox = <div />;
|
||||
const groups = this.state.groups;
|
||||
|
||||
if (this.state.error) {
|
||||
text = _t('Something went wrong when trying to get your communities.');
|
||||
} else if (groups === null) {
|
||||
text = _t('Loading...');
|
||||
} else if (groups.length > 0) {
|
||||
const GroupPublicityToggle = sdk.getComponent('groups.GroupPublicityToggle');
|
||||
const groupPublicityToggles = groups.map((groupId, index) => {
|
||||
return <GroupPublicityToggle key={index} groupId={groupId} />;
|
||||
});
|
||||
text = _t('Display your community flair in rooms configured to show it.');
|
||||
scrollbox = <div className="mx_GroupUserSettings_groupPublicity_scrollbox">
|
||||
<GeminiScrollbar>
|
||||
{ groupPublicityToggles }
|
||||
</GeminiScrollbar>
|
||||
</div>;
|
||||
} else {
|
||||
text = _t("You're not currently a member of any communities.");
|
||||
}
|
||||
|
||||
return <div>
|
||||
<h3>{ _t('Flair') }</h3>
|
||||
<div className="mx_UserSettings_section">
|
||||
<p>
|
||||
{ text }
|
||||
</p>
|
||||
{ scrollbox }
|
||||
</div>
|
||||
</div>;
|
||||
},
|
||||
|
||||
render() {
|
||||
const groupPublicity = this._renderGroupPublicity();
|
||||
|
||||
return <div>
|
||||
{ groupPublicity }
|
||||
</div>;
|
||||
},
|
||||
});
|
|
@ -18,9 +18,9 @@ limitations under the License.
|
|||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { _t, _tJsx } from '../../../languageHandler';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
var DIV_ID = 'mx_recaptcha';
|
||||
const DIV_ID = 'mx_recaptcha';
|
||||
|
||||
/**
|
||||
* A pure UI component which displays a captcha form.
|
||||
|
@ -60,23 +60,22 @@ module.exports = React.createClass({
|
|||
} else {
|
||||
console.log("Loading recaptcha script...");
|
||||
window.mx_on_recaptcha_loaded = () => {this._onCaptchaLoaded();};
|
||||
var protocol = global.location.protocol;
|
||||
const protocol = global.location.protocol;
|
||||
if (protocol === "file:") {
|
||||
var warning = document.createElement('div');
|
||||
const warning = document.createElement('div');
|
||||
// XXX: fix hardcoded app URL. Better solutions include:
|
||||
// * jumping straight to a hosted captcha page (but we don't support that yet)
|
||||
// * embedding the captcha in an iframe (if that works)
|
||||
// * using a better captcha lib
|
||||
ReactDOM.render(_tJsx(
|
||||
ReactDOM.render(_t(
|
||||
"Robot check is currently unavailable on desktop - please use a <a>web browser</a>",
|
||||
/<a>(.*?)<\/a>/,
|
||||
(sub) => { return <a href='https://riot.im/app'>{ sub }</a>; }), warning);
|
||||
{},
|
||||
{ 'a': (sub) => { return <a href='https://riot.im/app'>{ sub }</a>; }}), warning);
|
||||
this.refs.recaptchaContainer.appendChild(warning);
|
||||
}
|
||||
else {
|
||||
var scriptTag = document.createElement('script');
|
||||
} else {
|
||||
const scriptTag = document.createElement('script');
|
||||
scriptTag.setAttribute(
|
||||
'src', protocol+"//www.google.com/recaptcha/api.js?onload=mx_on_recaptcha_loaded&render=explicit"
|
||||
'src', protocol+"//www.google.com/recaptcha/api.js?onload=mx_on_recaptcha_loaded&render=explicit",
|
||||
);
|
||||
this.refs.recaptchaContainer.appendChild(scriptTag);
|
||||
}
|
||||
|
@ -93,7 +92,7 @@ module.exports = React.createClass({
|
|||
throw new Error("Recaptcha did not load successfully");
|
||||
}
|
||||
|
||||
var publicKey = this.props.sitePublicKey;
|
||||
const publicKey = this.props.sitePublicKey;
|
||||
if (!publicKey) {
|
||||
console.error("No public key for recaptcha!");
|
||||
throw new Error(
|
||||
|
@ -130,18 +129,18 @@ module.exports = React.createClass({
|
|||
if (this.state.errorText) {
|
||||
error = (
|
||||
<div className="error">
|
||||
{this.state.errorText}
|
||||
{ this.state.errorText }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div ref="recaptchaContainer">
|
||||
{_t("This Home Server would like to make sure you are not a robot")}
|
||||
<br/>
|
||||
{ _t("This Home Server would like to make sure you are not a robot") }
|
||||
<br />
|
||||
<div id={DIV_ID}></div>
|
||||
{error}
|
||||
{ error }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -29,9 +29,9 @@ module.exports = React.createClass({
|
|||
render: function() {
|
||||
return (
|
||||
<div>
|
||||
<button onClick={this.props.onSubmit}>{_t("Sign in with CAS")}</button>
|
||||
<button onClick={this.props.onSubmit}>{ _t("Sign in with CAS") }</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
});
|
||||
|
|
|
@ -69,7 +69,7 @@ export default class CountryDropdown extends React.Component {
|
|||
}
|
||||
|
||||
_flagImgForIso2(iso2) {
|
||||
return <img src={`flags/${iso2}.png`}/>;
|
||||
return <img src={`flags/${iso2}.png`} />;
|
||||
}
|
||||
|
||||
_getShortOption(iso2) {
|
||||
|
@ -111,8 +111,8 @@ export default class CountryDropdown extends React.Component {
|
|||
|
||||
const options = displayedCountries.map((country) => {
|
||||
return <div key={country.iso2}>
|
||||
{this._flagImgForIso2(country.iso2)}
|
||||
{country.name} <span>(+{country.prefix})</span>
|
||||
{ this._flagImgForIso2(country.iso2) }
|
||||
{ country.name } <span>(+{ country.prefix })</span>
|
||||
</div>;
|
||||
});
|
||||
|
||||
|
@ -123,9 +123,9 @@ export default class CountryDropdown extends React.Component {
|
|||
return <Dropdown className={this.props.className + " left_aligned"}
|
||||
onOptionChange={this._onOptionChange} onSearchChange={this._onSearchChange}
|
||||
menuWidth={298} getShortOption={this._getShortOption}
|
||||
value={value} searchEnabled={true}
|
||||
value={value} searchEnabled={true} disabled={this.props.disabled}
|
||||
>
|
||||
{options}
|
||||
{ options }
|
||||
</Dropdown>;
|
||||
}
|
||||
}
|
||||
|
@ -137,4 +137,5 @@ CountryDropdown.propTypes = {
|
|||
showPrefix: React.PropTypes.bool,
|
||||
onOptionChange: React.PropTypes.func.isRequired,
|
||||
value: React.PropTypes.string,
|
||||
disabled: React.PropTypes.bool,
|
||||
};
|
||||
|
|
|
@ -24,27 +24,27 @@ module.exports = React.createClass({
|
|||
return (
|
||||
<div className="mx_ErrorDialog">
|
||||
<div className="mx_Dialog_title">
|
||||
{_t("Custom Server Options")}
|
||||
{ _t("Custom Server Options") }
|
||||
</div>
|
||||
<div className="mx_Dialog_content">
|
||||
<span>
|
||||
{_t("You can use the custom server options to sign into other Matrix " +
|
||||
"servers by specifying a different Home server URL.")}
|
||||
<br/>
|
||||
{_t("This allows you to use this app with an existing Matrix account on " +
|
||||
"a different home server.")}
|
||||
<br/>
|
||||
<br/>
|
||||
{_t("You can also set a custom identity server but this will typically prevent " +
|
||||
"interaction with users based on email address.")}
|
||||
{ _t("You can use the custom server options to sign into other Matrix " +
|
||||
"servers by specifying a different Home server URL.") }
|
||||
<br />
|
||||
{ _t("This allows you to use this app with an existing Matrix account on " +
|
||||
"a different home server.") }
|
||||
<br />
|
||||
<br />
|
||||
{ _t("You can also set a custom identity server but this will typically prevent " +
|
||||
"interaction with users based on email address.") }
|
||||
</span>
|
||||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button onClick={this.props.onFinished} autoFocus={true}>
|
||||
{_t("Dismiss")}
|
||||
{ _t("Dismiss") }
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -129,8 +129,8 @@ export const PasswordAuthEntry = React.createClass({
|
|||
|
||||
return (
|
||||
<div>
|
||||
<p>{_t("To continue, please enter your password.")}</p>
|
||||
<p>{_t("Password:")}</p>
|
||||
<p>{ _t("To continue, please enter your password.") }</p>
|
||||
<p>{ _t("Password:") }</p>
|
||||
<form onSubmit={this._onSubmit}>
|
||||
<input
|
||||
ref="passwordField"
|
||||
|
@ -139,11 +139,11 @@ export const PasswordAuthEntry = React.createClass({
|
|||
type="password"
|
||||
/>
|
||||
<div className="mx_button_row">
|
||||
{submitButtonOrSpinner}
|
||||
{ submitButtonOrSpinner }
|
||||
</div>
|
||||
</form>
|
||||
<div className="error">
|
||||
{this.props.errorText}
|
||||
{ this.props.errorText }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -178,14 +178,14 @@ export const RecaptchaAuthEntry = React.createClass({
|
|||
}
|
||||
|
||||
const CaptchaForm = sdk.getComponent("views.login.CaptchaForm");
|
||||
var sitePublicKey = this.props.stageParams.public_key;
|
||||
const sitePublicKey = this.props.stageParams.public_key;
|
||||
return (
|
||||
<div>
|
||||
<CaptchaForm sitePublicKey={sitePublicKey}
|
||||
onCaptchaResponse={this._onCaptchaResponse}
|
||||
/>
|
||||
<div className="error">
|
||||
{this.props.errorText}
|
||||
{ this.props.errorText }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -256,8 +256,11 @@ export const EmailIdentityAuthEntry = React.createClass({
|
|||
} else {
|
||||
return (
|
||||
<div>
|
||||
<p>{_t("An email has been sent to")} <i>{this.props.inputs.emailAddress}</i></p>
|
||||
<p>{_t("Please check your email to continue registration.")}</p>
|
||||
<p>{ _t("An email has been sent to %(emailAddress)s",
|
||||
{ emailAddress: (sub) => <i>{ this.props.inputs.emailAddress }</i> },
|
||||
) }
|
||||
</p>
|
||||
<p>{ _t("Please check your email to continue registration.") }</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -333,12 +336,12 @@ export const MsisdnAuthEntry = React.createClass({
|
|||
});
|
||||
|
||||
this.props.matrixClient.submitMsisdnToken(
|
||||
this._sid, this.props.clientSecret, this.state.token
|
||||
this._sid, this.props.clientSecret, this.state.token,
|
||||
).then((result) => {
|
||||
if (result.success) {
|
||||
const idServerParsedUrl = url.parse(
|
||||
this.props.matrixClient.getIdentityServerUrl(),
|
||||
)
|
||||
);
|
||||
this.props.submitAuthDict({
|
||||
type: MsisdnAuthEntry.LOGIN_TYPE,
|
||||
threepid_creds: {
|
||||
|
@ -370,8 +373,11 @@ export const MsisdnAuthEntry = React.createClass({
|
|||
});
|
||||
return (
|
||||
<div>
|
||||
<p>{_t("A text message has been sent to")} +<i>{this._msisdn}</i></p>
|
||||
<p>{_t("Please enter the code it contains:")}</p>
|
||||
<p>{ _t("A text message has been sent to %(msisdn)s",
|
||||
{ msisdn: <i>{ this._msisdn }</i> },
|
||||
) }
|
||||
</p>
|
||||
<p>{ _t("Please enter the code it contains:") }</p>
|
||||
<div className="mx_InteractiveAuthEntryComponents_msisdnWrapper">
|
||||
<form onSubmit={this._onFormSubmit}>
|
||||
<input type="text"
|
||||
|
@ -386,7 +392,7 @@ export const MsisdnAuthEntry = React.createClass({
|
|||
/>
|
||||
</form>
|
||||
<div className="error">
|
||||
{this.state.errorText}
|
||||
{ this.state.errorText }
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -421,9 +427,9 @@ export const FallbackAuthEntry = React.createClass({
|
|||
},
|
||||
|
||||
_onShowFallbackClick: function() {
|
||||
var url = this.props.matrixClient.getFallbackAuthUrl(
|
||||
const url = this.props.matrixClient.getFallbackAuthUrl(
|
||||
this.props.loginType,
|
||||
this.props.authSessionId
|
||||
this.props.authSessionId,
|
||||
);
|
||||
this._popupWindow = window.open(url);
|
||||
},
|
||||
|
@ -440,9 +446,9 @@ export const FallbackAuthEntry = React.createClass({
|
|||
render: function() {
|
||||
return (
|
||||
<div>
|
||||
<a onClick={this._onShowFallbackClick}>{_t("Start authentication")}</a>
|
||||
<a onClick={this._onShowFallbackClick}>{ _t("Start authentication") }</a>
|
||||
<div className="error">
|
||||
{this.props.errorText}
|
||||
{ this.props.errorText }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -457,7 +463,7 @@ const AuthEntryComponents = [
|
|||
];
|
||||
|
||||
export function getEntryComponentForLoginType(loginType) {
|
||||
for (var c of AuthEntryComponents) {
|
||||
for (const c of AuthEntryComponents) {
|
||||
if (c.LOGIN_TYPE == loginType) {
|
||||
return c;
|
||||
}
|
||||
|
|
|
@ -25,7 +25,7 @@ module.exports = React.createClass({
|
|||
render: function() {
|
||||
return (
|
||||
<div className="mx_Login_links">
|
||||
<a href="https://matrix.org">{_t("powered by Matrix")}</a>
|
||||
<a href="https://matrix.org">{ _t("powered by Matrix") }</a>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -16,7 +16,7 @@ limitations under the License.
|
|||
|
||||
'use strict';
|
||||
|
||||
var React = require('react');
|
||||
const React = require('react');
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'LoginHeader',
|
||||
|
@ -27,5 +27,5 @@ module.exports = React.createClass({
|
|||
Matrix
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
59
src/components/views/login/LoginPage.js
Normal file
59
src/components/views/login/LoginPage.js
Normal file
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
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';
|
||||
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
|
||||
const React = require('react');
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'LoginPage',
|
||||
|
||||
render: function() {
|
||||
// FIXME: this should be turned into a proper skin with a StatusLoginPage component
|
||||
if (SettingsStore.getValue("theme") === 'status') {
|
||||
return (
|
||||
<div className="mx_StatusLogin">
|
||||
<div className="mx_StatusLogin_brand">
|
||||
<img src="themes/status/img/logo.svg" alt="Status" width="221" height="53" />
|
||||
</div>
|
||||
<div className="mx_StatusLogin_content">
|
||||
<div className="mx_StatusLogin_header">
|
||||
<h1>Status Community Chat</h1>
|
||||
<div className="mx_StatusLogin_subtitle">
|
||||
A safer, decentralised communication
|
||||
platform <a href="https://riot.im">powered by Riot</a>
|
||||
</div>
|
||||
</div>
|
||||
{ this.props.children }
|
||||
<div className="mx_StatusLogin_footer">
|
||||
<p>This channel is for our development community.</p>
|
||||
<p>Interested in SNT and discussions on the cryptocurrency market?</p>
|
||||
<p><a href="https://t.me/StatusNetworkChat" target="_blank" className="mx_StatusLogin_footer_cta">Join Telegram Chat</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div className="mx_Login">
|
||||
{ this.props.children }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
|
@ -20,7 +20,7 @@ import classNames from 'classnames';
|
|||
import sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import {field_input_incorrect} from '../../../UiEffects';
|
||||
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
|
||||
/**
|
||||
* A pure UI component which displays a username/password form.
|
||||
|
@ -94,7 +94,7 @@ class PasswordLogin extends React.Component {
|
|||
onLoginTypeChange(loginType) {
|
||||
this.setState({
|
||||
loginType: loginType,
|
||||
username: "" // Reset because email and username use the same state
|
||||
username: "", // Reset because email and username use the same state
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -116,11 +116,17 @@ class PasswordLogin extends React.Component {
|
|||
this.props.onPasswordChanged(ev.target.value);
|
||||
}
|
||||
|
||||
renderLoginField(loginType) {
|
||||
switch(loginType) {
|
||||
renderLoginField(loginType, disabled) {
|
||||
const classes = {
|
||||
mx_Login_field: true,
|
||||
mx_Login_field_disabled: disabled,
|
||||
};
|
||||
|
||||
switch (loginType) {
|
||||
case PasswordLogin.LOGIN_FIELD_EMAIL:
|
||||
classes.mx_Login_email = true;
|
||||
return <input
|
||||
className="mx_Login_field mx_Login_email"
|
||||
className={classNames(classes)}
|
||||
key="email_input"
|
||||
type="text"
|
||||
name="username" // make it a little easier for browser's remember-password
|
||||
|
@ -128,20 +134,28 @@ class PasswordLogin extends React.Component {
|
|||
placeholder="joe@example.com"
|
||||
value={this.state.username}
|
||||
autoFocus
|
||||
disabled={disabled}
|
||||
/>;
|
||||
case PasswordLogin.LOGIN_FIELD_MXID:
|
||||
classes.mx_Login_username = true;
|
||||
return <input
|
||||
className="mx_Login_field mx_Login_username"
|
||||
className={classNames(classes)}
|
||||
key="username_input"
|
||||
type="text"
|
||||
name="username" // make it a little easier for browser's remember-password
|
||||
onChange={this.onUsernameChanged}
|
||||
placeholder={_t('User name')}
|
||||
placeholder={SdkConfig.get().disable_custom_urls ?
|
||||
_t("Username on %(hs)s", {
|
||||
hs: this.props.hsUrl.replace(/^https?:\/\//, ''),
|
||||
}) : _t("User name")}
|
||||
value={this.state.username}
|
||||
autoFocus
|
||||
disabled={disabled}
|
||||
/>;
|
||||
case PasswordLogin.LOGIN_FIELD_PHONE:
|
||||
const CountryDropdown = sdk.getComponent('views.login.CountryDropdown');
|
||||
classes.mx_Login_phoneNumberField = true;
|
||||
classes.mx_Login_field_has_prefix = true;
|
||||
return <div className="mx_Login_phoneSection">
|
||||
<CountryDropdown
|
||||
className="mx_Login_phoneCountry mx_Login_field_prefix"
|
||||
|
@ -150,9 +164,10 @@ class PasswordLogin extends React.Component {
|
|||
value={this.state.phoneCountry}
|
||||
isSmall={true}
|
||||
showPrefix={true}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<input
|
||||
className="mx_Login_phoneNumberField mx_Login_field mx_Login_field_has_prefix"
|
||||
className={classNames(classes)}
|
||||
ref="phoneNumber"
|
||||
key="phone_input"
|
||||
type="text"
|
||||
|
@ -161,13 +176,14 @@ class PasswordLogin extends React.Component {
|
|||
placeholder={_t("Mobile phone number")}
|
||||
value={this.state.phoneNumber}
|
||||
autoFocus
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
var forgotPasswordJsx;
|
||||
let forgotPasswordJsx;
|
||||
|
||||
if (this.props.onForgotPasswordClick) {
|
||||
forgotPasswordJsx = (
|
||||
|
@ -177,37 +193,58 @@ class PasswordLogin extends React.Component {
|
|||
);
|
||||
}
|
||||
|
||||
let matrixIdText = '';
|
||||
if (this.props.hsUrl) {
|
||||
try {
|
||||
const parsedHsUrl = new URL(this.props.hsUrl);
|
||||
matrixIdText = _t('%(serverName)s Matrix ID', {serverName: parsedHsUrl.hostname});
|
||||
} catch (e) {
|
||||
// pass
|
||||
}
|
||||
}
|
||||
|
||||
const pwFieldClass = classNames({
|
||||
mx_Login_field: true,
|
||||
mx_Login_field_disabled: matrixIdText === '',
|
||||
error: this.props.loginIncorrect,
|
||||
});
|
||||
|
||||
const Dropdown = sdk.getComponent('elements.Dropdown');
|
||||
|
||||
const loginField = this.renderLoginField(this.state.loginType);
|
||||
const loginField = this.renderLoginField(this.state.loginType, matrixIdText === '');
|
||||
|
||||
return (
|
||||
<div>
|
||||
<form onSubmit={this.onSubmitForm}>
|
||||
let loginType;
|
||||
if (!SdkConfig.get().disable_3pid_login) {
|
||||
loginType = (
|
||||
<div className="mx_Login_type_container">
|
||||
<label className="mx_Login_type_label">{ _t('Sign in with') }</label>
|
||||
<Dropdown
|
||||
className="mx_Login_type_dropdown"
|
||||
value={this.state.loginType}
|
||||
disabled={matrixIdText === ''}
|
||||
onOptionChange={this.onLoginTypeChange}>
|
||||
<span key={PasswordLogin.LOGIN_FIELD_MXID}>{ _t('my Matrix ID') }</span>
|
||||
<span key={PasswordLogin.LOGIN_FIELD_MXID}>{ matrixIdText }</span>
|
||||
<span key={PasswordLogin.LOGIN_FIELD_EMAIL}>{ _t('Email address') }</span>
|
||||
<span key={PasswordLogin.LOGIN_FIELD_PHONE}>{ _t('Phone') }</span>
|
||||
</Dropdown>
|
||||
</div>
|
||||
{loginField}
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<form onSubmit={this.onSubmitForm}>
|
||||
{ loginType }
|
||||
{ loginField }
|
||||
<input className={pwFieldClass} ref={(e) => {this._passwordField = e;}} type="password"
|
||||
name="password"
|
||||
value={this.state.password} onChange={this.onPasswordChanged}
|
||||
placeholder={ _t('Password') } />
|
||||
placeholder={_t('Password')}
|
||||
disabled={matrixIdText === ''}
|
||||
/>
|
||||
<br />
|
||||
{forgotPasswordJsx}
|
||||
<input className="mx_Login_submit" type="submit" value={ _t('Sign in') } />
|
||||
{ forgotPasswordJsx }
|
||||
<input className="mx_Login_submit" type="submit" value={_t('Sign in')} disabled={matrixIdText === ''} />
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -22,6 +22,8 @@ import Email from '../../../email';
|
|||
import { looksValid as phoneNumberLooksValid } from '../../../phonenumber';
|
||||
import Modal from '../../../Modal';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
|
||||
const FIELD_EMAIL = 'field_email';
|
||||
const FIELD_PHONE_COUNTRY = 'field_phone_country';
|
||||
|
@ -64,7 +66,7 @@ module.exports = React.createClass({
|
|||
minPasswordLength: 6,
|
||||
onError: function(e) {
|
||||
console.error(e);
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -91,16 +93,16 @@ module.exports = React.createClass({
|
|||
this.validateField(FIELD_PHONE_NUMBER);
|
||||
this.validateField(FIELD_EMAIL);
|
||||
|
||||
var self = this;
|
||||
const self = this;
|
||||
if (this.allFieldsValid()) {
|
||||
if (this.refs.email.value == '') {
|
||||
var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
Modal.createTrackedDialog('If you don\'t specify an email address...', '', QuestionDialog, {
|
||||
title: _t("Warning!"),
|
||||
description:
|
||||
<div>
|
||||
{_t("If you don't specify an email address, you won't be able to reset your password. " +
|
||||
"Are you sure?")}
|
||||
{ _t("If you don't specify an email address, you won't be able to reset your password. " +
|
||||
"Are you sure?") }
|
||||
</div>,
|
||||
button: _t("Continue"),
|
||||
onFinished: function(confirmed) {
|
||||
|
@ -116,13 +118,13 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
_doSubmit: function(ev) {
|
||||
let email = this.refs.email.value.trim();
|
||||
var promise = this.props.onRegisterClick({
|
||||
const email = this.refs.email.value.trim();
|
||||
const promise = this.props.onRegisterClick({
|
||||
username: this.refs.username.value.trim(),
|
||||
password: this.refs.password.value.trim(),
|
||||
email: email,
|
||||
phoneCountry: this.state.phoneCountry,
|
||||
phoneNumber: this.refs.phoneNumber.value.trim(),
|
||||
phoneNumber: this.refs.phoneNumber ? this.refs.phoneNumber.value.trim() : '',
|
||||
});
|
||||
|
||||
if (promise) {
|
||||
|
@ -138,8 +140,8 @@ module.exports = React.createClass({
|
|||
* they were validated.
|
||||
*/
|
||||
allFieldsValid: function() {
|
||||
var keys = Object.keys(this.state.fieldValid);
|
||||
for (var i = 0; i < keys.length; ++i) {
|
||||
const keys = Object.keys(this.state.fieldValid);
|
||||
for (let i = 0; i < keys.length; ++i) {
|
||||
if (this.state.fieldValid[keys[i]] == false) {
|
||||
return false;
|
||||
}
|
||||
|
@ -152,8 +154,8 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
validateField: function(field_id) {
|
||||
var pwd1 = this.refs.password.value.trim();
|
||||
var pwd2 = this.refs.passwordConfirm.value.trim();
|
||||
const pwd1 = this.refs.password.value.trim();
|
||||
const pwd2 = this.refs.passwordConfirm.value.trim();
|
||||
|
||||
switch (field_id) {
|
||||
case FIELD_EMAIL:
|
||||
|
@ -162,7 +164,7 @@ module.exports = React.createClass({
|
|||
const matchingTeam = this.props.teamsConfig.teams.find(
|
||||
(team) => {
|
||||
return email.split('@').pop() === team.domain;
|
||||
}
|
||||
},
|
||||
) || null;
|
||||
this.setState({
|
||||
selectedTeam: matchingTeam,
|
||||
|
@ -180,7 +182,7 @@ module.exports = React.createClass({
|
|||
this.markFieldValid(field_id, emailValid, "RegistrationForm.ERR_EMAIL_INVALID");
|
||||
break;
|
||||
case FIELD_PHONE_NUMBER:
|
||||
const phoneNumber = this.refs.phoneNumber.value;
|
||||
const phoneNumber = this.refs.phoneNumber ? this.refs.phoneNumber.value : '';
|
||||
const phoneNumberValid = phoneNumber === '' || phoneNumberLooksValid(phoneNumber);
|
||||
this.markFieldValid(field_id, phoneNumberValid, "RegistrationForm.ERR_PHONE_NUMBER_INVALID");
|
||||
break;
|
||||
|
@ -191,13 +193,13 @@ module.exports = React.createClass({
|
|||
this.markFieldValid(
|
||||
field_id,
|
||||
false,
|
||||
"RegistrationForm.ERR_USERNAME_INVALID"
|
||||
"RegistrationForm.ERR_USERNAME_INVALID",
|
||||
);
|
||||
} else if (username == '') {
|
||||
this.markFieldValid(
|
||||
field_id,
|
||||
false,
|
||||
"RegistrationForm.ERR_USERNAME_BLANK"
|
||||
"RegistrationForm.ERR_USERNAME_BLANK",
|
||||
);
|
||||
} else {
|
||||
this.markFieldValid(field_id, true);
|
||||
|
@ -208,13 +210,13 @@ module.exports = React.createClass({
|
|||
this.markFieldValid(
|
||||
field_id,
|
||||
false,
|
||||
"RegistrationForm.ERR_PASSWORD_MISSING"
|
||||
"RegistrationForm.ERR_PASSWORD_MISSING",
|
||||
);
|
||||
} else if (pwd1.length < this.props.minPasswordLength) {
|
||||
this.markFieldValid(
|
||||
field_id,
|
||||
false,
|
||||
"RegistrationForm.ERR_PASSWORD_LENGTH"
|
||||
"RegistrationForm.ERR_PASSWORD_LENGTH",
|
||||
);
|
||||
} else {
|
||||
this.markFieldValid(field_id, true);
|
||||
|
@ -223,14 +225,14 @@ module.exports = React.createClass({
|
|||
case FIELD_PASSWORD_CONFIRM:
|
||||
this.markFieldValid(
|
||||
field_id, pwd1 == pwd2,
|
||||
"RegistrationForm.ERR_PASSWORD_MISMATCH"
|
||||
"RegistrationForm.ERR_PASSWORD_MISMATCH",
|
||||
);
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
markFieldValid: function(field_id, val, error_code) {
|
||||
var fieldValid = this.state.fieldValid;
|
||||
const fieldValid = this.state.fieldValid;
|
||||
fieldValid[field_id] = val;
|
||||
this.setState({fieldValid: fieldValid});
|
||||
if (!val) {
|
||||
|
@ -271,16 +273,20 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
render: function() {
|
||||
var self = this;
|
||||
const self = this;
|
||||
|
||||
const theme = SettingsStore.getValue("theme");
|
||||
// FIXME: remove hardcoded Status team tweaks at some point
|
||||
const emailPlaceholder = theme === 'status' ? _t("Email address") : _t("Email address (optional)");
|
||||
|
||||
const emailSection = (
|
||||
<div>
|
||||
<input type="text" ref="email"
|
||||
autoFocus={true} placeholder={_t("Email address (optional)")}
|
||||
autoFocus={true} placeholder={emailPlaceholder}
|
||||
defaultValue={this.props.defaultEmail}
|
||||
className={this._classForField(FIELD_EMAIL, 'mx_Login_field')}
|
||||
onBlur={function() {self.validateField(FIELD_EMAIL);}}
|
||||
value={self.state.email}/>
|
||||
value={self.state.email} />
|
||||
</div>
|
||||
);
|
||||
let belowEmailSection;
|
||||
|
@ -291,7 +297,7 @@ module.exports = React.createClass({
|
|||
Sorry, but your university is not registered with us just yet.
|
||||
Email us on
|
||||
<a href={"mailto:" + this.props.teamsConfig.supportEmail}>
|
||||
{this.props.teamsConfig.supportEmail}
|
||||
{ this.props.teamsConfig.supportEmail }
|
||||
</a>
|
||||
to get your university signed up. Or continue to register with Riot to enjoy our open source platform.
|
||||
</p>
|
||||
|
@ -299,50 +305,53 @@ module.exports = React.createClass({
|
|||
} else if (this.state.selectedTeam) {
|
||||
belowEmailSection = (
|
||||
<p className="mx_Login_support">
|
||||
{_t("You are registering with %(SelectedTeamName)s", {SelectedTeamName: this.state.selectedTeam.name})}
|
||||
{ _t("You are registering with %(SelectedTeamName)s", {SelectedTeamName: this.state.selectedTeam.name}) }
|
||||
</p>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const CountryDropdown = sdk.getComponent('views.login.CountryDropdown');
|
||||
const phoneSection = (
|
||||
<div className="mx_Login_phoneSection">
|
||||
<CountryDropdown ref="phone_country" onOptionChange={this._onPhoneCountryChange}
|
||||
className="mx_Login_phoneCountry mx_Login_field_prefix"
|
||||
value={this.state.phoneCountry}
|
||||
isSmall={true}
|
||||
showPrefix={true}
|
||||
/>
|
||||
<input type="text" ref="phoneNumber"
|
||||
placeholder={_t("Mobile phone number (optional)")}
|
||||
defaultValue={this.props.defaultPhoneNumber}
|
||||
className={this._classForField(
|
||||
FIELD_PHONE_NUMBER,
|
||||
'mx_Login_phoneNumberField',
|
||||
'mx_Login_field',
|
||||
'mx_Login_field_has_prefix'
|
||||
)}
|
||||
onBlur={function() {self.validateField(FIELD_PHONE_NUMBER);}}
|
||||
value={self.state.phoneNumber}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
let phoneSection;
|
||||
if (!SdkConfig.get().disable_3pid_login) {
|
||||
phoneSection = (
|
||||
<div className="mx_Login_phoneSection">
|
||||
<CountryDropdown ref="phone_country" onOptionChange={this._onPhoneCountryChange}
|
||||
className="mx_Login_phoneCountry mx_Login_field_prefix"
|
||||
value={this.state.phoneCountry}
|
||||
isSmall={true}
|
||||
showPrefix={true}
|
||||
/>
|
||||
<input type="text" ref="phoneNumber"
|
||||
placeholder={_t("Mobile phone number (optional)")}
|
||||
defaultValue={this.props.defaultPhoneNumber}
|
||||
className={this._classForField(
|
||||
FIELD_PHONE_NUMBER,
|
||||
'mx_Login_phoneNumberField',
|
||||
'mx_Login_field',
|
||||
'mx_Login_field_has_prefix',
|
||||
)}
|
||||
onBlur={function() {self.validateField(FIELD_PHONE_NUMBER);}}
|
||||
value={self.state.phoneNumber}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const registerButton = (
|
||||
<input className="mx_Login_submit" type="submit" value={_t("Register")} />
|
||||
);
|
||||
|
||||
let placeholderUserName = _t("User name");
|
||||
const placeholderUserName = _t("User name");
|
||||
|
||||
return (
|
||||
<div>
|
||||
<form onSubmit={this.onSubmit}>
|
||||
{emailSection}
|
||||
{belowEmailSection}
|
||||
{phoneSection}
|
||||
{ emailSection }
|
||||
{ belowEmailSection }
|
||||
{ phoneSection }
|
||||
<input type="text" ref="username"
|
||||
placeholder={ placeholderUserName } defaultValue={this.props.defaultUsername}
|
||||
placeholder={placeholderUserName} defaultValue={this.props.defaultUsername}
|
||||
className={this._classForField(FIELD_USERNAME, 'mx_Login_field')}
|
||||
onBlur={function() {self.validateField(FIELD_USERNAME);}} />
|
||||
<br />
|
||||
|
@ -357,9 +366,9 @@ module.exports = React.createClass({
|
|||
onBlur={function() {self.validateField(FIELD_PASSWORD_CONFIRM);}}
|
||||
defaultValue={this.props.defaultPassword} />
|
||||
<br />
|
||||
{registerButton}
|
||||
{ registerButton }
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -16,9 +16,9 @@ limitations under the License.
|
|||
|
||||
'use strict';
|
||||
|
||||
var React = require('react');
|
||||
var Modal = require('../../../Modal');
|
||||
var sdk = require('../../../index');
|
||||
const React = require('react');
|
||||
const Modal = require('../../../Modal');
|
||||
const sdk = require('../../../index');
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
/**
|
||||
|
@ -45,7 +45,7 @@ module.exports = React.createClass({
|
|||
customIsUrl: React.PropTypes.string,
|
||||
|
||||
withToggleButton: React.PropTypes.bool,
|
||||
delayTimeMs: React.PropTypes.number // time to wait before invoking onChanged
|
||||
delayTimeMs: React.PropTypes.number, // time to wait before invoking onChanged
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
|
@ -54,7 +54,7 @@ module.exports = React.createClass({
|
|||
customHsUrl: "",
|
||||
customIsUrl: "",
|
||||
withToggleButton: false,
|
||||
delayTimeMs: 0
|
||||
delayTimeMs: 0,
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -65,18 +65,18 @@ module.exports = React.createClass({
|
|||
// if withToggleButton is false, then show the config all the time given we have no way otherwise of making it visible
|
||||
configVisible: !this.props.withToggleButton ||
|
||||
(this.props.customHsUrl !== this.props.defaultHsUrl) ||
|
||||
(this.props.customIsUrl !== this.props.defaultIsUrl)
|
||||
(this.props.customIsUrl !== this.props.defaultIsUrl),
|
||||
};
|
||||
},
|
||||
|
||||
onHomeserverChanged: function(ev) {
|
||||
this.setState({hs_url: ev.target.value}, function() {
|
||||
this._hsTimeoutId = this._waitThenInvoke(this._hsTimeoutId, function() {
|
||||
var hsUrl = this.state.hs_url.trim().replace(/\/$/, "");
|
||||
let hsUrl = this.state.hs_url.trim().replace(/\/$/, "");
|
||||
if (hsUrl === "") hsUrl = this.props.defaultHsUrl;
|
||||
this.props.onServerConfigChange({
|
||||
hsUrl : this.state.hs_url,
|
||||
isUrl : this.state.is_url,
|
||||
hsUrl: this.state.hs_url,
|
||||
isUrl: this.state.is_url,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -85,11 +85,11 @@ module.exports = React.createClass({
|
|||
onIdentityServerChanged: function(ev) {
|
||||
this.setState({is_url: ev.target.value}, function() {
|
||||
this._isTimeoutId = this._waitThenInvoke(this._isTimeoutId, function() {
|
||||
var isUrl = this.state.is_url.trim().replace(/\/$/, "");
|
||||
let isUrl = this.state.is_url.trim().replace(/\/$/, "");
|
||||
if (isUrl === "") isUrl = this.props.defaultIsUrl;
|
||||
this.props.onServerConfigChange({
|
||||
hsUrl : this.state.hs_url,
|
||||
isUrl : this.state.is_url,
|
||||
hsUrl: this.state.hs_url,
|
||||
isUrl: this.state.is_url,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -104,32 +104,31 @@ module.exports = React.createClass({
|
|||
|
||||
onServerConfigVisibleChange: function(visible, ev) {
|
||||
this.setState({
|
||||
configVisible: visible
|
||||
configVisible: visible,
|
||||
});
|
||||
if (!visible) {
|
||||
this.props.onServerConfigChange({
|
||||
hsUrl : this.props.defaultHsUrl,
|
||||
isUrl : this.props.defaultIsUrl,
|
||||
hsUrl: this.props.defaultHsUrl,
|
||||
isUrl: this.props.defaultIsUrl,
|
||||
});
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
this.props.onServerConfigChange({
|
||||
hsUrl : this.state.hs_url,
|
||||
isUrl : this.state.is_url,
|
||||
hsUrl: this.state.hs_url,
|
||||
isUrl: this.state.is_url,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
showHelpPopup: function() {
|
||||
var CustomServerDialog = sdk.getComponent('login.CustomServerDialog');
|
||||
const CustomServerDialog = sdk.getComponent('login.CustomServerDialog');
|
||||
Modal.createTrackedDialog('Custom Server Dialog', '', CustomServerDialog);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var serverConfigStyle = {};
|
||||
const serverConfigStyle = {};
|
||||
serverConfigStyle.display = this.state.configVisible ? 'block' : 'none';
|
||||
|
||||
var toggleButton;
|
||||
let toggleButton;
|
||||
if (this.props.withToggleButton) {
|
||||
toggleButton = (
|
||||
<div className="mx_ServerConfig_selector">
|
||||
|
@ -137,14 +136,14 @@ module.exports = React.createClass({
|
|||
checked={!this.state.configVisible}
|
||||
onChange={this.onServerConfigVisibleChange.bind(this, false)} />
|
||||
<label className="mx_Login_label" htmlFor="basic">
|
||||
{_t("Default server")}
|
||||
{ _t("Default server") }
|
||||
</label>
|
||||
|
||||
<input className="mx_Login_radio" id="advanced" name="configVisible" type="radio"
|
||||
checked={this.state.configVisible}
|
||||
onChange={this.onServerConfigVisibleChange.bind(this, true)} />
|
||||
<label className="mx_Login_label" htmlFor="advanced">
|
||||
{_t("Custom server")}
|
||||
{ _t("Custom server") }
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
|
@ -152,11 +151,11 @@ module.exports = React.createClass({
|
|||
|
||||
return (
|
||||
<div>
|
||||
{toggleButton}
|
||||
{ toggleButton }
|
||||
<div style={serverConfigStyle}>
|
||||
<div className="mx_ServerConfig">
|
||||
<label className="mx_Login_label mx_ServerConfig_hslabel" htmlFor="hsurl">
|
||||
{_t("Home server URL")}
|
||||
{ _t("Home server URL") }
|
||||
</label>
|
||||
<input className="mx_Login_field" id="hsurl" type="text"
|
||||
placeholder={this.props.defaultHsUrl}
|
||||
|
@ -164,7 +163,7 @@ module.exports = React.createClass({
|
|||
value={this.state.hs_url}
|
||||
onChange={this.onHomeserverChanged} />
|
||||
<label className="mx_Login_label mx_ServerConfig_islabel" htmlFor="isurl">
|
||||
{_t("Identity server URL")}
|
||||
{ _t("Identity server URL") }
|
||||
</label>
|
||||
<input className="mx_Login_field" id="isurl" type="text"
|
||||
placeholder={this.props.defaultIsUrl}
|
||||
|
@ -172,11 +171,11 @@ module.exports = React.createClass({
|
|||
value={this.state.is_url}
|
||||
onChange={this.onIdentityServerChanged} />
|
||||
<a className="mx_ServerConfig_help" href="#" onClick={this.showHelpPopup}>
|
||||
{_t("What does this mean?")}
|
||||
{ _t("What does this mean?") }
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -35,7 +35,7 @@ export default class MAudioBody extends React.Component {
|
|||
}
|
||||
onPlayToggle() {
|
||||
this.setState({
|
||||
playing: !this.state.playing
|
||||
playing: !this.state.playing,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -49,9 +49,9 @@ export default class MAudioBody extends React.Component {
|
|||
}
|
||||
|
||||
componentDidMount() {
|
||||
var content = this.props.mxEvent.getContent();
|
||||
const content = this.props.mxEvent.getContent();
|
||||
if (content.file !== undefined && this.state.decryptedUrl === null) {
|
||||
var decryptedBlob;
|
||||
let decryptedBlob;
|
||||
decryptFile(content.file).then(function(blob) {
|
||||
decryptedBlob = blob;
|
||||
return readBlobAsDataUri(decryptedBlob);
|
||||
|
@ -70,14 +70,13 @@ export default class MAudioBody extends React.Component {
|
|||
}
|
||||
|
||||
render() {
|
||||
|
||||
const content = this.props.mxEvent.getContent();
|
||||
|
||||
if (this.state.error !== null) {
|
||||
return (
|
||||
<span className="mx_MAudioBody" ref="body">
|
||||
<img src="img/warning.svg" width="16" height="16"/>
|
||||
{_t("Error decrypting audio")}
|
||||
<img src="img/warning.svg" width="16" height="16" />
|
||||
{ _t("Error decrypting audio") }
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
@ -89,7 +88,7 @@ export default class MAudioBody extends React.Component {
|
|||
// Not sure how tall the audio player is so not sure how tall it should actually be.
|
||||
return (
|
||||
<span className="mx_MAudioBody">
|
||||
<img src="img/spinner.gif" alt={content.body} width="16" height="16"/>
|
||||
<img src="img/spinner.gif" alt={content.body} width="16" height="16" />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -28,10 +28,10 @@ import Modal from '../../../Modal';
|
|||
|
||||
|
||||
// A cached tinted copy of "img/download.svg"
|
||||
var tintedDownloadImageURL;
|
||||
let tintedDownloadImageURL;
|
||||
// Track a list of mounted MFileBody instances so that we can update
|
||||
// the "img/download.svg" when the tint changes.
|
||||
var nextMountId = 0;
|
||||
let nextMountId = 0;
|
||||
const mounts = {};
|
||||
|
||||
/**
|
||||
|
@ -169,11 +169,11 @@ function computedStyle(element) {
|
|||
return "";
|
||||
}
|
||||
const style = window.getComputedStyle(element, null);
|
||||
var cssText = style.cssText;
|
||||
let cssText = style.cssText;
|
||||
if (cssText == "") {
|
||||
// Firefox doesn't implement ".cssText" for computed styles.
|
||||
// https://bugzilla.mozilla.org/show_bug.cgi?id=137687
|
||||
for (var i = 0; i < style.length; i++) {
|
||||
for (let i = 0; i < style.length; i++) {
|
||||
cssText += style[i] + ":";
|
||||
cssText += style.getPropertyValue(style[i]) + ";";
|
||||
}
|
||||
|
@ -202,7 +202,7 @@ module.exports = React.createClass({
|
|||
* @return {string} the human readable link text for the attachment.
|
||||
*/
|
||||
presentableTextForFile: function(content) {
|
||||
var linkText = _t("Attachment");
|
||||
let linkText = _t("Attachment");
|
||||
if (content.body && content.body.length > 0) {
|
||||
// The content body should be the name of the file including a
|
||||
// file extension.
|
||||
|
@ -270,7 +270,7 @@ module.exports = React.createClass({
|
|||
// Need to decrypt the attachment
|
||||
// Wait for the user to click on the link before downloading
|
||||
// and decrypting the attachment.
|
||||
var decrypting = false;
|
||||
let decrypting = false;
|
||||
const decrypt = () => {
|
||||
if (decrypting) {
|
||||
return false;
|
||||
|
@ -328,14 +328,14 @@ module.exports = React.createClass({
|
|||
<span className="mx_MFileBody">
|
||||
<div className="mx_MImageBody_download">
|
||||
<div style={{display: "none"}}>
|
||||
{/*
|
||||
{ /*
|
||||
* Add dummy copy of the "a" tag
|
||||
* We'll use it to learn how the download link
|
||||
* would have been styled if it was rendered inline.
|
||||
*/}
|
||||
<a ref="dummyLink"/>
|
||||
*/ }
|
||||
<a ref="dummyLink" />
|
||||
</div>
|
||||
<iframe src={renderer_url} onLoad={onIframeLoad} ref="iframe"/>
|
||||
<iframe src={renderer_url} onLoad={onIframeLoad} ref="iframe" />
|
||||
</div>
|
||||
</span>
|
||||
);
|
||||
|
@ -356,13 +356,12 @@ module.exports = React.createClass({
|
|||
</div>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
return (
|
||||
<span className="mx_MFileBody">
|
||||
<div className="mx_MImageBody_download">
|
||||
<a href={contentUrl} download={fileName} target="_blank" rel="noopener">
|
||||
<img src={tintedDownloadImageURL} width="12" height="14" ref="downloadImage"/>
|
||||
<img src={tintedDownloadImageURL} width="12" height="14" ref="downloadImage" />
|
||||
{ _t("Download %(text)s", { text: text }) }
|
||||
</a>
|
||||
</div>
|
||||
|
@ -370,7 +369,7 @@ module.exports = React.createClass({
|
|||
);
|
||||
}
|
||||
} else {
|
||||
var extra = text ? (': ' + text) : '';
|
||||
const extra = text ? (': ' + text) : '';
|
||||
return <span className="mx_MFileBody">
|
||||
{ _t("Invalid file%(extra)s", { extra: extra }) }
|
||||
</span>;
|
||||
|
|
|
@ -25,8 +25,8 @@ import sdk from '../../../index';
|
|||
import dis from '../../../dispatcher';
|
||||
import { decryptFile, readBlobAsDataUri } from '../../../utils/DecryptFile';
|
||||
import Promise from 'bluebird';
|
||||
import UserSettingsStore from '../../../UserSettingsStore';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'MImageBody',
|
||||
|
@ -81,7 +81,7 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
onImageEnter: function(e) {
|
||||
if (!this._isGif() || UserSettingsStore.getSyncedSetting("autoplayGifsAndVideos", false)) {
|
||||
if (!this._isGif() || SettingsStore.getValue("autoplayGifsAndVideos")) {
|
||||
return;
|
||||
}
|
||||
const imgElement = e.target;
|
||||
|
@ -89,7 +89,7 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
onImageLeave: function(e) {
|
||||
if (!this._isGif() || UserSettingsStore.getSyncedSetting("autoplayGifsAndVideos", false)) {
|
||||
if (!this._isGif() || SettingsStore.getValue("autoplayGifsAndVideos")) {
|
||||
return;
|
||||
}
|
||||
const imgElement = e.target;
|
||||
|
@ -191,8 +191,8 @@ module.exports = React.createClass({
|
|||
if (this.state.error !== null) {
|
||||
return (
|
||||
<span className="mx_MImageBody" ref="body">
|
||||
<img src="img/warning.svg" width="16" height="16"/>
|
||||
{_t("Error decrypting image")}
|
||||
<img src="img/warning.svg" width="16" height="16" />
|
||||
{ _t("Error decrypting image") }
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
@ -210,7 +210,7 @@ module.exports = React.createClass({
|
|||
}}>
|
||||
<img src="img/spinner.gif" alt={content.body} width="32" height="32" style={{
|
||||
"margin": "auto",
|
||||
}}/>
|
||||
}} />
|
||||
</div>
|
||||
</span>
|
||||
);
|
||||
|
@ -218,7 +218,7 @@ module.exports = React.createClass({
|
|||
|
||||
const contentUrl = this._getContentUrl();
|
||||
let thumbUrl;
|
||||
if (this._isGif() && UserSettingsStore.getSyncedSetting("autoplayGifsAndVideos", false)) {
|
||||
if (this._isGif() && SettingsStore.getValue("autoplayGifsAndVideos")) {
|
||||
thumbUrl = contentUrl;
|
||||
} else {
|
||||
thumbUrl = this._getThumbUrl();
|
||||
|
@ -227,9 +227,10 @@ module.exports = React.createClass({
|
|||
if (thumbUrl) {
|
||||
return (
|
||||
<span className="mx_MImageBody" ref="body">
|
||||
<a href={contentUrl} onClick={ this.onClick }>
|
||||
<a href={contentUrl} onClick={this.onClick}>
|
||||
<img className="mx_MImageBody_thumbnail" src={thumbUrl} ref="image"
|
||||
alt={content.body}
|
||||
onLoad={this.props.onWidgetLoad}
|
||||
onMouseEnter={this.onImageEnter}
|
||||
onMouseLeave={this.onImageLeave} />
|
||||
</a>
|
||||
|
@ -239,13 +240,13 @@ module.exports = React.createClass({
|
|||
} else if (content.body) {
|
||||
return (
|
||||
<span className="mx_MImageBody">
|
||||
{_t("Image '%(Body)s' cannot be displayed.", {Body: content.body})}
|
||||
{ _t("Image '%(Body)s' cannot be displayed.", {Body: content.body}) }
|
||||
</span>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<span className="mx_MImageBody">
|
||||
{_t("This image cannot be displayed.")}
|
||||
{ _t("This image cannot be displayed.") }
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -21,8 +21,8 @@ import MFileBody from './MFileBody';
|
|||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import { decryptFile, readBlobAsDataUri } from '../../../utils/DecryptFile';
|
||||
import Promise from 'bluebird';
|
||||
import UserSettingsStore from '../../../UserSettingsStore';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'MVideoBody',
|
||||
|
@ -54,13 +54,12 @@ module.exports = React.createClass({
|
|||
// no scaling needs to be applied
|
||||
return 1;
|
||||
}
|
||||
var widthMulti = thumbWidth / fullWidth;
|
||||
var heightMulti = thumbHeight / fullHeight;
|
||||
const widthMulti = thumbWidth / fullWidth;
|
||||
const heightMulti = thumbHeight / fullHeight;
|
||||
if (widthMulti < heightMulti) {
|
||||
// width is the dominant dimension so scaling will be fixed on that
|
||||
return widthMulti;
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
// height is the dominant dimension so scaling will be fixed on that
|
||||
return heightMulti;
|
||||
}
|
||||
|
@ -89,15 +88,15 @@ module.exports = React.createClass({
|
|||
componentDidMount: function() {
|
||||
const content = this.props.mxEvent.getContent();
|
||||
if (content.file !== undefined && this.state.decryptedUrl === null) {
|
||||
var thumbnailPromise = Promise.resolve(null);
|
||||
let thumbnailPromise = Promise.resolve(null);
|
||||
if (content.info.thumbnail_file) {
|
||||
thumbnailPromise = decryptFile(
|
||||
content.info.thumbnail_file
|
||||
content.info.thumbnail_file,
|
||||
).then(function(blob) {
|
||||
return readBlobAsDataUri(blob);
|
||||
});
|
||||
}
|
||||
var decryptedBlob;
|
||||
let decryptedBlob;
|
||||
thumbnailPromise.then((thumbnailUrl) => {
|
||||
return decryptFile(content.file).then(function(blob) {
|
||||
decryptedBlob = blob;
|
||||
|
@ -126,8 +125,8 @@ module.exports = React.createClass({
|
|||
if (this.state.error !== null) {
|
||||
return (
|
||||
<span className="mx_MVideoBody" ref="body">
|
||||
<img src="img/warning.svg" width="16" height="16"/>
|
||||
{_t("Error decrypting video")}
|
||||
<img src="img/warning.svg" width="16" height="16" />
|
||||
{ _t("Error decrypting video") }
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
@ -144,7 +143,7 @@ module.exports = React.createClass({
|
|||
"justify-items": "center",
|
||||
"width": "100%",
|
||||
}}>
|
||||
<img src="img/spinner.gif" alt={content.body} width="16" height="16"/>
|
||||
<img src="img/spinner.gif" alt={content.body} width="16" height="16" />
|
||||
</div>
|
||||
</span>
|
||||
);
|
||||
|
@ -152,7 +151,7 @@ module.exports = React.createClass({
|
|||
|
||||
const contentUrl = this._getContentUrl();
|
||||
const thumbUrl = this._getThumbUrl();
|
||||
const autoplay = UserSettingsStore.getSyncedSetting("autoplayGifsAndVideos", false);
|
||||
const autoplay = SettingsStore.getValue("autoplayGifsAndVideos");
|
||||
let height = null;
|
||||
let width = null;
|
||||
let poster = null;
|
||||
|
|
|
@ -16,8 +16,8 @@ limitations under the License.
|
|||
|
||||
'use strict';
|
||||
|
||||
var React = require('react');
|
||||
var sdk = require('../../../index');
|
||||
const React = require('react');
|
||||
const sdk = require('../../../index');
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'MessageEvent',
|
||||
|
@ -47,21 +47,21 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
render: function() {
|
||||
var UnknownBody = sdk.getComponent('messages.UnknownBody');
|
||||
const UnknownBody = sdk.getComponent('messages.UnknownBody');
|
||||
|
||||
var bodyTypes = {
|
||||
const bodyTypes = {
|
||||
'm.text': sdk.getComponent('messages.TextualBody'),
|
||||
'm.notice': sdk.getComponent('messages.TextualBody'),
|
||||
'm.emote': sdk.getComponent('messages.TextualBody'),
|
||||
'm.image': sdk.getComponent('messages.MImageBody'),
|
||||
'm.file': sdk.getComponent('messages.MFileBody'),
|
||||
'm.audio': sdk.getComponent('messages.MAudioBody'),
|
||||
'm.video': sdk.getComponent('messages.MVideoBody')
|
||||
'm.video': sdk.getComponent('messages.MVideoBody'),
|
||||
};
|
||||
|
||||
var content = this.props.mxEvent.getContent();
|
||||
var msgtype = content.msgtype;
|
||||
var BodyType = UnknownBody;
|
||||
const content = this.props.mxEvent.getContent();
|
||||
const msgtype = content.msgtype;
|
||||
let BodyType = UnknownBody;
|
||||
if (msgtype && bodyTypes[msgtype]) {
|
||||
BodyType = bodyTypes[msgtype];
|
||||
} else if (content.url) {
|
||||
|
|
|
@ -17,7 +17,7 @@ limitations under the License.
|
|||
import React from 'react';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import { ContentRepo } from 'matrix-js-sdk';
|
||||
import { _t, _tJsx } from '../../../languageHandler';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import sdk from '../../../index';
|
||||
import Modal from '../../../Modal';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
|
@ -31,9 +31,9 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
onAvatarClick: function(name) {
|
||||
var httpUrl = MatrixClientPeg.get().mxcUrlToHttp(this.props.mxEvent.getContent().url);
|
||||
var ImageView = sdk.getComponent("elements.ImageView");
|
||||
var params = {
|
||||
const httpUrl = MatrixClientPeg.get().mxcUrlToHttp(this.props.mxEvent.getContent().url);
|
||||
const ImageView = sdk.getComponent("elements.ImageView");
|
||||
const params = {
|
||||
src: httpUrl,
|
||||
name: name,
|
||||
};
|
||||
|
@ -41,12 +41,12 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
render: function() {
|
||||
var ev = this.props.mxEvent;
|
||||
var senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
|
||||
var BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
|
||||
const ev = this.props.mxEvent;
|
||||
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
|
||||
const BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
|
||||
|
||||
var room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId());
|
||||
var name = _t('%(senderDisplayName)s changed the avatar for %(roomName)s', {
|
||||
const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId());
|
||||
const name = _t('%(senderDisplayName)s changed the avatar for %(roomName)s', {
|
||||
senderDisplayName: senderDisplayName,
|
||||
roomName: room ? room.name : '',
|
||||
});
|
||||
|
@ -59,32 +59,25 @@ module.exports = React.createClass({
|
|||
);
|
||||
}
|
||||
|
||||
var url = ContentRepo.getHttpUriForMxc(
|
||||
const url = ContentRepo.getHttpUriForMxc(
|
||||
MatrixClientPeg.get().getHomeserverUrl(),
|
||||
ev.getContent().url,
|
||||
Math.ceil(14 * window.devicePixelRatio),
|
||||
Math.ceil(14 * window.devicePixelRatio),
|
||||
'crop'
|
||||
'crop',
|
||||
);
|
||||
|
||||
// it sucks that _tJsx doesn't support normal _t substitutions :((
|
||||
return (
|
||||
<div className="mx_RoomAvatarEvent">
|
||||
{ _tJsx('$senderDisplayName changed the room avatar to <img/>',
|
||||
[
|
||||
/\$senderDisplayName/,
|
||||
/<img\/>/,
|
||||
],
|
||||
[
|
||||
(sub) => senderDisplayName,
|
||||
(sub) =>
|
||||
<AccessibleButton key="avatar" className="mx_RoomAvatarEvent_avatar"
|
||||
onClick={ this.onAvatarClick.bind(this, name) }>
|
||||
<BaseAvatar width={14} height={14} url={ url }
|
||||
name={ name } />
|
||||
</AccessibleButton>,
|
||||
]
|
||||
)
|
||||
{ _t('%(senderDisplayName)s changed the room avatar to <img/>',
|
||||
{ senderDisplayName: senderDisplayName },
|
||||
{
|
||||
'img': () =>
|
||||
<AccessibleButton key="avatar" className="mx_RoomAvatarEvent_avatar"
|
||||
onClick={this.onAvatarClick.bind(this, name)}>
|
||||
<BaseAvatar width={14} height={14} url={url} name={name} />
|
||||
</AccessibleButton>,
|
||||
})
|
||||
}
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -17,26 +17,128 @@
|
|||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {MatrixClient} from 'matrix-js-sdk';
|
||||
import sdk from '../../../index';
|
||||
import Flair from '../elements/Flair.js';
|
||||
import FlairStore from '../../../stores/FlairStore';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
export default function SenderProfile(props) {
|
||||
const EmojiText = sdk.getComponent('elements.EmojiText');
|
||||
const {mxEvent} = props;
|
||||
const name = mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender();
|
||||
const {msgtype} = mxEvent.getContent();
|
||||
export default React.createClass({
|
||||
displayName: 'SenderProfile',
|
||||
propTypes: {
|
||||
mxEvent: PropTypes.object.isRequired, // event whose sender we're showing
|
||||
text: PropTypes.string, // Text to show. Defaults to sender name
|
||||
onClick: PropTypes.func,
|
||||
},
|
||||
|
||||
if (msgtype === 'm.emote') {
|
||||
return <span />; // emote message must include the name so don't duplicate it
|
||||
}
|
||||
contextTypes: {
|
||||
matrixClient: PropTypes.instanceOf(MatrixClient),
|
||||
},
|
||||
|
||||
return (
|
||||
<EmojiText className="mx_SenderProfile" dir="auto"
|
||||
onClick={props.onClick}>{`${name || ''} ${props.aux || ''}`}</EmojiText>
|
||||
);
|
||||
}
|
||||
getInitialState() {
|
||||
return {
|
||||
userGroups: null,
|
||||
relatedGroups: [],
|
||||
};
|
||||
},
|
||||
|
||||
SenderProfile.propTypes = {
|
||||
mxEvent: React.PropTypes.object.isRequired, // event whose sender we're showing
|
||||
aux: React.PropTypes.string, // stuff to go after the sender name, if anything
|
||||
onClick: React.PropTypes.func,
|
||||
};
|
||||
componentWillMount() {
|
||||
this.unmounted = false;
|
||||
this._updateRelatedGroups();
|
||||
|
||||
FlairStore.getPublicisedGroupsCached(
|
||||
this.context.matrixClient, this.props.mxEvent.getSender(),
|
||||
).then((userGroups) => {
|
||||
if (this.unmounted) return;
|
||||
this.setState({userGroups});
|
||||
});
|
||||
|
||||
this.context.matrixClient.on('RoomState.events', this.onRoomStateEvents);
|
||||
},
|
||||
|
||||
componentWillUnmount() {
|
||||
this.unmounted = true;
|
||||
this.context.matrixClient.removeListener('RoomState.events', this.onRoomStateEvents);
|
||||
},
|
||||
|
||||
onRoomStateEvents(event) {
|
||||
if (event.getType() === 'm.room.related_groups' &&
|
||||
event.getRoomId() === this.props.mxEvent.getRoomId()
|
||||
) {
|
||||
this._updateRelatedGroups();
|
||||
}
|
||||
},
|
||||
|
||||
_updateRelatedGroups() {
|
||||
if (this.unmounted) return;
|
||||
const relatedGroupsEvent = this.context.matrixClient
|
||||
.getRoom(this.props.mxEvent.getRoomId())
|
||||
.currentState
|
||||
.getStateEvents('m.room.related_groups', '');
|
||||
this.setState({
|
||||
relatedGroups: relatedGroupsEvent ?
|
||||
relatedGroupsEvent.getContent().groups || []
|
||||
: [],
|
||||
});
|
||||
},
|
||||
|
||||
_getDisplayedGroups(userGroups, relatedGroups) {
|
||||
let displayedGroups = userGroups || [];
|
||||
if (relatedGroups && relatedGroups.length > 0) {
|
||||
displayedGroups = displayedGroups.filter((groupId) => {
|
||||
return relatedGroups.includes(groupId);
|
||||
});
|
||||
} else {
|
||||
displayedGroups = [];
|
||||
}
|
||||
return displayedGroups;
|
||||
},
|
||||
|
||||
render() {
|
||||
const EmojiText = sdk.getComponent('elements.EmojiText');
|
||||
const {mxEvent} = this.props;
|
||||
let name = mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender();
|
||||
const {msgtype} = mxEvent.getContent();
|
||||
|
||||
if (msgtype === 'm.emote') {
|
||||
return <span />; // emote message must include the name so don't duplicate it
|
||||
}
|
||||
|
||||
let flair = <div />;
|
||||
if (this.props.enableFlair) {
|
||||
const displayedGroups = this._getDisplayedGroups(
|
||||
this.state.userGroups, this.state.relatedGroups,
|
||||
);
|
||||
|
||||
// Backwards-compatible replacing of "(IRC)" with AS user flair
|
||||
name = displayedGroups.length > 0 ? name.replace(' (IRC)', '') : name;
|
||||
|
||||
flair = <Flair key='flair'
|
||||
userId={mxEvent.getSender()}
|
||||
groups={displayedGroups}
|
||||
/>;
|
||||
}
|
||||
|
||||
const nameElem = <EmojiText key='name'>{ name || '' }</EmojiText>;
|
||||
|
||||
// Name + flair
|
||||
const nameFlair = <span>
|
||||
<span className="mx_SenderProfile_name">
|
||||
{ nameElem }
|
||||
</span>
|
||||
{ flair }
|
||||
</span>;
|
||||
|
||||
const content = this.props.text ?
|
||||
<span className="mx_SenderProfile_aux">
|
||||
{ _t(this.props.text, { senderName: () => nameElem }) }
|
||||
</span> : nameFlair;
|
||||
|
||||
return (
|
||||
<div className="mx_SenderProfile" dir="auto" onClick={this.props.onClick}>
|
||||
{ content }
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
|
|
@ -29,10 +29,12 @@ import Modal from '../../../Modal';
|
|||
import SdkConfig from '../../../SdkConfig';
|
||||
import dis from '../../../dispatcher';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import UserSettingsStore from "../../../UserSettingsStore";
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import ContextualMenu from '../../structures/ContextualMenu';
|
||||
import {RoomMember} from 'matrix-js-sdk';
|
||||
import classNames from 'classnames';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import PushProcessor from 'matrix-js-sdk/lib/pushprocessor';
|
||||
|
||||
linkifyMatrix(linkify);
|
||||
|
||||
|
@ -72,12 +74,16 @@ module.exports = React.createClass({
|
|||
textArea.value = text;
|
||||
document.body.appendChild(textArea);
|
||||
textArea.select();
|
||||
|
||||
let successful = false;
|
||||
try {
|
||||
const successful = document.execCommand('copy');
|
||||
successful = document.execCommand('copy');
|
||||
} catch (err) {
|
||||
console.log('Unable to copy');
|
||||
}
|
||||
|
||||
document.body.removeChild(textArea);
|
||||
return successful;
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
|
@ -98,11 +104,11 @@ module.exports = React.createClass({
|
|||
setTimeout(() => {
|
||||
if (this._unmounted) return;
|
||||
for (let i = 0; i < blocks.length; i++) {
|
||||
if (UserSettingsStore.getSyncedSetting("enableSyntaxHighlightLanguageDetection", false)) {
|
||||
highlight.highlightBlock(blocks[i])
|
||||
if (SettingsStore.getValue("enableSyntaxHighlightLanguageDetection")) {
|
||||
highlight.highlightBlock(blocks[i]);
|
||||
} else {
|
||||
// Only syntax highlight if there's a class starting with language-
|
||||
let classes = blocks[i].className.split(/\s+/).filter(function (cl) {
|
||||
const classes = blocks[i].className.split(/\s+/).filter(function(cl) {
|
||||
return cl.startsWith('language-');
|
||||
});
|
||||
|
||||
|
@ -113,14 +119,7 @@ module.exports = React.createClass({
|
|||
}
|
||||
}, 10);
|
||||
}
|
||||
// add event handlers to the 'copy code' buttons
|
||||
const buttons = ReactDOM.findDOMNode(this).getElementsByClassName("mx_EventTile_copyButton");
|
||||
for (let i = 0; i < buttons.length; i++) {
|
||||
buttons[i].onclick = (e) => {
|
||||
const copyCode = buttons[i].parentNode.getElementsByTagName("code")[0];
|
||||
this.copyToClipboard(copyCode.textContent);
|
||||
};
|
||||
}
|
||||
this._addCodeCopyButton();
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -148,7 +147,7 @@ module.exports = React.createClass({
|
|||
//console.log("calculateUrlPreview: ShowUrlPreview for %s is %s", this.props.mxEvent.getId(), this.props.showUrlPreview);
|
||||
|
||||
if (this.props.showUrlPreview && !this.state.links.length) {
|
||||
var links = this.findLinks(this.refs.content.children);
|
||||
let links = this.findLinks(this.refs.content.children);
|
||||
if (links.length) {
|
||||
// de-dup the links (but preserve ordering)
|
||||
const seen = new Set();
|
||||
|
@ -162,7 +161,7 @@ module.exports = React.createClass({
|
|||
|
||||
// lazy-load the hidden state of the preview widget from localstorage
|
||||
if (global.localStorage) {
|
||||
var hidden = global.localStorage.getItem("hide_preview_" + this.props.mxEvent.getId());
|
||||
const hidden = global.localStorage.getItem("hide_preview_" + this.props.mxEvent.getId());
|
||||
this.setState({ widgetHidden: hidden });
|
||||
}
|
||||
}
|
||||
|
@ -170,9 +169,11 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
pillifyLinks: function(nodes) {
|
||||
const shouldShowPillAvatar = !UserSettingsStore.getSyncedSetting("Pill.shouldHidePillAvatar", false);
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
const node = nodes[i];
|
||||
const shouldShowPillAvatar = !SettingsStore.getValue("Pill.shouldHidePillAvatar");
|
||||
let node = nodes[0];
|
||||
while (node) {
|
||||
let pillified = false;
|
||||
|
||||
if (node.tagName === "A" && node.getAttribute("href")) {
|
||||
const href = node.getAttribute("href");
|
||||
|
||||
|
@ -191,29 +192,87 @@ module.exports = React.createClass({
|
|||
|
||||
ReactDOM.render(pill, pillContainer);
|
||||
node.parentNode.replaceChild(pillContainer, node);
|
||||
// Pills within pills aren't going to go well, so move on
|
||||
pillified = true;
|
||||
|
||||
// update the current node with one that's now taken its place
|
||||
node = pillContainer;
|
||||
}
|
||||
} else if (node.nodeType == Node.TEXT_NODE) {
|
||||
const Pill = sdk.getComponent('elements.Pill');
|
||||
|
||||
let currentTextNode = node;
|
||||
const roomNotifTextNodes = [];
|
||||
|
||||
// Take a textNode and break it up to make all the instances of @room their
|
||||
// own textNode, adding those nodes to roomNotifTextNodes
|
||||
while (currentTextNode !== null) {
|
||||
const roomNotifPos = Pill.roomNotifPos(currentTextNode.textContent);
|
||||
let nextTextNode = null;
|
||||
if (roomNotifPos > -1) {
|
||||
let roomTextNode = currentTextNode;
|
||||
|
||||
if (roomNotifPos > 0) roomTextNode = roomTextNode.splitText(roomNotifPos);
|
||||
if (roomTextNode.textContent.length > Pill.roomNotifLen()) {
|
||||
nextTextNode = roomTextNode.splitText(Pill.roomNotifLen());
|
||||
}
|
||||
roomNotifTextNodes.push(roomTextNode);
|
||||
}
|
||||
currentTextNode = nextTextNode;
|
||||
}
|
||||
|
||||
if (roomNotifTextNodes.length > 0) {
|
||||
const pushProcessor = new PushProcessor(MatrixClientPeg.get());
|
||||
const atRoomRule = pushProcessor.getPushRuleById(".m.rule.roomnotif");
|
||||
if (atRoomRule && pushProcessor.ruleMatchesEvent(atRoomRule, this.props.mxEvent)) {
|
||||
// Now replace all those nodes with Pills
|
||||
for (const roomNotifTextNode of roomNotifTextNodes) {
|
||||
const pillContainer = document.createElement('span');
|
||||
const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId());
|
||||
const pill = <Pill
|
||||
type={Pill.TYPE_AT_ROOM_MENTION}
|
||||
inMessage={true}
|
||||
room={room}
|
||||
shouldShowPillAvatar={true}
|
||||
/>;
|
||||
|
||||
ReactDOM.render(pill, pillContainer);
|
||||
roomNotifTextNode.parentNode.replaceChild(pillContainer, roomNotifTextNode);
|
||||
|
||||
// Set the next node to be processed to the one after the node
|
||||
// we're adding now, since we've just inserted nodes into the structure
|
||||
// we're iterating over.
|
||||
// Note we've checked roomNotifTextNodes.length > 0 so we'll do this at least once
|
||||
node = roomNotifTextNode.nextSibling;
|
||||
}
|
||||
// Nothing else to do for a text node (and we don't need to advance
|
||||
// the loop pointer because we did it above)
|
||||
continue;
|
||||
}
|
||||
}
|
||||
} else if (node.children && node.children.length) {
|
||||
this.pillifyLinks(node.children);
|
||||
}
|
||||
|
||||
if (node.childNodes && node.childNodes.length && !pillified) {
|
||||
this.pillifyLinks(node.childNodes);
|
||||
}
|
||||
|
||||
node = node.nextSibling;
|
||||
}
|
||||
},
|
||||
|
||||
findLinks: function(nodes) {
|
||||
var links = [];
|
||||
let links = [];
|
||||
|
||||
for (var i = 0; i < nodes.length; i++) {
|
||||
var node = nodes[i];
|
||||
if (node.tagName === "A" && node.getAttribute("href"))
|
||||
{
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
const node = nodes[i];
|
||||
if (node.tagName === "A" && node.getAttribute("href")) {
|
||||
if (this.isLinkPreviewable(node)) {
|
||||
links.push(node.getAttribute("href"));
|
||||
}
|
||||
}
|
||||
else if (node.tagName === "PRE" || node.tagName === "CODE" ||
|
||||
} else if (node.tagName === "PRE" || node.tagName === "CODE" ||
|
||||
node.tagName === "BLOCKQUOTE") {
|
||||
continue;
|
||||
}
|
||||
else if (node.children && node.children.length) {
|
||||
} else if (node.children && node.children.length) {
|
||||
links = links.concat(this.findLinks(node.children));
|
||||
}
|
||||
}
|
||||
|
@ -223,8 +282,7 @@ module.exports = React.createClass({
|
|||
isLinkPreviewable: function(node) {
|
||||
// don't try to preview relative links
|
||||
if (!node.getAttribute("href").startsWith("http://") &&
|
||||
!node.getAttribute("href").startsWith("https://"))
|
||||
{
|
||||
!node.getAttribute("href").startsWith("https://")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -233,13 +291,11 @@ module.exports = React.createClass({
|
|||
// or from a full foo.bar/baz style schemeless URL) - or be a markdown-style
|
||||
// link, in which case we check the target text differs from the link value.
|
||||
// TODO: make this configurable?
|
||||
if (node.textContent.indexOf("/") > -1)
|
||||
{
|
||||
if (node.textContent.indexOf("/") > -1) {
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
var url = node.getAttribute("href");
|
||||
var host = url.match(/^https?:\/\/(.*?)(\/|$)/)[1];
|
||||
} else {
|
||||
const url = node.getAttribute("href");
|
||||
const host = url.match(/^https?:\/\/(.*?)(\/|$)/)[1];
|
||||
|
||||
// never preview matrix.to links (if anything we should give a smart
|
||||
// preview of the room/user they point to: nobody needs to be reminded
|
||||
|
@ -249,14 +305,40 @@ module.exports = React.createClass({
|
|||
if (node.textContent.toLowerCase().trim().startsWith(host.toLowerCase())) {
|
||||
// it's a "foo.pl" style link
|
||||
return false;
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
// it's a [foo bar](http://foo.com) style link
|
||||
return true;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
_addCodeCopyButton() {
|
||||
// Add 'copy' buttons to pre blocks
|
||||
ReactDOM.findDOMNode(this).querySelectorAll('.mx_EventTile_body pre').forEach((p) => {
|
||||
const button = document.createElement("span");
|
||||
button.className = "mx_EventTile_copyButton";
|
||||
button.onclick = (e) => {
|
||||
const copyCode = button.parentNode.getElementsByTagName("code")[0];
|
||||
const successful = this.copyToClipboard(copyCode.textContent);
|
||||
|
||||
const GenericTextContextMenu = sdk.getComponent('context_menus.GenericTextContextMenu');
|
||||
const buttonRect = e.target.getBoundingClientRect();
|
||||
|
||||
// The window X and Y offsets are to adjust position when zoomed in to page
|
||||
const x = buttonRect.right + window.pageXOffset;
|
||||
const y = (buttonRect.top + (buttonRect.height / 2) + window.pageYOffset) - 19;
|
||||
const {close} = ContextualMenu.createMenu(GenericTextContextMenu, {
|
||||
chevronOffset: 10,
|
||||
left: x,
|
||||
top: y,
|
||||
message: successful ? _t('Copied!') : _t('Failed to copy'),
|
||||
});
|
||||
e.target.onmouseout = close;
|
||||
};
|
||||
p.appendChild(button);
|
||||
});
|
||||
},
|
||||
|
||||
onCancelClick: function(event) {
|
||||
this.setState({ widgetHidden: true });
|
||||
// FIXME: persist this somewhere smarter than local storage
|
||||
|
@ -289,7 +371,7 @@ module.exports = React.createClass({
|
|||
|
||||
getInnerText: () => {
|
||||
return this.refs.content.innerText;
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -303,28 +385,28 @@ module.exports = React.createClass({
|
|||
// the window.open command occurs in the same stack frame as the onClick callback.
|
||||
|
||||
// Go fetch a scalar token
|
||||
let scalarClient = new ScalarAuthClient();
|
||||
const scalarClient = new ScalarAuthClient();
|
||||
scalarClient.connect().then(() => {
|
||||
let completeUrl = scalarClient.getStarterLink(starterLink);
|
||||
let QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
let integrationsUrl = SdkConfig.get().integrations_ui_url;
|
||||
const completeUrl = scalarClient.getStarterLink(starterLink);
|
||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
const integrationsUrl = SdkConfig.get().integrations_ui_url;
|
||||
Modal.createTrackedDialog('Add an integration', '', QuestionDialog, {
|
||||
title: _t("Add an Integration"),
|
||||
description:
|
||||
<div>
|
||||
{_t("You are about to be taken to a third-party site so you can " +
|
||||
{ _t("You are about to be taken to a third-party site so you can " +
|
||||
"authenticate your account for use with %(integrationsUrl)s. " +
|
||||
"Do you wish to continue?", { integrationsUrl: integrationsUrl })}
|
||||
"Do you wish to continue?", { integrationsUrl: integrationsUrl }) }
|
||||
</div>,
|
||||
button: _t("Continue"),
|
||||
onFinished: function(confirmed) {
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
let width = window.screen.width > 1024 ? 1024 : window.screen.width;
|
||||
let height = window.screen.height > 800 ? 800 : window.screen.height;
|
||||
let left = (window.screen.width - width) / 2;
|
||||
let top = (window.screen.height - height) / 2;
|
||||
const width = window.screen.width > 1024 ? 1024 : window.screen.width;
|
||||
const height = window.screen.height > 800 ? 800 : window.screen.height;
|
||||
const left = (window.screen.width - width) / 2;
|
||||
const top = (window.screen.height - height) / 2;
|
||||
window.open(completeUrl, '_blank', `height=${height}, width=${width}, top=${top}, left=${left},`);
|
||||
},
|
||||
});
|
||||
|
@ -333,28 +415,29 @@ module.exports = React.createClass({
|
|||
|
||||
render: function() {
|
||||
const EmojiText = sdk.getComponent('elements.EmojiText');
|
||||
var mxEvent = this.props.mxEvent;
|
||||
var content = mxEvent.getContent();
|
||||
const mxEvent = this.props.mxEvent;
|
||||
const content = mxEvent.getContent();
|
||||
|
||||
var body = HtmlUtils.bodyToHtml(content, this.props.highlights, {});
|
||||
let body = HtmlUtils.bodyToHtml(content, this.props.highlights, {
|
||||
disableBigEmoji: SettingsStore.getValue('TextualBody.disableBigEmoji'),
|
||||
});
|
||||
|
||||
if (this.props.highlightLink) {
|
||||
body = <a href={ this.props.highlightLink }>{ body }</a>;
|
||||
}
|
||||
else if (content.data && typeof content.data["org.matrix.neb.starter_link"] === "string") {
|
||||
body = <a href="#" onClick={ this.onStarterLinkClick.bind(this, content.data["org.matrix.neb.starter_link"]) }>{ body }</a>;
|
||||
body = <a href={this.props.highlightLink}>{ body }</a>;
|
||||
} else if (content.data && typeof content.data["org.matrix.neb.starter_link"] === "string") {
|
||||
body = <a href="#" onClick={this.onStarterLinkClick.bind(this, content.data["org.matrix.neb.starter_link"])}>{ body }</a>;
|
||||
}
|
||||
|
||||
var widgets;
|
||||
let widgets;
|
||||
if (this.state.links.length && !this.state.widgetHidden && this.props.showUrlPreview) {
|
||||
var LinkPreviewWidget = sdk.getComponent('rooms.LinkPreviewWidget');
|
||||
const LinkPreviewWidget = sdk.getComponent('rooms.LinkPreviewWidget');
|
||||
widgets = this.state.links.map((link)=>{
|
||||
return <LinkPreviewWidget
|
||||
key={ link }
|
||||
link={ link }
|
||||
mxEvent={ this.props.mxEvent }
|
||||
onCancelClick={ this.onCancelClick }
|
||||
onWidgetLoad={ this.props.onWidgetLoad }/>;
|
||||
key={link}
|
||||
link={link}
|
||||
mxEvent={this.props.mxEvent}
|
||||
onCancelClick={this.onCancelClick}
|
||||
onWidgetLoad={this.props.onWidgetLoad} />;
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -368,7 +451,7 @@ module.exports = React.createClass({
|
|||
className="mx_MEmoteBody_sender"
|
||||
onClick={this.onEmoteSenderClick}
|
||||
>
|
||||
{name}
|
||||
{ name }
|
||||
</EmojiText>
|
||||
|
||||
{ body }
|
||||
|
|
|
@ -16,9 +16,9 @@ limitations under the License.
|
|||
|
||||
'use strict';
|
||||
|
||||
var React = require('react');
|
||||
const React = require('react');
|
||||
|
||||
var TextForEvent = require('../../../TextForEvent');
|
||||
const TextForEvent = require('../../../TextForEvent');
|
||||
import sdk from '../../../index';
|
||||
|
||||
module.exports = React.createClass({
|
||||
|
@ -28,13 +28,13 @@ module.exports = React.createClass({
|
|||
/* the MatrixEvent to show */
|
||||
mxEvent: React.PropTypes.object.isRequired,
|
||||
},
|
||||
|
||||
|
||||
render: function() {
|
||||
const EmojiText = sdk.getComponent('elements.EmojiText');
|
||||
var text = TextForEvent.textForEvent(this.props.mxEvent);
|
||||
const text = TextForEvent.textForEvent(this.props.mxEvent);
|
||||
if (text == null || text.length === 0) return null;
|
||||
return (
|
||||
<EmojiText element="div" className="mx_TextualEvent">{text}</EmojiText>
|
||||
<EmojiText element="div" className="mx_TextualEvent">{ text }</EmojiText>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
|
|
@ -23,10 +23,18 @@ module.exports = React.createClass({
|
|||
displayName: 'UnknownBody',
|
||||
|
||||
render: function() {
|
||||
let tooltip = _t("Removed or unknown message type");
|
||||
if (this.props.mxEvent.isRedacted()) {
|
||||
const redactedBecauseUserId = this.props.mxEvent.getUnsigned().redacted_because.sender;
|
||||
tooltip = redactedBecauseUserId ?
|
||||
_t("Message removed by %(userId)s", { userId: redactedBecauseUserId }) :
|
||||
_t("Message removed");
|
||||
}
|
||||
|
||||
const text = this.props.mxEvent.getContent().body;
|
||||
return (
|
||||
<span className="mx_UnknownBody" title={_t("Removed or unknown message type")}>
|
||||
{text}
|
||||
<span className="mx_UnknownBody" title={tooltip}>
|
||||
{ text }
|
||||
</span>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -15,12 +15,12 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import Promise from 'bluebird';
|
||||
var React = require('react');
|
||||
var ObjectUtils = require("../../../ObjectUtils");
|
||||
var MatrixClientPeg = require('../../../MatrixClientPeg');
|
||||
var sdk = require("../../../index");
|
||||
const React = require('react');
|
||||
const ObjectUtils = require("../../../ObjectUtils");
|
||||
const MatrixClientPeg = require('../../../MatrixClientPeg');
|
||||
const sdk = require("../../../index");
|
||||
import { _t } from '../../../languageHandler';
|
||||
var Modal = require("../../../Modal");
|
||||
const Modal = require("../../../Modal");
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'AliasSettings',
|
||||
|
@ -30,14 +30,14 @@ module.exports = React.createClass({
|
|||
canSetCanonicalAlias: React.PropTypes.bool.isRequired,
|
||||
canSetAliases: React.PropTypes.bool.isRequired,
|
||||
aliasEvents: React.PropTypes.array, // [MatrixEvent]
|
||||
canonicalAliasEvent: React.PropTypes.object // MatrixEvent
|
||||
canonicalAliasEvent: React.PropTypes.object, // MatrixEvent
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
canSetAliases: false,
|
||||
canSetCanonicalAlias: false,
|
||||
aliasEvents: []
|
||||
aliasEvents: [],
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -48,12 +48,12 @@ module.exports = React.createClass({
|
|||
recalculateState: function(aliasEvents, canonicalAliasEvent) {
|
||||
aliasEvents = aliasEvents || [];
|
||||
|
||||
var state = {
|
||||
const state = {
|
||||
domainToAliases: {}, // { domain.com => [#alias1:domain.com, #alias2:domain.com] }
|
||||
remoteDomains: [], // [ domain.com, foobar.com ]
|
||||
canonicalAlias: null // #canonical:domain.com
|
||||
canonicalAlias: null, // #canonical:domain.com
|
||||
};
|
||||
var localDomain = MatrixClientPeg.get().getDomain();
|
||||
const localDomain = MatrixClientPeg.get().getDomain();
|
||||
|
||||
state.domainToAliases = this.aliasEventsToDictionary(aliasEvents);
|
||||
|
||||
|
@ -69,26 +69,26 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
saveSettings: function() {
|
||||
var promises = [];
|
||||
let promises = [];
|
||||
|
||||
// save new aliases for m.room.aliases
|
||||
var aliasOperations = this.getAliasOperations();
|
||||
for (var i = 0; i < aliasOperations.length; i++) {
|
||||
var alias_operation = aliasOperations[i];
|
||||
const aliasOperations = this.getAliasOperations();
|
||||
for (let i = 0; i < aliasOperations.length; i++) {
|
||||
const alias_operation = aliasOperations[i];
|
||||
console.log("alias %s %s", alias_operation.place, alias_operation.val);
|
||||
switch (alias_operation.place) {
|
||||
case 'add':
|
||||
promises.push(
|
||||
MatrixClientPeg.get().createAlias(
|
||||
alias_operation.val, this.props.roomId
|
||||
)
|
||||
alias_operation.val, this.props.roomId,
|
||||
),
|
||||
);
|
||||
break;
|
||||
case 'del':
|
||||
promises.push(
|
||||
MatrixClientPeg.get().deleteAlias(
|
||||
alias_operation.val
|
||||
)
|
||||
alias_operation.val,
|
||||
),
|
||||
);
|
||||
break;
|
||||
default:
|
||||
|
@ -98,7 +98,7 @@ module.exports = React.createClass({
|
|||
|
||||
|
||||
// save new canonical alias
|
||||
var oldCanonicalAlias = null;
|
||||
let oldCanonicalAlias = null;
|
||||
if (this.props.canonicalAliasEvent) {
|
||||
oldCanonicalAlias = this.props.canonicalAliasEvent.getContent().alias;
|
||||
}
|
||||
|
@ -107,9 +107,9 @@ module.exports = React.createClass({
|
|||
promises = [Promise.all(promises).then(
|
||||
MatrixClientPeg.get().sendStateEvent(
|
||||
this.props.roomId, "m.room.canonical_alias", {
|
||||
alias: this.state.canonicalAlias
|
||||
}, ""
|
||||
)
|
||||
alias: this.state.canonicalAlias,
|
||||
}, "",
|
||||
),
|
||||
)];
|
||||
}
|
||||
|
||||
|
@ -117,7 +117,7 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
aliasEventsToDictionary: function(aliasEvents) { // m.room.alias events
|
||||
var dict = {};
|
||||
const dict = {};
|
||||
aliasEvents.forEach((event) => {
|
||||
dict[event.getStateKey()] = (
|
||||
(event.getContent().aliases || []).slice() // shallow-copy
|
||||
|
@ -132,28 +132,29 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
getAliasOperations: function() {
|
||||
var oldAliases = this.aliasEventsToDictionary(this.props.aliasEvents);
|
||||
const oldAliases = this.aliasEventsToDictionary(this.props.aliasEvents);
|
||||
return ObjectUtils.getKeyValueArrayDiffs(oldAliases, this.state.domainToAliases);
|
||||
},
|
||||
|
||||
onAliasAdded: function(alias) {
|
||||
onNewAliasChanged: function(value) {
|
||||
this.setState({newAlias: value});
|
||||
},
|
||||
|
||||
onLocalAliasAdded: function(alias) {
|
||||
if (!alias || alias.length === 0) return; // ignore attempts to create blank aliases
|
||||
|
||||
if (this.isAliasValid(alias)) {
|
||||
// add this alias to the domain to aliases dict
|
||||
var domain = alias.replace(/^.*?:/, '');
|
||||
// XXX: do we need to deep copy aliases before editing it?
|
||||
this.state.domainToAliases[domain] = this.state.domainToAliases[domain] || [];
|
||||
this.state.domainToAliases[domain].push(alias);
|
||||
this.setState({
|
||||
domainToAliases: this.state.domainToAliases
|
||||
});
|
||||
const localDomain = MatrixClientPeg.get().getDomain();
|
||||
if (this.isAliasValid(alias) && alias.endsWith(localDomain)) {
|
||||
this.state.domainToAliases[localDomain] = this.state.domainToAliases[localDomain] || [];
|
||||
this.state.domainToAliases[localDomain].push(alias);
|
||||
|
||||
// reset the add field
|
||||
this.refs.add_alias.setValue(''); // FIXME
|
||||
}
|
||||
else {
|
||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
this.setState({
|
||||
domainToAliases: this.state.domainToAliases,
|
||||
// Reset the add field
|
||||
newAlias: "",
|
||||
});
|
||||
} else {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createTrackedDialog('Invalid alias format', '', ErrorDialog, {
|
||||
title: _t('Invalid alias format'),
|
||||
description: _t('\'%(alias)s\' is not a valid format for an alias', { alias: alias }),
|
||||
|
@ -161,15 +162,13 @@ module.exports = React.createClass({
|
|||
}
|
||||
},
|
||||
|
||||
onAliasChanged: function(domain, index, alias) {
|
||||
onLocalAliasChanged: function(alias, index) {
|
||||
if (alias === "") return; // hit the delete button to delete please
|
||||
var oldAlias;
|
||||
if (this.isAliasValid(alias)) {
|
||||
oldAlias = this.state.domainToAliases[domain][index];
|
||||
this.state.domainToAliases[domain][index] = alias;
|
||||
}
|
||||
else {
|
||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
const localDomain = MatrixClientPeg.get().getDomain();
|
||||
if (this.isAliasValid(alias) && alias.endsWith(localDomain)) {
|
||||
this.state.domainToAliases[localDomain][index] = alias;
|
||||
} else {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createTrackedDialog('Invalid address format', '', ErrorDialog, {
|
||||
title: _t('Invalid address format'),
|
||||
description: _t('\'%(alias)s\' is not a valid format for an address', { alias: alias }),
|
||||
|
@ -177,39 +176,41 @@ module.exports = React.createClass({
|
|||
}
|
||||
},
|
||||
|
||||
onAliasDeleted: function(domain, index) {
|
||||
onLocalAliasDeleted: function(index) {
|
||||
const localDomain = MatrixClientPeg.get().getDomain();
|
||||
// It's a bit naughty to directly manipulate this.state, and React would
|
||||
// normally whine at you, but it can't see us doing the splice. Given we
|
||||
// promptly setState anyway, it's just about acceptable. The alternative
|
||||
// would be to arbitrarily deepcopy to a temp variable and then setState
|
||||
// that, but why bother when we can cut this corner.
|
||||
var alias = this.state.domainToAliases[domain].splice(index, 1);
|
||||
this.state.domainToAliases[localDomain].splice(index, 1);
|
||||
this.setState({
|
||||
domainToAliases: this.state.domainToAliases
|
||||
domainToAliases: this.state.domainToAliases,
|
||||
});
|
||||
},
|
||||
|
||||
onCanonicalAliasChange: function(event) {
|
||||
this.setState({
|
||||
canonicalAlias: event.target.value
|
||||
canonicalAlias: event.target.value,
|
||||
});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var self = this;
|
||||
var EditableText = sdk.getComponent("elements.EditableText");
|
||||
var localDomain = MatrixClientPeg.get().getDomain();
|
||||
const self = this;
|
||||
const EditableText = sdk.getComponent("elements.EditableText");
|
||||
const EditableItemList = sdk.getComponent("elements.EditableItemList");
|
||||
const localDomain = MatrixClientPeg.get().getDomain();
|
||||
|
||||
var canonical_alias_section;
|
||||
let canonical_alias_section;
|
||||
if (this.props.canSetCanonicalAlias) {
|
||||
canonical_alias_section = (
|
||||
<select onChange={this.onCanonicalAliasChange} defaultValue={ this.state.canonicalAlias }>
|
||||
<select onChange={this.onCanonicalAliasChange} defaultValue={this.state.canonicalAlias}>
|
||||
<option value="" key="unset">{ _t('not specified') }</option>
|
||||
{
|
||||
Object.keys(self.state.domainToAliases).map(function(domain, i) {
|
||||
return self.state.domainToAliases[domain].map(function(alias, j) {
|
||||
return (
|
||||
<option value={ alias } key={ i + "_" + j }>
|
||||
<option value={alias} key={i + "_" + j}>
|
||||
{ alias }
|
||||
</option>
|
||||
);
|
||||
|
@ -218,34 +219,33 @@ module.exports = React.createClass({
|
|||
}
|
||||
</select>
|
||||
);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
canonical_alias_section = (
|
||||
<b>{ this.state.canonicalAlias || _t('not set') }</b>
|
||||
);
|
||||
}
|
||||
|
||||
var remote_aliases_section;
|
||||
let remote_aliases_section;
|
||||
if (this.state.remoteDomains.length) {
|
||||
remote_aliases_section = (
|
||||
<div>
|
||||
<div className="mx_RoomSettings_aliasLabel">
|
||||
{_t("Remote addresses for this room:")}
|
||||
{ _t("Remote addresses for this room:") }
|
||||
</div>
|
||||
<div className="mx_RoomSettings_aliasesTable">
|
||||
{ this.state.remoteDomains.map((domain, i) => {
|
||||
return this.state.domainToAliases[domain].map(function(alias, j) {
|
||||
return (
|
||||
<div className="mx_RoomSettings_aliasesTableRow" key={ i + "_" + j }>
|
||||
<div className="mx_RoomSettings_aliasesTableRow" key={i + "_" + j}>
|
||||
<EditableText
|
||||
className="mx_RoomSettings_alias mx_RoomSettings_editable"
|
||||
blurToCancel={ false }
|
||||
editable={ false }
|
||||
initialValue={ alias } />
|
||||
blurToCancel={false}
|
||||
editable={false}
|
||||
initialValue={alias} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
})}
|
||||
}) }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -253,62 +253,29 @@ module.exports = React.createClass({
|
|||
|
||||
return (
|
||||
<div>
|
||||
<h3>Addresses</h3>
|
||||
<h3>{ _t('Addresses') }</h3>
|
||||
<div className="mx_RoomSettings_aliasLabel">
|
||||
{ _t('The main address for this room is') }: { canonical_alias_section }
|
||||
</div>
|
||||
<div className="mx_RoomSettings_aliasLabel">
|
||||
{ (this.state.domainToAliases[localDomain] &&
|
||||
this.state.domainToAliases[localDomain].length > 0)
|
||||
? _t('Local addresses for this room:')
|
||||
: _t('This room has no local addresses') }
|
||||
</div>
|
||||
<div className="mx_RoomSettings_aliasesTable">
|
||||
{ (this.state.domainToAliases[localDomain] || []).map((alias, i) => {
|
||||
var deleteButton;
|
||||
if (this.props.canSetAliases) {
|
||||
deleteButton = (
|
||||
<img src="img/cancel-small.svg" width="14" height="14"
|
||||
alt={ _t('Delete') } onClick={ self.onAliasDeleted.bind(self, localDomain, i) } />
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="mx_RoomSettings_aliasesTableRow" key={ i }>
|
||||
<EditableText
|
||||
className="mx_RoomSettings_alias mx_RoomSettings_editable"
|
||||
placeholderClassName="mx_RoomSettings_aliasPlaceholder"
|
||||
placeholder={ _t('New address (e.g. #foo:%(localDomain)s)', { localDomain: localDomain}) }
|
||||
blurToCancel={ false }
|
||||
onValueChanged={ self.onAliasChanged.bind(self, localDomain, i) }
|
||||
editable={ self.props.canSetAliases }
|
||||
initialValue={ alias } />
|
||||
<div className="mx_RoomSettings_deleteAlias mx_filterFlipColor">
|
||||
{ deleteButton }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
{ this.props.canSetAliases ?
|
||||
<div className="mx_RoomSettings_aliasesTableRow" key="new">
|
||||
<EditableText
|
||||
ref="add_alias"
|
||||
className="mx_RoomSettings_alias mx_RoomSettings_editable"
|
||||
placeholderClassName="mx_RoomSettings_aliasPlaceholder"
|
||||
placeholder={ _t('New address (e.g. #foo:%(localDomain)s)', { localDomain: localDomain}) }
|
||||
blurToCancel={ false }
|
||||
onValueChanged={ self.onAliasAdded } />
|
||||
<div className="mx_RoomSettings_addAlias mx_filterFlipColor">
|
||||
<img src="img/plus.svg" width="14" height="14" alt="Add"
|
||||
onClick={ self.onAliasAdded.bind(self, undefined) }/>
|
||||
</div>
|
||||
</div> : ""
|
||||
}
|
||||
</div>
|
||||
<EditableItemList
|
||||
className={"mx_RoomSettings_localAliases"}
|
||||
items={this.state.domainToAliases[localDomain] || []}
|
||||
newItem={this.state.newAlias}
|
||||
onNewItemChanged={this.onNewAliasChanged}
|
||||
canEdit={this.props.canSetAliases}
|
||||
onItemAdded={this.onLocalAliasAdded}
|
||||
onItemEdited={this.onLocalAliasChanged}
|
||||
onItemRemoved={this.onLocalAliasDeleted}
|
||||
itemsLabel={_t('Local addresses for this room:')}
|
||||
noItemsLabel={_t('This room has no local addresses')}
|
||||
placeholder={_t(
|
||||
'New address (e.g. #foo:%(localDomain)s)', {localDomain: localDomain},
|
||||
)}
|
||||
/>
|
||||
|
||||
{ remote_aliases_section }
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -14,18 +14,19 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
import Promise from 'bluebird';
|
||||
var React = require('react');
|
||||
const React = require('react');
|
||||
|
||||
var sdk = require('../../../index');
|
||||
var Tinter = require('../../../Tinter');
|
||||
var MatrixClientPeg = require("../../../MatrixClientPeg");
|
||||
var Modal = require("../../../Modal");
|
||||
const sdk = require('../../../index');
|
||||
const Tinter = require('../../../Tinter');
|
||||
const MatrixClientPeg = require("../../../MatrixClientPeg");
|
||||
const Modal = require("../../../Modal");
|
||||
|
||||
import dis from '../../../dispatcher';
|
||||
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
|
||||
|
||||
var ROOM_COLORS = [
|
||||
const ROOM_COLORS = [
|
||||
// magic room default values courtesy of Ribot
|
||||
["#76cfa6", "#eaf5f0"],
|
||||
[Tinter.getKeyRgb()[0], Tinter.getKeyRgb()[1]],
|
||||
["#81bddb", "#eaf1f4"],
|
||||
["#bd79cb", "#f3eaf5"],
|
||||
["#c65d94", "#f5eaef"],
|
||||
|
@ -34,37 +35,37 @@ var ROOM_COLORS = [
|
|||
["#dad658", "#f5f4ea"],
|
||||
["#80c553", "#eef5ea"],
|
||||
["#bb814e", "#eee8e3"],
|
||||
["#595959", "#ececec"],
|
||||
//["#595959", "#ececec"], // Grey makes everything appear disabled, so remove it for now
|
||||
];
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'ColorSettings',
|
||||
|
||||
propTypes: {
|
||||
room: React.PropTypes.object.isRequired
|
||||
room: React.PropTypes.object.isRequired,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
var data = {
|
||||
const data = {
|
||||
index: 0,
|
||||
primary_color: ROOM_COLORS[0].primary_color,
|
||||
secondary_color: ROOM_COLORS[0].secondary_color,
|
||||
hasChanged: false
|
||||
primary_color: ROOM_COLORS[0][0],
|
||||
secondary_color: ROOM_COLORS[0][1],
|
||||
hasChanged: false,
|
||||
};
|
||||
var event = this.props.room.getAccountData("org.matrix.room.color_scheme");
|
||||
if (!event) {
|
||||
return data;
|
||||
const scheme = SettingsStore.getValueAt(SettingLevel.ROOM_ACCOUNT, "roomColor", this.props.room.roomId);
|
||||
|
||||
if (scheme.primary_color && scheme.secondary_color) {
|
||||
// We only use the user's scheme if the scheme is valid.
|
||||
data.primary_color = scheme.primary_color;
|
||||
data.secondary_color = scheme.secondary_color;
|
||||
}
|
||||
var scheme = event.getContent();
|
||||
data.primary_color = scheme.primary_color;
|
||||
data.secondary_color = scheme.secondary_color;
|
||||
data.index = this._getColorIndex(data);
|
||||
|
||||
if (data.index === -1) {
|
||||
// append the unrecognised colours to our palette
|
||||
data.index = ROOM_COLORS.length;
|
||||
ROOM_COLORS.push([
|
||||
scheme.primary_color, scheme.secondary_color
|
||||
scheme.primary_color, scheme.secondary_color,
|
||||
]);
|
||||
}
|
||||
return data;
|
||||
|
@ -74,20 +75,20 @@ module.exports = React.createClass({
|
|||
if (!this.state.hasChanged) {
|
||||
return Promise.resolve(); // They didn't explicitly give a color to save.
|
||||
}
|
||||
var originalState = this.getInitialState();
|
||||
const originalState = this.getInitialState();
|
||||
if (originalState.primary_color !== this.state.primary_color ||
|
||||
originalState.secondary_color !== this.state.secondary_color) {
|
||||
console.log("ColorSettings: Saving new color");
|
||||
// We would like guests to be able to set room colour but currently
|
||||
// they can't, so we still send the request but display a sensible
|
||||
// error if it fails.
|
||||
return MatrixClientPeg.get().setRoomAccountData(
|
||||
this.props.room.roomId, "org.matrix.room.color_scheme", {
|
||||
primary_color: this.state.primary_color,
|
||||
secondary_color: this.state.secondary_color
|
||||
}
|
||||
).catch(function(err) {
|
||||
if (err.errcode == 'M_GUEST_ACCESS_FORBIDDEN') {
|
||||
// TODO: Support guests for room color. Technically this is possible via granular settings
|
||||
// Granular settings would mean the guest is forced to use the DEVICE level though.
|
||||
SettingsStore.setValue("roomColor", this.props.room.roomId, SettingLevel.ROOM_ACCOUNT, {
|
||||
primary_color: this.state.primary_color,
|
||||
secondary_color: this.state.secondary_color,
|
||||
}).catch(function(err) {
|
||||
if (err.errcode === 'M_GUEST_ACCESS_FORBIDDEN') {
|
||||
dis.dispatch({action: 'view_set_mxid'});
|
||||
}
|
||||
});
|
||||
|
@ -100,8 +101,8 @@ module.exports = React.createClass({
|
|||
return -1;
|
||||
}
|
||||
// XXX: we should validate these values
|
||||
for (var i = 0; i < ROOM_COLORS.length; i++) {
|
||||
var room_color = ROOM_COLORS[i];
|
||||
for (let i = 0; i < ROOM_COLORS.length; i++) {
|
||||
const room_color = ROOM_COLORS[i];
|
||||
if (room_color[0] === String(scheme.primary_color).toLowerCase() &&
|
||||
room_color[1] === String(scheme.secondary_color).toLowerCase()) {
|
||||
return i;
|
||||
|
@ -117,34 +118,34 @@ module.exports = React.createClass({
|
|||
index: index,
|
||||
primary_color: ROOM_COLORS[index][0],
|
||||
secondary_color: ROOM_COLORS[index][1],
|
||||
hasChanged: true
|
||||
hasChanged: true,
|
||||
});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return (
|
||||
<div className="mx_RoomSettings_roomColors">
|
||||
{ROOM_COLORS.map((room_color, i) => {
|
||||
var selected;
|
||||
{ ROOM_COLORS.map((room_color, i) => {
|
||||
let selected;
|
||||
if (i === this.state.index) {
|
||||
selected = (
|
||||
<div className="mx_RoomSettings_roomColor_selected">
|
||||
<img src="img/tick.svg" width="17" height="14" alt="./"/>
|
||||
<img src="img/tick.svg" width="17" height="14" alt="./" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
var boundClick = this._onColorSchemeChanged.bind(this, i);
|
||||
const boundClick = this._onColorSchemeChanged.bind(this, i);
|
||||
return (
|
||||
<div className="mx_RoomSettings_roomColor"
|
||||
key={ "room_color_" + i }
|
||||
key={"room_color_" + i}
|
||||
style={{ backgroundColor: room_color[1] }}
|
||||
onClick={ boundClick }>
|
||||
onClick={boundClick}>
|
||||
{ selected }
|
||||
<div className="mx_RoomSettings_roomColorPrimary" style={{ backgroundColor: room_color[0] }}></div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
}) }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
126
src/components/views/room_settings/RelatedGroupSettings.js
Normal file
126
src/components/views/room_settings/RelatedGroupSettings.js
Normal file
|
@ -0,0 +1,126 @@
|
|||
/*
|
||||
Copyright 2017 New Vector 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.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import {MatrixEvent, MatrixClient} from 'matrix-js-sdk';
|
||||
import sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import Modal from '../../../Modal';
|
||||
|
||||
const GROUP_ID_REGEX = /\+\S+\:\S+/;
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'RelatedGroupSettings',
|
||||
|
||||
propTypes: {
|
||||
roomId: React.PropTypes.string.isRequired,
|
||||
canSetRelatedGroups: React.PropTypes.bool.isRequired,
|
||||
relatedGroupsEvent: React.PropTypes.instanceOf(MatrixEvent),
|
||||
},
|
||||
|
||||
contextTypes: {
|
||||
matrixClient: React.PropTypes.instanceOf(MatrixClient),
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
canSetRelatedGroups: false,
|
||||
};
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
newGroupsList: this.props.relatedGroupsEvent ?
|
||||
(this.props.relatedGroupsEvent.getContent().groups || []) : [],
|
||||
newGroupId: null,
|
||||
};
|
||||
},
|
||||
|
||||
saveSettings: function() {
|
||||
return this.context.matrixClient.sendStateEvent(
|
||||
this.props.roomId,
|
||||
'm.room.related_groups',
|
||||
{
|
||||
groups: this.state.newGroupsList,
|
||||
},
|
||||
'',
|
||||
);
|
||||
},
|
||||
|
||||
validateGroupId: function(groupId) {
|
||||
if (!GROUP_ID_REGEX.test(groupId)) {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createTrackedDialog('Invalid related community ID', '', ErrorDialog, {
|
||||
title: _t('Invalid community ID'),
|
||||
description: _t('\'%(groupId)s\' is not a valid community ID', { groupId }),
|
||||
});
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
onNewGroupChanged: function(newGroupId) {
|
||||
this.setState({ newGroupId });
|
||||
},
|
||||
|
||||
onGroupAdded: function(groupId) {
|
||||
if (groupId.length === 0 || !this.validateGroupId(groupId)) {
|
||||
return;
|
||||
}
|
||||
this.setState({
|
||||
newGroupsList: this.state.newGroupsList.concat([groupId]),
|
||||
newGroupId: '',
|
||||
});
|
||||
},
|
||||
|
||||
onGroupEdited: function(groupId, index) {
|
||||
if (groupId.length === 0 || !this.validateGroupId(groupId)) {
|
||||
return;
|
||||
}
|
||||
this.setState({
|
||||
newGroupsList: Object.assign(this.state.newGroupsList, {[index]: groupId}),
|
||||
});
|
||||
},
|
||||
|
||||
onGroupDeleted: function(index) {
|
||||
const newGroupsList = this.state.newGroupsList.slice();
|
||||
newGroupsList.splice(index, 1),
|
||||
this.setState({ newGroupsList });
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const localDomain = this.context.matrixClient.getDomain();
|
||||
const EditableItemList = sdk.getComponent('elements.EditableItemList');
|
||||
return <div>
|
||||
<h3>{ _t('Flair') }</h3>
|
||||
<EditableItemList
|
||||
items={this.state.newGroupsList}
|
||||
className={"mx_RelatedGroupSettings"}
|
||||
newItem={this.state.newGroupId}
|
||||
canEdit={this.props.canSetRelatedGroups}
|
||||
onNewItemChanged={this.onNewGroupChanged}
|
||||
onItemAdded={this.onGroupAdded}
|
||||
onItemEdited={this.onGroupEdited}
|
||||
onItemRemoved={this.onGroupDeleted}
|
||||
itemsLabel={_t('Showing flair for these communities:')}
|
||||
noItemsLabel={_t('This room is not showing flair for any communities')}
|
||||
placeholder={_t(
|
||||
'New community ID (e.g. +foo:%(localDomain)s)', {localDomain},
|
||||
)}
|
||||
/>
|
||||
</div>;
|
||||
},
|
||||
});
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
Copyright 2017 Travis Ralston
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -14,13 +15,10 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import Promise from 'bluebird';
|
||||
var React = require('react');
|
||||
var MatrixClientPeg = require('../../../MatrixClientPeg');
|
||||
var sdk = require("../../../index");
|
||||
var Modal = require("../../../Modal");
|
||||
var UserSettingsStore = require('../../../UserSettingsStore');
|
||||
import { _t, _tJsx } from '../../../languageHandler';
|
||||
const React = require('react');
|
||||
const sdk = require("../../../index");
|
||||
import { _t, _td } from '../../../languageHandler';
|
||||
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
|
||||
|
||||
|
||||
module.exports = React.createClass({
|
||||
|
@ -30,141 +28,65 @@ module.exports = React.createClass({
|
|||
room: React.PropTypes.object,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
var cli = MatrixClientPeg.get();
|
||||
var roomState = this.props.room.currentState;
|
||||
|
||||
var roomPreviewUrls = this.props.room.currentState.getStateEvents('org.matrix.room.preview_urls', '');
|
||||
var userPreviewUrls = this.props.room.getAccountData("org.matrix.room.preview_urls");
|
||||
|
||||
return {
|
||||
globalDisableUrlPreview: (roomPreviewUrls && roomPreviewUrls.getContent().disable) || false,
|
||||
userDisableUrlPreview: (userPreviewUrls && (userPreviewUrls.getContent().disable === true)) || false,
|
||||
userEnableUrlPreview: (userPreviewUrls && (userPreviewUrls.getContent().disable === false)) || false,
|
||||
};
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
this.originalState = Object.assign({}, this.state);
|
||||
},
|
||||
|
||||
saveSettings: function() {
|
||||
var promises = [];
|
||||
|
||||
if (this.state.globalDisableUrlPreview !== this.originalState.globalDisableUrlPreview) {
|
||||
console.log("UrlPreviewSettings: Updating room's preview_urls state event");
|
||||
promises.push(
|
||||
MatrixClientPeg.get().sendStateEvent(
|
||||
this.props.room.roomId, "org.matrix.room.preview_urls", {
|
||||
disable: this.state.globalDisableUrlPreview
|
||||
}, ""
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
var content = undefined;
|
||||
if (this.state.userDisableUrlPreview !== this.originalState.userDisableUrlPreview) {
|
||||
console.log("UrlPreviewSettings: Disabling user's per-room preview_urls");
|
||||
content = this.state.userDisableUrlPreview ? { disable : true } : {};
|
||||
}
|
||||
|
||||
if (this.state.userEnableUrlPreview !== this.originalState.userEnableUrlPreview) {
|
||||
console.log("UrlPreviewSettings: Enabling user's per-room preview_urls");
|
||||
if (!content || content.disable === undefined) {
|
||||
content = this.state.userEnableUrlPreview ? { disable : false } : {};
|
||||
}
|
||||
}
|
||||
|
||||
if (content) {
|
||||
promises.push(
|
||||
MatrixClientPeg.get().setRoomAccountData(
|
||||
this.props.room.roomId, "org.matrix.room.preview_urls", content
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
console.log("UrlPreviewSettings: saveSettings: " + JSON.stringify(promises));
|
||||
|
||||
const promises = [];
|
||||
if (this.refs.urlPreviewsRoom) promises.push(this.refs.urlPreviewsRoom.save());
|
||||
if (this.refs.urlPreviewsSelf) promises.push(this.refs.urlPreviewsSelf.save());
|
||||
return promises;
|
||||
},
|
||||
|
||||
onGlobalDisableUrlPreviewChange: function() {
|
||||
this.setState({
|
||||
globalDisableUrlPreview: this.refs.globalDisableUrlPreview.checked ? true : false,
|
||||
});
|
||||
},
|
||||
|
||||
onUserEnableUrlPreviewChange: function() {
|
||||
this.setState({
|
||||
userDisableUrlPreview: false,
|
||||
userEnableUrlPreview: this.refs.userEnableUrlPreview.checked ? true : false,
|
||||
});
|
||||
},
|
||||
|
||||
onUserDisableUrlPreviewChange: function() {
|
||||
this.setState({
|
||||
userDisableUrlPreview: this.refs.userDisableUrlPreview.checked ? true : false,
|
||||
userEnableUrlPreview: false,
|
||||
});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var self = this;
|
||||
var roomState = this.props.room.currentState;
|
||||
var cli = MatrixClientPeg.get();
|
||||
const SettingsFlag = sdk.getComponent("elements.SettingsFlag");
|
||||
const roomId = this.props.room.roomId;
|
||||
|
||||
var maySetRoomPreviewUrls = roomState.mayClientSendStateEvent('org.matrix.room.preview_urls', cli);
|
||||
var disableRoomPreviewUrls;
|
||||
if (maySetRoomPreviewUrls) {
|
||||
disableRoomPreviewUrls =
|
||||
<label>
|
||||
<input type="checkbox" ref="globalDisableUrlPreview"
|
||||
onChange={ this.onGlobalDisableUrlPreviewChange }
|
||||
checked={ this.state.globalDisableUrlPreview } />
|
||||
{_t("Disable URL previews by default for participants in this room")}
|
||||
</label>;
|
||||
}
|
||||
else {
|
||||
disableRoomPreviewUrls =
|
||||
<label>
|
||||
{_t("URL previews are %(globalDisableUrlPreview)s by default for participants in this room.", {globalDisableUrlPreview: this.state.globalDisableUrlPreview ? _t("disabled") : _t("enabled")})}
|
||||
</label>;
|
||||
}
|
||||
|
||||
let urlPreviewText = null;
|
||||
if (UserSettingsStore.getUrlPreviewsDisabled()) {
|
||||
urlPreviewText = (
|
||||
_tJsx("You have <a>disabled</a> URL previews by default.", /<a>(.*?)<\/a>/, (sub)=><a href="#/settings">{sub}</a>)
|
||||
let previewsForAccount = null;
|
||||
if (SettingsStore.getValueAt(SettingLevel.ACCOUNT, "urlPreviewsEnabled")) {
|
||||
previewsForAccount = (
|
||||
_t("You have <a>enabled</a> URL previews by default.", {}, { 'a': (sub)=><a href="#/settings">{ sub }</a> })
|
||||
);
|
||||
} else {
|
||||
previewsForAccount = (
|
||||
_t("You have <a>disabled</a> URL previews by default.", {}, { 'a': (sub)=><a href="#/settings">{ sub }</a> })
|
||||
);
|
||||
}
|
||||
else {
|
||||
urlPreviewText = (
|
||||
_tJsx("You have <a>enabled</a> URL previews by default.", /<a>(.*?)<\/a>/, (sub)=><a href="#/settings">{sub}</a>)
|
||||
|
||||
let previewsForRoom = null;
|
||||
if (SettingsStore.canSetValue("urlPreviewsEnabled", roomId, "room")) {
|
||||
previewsForRoom = (
|
||||
<label>
|
||||
<SettingsFlag name="urlPreviewsEnabled"
|
||||
level={SettingLevel.ROOM}
|
||||
roomId={this.props.room.roomId}
|
||||
isExplicit={true}
|
||||
manualSave={true}
|
||||
ref="urlPreviewsRoom" />
|
||||
</label>
|
||||
);
|
||||
} else {
|
||||
let str = _td("URL previews are enabled by default for participants in this room.");
|
||||
if (!SettingsStore.getValueAt(SettingLevel.ROOM, "urlPreviewsEnabled", roomId, /*explicit=*/true)) {
|
||||
str = _td("URL previews are disabled by default for participants in this room.");
|
||||
}
|
||||
previewsForRoom = (<label>{ _t(str) }</label>);
|
||||
}
|
||||
|
||||
const previewsForRoomAccount = (
|
||||
<SettingsFlag name="urlPreviewsEnabled"
|
||||
level={SettingLevel.ROOM_ACCOUNT}
|
||||
roomId={this.props.room.roomId}
|
||||
manualSave={true}
|
||||
ref="urlPreviewsSelf"
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="mx_RoomSettings_toggles">
|
||||
<h3>{_t("URL Previews")}</h3>
|
||||
<h3>{ _t("URL Previews") }</h3>
|
||||
|
||||
<label>
|
||||
{urlPreviewText}
|
||||
</label>
|
||||
{ disableRoomPreviewUrls }
|
||||
<label>
|
||||
<input type="checkbox" ref="userEnableUrlPreview"
|
||||
onChange={ this.onUserEnableUrlPreviewChange }
|
||||
checked={ this.state.userEnableUrlPreview } />
|
||||
{_t("Enable URL previews for this room (affects only you)")}
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" ref="userDisableUrlPreview"
|
||||
onChange={ this.onUserDisableUrlPreviewChange }
|
||||
checked={ this.state.userDisableUrlPreview } />
|
||||
{_t("Disable URL previews for this room (affects only you)")}
|
||||
</label>
|
||||
<label>{ previewsForAccount }</label>
|
||||
{ previewsForRoom }
|
||||
<label>{ previewsForRoomAccount }</label>
|
||||
</div>
|
||||
);
|
||||
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -27,6 +27,7 @@ import ScalarAuthClient from '../../../ScalarAuthClient';
|
|||
import ScalarMessaging from '../../../ScalarMessaging';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import WidgetUtils from '../../../WidgetUtils';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
|
||||
// The maximum number of widgets that can be added in a room
|
||||
const MAX_WIDGETS = 2;
|
||||
|
@ -81,16 +82,25 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
onAction: function(action) {
|
||||
const hideWidgetKey = this.props.room.roomId + "_hide_widget_drawer";
|
||||
switch (action.action) {
|
||||
case 'appsDrawer':
|
||||
// When opening the app draw when there aren't any apps, auto-launch the
|
||||
// integrations manager to skip the awkward click on "Add widget"
|
||||
// When opening the app drawer when there aren't any apps,
|
||||
// auto-launch the integrations manager to skip the awkward
|
||||
// click on "Add widget"
|
||||
if (action.show) {
|
||||
const apps = this._getApps();
|
||||
if (apps.length === 0) {
|
||||
this._launchManageIntegrations();
|
||||
}
|
||||
|
||||
localStorage.removeItem(hideWidgetKey);
|
||||
} else {
|
||||
// Store hidden state of widget
|
||||
// Don't show if previously hidden
|
||||
localStorage.setItem(hideWidgetKey, true);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
@ -122,16 +132,22 @@ module.exports = React.createClass({
|
|||
'$matrix_room_id': this.props.room.roomId,
|
||||
'$matrix_display_name': user ? user.displayName : this.props.userId,
|
||||
'$matrix_avatar_url': user ? MatrixClientPeg.get().mxcUrlToHttp(user.avatarUrl) : '',
|
||||
};
|
||||
|
||||
if(app.data) {
|
||||
Object.keys(app.data).forEach((key) => {
|
||||
params['$' + key] = app.data[key];
|
||||
});
|
||||
}
|
||||
// TODO: Namespace themes through some standard
|
||||
'$theme': SettingsStore.getValue("theme"),
|
||||
};
|
||||
|
||||
app.id = appId;
|
||||
app.name = app.name || app.type;
|
||||
|
||||
if (app.data) {
|
||||
Object.keys(app.data).forEach((key) => {
|
||||
params['$' + key] = app.data[key];
|
||||
});
|
||||
|
||||
app.waitForIframeLoad = (app.data.waitForIframeLoad === 'false' ? false : true);
|
||||
}
|
||||
|
||||
app.url = this.encodeUri(app.url, params);
|
||||
app.creatorUserId = (sender && sender.userId) ? sender.userId : null;
|
||||
|
||||
|
@ -168,7 +184,7 @@ module.exports = React.createClass({
|
|||
_canUserModify: function() {
|
||||
try {
|
||||
return WidgetUtils.canUserModifyWidgets(this.props.room.roomId);
|
||||
} catch(err) {
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
return false;
|
||||
}
|
||||
|
@ -215,6 +231,8 @@ module.exports = React.createClass({
|
|||
userId={this.props.userId}
|
||||
show={this.props.showApps}
|
||||
creatorUserId={app.creatorUserId}
|
||||
widgetPageTitle={(app.data && app.data.title) ? app.data.title : ''}
|
||||
waitForIframeLoad={app.waitForIframeLoad}
|
||||
/>);
|
||||
});
|
||||
|
||||
|
@ -231,16 +249,16 @@ module.exports = React.createClass({
|
|||
"mx_AddWidget_button"
|
||||
}
|
||||
title={_t('Add a widget')}>
|
||||
[+] {_t('Add a widget')}
|
||||
[+] { _t('Add a widget') }
|
||||
</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx_AppsDrawer">
|
||||
<div id="apps" className="mx_AppsContainer">
|
||||
{apps}
|
||||
{ apps }
|
||||
</div>
|
||||
{this._canUserModify() && addWidget}
|
||||
{ this._canUserModify() && addWidget }
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -1,14 +1,34 @@
|
|||
/*
|
||||
Copyright 2016 Aviral Dasgupta
|
||||
Copyright 2017 New Vector 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.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import flatMap from 'lodash/flatMap';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
import sdk from '../../../index';
|
||||
import type {Completion} from '../../../autocomplete/Autocompleter';
|
||||
import Promise from 'bluebird';
|
||||
import UserSettingsStore from '../../../UserSettingsStore';
|
||||
import { Room } from 'matrix-js-sdk';
|
||||
|
||||
import {getCompletions} from '../../../autocomplete/Autocompleter';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import Autocompleter from '../../../autocomplete/Autocompleter';
|
||||
|
||||
const COMPOSER_SELECTED = 0;
|
||||
|
||||
|
@ -17,6 +37,7 @@ export default class Autocomplete extends React.Component {
|
|||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.autocompleter = new Autocompleter(props.room);
|
||||
this.completionPromise = null;
|
||||
this.hide = this.hide.bind(this);
|
||||
this.onCompletionClicked = this.onCompletionClicked.bind(this);
|
||||
|
@ -41,6 +62,11 @@ export default class Autocomplete extends React.Component {
|
|||
}
|
||||
|
||||
componentWillReceiveProps(newProps, state) {
|
||||
if (this.props.room.roomId !== newProps.room.roomId) {
|
||||
this.autocompleter.destroy();
|
||||
this.autocompleter = new Autocompleter(newProps.room);
|
||||
}
|
||||
|
||||
// Query hasn't changed so don't try to complete it
|
||||
if (newProps.query === this.props.query) {
|
||||
return;
|
||||
|
@ -49,6 +75,10 @@ export default class Autocomplete extends React.Component {
|
|||
this.complete(newProps.query, newProps.selection);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.autocompleter.destroy();
|
||||
}
|
||||
|
||||
complete(query, selection) {
|
||||
this.queryRequested = query;
|
||||
if (this.debounceCompletionsRequest) {
|
||||
|
@ -66,7 +96,7 @@ export default class Autocomplete extends React.Component {
|
|||
});
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
let autocompleteDelay = UserSettingsStore.getLocalSetting('autocompleteDelay', 200);
|
||||
let autocompleteDelay = SettingsStore.getValue("autocompleteDelay");
|
||||
|
||||
// Don't debounce if we are already showing completions
|
||||
if (this.state.completions.length > 0 || this.state.forceComplete) {
|
||||
|
@ -83,7 +113,7 @@ export default class Autocomplete extends React.Component {
|
|||
}
|
||||
|
||||
processQuery(query, selection) {
|
||||
return getCompletions(
|
||||
return this.autocompleter.getCompletions(
|
||||
query, selection, this.state.forceComplete,
|
||||
).then((completions) => {
|
||||
// Only ever process the completions for the most recent query being processed
|
||||
|
@ -233,7 +263,7 @@ export default class Autocomplete extends React.Component {
|
|||
const componentPosition = position;
|
||||
position++;
|
||||
|
||||
const onMouseOver = () => this.setSelection(componentPosition);
|
||||
const onMouseMove = () => this.setSelection(componentPosition);
|
||||
const onClick = () => {
|
||||
this.setSelection(componentPosition);
|
||||
this.onCompletionClicked();
|
||||
|
@ -243,7 +273,7 @@ export default class Autocomplete extends React.Component {
|
|||
key: i,
|
||||
ref: `completion${position - 1}`,
|
||||
className,
|
||||
onMouseOver,
|
||||
onMouseMove,
|
||||
onClick,
|
||||
});
|
||||
});
|
||||
|
@ -251,15 +281,15 @@ export default class Autocomplete extends React.Component {
|
|||
|
||||
return completions.length > 0 ? (
|
||||
<div key={i} className="mx_Autocomplete_ProviderSection">
|
||||
<EmojiText element="div" className="mx_Autocomplete_provider_name">{completionResult.provider.getName()}</EmojiText>
|
||||
{completionResult.provider.renderCompletions(completions)}
|
||||
<EmojiText element="div" className="mx_Autocomplete_provider_name">{ completionResult.provider.getName() }</EmojiText>
|
||||
{ completionResult.provider.renderCompletions(completions) }
|
||||
</div>
|
||||
) : null;
|
||||
}).filter((completion) => !!completion);
|
||||
|
||||
return !this.state.hide && renderedCompletions.length > 0 ? (
|
||||
<div className="mx_Autocomplete" ref={(e) => this.container = e}>
|
||||
{renderedCompletions}
|
||||
{ renderedCompletions }
|
||||
</div>
|
||||
) : null;
|
||||
}
|
||||
|
@ -267,8 +297,11 @@ export default class Autocomplete extends React.Component {
|
|||
|
||||
Autocomplete.propTypes = {
|
||||
// the query string for which to show autocomplete suggestions
|
||||
query: React.PropTypes.string.isRequired,
|
||||
query: PropTypes.string.isRequired,
|
||||
|
||||
// method invoked with range and text content when completion is confirmed
|
||||
onConfirm: React.PropTypes.func.isRequired,
|
||||
onConfirm: PropTypes.func.isRequired,
|
||||
|
||||
// The room in which we're autocompleting
|
||||
room: PropTypes.instanceOf(Room),
|
||||
};
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -20,8 +21,7 @@ import sdk from '../../../index';
|
|||
import dis from "../../../dispatcher";
|
||||
import ObjectUtils from '../../../ObjectUtils';
|
||||
import AppsDrawer from './AppsDrawer';
|
||||
import { _t, _tJsx} from '../../../languageHandler';
|
||||
import UserSettingsStore from '../../../UserSettingsStore';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
|
||||
module.exports = React.createClass({
|
||||
|
@ -83,9 +83,9 @@ module.exports = React.createClass({
|
|||
<div className="mx_RoomView_fileDropTarget">
|
||||
<div className="mx_RoomView_fileDropTargetLabel"
|
||||
title={_t("Drop File Here")}>
|
||||
<TintableSvg src="img/upload-big.svg" width="45" height="59"/>
|
||||
<br/>
|
||||
{_t("Drop file here to upload")}
|
||||
<TintableSvg src="img/upload-big.svg" width="45" height="59" />
|
||||
<br />
|
||||
{ _t("Drop file here to upload") }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -99,23 +99,23 @@ module.exports = React.createClass({
|
|||
supportedText = _t(" (unsupported)");
|
||||
} else {
|
||||
joinNode = (<span>
|
||||
{_tJsx(
|
||||
{ _t(
|
||||
"Join as <voiceText>voice</voiceText> or <videoText>video</videoText>.",
|
||||
[/<voiceText>(.*?)<\/voiceText>/, /<videoText>(.*?)<\/videoText>/],
|
||||
[
|
||||
(sub) => <a onClick={(event)=>{ this.onConferenceNotificationClick(event, 'voice');}} href="#">{sub}</a>,
|
||||
(sub) => <a onClick={(event)=>{ this.onConferenceNotificationClick(event, 'video');}} href="#">{sub}</a>,
|
||||
]
|
||||
)}
|
||||
{},
|
||||
{
|
||||
'voiceText': (sub) => <a onClick={(event)=>{ this.onConferenceNotificationClick(event, 'voice');}} href="#">{ sub }</a>,
|
||||
'videoText': (sub) => <a onClick={(event)=>{ this.onConferenceNotificationClick(event, 'video');}} href="#">{ sub }</a>,
|
||||
},
|
||||
) }
|
||||
</span>);
|
||||
}
|
||||
// XXX: the translation here isn't great: appending ' (unsupported)' is likely to not make sense in many languages,
|
||||
// but there are translations for this in the languages we do have so I'm leaving it for now.
|
||||
conferenceCallNotification = (
|
||||
<div className="mx_RoomView_ongoingConfCallNotification">
|
||||
{_t("Ongoing conference call%(supportedText)s.", {supportedText: supportedText})}
|
||||
{ _t("Ongoing conference call%(supportedText)s.", {supportedText: supportedText}) }
|
||||
|
||||
{joinNode}
|
||||
{ joinNode }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -128,15 +128,12 @@ module.exports = React.createClass({
|
|||
/>
|
||||
);
|
||||
|
||||
let appsDrawer = null;
|
||||
if(UserSettingsStore.isFeatureEnabled('matrix_apps')) {
|
||||
appsDrawer = <AppsDrawer ref="appsDrawer"
|
||||
room={this.props.room}
|
||||
userId={this.props.userId}
|
||||
maxHeight={this.props.maxHeight}
|
||||
showApps={this.props.showApps}
|
||||
/>;
|
||||
}
|
||||
const appsDrawer = <AppsDrawer ref="appsDrawer"
|
||||
room={this.props.room}
|
||||
userId={this.props.userId}
|
||||
maxHeight={this.props.maxHeight}
|
||||
showApps={this.props.showApps}
|
||||
/>;
|
||||
|
||||
return (
|
||||
<div className="mx_RoomView_auxPanel" style={{maxHeight: this.props.maxHeight}} >
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue