Merge pull request #1328 from matrix-org/dbkr/group_userlist

Group Membership UI
This commit is contained in:
Luke Barnard 2017-09-19 14:54:08 +01:00 committed by GitHub
commit a1e3115046
39 changed files with 848 additions and 71 deletions

67
src/GroupInvite.js Normal file
View file

@ -0,0 +1,67 @@
/*
Copyright 2017 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import Modal from './Modal';
import sdk from './';
import MultiInviter from './utils/MultiInviter';
import { _t } from './languageHandler';
export function showGroupInviteDialog(groupId) {
const UserPickerDialog = sdk.getComponent("dialogs.UserPickerDialog");
Modal.createTrackedDialog('Group Invite', '', UserPickerDialog, {
title: _t('Invite new group members'),
description: _t("Who would you like to add to this group?"),
placeholder: _t("Name or matrix ID"),
button: _t("Invite to Group"),
validAddressTypes: ['mx'],
onFinished: (success, addrs) => {
if (!success) return;
_onGroupInviteFinished(groupId, addrs);
},
});
}
function _onGroupInviteFinished(groupId, addrs) {
const multiInviter = new MultiInviter(groupId);
const addrTexts = addrs.map((addr) => addr.address);
multiInviter.invite(addrTexts).then((completionStates) => {
// Show user any errors
const errorList = [];
for (const addr of Object.keys(completionStates)) {
if (addrs[addr] === "error") {
errorList.push(addr);
}
}
if (errorList.length > 0) {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Failed to invite the following users to the group', '', ErrorDialog, {
title: _t("Failed to invite the following users to %(groupId)s:", {groupId: groupId}),
description: errorList.join(", "),
});
}
}).catch((err) => {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Failed to invite users to group', '', ErrorDialog, {
title: _t("Failed to invite users group"),
description: _t("Failed to invite users to %(groupId)s", {groupId: groupId}),
});
});
}

View file

@ -1,5 +1,6 @@
/* /*
Copyright 2017 Vector Creations Ltd. Copyright 2017 Vector Creations Ltd.
Copyright 2017 New Vector Ltd.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -183,12 +184,19 @@ export default React.createClass({
editing: false, editing: false,
saving: false, saving: false,
uploadingAvatar: false, uploadingAvatar: false,
membershipBusy: false,
}; };
}, },
componentWillMount: function() { componentWillMount: function() {
this._changeAvatarComponent = null; this._changeAvatarComponent = null;
this._loadGroupFromServer(this.props.groupId); this._loadGroupFromServer(this.props.groupId);
MatrixClientPeg.get().on("Group.myMembership", this._onGroupMyMembership);
},
componentWillUnmount: function() {
MatrixClientPeg.get().removeListener("Group.myMembership", this._onGroupMyMembership);
}, },
componentWillReceiveProps: function(newProps) { componentWillReceiveProps: function(newProps) {
@ -202,6 +210,12 @@ export default React.createClass({
} }
}, },
_onGroupMyMembership: function(group) {
if (group.groupId !== this.props.groupId) return;
this.setState({membershipBusy: false});
},
_loadGroupFromServer: function(groupId) { _loadGroupFromServer: function(groupId) {
MatrixClientPeg.get().getGroupSummary(groupId).done((res) => { MatrixClientPeg.get().getGroupSummary(groupId).done((res) => {
this.setState({ this.setState({
@ -216,6 +230,10 @@ export default React.createClass({
}); });
}, },
_onShowRhsClick: function(ev) {
dis.dispatch({ action: 'show_right_panel' });
},
_onEditClick: function() { _onEditClick: function() {
this.setState({ this.setState({
editing: true, editing: true,
@ -295,6 +313,59 @@ export default React.createClass({
}).done(); }).done();
}, },
_onAcceptInviteClick: function() {
this.setState({membershipBusy: true});
MatrixClientPeg.get().acceptGroupInvite(this.props.groupId).then(() => {
// don't reset membershipBusy here: wait for the membership change to come down the sync
}).catch((e) => {
this.setState({membershipBusy: false});
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Error accepting invite', '', ErrorDialog, {
title: _t("Error"),
description: _t("Unable to accept invite"),
});
});
},
_onRejectInviteClick: function() {
this.setState({membershipBusy: true});
MatrixClientPeg.get().leaveGroup(this.props.groupId).then(() => {
// don't reset membershipBusy here: wait for the membership change to come down the sync
}).catch((e) => {
this.setState({membershipBusy: false});
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Error rejecting invite', '', ErrorDialog, {
title: _t("Error"),
description: _t("Unable to reject invite"),
});
});
},
_onLeaveClick: function() {
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
Modal.createTrackedDialog('Leave Group', '', QuestionDialog, {
title: _t("Leave Group"),
description: _t("Leave %(groupName)s?", {groupName: this.props.groupId}),
button: _t("Leave"),
danger: true,
onFinished: (confirmed) => {
if (!confirmed) return;
this.setState({membershipBusy: true});
MatrixClientPeg.get().leaveGroup(this.props.groupId).then(() => {
// don't reset membershipBusy here: wait for the membership change to come down the sync
}).catch((e) => {
this.setState({membershipBusy: false});
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Error leaving room', '', ErrorDialog, {
title: _t("Error"),
description: _t("Unable to leave room"),
});
});
},
});
},
_getFeaturedRoomsNode() { _getFeaturedRoomsNode() {
const summary = this.state.summary; const summary = this.state.summary;
@ -371,6 +442,50 @@ export default React.createClass({
</div>; </div>;
}, },
_getMembershipSection: function() {
const group = MatrixClientPeg.get().getGroup(this.props.groupId);
if (!group) return null;
if (group.myMembership === 'invite') {
const Spinner = sdk.getComponent("elements.Spinner");
if (this.state.membershipBusy) {
return <div className="mx_GroupView_invitedSection">
<Spinner />
</div>;
}
return <div className="mx_GroupView_invitedSection">
{_t("%(inviter)s has invited you to join this group", {inviter: group.inviter.userId})}
<div className="mx_GroupView_membership_buttonContainer">
<AccessibleButton className="mx_GroupView_textButton mx_RoomHeader_textButton"
onClick={this._onAcceptInviteClick}
>
{_t("Accept")}
</AccessibleButton>
<AccessibleButton className="mx_GroupView_textButton mx_RoomHeader_textButton"
onClick={this._onRejectInviteClick}
>
{_t("Decline")}
</AccessibleButton>
</div>
</div>;
} else if (group.myMembership === 'join') {
return <div className="mx_GroupView_invitedSection">
{_t("You are a member of this group")}
<div className="mx_GroupView_membership_buttonContainer">
<AccessibleButton className="mx_GroupView_textButton mx_RoomHeader_textButton"
onClick={this._onLeaveClick}
>
{_t("Leave")}
</AccessibleButton>
</div>
</div>;
}
return null;
},
render: function() { render: function() {
const GroupAvatar = sdk.getComponent("avatars.GroupAvatar"); const GroupAvatar = sdk.getComponent("avatars.GroupAvatar");
const Loader = sdk.getComponent("elements.Spinner"); const Loader = sdk.getComponent("elements.Spinner");
@ -384,8 +499,8 @@ export default React.createClass({
let avatarNode; let avatarNode;
let nameNode; let nameNode;
let shortDescNode; let shortDescNode;
let rightButtons;
let roomBody; let roomBody;
const rightButtons = [];
const headerClasses = { const headerClasses = {
mx_GroupView_header: true, mx_GroupView_header: true,
}; };
@ -428,15 +543,19 @@ export default React.createClass({
placeholder={_t('Description')} placeholder={_t('Description')}
tabIndex="2" tabIndex="2"
/>; />;
rightButtons = <span> rightButtons.push(
<AccessibleButton className="mx_GroupView_saveButton mx_RoomHeader_textButton" onClick={this._onSaveClick}> <AccessibleButton className="mx_GroupView_textButton mx_RoomHeader_textButton"
onClick={this._onSaveClick} key="_saveButton"
>
{_t('Save')} {_t('Save')}
</AccessibleButton> </AccessibleButton>,
<AccessibleButton className='mx_GroupView_cancelButton' onClick={this._onCancelClick}> );
rightButtons.push(
<AccessibleButton className='mx_GroupView_textButton' onClick={this._onCancelClick} key="_cancelButton">
<img src="img/cancel.svg" className='mx_filterFlipColor' <img src="img/cancel.svg" className='mx_filterFlipColor'
width="18" height="18" alt={_t("Cancel")}/> width="18" height="18" alt={_t("Cancel")}/>
</AccessibleButton> </AccessibleButton>,
</span>; );
roomBody = <div> roomBody = <div>
<textarea className="mx_GroupView_editLongDesc" value={this.state.profileForm.long_description} <textarea className="mx_GroupView_editLongDesc" value={this.state.profileForm.long_description}
onChange={this._onLongDescChange} onChange={this._onLongDescChange}
@ -467,16 +586,27 @@ export default React.createClass({
description = sanitizedHtmlNode(summary.profile.long_description); description = sanitizedHtmlNode(summary.profile.long_description);
} }
roomBody = <div> roomBody = <div>
{this._getMembershipSection()}
<div className="mx_GroupView_groupDesc">{description}</div> <div className="mx_GroupView_groupDesc">{description}</div>
{this._getFeaturedRoomsNode()} {this._getFeaturedRoomsNode()}
{this._getFeaturedUsersNode()} {this._getFeaturedUsersNode()}
</div>; </div>;
// disabled until editing works rightButtons.push(
rightButtons = <AccessibleButton className="mx_GroupHeader_button" <AccessibleButton className="mx_GroupHeader_button"
onClick={this._onEditClick} title={_t("Edit Group")} onClick={this._onEditClick} title={_t("Edit Group")} key="_editButton"
> >
<TintableSvg src="img/icons-settings-room.svg" width="16" height="16"/> <TintableSvg src="img/icons-settings-room.svg" width="16" height="16"/>
</AccessibleButton>; </AccessibleButton>,
);
if (this.props.collapsedRhs) {
rightButtons.push(
<AccessibleButton className="mx_GroupHeader_button"
onClick={this._onShowRhsClick} title={ _t('Show panel') } key="_maximiseButton"
>
<TintableSvg src="img/maximise.svg" width="10" height="16"/>
</AccessibleButton>,
);
}
headerClasses.mx_GroupView_header_view = true; headerClasses.mx_GroupView_header_view = true;
} }

View file

@ -241,10 +241,10 @@ export default React.createClass({
eventPixelOffset={this.props.initialEventPixelOffset} eventPixelOffset={this.props.initialEventPixelOffset}
key={this.props.currentRoomId || 'roomview'} key={this.props.currentRoomId || 'roomview'}
opacity={this.props.middleOpacity} opacity={this.props.middleOpacity}
collapsedRhs={this.props.collapse_rhs} collapsedRhs={this.props.collapseRhs}
ConferenceHandler={this.props.ConferenceHandler} ConferenceHandler={this.props.ConferenceHandler}
/>; />;
if (!this.props.collapse_rhs) right_panel = <RightPanel roomId={this.props.currentRoomId} opacity={this.props.rightOpacity} />; if (!this.props.collapseRhs) right_panel = <RightPanel roomId={this.props.currentRoomId} opacity={this.props.rightOpacity} />;
break; break;
case PageTypes.UserSettings: case PageTypes.UserSettings:
@ -255,7 +255,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.collapse_rhs) right_panel = <RightPanel opacity={this.props.rightOpacity}/>; if (!this.props.collapseRhs) right_panel = <RightPanel opacity={this.props.rightOpacity}/>;
break; break;
case PageTypes.MyGroups: case PageTypes.MyGroups:
@ -265,9 +265,9 @@ export default React.createClass({
case PageTypes.CreateRoom: case PageTypes.CreateRoom:
page_element = <CreateRoom page_element = <CreateRoom
onRoomCreated={this.props.onRoomCreated} onRoomCreated={this.props.onRoomCreated}
collapsedRhs={this.props.collapse_rhs} collapsedRhs={this.props.collapseRhs}
/>; />;
if (!this.props.collapse_rhs) right_panel = <RightPanel opacity={this.props.rightOpacity}/>; if (!this.props.collapseRhs) right_panel = <RightPanel opacity={this.props.rightOpacity}/>;
break; break;
case PageTypes.RoomDirectory: case PageTypes.RoomDirectory:
@ -300,8 +300,9 @@ export default React.createClass({
case PageTypes.GroupView: case PageTypes.GroupView:
page_element = <GroupView page_element = <GroupView
groupId={this.props.currentGroupId} groupId={this.props.currentGroupId}
collapsedRhs={this.props.collapseRhs}
/>; />;
//right_panel = <RightPanel opacity={this.props.rightOpacity} />; if (!this.props.collapseRhs) right_panel = <RightPanel groupId={this.props.currentGroupId} opacity={this.props.rightOpacity} />;
break; break;
} }
@ -333,7 +334,7 @@ export default React.createClass({
<div className={bodyClasses}> <div className={bodyClasses}>
<LeftPanel <LeftPanel
selectedRoom={this.props.currentRoomId} selectedRoom={this.props.currentRoomId}
collapsed={this.props.collapse_lhs || false} collapsed={this.props.collapseLhs || false}
opacity={this.props.leftOpacity} opacity={this.props.leftOpacity}
/> />
<main className='mx_MatrixChat_middlePanel'> <main className='mx_MatrixChat_middlePanel'>

View file

@ -32,7 +32,7 @@ import dis from "../../dispatcher";
import Modal from "../../Modal"; import Modal from "../../Modal";
import Tinter from "../../Tinter"; import Tinter from "../../Tinter";
import sdk from '../../index'; import sdk from '../../index';
import { showStartChatInviteDialog, showRoomInviteDialog } from '../../Invite'; import { showStartChatInviteDialog, showRoomInviteDialog } from '../../RoomInvite';
import * as Rooms from '../../Rooms'; import * as Rooms from '../../Rooms';
import linkifyMatrix from "../../linkify-matrix"; import linkifyMatrix from "../../linkify-matrix";
import * as Lifecycle from '../../Lifecycle'; import * as Lifecycle from '../../Lifecycle';
@ -143,8 +143,8 @@ module.exports = React.createClass({
// If we're trying to just view a user ID (i.e. /user URL), this is it // If we're trying to just view a user ID (i.e. /user URL), this is it
viewUserId: null, viewUserId: null,
collapse_lhs: false, collapseLhs: false,
collapse_rhs: false, collapseRhs: false,
leftOpacity: 1.0, leftOpacity: 1.0,
middleOpacity: 1.0, middleOpacity: 1.0,
rightOpacity: 1.0, rightOpacity: 1.0,
@ -434,7 +434,7 @@ module.exports = React.createClass({
break; break;
case 'view_user': case 'view_user':
// FIXME: ugly hack to expand the RightPanel and then re-dispatch. // FIXME: ugly hack to expand the RightPanel and then re-dispatch.
if (this.state.collapse_rhs) { if (this.state.collapseRhs) {
setTimeout(()=>{ setTimeout(()=>{
dis.dispatch({ dis.dispatch({
action: 'show_right_panel', action: 'show_right_panel',
@ -516,22 +516,22 @@ module.exports = React.createClass({
break; break;
case 'hide_left_panel': case 'hide_left_panel':
this.setState({ this.setState({
collapse_lhs: true, collapseLhs: true,
}); });
break; break;
case 'show_left_panel': case 'show_left_panel':
this.setState({ this.setState({
collapse_lhs: false, collapseLhs: false,
}); });
break; break;
case 'hide_right_panel': case 'hide_right_panel':
this.setState({ this.setState({
collapse_rhs: true, collapseRhs: true,
}); });
break; break;
case 'show_right_panel': case 'show_right_panel':
this.setState({ this.setState({
collapse_rhs: false, collapseRhs: false,
}); });
break; break;
case 'ui_opacity': { case 'ui_opacity': {
@ -993,8 +993,8 @@ module.exports = React.createClass({
this.setStateForNewView({ this.setStateForNewView({
view: VIEWS.LOGIN, view: VIEWS.LOGIN,
ready: false, ready: false,
collapse_lhs: false, collapseLhs: false,
collapse_rhs: false, collapseRhs: false,
currentRoomId: null, currentRoomId: null,
page_type: PageTypes.RoomDirectory, page_type: PageTypes.RoomDirectory,
}); });

View file

@ -14,10 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
'use strict'; import React from 'react';
import AvatarLogic from '../../../Avatar';
var React = require('react');
var AvatarLogic = require("../../../Avatar");
import sdk from '../../../index'; import sdk from '../../../index';
import AccessibleButton from '../elements/AccessibleButton'; import AccessibleButton from '../elements/AccessibleButton';

View file

@ -18,6 +18,7 @@ import React from 'react';
import sdk from '../../../index'; import sdk from '../../../index';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import classnames from 'classnames'; import classnames from 'classnames';
import { GroupMemberType } from '../../../groups';
/* /*
* A dialog for confirming an operation on another user. * A dialog for confirming an operation on another user.
@ -30,7 +31,10 @@ import classnames from 'classnames';
export default React.createClass({ export default React.createClass({
displayName: 'ConfirmUserActionDialog', displayName: 'ConfirmUserActionDialog',
propTypes: { propTypes: {
member: React.PropTypes.object.isRequired, // matrix-js-sdk member object // matrix-js-sdk (room) member object. Supply either this or 'groupMember'
member: React.PropTypes.object,
// group member object. Supply either this or 'member'
groupMember: GroupMemberType,
action: React.PropTypes.string.isRequired, // eg. 'Ban' action: React.PropTypes.string.isRequired, // eg. 'Ban'
// Whether to display a text field for a reason // Whether to display a text field for a reason
@ -69,6 +73,7 @@ export default React.createClass({
render: function() { render: function() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const MemberAvatar = sdk.getComponent("views.avatars.MemberAvatar"); const MemberAvatar = sdk.getComponent("views.avatars.MemberAvatar");
const BaseAvatar = sdk.getComponent("views.avatars.BaseAvatar");
const title = _t("%(actionVerb)s this person?", { actionVerb: this.props.action}); const title = _t("%(actionVerb)s this person?", { actionVerb: this.props.action});
const confirmButtonClass = classnames({ const confirmButtonClass = classnames({
@ -91,6 +96,20 @@ export default React.createClass({
); );
} }
let avatar;
let name;
let userId;
if (this.props.member) {
avatar = <MemberAvatar member={this.props.member} width={48} height={48} />;
name = this.props.member.name;
userId = this.props.member.userId;
} else {
// we don't get this info from the API yet
avatar = <BaseAvatar name={this.props.groupMember.userId} width={48} height={48} />;
name = this.props.groupMember.userId;
userId = this.props.groupMember.userId;
}
return ( return (
<BaseDialog className="mx_ConfirmUserActionDialog" onFinished={this.props.onFinished} <BaseDialog className="mx_ConfirmUserActionDialog" onFinished={this.props.onFinished}
onEnterPressed={ this.onOk } onEnterPressed={ this.onOk }
@ -98,10 +117,10 @@ export default React.createClass({
> >
<div className="mx_Dialog_content"> <div className="mx_Dialog_content">
<div className="mx_ConfirmUserActionDialog_avatar"> <div className="mx_ConfirmUserActionDialog_avatar">
<MemberAvatar member={this.props.member} width={48} height={48} /> {avatar}
</div> </div>
<div className="mx_ConfirmUserActionDialog_name">{this.props.member.name}</div> <div className="mx_ConfirmUserActionDialog_name">{name}</div>
<div className="mx_ConfirmUserActionDialog_userId">{this.props.member.userId}</div> <div className="mx_ConfirmUserActionDialog_userId">{userId}</div>
</div> </div>
{reasonBox} {reasonBox}
<div className="mx_Dialog_buttons"> <div className="mx_Dialog_buttons">

View file

@ -1,5 +1,6 @@
/* /*
Copyright 2015, 2016 OpenMarket Ltd Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 New Vector Ltd.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -17,6 +18,7 @@ limitations under the License.
import React from 'react'; import React from 'react';
import sdk from '../../../index'; import sdk from '../../../index';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import classnames from 'classnames';
export default React.createClass({ export default React.createClass({
displayName: 'QuestionDialog', displayName: 'QuestionDialog',
@ -25,6 +27,7 @@ export default React.createClass({
description: React.PropTypes.node, description: React.PropTypes.node,
extraButtons: React.PropTypes.node, extraButtons: React.PropTypes.node,
button: React.PropTypes.string, button: React.PropTypes.string,
danger: React.PropTypes.bool,
focus: React.PropTypes.bool, focus: React.PropTypes.bool,
onFinished: React.PropTypes.func.isRequired, onFinished: React.PropTypes.func.isRequired,
}, },
@ -36,6 +39,7 @@ export default React.createClass({
extraButtons: null, extraButtons: null,
focus: true, focus: true,
hasCancelButton: true, hasCancelButton: true,
danger: false,
}; };
}, },
@ -54,6 +58,10 @@ export default React.createClass({
{_t("Cancel")} {_t("Cancel")}
</button> </button>
) : null; ) : null;
const buttonClasses = classnames({
mx_Dialog_primary: true,
danger: this.props.danger,
});
return ( return (
<BaseDialog className="mx_QuestionDialog" onFinished={this.props.onFinished} <BaseDialog className="mx_QuestionDialog" onFinished={this.props.onFinished}
onEnterPressed={ this.onOk } onEnterPressed={ this.onOk }
@ -63,7 +71,7 @@ export default React.createClass({
{this.props.description} {this.props.description}
</div> </div>
<div className="mx_Dialog_buttons"> <div className="mx_Dialog_buttons">
<button className="mx_Dialog_primary" onClick={this.onOk} autoFocus={this.props.focus}> <button className={buttonClasses} onClick={this.onOk} autoFocus={this.props.focus}>
{this.props.button || _t('OK')} {this.props.button || _t('OK')}
</button> </button>
{this.props.extraButtons} {this.props.extraButtons}

View file

@ -0,0 +1,70 @@
/*
Copyright 2017 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import sdk from '../../../index';
import dis from '../../../dispatcher';
import AccessibleButton from '../elements/AccessibleButton';
export default React.createClass({
displayName: 'GroupInviteTile',
propTypes: {
group: PropTypes.object.isRequired,
},
onClick: function(e) {
dis.dispatch({
action: 'view_group',
group_id: this.props.group.groupId,
});
},
render: function() {
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
const EmojiText = sdk.getComponent('elements.EmojiText');
const av = (
<BaseAvatar name={this.props.group.name} width={24} height={24}
url={this.props.group.avatarUrl}
/>
);
const label = <EmojiText
element="div"
title={this.props.group.name}
className="mx_GroupInviteTile_name"
dir="auto"
>
{this.props.group.name}
</EmojiText>;
const badge = <div className="mx_GroupInviteTile_badge">!</div>;
return (
<AccessibleButton className="mx_GroupInviteTile" onClick={this.onClick}>
<div className="mx_GroupInviteTile_avatarContainer">
{av}
</div>
<div className="mx_GroupInviteTile_nameContainer">
{label}
{badge}
</div>
</AccessibleButton>
);
},
});

View file

@ -0,0 +1,186 @@
/*
Copyright 2017 Vector Creations Ltd
Copyright 2017 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import PropTypes from 'prop-types';
import React from 'react';
import dis from '../../../dispatcher';
import Modal from '../../../Modal';
import sdk from '../../../index';
import { _t } from '../../../languageHandler';
import { GroupMemberType } from '../../../groups';
import { groupMemberFromApiObject } from '../../../groups';
import withMatrixClient from '../../../wrappers/withMatrixClient';
import AccessibleButton from '../elements/AccessibleButton';
import GeminiScrollbar from 'react-gemini-scrollbar';
module.exports = withMatrixClient(React.createClass({
displayName: 'GroupMemberInfo',
propTypes: {
matrixClient: PropTypes.object.isRequired,
groupId: PropTypes.string,
groupMember: GroupMemberType,
},
getInitialState: function() {
return {
fetching: false,
removingUser: false,
groupMembers: null,
};
},
componentWillMount: function() {
this._fetchMembers();
},
_fetchMembers: function() {
this.setState({fetching: true});
this.props.matrixClient.getGroupUsers(this.props.groupId).then((result) => {
this.setState({
groupMembers: result.chunk.map((apiMember) => {
return groupMemberFromApiObject(apiMember);
}),
fetching: false,
});
}).catch((e) => {
this.setState({fetching: false});
console.error("Failed to get group groupMember list: ", e);
});
},
_onKick: function() {
const ConfirmUserActionDialog = sdk.getComponent("dialogs.ConfirmUserActionDialog");
Modal.createDialog(ConfirmUserActionDialog, {
groupMember: this.props.groupMember,
action: _t('Remove from group'),
danger: true,
onFinished: (proceed) => {
if (!proceed) return;
this.setState({removingUser: true});
this.props.matrixClient.removeUserFromGroup(
this.props.groupId, this.props.groupMember.userId,
).then(() => {
// return to the user list
dis.dispatch({
action: "view_user",
member: null,
});
}).catch((e) => {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Failed to remove user from group', '', ErrorDialog, {
title: _t('Error'),
description: _t('Failed to remove user from group'),
});
}).finally(() => {
this.setState({removingUser: false});
});
},
});
},
_onCancel: function(e) {
// Go back to the user list
dis.dispatch({
action: "view_user",
member: null,
});
},
onRoomTileClick(roomId) {
dis.dispatch({
action: 'view_room',
room_id: roomId,
});
},
render: function() {
if (this.state.fetching || this.state.removingUser) {
const Spinner = sdk.getComponent("elements.Spinner");
return <Spinner />;
}
if (!this.state.groupMembers) return null;
const targetIsInGroup = this.state.groupMembers.some((m) => {
return m.userId === this.props.groupMember.userId;
});
let kickButton;
let adminButton;
if (targetIsInGroup) {
kickButton = (
<AccessibleButton className="mx_MemberInfo_field"
onClick={this._onKick}>
{_t('Remove from group')}
</AccessibleButton>
);
// No make/revoke admin API yet
/*const opLabel = this.state.isTargetMod ? _t("Revoke Moderator") : _t("Make Moderator");
giveModButton = <AccessibleButton className="mx_MemberInfo_field" onClick={this.onModToggle}>
{giveOpLabel}
</AccessibleButton>;*/
}
let adminTools;
if (kickButton || adminButton) {
adminTools =
<div className="mx_MemberInfo_adminTools">
<h3>{_t("Admin Tools")}</h3>
<div className="mx_MemberInfo_buttons">
{kickButton}
{adminButton}
</div>
</div>;
}
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
const avatar = (
<BaseAvatar name={this.props.groupMember.userId} width={36} height={36} />
);
const groupMemberName = this.props.groupMember.userId;
const EmojiText = sdk.getComponent('elements.EmojiText');
return (
<div className="mx_MemberInfo">
<GeminiScrollbar autoshow={true}>
<AccessibleButton className="mx_MemberInfo_cancel"onClick={this._onCancel}>
<img src="img/cancel.svg" width="18" height="18"/>
</AccessibleButton>
<div className="mx_MemberInfo_avatar">
{avatar}
</div>
<EmojiText element="h2">{groupMemberName}</EmojiText>
<div className="mx_MemberInfo_profile">
<div className="mx_MemberInfo_profileField">
{ this.props.groupMember.userId }
</div>
</div>
{ adminTools }
</GeminiScrollbar>
</div>
);
},
}));

