+
{ label }
{ badge }
diff --git a/src/components/views/groups/GroupMemberTile.js b/src/components/views/groups/GroupMemberTile.js
index f40c7ed1c5..84c2adcb41 100644
--- a/src/components/views/groups/GroupMemberTile.js
+++ b/src/components/views/groups/GroupMemberTile.js
@@ -61,9 +61,9 @@ export default withMatrixClient(React.createClass({
);
return (
-
);
},
diff --git a/src/components/views/messages/TextualBody.js b/src/components/views/messages/TextualBody.js
index faa4d6cf77..911f2c98d1 100644
--- a/src/components/views/messages/TextualBody.js
+++ b/src/components/views/messages/TextualBody.js
@@ -194,6 +194,9 @@ module.exports = React.createClass({
node.parentNode.replaceChild(pillContainer, node);
// Pills within pills aren't going to go well, so move on
pillified = true;
+
+ // update the current node with one that's now taken its place
+ node = pillContainer;
}
} else if (node.nodeType == Node.TEXT_NODE) {
const Pill = sdk.getComponent('elements.Pill');
diff --git a/src/components/views/rooms/AppsDrawer.js b/src/components/views/rooms/AppsDrawer.js
index 1c9296228d..9a3ba5f329 100644
--- a/src/components/views/rooms/AppsDrawer.js
+++ b/src/components/views/rooms/AppsDrawer.js
@@ -81,16 +81,25 @@ module.exports = React.createClass({
},
onAction: function(action) {
+ const hideWidgetKey = this.props.room.roomId + "_hide_widget_drawer";
switch (action.action) {
case 'appsDrawer':
- // When opening the app draw when there aren't any apps, auto-launch the
- // integrations manager to skip the awkward click on "Add widget"
+ // When opening the app drawer when there aren't any apps,
+ // auto-launch the integrations manager to skip the awkward
+ // click on "Add widget"
if (action.show) {
const apps = this._getApps();
if (apps.length === 0) {
this._launchManageIntegrations();
}
+
+ localStorage.removeItem(hideWidgetKey);
+ } else {
+ // Store hidden state of widget
+ // Don't show if previously hidden
+ localStorage.setItem(hideWidgetKey, true);
}
+
break;
}
},
diff --git a/src/components/views/rooms/Autocomplete.js b/src/components/views/rooms/Autocomplete.js
index ecc908a02c..958d16073c 100644
--- a/src/components/views/rooms/Autocomplete.js
+++ b/src/components/views/rooms/Autocomplete.js
@@ -1,5 +1,23 @@
+/*
+Copyright 2016 Aviral Dasgupta
+Copyright 2017 New Vector Ltd
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
import React from 'react';
import ReactDOM from 'react-dom';
+import PropTypes from 'prop-types';
import classNames from 'classnames';
import flatMap from 'lodash/flatMap';
import isEqual from 'lodash/isEqual';
@@ -7,8 +25,9 @@ import sdk from '../../../index';
import type {Completion} from '../../../autocomplete/Autocompleter';
import Promise from 'bluebird';
import UserSettingsStore from '../../../UserSettingsStore';
+import { Room } from 'matrix-js-sdk';
-import {getCompletions} from '../../../autocomplete/Autocompleter';
+import Autocompleter from '../../../autocomplete/Autocompleter';
const COMPOSER_SELECTED = 0;
@@ -17,6 +36,7 @@ export default class Autocomplete extends React.Component {
constructor(props) {
super(props);
+ this.autocompleter = new Autocompleter(props.room);
this.completionPromise = null;
this.hide = this.hide.bind(this);
this.onCompletionClicked = this.onCompletionClicked.bind(this);
@@ -41,6 +61,11 @@ export default class Autocomplete extends React.Component {
}
componentWillReceiveProps(newProps, state) {
+ if (this.props.room.roomId !== newProps.room.roomId) {
+ this.autocompleter.destroy();
+ this.autocompleter = new Autocompleter(newProps.room);
+ }
+
// Query hasn't changed so don't try to complete it
if (newProps.query === this.props.query) {
return;
@@ -49,6 +74,10 @@ export default class Autocomplete extends React.Component {
this.complete(newProps.query, newProps.selection);
}
+ componentWillUnmount() {
+ this.autocompleter.destroy();
+ }
+
complete(query, selection) {
this.queryRequested = query;
if (this.debounceCompletionsRequest) {
@@ -83,7 +112,7 @@ export default class Autocomplete extends React.Component {
}
processQuery(query, selection) {
- return getCompletions(
+ return this.autocompleter.getCompletions(
query, selection, this.state.forceComplete,
).then((completions) => {
// Only ever process the completions for the most recent query being processed
@@ -267,8 +296,11 @@ export default class Autocomplete extends React.Component {
Autocomplete.propTypes = {
// the query string for which to show autocomplete suggestions
- query: React.PropTypes.string.isRequired,
+ query: PropTypes.string.isRequired,
// method invoked with range and text content when completion is confirmed
- onConfirm: React.PropTypes.func.isRequired,
+ onConfirm: PropTypes.func.isRequired,
+
+ // The room in which we're autocompleting
+ room: PropTypes.instanceOf(Room),
};
diff --git a/src/components/views/rooms/EntityTile.js b/src/components/views/rooms/EntityTile.js
index 1df19340cd..ffcb289437 100644
--- a/src/components/views/rooms/EntityTile.js
+++ b/src/components/views/rooms/EntityTile.js
@@ -47,7 +47,7 @@ function presenceClassForMember(presenceState, lastActiveAgo) {
}
}
-module.exports = React.createClass({
+const EntityTile = React.createClass({
displayName: 'EntityTile',
propTypes: {
@@ -140,16 +140,19 @@ module.exports = React.createClass({
}
let power;
- const powerLevel = this.props.powerLevel;
- if (powerLevel >= 50 && powerLevel < 99) {
- power =
;
- }
- if (powerLevel >= 99) {
- power =
;
+ const powerStatus = this.props.powerStatus;
+ if (powerStatus) {
+ const src = {
+ [EntityTile.POWER_STATUS_MODERATOR]: "img/mod.svg",
+ [EntityTile.POWER_STATUS_ADMIN]: "img/admin.svg",
+ }[powerStatus];
+ const alt = {
+ [EntityTile.POWER_STATUS_MODERATOR]: _t("Moderator"),
+ [EntityTile.POWER_STATUS_ADMIN]: _t("Admin"),
+ }[powerStatus];
+ power =
;
}
-
- const MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
const av = this.props.avatarJsx ||
;
@@ -168,3 +171,9 @@ module.exports = React.createClass({
);
},
});
+
+EntityTile.POWER_STATUS_MODERATOR = "moderator";
+EntityTile.POWER_STATUS_ADMIN = "admin";
+
+
+export default EntityTile;
diff --git a/src/components/views/rooms/MemberTile.js b/src/components/views/rooms/MemberTile.js
index e21f7c91f4..4848c4b258 100644
--- a/src/components/views/rooms/MemberTile.js
+++ b/src/components/views/rooms/MemberTile.js
@@ -86,13 +86,19 @@ module.exports = React.createClass({
}
this.member_last_modified_time = member.getLastModifiedTime();
+ // We deliberately leave power levels that are not 100 or 50 undefined
+ const powerStatus = {
+ 100: EntityTile.POWER_STATUS_ADMIN,
+ 50: EntityTile.POWER_STATUS_MODERATOR,
+ }[this.props.member.powerLevel];
+
return (
+ name={name} powerStatus={powerStatus} />
);
},
});
diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js
index 4850428621..aa019de091 100644
--- a/src/components/views/rooms/MessageComposerInput.js
+++ b/src/components/views/rooms/MessageComposerInput.js
@@ -1,5 +1,6 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
+Copyright 2017 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -57,6 +58,11 @@ const TYPING_USER_TIMEOUT = 10000, TYPING_SERVER_TIMEOUT = 30000;
const ZWS_CODE = 8203;
const ZWS = String.fromCharCode(ZWS_CODE); // zero width space
+
+const ENTITY_TYPES = {
+ AT_ROOM_PILL: 'ATROOMPILL',
+};
+
function stateToMarkdown(state) {
return __stateToMarkdown(state)
.replace(
@@ -187,13 +193,16 @@ export default class MessageComposerInput extends React.Component {
this.client = MatrixClientPeg.get();
}
- findLinkEntities(contentState: ContentState, contentBlock: ContentBlock, callback) {
+ findPillEntities(contentState: ContentState, contentBlock: ContentBlock, callback) {
contentBlock.findEntityRanges(
(character) => {
const entityKey = character.getEntity();
return (
entityKey !== null &&
- contentState.getEntity(entityKey).getType() === 'LINK'
+ (
+ contentState.getEntity(entityKey).getType() === 'LINK' ||
+ contentState.getEntity(entityKey).getType() === ENTITY_TYPES.AT_ROOM_PILL
+ )
);
}, callback,
);
@@ -209,11 +218,19 @@ export default class MessageComposerInput extends React.Component {
RichText.getScopedMDDecorators(this.props);
const shouldShowPillAvatar = !UserSettingsStore.getSyncedSetting("Pill.shouldHidePillAvatar", false);
decorators.push({
- strategy: this.findLinkEntities.bind(this),
+ strategy: this.findPillEntities.bind(this),
component: (entityProps) => {
const Pill = sdk.getComponent('elements.Pill');
+ const type = entityProps.contentState.getEntity(entityProps.entityKey).getType();
const {url} = entityProps.contentState.getEntity(entityProps.entityKey).getData();
- if (Pill.isPillUrl(url)) {
+ if (type === ENTITY_TYPES.AT_ROOM_PILL) {
+ return
;
+ } else if (Pill.isPillUrl(url)) {
return
{
let blockText = block.getText();
let offset = 0;
- this.findLinkEntities(contentState, block, (start, end) => {
+ this.findPillEntities(contentState, block, (start, end) => {
const entity = contentState.getEntity(block.getEntityAt(start));
if (entity.getType() !== 'LINK') {
return;
@@ -988,6 +1005,11 @@ export default class MessageComposerInput extends React.Component {
isCompletion: true,
});
entityKey = contentState.getLastCreatedEntityKey();
+ } else if (completion === '@room') {
+ contentState = contentState.createEntity(ENTITY_TYPES.AT_ROOM_PILL, 'IMMUTABLE', {
+ isCompletion: true,
+ });
+ entityKey = contentState.getLastCreatedEntityKey();
}
let selection;
@@ -1130,10 +1152,12 @@ export default class MessageComposerInput extends React.Component {
this.autocomplete = e}
+ room={this.props.room}
onConfirm={this.setDisplayedCompletion}
onSelectionChange={this.setDisplayedCompletion}
query={this.getAutocompleteQuery(content)}
- selection={selection} />
+ selection={selection}
+ />
+
+
to start a chat with someone": "Press to start a chat with someone",
"You're not in any rooms yet! Press to make a room or to browse the directory": "You're not in any rooms yet! Press to make a room or to browse the directory",
+ "Community Invites": "Community Invites",
"Invites": "Invites",
"Favourites": "Favourites",
"People": "People",
@@ -891,6 +892,8 @@
"Commands": "Commands",
"Results from DuckDuckGo": "Results from DuckDuckGo",
"Emoji": "Emoji",
+ "Notify the whole room": "Notify the whole room",
+ "Room Notification": "Room Notification",
"Users": "Users",
"unknown device": "unknown device",
"NOT verified": "NOT verified",
diff --git a/src/stores/FlairStore.js b/src/stores/FlairStore.js
index 9424503390..1d20148c0d 100644
--- a/src/stores/FlairStore.js
+++ b/src/stores/FlairStore.js
@@ -48,6 +48,9 @@ class FlairStore extends EventEmitter {
// reject: () => {}
// }
};
+ this._usersInFlight = {
+ // This has the same schema as _usersPending
+ };
this._debounceTimeoutID = null;
}
@@ -125,12 +128,16 @@ class FlairStore extends EventEmitter {
} catch (err) {
// Propagate the same error to all usersInFlight
Object.keys(this._usersInFlight).forEach((userId) => {
+ // The promise should always exist for userId, but do a null-check anyway
+ if (!this._usersInFlight[userId]) return;
this._usersInFlight[userId].reject(err);
});
return;
}
const updatedUserGroups = resp.users;
Object.keys(this._usersInFlight).forEach((userId) => {
+ // The promise should always exist for userId, but do a null-check anyway
+ if (!this._usersInFlight[userId]) return;
this._usersInFlight[userId].resolve(updatedUserGroups[userId] || []);
});
}
diff --git a/src/stores/GroupStore.js b/src/stores/GroupStore.js
index 2578d373a7..11dd664053 100644
--- a/src/stores/GroupStore.js
+++ b/src/stores/GroupStore.js
@@ -33,6 +33,9 @@ export default class GroupStore extends EventEmitter {
constructor(matrixClient, groupId) {
super();
+ if (!groupId) {
+ throw new Error('GroupStore needs a valid groupId to be created');
+ }
this.groupId = groupId;
this._matrixClient = matrixClient;
this._summary = {};