:
;
+ return (
+
+ ?
+ { tip }
+
+ );
+ },
+});
diff --git a/src/components/views/groups/GroupMemberTile.js b/src/components/views/groups/GroupMemberTile.js
index f40c7ed1c5..f967a33f46 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/groups/GroupRoomList.js b/src/components/views/groups/GroupRoomList.js
index aeded2dfb0..ff27d093b2 100644
--- a/src/components/views/groups/GroupRoomList.js
+++ b/src/components/views/groups/GroupRoomList.js
@@ -94,7 +94,7 @@ export default React.createClass({
let roomList = this.state.rooms;
if (query) {
roomList = roomList.filter((room) => {
- const matchesName = (room.name || "").toLowerCase().include(query);
+ const matchesName = (room.name || "").toLowerCase().includes(query);
const matchesAlias = (room.canonicalAlias || "").toLowerCase().includes(query);
return matchesName || matchesAlias;
});
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/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 43f3aa5d88..aa019de091 100644
--- a/src/components/views/rooms/MessageComposerInput.js
+++ b/src/components/views/rooms/MessageComposerInput.js
@@ -58,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(
@@ -188,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,
);
@@ -210,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;
@@ -989,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;
diff --git a/src/components/views/rooms/PinnedEventsPanel.js b/src/components/views/rooms/PinnedEventsPanel.js
index 5b1b8a4590..a8fb35fcd5 100644
--- a/src/components/views/rooms/PinnedEventsPanel.js
+++ b/src/components/views/rooms/PinnedEventsPanel.js
@@ -19,6 +19,7 @@ import MatrixClientPeg from "../../../MatrixClientPeg";
import AccessibleButton from "../elements/AccessibleButton";
import PinnedEventTile from "./PinnedEventTile";
import { _t } from '../../../languageHandler';
+import PinningUtils from "../../../utils/PinningUtils";
module.exports = React.createClass({
displayName: 'PinnedEventsPanel',
@@ -61,20 +62,39 @@ module.exports = React.createClass({
Promise.all(promises).then((contexts) => {
// Filter out the messages before we try to render them
- const pinned = contexts.filter((context) => {
- if (!context) return false; // no context == not applicable for the room
- if (context.event.getType() !== "m.room.message") return false;
- if (context.event.isRedacted()) return false;
- return true;
- });
+ const pinned = contexts.filter((context) => PinningUtils.isPinnable(context.event));
this.setState({ loading: false, pinned });
});
}
+
+ this._updateReadState();
+ },
+
+ _updateReadState: function() {
+ const pinnedEvents = this.props.room.currentState.getStateEvents("m.room.pinned_events", "");
+ if (!pinnedEvents) return; // nothing to read
+
+ let readStateEvents = [];
+ const readPinsEvent = this.props.room.getAccountData("im.vector.room.read_pins");
+ if (readPinsEvent && readPinsEvent.getContent()) {
+ readStateEvents = readPinsEvent.getContent().event_ids || [];
+ }
+
+ if (!readStateEvents.includes(pinnedEvents.getId())) {
+ readStateEvents.push(pinnedEvents.getId());
+
+ // Only keep the last 10 event IDs to avoid infinite growth
+ readStateEvents = readStateEvents.reverse().splice(0, 10).reverse();
+
+ MatrixClientPeg.get().setRoomAccountData(this.props.room.roomId, "im.vector.room.read_pins", {
+ event_ids: readStateEvents,
+ });
+ }
},
_getPinnedTiles: function() {
- if (this.state.pinned.length == 0) {
+ if (this.state.pinned.length === 0) {
return ({ _t("No pinned messages.") }
);
}
diff --git a/src/components/views/rooms/RoomDetailList.js b/src/components/views/rooms/RoomDetailList.js
index c44b662fa7..27972af484 100644
--- a/src/components/views/rooms/RoomDetailList.js
+++ b/src/components/views/rooms/RoomDetailList.js
@@ -23,6 +23,7 @@ import sanitizeHtml from 'sanitize-html';
import { ContentRepo } from 'matrix-js-sdk';
import MatrixClientPeg from '../../../MatrixClientPeg';
import PropTypes from 'prop-types';
+import classNames from 'classnames';
function getDisplayAliasForRoom(room) {
return room.canonicalAlias || (room.aliases ? room.aliases[0] : "");
@@ -117,6 +118,8 @@ export default React.createClass({
worldReadable: PropTypes.bool,
guestCanJoin: PropTypes.bool,
})),
+
+ className: PropTypes.string,
},
getRows: function() {
@@ -138,7 +141,7 @@ export default React.createClass({
;
}
- return
+ return
{ rooms }
;
},
diff --git a/src/components/views/rooms/RoomHeader.js b/src/components/views/rooms/RoomHeader.js
index 4dfbdb3644..f558f44b4e 100644
--- a/src/components/views/rooms/RoomHeader.js
+++ b/src/components/views/rooms/RoomHeader.js
@@ -65,6 +65,7 @@ module.exports = React.createClass({
componentDidMount: function() {
const cli = MatrixClientPeg.get();
cli.on("RoomState.events", this._onRoomStateEvents);
+ cli.on("Room.accountData", this._onRoomAccountData);
// When a room name occurs, RoomState.events is fired *before*
// room.name is updated. So we have to listen to Room.name as well as
@@ -87,6 +88,7 @@ module.exports = React.createClass({
const cli = MatrixClientPeg.get();
if (cli) {
cli.removeListener("RoomState.events", this._onRoomStateEvents);
+ cli.removeListener("Room.accountData", this._onRoomAccountData);
}
},
@@ -99,6 +101,13 @@ module.exports = React.createClass({
this._rateLimitedUpdate();
},
+ _onRoomAccountData: function(event, room) {
+ if (!this.props.room || room.roomId !== this.props.room.roomId) return;
+ if (event.getType() !== "im.vector.room.read_pins") return;
+
+ this._rateLimitedUpdate();
+ },
+
_rateLimitedUpdate: new RateLimitedFunc(function() {
/* eslint-disable babel/no-invalid-this */
this.forceUpdate();
@@ -139,6 +148,32 @@ module.exports = React.createClass({
dis.dispatch({ action: 'show_right_panel' });
},
+ _hasUnreadPins: function() {
+ const currentPinEvent = this.props.room.currentState.getStateEvents("m.room.pinned_events", '');
+ if (!currentPinEvent) return false;
+ if (currentPinEvent.getContent().pinned && currentPinEvent.getContent().pinned.length <= 0) {
+ return false; // no pins == nothing to read
+ }
+
+ const readPinsEvent = this.props.room.getAccountData("im.vector.room.read_pins");
+ if (readPinsEvent && readPinsEvent.getContent()) {
+ const readStateEvents = readPinsEvent.getContent().event_ids || [];
+ if (readStateEvents) {
+ return !readStateEvents.includes(currentPinEvent.getId());
+ }
+ }
+
+ // There's pins, and we haven't read any of them
+ return true;
+ },
+
+ _hasPins: function() {
+ const currentPinEvent = this.props.room.currentState.getStateEvents("m.room.pinned_events", '');
+ if (!currentPinEvent) return false;
+
+ return !(currentPinEvent.getContent().pinned && currentPinEvent.getContent().pinned.length <= 0);
+ },
+
/**
* After editing the settings, get the new name for the room
*
@@ -305,8 +340,17 @@ module.exports = React.createClass({
}
if (this.props.onPinnedClick && UserSettingsStore.isFeatureEnabled('feature_pinning')) {
+ let pinsIndicator = null;
+ if (this._hasUnreadPins()) {
+ pinsIndicator = (
);
+ } else if (this._hasPins()) {
+ pinsIndicator = (
);
+ }
+
pinnedEventsButton =
-
+
+ { pinsIndicator }
;
}
diff --git a/src/groups.js b/src/groups.js
index 6c266e0fb6..860cf71fff 100644
--- a/src/groups.js
+++ b/src/groups.js
@@ -36,6 +36,7 @@ export function groupMemberFromApiObject(apiObject) {
userId: apiObject.user_id,
displayname: apiObject.displayname,
avatarUrl: apiObject.avatar_url,
+ isPrivileged: apiObject.is_privileged,
};
}
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index f41d5add54..2f19c4780e 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -49,7 +49,7 @@
"Name or matrix ID": "Name or matrix ID",
"Invite to Community": "Invite to Community",
"Which rooms would you like to add to this community?": "Which rooms would you like to add to this community?",
- "Warning: any room you add to a community will be publicly visible to anyone who knows the community ID": "Warning: any room you add to a community will be publicly visible to anyone who knows the community ID",
+ "Show these rooms to non-members on the community page and room list?": "Show these rooms to non-members on the community page and room list?",
"Add rooms to the community": "Add rooms to the community",
"Room name or alias": "Room name or alias",
"Add to community": "Add to community",
@@ -673,6 +673,7 @@
"You must register to use this functionality": "You must register to use this functionality",
"You must join the room to see its files": "You must join the room to see its files",
"There are no visible files in this room": "There are no visible files in this room",
+ "HTML for your community's page \n\n Use the long description to introduce new members to the community, or distribute\n some important links \n
\n\n You can even use 'img' tags\n
\n": "HTML for your community's page \n\n Use the long description to introduce new members to the community, or distribute\n some important links \n
\n\n You can even use 'img' tags\n
\n",
"Add rooms to the community summary": "Add rooms to the community summary",
"Which rooms would you like to add to this summary?": "Which rooms would you like to add to this summary?",
"Add to summary": "Add to summary",
@@ -695,6 +696,7 @@
"Leave": "Leave",
"Unable to leave room": "Unable to leave room",
"Community Settings": "Community Settings",
+ "These rooms are displayed to community members on the community page. Community members can join the rooms by clicking on them.": "These rooms are displayed to community members on the community page. Community members can join the rooms by clicking on them.",
"Add rooms to this community": "Add rooms to this community",
"Featured Rooms:": "Featured Rooms:",
"Featured Users:": "Featured Users:",
@@ -703,6 +705,7 @@
"You are a member of this community": "You are a member of this community",
"Community Member Settings": "Community Member Settings",
"Publish this community on your profile": "Publish this community on your profile",
+ "Your community hasn't got a Long Description, a HTML page to show to community members. Click here to open settings and give it one!": "Your community hasn't got a Long Description, a HTML page to show to community members. Click here to open settings and give it one!",
"Long Description (HTML)": "Long Description (HTML)",
"Description": "Description",
"Community %(groupId)s not found": "Community %(groupId)s not found",
@@ -893,6 +896,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 1d20148c0d..0f339eaf63 100644
--- a/src/stores/FlairStore.js
+++ b/src/stores/FlairStore.js
@@ -69,9 +69,13 @@ class FlairStore extends EventEmitter {
}
// Bulk lookup ongoing, return promise to resolve/reject
- if (this._usersPending[userId] || this._usersInFlight[userId]) {
+ if (this._usersPending[userId]) {
return this._usersPending[userId].prom;
}
+ // User has been moved from pending to in-flight
+ if (this._usersInFlight[userId]) {
+ return this._usersInFlight[userId].prom;
+ }
this._usersPending[userId] = {};
this._usersPending[userId].prom = new Promise((resolve, reject) => {
diff --git a/src/stores/GroupStore.js b/src/stores/GroupStore.js
index 2578d373a7..2c094a136b 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 = {};
@@ -166,6 +169,12 @@ export default class GroupStore extends EventEmitter {
.then(this._fetchMembers.bind(this));
}
+ acceptGroupInvite() {
+ return this._matrixClient.acceptGroupInvite(this.groupId)
+ // The user might be able to see more rooms now
+ .then(this._fetchRooms.bind(this));
+ }
+
addRoomToGroupSummary(roomId, categoryId) {
return this._matrixClient
.addRoomToGroupSummary(this.groupId, roomId, categoryId)
diff --git a/src/utils/PinningUtils.js b/src/utils/PinningUtils.js
new file mode 100644
index 0000000000..90d26cc988
--- /dev/null
+++ b/src/utils/PinningUtils.js
@@ -0,0 +1,30 @@
+/*
+Copyright 2017 Travis Ralston
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+export default class PinningUtils {
+ /**
+ * Determines if the given event may be pinned.
+ * @param {MatrixEvent} event The event to check.
+ * @return {boolean} True if the event may be pinned, false otherwise.
+ */
+ static isPinnable(event) {
+ if (!event) return false;
+ if (event.getType() !== "m.room.message") return false;
+ if (event.isRedacted()) return false;
+
+ return true;
+ }
+}