Merge branch 'develop' into travis/room-list/css-layout
commit
000c92a53f
|
@ -19,7 +19,7 @@ limitations under the License.
|
||||||
@import "./_font-sizes.scss";
|
@import "./_font-sizes.scss";
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
font-size: 15px;
|
font-size: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
html {
|
html {
|
||||||
|
|
|
@ -14,59 +14,59 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
$font-1px: 0.067rem;
|
$font-1px: 0.1rem;
|
||||||
$font-1-5px: 0.100rem;
|
$font-1-5px: 0.15rem;
|
||||||
$font-2px: 0.133rem;
|
$font-2px: 0.2rem;
|
||||||
$font-3px: 0.200rem;
|
$font-3px: 0.3rem;
|
||||||
$font-4px: 0.267rem;
|
$font-4px: 0.4rem;
|
||||||
$font-5px: 0.333rem;
|
$font-5px: 0.5rem;
|
||||||
$font-6px: 0.400rem;
|
$font-6px: 0.6rem;
|
||||||
$font-7px: 0.467rem;
|
$font-7px: 0.7rem;
|
||||||
$font-8px: 0.533rem;
|
$font-8px: 0.8rem;
|
||||||
$font-9px: 0.600rem;
|
$font-9px: 0.9rem;
|
||||||
$font-10px: 0.667rem;
|
$font-10px: 1.0rem;
|
||||||
$font-10-4px: 0.693rem;
|
$font-10-4px: 1.04rem;
|
||||||
$font-11px: 0.733rem;
|
$font-11px: 1.1rem;
|
||||||
$font-12px: 0.800rem;
|
$font-12px: 1.2rem;
|
||||||
$font-13px: 0.867rem;
|
$font-13px: 1.3rem;
|
||||||
$font-14px: 0.933rem;
|
$font-14px: 1.4rem;
|
||||||
$font-15px: 1.000rem;
|
$font-15px: 1.5rem;
|
||||||
$font-16px: 1.067rem;
|
$font-16px: 1.6rem;
|
||||||
$font-17px: 1.133rem;
|
$font-17px: 1.7rem;
|
||||||
$font-18px: 1.200rem;
|
$font-18px: 1.8rem;
|
||||||
$font-19px: 1.267rem;
|
$font-19px: 1.9rem;
|
||||||
$font-20px: 1.3333333rem;
|
$font-20px: 2.0rem;
|
||||||
$font-21px: 1.400rem;
|
$font-21px: 2.1rem;
|
||||||
$font-22px: 1.467rem;
|
$font-22px: 2.2rem;
|
||||||
$font-23px: 1.533rem;
|
$font-23px: 2.3rem;
|
||||||
$font-24px: 1.600rem;
|
$font-24px: 2.4rem;
|
||||||
$font-25px: 1.667rem;
|
$font-25px: 2.5rem;
|
||||||
$font-26px: 1.733rem;
|
$font-26px: 2.6rem;
|
||||||
$font-27px: 1.800rem;
|
$font-27px: 2.7rem;
|
||||||
$font-28px: 1.867rem;
|
$font-28px: 2.8rem;
|
||||||
$font-29px: 1.933rem;
|
$font-29px: 2.9rem;
|
||||||
$font-30px: 2.000rem;
|
$font-30px: 3.0rem;
|
||||||
$font-31px: 2.067rem;
|
$font-31px: 3.1rem;
|
||||||
$font-32px: 2.133rem;
|
$font-32px: 3.2rem;
|
||||||
$font-33px: 2.200rem;
|
$font-33px: 3.3rem;
|
||||||
$font-34px: 2.267rem;
|
$font-34px: 3.4rem;
|
||||||
$font-35px: 2.333rem;
|
$font-35px: 3.5rem;
|
||||||
$font-36px: 2.400rem;
|
$font-36px: 3.6rem;
|
||||||
$font-37px: 2.467rem;
|
$font-37px: 3.7rem;
|
||||||
$font-38px: 2.533rem;
|
$font-38px: 3.8rem;
|
||||||
$font-39px: 2.600rem;
|
$font-39px: 3.9rem;
|
||||||
$font-40px: 2.667rem;
|
$font-40px: 4.0rem;
|
||||||
$font-41px: 2.733rem;
|
$font-41px: 4.1rem;
|
||||||
$font-42px: 2.800rem;
|
$font-42px: 4.2rem;
|
||||||
$font-43px: 2.867rem;
|
$font-43px: 4.3rem;
|
||||||
$font-44px: 2.933rem;
|
$font-44px: 4.4rem;
|
||||||
$font-45px: 3.000rem;
|
$font-45px: 4.5rem;
|
||||||
$font-46px: 3.067rem;
|
$font-46px: 4.6rem;
|
||||||
$font-47px: 3.133rem;
|
$font-47px: 4.7rem;
|
||||||
$font-48px: 3.200rem;
|
$font-48px: 4.8rem;
|
||||||
$font-49px: 3.267rem;
|
$font-49px: 4.9rem;
|
||||||
$font-50px: 3.333rem;
|
$font-50px: 5.0rem;
|
||||||
$font-51px: 3.400rem;
|
$font-51px: 5.1rem;
|
||||||
$font-52px: 3.467rem;
|
$font-52px: 5.2rem;
|
||||||
$font-88px: 5.887rem;
|
$font-88px: 8.8rem;
|
||||||
$font-400px: 26.667rem;
|
$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
|
* Request a screenshot from a widget
|
||||||
* @return {Promise} To be resolved with screenshot data when it has been generated
|
* @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 {aboveLeftOf, ContextMenu, ContextMenuButton} from "../../structures/ContextMenu";
|
||||||
import PersistedElement from "./PersistedElement";
|
import PersistedElement from "./PersistedElement";
|
||||||
import {WidgetType} from "../../../widgets/WidgetType";
|
import {WidgetType} from "../../../widgets/WidgetType";
|
||||||
|
import {Capability} from "../../../widgets/WidgetApi";
|
||||||
|
import {sleep} from "../../../utils/promise";
|
||||||
|
|
||||||
const ALLOWED_APP_URL_SCHEMES = ['https:', 'http:'];
|
const ALLOWED_APP_URL_SCHEMES = ['https:', 'http:'];
|
||||||
const ENABLE_REACT_PERF = false;
|
const ENABLE_REACT_PERF = false;
|
||||||
|
@ -341,8 +343,21 @@ export default class AppTile extends React.Component {
|
||||||
/**
|
/**
|
||||||
* Ends all widget interaction, such as cancelling calls and disabling webcams.
|
* Ends all widget interaction, such as cancelling calls and disabling webcams.
|
||||||
* @private
|
* @private
|
||||||
|
* @returns {Promise<*>} Resolves when the widget is terminated, or timeout passed.
|
||||||
*/
|
*/
|
||||||
_endWidgetActions() {
|
_endWidgetActions() {
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
return terminationPromise.finally(() => {
|
||||||
// HACK: This is a really dirty way to ensure that Jitsi cleans up
|
// 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
|
// 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
|
// stream open, even after death. See https://github.com/vector-im/riot-web/issues/7351
|
||||||
|
@ -358,6 +373,7 @@ export default class AppTile extends React.Component {
|
||||||
|
|
||||||
// Delete the widget from the persisted store for good measure.
|
// Delete the widget from the persisted store for good measure.
|
||||||
PersistedElement.destroyElement(this._persistKey);
|
PersistedElement.destroyElement(this._persistKey);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/* If user has permission to modify widgets, delete the widget,
|
/* 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.setState({deleting: true});
|
||||||
|
|
||||||
this._endWidgetActions();
|
this._endWidgetActions().then(() => {
|
||||||
|
return WidgetUtils.setRoomWidget(
|
||||||
WidgetUtils.setRoomWidget(
|
|
||||||
this.props.room.roomId,
|
this.props.room.roomId,
|
||||||
this.props.app.id,
|
this.props.app.id,
|
||||||
).catch((e) => {
|
);
|
||||||
|
}).catch((e) => {
|
||||||
console.error('Failed to delete widget', e);
|
console.error('Failed to delete widget', e);
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
|
|
||||||
|
@ -669,6 +685,17 @@ export default class AppTile extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
_onPopoutWidgetClick() {
|
_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.
|
// Using Object.assign workaround as the following opens in a new window instead of a new tab.
|
||||||
// window.open(this._getPopoutUrl(), '_blank', 'noopener=yes');
|
// window.open(this._getPopoutUrl(), '_blank', 'noopener=yes');
|
||||||
Object.assign(document.createElement('a'),
|
Object.assign(document.createElement('a'),
|
||||||
|
|
|
@ -23,7 +23,7 @@ import { _t } from '../../../languageHandler';
|
||||||
import {formatDate} from '../../../DateUtils';
|
import {formatDate} from '../../../DateUtils';
|
||||||
import Velociraptor from "../../../Velociraptor";
|
import Velociraptor from "../../../Velociraptor";
|
||||||
import * as sdk from "../../../index";
|
import * as sdk from "../../../index";
|
||||||
import {toRem} from "../../../utils/units";
|
import {toPx} from "../../../utils/units";
|
||||||
|
|
||||||
let bounce = false;
|
let bounce = false;
|
||||||
try {
|
try {
|
||||||
|
@ -149,7 +149,7 @@ export default createReactClass({
|
||||||
// start at the old height and in the old h pos
|
// start at the old height and in the old h pos
|
||||||
|
|
||||||
startStyles.push({ top: startTopOffset+"px",
|
startStyles.push({ top: startTopOffset+"px",
|
||||||
left: toRem(oldInfo.left) });
|
left: toPx(oldInfo.left) });
|
||||||
|
|
||||||
const reorderTransitionOpts = {
|
const reorderTransitionOpts = {
|
||||||
duration: 100,
|
duration: 100,
|
||||||
|
@ -182,7 +182,7 @@ export default createReactClass({
|
||||||
}
|
}
|
||||||
|
|
||||||
const style = {
|
const style = {
|
||||||
left: toRem(this.props.leftOffset),
|
left: toPx(this.props.leftOffset),
|
||||||
top: '0px',
|
top: '0px',
|
||||||
visibility: this.props.hidden ? 'hidden' : 'visible',
|
visibility: this.props.hidden ? 'hidden' : 'visible',
|
||||||
};
|
};
|
||||||
|
|
|
@ -62,7 +62,7 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
fontSize: SettingsStore.getValue("fontSize", null).toString(),
|
fontSize: (SettingsStore.getValue("baseFontSize", null) + FontWatcher.SIZE_DIFF).toString(),
|
||||||
...this.calculateThemeState(),
|
...this.calculateThemeState(),
|
||||||
customThemeUrl: "",
|
customThemeUrl: "",
|
||||||
customThemeMessage: {isError: false, text: ""},
|
customThemeMessage: {isError: false, text: ""},
|
||||||
|
@ -132,13 +132,13 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
|
||||||
|
|
||||||
private onFontSizeChanged = (size: number): void => {
|
private onFontSizeChanged = (size: number): void => {
|
||||||
this.setState({fontSize: size.toString()});
|
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> => {
|
private onValidateFontSize = async ({value}: Pick<IFieldState, "value">): Promise<IValidationResult> => {
|
||||||
const parsedSize = parseFloat(value);
|
const parsedSize = parseFloat(value);
|
||||||
const min = FontWatcher.MIN_SIZE;
|
const min = FontWatcher.MIN_SIZE + FontWatcher.SIZE_DIFF;
|
||||||
const max = FontWatcher.MAX_SIZE;
|
const max = FontWatcher.MAX_SIZE + FontWatcher.SIZE_DIFF;
|
||||||
|
|
||||||
if (isNaN(parsedSize)) {
|
if (isNaN(parsedSize)) {
|
||||||
return {valid: false, feedback: _t("Size must be a number")};
|
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})};
|
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) {
|
if (enabledLabs.length) {
|
||||||
body.append('enabled_labs', enabledLabs.join(', '));
|
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
|
// add storage persistence/quota information
|
||||||
if (navigator.storage && navigator.storage.persisted) {
|
if (navigator.storage && navigator.storage.persisted) {
|
||||||
|
|
|
@ -170,10 +170,10 @@ export const SETTINGS = {
|
||||||
displayName: _td("Show info about bridges in room settings"),
|
displayName: _td("Show info about bridges in room settings"),
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
"fontSize": {
|
"baseFontSize": {
|
||||||
displayName: _td("Font size"),
|
displayName: _td("Font size"),
|
||||||
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
|
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
|
||||||
default: 15,
|
default: 10,
|
||||||
controller: new FontSizeController(),
|
controller: new FontSizeController(),
|
||||||
},
|
},
|
||||||
"useCustomFontSize": {
|
"useCustomFontSize": {
|
||||||
|
|
|
@ -20,8 +20,10 @@ import IWatcher from "./Watcher";
|
||||||
import { toPx } from '../../utils/units';
|
import { toPx } from '../../utils/units';
|
||||||
|
|
||||||
export class FontWatcher implements IWatcher {
|
export class FontWatcher implements IWatcher {
|
||||||
public static readonly MIN_SIZE = 13;
|
public static readonly MIN_SIZE = 8;
|
||||||
public static readonly MAX_SIZE = 20;
|
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;
|
private dispatcherRef: string;
|
||||||
|
|
||||||
|
@ -30,7 +32,7 @@ export class FontWatcher implements IWatcher {
|
||||||
}
|
}
|
||||||
|
|
||||||
public start() {
|
public start() {
|
||||||
this.setRootFontSize(SettingsStore.getValue("fontSize"));
|
this.setRootFontSize(SettingsStore.getValue("baseFontSize"));
|
||||||
this.dispatcherRef = dis.register(this.onAction);
|
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);
|
const fontSize = Math.max(Math.min(FontWatcher.MAX_SIZE, size), FontWatcher.MIN_SIZE);
|
||||||
|
|
||||||
if (fontSize !== 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);
|
(<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
|
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.
|
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.
|
When the user visits a room, that room becomes 'sticky' in the list, regardless of ordering algorithm.
|
||||||
The sticky room will remain in position on the room list regardless of other factors going on as typically
|
From a code perspective, the underlying algorithm is not aware of a sticky room and instead the base class
|
||||||
clicking on a room will cause it to change categories into 'idle'. This is done by preserving N rooms
|
manages which room is sticky. This is to ensure that all algorithms handle it the same.
|
||||||
above the selected room at all times, where N is the number of rooms above the selected rooms when it was
|
|
||||||
selected.
|
|
||||||
|
|
||||||
For example, if the user has 3 red rooms and selects the middle room, they will always see exactly one
|
The sticky flag is simply to say it will not move higher or lower down the list while it is active. For
|
||||||
room above their selection at all times. If they receive another notification, and the tag ordering is
|
example, if using the importance algorithm, the room would naturally become idle once viewed and thus
|
||||||
specified as Recent, they'll see the new notification go to the top position, and the one that was previously
|
would normally fly down the list out of sight. The sticky room concept instead holds it in place, never
|
||||||
there fall behind the sticky room.
|
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
|
Only one room can be sticky at a time. Room updates around the sticky room will still hold the sticky
|
||||||
tag sorting algorithm's input as it must maintain its position in the list. When the user moves to another
|
room in place. The best example of this is the importance algorithm: if the user has 3 red rooms and
|
||||||
room, the previous sticky room gets recalculated to determine which category it needs to be in as the user
|
selects the middle room, they will see exactly one room above their selection at all times. If they
|
||||||
could have been scrolled up while new messages were received.
|
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
|
Though only applicable to the importance algorithm, the sticky room is not aware of category boundaries
|
||||||
kinds of rooms move around their selection. An example would be the user having 4 red rooms, the user
|
and thus the user can see a shift in what kinds of rooms move around their selection. An example would
|
||||||
selecting the third room (leaving 2 above it), and then having the rooms above it read on another device.
|
be the user having 4 red rooms, the user selecting the third room (leaving 2 above it), and then having
|
||||||
This would result in 1 red room and 1 other kind of room above the sticky room as it will try to maintain
|
the rooms above it read on another device. This would result in 1 red room and 1 other kind of room
|
||||||
2 rooms above the sticky 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
|
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
|
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 { readReceiptChangeIsFor } from "../../utils/read-receipts";
|
||||||
import { IFilterCondition } from "./filters/IFilterCondition";
|
import { IFilterCondition } from "./filters/IFilterCondition";
|
||||||
import { TagWatcher } from "./TagWatcher";
|
import { TagWatcher } from "./TagWatcher";
|
||||||
|
import RoomViewStore from "../RoomViewStore";
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
tagsEnabled?: boolean;
|
tagsEnabled?: boolean;
|
||||||
|
@ -62,6 +63,7 @@ export class RoomListStore2 extends AsyncStore<ActionPayload> {
|
||||||
|
|
||||||
this.checkEnabled();
|
this.checkEnabled();
|
||||||
for (const settingName of this.watchedSettings) SettingsStore.monitorSetting(settingName, null);
|
for (const settingName of this.watchedSettings) SettingsStore.monitorSetting(settingName, null);
|
||||||
|
RoomViewStore.addListener(this.onRVSUpdate);
|
||||||
}
|
}
|
||||||
|
|
||||||
public get orderedLists(): ITagMap {
|
public get orderedLists(): ITagMap {
|
||||||
|
@ -93,6 +95,23 @@ export class RoomListStore2 extends AsyncStore<ActionPayload> {
|
||||||
this.setAlgorithmClass();
|
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) {
|
protected async onDispatch(payload: ActionPayload) {
|
||||||
if (payload.action === 'MatrixActions.sync') {
|
if (payload.action === 'MatrixActions.sync') {
|
||||||
// Filter out anything that isn't the first PREPARED 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");
|
console.log("Regenerating room lists: Startup");
|
||||||
await this.readAndCacheSettingsFromStore();
|
await this.readAndCacheSettingsFromStore();
|
||||||
await this.regenerateAllLists();
|
await this.regenerateAllLists();
|
||||||
|
this.onRVSUpdate(); // fake an RVS update to adjust sticky room, if needed
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Remove this once the RoomListStore becomes default
|
// 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
|
// 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).
|
// a room update (we probably read the room on a different device).
|
||||||
if (readReceiptChangeIsFor(payload.event, this.matrixClient)) {
|
if (readReceiptChangeIsFor(payload.event, this.matrixClient)) {
|
||||||
// TODO: Update room now that it's been read
|
console.log(`[RoomListDebug] Got own read receipt in ${payload.event.roomId}`);
|
||||||
console.log(payload);
|
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;
|
return;
|
||||||
}
|
}
|
||||||
} else if (payload.action === 'MatrixActions.Room.tags') {
|
} else if (payload.action === 'MatrixActions.Room.tags') {
|
||||||
// TODO: Update room from tags
|
const roomPayload = (<any>payload); // TODO: Type out the dispatcher types
|
||||||
console.log(payload);
|
console.log(`[RoomListDebug] Got tag change in ${roomPayload.room.roomId}`);
|
||||||
|
await this.handleRoomUpdate(roomPayload.room, RoomUpdateCause.PossibleTagChange);
|
||||||
} else if (payload.action === 'MatrixActions.Room.timeline') {
|
} else if (payload.action === 'MatrixActions.Room.timeline') {
|
||||||
const eventPayload = (<any>payload); // TODO: Type out the dispatcher types
|
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 :(
|
// cause inaccuracies with the list ordering. We may have to decrypt the last N messages of every room :(
|
||||||
await this.handleRoomUpdate(room, RoomUpdateCause.Timeline);
|
await this.handleRoomUpdate(room, RoomUpdateCause.Timeline);
|
||||||
} else if (payload.action === 'MatrixActions.accountData' && payload.event_type === 'm.direct') {
|
} else if (payload.action === 'MatrixActions.accountData' && payload.event_type === 'm.direct') {
|
||||||
// TODO: Update DMs
|
const eventPayload = (<any>payload); // TODO: Type out the dispatcher types
|
||||||
console.log(payload);
|
console.log(`[RoomListDebug] Received updated DM map`);
|
||||||
} else if (payload.action === 'MatrixActions.Room.myMembership') {
|
const dmMap = eventPayload.event.getContent();
|
||||||
// TODO: Improve new room check
|
for (const userId of Object.keys(dmMap)) {
|
||||||
const membershipPayload = (<any>payload); // TODO: Type out the dispatcher types
|
const roomIds = dmMap[userId];
|
||||||
if (!membershipPayload.oldMembership && membershipPayload.membership === "join") {
|
for (const roomId of roomIds) {
|
||||||
console.log(`[RoomListDebug] Handling new room ${membershipPayload.room.roomId}`);
|
const room = this.matrixClient.getRoom(roomId);
|
||||||
await this.algorithm.handleRoomUpdate(membershipPayload.room, RoomUpdateCause.NewRoom);
|
if (!room) {
|
||||||
|
console.warn(`${roomId} was found in DMs but the room is not in the store`);
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Update room from membership change
|
// We expect this RoomUpdateCause to no-op if there's no change, and we don't expect
|
||||||
console.log(payload);
|
// the user to have hundreds of rooms to update in one event. As such, we just hammer
|
||||||
} else if (payload.action === 'MatrixActions.Room') {
|
// away at updates until the problem is solved. If we were expecting more than a couple
|
||||||
// TODO: Improve new room check
|
// of rooms to be updated at once, we would consider batching the rooms up.
|
||||||
// const roomPayload = (<any>payload); // TODO: Type out the dispatcher types
|
await this.handleRoomUpdate(room, RoomUpdateCause.PossibleTagChange);
|
||||||
// console.log(`[RoomListDebug] Handling new room ${roomPayload.room.roomId}`);
|
}
|
||||||
// await this.algorithm.handleRoomUpdate(roomPayload.room, RoomUpdateCause.NewRoom);
|
}
|
||||||
} else if (payload.action === 'view_room') {
|
} else if (payload.action === 'MatrixActions.Room.myMembership') {
|
||||||
// TODO: Update sticky room
|
const membershipPayload = (<any>payload); // TODO: Type out the dispatcher types
|
||||||
console.log(payload);
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 DMRoomMap from "../../../../utils/DMRoomMap";
|
||||||
import { FILTER_CHANGED, IFilterCondition } from "../../filters/IFilterCondition";
|
import { FILTER_CHANGED, IFilterCondition } from "../../filters/IFilterCondition";
|
||||||
import { EventEmitter } from "events";
|
import { EventEmitter } from "events";
|
||||||
|
import { UPDATE_EVENT } from "../../../AsyncStore";
|
||||||
|
|
||||||
// TODO: Add locking support to avoid concurrent writes?
|
// TODO: Add locking support to avoid concurrent writes?
|
||||||
|
|
||||||
|
@ -30,6 +31,12 @@ import { EventEmitter } from "events";
|
||||||
*/
|
*/
|
||||||
export const LIST_UPDATED_EVENT = "list_updated_event";
|
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
|
* Represents a list ordering algorithm. This class will take care of tag
|
||||||
* management (which rooms go in which tags) and ask the implementation to
|
* 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 {
|
export abstract class Algorithm extends EventEmitter {
|
||||||
private _cachedRooms: ITagMap = {};
|
private _cachedRooms: ITagMap = {};
|
||||||
|
private _cachedStickyRooms: ITagMap = {}; // a clone of the _cachedRooms, with the sticky room
|
||||||
private filteredRooms: ITagMap = {};
|
private filteredRooms: ITagMap = {};
|
||||||
|
private _stickyRoom: IStickyRoom = null;
|
||||||
|
|
||||||
protected sortAlgorithms: ITagSortingMap;
|
protected sortAlgorithms: ITagSortingMap;
|
||||||
protected rooms: Room[] = [];
|
protected rooms: Room[] = [];
|
||||||
|
@ -51,6 +60,15 @@ export abstract class Algorithm extends EventEmitter {
|
||||||
super();
|
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 {
|
protected get hasFilters(): boolean {
|
||||||
return this.allowedByFilter.size > 0;
|
return this.allowedByFilter.size > 0;
|
||||||
}
|
}
|
||||||
|
@ -58,9 +76,14 @@ export abstract class Algorithm extends EventEmitter {
|
||||||
protected set cachedRooms(val: ITagMap) {
|
protected set cachedRooms(val: ITagMap) {
|
||||||
this._cachedRooms = val;
|
this._cachedRooms = val;
|
||||||
this.recalculateFilteredRooms();
|
this.recalculateFilteredRooms();
|
||||||
|
this.recalculateStickyRoom();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected get cachedRooms(): ITagMap {
|
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;
|
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() {
|
protected recalculateFilteredRooms() {
|
||||||
if (!this.hasFilters) {
|
if (!this.hasFilters) {
|
||||||
return;
|
return;
|
||||||
|
@ -154,6 +238,59 @@ export abstract class Algorithm extends EventEmitter {
|
||||||
console.log(`[DEBUG] ${filteredRooms.length}/${rooms.length} rooms filtered into ${tagId}`);
|
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
|
* Asks the Algorithm to regenerate all lists, using the tags given
|
||||||
* as reference for which lists to generate and which way to generate
|
* 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 {
|
public getOrderedRooms(): ITagMap {
|
||||||
if (!this.hasFilters) {
|
if (!this.hasFilters) {
|
||||||
return this.cachedRooms;
|
return this._cachedStickyRooms || this.cachedRooms;
|
||||||
}
|
}
|
||||||
return this.filteredRooms;
|
return this.filteredRooms;
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,7 +17,7 @@ limitations under the License.
|
||||||
|
|
||||||
import { Algorithm } from "./Algorithm";
|
import { Algorithm } from "./Algorithm";
|
||||||
import { Room } from "matrix-js-sdk/src/models/room";
|
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 { ITagMap, SortAlgorithm } from "../models";
|
||||||
import { sortRoomsWithAlgorithm } from "../tag-sorting";
|
import { sortRoomsWithAlgorithm } from "../tag-sorting";
|
||||||
import * as Unread from '../../../../Unread';
|
import * as Unread from '../../../../Unread';
|
||||||
|
@ -82,15 +82,14 @@ export class ImportanceAlgorithm extends Algorithm {
|
||||||
// HOW THIS WORKS
|
// 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.
|
// You should do that if you haven't already.
|
||||||
//
|
//
|
||||||
// Tags are fed into the algorithmic functions from the Algorithm superclass,
|
// Tags are fed into the algorithmic functions from the Algorithm superclass,
|
||||||
// which cause subsequent updates to the room list itself. Categories within
|
// which cause subsequent updates to the room list itself. Categories within
|
||||||
// those tags are tracked as index numbers within the array (zero = top), with
|
// those tags are tracked as index numbers within the array (zero = top), with
|
||||||
// each sticky room being tracked separately. Internally, the category index
|
// each sticky room being tracked separately. Internally, the category index
|
||||||
// can be found from `this.indices[tag][category]` and the sticky room information
|
// can be found from `this.indices[tag][category]`.
|
||||||
// from `this.stickyRoom`.
|
|
||||||
//
|
//
|
||||||
// The room list store is always provided with the `this.cachedRooms` results, which are
|
// 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
|
// 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;
|
[tag: TagID]: ICategoryIndex;
|
||||||
} = {};
|
} = {};
|
||||||
|
|
||||||
// TODO: Use this (see docs above)
|
|
||||||
private stickyRoom: {
|
|
||||||
roomId: string;
|
|
||||||
tag: TagID;
|
|
||||||
fromTop: number;
|
|
||||||
} = {
|
|
||||||
roomId: null,
|
|
||||||
tag: null,
|
|
||||||
fromTop: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
console.log("Constructed an ImportanceAlgorithm");
|
console.log("Constructed an ImportanceAlgorithm");
|
||||||
|
@ -189,12 +177,25 @@ export class ImportanceAlgorithm extends Algorithm {
|
||||||
}
|
}
|
||||||
|
|
||||||
public async handleRoomUpdate(room: Room, cause: RoomUpdateCause): Promise<boolean> {
|
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) {
|
if (cause === RoomUpdateCause.NewRoom) {
|
||||||
// TODO: Be smarter and insert rather than regen the planet.
|
// TODO: Be smarter and insert rather than regen the planet.
|
||||||
await this.setKnownRooms([room, ...this.rooms]);
|
await this.setKnownRooms([room, ...this.rooms]);
|
||||||
return;
|
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];
|
let tags = this.roomIdsToTags[room.roomId];
|
||||||
if (!tags) {
|
if (!tags) {
|
||||||
console.warn(`No tags known for "${room.name}" (${room.roomId})`);
|
console.warn(`No tags known for "${room.name}" (${room.roomId})`);
|
||||||
|
@ -251,6 +252,8 @@ export class ImportanceAlgorithm extends Algorithm {
|
||||||
taggedRooms.splice(startIdx, 0, ...sorted);
|
taggedRooms.splice(startIdx, 0, ...sorted);
|
||||||
|
|
||||||
// Finally, flag that we've done something
|
// 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;
|
changed = true;
|
||||||
}
|
}
|
||||||
return changed;
|
return changed;
|
||||||
|
|
|
@ -46,11 +46,17 @@ export class NaturalAlgorithm extends Algorithm {
|
||||||
console.warn(`No tags known for "${room.name}" (${room.roomId})`);
|
console.warn(`No tags known for "${room.name}" (${room.roomId})`);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
let changed = false;
|
||||||
for (const tag of tags) {
|
for (const tag of tags) {
|
||||||
// TODO: Optimize this loop to avoid useless operations
|
// TODO: Optimize this loop to avoid useless operations
|
||||||
// For example, we can skip updates to alphabetic (sometimes) and manually ordered tags
|
// 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]);
|
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 {
|
export enum RoomUpdateCause {
|
||||||
Timeline = "TIMELINE",
|
Timeline = "TIMELINE",
|
||||||
RoomRead = "ROOM_READ", // TODO: Use this.
|
PossibleTagChange = "POSSIBLE_TAG_CHANGE",
|
||||||
|
ReadReceipt = "READ_RECEIPT",
|
||||||
NewRoom = "NEW_ROOM",
|
NewRoom = "NEW_ROOM",
|
||||||
|
RoomRemoved = "ROOM_REMOVED",
|
||||||
}
|
}
|
||||||
|
|
|
@ -421,6 +421,7 @@ export default class WidgetUtils {
|
||||||
if (WidgetType.JITSI.matches(appType)) {
|
if (WidgetType.JITSI.matches(appType)) {
|
||||||
capWhitelist.push(Capability.AlwaysOnScreen);
|
capWhitelist.push(Capability.AlwaysOnScreen);
|
||||||
}
|
}
|
||||||
|
capWhitelist.push(Capability.ReceiveTerminate);
|
||||||
|
|
||||||
return capWhitelist;
|
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
|
// 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 { randomString } from "matrix-js-sdk/src/randomstring";
|
||||||
|
import { EventEmitter } from "events";
|
||||||
|
|
||||||
export enum Capability {
|
export enum Capability {
|
||||||
Screenshot = "m.capability.screenshot",
|
Screenshot = "m.capability.screenshot",
|
||||||
Sticker = "m.sticker",
|
Sticker = "m.sticker",
|
||||||
AlwaysOnScreen = "m.always_on_screen",
|
AlwaysOnScreen = "m.always_on_screen",
|
||||||
|
ReceiveTerminate = "im.vector.receive_terminate",
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum KnownWidgetActions {
|
export enum KnownWidgetActions {
|
||||||
|
@ -34,6 +36,7 @@ export enum KnownWidgetActions {
|
||||||
ReceiveOpenIDCredentials = "openid_credentials",
|
ReceiveOpenIDCredentials = "openid_credentials",
|
||||||
SetAlwaysOnScreen = "set_always_on_screen",
|
SetAlwaysOnScreen = "set_always_on_screen",
|
||||||
ClientReady = "im.vector.ready",
|
ClientReady = "im.vector.ready",
|
||||||
|
Terminate = "im.vector.terminate",
|
||||||
}
|
}
|
||||||
|
|
||||||
export type WidgetAction = KnownWidgetActions | string;
|
export type WidgetAction = KnownWidgetActions | string;
|
||||||
|
@ -62,8 +65,13 @@ export interface FromWidgetRequest extends WidgetRequest {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles Riot <--> Widget interactions for embedded/standalone widgets.
|
* 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 origin: string;
|
||||||
private inFlightRequests: { [requestId: string]: (reply: FromWidgetRequest) => void } = {};
|
private inFlightRequests: { [requestId: string]: (reply: FromWidgetRequest) => void } = {};
|
||||||
private readyPromise: Promise<any>;
|
private readyPromise: Promise<any>;
|
||||||
|
@ -75,6 +83,8 @@ export class WidgetApi {
|
||||||
public expectingExplicitReady = false;
|
public expectingExplicitReady = false;
|
||||||
|
|
||||||
constructor(currentUrl: string, private widgetId: string, private requestedCapabilities: string[]) {
|
constructor(currentUrl: string, private widgetId: string, private requestedCapabilities: string[]) {
|
||||||
|
super();
|
||||||
|
|
||||||
this.origin = new URL(currentUrl).origin;
|
this.origin = new URL(currentUrl).origin;
|
||||||
|
|
||||||
this.readyPromise = new Promise<any>(resolve => this.readyPromiseResolve = resolve);
|
this.readyPromise = new Promise<any>(resolve => this.readyPromiseResolve = resolve);
|
||||||
|
@ -98,6 +108,17 @@ export class WidgetApi {
|
||||||
|
|
||||||
// Automatically acknowledge so we can move on
|
// Automatically acknowledge so we can move on
|
||||||
this.replyToRequest(<ToWidgetRequest>payload, {});
|
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 {
|
} else {
|
||||||
console.warn(`[WidgetAPI] Got unexpected action: ${payload.action}`);
|
console.warn(`[WidgetAPI] Got unexpected action: ${payload.action}`);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue