diff --git a/src/GroupAddressPicker.js b/src/GroupAddressPicker.js index 595f0cfe46..c4b3744575 100644 --- a/src/GroupAddressPicker.js +++ b/src/GroupAddressPicker.js @@ -49,20 +49,26 @@ export function showGroupInviteDialog(groupId) { export function showGroupAddRoomDialog(groupId) { return new Promise((resolve, reject) => { + let addRoomsPublicly = false; + const onCheckboxClicked = (e) => { + addRoomsPublicly = e.target.checked; + }; const description =
{ _t("Which rooms would you like to add to this community?") }
-
- { _t( - "Warning: any room you add to a community will be publicly "+ - "visible to anyone who knows the community ID", - ) } -
; + const checkboxContainer = ; + const AddressPickerDialog = sdk.getComponent("dialogs.AddressPickerDialog"); Modal.createTrackedDialog('Add Rooms to Group', '', AddressPickerDialog, { title: _t("Add rooms to the community"), description: description, + extraNode: checkboxContainer, placeholder: _t("Room name or alias"), button: _t("Add to community"), pickerType: 'room', @@ -70,7 +76,7 @@ export function showGroupAddRoomDialog(groupId) { onFinished: (success, addrs) => { if (!success) return; - _onGroupAddRoomFinished(groupId, addrs).then(resolve, reject); + _onGroupAddRoomFinished(groupId, addrs, addRoomsPublicly).then(resolve, reject); }, }); }); @@ -106,13 +112,13 @@ function _onGroupInviteFinished(groupId, addrs) { }); } -function _onGroupAddRoomFinished(groupId, addrs) { +function _onGroupAddRoomFinished(groupId, addrs, addRoomsPublicly) { const matrixClient = MatrixClientPeg.get(); const groupStore = GroupStoreCache.getGroupStore(matrixClient, groupId); const errorList = []; return Promise.all(addrs.map((addr) => { return groupStore - .addRoomToGroup(addr.address) + .addRoomToGroup(addr.address, addRoomsPublicly) .catch(() => { errorList.push(addr.address); }) .then(() => { const roomId = addr.address; diff --git a/src/HtmlUtils.js b/src/HtmlUtils.js index b306eab23c..0c262fe89a 100644 --- a/src/HtmlUtils.js +++ b/src/HtmlUtils.js @@ -208,7 +208,7 @@ const sanitizeHtmlParams = { // Strip out imgs that aren't `mxc` here instead of using allowedSchemesByTag // because transformTags is used _before_ we filter by allowedSchemesByTag and // we don't want to allow images with `https?` `src`s. - if (!attribs.src.startsWith('mxc://')) { + if (!attribs.src || !attribs.src.startsWith('mxc://')) { return { tagName, attribs: {}}; } attribs.src = MatrixClientPeg.get().mxcUrlToHttp( diff --git a/src/UnknownDeviceErrorHandler.js b/src/UnknownDeviceErrorHandler.js index e7d77b3b66..664fe14eb5 100644 --- a/src/UnknownDeviceErrorHandler.js +++ b/src/UnknownDeviceErrorHandler.js @@ -25,7 +25,6 @@ const onAction = function(payload) { const UnknownDeviceDialog = sdk.getComponent('dialogs.UnknownDeviceDialog'); isDialogOpen = true; Modal.createTrackedDialog('Unknown Device Error', '', UnknownDeviceDialog, { - devices: payload.err.devices, room: payload.room, onFinished: (r) => { isDialogOpen = false; diff --git a/src/autocomplete/Autocompleter.js b/src/autocomplete/Autocompleter.js index 94d2ed28de..3d30363d9f 100644 --- a/src/autocomplete/Autocompleter.js +++ b/src/autocomplete/Autocompleter.js @@ -23,6 +23,7 @@ import DuckDuckGoProvider from './DuckDuckGoProvider'; import RoomProvider from './RoomProvider'; import UserProvider from './UserProvider'; import EmojiProvider from './EmojiProvider'; +import NotifProvider from './NotifProvider'; import Promise from 'bluebird'; export type SelectionRange = { @@ -44,6 +45,7 @@ const PROVIDERS = [ UserProvider, RoomProvider, EmojiProvider, + NotifProvider, CommandProvider, DuckDuckGoProvider, ]; diff --git a/src/autocomplete/NotifProvider.js b/src/autocomplete/NotifProvider.js new file mode 100644 index 0000000000..b7ac645525 --- /dev/null +++ b/src/autocomplete/NotifProvider.js @@ -0,0 +1,62 @@ +/* +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 AutocompleteProvider from './AutocompleteProvider'; +import { _t } from '../languageHandler'; +import MatrixClientPeg from '../MatrixClientPeg'; +import {PillCompletion} from './Components'; +import sdk from '../index'; + +const AT_ROOM_REGEX = /@\S*/g; + +export default class NotifProvider extends AutocompleteProvider { + constructor(room) { + super(AT_ROOM_REGEX); + this.room = room; + } + + async getCompletions(query: string, selection: {start: number, end: number}, force = false) { + const RoomAvatar = sdk.getComponent('views.avatars.RoomAvatar'); + + const client = MatrixClientPeg.get(); + + if (!this.room.currentState.mayTriggerNotifOfType('room', client.credentials.userId)) return []; + + const {command, range} = this.getCurrentCommand(query, selection, force); + if (command && command[0] && '@room'.startsWith(command[0]) && command[0].length > 1) { + return [{ + completion: '@room', + suffix: ' ', + component: ( + } title="@room" description={_t("Notify the whole room")} /> + ), + range, + }]; + } + return []; + } + + getName() { + return '❗️ ' + _t('Room Notification'); + } + + renderCompletions(completions: [React.Component]): ?React.Component { + return
+ { completions } +
; + } +} diff --git a/src/components/structures/GroupView.js b/src/components/structures/GroupView.js index 24aa552890..caa5e7cb01 100644 --- a/src/components/structures/GroupView.js +++ b/src/components/structures/GroupView.js @@ -22,7 +22,7 @@ import MatrixClientPeg from '../../MatrixClientPeg'; import sdk from '../../index'; import dis from '../../dispatcher'; import { sanitizedHtmlNode } from '../../HtmlUtils'; -import { _t } from '../../languageHandler'; +import { _t, _td, _tJsx } from '../../languageHandler'; import AccessibleButton from '../views/elements/AccessibleButton'; import Modal from '../../Modal'; import classnames from 'classnames'; @@ -32,6 +32,17 @@ import GroupStore from '../../stores/GroupStore'; import { showGroupAddRoomDialog } from '../../GroupAddressPicker'; import GeminiScrollbar from 'react-gemini-scrollbar'; +const LONG_DESC_PLACEHOLDER = _td( +`

HTML for your community's page

+

+ Use the long description to introduce new members to the community, or distribute + some important links +

+

+ You can even use 'img' tags +

+`); + const RoomSummaryType = PropTypes.shape({ room_id: PropTypes.string.isRequired, profile: PropTypes.shape({ @@ -392,6 +403,8 @@ export default React.createClass({ propTypes: { groupId: PropTypes.string.isRequired, + // Whether this is the first time the group admin is viewing the group + groupIsNew: PropTypes.bool, }, childContextTypes: { @@ -417,12 +430,13 @@ export default React.createClass({ uploadingAvatar: false, membershipBusy: false, publicityBusy: false, + inviterProfile: null, }; }, componentWillMount: function() { this._changeAvatarComponent = null; - this._initGroupStore(this.props.groupId); + this._initGroupStore(this.props.groupId, true); MatrixClientPeg.get().on("Group.myMembership", this._onGroupMyMembership); }, @@ -449,7 +463,11 @@ export default React.createClass({ this.setState({membershipBusy: false}); }, - _initGroupStore: function(groupId) { + _initGroupStore: function(groupId, firstInit) { + const group = MatrixClientPeg.get().getGroup(groupId); + if (group && group.inviter && group.inviter.userId) { + this._fetchInviterProfile(group.inviter.userId); + } this._groupStore = GroupStoreCache.getGroupStore(MatrixClientPeg.get(), groupId); this._groupStore.registerListener(() => { const summary = this._groupStore.getSummary(); @@ -472,6 +490,9 @@ export default React.createClass({ ), error: null, }); + if (this.props.groupIsNew && firstInit) { + this._onEditClick(); + } }); this._groupStore.on('error', (err) => { this.setState({ @@ -481,6 +502,26 @@ export default React.createClass({ }); }, + _fetchInviterProfile(userId) { + this.setState({ + inviterProfileBusy: true, + }); + MatrixClientPeg.get().getProfileInfo(userId).then((resp) => { + this.setState({ + inviterProfile: { + avatarUrl: resp.avatar_url, + displayName: resp.displayname, + }, + }); + }).catch((e) => { + console.error('Error getting group inviter profile', e); + }).finally(() => { + this.setState({ + inviterProfileBusy: false, + }); + }); + }, + _onShowRhsClick: function(ev) { dis.dispatch({ action: 'show_right_panel' }); }, @@ -575,7 +616,7 @@ export default React.createClass({ _onAcceptInviteClick: function() { this.setState({membershipBusy: true}); - MatrixClientPeg.get().acceptGroupInvite(this.props.groupId).then(() => { + this._groupStore.acceptGroupInvite().then(() => { // don't reset membershipBusy here: wait for the membership change to come down the sync }).catch((e) => { this.setState({membershipBusy: false}); @@ -661,6 +702,14 @@ export default React.createClass({ const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); const TintableSvg = sdk.getComponent('elements.TintableSvg'); const Spinner = sdk.getComponent('elements.Spinner'); + const ToolTipButton = sdk.getComponent('elements.ToolTipButton'); + + const roomsHelpNode = this.state.editing ? :
; const addRoomRow = this.state.editing ? ( ) :
; + const roomDetailListClassName = classnames({ + "mx_fadable": true, + "mx_fadable_faded": this.state.editing, + }); return
-

{ _t('Rooms') }

+

+ { _t('Rooms') } + { roomsHelpNode } +

{ addRoomRow }
{ this.state.groupRoomsLoading ? : - + }
; }, @@ -769,20 +827,37 @@ export default React.createClass({ _getMembershipSection: function() { const Spinner = sdk.getComponent("elements.Spinner"); + const BaseAvatar = sdk.getComponent("avatars.BaseAvatar"); const group = MatrixClientPeg.get().getGroup(this.props.groupId); if (!group) return null; if (group.myMembership === 'invite') { - if (this.state.membershipBusy) { + if (this.state.membershipBusy || this.state.inviterProfileBusy) { return
; } + const httpInviterAvatar = this.state.inviterProfile ? + MatrixClientPeg.get().mxcUrlToHttp( + this.state.inviterProfile.avatarUrl, 36, 36, + ) : null; + + let inviterName = group.inviter.userId; + if (this.state.inviterProfile) { + inviterName = this.state.inviterProfile.displayName || group.inviter.userId; + } return
- { _t("%(inviter)s has invited you to join this community", {inviter: group.inviter.userId}) } + + { _t("%(inviter)s has invited you to join this community", { + inviter: inviterName, + }) }
+ { _tJsx( + 'Your community hasn\'t got a Long Description, a HTML page to show to community members.
' + + 'Click here to open settings and give it one!', + [/
/], + [(sub) =>
]) + } +
; } const groupDescEditingClasses = classnames({ "mx_GroupView_groupDesc": true, @@ -862,6 +949,7 @@ export default React.createClass({

{ _t("Long Description (HTML)") }