This commit is contained in:
Matthew Hodgson 2017-11-03 15:12:24 +00:00
commit 6747390333
35 changed files with 767 additions and 465 deletions

View file

@ -15,13 +15,14 @@ limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import GeminiScrollbar from 'react-gemini-scrollbar';
import {MatrixClient} from 'matrix-js-sdk';
import sdk from '../../index';
import { _t, _tJsx } from '../../languageHandler';
import withMatrixClient from '../../wrappers/withMatrixClient';
import AccessibleButton from '../views/elements/AccessibleButton';
import dis from '../../dispatcher';
import PropTypes from 'prop-types';
import Modal from '../../Modal';
import FlairStore from '../../stores/FlairStore';
@ -115,18 +116,17 @@ export default withMatrixClient(React.createClass({
const TintableSvg = sdk.getComponent("elements.TintableSvg");
let content;
let contentHeader;
if (this.state.groups) {
const groupNodes = [];
this.state.groups.forEach((g) => {
groupNodes.push(<GroupTile groupId={g} />);
});
contentHeader = groupNodes.length > 0 ? <h3>{ _t('Your Communities') }</h3> : <div />;
content = groupNodes.length > 0 ?
<div>
<h3>{ _t('Your Communities') }</h3>
<div className="mx_MyGroups_joinedGroups">
{ groupNodes }
</div>
</div> :
<GeminiScrollbar className="mx_MyGroups_joinedGroups">
{ groupNodes }
</GeminiScrollbar> :
<div className="mx_MyGroups_placeholder">
{ _t(
"You're not currently a member of any communities.",
@ -176,6 +176,7 @@ export default withMatrixClient(React.createClass({
</div>
</div>
<div className="mx_MyGroups_content">
{ contentHeader }
{ content }
</div>
</div>;

View file

@ -44,8 +44,6 @@ const Rooms = require('../../Rooms');
import KeyCode from '../../KeyCode';
import UserProvider from '../../autocomplete/UserProvider';
import RoomViewStore from '../../stores/RoomViewStore';
import RoomScrollStateStore from '../../stores/RoomScrollStateStore';
@ -541,12 +539,6 @@ module.exports = React.createClass({
});
}
}
// update the tab complete list as it depends on who most recently spoke,
// and that has probably just changed
if (ev.sender) {
UserProvider.getInstance().onUserSpoke(ev.sender);
}
},
onRoomName: function(room) {
@ -568,7 +560,6 @@ module.exports = React.createClass({
this._warnAboutEncryption(room);
this._calculatePeekRules(room);
this._updatePreviewUrlVisibility(room);
UserProvider.getInstance().setUserListFromRoom(room);
},
_warnAboutEncryption: function(room) {
@ -722,9 +713,6 @@ module.exports = React.createClass({
// refresh the conf call notification state
this._updateConfCallNotification();
// refresh the tab complete list
UserProvider.getInstance().setUserListFromRoom(this.state.room);
// if we are now a member of the room, where we were not before, that
// means we have finished joining a room we were previously peeking
// into.

View file

@ -168,7 +168,7 @@ module.exports = React.createClass({
} else if (this.state.progress === "sent_email") {
resetPasswordJsx = (
<div className="mx_Login_prompt">
{ _t('An email has been sent to') } { this.state.email }. { _t("Once you've followed the link it contains, click below") }.
{ _t("An email has been sent to %(emailAddress)s. Once you've followed the link it contains, click below.", { emailAddress: this.state.email }) }
<br />
<input className="mx_Login_submit" type="button" onClick={this.onVerify}
value={_t('I have verified my email address')} />

View file

@ -54,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.groupName || this.props.groupId[1]}
name={groupName || this.props.groupId[1]}
idName={this.props.groupId}
url={this.getGroupAvatarUrl()}
{...otherProps}

View file

@ -36,6 +36,7 @@ export default React.createClass({
// group member object. Supply either this or 'member'
groupMember: GroupMemberType,
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
@ -75,7 +76,6 @@ export default React.createClass({
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,
@ -113,7 +113,7 @@ export default React.createClass({
return (
<BaseDialog className="mx_ConfirmUserActionDialog" onFinished={this.props.onFinished}
onEnterPressed={this.onOk}
title={title}
title={this.props.title}
>
<div className="mx_Dialog_content">
<div className="mx_ConfirmUserActionDialog_avatar">

View file

@ -86,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(',');
@ -101,13 +100,13 @@ module.exports = React.createClass({
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) {
@ -208,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) {
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;
}
@ -376,11 +302,9 @@ module.exports = React.createClass({
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 });

View file

@ -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;

View file

@ -44,21 +44,21 @@ export default React.createClass({
const label = <EmojiText
element="div"
title={groupName}
className="mx_GroupInviteTile_name"
title={this.props.group.groupId}
className="mx_RoomTile_name"
dir="auto"
>
{ groupName }
</EmojiText>;
const badge = <div className="mx_GroupInviteTile_badge">!</div>;
const badge = <div className="mx_RoomSubList_badge mx_RoomSubList_badgeHighlight">!</div>;
return (
<AccessibleButton className="mx_GroupInviteTile" onClick={this.onClick}>
<div className="mx_GroupInviteTile_avatarContainer">
<AccessibleButton className="mx_RoomTile mx_RoomTile_highlight" onClick={this.onClick}>
<div className="mx_RoomTile_avatar">
{ av }
</div>
<div className="mx_GroupInviteTile_nameContainer">
<div className="mx_RoomTile_nameContainer">
{ label }
{ badge }
</div>

View file

@ -85,6 +85,8 @@ module.exports = React.createClass({
Modal.createDialog(ConfirmUserActionDialog, {
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;

View file

@ -0,0 +1,242 @@
/*
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.context.matrixClient, 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.updateGroupRoomAssociation(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>
);
},
});

View file

@ -16,13 +16,10 @@ limitations under the License.
import React from 'react';
import {MatrixClient} from 'matrix-js-sdk';
import { _t } from '../../../languageHandler';
import PropTypes from 'prop-types';
import sdk from '../../../index';
import dis from '../../../dispatcher';
import { GroupRoomType } from '../../../groups';
import GroupStoreCache from '../../../stores/GroupStoreCache';
import Modal from '../../../Modal';
const GroupRoomTile = React.createClass({
displayName: 'GroupRoomTile',
@ -32,68 +29,11 @@ const GroupRoomTile = React.createClass({
groupRoom: GroupRoomType.isRequired,
},
getInitialState: function() {
return {
name: this.calculateRoomName(this.props.groupRoom),
};
},
componentWillReceiveProps: function(newProps) {
this.setState({
name: this.calculateRoomName(newProps.groupRoom),
});
},
calculateRoomName: function(groupRoom) {
return groupRoom.name || groupRoom.canonicalAlias || _t("Unnamed Room");
},
removeRoomFromGroup: function() {
const groupId = this.props.groupId;
const groupStore = GroupStoreCache.getGroupStore(this.context.matrixClient, groupId);
const roomName = this.state.name;
const roomId = this.props.groupRoom.roomId;
groupStore.removeRoomFromGroup(roomId)
.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}),
});
});
},
onClick: function(e) {
let roomId;
let roomAlias;
if (this.props.groupRoom.canonicalAlias) {
roomAlias = this.props.groupRoom.canonicalAlias;
} else {
roomId = this.props.groupRoom.roomId;
}
dis.dispatch({
action: 'view_room',
room_id: roomId,
room_alias: roomAlias,
});
},
onDeleteClick: function(e) {
const groupId = this.props.groupId;
const roomName = this.state.name;
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: (success) => {
if (success) {
this.removeRoomFromGroup();
}
},
action: 'view_group_room',
groupId: this.props.groupId,
groupRoomId: this.props.groupRoom.roomId,
});
},
@ -106,7 +46,7 @@ const GroupRoomTile = React.createClass({
);
const av = (
<BaseAvatar name={this.state.name}
<BaseAvatar name={this.props.groupRoom.displayname}
width={36} height={36}
url={avatarUrl}
/>
@ -118,14 +58,8 @@ const GroupRoomTile = React.createClass({
{ av }
</div>
<div className="mx_GroupRoomTile_name">
{ this.state.name }
{ this.props.groupRoom.displayname }
</div>
<AccessibleButton className="mx_GroupRoomTile_delete"
onClick={this.onDeleteClick}
tooltip={_t("Remove this room from the community")}
>
<img src="img/cancel.svg" width="15" height="15" className="mx_filterFlipColor" />
</AccessibleButton>
</AccessibleButton>
);
},

View file

@ -20,7 +20,7 @@ import url from 'url';
import classnames from 'classnames';
import sdk from '../../../index';
import { _t } from '../../../languageHandler';
import { _t, _tJsx } from '../../../languageHandler';
/* This file contains a collection of components which are used by the
* InteractiveAuth to prompt the user to enter the information needed
@ -256,7 +256,7 @@ export const EmailIdentityAuthEntry = React.createClass({
} else {
return (
<div>
<p>{ _t("An email has been sent to") } <i>{ this.props.inputs.emailAddress }</i></p>
<p>{ _tJsx("An email has been sent to %(emailAddress)s", /%\(emailAddress\)s/, (sub) => <i>{this.props.inputs.emailAddress}</i>) }</p>
<p>{ _t("Please check your email to continue registration.") }</p>
</div>
);
@ -370,7 +370,7 @@ export const MsisdnAuthEntry = React.createClass({
});
return (
<div>
<p>{ _t("A text message has been sent to") } +<i>{ this._msisdn }</i></p>
<p>{ _tJsx("A text message has been sent to %(msisdn)s", /%\(msisdn\)s/, (sub) => <i>{this._msisdn}</i>) }</p>
<p>{ _t("Please enter the code it contains:") }</p>
<div className="mx_InteractiveAuthEntryComponents_msisdnWrapper">
<form onSubmit={this._onFormSubmit}>

View file

@ -19,6 +19,7 @@
import React from 'react';
import sdk from '../../../index';
import Flair from '../elements/Flair.js';
import { _tJsx } from '../../../languageHandler';
export default function SenderProfile(props) {
const EmojiText = sdk.getComponent('elements.EmojiText');
@ -30,23 +31,39 @@ export default function SenderProfile(props) {
return <span />; // emote message must include the name so don't duplicate it
}
// Name + flair
const nameElem = [
<EmojiText key='name' className="mx_SenderProfile_name">{ name || '' }</EmojiText>,
props.enableFlair ?
<Flair key='flair'
userId={mxEvent.getSender()}
roomId={mxEvent.getRoomId()}
showRelated={true} />
: null,
];
let content = '';
if(props.text) {
// Replace senderName, and wrap surrounding text in spans with the right class
content = _tJsx(props.text, /^(.*)\%\(senderName\)s(.*)$/m, (p1, p2) => [
p1 ? <span className='mx_SenderProfile_aux'>{ p1 }</span> : null,
nameElem,
p2 ? <span className='mx_SenderProfile_aux'>{ p2 }</span> : null,
]);
} else {
content = nameElem;
}
return (
<div className="mx_SenderProfile" dir="auto" onClick={props.onClick}>
<EmojiText className="mx_SenderProfile_name">{ name || '' }</EmojiText>
{ props.enableFlair ?
<Flair
userId={mxEvent.getSender()}
roomId={mxEvent.getRoomId()}
showRelated={true} />
: null
}
{ props.aux ? <EmojiText className="mx_SenderProfile_aux"> { props.aux }</EmojiText> : null }
{ content }
</div>
);
}
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
text: React.PropTypes.string, // Text to show. Defaults to sender name
onClick: React.PropTypes.func,
};

View file

@ -34,6 +34,7 @@ import MatrixClientPeg from '../../../MatrixClientPeg';
import ContextualMenu from '../../structures/ContextualMenu';
import {RoomMember} from 'matrix-js-sdk';
import classNames from 'classnames';
import PushProcessor from 'matrix-js-sdk/lib/pushprocessor';
linkifyMatrix(linkify);
@ -169,8 +170,10 @@ 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];
let node = nodes[0];
while (node) {
let pillified = false;
if (node.tagName === "A" && node.getAttribute("href")) {
const href = node.getAttribute("href");
@ -189,10 +192,71 @@ 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 (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;
}
},

View file

@ -1,5 +1,23 @@
/*
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';
@ -7,8 +25,9 @@ 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 Autocompleter from '../../../autocomplete/Autocompleter';
const COMPOSER_SELECTED = 0;
@ -17,6 +36,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 +61,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 +74,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) {
@ -83,7 +112,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
@ -267,8 +296,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),
};

View file

@ -19,7 +19,7 @@ limitations under the License.
const React = require('react');
const classNames = require("classnames");
import { _t } from '../../../languageHandler';
import { _t, _td } from '../../../languageHandler';
const Modal = require('../../../Modal');
const sdk = require('../../../index');
@ -502,12 +502,12 @@ module.exports = withMatrixClient(React.createClass({
}
if (needsSenderProfile) {
let aux = null;
let text = null;
if (!this.props.tileShape) {
if (msgtype === 'm.image') aux = _t('sent an image');
else if (msgtype === 'm.video') aux = _t('sent a video');
else if (msgtype === 'm.file') aux = _t('uploaded a file');
sender = <SenderProfile onClick={this.onSenderProfileClick} mxEvent={this.props.mxEvent} enableFlair={!aux} aux={aux} />;
if (msgtype === 'm.image') text = _td('%(senderName)s sent an image');
else if (msgtype === 'm.video') text = _td('%(senderName)s sent a video');
else if (msgtype === 'm.file') text = _td('%(senderName)s uploaded a file');
sender = <SenderProfile onClick={this.onSenderProfileClick} mxEvent={this.props.mxEvent} enableFlair={!text} text={text} />;
} else {
sender = <SenderProfile mxEvent={this.props.mxEvent} enableFlair={true} />;
}

View file

@ -256,11 +256,11 @@ module.exports = withMatrixClient(React.createClass({
onKick: function() {
const membership = this.props.member.membership;
const kickLabel = membership === "invite" ? _t("Disinvite") : _t("Kick");
const ConfirmUserActionDialog = sdk.getComponent("dialogs.ConfirmUserActionDialog");
Modal.createTrackedDialog('Confirm User Action Dialog', 'onKick', ConfirmUserActionDialog, {
member: this.props.member,
action: kickLabel,
action: membership === "invite" ? _t("Disinvite") : _t("Kick"),
title: membership === "invite" ? _t("Disinvite this user?") : _t("Kick this user?"),
askReason: membership === "join",
danger: true,
onFinished: (proceed, reason) => {
@ -294,6 +294,7 @@ module.exports = withMatrixClient(React.createClass({
Modal.createTrackedDialog('Confirm User Action Dialog', 'onBanOrUnban', ConfirmUserActionDialog, {
member: this.props.member,
action: this.props.member.membership === 'ban' ? _t("Unban") : _t("Ban"),
title: this.props.member.membership === 'ban' ? _t("Unban this user?") : _t("Ban this user?"),
askReason: this.props.member.membership !== 'ban',
danger: this.props.member.membership !== 'ban',
onFinished: (proceed, reason) => {

View file

@ -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.
@ -1130,10 +1131,12 @@ export default class MessageComposerInput extends React.Component {
<div className="mx_MessageComposer_autocomplete_wrapper">
<Autocomplete
ref={(e) => this.autocomplete = e}
room={this.props.room}
onConfirm={this.setDisplayedCompletion}
onSelectionChange={this.setDisplayedCompletion}
query={this.getAutocompleteQuery(content)}
selection={selection} />
selection={selection}
/>
</div>
<div className={className}>
<img className="mx_MessageComposer_input_markdownIndicator mx_filterFlipColor"

View file

@ -29,18 +29,20 @@ function getDisplayAliasForRoom(room) {
}
const RoomDetailRow = React.createClass({
propTypes: PropTypes.shape({
name: PropTypes.string,
topic: PropTypes.string,
roomId: PropTypes.string,
avatarUrl: PropTypes.string,
numJoinedMembers: PropTypes.number,
canonicalAlias: PropTypes.string,
aliases: PropTypes.arrayOf(PropTypes.string),
propTypes: {
room: PropTypes.shape({
name: PropTypes.string,
topic: PropTypes.string,
roomId: PropTypes.string,
avatarUrl: PropTypes.string,
numJoinedMembers: PropTypes.number,
canonicalAlias: PropTypes.string,
aliases: PropTypes.arrayOf(PropTypes.string),
worldReadable: PropTypes.bool,
guestCanJoin: PropTypes.bool,
}),
worldReadable: PropTypes.bool,
guestCanJoin: PropTypes.bool,
}),
},
onClick: function(ev) {
ev.preventDefault();

View file

@ -34,27 +34,18 @@ const Receipt = require('../../../utils/Receipt');
const HIDE_CONFERENCE_CHANS = true;
function phraseForSection(section) {
// These would probably be better as individual strings,
// but for some reason we have translations for these strings
// as-is, so keeping it like this for now.
let verb;
switch (section) {
case 'm.favourite':
verb = _t('to favourite');
break;
return _t('Drop here to favourite');
case 'im.vector.fake.direct':
verb = _t('to tag direct chat');
break;
return _t('Drop here to tag direct chat');
case 'im.vector.fake.recent':
verb = _t('to restore');
break;
return _t('Drop here to restore');
case 'm.lowpriority':
verb = _t('to demote');
break;
return _t('Drop here to demote');
default:
return _t('Drop here to tag %(section)s', {section: section});
}
return _t('Drop here %(toAction)s', {toAction: verb});
}
module.exports = React.createClass({
@ -564,13 +555,23 @@ module.exports = React.createClass({
render: function() {
const RoomSubList = sdk.getComponent('structures.RoomSubList');
const inviteSectionExtraTiles = this._makeGroupInviteTiles();
const self = this;
return (
<GeminiScrollbar className="mx_RoomList_scrollbar"
autoshow={true} onScroll={self._whenScrolling} ref="gemscroll">
<div className="mx_RoomList">
<RoomSubList list={[]}
extraTiles={this._makeGroupInviteTiles()}
label={_t('Community Invites')}
editable={false}
order="recent"
isInvite={true}
collapsed={self.props.collapsed}
searchFilter={self.props.searchFilter}
onHeaderClick={self.onSubListHeaderClick}
onShowMoreRooms={self.onShowMoreRooms}
/>
<RoomSubList list={self.state.lists['im.vector.fake.invite']}
label={_t('Invites')}
editable={false}
@ -582,7 +583,6 @@ module.exports = React.createClass({
searchFilter={self.props.searchFilter}
onHeaderClick={self.onSubListHeaderClick}
onShowMoreRooms={self.onShowMoreRooms}
extraTiles={inviteSectionExtraTiles}
/>
<RoomSubList list={self.state.lists['m.favourite']}

View file

@ -83,10 +83,8 @@ module.exports = React.createClass({
}
},
_roomNameElement: function(fallback) {
fallback = fallback || _t('a room');
const name = this.props.room ? this.props.room.name : (this.props.room_alias || "");
return name ? name : fallback;
_roomNameElement: function() {
return this.props.room ? this.props.room.name : (this.props.room_alias || "");
},
render: function() {
@ -150,7 +148,7 @@ module.exports = React.createClass({
</div>
);
} else if (kicked || banned) {
const roomName = this._roomNameElement(_t('This room'));
const roomName = this._roomNameElement();
const kickerMember = this.props.room.currentState.getMember(
myMember.events.member.getSender(),
);
@ -167,9 +165,17 @@ module.exports = React.createClass({
let actionText;
if (kicked) {
actionText = _t("You have been kicked from %(roomName)s by %(userName)s.", {roomName: roomName, userName: kickerName});
if(roomName) {
actionText = _t("You have been kicked from %(roomName)s by %(userName)s.", {roomName: roomName, userName: kickerName});
} else {
actionText = _t("You have been kicked from this room by %(userName)s.", {userName: kickerName});
}
} else if (banned) {
actionText = _t("You have been banned from %(roomName)s by %(userName)s.", {roomName: roomName, userName: kickerName});
if(roomName) {
actionText = _t("You have been banned from %(roomName)s by %(userName)s.", {roomName: roomName, userName: kickerName});
} else {
actionText = _t("You have been banned from this room by %(userName)s.", {userName: kickerName});
}
} // no other options possible due to the kicked || banned check above.
joinBlock = (
@ -203,7 +209,7 @@ module.exports = React.createClass({
joinBlock = (
<div>
<div className="mx_RoomPreviewBar_join_text">
{ _t('You are trying to access %(roomName)s.', {roomName: name}) }
{ name ? _t('You are trying to access %(roomName)s.', {roomName: name}) : _t('You are trying to access a room.') }
<br />
{ _tJsx("<a>Click here</a> to join the discussion!",
/<a>(.*?)<\/a>/,

View file

@ -71,6 +71,7 @@ const BannedUser = React.createClass({
Modal.createTrackedDialog('Confirm User Action Dialog', 'onUnbanClick', ConfirmUserActionDialog, {
member: this.props.member,
action: _t('Unban'),
title: _t('Unban this user?'),
danger: false,
onFinished: (proceed) => {
if (!proceed) return;
@ -866,21 +867,21 @@ module.exports = React.createClass({
disabled={!roomState.mayClientSendStateEvent("m.room.history_visibility", cli)}
checked={historyVisibility === "shared"}
onChange={this._onHistoryRadioToggle} />
{ _t('Members only') } ({ _t('since the point in time of selecting this option') })
{ _t('Members only (since the point in time of selecting this option)') }
</label>
<label>
<input type="radio" name="historyVis" value="invited"
disabled={!roomState.mayClientSendStateEvent("m.room.history_visibility", cli)}
checked={historyVisibility === "invited"}
onChange={this._onHistoryRadioToggle} />
{ _t('Members only') } ({ _t('since they were invited') })
{ _t('Members only (since they were invited)') }
</label>
<label >
<input type="radio" name="historyVis" value="joined"
disabled={!roomState.mayClientSendStateEvent("m.room.history_visibility", cli)}
checked={historyVisibility === "joined"}
onChange={this._onHistoryRadioToggle} />
{ _t('Members only') } ({ _t('since they joined') })
{ _t('Members only (since they joined)') }
</label>
</div>
</div>