Merge branch 'develop' of https://github.com/matrix-org/matrix-react-sdk into rxl881/widgetrendering

This commit is contained in:
Richard Lewis 2017-11-02 18:34:46 +00:00
commit 853ada027d
34 changed files with 854 additions and 454 deletions

View file

@ -19,7 +19,7 @@ import PlatformPeg from './PlatformPeg';
import SdkConfig from './SdkConfig'; import SdkConfig from './SdkConfig';
function getRedactedUrl() { function getRedactedUrl() {
const redactedHash = window.location.hash.replace(/#\/(room|user)\/(.+)/, "#/$1/<redacted>"); const redactedHash = window.location.hash.replace(/#\/(group|room|user)\/(.+)/, "#/$1/<redacted>");
// hardcoded url to make piwik happy // hardcoded url to make piwik happy
return 'https://riot.im/app/' + redactedHash; return 'https://riot.im/app/' + redactedHash;
} }

View file

@ -143,17 +143,8 @@ export default class Login {
Object.assign(loginParams, legacyParams); Object.assign(loginParams, legacyParams);
const client = this._createTemporaryClient(); const client = this._createTemporaryClient();
return client.login('m.login.password', loginParams).then(function(data) {
return Promise.resolve({ const tryFallbackHs = (originalError) => {
homeserverUrl: self._hsUrl,
identityServerUrl: self._isUrl,
userId: data.user_id,
deviceId: data.device_id,
accessToken: data.access_token,
});
}, function(error) {
if (error.httpStatus === 403) {
if (self._fallbackHsUrl) {
const fbClient = Matrix.createClient({ const fbClient = Matrix.createClient({
baseUrl: self._fallbackHsUrl, baseUrl: self._fallbackHsUrl,
idBaseUrl: this._isUrl, idBaseUrl: this._isUrl,
@ -167,12 +158,62 @@ export default class Login {
deviceId: data.device_id, deviceId: data.device_id,
accessToken: data.access_token, accessToken: data.access_token,
}); });
}, function(fallback_error) { }).catch((fallback_error) => {
console.log("fallback HS login failed", fallback_error);
// throw the original error // throw the original error
throw error; throw originalError;
}); });
};
const tryLowercaseUsername = (originalError) => {
const loginParamsLowercase = Object.assign({}, loginParams, {
user: username.toLowerCase(),
identifier: {
user: username.toLowerCase(),
},
});
return client.login('m.login.password', loginParamsLowercase).then(function(data) {
return Promise.resolve({
homeserverUrl: self._hsUrl,
identityServerUrl: self._isUrl,
userId: data.user_id,
deviceId: data.device_id,
accessToken: data.access_token,
});
}).catch((fallback_error) => {
console.log("Lowercase username login failed", fallback_error);
// throw the original error
throw originalError;
});
};
let originalLoginError = null;
return client.login('m.login.password', loginParams).then(function(data) {
return Promise.resolve({
homeserverUrl: self._hsUrl,
identityServerUrl: self._isUrl,
userId: data.user_id,
deviceId: data.device_id,
accessToken: data.access_token,
});
}).catch((error) => {
originalLoginError = error;
if (error.httpStatus === 403) {
if (self._fallbackHsUrl) {
return tryFallbackHs(originalLoginError);
} }
} }
throw originalLoginError;
}).catch((error) => {
if (
error.httpStatus === 403 &&
loginParams.identifier.type === 'm.id.user' &&
username.search(/[A-Z]/) > -1
) {
return tryLowercaseUsername(originalLoginError);
}
throw originalLoginError;
}).catch((error) => {
console.log("Login failed", error);
throw error; throw error;
}); });
} }

View file

@ -21,6 +21,8 @@ import Modal from './Modal';
import { getAddressType } from './UserAddress'; import { getAddressType } from './UserAddress';
import createRoom from './createRoom'; import createRoom from './createRoom';
import sdk from './'; import sdk from './';
import dis from './dispatcher';
import DMRoomMap from './utils/DMRoomMap';
import { _t } from './languageHandler'; import { _t } from './languageHandler';
export function inviteToRoom(roomId, addr) { export function inviteToRoom(roomId, addr) {
@ -79,15 +81,40 @@ function _onStartChatFinished(shouldInvite, addrs) {
const addrTexts = addrs.map((addr) => addr.address); const addrTexts = addrs.map((addr) => addr.address);
if (_isDmChat(addrTexts)) { if (_isDmChat(addrTexts)) {
const rooms = _getDirectMessageRooms(addrTexts[0]);
if (rooms.length > 0) {
// A Direct Message room already exists for this user, so select a
// room from a list that is similar to the one in MemberInfo panel
const ChatCreateOrReuseDialog = sdk.getComponent(
"views.dialogs.ChatCreateOrReuseDialog",
);
const close = Modal.createTrackedDialog('Create or Reuse', '', ChatCreateOrReuseDialog, {
userId: addrTexts[0],
onNewDMClick: () => {
dis.dispatch({
action: 'start_chat',
user_id: addrTexts[0],
});
close(true);
},
onExistingRoomSelected: (roomId) => {
dis.dispatch({
action: 'view_room',
room_id: roomId,
});
close(true);
},
}).close;
} else {
// Start a new DM chat // Start a new DM chat
createRoom({dmUserId: addrTexts[0]}).catch((err) => { createRoom({dmUserId: addrTexts[0]}).catch((err) => {
console.error(err.stack);
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Failed to invite user', '', ErrorDialog, { Modal.createTrackedDialog('Failed to invite user', '', ErrorDialog, {
title: _t("Failed to invite user"), title: _t("Failed to invite user"),
description: ((err && err.message) ? err.message : _t("Operation failed")), description: ((err && err.message) ? err.message : _t("Operation failed")),
}); });
}); });
}
} else { } else {
// Start multi user chat // Start multi user chat
let room; let room;
@ -153,3 +180,19 @@ function _showAnyInviteErrors(addrs, room) {
return addrs; return addrs;
} }
function _getDirectMessageRooms(addr) {
const dmRoomMap = new DMRoomMap(MatrixClientPeg.get());
const dmRooms = dmRoomMap.getDMRoomsForUserId(addr);
const rooms = [];
dmRooms.forEach((dmRoom) => {
const room = MatrixClientPeg.get().getRoom(dmRoom);
if (room) {
const me = room.getMember(MatrixClientPeg.get().credentials.userId);
if (me.membership == 'join') {
rooms.push(room);
}
}
});
return rooms;
}

View file

