Merge remote-tracking branch 'origin/develop' into matthew/status
						commit
						e729bc431d
					
				| 
						 | 
					@ -49,20 +49,26 @@ export function showGroupInviteDialog(groupId) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function showGroupAddRoomDialog(groupId) {
 | 
					export function showGroupAddRoomDialog(groupId) {
 | 
				
			||||||
    return new Promise((resolve, reject) => {
 | 
					    return new Promise((resolve, reject) => {
 | 
				
			||||||
 | 
					        let addRoomsPublicly = false;
 | 
				
			||||||
 | 
					        const onCheckboxClicked = (e) => {
 | 
				
			||||||
 | 
					            addRoomsPublicly = e.target.checked;
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
        const description = <div>
 | 
					        const description = <div>
 | 
				
			||||||
            <div>{ _t("Which rooms would you like to add to this community?") }</div>
 | 
					            <div>{ _t("Which rooms would you like to add to this community?") }</div>
 | 
				
			||||||
            <div className="warning">
 | 
					 | 
				
			||||||
                { _t(
 | 
					 | 
				
			||||||
                    "Warning: any room you add to a community will be publicly "+
 | 
					 | 
				
			||||||
                    "visible to anyone who knows the community ID",
 | 
					 | 
				
			||||||
                ) }
 | 
					 | 
				
			||||||
            </div>
 | 
					 | 
				
			||||||
        </div>;
 | 
					        </div>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const checkboxContainer = <label className="mx_GroupAddressPicker_checkboxContainer">
 | 
				
			||||||
 | 
					            <input type="checkbox" onClick={onCheckboxClicked} />
 | 
				
			||||||
 | 
					            <div>
 | 
				
			||||||
 | 
					                { _t("Show these rooms to non-members on the community page and room list?") }
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					        </label>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const AddressPickerDialog = sdk.getComponent("dialogs.AddressPickerDialog");
 | 
					        const AddressPickerDialog = sdk.getComponent("dialogs.AddressPickerDialog");
 | 
				
			||||||
        Modal.createTrackedDialog('Add Rooms to Group', '', AddressPickerDialog, {
 | 
					        Modal.createTrackedDialog('Add Rooms to Group', '', AddressPickerDialog, {
 | 
				
			||||||
            title: _t("Add rooms to the community"),
 | 
					            title: _t("Add rooms to the community"),
 | 
				
			||||||
            description: description,
 | 
					            description: description,
 | 
				
			||||||
 | 
					            extraNode: checkboxContainer,
 | 
				
			||||||
            placeholder: _t("Room name or alias"),
 | 
					            placeholder: _t("Room name or alias"),
 | 
				
			||||||
            button: _t("Add to community"),
 | 
					            button: _t("Add to community"),
 | 
				
			||||||
            pickerType: 'room',
 | 
					            pickerType: 'room',
 | 
				
			||||||
| 
						 | 
					@ -70,7 +76,7 @@ export function showGroupAddRoomDialog(groupId) {
 | 
				
			||||||
            onFinished: (success, addrs) => {
 | 
					            onFinished: (success, addrs) => {
 | 
				
			||||||
                if (!success) return;
 | 
					                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 matrixClient = MatrixClientPeg.get();
 | 
				
			||||||
    const groupStore = GroupStoreCache.getGroupStore(matrixClient, groupId);
 | 
					    const groupStore = GroupStoreCache.getGroupStore(matrixClient, groupId);
 | 
				
			||||||
    const errorList = [];
 | 
					    const errorList = [];
 | 
				
			||||||
    return Promise.all(addrs.map((addr) => {
 | 
					    return Promise.all(addrs.map((addr) => {
 | 
				
			||||||
        return groupStore
 | 
					        return groupStore
 | 
				
			||||||
            .addRoomToGroup(addr.address)
 | 
					            .addRoomToGroup(addr.address, addRoomsPublicly)
 | 
				
			||||||
            .catch(() => { errorList.push(addr.address); })
 | 
					            .catch(() => { errorList.push(addr.address); })
 | 
				
			||||||
            .then(() => {
 | 
					            .then(() => {
 | 
				
			||||||
                const roomId = addr.address;
 | 
					                const roomId = addr.address;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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,
 | 
				
			||||||
];
 | 
					];
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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: {
 | 
				
			||||||
| 
						 | 
					@ -417,12 +430,13 @@ export default React.createClass({
 | 
				
			||||||
            uploadingAvatar: false,
 | 
					            uploadingAvatar: false,
 | 
				
			||||||
            membershipBusy: false,
 | 
					            membershipBusy: false,
 | 
				
			||||||
            publicityBusy: false,
 | 
					            publicityBusy: false,
 | 
				
			||||||
 | 
					            inviterProfile: null,
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    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 +463,11 @@ export default React.createClass({
 | 
				
			||||||
        this.setState({membershipBusy: false});
 | 
					        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 = GroupStoreCache.getGroupStore(MatrixClientPeg.get(), groupId);
 | 
				
			||||||
        this._groupStore.registerListener(() => {
 | 
					        this._groupStore.registerListener(() => {
 | 
				
			||||||
            const summary = this._groupStore.getSummary();
 | 
					            const summary = this._groupStore.getSummary();
 | 
				
			||||||
| 
						 | 
					@ -472,6 +490,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({
 | 
				
			||||||
| 
						 | 
					@ -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) {
 | 
					    _onShowRhsClick: function(ev) {
 | 
				
			||||||
        dis.dispatch({ action: 'show_right_panel' });
 | 
					        dis.dispatch({ action: 'show_right_panel' });
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
| 
						 | 
					@ -575,7 +616,7 @@ export default React.createClass({
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    _onAcceptInviteClick: function() {
 | 
					    _onAcceptInviteClick: function() {
 | 
				
			||||||
        this.setState({membershipBusy: true});
 | 
					        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
 | 
					            // don't reset membershipBusy here: wait for the membership change to come down the sync
 | 
				
			||||||
        }).catch((e) => {
 | 
					        }).catch((e) => {
 | 
				
			||||||
            this.setState({membershipBusy: false});
 | 
					            this.setState({membershipBusy: false});
 | 
				
			||||||
| 
						 | 
					@ -661,6 +702,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 +722,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>;
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
| 
						 | 
					@ -769,20 +827,37 @@ export default React.createClass({
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    _getMembershipSection: function() {
 | 
					    _getMembershipSection: function() {
 | 
				
			||||||
        const Spinner = sdk.getComponent("elements.Spinner");
 | 
					        const Spinner = sdk.getComponent("elements.Spinner");
 | 
				
			||||||
 | 
					        const BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const group = MatrixClientPeg.get().getGroup(this.props.groupId);
 | 
					        const group = MatrixClientPeg.get().getGroup(this.props.groupId);
 | 
				
			||||||
        if (!group) return null;
 | 
					        if (!group) return null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (group.myMembership === 'invite') {
 | 
					        if (group.myMembership === 'invite') {
 | 
				
			||||||
            if (this.state.membershipBusy) {
 | 
					            if (this.state.membershipBusy || this.state.inviterProfileBusy) {
 | 
				
			||||||
                return <div className="mx_GroupView_membershipSection">
 | 
					                return <div className="mx_GroupView_membershipSection">
 | 
				
			||||||
                    <Spinner />
 | 
					                    <Spinner />
 | 
				
			||||||
                </div>;
 | 
					                </div>;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					            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 <div className="mx_GroupView_membershipSection mx_GroupView_membershipSection_invited">
 | 
					            return <div className="mx_GroupView_membershipSection mx_GroupView_membershipSection_invited">
 | 
				
			||||||
                <div className="mx_GroupView_membershipSubSection">
 | 
					                <div className="mx_GroupView_membershipSubSection">
 | 
				
			||||||
                    <div className="mx_GroupView_membershipSection_description">
 | 
					                    <div className="mx_GroupView_membershipSection_description">
 | 
				
			||||||
                        { _t("%(inviter)s has invited you to join this community", {inviter: group.inviter.userId}) }
 | 
					                        <BaseAvatar url={httpInviterAvatar}
 | 
				
			||||||
 | 
					                            name={inviterName}
 | 
				
			||||||
 | 
					                            width={36}
 | 
				
			||||||
 | 
					                            height={36}
 | 
				
			||||||
 | 
					                        />
 | 
				
			||||||
 | 
					                        { _t("%(inviter)s has invited you to join this community", {
 | 
				
			||||||
 | 
					                            inviter: inviterName,
 | 
				
			||||||
 | 
					                        }) }
 | 
				
			||||||
                    </div>
 | 
					                    </div>
 | 
				
			||||||
                    <div className="mx_GroupView_membership_buttonContainer">
 | 
					                    <div className="mx_GroupView_membership_buttonContainer">
 | 
				
			||||||
                        <AccessibleButton className="mx_GroupView_textButton mx_RoomHeader_textButton"
 | 
					                        <AccessibleButton className="mx_GroupView_textButton mx_RoomHeader_textButton"
 | 
				
			||||||
| 
						 | 
					@ -851,6 +926,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 +949,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} />;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -493,7 +493,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);
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -303,6 +303,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++) {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -34,6 +34,8 @@ module.exports = React.createClass({
 | 
				
			||||||
    propTypes: {
 | 
					    propTypes: {
 | 
				
			||||||
        title: PropTypes.string.isRequired,
 | 
					        title: PropTypes.string.isRequired,
 | 
				
			||||||
        description: PropTypes.node,
 | 
					        description: PropTypes.node,
 | 
				
			||||||
 | 
					        // Extra node inserted after picker input, dropdown and errors
 | 
				
			||||||
 | 
					        extraNode: PropTypes.node,
 | 
				
			||||||
        value: PropTypes.string,
 | 
					        value: PropTypes.string,
 | 
				
			||||||
        placeholder: PropTypes.string,
 | 
					        placeholder: PropTypes.string,
 | 
				
			||||||
        roomId: PropTypes.string,
 | 
					        roomId: PropTypes.string,
 | 
				
			||||||
| 
						 | 
					@ -268,34 +270,53 @@ module.exports = React.createClass({
 | 
				
			||||||
        const rooms = MatrixClientPeg.get().getRooms();
 | 
					        const rooms = MatrixClientPeg.get().getRooms();
 | 
				
			||||||
        const results = [];
 | 
					        const results = [];
 | 
				
			||||||
        rooms.forEach((room) => {
 | 
					        rooms.forEach((room) => {
 | 
				
			||||||
 | 
					            let rank = Infinity;
 | 
				
			||||||
            const nameEvent = room.currentState.getStateEvents('m.room.name', '');
 | 
					            const nameEvent = room.currentState.getStateEvents('m.room.name', '');
 | 
				
			||||||
            const topicEvent = room.currentState.getStateEvents('m.room.topic', '');
 | 
					 | 
				
			||||||
            const name = nameEvent ? nameEvent.getContent().name : '';
 | 
					            const name = nameEvent ? nameEvent.getContent().name : '';
 | 
				
			||||||
            const canonicalAlias = room.getCanonicalAlias();
 | 
					            const canonicalAlias = room.getCanonicalAlias();
 | 
				
			||||||
            const aliasEvents = room.currentState.getStateEvents('m.room.aliases');
 | 
					            const aliasEvents = room.currentState.getStateEvents('m.room.aliases');
 | 
				
			||||||
            const aliases = aliasEvents.map((ev) => ev.getContent().aliases).reduce((a, b) => {
 | 
					            const aliases = aliasEvents.map((ev) => ev.getContent().aliases).reduce((a, b) => {
 | 
				
			||||||
                return a.concat(b);
 | 
					                return a.concat(b);
 | 
				
			||||||
            }, []);
 | 
					            }, []);
 | 
				
			||||||
            const topic = topicEvent ? topicEvent.getContent().topic : '';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
            const nameMatch = (name || '').toLowerCase().includes(lowerCaseQuery);
 | 
					            const nameMatch = (name || '').toLowerCase().includes(lowerCaseQuery);
 | 
				
			||||||
            const aliasMatch = aliases.some((alias) =>
 | 
					            let aliasMatch = false;
 | 
				
			||||||
                (alias || '').toLowerCase().includes(lowerCaseQuery),
 | 
					            let shortestMatchingAliasLength = Infinity;
 | 
				
			||||||
            );
 | 
					            aliases.forEach((alias) => {
 | 
				
			||||||
            const topicMatch = (topic || '').toLowerCase().includes(lowerCaseQuery);
 | 
					                if ((alias || '').toLowerCase().includes(lowerCaseQuery)) {
 | 
				
			||||||
            if (!(nameMatch || topicMatch || aliasMatch)) {
 | 
					                    aliasMatch = true;
 | 
				
			||||||
 | 
					                    if (shortestMatchingAliasLength > alias.length) {
 | 
				
			||||||
 | 
					                        shortestMatchingAliasLength = alias.length;
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (!(nameMatch || aliasMatch)) {
 | 
				
			||||||
                return;
 | 
					                return;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (aliasMatch) {
 | 
				
			||||||
 | 
					                // A shorter matching alias will give a better rank
 | 
				
			||||||
 | 
					                rank = shortestMatchingAliasLength;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            const avatarEvent = room.currentState.getStateEvents('m.room.avatar', '');
 | 
					            const avatarEvent = room.currentState.getStateEvents('m.room.avatar', '');
 | 
				
			||||||
            const avatarUrl = avatarEvent ? avatarEvent.getContent().url : undefined;
 | 
					            const avatarUrl = avatarEvent ? avatarEvent.getContent().url : undefined;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            results.push({
 | 
					            results.push({
 | 
				
			||||||
 | 
					                rank,
 | 
				
			||||||
                room_id: room.roomId,
 | 
					                room_id: room.roomId,
 | 
				
			||||||
                avatar_url: avatarUrl,
 | 
					                avatar_url: avatarUrl,
 | 
				
			||||||
                name: name || canonicalAlias || aliases[0] || _t('Unnamed Room'),
 | 
					                name: name || canonicalAlias || aliases[0] || _t('Unnamed Room'),
 | 
				
			||||||
            });
 | 
					            });
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
        this._processResults(results, query);
 | 
					
 | 
				
			||||||
 | 
					        // Sort by rank ascending (a high rank being less relevant)
 | 
				
			||||||
 | 
					        const sortedResults = results.sort((a, b) => {
 | 
				
			||||||
 | 
					            return a.rank - b.rank;
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        this._processResults(sortedResults, query);
 | 
				
			||||||
        this.setState({
 | 
					        this.setState({
 | 
				
			||||||
            busy: false,
 | 
					            busy: false,
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
| 
						 | 
					@ -574,6 +595,7 @@ module.exports = React.createClass({
 | 
				
			||||||
                    <div className="mx_ChatInviteDialog_inputContainer">{ query }</div>
 | 
					                    <div className="mx_ChatInviteDialog_inputContainer">{ query }</div>
 | 
				
			||||||
                    { error }
 | 
					                    { error }
 | 
				
			||||||
                    { addressSelector }
 | 
					                    { addressSelector }
 | 
				
			||||||
 | 
					                    { this.props.extraNode }
 | 
				
			||||||
                </div>
 | 
					                </div>
 | 
				
			||||||
                <div className="mx_Dialog_buttons">
 | 
					                <div className="mx_Dialog_buttons">
 | 
				
			||||||
                    <button className="mx_Dialog_primary" onClick={this.onButtonClick}>
 | 
					                    <button className="mx_Dialog_primary" onClick={this.onButtonClick}>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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.isPrivileged ? EntityTile.POWER_STATUS_ADMIN : null}
 | 
				
			||||||
            />
 | 
					            />
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -94,7 +94,7 @@ export default React.createClass({
 | 
				
			||||||
        let roomList = this.state.rooms;
 | 
					        let roomList = this.state.rooms;
 | 
				
			||||||
        if (query) {
 | 
					        if (query) {
 | 
				
			||||||
            roomList = roomList.filter((room) => {
 | 
					            roomList = roomList.filter((room) => {
 | 
				
			||||||
                const matchesName = (room.name || "").toLowerCase().include(query);
 | 
					                const matchesName = (room.name || "").toLowerCase().includes(query);
 | 
				
			||||||
                const matchesAlias = (room.canonicalAlias || "").toLowerCase().includes(query);
 | 
					                const matchesAlias = (room.canonicalAlias || "").toLowerCase().includes(query);
 | 
				
			||||||
                return matchesName || matchesAlias;
 | 
					                return matchesName || matchesAlias;
 | 
				
			||||||
            });
 | 
					            });
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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 = !UserSettingsStore.getSyncedSetting("Pill.shouldHidePillAvatar", false);
 | 
					        const shouldShowPillAvatar = !UserSettingsStore.getSyncedSetting("Pill.shouldHidePillAvatar", false);
 | 
				
			||||||
        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 && UserSettingsStore.isFeatureEnabled('feature_pinning')) {
 | 
					        if (this.props.onPinnedClick && UserSettingsStore.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,
 | 
				
			||||||
 | 
					        isPrivileged: apiObject.is_privileged,
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -49,7 +49,7 @@
 | 
				
			||||||
    "Name or matrix ID": "Name or matrix ID",
 | 
					    "Name or matrix ID": "Name or matrix ID",
 | 
				
			||||||
    "Invite to Community": "Invite to Community",
 | 
					    "Invite to Community": "Invite to Community",
 | 
				
			||||||
    "Which rooms would you like to add to this community?": "Which rooms would you like to add to this community?",
 | 
					    "Which rooms would you like to add to this community?": "Which rooms would you like to add to this community?",
 | 
				
			||||||
    "Warning: any room you add to a community will be publicly visible to anyone who knows the community ID": "Warning: any room you add to a community will be publicly visible to anyone who knows the community ID",
 | 
					    "Show these rooms to non-members on the community page and room list?": "Show these rooms to non-members on the community page and room list?",
 | 
				
			||||||
    "Add rooms to the community": "Add rooms to the community",
 | 
					    "Add rooms to the community": "Add rooms to the community",
 | 
				
			||||||
    "Room name or alias": "Room name or alias",
 | 
					    "Room name or alias": "Room name or alias",
 | 
				
			||||||
    "Add to community": "Add to community",
 | 
					    "Add to community": "Add to community",
 | 
				
			||||||
| 
						 | 
					@ -673,6 +673,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",
 | 
				
			||||||
| 
						 | 
					@ -695,6 +696,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:",
 | 
				
			||||||
| 
						 | 
					@ -703,6 +705,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",
 | 
				
			||||||
| 
						 | 
					@ -893,6 +896,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",
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -69,9 +69,13 @@ class FlairStore extends EventEmitter {
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // Bulk lookup ongoing, return promise to resolve/reject
 | 
					        // Bulk lookup ongoing, return promise to resolve/reject
 | 
				
			||||||
        if (this._usersPending[userId] || this._usersInFlight[userId]) {
 | 
					        if (this._usersPending[userId]) {
 | 
				
			||||||
            return this._usersPending[userId].prom;
 | 
					            return this._usersPending[userId].prom;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					        // User has been moved from pending to in-flight
 | 
				
			||||||
 | 
					        if (this._usersInFlight[userId]) {
 | 
				
			||||||
 | 
					            return this._usersInFlight[userId].prom;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        this._usersPending[userId] = {};
 | 
					        this._usersPending[userId] = {};
 | 
				
			||||||
        this._usersPending[userId].prom = new Promise((resolve, reject) => {
 | 
					        this._usersPending[userId].prom = new Promise((resolve, reject) => {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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 = {};
 | 
				
			||||||
| 
						 | 
					@ -166,6 +169,12 @@ export default class GroupStore extends EventEmitter {
 | 
				
			||||||
            .then(this._fetchMembers.bind(this));
 | 
					            .then(this._fetchMembers.bind(this));
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    acceptGroupInvite() {
 | 
				
			||||||
 | 
					        return this._matrixClient.acceptGroupInvite(this.groupId)
 | 
				
			||||||
 | 
					            // The user might be able to see more rooms now
 | 
				
			||||||
 | 
					            .then(this._fetchRooms.bind(this));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    addRoomToGroupSummary(roomId, categoryId) {
 | 
					    addRoomToGroupSummary(roomId, categoryId) {
 | 
				
			||||||
        return this._matrixClient
 | 
					        return this._matrixClient
 | 
				
			||||||
            .addRoomToGroupSummary(this.groupId, roomId, categoryId)
 | 
					            .addRoomToGroupSummary(this.groupId, roomId, categoryId)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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 New Issue