Merge branch 'develop' into luke/fix-ugly-integ-button
						commit
						979b580e1f
					
				|  | @ -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}), | ||||
|         }); | ||||
|     }); | ||||
| } | ||||
| 
 | ||||
|  | @ -33,11 +33,17 @@ export default { | |||
|             // XXX: Always use default, ignore localStorage and remove from labs
 | ||||
|             override: true, | ||||
|         }, | ||||
|         { | ||||
|             name: "-", | ||||
|             id: 'feature_flair', | ||||
|             default: false, | ||||
|         }, | ||||
|     ], | ||||
| 
 | ||||
|     // horrible but it works. The locality makes this somewhat more palatable.
 | ||||
|     doTranslations: function() { | ||||
|         this.LABS_FEATURES[0].name = _t("Matrix Apps"); | ||||
|         this.LABS_FEATURES[1].name = _t("Flair"); | ||||
|     }, | ||||
| 
 | ||||
|     loadProfileInfo: function() { | ||||
|  |  | |||
|  | @ -1,5 +1,6 @@ | |||
| /* | ||||
| 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. | ||||
|  | @ -183,12 +184,19 @@ export default React.createClass({ | |||
|             editing: false, | ||||
|             saving: false, | ||||
|             uploadingAvatar: false, | ||||
|             membershipBusy: false, | ||||
|         }; | ||||
|     }, | ||||
| 
 | ||||
|     componentWillMount: function() { | ||||
|         this._changeAvatarComponent = null; | ||||
|         this._loadGroupFromServer(this.props.groupId); | ||||
| 
 | ||||
|         MatrixClientPeg.get().on("Group.myMembership", this._onGroupMyMembership); | ||||
|     }, | ||||
| 
 | ||||
|     componentWillUnmount: function() { | ||||
|         MatrixClientPeg.get().removeListener("Group.myMembership", this._onGroupMyMembership); | ||||
|     }, | ||||
| 
 | ||||
|     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) { | ||||
|         MatrixClientPeg.get().getGroupSummary(groupId).done((res) => { | ||||
|             this.setState({ | ||||
|  | @ -216,6 +230,10 @@ export default React.createClass({ | |||
|         }); | ||||
|     }, | ||||
| 
 | ||||
|     _onShowRhsClick: function(ev) { | ||||
|         dis.dispatch({ action: 'show_right_panel' }); | ||||
|     }, | ||||
| 
 | ||||
|     _onEditClick: function() { | ||||
|         this.setState({ | ||||
|             editing: true, | ||||
|  | @ -295,6 +313,59 @@ export default React.createClass({ | |||
|         }).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() { | ||||
|         const summary = this.state.summary; | ||||
| 
 | ||||
|  | @ -371,6 +442,50 @@ export default React.createClass({ | |||
|         </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() { | ||||
|         const GroupAvatar = sdk.getComponent("avatars.GroupAvatar"); | ||||
|         const Loader = sdk.getComponent("elements.Spinner"); | ||||
|  | @ -384,8 +499,8 @@ export default React.createClass({ | |||
|             let avatarNode; | ||||
|             let nameNode; | ||||
|             let shortDescNode; | ||||
|             let rightButtons; | ||||
|             let roomBody; | ||||
|             const rightButtons = []; | ||||
|             const headerClasses = { | ||||
|                 mx_GroupView_header: true, | ||||
|             }; | ||||
|  | @ -428,15 +543,19 @@ export default React.createClass({ | |||
|                     placeholder={_t('Description')} | ||||
|                     tabIndex="2" | ||||
|                 />; | ||||
|                 rightButtons = <span> | ||||
|                     <AccessibleButton className="mx_GroupView_saveButton mx_RoomHeader_textButton" onClick={this._onSaveClick}> | ||||
|                 rightButtons.push( | ||||
|                     <AccessibleButton className="mx_GroupView_textButton mx_RoomHeader_textButton" | ||||
|                         onClick={this._onSaveClick} key="_saveButton" | ||||
|                     > | ||||
|                         {_t('Save')} | ||||
|                     </AccessibleButton> | ||||
|                     <AccessibleButton className='mx_GroupView_cancelButton' onClick={this._onCancelClick}> | ||||
|                     </AccessibleButton>, | ||||
|                 ); | ||||
|                 rightButtons.push( | ||||
|                     <AccessibleButton className='mx_GroupView_textButton' onClick={this._onCancelClick} key="_cancelButton"> | ||||
|                         <img src="img/cancel.svg" className='mx_filterFlipColor' | ||||
|                             width="18" height="18" alt={_t("Cancel")}/> | ||||
|                     </AccessibleButton> | ||||
|                 </span>; | ||||
|                     </AccessibleButton>, | ||||
|                 ); | ||||
|                 roomBody = <div> | ||||
|                     <textarea className="mx_GroupView_editLongDesc" value={this.state.profileForm.long_description} | ||||
|                         onChange={this._onLongDescChange} | ||||
|  | @ -467,16 +586,27 @@ export default React.createClass({ | |||
|                     description = sanitizedHtmlNode(summary.profile.long_description); | ||||
|                 } | ||||
|                 roomBody = <div> | ||||
|                     {this._getMembershipSection()} | ||||
|                     <div className="mx_GroupView_groupDesc">{description}</div> | ||||
|                     {this._getFeaturedRoomsNode()} | ||||
|                     {this._getFeaturedUsersNode()} | ||||
|                 </div>; | ||||
|                 // disabled until editing works
 | ||||
|                 rightButtons = <AccessibleButton className="mx_GroupHeader_button" | ||||
|                     onClick={this._onEditClick} title={_t("Edit Group")} | ||||
|                 > | ||||
|                     <TintableSvg src="img/icons-settings-room.svg" width="16" height="16"/> | ||||
|                 </AccessibleButton>; | ||||
|                 rightButtons.push( | ||||
|                     <AccessibleButton className="mx_GroupHeader_button" | ||||
|                         onClick={this._onEditClick} title={_t("Edit Group")} key="_editButton" | ||||
|                     > | ||||
|                         <TintableSvg src="img/icons-settings-room.svg" width="16" height="16"/> | ||||
|                     </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; | ||||
|             } | ||||
|  |  | |||
|  | @ -241,10 +241,10 @@ export default React.createClass({ | |||
|                         eventPixelOffset={this.props.initialEventPixelOffset} | ||||
|                         key={this.props.currentRoomId || 'roomview'} | ||||
|                         opacity={this.props.middleOpacity} | ||||
|                         collapsedRhs={this.props.collapse_rhs} | ||||
|                         collapsedRhs={this.props.collapseRhs} | ||||
|                         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; | ||||
| 
 | ||||
|             case PageTypes.UserSettings: | ||||
|  | @ -255,7 +255,7 @@ export default React.createClass({ | |||
|                     referralBaseUrl={this.props.config.referralBaseUrl} | ||||
|                     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; | ||||
| 
 | ||||
|             case PageTypes.MyGroups: | ||||
|  | @ -265,9 +265,9 @@ export default React.createClass({ | |||
|             case PageTypes.CreateRoom: | ||||
|                 page_element = <CreateRoom | ||||
|                     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; | ||||
| 
 | ||||
|             case PageTypes.RoomDirectory: | ||||
|  | @ -300,8 +300,9 @@ export default React.createClass({ | |||
|             case PageTypes.GroupView: | ||||
|                 page_element = <GroupView | ||||
|                     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; | ||||
|         } | ||||
| 
 | ||||
|  | @ -333,7 +334,7 @@ export default React.createClass({ | |||
|                 <div className={bodyClasses}> | ||||
|                     <LeftPanel | ||||
|                         selectedRoom={this.props.currentRoomId} | ||||
|                         collapsed={this.props.collapse_lhs || false} | ||||
|                         collapsed={this.props.collapseLhs || false} | ||||
|                         opacity={this.props.leftOpacity} | ||||
|                     /> | ||||
|                     <main className='mx_MatrixChat_middlePanel'> | ||||
|  |  | |||
|  | @ -32,7 +32,7 @@ import dis from "../../dispatcher"; | |||
| import Modal from "../../Modal"; | ||||
| import Tinter from "../../Tinter"; | ||||
| import sdk from '../../index'; | ||||
| import { showStartChatInviteDialog, showRoomInviteDialog } from '../../Invite'; | ||||
| import { showStartChatInviteDialog, showRoomInviteDialog } from '../../RoomInvite'; | ||||
| import * as Rooms from '../../Rooms'; | ||||
| import linkifyMatrix from "../../linkify-matrix"; | ||||
| 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
 | ||||
|             viewUserId: null, | ||||
| 
 | ||||
|             collapse_lhs: false, | ||||
|             collapse_rhs: false, | ||||
|             collapseLhs: false, | ||||
|             collapseRhs: false, | ||||
|             leftOpacity: 1.0, | ||||
|             middleOpacity: 1.0, | ||||
|             rightOpacity: 1.0, | ||||
|  | @ -434,7 +434,7 @@ module.exports = React.createClass({ | |||
|                 break; | ||||
|             case 'view_user': | ||||
|                 // FIXME: ugly hack to expand the RightPanel and then re-dispatch.
 | ||||
|                 if (this.state.collapse_rhs) { | ||||
|                 if (this.state.collapseRhs) { | ||||
|                     setTimeout(()=>{ | ||||
|                         dis.dispatch({ | ||||
|                             action: 'show_right_panel', | ||||
|  | @ -516,22 +516,22 @@ module.exports = React.createClass({ | |||
|                 break; | ||||
|             case 'hide_left_panel': | ||||
|                 this.setState({ | ||||
|                     collapse_lhs: true, | ||||
|                     collapseLhs: true, | ||||
|                 }); | ||||
|                 break; | ||||
|             case 'show_left_panel': | ||||
|                 this.setState({ | ||||
|                     collapse_lhs: false, | ||||
|                     collapseLhs: false, | ||||
|                 }); | ||||
|                 break; | ||||
|             case 'hide_right_panel': | ||||
|                 this.setState({ | ||||
|                     collapse_rhs: true, | ||||
|                     collapseRhs: true, | ||||
|                 }); | ||||
|                 break; | ||||
|             case 'show_right_panel': | ||||
|                 this.setState({ | ||||
|                     collapse_rhs: false, | ||||
|                     collapseRhs: false, | ||||
|                 }); | ||||
|                 break; | ||||
|             case 'ui_opacity': { | ||||
|  | @ -993,8 +993,8 @@ module.exports = React.createClass({ | |||
|         this.setStateForNewView({ | ||||
|             view: VIEWS.LOGIN, | ||||
|             ready: false, | ||||
|             collapse_lhs: false, | ||||
|             collapse_rhs: false, | ||||
|             collapseLhs: false, | ||||
|             collapseRhs: false, | ||||
|             currentRoomId: null, | ||||
|             page_type: PageTypes.RoomDirectory, | ||||
|         }); | ||||
|  |  | |||
|  | @ -14,10 +14,8 @@ See the License for the specific language governing permissions and | |||
| limitations under the License. | ||||
| */ | ||||
| 
 | ||||
| 'use strict'; | ||||
| 
 | ||||
| var React = require('react'); | ||||
| var AvatarLogic = require("../../../Avatar"); | ||||
| import React from 'react'; | ||||
| import AvatarLogic from '../../../Avatar'; | ||||
| import sdk from '../../../index'; | ||||
| import AccessibleButton from '../elements/AccessibleButton'; | ||||
| 
 | ||||
|  |  | |||
|  | @ -18,6 +18,7 @@ import React from 'react'; | |||
| import sdk from '../../../index'; | ||||
| import { _t } from '../../../languageHandler'; | ||||
| import classnames from 'classnames'; | ||||
| import { GroupMemberType } from '../../../groups'; | ||||
| 
 | ||||
| /* | ||||
|  * A dialog for confirming an operation on another user. | ||||
|  | @ -30,7 +31,10 @@ import classnames from 'classnames'; | |||
| export default React.createClass({ | ||||
|     displayName: 'ConfirmUserActionDialog', | ||||
|     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'
 | ||||
| 
 | ||||
|         // Whether to display a text field for a reason
 | ||||
|  | @ -69,6 +73,7 @@ export default React.createClass({ | |||
|     render: function() { | ||||
|         const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); | ||||
|         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 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 ( | ||||
|             <BaseDialog className="mx_ConfirmUserActionDialog" onFinished={this.props.onFinished} | ||||
|                 onEnterPressed={ this.onOk } | ||||
|  | @ -98,10 +117,10 @@ export default React.createClass({ | |||
|             > | ||||
|                 <div className="mx_Dialog_content"> | ||||
|                     <div className="mx_ConfirmUserActionDialog_avatar"> | ||||
|                         <MemberAvatar member={this.props.member} width={48} height={48} /> | ||||
|                         {avatar} | ||||
|                     </div> | ||||
|                     <div className="mx_ConfirmUserActionDialog_name">{this.props.member.name}</div> | ||||
|                     <div className="mx_ConfirmUserActionDialog_userId">{this.props.member.userId}</div> | ||||
|                     <div className="mx_ConfirmUserActionDialog_name">{name}</div> | ||||
|                     <div className="mx_ConfirmUserActionDialog_userId">{userId}</div> | ||||
|                 </div> | ||||
|                 {reasonBox} | ||||
|                 <div className="mx_Dialog_buttons"> | ||||
|  |  | |||
|  | @ -1,5 +1,6 @@ | |||
| /* | ||||
| Copyright 2015, 2016 OpenMarket Ltd | ||||
| Copyright 2017 New Vector Ltd. | ||||
| 
 | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
|  | @ -17,6 +18,7 @@ limitations under the License. | |||
| import React from 'react'; | ||||
| import sdk from '../../../index'; | ||||
| import { _t } from '../../../languageHandler'; | ||||
| import classnames from 'classnames'; | ||||
| 
 | ||||
| export default React.createClass({ | ||||
|     displayName: 'QuestionDialog', | ||||
|  | @ -25,6 +27,7 @@ export default React.createClass({ | |||
|         description: React.PropTypes.node, | ||||
|         extraButtons: React.PropTypes.node, | ||||
|         button: React.PropTypes.string, | ||||
|         danger: React.PropTypes.bool, | ||||
|         focus: React.PropTypes.bool, | ||||
|         onFinished: React.PropTypes.func.isRequired, | ||||
|     }, | ||||
|  | @ -36,6 +39,7 @@ export default React.createClass({ | |||
|             extraButtons: null, | ||||
|             focus: true, | ||||
|             hasCancelButton: true, | ||||
|             danger: false, | ||||
|         }; | ||||
|     }, | ||||
| 
 | ||||
|  | @ -54,6 +58,10 @@ export default React.createClass({ | |||
|                 {_t("Cancel")} | ||||
|             </button> | ||||
|         ) : null; | ||||
|         const buttonClasses = classnames({ | ||||
|             mx_Dialog_primary: true, | ||||
|             danger: this.props.danger, | ||||
|         }); | ||||
|         return ( | ||||
|             <BaseDialog className="mx_QuestionDialog" onFinished={this.props.onFinished} | ||||
|                 onEnterPressed={ this.onOk } | ||||
|  | @ -63,7 +71,7 @@ export default React.createClass({ | |||
|                     {this.props.description} | ||||
|                 </div> | ||||
|                 <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')} | ||||
|                     </button> | ||||
|                     {this.props.extraButtons} | ||||
|  |  | |||
|  | @ -0,0 +1,210 @@ | |||
| /* | ||||
|  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. | ||||
|  */ | ||||
| 
 | ||||
| 'use strict'; | ||||
| 
 | ||||
| import React from 'react'; | ||||
| import PropTypes from 'prop-types'; | ||||
| import {MatrixClient} from 'matrix-js-sdk'; | ||||
| import UserSettingsStore from '../../../UserSettingsStore'; | ||||
| import Promise from 'bluebird'; | ||||
| 
 | ||||
| const BULK_REQUEST_DEBOUNCE_MS = 200; | ||||
| 
 | ||||
| // Does the server support groups? Assume yes until we receive M_UNRECOGNIZED.
 | ||||
| // If true, flair can function and we should keep sending requests for groups and avatars.
 | ||||
| let groupSupport = true; | ||||
| 
 | ||||
| // TODO: Cache-busting based on time. (The server won't inform us of membership changes.)
 | ||||
| // This applies to userGroups and groupProfiles. We can provide a slightly better UX by
 | ||||
| // cache-busting when the current user joins/leaves a group.
 | ||||
| const userGroups = { | ||||
|     // $userId: ['+group1:domain', '+group2:domain', ...]
 | ||||
| }; | ||||
| 
 | ||||
| const groupProfiles = { | ||||
| //  $groupId: {
 | ||||
| //      avatar_url: 'mxc://...'
 | ||||
| //  }
 | ||||
| }; | ||||
| 
 | ||||
| // Represents all unsettled promises to retrieve the groups for each userId. When a promise
 | ||||
| // is settled, it is deleted from this object.
 | ||||
| const usersPending = { | ||||
| //  $userId: {
 | ||||
| //      prom: Promise
 | ||||
| //      resolve: () => {}
 | ||||
| //      reject: () => {}
 | ||||
| //  }
 | ||||
| }; | ||||
| 
 | ||||
| let debounceTimeoutID; | ||||
| function getPublicisedGroupsCached(matrixClient, userId) { | ||||
|     if (userGroups[userId]) { | ||||
|         return Promise.resolve(userGroups[userId]); | ||||
|     } | ||||
| 
 | ||||
|     // Bulk lookup ongoing, return promise to resolve/reject
 | ||||
|     if (usersPending[userId]) { | ||||
|         return usersPending[userId].prom; | ||||
|     } | ||||
| 
 | ||||
|     usersPending[userId] = {}; | ||||
|     usersPending[userId].prom = new Promise((resolve, reject) => { | ||||
|         usersPending[userId].resolve = resolve; | ||||
|         usersPending[userId].reject = reject; | ||||
|     }).then((groups) => { | ||||
|         userGroups[userId] = groups; | ||||
|         // TODO: Reset cache at this point
 | ||||
|         return userGroups[userId]; | ||||
|     }).catch((err) => { | ||||
|         throw err; | ||||
|     }).finally(() => { | ||||
|         delete usersPending[userId]; | ||||
|     }); | ||||
| 
 | ||||
|     // This debounce will allow consecutive requests for the public groups of users that
 | ||||
|     // are sent in intervals of < BULK_REQUEST_DEBOUNCE_MS to be batched and only requested
 | ||||
|     // when no more requests are received within the next BULK_REQUEST_DEBOUNCE_MS. The naive
 | ||||
|     // implementation would do a request that only requested the groups for `userId`, leading
 | ||||
|     // to a worst and best case of 1 user per request. This implementation's worst is still
 | ||||
|     // 1 user per request but only if the requests are > BULK_REQUEST_DEBOUNCE_MS apart and the
 | ||||
|     // best case is N users per request.
 | ||||
|     //
 | ||||
|     // This is to reduce the number of requests made whilst trading off latency when viewing
 | ||||
|     // a Flair component.
 | ||||
|     if (debounceTimeoutID) clearTimeout(debounceTimeoutID); | ||||
|     debounceTimeoutID = setTimeout(() => { | ||||
|         batchedGetPublicGroups(matrixClient); | ||||
|     }, BULK_REQUEST_DEBOUNCE_MS); | ||||
| 
 | ||||
|     return usersPending[userId].prom; | ||||
| } | ||||
| 
 | ||||
| async function batchedGetPublicGroups(matrixClient) { | ||||
|     // Take the userIds from the keys of usersPending
 | ||||
|     const usersInFlight = Object.keys(usersPending); | ||||
|     let resp = { | ||||
|         users: [], | ||||
|     }; | ||||
|     try { | ||||
|         resp = await matrixClient.getPublicisedGroups(usersInFlight); | ||||
|     } catch (err) { | ||||
|         // Propagate the same error to all usersInFlight
 | ||||
|         usersInFlight.forEach((userId) => { | ||||
|             usersPending[userId].reject(err); | ||||
|         }); | ||||
|         return; | ||||
|     } | ||||
|     const updatedUserGroups = resp.users; | ||||
|     usersInFlight.forEach((userId) => { | ||||
|         usersPending[userId].resolve(updatedUserGroups[userId] || []); | ||||
|     }); | ||||
| } | ||||
| 
 | ||||
| async function getGroupProfileCached(matrixClient, groupId) { | ||||
|     if (groupProfiles[groupId]) { | ||||
|         return groupProfiles[groupId]; | ||||
|     } | ||||
| 
 | ||||
|     groupProfiles[groupId] = await matrixClient.getGroupProfile(groupId); | ||||
|     return groupProfiles[groupId]; | ||||
| } | ||||
| 
 | ||||
| export default class Flair extends React.Component { | ||||
|     constructor() { | ||||
|         super(); | ||||
|         this.state = { | ||||
|             avatarUrls: [], | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
|     componentWillUnmount() { | ||||
|         this._unmounted = true; | ||||
|     } | ||||
| 
 | ||||
|     componentWillMount() { | ||||
|         this._unmounted = false; | ||||
|         if (UserSettingsStore.isFeatureEnabled('feature_flair') && groupSupport) { | ||||
|             this._generateAvatars(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     async _getAvatarUrls(groups) { | ||||
|         const profiles = []; | ||||
|         for (const groupId of groups) { | ||||
|             let groupProfile = null; | ||||
|             try { | ||||
|                 groupProfile = await getGroupProfileCached(this.context.matrixClient, groupId); | ||||
|             } catch (err) { | ||||
|                 console.error('Could not get profile for group', groupId, err); | ||||
|             } | ||||
|             profiles.push(groupProfile); | ||||
|         } | ||||
| 
 | ||||
|         const avatarUrls = profiles.filter((p) => p !== null).map((p) => p.avatar_url); | ||||
|         return avatarUrls; | ||||
|     } | ||||
| 
 | ||||
|     async _generateAvatars() { | ||||
|         let groups; | ||||
|         try { | ||||
|             groups = await getPublicisedGroupsCached(this.context.matrixClient, this.props.userId); | ||||
|         } catch (err) { | ||||
|             // Indicate whether the homeserver supports groups
 | ||||
|             if (err.errcode === 'M_UNRECOGNIZED') { | ||||
|                 console.warn('Cannot display flair, server does not support groups'); | ||||
|                 groupSupport = false; | ||||
|                 // Return silently to avoid spamming for non-supporting servers
 | ||||
|                 return; | ||||
|             } | ||||
|             console.error('Could not get groups for user', this.props.userId, err); | ||||
|         } | ||||
|         if (!groups || groups.length === 0) { | ||||
|             return; | ||||
|         } | ||||
|         const avatarUrls = await this._getAvatarUrls(groups); | ||||
|         if (!this.unmounted) { | ||||
|             this.setState({avatarUrls}); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     render() { | ||||
|         if (this.state.avatarUrls.length === 0) { | ||||
|             return <div />; | ||||
|         } | ||||
|         const avatars = this.state.avatarUrls.map((avatarUrl, index) => { | ||||
|             const httpUrl = this.context.matrixClient.mxcUrlToHttp(avatarUrl, 14, 14, 'scale', false); | ||||
|             return <img key={index} src={httpUrl} width="14px" height="14px"/>; | ||||
|         }); | ||||
|         return ( | ||||
|             <span className="mx_Flair" style={{"marginLeft": "5px", "verticalAlign": "-3px"}}> | ||||
|                 {avatars} | ||||
|             </span> | ||||
|         ); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| Flair.propTypes = { | ||||
|     userId: PropTypes.string, | ||||
| }; | ||||
| 
 | ||||
| // TODO: We've decided that all components should follow this pattern, which means removing withMatrixClient and using
 | ||||
| // this.context.matrixClient everywhere instead of this.props.matrixClient.
 | ||||
| // See https://github.com/vector-im/riot-web/issues/4951.
 | ||||
| Flair.contextTypes = { | ||||
|     matrixClient: React.PropTypes.instanceOf(MatrixClient).isRequired, | ||||
| }; | ||||
|  | @ -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> | ||||
|         ); | ||||
|     }, | ||||
| }); | ||||
|  | @ -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> | ||||
|         ); | ||||
|     }, | ||||
| })); | ||||
|  | @ -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> | ||||
|         ); | ||||
|     }, | ||||
| })); | ||||
|  | @ -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} | ||||
|             /> | ||||
|         ); | ||||
|     }, | ||||
| })); | ||||
|  | @ -18,6 +18,7 @@ | |||
| 
 | ||||
| import React from 'react'; | ||||
| import sdk from '../../../index'; | ||||
| import Flair from '../elements/Flair.js'; | ||||
| 
 | ||||
| export default function SenderProfile(props) { | ||||
|     const EmojiText = sdk.getComponent('elements.EmojiText'); | ||||
|  | @ -30,8 +31,11 @@ export default function SenderProfile(props) { | |||
|     } | ||||
| 
 | ||||
|     return ( | ||||
|         <EmojiText className="mx_SenderProfile" dir="auto" | ||||
|               onClick={props.onClick}>{`${name || ''} ${props.aux || ''}`}</EmojiText> | ||||
|         <div className="mx_SenderProfile" dir="auto" onClick={props.onClick}> | ||||
|             <EmojiText>{name || ''}</EmojiText> | ||||
|             {props.enableFlair ? <Flair userId={mxEvent.getSender()} /> : null} | ||||
|             {props.aux ? <EmojiText> {props.aux}</EmojiText> : null} | ||||
|         </div> | ||||
|     ); | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -508,10 +508,10 @@ module.exports = withMatrixClient(React.createClass({ | |||
|                 if (msgtype === 'm.image') aux = _t('sent an image'); | ||||
|                 else if (msgtype === 'm.video') aux = _t('sent a video'); | ||||
|                 else if (msgtype === 'm.file') aux = _t('uploaded a file'); | ||||
|                 sender = <SenderProfile onClick={ this.onSenderProfileClick } mxEvent={this.props.mxEvent} aux={aux} />; | ||||
|                 sender = <SenderProfile onClick={ this.onSenderProfileClick } mxEvent={this.props.mxEvent} enableFlair={!aux} aux={aux} />; | ||||
|             } | ||||
|             else { | ||||
|                 sender = <SenderProfile mxEvent={this.props.mxEvent} />; | ||||
|                 sender = <SenderProfile mxEvent={this.props.mxEvent} enableFlair={true} />; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|  |  | |||
|  | @ -751,7 +751,7 @@ module.exports = withMatrixClient(React.createClass({ | |||
|         if (kickButton || banButton || muteButton || giveModButton) { | ||||
|             adminTools = | ||||
|                 <div> | ||||
|                     <h3>{_t("Admin tools")}</h3> | ||||
|                     <h3>{_t("Admin Tools")}</h3> | ||||
| 
 | ||||
|                     <div className="mx_MemberInfo_buttons"> | ||||
|                         {muteButton} | ||||
|  |  | |||
|  | @ -26,7 +26,6 @@ var sdk = require('../../../index'); | |||
| var GeminiScrollbar = require('react-gemini-scrollbar'); | ||||
| var rate_limited_func = require('../../../ratelimitedfunc'); | ||||
| var CallHandler = require("../../../CallHandler"); | ||||
| var Invite = require("../../../Invite"); | ||||
| 
 | ||||
| var INITIAL_LOAD_NUM_MEMBERS = 30; | ||||
| 
 | ||||
|  |  | |||
|  | @ -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() { | ||||
|         var RoomSubList = sdk.getComponent('structures.RoomSubList'); | ||||
|         const RoomSubList = sdk.getComponent('structures.RoomSubList'); | ||||
| 
 | ||||
|         const inviteSectionExtraTiles = this._makeGroupInviteTiles(); | ||||
| 
 | ||||
|         var self = this; | ||||
|         return ( | ||||
|             <GeminiScrollbar className="mx_RoomList_scrollbar" | ||||
|  | @ -567,7 +583,9 @@ module.exports = React.createClass({ | |||
|                              collapsed={ self.props.collapsed } | ||||
|                              searchFilter={ self.props.searchFilter } | ||||
|                              onHeaderClick={ self.onSubListHeaderClick } | ||||
|                              onShowMoreRooms={ self.onShowMoreRooms } /> | ||||
|                              onShowMoreRooms={ self.onShowMoreRooms } | ||||
|                              extraTiles={ inviteSectionExtraTiles } | ||||
|                 /> | ||||
| 
 | ||||
|                 <RoomSubList list={ self.state.lists['m.favourite'] } | ||||
|                              label={ _t('Favourites') } | ||||
|  |  | |||
|  | @ -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, | ||||
|     }; | ||||
| } | ||||
|  | @ -790,7 +790,7 @@ | |||
|     "a room": "einen Raum", | ||||
|     "Accept": "Akzeptieren", | ||||
|     "Active call (%(roomName)s)": "Aktiver Anruf (%(roomName)s)", | ||||
|     "Admin tools": "Admin-Werkzeuge", | ||||
|     "Admin Tools": "Admin-Werkzeuge", | ||||
|     "And %(count)s more...": "Und %(count)s weitere...", | ||||
|     "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.", | ||||
|  |  | |||
|  | @ -265,7 +265,7 @@ | |||
|     "Accept": "Αποδοχή", | ||||
|     "Active call (%(roomName)s)": "Ενεργή κλήση (%(roomName)s)", | ||||
|     "Add": "Προσθήκη", | ||||
|     "Admin tools": "Εργαλεία διαχειριστή", | ||||
|     "Admin Tools": "Εργαλεία διαχειριστή", | ||||
|     "And %(count)s more...": "Και %(count)s περισσότερα...", | ||||
|     "No media permissions": "Χωρίς δικαιώματα πολυμέσων", | ||||
|     "Alias (optional)": "Ψευδώνυμο (προαιρετικό)", | ||||
|  |  | |||
|  | @ -13,7 +13,7 @@ | |||
|     "Add email address": "Add email address", | ||||
|     "Add phone number": "Add phone number", | ||||
|     "Admin": "Admin", | ||||
|     "Admin tools": "Admin tools", | ||||
|     "Admin Tools": "Admin tools", | ||||
|     "Allow": "Allow", | ||||
|     "And %(count)s more...": "And %(count)s more...", | ||||
|     "VoIP": "VoIP", | ||||
|  | @ -864,5 +864,27 @@ | |||
|     "%(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 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?", | ||||
|     "%(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>", | ||||
|     "Flair": "Flair" | ||||
| } | ||||
|  |  | |||
|  | @ -724,7 +724,7 @@ | |||
|     "Accept": "Accept", | ||||
|     "a room": "a room", | ||||
|     "Add": "Add", | ||||
|     "Admin tools": "Admin tools", | ||||
|     "Admin Tools": "Admin tools", | ||||
|     "And %(count)s more...": "And %(count)s more...", | ||||
|     "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.", | ||||
|  |  | |||
|  | @ -204,7 +204,7 @@ | |||
|     "Low priority": "Baja prioridad", | ||||
|     "Accept": "Aceptar", | ||||
|     "Add": "Añadir", | ||||
|     "Admin tools": "Herramientas de administración", | ||||
|     "Admin Tools": "Herramientas de administración", | ||||
|     "VoIP": "Voz IP", | ||||
|     "No Microphones detected": "No se ha detectado micrófono", | ||||
|     "No Webcams detected": "No se ha detectado cámara", | ||||
|  |  | |||
|  | @ -167,7 +167,7 @@ | |||
|     "Add": "Gehitu", | ||||
|     "Add a topic": "Gehitu gai bat", | ||||
|     "Admin": "Kudeatzailea", | ||||
|     "Admin tools": "Kudeaketa tresnak", | ||||
|     "Admin Tools": "Kudeaketa tresnak", | ||||
|     "And %(count)s more...": "Eta %(count)s gehiago...", | ||||
|     "VoIP": "VoIP", | ||||
|     "Missing Media Permissions, click here to request.": "Media baimenak falta dira, egin klik eskatzeko.", | ||||
|  |  | |||
|  | @ -39,7 +39,7 @@ | |||
|     "Add email address": "E-mail cím megadása", | ||||
|     "Add phone number": "Telefonszám megadása", | ||||
|     "Admin": "Adminisztrátor", | ||||
|     "Admin tools": "Admin. eszközök", | ||||
|     "Admin Tools": "Admin. eszközök", | ||||
|     "And %(count)s more...": "És még %(count)s...", | ||||
|     "VoIP": "VoIP", | ||||
|     "Missing Media Permissions, click here to request.": "Hiányzó Média jogosultság, kattintson ide az igényléshez.", | ||||
|  |  | |||
|  | @ -172,7 +172,7 @@ | |||
|     "Access Token:": "Token Akses:", | ||||
|     "Active call (%(roomName)s)": "Panggilan aktif (%(roomName)s)", | ||||
|     "Admin": "Admin", | ||||
|     "Admin tools": "Alat admin", | ||||
|     "Admin Tools": "Alat admin", | ||||
|     "And %(count)s more...": "Dan %(count)s lagi...", | ||||
|     "VoIP": "VoIP", | ||||
|     "Missing Media Permissions, click here to request.": "Tidak ada Izin Media, klik disini untuk meminta.", | ||||
|  |  | |||
|  | @ -42,7 +42,7 @@ | |||
|     "Add email address": "Aggiungi indirizzo email", | ||||
|     "Add phone number": "Aggiungi numero di telefono", | ||||
|     "Admin": "Amministratore", | ||||
|     "Admin tools": "Strumenti di amministrazione", | ||||
|     "Admin Tools": "Strumenti di amministrazione", | ||||
|     "VoIP": "VoIP", | ||||
|     "No Microphones detected": "Nessun Microfono rilevato", | ||||
|     "No Webcams detected": "Nessuna Webcam rilevata", | ||||
|  |  | |||
|  | @ -33,7 +33,7 @@ | |||
|     "Add email address": "이메일 주소 추가하기", | ||||
|     "Add phone number": "전화번호 추가하기", | ||||
|     "Admin": "관리자", | ||||
|     "Admin tools": "관리 도구", | ||||
|     "Admin Tools": "관리 도구", | ||||
|     "VoIP": "인터넷전화", | ||||
|     "No Microphones detected": "마이크를 찾지 못했어요", | ||||
|     "No Webcams detected": "카메라를 찾지 못했어요", | ||||
|  |  | |||
|  | @ -12,7 +12,7 @@ | |||
|     "Add email address": "Pievieno Epasta adresi", | ||||
|     "Add phone number": "Pievieno tālruņa numuru", | ||||
|     "Admin": "Administrators", | ||||
|     "Admin tools": "Administratora rīki", | ||||
|     "Admin Tools": "Administratora rīki", | ||||
|     "And %(count)s more...": "Un vēl %(count)s citi...", | ||||
|     "VoIP": "VoIP", | ||||
|     "Missing Media Permissions, click here to request.": "Nav pieejas medija saturam. Klikšķini šeit, lai pieprasītu.", | ||||
|  |  | |||
|  | @ -79,7 +79,7 @@ | |||
|     "Active call (%(roomName)s)": "Actief gesprek (%(roomName)s)", | ||||
|     "Add": "Toevoegen", | ||||
|     "Add a topic": "Een onderwerp toevoegen", | ||||
|     "Admin tools": "Beheerhulpmiddelen", | ||||
|     "Admin Tools": "Beheerhulpmiddelen", | ||||
|     "And %(count)s more...": "Nog %(count)s andere...", | ||||
|     "VoIP": "VoiP", | ||||
|     "Missing Media Permissions, click here to request.": "Ontbrekende mediatoestemmingen, klik hier om aan te vragen.", | ||||
|  |  | |||
|  | @ -123,7 +123,7 @@ | |||
|     "Active call (%(roomName)s)": "Aktywne połączenie (%(roomName)s)", | ||||
|     "Add email address": "Dodaj adres e-mail", | ||||
|     "Admin": "Administrator", | ||||
|     "Admin tools": "Narzędzia administracyjne", | ||||
|     "Admin Tools": "Narzędzia administracyjne", | ||||
|     "And %(count)s more...": "Oraz %(count)s więcej...", | ||||
|     "VoIP": "VoIP (połączenie głosowe)", | ||||
|     "No Microphones detected": "Nie wykryto żadnego mikrofonu", | ||||
|  |  | |||
|  | @ -772,7 +772,7 @@ | |||
|     "Public Chat": "Conversa pública", | ||||
|     "Uploading %(filename)s and %(count)s others|zero": "Enviando o arquivo %(filename)s", | ||||
|     "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.", | ||||
|     "Undecryptable": "Não é possível descriptografar", | ||||
|     "Incoming video call from %(name)s": "Chamada de vídeo de %(name)s recebida", | ||||
|  |  | |||
|  | @ -783,7 +783,7 @@ | |||
|     "a room": "uma sala", | ||||
|     "Accept": "Aceitar", | ||||
|     "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...", | ||||
|     "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.", | ||||
|  |  | |||
|  | @ -767,7 +767,7 @@ | |||
|     "a room": "комната", | ||||
|     "Accept": "Принять", | ||||
|     "Active call (%(roomName)s)": "Активный вызов (%(roomName)s)", | ||||
|     "Admin tools": "Инструменты администратора", | ||||
|     "Admin Tools": "Инструменты администратора", | ||||
|     "And %(count)s more...": "И %(count)s больше...", | ||||
|     "Alias (optional)": "Псевдоним (опционально)", | ||||
|     "<a>Click here</a> to join the discussion!": "<a>Нажмите здесь</a>, чтобы присоединиться к обсуждению!", | ||||
|  |  | |||
|  | @ -171,7 +171,7 @@ | |||
|     "Access Token:": "Åtkomsttoken:", | ||||
|     "Active call (%(roomName)s)": "Aktiv samtal (%(roomName)s)", | ||||
|     "Add": "Lägg till", | ||||
|     "Admin tools": "Admin verktyg", | ||||
|     "Admin Tools": "Admin verktyg", | ||||
|     "And %(count)s more...": "Och %(count)s till...", | ||||
|     "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.", | ||||
|  |  | |||
|  | @ -11,7 +11,7 @@ | |||
|     "Add email address": "ఇమెయిల్ చిరునామాను జోడించండి", | ||||
|     "Add phone number": "ఫోన్ నంబర్ను జోడించండి", | ||||
|     "Admin": "అడ్మిన్", | ||||
|     "Admin tools": "నిర్వాహక ఉపకరణాలు", | ||||
|     "Admin Tools": "నిర్వాహక ఉపకరణాలు", | ||||
|     "VoIP": "విఒఐపి", | ||||
|     "Missing Media Permissions, click here to request.": "మీడియా అనుమతులు మిస్ అయయి, అభ్యర్థించడానికి ఇక్కడ క్లిక్ చేయండి.", | ||||
|     "No Microphones detected": "మైక్రోఫోన్లు కనుగొనబడలేదు", | ||||
|  |  | |||
|  | @ -12,7 +12,7 @@ | |||
|     "Add email address": "E-posta adresi ekle", | ||||
|     "Add phone number": "Telefon numarası ekle", | ||||
|     "Admin": "Admin", | ||||
|     "Admin tools": "Admin araçları", | ||||
|     "Admin Tools": "Admin araçları", | ||||
|     "And %(count)s more...": "Ve %(count)s fazlası...", | ||||
|     "VoIP": "VoIP", | ||||
|     "Missing Media Permissions, click here to request.": "Medya İzinleri Yok , talep etmek için burayı tıklayın.", | ||||
|  |  | |||
|  | @ -45,7 +45,7 @@ | |||
|     "Add email address": "Додати адресу е-пошти", | ||||
|     "Add phone number": "Додати номер телефону", | ||||
|     "Admin": "Адміністратор", | ||||
|     "Admin tools": "Засоби адміністрування", | ||||
|     "Admin Tools": "Засоби адміністрування", | ||||
|     "And %(count)s more...": "І %(count)s більше...", | ||||
|     "VoIP": "VoIP", | ||||
|     "Missing Media Permissions, click here to request.": "Відсутні дозволи, натисніть для запиту.", | ||||
|  |  | |||
|  | @ -190,7 +190,7 @@ | |||
|     "New password": "新密码", | ||||
|     "Add a topic": "添加一个主题", | ||||
|     "Admin": "管理员", | ||||
|     "Admin tools": "管理工具", | ||||
|     "Admin Tools": "管理工具", | ||||
|     "VoIP": "IP 电话", | ||||
|     "Missing Media Permissions, click here to request.": "没有媒体存储权限,点此获取。", | ||||
|     "No Microphones detected": "未检测到麦克风", | ||||
|  |  | |||
|  | @ -302,7 +302,7 @@ | |||
|     "%(targetName)s accepted the invitation for %(displayName)s.": "%(targetName)s 已接受 %(displayName)s 的邀請。", | ||||
|     "Active call (%(roomName)s)": "活躍的通話(%(roomName)s)", | ||||
|     "Add": "新增", | ||||
|     "Admin tools": "管理員工具", | ||||
|     "Admin Tools": "管理員工具", | ||||
|     "And %(count)s more...": "還有 %(count)s 個...", | ||||
|     "Missing Media Permissions, click here to request.": "遺失媒體權限,點選這裡來要求。", | ||||
|     "No Microphones detected": "未偵測到麥克風", | ||||
|  |  | |||
|  | @ -1,5 +1,6 @@ | |||
| /* | ||||
| Copyright 2016 OpenMarket Ltd | ||||
| Copyright 2017 New Vector Ltd | ||||
| 
 | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
|  | @ -14,16 +15,26 @@ See the License for the specific language governing permissions and | |||
| limitations under the License. | ||||
| */ | ||||
| 
 | ||||
| import MatrixClientPeg from '../MatrixClientPeg'; | ||||
| import {getAddressType} from '../UserAddress'; | ||||
| import {inviteToRoom} from '../Invite'; | ||||
| import {inviteToRoom} from '../RoomInvite'; | ||||
| 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 { | ||||
|     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.addrs = []; | ||||
|  | @ -104,7 +115,14 @@ export default class MultiInviter { | |||
|             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; } | ||||
| 
 | ||||
|             this.completionStates[addr] = 'invited'; | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue
	
	 Luke Barnard
						Luke Barnard