Merge branch 'develop' into travis/pl_enhancements

pull/21833/head
Travis Ralston 2017-10-11 08:42:45 -06:00
commit f874fc1203
53 changed files with 815 additions and 270 deletions

View File

@ -19,6 +19,7 @@ import sdk from './';
import MultiInviter from './utils/MultiInviter';
import { _t } from './languageHandler';
import MatrixClientPeg from './MatrixClientPeg';
import GroupStoreCache from './stores/GroupStoreCache';
export function showGroupInviteDialog(groupId) {
const AddressPickerDialog = sdk.getComponent("dialogs.AddressPickerDialog");
@ -86,10 +87,11 @@ function _onGroupInviteFinished(groupId, addrs) {
}
function _onGroupAddRoomFinished(groupId, addrs) {
const groupStore = GroupStoreCache.getGroupStore(MatrixClientPeg.get(), groupId);
const errorList = [];
return Promise.all(addrs.map((addr) => {
return MatrixClientPeg.get()
.addRoomToGroup(groupId, addr.address)
return groupStore
.addRoomToGroup(addr.address)
.catch(() => { errorList.push(addr.address); })
.reflect();
})).then(() => {

View File

@ -243,7 +243,7 @@ function textForPowerEvent(event) {
if (to !== from) {
diff.push(
_t('%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s', {
userId: userId,
userId,
fromPowerLevel: Roles.textualPowerLevel(from, userDefault),
toPowerLevel: Roles.textualPowerLevel(to, userDefault),
}),
@ -254,7 +254,7 @@ function textForPowerEvent(event) {
return '';
}
return _t('%(senderName)s changed the power level of %(powerLevelDiffText)s.', {
senderName: senderName,
senderName,
powerLevelDiffText: diff.join(", "),
});
}
@ -291,12 +291,15 @@ function textForWidgetEvent(event) {
const handlers = {
'm.room.message': textForMessageEvent,
'm.room.name': textForRoomNameEvent,
'm.room.topic': textForTopicEvent,
'm.room.member': textForMemberEvent,
'm.call.invite': textForCallInviteEvent,
'm.call.answer': textForCallAnswerEvent,
'm.call.hangup': textForCallHangupEvent,
};
const stateHandlers = {
'm.room.name': textForRoomNameEvent,
'm.room.topic': textForTopicEvent,
'm.room.member': textForMemberEvent,
'm.room.third_party_invite': textForThreePidInviteEvent,
'm.room.history_visibility': textForHistoryVisibilityEvent,
'm.room.encryption': textForEncryptionEvent,
@ -307,8 +310,8 @@ const handlers = {
module.exports = {
textForEvent: function(ev) {
const hdlr = handlers[ev.getType()];
if (!hdlr) return '';
return hdlr(ev);
const handler = (ev.isState() ? stateHandlers : handlers)[ev.getType()];
if (handler) return handler(ev);
return '';
},
};

View File

@ -16,7 +16,7 @@ limitations under the License.
*/
import React from 'react';
import { _t } from '../languageHandler';
import { _t, _td } from '../languageHandler';
import AutocompleteProvider from './AutocompleteProvider';
import FuzzyMatcher from './FuzzyMatcher';
import {TextualCompletion} from './Components';
@ -27,82 +27,82 @@ const COMMANDS = [
{
command: '/me',
args: '<message>',
description: 'Displays action',
description: _td('Displays action'),
},
{
command: '/ban',
args: '<user-id> [reason]',
description: 'Bans user with given id',
description: _td('Bans user with given id'),
},
{
command: '/unban',
args: '<user-id>',
description: 'Unbans user with given id',
description: _td('Unbans user with given id'),
},
{
command: '/op',
args: '<user-id> [<power-level>]',
description: 'Define the power level of a user',
description: _td('Define the power level of a user'),
},
{
command: '/deop',
args: '<user-id>',
description: 'Deops user with given id',
description: _td('Deops user with given id'),
},
{
command: '/invite',
args: '<user-id>',
description: 'Invites user with given id to current room',
description: _td('Invites user with given id to current room'),
},
{
command: '/join',
args: '<room-alias>',
description: 'Joins room with given alias',
description: _td('Joins room with given alias'),
},
{
command: '/part',
args: '[<room-alias>]',
description: 'Leave room',
description: _td('Leave room'),
},
{
command: '/topic',
args: '<topic>',
description: 'Sets the room topic',
description: _td('Sets the room topic'),
},
{
command: '/kick',
args: '<user-id> [reason]',
description: 'Kicks user with given id',
description: _td('Kicks user with given id'),
},
{
command: '/nick',
args: '<display-name>',
description: 'Changes your display nickname',
description: _td('Changes your display nickname'),
},
{
command: '/ddg',
args: '<query>',
description: 'Searches DuckDuckGo for results',
description: _td('Searches DuckDuckGo for results'),
},
{
command: '/tint',
args: '<color1> [<color2>]',
description: 'Changes colour scheme of current room',
description: _td('Changes colour scheme of current room'),
},
{
command: '/verify',
args: '<user-id> <device-id> <device-signing-key>',
description: 'Verifies a user, device, and pubkey tuple',
description: _td('Verifies a user, device, and pubkey tuple'),
},
{
command: '/ignore',
args: '<user-id>',
description: 'Ignores a user, hiding their messages from you',
description: _td('Ignores a user, hiding their messages from you'),
},
{
command: '/unignore',
args: '<user-id>',
description: 'Stops ignoring a user, showing their messages going forward',
description: _td('Stops ignoring a user, showing their messages going forward'),
},
// Omitting `/markdown` as it only seems to apply to OldComposer
];

View File

@ -27,7 +27,8 @@ import AccessibleButton from '../views/elements/AccessibleButton';
import Modal from '../../Modal';
import classnames from 'classnames';
import GroupSummaryStore from '../../stores/GroupSummaryStore';
import GroupStoreCache from '../../stores/GroupStoreCache';
import GroupStore from '../../stores/GroupStore';
const RoomSummaryType = PropTypes.shape({
room_id: PropTypes.string.isRequired,
@ -78,7 +79,7 @@ const CategoryRoomList = React.createClass({
if (!success) return;
const errorList = [];
Promise.all(addrs.map((addr) => {
return this.context.groupSummaryStore
return this.context.groupStore
.addRoomToGroupSummary(addr.address)
.catch(() => { errorList.push(addr.address); })
.reflect();
@ -157,7 +158,7 @@ const FeaturedRoom = React.createClass({
onDeleteClicked: function(e) {
e.preventDefault();
e.stopPropagation();
this.context.groupSummaryStore.removeRoomFromGroupSummary(
this.context.groupStore.removeRoomFromGroupSummary(
this.props.summaryInfo.room_id,
).catch((err) => {
console.error('Error whilst removing room from group summary', err);
@ -252,7 +253,7 @@ const RoleUserList = React.createClass({
if (!success) return;
const errorList = [];
Promise.all(addrs.map((addr) => {
return this.context.groupSummaryStore
return this.context.groupStore
.addUserToGroupSummary(addr.address)
.catch(() => { errorList.push(addr.address); })
.reflect();
@ -327,7 +328,7 @@ const FeaturedUser = React.createClass({
onDeleteClicked: function(e) {
e.preventDefault();
e.stopPropagation();
this.context.groupSummaryStore.removeUserFromGroupSummary(
this.context.groupStore.removeUserFromGroupSummary(
this.props.summaryInfo.user_id,
).catch((err) => {
console.error('Error whilst removing user from group summary', err);
@ -373,14 +374,14 @@ const FeaturedUser = React.createClass({
},
});
const GroupSummaryContext = {
groupSummaryStore: React.PropTypes.instanceOf(GroupSummaryStore).isRequired,
const GroupContext = {
groupStore: React.PropTypes.instanceOf(GroupStore).isRequired,
};
CategoryRoomList.contextTypes = GroupSummaryContext;
FeaturedRoom.contextTypes = GroupSummaryContext;
RoleUserList.contextTypes = GroupSummaryContext;
FeaturedUser.contextTypes = GroupSummaryContext;
CategoryRoomList.contextTypes = GroupContext;
FeaturedRoom.contextTypes = GroupContext;
RoleUserList.contextTypes = GroupContext;
FeaturedUser.contextTypes = GroupContext;
export default React.createClass({
displayName: 'GroupView',
@ -390,12 +391,12 @@ export default React.createClass({
},
childContextTypes: {
groupSummaryStore: React.PropTypes.instanceOf(GroupSummaryStore),
groupStore: React.PropTypes.instanceOf(GroupStore),
},
getChildContext: function() {
return {
groupSummaryStore: this._groupSummaryStore,
groupStore: this._groupStore,
};
},
@ -413,14 +414,14 @@ export default React.createClass({
componentWillMount: function() {
this._changeAvatarComponent = null;
this._initGroupSummaryStore(this.props.groupId);
this._initGroupStore(this.props.groupId);
MatrixClientPeg.get().on("Group.myMembership", this._onGroupMyMembership);
},
componentWillUnmount: function() {
MatrixClientPeg.get().removeListener("Group.myMembership", this._onGroupMyMembership);
this._groupSummaryStore.removeAllListeners();
this._groupStore.removeAllListeners();
},
componentWillReceiveProps: function(newProps) {
@ -429,7 +430,7 @@ export default React.createClass({
summary: null,
error: null,
}, () => {
this._initGroupSummaryStore(newProps.groupId);
this._initGroupStore(newProps.groupId);
});
}
},
@ -440,17 +441,15 @@ export default React.createClass({
this.setState({membershipBusy: false});
},
_initGroupSummaryStore: function(groupId) {
this._groupSummaryStore = new GroupSummaryStore(
MatrixClientPeg.get(), this.props.groupId,
);
this._groupSummaryStore.on('update', () => {
_initGroupStore: function(groupId) {
this._groupStore = GroupStoreCache.getGroupStore(MatrixClientPeg.get(), groupId);
this._groupStore.on('update', () => {
this.setState({
summary: this._groupSummaryStore.getSummary(),
summary: this._groupStore.getSummary(),
error: null,
});
});
this._groupSummaryStore.on('error', (err) => {
this._groupStore.on('error', (err) => {
this.setState({
summary: null,
error: err,
@ -527,7 +526,7 @@ export default React.createClass({
editing: false,
summary: null,
});
this._initGroupSummaryStore(this.props.groupId);
this._initGroupStore(this.props.groupId);
}).catch((e) => {
this.setState({
saving: false,
@ -606,7 +605,7 @@ export default React.createClass({
this.setState({
publicityBusy: true,
});
this._groupSummaryStore.setGroupPublicity(publicity).then(() => {
this._groupStore.setGroupPublicity(publicity).then(() => {
this.setState({
publicityBusy: false,
});

View File

@ -773,15 +773,13 @@ module.exports = React.createClass({
dis.dispatch({action: 'view_set_mxid'});
return;
}
const TextInputDialog = sdk.getComponent("dialogs.TextInputDialog");
Modal.createTrackedDialog('Create Room', '', TextInputDialog, {
title: _t('Create Room'),
description: _t('Room name (optional)'),
button: _t('Create Room'),
onFinished: (shouldCreate, name) => {
const CreateRoomDialog = sdk.getComponent('dialogs.CreateRoomDialog');
Modal.createTrackedDialog('Create Room', '', CreateRoomDialog, {
onFinished: (shouldCreate, name, noFederate) => {
if (shouldCreate) {
const createOpts = {};
if (name) createOpts.name = name;
if (noFederate) createOpts.creation_content = {'m.federate': false};
createRoom({createOpts}).done();
}
},

View File

@ -32,7 +32,7 @@ const AddThreepid = require('../../AddThreepid');
const SdkConfig = require('../../SdkConfig');
import Analytics from '../../Analytics';
import AccessibleButton from '../views/elements/AccessibleButton';
import { _t } from '../../languageHandler';
import { _t, _td } from '../../languageHandler';
import * as languageHandler from '../../languageHandler';
import * as FormattingUtils from '../../utils/FormattingUtils';
@ -63,55 +63,55 @@ const gHVersionLabel = function(repo, token='') {
const SETTINGS_LABELS = [
{
id: 'autoplayGifsAndVideos',
label: 'Autoplay GIFs and videos',
label: _td('Autoplay GIFs and videos'),
},
{
id: 'hideReadReceipts',
label: 'Hide read receipts',
label: _td('Hide read receipts'),
},
{
id: 'dontSendTypingNotifications',
label: "Don't send typing notifications",
label: _td("Don't send typing notifications"),
},
{
id: 'alwaysShowTimestamps',
label: 'Always show message timestamps',
label: _td('Always show message timestamps'),
},
{
id: 'showTwelveHourTimestamps',
label: 'Show timestamps in 12 hour format (e.g. 2:30pm)',
label: _td('Show timestamps in 12 hour format (e.g. 2:30pm)'),
},
{
id: 'hideJoinLeaves',
label: 'Hide join/leave messages (invites/kicks/bans unaffected)',
label: _td('Hide join/leave messages (invites/kicks/bans unaffected)'),
},
{
id: 'hideAvatarDisplaynameChanges',
label: 'Hide avatar and display name changes',
label: _td('Hide avatar and display name changes'),
},
{
id: 'useCompactLayout',
label: 'Use compact timeline layout',
label: _td('Use compact timeline layout'),
},
{
id: 'hideRedactions',
label: 'Hide removed messages',
label: _td('Hide removed messages'),
},
{
id: 'enableSyntaxHighlightLanguageDetection',
label: 'Enable automatic language detection for syntax highlighting',
label: _td('Enable automatic language detection for syntax highlighting'),
},
{
id: 'MessageComposerInput.autoReplaceEmoji',
label: 'Automatically replace plain text Emoji',
label: _td('Automatically replace plain text Emoji'),
},
{
id: 'MessageComposerInput.dontSuggestEmoji',
label: 'Disable Emoji suggestions while typing',
label: _td('Disable Emoji suggestions while typing'),
},
{
id: 'Pill.shouldHidePillAvatar',
label: 'Hide avatars in user and room mentions',
label: _td('Hide avatars in user and room mentions'),
},
/*
{
@ -124,7 +124,7 @@ const SETTINGS_LABELS = [
const ANALYTICS_SETTINGS_LABELS = [
{
id: 'analyticsOptOut',
label: 'Opt out of analytics',
label: _td('Opt out of analytics'),
fn: function(checked) {
Analytics[checked ? 'disable' : 'enable']();
},
@ -134,7 +134,7 @@ const ANALYTICS_SETTINGS_LABELS = [
const WEBRTC_SETTINGS_LABELS = [
{
id: 'webRtcForceTURN',
label: 'Disable Peer-to-Peer for 1:1 calls',
label: _td('Disable Peer-to-Peer for 1:1 calls'),
},
];
@ -143,7 +143,7 @@ const WEBRTC_SETTINGS_LABELS = [
const CRYPTO_SETTINGS_LABELS = [
{
id: 'blacklistUnverifiedDevices',
label: 'Never send encrypted messages to unverified devices from this device',
label: _td('Never send encrypted messages to unverified devices from this device'),
fn: function(checked) {
MatrixClientPeg.get().setGlobalBlacklistUnverifiedDevices(checked);
},
@ -166,12 +166,12 @@ const CRYPTO_SETTINGS_LABELS = [
const THEMES = [
{
id: 'theme',
label: 'Light theme',
label: _td('Light theme'),
value: 'light',
},
{
id: 'theme',
label: 'Dark theme',
label: _td('Dark theme'),
value: 'dark',
},
];
@ -793,7 +793,7 @@ module.exports = React.createClass({
onChange={onChange}
/>
<label htmlFor={setting.id + "_" + setting.value}>
{ setting.label }
{ _t(setting.label) }
</label>
</div>;
},

View File

@ -290,6 +290,7 @@ module.exports = React.createClass({
onPhoneNumberChanged={this.onPhoneNumberChanged}
onForgotPasswordClick={this.props.onForgotPasswordClick}
loginIncorrect={this.state.loginIncorrect}
hsUrl={this.state.enteredHomeserverUrl}
/>
);
case 'm.login.cas':

View File

@ -23,6 +23,7 @@ import MatrixClientPeg from '../../../MatrixClientPeg';
import AccessibleButton from '../elements/AccessibleButton';
import Promise from 'bluebird';
import { addressTypes, getAddressType } from '../../../UserAddress.js';
import GroupStoreCache from '../../../stores/GroupStoreCache';
const TRUNCATE_QUERY_LIST = 40;
const QUERY_USER_DIRECTORY_DEBOUNCE_MS = 200;
@ -241,32 +242,25 @@ module.exports = React.createClass({
_doNaiveGroupRoomSearch: function(query) {
const lowerCaseQuery = query.toLowerCase();
MatrixClientPeg.get().getGroupRooms(this.props.groupId).then((resp) => {
const results = [];
resp.chunk.forEach((r) => {
const nameMatch = (r.name || '').toLowerCase().includes(lowerCaseQuery);
const topicMatch = (r.topic || '').toLowerCase().includes(lowerCaseQuery);
const aliasMatch = (r.canonical_alias || '').toLowerCase().includes(lowerCaseQuery);
if (!(nameMatch || topicMatch || aliasMatch)) {
return;
}
results.push({
room_id: r.room_id,
avatar_url: r.avatar_url,
name: r.name || r.canonical_alias,
});
});
this._processResults(results, query);
}).catch((err) => {
console.error('Error whilst searching group users: ', err);
this.setState({
searchError: err.errcode ? err.message : _t('Something went wrong!'),
});
}).done(() => {
this.setState({
busy: false,
const groupStore = GroupStoreCache.getGroupStore(MatrixClientPeg.get(), this.props.groupId);
const results = [];
groupStore.getGroupRooms().forEach((r) => {
const nameMatch = (r.name || '').toLowerCase().includes(lowerCaseQuery);
const topicMatch = (r.topic || '').toLowerCase().includes(lowerCaseQuery);
const aliasMatch = (r.canonical_alias || '').toLowerCase().includes(lowerCaseQuery);
if (!(nameMatch || topicMatch || aliasMatch)) {
return;
}
results.push({
room_id: r.room_id,
avatar_url: r.avatar_url,
name: r.name || r.canonical_alias,
});
});
this._processResults(results, query);
this.setState({
busy: false,
});
},
_doRoomSearch: function(query) {

View File

@ -0,0 +1,81 @@
/*
Copyright 2017 Michael Telatynski <7t3chguy@gmail.com>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import sdk from '../../../index';
import SdkConfig from '../../../SdkConfig';
import { _t } from '../../../languageHandler';
export default React.createClass({
displayName: 'CreateRoomDialog',
propTypes: {
onFinished: React.PropTypes.func.isRequired,
},
componentDidMount: function() {
const config = SdkConfig.get();
// Dialog shows inverse of m.federate (noFederate) strict false check to skip undefined check (default = true)
this.defaultNoFederate = config.default_federate === false;
},
onOk: function() {
this.props.onFinished(true, this.refs.textinput.value, this.refs.checkbox.checked);
},
onCancel: function() {
this.props.onFinished(false);
},
render: function() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
return (
<BaseDialog className="mx_CreateRoomDialog" onFinished={this.props.onFinished}
onEnterPressed={this.onOk}
title={_t('Create Room')}
>
<div className="mx_Dialog_content">
<div className="mx_CreateRoomDialog_label">
<label htmlFor="textinput"> { _t('Room name (optional)') } </label>
</div>
<div>
<input id="textinput" ref="textinput" className="mx_CreateRoomDialog_input" autoFocus={true} size="64" />
</div>
<br />
<details className="mx_CreateRoomDialog_details">
<summary className="mx_CreateRoomDialog_details_summary">{ _t('Advanced options') }</summary>
<div>
<input type="checkbox" id="checkbox" ref="checkbox" defaultChecked={this.defaultNoFederate} />
<label htmlFor="checkbox">
{ _t('Block users on other matrix homeservers from joining this room') }
<br />
({ _t('This setting cannot be changed later!') })
</label>
</div>
</details>
</div>
<div className="mx_Dialog_buttons">
<button onClick={this.onCancel}>
{ _t('Cancel') }
</button>
<button className="mx_Dialog_primary" onClick={this.onOk}>
{ _t('Create Room') }
</button>
</div>
</BaseDialog>
);
},
});

View File

@ -18,7 +18,7 @@ import Modal from '../../../Modal';
import React from 'react';
import sdk from '../../../index';
import { _t } from '../../../languageHandler';
import { _t, _td } from '../../../languageHandler';
/**
* Dialog which asks the user whether they want to share their keys with
@ -116,11 +116,11 @@ export default React.createClass({
let text;
if (this.state.wasNewDevice) {
text = "You added a new device '%(displayName)s', which is"
+ " requesting encryption keys.";
text = _td("You added a new device '%(displayName)s', which is"
+ " requesting encryption keys.");
} else {
text = "Your unverified device '%(displayName)s' is requesting"
+ " encryption keys.";
text = _td("Your unverified device '%(displayName)s' is requesting"
+ " encryption keys.");
}
text = _t(text, {displayName: displayName});

View File

@ -68,7 +68,7 @@ export default React.createClass({
<label htmlFor="textinput"> { this.props.description } </label>
</div>
<div>
<input id="textinput" ref="textinput" className="mx_TextInputDialog_input" defaultValue={this.props.value} autoFocus={this.props.focus} size="64" onKeyDown={this.onKeyDown} />
<input id="textinput" ref="textinput" className="mx_TextInputDialog_input" defaultValue={this.props.value} autoFocus={this.props.focus} size="64" />
</div>
</div>
<div className="mx_Dialog_buttons">

View File

@ -23,7 +23,7 @@ import PlatformPeg from '../../../PlatformPeg';
import ScalarAuthClient from '../../../ScalarAuthClient';
import SdkConfig from '../../../SdkConfig';
import Modal from '../../../Modal';
import { _t } from '../../../languageHandler';
import { _t, _td } from '../../../languageHandler';
import sdk from '../../../index';
import AppPermission from './AppPermission';
import AppWarning from './AppWarning';
@ -195,9 +195,9 @@ export default React.createClass({
// These strings are translated at the point that they are inserted in to the DOM, in the render method
_deleteWidgetLabel() {
if (this._canUserModify()) {
return 'Delete widget';
return _td('Delete widget');
}
return 'Revoke widget access';
return _td('Revoke widget access');
},
/* TODO -- Store permission in account data so that it is persisted across multiple devices */

View File

@ -26,6 +26,12 @@ class MenuOption extends React.Component {
this._onClick = this._onClick.bind(this);
}
getDefaultProps() {
return {
disabled: false,
}
}
_onMouseEnter() {
this.props.onMouseEnter(this.props.dropdownKey);
}
@ -153,6 +159,8 @@ export default class Dropdown extends React.Component {
}
_onInputClick(ev) {
if (this.props.disabled) return;
if (!this.state.expanded) {
this.setState({
expanded: true,
@ -294,6 +302,7 @@ export default class Dropdown extends React.Component {
const dropdownClasses = {
mx_Dropdown: true,
mx_Dropdown_disabled: this.props.disabled,
};
if (this.props.className) {
dropdownClasses[this.props.className] = true;
@ -329,4 +338,6 @@ Dropdown.propTypes = {
// in the dropped-down menu.
getShortOption: React.PropTypes.func,
value: React.PropTypes.string,
// negative for consistency with HTML
disabled: React.PropTypes.bool,
}

View File

@ -0,0 +1,149 @@
/*
Copyright 2017 New Vector Ltd.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import sdk from '../../../index';
import {_t} from '../../../languageHandler.js';
const EditableItem = React.createClass({
displayName: 'EditableItem',
propTypes: {
initialValue: PropTypes.string,
index: PropTypes.number,
placeholder: PropTypes.string,
onChange: PropTypes.func,
onRemove: PropTypes.func,
onAdd: PropTypes.func,
addOnChange: PropTypes.bool,
},
onChange: function(value) {
this.setState({ value });
if (this.props.onChange) this.props.onChange(value, this.props.index);
if (this.props.addOnChange && this.props.onAdd) this.props.onAdd(value);
},
onRemove: function() {
if (this.props.onRemove) this.props.onRemove(this.props.index);
},
onAdd: function() {
if (this.props.onAdd) this.props.onAdd(this.state.value);
},
render: function() {
const EditableText = sdk.getComponent('elements.EditableText');
return <div className="mx_EditableItem">
<EditableText
className="mx_EditableItem_editable"
placeholderClassName="mx_EditableItem_editablePlaceholder"
placeholder={this.props.placeholder}
blurToCancel={false}
editable={true}
initialValue={this.props.initialValue}
onValueChanged={this.onChange} />
{ this.props.onAdd ?
<div className="mx_EditableItem_addButton">
<img className="mx_filterFlipColor"
src="img/plus.svg" width="14" height="14"
alt={_t("Add")} onClick={this.onAdd} />
</div>
:
<div className="mx_EditableItem_removeButton">
<img className="mx_filterFlipColor"
src="img/cancel-small.svg" width="14" height="14"
alt={_t("Delete")} onClick={this.onRemove} />
</div>
}
</div>;
},
});
module.exports = React.createClass({
displayName: 'EditableItemList',
propTypes: {
items: PropTypes.arrayOf(PropTypes.string).isRequired,
onNewItemChanged: PropTypes.func,
onItemAdded: PropTypes.func,
onItemEdited: PropTypes.func,
onItemRemoved: PropTypes. func,
},
getDefaultProps: function() {
return {
onItemAdded: () => {},
onItemEdited: () => {},
onItemRemoved: () => {},
onNewItemChanged: () => {},
};
},
onItemAdded: function(value) {
this.props.onItemAdded(value);
},
onItemEdited: function(value, index) {
if (value.length === 0) {
this.onItemRemoved(index);
} else {
this.props.onItemEdited(value, index);
}
},
onItemRemoved: function(index) {
this.props.onItemRemoved(index);
},
onNewItemChanged: function(value) {
this.props.onNewItemChanged(value);
},
render: function() {
const editableItems = this.props.items.map((item, index) => {
return <EditableItem
key={index}
index={index}
initialValue={item}
onChange={this.onItemEdited}
onRemove={this.onItemRemoved}
placeholder={this.props.placeholder}
/>;
});
const label = this.props.items.length > 0 ?
this.props.itemsLabel : this.props.noItemsLabel;
return (<div className="mx_EditableItemList">
<div className="mx_EditableItemList_label">
{ label }
</div>
{ editableItems }
<EditableItem
key={-1}
initialValue={this.props.newItem}
onAdd={this.onItemAdded}
onChange={this.onNewItemChanged}
addOnChange={true}
placeholder={this.props.placeholder}
/>
</div>);
},
});

View File

@ -65,7 +65,9 @@ module.exports = React.createClass({
},
componentWillReceiveProps: function(nextProps) {
if (nextProps.initialValue !== this.props.initialValue) {
if (nextProps.initialValue !== this.props.initialValue ||
nextProps.initialValue !== this.value
) {
this.value = nextProps.initialValue;
if (this.refs.editable_div) {
this.showPlaceholder(!this.value);

View File

@ -183,10 +183,12 @@ export default class Flair extends React.Component {
this.state = {
profiles: [],
};
this.onRoomStateEvents = this.onRoomStateEvents.bind(this);
}
componentWillUnmount() {
this._unmounted = true;
this.context.matrixClient.removeListener('RoomState.events', this.onRoomStateEvents);
}
componentWillMount() {
@ -194,6 +196,13 @@ export default class Flair extends React.Component {
if (UserSettingsStore.isFeatureEnabled('feature_groups') && groupSupport) {
this._generateAvatars();
}
this.context.matrixClient.on('RoomState.events', this.onRoomStateEvents);
}
onRoomStateEvents(event) {
if (event.getType() === 'm.room.related_groups' && groupSupport) {
this._generateAvatars();
}
}
async _getGroupProfiles(groups) {
@ -224,6 +233,21 @@ export default class Flair extends React.Component {
}
console.error('Could not get groups for user', this.props.userId, err);
}
if (this.props.roomId && this.props.showRelated) {
const relatedGroupsEvent = this.context.matrixClient
.getRoom(this.props.roomId)
.currentState
.getStateEvents('m.room.related_groups', '');
const relatedGroups = relatedGroupsEvent ?
relatedGroupsEvent.getContent().groups || [] : [];
if (relatedGroups && relatedGroups.length > 0) {
groups = groups.filter((groupId) => {
return relatedGroups.includes(groupId);
});
} else {
groups = [];
}
}
if (!groups || groups.length === 0) {
return;
}
@ -250,6 +274,12 @@ export default class Flair extends React.Component {
Flair.propTypes = {
userId: PropTypes.string,
// Whether to show only the flair associated with related groups of the given room,
// or all flair associated with a user.
showRelated: PropTypes.bool,
// The room that this flair will be displayed in. Optional. Only applies when showRelated = true.
roomId: PropTypes.string,
};
// TODO: We've decided that all components should follow this pattern, which means removing withMatrixClient and using

View File

@ -17,6 +17,7 @@ import React from 'react';
import { _t } from '../../../languageHandler';
import sdk from '../../../index';
import { groupRoomFromApiObject } from '../../../groups';
import GroupStoreCache from '../../../stores/GroupStoreCache';
import GeminiScrollbar from 'react-gemini-scrollbar';
import PropTypes from 'prop-types';
import {MatrixClient} from 'matrix-js-sdk';
@ -34,7 +35,6 @@ export default React.createClass({
getInitialState: function() {
return {
fetching: false,
rooms: null,
truncateAt: INITIAL_LOAD_NUM_ROOMS,
searchQuery: "",
@ -43,21 +43,29 @@ export default React.createClass({
componentWillMount: function() {
this._unmounted = false;
this._initGroupStore(this.props.groupId);
},
_initGroupStore: function(groupId) {
this._groupStore = GroupStoreCache.getGroupStore(this.context.matrixClient, groupId);
this._groupStore.on('update', () => {
this._fetchRooms();
});
this._groupStore.on('error', (err) => {
console.error('Error in group store (listened to by GroupRoomList)', err);
this.setState({
rooms: null,
});
});
this._fetchRooms();
},
_fetchRooms: function() {
this.setState({fetching: true});
this.context.matrixClient.getGroupRooms(this.props.groupId).then((result) => {
this.setState({
rooms: result.chunk.map((apiRoom) => {
return groupRoomFromApiObject(apiRoom);
}),
fetching: false,
});
}).catch((e) => {
this.setState({fetching: false});
console.error("Failed to get group room list: ", e);
if (this._unmounted) return;
this.setState({
rooms: this._groupStore.getGroupRooms().map((apiRoom) => {
return groupRoomFromApiObject(apiRoom);
}),
});
},
@ -110,12 +118,7 @@ export default React.createClass({
},
render: function() {
if (this.state.fetching) {
const Spinner = sdk.getComponent("elements.Spinner");
return (<div className="mx_GroupRoomList">
<Spinner />
</div>);
} else if (this.state.rooms === null) {
if (this.state.rooms === null) {
return null;
}

View File

@ -21,6 +21,8 @@ import PropTypes from 'prop-types';
import sdk from '../../../index';
import dis from '../../../dispatcher';
import { GroupRoomType } from '../../../groups';
import GroupStoreCache from '../../../stores/GroupStoreCache';
import Modal from '../../../Modal';
const GroupRoomTile = React.createClass({
displayName: 'GroupRoomTile',
@ -31,7 +33,35 @@ const GroupRoomTile = React.createClass({
},
getInitialState: function() {
return {};
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 group"),
description: _t("Failed to remove '%(roomName)s' from %(groupId)s", {groupId, roomName}),
});
});
},
onClick: function(e) {
@ -49,20 +79,34 @@ const GroupRoomTile = React.createClass({
});
},
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 group will also remove it from the group page."),
button: _t("Remove"),
onFinished: (success) => {
if (success) {
this.removeRoomFromGroup();
}
},
});
},
render: function() {
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const name = this.props.groupRoom.name ||
this.props.groupRoom.canonicalAlias ||
_t("Unnamed Room");
const avatarUrl = this.context.matrixClient.mxcUrlToHttp(
this.props.groupRoom.avatarUrl,
36, 36, 'crop',
);
const av = (
<BaseAvatar name={name}
<BaseAvatar name={this.state.name}
width={36} height={36}
url={avatarUrl}
/>
@ -74,8 +118,11 @@ const GroupRoomTile = React.createClass({
{ av }
</div>
<div className="mx_GroupRoomTile_name">
{ name }
{ this.state.name }
</div>
<AccessibleButton className="mx_GroupRoomTile_delete" onClick={this.onDeleteClick}>
<img src="img/cancel-small.svg" />
</AccessibleButton>
</AccessibleButton>
);
},

View File

@ -123,7 +123,7 @@ export default class CountryDropdown extends React.Component {
return <Dropdown className={this.props.className + " left_aligned"}
onOptionChange={this._onOptionChange} onSearchChange={this._onSearchChange}
menuWidth={298} getShortOption={this._getShortOption}
value={value} searchEnabled={true}
value={value} searchEnabled={true} disabled={this.props.disabled}
>
{options}
</Dropdown>;
@ -137,4 +137,5 @@ CountryDropdown.propTypes = {
showPrefix: React.PropTypes.bool,
onOptionChange: React.PropTypes.func.isRequired,
value: React.PropTypes.string,
disabled: React.PropTypes.bool,
};

View File

@ -116,11 +116,17 @@ class PasswordLogin extends React.Component {
this.props.onPasswordChanged(ev.target.value);
}
renderLoginField(loginType) {
renderLoginField(loginType, disabled) {
const classes = {
mx_Login_field: true,
mx_Login_field_disabled: disabled,
};
switch(loginType) {
case PasswordLogin.LOGIN_FIELD_EMAIL:
classes.mx_Login_email = true;
return <input
className="mx_Login_field mx_Login_email"
className={classNames(classes)}
key="email_input"
type="text"
name="username" // make it a little easier for browser's remember-password
@ -128,10 +134,12 @@ class PasswordLogin extends React.Component {
placeholder="joe@example.com"
value={this.state.username}
autoFocus
disabled={disabled}
/>;
case PasswordLogin.LOGIN_FIELD_MXID:
classes.mx_Login_username = true;
return <input
className="mx_Login_field mx_Login_username"
className={classNames(classes)}
key="username_input"
type="text"
name="username" // make it a little easier for browser's remember-password
@ -139,9 +147,12 @@ class PasswordLogin extends React.Component {
placeholder={_t('User name')}
value={this.state.username}
autoFocus
disabled={disabled}
/>;
case PasswordLogin.LOGIN_FIELD_PHONE:
const CountryDropdown = sdk.getComponent('views.login.CountryDropdown');
classes.mx_Login_phoneNumberField = true;
classes.mx_Login_field_has_prefix = true;
return <div className="mx_Login_phoneSection">
<CountryDropdown
className="mx_Login_phoneCountry mx_Login_field_prefix"
@ -150,9 +161,10 @@ class PasswordLogin extends React.Component {
value={this.state.phoneCountry}
isSmall={true}
showPrefix={true}
disabled={disabled}
/>
<input
className="mx_Login_phoneNumberField mx_Login_field mx_Login_field_has_prefix"
className={classNames(classes)}
ref="phoneNumber"
key="phone_input"
type="text"
@ -161,6 +173,7 @@ class PasswordLogin extends React.Component {
placeholder={_t("Mobile phone number")}
value={this.state.phoneNumber}
autoFocus
disabled={disabled}
/>
</div>;
}
@ -177,14 +190,25 @@ class PasswordLogin extends React.Component {
);
}
let matrixIdText = '';
if (this.props.hsUrl) {
try {
const parsedHsUrl = new URL(this.props.hsUrl);
matrixIdText = _t('%(serverName)s Matrix ID', {serverName: parsedHsUrl.hostname});
} catch (e) {
// pass
}
}
const pwFieldClass = classNames({
mx_Login_field: true,
mx_Login_field_disabled: matrixIdText === '',
error: this.props.loginIncorrect,
});
const Dropdown = sdk.getComponent('elements.Dropdown');
const loginField = this.renderLoginField(this.state.loginType);
const loginField = this.renderLoginField(this.state.loginType, matrixIdText === '');
return (
<div>
@ -194,8 +218,9 @@ class PasswordLogin extends React.Component {
<Dropdown
className="mx_Login_type_dropdown"
value={this.state.loginType}
disabled={matrixIdText === ''}
onOptionChange={this.onLoginTypeChange}>
<span key={PasswordLogin.LOGIN_FIELD_MXID}>{ _t('my Matrix ID') }</span>
<span key={PasswordLogin.LOGIN_FIELD_MXID}>{matrixIdText}</span>
<span key={PasswordLogin.LOGIN_FIELD_EMAIL}>{ _t('Email address') }</span>
<span key={PasswordLogin.LOGIN_FIELD_PHONE}>{ _t('Phone') }</span>
</Dropdown>
@ -204,10 +229,12 @@ class PasswordLogin extends React.Component {
<input className={pwFieldClass} ref={(e) => {this._passwordField = e;}} type="password"
name="password"
value={this.state.password} onChange={this.onPasswordChanged}
placeholder={ _t('Password') } />
placeholder={ _t('Password') }
disabled={matrixIdText === ''}
/>
<br />
{forgotPasswordJsx}
<input className="mx_Login_submit" type="submit" value={ _t('Sign in') } />
<input className="mx_Login_submit" type="submit" value={ _t('Sign in') } disabled={matrixIdText === ''} />
</form>
</div>
);

View File

@ -33,7 +33,13 @@ export default function SenderProfile(props) {
return (
<div className="mx_SenderProfile" dir="auto" onClick={props.onClick}>
<EmojiText className="mx_SenderProfile_name">{ name || '' }</EmojiText>
{ props.enableFlair ? <Flair userId={mxEvent.getSender()} /> : null }
{ props.enableFlair ?
<Flair
userId={mxEvent.getSender()}
roomId={mxEvent.getRoomId()}
showRelated={true} />
: null
}
{ props.aux ? <EmojiText className="mx_SenderProfile_aux"> { props.aux }</EmojiText> : null }
</div>
);

View File

@ -136,24 +136,25 @@ module.exports = React.createClass({
return ObjectUtils.getKeyValueArrayDiffs(oldAliases, this.state.domainToAliases);
},
onAliasAdded: function(alias) {
onNewAliasChanged: function(value) {
this.setState({newAlias: value});
},
onLocalAliasAdded: function(alias) {
if (!alias || alias.length === 0) return; // ignore attempts to create blank aliases
if (this.isAliasValid(alias)) {
// add this alias to the domain to aliases dict
var domain = alias.replace(/^.*?:/, '');
// XXX: do we need to deep copy aliases before editing it?
this.state.domainToAliases[domain] = this.state.domainToAliases[domain] || [];
this.state.domainToAliases[domain].push(alias);
this.setState({
domainToAliases: this.state.domainToAliases
});
const localDomain = MatrixClientPeg.get().getDomain();
if (this.isAliasValid(alias) && alias.endsWith(localDomain)) {
this.state.domainToAliases[localDomain] = this.state.domainToAliases[localDomain] || [];
this.state.domainToAliases[localDomain].push(alias);
// reset the add field
this.refs.add_alias.setValue(''); // FIXME
}
else {
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
this.setState({
domainToAliases: this.state.domainToAliases,
// Reset the add field
newAlias: "",
});
} else {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Invalid alias format', '', ErrorDialog, {
title: _t('Invalid alias format'),
description: _t('\'%(alias)s\' is not a valid format for an alias', { alias: alias }),
@ -161,15 +162,13 @@ module.exports = React.createClass({
}
},
onAliasChanged: function(domain, index, alias) {
onLocalAliasChanged: function(alias, index) {
if (alias === "") return; // hit the delete button to delete please
var oldAlias;
if (this.isAliasValid(alias)) {
oldAlias = this.state.domainToAliases[domain][index];
this.state.domainToAliases[domain][index] = alias;
}
else {
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
const localDomain = MatrixClientPeg.get().getDomain();
if (this.isAliasValid(alias) && alias.endsWith(localDomain)) {
this.state.domainToAliases[localDomain][index] = alias;
} else {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Invalid address format', '', ErrorDialog, {
title: _t('Invalid address format'),
description: _t('\'%(alias)s\' is not a valid format for an address', { alias: alias }),
@ -177,15 +176,16 @@ module.exports = React.createClass({
}
},
onAliasDeleted: function(domain, index) {
onLocalAliasDeleted: function(index) {
const localDomain = MatrixClientPeg.get().getDomain();
// It's a bit naughty to directly manipulate this.state, and React would
// normally whine at you, but it can't see us doing the splice. Given we
// promptly setState anyway, it's just about acceptable. The alternative
// would be to arbitrarily deepcopy to a temp variable and then setState
// that, but why bother when we can cut this corner.
var alias = this.state.domainToAliases[domain].splice(index, 1);
this.state.domainToAliases[localDomain].splice(index, 1);
this.setState({
domainToAliases: this.state.domainToAliases
domainToAliases: this.state.domainToAliases,
});
},
@ -198,6 +198,7 @@ module.exports = React.createClass({
render: function() {
var self = this;
var EditableText = sdk.getComponent("elements.EditableText");
var EditableItemList = sdk.getComponent("elements.EditableItemList");
var localDomain = MatrixClientPeg.get().getDomain();
var canonical_alias_section;
@ -257,58 +258,24 @@ module.exports = React.createClass({
<div className="mx_RoomSettings_aliasLabel">
{ _t('The main address for this room is') }: { canonical_alias_section }
</div>
<div className="mx_RoomSettings_aliasLabel">
{ (this.state.domainToAliases[localDomain] &&
this.state.domainToAliases[localDomain].length > 0)
? _t('Local addresses for this room:')
: _t('This room has no local addresses') }
</div>
<div className="mx_RoomSettings_aliasesTable">
{ (this.state.domainToAliases[localDomain] || []).map((alias, i) => {
var deleteButton;
if (this.props.canSetAliases) {
deleteButton = (
<img src="img/cancel-small.svg" width="14" height="14"
alt={ _t('Delete') } onClick={ self.onAliasDeleted.bind(self, localDomain, i) } />
);
}
return (
<div className="mx_RoomSettings_aliasesTableRow" key={ i }>
<EditableText
className="mx_RoomSettings_alias mx_RoomSettings_editable"
placeholderClassName="mx_RoomSettings_aliasPlaceholder"
placeholder={ _t('New address (e.g. #foo:%(localDomain)s)', { localDomain: localDomain}) }
blurToCancel={ false }
onValueChanged={ self.onAliasChanged.bind(self, localDomain, i) }
editable={ self.props.canSetAliases }
initialValue={ alias } />
<div className="mx_RoomSettings_deleteAlias mx_filterFlipColor">
{ deleteButton }
</div>
</div>
);
})}
{ this.props.canSetAliases ?
<div className="mx_RoomSettings_aliasesTableRow" key="new">
<EditableText
ref="add_alias"
className="mx_RoomSettings_alias mx_RoomSettings_editable"
placeholderClassName="mx_RoomSettings_aliasPlaceholder"
placeholder={ _t('New address (e.g. #foo:%(localDomain)s)', { localDomain: localDomain}) }
blurToCancel={ false }
onValueChanged={ self.onAliasAdded } />
<div className="mx_RoomSettings_addAlias mx_filterFlipColor">
<img src="img/plus.svg" width="14" height="14" alt="Add"
onClick={ self.onAliasAdded.bind(self, undefined) }/>
</div>
</div> : ""
}
</div>
<EditableItemList
className={"mx_RoomSettings_localAliases"}
items={this.state.domainToAliases[localDomain] || []}
newItem={this.state.newAlias}
onNewItemChanged={this.onNewAliasChanged}
onItemAdded={this.onLocalAliasAdded}
onItemEdited={this.onLocalAliasChanged}
onItemRemoved={this.onLocalAliasDeleted}
itemsLabel={_t('Local addresses for this room:')}
noItemsLabel={_t('This room has no local addresses')}
placeholder={_t(
'New address (e.g. #foo:%(localDomain)s)', {localDomain: localDomain},
)}
/>
{ remote_aliases_section }
</div>
);
}
},
});

View File

@ -0,0 +1,125 @@
/*
Copyright 2017 New Vector Ltd.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import {MatrixEvent, MatrixClient} from 'matrix-js-sdk';
import sdk from '../../../index';
import { _t } from '../../../languageHandler';
import Modal from '../../../Modal';
const GROUP_ID_REGEX = /\+\S+\:\S+/;
module.exports = React.createClass({
displayName: 'RelatedGroupSettings',
propTypes: {
roomId: React.PropTypes.string.isRequired,
canSetRelatedRooms: React.PropTypes.bool.isRequired,
relatedGroupsEvent: React.PropTypes.instanceOf(MatrixEvent),
},
contextTypes: {
matrixClient: React.PropTypes.instanceOf(MatrixClient),
},
getDefaultProps: function() {
return {
canSetRelatedRooms: false,
};
},
getInitialState: function() {
return {
newGroupsList: this.props.relatedGroupsEvent ?
(this.props.relatedGroupsEvent.getContent().groups || []) : [],
newGroupId: null,
};
},
saveSettings: function() {
return this.context.matrixClient.sendStateEvent(
this.props.roomId,
'm.room.related_groups',
{
groups: this.state.newGroupsList,
},
'',
);
},
validateGroupId: function(groupId) {
if (!GROUP_ID_REGEX.test(groupId)) {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Invalid related group ID', '', ErrorDialog, {
title: _t('Invalid group ID'),
description: _t('\'%(groupId)s\' is not a valid group ID', { groupId }),
});
return false;
}
return true;
},
onNewGroupChanged: function(newGroupId) {
this.setState({ newGroupId });
},
onGroupAdded: function(groupId) {
if (groupId.length === 0 || !this.validateGroupId(groupId)) {
return;
}
this.setState({
newGroupsList: this.state.newGroupsList.concat([groupId]),
newGroupId: '',
});
},
onGroupEdited: function(groupId, index) {
if (groupId.length === 0 || !this.validateGroupId(groupId)) {
return;
}
this.setState({
newGroupsList: Object.assign(this.state.newGroupsList, {[index]: groupId}),
});
},
onGroupDeleted: function(index) {
const newGroupsList = this.state.newGroupsList.slice();
newGroupsList.splice(index, 1),
this.setState({ newGroupsList });
},
render: function() {
const localDomain = this.context.matrixClient.getDomain();
const EditableItemList = sdk.getComponent('elements.EditableItemList');
return (<div>
<h3>{ _t('Related Groups') }</h3>
<EditableItemList
items={this.state.newGroupsList}
className={"mx_RelatedGroupSettings"}
newItem={this.state.newGroupId}
onNewItemChanged={this.onNewGroupChanged}
onItemAdded={this.onGroupAdded}
onItemEdited={this.onGroupEdited}
onItemRemoved={this.onGroupDeleted}
itemsLabel={_t('Related groups for this room:')}
noItemsLabel={_t('This room has no related groups')}
placeholder={_t(
'New group ID (e.g. +foo:%(localDomain)s)', {localDomain},
)}
/>
</div>);
},
});

View File

@ -372,7 +372,7 @@ module.exports = React.createClass({
},
_getChildrenInvited: function(start, end) {
return this._makeMemberTiles(this.state.filteredInvitedMembers.slice(start, end));
return this._makeMemberTiles(this.state.filteredInvitedMembers.slice(start, end), 'invite');
},
_getChildCountInvited: function() {

View File

@ -30,7 +30,7 @@ import SlashCommands from '../../../SlashCommands';
import KeyCode from '../../../KeyCode';
import Modal from '../../../Modal';
import sdk from '../../../index';
import { _t } from '../../../languageHandler';
import { _t, _td } from '../../../languageHandler';
import Analytics from '../../../Analytics';
import dis from '../../../dispatcher';
@ -1032,10 +1032,10 @@ export default class MessageComposerInput extends React.Component {
buttons. */
getSelectionInfo(editorState: EditorState) {
const styleName = {
BOLD: 'bold',
ITALIC: 'italic',
STRIKETHROUGH: 'strike',
UNDERLINE: 'underline',
BOLD: _td('bold'),
ITALIC: _td('italic'),
STRIKETHROUGH: _td('strike'),
UNDERLINE: _td('underline'),
};
const originalStyle = editorState.getCurrentInlineStyle().toArray();
@ -1044,10 +1044,10 @@ export default class MessageComposerInput extends React.Component {
.filter((styleName) => !!styleName);
const blockName = {
'code-block': 'code',
'blockquote': 'quote',
'unordered-list-item': 'bullet',
'ordered-list-item': 'numbullet',
'code-block': _td('code'),
'blockquote': _td('quote'),
'unordered-list-item': _td('bullet'),
'ordered-list-item': _td('numbullet'),
};
const originalBlockType = editorState.getCurrentContent()
.getBlockForKey(editorState.getSelection().getStartKey())

View File

@ -70,7 +70,7 @@ module.exports = React.createClass({
if (presence === "online") return _t("Online");
if (presence === "unavailable") return _t("Idle"); // XXX: is this actually right?
if (presence === "offline") return _t("Offline");
return "Unknown";
return _t("Unknown");
},
render: function() {

View File

@ -312,6 +312,9 @@ module.exports = React.createClass({
promises.push(ps);
}
// related groups
promises.push(this.saveRelatedGroups());
// encryption
p = this.saveEnableEncryption();
if (!p.isFulfilled()) {
@ -329,6 +332,11 @@ module.exports = React.createClass({
return this.refs.alias_settings.saveSettings();
},
saveRelatedGroups: function() {
if (!this.refs.related_groups) { return Promise.resolve(); }
return this.refs.related_groups.saveSettings();
},
saveColor: function() {
if (!this.refs.color_settings) { return Promise.resolve(); }
return this.refs.color_settings.saveSettings();
@ -417,11 +425,12 @@ module.exports = React.createClass({
_yankValueFromEvent: function(stateEventType, keyName, defaultValue) {
// E.g.("m.room.name","name") would yank the "name" content key from "m.room.name"
var event = this.props.room.currentState.getStateEvents(stateEventType, '');
const event = this.props.room.currentState.getStateEvents(stateEventType, '');
if (!event) {
return defaultValue;
}
return event.getContent()[keyName] || defaultValue;
const content = event.getContent();
return keyName in content ? content[keyName] : defaultValue;
},
_onHistoryRadioToggle: function(ev) {
@ -628,6 +637,7 @@ module.exports = React.createClass({
var AliasSettings = sdk.getComponent("room_settings.AliasSettings");
var ColorSettings = sdk.getComponent("room_settings.ColorSettings");
var UrlPreviewSettings = sdk.getComponent("room_settings.UrlPreviewSettings");
var RelatedGroupSettings = sdk.getComponent("room_settings.RelatedGroupSettings");
var EditableText = sdk.getComponent('elements.EditableText');
var PowerSelector = sdk.getComponent('elements.PowerSelector');
var Loader = sdk.getComponent("elements.Spinner");
@ -662,6 +672,14 @@ module.exports = React.createClass({
var self = this;
let relatedGroupsSection;
if (UserSettingsStore.isFeatureEnabled('feature_groups')) {
relatedGroupsSection = <RelatedGroupSettings ref="related_groups"
roomId={this.props.room.roomId}
canSetRelatedGroups={roomState.mayClientSendStateEvent("m.room.related_groups", cli)}
relatedGroupsEvent={this.props.room.currentState.getStateEvents('m.room.related_groups', '')} />;
}
var userLevelsSection;
if (Object.keys(user_levels).length) {
userLevelsSection =
@ -701,7 +719,7 @@ module.exports = React.createClass({
}
var unfederatableSection;
if (this._yankValueFromEvent("m.room.create", "m.federate") === false) {
if (this._yankValueFromEvent("m.room.create", "m.federate", true) === false) {
unfederatableSection = (
<div className="mx_RoomSettings_powerLevel">
{ _t('This room is not accessible by remote Matrix servers') }.
@ -886,6 +904,8 @@ module.exports = React.createClass({
canonicalAliasEvent={this.props.room.currentState.getStateEvents('m.room.canonical_alias', '')}
aliasEvents={this.props.room.currentState.getStateEvents('m.room.aliases')} />
{ relatedGroupsSection }
<UrlPreviewSettings ref="url_preview_settings" room={this.props.room} />
<h3>{ _t('Permissions') }</h3>

View File

@ -24,8 +24,7 @@ export const GroupMemberType = PropTypes.shape({
export const GroupRoomType = PropTypes.shape({
name: PropTypes.string,
// TODO: API doesn't return this yet
// roomId: PropTypes.string.isRequired,
roomId: PropTypes.string.isRequired,
canonicalAlias: PropTypes.string,
avatarUrl: PropTypes.string,
});
@ -41,6 +40,7 @@ export function groupMemberFromApiObject(apiObject) {
export function groupRoomFromApiObject(apiObject) {
return {
name: apiObject.name,
roomId: apiObject.room_id,
canonicalAlias: apiObject.canonical_alias,
avatarUrl: apiObject.avatar_url,
};

View File

@ -146,7 +146,7 @@
"Members only": "Nur Mitglieder",
"Mobile phone number": "Mobiltelefonnummer",
"Moderator": "Moderator",
"my Matrix ID": "meiner Matrix-ID",
"%(serverName)s Matrix ID": "%(serverName)s Matrix-ID",
"Never send encrypted messages to unverified devices from this device": "Niemals verschlüsselte Nachrichten an unverifizierte Geräte von diesem Gerät aus versenden",
"Never send encrypted messages to unverified devices in this room from this device": "Niemals verschlüsselte Nachrichten an unverifizierte Geräte in diesem Raum von diesem Gerät aus senden",
"New password": "Neues Passwort",

View File

@ -188,7 +188,6 @@
"Failure to create room": "Δεν ήταν δυνατή η δημιουργία δωματίου",
"Join Room": "Είσοδος σε δωμάτιο",
"Moderator": "Συντονιστής",
"my Matrix ID": "το Matrix ID μου",
"Name": "Όνομα",
"New address (e.g. #foo:%(localDomain)s)": "Νέα διεύθυνση (e.g. #όνομα:%(localDomain)s)",
"New password": "Νέος κωδικός πρόσβασης",

View File

@ -26,6 +26,7 @@
"Microphone": "Microphone",
"Camera": "Camera",
"Advanced": "Advanced",
"Advanced options": "Advanced options",
"Algorithm": "Algorithm",
"Hide removed messages": "Hide removed messages",
"Always show message timestamps": "Always show message timestamps",
@ -58,6 +59,8 @@
"Banned users": "Banned users",
"Bans user with given id": "Bans user with given id",
"Blacklisted": "Blacklisted",
"Block users on other matrix homeservers from joining this room": "Block users on other matrix homeservers from joining this room",
"This setting cannot be changed later!": "This setting cannot be changed later!",
"Bug Report": "Bug Report",
"Bulk Options": "Bulk Options",
"Call Timeout": "Call Timeout",
@ -295,7 +298,6 @@
"Moderator": "Moderator",
"Must be viewing a room": "Must be viewing a room",
"Mute": "Mute",
"my Matrix ID": "my Matrix ID",
"Name": "Name",
"Never send encrypted messages to unverified devices from this device": "Never send encrypted messages to unverified devices from this device",
"Never send encrypted messages to unverified devices in this room": "Never send encrypted messages to unverified devices in this room",
@ -888,6 +890,9 @@
"The room '%(roomName)s' could not be removed from the summary.": "The room '%(roomName)s' could not be removed from the summary.",
"Failed to remove a user from the summary of %(groupId)s": "Failed to remove a user from the summary of %(groupId)s",
"The user '%(displayName)s' could not be removed from the summary.": "The user '%(displayName)s' could not be removed from the summary.",
"Light theme": "Light theme",
"Dark theme": "Dark theme",
"Unknown": "Unknown",
"Failed to add the following rooms to the summary of %(groupId)s:": "Failed to add the following rooms to the summary of %(groupId)s:",
"The room '%(roomName)s' could not be removed from the summary.": "The room '%(roomName)s' could not be removed from the summary.",
"Add rooms to the group": "Add rooms to the group",
@ -902,5 +907,14 @@
"Matrix Room ID": "Matrix Room ID",
"email address": "email address",
"Try using one of the following valid address types: %(validTypesList)s.": "Try using one of the following valid address types: %(validTypesList)s.",
"You have entered an invalid address.": "You have entered an invalid address."
"You have entered an invalid address.": "You have entered an invalid address.",
"Failed to remove room from group": "Failed to remove room from group",
"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?",
"Removing a room from the group will also remove it from the group page.": "Removing a room from the group will also remove it from the group page.",
"Related Groups": "Related Groups",
"Related groups for this room:": "Related groups for this room:",
"This room has no related groups": "This room has no related groups",
"New group ID (e.g. +foo:%(localDomain)s)": "New group ID (e.g. +foo:%(localDomain)s)",
"%(serverName)s Matrix ID": "%(serverName)s Matrix ID"
}

View File

@ -262,7 +262,6 @@
"Moderator": "Moderator",
"Must be viewing a room": "Must be viewing a room",
"Mute": "Mute",
"my Matrix ID": "my Matrix ID",
"Name": "Name",
"Never send encrypted messages to unverified devices from this device": "Never send encrypted messages to unverified devices from this device",
"Never send encrypted messages to unverified devices in this room": "Never send encrypted messages to unverified devices in this room",

View File

@ -379,7 +379,7 @@
"Moderator": "Moderador",
"Must be viewing a room": "Debe estar viendo una sala",
"Mute": "Silenciar",
"my Matrix ID": "Mi ID de Matrix",
"%(serverName)s Matrix ID": "%(serverName)s ID de Matrix",
"Name": "Nombre",
"Never send encrypted messages to unverified devices from this device": "No enviar nunca mensajes cifrados, desde este dispositivo, a dispositivos sin verificar",
"Never send encrypted messages to unverified devices in this room": "No enviar nunca mensajes cifrados a dispositivos no verificados, en esta sala",

View File

@ -346,7 +346,6 @@
"Missing room_id in request": "Gelaren ID-a falta da eskaeran",
"Missing user_id in request": "Erabiltzailearen ID-a falta da eskaeran",
"Mobile phone number": "Mugikorraren telefono zenbakia",
"my Matrix ID": "Nire Matrix ID-a",
"Never send encrypted messages to unverified devices in this room": "Ez bidali inoiz zifratutako mezuak egiaztatu gabeko gailuetara gela honetan",
"Never send encrypted messages to unverified devices in this room from this device": "Ez bidali inoiz zifratutako mezuak egiaztatu gabeko gailuetara gela honetan gailu honetatik",
"New address (e.g. #foo:%(localDomain)s)": "Helbide berria (adib. #foo:%(localDomain)s)",

View File

@ -245,7 +245,7 @@
"Mobile phone number": "Matkapuhelinnumero",
"Mobile phone number (optional)": "Matkapuhelinnumero (valinnainen)",
"Moderator": "Moderaattori",
"my Matrix ID": "minun Matrix tunniste",
"%(serverName)s Matrix ID": "%(serverName)s Matrix tunniste",
"Name": "Nimi",
"New password": "Uusi salasana",
"New passwords don't match": "Uudet salasanat eivät täsmää",

View File

@ -224,7 +224,7 @@
"Mobile phone number": "Numéro de téléphone mobile",
"Moderator": "Modérateur",
"Must be viewing a room": "Doit être en train de visualiser un salon",
"my Matrix ID": "mon identifiant Matrix",
"%(serverName)s Matrix ID": "%(serverName)s identifiant Matrix",
"Name": "Nom",
"Never send encrypted messages to unverified devices from this device": "Ne jamais envoyer de message chiffré aux appareils non-vérifiés depuis cet appareil",
"Never send encrypted messages to unverified devices in this room": "Ne jamais envoyer de message chiffré aux appareils non-vérifiés dans ce salon",

View File

@ -293,7 +293,7 @@
"Mobile phone number (optional)": "Mobill telefonszám (opcionális)",
"Moderator": "Moderátor",
"Must be viewing a room": "Meg kell nézni a szobát",
"my Matrix ID": "Matrix azonosítóm",
"%(serverName)s Matrix ID": "%(serverName)s Matrix azonosítóm",
"Name": "Név",
"Never send encrypted messages to unverified devices from this device": "Soha ne küldj titkosított üzenetet ellenőrizetlen eszközre erről az eszközről",
"Never send encrypted messages to unverified devices in this room": "Soha ne küldj titkosított üzenetet ebből a szobából ellenőrizetlen eszközre",

View File

@ -78,7 +78,6 @@
"Members only": "Hanya anggota",
"Mobile phone number": "Nomor telpon seluler",
"Mute": "Bisu",
"my Matrix ID": "ID Matrix saya",
"Name": "Nama",
"New password": "Password Baru",
"New passwords don't match": "Password baru tidak cocok",

View File

@ -293,7 +293,6 @@
"Mobile phone number (optional)": "휴대 전화번호 (선택)",
"Moderator": "조정자",
"Must be viewing a room": "방을 둘러봐야만 해요",
"my Matrix ID": "내 매트릭스 ID",
"Name": "이름",
"Never send encrypted messages to unverified devices from this device": "이 장치에서 인증받지 않은 장치로 암호화한 메시지를 보내지 마세요",
"Never send encrypted messages to unverified devices in this room": "이 방에서 인증받지 않은 장치로 암호화한 메시지를 보내지 마세요",

View File

@ -274,7 +274,7 @@
"Moderator": "Moderators",
"Must be viewing a room": "Jāapskata istaba",
"Mute": "Apklusināt",
"my Matrix ID": "mans Matrix ID",
"%(serverName)s Matrix ID": "%(serverName)s Matrix ID",
"Name": "Vārds",
"Never send encrypted messages to unverified devices from this device": "Nekad nesūti no šīs ierīces šifrētas ziņas uz neverificētām ierīcēm",
"Never send encrypted messages to unverified devices in this room": "Nekad nesūti šifrētas ziņas uz neverificētām ierīcēm šajā istabā",

View File

@ -126,7 +126,7 @@
"disabled": "uitgeschakeld",
"Moderator": "Moderator",
"Must be viewing a room": "Moet een ruimte weergeven",
"my Matrix ID": "mijn Matrix-ID",
"%(serverName)s Matrix ID": "%(serverName)s Matrix-ID",
"Name": "Naam",
"New password": "Nieuw wachtwoord",
"none": "geen",

View File

@ -363,7 +363,7 @@
"Mobile phone number": "Numer telefonu komórkowego",
"Mobile phone number (optional)": "Numer telefonu komórkowego (opcjonalne)",
"Moderator": "Moderator",
"my Matrix ID": "mój Matrix ID",
"%(serverName)s Matrix ID": "%(serverName)s Matrix ID",
"Name": "Imię",
"Never send encrypted messages to unverified devices from this device": "Nigdy nie wysyłaj zaszyfrowanych wiadomości do niezweryfikowanych urządzeń z tego urządzenia",
"Never send encrypted messages to unverified devices in this room": "Nigdy nie wysyłaj zaszyfrowanych wiadomości do niezweryfikowanych urządzeń w tym pokoju",

View File

@ -127,7 +127,6 @@
"Members only": "Apenas integrantes da sala",
"Mobile phone number": "Telefone celular",
"Moderator": "Moderador/a",
"my Matrix ID": "com meu ID do Matrix",
"Name": "Nome",
"Never send encrypted messages to unverified devices from this device": "Nunca envie mensagens criptografada para um dispositivo não verificado a partir deste dispositivo",
"Never send encrypted messages to unverified devices in this room from this device": "Nunca envie mensagens criptografadas para dispositivos não verificados nesta sala a partir deste dispositivo",

View File

@ -127,7 +127,6 @@
"Members only": "Apenas integrantes da sala",
"Mobile phone number": "Telefone celular",
"Moderator": "Moderador/a",
"my Matrix ID": "com meu ID do Matrix",
"Name": "Nome",
"Never send encrypted messages to unverified devices from this device": "Nunca envie mensagens criptografada para um dispositivo não verificado a partir deste dispositivo",
"Never send encrypted messages to unverified devices in this room from this device": "Nunca envie mensagens criptografadas para dispositivos não verificados nesta sala a partir deste dispositivo",

View File

@ -117,7 +117,7 @@
"Members only": "Только участники",
"Mobile phone number": "Номер мобильного телефона",
"Moderator": "Модератор",
"my Matrix ID": "мой Matrix ID",
"%(serverName)s Matrix ID": "%(serverName)s Matrix ID",
"Name": "Имя",
"Never send encrypted messages to unverified devices from this device": "Никогда не отправлять зашифрованные сообщения на непроверенные устройства с этого устройства",
"Never send encrypted messages to unverified devices in this room from this device": "Никогда не отправлять зашифрованные сообщения на непроверенные устройства в этой комнате с этого устройства",

View File

@ -273,7 +273,7 @@
"Moderator": "Moderator",
"Must be viewing a room": "Du måste ha ett öppet rum",
"Mute": "Dämpa",
"my Matrix ID": "mitt Matrix-ID",
"%(serverName)s Matrix ID": "%(serverName)s Matrix-ID",
"Name": "Namn",
"Never send encrypted messages to unverified devices from this device": "Skicka aldrig krypterade meddelanden till overifierade enheter från den här enheten",
"Never send encrypted messages to unverified devices in this room": "Skicka aldrig krypterade meddelanden till overifierade enheter i det här rummet",

View File

@ -219,7 +219,6 @@
"Markdown is enabled": "เปิดใช้งาน Markdown แล้ว",
"Missing user_id in request": "ไม่พบ user_id ในคำขอ",
"Moderator": "ผู้ช่วยดูแล",
"my Matrix ID": "Matrix ID ของฉัน",
"New address (e.g. #foo:%(localDomain)s)": "ที่อยู่ใหม่ (เช่น #foo:%(localDomain)s)",
"New password": "รหัสผ่านใหม่",
"New passwords don't match": "รหัสผ่านใหม่ไม่ตรงกัน",

View File

@ -269,7 +269,6 @@
"Moderator": "Moderatör",
"Must be viewing a room": "Bir oda görüntülemeli olmalı",
"Mute": "Sessiz",
"my Matrix ID": "Benim Matrix ID'm",
"Name": "İsim",
"Never send encrypted messages to unverified devices from this device": "Bu cihazdan doğrulanmamış cihazlara asla şifrelenmiş mesajlar göndermeyin",
"Never send encrypted messages to unverified devices in this room": "Bu odada doğrulanmamış cihazlara asla şifreli mesajlar göndermeyin",

View File

@ -289,7 +289,6 @@
"Mobile phone number (optional)": "手机号码 (可选)",
"Moderator": "管理员",
"Mute": "静音",
"my Matrix ID": "我的 Matrix ID",
"Name": "姓名",
"Never send encrypted messages to unverified devices from this device": "不要从此设备向未验证的设备发送消息",
"Never send encrypted messages to unverified devices in this room": "不要在此聊天室里向未验证的设备发送消息",

View File

@ -403,7 +403,6 @@
"Mobile phone number (optional)": "行動電話號碼(選擇性)",
"Moderator": "仲裁者",
"Must be viewing a room": "必須檢視房間",
"my Matrix ID": "我的 Matrix ID",
"Name": "名稱",
"Never send encrypted messages to unverified devices from this device": "從不自此裝置傳送加密的訊息到未驗證的裝置",
"Never send encrypted messages to unverified devices in this room": "從不在此房間傳送加密的訊息到未驗證的裝置",

View File

@ -29,6 +29,12 @@ counterpart.setSeparator('|');
// Fall back to English
counterpart.setFallbackLocale('en');
// Function which only purpose is to mark that a string is translatable
// Does not actually do anything. It's helpful for automatic extraction of translatable strings
export function _td(s) {
return s;
}
// The translation function. This is just a simple wrapper to counterpart,
// but exists mostly because we must use the same counterpart instance
// between modules (ie. here (react-sdk) and the app (riot-web), and if we

View File

@ -17,19 +17,22 @@ limitations under the License.
import EventEmitter from 'events';
/**
* Stores the group summary for a room and provides an API to change it
* Stores the group summary for a room and provides an API to change it and
* other useful group APIs that may have an effect on the group summary.
*/
export default class GroupSummaryStore extends EventEmitter {
export default class GroupStore extends EventEmitter {
constructor(matrixClient, groupId) {
super();
this._groupId = groupId;
this.groupId = groupId;
this._matrixClient = matrixClient;
this._summary = {};
this._rooms = [];
this._fetchSummary();
this._fetchRooms();
}
_fetchSummary() {
this._matrixClient.getGroupSummary(this._groupId).then((resp) => {
this._matrixClient.getGroupSummary(this.groupId).then((resp) => {
this._summary = resp;
this._notifyListeners();
}).catch((err) => {
@ -37,6 +40,15 @@ export default class GroupSummaryStore extends EventEmitter {
});
}
_fetchRooms() {
this._matrixClient.getGroupRooms(this.groupId).then((resp) => {
this._rooms = resp.chunk;
this._notifyListeners();
}).catch((err) => {
this.emit('error', err);
});
}
_notifyListeners() {
this.emit('update');
}
@ -45,33 +57,51 @@ export default class GroupSummaryStore extends EventEmitter {
return this._summary;
}
getGroupRooms() {
return this._rooms;
}
addRoomToGroup(roomId) {
return this._matrixClient
.addRoomToGroup(this.groupId, roomId)
.then(this._fetchRooms.bind(this));
}
removeRoomFromGroup(roomId) {
return this._matrixClient
.removeRoomFromGroup(this.groupId, roomId)
// Room might be in the summary, refresh just in case
.then(this._fetchSummary.bind(this))
.then(this._fetchRooms.bind(this));
}
addRoomToGroupSummary(roomId, categoryId) {
return this._matrixClient
.addRoomToGroupSummary(this._groupId, roomId, categoryId)
.addRoomToGroupSummary(this.groupId, roomId, categoryId)
.then(this._fetchSummary.bind(this));
}
addUserToGroupSummary(userId, roleId) {
return this._matrixClient
.addUserToGroupSummary(this._groupId, userId, roleId)
.addUserToGroupSummary(this.groupId, userId, roleId)
.then(this._fetchSummary.bind(this));
}
removeRoomFromGroupSummary(roomId) {
return this._matrixClient
.removeRoomFromGroupSummary(this._groupId, roomId)
.removeRoomFromGroupSummary(this.groupId, roomId)
.then(this._fetchSummary.bind(this));
}
removeUserFromGroupSummary(userId) {
return this._matrixClient
.removeUserFromGroupSummary(this._groupId, userId)
.removeUserFromGroupSummary(this.groupId, userId)
.then(this._fetchSummary.bind(this));
}
setGroupPublicity(isPublished) {
return this._matrixClient
.setGroupPublicity(this._groupId, isPublished)
.setGroupPublicity(this.groupId, isPublished)
.then(this._fetchSummary.bind(this));
}
}

View File

@ -0,0 +1,40 @@
/*
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 GroupStore from './GroupStore';
class GroupStoreCache {
constructor() {
this.groupStore = null;
}
getGroupStore(matrixClient, groupId) {
if (!this.groupStore || this.groupStore.groupId !== groupId) {
// This effectively throws away the reference to any previous GroupStore,
// allowing it to be GCd once the components referencing it have stopped
// referencing it.
this.groupStore = new GroupStore(matrixClient, groupId);
}
this.groupStore._fetchSummary();
return this.groupStore;
}
}
let singletonGroupStoreCache = null;
if (!singletonGroupStoreCache) {
singletonGroupStoreCache = new GroupStoreCache();
}
module.exports = singletonGroupStoreCache;