@ -68,9 +68,7 @@ module.exports = {
const names = whoIsTyping.map(function(m) { const names = whoIsTyping.map(function(m) {
return m.name; return m.name;
}); });
if (othersCount==1) { if (othersCount>=1) {
return _t('%(names)s and one other are typing', {names: names.slice(0, limit - 1).join(', ')});
} else if (othersCount>1) {
return _t('%(names)s and %(count)s others are typing', {names: names.slice(0, limit - 1).join(', '), count: othersCount}); return _t('%(names)s and %(count)s others are typing', {names: names.slice(0, limit - 1).join(', '), count: othersCount});
} else { } else {
const lastPerson = names.pop(); const lastPerson = names.pop();

View file

@ -407,6 +407,10 @@ export default React.createClass({
getInitialState: function() { getInitialState: function() {
return { return {
summary: null, summary: null,
isGroupPublicised: null,
isUserPrivileged: null,
groupRooms: null,
groupRoomsLoading: null,
error: null, error: null,
editing: false, editing: false,
saving: false, saving: false,
@ -447,7 +451,7 @@ export default React.createClass({
_initGroupStore: function(groupId) { _initGroupStore: function(groupId) {
this._groupStore = GroupStoreCache.getGroupStore(MatrixClientPeg.get(), groupId); this._groupStore = GroupStoreCache.getGroupStore(MatrixClientPeg.get(), groupId);
this._groupStore.on('update', () => { this._groupStore.registerListener(() => {
const summary = this._groupStore.getSummary(); const summary = this._groupStore.getSummary();
if (summary.profile) { if (summary.profile) {
// Default profile fields should be "" for later sending to the server (which // Default profile fields should be "" for later sending to the server (which
@ -458,13 +462,18 @@ export default React.createClass({
} }
this.setState({ this.setState({
summary, summary,
summaryLoading: !this._groupStore.isStateReady(GroupStore.STATE_KEY.Summary),
isGroupPublicised: this._groupStore.getGroupPublicity(), isGroupPublicised: this._groupStore.getGroupPublicity(),
isUserPrivileged: this._groupStore.isUserPrivileged(), isUserPrivileged: this._groupStore.isUserPrivileged(),
groupRooms: this._groupStore.getGroupRooms(),
groupRoomsLoading: !this._groupStore.isStateReady(GroupStore.STATE_KEY.GroupRooms),
isUserMember: this._groupStore.getGroupMembers().some(
(m) => m.userId === MatrixClientPeg.get().credentials.userId,
),
error: null, error: null,
}); });
}); });
this._groupStore.on('error', (err) => { this._groupStore.on('error', (err) => {
console.error(err);
this.setState({ this.setState({
summary: null, summary: null,
error: err, error: err,
@ -651,6 +660,7 @@ export default React.createClass({
const RoomDetailList = sdk.getComponent('rooms.RoomDetailList'); const RoomDetailList = sdk.getComponent('rooms.RoomDetailList');
const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const TintableSvg = sdk.getComponent('elements.TintableSvg'); const TintableSvg = sdk.getComponent('elements.TintableSvg');
const Spinner = sdk.getComponent('elements.Spinner');
const addRoomRow = this.state.editing ? const addRoomRow = this.state.editing ?
(<AccessibleButton className="mx_GroupView_rooms_header_addRow" (<AccessibleButton className="mx_GroupView_rooms_header_addRow"
@ -668,7 +678,10 @@ export default React.createClass({
<h3>{ _t('Rooms') }</h3> <h3>{ _t('Rooms') }</h3>
{ addRoomRow } { addRoomRow }
</div> </div>
<RoomDetailList rooms={this._groupStore.getGroupRooms()} /> { this.state.groupRoomsLoading ?
<Spinner /> :
<RoomDetailList rooms={this.state.groupRooms} />
}
</div>; </div>;
}, },
@ -864,7 +877,7 @@ export default React.createClass({
const Spinner = sdk.getComponent("elements.Spinner"); const Spinner = sdk.getComponent("elements.Spinner");
const TintableSvg = sdk.getComponent("elements.TintableSvg"); const TintableSvg = sdk.getComponent("elements.TintableSvg");
if (this.state.summary === null && this.state.error === null || this.state.saving) { if (this.state.summaryLoading && this.state.error === null || this.state.saving) {
return <Spinner />; return <Spinner />;
} else if (this.state.summary) { } else if (this.state.summary) {
const summary = this.state.summary; const summary = this.state.summary;
@ -885,6 +898,7 @@ export default React.createClass({
} else { } else {
const GroupAvatar = sdk.getComponent('avatars.GroupAvatar'); const GroupAvatar = sdk.getComponent('avatars.GroupAvatar');
avatarImage = <GroupAvatar groupId={this.props.groupId} avatarImage = <GroupAvatar groupId={this.props.groupId}
groupName={this.state.profileForm.name}
groupAvatarUrl={this.state.profileForm.avatar_url} groupAvatarUrl={this.state.profileForm.avatar_url}
width={48} height={48} resizeMethod='crop' width={48} height={48} resizeMethod='crop'
/>; />;
@ -928,25 +942,28 @@ export default React.createClass({
tabIndex="2" tabIndex="2"
dir="auto" />; dir="auto" />;
} else { } else {
const onGroupHeaderItemClick = this.state.isUserMember ? this._onEditClick : null;
const groupAvatarUrl = summary.profile ? summary.profile.avatar_url : null; const groupAvatarUrl = summary.profile ? summary.profile.avatar_url : null;
const groupName = summary.profile ? summary.profile.name : null;
avatarNode = <GroupAvatar avatarNode = <GroupAvatar
groupId={this.props.groupId} groupId={this.props.groupId}
groupAvatarUrl={groupAvatarUrl} groupAvatarUrl={groupAvatarUrl}
onClick={this._onEditClick} groupName={groupName}
onClick={onGroupHeaderItemClick}
width={48} height={48} width={48} height={48}
/>; />;
if (summary.profile && summary.profile.name) { if (summary.profile && summary.profile.name) {
nameNode = <div onClick={this._onEditClick}> nameNode = <div onClick={onGroupHeaderItemClick}>
<span>{ summary.profile.name }</span> <span>{ summary.profile.name }</span>
<span className="mx_GroupView_header_groupid"> <span className="mx_GroupView_header_groupid">
({ this.props.groupId }) ({ this.props.groupId })
</span> </span>
</div>; </div>;
} else { } else {
nameNode = <span onClick={this._onEditClick}>{ this.props.groupId }</span>; nameNode = <span onClick={onGroupHeaderItemClick}>{ this.props.groupId }</span>;
} }
if (summary.profile && summary.profile.short_description) { if (summary.profile && summary.profile.short_description) {
shortDescNode = <span onClick={this._onEditClick}>{ summary.profile.short_description }</span>; shortDescNode = <span onClick={onGroupHeaderItemClick}>{ summary.profile.short_description }</span>;
} }
} }
if (this.state.editing) { if (this.state.editing) {
@ -987,6 +1004,7 @@ export default React.createClass({
const headerClasses = { const headerClasses = {
mx_GroupView_header: true, mx_GroupView_header: true,
mx_GroupView_header_view: !this.state.editing, mx_GroupView_header_view: !this.state.editing,
mx_GroupView_header_isUserMember: this.state.isUserMember,
}; };
return ( return (

View file

@ -62,7 +62,9 @@ const GroupTile = React.createClass({
const profile = this.state.profile || {}; const profile = this.state.profile || {};
const name = profile.name || this.props.groupId; const name = profile.name || this.props.groupId;
const desc = profile.shortDescription; const desc = profile.shortDescription;
const httpUrl = profile.avatarUrl ? this.context.matrixClient.mxcUrlToHttp(profile.avatarUrl, 50, 50) : null; const httpUrl = profile.avatarUrl ? this.context.matrixClient.mxcUrlToHttp(
profile.avatarUrl, 50, 50, "crop",
) : null;
return <AccessibleButton className="mx_GroupTile" onClick={this.onClick}> return <AccessibleButton className="mx_GroupTile" onClick={this.onClick}>
<div className="mx_GroupTile_avatar"> <div className="mx_GroupTile_avatar">
<BaseAvatar name={name} url={httpUrl} width={50} height={50} /> <BaseAvatar name={name} url={httpUrl} width={50} height={50} />

View file

@ -166,7 +166,7 @@ module.exports = React.createClass({
} else if (this.state.progress === "sent_email") { } else if (this.state.progress === "sent_email") {
resetPasswordJsx = ( resetPasswordJsx = (
<div> <div>
{ _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 /> <br />
<input className="mx_Login_submit" type="button" onClick={this.onVerify} <input className="mx_Login_submit" type="button" onClick={this.onVerify}
value={_t('I have verified my email address')} /> value={_t('I have verified my email address')} />

View file

@ -302,7 +302,7 @@ module.exports = React.createClass({
} : {}; } : {};
return this._matrixClient.register( return this._matrixClient.register(
this.state.formVals.username, this.state.formVals.username.toLowerCase(),
this.state.formVals.password, this.state.formVals.password,
undefined, // session id: included in the auth dict already undefined, // session id: included in the auth dict already
auth, auth,

View file

@ -24,6 +24,7 @@ export default React.createClass({
propTypes: { propTypes: {
groupId: PropTypes.string, groupId: PropTypes.string,
groupName: PropTypes.string,
groupAvatarUrl: PropTypes.string, groupAvatarUrl: PropTypes.string,
width: PropTypes.number, width: PropTypes.number,
height: PropTypes.number, height: PropTypes.number,
@ -53,11 +54,11 @@ export default React.createClass({
// extract the props we use from props so we can pass any others through // 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? // should consider adding this as a global rule in js-sdk?
/*eslint no-unused-vars: ["error", { "ignoreRestSiblings": true }]*/ /*eslint no-unused-vars: ["error", { "ignoreRestSiblings": true }]*/
const {groupId, groupAvatarUrl, ...otherProps} = this.props; const {groupId, groupAvatarUrl, groupName, ...otherProps} = this.props;
return ( return (
<BaseAvatar <BaseAvatar
name={this.props.groupId[1]} name={groupName || this.props.groupId[1]}
idName={this.props.groupId} idName={this.props.groupId}
url={this.getGroupAvatarUrl()} url={this.getGroupAvatarUrl()}
{...otherProps} {...otherProps}

View file

@ -36,6 +36,7 @@ export default React.createClass({
// group member object. Supply either this or 'member' // group member object. Supply either this or 'member'
groupMember: GroupMemberType, groupMember: GroupMemberType,
action: React.PropTypes.string.isRequired, // eg. 'Ban' 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 // Whether to display a text field for a reason
// If true, the second argument to onFinished will // If true, the second argument to onFinished will
@ -75,7 +76,6 @@ export default React.createClass({
const MemberAvatar = sdk.getComponent("views.avatars.MemberAvatar"); const MemberAvatar = sdk.getComponent("views.avatars.MemberAvatar");
const BaseAvatar = sdk.getComponent("views.avatars.BaseAvatar"); const BaseAvatar = sdk.getComponent("views.avatars.BaseAvatar");
const title = _t("%(actionVerb)s this person?", { actionVerb: this.props.action});
const confirmButtonClass = classnames({ const confirmButtonClass = classnames({
'mx_Dialog_primary': true, 'mx_Dialog_primary': true,
'danger': this.props.danger, 'danger': this.props.danger,
@ -113,7 +113,7 @@ export default React.createClass({
return ( return (
<BaseDialog className="mx_ConfirmUserActionDialog" onFinished={this.props.onFinished} <BaseDialog className="mx_ConfirmUserActionDialog" onFinished={this.props.onFinished}
onEnterPressed={this.onOk} onEnterPressed={this.onOk}
title={title} title={this.props.title}
> >
<div className="mx_Dialog_content"> <div className="mx_Dialog_content">
<div className="mx_ConfirmUserActionDialog_avatar"> <div className="mx_ConfirmUserActionDialog_avatar">

View file

@ -55,8 +55,8 @@ export default React.createClass({
_checkGroupId: function(e) { _checkGroupId: function(e) {
let error = null; let error = null;
if (!/^[a-zA-Z0-9]*$/.test(this.state.groupId)) { if (!/^[a-z0-9=_\-\.\/]*$/.test(this.state.groupId)) {
error = _t("Community IDs may only contain alphanumeric characters"); error = _t("Community IDs may only contain characters a-z, 0-9, or '=_-./'");
} }
this.setState({ this.setState({
groupIdError: error, groupIdError: error,

View file

@ -86,7 +86,6 @@ module.exports = React.createClass({
const summaries = orderedTransitionSequences.map((transitions) => { const summaries = orderedTransitionSequences.map((transitions) => {
const userNames = eventAggregates[transitions]; const userNames = eventAggregates[transitions];
const nameList = this._renderNameList(userNames); const nameList = this._renderNameList(userNames);
const plural = userNames.length > 1;
const splitTransitions = transitions.split(','); const splitTransitions = transitions.split(',');
@ -101,13 +100,13 @@ module.exports = React.createClass({
const descs = coalescedTransitions.map((t) => { const descs = coalescedTransitions.map((t) => {
return this._getDescriptionForTransition( return this._getDescriptionForTransition(
t.transitionType, plural, t.repeats, t.transitionType, userNames.length, t.repeats,
); );
}); });
const desc = this._renderCommaSeparatedList(descs); const desc = this._renderCommaSeparatedList(descs);
return nameList + " " + desc; return _t('%(nameList)s %(transitionList)s', { nameList: nameList, transitionList: desc });
}); });
if (!summaries) { if (!summaries) {
@ -208,148 +207,75 @@ module.exports = React.createClass({
* For a certain transition, t, describe what happened to the users that * For a certain transition, t, describe what happened to the users that
* underwent the transition. * underwent the transition.
* @param {string} t the transition type. * @param {string} t the transition type.
* @param {boolean} plural whether there were multiple users undergoing the same * @param {integer} userCount number of usernames
* transition.
* @param {number} repeats the number of times the transition was repeated in a row. * @param {number} repeats the number of times the transition was repeated in a row.
* @returns {string} the written Human Readable equivalent of the transition. * @returns {string} the written Human Readable equivalent of the transition.
*/ */
_getDescriptionForTransition(t, plural, repeats) { _getDescriptionForTransition(t, userCount, repeats) {
// The empty interpolations 'severalUsers' and 'oneUser' // The empty interpolations 'severalUsers' and 'oneUser'
// are there only to show translators to non-English languages // are there only to show translators to non-English languages
// that the verb is conjugated to plural or singular Subject. // that the verb is conjugated to plural or singular Subject.
let res = null; let res = null;
switch(t) { switch(t) {
case "joined": case "joined":
if (repeats > 1) { res = (userCount > 1)
res = (plural) ? _t("%(severalUsers)sjoined %(count)s times", { severalUsers: "", count: repeats })
? _t("%(severalUsers)sjoined %(repeats)s times", { severalUsers: "", repeats: repeats }) : _t("%(oneUser)sjoined %(count)s times", { oneUser: "", count: repeats });
: _t("%(oneUser)sjoined %(repeats)s times", { oneUser: "", repeats: repeats });
} else {
res = (plural)
? _t("%(severalUsers)sjoined", { severalUsers: "" })
: _t("%(oneUser)sjoined", { oneUser: "" });
}
break; break;
case "left": case "left":
if (repeats > 1) { res = (userCount > 1)
res = (plural) ? _t("%(severalUsers)sleft %(count)s times", { severalUsers: "", count: repeats })
? _t("%(severalUsers)sleft %(repeats)s times", { severalUsers: "", repeats: repeats }) : _t("%(oneUser)sleft %(count)s times", { oneUser: "", count: repeats });
: _t("%(oneUser)sleft %(repeats)s times", { oneUser: "", repeats: repeats });
} else {
res = (plural)
? _t("%(severalUsers)sleft", { severalUsers: "" })
: _t("%(oneUser)sleft", { oneUser: "" });
}
break; break;
case "joined_and_left": case "joined_and_left":
if (repeats > 1) { res = (userCount > 1)
res = (plural) ? _t("%(severalUsers)sjoined and left %(count)s times", { severalUsers: "", count: repeats })
? _t("%(severalUsers)sjoined and left %(repeats)s times", { severalUsers: "", repeats: repeats }) : _t("%(oneUser)sjoined and left %(count)s times", { oneUser: "", count: 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: "" });
}
break; break;
case "left_and_joined": case "left_and_joined":
if (repeats > 1) { res = (userCount > 1)
res = (plural) ? _t("%(severalUsers)sleft and rejoined %(count)s times", { severalUsers: "", count: repeats })
? _t("%(severalUsers)sleft and rejoined %(repeats)s times", { severalUsers: "", repeats: repeats }) : _t("%(oneUser)sleft and rejoined %(count)s times", { oneUser: "", count: 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: "" });
}
break; break;
case "invite_reject": case "invite_reject":
if (repeats > 1) { res = (userCount > 1)
res = (plural) ? _t("%(severalUsers)srejected their invitations %(count)s times", { severalUsers: "", count: repeats })
? _t("%(severalUsers)srejected their invitations %(repeats)s times", { severalUsers: "", repeats: repeats }) : _t("%(oneUser)srejected their invitation %(count)s times", { oneUser: "", count: 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: "" });
}
break; break;
case "invite_withdrawal": case "invite_withdrawal":
if (repeats > 1) { res = (userCount > 1)
res = (plural) ? _t("%(severalUsers)shad their invitations withdrawn %(count)s times", { severalUsers: "", count: repeats })
? _t("%(severalUsers)shad their invitations withdrawn %(repeats)s times", { severalUsers: "", repeats: repeats }) : _t("%(oneUser)shad their invitation withdrawn %(count)s times", { oneUser: "", count: 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: "" });
}
break; break;
case "invited": case "invited":
if (repeats > 1) { res = (userCount > 1)
res = (plural) ? _t("were invited %(count)s times", { count: repeats })
? _t("were invited %(repeats)s times", { repeats: repeats }) : _t("was invited %(count)s times", { count: repeats });
: _t("was invited %(repeats)s times", { repeats: repeats });
} else {
res = (plural)
? _t("were invited")
: _t("was invited");
}
break; break;
case "banned": case "banned":
if (repeats > 1) { res = (userCount > 1)
res = (plural) ? _t("were banned %(count)s times", { count: repeats })
? _t("were banned %(repeats)s times", { repeats: repeats }) : _t("was banned %(count)s times", { count: repeats });
: _t("was banned %(repeats)s times", { repeats: repeats });
} else {
res = (plural)
? _t("were banned")
: _t("was banned");
}
break; break;
case "unbanned": case "unbanned":
if (repeats > 1) { res = (userCount > 1)
res = (plural) ? _t("were unbanned %(count)s times", { count: repeats })
? _t("were unbanned %(repeats)s times", { repeats: repeats }) : _t("was unbanned %(count)s times", { count: repeats });
: _t("was unbanned %(repeats)s times", { repeats: repeats });
} else {
res = (plural)
? _t("were unbanned")
: _t("was unbanned");
}
break; break;
case "kicked": case "kicked":
if (repeats > 1) { res = (userCount > 1)
res = (plural) ? _t("were kicked %(count)s times", { count: repeats })
? _t("were kicked %(repeats)s times", { repeats: repeats }) : _t("was kicked %(count)s times", { count: repeats });
: _t("was kicked %(repeats)s times", { repeats: repeats });
} else {
res = (plural)
? _t("were kicked")
: _t("was kicked");
}
break; break;
case "changed_name": case "changed_name":
if (repeats > 1) { res = (userCount > 1)
res = (plural) ? _t("%(severalUsers)schanged their name %(count)s times", { severalUsers: "", count: repeats })
? _t("%(severalUsers)schanged their name %(repeats)s times", { severalUsers: "", repeats: repeats }) : _t("%(oneUser)schanged their name %(count)s times", { oneUser: "", count: 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: "" });
}
break; break;
case "changed_avatar": case "changed_avatar":
if (repeats > 1) { res = (userCount > 1)
res = (plural) ? _t("%(severalUsers)schanged their avatar %(count)s times", { severalUsers: "", count: repeats })
? _t("%(severalUsers)schanged their avatar %(repeats)s times", { severalUsers: "", repeats: repeats }) : _t("%(oneUser)schanged their avatar %(count)s times", { oneUser: "", count: 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: "" });
}
break; break;
} }
@ -376,11 +302,9 @@ module.exports = React.createClass({
return ""; return "";
} else if (items.length === 1) { } else if (items.length === 1) {
return items[0]; return items[0];
} else if (remaining) { } else if (remaining > 0) {
items = items.slice(0, itemLimit); items = items.slice(0, itemLimit);
return (remaining > 1) return _t("%(items)s and %(count)s others", { items: items.join(', '), count: remaining } )
? _t("%(items)s and %(remaining)s others", { items: items.join(', '), remaining: remaining } )
: _t("%(items)s and one other", { items: items.join(', ') });
} else { } else {
const lastItem = items.pop(); const lastItem = items.pop();
return _t("%(items)s and %(lastItem)s", { items: items.join(', '), lastItem: lastItem }); return _t("%(items)s and %(lastItem)s", { items: items.join(', '), lastItem: lastItem });

View file

@ -37,11 +37,20 @@ const Pill = React.createClass({
isMessagePillUrl: (url) => { isMessagePillUrl: (url) => {
return !!REGEX_LOCAL_MATRIXTO.exec(url); return !!REGEX_LOCAL_MATRIXTO.exec(url);
}, },
roomNotifPos: (text) => {
return text.indexOf("@room");
},
roomNotifLen: () => {
return "@room".length;
},
TYPE_USER_MENTION: 'TYPE_USER_MENTION', TYPE_USER_MENTION: 'TYPE_USER_MENTION',
TYPE_ROOM_MENTION: 'TYPE_ROOM_MENTION', TYPE_ROOM_MENTION: 'TYPE_ROOM_MENTION',
TYPE_AT_ROOM_MENTION: 'TYPE_AT_ROOM_MENTION', // '@room' mention
}, },
props: { 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) // The URL to pillify (no validation is done, see isPillUrl and isMessagePillUrl)
url: PropTypes.string, url: PropTypes.string,
// Whether the pill is in a message // Whether the pill is in a message
@ -72,14 +81,20 @@ const Pill = React.createClass({
regex = REGEX_LOCAL_MATRIXTO; regex = REGEX_LOCAL_MATRIXTO;
} }
let matrixToMatch;
let resourceId;
let prefix;
if (nextProps.url) {
// Default to the empty array if no match for simplicity // Default to the empty array if no match for simplicity
// resource and prefix will be undefined instead of throwing // resource and prefix will be undefined instead of throwing
const matrixToMatch = regex.exec(nextProps.url) || []; matrixToMatch = regex.exec(nextProps.url) || [];
const resourceId = matrixToMatch[1]; // The room/user ID resourceId = matrixToMatch[1]; // The room/user ID
const prefix = matrixToMatch[2]; // The first character of prefix prefix = matrixToMatch[2]; // The first character of prefix
}
const pillType = { const pillType = this.props.type || {
'@': Pill.TYPE_USER_MENTION, '@': Pill.TYPE_USER_MENTION,
'#': Pill.TYPE_ROOM_MENTION, '#': Pill.TYPE_ROOM_MENTION,
'!': Pill.TYPE_ROOM_MENTION, '!': Pill.TYPE_ROOM_MENTION,
@ -88,6 +103,10 @@ const Pill = React.createClass({
let member; let member;
let room; let room;
switch (pillType) { switch (pillType) {
case Pill.TYPE_AT_ROOM_MENTION: {
room = nextProps.room;
}
break;
case Pill.TYPE_USER_MENTION: { case Pill.TYPE_USER_MENTION: {
const localMember = nextProps.room.getMember(resourceId); const localMember = nextProps.room.getMember(resourceId);
member = localMember; member = localMember;
@ -160,6 +179,17 @@ const Pill = React.createClass({
let href = this.props.url; let href = this.props.url;
let onClick; let onClick;
switch (this.state.pillType) { 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: { case Pill.TYPE_USER_MENTION: {
// If this user is not a member of this room, default to the empty member // If this user is not a member of this room, default to the empty member
const member = this.state.member; const member = this.state.member;

View file

@ -17,50 +17,66 @@ limitations under the License.
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import { MatrixClient } from 'matrix-js-sdk';
import dis from '../../../dispatcher'; import dis from '../../../dispatcher';
import Modal from '../../../Modal'; import Modal from '../../../Modal';
import sdk from '../../../index'; import sdk from '../../../index';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import { GroupMemberType } from '../../../groups'; import { GroupMemberType } from '../../../groups';
import { groupMemberFromApiObject } from '../../../groups'; import GroupStoreCache from '../../../stores/GroupStoreCache';
import withMatrixClient from '../../../wrappers/withMatrixClient';
import AccessibleButton from '../elements/AccessibleButton'; import AccessibleButton from '../elements/AccessibleButton';
import GeminiScrollbar from 'react-gemini-scrollbar'; import GeminiScrollbar from 'react-gemini-scrollbar';
module.exports = React.createClass({
module.exports = withMatrixClient(React.createClass({
displayName: 'GroupMemberInfo', displayName: 'GroupMemberInfo',
contextTypes: {
matrixClient: PropTypes.instanceOf(MatrixClient),
},
propTypes: { propTypes: {
matrixClient: PropTypes.object.isRequired,
groupId: PropTypes.string, groupId: PropTypes.string,
groupMember: GroupMemberType, groupMember: GroupMemberType,
isInvited: PropTypes.bool,
}, },
getInitialState: function() { getInitialState: function() {
return { return {
fetching: false,
removingUser: false, removingUser: false,
groupMembers: null, isUserPrivilegedInGroup: null,
}; };
}, },
componentWillMount: function() { componentWillMount: function() {
this._fetchMembers(); this._initGroupStore(this.props.groupId);
}, },
_fetchMembers: function() { componentWillReceiveProps(newProps) {
this.setState({fetching: true}); if (newProps.groupId !== this.props.groupId) {
this.props.matrixClient.getGroupUsers(this.props.groupId).then((result) => { this._unregisterGroupStore();
this._initGroupStore(newProps.groupId);
}
},
_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);
}
},
onGroupStoreUpdated: function() {
this.setState({ this.setState({
groupMembers: result.chunk.map((apiMember) => { isUserInvited: this._groupStore.getGroupInvitedMembers().some(
return groupMemberFromApiObject(apiMember); (m) => m.userId === this.props.groupMember.userId,
}), ),
fetching: false, isUserPrivilegedInGroup: this._groupStore.isUserPrivileged(),
});
}).catch((e) => {
this.setState({fetching: false});
console.error("Failed to get group groupMember list: ", e);
}); });
}, },
@ -68,13 +84,15 @@ module.exports = withMatrixClient(React.createClass({
const ConfirmUserActionDialog = sdk.getComponent("dialogs.ConfirmUserActionDialog"); const ConfirmUserActionDialog = sdk.getComponent("dialogs.ConfirmUserActionDialog");
Modal.createDialog(ConfirmUserActionDialog, { Modal.createDialog(ConfirmUserActionDialog, {
groupMember: this.props.groupMember, groupMember: this.props.groupMember,
action: _t('Remove from community'), 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, danger: true,
onFinished: (proceed) => { onFinished: (proceed) => {
if (!proceed) return; if (!proceed) return;
this.setState({removingUser: true}); this.setState({removingUser: true});
this.props.matrixClient.removeUserFromGroup( this.context.matrixClient.removeUserFromGroup(
this.props.groupId, this.props.groupMember.userId, this.props.groupId, this.props.groupMember.userId,
).then(() => { ).then(() => {
// return to the user list // return to the user list
@ -86,7 +104,9 @@ module.exports = withMatrixClient(React.createClass({
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Failed to remove user from group', '', ErrorDialog, { Modal.createTrackedDialog('Failed to remove user from group', '', ErrorDialog, {
title: _t('Error'), title: _t('Error'),
description: _t('Failed to remove user from community'), description: this.state.isUserInvited ?
_t('Failed to withdraw invitation') :
_t('Failed to remove user from community'),
}); });
}).finally(() => { }).finally(() => {
this.setState({removingUser: false}); this.setState({removingUser: false});
@ -111,24 +131,17 @@ module.exports = withMatrixClient(React.createClass({
}, },
render: function() { render: function() {
if (this.state.fetching || this.state.removingUser) { if (this.state.removingUser) {
const Spinner = sdk.getComponent("elements.Spinner"); const Spinner = sdk.getComponent("elements.Spinner");
return <Spinner />; return <Spinner />;
} }
if (!this.state.groupMembers) return null;
const targetIsInGroup = this.state.groupMembers.some((m) => { let adminTools;
return m.userId === this.props.groupMember.userId; if (this.state.isUserPrivilegedInGroup) {
}); const kickButton = (
let kickButton;
let adminButton;
if (targetIsInGroup) {
kickButton = (
<AccessibleButton className="mx_MemberInfo_field" <AccessibleButton className="mx_MemberInfo_field"
onClick={this._onKick}> onClick={this._onKick}>
{ _t('Remove from community') } { this.state.isUserInvited ? _t('Disinvite') : _t('Remove from community') }
</AccessibleButton> </AccessibleButton>
); );
@ -137,22 +150,19 @@ module.exports = withMatrixClient(React.createClass({
giveModButton = <AccessibleButton className="mx_MemberInfo_field" onClick={this.onModToggle}> giveModButton = <AccessibleButton className="mx_MemberInfo_field" onClick={this.onModToggle}>
{giveOpLabel} {giveOpLabel}
</AccessibleButton>;*/ </AccessibleButton>;*/
}
let adminTools; if (kickButton) {
if (kickButton || adminButton) {
adminTools = adminTools =
<div className="mx_MemberInfo_adminTools"> <div className="mx_MemberInfo_adminTools">
<h3>{ _t("Admin Tools") }</h3> <h3>{ _t("Admin Tools") }</h3>
<div className="mx_MemberInfo_buttons"> <div className="mx_MemberInfo_buttons">
{ kickButton } { kickButton }
{ adminButton }
</div> </div>
</div>; </div>;
} }
}
const avatarUrl = this.props.matrixClient.mxcUrlToHttp( const avatarUrl = this.context.matrixClient.mxcUrlToHttp(
this.props.groupMember.avatarUrl, this.props.groupMember.avatarUrl,
36, 36, 'crop', 36, 36, 'crop',
); );
@ -192,4 +202,4 @@ module.exports = withMatrixClient(React.createClass({
</div> </div>
); );
}, },
})); });

View file

@ -50,12 +50,9 @@ export default withMatrixClient(React.createClass({
_initGroupStore: function(groupId) { _initGroupStore: function(groupId) {
this._groupStore = GroupStoreCache.getGroupStore(this.context.matrixClient, groupId); this._groupStore = GroupStoreCache.getGroupStore(this.context.matrixClient, groupId);
this._groupStore.on('update', () => { this._groupStore.registerListener(() => {
this._fetchMembers(); this._fetchMembers();
}); });
this._groupStore.on('error', (err) => {
console.error(err);
});
}, },
_fetchMembers: function() { _fetchMembers: function() {

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

@ -47,16 +47,14 @@ export default React.createClass({
_initGroupStore: function(groupId) { _initGroupStore: function(groupId) {
this._groupStore = GroupStoreCache.getGroupStore(this.context.matrixClient, groupId); this._groupStore = GroupStoreCache.getGroupStore(this.context.matrixClient, groupId);
this._groupStore.on('update', () => { this._groupStore.registerListener(() => {
this._fetchRooms(); this._fetchRooms();
}); });
this._groupStore.on('error', (err) => { this._groupStore.on('error', (err) => {
console.error('Error in group store (listened to by GroupRoomList)', err);
this.setState({ this.setState({
rooms: null, rooms: null,
}); });
}); });
this._fetchRooms();
}, },
_fetchRooms: function() { _fetchRooms: function() {

View file

@ -16,13 +16,10 @@ limitations under the License.
import React from 'react'; import React from 'react';
import {MatrixClient} from 'matrix-js-sdk'; import {MatrixClient} from 'matrix-js-sdk';
import { _t } from '../../../languageHandler';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import sdk from '../../../index'; import sdk from '../../../index';
import dis from '../../../dispatcher'; import dis from '../../../dispatcher';
import { GroupRoomType } from '../../../groups'; import { GroupRoomType } from '../../../groups';
import GroupStoreCache from '../../../stores/GroupStoreCache';
import Modal from '../../../Modal';
const GroupRoomTile = React.createClass({ const GroupRoomTile = React.createClass({
displayName: 'GroupRoomTile', displayName: 'GroupRoomTile',
@ -32,68 +29,11 @@ const GroupRoomTile = React.createClass({
groupRoom: GroupRoomType.isRequired, 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) { 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({ dis.dispatch({
action: 'view_room', action: 'view_group_room',
room_id: roomId, groupId: this.props.groupId,
room_alias: roomAlias, groupRoomId: this.props.groupRoom.roomId,
});
},
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();
}
},
}); });
}, },
@ -106,7 +46,7 @@ const GroupRoomTile = React.createClass({
); );
const av = ( const av = (
<BaseAvatar name={this.state.name} <BaseAvatar name={this.props.groupRoom.displayname}
width={36} height={36} width={36} height={36}
url={avatarUrl} url={avatarUrl}
/> />
@ -118,14 +58,8 @@ const GroupRoomTile = React.createClass({
{ av } { av }
</div> </div>
<div className="mx_GroupRoomTile_name"> <div className="mx_GroupRoomTile_name">
{ this.state.name } { this.props.groupRoom.displayname }
</div> </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> </AccessibleButton>
); );
}, },

View file

@ -20,7 +20,7 @@ import url from 'url';
import classnames from 'classnames'; import classnames from 'classnames';
import sdk from '../../../index'; 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 /* This file contains a collection of components which are used by the
* InteractiveAuth to prompt the user to enter the information needed * InteractiveAuth to prompt the user to enter the information needed
@ -256,7 +256,7 @@ export const EmailIdentityAuthEntry = React.createClass({
} else { } else {
return ( return (
<div> <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> <p>{ _t("Please check your email to continue registration.") }</p>
</div> </div>
); );
@ -370,7 +370,7 @@ export const MsisdnAuthEntry = React.createClass({
}); });
return ( return (
<div> <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> <p>{ _t("Please enter the code it contains:") }</p>
<div className="mx_InteractiveAuthEntryComponents_msisdnWrapper"> <div className="mx_InteractiveAuthEntryComponents_msisdnWrapper">
<form onSubmit={this._onFormSubmit}> <form onSubmit={this._onFormSubmit}>

View file

@ -19,6 +19,7 @@
import React from 'react'; import React from 'react';
import sdk from '../../../index'; import sdk from '../../../index';
import Flair from '../elements/Flair.js'; import Flair from '../elements/Flair.js';
import { _tJsx } from '../../../languageHandler';
export default function SenderProfile(props) { export default function SenderProfile(props) {
const EmojiText = sdk.getComponent('elements.EmojiText'); 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 return <span />; // emote message must include the name so don't duplicate it
} }
return ( // Name + flair
<div className="mx_SenderProfile" dir="auto" onClick={props.onClick}> const nameElem = [
<EmojiText className="mx_SenderProfile_name">{ name || '' }</EmojiText> <EmojiText key='name' className="mx_SenderProfile_name">{ name || '' }</EmojiText>,
{ props.enableFlair ? props.enableFlair ?
<Flair <Flair key='flair'
userId={mxEvent.getSender()} userId={mxEvent.getSender()}
roomId={mxEvent.getRoomId()} roomId={mxEvent.getRoomId()}
showRelated={true} /> showRelated={true} />
: null : 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;
} }
{ props.aux ? <EmojiText className="mx_SenderProfile_aux"> { props.aux }</EmojiText> : null }
return (
<div className="mx_SenderProfile" dir="auto" onClick={props.onClick}>
{ content }
</div> </div>
); );
} }
SenderProfile.propTypes = { SenderProfile.propTypes = {
mxEvent: React.PropTypes.object.isRequired, // event whose sender we're showing 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, onClick: React.PropTypes.func,
}; };

View file

@ -34,6 +34,7 @@ import MatrixClientPeg from '../../../MatrixClientPeg';
import ContextualMenu from '../../structures/ContextualMenu'; import ContextualMenu from '../../structures/ContextualMenu';
import {RoomMember} from 'matrix-js-sdk'; import {RoomMember} from 'matrix-js-sdk';
import classNames from 'classnames'; import classNames from 'classnames';
import PushProcessor from 'matrix-js-sdk/lib/pushprocessor';
linkifyMatrix(linkify); linkifyMatrix(linkify);
@ -169,8 +170,10 @@ module.exports = React.createClass({
pillifyLinks: function(nodes) { pillifyLinks: function(nodes) {
const shouldShowPillAvatar = !UserSettingsStore.getSyncedSetting("Pill.shouldHidePillAvatar", false); const shouldShowPillAvatar = !UserSettingsStore.getSyncedSetting("Pill.shouldHidePillAvatar", false);
for (let i = 0; i < nodes.length; i++) { let node = nodes[0];
const node = nodes[i]; while (node) {
let pillified = false;
if (node.tagName === "A" && node.getAttribute("href")) { if (node.tagName === "A" && node.getAttribute("href")) {
const href = node.getAttribute("href"); const href = node.getAttribute("href");
@ -189,10 +192,68 @@ module.exports = React.createClass({
ReactDOM.render(pill, pillContainer); ReactDOM.render(pill, pillContainer);
node.parentNode.replaceChild(pillContainer, node); node.parentNode.replaceChild(pillContainer, node);
// Pills within pills aren't going to go well, so move on
pillified = true;
} }
} else if (node.children && node.children.length) { } else if (node.nodeType == Node.TEXT_NODE) {
this.pillifyLinks(node.children); 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;
}
}
}
if (node.childNodes && node.childNodes.length && !pillified) {
this.pillifyLinks(node.childNodes);
}
node = node.nextSibling;
} }
}, },

View file

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

View file

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

View file

@ -29,7 +29,8 @@ function getDisplayAliasForRoom(room) {
} }
const RoomDetailRow = React.createClass({ const RoomDetailRow = React.createClass({
propTypes: PropTypes.shape({ propTypes: {
room: PropTypes.shape({
name: PropTypes.string, name: PropTypes.string,
topic: PropTypes.string, topic: PropTypes.string,
roomId: PropTypes.string, roomId: PropTypes.string,
@ -41,6 +42,7 @@ const RoomDetailRow = React.createClass({
worldReadable: PropTypes.bool, worldReadable: PropTypes.bool,
guestCanJoin: PropTypes.bool, guestCanJoin: PropTypes.bool,
}), }),
},
onClick: function(ev) { onClick: function(ev) {
ev.preventDefault(); ev.preventDefault();

View file

@ -34,27 +34,18 @@ const Receipt = require('../../../utils/Receipt');
const HIDE_CONFERENCE_CHANS = true; const HIDE_CONFERENCE_CHANS = true;
function phraseForSection(section) { 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) { switch (section) {
case 'm.favourite': case 'm.favourite':
verb = _t('to favourite'); return _t('Drop here to favourite');
break;
case 'im.vector.fake.direct': case 'im.vector.fake.direct':
verb = _t('to tag direct chat'); return _t('Drop here to tag direct chat');
break;
case 'im.vector.fake.recent': case 'im.vector.fake.recent':
verb = _t('to restore'); return _t('Drop here to restore');
break;
case 'm.lowpriority': case 'm.lowpriority':
verb = _t('to demote'); return _t('Drop here to demote');
break;
default: default:
return _t('Drop here to tag %(section)s', {section: section}); return _t('Drop here to tag %(section)s', {section: section});
} }
return _t('Drop here %(toAction)s', {toAction: verb});
} }
module.exports = React.createClass({ module.exports = React.createClass({

View file

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

View file

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

View file

@ -184,7 +184,8 @@ module.exports = React.createClass({
}); });
}, },
onClickChange: function() { onClickChange: function(ev) {
ev.preventDefault();
const oldPassword = this.state.cachedPassword || this.refs.old_input.value; const oldPassword = this.state.cachedPassword || this.refs.old_input.value;
const newPassword = this.refs.new_input.value; const newPassword = this.refs.new_input.value;
const confirmPassword = this.refs.confirm_input.value; const confirmPassword = this.refs.confirm_input.value;

View file

@ -15,6 +15,7 @@ limitations under the License.
*/ */
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { _t } from './languageHandler.js';
export const GroupMemberType = PropTypes.shape({ export const GroupMemberType = PropTypes.shape({
userId: PropTypes.string.isRequired, userId: PropTypes.string.isRequired,
@ -23,6 +24,7 @@ export const GroupMemberType = PropTypes.shape({
}); });
export const GroupRoomType = PropTypes.shape({ export const GroupRoomType = PropTypes.shape({
displayname: PropTypes.string,
name: PropTypes.string, name: PropTypes.string,
roomId: PropTypes.string.isRequired, roomId: PropTypes.string.isRequired,
canonicalAlias: PropTypes.string, canonicalAlias: PropTypes.string,
@ -39,6 +41,7 @@ export function groupMemberFromApiObject(apiObject) {
export function groupRoomFromApiObject(apiObject) { export function groupRoomFromApiObject(apiObject) {
return { return {
displayname: apiObject.name || apiObject.canonical_alias || _t("Unnamed Room"),
name: apiObject.name, name: apiObject.name,
roomId: apiObject.room_id, roomId: apiObject.room_id,
canonicalAlias: apiObject.canonical_alias, canonicalAlias: apiObject.canonical_alias,
@ -47,5 +50,6 @@ export function groupRoomFromApiObject(apiObject) {
numJoinedMembers: apiObject.num_joined_members, numJoinedMembers: apiObject.num_joined_members,
worldReadable: apiObject.world_readable, worldReadable: apiObject.world_readable,
guestCanJoin: apiObject.guest_can_join, guestCanJoin: apiObject.guest_can_join,
isPublic: apiObject.is_public !== false,
}; };
} }

View file

@ -152,13 +152,13 @@
"%(widgetName)s widget removed by %(senderName)s": "%(widgetName)s widget removed by %(senderName)s", "%(widgetName)s widget removed by %(senderName)s": "%(widgetName)s widget removed by %(senderName)s",
"Communities": "Communities", "Communities": "Communities",
"Message Pinning": "Message Pinning", "Message Pinning": "Message Pinning",
"Mention": "Mention",
"%(displayName)s is typing": "%(displayName)s is typing", "%(displayName)s is typing": "%(displayName)s is typing",
"%(names)s and one other are typing": "%(names)s and one other are typing",
"%(names)s and %(count)s others are typing|other": "%(names)s and %(count)s others are typing", "%(names)s and %(count)s others are typing|other": "%(names)s and %(count)s others are typing",
"%(names)s and %(count)s others are typing|one": "%(names)s and one other is typing",
"%(names)s and %(lastPerson)s are typing": "%(names)s and %(lastPerson)s are typing", "%(names)s and %(lastPerson)s are typing": "%(names)s and %(lastPerson)s are typing",
"Failure to create room": "Failure to create room", "Failure to create room": "Failure to create room",
"Server may be unavailable, overloaded, or you hit a bug.": "Server may be unavailable, overloaded, or you hit a bug.", "Server may be unavailable, overloaded, or you hit a bug.": "Server may be unavailable, overloaded, or you hit a bug.",
"Unnamed Room": "Unnamed Room",
"Your browser does not support the required cryptography extensions": "Your browser does not support the required cryptography extensions", "Your browser does not support the required cryptography extensions": "Your browser does not support the required cryptography extensions",
"Not a valid Riot keyfile": "Not a valid Riot keyfile", "Not a valid Riot keyfile": "Not a valid Riot keyfile",
"Authentication check failed: incorrect password?": "Authentication check failed: incorrect password?", "Authentication check failed: incorrect password?": "Authentication check failed: incorrect password?",
@ -211,9 +211,9 @@
" (unsupported)": " (unsupported)", " (unsupported)": " (unsupported)",
"Join as <voiceText>voice</voiceText> or <videoText>video</videoText>.": "Join as <voiceText>voice</voiceText> or <videoText>video</videoText>.", "Join as <voiceText>voice</voiceText> or <videoText>video</videoText>.": "Join as <voiceText>voice</voiceText> or <videoText>video</videoText>.",
"Ongoing conference call%(supportedText)s.": "Ongoing conference call%(supportedText)s.", "Ongoing conference call%(supportedText)s.": "Ongoing conference call%(supportedText)s.",
"sent an image": "sent an image", "%(senderName)s sent an image": "%(senderName)s sent an image",
"sent a video": "sent a video", "%(senderName)s sent a video": "%(senderName)s sent a video",
"uploaded a file": "uploaded a file", "%(senderName)s uploaded a file": "%(senderName)s uploaded a file",
"Options": "Options", "Options": "Options",
"Undecryptable": "Undecryptable", "Undecryptable": "Undecryptable",
"Encrypted by a verified device": "Encrypted by a verified device", "Encrypted by a verified device": "Encrypted by a verified device",
@ -226,9 +226,13 @@
"device id: ": "device id: ", "device id: ": "device id: ",
"Disinvite": "Disinvite", "Disinvite": "Disinvite",
"Kick": "Kick", "Kick": "Kick",
"Disinvite this user?": "Disinvite this user?",
"Kick this user?": "Kick this user?",
"Failed to kick": "Failed to kick", "Failed to kick": "Failed to kick",
"Unban": "Unban", "Unban": "Unban",
"Ban": "Ban", "Ban": "Ban",
"Unban this user?": "Unban this user?",
"Ban this user?": "Ban this user?",
"Failed to ban user": "Failed to ban user", "Failed to ban user": "Failed to ban user",
"Failed to mute user": "Failed to mute user", "Failed to mute user": "Failed to mute user",
"Failed to toggle moderator status": "Failed to toggle moderator status", "Failed to toggle moderator status": "Failed to toggle moderator status",
@ -240,6 +244,7 @@
"Unignore": "Unignore", "Unignore": "Unignore",
"Ignore": "Ignore", "Ignore": "Ignore",
"Jump to read receipt": "Jump to read receipt", "Jump to read receipt": "Jump to read receipt",
"Mention": "Mention",
"Invite": "Invite", "Invite": "Invite",
"User Options": "User Options", "User Options": "User Options",
"Direct chats": "Direct chats", "Direct chats": "Direct chats",
@ -314,12 +319,11 @@
"Forget room": "Forget room", "Forget room": "Forget room",
"Search": "Search", "Search": "Search",
"Show panel": "Show panel", "Show panel": "Show panel",
"to favourite": "to favourite", "Drop here to favourite": "Drop here to favourite",
"to tag direct chat": "to tag direct chat", "Drop here to tag direct chat": "Drop here to tag direct chat",
"to restore": "to restore", "Drop here to restore": "Drop here to restore",
"to demote": "to demote", "Drop here to demote": "Drop here to demote",
"Drop here to tag %(section)s": "Drop here to tag %(section)s", "Drop here to tag %(section)s": "Drop here to tag %(section)s",
"Drop here %(toAction)s": "Drop here %(toAction)s",
"Press <StartChatButton> to start a chat with someone": "Press <StartChatButton> to start a chat with someone", "Press <StartChatButton> to start a chat with someone": "Press <StartChatButton> to start a chat with someone",
"You're not in any rooms yet! Press <CreateRoomButton> to make a room or <RoomDirectoryButton> to browse the directory": "You're not in any rooms yet! Press <CreateRoomButton> to make a room or <RoomDirectoryButton> to browse the directory", "You're not in any rooms yet! Press <CreateRoomButton> to make a room or <RoomDirectoryButton> to browse the directory": "You're not in any rooms yet! Press <CreateRoomButton> to make a room or <RoomDirectoryButton> to browse the directory",
"Invites": "Invites", "Invites": "Invites",
@ -328,21 +332,22 @@
"Rooms": "Rooms", "Rooms": "Rooms",
"Low priority": "Low priority", "Low priority": "Low priority",
"Historical": "Historical", "Historical": "Historical",
"Unnamed Room": "Unnamed Room",
"a room": "a room",
"Unable to ascertain that the address this invite was sent to matches one associated with your account.": "Unable to ascertain that the address this invite was sent to matches one associated with your account.", "Unable to ascertain that the address this invite was sent to matches one associated with your account.": "Unable to ascertain that the address this invite was sent to matches one associated with your account.",
"This invitation was sent to an email address which is not associated with this account:": "This invitation was sent to an email address which is not associated with this account:", "This invitation was sent to an email address which is not associated with this account:": "This invitation was sent to an email address which is not associated with this account:",
"You may wish to login with a different account, or add this email to this account.": "You may wish to login with a different account, or add this email to this account.", "You may wish to login with a different account, or add this email to this account.": "You may wish to login with a different account, or add this email to this account.",
"You have been invited to join this room by %(inviterName)s": "You have been invited to join this room by %(inviterName)s", "You have been invited to join this room by %(inviterName)s": "You have been invited to join this room by %(inviterName)s",
"Would you like to <acceptText>accept</acceptText> or <declineText>decline</declineText> this invitation?": "Would you like to <acceptText>accept</acceptText> or <declineText>decline</declineText> this invitation?", "Would you like to <acceptText>accept</acceptText> or <declineText>decline</declineText> this invitation?": "Would you like to <acceptText>accept</acceptText> or <declineText>decline</declineText> this invitation?",
"This room": "This room",
"Reason: %(reasonText)s": "Reason: %(reasonText)s", "Reason: %(reasonText)s": "Reason: %(reasonText)s",
"Rejoin": "Rejoin", "Rejoin": "Rejoin",
"You have been kicked from %(roomName)s by %(userName)s.": "You have been kicked from %(roomName)s by %(userName)s.", "You have been kicked from %(roomName)s by %(userName)s.": "You have been kicked from %(roomName)s by %(userName)s.",
"You have been kicked from this room by %(userName)s.": "You have been kicked from this room by %(userName)s.",
"You have been banned from %(roomName)s by %(userName)s.": "You have been banned from %(roomName)s by %(userName)s.", "You have been banned from %(roomName)s by %(userName)s.": "You have been banned from %(roomName)s by %(userName)s.",
"You have been banned from this room by %(userName)s.": "You have been banned from this room by %(userName)s.",
"This room": "This room",
"%(roomName)s does not exist.": "%(roomName)s does not exist.", "%(roomName)s does not exist.": "%(roomName)s does not exist.",
"%(roomName)s is not accessible at this time.": "%(roomName)s is not accessible at this time.", "%(roomName)s is not accessible at this time.": "%(roomName)s is not accessible at this time.",
"You are trying to access %(roomName)s.": "You are trying to access %(roomName)s.", "You are trying to access %(roomName)s.": "You are trying to access %(roomName)s.",
"You are trying to access a room.": "You are trying to access a room.",
"<a>Click here</a> to join the discussion!": "<a>Click here</a> to join the discussion!", "<a>Click here</a> to join the discussion!": "<a>Click here</a> to join the discussion!",
"This is a preview of this room. Room interactions have been disabled": "This is a preview of this room. Room interactions have been disabled", "This is a preview of this room. Room interactions have been disabled": "This is a preview of this room. Room interactions have been disabled",
"To change the room's avatar, you must be a": "To change the room's avatar, you must be a", "To change the room's avatar, you must be a": "To change the room's avatar, you must be a",
@ -387,10 +392,9 @@
"Publish this room to the public in %(domain)s's room directory?": "Publish this room to the public in %(domain)s's room directory?", "Publish this room to the public in %(domain)s's room directory?": "Publish this room to the public in %(domain)s's room directory?",
"Who can read history?": "Who can read history?", "Who can read history?": "Who can read history?",
"Anyone": "Anyone", "Anyone": "Anyone",
"Members only": "Members only", "Members only (since the point in time of selecting this option)": "Members only (since the point in time of selecting this option)",
"since the point in time of selecting this option": "since the point in time of selecting this option", "Members only (since they were invited)": "Members only (since they were invited)",
"since they were invited": "since they were invited", "Members only (since they joined)": "Members only (since they joined)",
"since they joined": "since they joined",
"Room Colour": "Room Colour", "Room Colour": "Room Colour",
"Permissions": "Permissions", "Permissions": "Permissions",
"The default role for new room members is": "The default role for new room members is", "The default role for new room members is": "The default role for new room members is",
@ -463,10 +467,10 @@
"Dismiss": "Dismiss", "Dismiss": "Dismiss",
"To continue, please enter your password.": "To continue, please enter your password.", "To continue, please enter your password.": "To continue, please enter your password.",
"Password:": "Password:", "Password:": "Password:",
"An email has been sent to": "An email has been sent to", "An email has been sent to %(emailAddress)s": "An email has been sent to %(emailAddress)s",
"Please check your email to continue registration.": "Please check your email to continue registration.", "Please check your email to continue registration.": "Please check your email to continue registration.",
"Token incorrect": "Token incorrect", "Token incorrect": "Token incorrect",
"A text message has been sent to": "A text message has been sent to", "A text message has been sent to %(msisdn)s": "A text message has been sent to %(msisdn)s",
"Please enter the code it contains:": "Please enter the code it contains:", "Please enter the code it contains:": "Please enter the code it contains:",
"Start authentication": "Start authentication", "Start authentication": "Start authentication",
"powered by Matrix": "powered by Matrix", "powered by Matrix": "powered by Matrix",
@ -488,15 +492,22 @@
"Identity server URL": "Identity server URL", "Identity server URL": "Identity server URL",
"What does this mean?": "What does this mean?", "What does this mean?": "What does this mean?",
"Remove from community": "Remove from community", "Remove from community": "Remove from community",
"Disinvite this user from community?": "Disinvite this user from community?",
"Remove this user from community?": "Remove this user from community?",
"Failed to withdraw invitation": "Failed to withdraw invitation",
"Failed to remove user from community": "Failed to remove user from community", "Failed to remove user from community": "Failed to remove user from community",
"Filter community members": "Filter community members", "Filter community members": "Filter community members",
"Filter community rooms": "Filter community rooms",
"Failed to remove room from community": "Failed to remove room from community",
"Failed to remove '%(roomName)s' from %(groupId)s": "Failed to remove '%(roomName)s' from %(groupId)s",
"Are you sure you want to remove '%(roomName)s' from %(groupId)s?": "Are you sure you want to remove '%(roomName)s' from %(groupId)s?", "Are you sure you want to remove '%(roomName)s' from %(groupId)s?": "Are you sure you want to remove '%(roomName)s' from %(groupId)s?",
"Removing a room from the community will also remove it from the community page.": "Removing a room from the community will also remove it from the community page.", "Removing a room from the community will also remove it from the community page.": "Removing a room from the community will also remove it from the community page.",
"Remove": "Remove", "Remove": "Remove",
"Remove this room from the community": "Remove this room from the community", "Failed to remove room from community": "Failed to remove room from community",
"Failed to remove '%(roomName)s' from %(groupId)s": "Failed to remove '%(roomName)s' from %(groupId)s",
"Something went wrong!": "Something went wrong!",
"The visibility of '%(roomName)s' in %(groupId)s could not be updated.": "The visibility of '%(roomName)s' in %(groupId)s could not be updated.",
"Visibility in Room List": "Visibility in Room List",
"Visible to everyone": "Visible to everyone",
"Only visible to community members": "Only visible to community members",
"Filter community rooms": "Filter community rooms",
"Unknown Address": "Unknown Address", "Unknown Address": "Unknown Address",
"NOTE: Apps are not end-to-end encrypted": "NOTE: Apps are not end-to-end encrypted", "NOTE: Apps are not end-to-end encrypted": "NOTE: Apps are not end-to-end encrypted",
"Do you want to load widget from URL:": "Do you want to load widget from URL:", "Do you want to load widget from URL:": "Do you want to load widget from URL:",
@ -516,56 +527,57 @@
"Integrations Error": "Integrations Error", "Integrations Error": "Integrations Error",
"Could not connect to the integration server": "Could not connect to the integration server", "Could not connect to the integration server": "Could not connect to the integration server",
"Manage Integrations": "Manage Integrations", "Manage Integrations": "Manage Integrations",
"%(severalUsers)sjoined %(repeats)s times": "%(severalUsers)sjoined %(repeats)s times", "%(nameList)s %(transitionList)s": "%(nameList)s %(transitionList)s",
"%(oneUser)sjoined %(repeats)s times": "%(oneUser)sjoined %(repeats)s times", "%(severalUsers)sjoined %(count)s times|other": "%(severalUsers)sjoined %(count)s times",
"%(severalUsers)sjoined": "%(severalUsers)sjoined", "%(severalUsers)sjoined %(count)s times|one": "%(severalUsers)sjoined",
"%(oneUser)sjoined": "%(oneUser)sjoined", "%(oneUser)sjoined %(count)s times|other": "%(oneUser)sjoined %(count)s times",
"%(severalUsers)sleft %(repeats)s times": "%(severalUsers)sleft %(repeats)s times", "%(oneUser)sjoined %(count)s times|one": "%(oneUser)sjoined",
"%(oneUser)sleft %(repeats)s times": "%(oneUser)sleft %(repeats)s times", "%(severalUsers)sleft %(count)s times|other": "%(severalUsers)sleft %(count)s times",
"%(severalUsers)sleft": "%(severalUsers)sleft", "%(severalUsers)sleft %(count)s times|one": "%(severalUsers)sleft",
"%(oneUser)sleft": "%(oneUser)sleft", "%(oneUser)sleft %(count)s times|other": "%(oneUser)sleft %(count)s times",
"%(severalUsers)sjoined and left %(repeats)s times": "%(severalUsers)sjoined and left %(repeats)s times", "%(oneUser)sleft %(count)s times|one": "%(oneUser)sleft",
"%(oneUser)sjoined and left %(repeats)s times": "%(oneUser)sjoined and left %(repeats)s times", "%(severalUsers)sjoined and left %(count)s times|other": "%(severalUsers)sjoined and left %(count)s times",
"%(severalUsers)sjoined and left": "%(severalUsers)sjoined and left", "%(severalUsers)sjoined and left %(count)s times|one": "%(severalUsers)sjoined and left",
"%(oneUser)sjoined and left": "%(oneUser)sjoined and left", "%(oneUser)sjoined and left %(count)s times|other": "%(oneUser)sjoined and left %(count)s times",
"%(severalUsers)sleft and rejoined %(repeats)s times": "%(severalUsers)sleft and rejoined %(repeats)s times", "%(oneUser)sjoined and left %(count)s times|one": "%(oneUser)sjoined and left",
"%(oneUser)sleft and rejoined %(repeats)s times": "%(oneUser)sleft and rejoined %(repeats)s times", "%(severalUsers)sleft and rejoined %(count)s times|other": "%(severalUsers)sleft and rejoined %(count)s times",
"%(severalUsers)sleft and rejoined": "%(severalUsers)sleft and rejoined", "%(severalUsers)sleft and rejoined %(count)s times|one": "%(severalUsers)sleft and rejoined",
"%(oneUser)sleft and rejoined": "%(oneUser)sleft and rejoined", "%(oneUser)sleft and rejoined %(count)s times|other": "%(oneUser)sleft and rejoined %(count)s times",
"%(severalUsers)srejected their invitations %(repeats)s times": "%(severalUsers)srejected their invitations %(repeats)s times", "%(oneUser)sleft and rejoined %(count)s times|one": "%(oneUser)sleft and rejoined",
"%(oneUser)srejected their invitation %(repeats)s times": "%(oneUser)srejected their invitation %(repeats)s times", "%(severalUsers)srejected their invitations %(count)s times|other": "%(severalUsers)srejected their invitations %(count)s times",
"%(severalUsers)srejected their invitations": "%(severalUsers)srejected their invitations", "%(severalUsers)srejected their invitations %(count)s times|one": "%(severalUsers)srejected their invitations",
"%(oneUser)srejected their invitation": "%(oneUser)srejected their invitation", "%(oneUser)srejected their invitation %(count)s times|other": "%(oneUser)srejected their invitation %(count)s times",
"%(severalUsers)shad their invitations withdrawn %(repeats)s times": "%(severalUsers)shad their invitations withdrawn %(repeats)s times", "%(oneUser)srejected their invitation %(count)s times|one": "%(oneUser)srejected their invitation",
"%(oneUser)shad their invitation withdrawn %(repeats)s times": "%(oneUser)shad their invitation withdrawn %(repeats)s times", "%(severalUsers)shad their invitations withdrawn %(count)s times|other": "%(severalUsers)shad their invitations withdrawn %(count)s times",
"%(severalUsers)shad their invitations withdrawn": "%(severalUsers)shad their invitations withdrawn", "%(severalUsers)shad their invitations withdrawn %(count)s times|one": "%(severalUsers)shad their invitations withdrawn",
"%(oneUser)shad their invitation withdrawn": "%(oneUser)shad their invitation withdrawn", "%(oneUser)shad their invitation withdrawn %(count)s times|other": "%(oneUser)shad their invitation withdrawn %(count)s times",
"were invited %(repeats)s times": "were invited %(repeats)s times", "%(oneUser)shad their invitation withdrawn %(count)s times|one": "%(oneUser)shad their invitation withdrawn",
"was invited %(repeats)s times": "was invited %(repeats)s times", "were invited %(count)s times|other": "were invited %(count)s times",
"were invited": "were invited", "were invited %(count)s times|one": "were invited",
"was invited": "was invited", "was invited %(count)s times|other": "was invited %(count)s times",
"were banned %(repeats)s times": "were banned %(repeats)s times", "was invited %(count)s times|one": "was invited",
"was banned %(repeats)s times": "was banned %(repeats)s times", "were banned %(count)s times|other": "were banned %(count)s times",
"were banned": "were banned", "were banned %(count)s times|one": "were banned",
"was banned": "was banned", "was banned %(count)s times|other": "was banned %(count)s times",
"were unbanned %(repeats)s times": "were unbanned %(repeats)s times", "was banned %(count)s times|one": "was banned",
"was unbanned %(repeats)s times": "was unbanned %(repeats)s times", "were unbanned %(count)s times|other": "were unbanned %(count)s times",
"were unbanned": "were unbanned", "were unbanned %(count)s times|one": "were unbanned",
"was unbanned": "was unbanned", "was unbanned %(count)s times|other": "was unbanned %(count)s times",
"were kicked %(repeats)s times": "were kicked %(repeats)s times", "was unbanned %(count)s times|one": "was unbanned",
"was kicked %(repeats)s times": "was kicked %(repeats)s times", "were kicked %(count)s times|other": "were kicked %(count)s times",
"were kicked": "were kicked", "were kicked %(count)s times|one": "were kicked",
"was kicked": "was kicked", "was kicked %(count)s times|other": "was kicked %(count)s times",
"%(severalUsers)schanged their name %(repeats)s times": "%(severalUsers)schanged their name %(repeats)s times", "was kicked %(count)s times|one": "was kicked",
"%(oneUser)schanged their name %(repeats)s times": "%(oneUser)schanged their name %(repeats)s times", "%(severalUsers)schanged their name %(count)s times|other": "%(severalUsers)schanged their name %(count)s times",
"%(severalUsers)schanged their name": "%(severalUsers)schanged their name", "%(severalUsers)schanged their name %(count)s times|one": "%(severalUsers)schanged their name",
"%(oneUser)schanged their name": "%(oneUser)schanged their name", "%(oneUser)schanged their name %(count)s times|other": "%(oneUser)schanged their name %(count)s times",
"%(severalUsers)schanged their avatar %(repeats)s times": "%(severalUsers)schanged their avatar %(repeats)s times", "%(oneUser)schanged their name %(count)s times|one": "%(oneUser)schanged their name",
"%(oneUser)schanged their avatar %(repeats)s times": "%(oneUser)schanged their avatar %(repeats)s times", "%(severalUsers)schanged their avatar %(count)s times|other": "%(severalUsers)schanged their avatar %(count)s times",
"%(severalUsers)schanged their avatar": "%(severalUsers)schanged their avatar", "%(severalUsers)schanged their avatar %(count)s times|one": "%(severalUsers)schanged their avatar",
"%(oneUser)schanged their avatar": "%(oneUser)schanged their avatar", "%(oneUser)schanged their avatar %(count)s times|other": "%(oneUser)schanged their avatar %(count)s times",
"%(items)s and %(remaining)s others": "%(items)s and %(remaining)s others", "%(oneUser)schanged their avatar %(count)s times|one": "%(oneUser)schanged their avatar",
"%(items)s and one other": "%(items)s and one other", "%(items)s and %(count)s others|other": "%(items)s and %(count)s others",
"%(items)s and %(count)s others|one": "%(items)s and one other",
"%(items)s and %(lastItem)s": "%(items)s and %(lastItem)s", "%(items)s and %(lastItem)s": "%(items)s and %(lastItem)s",
"Custom level": "Custom level", "Custom level": "Custom level",
"Room directory": "Room directory", "Room directory": "Room directory",
@ -573,7 +585,6 @@
"And %(count)s more...|other": "And %(count)s more...", "And %(count)s more...|other": "And %(count)s more...",
"ex. @bob:example.com": "ex. @bob:example.com", "ex. @bob:example.com": "ex. @bob:example.com",
"Add User": "Add User", "Add User": "Add User",
"Something went wrong!": "Something went wrong!",
"Matrix ID": "Matrix ID", "Matrix ID": "Matrix ID",
"Matrix Room ID": "Matrix Room ID", "Matrix Room ID": "Matrix Room ID",
"email address": "email address", "email address": "email address",
@ -587,8 +598,7 @@
"Start Chatting": "Start Chatting", "Start Chatting": "Start Chatting",
"Confirm Removal": "Confirm Removal", "Confirm Removal": "Confirm Removal",
"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.": "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.", "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.": "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.",
"%(actionVerb)s this person?": "%(actionVerb)s this person?", "Community IDs may only contain characters a-z, 0-9, or '=_-./'": "Community IDs may only contain characters a-z, 0-9, or '=_-./'",
"Community IDs may only contain alphanumeric characters": "Community IDs may only contain alphanumeric characters",
"Something went wrong whilst creating your community": "Something went wrong whilst creating your community", "Something went wrong whilst creating your community": "Something went wrong whilst creating your community",
"Create Community": "Create Community", "Create Community": "Create Community",
"Community Name": "Community Name", "Community Name": "Community Name",
@ -831,7 +841,7 @@
"A new password must be entered.": "A new password must be entered.", "A new password must be entered.": "A new password must be entered.",
"New passwords must match each other.": "New passwords must match each other.", "New passwords must match each other.": "New passwords must match each other.",
"Resetting password will currently reset any end-to-end encryption keys on all devices, making encrypted chat history unreadable, unless you first export your room keys and re-import them afterwards. In future this will be improved.": "Resetting password will currently reset any end-to-end encryption keys on all devices, making encrypted chat history unreadable, unless you first export your room keys and re-import them afterwards. In future this will be improved.", "Resetting password will currently reset any end-to-end encryption keys on all devices, making encrypted chat history unreadable, unless you first export your room keys and re-import them afterwards. In future this will be improved.": "Resetting password will currently reset any end-to-end encryption keys on all devices, making encrypted chat history unreadable, unless you first export your room keys and re-import them afterwards. In future this will be improved.",
"Once you've followed the link it contains, click below": "Once you've followed the link it contains, click below", "An email has been sent to %(emailAddress)s. Once you've followed the link it contains, click below.": "An email has been sent to %(emailAddress)s. Once you've followed the link it contains, click below.",
"I have verified my email address": "I have verified my email address", "I have verified my email address": "I have verified my email address",
"Your password has been reset": "Your password has been reset", "Your password has been reset": "Your password has been reset",
"You have been logged out of all devices and will no longer receive push notifications. To re-enable notifications, sign in again on each device": "You have been logged out of all devices and will no longer receive push notifications. To re-enable notifications, sign in again on each device", "You have been logged out of all devices and will no longer receive push notifications. To re-enable notifications, sign in again on each device": "You have been logged out of all devices and will no longer receive push notifications. To re-enable notifications, sign in again on each device",

View file

@ -252,6 +252,26 @@ function getLangsJson() {
}); });
} }
function weblateToCounterpart(inTrs) {
const outTrs = {};
for (const key of Object.keys(inTrs)) {
const keyParts = key.split('|', 2);
if (keyParts.length === 2) {
let obj = outTrs[keyParts[0]];
if (obj === undefined) {
obj = {};
outTrs[keyParts[0]] = obj;
}
obj[keyParts[1]] = inTrs[key];
} else {
outTrs[key] = inTrs[key];
}
}
return outTrs;
}
function getLanguage(langPath) { function getLanguage(langPath) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
request( request(
@ -261,7 +281,7 @@ function getLanguage(langPath) {
reject({err: err, response: response}); reject({err: err, response: response});
return; return;
} }
resolve(JSON.parse(body)); resolve(weblateToCounterpart(JSON.parse(body)));
}, },
); );
}); });

View file

@ -66,7 +66,7 @@ class FlairStore extends EventEmitter {
} }
// Bulk lookup ongoing, return promise to resolve/reject // Bulk lookup ongoing, return promise to resolve/reject
if (this._usersPending[userId]) { if (this._usersPending[userId] || this._usersInFlight[userId]) {
return this._usersPending[userId].prom; return this._usersPending[userId].prom;
} }
@ -91,7 +91,7 @@ class FlairStore extends EventEmitter {
console.error('Could not get groups for user', this.props.userId, err); console.error('Could not get groups for user', this.props.userId, err);
throw err; throw err;
}).finally(() => { }).finally(() => {
delete this._usersPending[userId]; delete this._usersInFlight[userId];
}); });
// This debounce will allow consecutive requests for the public groups of users that // This debounce will allow consecutive requests for the public groups of users that
@ -113,23 +113,25 @@ class FlairStore extends EventEmitter {
} }
async _batchedGetPublicGroups(matrixClient) { async _batchedGetPublicGroups(matrixClient) {
// Take the userIds from the keys of this._usersPending // Move users pending to users in flight
const usersInFlight = Object.keys(this._usersPending); this._usersInFlight = this._usersPending;
this._usersPending = {};
let resp = { let resp = {
users: [], users: [],
}; };
try { try {
resp = await matrixClient.getPublicisedGroups(usersInFlight); resp = await matrixClient.getPublicisedGroups(Object.keys(this._usersInFlight));
} catch (err) { } catch (err) {
// Propagate the same error to all usersInFlight // Propagate the same error to all usersInFlight
usersInFlight.forEach((userId) => { Object.keys(this._usersInFlight).forEach((userId) => {
this._usersPending[userId].reject(err); this._usersInFlight[userId].reject(err);
}); });
return; return;
} }
const updatedUserGroups = resp.users; const updatedUserGroups = resp.users;
usersInFlight.forEach((userId) => { Object.keys(this._usersInFlight).forEach((userId) => {
this._usersPending[userId].resolve(updatedUserGroups[userId] || []); this._usersInFlight[userId].resolve(updatedUserGroups[userId] || []);
}); });
} }

View file

@ -23,15 +23,27 @@ import FlairStore from './FlairStore';
* other useful group APIs that may have an effect on the group summary. * other useful group APIs that may have an effect on the group summary.
*/ */
export default class GroupStore extends EventEmitter { export default class GroupStore extends EventEmitter {
static STATE_KEY = {
GroupMembers: 'GroupMembers',
GroupInvitedMembers: 'GroupInvitedMembers',
Summary: 'Summary',
GroupRooms: 'GroupRooms',
};
constructor(matrixClient, groupId) { constructor(matrixClient, groupId) {
super(); super();
this.groupId = groupId; this.groupId = groupId;
this._matrixClient = matrixClient; this._matrixClient = matrixClient;
this._summary = {}; this._summary = {};
this._rooms = []; this._rooms = [];
this._fetchSummary(); this._members = [];
this._fetchRooms(); this._invitedMembers = [];
this._fetchMembers(); this._ready = {};
this.on('error', (err) => {
console.error(`GroupStore for ${this.groupId} encountered error`, err);
});
} }
_fetchMembers() { _fetchMembers() {
@ -39,6 +51,7 @@ export default class GroupStore extends EventEmitter {
this._members = result.chunk.map((apiMember) => { this._members = result.chunk.map((apiMember) => {
return groupMemberFromApiObject(apiMember); return groupMemberFromApiObject(apiMember);
}); });
this._ready[GroupStore.STATE_KEY.GroupMembers] = true;
this._notifyListeners(); this._notifyListeners();
}).catch((err) => { }).catch((err) => {
console.error("Failed to get group member list: " + err); console.error("Failed to get group member list: " + err);
@ -49,8 +62,13 @@ export default class GroupStore extends EventEmitter {
this._invitedMembers = result.chunk.map((apiMember) => { this._invitedMembers = result.chunk.map((apiMember) => {
return groupMemberFromApiObject(apiMember); return groupMemberFromApiObject(apiMember);
}); });
this._ready[GroupStore.STATE_KEY.GroupInvitedMembers] = true;
this._notifyListeners(); this._notifyListeners();
}).catch((err) => { }).catch((err) => {
// Invited users not visible to non-members
if (err.httpStatus === 403) {
return;
}
console.error("Failed to get group invited member list: " + err); console.error("Failed to get group invited member list: " + err);
this.emit('error', err); this.emit('error', err);
}); });
@ -59,6 +77,7 @@ export default class GroupStore extends EventEmitter {
_fetchSummary() { _fetchSummary() {
this._matrixClient.getGroupSummary(this.groupId).then((resp) => { this._matrixClient.getGroupSummary(this.groupId).then((resp) => {
this._summary = resp; this._summary = resp;
this._ready[GroupStore.STATE_KEY.Summary] = true;
this._notifyListeners(); this._notifyListeners();
}).catch((err) => { }).catch((err) => {
this.emit('error', err); this.emit('error', err);
@ -70,6 +89,7 @@ export default class GroupStore extends EventEmitter {
this._rooms = resp.chunk.map((apiRoom) => { this._rooms = resp.chunk.map((apiRoom) => {
return groupRoomFromApiObject(apiRoom); return groupRoomFromApiObject(apiRoom);
}); });
this._ready[GroupStore.STATE_KEY.GroupRooms] = true;
this._notifyListeners(); this._notifyListeners();
}).catch((err) => { }).catch((err) => {
this.emit('error', err); this.emit('error', err);
@ -80,6 +100,23 @@ export default class GroupStore extends EventEmitter {
this.emit('update'); this.emit('update');
} }
registerListener(fn) {
this.on('update', fn);
// Call to set initial state (before fetching starts)
this.emit('update');
this._fetchSummary();
this._fetchRooms();
this._fetchMembers();
}
unregisterListener(fn) {
this.removeListener('update', fn);
}
isStateReady(id) {
return this._ready[id];
}
getSummary() { getSummary() {
return this._summary; return this._summary;
} }
@ -104,9 +141,15 @@ export default class GroupStore extends EventEmitter {
return this._summary.user ? this._summary.user.is_privileged : null; return this._summary.user ? this._summary.user.is_privileged : null;
} }
addRoomToGroup(roomId) { addRoomToGroup(roomId, isPublic) {
return this._matrixClient return this._matrixClient
.addRoomToGroup(this.groupId, roomId) .addRoomToGroup(this.groupId, roomId, isPublic)
.then(this._fetchRooms.bind(this));
}
updateGroupRoomAssociation(roomId, isPublic) {
return this._matrixClient
.updateGroupRoomAssociation(this.groupId, roomId, isPublic)
.then(this._fetchRooms.bind(this)); .then(this._fetchRooms.bind(this));
} }

View file

@ -88,6 +88,9 @@ describe('MemberEventListSummary', function() {
sandbox = testUtils.stubClient(); sandbox = testUtils.stubClient();
languageHandler.setLanguage('en').done(done); languageHandler.setLanguage('en').done(done);
languageHandler.setMissingEntryGenerator(function(key) {
return key.split('|', 2)[1];
});
}); });
afterEach(function() { afterEach(function() {