View file

@ -0,0 +1,154 @@
/*
Copyright 2017 Vector Creations Ltd.
Copyright 2017 New Vector Ltd.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import { _t } from '../../../languageHandler';
import sdk from '../../../index';
import { groupMemberFromApiObject } from '../../../groups';
import GeminiScrollbar from 'react-gemini-scrollbar';
import PropTypes from 'prop-types';
import withMatrixClient from '../../../wrappers/withMatrixClient';
const INITIAL_LOAD_NUM_MEMBERS = 30;
export default withMatrixClient(React.createClass({
displayName: 'GroupMemberList',
propTypes: {
matrixClient: PropTypes.object.isRequired,
groupId: PropTypes.string.isRequired,
},
getInitialState: function() {
return {
fetching: false,
members: null,
truncateAt: INITIAL_LOAD_NUM_MEMBERS,
};
},
componentWillMount: function() {
this._unmounted = false;
this._fetchMembers();
},
_fetchMembers: function() {
this.setState({fetching: true});
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);
});
},
_createOverflowTile: function(overflowCount, totalCount) {
// For now we'll pretend this is any entity. It should probably be a separate tile.
const EntityTile = sdk.getComponent("rooms.EntityTile");
const BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
const text = _t("and %(count)s others...", { count: overflowCount });
return (
<EntityTile className="mx_EntityTile_ellipsis" avatarJsx={
<BaseAvatar url="img/ellipsis.svg" name="..." width={36} height={36} />
} name={text} presenceState="online" suppressOnHover={true}
onClick={this._showFullMemberList} />
);
},
_showFullMemberList: function() {
this.setState({
truncateAt: -1,
});
},
onSearchQueryChanged: function(ev) {
this.setState({ searchQuery: ev.target.value });
},
makeGroupMemberTiles: function(query) {
const GroupMemberTile = sdk.getComponent("groups.GroupMemberTile");
query = (query || "").toLowerCase();
let memberList = this.state.members;
if (query) {
memberList = memberList.filter((m) => {
// TODO: add this when we have this info from the API
//const matchesName = m.name.toLowerCase().indexOf(query) !== -1;
const matchesId = m.userId.toLowerCase().includes(query);
if (/*!matchesName &&*/ !matchesId) {
return false;
}
return true;
});
}
memberList = memberList.map((m) => {
return (
<GroupMemberTile key={m.userId} groupId={this.props.groupId} member={m} />
);
});
memberList.sort((a, b) => {
// TODO: should put admins at the top: we don't yet have that info
if (a < b) {
return -1;
} else if (a > b) {
return 1;
} else {
return 0;
}
});
return memberList;
},
render: function() {
if (this.state.fetching) {
const Spinner = sdk.getComponent("elements.Spinner");
return <Spinner />;
} else if (this.state.members === null) {
return null;
}
const inputBox = (
<form autoComplete="off">
<input className="mx_MemberList_query" id="mx_MemberList_query" type="text"
onChange={this.onSearchQueryChanged} value={this.state.searchQuery}
placeholder={ _t('Filter group members') } />
</form>
);
const TruncatedList = sdk.getComponent("elements.TruncatedList");
return (
<div className="mx_MemberList">
{ inputBox }
<GeminiScrollbar autoshow={true} className="mx_MemberList_joined mx_MemberList_outerWrapper">
<TruncatedList className="mx_MemberList_wrapper" truncateAt={this.state.truncateAt}
createOverflowElement={this._createOverflowTile}>
{this.makeGroupMemberTiles(this.state.searchQuery)}
</TruncatedList>
</GeminiScrollbar>
</div>
);
},
}));

