diff --git a/src/GroupAddressPicker.js b/src/GroupAddressPicker.js index 7da37b6df1..793f5c9227 100644 --- a/src/GroupAddressPicker.js +++ b/src/GroupAddressPicker.js @@ -21,6 +21,7 @@ import MultiInviter from './utils/MultiInviter'; import { _t } from './languageHandler'; import MatrixClientPeg from './MatrixClientPeg'; import GroupStore from './stores/GroupStore'; +import {allSettled} from "./utils/promise"; export function showGroupInviteDialog(groupId) { return new Promise((resolve, reject) => { @@ -118,7 +119,7 @@ function _onGroupInviteFinished(groupId, addrs) { function _onGroupAddRoomFinished(groupId, addrs, addRoomsPublicly) { const matrixClient = MatrixClientPeg.get(); const errorList = []; - return Promise.all(addrs.map((addr) => { + return allSettled(addrs.map((addr) => { return GroupStore .addRoomToGroup(groupId, addr.address, addRoomsPublicly) .catch(() => { errorList.push(addr.address); }) @@ -138,7 +139,7 @@ function _onGroupAddRoomFinished(groupId, addrs, addRoomsPublicly) { groups.push(groupId); return MatrixClientPeg.get().sendStateEvent(roomId, 'm.room.related_groups', {groups}, ''); } - }).reflect(); + }); })).then(() => { if (errorList.length === 0) { return; diff --git a/src/autocomplete/Autocompleter.js b/src/autocomplete/Autocompleter.js index af2744950f..c385e13878 100644 --- a/src/autocomplete/Autocompleter.js +++ b/src/autocomplete/Autocompleter.js @@ -27,6 +27,7 @@ import UserProvider from './UserProvider'; import EmojiProvider from './EmojiProvider'; import NotifProvider from './NotifProvider'; import Promise from 'bluebird'; +import {timeout} from "../utils/promise"; export type SelectionRange = { beginning: boolean, // whether the selection is in the first block of the editor or not @@ -77,23 +78,16 @@ export default class Autocompleter { while the user is interacting with the list, which makes it difficult to predict whether an action will actually do what is intended */ - const completionsList = await Promise.all( - // Array of inspections of promises that might timeout. Instead of allowing a - // single timeout to reject the Promise.all, reflect each one and once they've all - // settled, filter for the fulfilled ones - this.providers.map(provider => - provider - .getCompletions(query, selection, force) - .timeout(PROVIDER_COMPLETION_TIMEOUT) - .reflect(), - ), - ); + const completionsList = await Promise.all(this.providers.map(provider => { + return timeout(provider.getCompletions(query, selection, force), null, PROVIDER_COMPLETION_TIMEOUT); + })); + + // map then filter to maintain the index for the map-operation, for this.providers to line up + return completionsList.map((completions, i) => { + if (!completions || !completions.length) return; - return completionsList.filter( - (inspection) => inspection.isFulfilled(), - ).map((completionsState, i) => { return { - completions: completionsState.value(), + completions, provider: this.providers[i], /* the currently matched "command" the completer tried to complete @@ -102,6 +96,6 @@ export default class Autocompleter { */ command: this.providers[i].getCurrentCommand(query, selection, force), }; - }); + }).filter(Boolean); } } diff --git a/src/components/structures/GroupView.js b/src/components/structures/GroupView.js index 4056557a7c..776e7f0d6d 100644 --- a/src/components/structures/GroupView.js +++ b/src/components/structures/GroupView.js @@ -38,7 +38,7 @@ import FlairStore from '../../stores/FlairStore'; import { showGroupAddRoomDialog } from '../../GroupAddressPicker'; import {makeGroupPermalink, makeUserPermalink} from "../../utils/permalinks/Permalinks"; import {Group} from "matrix-js-sdk"; -import {sleep} from "../../utils/promise"; +import {allSettled, sleep} from "../../utils/promise"; const LONG_DESC_PLACEHOLDER = _td( `

HTML for your community's page

@@ -99,11 +99,10 @@ const CategoryRoomList = createReactClass({ onFinished: (success, addrs) => { if (!success) return; const errorList = []; - Promise.all(addrs.map((addr) => { + allSettled(addrs.map((addr) => { return GroupStore .addRoomToGroupSummary(this.props.groupId, addr.address) - .catch(() => { errorList.push(addr.address); }) - .reflect(); + .catch(() => { errorList.push(addr.address); }); })).then(() => { if (errorList.length === 0) { return; @@ -276,11 +275,10 @@ const RoleUserList = createReactClass({ onFinished: (success, addrs) => { if (!success) return; const errorList = []; - Promise.all(addrs.map((addr) => { + allSettled(addrs.map((addr) => { return GroupStore .addUserToGroupSummary(addr.address) - .catch(() => { errorList.push(addr.address); }) - .reflect(); + .catch(() => { errorList.push(addr.address); }); })).then(() => { if (errorList.length === 0) { return; diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index faa6f2564a..3dd5ea761e 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -1064,8 +1064,6 @@ const TimelinePanel = createReactClass({ }); }; - let prom = this._timelineWindow.load(eventId, INITIAL_SIZE); - // if we already have the event in question, TimelineWindow.load // returns a resolved promise. // @@ -1074,9 +1072,13 @@ const TimelinePanel = createReactClass({ // quite slow. So we detect that situation and shortcut straight to // calling _reloadEvents and updating the state. - if (prom.isFulfilled()) { + const timeline = this.props.timelineSet.getTimelineForEvent(eventId); + if (timeline) { + // This is a hot-path optimization by skipping a promise tick + // by repeating a no-op sync branch in TimelineSet.getTimelineForEvent & MatrixClient.getEventTimeline onLoaded(); } else { + const prom = this._timelineWindow.load(eventId, INITIAL_SIZE); this.setState({ events: [], liveEvents: [], @@ -1084,11 +1086,8 @@ const TimelinePanel = createReactClass({ canForwardPaginate: false, timelineLoading: true, }); - - prom = prom.then(onLoaded, onError); + prom.then(onLoaded, onError); } - - prom.done(); }, // handle the completion of a timeline load or localEchoUpdate, by diff --git a/src/utils/promise.js b/src/utils/promise.js index f7a2e7c3e7..e6e6ccb5c8 100644 --- a/src/utils/promise.js +++ b/src/utils/promise.js @@ -47,3 +47,20 @@ export function defer(): {resolve: () => {}, reject: () => {}, promise: Promise} return {resolve, reject, promise}; } + +// Promise.allSettled polyfill until browser support is stable in Firefox +export function allSettled(promises: Promise[]): {status: string, value?: any, reason?: any}[] { + if (Promise.allSettled) { + return Promise.allSettled(promises); + } + + return Promise.all(promises.map((promise) => { + return promise.then(value => ({ + status: "fulfilled", + value, + })).catch(reason => ({ + status: "rejected", + reason, + })); + })); +} diff --git a/test/components/views/dialogs/InteractiveAuthDialog-test.js b/test/components/views/dialogs/InteractiveAuthDialog-test.js index b14ea7c242..7612b43b48 100644 --- a/test/components/views/dialogs/InteractiveAuthDialog-test.js +++ b/test/components/views/dialogs/InteractiveAuthDialog-test.js @@ -26,6 +26,7 @@ import sdk from 'matrix-react-sdk'; import MatrixClientPeg from '../../../../src/MatrixClientPeg'; import * as test_utils from '../../../test-utils'; +import {sleep} from "../../../../src/utils/promise"; const InteractiveAuthDialog = sdk.getComponent( 'views.dialogs.InteractiveAuthDialog', @@ -107,7 +108,7 @@ describe('InteractiveAuthDialog', function() { }, })).toBe(true); // let the request complete - return Promise.delay(1); + return sleep(1); }).then(() => { expect(onFinished.callCount).toEqual(1); expect(onFinished.calledWithExactly(true, {a: 1})).toBe(true); diff --git a/test/components/views/rooms/MessageComposerInput-test.js b/test/components/views/rooms/MessageComposerInput-test.js index 1105a4af17..04a5c83ed0 100644 --- a/test/components/views/rooms/MessageComposerInput-test.js +++ b/test/components/views/rooms/MessageComposerInput-test.js @@ -8,6 +8,7 @@ import * as testUtils from '../../../test-utils'; import sdk from 'matrix-react-sdk'; const MessageComposerInput = sdk.getComponent('views.rooms.MessageComposerInput'); import MatrixClientPeg from '../../../../src/MatrixClientPeg'; +import {sleep} from "../../../../src/utils/promise"; function addTextToDraft(text) { const components = document.getElementsByClassName('public-DraftEditor-content'); @@ -49,7 +50,7 @@ xdescribe('MessageComposerInput', () => { // warnings // (please can we make the components not setState() after // they are unmounted?) - Promise.delay(10).done(() => { + sleep(10).done(() => { if (parentDiv) { ReactDOM.unmountComponentAtNode(parentDiv); parentDiv.remove(); diff --git a/test/end-to-end-tests/src/usecases/signup.js b/test/end-to-end-tests/src/usecases/signup.js index 391ce76441..fd2b948572 100644 --- a/test/end-to-end-tests/src/usecases/signup.js +++ b/test/end-to-end-tests/src/usecases/signup.js @@ -61,7 +61,7 @@ module.exports = async function signup(session, username, password, homeserver) await session.query(".mx_Field_valid #mx_RegistrationForm_password"); //check no errors const errorText = await session.tryGetInnertext('.mx_Login_error'); - assert.strictEqual(!!errorText, false); + assert.strictEqual(errorText, null); //submit form //await page.screenshot({path: "beforesubmit.png", fullPage: true}); await registerButton.click();