diff --git a/src/components/structures/RightPanel.js b/src/components/structures/RightPanel.js index 18567b57..dedd715f 100644 --- a/src/components/structures/RightPanel.js +++ b/src/components/structures/RightPanel.js @@ -22,7 +22,7 @@ import classNames from 'classnames'; import { _t } from 'matrix-react-sdk/lib/languageHandler'; import sdk from 'matrix-react-sdk'; import dis from 'matrix-react-sdk/lib/dispatcher'; -import MatrixClient from 'matrix-js-sdk'; +import { MatrixClient } from 'matrix-js-sdk'; import Analytics from 'matrix-react-sdk/lib/Analytics'; import rate_limited_func from 'matrix-react-sdk/lib/ratelimitedfunc'; import AccessibleButton from 'matrix-react-sdk/lib/components/views/elements/AccessibleButton'; @@ -90,6 +90,7 @@ module.exports = React.createClass({ RoomMemberList: 'RoomMemberList', GroupMemberList: 'GroupMemberList', GroupRoomList: 'GroupRoomList', + GroupRoomInfo: 'GroupRoomInfo', FilePanel: 'FilePanel', NotificationPanel: 'NotificationPanel', RoomMemberInfo: 'RoomMemberInfo', @@ -205,7 +206,6 @@ module.exports = React.createClass({ } else if (this.props.groupId) { this.setState({ phase: this.Phase.GroupMemberList, - groupId: payload.groupId, member: payload.member, }); } @@ -213,13 +213,20 @@ module.exports = React.createClass({ } else if (payload.action === "view_group") { this.setState({ phase: this.Phase.GroupMemberList, - groupId: payload.groupId, member: null, }); + } else if (payload.action === "view_group_room") { + this.setState({ + phase: this.Phase.GroupRoomInfo, + groupRoomId: payload.groupRoomId, + }); + } else if (payload.action === "view_group_room_list") { + this.setState({ + phase: this.Phase.GroupRoomList, + }); } else if (payload.action === "view_group_user") { this.setState({ phase: this.Phase.GroupMemberInfo, - groupId: payload.groupId, member: payload.member, }); } else if (payload.action === "view_room") { @@ -242,6 +249,7 @@ module.exports = React.createClass({ const GroupMemberList = sdk.getComponent('groups.GroupMemberList'); const GroupMemberInfo = sdk.getComponent('groups.GroupMemberInfo'); const GroupRoomList = sdk.getComponent('groups.GroupRoomList'); + const GroupRoomInfo = sdk.getComponent('groups.GroupRoomInfo'); const TintableSvg = sdk.getComponent("elements.TintableSvg"); @@ -305,7 +313,7 @@ module.exports = React.createClass({ analytics={['Right Panel', 'Group Member List Button', 'click']} />, <HeaderButton key="_roomsButton" title={_t('Rooms')} iconSrc="img/icons-room.svg" - isHighlighted={this.state.phase === this.Phase.GroupRoomList} + isHighlighted={[this.Phase.GroupRoomList, this.Phase.GroupRoomInfo].includes(this.state.phase)} clickPhase={this.Phase.GroupRoomList} analytics={['Right Panel', 'Group Room List Button', 'click']} />, @@ -340,6 +348,11 @@ module.exports = React.createClass({ groupMember={this.state.member} groupId={this.props.groupId} key={this.state.member.user_id} />; + } else if (this.state.phase == this.Phase.GroupRoomInfo) { + panel = <GroupRoomInfo + groupRoomId={this.state.groupRoomId} + groupId={this.props.groupId} + key={this.state.groupRoomId} />; } else if (this.state.phase == this.Phase.NotificationPanel) { panel = <NotificationPanel />; } else if (this.state.phase == this.Phase.FilePanel) { diff --git a/src/components/structures/RoomDirectory.js b/src/components/structures/RoomDirectory.js index 323af86c..126ae404 100644 --- a/src/components/structures/RoomDirectory.js +++ b/src/components/structures/RoomDirectory.js @@ -409,7 +409,7 @@ module.exports = React.createClass({ perms = null; if (guestRead || guestJoin) { - perms = <div className="mx_RoomDirectory_perms">{guestRead} {guestJoin}</div>; + perms = <div className="mx_RoomDirectory_perms">{guestRead}{guestJoin}</div>; } var topic = rooms[i].topic || ''; diff --git a/src/components/structures/RoomSubList.js b/src/components/structures/RoomSubList.js index 8fb7562f..c3f51bc8 100644 --- a/src/components/structures/RoomSubList.js +++ b/src/components/structures/RoomSubList.js @@ -246,10 +246,14 @@ var RoomSubList = React.createClass({ roomNotificationCount: function(truncateAt) { var self = this; + if (this.props.isInvite) { + return [0, true]; + } + return this.props.list.reduce(function(result, room, index) { if (truncateAt === undefined || index >= truncateAt) { var roomNotifState = RoomNotifs.getRoomNotifsState(room.roomId); - var highlight = room.getUnreadNotificationCount('highlight') > 0 || self.props.isInvite; + var highlight = room.getUnreadNotificationCount('highlight') > 0; var notificationCount = room.getUnreadNotificationCount(); const notifBadges = notificationCount > 0 && self._shouldShowNotifBadge(roomNotifState); @@ -394,7 +398,8 @@ var RoomSubList = React.createClass({ var subListNotifCount = subListNotifications[0]; var subListNotifHighlight = subListNotifications[1]; - var roomCount = this.props.list.length > 0 ? this.props.list.length : ''; + var totalTiles = this.props.list.length + (this.props.extraTiles || []).length; + var roomCount = totalTiles > 0 ? totalTiles : ''; var chevronClasses = classNames({ 'mx_RoomSubList_chevron': true, diff --git a/src/components/views/dialogs/DevtoolsDialog.js b/src/components/views/dialogs/DevtoolsDialog.js index 7760ea84..a7155ad1 100644 --- a/src/components/views/dialogs/DevtoolsDialog.js +++ b/src/components/views/dialogs/DevtoolsDialog.js @@ -15,35 +15,24 @@ limitations under the License. */ import React from 'react'; +import PropTypes from 'prop-types'; import sdk from 'matrix-react-sdk'; import { _t } from 'matrix-react-sdk/lib/languageHandler'; import MatrixClientPeg from 'matrix-react-sdk/lib/MatrixClientPeg'; -class SendCustomEvent extends React.Component { - static propTypes = { - roomId: React.PropTypes.string.isRequired, - onBack: React.PropTypes.func.isRequired, - - eventType: React.PropTypes.string.isRequired, - evContent: React.PropTypes.string.isRequired, +class DevtoolsComponent extends React.Component { + static contextTypes = { + roomId: PropTypes.string.isRequired, }; +} - static defaultProps = { - eventType: '', - evContent: '{\n\n}', - }; +class GenericEditor extends DevtoolsComponent { + // static propTypes = {onBack: PropTypes.func.isRequired}; constructor(props, context) { super(props, context); - this._send = this._send.bind(this); - this.onBack = this.onBack.bind(this); this._onChange = this._onChange.bind(this); - - this.state = { - message: null, - input_eventType: this.props.eventType, - input_evContent: this.props.evContent, - }; + this.onBack = this.onBack.bind(this); } onBack() { @@ -54,6 +43,10 @@ class SendCustomEvent extends React.Component { } } + _onChange(e) { + this.setState({[e.target.id]: e.target.type === 'checkbox' ? e.target.checked : e.target.value}); + } + _buttons() { return <div className="mx_Dialog_buttons"> <button onClick={this.onBack}>{ _t('Back') }</button> @@ -61,19 +54,64 @@ class SendCustomEvent extends React.Component { </div>; } + textInput(id, label) { + return <div className="mx_DevTools_inputRow"> + <div className="mx_DevTools_inputLabelCell"> + <label htmlFor={id}>{ label }</label> + </div> + <div className="mx_DevTools_inputCell"> + <input id={id} onChange={this._onChange} value={this.state[id]} size="32" /> + </div> + </div>; + } +} + +class SendCustomEvent extends GenericEditor { + static getLabel() { return _t('Send Custom Event'); } + + static propTypes = { + onBack: PropTypes.func.isRequired, + forceStateEvent: PropTypes.bool, + inputs: PropTypes.object, + }; + + constructor(props, context) { + super(props, context); + this._send = this._send.bind(this); + + const {eventType, stateKey, evContent} = Object.assign({ + eventType: '', + stateKey: '', + evContent: '{\n\n}', + }, this.props.inputs); + + this.state = { + isStateEvent: Boolean(this.props.forceStateEvent), + + eventType, + stateKey, + evContent, + }; + } + send(content) { - return MatrixClientPeg.get().sendEvent(this.props.roomId, this.state.input_eventType, content); + const cli = MatrixClientPeg.get(); + if (this.state.isStateEvent) { + return cli.sendStateEvent(this.context.roomId, this.state.eventType, content, this.state.stateKey); + } else { + return cli.sendEvent(this.context.roomId, this.state.eventType, content); + } } async _send() { - if (this.state.input_eventType === '') { + if (this.state.eventType === '') { this.setState({ message: _t('You must specify an event type!') }); return; } let message; try { - const content = JSON.parse(this.state.input_evContent); + const content = JSON.parse(this.state.evContent); await this.send(content); message = _t('Event sent!'); } catch (e) { @@ -82,14 +120,6 @@ class SendCustomEvent extends React.Component { this.setState({ message }); } - _additionalFields() { - return <div />; - } - - _onChange(e) { - this.setState({[`input_${e.target.id}`]: e.target.value}); - } - render() { if (this.state.message) { return <div> @@ -102,87 +132,176 @@ class SendCustomEvent extends React.Component { return <div> <div className="mx_Dialog_content"> - { this._additionalFields() } - <div className="mx_TextInputDialog_label"> - <label htmlFor="eventType"> { _t('Event Type') } </label> - </div> - <div> - <input id="eventType" onChange={this._onChange} value={this.state.input_eventType} className="mx_TextInputDialog_input" size="64" /> - </div> + { this.textInput('eventType', _t('Event Type')) } + { this.state.isStateEvent && this.textInput('stateKey', _t('State Key')) } - <div className="mx_TextInputDialog_label"> + <br /> + + <div className="mx_UserSettings_profileLabelCell"> <label htmlFor="evContent"> { _t('Event Content') } </label> </div> <div> - <textarea id="evContent" onChange={this._onChange} value={this.state.input_evContent} className="mx_TextInputDialog_input" cols="63" rows="5" /> + <textarea id="evContent" onChange={this._onChange} value={this.state.evContent} className="mx_TextInputDialog_input" cols="63" rows="5" /> </div> </div> - { this._buttons() } + <div className="mx_Dialog_buttons"> + <button onClick={this.onBack}>{ _t('Back') }</button> + { !this.state.message && <button onClick={this._send}>{ _t('Send') }</button> } + { !this.state.message && !this.props.forceStateEvent && <div style={{float: "right"}}> + <input id="isStateEvent" className="mx_DevTools_tgl mx_DevTools_tgl-flip" type="checkbox" onChange={this._onChange} checked={this.state.isStateEvent} /> + <label className="mx_DevTools_tgl-btn" data-tg-off="Event" data-tg-on="State Event" htmlFor="isStateEvent" /> + </div> } + </div> </div>; } } -class SendCustomStateEvent extends SendCustomEvent { +class SendAccountData extends GenericEditor { + static getLabel() { return _t('Send Account Data'); } + static propTypes = { - roomId: React.PropTypes.string.isRequired, - onBack: React.PropTypes.func.isRequired, - - eventType: React.PropTypes.string.isRequired, - evContent: React.PropTypes.string.isRequired, - stateKey: React.PropTypes.string.isRequired, - }; - - static defaultProps = { - eventType: '', - evContent: '{\n\n}', - stateKey: '', + isRoomAccountData: PropTypes.bool, + forceMode: PropTypes.bool, + inputs: PropTypes.object, }; constructor(props, context) { super(props, context); - this.state['input_stateKey'] = this.props.stateKey; + this._send = this._send.bind(this); + + const {eventType, evContent} = Object.assign({ + eventType: '', + evContent: '{\n\n}', + }, this.props.inputs); + + this.state = { + isRoomAccountData: Boolean(this.props.isRoomAccountData), + + eventType, + evContent, + }; } send(content) { const cli = MatrixClientPeg.get(); - return cli.sendStateEvent(this.props.roomId, this.state.input_eventType, content, this.state.input_stateKey); + if (this.state.isRoomAccountData) { + return cli.setRoomAccountData(this.context.roomId, this.state.eventType, content); + } + return cli.setAccountData(this.state.eventType, content); } - _additionalFields() { + async _send() { + if (this.state.eventType === '') { + this.setState({ message: _t('You must specify an event type!') }); + return; + } + + let message; + try { + const content = JSON.parse(this.state.evContent); + await this.send(content); + message = _t('Event sent!'); + } catch (e) { + message = _t('Failed to send custom event.') + ' (' + e.toString() + ')'; + } + this.setState({ message }); + } + + render() { + if (this.state.message) { + return <div> + <div className="mx_Dialog_content"> + { this.state.message } + </div> + { this._buttons() } + </div>; + } + return <div> - <div className="mx_TextInputDialog_label"> - <label htmlFor="stateKey"> { _t('State Key') } </label> + <div className="mx_Dialog_content"> + { this.textInput('eventType', _t('Event Type')) } + <br /> + + <div className="mx_UserSettings_profileLabelCell"> + <label htmlFor="evContent"> { _t('Event Content') } </label> + </div> + <div> + <textarea id="evContent" onChange={this._onChange} value={this.state.evContent} className="mx_TextInputDialog_input" cols="63" rows="5" /> + </div> </div> - <div> - <input id="stateKey" onChange={this._onChange} value={this.state.input_stateKey} className="mx_TextInputDialog_input" size="64" /> + <div className="mx_Dialog_buttons"> + <button onClick={this.onBack}>{ _t('Back') }</button> + { !this.state.message && <button onClick={this._send}>{ _t('Send') }</button> } + { !this.state.message && <div style={{float: "right"}}> + <input id="isRoomAccountData" className="mx_DevTools_tgl mx_DevTools_tgl-flip" type="checkbox" onChange={this._onChange} checked={this.state.isRoomAccountData} disabled={this.props.forceMode} /> + <label className="mx_DevTools_tgl-btn" data-tg-off="Account Data" data-tg-on="Room Data" htmlFor="isRoomAccountData" /> + </div> } </div> </div>; } } -class RoomStateExplorer extends React.Component { +class FilteredList extends React.Component { static propTypes = { - setMode: React.PropTypes.func.isRequired, - roomId: React.PropTypes.string.isRequired, - onBack: React.PropTypes.func.isRequired, + children: PropTypes.any, + }; + + constructor(props, context) { + super(props, context); + this.onQuery = this.onQuery.bind(this); + + this.state = { + query: '', + }; + } + + onQuery(ev) { + this.setState({ query: ev.target.value }); + } + + filterChildren() { + if (this.state.query) { + const lowerQuery = this.state.query.toLowerCase(); + return this.props.children.filter((child) => child.key.toLowerCase().includes(lowerQuery)); + } + return this.props.children; + } + + render() { + return <div> + <input size="64" + onChange={this.onQuery} + value={this.state.query} + placeholder={_t('Filter results')} + className="mx_TextInputDialog_input mx_DevTools_RoomStateExplorer_query" /> + { this.filterChildren() } + </div>; + } +} + +class RoomStateExplorer extends DevtoolsComponent { + static getLabel() { return _t('Explore Room State'); } + + + static propTypes = { + onBack: PropTypes.func.isRequired, }; constructor(props, context) { super(props, context); - const room = MatrixClientPeg.get().getRoom(this.props.roomId); + const room = MatrixClientPeg.get().getRoom(this.context.roomId); this.roomStateEvents = room.currentState.events; this.onBack = this.onBack.bind(this); this.editEv = this.editEv.bind(this); - this.onQuery = this.onQuery.bind(this); - } - state = { - query: '', - eventType: null, - event: null, - }; + this.state = { + eventType: null, + event: null, + editing: false, + }; + } browseEventType(eventType) { return () => { @@ -197,7 +316,9 @@ class RoomStateExplorer extends React.Component { } onBack() { - if (this.state.event) { + if (this.state.editing) { + this.setState({ editing: false }); + } else if (this.state.event) { this.setState({ event: null }); } else if (this.state.eventType) { this.setState({ eventType: null }); @@ -207,20 +328,19 @@ class RoomStateExplorer extends React.Component { } editEv() { - const ev = this.state.event; - this.props.setMode(SendCustomStateEvent, { - eventType: ev.getType(), - evContent: JSON.stringify(ev.getContent(), null, '\t'), - stateKey: ev.getStateKey(), - }); - } - - onQuery(ev) { - this.setState({ query: ev.target.value }); + this.setState({ editing: true }); } render() { if (this.state.event) { + if (this.state.editing) { + return <SendCustomEvent forceStateEvent={true} onBack={this.onBack} inputs={{ + eventType: this.state.event.getType(), + evContent: JSON.stringify(this.state.event.getContent(), null, '\t'), + stateKey: this.state.event.getStateKey(), + }} />; + } + return <div className="mx_ViewSource"> <div className="mx_Dialog_content"> <pre>{ JSON.stringify(this.state.event.event, null, 2) }</pre> @@ -234,11 +354,9 @@ class RoomStateExplorer extends React.Component { const rows = []; + const classes = 'mx_DevTools_RoomStateExplorer_button'; if (this.state.eventType === null) { Object.keys(this.roomStateEvents).forEach((evType) => { - // Skip this entry if does not contain search query - if (this.state.query && !evType.toLowerCase().includes(this.state.query.toLowerCase())) return; - const stateGroup = this.roomStateEvents[evType]; const stateKeys = Object.keys(stateGroup); @@ -249,7 +367,7 @@ class RoomStateExplorer extends React.Component { onClickFn = this.onViewSourceClick(stateGroup[stateKeys[0]]); } - rows.push(<button className="mx_DevTools_RoomStateExplorer_button" key={evType} onClick={onClickFn}> + rows.push(<button className={classes} key={evType} onClick={onClickFn}> { evType } </button>); }); @@ -257,12 +375,8 @@ class RoomStateExplorer extends React.Component { const evType = this.state.eventType; const stateGroup = this.roomStateEvents[evType]; Object.keys(stateGroup).forEach((stateKey) => { - // Skip this entry if does not contain search query - if (this.state.query && !stateKey.toLowerCase().includes(this.state.query.toLowerCase())) return; - const ev = stateGroup[stateKey]; - rows.push(<button className="mx_DevTools_RoomStateExplorer_button" key={stateKey} - onClick={this.onViewSourceClick(ev)}> + rows.push(<button className={classes} key={stateKey} onClick={this.onViewSourceClick(ev)}> { stateKey } </button>); }); @@ -270,8 +384,9 @@ class RoomStateExplorer extends React.Component { return <div> <div className="mx_Dialog_content"> - <input onChange={this.onQuery} placeholder={_t('Filter results')} size="64" className="mx_TextInputDialog_input mx_DevTools_RoomStateExplorer_query" value={this.state.query} /> - { rows } + <FilteredList> + { rows } + </FilteredList> </div> <div className="mx_Dialog_buttons"> <button onClick={this.onBack}>{ _t('Back') }</button> @@ -280,40 +395,157 @@ class RoomStateExplorer extends React.Component { } } -export default class DevtoolsDialog extends React.Component { +class AccountDataExplorer extends DevtoolsComponent { + static getLabel() { return _t('Explore Account Data'); } + static propTypes = { - roomId: React.PropTypes.string.isRequired, - onFinished: React.PropTypes.func.isRequired, + onBack: PropTypes.func.isRequired, }; - state = { - mode: null, - modeArgs: {}, + constructor(props, context) { + super(props, context); + + this.onBack = this.onBack.bind(this); + this.editEv = this.editEv.bind(this); + this._onChange = this._onChange.bind(this); + + this.state = { + isRoomAccountData: false, + event: null, + editing: false, + }; + } + + getData() { + const cli = MatrixClientPeg.get(); + if (this.state.isRoomAccountData) { + return cli.getRoom(this.context.roomId).accountData; + } + return cli.store.accountData; + } + + onViewSourceClick(event) { + return () => { + this.setState({ event }); + }; + } + + onBack() { + if (this.state.editing) { + this.setState({ editing: false }); + } else if (this.state.event) { + this.setState({ event: null }); + } else { + this.props.onBack(); + } + } + + _onChange(e) { + this.setState({[e.target.id]: e.target.type === 'checkbox' ? e.target.checked : e.target.value}); + } + + editEv() { + this.setState({ editing: true }); + } + + render() { + if (this.state.event) { + if (this.state.editing) { + return <SendAccountData isRoomAccountData={this.state.isRoomAccountData} onBack={this.onBack} inputs={{ + eventType: this.state.event.getType(), + evContent: JSON.stringify(this.state.event.getContent(), null, '\t'), + }} forceMode={true} />; + } + + return <div className="mx_ViewSource"> + <div className="mx_Dialog_content"> + <pre>{ JSON.stringify(this.state.event.event, null, 2) }</pre> + </div> + <div className="mx_Dialog_buttons"> + <button onClick={this.onBack}>{ _t('Back') }</button> + <button onClick={this.editEv}>{ _t('Edit') }</button> + </div> + </div>; + } + + const rows = []; + + const classes = 'mx_DevTools_RoomStateExplorer_button'; + + const data = this.getData(); + Object.keys(data).forEach((evType) => { + const ev = data[evType]; + rows.push(<button className={classes} key={evType} onClick={this.onViewSourceClick(ev)}> + { evType } + </button>); + }); + + return <div> + <div className="mx_Dialog_content"> + <FilteredList> + { rows } + </FilteredList> + </div> + <div className="mx_Dialog_buttons"> + <button onClick={this.onBack}>{ _t('Back') }</button> + { !this.state.message && <div style={{float: "right"}}> + <input id="isRoomAccountData" className="mx_DevTools_tgl mx_DevTools_tgl-flip" type="checkbox" onChange={this._onChange} checked={this.state.isRoomAccountData} /> + <label className="mx_DevTools_tgl-btn" data-tg-off="Account Data" data-tg-on="Room Data" htmlFor="isRoomAccountData" /> + </div> } + </div> + </div>; + } +} + +const Entries = [ + SendCustomEvent, + RoomStateExplorer, + SendAccountData, + AccountDataExplorer, +]; + +export default class DevtoolsDialog extends React.Component { + static childContextTypes = { + roomId: PropTypes.string.isRequired, + // client: PropTypes.instanceOf(MatixClient), + }; + + static propTypes = { + roomId: PropTypes.string.isRequired, + onFinished: PropTypes.func.isRequired, }; constructor(props, context) { super(props, context); this.onBack = this.onBack.bind(this); - this.setMode = this.setMode.bind(this); this.onCancel = this.onCancel.bind(this); + + this.state = { + mode: null, + }; } componentWillUnmount() { this._unmounted = true; } + getChildContext() { + return { roomId: this.props.roomId }; + } + _setMode(mode) { return () => { - this.setMode(mode); + this.setState({ mode }); }; } - setMode(mode, modeArgs={}) { - this.setState({ mode, modeArgs }); - } - onBack() { - this.setState({ mode: null }); + if (this.prevMode) { + this.setState({ mode: this.prevMode }); + this.prevMode = null; + } else { + this.setState({ mode: null }); + } } onCancel() { @@ -324,14 +556,27 @@ export default class DevtoolsDialog extends React.Component { let body; if (this.state.mode) { - body = - <this.state.mode {...this.props} {...this.state.modeArgs} onBack={this.onBack} setMode={this.setMode} />; - } else { body = <div> - <div className="mx_Dialog_content"> - <button onClick={this._setMode(SendCustomEvent)}>{ _t('Send Custom Event') }</button> - <button onClick={this._setMode(SendCustomStateEvent)}>{ _t('Send Custom State Event') }</button> - <button onClick={this._setMode(RoomStateExplorer)}>{ _t('Explore Room State') }</button> + <div className="mx_DevTools_label_left">{ this.state.mode.getLabel() }</div> + <div className="mx_DevTools_label_right">Room ID: { this.props.roomId }</div> + <div className="mx_DevTools_label_bottom" /> + <this.state.mode onBack={this.onBack} /> + </div>; + } else { + const classes = "mx_DevTools_RoomStateExplorer_button"; + body = <div> + <div> + <div className="mx_DevTools_label_left">{ _t('Toolbox') }</div> + <div className="mx_DevTools_label_right">Room ID: { this.props.roomId }</div> + <div className="mx_DevTools_label_bottom" /> + + <div className="mx_Dialog_content"> + { Entries.map((Entry) => { + const label = Entry.getLabel(); + const onClick = this._setMode(Entry); + return <button className={classes} key={label} onClick={onClick}>{ label }</button>; + }) } + </div> </div> <div className="mx_Dialog_buttons"> <button onClick={this.onCancel}>{ _t('Cancel') }</button> @@ -342,7 +587,6 @@ export default class DevtoolsDialog extends React.Component { const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); return ( <BaseDialog className="mx_QuestionDialog" onFinished={this.props.onFinished} title={_t('Developer Tools')}> - <div>Room ID: { this.props.roomId }</div> { body } </BaseDialog> ); diff --git a/src/components/views/elements/InlineSpinner.js b/src/components/views/elements/InlineSpinner.js new file mode 100644 index 00000000..adb916fc --- /dev/null +++ b/src/components/views/elements/InlineSpinner.js @@ -0,0 +1,33 @@ +/* +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. +*/ + +const React = require('react'); + +module.exports = React.createClass({ + displayName: 'InlineSpinner', + + render: function() { + var w = this.props.w || 16; + var h = this.props.h || 16; + var imgClass = this.props.imgClassName || ""; + + return ( + <div className="mx_InlineSpinner"> + <img src="img/spinner.gif" width={w} height={h} className={imgClass}/> + </div> + ); + } +}); diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 543ee7bb..4702ae93 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -70,6 +70,7 @@ "What's new?": "What's new?", "A new version of Riot is available.": "A new version of Riot is available.", "To return to your account in future you need to <u>set a password</u>": "To return to your account in future you need to <u>set a password</u>", + "Toolbox": "Toolbox", "Set Password": "Set Password", "Error encountered (%(errorDetail)s).": "Error encountered (%(errorDetail)s).", "Checking for an update...": "Checking for an update...", @@ -106,7 +107,8 @@ "Edit": "Edit", "Filter results": "Filter results", "Send Custom Event": "Send Custom Event", - "Send Custom State Event": "Send Custom State Event", + "Send Account Data": "Send Account Data", + "Explore Account Data": "Explore Account Data", "Explore Room State": "Explore Room State", "Developer Tools": "Developer Tools", "You have successfully set a password!": "You have successfully set a password!", diff --git a/src/skins/vector/css/_components.scss b/src/skins/vector/css/_components.scss index 82c2c186..230b5ae4 100644 --- a/src/skins/vector/css/_components.scss +++ b/src/skins/vector/css/_components.scss @@ -35,7 +35,6 @@ @import "./matrix-react-sdk/views/elements/_ProgressBar.scss"; @import "./matrix-react-sdk/views/elements/_RichText.scss"; @import "./matrix-react-sdk/views/elements/_RoleButton.scss"; -@import "./matrix-react-sdk/views/groups/_GroupInviteTile.scss"; @import "./matrix-react-sdk/views/groups/_GroupRoomList.scss"; @import "./matrix-react-sdk/views/login/_InteractiveAuthEntryComponents.scss"; @import "./matrix-react-sdk/views/login/_ServerConfig.scss"; @@ -55,6 +54,8 @@ @import "./matrix-react-sdk/views/rooms/_MemberInfo.scss"; @import "./matrix-react-sdk/views/rooms/_MemberList.scss"; @import "./matrix-react-sdk/views/rooms/_MessageComposer.scss"; +@import "./matrix-react-sdk/views/rooms/_PinnedEventTile.scss"; +@import "./matrix-react-sdk/views/rooms/_PinnedEventsPanel.scss"; @import "./matrix-react-sdk/views/rooms/_PresenceLabel.scss"; @import "./matrix-react-sdk/views/rooms/_RoomHeader.scss"; @import "./matrix-react-sdk/views/rooms/_RoomList.scss"; @@ -68,8 +69,6 @@ @import "./matrix-react-sdk/views/voip/_CallView.scss"; @import "./matrix-react-sdk/views/voip/_IncomingCallbox.scss"; @import "./matrix-react-sdk/views/voip/_VideoView.scss"; -@import "./matrix-react-sdk/views/rooms/_PinnedEventsPanel.scss"; -@import "./matrix-react-sdk/views/rooms/_PinnedEventTile.scss"; @import "./vector-web/_fonts.scss"; @import "./vector-web/structures/_CompatibilityPage.scss"; @import "./vector-web/structures/_HomePage.scss"; @@ -86,6 +85,7 @@ @import "./vector-web/views/dialogs/_SetPasswordDialog.scss"; @import "./vector-web/views/directory/_NetworkDropdown.scss"; @import "./vector-web/views/elements/_ImageView.scss"; +@import "./vector-web/views/elements/_InlineSpinner.scss"; @import "./vector-web/views/elements/_Spinner.scss"; @import "./vector-web/views/globals/_MatrixToolbar.scss"; @import "./vector-web/views/messages/_DateSeparator.scss"; diff --git a/src/skins/vector/css/matrix-react-sdk/structures/_GroupView.scss b/src/skins/vector/css/matrix-react-sdk/structures/_GroupView.scss index 1a92fc10..583ab2ce 100644 --- a/src/skins/vector/css/matrix-react-sdk/structures/_GroupView.scss +++ b/src/skins/vector/css/matrix-react-sdk/structures/_GroupView.scss @@ -68,7 +68,7 @@ limitations under the License. box-shadow: none; } -.mx_GroupView_header_name:hover div:not(.mx_GroupView_editable) { +.mx_GroupView_header_isUserMember .mx_GroupView_header_name:hover div:not(.mx_GroupView_editable) { color: $accent-color; cursor: pointer; } @@ -204,6 +204,7 @@ limitations under the License. flex-grow: 1; border-top: 1px solid $primary-hairline-color; padding-top: 10px; + word-break: break-word; } .mx_GroupView .mx_RoomView_messageListWrapper { diff --git a/src/skins/vector/css/matrix-react-sdk/structures/_MyGroups.scss b/src/skins/vector/css/matrix-react-sdk/structures/_MyGroups.scss index ddcf46fb..d7cbda9a 100644 --- a/src/skins/vector/css/matrix-react-sdk/structures/_MyGroups.scss +++ b/src/skins/vector/css/matrix-react-sdk/structures/_MyGroups.scss @@ -18,6 +18,9 @@ limitations under the License. max-width: 960px; margin-left: auto; margin-right: auto; + + display: flex; + flex-direction: column; } .mx_MyGroups .mx_RoomHeader_simpleHeader { @@ -61,10 +64,26 @@ limitations under the License. /* Until the button is wired up */ .mx_MyGroups_joinBox { visibility: hidden; + + /* When joinBox wraps onto its own row, it should take up zero height so + that there isn't an awkward gap between MyGroups_createBox and + MyGroups_content. + */ + height: 0px; + margin: 0px; } .mx_MyGroups_content { margin-left: 2px; + + flex: 1 0 0; + + display: flex; + flex-direction: column; +} + +.mx_MyGroups_content h3 { + margin-bottom: 10px; } .mx_MyGroups_placeholder { @@ -75,19 +94,22 @@ limitations under the License. text-align: center; } -.mx_MyGroups_joinedGroups { +.mx_MyGroups_joinedGroups .gm-scroll-view { + border-top: 1px solid $primary-hairline-color; + overflow-x: hidden; + display: flex; flex-direction: row; flex-flow: wrap; - justify-content: space-around; + align-content: flex-start; } -.mx_MyGroups_joinedGroups .mx_GroupTile { +.mx_MyGroups_joinedGroups .gm-scroll-view .mx_GroupTile { min-width: 300px; - flex: 1 0 25%; + max-width: 33%; + flex: 1 0 300px; height: 75px; - margin-bottom: 15px; - margin-right: 10px; + margin: 10px 0px; display: flex; align-items: flex-start; cursor: pointer; @@ -100,6 +122,12 @@ limitations under the License. justify-content: center; } +.mx_GroupTile_profile h3.mx_GroupTile_name, +.mx_GroupTile_profile .mx_GroupTile_groupId, +.mx_GroupTile_profile .mx_GroupTile_desc { + padding-right: 10px; +} + .mx_GroupTile_profile h3.mx_GroupTile_name { margin: 0px; font-size: 15px; diff --git a/src/skins/vector/css/matrix-react-sdk/views/dialogs/_CreateGroupDialog.scss b/src/skins/vector/css/matrix-react-sdk/views/dialogs/_CreateGroupDialog.scss index ebe89027..500e12ee 100644 --- a/src/skins/vector/css/matrix-react-sdk/views/dialogs/_CreateGroupDialog.scss +++ b/src/skins/vector/css/matrix-react-sdk/views/dialogs/_CreateGroupDialog.scss @@ -33,3 +33,30 @@ limitations under the License. background-color: $primary-bg-color; } +.mx_CreateGroupDialog_input_hasPrefixAndSuffix { + border-radius: 0px; +} + +.mx_CreateGroupDialog_input_group { + display: flex; +} + +.mx_CreateGroupDialog_prefix, +.mx_CreateGroupDialog_suffix { + height: 35px; + padding: 0px 5px; + line-height: 37px; + background-color: $input-border-color; + border: 1px solid $input-border-color; + text-align: center; +} + +.mx_CreateGroupDialog_prefix { + border-right: 0px; + border-radius: 3px 0px 0px 3px; +} + +.mx_CreateGroupDialog_suffix { + border-left: 0px; + border-radius: 0px 3px 3px 0px; +} diff --git a/src/skins/vector/css/matrix-react-sdk/views/elements/_RichText.scss b/src/skins/vector/css/matrix-react-sdk/views/elements/_RichText.scss index 0927cfbc..8b7c8f06 100644 --- a/src/skins/vector/css/matrix-react-sdk/views/elements/_RichText.scss +++ b/src/skins/vector/css/matrix-react-sdk/views/elements/_RichText.scss @@ -3,7 +3,8 @@ // --Matthew .mx_UserPill, -.mx_RoomPill { +.mx_RoomPill, +.mx_AtRoomPill { border-radius: 16px; display: inline-block; height: 20px; @@ -24,7 +25,8 @@ padding-right: 5px; } -.mx_EventTile_highlight .mx_EventTile_content .markdown-body a.mx_UserPill_me { +.mx_EventTile_highlight .mx_EventTile_content .markdown-body a.mx_UserPill_me, +.mx_EventTile_content .mx_AtRoomPill { color: $accent-fg-color; background-color: $mention-user-pill-bg-color; padding-right: 5px; @@ -39,7 +41,8 @@ } .mx_UserPill .mx_BaseAvatar, -.mx_RoomPill .mx_BaseAvatar { +.mx_RoomPill .mx_BaseAvatar, +.mx_AtRoomPill .mx_BaseAvatar { position: relative; left: -3px; top: 2px; diff --git a/src/skins/vector/css/matrix-react-sdk/views/groups/_GroupInviteTile.scss b/src/skins/vector/css/matrix-react-sdk/views/groups/_GroupInviteTile.scss deleted file mode 100644 index 6b4034b9..00000000 --- a/src/skins/vector/css/matrix-react-sdk/views/groups/_GroupInviteTile.scss +++ /dev/null @@ -1,74 +0,0 @@ -/* -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. -*/ - -.mx_GroupInviteTile { - position: relative; - cursor: pointer; - font-size: 13px; - display: block; - height: 34px; -} - -.mx_GroupInviteTile_nameContainer { - display: inline-block; - width: 180px; - height: 24px; -} - -.mx_GroupInviteTile_avatarContainer { - display: inline-block; - padding-top: 5px; - padding-bottom: 5px; - padding-left: 16px; - padding-right: 6px; - width: 24px; - height: 24px; - vertical-align: middle; -} - -.mx_GroupInviteTile_name { - display: inline-block; - position: relative; - width: 165px; - vertical-align: middle; - padding-left: 6px; - padding-right: 6px; - padding-top: 2px; - padding-bottom: 3px; - color: $roomtile-name-color; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.mx_GroupInviteTile_badge { - display: inline-block; - min-width: 15px; - height: 15px; - position: absolute; - right: 8px; /*gutter */ - top: 9px; - border-radius: 8px; - color: $accent-fg-color; - background-color: $group-alert-color; - font-weight: 600; - font-size: 10px; - text-align: center; - padding-top: 1px; - padding-left: 4px; - padding-right: 4px; -} - diff --git a/src/skins/vector/css/matrix-react-sdk/views/groups/_GroupRoomList.scss b/src/skins/vector/css/matrix-react-sdk/views/groups/_GroupRoomList.scss index 91f0c347..fb41ebaa 100644 --- a/src/skins/vector/css/matrix-react-sdk/views/groups/_GroupRoomList.scss +++ b/src/skins/vector/css/matrix-react-sdk/views/groups/_GroupRoomList.scss @@ -19,18 +19,3 @@ limitations under the License. color: $primary-fg-color; cursor: pointer; } - -.mx_GroupRoomTile_delete { - opacity: 0.4; - position: absolute; - top: 6px; - right: 10px; - cursor: pointer; - - display: none; -} - -.mx_GroupRoomTile:hover > .mx_GroupRoomTile_delete { - display: initial; -} - diff --git a/src/skins/vector/css/matrix-react-sdk/views/rooms/_AppsDrawer.scss b/src/skins/vector/css/matrix-react-sdk/views/rooms/_AppsDrawer.scss index 14b8cb45..f9c09287 100644 --- a/src/skins/vector/css/matrix-react-sdk/views/rooms/_AppsDrawer.scss +++ b/src/skins/vector/css/matrix-react-sdk/views/rooms/_AppsDrawer.scss @@ -74,7 +74,7 @@ limitations under the License. margin: 0; padding: 2px 10px; // background-color: $e2e-verified-color; - // border-bottom: 1px solid $primary-hairline-color; + border-bottom: 1px solid $primary-hairline-color; font-size: 10px; } diff --git a/src/skins/vector/css/matrix-react-sdk/views/rooms/_EntityTile.scss b/src/skins/vector/css/matrix-react-sdk/views/rooms/_EntityTile.scss index 712e4bae..031894af 100644 --- a/src/skins/vector/css/matrix-react-sdk/views/rooms/_EntityTile.scss +++ b/src/skins/vector/css/matrix-react-sdk/views/rooms/_EntityTile.scss @@ -57,7 +57,7 @@ limitations under the License. font-size: 14px; text-overflow: ellipsis; white-space: nowrap; - max-width: 135px; + max-width: 155px; } .mx_EntityTile_details { diff --git a/src/skins/vector/css/matrix-react-sdk/views/rooms/_MemberInfo.scss b/src/skins/vector/css/matrix-react-sdk/views/rooms/_MemberInfo.scss index 8920c6f6..5d47275e 100644 --- a/src/skins/vector/css/matrix-react-sdk/views/rooms/_MemberInfo.scss +++ b/src/skins/vector/css/matrix-react-sdk/views/rooms/_MemberInfo.scss @@ -94,3 +94,19 @@ limitations under the License. cursor: pointer; } +.mx_MemberInfo label { + font-size: 13px; +} + +.mx_MemberInfo label .mx_MemberInfo_label_text { + display: inline-block; + max-width: 180px; + vertical-align: text-top; +} + +.mx_MemberInfo input[type="radio"] { + vertical-align: -2px; + margin-right: 5px; + margin-left: 8px; +} + diff --git a/src/skins/vector/css/matrix-react-sdk/views/rooms/_RoomHeader.scss b/src/skins/vector/css/matrix-react-sdk/views/rooms/_RoomHeader.scss index fd4bdca0..85ad08ec 100644 --- a/src/skins/vector/css/matrix-react-sdk/views/rooms/_RoomHeader.scss +++ b/src/skins/vector/css/matrix-react-sdk/views/rooms/_RoomHeader.scss @@ -232,3 +232,17 @@ limitations under the License. .mx_RoomHeader_voipButtons { margin-top: 18px; } + +.mx_RoomHeader_pinnedButton { + position: relative; +} + +.mx_RoomHeader_unreadPinsIndicator { + position: absolute; + right: 0; + bottom: 4px; + width: 8px; + height: 8px; + border-radius: 8px; + background-color: $warning-color; +} diff --git a/src/skins/vector/css/themes/_base.scss b/src/skins/vector/css/themes/_base.scss index f1117a7a..1a0fa5b4 100644 --- a/src/skins/vector/css/themes/_base.scss +++ b/src/skins/vector/css/themes/_base.scss @@ -23,7 +23,6 @@ $mention-user-pill-bg-color: #ff0064; $other-user-pill-bg-color: rgba(0, 0, 0, 0.1); // groups -$group-alert-color: #774f7e; $group-my-groups-placeholder-bg: #f7f7f7; $group-my-groups-placeholder-fg: #888; diff --git a/src/skins/vector/css/vector-web/structures/_RoomDirectory.scss b/src/skins/vector/css/vector-web/structures/_RoomDirectory.scss index 91cf86c9..9cd3e728 100644 --- a/src/skins/vector/css/vector-web/structures/_RoomDirectory.scss +++ b/src/skins/vector/css/vector-web/structures/_RoomDirectory.scss @@ -100,6 +100,7 @@ limitations under the License. display: inline; padding-left: 5px; padding-right: 5px; + margin-right: 5px; height: 15px; border-radius: 11px; background-color: $plinth-bg-color; diff --git a/src/skins/vector/css/vector-web/views/dialogs/_DevtoolsDialog.scss b/src/skins/vector/css/vector-web/views/dialogs/_DevtoolsDialog.scss index 975ee8c1..8918373e 100644 --- a/src/skins/vector/css/vector-web/views/dialogs/_DevtoolsDialog.scss +++ b/src/skins/vector/css/vector-web/views/dialogs/_DevtoolsDialog.scss @@ -17,3 +17,150 @@ limitations under the License. .mx_DevTools_RoomStateExplorer_button, .mx_DevTools_RoomStateExplorer_query { margin-bottom: 10px; } + +.mx_DevTools_label_left { + float: left; +} + +.mx_DevTools_label_right { + float: right; +} + +.mx_DevTools_label_bottom { + clear: both; + border-bottom: 1px solid #e5e5e5; +} + +.mx_DevTools_inputRow +{ + display: table-row; +} + +.mx_DevTools_inputLabelCell +{ + padding-bottom: 21px; + display: table-cell; + font-weight: bold; + padding-right: 24px; +} + +.mx_DevTools_inputCell { + display: table-cell; + padding-bottom: 21px; + width: 240px; +} + +.mx_DevTools_inputCell input +{ + display: inline-block; + border: 0; + border-bottom: 1px solid $input-underline-color; + padding: 0; + width: 240px; + color: $input-fg-color; + font-family: 'Open Sans', Helvetica, Arial, Sans-Serif; + font-size: 16px; +} + +.mx_DevTools_tgl { + display: none; + + // add default box-sizing for this scope + &, + &:after, + &:before, + & *, + & *:after, + & *:before, + & + .mx_DevTools_tgl-btn { + box-sizing: border-box; + &::selection { + background: none; + } + } + + + .mx_DevTools_tgl-btn { + outline: 0; + display: block; + width: 7em; + height: 2em; + position: relative; + cursor: pointer; + user-select: none; + &:after, + &:before { + position: relative; + display: block; + content: ""; + width: 50%; + height: 100%; + } + + &:after { + left: 0; + } + + &:before { + display: none; + } + } + + &:checked + .mx_DevTools_tgl-btn:after { + left: 50%; + } +} + +.mx_DevTools_tgl-flip { + + .mx_DevTools_tgl-btn { + padding: 2px; + transition: all .2s ease; + font-family: sans-serif; + perspective: 100px; + &:after, + &:before { + display: inline-block; + transition: all .4s ease; + width: 100%; + text-align: center; + position: absolute; + line-height: 2em; + font-weight: bold; + color: #fff; + top: 0; + left: 0; + backface-visibility: hidden; + border-radius: 4px; + } + + &:after { + content: attr(data-tg-on); + background: #02C66F; + transform: rotateY(-180deg); + } + + &:before { + background: #FF3A19; + content: attr(data-tg-off); + } + + &:active:before { + transform: rotateY(-20deg); + } + } + + &:checked + .mx_DevTools_tgl-btn { + &:before { + transform: rotateY(180deg); + } + + &:after { + transform: rotateY(0); + left: 0; + background: #7FC6A6; + } + + &:active:after { + transform: rotateY(20deg); + } + } +} diff --git a/src/skins/vector/css/vector-web/views/elements/_InlineSpinner.scss b/src/skins/vector/css/vector-web/views/elements/_InlineSpinner.scss new file mode 100644 index 00000000..612b6209 --- /dev/null +++ b/src/skins/vector/css/vector-web/views/elements/_InlineSpinner.scss @@ -0,0 +1,24 @@ +/* +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. +*/ + +.mx_InlineSpinner { + display: inline; +} + +.mx_InlineSpinner img { + margin: 0px 6px; + vertical-align: -3px; +}