mirror of
https://github.com/element-hq/element-web
synced 2024-11-24 02:05:45 +03:00
Merge branch 'develop' of github.com:matrix-org/matrix-react-sdk into t3chguy/fix_hide_joins_parts
This commit is contained in:
commit
4c4c9506ca
34 changed files with 1150 additions and 415 deletions
|
@ -1,3 +1,9 @@
|
|||
Changes in [0.11.4](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.11.4) (2018-02-09)
|
||||
=====================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.11.3...v0.11.4)
|
||||
|
||||
* Add isUrlPermitted function to sanity check URLs
|
||||
|
||||
Changes in [0.11.3](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.11.3) (2017-12-04)
|
||||
=====================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.11.2...v0.11.3)
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "matrix-react-sdk",
|
||||
"version": "0.11.3",
|
||||
"version": "0.11.4",
|
||||
"description": "SDK for matrix.org using React",
|
||||
"author": "matrix.org",
|
||||
"repository": {
|
||||
|
|
|
@ -50,11 +50,15 @@ function pad(n) {
|
|||
return (n < 10 ? '0' : '') + n;
|
||||
}
|
||||
|
||||
function twelveHourTime(date) {
|
||||
function twelveHourTime(date, showSeconds=false) {
|
||||
let hours = date.getHours() % 12;
|
||||
const minutes = pad(date.getMinutes());
|
||||
const ampm = date.getHours() >= 12 ? _t('PM') : _t('AM');
|
||||
hours = hours ? hours : 12; // convert 0 -> 12
|
||||
if (showSeconds) {
|
||||
const seconds = pad(date.getSeconds());
|
||||
return `${hours}:${minutes}:${seconds}${ampm}`;
|
||||
}
|
||||
return `${hours}:${minutes}${ampm}`;
|
||||
}
|
||||
|
||||
|
@ -101,10 +105,17 @@ export function formatFullDate(date, showTwelveHour=false) {
|
|||
monthName: months[date.getMonth()],
|
||||
day: date.getDate(),
|
||||
fullYear: date.getFullYear(),
|
||||
time: formatTime(date, showTwelveHour),
|
||||
time: formatFullTime(date, showTwelveHour),
|
||||
});
|
||||
}
|
||||
|
||||
export function formatFullTime(date, showTwelveHour=false) {
|
||||
if (showTwelveHour) {
|
||||
return twelveHourTime(date, true);
|
||||
}
|
||||
return pad(date.getHours()) + ':' + pad(date.getMinutes()) + ':' + pad(date.getSeconds());
|
||||
}
|
||||
|
||||
export function formatTime(date, showTwelveHour=false) {
|
||||
if (showTwelveHour) {
|
||||
return twelveHourTime(date);
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 New Vector Ltd
|
||||
Copyright 2017, 2018 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.
|
||||
|
@ -25,6 +25,7 @@ import escape from 'lodash/escape';
|
|||
import emojione from 'emojione';
|
||||
import classNames from 'classnames';
|
||||
import MatrixClientPeg from './MatrixClientPeg';
|
||||
import url from 'url';
|
||||
|
||||
emojione.imagePathSVG = 'emojione/svg/';
|
||||
// Store PNG path for displaying many flags at once (for increased performance over SVG)
|
||||
|
@ -44,6 +45,8 @@ const SYMBOL_PATTERN = /([\u2100-\u2bff])/;
|
|||
const EMOJI_REGEX = new RegExp(emojione.unicodeRegexp+"+", "gi");
|
||||
const COLOR_REGEX = /^#[0-9a-fA-F]{6}$/;
|
||||
|
||||
const PERMITTED_URL_SCHEMES = ['http', 'https', 'ftp', 'mailto', 'magnet'];
|
||||
|
||||
/*
|
||||
* Return true if the given string contains emoji
|
||||
* Uses a much, much simpler regex than emojione's so will give false
|
||||
|
@ -152,6 +155,25 @@ export function sanitizedHtmlNode(insaneHtml) {
|
|||
return <div dangerouslySetInnerHTML={{ __html: saneHtml }} dir="auto" />;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests if a URL from an untrusted source may be safely put into the DOM
|
||||
* The biggest threat here is javascript: URIs.
|
||||
* Note that the HTML sanitiser library has its own internal logic for
|
||||
* doing this, to which we pass the same list of schemes. This is used in
|
||||
* other places we need to sanitise URLs.
|
||||
* @return true if permitted, otherwise false
|
||||
*/
|
||||
export function isUrlPermitted(inputUrl) {
|
||||
try {
|
||||
const parsed = url.parse(inputUrl);
|
||||
if (!parsed.protocol) return false;
|
||||
// URL parser protocol includes the trailing colon
|
||||
return PERMITTED_URL_SCHEMES.includes(parsed.protocol.slice(0, -1));
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const sanitizeHtmlParams = {
|
||||
allowedTags: [
|
||||
'font', // custom to matrix for IRC-style font coloring
|
||||
|
@ -172,7 +194,7 @@ const sanitizeHtmlParams = {
|
|||
// Lots of these won't come up by default because we don't allow them
|
||||
selfClosing: ['img', 'br', 'hr', 'area', 'base', 'basefont', 'input', 'link', 'meta'],
|
||||
// URL schemes we permit
|
||||
allowedSchemes: ['http', 'https', 'ftp', 'mailto', 'magnet'],
|
||||
allowedSchemes: PERMITTED_URL_SCHEMES,
|
||||
|
||||
allowProtocolRelative: false,
|
||||
|
||||
|
|
|
@ -563,7 +563,7 @@ const onMessage = function(event) {
|
|||
const url = SdkConfig.get().integrations_ui_url;
|
||||
if (
|
||||
event.origin.length === 0 ||
|
||||
!url.startsWith(event.origin) ||
|
||||
!url.startsWith(event.origin + '/') ||
|
||||
!event.data.action ||
|
||||
event.data.api // Ignore messages with specific API set
|
||||
) {
|
||||
|
|
|
@ -28,6 +28,8 @@ module.exports = {
|
|||
return false;
|
||||
} else if (ev.getType() == 'm.room.member') {
|
||||
return false;
|
||||
} else if (ev.getType() == 'm.room.third_party_invite') {
|
||||
return false;
|
||||
} else if (ev.getType() == 'm.call.answer' || ev.getType() == 'm.call.hangup') {
|
||||
return false;
|
||||
} else if (ev.getType == 'm.room.message' && ev.getContent().msgtype == 'm.notify') {
|
||||
|
|
|
@ -62,6 +62,107 @@ function createAccountDataAction(matrixClient, accountDataEvent) {
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef RoomAction
|
||||
* @type {Object}
|
||||
* @property {string} action 'MatrixActions.Room'.
|
||||
* @property {Room} room the Room that was stored.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Create a MatrixActions.Room action that represents a MatrixClient `Room`
|
||||
* matrix event, emitted when a Room is stored in the client.
|
||||
*
|
||||
* @param {MatrixClient} matrixClient the matrix client.
|
||||
* @param {Room} room the Room that was stored.
|
||||
* @returns {RoomAction} an action of type `MatrixActions.Room`.
|
||||
*/
|
||||
function createRoomAction(matrixClient, room) {
|
||||
return { action: 'MatrixActions.Room', room };
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef RoomTagsAction
|
||||
* @type {Object}
|
||||
* @property {string} action 'MatrixActions.Room.tags'.
|
||||
* @property {Room} room the Room whose tags changed.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Create a MatrixActions.Room.tags action that represents a MatrixClient
|
||||
* `Room.tags` matrix event, emitted when the m.tag room account data
|
||||
* event is updated.
|
||||
*
|
||||
* @param {MatrixClient} matrixClient the matrix client.
|
||||
* @param {MatrixEvent} roomTagsEvent the m.tag event.
|
||||
* @param {Room} room the Room whose tags were changed.
|
||||
* @returns {RoomTagsAction} an action of type `MatrixActions.Room.tags`.
|
||||
*/
|
||||
function createRoomTagsAction(matrixClient, roomTagsEvent, room) {
|
||||
return { action: 'MatrixActions.Room.tags', room };
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef RoomTimelineAction
|
||||
* @type {Object}
|
||||
* @property {string} action 'MatrixActions.Room.timeline'.
|
||||
* @property {boolean} isLiveEvent whether the event was attached to a
|
||||
* live timeline.
|
||||
* @property {boolean} isLiveUnfilteredRoomTimelineEvent whether the
|
||||
* event was attached to a timeline in the set of unfiltered timelines.
|
||||
* @property {Room} room the Room whose tags changed.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Create a MatrixActions.Room.timeline action that represents a
|
||||
* MatrixClient `Room.timeline` matrix event, emitted when an event
|
||||
* is added to or removed from a timeline of a room.
|
||||
*
|
||||
* @param {MatrixClient} matrixClient the matrix client.
|
||||
* @param {MatrixEvent} timelineEvent the event that was added/removed.
|
||||
* @param {Room} room the Room that was stored.
|
||||
* @param {boolean} toStartOfTimeline whether the event is being added
|
||||
* to the start (and not the end) of the timeline.
|
||||
* @param {boolean} removed whether the event was removed from the
|
||||
* timeline.
|
||||
* @param {Object} data
|
||||
* @param {boolean} data.liveEvent whether the event is a live event,
|
||||
* belonging to a live timeline.
|
||||
* @param {EventTimeline} data.timeline the timeline being altered.
|
||||
* @returns {RoomTimelineAction} an action of type `MatrixActions.Room.timeline`.
|
||||
*/
|
||||
function createRoomTimelineAction(matrixClient, timelineEvent, room, toStartOfTimeline, removed, data) {
|
||||
return {
|
||||
action: 'MatrixActions.Room.timeline',
|
||||
event: timelineEvent,
|
||||
isLiveEvent: data.liveEvent,
|
||||
isLiveUnfilteredRoomTimelineEvent:
|
||||
room && data.timeline.getTimelineSet() === room.getUnfilteredTimelineSet(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef RoomMembershipAction
|
||||
* @type {Object}
|
||||
* @property {string} action 'MatrixActions.RoomMember.membership'.
|
||||
* @property {RoomMember} member the member whose membership was updated.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Create a MatrixActions.RoomMember.membership action that represents
|
||||
* a MatrixClient `RoomMember.membership` matrix event, emitted when a
|
||||
* member's membership is updated.
|
||||
*
|
||||
* @param {MatrixClient} matrixClient the matrix client.
|
||||
* @param {MatrixEvent} membershipEvent the m.room.member event.
|
||||
* @param {RoomMember} member the member whose membership was updated.
|
||||
* @param {string} oldMembership the member's previous membership.
|
||||
* @returns {RoomMembershipAction} an action of type `MatrixActions.RoomMember.membership`.
|
||||
*/
|
||||
function createRoomMembershipAction(matrixClient, membershipEvent, member, oldMembership) {
|
||||
return { action: 'MatrixActions.RoomMember.membership', member };
|
||||
}
|
||||
|
||||
/**
|
||||
* This object is responsible for dispatching actions when certain events are emitted by
|
||||
* the given MatrixClient.
|
||||
|
@ -78,6 +179,10 @@ export default {
|
|||
start(matrixClient) {
|
||||
this._addMatrixClientListener(matrixClient, 'sync', createSyncAction);
|
||||
this._addMatrixClientListener(matrixClient, 'accountData', createAccountDataAction);
|
||||
this._addMatrixClientListener(matrixClient, 'Room', createRoomAction);
|
||||
this._addMatrixClientListener(matrixClient, 'Room.tags', createRoomTagsAction);
|
||||
this._addMatrixClientListener(matrixClient, 'Room.timeline', createRoomTimelineAction);
|
||||
this._addMatrixClientListener(matrixClient, 'RoomMember.membership', createRoomMembershipAction);
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -91,7 +196,7 @@ export default {
|
|||
*/
|
||||
_addMatrixClientListener(matrixClient, eventName, actionCreator) {
|
||||
const listener = (...args) => {
|
||||
dis.dispatch(actionCreator(matrixClient, ...args));
|
||||
dis.dispatch(actionCreator(matrixClient, ...args), true);
|
||||
};
|
||||
matrixClient.on(eventName, listener);
|
||||
this._matrixClientListenersStop.push(() => {
|
||||
|
|
146
src/actions/RoomListActions.js
Normal file
146
src/actions/RoomListActions.js
Normal file
|
@ -0,0 +1,146 @@
|
|||
/*
|
||||
Copyright 2018 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 { asyncAction } from './actionCreators';
|
||||
import RoomListStore from '../stores/RoomListStore';
|
||||
|
||||
import Modal from '../Modal';
|
||||
import Rooms from '../Rooms';
|
||||
import { _t } from '../languageHandler';
|
||||
import sdk from '../index';
|
||||
|
||||
const RoomListActions = {};
|
||||
|
||||
/**
|
||||
* Creates an action thunk that will do an asynchronous request to
|
||||
* tag room.
|
||||
*
|
||||
* @param {MatrixClient} matrixClient the matrix client to set the
|
||||
* account data on.
|
||||
* @param {Room} room the room to tag.
|
||||
* @param {string} oldTag the tag to remove (unless oldTag ==== newTag)
|
||||
* @param {string} newTag the tag with which to tag the room.
|
||||
* @param {?number} oldIndex the previous position of the room in the
|
||||
* list of rooms.
|
||||
* @param {?number} newIndex the new position of the room in the list
|
||||
* of rooms.
|
||||
* @returns {function} an action thunk.
|
||||
* @see asyncAction
|
||||
*/
|
||||
RoomListActions.tagRoom = function(matrixClient, room, oldTag, newTag, oldIndex, newIndex) {
|
||||
let metaData = null;
|
||||
|
||||
// Is the tag ordered manually?
|
||||
if (newTag && !newTag.match(/^(m\.lowpriority|im\.vector\.fake\.(invite|recent|direct|archived))$/)) {
|
||||
const lists = RoomListStore.getRoomLists();
|
||||
const newList = [...lists[newTag]];
|
||||
|
||||
newList.sort((a, b) => a.tags[newTag].order - b.tags[newTag].order);
|
||||
|
||||
// If the room was moved "down" (increasing index) in the same list we
|
||||
// need to use the orders of the tiles with indices shifted by +1
|
||||
const offset = (
|
||||
newTag === oldTag && oldIndex < newIndex
|
||||
) ? 1 : 0;
|
||||
|
||||
const indexBefore = offset + newIndex - 1;
|
||||
const indexAfter = offset + newIndex;
|
||||
|
||||
const prevOrder = indexBefore <= 0 ?
|
||||
0 : newList[indexBefore].tags[newTag].order;
|
||||
const nextOrder = indexAfter >= newList.length ?
|
||||
1 : newList[indexAfter].tags[newTag].order;
|
||||
|
||||
metaData = {
|
||||
order: (prevOrder + nextOrder) / 2.0,
|
||||
};
|
||||
}
|
||||
|
||||
return asyncAction('RoomListActions.tagRoom', () => {
|
||||
const promises = [];
|
||||
const roomId = room.roomId;
|
||||
|
||||
// Evil hack to get DMs behaving
|
||||
if ((oldTag === undefined && newTag === 'im.vector.fake.direct') ||
|
||||
(oldTag === 'im.vector.fake.direct' && newTag === undefined)
|
||||
) {
|
||||
return Rooms.guessAndSetDMRoom(
|
||||
room, newTag === 'im.vector.fake.direct',
|
||||
).catch((err) => {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
console.error("Failed to set direct chat tag " + err);
|
||||
Modal.createTrackedDialog('Failed to set direct chat tag', '', ErrorDialog, {
|
||||
title: _t('Failed to set direct chat tag'),
|
||||
description: ((err && err.message) ? err.message : _t('Operation failed')),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const hasChangedSubLists = oldTag !== newTag;
|
||||
|
||||
// More evilness: We will still be dealing with moving to favourites/low prio,
|
||||
// but we avoid ever doing a request with 'im.vector.fake.direct`.
|
||||
//
|
||||
// if we moved lists, remove the old tag
|
||||
if (oldTag && oldTag !== 'im.vector.fake.direct' &&
|
||||
hasChangedSubLists
|
||||
) {
|
||||
const promiseToDelete = matrixClient.deleteRoomTag(
|
||||
roomId, oldTag,
|
||||
).catch(function(err) {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
console.error("Failed to remove tag " + oldTag + " from room: " + err);
|
||||
Modal.createTrackedDialog('Failed to remove tag from room', '', ErrorDialog, {
|
||||
title: _t('Failed to remove tag %(tagName)s from room', {tagName: oldTag}),
|
||||
description: ((err && err.message) ? err.message : _t('Operation failed')),
|
||||
});
|
||||
});
|
||||
|
||||
promises.push(promiseToDelete);
|
||||
}
|
||||
|
||||
// if we moved lists or the ordering changed, add the new tag
|
||||
if (newTag && newTag !== 'im.vector.fake.direct' &&
|
||||
(hasChangedSubLists || metaData)
|
||||
) {
|
||||
// metaData is the body of the PUT to set the tag, so it must
|
||||
// at least be an empty object.
|
||||
metaData = metaData || {};
|
||||
|
||||
const promiseToAdd = matrixClient.setRoomTag(roomId, newTag, metaData).catch(function(err) {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
console.error("Failed to add tag " + newTag + " to room: " + err);
|
||||
Modal.createTrackedDialog('Failed to add tag to room', '', ErrorDialog, {
|
||||
title: _t('Failed to add tag %(tagName)s to room', {tagName: newTag}),
|
||||
description: ((err && err.message) ? err.message : _t('Operation failed')),
|
||||
});
|
||||
|
||||
throw err;
|
||||
});
|
||||
|
||||
promises.push(promiseToAdd);
|
||||
}
|
||||
|
||||
return Promise.all(promises);
|
||||
}, () => {
|
||||
// For an optimistic update
|
||||
return {
|
||||
room, oldTag, newTag, metaData,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
export default RoomListActions;
|
|
@ -35,6 +35,7 @@ const TagOrderActions = {};
|
|||
TagOrderActions.moveTag = function(matrixClient, tag, destinationIx) {
|
||||
// Only commit tags if the state is ready, i.e. not null
|
||||
let tags = TagOrderStore.getOrderedTags();
|
||||
let removedTags = TagOrderStore.getRemovedTagsAccountData() || [];
|
||||
if (!tags) {
|
||||
return;
|
||||
}
|
||||
|
@ -42,17 +43,66 @@ TagOrderActions.moveTag = function(matrixClient, tag, destinationIx) {
|
|||
tags = tags.filter((t) => t !== tag);
|
||||
tags = [...tags.slice(0, destinationIx), tag, ...tags.slice(destinationIx)];
|
||||
|
||||
removedTags = removedTags.filter((t) => t !== tag);
|
||||
|
||||
const storeId = TagOrderStore.getStoreId();
|
||||
|
||||
return asyncAction('TagOrderActions.moveTag', () => {
|
||||
Analytics.trackEvent('TagOrderActions', 'commitTagOrdering');
|
||||
return matrixClient.setAccountData(
|
||||
'im.vector.web.tag_ordering',
|
||||
{tags, _storeId: storeId},
|
||||
{tags, removedTags, _storeId: storeId},
|
||||
);
|
||||
}, () => {
|
||||
// For an optimistic update
|
||||
return {tags};
|
||||
return {tags, removedTags};
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates an action thunk that will do an asynchronous request to
|
||||
* label a tag as removed in im.vector.web.tag_ordering account data.
|
||||
*
|
||||
* The reason this is implemented with new state `removedTags` is that
|
||||
* we incrementally and initially populate `tags` with groups that
|
||||
* have been joined. If we remove a group from `tags`, it will just
|
||||
* get added (as it looks like a group we've recently joined).
|
||||
*
|
||||
* NB: If we ever support adding of tags (which is planned), we should
|
||||
* take special care to remove the tag from `removedTags` when we add
|
||||
* it.
|
||||
*
|
||||
* @param {MatrixClient} matrixClient the matrix client to set the
|
||||
* account data on.
|
||||
* @param {string} tag the tag to remove.
|
||||
* @returns {function} an action thunk that will dispatch actions
|
||||
* indicating the status of the request.
|
||||
* @see asyncAction
|
||||
*/
|
||||
TagOrderActions.removeTag = function(matrixClient, tag) {
|
||||
// Don't change tags, just removedTags
|
||||
const tags = TagOrderStore.getOrderedTags();
|
||||
const removedTags = TagOrderStore.getRemovedTagsAccountData() || [];
|
||||
|
||||
if (removedTags.includes(tag)) {
|
||||
// Return a thunk that doesn't do anything, we don't even need
|
||||
// an asynchronous action here, the tag is already removed.
|
||||
return () => {};
|
||||
}
|
||||
|
||||
removedTags.push(tag);
|
||||
|
||||
const storeId = TagOrderStore.getStoreId();
|
||||
|
||||
return asyncAction('TagOrderActions.removeTag', () => {
|
||||
Analytics.trackEvent('TagOrderActions', 'removeTag');
|
||||
return matrixClient.setAccountData(
|
||||
'im.vector.web.tag_ordering',
|
||||
{tags, removedTags, _storeId: storeId},
|
||||
);
|
||||
}, () => {
|
||||
// For an optimistic update
|
||||
return {removedTags};
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -31,6 +31,15 @@ limitations under the License.
|
|||
* `${id}.pending` and either
|
||||
* `${id}.success` or
|
||||
* `${id}.failure`.
|
||||
*
|
||||
* The shape of each are:
|
||||
* { action: '${id}.pending', request }
|
||||
* { action: '${id}.success', result }
|
||||
* { action: '${id}.failure', err }
|
||||
*
|
||||
* where `request` is returned by `pendingFn` and
|
||||
* result is the result of the promise returned by
|
||||
* `fn`.
|
||||
*/
|
||||
export function asyncAction(id, fn, pendingFn) {
|
||||
return (dispatch) => {
|
||||
|
|
|
@ -158,7 +158,7 @@ export default class UserProvider extends AutocompleteProvider {
|
|||
}
|
||||
|
||||
renderCompletions(completions: [React.Component]): ?React.Component {
|
||||
return <div className="mx_Autocomplete_Completion_container_pill mx_Autocomplete_Completion_container_truncate">
|
||||
return <div className="mx_Autocomplete_Completion_container_pill">
|
||||
{ completions }
|
||||
</div>;
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ limitations under the License.
|
|||
import * as Matrix from 'matrix-js-sdk';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { DragDropContext } from 'react-beautiful-dnd';
|
||||
|
||||
import { KeyCode, isOnlyCtrlOrCmdKeyEvent } from '../../Keyboard';
|
||||
import Notifier from '../../Notifier';
|
||||
|
@ -30,6 +31,9 @@ import sessionStore from '../../stores/SessionStore';
|
|||
import MatrixClientPeg from '../../MatrixClientPeg';
|
||||
import SettingsStore from "../../settings/SettingsStore";
|
||||
|
||||
import TagOrderActions from '../../actions/TagOrderActions';
|
||||
import RoomListActions from '../../actions/RoomListActions';
|
||||
|
||||
/**
|
||||
* This is what our MatrixChat shows when we are logged in. The precise view is
|
||||
* determined by the page_type property.
|
||||
|
@ -207,8 +211,51 @@ const LoggedInView = React.createClass({
|
|||
}
|
||||
},
|
||||
|
||||
_onDragEnd: function(result) {
|
||||
// Dragged to an invalid destination, not onto a droppable
|
||||
if (!result.destination) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dest = result.destination.droppableId;
|
||||
|
||||
if (dest === 'tag-panel-droppable') {
|
||||
// Could be "GroupTile +groupId:domain"
|
||||
const draggableId = result.draggableId.split(' ').pop();
|
||||
|
||||
// Dispatch synchronously so that the TagPanel receives an
|
||||
// optimistic update from TagOrderStore before the previous
|
||||
// state is shown.
|
||||
dis.dispatch(TagOrderActions.moveTag(
|
||||
this._matrixClient,
|
||||
draggableId,
|
||||
result.destination.index,
|
||||
), true);
|
||||
} else if (dest.startsWith('room-sub-list-droppable_')) {
|
||||
this._onRoomTileEndDrag(result);
|
||||
}
|
||||
},
|
||||
|
||||
_onRoomTileEndDrag: function(result) {
|
||||
let newTag = result.destination.droppableId.split('_')[1];
|
||||
let prevTag = result.source.droppableId.split('_')[1];
|
||||
if (newTag === 'undefined') newTag = undefined;
|
||||
if (prevTag === 'undefined') prevTag = undefined;
|
||||
|
||||
const roomId = result.draggableId.split('_')[1];
|
||||
|
||||
const oldIndex = result.source.index;
|
||||
const newIndex = result.destination.index;
|
||||
|
||||
dis.dispatch(RoomListActions.tagRoom(
|
||||
this._matrixClient,
|
||||
this._matrixClient.getRoom(roomId),
|
||||
prevTag, newTag,
|
||||
oldIndex, newIndex,
|
||||
), true);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const TagPanel = sdk.getComponent('structures.TagPanel');
|
||||
const LeftPanel = sdk.getComponent('structures.LeftPanel');
|
||||
const RightPanel = sdk.getComponent('structures.RightPanel');
|
||||
const RoomView = sdk.getComponent('structures.RoomView');
|
||||
|
@ -329,17 +376,18 @@ const LoggedInView = React.createClass({
|
|||
return (
|
||||
<div className='mx_MatrixChat_wrapper'>
|
||||
{ topBar }
|
||||
<div className={bodyClasses}>
|
||||
{ SettingsStore.isFeatureEnabled("feature_tag_panel") ? <TagPanel /> : <div /> }
|
||||
<LeftPanel
|
||||
collapsed={this.props.collapseLhs || false}
|
||||
disabled={this.props.leftDisabled}
|
||||
/>
|
||||
<main className='mx_MatrixChat_middlePanel'>
|
||||
{ page_element }
|
||||
</main>
|
||||
{ right_panel }
|
||||
</div>
|
||||
<DragDropContext onDragEnd={this._onDragEnd}>
|
||||
<div className={bodyClasses}>
|
||||
<LeftPanel
|
||||
collapsed={this.props.collapseLhs || false}
|
||||
disabled={this.props.leftDisabled}
|
||||
/>
|
||||
<main className='mx_MatrixChat_middlePanel'>
|
||||
{ page_element }
|
||||
</main>
|
||||
{ right_panel }
|
||||
</div>
|
||||
</DragDropContext>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -618,18 +618,26 @@ export default React.createClass({
|
|||
},
|
||||
|
||||
_startRegistration: function(params) {
|
||||
this.setStateForNewView({
|
||||
const newState = {
|
||||
view: VIEWS.REGISTER,
|
||||
// these params may be undefined, but if they are,
|
||||
// unset them from our state: we don't want to
|
||||
// resume a previous registration session if the
|
||||
// user just clicked 'register'
|
||||
register_client_secret: params.client_secret,
|
||||
register_session_id: params.session_id,
|
||||
register_hs_url: params.hs_url,
|
||||
register_is_url: params.is_url,
|
||||
register_id_sid: params.sid,
|
||||
});
|
||||
};
|
||||
|
||||
// Only honour params if they are all present, otherwise we reset
|
||||
// HS and IS URLs when switching to registration.
|
||||
if (params.client_secret &&
|
||||
params.session_id &&
|
||||
params.hs_url &&
|
||||
params.is_url &&
|
||||
params.sid
|
||||
) {
|
||||
newState.register_client_secret = params.client_secret;
|
||||
newState.register_session_id = params.session_id;
|
||||
newState.register_hs_url = params.hs_url;
|
||||
newState.register_is_url = params.is_url;
|
||||
newState.register_id_sid = params.sid;
|
||||
}
|
||||
|
||||
this.setStateForNewView(newState);
|
||||
this.notifyNewScreen('register');
|
||||
},
|
||||
|
||||
|
@ -1501,6 +1509,17 @@ export default React.createClass({
|
|||
}
|
||||
},
|
||||
|
||||
onServerConfigChange(config) {
|
||||
const newState = {};
|
||||
if (config.hsUrl) {
|
||||
newState.register_hs_url = config.hsUrl;
|
||||
}
|
||||
if (config.isUrl) {
|
||||
newState.register_is_url = config.isUrl;
|
||||
}
|
||||
this.setState(newState);
|
||||
},
|
||||
|
||||
_makeRegistrationUrl: function(params) {
|
||||
if (this.props.startingFragmentQueryParams.referrer) {
|
||||
params.referrer = this.props.startingFragmentQueryParams.referrer;
|
||||
|
@ -1589,6 +1608,7 @@ export default React.createClass({
|
|||
onLoginClick={this.onLoginClick}
|
||||
onRegisterClick={this.onRegisterClick}
|
||||
onCancelClick={MatrixClientPeg.get() ? this.onReturnToAppClick : null}
|
||||
onServerConfigChange={this.onServerConfigChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -1623,6 +1643,7 @@ export default React.createClass({
|
|||
onForgotPasswordClick={this.onForgotPasswordClick}
|
||||
enableGuest={this.props.enableGuest}
|
||||
onCancelClick={MatrixClientPeg.get() ? this.onReturnToAppClick : null}
|
||||
onServerConfigChange={this.onServerConfigChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -73,8 +73,10 @@ export default withMatrixClient(React.createClass({
|
|||
});
|
||||
contentHeader = groupNodes.length > 0 ? <h3>{ _t('Your Communities') }</h3> : <div />;
|
||||
content = groupNodes.length > 0 ?
|
||||
<GeminiScrollbar className="mx_MyGroups_joinedGroups">
|
||||
{ groupNodes }
|
||||
<GeminiScrollbar>
|
||||
<div className="mx_MyGroups_joinedGroups">
|
||||
{ groupNodes }
|
||||
</div>
|
||||
</GeminiScrollbar> :
|
||||
<div className="mx_MyGroups_placeholder">
|
||||
{ _t(
|
||||
|
|
|
@ -264,12 +264,19 @@ module.exports = React.createClass({
|
|||
isPeeking: true, // this will change to false if peeking fails
|
||||
});
|
||||
MatrixClientPeg.get().peekInRoom(roomId).then((room) => {
|
||||
if (this.unmounted) {
|
||||
return;
|
||||
}
|
||||
this.setState({
|
||||
room: room,
|
||||
peekLoading: false,
|
||||
});
|
||||
this._onRoomLoaded(room);
|
||||
}, (err) => {
|
||||
if (this.unmounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Stop peeking if anything went wrong
|
||||
this.setState({
|
||||
isPeeking: false,
|
||||
|
@ -286,7 +293,7 @@ module.exports = React.createClass({
|
|||
} else {
|
||||
throw err;
|
||||
}
|
||||
}).done();
|
||||
});
|
||||
}
|
||||
} else if (room) {
|
||||
// Stop peeking because we have joined this room previously
|
||||
|
@ -620,8 +627,8 @@ module.exports = React.createClass({
|
|||
const room = this.state.room;
|
||||
if (!room) return;
|
||||
|
||||
const color_scheme = SettingsStore.getValue("roomColor", room.room_id);
|
||||
console.log("Tinter.tint from updateTint");
|
||||
const color_scheme = SettingsStore.getValue("roomColor", room.roomId);
|
||||
Tinter.tint(color_scheme.primary_color, color_scheme.secondary_color);
|
||||
},
|
||||
|
||||
|
@ -670,23 +677,7 @@ module.exports = React.createClass({
|
|||
// a member state changed in this room
|
||||
// refresh the conf call notification state
|
||||
this._updateConfCallNotification();
|
||||
|
||||
// if we are now a member of the room, where we were not before, that
|
||||
// means we have finished joining a room we were previously peeking
|
||||
// into.
|
||||
const me = MatrixClientPeg.get().credentials.userId;
|
||||
if (this.state.joining && this.state.room.hasMembershipState(me, "join")) {
|
||||
// Having just joined a room, check to see if it looks like a DM room, and if so,
|
||||
// mark it as one. This is to work around the fact that some clients don't support
|
||||
// is_direct. We should remove this once they do.
|
||||
const me = this.state.room.getMember(MatrixClientPeg.get().credentials.userId);
|
||||
if (Rooms.looksLikeDirectMessageRoom(this.state.room, me)) {
|
||||
// XXX: There's not a whole lot we can really do if this fails: at best
|
||||
// perhaps we could try a couple more times, but since it's a temporary
|
||||
// compatability workaround, let's not bother.
|
||||
Rooms.setDMRoom(this.state.room.roomId, me.events.member.getSender()).done();
|
||||
}
|
||||
}
|
||||
this._updateDMState();
|
||||
}, 500),
|
||||
|
||||
_checkIfAlone: function(room) {
|
||||
|
@ -727,6 +718,44 @@ module.exports = React.createClass({
|
|||
});
|
||||
},
|
||||
|
||||
_updateDMState() {
|
||||
const me = this.state.room.getMember(MatrixClientPeg.get().credentials.userId);
|
||||
if (!me || me.membership !== "join") {
|
||||
return;
|
||||
}
|
||||
|
||||
// The user may have accepted an invite with is_direct set
|
||||
if (me.events.member.getPrevContent().membership === "invite" &&
|
||||
me.events.member.getPrevContent().is_direct
|
||||
) {
|
||||
// This is a DM with the sender of the invite event (which we assume
|
||||
// preceded the join event)
|
||||
Rooms.setDMRoom(
|
||||
this.state.room.roomId,
|
||||
me.events.member.getUnsigned().prev_sender,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const invitedMembers = this.state.room.getMembersWithMembership("invite");
|
||||
const joinedMembers = this.state.room.getMembersWithMembership("join");
|
||||
|
||||
// There must be one invited member and one joined member
|
||||
if (invitedMembers.length !== 1 || joinedMembers.length !== 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
// The user may have sent an invite with is_direct sent
|
||||
const other = invitedMembers[0];
|
||||
if (other &&
|
||||
other.membership === "invite" &&
|
||||
other.events.member.getContent().is_direct
|
||||
) {
|
||||
Rooms.setDMRoom(this.state.room.roomId, other.userId);
|
||||
return;
|
||||
}
|
||||
},
|
||||
|
||||
onSearchResultsResize: function() {
|
||||
dis.dispatch({ action: 'timeline_resize' }, true);
|
||||
},
|
||||
|
@ -819,18 +848,6 @@ module.exports = React.createClass({
|
|||
action: 'join_room',
|
||||
opts: { inviteSignUrl: signUrl },
|
||||
});
|
||||
|
||||
// if this is an invite and has the 'direct' hint set, mark it as a DM room now.
|
||||
if (this.state.room) {
|
||||
const me = this.state.room.getMember(MatrixClientPeg.get().credentials.userId);
|
||||
if (me && me.membership == 'invite') {
|
||||
if (me.events.member.getContent().is_direct) {
|
||||
// The 'direct' hint is there, so declare that this is a DM room for
|
||||
// whoever invited us.
|
||||
return Rooms.setDMRoom(this.state.room.roomId, me.events.member.getSender());
|
||||
}
|
||||
}
|
||||
}
|
||||
return Promise.resolve();
|
||||
});
|
||||
},
|
||||
|
|
|
@ -17,15 +17,16 @@ limitations under the License.
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { MatrixClient } from 'matrix-js-sdk';
|
||||
import GeminiScrollbar from 'react-gemini-scrollbar';
|
||||
import TagOrderStore from '../../stores/TagOrderStore';
|
||||
|
||||
import GroupActions from '../../actions/GroupActions';
|
||||
import TagOrderActions from '../../actions/TagOrderActions';
|
||||
|
||||
import sdk from '../../index';
|
||||
import dis from '../../dispatcher';
|
||||
import { _t } from '../../languageHandler';
|
||||
|
||||
import { DragDropContext, Droppable } from 'react-beautiful-dnd';
|
||||
import { Droppable } from 'react-beautiful-dnd';
|
||||
|
||||
const TagPanel = React.createClass({
|
||||
displayName: 'TagPanel',
|
||||
|
@ -84,8 +85,6 @@ const TagPanel = React.createClass({
|
|||
},
|
||||
|
||||
onClick(e) {
|
||||
// Ignore clicks on children
|
||||
if (e.target !== e.currentTarget) return;
|
||||
dis.dispatch({action: 'deselect_tags'});
|
||||
},
|
||||
|
||||
|
@ -94,26 +93,14 @@ const TagPanel = React.createClass({
|
|||
dis.dispatch({action: 'view_create_group'});
|
||||
},
|
||||
|
||||
onTagTileEndDrag(result) {
|
||||
// Dragged to an invalid destination, not onto a droppable
|
||||
if (!result.destination) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Dispatch synchronously so that the TagPanel receives an
|
||||
// optimistic update from TagOrderStore before the previous
|
||||
// state is shown.
|
||||
dis.dispatch(TagOrderActions.moveTag(
|
||||
this.context.matrixClient,
|
||||
result.draggableId,
|
||||
result.destination.index,
|
||||
), true);
|
||||
onClearFilterClick(ev) {
|
||||
dis.dispatch({action: 'deselect_tags'});
|
||||
},
|
||||
|
||||
render() {
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
const TintableSvg = sdk.getComponent('elements.TintableSvg');
|
||||
const GroupsButton = sdk.getComponent('elements.GroupsButton');
|
||||
const DNDTagTile = sdk.getComponent('elements.DNDTagTile');
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
|
||||
const tags = this.state.orderedTags.map((tag, index) => {
|
||||
return <DNDTagTile
|
||||
|
@ -123,28 +110,45 @@ const TagPanel = React.createClass({
|
|||
selected={this.state.selectedTags.includes(tag)}
|
||||
/>;
|
||||
});
|
||||
|
||||
const clearButton = this.state.selectedTags.length > 0 ?
|
||||
<img
|
||||
src="img/icons-close.svg"
|
||||
alt={_t("Clear filter")}
|
||||
title={_t("Clear filter")}
|
||||
width="24"
|
||||
height="24" /> :
|
||||
<div />;
|
||||
|
||||
return <div className="mx_TagPanel">
|
||||
<DragDropContext onDragEnd={this.onTagTileEndDrag}>
|
||||
<Droppable droppableId="tag-panel-droppable">
|
||||
<AccessibleButton className="mx_TagPanel_clearButton" onClick={this.onClearFilterClick}>
|
||||
{ clearButton }
|
||||
</AccessibleButton>
|
||||
<div className="mx_TagPanel_divider" />
|
||||
<GeminiScrollbar
|
||||
className="mx_TagPanel_scroller"
|
||||
autoShow={true}
|
||||
onClick={this.onClick}
|
||||
>
|
||||
<Droppable
|
||||
droppableId="tag-panel-droppable"
|
||||
type="draggable-TagTile"
|
||||
>
|
||||
{ (provided, snapshot) => (
|
||||
<div
|
||||
className="mx_TagPanel_tagTileContainer"
|
||||
ref={provided.innerRef}
|
||||
// react-beautiful-dnd has a bug that emits a click to the parent
|
||||
// of draggables upon dropping
|
||||
// https://github.com/atlassian/react-beautiful-dnd/issues/273
|
||||
// so we use onMouseDown here as a workaround.
|
||||
onMouseDown={this.onClick}
|
||||
>
|
||||
{ tags }
|
||||
{ provided.placeholder }
|
||||
</div>
|
||||
<div
|
||||
className="mx_TagPanel_tagTileContainer"
|
||||
ref={provided.innerRef}
|
||||
>
|
||||
{ tags }
|
||||
{ provided.placeholder }
|
||||
</div>
|
||||
) }
|
||||
</Droppable>
|
||||
</DragDropContext>
|
||||
<AccessibleButton className="mx_TagPanel_createGroupButton" onClick={this.onCreateGroupClick}>
|
||||
<TintableSvg src="img/icons-create-room.svg" width="25" height="25" />
|
||||
</AccessibleButton>
|
||||
</GeminiScrollbar>
|
||||
<div className="mx_TagPanel_divider" />
|
||||
<div className="mx_TagPanel_createGroupButton">
|
||||
<GroupsButton tooltip={true} />
|
||||
</div>
|
||||
</div>;
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1121,9 +1121,9 @@ var TimelinePanel = React.createClass({
|
|||
// exist.
|
||||
if (this.state.timelineLoading) {
|
||||
return (
|
||||
<div className={this.props.className + " mx_RoomView_messageListWrapper"}>
|
||||
<Loader />
|
||||
</div>
|
||||
<div className="mx_RoomView_messagePanelSpinner">
|
||||
<Loader />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -58,6 +58,7 @@ module.exports = React.createClass({
|
|||
// login shouldn't care how password recovery is done.
|
||||
onForgotPasswordClick: PropTypes.func,
|
||||
onCancelClick: PropTypes.func,
|
||||
onServerConfigChange: PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
|
@ -218,6 +219,8 @@ module.exports = React.createClass({
|
|||
if (config.isUrl !== undefined) {
|
||||
newState.enteredIdentityServerUrl = config.isUrl;
|
||||
}
|
||||
|
||||
this.props.onServerConfigChange(config);
|
||||
this.setState(newState, function() {
|
||||
self._initLoginLogic(config.hsUrl || null, config.isUrl);
|
||||
});
|
||||
|
@ -428,10 +431,10 @@ module.exports = React.createClass({
|
|||
// FIXME: remove status.im theme tweaks
|
||||
const theme = SettingsStore.getValue("theme");
|
||||
if (theme !== "status") {
|
||||
header = <h2>{ _t('Sign in') }</h2>;
|
||||
header = <h2>{ _t('Sign in') } { loader }</h2>;
|
||||
} else {
|
||||
if (!this.state.errorText) {
|
||||
header = <h2>{ _t('Sign in to get started') }</h2>;
|
||||
header = <h2>{ _t('Sign in to get started') } { loader }</h2>;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -61,6 +61,7 @@ module.exports = React.createClass({
|
|||
// registration shouldn't know or care how login is done.
|
||||
onLoginClick: PropTypes.func.isRequired,
|
||||
onCancelClick: PropTypes.func,
|
||||
onServerConfigChange: PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
|
@ -131,6 +132,7 @@ module.exports = React.createClass({
|
|||
if (config.isUrl !== undefined) {
|
||||
newState.isUrl = config.isUrl;
|
||||
}
|
||||
this.props.onServerConfigChange(config);
|
||||
this.setState(newState, function() {
|
||||
this._replaceClient();
|
||||
});
|
||||
|
|
|
@ -17,9 +17,12 @@ limitations under the License.
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { MatrixClient } from 'matrix-js-sdk';
|
||||
|
||||
import { KeyCode } from '../../../Keyboard';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import sdk from '../../../index';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
|
||||
/**
|
||||
* Basic container for modal dialogs.
|
||||
|
@ -51,6 +54,20 @@ export default React.createClass({
|
|||
children: PropTypes.node,
|
||||
},
|
||||
|
||||
childContextTypes: {
|
||||
matrixClient: PropTypes.instanceOf(MatrixClient),
|
||||
},
|
||||
|
||||
getChildContext: function() {
|
||||
return {
|
||||
matrixClient: this._matrixClient,
|
||||
};
|
||||
},
|
||||
|
||||
componentWillMount() {
|
||||
this._matrixClient = MatrixClientPeg.get();
|
||||
},
|
||||
|
||||
_onKeyDown: function(e) {
|
||||
if (this.props.onKeyDown) {
|
||||
this.props.onKeyDown(e);
|
||||
|
|
|
@ -393,6 +393,10 @@ export default React.createClass({
|
|||
const sandboxFlags = "allow-forms allow-popups allow-popups-to-escape-sandbox "+
|
||||
"allow-same-origin allow-scripts allow-presentation";
|
||||
|
||||
// Additional iframe feature pemissions
|
||||
// (see - https://sites.google.com/a/chromium.org/dev/Home/chromium-security/deprecating-permissions-in-cross-origin-iframes and https://wicg.github.io/feature-policy/)
|
||||
const iframeFeatures = "microphone; camera; encrypted-media;";
|
||||
|
||||
if (this.props.show) {
|
||||
const loadingElement = (
|
||||
<div className='mx_AppTileBody mx_AppLoading'>
|
||||
|
@ -412,7 +416,13 @@ export default React.createClass({
|
|||
appTileBody = (
|
||||
<div className={this.state.loading ? 'mx_AppTileBody mx_AppLoading' : 'mx_AppTileBody'}>
|
||||
{ this.state.loading && loadingElement }
|
||||
{ /*
|
||||
The "is" attribute in the following iframe tag is needed in order to enable rendering of the
|
||||
"allow" attribute, which is unknown to react 15.
|
||||
*/ }
|
||||
<iframe
|
||||
is
|
||||
allow={iframeFeatures}
|
||||
ref="appFrame"
|
||||
src={this._getSafeUrl()}
|
||||
allowFullScreen="true"
|
||||
|
|
|
@ -25,6 +25,7 @@ export default function DNDTagTile(props) {
|
|||
key={props.tag}
|
||||
draggableId={props.tag}
|
||||
index={props.index}
|
||||
type="draggable-TagTile"
|
||||
>
|
||||
{ (provided, snapshot) => (
|
||||
<div>
|
||||
|
|
|
@ -21,6 +21,7 @@ import { MatrixClient } from 'matrix-js-sdk';
|
|||
import sdk from '../../../index';
|
||||
import dis from '../../../dispatcher';
|
||||
import { isOnlyCtrlOrCmdIgnoreShiftKeyEvent } from '../../../Keyboard';
|
||||
import ContextualMenu from '../../structures/ContextualMenu';
|
||||
|
||||
import FlairStore from '../../../stores/FlairStore';
|
||||
|
||||
|
@ -81,6 +82,35 @@ export default React.createClass({
|
|||
});
|
||||
},
|
||||
|
||||
onContextButtonClick: function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// Hide the (...) immediately
|
||||
this.setState({ hover: false });
|
||||
|
||||
const TagTileContextMenu = sdk.getComponent('context_menus.TagTileContextMenu');
|
||||
const elementRect = e.target.getBoundingClientRect();
|
||||
|
||||
// The window X and Y offsets are to adjust position when zoomed in to page
|
||||
const x = elementRect.right + window.pageXOffset + 3;
|
||||
const chevronOffset = 12;
|
||||
let y = (elementRect.top + (elementRect.height / 2) + window.pageYOffset);
|
||||
y = y - (chevronOffset + 8); // where 8 is half the height of the chevron
|
||||
|
||||
const self = this;
|
||||
ContextualMenu.createMenu(TagTileContextMenu, {
|
||||
chevronOffset: chevronOffset,
|
||||
left: x,
|
||||
top: y,
|
||||
tag: this.props.tag,
|
||||
onFinished: function() {
|
||||
self.setState({ menuDisplayed: false });
|
||||
},
|
||||
});
|
||||
this.setState({ menuDisplayed: true });
|
||||
},
|
||||
|
||||
onMouseOver: function() {
|
||||
this.setState({hover: true});
|
||||
},
|
||||
|
@ -109,10 +139,15 @@ export default React.createClass({
|
|||
const tip = this.state.hover ?
|
||||
<RoomTooltip className="mx_TagTile_tooltip" label={name} /> :
|
||||
<div />;
|
||||
const contextButton = this.state.hover || this.state.menuDisplayed ?
|
||||
<div className="mx_TagTile_context_button" onClick={this.onContextButtonClick}>
|
||||
{ "\u00B7\u00B7\u00B7" }
|
||||
</div> : <div />;
|
||||
return <AccessibleButton className={className} onClick={this.onClick}>
|
||||
<div className="mx_TagTile_avatar" onMouseOver={this.onMouseOver} onMouseOut={this.onMouseOut}>
|
||||
<BaseAvatar name={name} url={httpUrl} width={avatarHeight} height={avatarHeight} />
|
||||
{ tip }
|
||||
{ contextButton }
|
||||
</div>
|
||||
</AccessibleButton>;
|
||||
},
|
||||
|
|
|
@ -132,7 +132,9 @@ module.exports = React.createClass({
|
|||
render: function() {
|
||||
if (this.state.removingUser) {
|
||||
const Spinner = sdk.getComponent("elements.Spinner");
|
||||
return <Spinner />;
|
||||
return <div className="mx_MemberInfo">
|
||||
<Spinner />
|
||||
</div>;
|
||||
}
|
||||
|
||||
let adminTools;
|
||||
|
|
|
@ -17,10 +17,12 @@ limitations under the License.
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {MatrixClient} from 'matrix-js-sdk';
|
||||
import { Draggable, Droppable } from 'react-beautiful-dnd';
|
||||
import sdk from '../../../index';
|
||||
import dis from '../../../dispatcher';
|
||||
import FlairStore from '../../../stores/FlairStore';
|
||||
|
||||
|
||||
const GroupTile = React.createClass({
|
||||
displayName: 'GroupTile',
|
||||
|
||||
|
@ -78,9 +80,39 @@ const GroupTile = React.createClass({
|
|||
profile.avatarUrl, avatarHeight, avatarHeight, "crop",
|
||||
) : null;
|
||||
return <AccessibleButton className="mx_GroupTile" onClick={this.onClick}>
|
||||
<div className="mx_GroupTile_avatar">
|
||||
<BaseAvatar name={name} url={httpUrl} width={avatarHeight} height={avatarHeight} />
|
||||
</div>
|
||||
<Droppable droppableId="my-groups-droppable" type="draggable-TagTile">
|
||||
{ (droppableProvided, droppableSnapshot) => (
|
||||
<div ref={droppableProvided.innerRef}>
|
||||
<Draggable
|
||||
key={"GroupTile " + this.props.groupId}
|
||||
draggableId={"GroupTile " + this.props.groupId}
|
||||
index={this.props.groupId}
|
||||
type="draggable-TagTile"
|
||||
>
|
||||
{ (provided, snapshot) => (
|
||||
<div>
|
||||
<div
|
||||
ref={provided.innerRef}
|
||||
{...provided.draggableProps}
|
||||
{...provided.dragHandleProps}
|
||||
>
|
||||
<div className="mx_GroupTile_avatar">
|
||||
<BaseAvatar name={name} url={httpUrl} width={avatarHeight} height={avatarHeight} />
|
||||
</div>
|
||||
</div>
|
||||
{ /* Instead of a blank placeholder, use a copy of the avatar itself. */ }
|
||||
{ provided.placeholder ?
|
||||
<div className="mx_GroupTile_avatar">
|
||||
<BaseAvatar name={name} url={httpUrl} width={avatarHeight} height={avatarHeight} />
|
||||
</div> :
|
||||
<div />
|
||||
}
|
||||
</div>
|
||||
) }
|
||||
</Draggable>
|
||||
</div>
|
||||
) }
|
||||
</Droppable>
|
||||
<div className="mx_GroupTile_profile">
|
||||
<div className="mx_GroupTile_name">{ name }</div>
|
||||
{ descElement }
|
||||
|
|
|
@ -82,7 +82,7 @@ Tinter.registerTintable(updateTintedDownloadImage);
|
|||
// downloaded. This limit does not seem to apply when the url is used as
|
||||
// the source attribute of an image tag.
|
||||
//
|
||||
// Blob URLs are generated using window.URL.createObjectURL and unforuntately
|
||||
// Blob URLs are generated using window.URL.createObjectURL and unfortunately
|
||||
// for our purposes they inherit the origin of the page that created them.
|
||||
// This means that any scripts that run when the URL is viewed will be able
|
||||
// to access local storage.
|
||||
|
|
|
@ -18,7 +18,6 @@ limitations under the License.
|
|||
'use strict';
|
||||
const React = require("react");
|
||||
const ReactDOM = require("react-dom");
|
||||
import { DragDropContext } from 'react-beautiful-dnd';
|
||||
import PropTypes from 'prop-types';
|
||||
import { _t } from '../../../languageHandler';
|
||||
const GeminiScrollbar = require('react-gemini-scrollbar');
|
||||
|
@ -27,15 +26,15 @@ const CallHandler = require('../../../CallHandler');
|
|||
const dis = require("../../../dispatcher");
|
||||
const sdk = require('../../../index');
|
||||
const rate_limited_func = require('../../../ratelimitedfunc');
|
||||
const Rooms = require('../../../Rooms');
|
||||
import * as Rooms from '../../../Rooms';
|
||||
import DMRoomMap from '../../../utils/DMRoomMap';
|
||||
const Receipt = require('../../../utils/Receipt');
|
||||
import TagOrderStore from '../../../stores/TagOrderStore';
|
||||
import RoomListStore from '../../../stores/RoomListStore';
|
||||
import GroupStoreCache from '../../../stores/GroupStoreCache';
|
||||
|
||||
import Modal from '../../../Modal';
|
||||
|
||||
const HIDE_CONFERENCE_CHANS = true;
|
||||
const STANDARD_TAGS_REGEX = /^(m\.(favourite|lowpriority)|im\.vector\.fake\.(invite|recent|direct|archived))$/;
|
||||
|
||||
function phraseForSection(section) {
|
||||
switch (section) {
|
||||
|
@ -78,9 +77,7 @@ module.exports = React.createClass({
|
|||
|
||||
cli.on("Room", this.onRoom);
|
||||
cli.on("deleteRoom", this.onDeleteRoom);
|
||||
cli.on("Room.timeline", this.onRoomTimeline);
|
||||
cli.on("Room.name", this.onRoomName);
|
||||
cli.on("Room.tags", this.onRoomTags);
|
||||
cli.on("Room.receipt", this.onRoomReceipt);
|
||||
cli.on("RoomState.events", this.onRoomStateEvents);
|
||||
cli.on("RoomMember.name", this.onRoomMemberName);
|
||||
|
@ -118,6 +115,10 @@ module.exports = React.createClass({
|
|||
this.updateVisibleRooms();
|
||||
});
|
||||
|
||||
this._roomListStoreToken = RoomListStore.addListener(() => {
|
||||
this._delayedRefreshRoomList();
|
||||
});
|
||||
|
||||
this.refreshRoomList();
|
||||
|
||||
// order of the sublists
|
||||
|
@ -176,9 +177,7 @@ module.exports = React.createClass({
|
|||
if (MatrixClientPeg.get()) {
|
||||
MatrixClientPeg.get().removeListener("Room", this.onRoom);
|
||||
MatrixClientPeg.get().removeListener("deleteRoom", this.onDeleteRoom);
|
||||
MatrixClientPeg.get().removeListener("Room.timeline", this.onRoomTimeline);
|
||||
MatrixClientPeg.get().removeListener("Room.name", this.onRoomName);
|
||||
MatrixClientPeg.get().removeListener("Room.tags", this.onRoomTags);
|
||||
MatrixClientPeg.get().removeListener("Room.receipt", this.onRoomReceipt);
|
||||
MatrixClientPeg.get().removeListener("RoomState.events", this.onRoomStateEvents);
|
||||
MatrixClientPeg.get().removeListener("RoomMember.name", this.onRoomMemberName);
|
||||
|
@ -191,6 +190,10 @@ module.exports = React.createClass({
|
|||
this._tagStoreToken.remove();
|
||||
}
|
||||
|
||||
if (this._roomListStoreToken) {
|
||||
this._roomListStoreToken.remove();
|
||||
}
|
||||
|
||||
if (this._groupStoreTokens.length > 0) {
|
||||
// NB: GroupStore is not a Flux.Store
|
||||
this._groupStoreTokens.forEach((token) => token.unregister());
|
||||
|
@ -232,13 +235,6 @@ module.exports = React.createClass({
|
|||
this._updateStickyHeaders(true, scrollToPosition);
|
||||
},
|
||||
|
||||
onRoomTimeline: function(ev, room, toStartOfTimeline, removed, data) {
|
||||
if (toStartOfTimeline) return;
|
||||
if (!room) return;
|
||||
if (data.timeline.getTimelineSet() !== room.getUnfilteredTimelineSet()) return;
|
||||
this._delayedRefreshRoomList();
|
||||
},
|
||||
|
||||
onRoomReceipt: function(receiptEvent, room) {
|
||||
// because if we read a notification, it will affect notification count
|
||||
// only bother updating if there's a receipt from us
|
||||
|
@ -251,10 +247,6 @@ module.exports = React.createClass({
|
|||
this._delayedRefreshRoomList();
|
||||
},
|
||||
|
||||
onRoomTags: function(event, room) {
|
||||
this._delayedRefreshRoomList();
|
||||
},
|
||||
|
||||
onRoomStateEvents: function(ev, state) {
|
||||
this._delayedRefreshRoomList();
|
||||
},
|
||||
|
@ -278,106 +270,6 @@ module.exports = React.createClass({
|
|||
this.forceUpdate();
|
||||
},
|
||||
|
||||
onRoomTileEndDrag: function(result) {
|
||||
if (!result.destination) return;
|
||||
|
||||
let newTag = result.destination.droppableId.split('_')[1];
|
||||
let prevTag = result.source.droppableId.split('_')[1];
|
||||
if (newTag === 'undefined') newTag = undefined;
|
||||
if (prevTag === 'undefined') prevTag = undefined;
|
||||
|
||||
const roomId = result.draggableId.split('_')[1];
|
||||
const room = MatrixClientPeg.get().getRoom(roomId);
|
||||
|
||||
const newIndex = result.destination.index;
|
||||
|
||||
// Evil hack to get DMs behaving
|
||||
if ((prevTag === undefined && newTag === 'im.vector.fake.direct') ||
|
||||
(prevTag === 'im.vector.fake.direct' && newTag === undefined)
|
||||
) {
|
||||
Rooms.guessAndSetDMRoom(
|
||||
room, newTag === 'im.vector.fake.direct',
|
||||
).catch((err) => {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
console.error("Failed to set direct chat tag " + err);
|
||||
Modal.createTrackedDialog('Failed to set direct chat tag', '', ErrorDialog, {
|
||||
title: _t('Failed to set direct chat tag'),
|
||||
description: ((err && err.message) ? err.message : _t('Operation failed')),
|
||||
});
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const hasChangedSubLists = result.source.droppableId !== result.destination.droppableId;
|
||||
|
||||
let newOrder = null;
|
||||
|
||||
// Is the tag ordered manually?
|
||||
if (newTag && !newTag.match(/^(m\.lowpriority|im\.vector\.fake\.(invite|recent|direct|archived))$/)) {
|
||||
const newList = this.state.lists[newTag];
|
||||
|
||||
// If the room was moved "down" (increasing index) in the same list we
|
||||
// need to use the orders of the tiles with indices shifted by +1
|
||||
const offset = (
|
||||
newTag === prevTag && result.source.index < result.destination.index
|
||||
) ? 1 : 0;
|
||||
|
||||
const indexBefore = offset + newIndex - 1;
|
||||
const indexAfter = offset + newIndex;
|
||||
|
||||
const prevOrder = indexBefore < 0 ?
|
||||
0 : newList[indexBefore].tags[newTag].order;
|
||||
const nextOrder = indexAfter >= newList.length ?
|
||||
1 : newList[indexAfter].tags[newTag].order;
|
||||
|
||||
newOrder = {
|
||||
order: (prevOrder + nextOrder) / 2.0,
|
||||
};
|
||||
}
|
||||
|
||||
// More evilness: We will still be dealing with moving to favourites/low prio,
|
||||
// but we avoid ever doing a request with 'im.vector.fake.direct`.
|
||||
//
|
||||
// if we moved lists, remove the old tag
|
||||
if (prevTag && prevTag !== 'im.vector.fake.direct' &&
|
||||
hasChangedSubLists
|
||||
) {
|
||||
// Optimistic update of what will happen to the room tags
|
||||
delete room.tags[prevTag];
|
||||
|
||||
MatrixClientPeg.get().deleteRoomTag(roomId, prevTag).catch(function(err) {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
console.error("Failed to remove tag " + prevTag + " from room: " + err);
|
||||
Modal.createTrackedDialog('Failed to remove tag from room', '', ErrorDialog, {
|
||||
title: _t('Failed to remove tag %(tagName)s from room', {tagName: prevTag}),
|
||||
description: ((err && err.message) ? err.message : _t('Operation failed')),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// if we moved lists or the ordering changed, add the new tag
|
||||
if (newTag && newTag !== 'im.vector.fake.direct' &&
|
||||
(hasChangedSubLists || newOrder)
|
||||
) {
|
||||
// Optimistic update of what will happen to the room tags
|
||||
room.tags[newTag] = newOrder;
|
||||
|
||||
MatrixClientPeg.get().setRoomTag(roomId, newTag, newOrder).catch(function(err) {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
console.error("Failed to add tag " + newTag + " to room: " + err);
|
||||
Modal.createTrackedDialog('Failed to add tag to room', '', ErrorDialog, {
|
||||
title: _t('Failed to add tag %(tagName)s to room', {tagName: newTag}),
|
||||
description: ((err && err.message) ? err.message : _t('Operation failed')),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Refresh to display the optimistic updates - this needs to be done in the
|
||||
// same tick as the drag finishing otherwise the room will pop back to its
|
||||
// previous position - hence no delayed refresh
|
||||
this.refreshRoomList();
|
||||
},
|
||||
|
||||
_delayedRefreshRoomList: new rate_limited_func(function() {
|
||||
this.refreshRoomList();
|
||||
}, 500),
|
||||
|
@ -441,7 +333,7 @@ module.exports = React.createClass({
|
|||
totalRooms += l.length;
|
||||
}
|
||||
this.setState({
|
||||
lists: this.getRoomLists(),
|
||||
lists,
|
||||
totalRoomCount: totalRooms,
|
||||
// Do this here so as to not render every time the selected tags
|
||||
// themselves change.
|
||||
|
@ -452,70 +344,38 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
getRoomLists: function() {
|
||||
const lists = {};
|
||||
lists["im.vector.fake.invite"] = [];
|
||||
lists["m.favourite"] = [];
|
||||
lists["im.vector.fake.recent"] = [];
|
||||
lists["im.vector.fake.direct"] = [];
|
||||
lists["m.lowpriority"] = [];
|
||||
lists["im.vector.fake.archived"] = [];
|
||||
const lists = RoomListStore.getRoomLists();
|
||||
|
||||
const dmRoomMap = DMRoomMap.shared();
|
||||
const filteredLists = {};
|
||||
|
||||
this._visibleRooms.forEach((room, index) => {
|
||||
const me = room.getMember(MatrixClientPeg.get().credentials.userId);
|
||||
if (!me) return;
|
||||
const isRoomVisible = {
|
||||
// $roomId: true,
|
||||
};
|
||||
|
||||
// console.log("room = " + room.name + ", me.membership = " + me.membership +
|
||||
// ", sender = " + me.events.member.getSender() +
|
||||
// ", target = " + me.events.member.getStateKey() +
|
||||
// ", prevMembership = " + me.events.member.getPrevContent().membership);
|
||||
this._visibleRooms.forEach((r) => {
|
||||
isRoomVisible[r.roomId] = true;
|
||||
});
|
||||
|
||||
if (me.membership == "invite") {
|
||||
lists["im.vector.fake.invite"].push(room);
|
||||
} else if (HIDE_CONFERENCE_CHANS && Rooms.isConfCallRoom(room, me, this.props.ConferenceHandler)) {
|
||||
// skip past this room & don't put it in any lists
|
||||
} else if (me.membership == "join" || me.membership === "ban" ||
|
||||
(me.membership === "leave" && me.events.member.getSender() !== me.events.member.getStateKey())) {
|
||||
// Used to split rooms via tags
|
||||
const tagNames = Object.keys(room.tags);
|
||||
if (tagNames.length) {
|
||||
for (let i = 0; i < tagNames.length; i++) {
|
||||
const tagName = tagNames[i];
|
||||
lists[tagName] = lists[tagName] || [];
|
||||
lists[tagName].push(room);
|
||||
}
|
||||
} else if (dmRoomMap.getUserIdForRoomId(room.roomId)) {
|
||||
// "Direct Message" rooms (that we're still in and that aren't otherwise tagged)
|
||||
lists["im.vector.fake.direct"].push(room);
|
||||
} else {
|
||||
lists["im.vector.fake.recent"].push(room);
|
||||
Object.keys(lists).forEach((tagName) => {
|
||||
const filteredRooms = lists[tagName].filter((taggedRoom) => {
|
||||
// Somewhat impossible, but guard against it anyway
|
||||
if (!taggedRoom) {
|
||||
return;
|
||||
}
|
||||
} else if (me.membership === "leave") {
|
||||
lists["im.vector.fake.archived"].push(room);
|
||||
} else {
|
||||
console.error("unrecognised membership: " + me.membership + " - this should never happen");
|
||||
const me = taggedRoom.getMember(MatrixClientPeg.get().credentials.userId);
|
||||
if (HIDE_CONFERENCE_CHANS && Rooms.isConfCallRoom(taggedRoom, me, this.props.ConferenceHandler)) {
|
||||
return;
|
||||
}
|
||||
|
||||
return Boolean(isRoomVisible[taggedRoom.roomId]);
|
||||
});
|
||||
|
||||
if (filteredRooms.length > 0 || tagName.match(STANDARD_TAGS_REGEX)) {
|
||||
filteredLists[tagName] = filteredRooms;
|
||||
}
|
||||
});
|
||||
|
||||
// we actually apply the sorting to this when receiving the prop in RoomSubLists.
|
||||
|
||||
// we'll need this when we get to iterating through lists programatically - e.g. ctrl-shift-up/down
|
||||
/*
|
||||
this.listOrder = [
|
||||
"im.vector.fake.invite",
|
||||
"m.favourite",
|
||||
"im.vector.fake.recent",
|
||||
"im.vector.fake.direct",
|
||||
Object.keys(otherTagNames).filter(tagName=>{
|
||||
return (!tagName.match(/^m\.(favourite|lowpriority)$/));
|
||||
}).sort(),
|
||||
"m.lowpriority",
|
||||
"im.vector.fake.archived"
|
||||
];
|
||||
*/
|
||||
|
||||
return lists;
|
||||
return filteredLists;
|
||||
},
|
||||
|
||||
_getScrollNode: function() {
|
||||
|
@ -752,116 +612,114 @@ module.exports = React.createClass({
|
|||
|
||||
const self = this;
|
||||
return (
|
||||
<DragDropContext onDragEnd={this.onRoomTileEndDrag}>
|
||||
<GeminiScrollbar className="mx_RoomList_scrollbar"
|
||||
autoshow={true} onScroll={self._whenScrolling} ref="gemscroll">
|
||||
<div className="mx_RoomList">
|
||||
<RoomSubList list={[]}
|
||||
extraTiles={this._makeGroupInviteTiles()}
|
||||
label={_t('Community Invites')}
|
||||
editable={false}
|
||||
order="recent"
|
||||
isInvite={true}
|
||||
collapsed={self.props.collapsed}
|
||||
searchFilter={self.props.searchFilter}
|
||||
onHeaderClick={self.onSubListHeaderClick}
|
||||
onShowMoreRooms={self.onShowMoreRooms}
|
||||
/>
|
||||
<GeminiScrollbar className="mx_RoomList_scrollbar"
|
||||
autoshow={true} onScroll={self._whenScrolling} ref="gemscroll">
|
||||
<div className="mx_RoomList">
|
||||
<RoomSubList list={[]}
|
||||
extraTiles={this._makeGroupInviteTiles()}
|
||||
label={_t('Community Invites')}
|
||||
editable={false}
|
||||
order="recent"
|
||||
isInvite={true}
|
||||
collapsed={self.props.collapsed}
|
||||
searchFilter={self.props.searchFilter}
|
||||
onHeaderClick={self.onSubListHeaderClick}
|
||||
onShowMoreRooms={self.onShowMoreRooms}
|
||||
/>
|
||||
|
||||
<RoomSubList list={self.state.lists['im.vector.fake.invite']}
|
||||
label={_t('Invites')}
|
||||
editable={false}
|
||||
order="recent"
|
||||
isInvite={true}
|
||||
incomingCall={self.state.incomingCall}
|
||||
collapsed={self.props.collapsed}
|
||||
searchFilter={self.props.searchFilter}
|
||||
onHeaderClick={self.onSubListHeaderClick}
|
||||
onShowMoreRooms={self.onShowMoreRooms}
|
||||
/>
|
||||
<RoomSubList list={self.state.lists['im.vector.fake.invite']}
|
||||
label={_t('Invites')}
|
||||
editable={false}
|
||||
order="recent"
|
||||
isInvite={true}
|
||||
incomingCall={self.state.incomingCall}
|
||||
collapsed={self.props.collapsed}
|
||||
searchFilter={self.props.searchFilter}
|
||||
onHeaderClick={self.onSubListHeaderClick}
|
||||
onShowMoreRooms={self.onShowMoreRooms}
|
||||
/>
|
||||
|
||||
<RoomSubList list={self.state.lists['m.favourite']}
|
||||
label={_t('Favourites')}
|
||||
tagName="m.favourite"
|
||||
emptyContent={this._getEmptyContent('m.favourite')}
|
||||
editable={true}
|
||||
order="manual"
|
||||
incomingCall={self.state.incomingCall}
|
||||
collapsed={self.props.collapsed}
|
||||
searchFilter={self.props.searchFilter}
|
||||
onHeaderClick={self.onSubListHeaderClick}
|
||||
onShowMoreRooms={self.onShowMoreRooms} />
|
||||
<RoomSubList list={self.state.lists['m.favourite']}
|
||||
label={_t('Favourites')}
|
||||
tagName="m.favourite"
|
||||
emptyContent={this._getEmptyContent('m.favourite')}
|
||||
editable={true}
|
||||
order="manual"
|
||||
incomingCall={self.state.incomingCall}
|
||||
collapsed={self.props.collapsed}
|
||||
searchFilter={self.props.searchFilter}
|
||||
onHeaderClick={self.onSubListHeaderClick}
|
||||
onShowMoreRooms={self.onShowMoreRooms} />
|
||||
|
||||
<RoomSubList list={self.state.lists['im.vector.fake.direct']}
|
||||
label={_t('People')}
|
||||
tagName="im.vector.fake.direct"
|
||||
emptyContent={this._getEmptyContent('im.vector.fake.direct')}
|
||||
headerItems={this._getHeaderItems('im.vector.fake.direct')}
|
||||
editable={true}
|
||||
order="recent"
|
||||
incomingCall={self.state.incomingCall}
|
||||
collapsed={self.props.collapsed}
|
||||
alwaysShowHeader={true}
|
||||
searchFilter={self.props.searchFilter}
|
||||
onHeaderClick={self.onSubListHeaderClick}
|
||||
onShowMoreRooms={self.onShowMoreRooms} />
|
||||
<RoomSubList list={self.state.lists['im.vector.fake.direct']}
|
||||
label={_t('People')}
|
||||
tagName="im.vector.fake.direct"
|
||||
emptyContent={this._getEmptyContent('im.vector.fake.direct')}
|
||||
headerItems={this._getHeaderItems('im.vector.fake.direct')}
|
||||
editable={true}
|
||||
order="recent"
|
||||
incomingCall={self.state.incomingCall}
|
||||
collapsed={self.props.collapsed}
|
||||
alwaysShowHeader={true}
|
||||
searchFilter={self.props.searchFilter}
|
||||
onHeaderClick={self.onSubListHeaderClick}
|
||||
onShowMoreRooms={self.onShowMoreRooms} />
|
||||
|
||||
<RoomSubList list={self.state.lists['im.vector.fake.recent']}
|
||||
label={_t('Rooms')}
|
||||
editable={true}
|
||||
emptyContent={this._getEmptyContent('im.vector.fake.recent')}
|
||||
headerItems={this._getHeaderItems('im.vector.fake.recent')}
|
||||
order="recent"
|
||||
incomingCall={self.state.incomingCall}
|
||||
collapsed={self.props.collapsed}
|
||||
searchFilter={self.props.searchFilter}
|
||||
onHeaderClick={self.onSubListHeaderClick}
|
||||
onShowMoreRooms={self.onShowMoreRooms} />
|
||||
<RoomSubList list={self.state.lists['im.vector.fake.recent']}
|
||||
label={_t('Rooms')}
|
||||
editable={true}
|
||||
emptyContent={this._getEmptyContent('im.vector.fake.recent')}
|
||||
headerItems={this._getHeaderItems('im.vector.fake.recent')}
|
||||
order="recent"
|
||||
incomingCall={self.state.incomingCall}
|
||||
collapsed={self.props.collapsed}
|
||||
searchFilter={self.props.searchFilter}
|
||||
onHeaderClick={self.onSubListHeaderClick}
|
||||
onShowMoreRooms={self.onShowMoreRooms} />
|
||||
|
||||
{ Object.keys(self.state.lists).map((tagName) => {
|
||||
if (!tagName.match(/^(m\.(favourite|lowpriority)|im\.vector\.fake\.(invite|recent|direct|archived))$/)) {
|
||||
return <RoomSubList list={self.state.lists[tagName]}
|
||||
key={tagName}
|
||||
label={tagName}
|
||||
tagName={tagName}
|
||||
emptyContent={this._getEmptyContent(tagName)}
|
||||
editable={true}
|
||||
order="manual"
|
||||
incomingCall={self.state.incomingCall}
|
||||
collapsed={self.props.collapsed}
|
||||
searchFilter={self.props.searchFilter}
|
||||
onHeaderClick={self.onSubListHeaderClick}
|
||||
onShowMoreRooms={self.onShowMoreRooms} />;
|
||||
}
|
||||
}) }
|
||||
{ Object.keys(self.state.lists).map((tagName) => {
|
||||
if (!tagName.match(STANDARD_TAGS_REGEX)) {
|
||||
return <RoomSubList list={self.state.lists[tagName]}
|
||||
key={tagName}
|
||||
label={tagName}
|
||||
tagName={tagName}
|
||||
emptyContent={this._getEmptyContent(tagName)}
|
||||
editable={true}
|
||||
order="manual"
|
||||
incomingCall={self.state.incomingCall}
|
||||
collapsed={self.props.collapsed}
|
||||
searchFilter={self.props.searchFilter}
|
||||
onHeaderClick={self.onSubListHeaderClick}
|
||||
onShowMoreRooms={self.onShowMoreRooms} />;
|
||||
}
|
||||
}) }
|
||||
|
||||
<RoomSubList list={self.state.lists['m.lowpriority']}
|
||||
label={_t('Low priority')}
|
||||
tagName="m.lowpriority"
|
||||
emptyContent={this._getEmptyContent('m.lowpriority')}
|
||||
editable={true}
|
||||
order="recent"
|
||||
incomingCall={self.state.incomingCall}
|
||||
collapsed={self.props.collapsed}
|
||||
searchFilter={self.props.searchFilter}
|
||||
onHeaderClick={self.onSubListHeaderClick}
|
||||
onShowMoreRooms={self.onShowMoreRooms} />
|
||||
<RoomSubList list={self.state.lists['m.lowpriority']}
|
||||
label={_t('Low priority')}
|
||||
tagName="m.lowpriority"
|
||||
emptyContent={this._getEmptyContent('m.lowpriority')}
|
||||
editable={true}
|
||||
order="recent"
|
||||
incomingCall={self.state.incomingCall}
|
||||
collapsed={self.props.collapsed}
|
||||
searchFilter={self.props.searchFilter}
|
||||
onHeaderClick={self.onSubListHeaderClick}
|
||||
onShowMoreRooms={self.onShowMoreRooms} />
|
||||
|
||||
<RoomSubList list={self.state.lists['im.vector.fake.archived']}
|
||||
label={_t('Historical')}
|
||||
editable={false}
|
||||
order="recent"
|
||||
collapsed={self.props.collapsed}
|
||||
alwaysShowHeader={true}
|
||||
startAsHidden={true}
|
||||
showSpinner={self.state.isLoadingLeftRooms}
|
||||
onHeaderClick= {self.onArchivedHeaderClick}
|
||||
incomingCall={self.state.incomingCall}
|
||||
searchFilter={self.props.searchFilter}
|
||||
onShowMoreRooms={self.onShowMoreRooms} />
|
||||
</div>
|
||||
</GeminiScrollbar>
|
||||
</DragDropContext>
|
||||
<RoomSubList list={self.state.lists['im.vector.fake.archived']}
|
||||
label={_t('Historical')}
|
||||
editable={false}
|
||||
order="recent"
|
||||
collapsed={self.props.collapsed}
|
||||
alwaysShowHeader={true}
|
||||
startAsHidden={true}
|
||||
showSpinner={self.state.isLoadingLeftRooms}
|
||||
onHeaderClick= {self.onArchivedHeaderClick}
|
||||
incomingCall={self.state.incomingCall}
|
||||
searchFilter={self.props.searchFilter}
|
||||
onShowMoreRooms={self.onShowMoreRooms} />
|
||||
</div>
|
||||
</GeminiScrollbar>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
|
|
@ -29,13 +29,21 @@ module.exports = React.createClass({
|
|||
room: PropTypes.object.isRequired,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
name: null,
|
||||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
const room = this.props.room;
|
||||
const name = room.currentState.getStateEvents('m.room.name', '');
|
||||
const myId = MatrixClientPeg.get().credentials.userId;
|
||||
const defaultName = room.getDefaultRoomName(myId);
|
||||
|
||||
this._initialName = name ? name.getContent().name : '';
|
||||
this.setState({
|
||||
name: name ? name.getContent().name : '',
|
||||
});
|
||||
|
||||
this._placeholderName = _t("Unnamed Room");
|
||||
if (defaultName && defaultName !== 'Empty room') { // default name from JS SDK, needs no translation as we don't ever show it.
|
||||
|
@ -44,7 +52,13 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
getRoomName: function() {
|
||||
return this.refs.editor.getValue();
|
||||
return this.state.name;
|
||||
},
|
||||
|
||||
_onValueChanged: function(value, shouldSubmit) {
|
||||
this.setState({
|
||||
name: value,
|
||||
});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
|
@ -57,7 +71,8 @@ module.exports = React.createClass({
|
|||
placeholderClassName="mx_RoomHeader_placeholder"
|
||||
placeholder={this._placeholderName}
|
||||
blurToCancel={false}
|
||||
initialValue={this._initialName}
|
||||
initialValue={this.state.name}
|
||||
onValueChanged={this._onValueChanged}
|
||||
dir="auto" />
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -28,26 +28,41 @@ module.exports = React.createClass({
|
|||
room: PropTypes.object.isRequired,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
topic: null,
|
||||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
const room = this.props.room;
|
||||
const topic = room.currentState.getStateEvents('m.room.topic', '');
|
||||
this._initialTopic = topic ? topic.getContent().topic : '';
|
||||
this.setState({
|
||||
topic: topic ? topic.getContent().topic : '',
|
||||
});
|
||||
},
|
||||
|
||||
getTopic: function() {
|
||||
return this.refs.editor.getValue();
|
||||
return this.state.topic;
|
||||
},
|
||||
|
||||
_onValueChanged: function(value) {
|
||||
this.setState({
|
||||
topic: value,
|
||||
});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const EditableText = sdk.getComponent("elements.EditableText");
|
||||
|
||||
return (
|
||||
<EditableText ref="editor"
|
||||
<EditableText
|
||||
className="mx_RoomHeader_topic mx_RoomHeader_editable"
|
||||
placeholderClassName="mx_RoomHeader_placeholder"
|
||||
placeholder={_t("Add a topic")}
|
||||
blurToCancel={false}
|
||||
initialValue={this._initialTopic}
|
||||
initialValue={this.state.topic}
|
||||
onValueChanged={this._onValueChanged}
|
||||
dir="auto" />
|
||||
);
|
||||
},
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2018 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.
|
||||
|
@ -18,6 +19,7 @@ import PropTypes from 'prop-types';
|
|||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import dis from '../../../dispatcher';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import sdk from '../../../index';
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'IncomingCallBox',
|
||||
|
@ -26,14 +28,16 @@ module.exports = React.createClass({
|
|||
incomingCall: PropTypes.object,
|
||||
},
|
||||
|
||||
onAnswerClick: function() {
|
||||
onAnswerClick: function(e) {
|
||||
e.stopPropagation();
|
||||
dis.dispatch({
|
||||
action: 'answer',
|
||||
room_id: this.props.incomingCall.roomId,
|
||||
});
|
||||
},
|
||||
|
||||
onRejectClick: function() {
|
||||
onRejectClick: function(e) {
|
||||
e.stopPropagation();
|
||||
dis.dispatch({
|
||||
action: 'hangup',
|
||||
room_id: this.props.incomingCall.roomId,
|
||||
|
@ -59,6 +63,7 @@ module.exports = React.createClass({
|
|||
}
|
||||
}
|
||||
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
return (
|
||||
<div className="mx_IncomingCallBox" id="incomingCallBox">
|
||||
<img className="mx_IncomingCallBox_chevron" src="img/chevron-left.png" width="9" height="16" />
|
||||
|
@ -67,14 +72,14 @@ module.exports = React.createClass({
|
|||
</div>
|
||||
<div className="mx_IncomingCallBox_buttons">
|
||||
<div className="mx_IncomingCallBox_buttons_cell">
|
||||
<div className="mx_IncomingCallBox_buttons_decline" onClick={this.onRejectClick}>
|
||||
<AccessibleButton className="mx_IncomingCallBox_buttons_decline" onClick={this.onRejectClick}>
|
||||
{ _t("Decline") }
|
||||
</div>
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
<div className="mx_IncomingCallBox_buttons_cell">
|
||||
<div className="mx_IncomingCallBox_buttons_accept" onClick={this.onAnswerClick}>
|
||||
<AccessibleButton className="mx_IncomingCallBox_buttons_accept" onClick={this.onAnswerClick}>
|
||||
{ _t("Accept") }
|
||||
</div>
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -381,9 +381,6 @@
|
|||
"Drop here to restore": "Drop here to restore",
|
||||
"Drop here to demote": "Drop here to demote",
|
||||
"Drop here to tag %(section)s": "Drop here to tag %(section)s",
|
||||
"Failed to set direct chat tag": "Failed to set direct chat tag",
|
||||
"Failed to remove tag %(tagName)s from room": "Failed to remove tag %(tagName)s from room",
|
||||
"Failed to add tag %(tagName)s to room": "Failed to add tag %(tagName)s to room",
|
||||
"Press <StartChatButton> to start a chat with someone": "Press <StartChatButton> to start a chat with someone",
|
||||
"You're not in any rooms yet! Press <CreateRoomButton> to make a room or <RoomDirectoryButton> to browse the directory": "You're not in any rooms yet! Press <CreateRoomButton> to make a room or <RoomDirectoryButton> to browse the directory",
|
||||
"Community Invites": "Community Invites",
|
||||
|
@ -500,8 +497,8 @@
|
|||
"Download %(text)s": "Download %(text)s",
|
||||
"Invalid file%(extra)s": "Invalid file%(extra)s",
|
||||
"Error decrypting image": "Error decrypting image",
|
||||
"Image '%(Body)s' cannot be displayed.": "Image '%(Body)s' cannot be displayed.",
|
||||
"This image cannot be displayed.": "This image cannot be displayed.",
|
||||
"Image '%(Body)s' cannot be displayed.": "Image '%(Body)s' cannot be displayed.",
|
||||
"Error decrypting video": "Error decrypting video",
|
||||
"%(senderDisplayName)s changed the avatar for %(roomName)s": "%(senderDisplayName)s changed the avatar for %(roomName)s",
|
||||
"%(senderDisplayName)s removed the room avatar.": "%(senderDisplayName)s removed the room avatar.",
|
||||
|
@ -826,6 +823,7 @@
|
|||
"Click to mute video": "Click to mute video",
|
||||
"Click to unmute audio": "Click to unmute audio",
|
||||
"Click to mute audio": "Click to mute audio",
|
||||
"Clear filter": "Clear filter",
|
||||
"Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.": "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.",
|
||||
"Tried to load a specific point in this room's timeline, but was unable to find it.": "Tried to load a specific point in this room's timeline, but was unable to find it.",
|
||||
"Failed to load timeline position": "Failed to load timeline position",
|
||||
|
@ -984,5 +982,8 @@
|
|||
"This process allows you to import encryption keys that you had previously exported from another Matrix client. You will then be able to decrypt any messages that the other client could decrypt.": "This process allows you to import encryption keys that you had previously exported from another Matrix client. You will then be able to decrypt any messages that the other client could decrypt.",
|
||||
"The export file will be protected with a passphrase. You should enter the passphrase here, to decrypt the file.": "The export file will be protected with a passphrase. You should enter the passphrase here, to decrypt the file.",
|
||||
"File to import": "File to import",
|
||||
"Import": "Import"
|
||||
"Import": "Import",
|
||||
"Failed to set direct chat tag": "Failed to set direct chat tag",
|
||||
"Failed to remove tag %(tagName)s from room": "Failed to remove tag %(tagName)s from room",
|
||||
"Failed to add tag %(tagName)s to room": "Failed to add tag %(tagName)s to room"
|
||||
}
|
||||
|
|
|
@ -35,13 +35,17 @@ module.exports = function(f, minIntervalMs) {
|
|||
|
||||
if (self.lastCall < now - minIntervalMs) {
|
||||
f.apply(this);
|
||||
self.lastCall = now;
|
||||
// get the time again now the function has finished, so if it
|
||||
// took longer than the delay time to execute, it doesn't
|
||||
// immediately become eligible to run again.
|
||||
self.lastCall = Date.now();
|
||||
} else if (self.scheduledCall === undefined) {
|
||||
self.scheduledCall = setTimeout(
|
||||
() => {
|
||||
self.scheduledCall = undefined;
|
||||
f.apply(this);
|
||||
self.lastCall = now;
|
||||
// get time again as per above
|
||||
self.lastCall = Date.now();
|
||||
},
|
||||
(self.lastCall + minIntervalMs) - now,
|
||||
);
|
||||
|
|
275
src/stores/RoomListStore.js
Normal file
275
src/stores/RoomListStore.js
Normal file
|
@ -0,0 +1,275 @@
|
|||
/*
|
||||
Copyright 2018 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 {Store} from 'flux/utils';
|
||||
import dis from '../dispatcher';
|
||||
import DMRoomMap from '../utils/DMRoomMap';
|
||||
import Unread from '../Unread';
|
||||
|
||||
/**
|
||||
* A class for storing application state for categorising rooms in
|
||||
* the RoomList.
|
||||
*/
|
||||
class RoomListStore extends Store {
|
||||
|
||||
static _listOrders = {
|
||||
"m.favourite": "manual",
|
||||
"im.vector.fake.invite": "recent",
|
||||
"im.vector.fake.recent": "recent",
|
||||
"im.vector.fake.direct": "recent",
|
||||
"m.lowpriority": "recent",
|
||||
"im.vector.fake.archived": "recent",
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super(dis);
|
||||
|
||||
this._init();
|
||||
this._getManualComparator = this._getManualComparator.bind(this);
|
||||
this._recentsComparator = this._recentsComparator.bind(this);
|
||||
}
|
||||
|
||||
_init() {
|
||||
// Initialise state
|
||||
this._state = {
|
||||
lists: {
|
||||
"im.vector.fake.invite": [],
|
||||
"m.favourite": [],
|
||||
"im.vector.fake.recent": [],
|
||||
"im.vector.fake.direct": [],
|
||||
"m.lowpriority": [],
|
||||
"im.vector.fake.archived": [],
|
||||
},
|
||||
ready: false,
|
||||
};
|
||||
}
|
||||
|
||||
_setState(newState) {
|
||||
this._state = Object.assign(this._state, newState);
|
||||
this.__emitChange();
|
||||
}
|
||||
|
||||
__onDispatch(payload) {
|
||||
switch (payload.action) {
|
||||
// Initialise state after initial sync
|
||||
case 'MatrixActions.sync': {
|
||||
if (!(payload.prevState !== 'PREPARED' && payload.state === 'PREPARED')) {
|
||||
break;
|
||||
}
|
||||
|
||||
this._matrixClient = payload.matrixClient;
|
||||
this._generateRoomLists();
|
||||
}
|
||||
break;
|
||||
case 'MatrixActions.Room.tags': {
|
||||
if (!this._state.ready) break;
|
||||
this._generateRoomLists();
|
||||
}
|
||||
break;
|
||||
case 'MatrixActions.Room.timeline': {
|
||||
if (!this._state.ready ||
|
||||
!payload.isLiveEvent ||
|
||||
!payload.isLiveUnfilteredRoomTimelineEvent ||
|
||||
!this._eventTriggersRecentReorder(payload.event)
|
||||
) break;
|
||||
this._generateRoomLists();
|
||||
}
|
||||
break;
|
||||
case 'MatrixActions.accountData': {
|
||||
if (payload.event_type !== 'm.direct') break;
|
||||
this._generateRoomLists();
|
||||
}
|
||||
break;
|
||||
case 'MatrixActions.RoomMember.membership': {
|
||||
if (!this._matrixClient || payload.member.userId !== this._matrixClient.credentials.userId) break;
|
||||
this._generateRoomLists();
|
||||
}
|
||||
break;
|
||||
// This could be a new room that we've been invited to, joined or created
|
||||
// we won't get a RoomMember.membership for these cases if we're not already
|
||||
// a member.
|
||||
case 'MatrixActions.Room': {
|
||||
if (!this._state.ready || !this._matrixClient.credentials.userId) break;
|
||||
this._generateRoomLists();
|
||||
}
|
||||
break;
|
||||
case 'RoomListActions.tagRoom.pending': {
|
||||
// XXX: we only show one optimistic update at any one time.
|
||||
// Ideally we should be making a list of in-flight requests
|
||||
// that are backed by transaction IDs. Until the js-sdk
|
||||
// supports this, we're stuck with only being able to use
|
||||
// the most recent optimistic update.
|
||||
this._generateRoomLists(payload.request);
|
||||
}
|
||||
break;
|
||||
case 'RoomListActions.tagRoom.failure': {
|
||||
// Reset state according to js-sdk
|
||||
this._generateRoomLists();
|
||||
}
|
||||
break;
|
||||
case 'on_logged_out': {
|
||||
// Reset state without pushing an update to the view, which generally assumes that
|
||||
// the matrix client isn't `null` and so causing a re-render will cause NPEs.
|
||||
this._init();
|
||||
this._matrixClient = null;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
_generateRoomLists(optimisticRequest) {
|
||||
const lists = {
|
||||
"im.vector.fake.invite": [],
|
||||
"m.favourite": [],
|
||||
"im.vector.fake.recent": [],
|
||||
"im.vector.fake.direct": [],
|
||||
"m.lowpriority": [],
|
||||
"im.vector.fake.archived": [],
|
||||
};
|
||||
|
||||
|
||||
const dmRoomMap = DMRoomMap.shared();
|
||||
|
||||
// If somehow we dispatched a RoomListActions.tagRoom.failure before a MatrixActions.sync
|
||||
if (!this._matrixClient) return;
|
||||
|
||||
this._matrixClient.getRooms().forEach((room, index) => {
|
||||
const me = room.getMember(this._matrixClient.credentials.userId);
|
||||
if (!me) return;
|
||||
|
||||
if (me.membership == "invite") {
|
||||
lists["im.vector.fake.invite"].push(room);
|
||||
} else if (me.membership == "join" || me.membership === "ban" ||
|
||||
(me.membership === "leave" && me.events.member.getSender() !== me.events.member.getStateKey())) {
|
||||
// Used to split rooms via tags
|
||||
let tagNames = Object.keys(room.tags);
|
||||
|
||||
if (optimisticRequest && optimisticRequest.room === room) {
|
||||
// Remove old tag
|
||||
tagNames = tagNames.filter((tagName) => tagName !== optimisticRequest.oldTag);
|
||||
// Add new tag
|
||||
if (optimisticRequest.newTag &&
|
||||
!tagNames.includes(optimisticRequest.newTag)
|
||||
) {
|
||||
tagNames.push(optimisticRequest.newTag);
|
||||
}
|
||||
}
|
||||
|
||||
if (tagNames.length) {
|
||||
for (let i = 0; i < tagNames.length; i++) {
|
||||
const tagName = tagNames[i];
|
||||
lists[tagName] = lists[tagName] || [];
|
||||
lists[tagName].push(room);
|
||||
}
|
||||
} else if (dmRoomMap.getUserIdForRoomId(room.roomId)) {
|
||||
// "Direct Message" rooms (that we're still in and that aren't otherwise tagged)
|
||||
lists["im.vector.fake.direct"].push(room);
|
||||
} else {
|
||||
lists["im.vector.fake.recent"].push(room);
|
||||
}
|
||||
} else if (me.membership === "leave") {
|
||||
lists["im.vector.fake.archived"].push(room);
|
||||
} else {
|
||||
console.error("unrecognised membership: " + me.membership + " - this should never happen");
|
||||
}
|
||||
});
|
||||
|
||||
Object.keys(lists).forEach((listKey) => {
|
||||
let comparator;
|
||||
switch (RoomListStore._listOrders[listKey]) {
|
||||
case "recent":
|
||||
comparator = this._recentsComparator;
|
||||
break;
|
||||
case "manual":
|
||||
default:
|
||||
comparator = this._getManualComparator(listKey, optimisticRequest);
|
||||
break;
|
||||
}
|
||||
lists[listKey].sort(comparator);
|
||||
});
|
||||
|
||||
this._setState({
|
||||
lists,
|
||||
ready: true, // Ready to receive updates via Room.tags events
|
||||
});
|
||||
}
|
||||
|
||||
_eventTriggersRecentReorder(ev) {
|
||||
return ev.getTs() && (
|
||||
Unread.eventTriggersUnreadCount(ev) ||
|
||||
ev.getSender() === this._matrixClient.credentials.userId
|
||||
);
|
||||
}
|
||||
|
||||
_tsOfNewestEvent(room) {
|
||||
for (let i = room.timeline.length - 1; i >= 0; --i) {
|
||||
const ev = room.timeline[i];
|
||||
if (this._eventTriggersRecentReorder(ev)) {
|
||||
return ev.getTs();
|
||||
}
|
||||
}
|
||||
|
||||
// we might only have events that don't trigger the unread indicator,
|
||||
// in which case use the oldest event even if normally it wouldn't count.
|
||||
// This is better than just assuming the last event was forever ago.
|
||||
if (room.timeline.length && room.timeline[0].getTs()) {
|
||||
return room.timeline[0].getTs();
|
||||
} else {
|
||||
return Number.MAX_SAFE_INTEGER;
|
||||
}
|
||||
}
|
||||
|
||||
_recentsComparator(roomA, roomB) {
|
||||
// XXX: We could use a cache here and update it when we see new
|
||||
// events that trigger a reorder
|
||||
return this._tsOfNewestEvent(roomB) - this._tsOfNewestEvent(roomA);
|
||||
}
|
||||
|
||||
_lexicographicalComparator(roomA, roomB) {
|
||||
return roomA.name > roomB.name ? 1 : -1;
|
||||
}
|
||||
|
||||
_getManualComparator(tagName, optimisticRequest) {
|
||||
return (roomA, roomB) => {
|
||||
let metaA = roomA.tags[tagName];
|
||||
let metaB = roomB.tags[tagName];
|
||||
|
||||
if (optimisticRequest && roomA === optimisticRequest.room) metaA = optimisticRequest.metaData;
|
||||
if (optimisticRequest && roomB === optimisticRequest.room) metaB = optimisticRequest.metaData;
|
||||
|
||||
// Make sure the room tag has an order element, if not set it to be the bottom
|
||||
const a = metaA.order;
|
||||
const b = metaB.order;
|
||||
|
||||
// Order undefined room tag orders to the bottom
|
||||
if (a === undefined && b !== undefined) {
|
||||
return 1;
|
||||
} else if (a !== undefined && b === undefined) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return a == b ? this._lexicographicalComparator(roomA, roomB) : ( a > b ? 1 : -1);
|
||||
};
|
||||
}
|
||||
|
||||
getRoomLists() {
|
||||
return this._state.lists;
|
||||
}
|
||||
}
|
||||
|
||||
if (global.singletonRoomListStore === undefined) {
|
||||
global.singletonRoomListStore = new RoomListStore();
|
||||
}
|
||||
export default global.singletonRoomListStore;
|
|
@ -55,6 +55,7 @@ class TagOrderStore extends Store {
|
|||
const tagOrderingEventContent = tagOrderingEvent ? tagOrderingEvent.getContent() : {};
|
||||
this._setState({
|
||||
orderedTagsAccountData: tagOrderingEventContent.tags || null,
|
||||
removedTagsAccountData: tagOrderingEventContent.removedTags || null,
|
||||
hasSynced: true,
|
||||
});
|
||||
this._updateOrderedTags();
|
||||
|
@ -70,6 +71,7 @@ class TagOrderStore extends Store {
|
|||
|
||||
this._setState({
|
||||
orderedTagsAccountData: payload.event_content ? payload.event_content.tags : null,
|
||||
removedTagsAccountData: payload.event_content ? payload.event_content.removedTags : null,
|
||||
});
|
||||
this._updateOrderedTags();
|
||||
break;
|
||||
|
@ -87,9 +89,18 @@ class TagOrderStore extends Store {
|
|||
// Optimistic update of a moved tag
|
||||
this._setState({
|
||||
orderedTags: payload.request.tags,
|
||||
removedTagsAccountData: payload.request.removedTags,
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'TagOrderActions.removeTag.pending': {
|
||||
// Optimistic update of a removed tag
|
||||
this._setState({
|
||||
removedTagsAccountData: payload.request.removedTags,
|
||||
});
|
||||
this._updateOrderedTags();
|
||||
break;
|
||||
}
|
||||
case 'select_tag': {
|
||||
let newTags = [];
|
||||
// Shift-click semantics
|
||||
|
@ -165,13 +176,15 @@ class TagOrderStore extends Store {
|
|||
_mergeGroupsAndTags() {
|
||||
const groupIds = this._state.joinedGroupIds || [];
|
||||
const tags = this._state.orderedTagsAccountData || [];
|
||||
const removedTags = new Set(this._state.removedTagsAccountData || []);
|
||||
|
||||
|
||||
const tagsToKeep = tags.filter(
|
||||
(t) => t[0] !== '+' || groupIds.includes(t),
|
||||
(t) => (t[0] !== '+' || groupIds.includes(t)) && !removedTags.has(t),
|
||||
);
|
||||
|
||||
const groupIdsToAdd = groupIds.filter(
|
||||
(groupId) => !tags.includes(groupId),
|
||||
(groupId) => !tags.includes(groupId) && !removedTags.has(groupId),
|
||||
);
|
||||
|
||||
return tagsToKeep.concat(groupIdsToAdd);
|
||||
|
@ -181,6 +194,10 @@ class TagOrderStore extends Store {
|
|||
return this._state.orderedTags;
|
||||
}
|
||||
|
||||
getRemovedTagsAccountData() {
|
||||
return this._state.removedTagsAccountData;
|
||||
}
|
||||
|
||||
getStoreId() {
|
||||
// Generate a random ID to prevent this store from clobbering its
|
||||
// state with redundant remote echos.
|
||||
|
|
Loading…
Reference in a new issue