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