View file

@ -0,0 +1,63 @@
/*
Copyright 2017 Vector Creations Ltd
Copyright 2017 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import sdk from '../../../index';
import dis from '../../../dispatcher';
import { GroupMemberType } from '../../../groups';
import withMatrixClient from '../../../wrappers/withMatrixClient';
export default withMatrixClient(React.createClass({
displayName: 'GroupMemberTile',
propTypes: {
matrixClient: PropTypes.object,
groupId: PropTypes.string.isRequired,
member: GroupMemberType.isRequired,
},
getInitialState: function() {
return {};
},
onClick: function(e) {
dis.dispatch({
action: 'view_group_user',
member: this.props.member,
groupId: this.props.groupId,
});
},
render: function() {
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
const EntityTile = sdk.getComponent('rooms.EntityTile');
const name = this.props.member.userId;
const av = (
<BaseAvatar name={this.props.member.userId} width={36} height={36} />
);
return (
<EntityTile presenceState="online"
avatarJsx={av} onClick={this.onClick}
name={name} powerLevel={0} suppressOnHover={true}
/>
);
},
}));

View file

@ -751,7 +751,7 @@ module.exports = withMatrixClient(React.createClass({
if (kickButton || banButton || muteButton || giveModButton) { if (kickButton || banButton || muteButton || giveModButton) {
adminTools = adminTools =
<div> <div>
<h3>{_t("Admin tools")}</h3> <h3>{_t("Admin Tools")}</h3>
<div className="mx_MemberInfo_buttons"> <div className="mx_MemberInfo_buttons">
{muteButton} {muteButton}

View file

@ -26,7 +26,6 @@ var sdk = require('../../../index');
var GeminiScrollbar = require('react-gemini-scrollbar'); var GeminiScrollbar = require('react-gemini-scrollbar');
var rate_limited_func = require('../../../ratelimitedfunc'); var rate_limited_func = require('../../../ratelimitedfunc');
var CallHandler = require("../../../CallHandler"); var CallHandler = require("../../../CallHandler");
var Invite = require("../../../Invite");
var INITIAL_LOAD_NUM_MEMBERS = 30; var INITIAL_LOAD_NUM_MEMBERS = 30;

View file

@ -550,8 +550,24 @@ module.exports = React.createClass({
} }
}, },
_makeGroupInviteTiles() {
const ret = [];
const GroupInviteTile = sdk.getComponent('groups.GroupInviteTile');
for (const group of MatrixClientPeg.get().getGroups()) {
if (group.myMembership !== 'invite') continue;
ret.push(<GroupInviteTile key={group.groupId} group={group} />);
}
return ret;
},
render: function() { render: function() {
var RoomSubList = sdk.getComponent('structures.RoomSubList'); const RoomSubList = sdk.getComponent('structures.RoomSubList');
const inviteSectionExtraTiles = this._makeGroupInviteTiles();
var self = this; var self = this;
return ( return (
<GeminiScrollbar className="mx_RoomList_scrollbar" <GeminiScrollbar className="mx_RoomList_scrollbar"
@ -567,7 +583,9 @@ module.exports = React.createClass({
collapsed={ self.props.collapsed } collapsed={ self.props.collapsed }
searchFilter={ self.props.searchFilter } searchFilter={ self.props.searchFilter }
onHeaderClick={ self.onSubListHeaderClick } onHeaderClick={ self.onSubListHeaderClick }
onShowMoreRooms={ self.onShowMoreRooms } /> onShowMoreRooms={ self.onShowMoreRooms }
extraTiles={ inviteSectionExtraTiles }
/>
<RoomSubList list={ self.state.lists['m.favourite'] } <RoomSubList list={ self.state.lists['m.favourite'] }
label={ _t('Favourites') } label={ _t('Favourites') }

27
src/groups.js Normal file
View file

@ -0,0 +1,27 @@
/*
Copyright 2017 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import PropTypes from 'prop-types';
export const GroupMemberType = PropTypes.shape({
userId: PropTypes.string.isRequired,
});
export function groupMemberFromApiObject(apiObject) {
return {
userId: apiObject.user_id,
};
}

View file

@ -790,7 +790,7 @@
"a room": "einen Raum", "a room": "einen Raum",
"Accept": "Akzeptieren", "Accept": "Akzeptieren",
"Active call (%(roomName)s)": "Aktiver Anruf (%(roomName)s)", "Active call (%(roomName)s)": "Aktiver Anruf (%(roomName)s)",
"Admin tools": "Admin-Werkzeuge", "Admin Tools": "Admin-Werkzeuge",
"And %(count)s more...": "Und %(count)s weitere...", "And %(count)s more...": "Und %(count)s weitere...",
"Alias (optional)": "Alias (optional)", "Alias (optional)": "Alias (optional)",
"Can't connect to homeserver - please check your connectivity, ensure your <a>homeserver's SSL certificate</a> is trusted, and that a browser extension is not blocking requests.": "Verbindung zum Heimserver fehlgeschlagen - bitte überprüfe die Internetverbindung und stelle sicher, dass dem <a>SSL-Zertifikat deines Heimservers</a> vertraut wird und dass Anfragen nicht durch eine Browser-Erweiterung blockiert werden.", "Can't connect to homeserver - please check your connectivity, ensure your <a>homeserver's SSL certificate</a> is trusted, and that a browser extension is not blocking requests.": "Verbindung zum Heimserver fehlgeschlagen - bitte überprüfe die Internetverbindung und stelle sicher, dass dem <a>SSL-Zertifikat deines Heimservers</a> vertraut wird und dass Anfragen nicht durch eine Browser-Erweiterung blockiert werden.",

View file

@ -265,7 +265,7 @@
"Accept": "Αποδοχή", "Accept": "Αποδοχή",
"Active call (%(roomName)s)": "Ενεργή κλήση (%(roomName)s)", "Active call (%(roomName)s)": "Ενεργή κλήση (%(roomName)s)",
"Add": "Προσθήκη", "Add": "Προσθήκη",
"Admin tools": "Εργαλεία διαχειριστή", "Admin Tools": "Εργαλεία διαχειριστή",
"And %(count)s more...": "Και %(count)s περισσότερα...", "And %(count)s more...": "Και %(count)s περισσότερα...",
"No media permissions": "Χωρίς δικαιώματα πολυμέσων", "No media permissions": "Χωρίς δικαιώματα πολυμέσων",
"Alias (optional)": "Ψευδώνυμο (προαιρετικό)", "Alias (optional)": "Ψευδώνυμο (προαιρετικό)",

View file

@ -13,7 +13,7 @@
"Add email address": "Add email address", "Add email address": "Add email address",
"Add phone number": "Add phone number", "Add phone number": "Add phone number",
"Admin": "Admin", "Admin": "Admin",
"Admin tools": "Admin tools", "Admin Tools": "Admin tools",
"Allow": "Allow", "Allow": "Allow",
"And %(count)s more...": "And %(count)s more...", "And %(count)s more...": "And %(count)s more...",
"VoIP": "VoIP", "VoIP": "VoIP",
@ -864,5 +864,24 @@
"%(widgetName)s widget added by %(senderName)s": "%(widgetName)s widget added by %(senderName)s", "%(widgetName)s widget added by %(senderName)s": "%(widgetName)s widget added by %(senderName)s",
"%(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",
"%(widgetName)s widget modified by %(senderName)s": "%(widgetName)s widget modified by %(senderName)s", "%(widgetName)s widget modified by %(senderName)s": "%(widgetName)s widget modified by %(senderName)s",
"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>",
"Description": "Description",
"Filter group members": "Filter group members",
"Remove from group": "Remove from group",
"Invite new group members": "Invite new group members",
"Who would you like to add to this group?": "Who would you like to add to this group?",
"Name or matrix ID": "Name or matrix ID",
"Invite to Group": "Invite to Group",
"Unable to accept invite": "Unable to accept invite",
"Unable to leave room": "Unable to leave room",
"%(inviter)s has invited you to join this group": "%(inviter)s has invited you to join this group",
"You are a member of this group": "You are a member of this group",
"Leave": "Leave",
"Failed to remove user from group": "Failed to remove user from group",
"Failed to invite the following users to %(groupId)s:": "Failed to invite the following users to %(groupId)s:",
"Failed to invite users group": "Failed to invite users group",
"Failed to invite users to %(groupId)s": "Failed to invite users to %(groupId)s",
"Unable to reject invite": "Unable to reject invite",
"Leave Group": "Leave Group",
"Leave %(groupName)s?": "Leave %(groupName)s?"
} }

View file

@ -724,7 +724,7 @@
"Accept": "Accept", "Accept": "Accept",
"a room": "a room", "a room": "a room",
"Add": "Add", "Add": "Add",
"Admin tools": "Admin tools", "Admin Tools": "Admin tools",
"And %(count)s more...": "And %(count)s more...", "And %(count)s more...": "And %(count)s more...",
"Alias (optional)": "Alias (optional)", "Alias (optional)": "Alias (optional)",
"Can't connect to homeserver - please check your connectivity, ensure your <a>homeserver's SSL certificate</a> is trusted, and that a browser extension is not blocking requests.": "Can't connect to homeserver - please check your connectivity, ensure your <a>homeserver's SSL certificate</a> is trusted, and that a browser extension is not blocking requests.", "Can't connect to homeserver - please check your connectivity, ensure your <a>homeserver's SSL certificate</a> is trusted, and that a browser extension is not blocking requests.": "Can't connect to homeserver - please check your connectivity, ensure your <a>homeserver's SSL certificate</a> is trusted, and that a browser extension is not blocking requests.",

View file

@ -204,7 +204,7 @@
"Low priority": "Baja prioridad", "Low priority": "Baja prioridad",
"Accept": "Aceptar", "Accept": "Aceptar",
"Add": "Añadir", "Add": "Añadir",
"Admin tools": "Herramientas de administración", "Admin Tools": "Herramientas de administración",
"VoIP": "Voz IP", "VoIP": "Voz IP",
"No Microphones detected": "No se ha detectado micrófono", "No Microphones detected": "No se ha detectado micrófono",
"No Webcams detected": "No se ha detectado cámara", "No Webcams detected": "No se ha detectado cámara",

View file

@ -167,7 +167,7 @@
"Add": "Gehitu", "Add": "Gehitu",
"Add a topic": "Gehitu gai bat", "Add a topic": "Gehitu gai bat",
"Admin": "Kudeatzailea", "Admin": "Kudeatzailea",
"Admin tools": "Kudeaketa tresnak", "Admin Tools": "Kudeaketa tresnak",
"And %(count)s more...": "Eta %(count)s gehiago...", "And %(count)s more...": "Eta %(count)s gehiago...",
"VoIP": "VoIP", "VoIP": "VoIP",
"Missing Media Permissions, click here to request.": "Media baimenak falta dira, egin klik eskatzeko.", "Missing Media Permissions, click here to request.": "Media baimenak falta dira, egin klik eskatzeko.",

View file

@ -39,7 +39,7 @@
"Add email address": "E-mail cím megadása", "Add email address": "E-mail cím megadása",
"Add phone number": "Telefonszám megadása", "Add phone number": "Telefonszám megadása",
"Admin": "Adminisztrátor", "Admin": "Adminisztrátor",
"Admin tools": "Admin. eszközök", "Admin Tools": "Admin. eszközök",
"And %(count)s more...": "És még %(count)s...", "And %(count)s more...": "És még %(count)s...",
"VoIP": "VoIP", "VoIP": "VoIP",
"Missing Media Permissions, click here to request.": "Hiányzó Média jogosultság, kattintson ide az igényléshez.", "Missing Media Permissions, click here to request.": "Hiányzó Média jogosultság, kattintson ide az igényléshez.",

View file

@ -172,7 +172,7 @@
"Access Token:": "Token Akses:", "Access Token:": "Token Akses:",
"Active call (%(roomName)s)": "Panggilan aktif (%(roomName)s)", "Active call (%(roomName)s)": "Panggilan aktif (%(roomName)s)",
"Admin": "Admin", "Admin": "Admin",
"Admin tools": "Alat admin", "Admin Tools": "Alat admin",
"And %(count)s more...": "Dan %(count)s lagi...", "And %(count)s more...": "Dan %(count)s lagi...",
"VoIP": "VoIP", "VoIP": "VoIP",
"Missing Media Permissions, click here to request.": "Tidak ada Izin Media, klik disini untuk meminta.", "Missing Media Permissions, click here to request.": "Tidak ada Izin Media, klik disini untuk meminta.",

View file

@ -42,7 +42,7 @@
"Add email address": "Aggiungi indirizzo email", "Add email address": "Aggiungi indirizzo email",
"Add phone number": "Aggiungi numero di telefono", "Add phone number": "Aggiungi numero di telefono",
"Admin": "Amministratore", "Admin": "Amministratore",
"Admin tools": "Strumenti di amministrazione", "Admin Tools": "Strumenti di amministrazione",
"VoIP": "VoIP", "VoIP": "VoIP",
"No Microphones detected": "Nessun Microfono rilevato", "No Microphones detected": "Nessun Microfono rilevato",
"No Webcams detected": "Nessuna Webcam rilevata", "No Webcams detected": "Nessuna Webcam rilevata",

View file

@ -33,7 +33,7 @@
"Add email address": "이메일 주소 추가하기", "Add email address": "이메일 주소 추가하기",
"Add phone number": "전화번호 추가하기", "Add phone number": "전화번호 추가하기",
"Admin": "관리자", "Admin": "관리자",
"Admin tools": "관리 도구", "Admin Tools": "관리 도구",
"VoIP": "인터넷전화", "VoIP": "인터넷전화",
"No Microphones detected": "마이크를 찾지 못했어요", "No Microphones detected": "마이크를 찾지 못했어요",
"No Webcams detected": "카메라를 찾지 못했어요", "No Webcams detected": "카메라를 찾지 못했어요",

View file

@ -12,7 +12,7 @@
"Add email address": "Pievieno Epasta adresi", "Add email address": "Pievieno Epasta adresi",
"Add phone number": "Pievieno tālruņa numuru", "Add phone number": "Pievieno tālruņa numuru",
"Admin": "Administrators", "Admin": "Administrators",
"Admin tools": "Administratora rīki", "Admin Tools": "Administratora rīki",
"And %(count)s more...": "Un vēl %(count)s citi...", "And %(count)s more...": "Un vēl %(count)s citi...",
"VoIP": "VoIP", "VoIP": "VoIP",
"Missing Media Permissions, click here to request.": "Nav pieejas medija saturam. Klikšķini šeit, lai pieprasītu.", "Missing Media Permissions, click here to request.": "Nav pieejas medija saturam. Klikšķini šeit, lai pieprasītu.",

View file

@ -79,7 +79,7 @@
"Active call (%(roomName)s)": "Actief gesprek (%(roomName)s)", "Active call (%(roomName)s)": "Actief gesprek (%(roomName)s)",
"Add": "Toevoegen", "Add": "Toevoegen",
"Add a topic": "Een onderwerp toevoegen", "Add a topic": "Een onderwerp toevoegen",
"Admin tools": "Beheerhulpmiddelen", "Admin Tools": "Beheerhulpmiddelen",
"And %(count)s more...": "Nog %(count)s andere...", "And %(count)s more...": "Nog %(count)s andere...",
"VoIP": "VoiP", "VoIP": "VoiP",
"Missing Media Permissions, click here to request.": "Ontbrekende mediatoestemmingen, klik hier om aan te vragen.", "Missing Media Permissions, click here to request.": "Ontbrekende mediatoestemmingen, klik hier om aan te vragen.",

View file

@ -123,7 +123,7 @@
"Active call (%(roomName)s)": "Aktywne połączenie (%(roomName)s)", "Active call (%(roomName)s)": "Aktywne połączenie (%(roomName)s)",
"Add email address": "Dodaj adres e-mail", "Add email address": "Dodaj adres e-mail",
"Admin": "Administrator", "Admin": "Administrator",
"Admin tools": "Narzędzia administracyjne", "Admin Tools": "Narzędzia administracyjne",
"And %(count)s more...": "Oraz %(count)s więcej...", "And %(count)s more...": "Oraz %(count)s więcej...",
"VoIP": "VoIP (połączenie głosowe)", "VoIP": "VoIP (połączenie głosowe)",
"No Microphones detected": "Nie wykryto żadnego mikrofonu", "No Microphones detected": "Nie wykryto żadnego mikrofonu",

View file

@ -772,7 +772,7 @@
"Public Chat": "Conversa pública", "Public Chat": "Conversa pública",
"Uploading %(filename)s and %(count)s others|zero": "Enviando o arquivo %(filename)s", "Uploading %(filename)s and %(count)s others|zero": "Enviando o arquivo %(filename)s",
"Room contains unknown devices": "Esta sala contém dispositivos desconhecidos", "Room contains unknown devices": "Esta sala contém dispositivos desconhecidos",
"Admin tools": "Ferramentas de administração", "Admin Tools": "Ferramentas de administração",
"You have been kicked from %(roomName)s by %(userName)s.": "Você foi removido(a) da sala %(roomName)s por %(userName)s.", "You have been kicked from %(roomName)s by %(userName)s.": "Você foi removido(a) da sala %(roomName)s por %(userName)s.",
"Undecryptable": "Não é possível descriptografar", "Undecryptable": "Não é possível descriptografar",
"Incoming video call from %(name)s": "Chamada de vídeo de %(name)s recebida", "Incoming video call from %(name)s": "Chamada de vídeo de %(name)s recebida",

View file

@ -783,7 +783,7 @@
"a room": "uma sala", "a room": "uma sala",
"Accept": "Aceitar", "Accept": "Aceitar",
"Active call (%(roomName)s)": "Chamada ativa (%(roomName)s)", "Active call (%(roomName)s)": "Chamada ativa (%(roomName)s)",
"Admin tools": "Ferramentas de administração", "Admin Tools": "Ferramentas de administração",
"And %(count)s more...": "E mais %(count)s...", "And %(count)s more...": "E mais %(count)s...",
"Alias (optional)": "Apelido (opcional)", "Alias (optional)": "Apelido (opcional)",
"Can't connect to homeserver - please check your connectivity, ensure your <a>homeserver's SSL certificate</a> is trusted, and that a browser extension is not blocking requests.": "Não foi possível conectar ao Servidor de Base. Por favor, confira sua conectividade à internet, garanta que o <a>certificado SSL do Servidor de Base</a> é confiável, e que uma extensão do navegador não esteja bloqueando as requisições de rede.", "Can't connect to homeserver - please check your connectivity, ensure your <a>homeserver's SSL certificate</a> is trusted, and that a browser extension is not blocking requests.": "Não foi possível conectar ao Servidor de Base. Por favor, confira sua conectividade à internet, garanta que o <a>certificado SSL do Servidor de Base</a> é confiável, e que uma extensão do navegador não esteja bloqueando as requisições de rede.",

View file

@ -767,7 +767,7 @@
"a room": "комната", "a room": "комната",
"Accept": "Принять", "Accept": "Принять",
"Active call (%(roomName)s)": "Активный вызов (%(roomName)s)", "Active call (%(roomName)s)": "Активный вызов (%(roomName)s)",
"Admin tools": "Инструменты администратора", "Admin Tools": "Инструменты администратора",
"And %(count)s more...": "И %(count)s больше...", "And %(count)s more...": "И %(count)s больше...",
"Alias (optional)": "Псевдоним (опционально)", "Alias (optional)": "Псевдоним (опционально)",
"<a>Click here</a> to join the discussion!": "<a>Нажмите здесь</a>, чтобы присоединиться к обсуждению!", "<a>Click here</a> to join the discussion!": "<a>Нажмите здесь</a>, чтобы присоединиться к обсуждению!",

View file

@ -171,7 +171,7 @@
"Access Token:": "Åtkomsttoken:", "Access Token:": "Åtkomsttoken:",
"Active call (%(roomName)s)": "Aktiv samtal (%(roomName)s)", "Active call (%(roomName)s)": "Aktiv samtal (%(roomName)s)",
"Add": "Lägg till", "Add": "Lägg till",
"Admin tools": "Admin verktyg", "Admin Tools": "Admin verktyg",
"And %(count)s more...": "Och %(count)s till...", "And %(count)s more...": "Och %(count)s till...",
"Alias (optional)": "Alias (valfri)", "Alias (optional)": "Alias (valfri)",
"Can't connect to homeserver - please check your connectivity, ensure your <a>homeserver's SSL certificate</a> is trusted, and that a browser extension is not blocking requests.": "Det gick inte att ansluta till servern - kontrollera anslutningen, försäkra att din <a>hemservers TLS-certifikat</a> är betrott, och att inget webbläsartillägg blockerar förfrågningar.", "Can't connect to homeserver - please check your connectivity, ensure your <a>homeserver's SSL certificate</a> is trusted, and that a browser extension is not blocking requests.": "Det gick inte att ansluta till servern - kontrollera anslutningen, försäkra att din <a>hemservers TLS-certifikat</a> är betrott, och att inget webbläsartillägg blockerar förfrågningar.",

View file

@ -11,7 +11,7 @@
"Add email address": "ఇమెయిల్ చిరునామాను జోడించండి", "Add email address": "ఇమెయిల్ చిరునామాను జోడించండి",
"Add phone number": "ఫోన్ నంబర్ను జోడించండి", "Add phone number": "ఫోన్ నంబర్ను జోడించండి",
"Admin": "అడ్మిన్", "Admin": "అడ్మిన్",
"Admin tools": "నిర్వాహక ఉపకరణాలు", "Admin Tools": "నిర్వాహక ఉపకరణాలు",
"VoIP": "విఒఐపి", "VoIP": "విఒఐపి",
"Missing Media Permissions, click here to request.": "మీడియా అనుమతులు మిస్ అయయి, అభ్యర్థించడానికి ఇక్కడ క్లిక్ చేయండి.", "Missing Media Permissions, click here to request.": "మీడియా అనుమతులు మిస్ అయయి, అభ్యర్థించడానికి ఇక్కడ క్లిక్ చేయండి.",
"No Microphones detected": "మైక్రోఫోన్లు కనుగొనబడలేదు", "No Microphones detected": "మైక్రోఫోన్లు కనుగొనబడలేదు",

View file

@ -12,7 +12,7 @@
"Add email address": "E-posta adresi ekle", "Add email address": "E-posta adresi ekle",
"Add phone number": "Telefon numarası ekle", "Add phone number": "Telefon numarası ekle",
"Admin": "Admin", "Admin": "Admin",
"Admin tools": "Admin araçları", "Admin Tools": "Admin araçları",
"And %(count)s more...": "Ve %(count)s fazlası...", "And %(count)s more...": "Ve %(count)s fazlası...",
"VoIP": "VoIP", "VoIP": "VoIP",
"Missing Media Permissions, click here to request.": "Medya İzinleri Yok , talep etmek için burayı tıklayın.", "Missing Media Permissions, click here to request.": "Medya İzinleri Yok , talep etmek için burayı tıklayın.",

View file

@ -45,7 +45,7 @@
"Add email address": "Додати адресу е-пошти", "Add email address": "Додати адресу е-пошти",
"Add phone number": "Додати номер телефону", "Add phone number": "Додати номер телефону",
"Admin": "Адміністратор", "Admin": "Адміністратор",
"Admin tools": "Засоби адміністрування", "Admin Tools": "Засоби адміністрування",
"And %(count)s more...": "І %(count)s більше...", "And %(count)s more...": "І %(count)s більше...",
"VoIP": "VoIP", "VoIP": "VoIP",
"Missing Media Permissions, click here to request.": "Відсутні дозволи, натисніть для запиту.", "Missing Media Permissions, click here to request.": "Відсутні дозволи, натисніть для запиту.",

View file

@ -190,7 +190,7 @@
"New password": "新密码", "New password": "新密码",
"Add a topic": "添加一个主题", "Add a topic": "添加一个主题",
"Admin": "管理员", "Admin": "管理员",
"Admin tools": "管理工具", "Admin Tools": "管理工具",
"VoIP": "IP 电话", "VoIP": "IP 电话",
"Missing Media Permissions, click here to request.": "没有媒体存储权限,点此获取。", "Missing Media Permissions, click here to request.": "没有媒体存储权限,点此获取。",
"No Microphones detected": "未检测到麦克风", "No Microphones detected": "未检测到麦克风",

View file

@ -302,7 +302,7 @@
"%(targetName)s accepted the invitation for %(displayName)s.": "%(targetName)s 已接受 %(displayName)s 的邀請。", "%(targetName)s accepted the invitation for %(displayName)s.": "%(targetName)s 已接受 %(displayName)s 的邀請。",
"Active call (%(roomName)s)": "活躍的通話(%(roomName)s", "Active call (%(roomName)s)": "活躍的通話(%(roomName)s",
"Add": "新增", "Add": "新增",
"Admin tools": "管理員工具", "Admin Tools": "管理員工具",
"And %(count)s more...": "還有 %(count)s 個...", "And %(count)s more...": "還有 %(count)s 個...",
"Missing Media Permissions, click here to request.": "遺失媒體權限,點選這裡來要求。", "Missing Media Permissions, click here to request.": "遺失媒體權限,點選這裡來要求。",
"No Microphones detected": "未偵測到麥克風", "No Microphones detected": "未偵測到麥克風",

View file

@ -1,5 +1,6 @@
/* /*
Copyright 2016 OpenMarket Ltd Copyright 2016 OpenMarket Ltd
Copyright 2017 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -14,16 +15,26 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import MatrixClientPeg from '../MatrixClientPeg';
import {getAddressType} from '../UserAddress'; import {getAddressType} from '../UserAddress';
import {inviteToRoom} from '../Invite'; import {inviteToRoom} from '../RoomInvite';
import Promise from 'bluebird'; import Promise from 'bluebird';
/** /**
* Invites multiple addresses to a room, handling rate limiting from the server * Invites multiple addresses to a room or group, handling rate limiting from the server
*/ */
export default class MultiInviter { export default class MultiInviter {
constructor(roomId) { /**
this.roomId = roomId; * @param {string} targetId The ID of the room or group to invite to
*/
constructor(targetId) {
if (targetId[0] === '+') {
this.roomId = null;
this.groupId = targetId;
} else {
this.roomId = targetId;
this.groupId = null;
}
this.canceled = false; this.canceled = false;
this.addrs = []; this.addrs = [];
@ -104,7 +115,14 @@ export default class MultiInviter {
return; return;
} }
inviteToRoom(this.roomId, addr).then(() => { let doInvite;
if (this.groupId !== null) {
doInvite = MatrixClientPeg.get().inviteUserToGroup(this.groupId, addr);
} else {
doInvite = inviteToRoom(this.roomId, addr);
}
doInvite.then(() => {
if (this._canceled) { return; } if (this._canceled) { return; }
this.completionStates[addr] = 'invited'; this.completionStates[addr] = 'invited';