mirror of https://github.com/vector-im/riot-web
				
				
				
			Merge branch 'develop' into travis/room-list/css-layout
						commit
						000c92a53f
					
				|  | @ -19,7 +19,7 @@ limitations under the License. | |||
| @import "./_font-sizes.scss"; | ||||
| 
 | ||||
| :root { | ||||
|     font-size: 15px; | ||||
|     font-size: 10px; | ||||
| } | ||||
| 
 | ||||
| html { | ||||
|  |  | |||
|  | @ -14,59 +14,59 @@ See the License for the specific language governing permissions and | |||
| limitations under the License. | ||||
| */ | ||||
| 
 | ||||
| $font-1px: 0.067rem; | ||||
| $font-1-5px: 0.100rem; | ||||
| $font-2px: 0.133rem; | ||||
| $font-3px: 0.200rem; | ||||
| $font-4px: 0.267rem; | ||||
| $font-5px: 0.333rem; | ||||
| $font-6px: 0.400rem; | ||||
| $font-7px: 0.467rem; | ||||
| $font-8px: 0.533rem; | ||||
| $font-9px: 0.600rem; | ||||
| $font-10px: 0.667rem; | ||||
| $font-10-4px: 0.693rem; | ||||
| $font-11px: 0.733rem; | ||||
| $font-12px: 0.800rem; | ||||
| $font-13px: 0.867rem; | ||||
| $font-14px: 0.933rem; | ||||
| $font-15px: 1.000rem; | ||||
| $font-16px: 1.067rem; | ||||
| $font-17px: 1.133rem; | ||||
| $font-18px: 1.200rem; | ||||
| $font-19px: 1.267rem; | ||||
| $font-20px: 1.3333333rem; | ||||
| $font-21px: 1.400rem; | ||||
| $font-22px: 1.467rem; | ||||
| $font-23px: 1.533rem; | ||||
| $font-24px: 1.600rem; | ||||
| $font-25px: 1.667rem; | ||||
| $font-26px: 1.733rem; | ||||
| $font-27px: 1.800rem; | ||||
| $font-28px: 1.867rem; | ||||
| $font-29px: 1.933rem; | ||||
| $font-30px: 2.000rem; | ||||
| $font-31px: 2.067rem; | ||||
| $font-32px: 2.133rem; | ||||
| $font-33px: 2.200rem; | ||||
| $font-34px: 2.267rem; | ||||
| $font-35px: 2.333rem; | ||||
| $font-36px: 2.400rem; | ||||
| $font-37px: 2.467rem; | ||||
| $font-38px: 2.533rem; | ||||
| $font-39px: 2.600rem; | ||||
| $font-40px: 2.667rem; | ||||
| $font-41px: 2.733rem; | ||||
| $font-42px: 2.800rem; | ||||
| $font-43px: 2.867rem; | ||||
| $font-44px: 2.933rem; | ||||
| $font-45px: 3.000rem; | ||||
| $font-46px: 3.067rem; | ||||
| $font-47px: 3.133rem; | ||||
| $font-48px: 3.200rem; | ||||
| $font-49px: 3.267rem; | ||||
| $font-50px: 3.333rem; | ||||
| $font-51px: 3.400rem; | ||||
| $font-52px: 3.467rem; | ||||
| $font-88px: 5.887rem; | ||||
| $font-400px: 26.667rem; | ||||
| $font-1px: 0.1rem; | ||||
| $font-1-5px: 0.15rem; | ||||
| $font-2px: 0.2rem; | ||||
| $font-3px: 0.3rem; | ||||
| $font-4px: 0.4rem; | ||||
| $font-5px: 0.5rem; | ||||
| $font-6px: 0.6rem; | ||||
| $font-7px: 0.7rem; | ||||
| $font-8px: 0.8rem; | ||||
| $font-9px: 0.9rem; | ||||
| $font-10px: 1.0rem; | ||||
| $font-10-4px: 1.04rem; | ||||
| $font-11px: 1.1rem; | ||||
| $font-12px: 1.2rem; | ||||
| $font-13px: 1.3rem; | ||||
| $font-14px: 1.4rem; | ||||
| $font-15px: 1.5rem; | ||||
| $font-16px: 1.6rem; | ||||
| $font-17px: 1.7rem; | ||||
| $font-18px: 1.8rem; | ||||
| $font-19px: 1.9rem; | ||||
| $font-20px: 2.0rem; | ||||
| $font-21px: 2.1rem; | ||||
| $font-22px: 2.2rem; | ||||
| $font-23px: 2.3rem; | ||||
| $font-24px: 2.4rem; | ||||
| $font-25px: 2.5rem; | ||||
| $font-26px: 2.6rem; | ||||
| $font-27px: 2.7rem; | ||||
| $font-28px: 2.8rem; | ||||
| $font-29px: 2.9rem; | ||||
| $font-30px: 3.0rem; | ||||
| $font-31px: 3.1rem; | ||||
| $font-32px: 3.2rem; | ||||
| $font-33px: 3.3rem; | ||||
| $font-34px: 3.4rem; | ||||
| $font-35px: 3.5rem; | ||||
| $font-36px: 3.6rem; | ||||
| $font-37px: 3.7rem; | ||||
| $font-38px: 3.8rem; | ||||
| $font-39px: 3.9rem; | ||||
| $font-40px: 4.0rem; | ||||
| $font-41px: 4.1rem; | ||||
| $font-42px: 4.2rem; | ||||
| $font-43px: 4.3rem; | ||||
| $font-44px: 4.4rem; | ||||
| $font-45px: 4.5rem; | ||||
| $font-46px: 4.6rem; | ||||
| $font-47px: 4.7rem; | ||||
| $font-48px: 4.8rem; | ||||
| $font-49px: 4.9rem; | ||||
| $font-50px: 5.0rem; | ||||
| $font-51px: 5.1rem; | ||||
| $font-52px: 5.2rem; | ||||
| $font-88px: 8.8rem; | ||||
| $font-400px: 40rem; | ||||
|  |  | |||
|  | @ -96,6 +96,17 @@ export default class WidgetMessaging { | |||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Tells the widget that it should terminate now. | ||||
|      * @returns {Promise<*>} Resolves when widget has acknowledged the message. | ||||
|      */ | ||||
|     terminate() { | ||||
|         return this.messageToWidget({ | ||||
|             api: OUTBOUND_API_NAME, | ||||
|             action: KnownWidgetActions.Terminate, | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Request a screenshot from a widget | ||||
|      * @return {Promise} To be resolved with screenshot data when it has been generated | ||||
|  |  | |||
|  | @ -39,6 +39,8 @@ import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore"; | |||
| import {aboveLeftOf, ContextMenu, ContextMenuButton} from "../../structures/ContextMenu"; | ||||
| import PersistedElement from "./PersistedElement"; | ||||
| import {WidgetType} from "../../../widgets/WidgetType"; | ||||
| import {Capability} from "../../../widgets/WidgetApi"; | ||||
| import {sleep} from "../../../utils/promise"; | ||||
| 
 | ||||
| const ALLOWED_APP_URL_SCHEMES = ['https:', 'http:']; | ||||
| const ENABLE_REACT_PERF = false; | ||||
|  | @ -341,23 +343,37 @@ export default class AppTile extends React.Component { | |||
|     /** | ||||
|      * Ends all widget interaction, such as cancelling calls and disabling webcams. | ||||
|      * @private | ||||
|      * @returns {Promise<*>} Resolves when the widget is terminated, or timeout passed. | ||||
|      */ | ||||
|     _endWidgetActions() { | ||||
|         // HACK: This is a really dirty way to ensure that Jitsi cleans up
 | ||||
|         // its hold on the webcam. Without this, the widget holds a media
 | ||||
|         // stream open, even after death. See https://github.com/vector-im/riot-web/issues/7351
 | ||||
|         if (this._appFrame.current) { | ||||
|             // In practice we could just do `+= ''` to trick the browser
 | ||||
|             // into thinking the URL changed, however I can foresee this
 | ||||
|             // being optimized out by a browser. Instead, we'll just point
 | ||||
|             // the iframe at a page that is reasonably safe to use in the
 | ||||
|             // event the iframe doesn't wink away.
 | ||||
|             // This is relative to where the Riot instance is located.
 | ||||
|             this._appFrame.current.src = 'about:blank'; | ||||
|         let terminationPromise; | ||||
| 
 | ||||
|         if (this._hasCapability(Capability.ReceiveTerminate)) { | ||||
|             // Wait for widget to terminate within a timeout
 | ||||
|             const timeout = 2000; | ||||
|             const messaging = ActiveWidgetStore.getWidgetMessaging(this.props.app.id); | ||||
|             terminationPromise = Promise.race([messaging.terminate(), sleep(timeout)]); | ||||
|         } else { | ||||
|             terminationPromise = Promise.resolve(); | ||||
|         } | ||||
| 
 | ||||
|         // Delete the widget from the persisted store for good measure.
 | ||||
|         PersistedElement.destroyElement(this._persistKey); | ||||
|         return terminationPromise.finally(() => { | ||||
|             // HACK: This is a really dirty way to ensure that Jitsi cleans up
 | ||||
|             // its hold on the webcam. Without this, the widget holds a media
 | ||||
|             // stream open, even after death. See https://github.com/vector-im/riot-web/issues/7351
 | ||||
|             if (this._appFrame.current) { | ||||
|                 // In practice we could just do `+= ''` to trick the browser
 | ||||
|                 // into thinking the URL changed, however I can foresee this
 | ||||
|                 // being optimized out by a browser. Instead, we'll just point
 | ||||
|                 // the iframe at a page that is reasonably safe to use in the
 | ||||
|                 // event the iframe doesn't wink away.
 | ||||
|                 // This is relative to where the Riot instance is located.
 | ||||
|                 this._appFrame.current.src = 'about:blank'; | ||||
|             } | ||||
| 
 | ||||
|             // Delete the widget from the persisted store for good measure.
 | ||||
|             PersistedElement.destroyElement(this._persistKey); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /* If user has permission to modify widgets, delete the widget, | ||||
|  | @ -381,12 +397,12 @@ export default class AppTile extends React.Component { | |||
|                     } | ||||
|                     this.setState({deleting: true}); | ||||
| 
 | ||||
|                     this._endWidgetActions(); | ||||
| 
 | ||||
|                     WidgetUtils.setRoomWidget( | ||||
|                         this.props.room.roomId, | ||||
|                         this.props.app.id, | ||||
|                     ).catch((e) => { | ||||
|                     this._endWidgetActions().then(() => { | ||||
|                         return WidgetUtils.setRoomWidget( | ||||
|                             this.props.room.roomId, | ||||
|                             this.props.app.id, | ||||
|                         ); | ||||
|                     }).catch((e) => { | ||||
|                         console.error('Failed to delete widget', e); | ||||
|                         const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); | ||||
| 
 | ||||
|  | @ -669,6 +685,17 @@ export default class AppTile extends React.Component { | |||
|     } | ||||
| 
 | ||||
|     _onPopoutWidgetClick() { | ||||
|         // Ensure Jitsi conferences are closed on pop-out, to not confuse the user to join them
 | ||||
|         // twice from the same computer, which Jitsi can have problems with (audio echo/gain-loop).
 | ||||
|         if (WidgetType.JITSI.matches(this.props.app.type) && this.props.show) { | ||||
|             this._endWidgetActions().then(() => { | ||||
|                 if (this._appFrame.current) { | ||||
|                     // Reload iframe
 | ||||
|                     this._appFrame.current.src = this._getRenderedUrl(); | ||||
|                     this.setState({}); | ||||
|                 } | ||||
|             }); | ||||
|         } | ||||
|         // Using Object.assign workaround as the following opens in a new window instead of a new tab.
 | ||||
|         // window.open(this._getPopoutUrl(), '_blank', 'noopener=yes');
 | ||||
|         Object.assign(document.createElement('a'), | ||||
|  |  | |||
|  | @ -23,7 +23,7 @@ import { _t } from '../../../languageHandler'; | |||
| import {formatDate} from '../../../DateUtils'; | ||||
| import Velociraptor from "../../../Velociraptor"; | ||||
| import * as sdk from "../../../index"; | ||||
| import {toRem} from "../../../utils/units"; | ||||
| import {toPx} from "../../../utils/units"; | ||||
| 
 | ||||
| let bounce = false; | ||||
| try { | ||||
|  | @ -149,7 +149,7 @@ export default createReactClass({ | |||
|             // start at the old height and in the old h pos
 | ||||
| 
 | ||||
|             startStyles.push({ top: startTopOffset+"px", | ||||
|                                left: toRem(oldInfo.left) }); | ||||
|                                left: toPx(oldInfo.left) }); | ||||
| 
 | ||||
|             const reorderTransitionOpts = { | ||||
|                 duration: 100, | ||||
|  | @ -182,7 +182,7 @@ export default createReactClass({ | |||
|         } | ||||
| 
 | ||||
|         const style = { | ||||
|             left: toRem(this.props.leftOffset), | ||||
|             left: toPx(this.props.leftOffset), | ||||
|             top: '0px', | ||||
|             visibility: this.props.hidden ? 'hidden' : 'visible', | ||||
|         }; | ||||
|  |  | |||
|  | @ -62,7 +62,7 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I | |||
|         super(props); | ||||
| 
 | ||||
|         this.state = { | ||||
|             fontSize: SettingsStore.getValue("fontSize", null).toString(), | ||||
|             fontSize: (SettingsStore.getValue("baseFontSize", null) + FontWatcher.SIZE_DIFF).toString(), | ||||
|             ...this.calculateThemeState(), | ||||
|             customThemeUrl: "", | ||||
|             customThemeMessage: {isError: false, text: ""}, | ||||
|  | @ -132,13 +132,13 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I | |||
| 
 | ||||
|     private onFontSizeChanged = (size: number): void => { | ||||
|         this.setState({fontSize: size.toString()}); | ||||
|         SettingsStore.setValue("fontSize", null, SettingLevel.DEVICE, size); | ||||
|         SettingsStore.setValue("baseFontSize", null, SettingLevel.DEVICE, size - FontWatcher.SIZE_DIFF); | ||||
|     }; | ||||
| 
 | ||||
|     private onValidateFontSize = async ({value}: Pick<IFieldState, "value">): Promise<IValidationResult> => { | ||||
|         const parsedSize = parseFloat(value); | ||||
|         const min = FontWatcher.MIN_SIZE; | ||||
|         const max = FontWatcher.MAX_SIZE; | ||||
|         const min = FontWatcher.MIN_SIZE + FontWatcher.SIZE_DIFF; | ||||
|         const max = FontWatcher.MAX_SIZE + FontWatcher.SIZE_DIFF; | ||||
| 
 | ||||
|         if (isNaN(parsedSize)) { | ||||
|             return {valid: false, feedback: _t("Size must be a number")}; | ||||
|  | @ -151,7 +151,13 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I | |||
|             }; | ||||
|         } | ||||
| 
 | ||||
|         SettingsStore.setValue("fontSize", null, SettingLevel.DEVICE, value); | ||||
|         SettingsStore.setValue( | ||||
|             "baseFontSize", | ||||
|             null, | ||||
|             SettingLevel.DEVICE, | ||||
|             parseInt(value, 10) - FontWatcher.SIZE_DIFF | ||||
|         ); | ||||
| 
 | ||||
|         return {valid: true, feedback: _t('Use between %(min)s pt and %(max)s pt', {min, max})}; | ||||
|     } | ||||
| 
 | ||||
|  |  | |||
|  | @ -145,6 +145,10 @@ export default async function sendBugReport(bugReportEndpoint: string, opts: IOp | |||
|     if (enabledLabs.length) { | ||||
|         body.append('enabled_labs', enabledLabs.join(', ')); | ||||
|     } | ||||
|     // if low bandwidth mode is enabled, say so over rageshake, it causes many issues
 | ||||
|     if (SettingsStore.getValue("lowBandwidth")) { | ||||
|         body.append("lowBandwidth", "enabled"); | ||||
|     } | ||||
| 
 | ||||
|     // add storage persistence/quota information
 | ||||
|     if (navigator.storage && navigator.storage.persisted) { | ||||
|  |  | |||
|  | @ -170,10 +170,10 @@ export const SETTINGS = { | |||
|         displayName: _td("Show info about bridges in room settings"), | ||||
|         default: false, | ||||
|     }, | ||||
|     "fontSize": { | ||||
|     "baseFontSize": { | ||||
|         displayName: _td("Font size"), | ||||
|         supportedLevels: LEVELS_ACCOUNT_SETTINGS, | ||||
|         default: 15, | ||||
|         default: 10, | ||||
|         controller: new FontSizeController(), | ||||
|     }, | ||||
|     "useCustomFontSize": { | ||||
|  |  | |||
|  | @ -20,8 +20,10 @@ import IWatcher from "./Watcher"; | |||
| import { toPx } from '../../utils/units'; | ||||
| 
 | ||||
| export class FontWatcher implements IWatcher { | ||||
|     public static readonly MIN_SIZE = 13; | ||||
|     public static readonly MAX_SIZE = 20; | ||||
|     public static readonly MIN_SIZE = 8; | ||||
|     public static readonly MAX_SIZE = 15; | ||||
|     // Externally we tell the user the font is size 15. Internally we use 10.
 | ||||
|     public static readonly SIZE_DIFF = 5; | ||||
| 
 | ||||
|     private dispatcherRef: string; | ||||
| 
 | ||||
|  | @ -30,7 +32,7 @@ export class FontWatcher implements IWatcher { | |||
|     } | ||||
| 
 | ||||
|     public start() { | ||||
|         this.setRootFontSize(SettingsStore.getValue("fontSize")); | ||||
|         this.setRootFontSize(SettingsStore.getValue("baseFontSize")); | ||||
|         this.dispatcherRef = dis.register(this.onAction); | ||||
|     } | ||||
| 
 | ||||
|  | @ -48,7 +50,7 @@ export class FontWatcher implements IWatcher { | |||
|         const fontSize = Math.max(Math.min(FontWatcher.MAX_SIZE, size), FontWatcher.MIN_SIZE); | ||||
| 
 | ||||
|         if (fontSize !== size) { | ||||
|             SettingsStore.setValue("fontSize", null, SettingLevel.Device, fontSize); | ||||
|             SettingsStore.setValue("baseFontSize", null, SettingLevel.DEVICE, fontSize); | ||||
|         } | ||||
|         (<HTMLElement>document.querySelector(":root")).style.fontSize = toPx(fontSize); | ||||
|     }; | ||||
|  |  | |||
|  | @ -74,29 +74,29 @@ gets applied to each category in a sub-sub-list fashion. This should result in t | |||
| being sorted alphabetically amongst each other as well as the grey rooms sorted amongst each other, but  | ||||
| collectively the tag will be sorted into categories with red being at the top. | ||||
| 
 | ||||
| <!-- TODO: Implement sticky rooms as described below --> | ||||
| ### Sticky rooms | ||||
| 
 | ||||
| The algorithm also has a concept of a 'sticky' room which is the room the user is currently viewing. | ||||
| The sticky room will remain in position on the room list regardless of other factors going on as typically | ||||
| clicking on a room will cause it to change categories into 'idle'. This is done by preserving N rooms | ||||
| above the selected room at all times, where N is the number of rooms above the selected rooms when it was | ||||
| selected. | ||||
| When the user visits a room, that room becomes 'sticky' in the list, regardless of ordering algorithm. | ||||
| From a code perspective, the underlying algorithm is not aware of a sticky room and instead the base class | ||||
| manages which room is sticky. This is to ensure that all algorithms handle it the same. | ||||
| 
 | ||||
| For example, if the user has 3 red rooms and selects the middle room, they will always see exactly one | ||||
| room above their selection at all times. If they receive another notification, and the tag ordering is  | ||||
| specified as Recent, they'll see the new notification go to the top position, and the one that was previously | ||||
| there fall behind the sticky room. | ||||
| The sticky flag is simply to say it will not move higher or lower down the list while it is active. For | ||||
| example, if using the importance algorithm, the room would naturally become idle once viewed and thus | ||||
| would normally fly down the list out of sight. The sticky room concept instead holds it in place, never | ||||
| letting it fly down until the user moves to another room. | ||||
| 
 | ||||
| The sticky room's category is technically 'idle' while being viewed and is explicitly pulled out of the | ||||
| tag sorting algorithm's input as it must maintain its position in the list. When the user moves to another | ||||
| room, the previous sticky room gets recalculated to determine which category it needs to be in as the user | ||||
| could have been scrolled up while new messages were received. | ||||
| Only one room can be sticky at a time. Room updates around the sticky room will still hold the sticky | ||||
| room in place. The best example of this is the importance algorithm: if the user has 3 red rooms and | ||||
| selects the middle room, they will see exactly one room above their selection at all times. If they | ||||
| receive another notification which causes the room to move into the topmost position, the room that was | ||||
| above the sticky room will move underneath to allow for the new room to take the top slot, maintaining | ||||
| the sticky room's position. | ||||
| 
 | ||||
| Further, the sticky room is not aware of category boundaries and thus the user can see a shift in what  | ||||
| kinds of rooms move around their selection. An example would be the user having 4 red rooms, the user  | ||||
| selecting the third room (leaving 2 above it), and then having the rooms above it read on another device.  | ||||
| This would result in 1 red room and 1 other kind of room above the sticky room as it will try to maintain  | ||||
| 2 rooms above the sticky room. | ||||
| Though only applicable to the importance algorithm, the sticky room is not aware of category boundaries  | ||||
| and thus the user can see a shift in what kinds of rooms move around their selection. An example would  | ||||
| be the user having 4 red rooms, the user selecting the third room (leaving 2 above it), and then having  | ||||
| the rooms above it read on another device. This would result in 1 red room and 1 other kind of room  | ||||
| above the sticky room as it will try to maintain 2 rooms above the sticky room. | ||||
| 
 | ||||
| An exception for the sticky room placement is when there's suddenly not enough rooms to maintain the placement | ||||
| exactly. This typically happens if the user selects a room and leaves enough rooms where it cannot maintain | ||||
|  |  | |||
|  | @ -29,6 +29,7 @@ import defaultDispatcher from "../../dispatcher/dispatcher"; | |||
| import { readReceiptChangeIsFor } from "../../utils/read-receipts"; | ||||
| import { IFilterCondition } from "./filters/IFilterCondition"; | ||||
| import { TagWatcher } from "./TagWatcher"; | ||||
| import RoomViewStore from "../RoomViewStore"; | ||||
| 
 | ||||
| interface IState { | ||||
|     tagsEnabled?: boolean; | ||||
|  | @ -62,6 +63,7 @@ export class RoomListStore2 extends AsyncStore<ActionPayload> { | |||
| 
 | ||||
|         this.checkEnabled(); | ||||
|         for (const settingName of this.watchedSettings) SettingsStore.monitorSetting(settingName, null); | ||||
|         RoomViewStore.addListener(this.onRVSUpdate); | ||||
|     } | ||||
| 
 | ||||
|     public get orderedLists(): ITagMap { | ||||
|  | @ -93,6 +95,23 @@ export class RoomListStore2 extends AsyncStore<ActionPayload> { | |||
|         this.setAlgorithmClass(); | ||||
|     } | ||||
| 
 | ||||
|     private onRVSUpdate = () => { | ||||
|         if (!this.enabled) return; // TODO: Remove enabled flag when RoomListStore2 takes over
 | ||||
|         if (!this.matrixClient) return; // We assume there won't be RVS updates without a client
 | ||||
| 
 | ||||
|         const activeRoomId = RoomViewStore.getRoomId(); | ||||
|         if (!activeRoomId && this.algorithm.stickyRoom) { | ||||
|             this.algorithm.stickyRoom = null; | ||||
|         } else if (activeRoomId) { | ||||
|             const activeRoom = this.matrixClient.getRoom(activeRoomId); | ||||
|             if (!activeRoom) throw new Error(`${activeRoomId} is current in RVS but missing from client`); | ||||
|             if (activeRoom !== this.algorithm.stickyRoom) { | ||||
|                 console.log(`Changing sticky room to ${activeRoomId}`); | ||||
|                 this.algorithm.stickyRoom = activeRoom; | ||||
|             } | ||||
|         } | ||||
|     }; | ||||
| 
 | ||||
|     protected async onDispatch(payload: ActionPayload) { | ||||
|         if (payload.action === 'MatrixActions.sync') { | ||||
|             // Filter out anything that isn't the first PREPARED sync.
 | ||||
|  | @ -110,6 +129,7 @@ export class RoomListStore2 extends AsyncStore<ActionPayload> { | |||
|             console.log("Regenerating room lists: Startup"); | ||||
|             await this.readAndCacheSettingsFromStore(); | ||||
|             await this.regenerateAllLists(); | ||||
|             this.onRVSUpdate(); // fake an RVS update to adjust sticky room, if needed
 | ||||
|         } | ||||
| 
 | ||||
|         // TODO: Remove this once the RoomListStore becomes default
 | ||||
|  | @ -145,13 +165,19 @@ export class RoomListStore2 extends AsyncStore<ActionPayload> { | |||
|             // First see if the receipt event is for our own user. If it was, trigger
 | ||||
|             // a room update (we probably read the room on a different device).
 | ||||
|             if (readReceiptChangeIsFor(payload.event, this.matrixClient)) { | ||||
|                 // TODO: Update room now that it's been read
 | ||||
|                 console.log(payload); | ||||
|                 console.log(`[RoomListDebug] Got own read receipt in ${payload.event.roomId}`); | ||||
|                 const room = this.matrixClient.getRoom(payload.event.roomId); | ||||
|                 if (!room) { | ||||
|                     console.warn(`Own read receipt was in unknown room ${payload.event.roomId}`); | ||||
|                     return; | ||||
|                 } | ||||
|                 await this.handleRoomUpdate(room, RoomUpdateCause.ReadReceipt); | ||||
|                 return; | ||||
|             } | ||||
|         } else if (payload.action === 'MatrixActions.Room.tags') { | ||||
|             // TODO: Update room from tags
 | ||||
|             console.log(payload); | ||||
|             const roomPayload = (<any>payload); // TODO: Type out the dispatcher types
 | ||||
|             console.log(`[RoomListDebug] Got tag change in ${roomPayload.room.roomId}`); | ||||
|             await this.handleRoomUpdate(roomPayload.room, RoomUpdateCause.PossibleTagChange); | ||||
|         } else if (payload.action === 'MatrixActions.Room.timeline') { | ||||
|             const eventPayload = (<any>payload); // TODO: Type out the dispatcher types
 | ||||
| 
 | ||||
|  | @ -189,26 +215,39 @@ export class RoomListStore2 extends AsyncStore<ActionPayload> { | |||
|             // cause inaccuracies with the list ordering. We may have to decrypt the last N messages of every room :(
 | ||||
|             await this.handleRoomUpdate(room, RoomUpdateCause.Timeline); | ||||
|         } else if (payload.action === 'MatrixActions.accountData' && payload.event_type === 'm.direct') { | ||||
|             // TODO: Update DMs
 | ||||
|             console.log(payload); | ||||
|             const eventPayload = (<any>payload); // TODO: Type out the dispatcher types
 | ||||
|             console.log(`[RoomListDebug] Received updated DM map`); | ||||
|             const dmMap = eventPayload.event.getContent(); | ||||
|             for (const userId of Object.keys(dmMap)) { | ||||
|                 const roomIds = dmMap[userId]; | ||||
|                 for (const roomId of roomIds) { | ||||
|                     const room = this.matrixClient.getRoom(roomId); | ||||
|                     if (!room) { | ||||
|                         console.warn(`${roomId} was found in DMs but the room is not in the store`); | ||||
|                         continue; | ||||
|                     } | ||||
| 
 | ||||
|                     // We expect this RoomUpdateCause to no-op if there's no change, and we don't expect
 | ||||
|                     // the user to have hundreds of rooms to update in one event. As such, we just hammer
 | ||||
|                     // away at updates until the problem is solved. If we were expecting more than a couple
 | ||||
|                     // of rooms to be updated at once, we would consider batching the rooms up.
 | ||||
|                     await this.handleRoomUpdate(room, RoomUpdateCause.PossibleTagChange); | ||||
|                 } | ||||
|             } | ||||
|         } else if (payload.action === 'MatrixActions.Room.myMembership') { | ||||
|             // TODO: Improve new room check
 | ||||
|             const membershipPayload = (<any>payload); // TODO: Type out the dispatcher types
 | ||||
|             if (!membershipPayload.oldMembership && membershipPayload.membership === "join") { | ||||
|             if (membershipPayload.oldMembership !== "join" && membershipPayload.membership === "join") { | ||||
|                 console.log(`[RoomListDebug] Handling new room ${membershipPayload.room.roomId}`); | ||||
|                 await this.algorithm.handleRoomUpdate(membershipPayload.room, RoomUpdateCause.NewRoom); | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             // TODO: Update room from membership change
 | ||||
|             console.log(payload); | ||||
|         } else if (payload.action === 'MatrixActions.Room') { | ||||
|             // TODO: Improve new room check
 | ||||
|             // const roomPayload = (<any>payload); // TODO: Type out the dispatcher types
 | ||||
|             // console.log(`[RoomListDebug] Handling new room ${roomPayload.room.roomId}`);
 | ||||
|             // await this.algorithm.handleRoomUpdate(roomPayload.room, RoomUpdateCause.NewRoom);
 | ||||
|         } else if (payload.action === 'view_room') { | ||||
|             // TODO: Update sticky room
 | ||||
|             console.log(payload); | ||||
|             // If it's not a join, it's transitioning into a different list (possibly historical)
 | ||||
|             if (membershipPayload.oldMembership !== membershipPayload.membership) { | ||||
|                 console.log(`[RoomListDebug] Handling membership change in ${membershipPayload.room.roomId}`); | ||||
|                 await this.algorithm.handleRoomUpdate(membershipPayload.room, RoomUpdateCause.PossibleTagChange); | ||||
|                 return; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|  |  | |||
|  | @ -22,6 +22,7 @@ import { ITagMap, ITagSortingMap } from "../models"; | |||
| import DMRoomMap from "../../../../utils/DMRoomMap"; | ||||
| import { FILTER_CHANGED, IFilterCondition } from "../../filters/IFilterCondition"; | ||||
| import { EventEmitter } from "events"; | ||||
| import { UPDATE_EVENT } from "../../../AsyncStore"; | ||||
| 
 | ||||
| // TODO: Add locking support to avoid concurrent writes?
 | ||||
| 
 | ||||
|  | @ -30,6 +31,12 @@ import { EventEmitter } from "events"; | |||
|  */ | ||||
| export const LIST_UPDATED_EVENT = "list_updated_event"; | ||||
| 
 | ||||
| interface IStickyRoom { | ||||
|     room: Room; | ||||
|     position: number; | ||||
|     tag: TagID; | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Represents a list ordering algorithm. This class will take care of tag | ||||
|  * management (which rooms go in which tags) and ask the implementation to | ||||
|  | @ -37,7 +44,9 @@ export const LIST_UPDATED_EVENT = "list_updated_event"; | |||
|  */ | ||||
| export abstract class Algorithm extends EventEmitter { | ||||
|     private _cachedRooms: ITagMap = {}; | ||||
|     private _cachedStickyRooms: ITagMap = {}; // a clone of the _cachedRooms, with the sticky room
 | ||||
|     private filteredRooms: ITagMap = {}; | ||||
|     private _stickyRoom: IStickyRoom = null; | ||||
| 
 | ||||
|     protected sortAlgorithms: ITagSortingMap; | ||||
|     protected rooms: Room[] = []; | ||||
|  | @ -51,6 +60,15 @@ export abstract class Algorithm extends EventEmitter { | |||
|         super(); | ||||
|     } | ||||
| 
 | ||||
|     public get stickyRoom(): Room { | ||||
|         return this._stickyRoom ? this._stickyRoom.room : null; | ||||
|     } | ||||
| 
 | ||||
|     public set stickyRoom(val: Room) { | ||||
|         // setters can't be async, so we call a private function to do the work
 | ||||
|         this.updateStickyRoom(val); | ||||
|     } | ||||
| 
 | ||||
|     protected get hasFilters(): boolean { | ||||
|         return this.allowedByFilter.size > 0; | ||||
|     } | ||||
|  | @ -58,9 +76,14 @@ export abstract class Algorithm extends EventEmitter { | |||
|     protected set cachedRooms(val: ITagMap) { | ||||
|         this._cachedRooms = val; | ||||
|         this.recalculateFilteredRooms(); | ||||
|         this.recalculateStickyRoom(); | ||||
|     } | ||||
| 
 | ||||
|     protected get cachedRooms(): ITagMap { | ||||
|         // 🐉 Here be dragons.
 | ||||
|         // Note: this is used by the underlying algorithm classes, so don't make it return
 | ||||
|         // the sticky room cache. If it ends up returning the sticky room cache, we end up
 | ||||
|         // corrupting our caches and confusing them.
 | ||||
|         return this._cachedRooms; | ||||
|     } | ||||
| 
 | ||||
|  | @ -94,6 +117,67 @@ export abstract class Algorithm extends EventEmitter { | |||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private async updateStickyRoom(val: Room) { | ||||
|         // Note throughout: We need async so we can wait for handleRoomUpdate() to do its thing,
 | ||||
|         // otherwise we risk duplicating rooms.
 | ||||
| 
 | ||||
|         // It's possible to have no selected room. In that case, clear the sticky room
 | ||||
|         if (!val) { | ||||
|             if (this._stickyRoom) { | ||||
|                 // Lie to the algorithm and re-add the room to the algorithm
 | ||||
|                 await this.handleRoomUpdate(this._stickyRoom.room, RoomUpdateCause.NewRoom); | ||||
|             } | ||||
|             this._stickyRoom = null; | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         // When we do have a room though, we expect to be able to find it
 | ||||
|         const tag = this.roomIdsToTags[val.roomId][0]; | ||||
|         if (!tag) throw new Error(`${val.roomId} does not belong to a tag and cannot be sticky`); | ||||
|         let position = this.cachedRooms[tag].indexOf(val); | ||||
|         if (position < 0) throw new Error(`${val.roomId} does not appear to be known and cannot be sticky`); | ||||
| 
 | ||||
|         // 🐉 Here be dragons.
 | ||||
|         // Before we can go through with lying to the underlying algorithm about a room
 | ||||
|         // we need to ensure that when we do we're ready for the innevitable sticky room
 | ||||
|         // update we'll receive. To prepare for that, we first remove the sticky room and
 | ||||
|         // recalculate the state ourselves so that when the underlying algorithm calls for
 | ||||
|         // the same thing it no-ops. After we're done calling the algorithm, we'll issue
 | ||||
|         // a new update for ourselves.
 | ||||
|         const lastStickyRoom = this._stickyRoom; | ||||
|         console.log(`Last sticky room:`, lastStickyRoom); | ||||
|         this._stickyRoom = null; | ||||
|         this.recalculateStickyRoom(); | ||||
| 
 | ||||
|         // When we do have the room, re-add the old room (if needed) to the algorithm
 | ||||
|         // and remove the sticky room from the algorithm. This is so the underlying
 | ||||
|         // algorithm doesn't try and confuse itself with the sticky room concept.
 | ||||
|         if (lastStickyRoom) { | ||||
|             // Lie to the algorithm and re-add the room to the algorithm
 | ||||
|             await this.handleRoomUpdate(lastStickyRoom.room, RoomUpdateCause.NewRoom); | ||||
|         } | ||||
|         // Lie to the algorithm and remove the room from it's field of view
 | ||||
|         await this.handleRoomUpdate(val, RoomUpdateCause.RoomRemoved); | ||||
| 
 | ||||
|         // Now that we're done lying to the algorithm, we need to update our position
 | ||||
|         // marker only if the user is moving further down the same list. If they're switching
 | ||||
|         // lists, or moving upwards, the position marker will splice in just fine but if
 | ||||
|         // they went downwards in the same list we'll be off by 1 due to the shifting rooms.
 | ||||
|         if (lastStickyRoom && lastStickyRoom.tag === tag && lastStickyRoom.position <= position) { | ||||
|             position++; | ||||
|         } | ||||
| 
 | ||||
|         this._stickyRoom = { | ||||
|             room: val, | ||||
|             position: position, | ||||
|             tag: tag, | ||||
|         }; | ||||
|         this.recalculateStickyRoom(); | ||||
| 
 | ||||
|         // Finally, trigger an update
 | ||||
|         this.emit(LIST_UPDATED_EVENT); | ||||
|     } | ||||
| 
 | ||||
|     protected recalculateFilteredRooms() { | ||||
|         if (!this.hasFilters) { | ||||
|             return; | ||||
|  | @ -154,6 +238,59 @@ export abstract class Algorithm extends EventEmitter { | |||
|         console.log(`[DEBUG] ${filteredRooms.length}/${rooms.length} rooms filtered into ${tagId}`); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Recalculate the sticky room position. If this is being called in relation to | ||||
|      * a specific tag being updated, it should be given to this function to optimize | ||||
|      * the call. | ||||
|      * @param updatedTag The tag that was updated, if possible. | ||||
|      */ | ||||
|     protected recalculateStickyRoom(updatedTag: TagID = null): void { | ||||
|         // 🐉 Here be dragons.
 | ||||
|         // This function does far too much for what it should, and is called by many places.
 | ||||
|         // Not only is this responsible for ensuring the sticky room is held in place at all
 | ||||
|         // times, it is also responsible for ensuring our clone of the cachedRooms is up to
 | ||||
|         // date. If either of these desyncs, we see weird behaviour like duplicated rooms,
 | ||||
|         // outdated lists, and other nonsensical issues that aren't necessarily obvious.
 | ||||
| 
 | ||||
|         if (!this._stickyRoom) { | ||||
|             // If there's no sticky room, just do nothing useful.
 | ||||
|             if (!!this._cachedStickyRooms) { | ||||
|                 // Clear the cache if we won't be needing it
 | ||||
|                 this._cachedStickyRooms = null; | ||||
|                 this.emit(LIST_UPDATED_EVENT); | ||||
|             } | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         if (!this._cachedStickyRooms || !updatedTag) { | ||||
|             console.log(`Generating clone of cached rooms for sticky room handling`); | ||||
|             const stickiedTagMap: ITagMap = {}; | ||||
|             for (const tagId of Object.keys(this.cachedRooms)) { | ||||
|                 stickiedTagMap[tagId] = this.cachedRooms[tagId].map(r => r); // shallow clone
 | ||||
|             } | ||||
|             this._cachedStickyRooms = stickiedTagMap; | ||||
|         } | ||||
| 
 | ||||
|         if (updatedTag) { | ||||
|             // Update the tag indicated by the caller, if possible. This is mostly to ensure
 | ||||
|             // our cache is up to date.
 | ||||
|             console.log(`Replacing cached sticky rooms for ${updatedTag}`); | ||||
|             this._cachedStickyRooms[updatedTag] = this.cachedRooms[updatedTag].map(r => r); // shallow clone
 | ||||
|         } | ||||
| 
 | ||||
|         // Now try to insert the sticky room, if we need to.
 | ||||
|         // We need to if there's no updated tag (we regenned the whole cache) or if the tag
 | ||||
|         // we might have updated from the cache is also our sticky room.
 | ||||
|         const sticky = this._stickyRoom; | ||||
|         if (!updatedTag || updatedTag === sticky.tag) { | ||||
|             console.log(`Inserting sticky room ${sticky.room.roomId} at position ${sticky.position} in ${sticky.tag}`); | ||||
|             this._cachedStickyRooms[sticky.tag].splice(sticky.position, 0, sticky.room); | ||||
|         } | ||||
| 
 | ||||
|         // Finally, trigger an update
 | ||||
|         this.emit(LIST_UPDATED_EVENT); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Asks the Algorithm to regenerate all lists, using the tags given | ||||
|      * as reference for which lists to generate and which way to generate | ||||
|  | @ -174,7 +311,7 @@ export abstract class Algorithm extends EventEmitter { | |||
|      */ | ||||
|     public getOrderedRooms(): ITagMap { | ||||
|         if (!this.hasFilters) { | ||||
|             return this.cachedRooms; | ||||
|             return this._cachedStickyRooms || this.cachedRooms; | ||||
|         } | ||||
|         return this.filteredRooms; | ||||
|     } | ||||
|  |  | |||
|  | @ -17,7 +17,7 @@ limitations under the License. | |||
| 
 | ||||
| import { Algorithm } from "./Algorithm"; | ||||
| import { Room } from "matrix-js-sdk/src/models/room"; | ||||
| import { DefaultTagID, RoomUpdateCause, TagID } from "../../models"; | ||||
| import { RoomUpdateCause, TagID } from "../../models"; | ||||
| import { ITagMap, SortAlgorithm } from "../models"; | ||||
| import { sortRoomsWithAlgorithm } from "../tag-sorting"; | ||||
| import * as Unread from '../../../../Unread'; | ||||
|  | @ -82,15 +82,14 @@ export class ImportanceAlgorithm extends Algorithm { | |||
|     // HOW THIS WORKS
 | ||||
|     // --------------
 | ||||
|     //
 | ||||
|     // This block of comments assumes you've read the README one level higher.
 | ||||
|     // This block of comments assumes you've read the README two levels higher.
 | ||||
|     // You should do that if you haven't already.
 | ||||
|     //
 | ||||
|     // Tags are fed into the algorithmic functions from the Algorithm superclass,
 | ||||
|     // which cause subsequent updates to the room list itself. Categories within
 | ||||
|     // those tags are tracked as index numbers within the array (zero = top), with
 | ||||
|     // each sticky room being tracked separately. Internally, the category index
 | ||||
|     // can be found from `this.indices[tag][category]` and the sticky room information
 | ||||
|     // from `this.stickyRoom`.
 | ||||
|     // can be found from `this.indices[tag][category]`.
 | ||||
|     //
 | ||||
|     // The room list store is always provided with the `this.cachedRooms` results, which are
 | ||||
|     // updated as needed and not recalculated often. For example, when a room needs to
 | ||||
|  | @ -102,17 +101,6 @@ export class ImportanceAlgorithm extends Algorithm { | |||
|         [tag: TagID]: ICategoryIndex; | ||||
|     } = {}; | ||||
| 
 | ||||
|     // TODO: Use this (see docs above)
 | ||||
|     private stickyRoom: { | ||||
|         roomId: string; | ||||
|         tag: TagID; | ||||
|         fromTop: number; | ||||
|     } = { | ||||
|         roomId: null, | ||||
|         tag: null, | ||||
|         fromTop: 0, | ||||
|     }; | ||||
| 
 | ||||
|     constructor() { | ||||
|         super(); | ||||
|         console.log("Constructed an ImportanceAlgorithm"); | ||||
|  | @ -189,12 +177,25 @@ export class ImportanceAlgorithm extends Algorithm { | |||
|     } | ||||
| 
 | ||||
|     public async handleRoomUpdate(room: Room, cause: RoomUpdateCause): Promise<boolean> { | ||||
|         if (cause === RoomUpdateCause.PossibleTagChange) { | ||||
|             // TODO: Be smarter and splice rather than regen the planet.
 | ||||
|             // TODO: No-op if no change.
 | ||||
|             await this.setKnownRooms(this.rooms); | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         if (cause === RoomUpdateCause.NewRoom) { | ||||
|             // TODO: Be smarter and insert rather than regen the planet.
 | ||||
|             await this.setKnownRooms([room, ...this.rooms]); | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         if (cause === RoomUpdateCause.RoomRemoved) { | ||||
|             // TODO: Be smarter and splice rather than regen the planet.
 | ||||
|             await this.setKnownRooms(this.rooms.filter(r => r !== room)); | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         let tags = this.roomIdsToTags[room.roomId]; | ||||
|         if (!tags) { | ||||
|             console.warn(`No tags known for "${room.name}" (${room.roomId})`); | ||||
|  | @ -251,6 +252,8 @@ export class ImportanceAlgorithm extends Algorithm { | |||
|             taggedRooms.splice(startIdx, 0, ...sorted); | ||||
| 
 | ||||
|             // Finally, flag that we've done something
 | ||||
|             this.recalculateFilteredRoomsForTag(tag); // update filter to re-sort the list
 | ||||
|             this.recalculateStickyRoom(tag); // update sticky room to make sure it appears if needed
 | ||||
|             changed = true; | ||||
|         } | ||||
|         return changed; | ||||
|  |  | |||
|  | @ -46,11 +46,17 @@ export class NaturalAlgorithm extends Algorithm { | |||
|             console.warn(`No tags known for "${room.name}" (${room.roomId})`); | ||||
|             return false; | ||||
|         } | ||||
|         let changed = false; | ||||
|         for (const tag of tags) { | ||||
|             // TODO: Optimize this loop to avoid useless operations
 | ||||
|             // For example, we can skip updates to alphabetic (sometimes) and manually ordered tags
 | ||||
|             this.cachedRooms[tag] = await sortRoomsWithAlgorithm(this.cachedRooms[tag], tag, this.sortAlgorithms[tag]); | ||||
| 
 | ||||
|             // Flag that we've done something
 | ||||
|             this.recalculateFilteredRoomsForTag(tag); // update filter to re-sort the list
 | ||||
|             this.recalculateStickyRoom(tag); // update sticky room to make sure it appears if needed
 | ||||
|             changed = true; | ||||
|         } | ||||
|         return true; // assume we changed something
 | ||||
|         return changed; | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -38,6 +38,8 @@ export type TagID = string | DefaultTagID; | |||
| 
 | ||||
| export enum RoomUpdateCause { | ||||
|     Timeline = "TIMELINE", | ||||
|     RoomRead = "ROOM_READ", // TODO: Use this.
 | ||||
|     PossibleTagChange = "POSSIBLE_TAG_CHANGE", | ||||
|     ReadReceipt = "READ_RECEIPT", | ||||
|     NewRoom = "NEW_ROOM", | ||||
|     RoomRemoved = "ROOM_REMOVED", | ||||
| } | ||||
|  |  | |||
|  | @ -421,6 +421,7 @@ export default class WidgetUtils { | |||
|         if (WidgetType.JITSI.matches(appType)) { | ||||
|             capWhitelist.push(Capability.AlwaysOnScreen); | ||||
|         } | ||||
|         capWhitelist.push(Capability.ReceiveTerminate); | ||||
| 
 | ||||
|         return capWhitelist; | ||||
|     } | ||||
|  |  | |||
|  | @ -18,11 +18,13 @@ limitations under the License. | |||
| // https://github.com/turt2live/matrix-dimension/blob/4f92d560266635e5a3c824606215b84e8c0b19f5/web/app/shared/services/scalar/scalar-widget.api.ts
 | ||||
| 
 | ||||
| import { randomString } from "matrix-js-sdk/src/randomstring"; | ||||
| import { EventEmitter } from "events"; | ||||
| 
 | ||||
| export enum Capability { | ||||
|     Screenshot = "m.capability.screenshot", | ||||
|     Sticker = "m.sticker", | ||||
|     AlwaysOnScreen = "m.always_on_screen", | ||||
|     ReceiveTerminate = "im.vector.receive_terminate", | ||||
| } | ||||
| 
 | ||||
| export enum KnownWidgetActions { | ||||
|  | @ -34,6 +36,7 @@ export enum KnownWidgetActions { | |||
|     ReceiveOpenIDCredentials = "openid_credentials", | ||||
|     SetAlwaysOnScreen = "set_always_on_screen", | ||||
|     ClientReady = "im.vector.ready", | ||||
|     Terminate = "im.vector.terminate", | ||||
| } | ||||
| 
 | ||||
| export type WidgetAction = KnownWidgetActions | string; | ||||
|  | @ -62,8 +65,13 @@ export interface FromWidgetRequest extends WidgetRequest { | |||
| 
 | ||||
| /** | ||||
|  * Handles Riot <--> Widget interactions for embedded/standalone widgets. | ||||
|  * | ||||
|  * Emitted events: | ||||
|  * - terminate(wait): client requested the widget to terminate. | ||||
|  *   Call the argument 'wait(promise)' to postpone the finalization until | ||||
|  *   the given promise resolves. | ||||
|  */ | ||||
| export class WidgetApi { | ||||
| export class WidgetApi extends EventEmitter { | ||||
|     private origin: string; | ||||
|     private inFlightRequests: { [requestId: string]: (reply: FromWidgetRequest) => void } = {}; | ||||
|     private readyPromise: Promise<any>; | ||||
|  | @ -75,6 +83,8 @@ export class WidgetApi { | |||
|     public expectingExplicitReady = false; | ||||
| 
 | ||||
|     constructor(currentUrl: string, private widgetId: string, private requestedCapabilities: string[]) { | ||||
|         super(); | ||||
| 
 | ||||
|         this.origin = new URL(currentUrl).origin; | ||||
| 
 | ||||
|         this.readyPromise = new Promise<any>(resolve => this.readyPromiseResolve = resolve); | ||||
|  | @ -98,6 +108,17 @@ export class WidgetApi { | |||
| 
 | ||||
|                     // Automatically acknowledge so we can move on
 | ||||
|                     this.replyToRequest(<ToWidgetRequest>payload, {}); | ||||
|                 } else if (payload.action === KnownWidgetActions.Terminate) { | ||||
|                     // Finalization needs to be async, so postpone with a promise
 | ||||
|                     let finalizePromise = Promise.resolve(); | ||||
|                     const wait = (promise) => { | ||||
|                         finalizePromise = finalizePromise.then(value => promise); | ||||
|                     }; | ||||
|                     this.emit('terminate', wait); | ||||
|                     Promise.resolve(finalizePromise).then(() => { | ||||
|                         // Acknowledge that we're shut down now
 | ||||
|                         this.replyToRequest(<ToWidgetRequest>payload, {}); | ||||
|                     }); | ||||
|                 } else { | ||||
|                     console.warn(`[WidgetAPI] Got unexpected action: ${payload.action}`); | ||||
|                 } | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue
	
	 Travis Ralston
						Travis Ralston