Merge branch 'develop' into travis/granular-settings
This commit is contained in:
commit
289b0c2b6a
24 changed files with 419 additions and 57 deletions
|
@ -208,7 +208,7 @@ const sanitizeHtmlParams = {
|
||||||
// Strip out imgs that aren't `mxc` here instead of using allowedSchemesByTag
|
// Strip out imgs that aren't `mxc` here instead of using allowedSchemesByTag
|
||||||
// because transformTags is used _before_ we filter by allowedSchemesByTag and
|
// because transformTags is used _before_ we filter by allowedSchemesByTag and
|
||||||
// we don't want to allow images with `https?` `src`s.
|
// 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: {}};
|
return { tagName, attribs: {}};
|
||||||
}
|
}
|
||||||
attribs.src = MatrixClientPeg.get().mxcUrlToHttp(
|
attribs.src = MatrixClientPeg.get().mxcUrlToHttp(
|
||||||
|
|
|
@ -25,7 +25,6 @@ const onAction = function(payload) {
|
||||||
const UnknownDeviceDialog = sdk.getComponent('dialogs.UnknownDeviceDialog');
|
const UnknownDeviceDialog = sdk.getComponent('dialogs.UnknownDeviceDialog');
|
||||||
isDialogOpen = true;
|
isDialogOpen = true;
|
||||||
Modal.createTrackedDialog('Unknown Device Error', '', UnknownDeviceDialog, {
|
Modal.createTrackedDialog('Unknown Device Error', '', UnknownDeviceDialog, {
|
||||||
devices: payload.err.devices,
|
|
||||||
room: payload.room,
|
room: payload.room,
|
||||||
onFinished: (r) => {
|
onFinished: (r) => {
|
||||||
isDialogOpen = false;
|
isDialogOpen = false;
|
||||||
|
|
|
@ -23,6 +23,7 @@ import DuckDuckGoProvider from './DuckDuckGoProvider';
|
||||||
import RoomProvider from './RoomProvider';
|
import RoomProvider from './RoomProvider';
|
||||||
import UserProvider from './UserProvider';
|
import UserProvider from './UserProvider';
|
||||||
import EmojiProvider from './EmojiProvider';
|
import EmojiProvider from './EmojiProvider';
|
||||||
|
import NotifProvider from './NotifProvider';
|
||||||
import Promise from 'bluebird';
|
import Promise from 'bluebird';
|
||||||
|
|
||||||
export type SelectionRange = {
|
export type SelectionRange = {
|
||||||
|
@ -44,6 +45,7 @@ const PROVIDERS = [
|
||||||
UserProvider,
|
UserProvider,
|
||||||
RoomProvider,
|
RoomProvider,
|
||||||
EmojiProvider,
|
EmojiProvider,
|
||||||
|
NotifProvider,
|
||||||
CommandProvider,
|
CommandProvider,
|
||||||
DuckDuckGoProvider,
|
DuckDuckGoProvider,
|
||||||
];
|
];
|
||||||
|
|
62
src/autocomplete/NotifProvider.js
Normal file
62
src/autocomplete/NotifProvider.js
Normal file
|
@ -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: (
|
||||||
|
<PillCompletion initialComponent={<RoomAvatar width={24} height={24} room={this.room} />} title="@room" description={_t("Notify the whole room")} />
|
||||||
|
),
|
||||||
|
range,
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
getName() {
|
||||||
|
return '❗️ ' + _t('Room Notification');
|
||||||
|
}
|
||||||
|
|
||||||
|
renderCompletions(completions: [React.Component]): ?React.Component {
|
||||||
|
return <div className="mx_Autocomplete_Completion_container_pill mx_Autocomplete_Completion_container_truncate">
|
||||||
|
{ completions }
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
}
|
|
@ -22,7 +22,7 @@ import MatrixClientPeg from '../../MatrixClientPeg';
|
||||||
import sdk from '../../index';
|
import sdk from '../../index';
|
||||||
import dis from '../../dispatcher';
|
import dis from '../../dispatcher';
|
||||||
import { sanitizedHtmlNode } from '../../HtmlUtils';
|
import { sanitizedHtmlNode } from '../../HtmlUtils';
|
||||||
import { _t } from '../../languageHandler';
|
import { _t, _td, _tJsx } from '../../languageHandler';
|
||||||
import AccessibleButton from '../views/elements/AccessibleButton';
|
import AccessibleButton from '../views/elements/AccessibleButton';
|
||||||
import Modal from '../../Modal';
|
import Modal from '../../Modal';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
|
@ -32,6 +32,17 @@ import GroupStore from '../../stores/GroupStore';
|
||||||
import { showGroupAddRoomDialog } from '../../GroupAddressPicker';
|
import { showGroupAddRoomDialog } from '../../GroupAddressPicker';
|
||||||
import GeminiScrollbar from 'react-gemini-scrollbar';
|
import GeminiScrollbar from 'react-gemini-scrollbar';
|
||||||
|
|
||||||
|
const LONG_DESC_PLACEHOLDER = _td(
|
||||||
|
`<h1>HTML for your community's page</h1>
|
||||||
|
<p>
|
||||||
|
Use the long description to introduce new members to the community, or distribute
|
||||||
|
some important <a href="foo">links</a>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
You can even use 'img' tags
|
||||||
|
</p>
|
||||||
|
`);
|
||||||
|
|
||||||
const RoomSummaryType = PropTypes.shape({
|
const RoomSummaryType = PropTypes.shape({
|
||||||
room_id: PropTypes.string.isRequired,
|
room_id: PropTypes.string.isRequired,
|
||||||
profile: PropTypes.shape({
|
profile: PropTypes.shape({
|
||||||
|
@ -392,6 +403,8 @@ export default React.createClass({
|
||||||
|
|
||||||
propTypes: {
|
propTypes: {
|
||||||
groupId: PropTypes.string.isRequired,
|
groupId: PropTypes.string.isRequired,
|
||||||
|
// Whether this is the first time the group admin is viewing the group
|
||||||
|
groupIsNew: PropTypes.bool,
|
||||||
},
|
},
|
||||||
|
|
||||||
childContextTypes: {
|
childContextTypes: {
|
||||||
|
@ -422,7 +435,7 @@ export default React.createClass({
|
||||||
|
|
||||||
componentWillMount: function() {
|
componentWillMount: function() {
|
||||||
this._changeAvatarComponent = null;
|
this._changeAvatarComponent = null;
|
||||||
this._initGroupStore(this.props.groupId);
|
this._initGroupStore(this.props.groupId, true);
|
||||||
|
|
||||||
MatrixClientPeg.get().on("Group.myMembership", this._onGroupMyMembership);
|
MatrixClientPeg.get().on("Group.myMembership", this._onGroupMyMembership);
|
||||||
},
|
},
|
||||||
|
@ -449,7 +462,7 @@ export default React.createClass({
|
||||||
this.setState({membershipBusy: false});
|
this.setState({membershipBusy: false});
|
||||||
},
|
},
|
||||||
|
|
||||||
_initGroupStore: function(groupId) {
|
_initGroupStore: function(groupId, firstInit) {
|
||||||
this._groupStore = GroupStoreCache.getGroupStore(MatrixClientPeg.get(), groupId);
|
this._groupStore = GroupStoreCache.getGroupStore(MatrixClientPeg.get(), groupId);
|
||||||
this._groupStore.registerListener(() => {
|
this._groupStore.registerListener(() => {
|
||||||
const summary = this._groupStore.getSummary();
|
const summary = this._groupStore.getSummary();
|
||||||
|
@ -472,6 +485,9 @@ export default React.createClass({
|
||||||
),
|
),
|
||||||
error: null,
|
error: null,
|
||||||
});
|
});
|
||||||
|
if (this.props.groupIsNew && firstInit) {
|
||||||
|
this._onEditClick();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
this._groupStore.on('error', (err) => {
|
this._groupStore.on('error', (err) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
|
@ -661,6 +677,14 @@ export default React.createClass({
|
||||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||||
const TintableSvg = sdk.getComponent('elements.TintableSvg');
|
const TintableSvg = sdk.getComponent('elements.TintableSvg');
|
||||||
const Spinner = sdk.getComponent('elements.Spinner');
|
const Spinner = sdk.getComponent('elements.Spinner');
|
||||||
|
const ToolTipButton = sdk.getComponent('elements.ToolTipButton');
|
||||||
|
|
||||||
|
const roomsHelpNode = this.state.editing ? <ToolTipButton helpText={
|
||||||
|
_t(
|
||||||
|
'These rooms are displayed to community members on the community page. '+
|
||||||
|
'Community members can join the rooms by clicking on them.',
|
||||||
|
)
|
||||||
|
} /> : <div />;
|
||||||
|
|
||||||
const addRoomRow = this.state.editing ?
|
const addRoomRow = this.state.editing ?
|
||||||
(<AccessibleButton className="mx_GroupView_rooms_header_addRow"
|
(<AccessibleButton className="mx_GroupView_rooms_header_addRow"
|
||||||
|
@ -673,14 +697,23 @@ export default React.createClass({
|
||||||
{ _t('Add rooms to this community') }
|
{ _t('Add rooms to this community') }
|
||||||
</div>
|
</div>
|
||||||
</AccessibleButton>) : <div />;
|
</AccessibleButton>) : <div />;
|
||||||
|
const roomDetailListClassName = classnames({
|
||||||
|
"mx_fadable": true,
|
||||||
|
"mx_fadable_faded": this.state.editing,
|
||||||
|
});
|
||||||
return <div className="mx_GroupView_rooms">
|
return <div className="mx_GroupView_rooms">
|
||||||
<div className="mx_GroupView_rooms_header">
|
<div className="mx_GroupView_rooms_header">
|
||||||
<h3>{ _t('Rooms') }</h3>
|
<h3>
|
||||||
|
{ _t('Rooms') }
|
||||||
|
{ roomsHelpNode }
|
||||||
|
</h3>
|
||||||
{ addRoomRow }
|
{ addRoomRow }
|
||||||
</div>
|
</div>
|
||||||
{ this.state.groupRoomsLoading ?
|
{ this.state.groupRoomsLoading ?
|
||||||
<Spinner /> :
|
<Spinner /> :
|
||||||
<RoomDetailList rooms={this.state.groupRooms} />
|
<RoomDetailList
|
||||||
|
rooms={this.state.groupRooms}
|
||||||
|
className={roomDetailListClassName} />
|
||||||
}
|
}
|
||||||
</div>;
|
</div>;
|
||||||
},
|
},
|
||||||
|
@ -851,6 +884,18 @@ export default React.createClass({
|
||||||
let description = null;
|
let description = null;
|
||||||
if (summary.profile && summary.profile.long_description) {
|
if (summary.profile && summary.profile.long_description) {
|
||||||
description = sanitizedHtmlNode(summary.profile.long_description);
|
description = sanitizedHtmlNode(summary.profile.long_description);
|
||||||
|
} else if (this.state.isUserPrivileged) {
|
||||||
|
description = <div
|
||||||
|
className="mx_GroupView_groupDesc_placeholder"
|
||||||
|
onClick={this._onEditClick}
|
||||||
|
>
|
||||||
|
{ _tJsx(
|
||||||
|
'Your community hasn\'t got a Long Description, a HTML page to show to community members.<br />' +
|
||||||
|
'Click here to open settings and give it one!',
|
||||||
|
[/<br \/>/],
|
||||||
|
[(sub) => <br />])
|
||||||
|
}
|
||||||
|
</div>;
|
||||||
}
|
}
|
||||||
const groupDescEditingClasses = classnames({
|
const groupDescEditingClasses = classnames({
|
||||||
"mx_GroupView_groupDesc": true,
|
"mx_GroupView_groupDesc": true,
|
||||||
|
@ -862,6 +907,7 @@ export default React.createClass({
|
||||||
<h3> { _t("Long Description (HTML)") } </h3>
|
<h3> { _t("Long Description (HTML)") } </h3>
|
||||||
<textarea
|
<textarea
|
||||||
value={this.state.profileForm.long_description}
|
value={this.state.profileForm.long_description}
|
||||||
|
placeholder={_t(LONG_DESC_PLACEHOLDER)}
|
||||||
onChange={this._onLongDescChange}
|
onChange={this._onLongDescChange}
|
||||||
tabIndex="4"
|
tabIndex="4"
|
||||||
key="editLongDesc"
|
key="editLongDesc"
|
||||||
|
|
|
@ -301,6 +301,7 @@ export default React.createClass({
|
||||||
case PageTypes.GroupView:
|
case PageTypes.GroupView:
|
||||||
page_element = <GroupView
|
page_element = <GroupView
|
||||||
groupId={this.props.currentGroupId}
|
groupId={this.props.currentGroupId}
|
||||||
|
isNew={this.props.currentGroupIsNew}
|
||||||
collapsedRhs={this.props.collapseRhs}
|
collapsedRhs={this.props.collapseRhs}
|
||||||
/>;
|
/>;
|
||||||
if (!this.props.collapseRhs) right_panel = <RightPanel groupId={this.props.currentGroupId} disabled={this.props.rightDisabled} />;
|
if (!this.props.collapseRhs) right_panel = <RightPanel groupId={this.props.currentGroupId} disabled={this.props.rightDisabled} />;
|
||||||
|
|
|
@ -490,7 +490,10 @@ module.exports = React.createClass({
|
||||||
case 'view_group':
|
case 'view_group':
|
||||||
{
|
{
|
||||||
const groupId = payload.group_id;
|
const groupId = payload.group_id;
|
||||||
this.setState({currentGroupId: groupId});
|
this.setState({
|
||||||
|
currentGroupId: groupId,
|
||||||
|
currentGroupIsNew: payload.group_is_new,
|
||||||
|
});
|
||||||
this._setPage(PageTypes.GroupView);
|
this._setPage(PageTypes.GroupView);
|
||||||
this.notifyNewScreen('group/' + groupId);
|
this.notifyNewScreen('group/' + groupId);
|
||||||
}
|
}
|
||||||
|
|
|
@ -301,6 +301,15 @@ module.exports = React.createClass({
|
||||||
_shouldShowApps: function(room) {
|
_shouldShowApps: function(room) {
|
||||||
if (!BROWSER_SUPPORTS_SANDBOX) return false;
|
if (!BROWSER_SUPPORTS_SANDBOX) return false;
|
||||||
|
|
||||||
|
// Check if user has previously chosen to hide the app drawer for this
|
||||||
|
// room. If so, do not show apps
|
||||||
|
let hideWidgetDrawer = localStorage.getItem(
|
||||||
|
room.roomId + "_hide_widget_drawer");
|
||||||
|
|
||||||
|
if (hideWidgetDrawer === "true") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
const appsStateEvents = room.currentState.getStateEvents('im.vector.modular.widgets');
|
const appsStateEvents = room.currentState.getStateEvents('im.vector.modular.widgets');
|
||||||
// any valid widget = show apps
|
// any valid widget = show apps
|
||||||
for (let i = 0; i < appsStateEvents.length; i++) {
|
for (let i = 0; i < appsStateEvents.length; i++) {
|
||||||
|
|
|
@ -81,6 +81,7 @@ export default React.createClass({
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'view_group',
|
action: 'view_group',
|
||||||
group_id: result.group_id,
|
group_id: result.group_id,
|
||||||
|
group_is_new: true,
|
||||||
});
|
});
|
||||||
this.props.onFinished(true);
|
this.props.onFinished(true);
|
||||||
}).catch((e) => {
|
}).catch((e) => {
|
||||||
|
|
|
@ -48,8 +48,9 @@ function UserUnknownDeviceList(props) {
|
||||||
const {userId, userDevices} = props;
|
const {userId, userDevices} = props;
|
||||||
|
|
||||||
const deviceListEntries = Object.keys(userDevices).map((deviceId) =>
|
const deviceListEntries = Object.keys(userDevices).map((deviceId) =>
|
||||||
<DeviceListEntry key={deviceId} userId={userId}
|
<DeviceListEntry key={deviceId} userId={userId}
|
||||||
device={userDevices[deviceId]} />,
|
device={userDevices[deviceId]}
|
||||||
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -92,26 +93,60 @@ export default React.createClass({
|
||||||
propTypes: {
|
propTypes: {
|
||||||
room: React.PropTypes.object.isRequired,
|
room: React.PropTypes.object.isRequired,
|
||||||
|
|
||||||
// map from userid -> deviceid -> deviceinfo
|
|
||||||
devices: React.PropTypes.object.isRequired,
|
|
||||||
onFinished: React.PropTypes.func.isRequired,
|
onFinished: React.PropTypes.func.isRequired,
|
||||||
},
|
},
|
||||||
|
|
||||||
componentDidMount: function() {
|
componentWillMount: function() {
|
||||||
// Given we've now shown the user the unknown device, it is no longer
|
this._unmounted = false;
|
||||||
// unknown to them. Therefore mark it as 'known'.
|
|
||||||
Object.keys(this.props.devices).forEach((userId) => {
|
const roomMembers = this.props.room.getJoinedMembers().map((m) => {
|
||||||
Object.keys(this.props.devices[userId]).map((deviceId) => {
|
return m.userId;
|
||||||
MatrixClientPeg.get().setDeviceKnown(userId, deviceId, true);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// XXX: temporary logging to try to diagnose
|
this.setState({
|
||||||
// https://github.com/vector-im/riot-web/issues/3148
|
// map from userid -> deviceid -> deviceinfo
|
||||||
console.log('Opening UnknownDeviceDialog');
|
devices: null,
|
||||||
|
});
|
||||||
|
MatrixClientPeg.get().downloadKeys(roomMembers, false).then((devices) => {
|
||||||
|
if (this._unmounted) return;
|
||||||
|
|
||||||
|
const unknownDevices = {};
|
||||||
|
// This is all devices in this room, so find the unknown ones.
|
||||||
|
Object.keys(devices).forEach((userId) => {
|
||||||
|
Object.keys(devices[userId]).map((deviceId) => {
|
||||||
|
const device = devices[userId][deviceId];
|
||||||
|
|
||||||
|
if (device.isUnverified() && !device.isKnown()) {
|
||||||
|
if (unknownDevices[userId] === undefined) {
|
||||||
|
unknownDevices[userId] = {};
|
||||||
|
}
|
||||||
|
unknownDevices[userId][deviceId] = device;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Given we've now shown the user the unknown device, it is no longer
|
||||||
|
// unknown to them. Therefore mark it as 'known'.
|
||||||
|
if (!device.isKnown()) {
|
||||||
|
MatrixClientPeg.get().setDeviceKnown(userId, deviceId, true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
devices: unknownDevices,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
componentWillUnmount: function() {
|
||||||
|
this._unmounted = true;
|
||||||
},
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
|
if (this.state.devices === null) {
|
||||||
|
const Spinner = sdk.getComponent("elements.Spinner");
|
||||||
|
return <Spinner />;
|
||||||
|
}
|
||||||
|
|
||||||
const client = MatrixClientPeg.get();
|
const client = MatrixClientPeg.get();
|
||||||
const blacklistUnverified = client.getGlobalBlacklistUnverifiedDevices() ||
|
const blacklistUnverified = client.getGlobalBlacklistUnverifiedDevices() ||
|
||||||
this.props.room.getBlacklistUnverifiedDevices();
|
this.props.room.getBlacklistUnverifiedDevices();
|
||||||
|
@ -154,7 +189,7 @@ export default React.createClass({
|
||||||
{ warning }
|
{ warning }
|
||||||
{ _t("Unknown devices") }:
|
{ _t("Unknown devices") }:
|
||||||
|
|
||||||
<UnknownDeviceList devices={this.props.devices} />
|
<UnknownDeviceList devices={this.state.devices} />
|
||||||
</GeminiScrollbar>
|
</GeminiScrollbar>
|
||||||
<div className="mx_Dialog_buttons">
|
<div className="mx_Dialog_buttons">
|
||||||
<button className="mx_Dialog_primary" autoFocus={true}
|
<button className="mx_Dialog_primary" autoFocus={true}
|
||||||
|
|
|
@ -26,11 +26,9 @@ class MenuOption extends React.Component {
|
||||||
this._onClick = this._onClick.bind(this);
|
this._onClick = this._onClick.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
getDefaultProps() {
|
static defaultProps = {
|
||||||
return {
|
disabled: false,
|
||||||
disabled: false,
|
};
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
_onMouseEnter() {
|
_onMouseEnter() {
|
||||||
this.props.onMouseEnter(this.props.dropdownKey);
|
this.props.onMouseEnter(this.props.dropdownKey);
|
||||||
|
|
55
src/components/views/elements/ToolTipButton.js
Normal file
55
src/components/views/elements/ToolTipButton.js
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
/*
|
||||||
|
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 sdk from '../../../index';
|
||||||
|
|
||||||
|
module.exports = React.createClass({
|
||||||
|
displayName: 'ToolTipButton',
|
||||||
|
|
||||||
|
getInitialState: function() {
|
||||||
|
return {
|
||||||
|
hover: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
onMouseOver: function() {
|
||||||
|
this.setState({
|
||||||
|
hover: true,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
onMouseOut: function() {
|
||||||
|
this.setState({
|
||||||
|
hover: false,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
const RoomTooltip = sdk.getComponent("rooms.RoomTooltip");
|
||||||
|
const tip = this.state.hover ? <RoomTooltip
|
||||||
|
className="mx_ToolTipButton_container"
|
||||||
|
tooltipClassName="mx_ToolTipButton_helpText"
|
||||||
|
label={this.props.helpText}
|
||||||
|
/> : <div />;
|
||||||
|
return (
|
||||||
|
<div className="mx_ToolTipButton" onMouseOver={this.onMouseOver} onMouseOut={this.onMouseOut} >
|
||||||
|
?
|
||||||
|
{ tip }
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
|
@ -61,9 +61,9 @@ export default withMatrixClient(React.createClass({
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EntityTile presenceState="online"
|
<EntityTile name={name} avatarJsx={av} onClick={this.onClick}
|
||||||
avatarJsx={av} onClick={this.onClick}
|
suppressOnHover={true} presenceState="online"
|
||||||
name={name} powerLevel={0} suppressOnHover={true}
|
powerStatus={this.props.member.isAdmin ? EntityTile.POWER_STATUS_ADMIN : null}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
@ -81,16 +81,25 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
onAction: function(action) {
|
onAction: function(action) {
|
||||||
|
const hideWidgetKey = this.props.room.roomId + "_hide_widget_drawer";
|
||||||
switch (action.action) {
|
switch (action.action) {
|
||||||
case 'appsDrawer':
|
case 'appsDrawer':
|
||||||
// When opening the app draw when there aren't any apps, auto-launch the
|
// When opening the app drawer when there aren't any apps,
|
||||||
// integrations manager to skip the awkward click on "Add widget"
|
// auto-launch the integrations manager to skip the awkward
|
||||||
|
// click on "Add widget"
|
||||||
if (action.show) {
|
if (action.show) {
|
||||||
const apps = this._getApps();
|
const apps = this._getApps();
|
||||||
if (apps.length === 0) {
|
if (apps.length === 0) {
|
||||||
this._launchManageIntegrations();
|
this._launchManageIntegrations();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
localStorage.removeItem(hideWidgetKey);
|
||||||
|
} else {
|
||||||
|
// Store hidden state of widget
|
||||||
|
// Don't show if previously hidden
|
||||||
|
localStorage.setItem(hideWidgetKey, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -47,7 +47,7 @@ function presenceClassForMember(presenceState, lastActiveAgo) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = React.createClass({
|
const EntityTile = React.createClass({
|
||||||
displayName: 'EntityTile',
|
displayName: 'EntityTile',
|
||||||
|
|
||||||
propTypes: {
|
propTypes: {
|
||||||
|
@ -140,16 +140,19 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
|
|
||||||
let power;
|
let power;
|
||||||
const powerLevel = this.props.powerLevel;
|
const powerStatus = this.props.powerStatus;
|
||||||
if (powerLevel >= 50 && powerLevel < 99) {
|
if (powerStatus) {
|
||||||
power = <img src="img/mod.svg" className="mx_EntityTile_power" width="16" height="17" alt={_t("Moderator")} />;
|
const src = {
|
||||||
}
|
[EntityTile.POWER_STATUS_MODERATOR]: "img/mod.svg",
|
||||||
if (powerLevel >= 99) {
|
[EntityTile.POWER_STATUS_ADMIN]: "img/admin.svg",
|
||||||
power = <img src="img/admin.svg" className="mx_EntityTile_power" width="16" height="17" alt={_t("Admin")} />;
|
}[powerStatus];
|
||||||
|
const alt = {
|
||||||
|
[EntityTile.POWER_STATUS_MODERATOR]: _t("Moderator"),
|
||||||
|
[EntityTile.POWER_STATUS_ADMIN]: _t("Admin"),
|
||||||
|
}[powerStatus];
|
||||||
|
power = <img src={src} className="mx_EntityTile_power" width="16" height="17" alt={alt} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
|
|
||||||
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
|
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
|
||||||
|
|
||||||
const av = this.props.avatarJsx || <BaseAvatar name={this.props.name} width={36} height={36} />;
|
const av = this.props.avatarJsx || <BaseAvatar name={this.props.name} width={36} height={36} />;
|
||||||
|
@ -168,3 +171,9 @@ module.exports = React.createClass({
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
EntityTile.POWER_STATUS_MODERATOR = "moderator";
|
||||||
|
EntityTile.POWER_STATUS_ADMIN = "admin";
|
||||||
|
|
||||||
|
|
||||||
|
export default EntityTile;
|
||||||
|
|
|
@ -86,13 +86,19 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
this.member_last_modified_time = member.getLastModifiedTime();
|
this.member_last_modified_time = member.getLastModifiedTime();
|
||||||
|
|
||||||
|
// We deliberately leave power levels that are not 100 or 50 undefined
|
||||||
|
const powerStatus = {
|
||||||
|
100: EntityTile.POWER_STATUS_ADMIN,
|
||||||
|
50: EntityTile.POWER_STATUS_MODERATOR,
|
||||||
|
}[this.props.member.powerLevel];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EntityTile {...this.props} presenceState={presenceState}
|
<EntityTile {...this.props} presenceState={presenceState}
|
||||||
presenceLastActiveAgo={member.user ? member.user.lastActiveAgo : 0}
|
presenceLastActiveAgo={member.user ? member.user.lastActiveAgo : 0}
|
||||||
presenceLastTs={member.user ? member.user.lastPresenceTs : 0}
|
presenceLastTs={member.user ? member.user.lastPresenceTs : 0}
|
||||||
presenceCurrentlyActive={member.user ? member.user.currentlyActive : false}
|
presenceCurrentlyActive={member.user ? member.user.currentlyActive : false}
|
||||||
avatarJsx={av} title={this.getPowerLabel()} onClick={this.onClick}
|
avatarJsx={av} title={this.getPowerLabel()} onClick={this.onClick}
|
||||||
name={name} powerLevel={this.props.member.powerLevel} />
|
name={name} powerStatus={powerStatus} />
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -58,6 +58,11 @@ const TYPING_USER_TIMEOUT = 10000, TYPING_SERVER_TIMEOUT = 30000;
|
||||||
|
|
||||||
const ZWS_CODE = 8203;
|
const ZWS_CODE = 8203;
|
||||||
const ZWS = String.fromCharCode(ZWS_CODE); // zero width space
|
const ZWS = String.fromCharCode(ZWS_CODE); // zero width space
|
||||||
|
|
||||||
|
const ENTITY_TYPES = {
|
||||||
|
AT_ROOM_PILL: 'ATROOMPILL',
|
||||||
|
};
|
||||||
|
|
||||||
function stateToMarkdown(state) {
|
function stateToMarkdown(state) {
|
||||||
return __stateToMarkdown(state)
|
return __stateToMarkdown(state)
|
||||||
.replace(
|
.replace(
|
||||||
|
@ -188,13 +193,16 @@ export default class MessageComposerInput extends React.Component {
|
||||||
this.client = MatrixClientPeg.get();
|
this.client = MatrixClientPeg.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
findLinkEntities(contentState: ContentState, contentBlock: ContentBlock, callback) {
|
findPillEntities(contentState: ContentState, contentBlock: ContentBlock, callback) {
|
||||||
contentBlock.findEntityRanges(
|
contentBlock.findEntityRanges(
|
||||||
(character) => {
|
(character) => {
|
||||||
const entityKey = character.getEntity();
|
const entityKey = character.getEntity();
|
||||||
return (
|
return (
|
||||||
entityKey !== null &&
|
entityKey !== null &&
|
||||||
contentState.getEntity(entityKey).getType() === 'LINK'
|
(
|
||||||
|
contentState.getEntity(entityKey).getType() === 'LINK' ||
|
||||||
|
contentState.getEntity(entityKey).getType() === ENTITY_TYPES.AT_ROOM_PILL
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}, callback,
|
}, callback,
|
||||||
);
|
);
|
||||||
|
@ -210,11 +218,19 @@ export default class MessageComposerInput extends React.Component {
|
||||||
RichText.getScopedMDDecorators(this.props);
|
RichText.getScopedMDDecorators(this.props);
|
||||||
const shouldShowPillAvatar = !SettingsStore.getValue("Pill.shouldHidePillAvatar");
|
const shouldShowPillAvatar = !SettingsStore.getValue("Pill.shouldHidePillAvatar");
|
||||||
decorators.push({
|
decorators.push({
|
||||||
strategy: this.findLinkEntities.bind(this),
|
strategy: this.findPillEntities.bind(this),
|
||||||
component: (entityProps) => {
|
component: (entityProps) => {
|
||||||
const Pill = sdk.getComponent('elements.Pill');
|
const Pill = sdk.getComponent('elements.Pill');
|
||||||
|
const type = entityProps.contentState.getEntity(entityProps.entityKey).getType();
|
||||||
const {url} = entityProps.contentState.getEntity(entityProps.entityKey).getData();
|
const {url} = entityProps.contentState.getEntity(entityProps.entityKey).getData();
|
||||||
if (Pill.isPillUrl(url)) {
|
if (type === ENTITY_TYPES.AT_ROOM_PILL) {
|
||||||
|
return <Pill
|
||||||
|
type={Pill.TYPE_AT_ROOM_MENTION}
|
||||||
|
room={this.props.room}
|
||||||
|
offsetKey={entityProps.offsetKey}
|
||||||
|
shouldShowPillAvatar={shouldShowPillAvatar}
|
||||||
|
/>;
|
||||||
|
} else if (Pill.isPillUrl(url)) {
|
||||||
return <Pill
|
return <Pill
|
||||||
url={url}
|
url={url}
|
||||||
room={this.props.room}
|
room={this.props.room}
|
||||||
|
@ -784,7 +800,7 @@ export default class MessageComposerInput extends React.Component {
|
||||||
const pt = contentState.getBlocksAsArray().map((block) => {
|
const pt = contentState.getBlocksAsArray().map((block) => {
|
||||||
let blockText = block.getText();
|
let blockText = block.getText();
|
||||||
let offset = 0;
|
let offset = 0;
|
||||||
this.findLinkEntities(contentState, block, (start, end) => {
|
this.findPillEntities(contentState, block, (start, end) => {
|
||||||
const entity = contentState.getEntity(block.getEntityAt(start));
|
const entity = contentState.getEntity(block.getEntityAt(start));
|
||||||
if (entity.getType() !== 'LINK') {
|
if (entity.getType() !== 'LINK') {
|
||||||
return;
|
return;
|
||||||
|
@ -989,6 +1005,11 @@ export default class MessageComposerInput extends React.Component {
|
||||||
isCompletion: true,
|
isCompletion: true,
|
||||||
});
|
});
|
||||||
entityKey = contentState.getLastCreatedEntityKey();
|
entityKey = contentState.getLastCreatedEntityKey();
|
||||||
|
} else if (completion === '@room') {
|
||||||
|
contentState = contentState.createEntity(ENTITY_TYPES.AT_ROOM_PILL, 'IMMUTABLE', {
|
||||||
|
isCompletion: true,
|
||||||
|
});
|
||||||
|
entityKey = contentState.getLastCreatedEntityKey();
|
||||||
}
|
}
|
||||||
|
|
||||||
let selection;
|
let selection;
|
||||||
|
|
|
@ -19,6 +19,7 @@ import MatrixClientPeg from "../../../MatrixClientPeg";
|
||||||
import AccessibleButton from "../elements/AccessibleButton";
|
import AccessibleButton from "../elements/AccessibleButton";
|
||||||
import PinnedEventTile from "./PinnedEventTile";
|
import PinnedEventTile from "./PinnedEventTile";
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
|
import PinningUtils from "../../../utils/PinningUtils";
|
||||||
|
|
||||||
module.exports = React.createClass({
|
module.exports = React.createClass({
|
||||||
displayName: 'PinnedEventsPanel',
|
displayName: 'PinnedEventsPanel',
|
||||||
|
@ -61,20 +62,39 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
Promise.all(promises).then((contexts) => {
|
Promise.all(promises).then((contexts) => {
|
||||||
// Filter out the messages before we try to render them
|
// Filter out the messages before we try to render them
|
||||||
const pinned = contexts.filter((context) => {
|
const pinned = contexts.filter((context) => PinningUtils.isPinnable(context.event));
|
||||||
if (!context) return false; // no context == not applicable for the room
|
|
||||||
if (context.event.getType() !== "m.room.message") return false;
|
|
||||||
if (context.event.isRedacted()) return false;
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
this.setState({ loading: false, pinned });
|
this.setState({ loading: false, pinned });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this._updateReadState();
|
||||||
|
},
|
||||||
|
|
||||||
|
_updateReadState: function() {
|
||||||
|
const pinnedEvents = this.props.room.currentState.getStateEvents("m.room.pinned_events", "");
|
||||||
|
if (!pinnedEvents) return; // nothing to read
|
||||||
|
|
||||||
|
let readStateEvents = [];
|
||||||
|
const readPinsEvent = this.props.room.getAccountData("im.vector.room.read_pins");
|
||||||
|
if (readPinsEvent && readPinsEvent.getContent()) {
|
||||||
|
readStateEvents = readPinsEvent.getContent().event_ids || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!readStateEvents.includes(pinnedEvents.getId())) {
|
||||||
|
readStateEvents.push(pinnedEvents.getId());
|
||||||
|
|
||||||
|
// Only keep the last 10 event IDs to avoid infinite growth
|
||||||
|
readStateEvents = readStateEvents.reverse().splice(0, 10).reverse();
|
||||||
|
|
||||||
|
MatrixClientPeg.get().setRoomAccountData(this.props.room.roomId, "im.vector.room.read_pins", {
|
||||||
|
event_ids: readStateEvents,
|
||||||
|
});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
_getPinnedTiles: function() {
|
_getPinnedTiles: function() {
|
||||||
if (this.state.pinned.length == 0) {
|
if (this.state.pinned.length === 0) {
|
||||||
return (<div>{ _t("No pinned messages.") }</div>);
|
return (<div>{ _t("No pinned messages.") }</div>);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -23,6 +23,7 @@ import sanitizeHtml from 'sanitize-html';
|
||||||
import { ContentRepo } from 'matrix-js-sdk';
|
import { ContentRepo } from 'matrix-js-sdk';
|
||||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
function getDisplayAliasForRoom(room) {
|
function getDisplayAliasForRoom(room) {
|
||||||
return room.canonicalAlias || (room.aliases ? room.aliases[0] : "");
|
return room.canonicalAlias || (room.aliases ? room.aliases[0] : "");
|
||||||
|
@ -117,6 +118,8 @@ export default React.createClass({
|
||||||
worldReadable: PropTypes.bool,
|
worldReadable: PropTypes.bool,
|
||||||
guestCanJoin: PropTypes.bool,
|
guestCanJoin: PropTypes.bool,
|
||||||
})),
|
})),
|
||||||
|
|
||||||
|
className: PropTypes.string,
|
||||||
},
|
},
|
||||||
|
|
||||||
getRows: function() {
|
getRows: function() {
|
||||||
|
@ -138,7 +141,7 @@ export default React.createClass({
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>;
|
</table>;
|
||||||
}
|
}
|
||||||
return <div className="mx_RoomDetailList">
|
return <div className={classNames("mx_RoomDetailList", this.props.className)}>
|
||||||
{ rooms }
|
{ rooms }
|
||||||
</div>;
|
</div>;
|
||||||
},
|
},
|
||||||
|
|
|
@ -65,6 +65,7 @@ module.exports = React.createClass({
|
||||||
componentDidMount: function() {
|
componentDidMount: function() {
|
||||||
const cli = MatrixClientPeg.get();
|
const cli = MatrixClientPeg.get();
|
||||||
cli.on("RoomState.events", this._onRoomStateEvents);
|
cli.on("RoomState.events", this._onRoomStateEvents);
|
||||||
|
cli.on("Room.accountData", this._onRoomAccountData);
|
||||||
|
|
||||||
// When a room name occurs, RoomState.events is fired *before*
|
// When a room name occurs, RoomState.events is fired *before*
|
||||||
// room.name is updated. So we have to listen to Room.name as well as
|
// room.name is updated. So we have to listen to Room.name as well as
|
||||||
|
@ -87,6 +88,7 @@ module.exports = React.createClass({
|
||||||
const cli = MatrixClientPeg.get();
|
const cli = MatrixClientPeg.get();
|
||||||
if (cli) {
|
if (cli) {
|
||||||
cli.removeListener("RoomState.events", this._onRoomStateEvents);
|
cli.removeListener("RoomState.events", this._onRoomStateEvents);
|
||||||
|
cli.removeListener("Room.accountData", this._onRoomAccountData);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -99,6 +101,13 @@ module.exports = React.createClass({
|
||||||
this._rateLimitedUpdate();
|
this._rateLimitedUpdate();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_onRoomAccountData: function(event, room) {
|
||||||
|
if (!this.props.room || room.roomId !== this.props.room.roomId) return;
|
||||||
|
if (event.getType() !== "im.vector.room.read_pins") return;
|
||||||
|
|
||||||
|
this._rateLimitedUpdate();
|
||||||
|
},
|
||||||
|
|
||||||
_rateLimitedUpdate: new RateLimitedFunc(function() {
|
_rateLimitedUpdate: new RateLimitedFunc(function() {
|
||||||
/* eslint-disable babel/no-invalid-this */
|
/* eslint-disable babel/no-invalid-this */
|
||||||
this.forceUpdate();
|
this.forceUpdate();
|
||||||
|
@ -139,6 +148,32 @@ module.exports = React.createClass({
|
||||||
dis.dispatch({ action: 'show_right_panel' });
|
dis.dispatch({ action: 'show_right_panel' });
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_hasUnreadPins: function() {
|
||||||
|
const currentPinEvent = this.props.room.currentState.getStateEvents("m.room.pinned_events", '');
|
||||||
|
if (!currentPinEvent) return false;
|
||||||
|
if (currentPinEvent.getContent().pinned && currentPinEvent.getContent().pinned.length <= 0) {
|
||||||
|
return false; // no pins == nothing to read
|
||||||
|
}
|
||||||
|
|
||||||
|
const readPinsEvent = this.props.room.getAccountData("im.vector.room.read_pins");
|
||||||
|
if (readPinsEvent && readPinsEvent.getContent()) {
|
||||||
|
const readStateEvents = readPinsEvent.getContent().event_ids || [];
|
||||||
|
if (readStateEvents) {
|
||||||
|
return !readStateEvents.includes(currentPinEvent.getId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// There's pins, and we haven't read any of them
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
|
||||||
|
_hasPins: function() {
|
||||||
|
const currentPinEvent = this.props.room.currentState.getStateEvents("m.room.pinned_events", '');
|
||||||
|
if (!currentPinEvent) return false;
|
||||||
|
|
||||||
|
return !(currentPinEvent.getContent().pinned && currentPinEvent.getContent().pinned.length <= 0);
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* After editing the settings, get the new name for the room
|
* After editing the settings, get the new name for the room
|
||||||
*
|
*
|
||||||
|
@ -305,8 +340,17 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.props.onPinnedClick && SettingsStore.isFeatureEnabled('feature_pinning')) {
|
if (this.props.onPinnedClick && SettingsStore.isFeatureEnabled('feature_pinning')) {
|
||||||
|
let pinsIndicator = null;
|
||||||
|
if (this._hasUnreadPins()) {
|
||||||
|
pinsIndicator = (<div className="mx_RoomHeader_pinsIndicator mx_RoomHeader_pinsIndicatorUnread" />);
|
||||||
|
} else if (this._hasPins()) {
|
||||||
|
pinsIndicator = (<div className="mx_RoomHeader_pinsIndicator" />);
|
||||||
|
}
|
||||||
|
|
||||||
pinnedEventsButton =
|
pinnedEventsButton =
|
||||||
<AccessibleButton className="mx_RoomHeader_button" onClick={this.props.onPinnedClick} title={_t("Pinned Messages")}>
|
<AccessibleButton className="mx_RoomHeader_button mx_RoomHeader_pinnedButton"
|
||||||
|
onClick={this.props.onPinnedClick} title={_t("Pinned Messages")}>
|
||||||
|
{ pinsIndicator }
|
||||||
<TintableSvg src="img/icons-pin.svg" width="16" height="16" />
|
<TintableSvg src="img/icons-pin.svg" width="16" height="16" />
|
||||||
</AccessibleButton>;
|
</AccessibleButton>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,6 +36,7 @@ export function groupMemberFromApiObject(apiObject) {
|
||||||
userId: apiObject.user_id,
|
userId: apiObject.user_id,
|
||||||
displayname: apiObject.displayname,
|
displayname: apiObject.displayname,
|
||||||
avatarUrl: apiObject.avatar_url,
|
avatarUrl: apiObject.avatar_url,
|
||||||
|
isAdmin: apiObject.is_admin,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -669,6 +669,7 @@
|
||||||
"You must <a>register</a> to use this functionality": "You must <a>register</a> to use this functionality",
|
"You must <a>register</a> to use this functionality": "You must <a>register</a> to use this functionality",
|
||||||
"You must join the room to see its files": "You must join the room to see its files",
|
"You must join the room to see its files": "You must join the room to see its files",
|
||||||
"There are no visible files in this room": "There are no visible files in this room",
|
"There are no visible files in this room": "There are no visible files in this room",
|
||||||
|
"<h1>HTML for your community's page</h1>\n<p>\n Use the long description to introduce new members to the community, or distribute\n some important <a href=\"foo\">links</a>\n</p>\n<p>\n You can even use 'img' tags\n</p>\n": "<h1>HTML for your community's page</h1>\n<p>\n Use the long description to introduce new members to the community, or distribute\n some important <a href=\"foo\">links</a>\n</p>\n<p>\n You can even use 'img' tags\n</p>\n",
|
||||||
"Add rooms to the community summary": "Add rooms to the community summary",
|
"Add rooms to the community summary": "Add rooms to the community summary",
|
||||||
"Which rooms would you like to add to this summary?": "Which rooms would you like to add to this summary?",
|
"Which rooms would you like to add to this summary?": "Which rooms would you like to add to this summary?",
|
||||||
"Add to summary": "Add to summary",
|
"Add to summary": "Add to summary",
|
||||||
|
@ -691,6 +692,7 @@
|
||||||
"Leave": "Leave",
|
"Leave": "Leave",
|
||||||
"Unable to leave room": "Unable to leave room",
|
"Unable to leave room": "Unable to leave room",
|
||||||
"Community Settings": "Community Settings",
|
"Community Settings": "Community Settings",
|
||||||
|
"These rooms are displayed to community members on the community page. Community members can join the rooms by clicking on them.": "These rooms are displayed to community members on the community page. Community members can join the rooms by clicking on them.",
|
||||||
"Add rooms to this community": "Add rooms to this community",
|
"Add rooms to this community": "Add rooms to this community",
|
||||||
"Featured Rooms:": "Featured Rooms:",
|
"Featured Rooms:": "Featured Rooms:",
|
||||||
"Featured Users:": "Featured Users:",
|
"Featured Users:": "Featured Users:",
|
||||||
|
@ -699,6 +701,7 @@
|
||||||
"You are a member of this community": "You are a member of this community",
|
"You are a member of this community": "You are a member of this community",
|
||||||
"Community Member Settings": "Community Member Settings",
|
"Community Member Settings": "Community Member Settings",
|
||||||
"Publish this community on your profile": "Publish this community on your profile",
|
"Publish this community on your profile": "Publish this community on your profile",
|
||||||
|
"Your community hasn't got a Long Description, a HTML page to show to community members.<br />Click here to open settings and give it one!": "Your community hasn't got a Long Description, a HTML page to show to community members.<br />Click here to open settings and give it one!",
|
||||||
"Long Description (HTML)": "Long Description (HTML)",
|
"Long Description (HTML)": "Long Description (HTML)",
|
||||||
"Description": "Description",
|
"Description": "Description",
|
||||||
"Community %(groupId)s not found": "Community %(groupId)s not found",
|
"Community %(groupId)s not found": "Community %(groupId)s not found",
|
||||||
|
@ -889,6 +892,8 @@
|
||||||
"Commands": "Commands",
|
"Commands": "Commands",
|
||||||
"Results from DuckDuckGo": "Results from DuckDuckGo",
|
"Results from DuckDuckGo": "Results from DuckDuckGo",
|
||||||
"Emoji": "Emoji",
|
"Emoji": "Emoji",
|
||||||
|
"Notify the whole room": "Notify the whole room",
|
||||||
|
"Room Notification": "Room Notification",
|
||||||
"Users": "Users",
|
"Users": "Users",
|
||||||
"unknown device": "unknown device",
|
"unknown device": "unknown device",
|
||||||
"NOT verified": "NOT verified",
|
"NOT verified": "NOT verified",
|
||||||
|
|
|
@ -33,6 +33,9 @@ export default class GroupStore extends EventEmitter {
|
||||||
|
|
||||||
constructor(matrixClient, groupId) {
|
constructor(matrixClient, groupId) {
|
||||||
super();
|
super();
|
||||||
|
if (!groupId) {
|
||||||
|
throw new Error('GroupStore needs a valid groupId to be created');
|
||||||
|
}
|
||||||
this.groupId = groupId;
|
this.groupId = groupId;
|
||||||
this._matrixClient = matrixClient;
|
this._matrixClient = matrixClient;
|
||||||
this._summary = {};
|
this._summary = {};
|
||||||
|
|
30
src/utils/PinningUtils.js
Normal file
30
src/utils/PinningUtils.js
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
/*
|
||||||
|
Copyright 2017 Travis Ralston
|
||||||
|
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export default class PinningUtils {
|
||||||
|
/**
|
||||||
|
* Determines if the given event may be pinned.
|
||||||
|
* @param {MatrixEvent} event The event to check.
|
||||||
|
* @return {boolean} True if the event may be pinned, false otherwise.
|
||||||
|
*/
|
||||||
|
static isPinnable(event) {
|
||||||
|
if (!event) return false;
|
||||||
|
if (event.getType() !== "m.room.message") return false;
|
||||||
|
if (event.isRedacted()) return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue