Offline mode (#1723)
* Allow the client to run without connection to HS (i.e. using indexeddb) Allows running without having pushRules (it's safe not to have these when running from indexeddb sync.) This means rooms will be displayed with "unknown" notifcation state. This assumes anything that uses the push rules will get pushRule state again when the client starts syncing again. For recovering from being disconnected, * If an avatar has fallen back, try again on reconnection * If a thumbnail image failed to load, retry on reconnect * Load joined groups when reconnecting Update tests to give MELS a context.matrixClient
This commit is contained in:
parent
9625180fbe
commit
cf4ae681f4
7 changed files with 148 additions and 15 deletions
|
@ -34,7 +34,14 @@ export function getRoomNotifsState(roomId) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// for everything else, look at the room rule.
|
// for everything else, look at the room rule.
|
||||||
const roomRule = MatrixClientPeg.get().getRoomPushRule('global', roomId);
|
let roomRule = null;
|
||||||
|
try {
|
||||||
|
roomRule = MatrixClientPeg.get().getRoomPushRule('global', roomId);
|
||||||
|
} catch (err) {
|
||||||
|
// Possible that the client doesn't have pushRules yet. If so, it
|
||||||
|
// hasn't started eiher, so indicate that this room is not notifying.
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
// XXX: We have to assume the default is to notify for all messages
|
// XXX: We have to assume the default is to notify for all messages
|
||||||
// (in particular this will be 'wrong' for one to one rooms because
|
// (in particular this will be 'wrong' for one to one rooms because
|
||||||
|
@ -130,6 +137,11 @@ function setRoomNotifsStateUnmuted(roomId, newState) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function findOverrideMuteRule(roomId) {
|
function findOverrideMuteRule(roomId) {
|
||||||
|
if (!MatrixClientPeg.get().pushRules ||
|
||||||
|
!MatrixClientPeg.get().pushRules['global'] ||
|
||||||
|
!MatrixClientPeg.get().pushRules['global'].override) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
for (const rule of MatrixClientPeg.get().pushRules['global'].override) {
|
for (const rule of MatrixClientPeg.get().pushRules['global'].override) {
|
||||||
if (isRuleForRoom(roomId, rule)) {
|
if (isRuleForRoom(roomId, rule)) {
|
||||||
if (isMuteRule(rule) && rule.enabled) {
|
if (isMuteRule(rule) && rule.enabled) {
|
||||||
|
|
|
@ -44,6 +44,7 @@ const TagPanel = React.createClass({
|
||||||
componentWillMount: function() {
|
componentWillMount: function() {
|
||||||
this.unmounted = false;
|
this.unmounted = false;
|
||||||
this.context.matrixClient.on("Group.myMembership", this._onGroupMyMembership);
|
this.context.matrixClient.on("Group.myMembership", this._onGroupMyMembership);
|
||||||
|
this.context.matrixClient.on("sync", this.onClientSync);
|
||||||
|
|
||||||
this._tagOrderStoreToken = TagOrderStore.addListener(() => {
|
this._tagOrderStoreToken = TagOrderStore.addListener(() => {
|
||||||
if (this.unmounted) {
|
if (this.unmounted) {
|
||||||
|
@ -61,6 +62,7 @@ const TagPanel = React.createClass({
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
this.unmounted = true;
|
this.unmounted = true;
|
||||||
this.context.matrixClient.removeListener("Group.myMembership", this._onGroupMyMembership);
|
this.context.matrixClient.removeListener("Group.myMembership", this._onGroupMyMembership);
|
||||||
|
this.context.matrixClient.removeListener("sync", this.onClientSync);
|
||||||
if (this._filterStoreToken) {
|
if (this._filterStoreToken) {
|
||||||
this._filterStoreToken.remove();
|
this._filterStoreToken.remove();
|
||||||
}
|
}
|
||||||
|
@ -71,6 +73,16 @@ const TagPanel = React.createClass({
|
||||||
dis.dispatch(GroupActions.fetchJoinedGroups(this.context.matrixClient));
|
dis.dispatch(GroupActions.fetchJoinedGroups(this.context.matrixClient));
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onClientSync(syncState, prevState) {
|
||||||
|
// Consider the client reconnected if there is no error with syncing.
|
||||||
|
// This means the state could be RECONNECTING, SYNCING or PREPARED.
|
||||||
|
const reconnected = syncState !== "ERROR" && prevState !== syncState;
|
||||||
|
if (reconnected) {
|
||||||
|
// Load joined groups
|
||||||
|
dis.dispatch(GroupActions.fetchJoinedGroups(this.context.matrixClient));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
onClick(e) {
|
onClick(e) {
|
||||||
// Ignore clicks on children
|
// Ignore clicks on children
|
||||||
if (e.target !== e.currentTarget) return;
|
if (e.target !== e.currentTarget) return;
|
||||||
|
|
|
@ -16,6 +16,7 @@ limitations under the License.
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
import { MatrixClient } from 'matrix-js-sdk';
|
||||||
import AvatarLogic from '../../../Avatar';
|
import AvatarLogic from '../../../Avatar';
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
import AccessibleButton from '../elements/AccessibleButton';
|
import AccessibleButton from '../elements/AccessibleButton';
|
||||||
|
@ -36,6 +37,10 @@ module.exports = React.createClass({
|
||||||
defaultToInitialLetter: PropTypes.bool, // true to add default url
|
defaultToInitialLetter: PropTypes.bool, // true to add default url
|
||||||
},
|
},
|
||||||
|
|
||||||
|
contextTypes: {
|
||||||
|
matrixClient: PropTypes.instanceOf(MatrixClient),
|
||||||
|
},
|
||||||
|
|
||||||
getDefaultProps: function() {
|
getDefaultProps: function() {
|
||||||
return {
|
return {
|
||||||
width: 40,
|
width: 40,
|
||||||
|
@ -49,6 +54,16 @@ module.exports = React.createClass({
|
||||||
return this._getState(this.props);
|
return this._getState(this.props);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
componentWillMount() {
|
||||||
|
this.unmounted = false;
|
||||||
|
this.context.matrixClient.on('sync', this.onClientSync);
|
||||||
|
},
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
this.unmounted = true;
|
||||||
|
this.context.matrixClient.removeListener('sync', this.onClientSync);
|
||||||
|
},
|
||||||
|
|
||||||
componentWillReceiveProps: function(nextProps) {
|
componentWillReceiveProps: function(nextProps) {
|
||||||
// work out if we need to call setState (if the image URLs array has changed)
|
// work out if we need to call setState (if the image URLs array has changed)
|
||||||
const newState = this._getState(nextProps);
|
const newState = this._getState(nextProps);
|
||||||
|
@ -67,6 +82,23 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onClientSync(syncState, prevState) {
|
||||||
|
if (this.unmounted) return;
|
||||||
|
|
||||||
|
// Consider the client reconnected if there is no error with syncing.
|
||||||
|
// This means the state could be RECONNECTING, SYNCING or PREPARED.
|
||||||
|
const reconnected = syncState !== "ERROR" && prevState !== syncState;
|
||||||
|
if (reconnected &&
|
||||||
|
// Did we fall back?
|
||||||
|
this.state.urlsIndex > 0
|
||||||
|
) {
|
||||||
|
// Start from the highest priority URL again
|
||||||
|
this.setState({
|
||||||
|
urlsIndex: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
_getState: function(props) {
|
_getState: function(props) {
|
||||||
// work out the full set of urls to try to load. This is formed like so:
|
// work out the full set of urls to try to load. This is formed like so:
|
||||||
// imageUrls: [ props.url, props.urls, default image ]
|
// imageUrls: [ props.url, props.urls, default image ]
|
||||||
|
|
|
@ -17,7 +17,7 @@ import React from 'react';
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
import dis from '../../../dispatcher';
|
import dis from '../../../dispatcher';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { Room, RoomMember } from 'matrix-js-sdk';
|
import { Room, RoomMember, MatrixClient } from 'matrix-js-sdk';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||||
import { MATRIXTO_URL_PATTERN } from '../../../linkify-matrix';
|
import { MATRIXTO_URL_PATTERN } from '../../../linkify-matrix';
|
||||||
|
@ -61,6 +61,17 @@ const Pill = React.createClass({
|
||||||
shouldShowPillAvatar: PropTypes.bool,
|
shouldShowPillAvatar: PropTypes.bool,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
|
childContextTypes: {
|
||||||
|
matrixClient: PropTypes.instanceOf(MatrixClient),
|
||||||
|
},
|
||||||
|
|
||||||
|
getChildContext() {
|
||||||
|
return {
|
||||||
|
matrixClient: this._matrixClient,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
getInitialState() {
|
getInitialState() {
|
||||||
return {
|
return {
|
||||||
// ID/alias of the room/user
|
// ID/alias of the room/user
|
||||||
|
@ -135,6 +146,7 @@ const Pill = React.createClass({
|
||||||
|
|
||||||
componentWillMount() {
|
componentWillMount() {
|
||||||
this._unmounted = false;
|
this._unmounted = false;
|
||||||
|
this._matrixClient = MatrixClientPeg.get();
|
||||||
this.componentWillReceiveProps(this.props);
|
this.componentWillReceiveProps(this.props);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -18,8 +18,9 @@ limitations under the License.
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
import { MatrixClient } from 'matrix-js-sdk';
|
||||||
|
|
||||||
import MFileBody from './MFileBody';
|
import MFileBody from './MFileBody';
|
||||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
|
||||||
import ImageUtils from '../../../ImageUtils';
|
import ImageUtils from '../../../ImageUtils';
|
||||||
import Modal from '../../../Modal';
|
import Modal from '../../../Modal';
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
|
@ -40,15 +41,37 @@ module.exports = React.createClass({
|
||||||
onWidgetLoad: PropTypes.func.isRequired,
|
onWidgetLoad: PropTypes.func.isRequired,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
contextTypes: {
|
||||||
|
matrixClient: PropTypes.instanceOf(MatrixClient),
|
||||||
|
},
|
||||||
|
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
return {
|
return {
|
||||||
decryptedUrl: null,
|
decryptedUrl: null,
|
||||||
decryptedThumbnailUrl: null,
|
decryptedThumbnailUrl: null,
|
||||||
decryptedBlob: null,
|
decryptedBlob: null,
|
||||||
error: null,
|
error: null,
|
||||||
|
imgError: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
componentWillMount() {
|
||||||
|
this.unmounted = false;
|
||||||
|
this.context.matrixClient.on('sync', this.onClientSync);
|
||||||
|
},
|
||||||
|
|
||||||
|
onClientSync(syncState, prevState) {
|
||||||
|
if (this.unmounted) return;
|
||||||
|
// Consider the client reconnected if there is no error with syncing.
|
||||||
|
// This means the state could be RECONNECTING, SYNCING or PREPARED.
|
||||||
|
const reconnected = syncState !== "ERROR" && prevState !== syncState;
|
||||||
|
if (reconnected && this.state.imgError) {
|
||||||
|
// Load the image again
|
||||||
|
this.setState({
|
||||||
|
imgError: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
onClick: function onClick(ev) {
|
onClick: function onClick(ev) {
|
||||||
if (ev.button == 0 && !ev.metaKey) {
|
if (ev.button == 0 && !ev.metaKey) {
|
||||||
|
@ -97,12 +120,18 @@ module.exports = React.createClass({
|
||||||
imgElement.src = this._getThumbUrl();
|
imgElement.src = this._getThumbUrl();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onImageError: function() {
|
||||||
|
this.setState({
|
||||||
|
imgError: true,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
_getContentUrl: function() {
|
_getContentUrl: function() {
|
||||||
const content = this.props.mxEvent.getContent();
|
const content = this.props.mxEvent.getContent();
|
||||||
if (content.file !== undefined) {
|
if (content.file !== undefined) {
|
||||||
return this.state.decryptedUrl;
|
return this.state.decryptedUrl;
|
||||||
} else {
|
} else {
|
||||||
return MatrixClientPeg.get().mxcUrlToHttp(content.url);
|
return this.context.matrixClient.mxcUrlToHttp(content.url);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -115,7 +144,7 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
return this.state.decryptedUrl;
|
return this.state.decryptedUrl;
|
||||||
} else {
|
} else {
|
||||||
return MatrixClientPeg.get().mxcUrlToHttp(content.url, 800, 600);
|
return this.context.matrixClient.mxcUrlToHttp(content.url, 800, 600);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -156,7 +185,9 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillUnmount: function() {
|
componentWillUnmount: function() {
|
||||||
|
this.unmounted = true;
|
||||||
dis.unregister(this.dispatcherRef);
|
dis.unregister(this.dispatcherRef);
|
||||||
|
this.context.matrixClient.removeListener('sync', this.onClientSync);
|
||||||
},
|
},
|
||||||
|
|
||||||
onAction: function(payload) {
|
onAction: function(payload) {
|
||||||
|
@ -217,6 +248,14 @@ module.exports = React.createClass({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.state.imgError) {
|
||||||
|
return (
|
||||||
|
<span className="mx_MImageBody">
|
||||||
|
{ _t("This image cannot be displayed.") }
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const contentUrl = this._getContentUrl();
|
const contentUrl = this._getContentUrl();
|
||||||
let thumbUrl;
|
let thumbUrl;
|
||||||
if (this._isGif() && SettingsStore.getValue("autoplayGifsAndVideos")) {
|
if (this._isGif() && SettingsStore.getValue("autoplayGifsAndVideos")) {
|
||||||
|
@ -231,6 +270,7 @@ module.exports = React.createClass({
|
||||||
<a href={contentUrl} onClick={this.onClick}>
|
<a href={contentUrl} onClick={this.onClick}>
|
||||||
<img className="mx_MImageBody_thumbnail" src={thumbUrl} ref="image"
|
<img className="mx_MImageBody_thumbnail" src={thumbUrl} ref="image"
|
||||||
alt={content.body}
|
alt={content.body}
|
||||||
|
onError={this.onImageError}
|
||||||
onLoad={this.props.onWidgetLoad}
|
onLoad={this.props.onWidgetLoad}
|
||||||
onMouseEnter={this.onImageEnter}
|
onMouseEnter={this.onImageEnter}
|
||||||
onMouseLeave={this.onImageLeave} />
|
onMouseLeave={this.onImageLeave} />
|
||||||
|
|
|
@ -1,12 +1,15 @@
|
||||||
const expect = require('expect');
|
import expect from 'expect';
|
||||||
const React = require('react');
|
import React from 'react';
|
||||||
const ReactDOM = require("react-dom");
|
import ReactTestUtils from 'react-addons-test-utils';
|
||||||
const ReactTestUtils = require('react-addons-test-utils');
|
import sdk from 'matrix-react-sdk';
|
||||||
const sdk = require('matrix-react-sdk');
|
|
||||||
const MemberEventListSummary = sdk.getComponent('views.elements.MemberEventListSummary');
|
|
||||||
import * as languageHandler from '../../../../src/languageHandler';
|
import * as languageHandler from '../../../../src/languageHandler';
|
||||||
|
import * as testUtils from '../../../test-utils';
|
||||||
|
|
||||||
|
// Give MELS a matrixClient in its child context
|
||||||
|
const MemberEventListSummary = testUtils.wrapInMatrixClientContext(
|
||||||
|
sdk.getComponent('views.elements.MemberEventListSummary'),
|
||||||
|
);
|
||||||
|
|
||||||
const testUtils = require('../../../test-utils');
|
|
||||||
describe('MemberEventListSummary', function() {
|
describe('MemberEventListSummary', function() {
|
||||||
let sandbox;
|
let sandbox;
|
||||||
|
|
||||||
|
@ -113,7 +116,6 @@ describe('MemberEventListSummary', function() {
|
||||||
renderer.render(<MemberEventListSummary {...props} />);
|
renderer.render(<MemberEventListSummary {...props} />);
|
||||||
const result = renderer.getRenderOutput();
|
const result = renderer.getRenderOutput();
|
||||||
|
|
||||||
expect(result.type).toBe('div');
|
|
||||||
expect(result.props.children).toEqual([
|
expect(result.props.children).toEqual([
|
||||||
<div className="event_tile" key="event0">Expanded membership</div>,
|
<div className="event_tile" key="event0">Expanded membership</div>,
|
||||||
]);
|
]);
|
||||||
|
@ -136,7 +138,6 @@ describe('MemberEventListSummary', function() {
|
||||||
renderer.render(<MemberEventListSummary {...props} />);
|
renderer.render(<MemberEventListSummary {...props} />);
|
||||||
const result = renderer.getRenderOutput();
|
const result = renderer.getRenderOutput();
|
||||||
|
|
||||||
expect(result.type).toBe('div');
|
|
||||||
expect(result.props.children).toEqual([
|
expect(result.props.children).toEqual([
|
||||||
<div className="event_tile" key="event0">Expanded membership</div>,
|
<div className="event_tile" key="event0">Expanded membership</div>,
|
||||||
<div className="event_tile" key="event1">Expanded membership</div>,
|
<div className="event_tile" key="event1">Expanded membership</div>,
|
||||||
|
|
|
@ -2,7 +2,8 @@
|
||||||
|
|
||||||
import sinon from 'sinon';
|
import sinon from 'sinon';
|
||||||
import Promise from 'bluebird';
|
import Promise from 'bluebird';
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import peg from '../src/MatrixClientPeg';
|
import peg from '../src/MatrixClientPeg';
|
||||||
import dis from '../src/dispatcher';
|
import dis from '../src/dispatcher';
|
||||||
import jssdk from 'matrix-js-sdk';
|
import jssdk from 'matrix-js-sdk';
|
||||||
|
@ -265,3 +266,26 @@ export function getDispatchForStore(store) {
|
||||||
dis._isDispatching = false;
|
dis._isDispatching = false;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function wrapInMatrixClientContext(WrappedComponent) {
|
||||||
|
class Wrapper extends React.Component {
|
||||||
|
static childContextTypes = {
|
||||||
|
matrixClient: PropTypes.object,
|
||||||
|
}
|
||||||
|
|
||||||
|
getChildContext() {
|
||||||
|
return {
|
||||||
|
matrixClient: this._matrixClient,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillMount() {
|
||||||
|
this._matrixClient = peg.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return <WrappedComponent {...this.props} />;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Wrapper;
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue