Merge remote-tracking branch 'origin/develop' into matthew/status

This commit is contained in:
David Baker 2017-10-27 18:59:57 +01:00
commit 858c3a2548
44 changed files with 576 additions and 317 deletions

View file

@ -232,9 +232,14 @@ for (const path of SEARCH_PATHS) {
const trObj = {}; const trObj = {};
for (const tr of translatables) { for (const tr of translatables) {
trObj[tr] = tr;
if (tr.includes("|")) { if (tr.includes("|")) {
trObj[tr] = inputTranslationsRaw[tr]; if (inputTranslationsRaw[tr]) {
trObj[tr] = inputTranslationsRaw[tr];
} else {
trObj[tr] = tr.split("|")[0];
}
} else {
trObj[tr] = tr;
} }
} }

View file

@ -96,13 +96,6 @@ function _onGroupInviteFinished(groupId, addrs) {
title: _t("Failed to invite the following users to %(groupId)s:", {groupId: groupId}), title: _t("Failed to invite the following users to %(groupId)s:", {groupId: groupId}),
description: errorList.join(", "), description: errorList.join(", "),
}); });
} else {
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
Modal.createTrackedDialog('Group invitations sent', '', QuestionDialog, {
title: _t("Invites sent"),
description: _t("Your community invitations have been sent."),
hasCancelButton: false,
});
} }
}).catch((err) => { }).catch((err) => {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");

View file

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

View file

@ -116,7 +116,6 @@ const FilePanel = React.createClass({
timelineSet={this.state.timelineSet} timelineSet={this.state.timelineSet}
showUrlPreview = {false} showUrlPreview = {false}
tileShape="file_grid" tileShape="file_grid"
opacity={this.props.opacity}
empty={_t('There are no visible files in this room')} empty={_t('There are no visible files in this room')}
/> />
); );

View file

@ -447,7 +447,7 @@ export default React.createClass({
_initGroupStore: function(groupId) { _initGroupStore: function(groupId) {
this._groupStore = GroupStoreCache.getGroupStore(MatrixClientPeg.get(), groupId); this._groupStore = GroupStoreCache.getGroupStore(MatrixClientPeg.get(), groupId);
this._groupStore.on('update', () => { this._groupStore.registerListener(() => {
const summary = this._groupStore.getSummary(); const summary = this._groupStore.getSummary();
if (summary.profile) { if (summary.profile) {
// Default profile fields should be "" for later sending to the server (which // Default profile fields should be "" for later sending to the server (which
@ -464,7 +464,6 @@ export default React.createClass({
}); });
}); });
this._groupStore.on('error', (err) => { this._groupStore.on('error', (err) => {
console.error(err);
this.setState({ this.setState({
summary: null, summary: null,
error: err, error: err,
@ -481,6 +480,10 @@ export default React.createClass({
editing: true, editing: true,
profileForm: Object.assign({}, this.state.summary.profile), profileForm: Object.assign({}, this.state.summary.profile),
}); });
dis.dispatch({
action: 'panel_disable',
sideDisabled: true,
});
}, },
_onCancelClick: function() { _onCancelClick: function() {
@ -488,6 +491,7 @@ export default React.createClass({
editing: false, editing: false,
profileForm: null, profileForm: null,
}); });
dis.dispatch({action: 'panel_disable'});
}, },
_onNameChange: function(value) { _onNameChange: function(value) {
@ -535,12 +539,16 @@ export default React.createClass({
_onSaveClick: function() { _onSaveClick: function() {
this.setState({saving: true}); this.setState({saving: true});
MatrixClientPeg.get().setGroupProfile(this.props.groupId, this.state.profileForm).then((result) => { const savePromise = this.state.isUserPrivileged ?
MatrixClientPeg.get().setGroupProfile(this.props.groupId, this.state.profileForm) :
Promise.resolve();
savePromise.then((result) => {
this.setState({ this.setState({
saving: false, saving: false,
editing: false, editing: false,
summary: null, summary: null,
}); });
dis.dispatch({action: 'panel_disable'});
this._initGroupStore(this.props.groupId); this._initGroupStore(this.props.groupId);
}).catch((e) => { }).catch((e) => {
this.setState({ this.setState({
@ -624,23 +632,40 @@ export default React.createClass({
}); });
}, },
_getGroupSection: function() {
const groupSettingsSectionClasses = classnames({
"mx_GroupView_group": this.state.editing,
"mx_GroupView_group_disabled": this.state.editing && !this.state.isUserPrivileged,
});
const header = this.state.editing ? <h2> { _t('Community Settings') } </h2> : <div />;
return <div className={groupSettingsSectionClasses}>
{ header }
{ this._getLongDescriptionNode() }
{ this._getRoomsNode() }
</div>;
},
_getRoomsNode: function() { _getRoomsNode: function() {
const RoomDetailList = sdk.getComponent('rooms.RoomDetailList'); const RoomDetailList = sdk.getComponent('rooms.RoomDetailList');
const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const TintableSvg = sdk.getComponent('elements.TintableSvg'); const TintableSvg = sdk.getComponent('elements.TintableSvg');
const addButton = this.state.editing ?
(<AccessibleButton onClick={this._onAddRoomsClick} > const addRoomRow = this.state.editing ?
<div className="mx_GroupView_rooms_header_addButton" > (<AccessibleButton className="mx_GroupView_rooms_header_addRow"
onClick={this._onAddRoomsClick}
>
<div className="mx_GroupView_rooms_header_addRow_button">
<TintableSvg src="img/icons-room-add.svg" width="24" height="24" /> <TintableSvg src="img/icons-room-add.svg" width="24" height="24" />
</div> </div>
<div className="mx_GroupView_rooms_header_addButton_label"> <div className="mx_GroupView_rooms_header_addRow_label">
{ _t('Add rooms to this community') } { _t('Add rooms to this community') }
</div> </div>
</AccessibleButton>) : <div />; </AccessibleButton>) : <div />;
return <div className="mx_GroupView_rooms"> return <div className="mx_GroupView_rooms">
<div className="mx_GroupView_rooms_header"> <div className="mx_GroupView_rooms_header">
<h3>{ _t('Rooms') }</h3> <h3>{ _t('Rooms') }</h3>
{ addButton } { addRoomRow }
</div> </div>
<RoomDetailList rooms={this._groupStore.getGroupRooms()} /> <RoomDetailList rooms={this._groupStore.getGroupRooms()} />
</div>; </div>;
@ -761,8 +786,8 @@ export default React.createClass({
</div>; </div>;
} else if (group.myMembership === 'join' && this.state.editing) { } else if (group.myMembership === 'join' && this.state.editing) {
const leaveButtonTooltip = this.state.isUserPrivileged ? const leaveButtonTooltip = this.state.isUserPrivileged ?
_t("You are a member of this community") : _t("You are an administrator of this community") :
_t("You are an administrator of this community"); _t("You are a member of this community");
const leaveButtonClasses = classnames({ const leaveButtonClasses = classnames({
"mx_RoomHeader_textButton": true, "mx_RoomHeader_textButton": true,
"mx_GroupView_textButton": true, "mx_GroupView_textButton": true,
@ -790,7 +815,7 @@ export default React.createClass({
_getMemberSettingsSection: function() { _getMemberSettingsSection: function() {
return <div className="mx_GroupView_memberSettings"> return <div className="mx_GroupView_memberSettings">
<h3> { _t("Community Member Settings") } </h3> <h2> { _t("Community Member Settings") } </h2>
<div className="mx_GroupView_memberSettings_toggle"> <div className="mx_GroupView_memberSettings_toggle">
<input type="checkbox" <input type="checkbox"
onClick={this._onPublicityToggle} onClick={this._onPublicityToggle}
@ -813,8 +838,13 @@ export default React.createClass({
if (summary.profile && summary.profile.long_description) { if (summary.profile && summary.profile.long_description) {
description = sanitizedHtmlNode(summary.profile.long_description); description = sanitizedHtmlNode(summary.profile.long_description);
} }
return this.state.editing && this.state.isUserPrivileged ? const groupDescEditingClasses = classnames({
<div className="mx_GroupView_groupDesc"> "mx_GroupView_groupDesc": true,
"mx_GroupView_groupDesc_disabled": !this.state.isUserPrivileged,
});
return this.state.editing ?
<div className={groupDescEditingClasses}>
<h3> { _t("Long Description (HTML)") } </h3> <h3> { _t("Long Description (HTML)") } </h3>
<textarea <textarea
value={this.state.profileForm.long_description} value={this.state.profileForm.long_description}
@ -844,14 +874,10 @@ export default React.createClass({
const bodyNodes = [ const bodyNodes = [
this._getMembershipSection(), this._getMembershipSection(),
this.state.editing ? this._getMemberSettingsSection() : null, this.state.editing ? this._getMemberSettingsSection() : null,
this._getLongDescriptionNode(), this._getGroupSection(),
this._getRoomsNode(),
]; ];
const rightButtons = []; const rightButtons = [];
const headerClasses = { if (this.state.editing && this.state.isUserPrivileged) {
mx_GroupView_header: true,
};
if (this.state.editing) {
let avatarImage; let avatarImage;
if (this.state.uploadingAvatar) { if (this.state.uploadingAvatar) {
avatarImage = <Spinner />; avatarImage = <Spinner />;
@ -900,24 +926,12 @@ export default React.createClass({
onValueChanged={this._onShortDescChange} onValueChanged={this._onShortDescChange}
tabIndex="2" tabIndex="2"
dir="auto" />; dir="auto" />;
rightButtons.push(
<AccessibleButton className="mx_GroupView_textButton mx_RoomHeader_textButton"
onClick={this._onSaveClick} key="_saveButton"
>
{ _t('Save') }
</AccessibleButton>,
);
rightButtons.push(
<AccessibleButton className="mx_RoomHeader_cancelButton" onClick={this._onCancelClick} key="_cancelButton">
<img src="img/cancel.svg" className="mx_filterFlipColor"
width="18" height="18" alt={_t("Cancel")} />
</AccessibleButton>,
);
} else { } else {
const groupAvatarUrl = summary.profile ? summary.profile.avatar_url : null; const groupAvatarUrl = summary.profile ? summary.profile.avatar_url : null;
avatarNode = <GroupAvatar avatarNode = <GroupAvatar
groupId={this.props.groupId} groupId={this.props.groupId}
groupAvatarUrl={groupAvatarUrl} groupAvatarUrl={groupAvatarUrl}
onClick={this._onEditClick}
width={48} height={48} width={48} height={48}
/>; />;
if (summary.profile && summary.profile.name) { if (summary.profile && summary.profile.name) {
@ -933,13 +947,31 @@ export default React.createClass({
if (summary.profile && summary.profile.short_description) { if (summary.profile && summary.profile.short_description) {
shortDescNode = <span onClick={this._onEditClick}>{ summary.profile.short_description }</span>; shortDescNode = <span onClick={this._onEditClick}>{ summary.profile.short_description }</span>;
} }
}
if (this.state.editing) {
rightButtons.push( rightButtons.push(
<AccessibleButton className="mx_GroupHeader_button" <AccessibleButton className="mx_GroupView_textButton mx_RoomHeader_textButton"
onClick={this._onEditClick} title={_t("Community Settings")} key="_editButton" onClick={this._onSaveClick} key="_saveButton"
> >
<TintableSvg src="img/icons-settings-room.svg" width="16" height="16" /> { _t('Save') }
</AccessibleButton>, </AccessibleButton>,
); );
rightButtons.push(
<AccessibleButton className="mx_RoomHeader_cancelButton" onClick={this._onCancelClick} key="_cancelButton">
<img src="img/cancel.svg" className="mx_filterFlipColor"
width="18" height="18" alt={_t("Cancel")} />
</AccessibleButton>,
);
} else {
if (summary.user && summary.user.membership === 'join') {
rightButtons.push(
<AccessibleButton className="mx_GroupHeader_button"
onClick={this._onEditClick} title={_t("Community Settings")} key="_editButton"
>
<TintableSvg src="img/icons-settings-room.svg" width="16" height="16" />
</AccessibleButton>,
);
}
if (this.props.collapsedRhs) { if (this.props.collapsedRhs) {
rightButtons.push( rightButtons.push(
<AccessibleButton className="mx_GroupHeader_button" <AccessibleButton className="mx_GroupHeader_button"
@ -949,10 +981,13 @@ export default React.createClass({
</AccessibleButton>, </AccessibleButton>,
); );
} }
headerClasses.mx_GroupView_header_view = true;
} }
const headerClasses = {
mx_GroupView_header: true,
mx_GroupView_header_view: !this.state.editing,
};
return ( return (
<div className="mx_GroupView"> <div className="mx_GroupView">
<div className={classnames(headerClasses)}> <div className={classnames(headerClasses)}>

View file

@ -240,11 +240,13 @@ export default React.createClass({
oobData={this.props.roomOobData} oobData={this.props.roomOobData}
eventPixelOffset={this.props.initialEventPixelOffset} eventPixelOffset={this.props.initialEventPixelOffset}
key={this.props.currentRoomId || 'roomview'} key={this.props.currentRoomId || 'roomview'}
opacity={this.props.middleOpacity} disabled={this.props.middleDisabled}
collapsedRhs={this.props.collapseRhs} collapsedRhs={this.props.collapseRhs}
ConferenceHandler={this.props.ConferenceHandler} ConferenceHandler={this.props.ConferenceHandler}
/>; />;
if (!this.props.collapseRhs) right_panel = <RightPanel roomId={this.props.currentRoomId} opacity={this.props.rightOpacity} />; if (!this.props.collapseRhs) {
right_panel = <RightPanel roomId={this.props.currentRoomId} disabled={this.props.rightDisabled} />;
}
break; break;
case PageTypes.UserSettings: case PageTypes.UserSettings:
@ -254,7 +256,7 @@ export default React.createClass({
referralBaseUrl={this.props.config.referralBaseUrl} referralBaseUrl={this.props.config.referralBaseUrl}
teamToken={this.props.teamToken} teamToken={this.props.teamToken}
/>; />;
if (!this.props.collapseRhs) right_panel = <RightPanel opacity={this.props.rightOpacity} />; if (!this.props.collapseRhs) right_panel = <RightPanel disabled={this.props.rightDisabled} />;
break; break;
case PageTypes.MyGroups: case PageTypes.MyGroups:
@ -266,7 +268,7 @@ export default React.createClass({
onRoomCreated={this.props.onRoomCreated} onRoomCreated={this.props.onRoomCreated}
collapsedRhs={this.props.collapseRhs} collapsedRhs={this.props.collapseRhs}
/>; />;
if (!this.props.collapseRhs) right_panel = <RightPanel opacity={this.props.rightOpacity} />; if (!this.props.collapseRhs) right_panel = <RightPanel disabled={this.props.rightDisabled} />;
break; break;
case PageTypes.RoomDirectory: case PageTypes.RoomDirectory:
@ -294,14 +296,14 @@ export default React.createClass({
case PageTypes.UserView: case PageTypes.UserView:
page_element = null; // deliberately null for now page_element = null; // deliberately null for now
right_panel = <RightPanel opacity={this.props.rightOpacity} />; right_panel = <RightPanel disabled={this.props.rightDisabled} />;
break; break;
case PageTypes.GroupView: case PageTypes.GroupView:
page_element = <GroupView page_element = <GroupView
groupId={this.props.currentGroupId} groupId={this.props.currentGroupId}
collapsedRhs={this.props.collapseRhs} collapsedRhs={this.props.collapseRhs}
/>; />;
if (!this.props.collapseRhs) right_panel = <RightPanel groupId={this.props.currentGroupId} opacity={this.props.rightOpacity} />; if (!this.props.collapseRhs) right_panel = <RightPanel groupId={this.props.currentGroupId} disabled={this.props.rightDisabled} />;
break; break;
} }
@ -334,7 +336,7 @@ export default React.createClass({
<LeftPanel <LeftPanel
selectedRoom={this.props.currentRoomId} selectedRoom={this.props.currentRoomId}
collapsed={this.props.collapseLhs || false} collapsed={this.props.collapseLhs || false}
opacity={this.props.leftOpacity} disabled={this.props.leftDisabled}
/> />
<main className='mx_MatrixChat_middlePanel'> <main className='mx_MatrixChat_middlePanel'>
{ page_element } { page_element }

View file

@ -145,9 +145,9 @@ module.exports = React.createClass({
collapseLhs: false, collapseLhs: false,
collapseRhs: false, collapseRhs: false,
leftOpacity: 1.0, leftDisabled: false,
middleOpacity: 1.0, middleDisabled: false,
rightOpacity: 1.0, rightDisabled: false,
version: null, version: null,
newVersion: null, newVersion: null,
@ -537,12 +537,11 @@ module.exports = React.createClass({
collapseRhs: false, collapseRhs: false,
}); });
break; break;
case 'ui_opacity': { case 'panel_disable': {
const sideDefault = payload.sideOpacity >= 0.0 ? payload.sideOpacity : 1.0;
this.setState({ this.setState({
leftOpacity: payload.leftOpacity >= 0.0 ? payload.leftOpacity : sideDefault, leftDisabled: payload.leftDisabled || payload.sideDisabled || false,
middleOpacity: payload.middleOpacity || 1.0, middleDisabled: payload.middleDisabled || false,
rightOpacity: payload.rightOpacity >= 0.0 ? payload.rightOpacity : sideDefault, rightDisabled: payload.rightDisabled || payload.sideDisabled || false,
}); });
break; } break; }
case 'set_theme': case 'set_theme':

View file

@ -16,6 +16,7 @@ limitations under the License.
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import classNames from 'classnames';
import UserSettingsStore from '../../UserSettingsStore'; import UserSettingsStore from '../../UserSettingsStore';
import shouldHideEvent from '../../shouldHideEvent'; import shouldHideEvent from '../../shouldHideEvent';
import dis from "../../dispatcher"; import dis from "../../dispatcher";
@ -78,9 +79,6 @@ module.exports = React.createClass({
// callback which is called when more content is needed. // callback which is called when more content is needed.
onFillRequest: React.PropTypes.func, onFillRequest: React.PropTypes.func,
// opacity for dynamic UI fading effects
opacity: React.PropTypes.number,
// className for the panel // className for the panel
className: React.PropTypes.string.isRequired, className: React.PropTypes.string.isRequired,
@ -353,7 +351,7 @@ module.exports = React.createClass({
} }
if (!isMembershipChange(collapsedMxEv) || if (!isMembershipChange(collapsedMxEv) ||
this._wantsDateSeparator(this.props.events[i], collapsedMxEv.getDate())) { this._wantsDateSeparator(mxEv, collapsedMxEv.getDate())) {
break; break;
} }
@ -376,9 +374,7 @@ module.exports = React.createClass({
// of MemberEventListSummary, render each member event as if the previous // of MemberEventListSummary, render each member event as if the previous
// one was itself. This way, the timestamp of the previous event === the // one was itself. This way, the timestamp of the previous event === the
// timestamp of the current event, and no DateSeperator is inserted. // timestamp of the current event, and no DateSeperator is inserted.
const ret = this._getTilesForEvent(e, e, e === lastShownEvent); return this._getTilesForEvent(e, e, e === lastShownEvent);
prevEvent = e;
return ret;
}).reduce((a, b) => a.concat(b)); }).reduce((a, b) => a.concat(b));
if (eventTiles.length === 0) { if (eventTiles.length === 0) {
@ -397,6 +393,7 @@ module.exports = React.createClass({
ret.push(this._getReadMarkerTile(visible)); ret.push(this._getReadMarkerTile(visible));
} }
prevEvent = mxEv;
continue; continue;
} }
@ -649,12 +646,13 @@ module.exports = React.createClass({
} }
const style = this.props.hidden ? { display: 'none' } : {}; const style = this.props.hidden ? { display: 'none' } : {};
style.opacity = this.props.opacity;
let className = this.props.className + " mx_fadable"; const className = classNames(
if (this.props.alwaysShowTimestamps) { this.props.className,
className += " mx_MessagePanel_alwaysShowTimestamps"; {
} "mx_MessagePanel_alwaysShowTimestamps": this.props.alwaysShowTimestamps,
},
);
return ( return (
<ScrollPanel ref="scrollPanel" className={className} <ScrollPanel ref="scrollPanel" className={className}

View file

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

View file

@ -45,7 +45,6 @@ const NotificationPanel = React.createClass({
manageReadMarkers={false} manageReadMarkers={false}
timelineSet={timelineSet} timelineSet={timelineSet}
showUrlPreview = {false} showUrlPreview = {false}
opacity={this.props.opacity}
tileShape="notif" tileShape="notif"
empty={_t('You have no visible notifications')} empty={_t('You have no visible notifications')}
/> />

View file

@ -1697,7 +1697,7 @@ module.exports = React.createClass({
onResize={this.onChildResize} onResize={this.onChildResize}
uploadFile={this.uploadFile} uploadFile={this.uploadFile}
callState={this.state.callState} callState={this.state.callState}
opacity={this.props.opacity} disabled={this.props.disabled}
showApps={this.state.showApps} showApps={this.state.showApps}
/>; />;
} }
@ -1758,7 +1758,6 @@ module.exports = React.createClass({
className="mx_RoomView_messagePanel mx_RoomView_searchResultsPanel" className="mx_RoomView_messagePanel mx_RoomView_searchResultsPanel"
onFillRequest={this.onSearchResultsFillRequest} onFillRequest={this.onSearchResultsFillRequest}
onResize={this.onSearchResultsResize} onResize={this.onSearchResultsResize}
style={{ opacity: this.props.opacity }}
> >
<li className={scrollheader_classes}></li> <li className={scrollheader_classes}></li>
{ this.getSearchResultTiles() } { this.getSearchResultTiles() }
@ -1789,7 +1788,6 @@ module.exports = React.createClass({
onScroll={this.onMessageListScroll} onScroll={this.onMessageListScroll}
onReadMarkerUpdated={this._updateTopUnreadMessagesBar} onReadMarkerUpdated={this._updateTopUnreadMessagesBar}
showUrlPreview = {this.state.showUrlPreview} showUrlPreview = {this.state.showUrlPreview}
opacity={this.props.opacity}
className="mx_RoomView_messagePanel" className="mx_RoomView_messagePanel"
/>); />);
@ -1797,7 +1795,7 @@ module.exports = React.createClass({
if (this.state.showTopUnreadMessagesBar) { if (this.state.showTopUnreadMessagesBar) {
const TopUnreadMessagesBar = sdk.getComponent('rooms.TopUnreadMessagesBar'); const TopUnreadMessagesBar = sdk.getComponent('rooms.TopUnreadMessagesBar');
topUnreadMessagesBar = ( topUnreadMessagesBar = (
<div className="mx_RoomView_topUnreadMessagesBar mx_fadable" style={{ opacity: this.props.opacity }}> <div className="mx_RoomView_topUnreadMessagesBar">
<TopUnreadMessagesBar <TopUnreadMessagesBar
onScrollUpClick={this.jumpToReadMarker} onScrollUpClick={this.jumpToReadMarker}
onCloseClick={this.forgetReadMarker} onCloseClick={this.forgetReadMarker}
@ -1805,10 +1803,19 @@ module.exports = React.createClass({
</div> </div>
); );
} }
let statusBarAreaClass = "mx_RoomView_statusArea mx_fadable"; const statusBarAreaClass = classNames(
if (isStatusAreaExpanded) { "mx_RoomView_statusArea",
statusBarAreaClass += " mx_RoomView_statusArea_expanded"; {
} "mx_RoomView_statusArea_expanded": isStatusAreaExpanded,
},
);
const fadableSectionClasses = classNames(
"mx_RoomView_body", "mx_fadable",
{
"mx_fadable_faded": this.props.disabled,
},
);
return ( return (
<div className={"mx_RoomView" + (inCall ? " mx_RoomView_inCall" : "")} ref="roomView"> <div className={"mx_RoomView" + (inCall ? " mx_RoomView_inCall" : "")} ref="roomView">
@ -1827,16 +1834,18 @@ module.exports = React.createClass({
onLeaveClick={(myMember && myMember.membership === "join") ? this.onLeaveClick : null} onLeaveClick={(myMember && myMember.membership === "join") ? this.onLeaveClick : null}
/> />
{ auxPanel } { auxPanel }
{ topUnreadMessagesBar } <div className={fadableSectionClasses}>
{ messagePanel } { topUnreadMessagesBar }
{ searchResultsPanel } { messagePanel }
<div className={statusBarAreaClass} style={{opacity: this.props.opacity}}> { searchResultsPanel }
<div className="mx_RoomView_statusAreaBox"> <div className={statusBarAreaClass}>
<div className="mx_RoomView_statusAreaBox_line"></div> <div className="mx_RoomView_statusAreaBox">
{ statusBar } <div className="mx_RoomView_statusAreaBox_line"></div>
{ statusBar }
</div>
</div> </div>
{ messageComposer }
</div> </div>
{ messageComposer }
</div> </div>
); );
}, },

View file

@ -89,9 +89,6 @@ var TimelinePanel = React.createClass({
// callback which is called when the read-up-to mark is updated. // callback which is called when the read-up-to mark is updated.
onReadMarkerUpdated: React.PropTypes.func, onReadMarkerUpdated: React.PropTypes.func,
// opacity for dynamic UI fading effects
opacity: React.PropTypes.number,
// maximum number of events to show in a timeline // maximum number of events to show in a timeline
timelineCap: React.PropTypes.number, timelineCap: React.PropTypes.number,
@ -1157,7 +1154,6 @@ var TimelinePanel = React.createClass({
onScroll={this.onMessageListScroll} onScroll={this.onMessageListScroll}
onFillRequest={this.onMessageListFillRequest} onFillRequest={this.onMessageListFillRequest}
onUnfillRequest={this.onMessageListUnfillRequest} onUnfillRequest={this.onMessageListUnfillRequest}
opacity={this.props.opacity}
isTwelveHour={this.state.isTwelveHour} isTwelveHour={this.state.isTwelveHour}
alwaysShowTimestamps={this.state.alwaysShowTimestamps} alwaysShowTimestamps={this.state.alwaysShowTimestamps}
className={this.props.className} className={this.props.className}

View file

@ -118,6 +118,10 @@ const SETTINGS_LABELS = [
id: 'TextualBody.disableBigEmoji', id: 'TextualBody.disableBigEmoji',
label: _td('Disable big emoji in chat'), label: _td('Disable big emoji in chat'),
}, },
{
id: 'VideoView.flipVideoHorizontally',
label: _td('Mirror local video feed'),
},
/* /*
{ {
id: 'useFixedWidthFont', id: 'useFixedWidthFont',
@ -276,9 +280,9 @@ module.exports = React.createClass({
MatrixClientPeg.get().on("RoomMember.membership", this._onInviteStateChange); MatrixClientPeg.get().on("RoomMember.membership", this._onInviteStateChange);
dis.dispatch({ dis.dispatch({
action: 'ui_opacity', action: 'panel_disable',
sideOpacity: 0.3, sideDisabled: true,
middleOpacity: 0.3, middleDisabled: true,
}); });
this._refreshFromServer(); this._refreshFromServer();
@ -316,9 +320,9 @@ module.exports = React.createClass({
componentWillUnmount: function() { componentWillUnmount: function() {
this._unmounted = true; this._unmounted = true;
dis.dispatch({ dis.dispatch({
action: 'ui_opacity', action: 'panel_disable',
sideOpacity: 1.0, sideDisabled: false,
middleOpacity: 1.0, middleDisabled: false,
}); });
dis.unregister(this.dispatcherRef); dis.unregister(this.dispatcherRef);
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
@ -1333,8 +1337,11 @@ module.exports = React.createClass({
<div className="mx_UserSettings_avatarPicker"> <div className="mx_UserSettings_avatarPicker">
<div className="mx_UserSettings_avatarPicker_remove" onClick={this.onAvatarRemoveClick}> <div className="mx_UserSettings_avatarPicker_remove" onClick={this.onAvatarRemoveClick}>
<img src="img/cancel.svg" width="15" height="15" <img src="img/cancel.svg"
alt={_t("Remove avatar")} title={_t("Remove avatar")} /> width="15" height="15"
className="mx_filterFlipColor"
alt={_t("Remove avatar")}
title={_t("Remove avatar")} />
</div> </div>
<div onClick={this.onAvatarPickerClick} className="mx_UserSettings_avatarPicker_imgContainer"> <div onClick={this.onAvatarPickerClick} className="mx_UserSettings_avatarPicker_imgContainer">
<ChangeAvatar ref="changeAvatar" initialAvatarUrl={avatarUrl} <ChangeAvatar ref="changeAvatar" initialAvatarUrl={avatarUrl}

View file

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

View file

@ -28,6 +28,7 @@ export default React.createClass({
width: PropTypes.number, width: PropTypes.number,
height: PropTypes.number, height: PropTypes.number,
resizeMethod: PropTypes.string, resizeMethod: PropTypes.string,
onClick: PropTypes.func,
}, },
getDefaultProps: function() { getDefaultProps: function() {

View file

@ -272,20 +272,27 @@ module.exports = React.createClass({
const topicEvent = room.currentState.getStateEvents('m.room.topic', ''); const topicEvent = room.currentState.getStateEvents('m.room.topic', '');
const name = nameEvent ? nameEvent.getContent().name : ''; const name = nameEvent ? nameEvent.getContent().name : '';
const canonicalAlias = room.getCanonicalAlias(); const canonicalAlias = room.getCanonicalAlias();
const aliasEvents = room.currentState.getStateEvents('m.room.aliases');
const aliases = aliasEvents.map((ev) => ev.getContent().aliases).reduce((a, b) => {
return a.concat(b);
}, []);
const topic = topicEvent ? topicEvent.getContent().topic : ''; const topic = topicEvent ? topicEvent.getContent().topic : '';
const nameMatch = (name || '').toLowerCase().includes(lowerCaseQuery); const nameMatch = (name || '').toLowerCase().includes(lowerCaseQuery);
const aliasMatch = (canonicalAlias || '').toLowerCase().includes(lowerCaseQuery); const aliasMatch = aliases.some((alias) =>
(alias || '').toLowerCase().includes(lowerCaseQuery),
);
const topicMatch = (topic || '').toLowerCase().includes(lowerCaseQuery); const topicMatch = (topic || '').toLowerCase().includes(lowerCaseQuery);
if (!(nameMatch || topicMatch || aliasMatch)) { if (!(nameMatch || topicMatch || aliasMatch)) {
return; return;
} }
const avatarEvent = room.currentState.getStateEvents('m.room.avatar', ''); const avatarEvent = room.currentState.getStateEvents('m.room.avatar', '');
const avatarUrl = avatarEvent ? avatarEvent.getContent().url : undefined; const avatarUrl = avatarEvent ? avatarEvent.getContent().url : undefined;
results.push({ results.push({
room_id: room.roomId, room_id: room.roomId,
avatar_url: avatarUrl, avatar_url: avatarUrl,
name: name || canonicalAlias, name: name || canonicalAlias || aliases[0] || _t('Unnamed Room'),
}); });
}); });
this._processResults(results, query); this._processResults(results, query);

View file

@ -108,7 +108,7 @@ export default React.createClass({
// rather than displaying what the server gives us, but synapse doesn't give // rather than displaying what the server gives us, but synapse doesn't give
// any yet. // any yet.
createErrorNode = <div className="error"> createErrorNode = <div className="error">
<div>{ _t('Room creation failed') }</div> <div>{ _t('Something went wrong whilst creating your community') }</div>
<div>{ this.state.createError.message }</div> <div>{ this.state.createError.message }</div>
</div>; </div>;
} }

View file

@ -172,18 +172,29 @@ export default React.createClass({
*/ */
_onDeleteClick: function() { _onDeleteClick: function() {
if (this._canUserModify()) { if (this._canUserModify()) {
console.log("Delete widget %s", this.props.id); // Show delete confirmation dialog
this.setState({deleting: true}); const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
MatrixClientPeg.get().sendStateEvent( Modal.createTrackedDialog('Delete Widget', '', QuestionDialog, {
this.props.room.roomId, title: _t("Delete Widget"),
'im.vector.modular.widgets', description: _t(
{}, // empty content "Deleting a widget removes it for all users in this room." +
this.props.id, " Are you sure you want to delete this widget?"),
).then(() => { button: _t("Delete widget"),
console.log('Deleted widget'); onFinished: (confirmed) => {
}, (e) => { if (!confirmed) {
console.error('Failed to delete widget', e); return;
this.setState({deleting: false}); }
this.setState({deleting: true});
MatrixClientPeg.get().sendStateEvent(
this.props.room.roomId,
'im.vector.modular.widgets',
{}, // empty content
this.props.id,
).catch((e) => {
console.error('Failed to delete widget', e);
this.setState({deleting: false});
});
},
}); });
} else { } else {
console.log("Revoke widget permissions - %s", this.props.id); console.log("Revoke widget permissions - %s", this.props.id);
@ -305,7 +316,7 @@ export default React.createClass({
let deleteIcon = 'img/cancel.svg'; let deleteIcon = 'img/cancel.svg';
let deleteClasses = 'mx_filterFlipColor mx_AppTileMenuBarWidget'; let deleteClasses = 'mx_filterFlipColor mx_AppTileMenuBarWidget';
if(this._canUserModify()) { if(this._canUserModify()) {
deleteIcon = 'img/cancel-red.svg'; deleteIcon = 'img/icon-delete-pink.svg';
deleteClasses += ' mx_AppTileMenuBarWidgetDelete'; deleteClasses += ' mx_AppTileMenuBarWidgetDelete';
} }

View file

@ -84,7 +84,9 @@ module.exports = React.createClass({
onNewItemChanged: PropTypes.func, onNewItemChanged: PropTypes.func,
onItemAdded: PropTypes.func, onItemAdded: PropTypes.func,
onItemEdited: PropTypes.func, onItemEdited: PropTypes.func,
onItemRemoved: PropTypes. func, onItemRemoved: PropTypes.func,
canEdit: PropTypes.bool,
}, },
getDefaultProps: function() { getDefaultProps: function() {
@ -136,14 +138,16 @@ module.exports = React.createClass({
{ label } { label }
</div> </div>
{ editableItems } { editableItems }
<EditableItem { this.props.canEdit ?
key={-1} <EditableItem
initialValue={this.props.newItem} key={-1}
onAdd={this.onItemAdded} initialValue={this.props.newItem}
onChange={this.onNewItemChanged} onAdd={this.onItemAdded}
addOnChange={true} onChange={this.onNewItemChanged}
placeholder={this.props.placeholder} addOnChange={true}
/> placeholder={this.props.placeholder}
/> : <div />
}
</div>); </div>);
}, },
}); });

View file

@ -127,7 +127,7 @@ export default class Flair extends React.Component {
} }
const profiles = await this._getGroupProfiles(groups); const profiles = await this._getGroupProfiles(groups);
if (!this.unmounted) { if (!this.unmounted) {
this.setState({profiles}); this.setState({profiles: profiles.filter((profile) => {return profile.avatarUrl;})});
} }
} }

View file

@ -173,7 +173,7 @@ module.exports = withMatrixClient(React.createClass({
<div className="mx_MemberInfo"> <div className="mx_MemberInfo">
<GeminiScrollbar autoshow={true}> <GeminiScrollbar autoshow={true}>
<AccessibleButton className="mx_MemberInfo_cancel"onClick={this._onCancel}> <AccessibleButton className="mx_MemberInfo_cancel"onClick={this._onCancel}>
<img src="img/cancel.svg" width="18" height="18" /> <img src="img/cancel.svg" width="18" height="18" className="mx_filterFlipColor" />
</AccessibleButton> </AccessibleButton>
<div className="mx_MemberInfo_avatar"> <div className="mx_MemberInfo_avatar">
{ avatar } { avatar }

View file

@ -17,7 +17,7 @@ limitations under the License.
import React from 'react'; import React from 'react';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import sdk from '../../../index'; import sdk from '../../../index';
import { groupMemberFromApiObject } from '../../../groups'; import GroupStoreCache from '../../../stores/GroupStoreCache';
import GeminiScrollbar from 'react-gemini-scrollbar'; import GeminiScrollbar from 'react-gemini-scrollbar';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import withMatrixClient from '../../../wrappers/withMatrixClient'; import withMatrixClient from '../../../wrappers/withMatrixClient';
@ -27,15 +27,16 @@ const INITIAL_LOAD_NUM_MEMBERS = 30;
export default withMatrixClient(React.createClass({ export default withMatrixClient(React.createClass({
displayName: 'GroupMemberList', displayName: 'GroupMemberList',
propTypes: { contextTypes: {
matrixClient: PropTypes.object.isRequired, matrixClient: PropTypes.object.isRequired,
},
propTypes: {
groupId: PropTypes.string.isRequired, groupId: PropTypes.string.isRequired,
}, },
getInitialState: function() { getInitialState: function() {
return { return {
fetching: false,
fetchingInvitedMembers: false,
members: null, members: null,
invitedMembers: null, invitedMembers: null,
truncateAt: INITIAL_LOAD_NUM_MEMBERS, truncateAt: INITIAL_LOAD_NUM_MEMBERS,
@ -44,36 +45,21 @@ export default withMatrixClient(React.createClass({
componentWillMount: function() { componentWillMount: function() {
this._unmounted = false; this._unmounted = false;
this._fetchMembers(); this._initGroupStore(this.props.groupId);
},
_initGroupStore: function(groupId) {
this._groupStore = GroupStoreCache.getGroupStore(this.context.matrixClient, groupId);
this._groupStore.registerListener(() => {
this._fetchMembers();
});
}, },
_fetchMembers: function() { _fetchMembers: function() {
if (this._unmounted) return;
this.setState({ this.setState({
fetching: true, members: this._groupStore.getGroupMembers(),
fetchingInvitedMembers: true, invitedMembers: this._groupStore.getGroupInvitedMembers(),
});
this.props.matrixClient.getGroupUsers(this.props.groupId).then((result) => {
this.setState({
members: result.chunk.map((apiMember) => {
return groupMemberFromApiObject(apiMember);
}),
fetching: false,
});
}).catch((e) => {
this.setState({fetching: false});
console.error("Failed to get group member list: " + e);
});
this.props.matrixClient.getGroupInvitedUsers(this.props.groupId).then((result) => {
this.setState({
invitedMembers: result.chunk.map((apiMember) => {
return groupMemberFromApiObject(apiMember);
}),
fetchingInvitedMembers: false,
});
}).catch((e) => {
this.setState({fetchingInvitedMembers: false});
console.error("Failed to get group invited member list: " + e);
}); });
}, },
@ -117,12 +103,11 @@ export default withMatrixClient(React.createClass({
}); });
} }
memberList = memberList.map((m) => { const uniqueMembers = {};
return ( memberList.forEach((m) => {
<GroupMemberTile key={m.userId} groupId={this.props.groupId} member={m} /> if (!uniqueMembers[m.userId]) uniqueMembers[m.userId] = m;
);
}); });
memberList = Object.keys(uniqueMembers).map((userId) => uniqueMembers[userId]);
memberList.sort((a, b) => { memberList.sort((a, b) => {
// TODO: should put admins at the top: we don't yet have that info // TODO: should put admins at the top: we don't yet have that info
if (a < b) { if (a < b) {
@ -134,10 +119,16 @@ export default withMatrixClient(React.createClass({
} }
}); });
const memberTiles = memberList.map((m) => {
return (
<GroupMemberTile key={m.userId} groupId={this.props.groupId} member={m} />
);
});
return <TruncatedList className="mx_MemberList_wrapper" truncateAt={this.state.truncateAt} return <TruncatedList className="mx_MemberList_wrapper" truncateAt={this.state.truncateAt}
createOverflowElement={this._createOverflowTile} createOverflowElement={this._createOverflowTile}
> >
{ memberList } { memberTiles }
</TruncatedList>; </TruncatedList>;
}, },

View file

@ -16,7 +16,6 @@ limitations under the License.
import React from 'react'; import React from 'react';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import sdk from '../../../index'; import sdk from '../../../index';
import { groupRoomFromApiObject } from '../../../groups';
import GroupStoreCache from '../../../stores/GroupStoreCache'; import GroupStoreCache from '../../../stores/GroupStoreCache';
import GeminiScrollbar from 'react-gemini-scrollbar'; import GeminiScrollbar from 'react-gemini-scrollbar';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
@ -48,24 +47,20 @@ export default React.createClass({
_initGroupStore: function(groupId) { _initGroupStore: function(groupId) {
this._groupStore = GroupStoreCache.getGroupStore(this.context.matrixClient, groupId); this._groupStore = GroupStoreCache.getGroupStore(this.context.matrixClient, groupId);
this._groupStore.on('update', () => { this._groupStore.registerListener(() => {
this._fetchRooms(); this._fetchRooms();
}); });
this._groupStore.on('error', (err) => { this._groupStore.on('error', (err) => {
console.error('Error in group store (listened to by GroupRoomList)', err);
this.setState({ this.setState({
rooms: null, rooms: null,
}); });
}); });
this._fetchRooms();
}, },
_fetchRooms: function() { _fetchRooms: function() {
if (this._unmounted) return; if (this._unmounted) return;
this.setState({ this.setState({
rooms: this._groupStore.getGroupRooms().map((apiRoom) => { rooms: this._groupStore.getGroupRooms(),
return groupRoomFromApiObject(apiRoom);
}),
}); });
}, },

View file

@ -120,8 +120,11 @@ const GroupRoomTile = React.createClass({
<div className="mx_GroupRoomTile_name"> <div className="mx_GroupRoomTile_name">
{ this.state.name } { this.state.name }
</div> </div>
<AccessibleButton className="mx_GroupRoomTile_delete" onClick={this.onDeleteClick}> <AccessibleButton className="mx_GroupRoomTile_delete"
<img src="img/cancel-small.svg" /> onClick={this.onDeleteClick}
tooltip={_t("Remove this room from the community")}
>
<img src="img/cancel.svg" width="15" height="15" className="mx_filterFlipColor" />
</AccessibleButton> </AccessibleButton>
</AccessibleButton> </AccessibleButton>
); );

View file

@ -25,7 +25,10 @@ module.exports = React.createClass({
render: function() { render: function() {
let tooltip = _t("Removed or unknown message type"); let tooltip = _t("Removed or unknown message type");
if (this.props.mxEvent.isRedacted()) { if (this.props.mxEvent.isRedacted()) {
tooltip = _t("Message removed by %(userId)s", {userId: this.props.mxEvent.getSender()}); const redactedBecauseUserId = this.props.mxEvent.getUnsigned().redacted_because.sender;
tooltip = redactedBecauseUserId ?
_t("Message removed by %(userId)s", { userId: redactedBecauseUserId }) :
_t("Message removed");
} }
const text = this.props.mxEvent.getContent().body; const text = this.props.mxEvent.getContent().body;

View file

@ -262,6 +262,7 @@ module.exports = React.createClass({
items={this.state.domainToAliases[localDomain] || []} items={this.state.domainToAliases[localDomain] || []}
newItem={this.state.newAlias} newItem={this.state.newAlias}
onNewItemChanged={this.onNewAliasChanged} onNewItemChanged={this.onNewAliasChanged}
canEdit={this.props.canSetAliases}
onItemAdded={this.onLocalAliasAdded} onItemAdded={this.onLocalAliasAdded}
onItemEdited={this.onLocalAliasChanged} onItemEdited={this.onLocalAliasChanged}
onItemRemoved={this.onLocalAliasDeleted} onItemRemoved={this.onLocalAliasDeleted}

View file

@ -27,7 +27,7 @@ module.exports = React.createClass({
propTypes: { propTypes: {
roomId: React.PropTypes.string.isRequired, roomId: React.PropTypes.string.isRequired,
canSetRelatedRooms: React.PropTypes.bool.isRequired, canSetRelatedGroups: React.PropTypes.bool.isRequired,
relatedGroupsEvent: React.PropTypes.instanceOf(MatrixEvent), relatedGroupsEvent: React.PropTypes.instanceOf(MatrixEvent),
}, },
@ -37,7 +37,7 @@ module.exports = React.createClass({
getDefaultProps: function() { getDefaultProps: function() {
return { return {
canSetRelatedRooms: false, canSetRelatedGroups: false,
}; };
}, },
@ -110,6 +110,7 @@ module.exports = React.createClass({
items={this.state.newGroupsList} items={this.state.newGroupsList}
className={"mx_RelatedGroupSettings"} className={"mx_RelatedGroupSettings"}
newItem={this.state.newGroupId} newItem={this.state.newGroupId}
canEdit={this.props.canSetRelatedGroups}
onNewItemChanged={this.onNewGroupChanged} onNewItemChanged={this.onNewGroupChanged}
onItemAdded={this.onGroupAdded} onItemAdded={this.onGroupAdded}
onItemEdited={this.onGroupEdited} onItemEdited={this.onGroupEdited}

View file

@ -30,10 +30,9 @@ module.exports = React.createClass({
componentWillMount: function() { componentWillMount: function() {
dis.dispatch({ dis.dispatch({
action: 'ui_opacity', action: 'panel_disable',
leftOpacity: 1.0, rightDisabled: true,
rightOpacity: 0.3, middleDisabled: true,
middleOpacity: 0.5,
}); });
}, },
@ -43,9 +42,9 @@ module.exports = React.createClass({
componentWillUnmount: function() { componentWillUnmount: function() {
dis.dispatch({ dis.dispatch({
action: 'ui_opacity', action: 'panel_disable',
sideOpacity: 1.0, sideDisabled: false,
middleOpacity: 1.0, middleDisabled: false,
}); });
document.removeEventListener('keydown', this._onKeyDown); document.removeEventListener('keydown', this._onKeyDown);
}, },

View file

@ -133,8 +133,9 @@ module.exports = React.createClass({
{ p["og:description"] } { p["og:description"] }
</div> </div>
</div> </div>
<img className="mx_LinkPreviewWidget_cancel" src="img/cancel.svg" width="18" height="18" <img className="mx_LinkPreviewWidget_cancel mx_filterFlipColor"
onClick={this.props.onCancelClick} /> src="img/cancel.svg" width="18" height="18"
onClick={this.props.onCancelClick} />
</div> </div>
); );
}, },

View file

@ -39,6 +39,7 @@ import { findReadReceiptFromUserId } from '../../../utils/Receipt';
import withMatrixClient from '../../../wrappers/withMatrixClient'; import withMatrixClient from '../../../wrappers/withMatrixClient';
import AccessibleButton from '../elements/AccessibleButton'; import AccessibleButton from '../elements/AccessibleButton';
import GeminiScrollbar from 'react-gemini-scrollbar'; import GeminiScrollbar from 'react-gemini-scrollbar';
import RoomViewStore from '../../../stores/RoomViewStore';
module.exports = withMatrixClient(React.createClass({ module.exports = withMatrixClient(React.createClass({
@ -81,6 +82,7 @@ module.exports = withMatrixClient(React.createClass({
cli.on("Room.receipt", this.onRoomReceipt); cli.on("Room.receipt", this.onRoomReceipt);
cli.on("RoomState.events", this.onRoomStateEvents); cli.on("RoomState.events", this.onRoomStateEvents);
cli.on("RoomMember.name", this.onRoomMemberName); cli.on("RoomMember.name", this.onRoomMemberName);
cli.on("RoomMember.membership", this.onRoomMemberMembership);
cli.on("accountData", this.onAccountData); cli.on("accountData", this.onAccountData);
this._checkIgnoreState(); this._checkIgnoreState();
@ -91,7 +93,7 @@ module.exports = withMatrixClient(React.createClass({
}, },
componentWillReceiveProps: function(newProps) { componentWillReceiveProps: function(newProps) {
if (this.props.member.userId != newProps.member.userId) { if (this.props.member.userId !== newProps.member.userId) {
this._updateStateForNewMember(newProps.member); this._updateStateForNewMember(newProps.member);
} }
}, },
@ -107,6 +109,7 @@ module.exports = withMatrixClient(React.createClass({
client.removeListener("Room.receipt", this.onRoomReceipt); client.removeListener("Room.receipt", this.onRoomReceipt);
client.removeListener("RoomState.events", this.onRoomStateEvents); client.removeListener("RoomState.events", this.onRoomStateEvents);
client.removeListener("RoomMember.name", this.onRoomMemberName); client.removeListener("RoomMember.name", this.onRoomMemberName);
client.removeListener("RoomMember.membership", this.onRoomMemberMembership);
client.removeListener("accountData", this.onAccountData); client.removeListener("accountData", this.onAccountData);
} }
if (this._cancelDeviceList) { if (this._cancelDeviceList) {
@ -122,12 +125,12 @@ module.exports = withMatrixClient(React.createClass({
_disambiguateDevices: function(devices) { _disambiguateDevices: function(devices) {
const names = Object.create(null); const names = Object.create(null);
for (let i = 0; i < devices.length; i++) { for (let i = 0; i < devices.length; i++) {
var name = devices[i].getDisplayName(); const name = devices[i].getDisplayName();
const indexList = names[name] || []; const indexList = names[name] || [];
indexList.push(i); indexList.push(i);
names[name] = indexList; names[name] = indexList;
} }
for (name in names) { for (const name in names) {
if (names[name].length > 1) { if (names[name].length > 1) {
names[name].forEach((j)=>{ names[name].forEach((j)=>{
devices[j].ambiguous = true; devices[j].ambiguous = true;
@ -141,7 +144,7 @@ module.exports = withMatrixClient(React.createClass({
return; return;
} }
if (userId == this.props.member.userId) { if (userId === this.props.member.userId) {
// no need to re-download the whole thing; just update our copy of // no need to re-download the whole thing; just update our copy of
// the list. // the list.
@ -186,8 +189,12 @@ module.exports = withMatrixClient(React.createClass({
this.forceUpdate(); this.forceUpdate();
}, },
onRoomMemberMembership: function(ev, member) {
if (this.props.member.userId === member.userId) this.forceUpdate();
},
onAccountData: function(ev) { onAccountData: function(ev) {
if (ev.getType() == 'm.direct') { if (ev.getType() === 'm.direct') {
this.forceUpdate(); this.forceUpdate();
} }
}, },
@ -242,7 +249,9 @@ module.exports = withMatrixClient(React.createClass({
ignoredUsers.push(this.props.member.userId); ignoredUsers.push(this.props.member.userId);
} }
this.props.matrixClient.setIgnoredUsers(ignoredUsers).then(() => this.setState({isIgnoring: !this.state.isIgnoring})); this.props.matrixClient.setIgnoredUsers(ignoredUsers).then(() => {
return this.setState({isIgnoring: !this.state.isIgnoring});
});
}, },
onKick: function() { onKick: function() {
@ -252,7 +261,7 @@ module.exports = withMatrixClient(React.createClass({
Modal.createTrackedDialog('Confirm User Action Dialog', 'onKick', ConfirmUserActionDialog, { Modal.createTrackedDialog('Confirm User Action Dialog', 'onKick', ConfirmUserActionDialog, {
member: this.props.member, member: this.props.member,
action: kickLabel, action: kickLabel,
askReason: membership == "join", askReason: membership === "join",
danger: true, danger: true,
onFinished: (proceed, reason) => { onFinished: (proceed, reason) => {
if (!proceed) return; if (!proceed) return;
@ -284,15 +293,15 @@ module.exports = withMatrixClient(React.createClass({
const ConfirmUserActionDialog = sdk.getComponent("dialogs.ConfirmUserActionDialog"); const ConfirmUserActionDialog = sdk.getComponent("dialogs.ConfirmUserActionDialog");
Modal.createTrackedDialog('Confirm User Action Dialog', 'onBanOrUnban', ConfirmUserActionDialog, { Modal.createTrackedDialog('Confirm User Action Dialog', 'onBanOrUnban', ConfirmUserActionDialog, {
member: this.props.member, member: this.props.member,
action: this.props.member.membership == 'ban' ? _t("Unban") : _t("Ban"), action: this.props.member.membership === 'ban' ? _t("Unban") : _t("Ban"),
askReason: this.props.member.membership != 'ban', askReason: this.props.member.membership !== 'ban',
danger: this.props.member.membership != 'ban', danger: this.props.member.membership !== 'ban',
onFinished: (proceed, reason) => { onFinished: (proceed, reason) => {
if (!proceed) return; if (!proceed) return;
this.setState({ updating: this.state.updating + 1 }); this.setState({ updating: this.state.updating + 1 });
let promise; let promise;
if (this.props.member.membership == 'ban') { if (this.props.member.membership === 'ban') {
promise = this.props.matrixClient.unban( promise = this.props.matrixClient.unban(
this.props.member.roomId, this.props.member.userId, this.props.member.roomId, this.props.member.userId,
); );
@ -327,15 +336,11 @@ module.exports = withMatrixClient(React.createClass({
const roomId = this.props.member.roomId; const roomId = this.props.member.roomId;
const target = this.props.member.userId; const target = this.props.member.userId;
const room = this.props.matrixClient.getRoom(roomId); const room = this.props.matrixClient.getRoom(roomId);
if (!room) { if (!room) return;
return;
} const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", "");
const powerLevelEvent = room.currentState.getStateEvents( if (!powerLevelEvent) return;
"m.room.power_levels", "",
);
if (!powerLevelEvent) {
return;
}
const isMuted = this.state.muted; const isMuted = this.state.muted;
const powerLevels = powerLevelEvent.getContent(); const powerLevels = powerLevelEvent.getContent();
const levelToSend = ( const levelToSend = (
@ -350,7 +355,7 @@ module.exports = withMatrixClient(React.createClass({
} }
level = parseInt(level); level = parseInt(level);
if (level !== NaN) { if (!isNaN(level)) {
this.setState({ updating: this.state.updating + 1 }); this.setState({ updating: this.state.updating + 1 });
this.props.matrixClient.setPowerLevel(roomId, target, level, powerLevelEvent).then( this.props.matrixClient.setPowerLevel(roomId, target, level, powerLevelEvent).then(
function() { function() {
@ -375,19 +380,14 @@ module.exports = withMatrixClient(React.createClass({
const roomId = this.props.member.roomId; const roomId = this.props.member.roomId;
const target = this.props.member.userId; const target = this.props.member.userId;
const room = this.props.matrixClient.getRoom(roomId); const room = this.props.matrixClient.getRoom(roomId);
if (!room) { if (!room) return;
return;
} const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", "");
const powerLevelEvent = room.currentState.getStateEvents( if (!powerLevelEvent) return;
"m.room.power_levels", "",
);
if (!powerLevelEvent) {
return;
}
const me = room.getMember(this.props.matrixClient.credentials.userId); const me = room.getMember(this.props.matrixClient.credentials.userId);
if (!me) { if (!me) return;
return;
}
const defaultLevel = powerLevelEvent.getContent().users_default; const defaultLevel = powerLevelEvent.getContent().users_default;
let modLevel = me.powerLevel - 1; let modLevel = me.powerLevel - 1;
if (modLevel > 50 && defaultLevel < 50) modLevel = 50; // try to stick with the vector level defaults if (modLevel > 50 && defaultLevel < 50) modLevel = 50; // try to stick with the vector level defaults
@ -400,7 +400,7 @@ module.exports = withMatrixClient(React.createClass({
// get out of sync if we force setState here! // get out of sync if we force setState here!
console.log("Mod toggle success"); console.log("Mod toggle success");
}, function(err) { }, function(err) {
if (err.errcode == 'M_GUEST_ACCESS_FORBIDDEN') { if (err.errcode === 'M_GUEST_ACCESS_FORBIDDEN') {
dis.dispatch({action: 'view_set_mxid'}); dis.dispatch({action: 'view_set_mxid'});
} else { } else {
console.error("Toggle moderator error:" + err); console.error("Toggle moderator error:" + err);
@ -436,7 +436,6 @@ module.exports = withMatrixClient(React.createClass({
}, },
onPowerChange: function(powerLevel) { onPowerChange: function(powerLevel) {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
const roomId = this.props.member.roomId; const roomId = this.props.member.roomId;
const target = this.props.member.userId; const target = this.props.member.userId;
const room = this.props.matrixClient.getRoom(roomId); const room = this.props.matrixClient.getRoom(roomId);
@ -497,19 +496,14 @@ module.exports = withMatrixClient(React.createClass({
modifyLevel: false, modifyLevel: false,
}; };
const room = this.props.matrixClient.getRoom(member.roomId); const room = this.props.matrixClient.getRoom(member.roomId);
if (!room) { if (!room) return defaultPerms;
return defaultPerms;
} const powerLevels = room.currentState.getStateEvents("m.room.power_levels", "");
const powerLevels = room.currentState.getStateEvents( if (!powerLevels) return defaultPerms;
"m.room.power_levels", "",
);
if (!powerLevels) {
return defaultPerms;
}
const me = room.getMember(this.props.matrixClient.credentials.userId); const me = room.getMember(this.props.matrixClient.credentials.userId);
if (!me) { if (!me) return defaultPerms;
return defaultPerms;
}
const them = member; const them = member;
return { return {
can: this._calculateCanPermissions( can: this._calculateCanPermissions(
@ -545,14 +539,13 @@ module.exports = withMatrixClient(React.createClass({
can.ban = me.powerLevel >= powerLevels.ban; can.ban = me.powerLevel >= powerLevels.ban;
can.mute = me.powerLevel >= editPowerLevel; can.mute = me.powerLevel >= editPowerLevel;
can.toggleMod = me.powerLevel > them.powerLevel && them.powerLevel >= levelToSend; can.toggleMod = me.powerLevel > them.powerLevel && them.powerLevel >= levelToSend;
can.modifyLevel = me.powerLevel > them.powerLevel; can.modifyLevel = me.powerLevel > them.powerLevel && me.powerLevel >= editPowerLevel;
return can; return can;
}, },
_isMuted: function(member, powerLevelContent) { _isMuted: function(member, powerLevelContent) {
if (!powerLevelContent || !member) { if (!powerLevelContent || !member) return false;
return false;
}
const levelToSend = ( const levelToSend = (
(powerLevelContent.events ? powerLevelContent.events["m.room.message"] : null) || (powerLevelContent.events ? powerLevelContent.events["m.room.message"] : null) ||
powerLevelContent.events_default powerLevelContent.events_default
@ -568,14 +561,15 @@ module.exports = withMatrixClient(React.createClass({
}, },
onMemberAvatarClick: function() { onMemberAvatarClick: function() {
const avatarUrl = this.props.member.user ? this.props.member.user.avatarUrl : this.props.member.events.member.getContent().avatar_url; const member = this.props.member;
const avatarUrl = member.user ? member.user.avatarUrl : member.events.member.getContent().avatar_url;
if(!avatarUrl) return; if(!avatarUrl) return;
const httpUrl = this.props.matrixClient.mxcUrlToHttp(avatarUrl); const httpUrl = this.props.matrixClient.mxcUrlToHttp(avatarUrl);
const ImageView = sdk.getComponent("elements.ImageView"); const ImageView = sdk.getComponent("elements.ImageView");
const params = { const params = {
src: httpUrl, src: httpUrl,
name: this.props.member.name, name: member.name,
}; };
Modal.createDialog(ImageView, params, "mx_Dialog_lightbox"); Modal.createDialog(ImageView, params, "mx_Dialog_lightbox");
@ -589,9 +583,7 @@ module.exports = withMatrixClient(React.createClass({
}, },
_renderDevices: function() { _renderDevices: function() {
if (!this._enableDevices) { if (!this._enableDevices) return null;
return null;
}
const devices = this.state.devices; const devices = this.state.devices;
const MemberDeviceInfo = sdk.getComponent('rooms.MemberDeviceInfo'); const MemberDeviceInfo = sdk.getComponent('rooms.MemberDeviceInfo');
@ -629,6 +621,8 @@ module.exports = withMatrixClient(React.createClass({
const member = this.props.member; const member = this.props.member;
let ignoreButton = null; let ignoreButton = null;
let insertPillButton = null;
let inviteUserButton = null;
let readReceiptButton = null; let readReceiptButton = null;
// Only allow the user to ignore the user if its not ourselves // Only allow the user to ignore the user if its not ourselves
@ -653,29 +647,71 @@ module.exports = withMatrixClient(React.createClass({
}); });
}; };
const onInsertPillButton = function() {
dis.dispatch({
action: 'insert_mention',
user_id: member.userId,
});
};
readReceiptButton = ( readReceiptButton = (
<AccessibleButton onClick={onReadReceiptButton} className="mx_MemberInfo_field"> <AccessibleButton onClick={onReadReceiptButton} className="mx_MemberInfo_field">
{ _t('Jump to read receipt') } { _t('Jump to read receipt') }
</AccessibleButton> </AccessibleButton>
); );
insertPillButton = (
<AccessibleButton onClick={onInsertPillButton} className={"mx_MemberInfo_field"}>
{ _t('Mention') }
</AccessibleButton>
);
}
if (!member || !member.membership || member.membership === 'leave') {
const roomId = member && member.roomId ? member.roomId : RoomViewStore.getRoomId();
const onInviteUserButton = async () => {
try {
await cli.invite(roomId, member.userId);
} catch (err) {
const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog');
Modal.createTrackedDialog('Failed to invite', '', ErrorDialog, {
title: _t('Failed to invite'),
description: ((err && err.message) ? err.message : "Operation failed"),
});
}
};
inviteUserButton = (
<AccessibleButton onClick={onInviteUserButton} className="mx_MemberInfo_field">
{ _t('Invite') }
</AccessibleButton>
);
} }
} }
if (!ignoreButton && !readReceiptButton) return null; if (!ignoreButton && !readReceiptButton && !insertPillButton && !inviteUserButton) return null;
return ( return (
<div> <div>
<h3>{ _t("User Options") }</h3> <h3>{ _t("User Options") }</h3>
<div className="mx_MemberInfo_buttons"> <div className="mx_MemberInfo_buttons">
{ readReceiptButton } { readReceiptButton }
{ insertPillButton }
{ ignoreButton } { ignoreButton }
{ inviteUserButton }
</div> </div>
</div> </div>
); );
}, },
render: function() { render: function() {
let startChat, kickButton, banButton, muteButton, giveModButton, spinner; let startChat;
let kickButton;
let banButton;
let muteButton;
let giveModButton;
let spinner;
if (this.props.member.userId !== this.props.matrixClient.credentials.userId) { if (this.props.member.userId !== this.props.matrixClient.credentials.userId) {
const dmRoomMap = new DMRoomMap(this.props.matrixClient); const dmRoomMap = new DMRoomMap(this.props.matrixClient);
const dmRooms = dmRoomMap.getDMRoomsForUserId(this.props.member.userId); const dmRooms = dmRoomMap.getDMRoomsForUserId(this.props.member.userId);
@ -689,7 +725,7 @@ module.exports = withMatrixClient(React.createClass({
const me = room.getMember(this.props.matrixClient.credentials.userId); const me = room.getMember(this.props.matrixClient.credentials.userId);
const highlight = ( const highlight = (
room.getUnreadNotificationCount('highlight') > 0 || room.getUnreadNotificationCount('highlight') > 0 ||
me.membership == "invite" me.membership === "invite"
); );
tiles.push( tiles.push(
<RoomTile key={room.roomId} room={room} <RoomTile key={room.roomId} room={room}
@ -697,7 +733,7 @@ module.exports = withMatrixClient(React.createClass({
selected={false} selected={false}
unread={Unread.doesRoomHaveUnreadMessages(room)} unread={Unread.doesRoomHaveUnreadMessages(room)}
highlight={highlight} highlight={highlight}
isInvite={me.membership == "invite"} isInvite={me.membership === "invite"}
onClick={this.onRoomTileClick} onClick={this.onRoomTileClick}
/>, />,
); );
@ -742,7 +778,7 @@ module.exports = withMatrixClient(React.createClass({
} }
if (this.state.can.ban) { if (this.state.can.ban) {
let label = _t("Ban"); let label = _t("Ban");
if (this.props.member.membership == 'ban') { if (this.props.member.membership === 'ban') {
label = _t("Unban"); label = _t("Unban");
} }
banButton = ( banButton = (
@ -768,9 +804,6 @@ module.exports = withMatrixClient(React.createClass({
</AccessibleButton>; </AccessibleButton>;
} }
// TODO: we should have an invite button if this MemberInfo is showing a user who isn't actually in the current room yet
// e.g. clicking on a linkified userid in a room
let adminTools; let adminTools;
if (kickButton || banButton || muteButton || giveModButton) { if (kickButton || banButton || muteButton || giveModButton) {
adminTools = adminTools =
@ -788,16 +821,39 @@ module.exports = withMatrixClient(React.createClass({
const memberName = this.props.member.name; const memberName = this.props.member.name;
let presenceState;
let presenceLastActiveAgo;
let presenceCurrentlyActive;
if (this.props.member.user) { if (this.props.member.user) {
var presenceState = this.props.member.user.presence; presenceState = this.props.member.user.presence;
var presenceLastActiveAgo = this.props.member.user.lastActiveAgo; presenceLastActiveAgo = this.props.member.user.lastActiveAgo;
const presenceLastTs = this.props.member.user.lastPresenceTs; presenceCurrentlyActive = this.props.member.user.currentlyActive;
var presenceCurrentlyActive = this.props.member.user.currentlyActive; }
let roomMemberDetails = null;
if (this.props.member.roomId) { // is in room
const PowerSelector = sdk.getComponent('elements.PowerSelector');
const PresenceLabel = sdk.getComponent('rooms.PresenceLabel');
roomMemberDetails = <div>
<div className="mx_MemberInfo_profileField">
{ _t("Level:") } <b>
<PowerSelector controlled={true}
value={parseInt(this.props.member.powerLevel)}
disabled={!this.state.can.modifyLevel}
onChange={this.onPowerChange} />
</b>
</div>
<div className="mx_MemberInfo_profileField">
<PresenceLabel activeAgo={presenceLastActiveAgo}
currentlyActive={presenceCurrentlyActive}
presenceState={presenceState} />
</div>
</div>;
} }
const MemberAvatar = sdk.getComponent('avatars.MemberAvatar'); const MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
const PowerSelector = sdk.getComponent('elements.PowerSelector');
const PresenceLabel = sdk.getComponent('rooms.PresenceLabel');
const EmojiText = sdk.getComponent('elements.EmojiText'); const EmojiText = sdk.getComponent('elements.EmojiText');
return ( return (
<div className="mx_MemberInfo"> <div className="mx_MemberInfo">
@ -813,14 +869,7 @@ module.exports = withMatrixClient(React.createClass({
<div className="mx_MemberInfo_profileField"> <div className="mx_MemberInfo_profileField">
{ this.props.member.userId } { this.props.member.userId }
</div> </div>
<div className="mx_MemberInfo_profileField"> { roomMemberDetails }
{ _t("Level:") } <b><PowerSelector controlled={true} value={parseInt(this.props.member.powerLevel)} disabled={!this.state.can.modifyLevel} onChange={this.onPowerChange} /></b>
</div>
<div className="mx_MemberInfo_profileField">
<PresenceLabel activeAgo={presenceLastActiveAgo}
currentlyActive={presenceCurrentlyActive}
presenceState={presenceState} />
</div>
</div> </div>
{ this._renderUserOptions() } { this._renderUserOptions() }

View file

@ -371,7 +371,7 @@ export default class MessageComposer extends React.Component {
); );
return ( return (
<div className="mx_MessageComposer mx_fadable" style={{ opacity: this.props.opacity }}> <div className="mx_MessageComposer">
<div className="mx_MessageComposer_wrapper"> <div className="mx_MessageComposer_wrapper">
<div className="mx_MessageComposer_row"> <div className="mx_MessageComposer_row">
{ controls } { controls }
@ -410,9 +410,6 @@ MessageComposer.propTypes = {
// callback when a file to upload is chosen // callback when a file to upload is chosen
uploadFile: React.PropTypes.func.isRequired, uploadFile: React.PropTypes.func.isRequired,
// opacity for dynamic UI fading effects
opacity: React.PropTypes.number,
// string representing the current room app drawer state // string representing the current room app drawer state
showApps: React.PropTypes.bool, showApps: React.PropTypes.bool,
}; };

View file

@ -95,7 +95,9 @@ module.exports = React.createClass({
return ( return (
<div className="mx_PinnedEventsPanel"> <div className="mx_PinnedEventsPanel">
<div className="mx_PinnedEventsPanel_body"> <div className="mx_PinnedEventsPanel_body">
<AccessibleButton className="mx_PinnedEventsPanel_cancel" onClick={this.props.onCancelClick}><img src="img/cancel.svg" width="18" height="18" /></AccessibleButton> <AccessibleButton className="mx_PinnedEventsPanel_cancel" onClick={this.props.onCancelClick}>
<img className="mx_filterFlipColor" src="img/cancel.svg" width="18" height="18" />
</AccessibleButton>
<h3 className="mx_PinnedEventsPanel_header">{ _t("Pinned Messages") }</h3> <h3 className="mx_PinnedEventsPanel_header">{ _t("Pinned Messages") }</h3>
{ tiles } { tiles }
</div> </div>

View file

@ -25,15 +25,28 @@ import MatrixClientPeg from '../../../MatrixClientPeg';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
function getDisplayAliasForRoom(room) { function getDisplayAliasForRoom(room) {
return room.canonical_alias || (room.aliases ? room.aliases[0] : ""); return room.canonicalAlias || (room.aliases ? room.aliases[0] : "");
} }
const RoomDetailRow = React.createClass({ const RoomDetailRow = React.createClass({
propTypes: PropTypes.shape({
name: PropTypes.string,
topic: PropTypes.string,
roomId: PropTypes.string,
avatarUrl: PropTypes.string,
numJoinedMembers: PropTypes.number,
canonicalAlias: PropTypes.string,
aliases: PropTypes.arrayOf(PropTypes.string),
worldReadable: PropTypes.bool,
guestCanJoin: PropTypes.bool,
}),
onClick: function(ev) { onClick: function(ev) {
ev.preventDefault(); ev.preventDefault();
dis.dispatch({ dis.dispatch({
action: 'view_room', action: 'view_room',
room_id: this.props.room.room_id, room_id: this.props.room.roomId,
}); });
}, },
@ -50,10 +63,10 @@ const RoomDetailRow = React.createClass({
const name = room.name || getDisplayAliasForRoom(room) || _t('Unnamed room'); const name = room.name || getDisplayAliasForRoom(room) || _t('Unnamed room');
const topic = linkifyString(sanitizeHtml(room.topic || '')); const topic = linkifyString(sanitizeHtml(room.topic || ''));
const guestRead = room.world_readable ? ( const guestRead = room.worldReadable ? (
<div className="mx_RoomDirectory_perm">{ _t('World readable') }</div> <div className="mx_RoomDirectory_perm">{ _t('World readable') }</div>
) : <div />; ) : <div />;
const guestJoin = room.guest_can_join ? ( const guestJoin = room.guestCanJoin ? (
<div className="mx_RoomDirectory_perm">{ _t('Guests can join') }</div> <div className="mx_RoomDirectory_perm">{ _t('Guests can join') }</div>
) : <div />; ) : <div />;
@ -62,13 +75,13 @@ const RoomDetailRow = React.createClass({
{ guestJoin } { guestJoin }
</div>) : <div />; </div>) : <div />;
return <tr key={room.room_id} onClick={this.onClick}> return <tr key={room.roomId} onClick={this.onClick}>
<td className="mx_RoomDirectory_roomAvatar"> <td className="mx_RoomDirectory_roomAvatar">
<BaseAvatar width={24} height={24} resizeMethod='crop' <BaseAvatar width={24} height={24} resizeMethod='crop'
name={name} idName={name} name={name} idName={name}
url={ContentRepo.getHttpUriForMxc( url={ContentRepo.getHttpUriForMxc(
MatrixClientPeg.get().getHomeserverUrl(), MatrixClientPeg.get().getHomeserverUrl(),
room.avatar_url, 24, 24, "crop")} /> room.avatarUrl, 24, 24, "crop")} />
</td> </td>
<td className="mx_RoomDirectory_roomDescription"> <td className="mx_RoomDirectory_roomDescription">
<div className="mx_RoomDirectory_name">{ name }</div>&nbsp; <div className="mx_RoomDirectory_name">{ name }</div>&nbsp;
@ -79,7 +92,7 @@ const RoomDetailRow = React.createClass({
<div className="mx_RoomDirectory_alias">{ getDisplayAliasForRoom(room) }</div> <div className="mx_RoomDirectory_alias">{ getDisplayAliasForRoom(room) }</div>
</td> </td>
<td className="mx_RoomDirectory_roomMemberCount"> <td className="mx_RoomDirectory_roomMemberCount">
{ room.num_joined_members } { room.numJoinedMembers }
</td> </td>
</tr>; </tr>;
}, },
@ -92,13 +105,14 @@ export default React.createClass({
rooms: PropTypes.arrayOf(PropTypes.shape({ rooms: PropTypes.arrayOf(PropTypes.shape({
name: PropTypes.string, name: PropTypes.string,
topic: PropTypes.string, topic: PropTypes.string,
room_id: PropTypes.string, roomId: PropTypes.string,
num_joined_members: PropTypes.number, avatarUrl: PropTypes.string,
canonical_alias: PropTypes.string, numJoinedMembers: PropTypes.number,
canonicalAlias: PropTypes.string,
aliases: PropTypes.arrayOf(PropTypes.string), aliases: PropTypes.arrayOf(PropTypes.string),
world_readable: PropTypes.bool, worldReadable: PropTypes.bool,
guest_can_join: PropTypes.bool, guestCanJoin: PropTypes.bool,
})), })),
}, },

View file

@ -281,8 +281,11 @@ module.exports = React.createClass({
<input id="avatarInput" type="file" onChange={this.onAvatarSelected} /> <input id="avatarInput" type="file" onChange={this.onAvatarSelected} />
</div> </div>
<div className="mx_RoomHeader_avatarPicker_remove" onClick={this.onAvatarRemoveClick}> <div className="mx_RoomHeader_avatarPicker_remove" onClick={this.onAvatarRemoveClick}>
<img src="img/cancel.svg" width="10" <img src="img/cancel.svg"
alt={_t("Remove avatar")} title={_t("Remove avatar")} /> className="mx_filterFlipColor"
width="10"
alt={_t("Remove avatar")}
title={_t("Remove avatar")} />
</div> </div>
</div> </div>
); );

View file

@ -19,7 +19,6 @@ import Promise from 'bluebird';
import React from 'react'; import React from 'react';
import { _t, _tJsx, _td } from '../../../languageHandler'; import { _t, _tJsx, _td } from '../../../languageHandler';
import MatrixClientPeg from '../../../MatrixClientPeg'; import MatrixClientPeg from '../../../MatrixClientPeg';
import SdkConfig from '../../../SdkConfig';
import sdk from '../../../index'; import sdk from '../../../index';
import Modal from '../../../Modal'; import Modal from '../../../Modal';
import ObjectUtils from '../../../ObjectUtils'; import ObjectUtils from '../../../ObjectUtils';
@ -158,9 +157,9 @@ module.exports = React.createClass({
}); });
dis.dispatch({ dis.dispatch({
action: 'ui_opacity', action: 'panel_disable',
sideOpacity: 0.3, sideDisabled: true,
middleOpacity: 0.3, middleDisabled: true,
}); });
}, },
@ -171,9 +170,9 @@ module.exports = React.createClass({
} }
dis.dispatch({ dis.dispatch({
action: 'ui_opacity', action: 'panel_disable',
sideOpacity: 1.0, sideDisabled: false,
middleOpacity: 1.0, middleDisabled: false,
}); });
}, },

View file

@ -18,10 +18,13 @@ limitations under the License.
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import classNames from 'classnames';
import sdk from '../../../index'; import sdk from '../../../index';
import dis from '../../../dispatcher'; import dis from '../../../dispatcher';
import UserSettingsStore from '../../../UserSettingsStore';
module.exports = React.createClass({ module.exports = React.createClass({
displayName: 'VideoView', displayName: 'VideoView',
@ -108,14 +111,18 @@ module.exports = React.createClass({
document.mozFullScreenElement || document.mozFullScreenElement ||
document.webkitFullscreenElement); document.webkitFullscreenElement);
const maxVideoHeight = fullscreenElement ? null : this.props.maxHeight; const maxVideoHeight = fullscreenElement ? null : this.props.maxHeight;
const localVideoFeedClasses = classNames("mx_VideoView_localVideoFeed",
{ "mx_VideoView_localVideoFeed_flipped":
UserSettingsStore.getSyncedSetting('VideoView.flipVideoHorizontally', false),
},
);
return ( return (
<div className="mx_VideoView" ref={this.setContainer} onClick={this.props.onClick}> <div className="mx_VideoView" ref={this.setContainer} onClick={this.props.onClick}>
<div className="mx_VideoView_remoteVideoFeed"> <div className="mx_VideoView_remoteVideoFeed">
<VideoFeed ref="remote" onResize={this.props.onResize} <VideoFeed ref="remote" onResize={this.props.onResize}
maxHeight={maxVideoHeight} /> maxHeight={maxVideoHeight} />
</div> </div>
<div className="mx_VideoView_localVideoFeed"> <div className={localVideoFeedClasses}>
<VideoFeed ref="local" /> <VideoFeed ref="local" />
</div> </div>
</div> </div>

View file

@ -43,5 +43,9 @@ export function groupRoomFromApiObject(apiObject) {
roomId: apiObject.room_id, roomId: apiObject.room_id,
canonicalAlias: apiObject.canonical_alias, canonicalAlias: apiObject.canonical_alias,
avatarUrl: apiObject.avatar_url, avatarUrl: apiObject.avatar_url,
topic: apiObject.topic,
numJoinedMembers: apiObject.num_joined_members,
worldReadable: apiObject.world_readable,
guestCanJoin: apiObject.guest_can_join,
}; };
} }

View file

@ -54,8 +54,6 @@
"Room name or alias": "Room name or alias", "Room name or alias": "Room name or alias",
"Add to community": "Add to community", "Add to community": "Add to community",
"Failed to invite the following users to %(groupId)s:": "Failed to invite the following users to %(groupId)s:", "Failed to invite the following users to %(groupId)s:": "Failed to invite the following users to %(groupId)s:",
"Invites sent": "Invites sent",
"Your community invitations have been sent.": "Your community invitations have been sent.",
"Failed to invite users to community": "Failed to invite users to community", "Failed to invite users to community": "Failed to invite users to community",
"Failed to invite users to %(groupId)s": "Failed to invite users to %(groupId)s", "Failed to invite users to %(groupId)s": "Failed to invite users to %(groupId)s",
"Failed to add the following rooms to %(groupId)s:": "Failed to add the following rooms to %(groupId)s:", "Failed to add the following rooms to %(groupId)s:": "Failed to add the following rooms to %(groupId)s:",
@ -154,8 +152,10 @@
"%(widgetName)s widget removed by %(senderName)s": "%(widgetName)s widget removed by %(senderName)s", "%(widgetName)s widget removed by %(senderName)s": "%(widgetName)s widget removed by %(senderName)s",
"Communities": "Communities", "Communities": "Communities",
"Message Pinning": "Message Pinning", "Message Pinning": "Message Pinning",
"Mention": "Mention",
"%(displayName)s is typing": "%(displayName)s is typing", "%(displayName)s is typing": "%(displayName)s is typing",
"%(names)s and one other are typing": "%(names)s and one other are typing", "%(names)s and one other are typing": "%(names)s and one other are typing",
"%(names)s and %(count)s others are typing|other": "%(names)s and %(count)s others are typing",
"%(names)s and %(lastPerson)s are typing": "%(names)s and %(lastPerson)s are typing", "%(names)s and %(lastPerson)s are typing": "%(names)s and %(lastPerson)s are typing",
"Failure to create room": "Failure to create room", "Failure to create room": "Failure to create room",
"Server may be unavailable, overloaded, or you hit a bug.": "Server may be unavailable, overloaded, or you hit a bug.", "Server may be unavailable, overloaded, or you hit a bug.": "Server may be unavailable, overloaded, or you hit a bug.",
@ -240,6 +240,7 @@
"Unignore": "Unignore", "Unignore": "Unignore",
"Ignore": "Ignore", "Ignore": "Ignore",
"Jump to read receipt": "Jump to read receipt", "Jump to read receipt": "Jump to read receipt",
"Invite": "Invite",
"User Options": "User Options", "User Options": "User Options",
"Direct chats": "Direct chats", "Direct chats": "Direct chats",
"Unmute": "Unmute", "Unmute": "Unmute",
@ -451,6 +452,7 @@
"You are about to be taken to a third-party site so you can authenticate your account for use with %(integrationsUrl)s. Do you wish to continue?": "You are about to be taken to a third-party site so you can authenticate your account for use with %(integrationsUrl)s. Do you wish to continue?", "You are about to be taken to a third-party site so you can authenticate your account for use with %(integrationsUrl)s. Do you wish to continue?": "You are about to be taken to a third-party site so you can authenticate your account for use with %(integrationsUrl)s. Do you wish to continue?",
"Removed or unknown message type": "Removed or unknown message type", "Removed or unknown message type": "Removed or unknown message type",
"Message removed by %(userId)s": "Message removed by %(userId)s", "Message removed by %(userId)s": "Message removed by %(userId)s",
"Message removed": "Message removed",
"Robot check is currently unavailable on desktop - please use a <a>web browser</a>": "Robot check is currently unavailable on desktop - please use a <a>web browser</a>", "Robot check is currently unavailable on desktop - please use a <a>web browser</a>": "Robot check is currently unavailable on desktop - please use a <a>web browser</a>",
"This Home Server would like to make sure you are not a robot": "This Home Server would like to make sure you are not a robot", "This Home Server would like to make sure you are not a robot": "This Home Server would like to make sure you are not a robot",
"Sign in with CAS": "Sign in with CAS", "Sign in with CAS": "Sign in with CAS",
@ -495,10 +497,13 @@
"Are you sure you want to remove '%(roomName)s' from %(groupId)s?": "Are you sure you want to remove '%(roomName)s' from %(groupId)s?", "Are you sure you want to remove '%(roomName)s' from %(groupId)s?": "Are you sure you want to remove '%(roomName)s' from %(groupId)s?",
"Removing a room from the community will also remove it from the community page.": "Removing a room from the community will also remove it from the community page.", "Removing a room from the community will also remove it from the community page.": "Removing a room from the community will also remove it from the community page.",
"Remove": "Remove", "Remove": "Remove",
"Remove this room from the community": "Remove this room from the community",
"Unknown Address": "Unknown Address", "Unknown Address": "Unknown Address",
"NOTE: Apps are not end-to-end encrypted": "NOTE: Apps are not end-to-end encrypted", "NOTE: Apps are not end-to-end encrypted": "NOTE: Apps are not end-to-end encrypted",
"Do you want to load widget from URL:": "Do you want to load widget from URL:", "Do you want to load widget from URL:": "Do you want to load widget from URL:",
"Allow": "Allow", "Allow": "Allow",
"Delete Widget": "Delete Widget",
"Deleting a widget removes it for all users in this room. Are you sure you want to delete this widget?": "Deleting a widget removes it for all users in this room. Are you sure you want to delete this widget?",
"Delete widget": "Delete widget", "Delete widget": "Delete widget",
"Revoke widget access": "Revoke widget access", "Revoke widget access": "Revoke widget access",
"Edit": "Edit", "Edit": "Edit",
@ -566,6 +571,7 @@
"Custom level": "Custom level", "Custom level": "Custom level",
"Room directory": "Room directory", "Room directory": "Room directory",
"Start chat": "Start chat", "Start chat": "Start chat",
"And %(count)s more...|other": "And %(count)s more...",
"ex. @bob:example.com": "ex. @bob:example.com", "ex. @bob:example.com": "ex. @bob:example.com",
"Add User": "Add User", "Add User": "Add User",
"Something went wrong!": "Something went wrong!", "Something went wrong!": "Something went wrong!",
@ -584,7 +590,7 @@
"Are you sure you wish to remove (delete) this event? Note that if you delete a room name or topic change, it could undo the change.": "Are you sure you wish to remove (delete) this event? Note that if you delete a room name or topic change, it could undo the change.", "Are you sure you wish to remove (delete) this event? Note that if you delete a room name or topic change, it could undo the change.": "Are you sure you wish to remove (delete) this event? Note that if you delete a room name or topic change, it could undo the change.",
"%(actionVerb)s this person?": "%(actionVerb)s this person?", "%(actionVerb)s this person?": "%(actionVerb)s this person?",
"Community IDs may only contain alphanumeric characters": "Community IDs may only contain alphanumeric characters", "Community IDs may only contain alphanumeric characters": "Community IDs may only contain alphanumeric characters",
"Room creation failed": "Room creation failed", "Something went wrong whilst creating your community": "Something went wrong whilst creating your community",
"Create Community": "Create Community", "Create Community": "Create Community",
"Community Name": "Community Name", "Community Name": "Community Name",
"Example": "Example", "Example": "Example",
@ -677,17 +683,17 @@
"Leave %(groupName)s?": "Leave %(groupName)s?", "Leave %(groupName)s?": "Leave %(groupName)s?",
"Leave": "Leave", "Leave": "Leave",
"Unable to leave room": "Unable to leave room", "Unable to leave room": "Unable to leave room",
"Community Settings": "Community Settings",
"Add rooms to this community": "Add rooms to this community", "Add rooms to this community": "Add rooms to this community",
"Featured Rooms:": "Featured Rooms:", "Featured Rooms:": "Featured Rooms:",
"Featured Users:": "Featured Users:", "Featured Users:": "Featured Users:",
"%(inviter)s has invited you to join this community": "%(inviter)s has invited you to join this community", "%(inviter)s has invited you to join this community": "%(inviter)s has invited you to join this community",
"You are a member of this community": "You are a member of this community",
"You are an administrator of this community": "You are an administrator of this community", "You are an administrator of this community": "You are an administrator of this community",
"You are a member of this community": "You are a member of this community",
"Community Member Settings": "Community Member Settings", "Community Member Settings": "Community Member Settings",
"Publish this community on your profile": "Publish this community on your profile", "Publish this community on your profile": "Publish this community on your profile",
"Long Description (HTML)": "Long Description (HTML)", "Long Description (HTML)": "Long Description (HTML)",
"Description": "Description", "Description": "Description",
"Community Settings": "Community Settings",
"Community %(groupId)s not found": "Community %(groupId)s not found", "Community %(groupId)s not found": "Community %(groupId)s not found",
"This Home server does not support communities": "This Home server does not support communities", "This Home server does not support communities": "This Home server does not support communities",
"Failed to load %(groupId)s": "Failed to load %(groupId)s", "Failed to load %(groupId)s": "Failed to load %(groupId)s",
@ -753,6 +759,7 @@
"Disable Emoji suggestions while typing": "Disable Emoji suggestions while typing", "Disable Emoji suggestions while typing": "Disable Emoji suggestions while typing",
"Hide avatars in user and room mentions": "Hide avatars in user and room mentions", "Hide avatars in user and room mentions": "Hide avatars in user and room mentions",
"Disable big emoji in chat": "Disable big emoji in chat", "Disable big emoji in chat": "Disable big emoji in chat",
"Mirror local video feed": "Mirror local video feed",
"Opt out of analytics": "Opt out of analytics", "Opt out of analytics": "Opt out of analytics",
"Disable Peer-to-Peer for 1:1 calls": "Disable Peer-to-Peer for 1:1 calls", "Disable Peer-to-Peer for 1:1 calls": "Disable Peer-to-Peer for 1:1 calls",
"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 from this device": "Never send encrypted messages to unverified devices from this device",

View file

@ -56,6 +56,10 @@ class FlairStore extends EventEmitter {
return groupSupport; return groupSupport;
} }
invalidatePublicisedGroups(userId) {
delete this._userGroups[userId];
}
getPublicisedGroupsCached(matrixClient, userId) { getPublicisedGroupsCached(matrixClient, userId) {
if (this._userGroups[userId]) { if (this._userGroups[userId]) {
return Promise.resolve(this._userGroups[userId]); return Promise.resolve(this._userGroups[userId]);

View file

@ -15,6 +15,8 @@ limitations under the License.
*/ */
import EventEmitter from 'events'; import EventEmitter from 'events';
import { groupMemberFromApiObject, groupRoomFromApiObject } from '../groups';
import FlairStore from './FlairStore';
/** /**
* Stores the group summary for a room and provides an API to change it and * Stores the group summary for a room and provides an API to change it and
@ -27,8 +29,36 @@ export default class GroupStore extends EventEmitter {
this._matrixClient = matrixClient; this._matrixClient = matrixClient;
this._summary = {}; this._summary = {};
this._rooms = []; this._rooms = [];
this._fetchSummary();
this._fetchRooms(); this.on('error', (err) => {
console.error(`GroupStore for ${this.groupId} encountered error`, err);
});
}
_fetchMembers() {
this._matrixClient.getGroupUsers(this.groupId).then((result) => {
this._members = result.chunk.map((apiMember) => {
return groupMemberFromApiObject(apiMember);
});
this._notifyListeners();
}).catch((err) => {
console.error("Failed to get group member list: " + err);
this.emit('error', err);
});
this._matrixClient.getGroupInvitedUsers(this.groupId).then((result) => {
this._invitedMembers = result.chunk.map((apiMember) => {
return groupMemberFromApiObject(apiMember);
});
this._notifyListeners();
}).catch((err) => {
// Invited users not visible to non-members
if (err.httpStatus === 403) {
return;
}
console.error("Failed to get group invited member list: " + err);
this.emit('error', err);
});
} }
_fetchSummary() { _fetchSummary() {
@ -42,7 +72,9 @@ export default class GroupStore extends EventEmitter {
_fetchRooms() { _fetchRooms() {
this._matrixClient.getGroupRooms(this.groupId).then((resp) => { this._matrixClient.getGroupRooms(this.groupId).then((resp) => {
this._rooms = resp.chunk; this._rooms = resp.chunk.map((apiRoom) => {
return groupRoomFromApiObject(apiRoom);
});
this._notifyListeners(); this._notifyListeners();
}).catch((err) => { }).catch((err) => {
this.emit('error', err); this.emit('error', err);
@ -53,6 +85,17 @@ export default class GroupStore extends EventEmitter {
this.emit('update'); this.emit('update');
} }
registerListener(fn) {
this.on('update', fn);
this._fetchSummary();
this._fetchRooms();
this._fetchMembers();
}
unregisterListener(fn) {
this.removeListener('update', fn);
}
getSummary() { getSummary() {
return this._summary; return this._summary;
} }
@ -61,6 +104,14 @@ export default class GroupStore extends EventEmitter {
return this._rooms; return this._rooms;
} }
getGroupMembers( ) {
return this._members;
}
getGroupInvitedMembers( ) {
return this._invitedMembers;
}
getGroupPublicity() { getGroupPublicity() {
return this._summary.user ? this._summary.user.is_publicised : null; return this._summary.user ? this._summary.user.is_publicised : null;
} }
@ -83,6 +134,11 @@ export default class GroupStore extends EventEmitter {
.then(this._fetchRooms.bind(this)); .then(this._fetchRooms.bind(this));
} }
inviteUserToGroup(userId) {
return this._matrixClient.inviteUserToGroup(this.groupId, userId)
.then(this._fetchMembers.bind(this));
}
addRoomToGroupSummary(roomId, categoryId) { addRoomToGroupSummary(roomId, categoryId) {
return this._matrixClient return this._matrixClient
.addRoomToGroupSummary(this.groupId, roomId, categoryId) .addRoomToGroupSummary(this.groupId, roomId, categoryId)
@ -110,6 +166,7 @@ export default class GroupStore extends EventEmitter {
setGroupPublicity(isPublished) { setGroupPublicity(isPublished) {
return this._matrixClient return this._matrixClient
.setGroupPublicity(this.groupId, isPublished) .setGroupPublicity(this.groupId, isPublished)
.then(() => { FlairStore.invalidatePublicisedGroups(this._matrixClient.credentials.userId); })
.then(this._fetchSummary.bind(this)); .then(this._fetchSummary.bind(this));
} }
} }

View file

@ -33,8 +33,7 @@ class GroupStoreCache {
} }
} }
let singletonGroupStoreCache = null; if (global.singletonGroupStoreCache === undefined) {
if (!singletonGroupStoreCache) { global.singletonGroupStoreCache = new GroupStoreCache();
singletonGroupStoreCache = new GroupStoreCache();
} }
module.exports = singletonGroupStoreCache; export default global.singletonGroupStoreCache;

View file

@ -72,6 +72,13 @@ class RoomViewStore extends Store {
case 'view_room': case 'view_room':
this._viewRoom(payload); this._viewRoom(payload);
break; break;
case 'view_my_groups':
case 'view_group':
this._setState({
roomId: null,
roomAlias: null,
});
break;
case 'view_room_error': case 'view_room_error':
this._viewRoomError(payload); this._viewRoomError(payload);
break; break;

View file

@ -18,6 +18,7 @@ limitations under the License.
import MatrixClientPeg from '../MatrixClientPeg'; import MatrixClientPeg from '../MatrixClientPeg';
import {getAddressType} from '../UserAddress'; import {getAddressType} from '../UserAddress';
import {inviteToRoom} from '../RoomInvite'; import {inviteToRoom} from '../RoomInvite';
import GroupStoreCache from '../stores/GroupStoreCache';
import Promise from 'bluebird'; import Promise from 'bluebird';
/** /**
@ -117,7 +118,9 @@ export default class MultiInviter {
let doInvite; let doInvite;
if (this.groupId !== null) { if (this.groupId !== null) {
doInvite = MatrixClientPeg.get().inviteUserToGroup(this.groupId, addr); doInvite = GroupStoreCache
.getGroupStore(MatrixClientPeg.get(), this.groupId)
.inviteUserToGroup(addr);
} else { } else {
doInvite = inviteToRoom(this.roomId, addr); doInvite = inviteToRoom(this.roomId, addr);
} }

View file

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