Merge pull request #6869 from SimonBrandner/task/src-ts

Convert `/src` to TS
pull/21833/head
Travis Ralston 2021-09-30 12:48:23 -06:00 committed by GitHub
commit fe0a68b71e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 335 additions and 279 deletions

View File

@ -25,9 +25,9 @@
"bin": {
"reskindex": "scripts/reskindex.js"
},
"main": "./src/index.js",
"matrix_src_main": "./src/index.js",
"matrix_lib_main": "./lib/index.js",
"main": "./src/index.ts",
"matrix_src_main": "./src/index.ts",
"matrix_lib_main": "./lib/index.ts",
"matrix_lib_typings": "./lib/index.d.ts",
"scripts": {
"prepublishOnly": "yarn build",

View File

@ -51,6 +51,7 @@ import { SetupEncryptionStore } from "../stores/SetupEncryptionStore";
import { RoomScrollStateStore } from "../stores/RoomScrollStateStore";
import { ConsoleLogger, IndexedDBLogStore } from "../rageshake/rageshake";
import ActiveWidgetStore from "../stores/ActiveWidgetStore";
import { Skinner } from "../Skinner";
/* eslint-disable @typescript-eslint/naming-convention */
@ -95,6 +96,7 @@ declare global {
mxSetupEncryptionStore?: SetupEncryptionStore;
mxRoomScrollStateStore?: RoomScrollStateStore;
mxActiveWidgetStore?: ActiveWidgetStore;
mxSkinner?: Skinner;
mxOnRecaptchaLoaded?: () => void;
electron?: Electron;
}
@ -157,6 +159,10 @@ declare global {
setSinkId(outputId: string);
}
interface HTMLStyleElement {
disabled?: boolean;
}
// Add Chrome-specific `instant` ScrollBehaviour
type _ScrollBehavior = ScrollBehavior | "instant";

View File

@ -17,13 +17,14 @@ limitations under the License.
*/
import { MatrixClientPeg } from './MatrixClientPeg';
import * as sdk from './index';
import Modal from './Modal';
import { _t } from './languageHandler';
import IdentityAuthClient from './IdentityAuthClient';
import { SSOAuthEntry } from "./components/views/auth/InteractiveAuthEntryComponents";
import { IRequestMsisdnTokenResponse, IRequestTokenResponse } from "matrix-js-sdk";
import InteractiveAuthDialog from "./components/views/dialogs/InteractiveAuthDialog";
function getIdServerDomain() {
function getIdServerDomain(): string {
return MatrixClientPeg.get().idBaseUrl.split("://")[1];
}
@ -40,10 +41,13 @@ function getIdServerDomain() {
* https://gist.github.com/jryans/839a09bf0c5a70e2f36ed990d50ed928
*/
export default class AddThreepid {
private sessionId: string;
private submitUrl: string;
private clientSecret: string;
private bind: boolean;
constructor() {
this.clientSecret = MatrixClientPeg.get().generateClientSecret();
this.sessionId = null;
this.submitUrl = null;
}
/**
@ -52,7 +56,7 @@ export default class AddThreepid {
* @param {string} emailAddress The email address to add
* @return {Promise} Resolves when the email has been sent. Then call checkEmailLinkClicked().
*/
addEmailAddress(emailAddress) {
public addEmailAddress(emailAddress: string): Promise<IRequestTokenResponse> {
return MatrixClientPeg.get().requestAdd3pidEmailToken(emailAddress, this.clientSecret, 1).then((res) => {
this.sessionId = res.sid;
return res;
@ -72,7 +76,7 @@ export default class AddThreepid {
* @param {string} emailAddress The email address to add
* @return {Promise} Resolves when the email has been sent. Then call checkEmailLinkClicked().
*/
async bindEmailAddress(emailAddress) {
public async bindEmailAddress(emailAddress: string): Promise<IRequestTokenResponse> {
this.bind = true;
if (await MatrixClientPeg.get().doesServerSupportSeparateAddAndBind()) {
// For separate bind, request a token directly from the IS.
@ -105,7 +109,7 @@ export default class AddThreepid {
* @param {string} phoneNumber The national or international formatted phone number to add
* @return {Promise} Resolves when the text message has been sent. Then call haveMsisdnToken().
*/
addMsisdn(phoneCountry, phoneNumber) {
public addMsisdn(phoneCountry: string, phoneNumber: string): Promise<IRequestMsisdnTokenResponse> {
return MatrixClientPeg.get().requestAdd3pidMsisdnToken(
phoneCountry, phoneNumber, this.clientSecret, 1,
).then((res) => {
@ -129,7 +133,7 @@ export default class AddThreepid {
* @param {string} phoneNumber The national or international formatted phone number to add
* @return {Promise} Resolves when the text message has been sent. Then call haveMsisdnToken().
*/
async bindMsisdn(phoneCountry, phoneNumber) {
public async bindMsisdn(phoneCountry: string, phoneNumber: string): Promise<IRequestMsisdnTokenResponse> {
this.bind = true;
if (await MatrixClientPeg.get().doesServerSupportSeparateAddAndBind()) {
// For separate bind, request a token directly from the IS.
@ -161,7 +165,7 @@ export default class AddThreepid {
* with a "message" property which contains a human-readable message detailing why
* the request failed.
*/
async checkEmailLinkClicked() {
public async checkEmailLinkClicked(): Promise<any[]> {
try {
if (await MatrixClientPeg.get().doesServerSupportSeparateAddAndBind()) {
if (this.bind) {
@ -175,7 +179,7 @@ export default class AddThreepid {
});
} else {
try {
await this._makeAddThreepidOnlyRequest();
await this.makeAddThreepidOnlyRequest();
// The spec has always required this to use UI auth but synapse briefly
// implemented it without, so this may just succeed and that's OK.
@ -186,9 +190,6 @@ export default class AddThreepid {
throw e;
}
// pop up an interactive auth dialog
const InteractiveAuthDialog = sdk.getComponent("dialogs.InteractiveAuthDialog");
const dialogAesthetics = {
[SSOAuthEntry.PHASE_PREAUTH]: {
title: _t("Use Single Sign On to continue"),
@ -208,7 +209,7 @@ export default class AddThreepid {
title: _t("Add Email Address"),
matrixClient: MatrixClientPeg.get(),
authData: e.data,
makeRequest: this._makeAddThreepidOnlyRequest,
makeRequest: this.makeAddThreepidOnlyRequest,
aestheticsForStagePhases: {
[SSOAuthEntry.LOGIN_TYPE]: dialogAesthetics,
[SSOAuthEntry.UNSTABLE_LOGIN_TYPE]: dialogAesthetics,
@ -235,16 +236,16 @@ export default class AddThreepid {
}
/**
* @param {Object} auth UI auth object
* @param {{type: string, session?: string}} auth UI auth object
* @return {Promise<Object>} Response from /3pid/add call (in current spec, an empty object)
*/
_makeAddThreepidOnlyRequest = (auth) => {
private makeAddThreepidOnlyRequest = (auth?: {type: string, session?: string}): Promise<{}> => {
return MatrixClientPeg.get().addThreePidOnly({
sid: this.sessionId,
client_secret: this.clientSecret,
auth,
});
}
};
/**
* Takes a phone number verification code as entered by the user and validates
@ -254,7 +255,7 @@ export default class AddThreepid {
* with a "message" property which contains a human-readable message detailing why
* the request failed.
*/
async haveMsisdnToken(msisdnToken) {
public async haveMsisdnToken(msisdnToken: string): Promise<any[]> {
const authClient = new IdentityAuthClient();
const supportsSeparateAddAndBind =
await MatrixClientPeg.get().doesServerSupportSeparateAddAndBind();
@ -291,7 +292,7 @@ export default class AddThreepid {
});
} else {
try {
await this._makeAddThreepidOnlyRequest();
await this.makeAddThreepidOnlyRequest();
// The spec has always required this to use UI auth but synapse briefly
// implemented it without, so this may just succeed and that's OK.
@ -302,9 +303,6 @@ export default class AddThreepid {
throw e;
}
// pop up an interactive auth dialog
const InteractiveAuthDialog = sdk.getComponent("dialogs.InteractiveAuthDialog");
const dialogAesthetics = {
[SSOAuthEntry.PHASE_PREAUTH]: {
title: _t("Use Single Sign On to continue"),
@ -324,7 +322,7 @@ export default class AddThreepid {
title: _t("Add Phone Number"),
matrixClient: MatrixClientPeg.get(),
authData: e.data,
makeRequest: this._makeAddThreepidOnlyRequest,
makeRequest: this.makeAddThreepidOnlyRequest,
aestheticsForStagePhases: {
[SSOAuthEntry.LOGIN_TYPE]: dialogAesthetics,
[SSOAuthEntry.UNSTABLE_LOGIN_TYPE]: dialogAesthetics,

View File

@ -14,12 +14,12 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from "react";
import { SERVICE_TYPES } from 'matrix-js-sdk/src/service-types';
import { createClient } from 'matrix-js-sdk/src/matrix';
import { createClient, MatrixClient } from 'matrix-js-sdk/src/matrix';
import { MatrixClientPeg } from './MatrixClientPeg';
import Modal from './Modal';
import * as sdk from './index';
import { _t } from './languageHandler';
import { Service, startTermsFlow, TermsNotSignedError } from './Terms';
import {
@ -27,23 +27,25 @@ import {
doesIdentityServerHaveTerms,
useDefaultIdentityServer,
} from './utils/IdentityServerUtils';
import { abbreviateUrl } from './utils/UrlUtils';
import { logger } from "matrix-js-sdk/src/logger";
import QuestionDialog from "./components/views/dialogs/QuestionDialog";
import { abbreviateUrl } from "./utils/UrlUtils";
export class AbortedIdentityActionError extends Error {}
export default class IdentityAuthClient {
private accessToken: string;
private tempClient: MatrixClient;
private authEnabled = true;
/**
* Creates a new identity auth client
* @param {string} identityUrl The URL to contact the identity server with.
* When provided, this class will operate solely within memory, refusing to
* persist any information such as tokens. Default null (not provided).
*/
constructor(identityUrl = null) {
this.accessToken = null;
this.authEnabled = true;
constructor(identityUrl?: string) {
if (identityUrl) {
// XXX: We shouldn't have to create a whole new MatrixClient just to
// do identity server auth. The functions don't take an identity URL
@ -54,32 +56,29 @@ export default class IdentityAuthClient {
baseUrl: "", // invalid by design
idBaseUrl: identityUrl,
});
} else {
// Indicates that we're using the real client, not some workaround.
this.tempClient = null;
}
}
get _matrixClient() {
private get matrixClient(): MatrixClient {
return this.tempClient ? this.tempClient : MatrixClientPeg.get();
}
_writeToken() {
private writeToken(): void {
if (this.tempClient) return; // temporary client: ignore
window.localStorage.setItem("mx_is_access_token", this.accessToken);
}
_readToken() {
private readToken(): string {
if (this.tempClient) return null; // temporary client: ignore
return window.localStorage.getItem("mx_is_access_token");
}
hasCredentials() {
return this.accessToken != null; // undef or null
public hasCredentials(): boolean {
return Boolean(this.accessToken);
}
// Returns a promise that resolves to the access_token string from the IS
async getAccessToken({ check = true } = {}) {
public async getAccessToken({ check = true } = {}): Promise<string> {
if (!this.authEnabled) {
// The current IS doesn't support authentication
return null;
@ -87,21 +86,21 @@ export default class IdentityAuthClient {
let token = this.accessToken;
if (!token) {
token = this._readToken();
token = this.readToken();
}
if (!token) {
token = await this.registerForToken(check);
if (token) {
this.accessToken = token;
this._writeToken();
this.writeToken();
}
return token;
}
if (check) {
try {
await this._checkToken(token);
await this.checkToken(token);
} catch (e) {
if (
e instanceof TermsNotSignedError ||
@ -114,7 +113,7 @@ export default class IdentityAuthClient {
token = await this.registerForToken();
if (token) {
this.accessToken = token;
this._writeToken();
this.writeToken();
}
}
}
@ -122,11 +121,11 @@ export default class IdentityAuthClient {
return token;
}
async _checkToken(token) {
const identityServerUrl = this._matrixClient.getIdentityServerUrl();
private async checkToken(token: string): Promise<void> {
const identityServerUrl = this.matrixClient.getIdentityServerUrl();
try {
await this._matrixClient.getIdentityAccount(token);
await this.matrixClient.getIdentityAccount(token);
} catch (e) {
if (e.errcode === "M_TERMS_NOT_SIGNED") {
logger.log("Identity server requires new terms to be agreed to");
@ -145,8 +144,8 @@ export default class IdentityAuthClient {
!doesAccountDataHaveIdentityServer() &&
!(await doesIdentityServerHaveTerms(identityServerUrl))
) {
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
const { finished } = Modal.createTrackedDialog('Default identity server terms warning', '',
const { finished } = Modal.createTrackedDialog(
'Default identity server terms warning', '',
QuestionDialog, {
title: _t("Identity server has no terms of service"),
description: (
@ -184,13 +183,13 @@ export default class IdentityAuthClient {
// See also https://github.com/vector-im/element-web/issues/10455.
}
async registerForToken(check=true) {
public async registerForToken(check = true): Promise<string> {
const hsOpenIdToken = await MatrixClientPeg.get().getOpenIdToken();
// XXX: The spec is `token`, but we used `access_token` for a Sydent release.
const { access_token: accessToken, token } =
await this._matrixClient.registerWithIdentityServer(hsOpenIdToken);
await this.matrixClient.registerWithIdentityServer(hsOpenIdToken);
const identityAccessToken = token ? token : accessToken;
if (check) await this._checkToken(identityAccessToken);
if (check) await this.checkToken(identityAccessToken);
return identityAccessToken;
}
}

View File

@ -1,6 +1,21 @@
import React from "react";
import ReactDom from "react-dom";
import PropTypes from 'prop-types';
interface IChildProps {
style: React.CSSProperties;
ref: (node: React.ReactInstance) => void;
}
interface IProps {
// either a list of child nodes, or a single child.
children: React.ReactNode;
// optional transition information for changing existing children
transition?: object;
// a list of state objects to apply to each child node in turn
startStyles: React.CSSProperties[];
}
/**
* The NodeAnimator contains components and animates transitions.
@ -9,55 +24,45 @@ import PropTypes from 'prop-types';
* from DOM order. This makes it a lot simpler and lighter: if you need fully
* automatic positional animation, look at react-shuffle or similar libraries.
*/
export default class NodeAnimator extends React.Component {
static propTypes = {
// either a list of child nodes, or a single child.
children: PropTypes.any,
// optional transition information for changing existing children
transition: PropTypes.object,
// a list of state objects to apply to each child node in turn
startStyles: PropTypes.array,
};
static defaultProps = {
export default class NodeAnimator extends React.Component<IProps> {
private nodes = {};
private children: { [key: string]: React.DetailedReactHTMLElement<any, HTMLElement> };
public static defaultProps: Partial<IProps> = {
startStyles: [],
};
constructor(props) {
constructor(props: IProps) {
super(props);
this.nodes = {};
this._updateChildren(this.props.children);
this.updateChildren(this.props.children);
}
componentDidUpdate() {
this._updateChildren(this.props.children);
public componentDidUpdate(): void {
this.updateChildren(this.props.children);
}
/**
*
* @param {HTMLElement} node element to apply styles to
* @param {object} styles a key/value pair of CSS properties
* @param {React.CSSProperties} styles a key/value pair of CSS properties
* @returns {void}
*/
_applyStyles(node, styles) {
private applyStyles(node: HTMLElement, styles: React.CSSProperties): void {
Object.entries(styles).forEach(([property, value]) => {
node.style[property] = value;
});
}
_updateChildren(newChildren) {
private updateChildren(newChildren: React.ReactNode): void {
const oldChildren = this.children || {};
this.children = {};
React.Children.toArray(newChildren).forEach((c) => {
React.Children.toArray(newChildren).forEach((c: any) => {
if (oldChildren[c.key]) {
const old = oldChildren[c.key];
const oldNode = ReactDom.findDOMNode(this.nodes[old.key]);
if (oldNode && oldNode.style.left !== c.props.style.left) {
this._applyStyles(oldNode, { left: c.props.style.left });
if (oldNode && (oldNode as HTMLElement).style.left !== c.props.style.left) {
this.applyStyles(oldNode as HTMLElement, { left: c.props.style.left });
// console.log("translation: "+oldNode.style.left+" -> "+c.props.style.left);
}
// clone the old element with the props (and children) of the new element
@ -66,7 +71,7 @@ export default class NodeAnimator extends React.Component {
} else {
// new element. If we have a startStyle, use that as the style and go through
// the enter animations
const newProps = {};
const newProps: Partial<IChildProps> = {};
const restingStyle = c.props.style;
const startStyles = this.props.startStyles;
@ -76,7 +81,7 @@ export default class NodeAnimator extends React.Component {
// console.log("mounted@startstyle0: "+JSON.stringify(startStyle));
}
newProps.ref = ((n) => this._collectNode(
newProps.ref = ((n) => this.collectNode(
c.key, n, restingStyle,
));
@ -85,7 +90,7 @@ export default class NodeAnimator extends React.Component {
});
}
_collectNode(k, node, restingStyle) {
private collectNode(k: string, node: React.ReactInstance, restingStyle: React.CSSProperties): void {
if (
node &&
this.nodes[k] === undefined &&
@ -96,7 +101,7 @@ export default class NodeAnimator extends React.Component {
// start from startStyle 1: 0 is the one we gave it
// to start with, so now we animate 1 etc.
for (let i = 1; i < startStyles.length; ++i) {
this._applyStyles(domNode, startStyles[i]);
this.applyStyles(domNode as HTMLElement, startStyles[i]);
// console.log("start:"
// JSON.stringify(startStyles[i]),
// );
@ -104,7 +109,7 @@ export default class NodeAnimator extends React.Component {
// and then we animate to the resting state
setTimeout(() => {
this._applyStyles(domNode, restingStyle);
this.applyStyles(domNode as HTMLElement, restingStyle);
}, 0);
// console.log("enter:",
@ -113,7 +118,7 @@ export default class NodeAnimator extends React.Component {
this.nodes[k] = node;
}
render() {
public render(): JSX.Element {
return (
<>{ Object.values(this.children) }</>
);

View File

@ -16,11 +16,13 @@ limitations under the License.
*/
/** The types of page which can be shown by the LoggedInView */
export default {
HomePage: "home_page",
RoomView: "room_view",
RoomDirectory: "room_directory",
UserView: "user_view",
GroupView: "group_view",
MyGroups: "my_groups",
};
enum PageType {
HomePage = "home_page",
RoomView = "room_view",
RoomDirectory = "room_directory",
UserView = "user_view",
GroupView = "group_view",
MyGroups = "my_groups",
}
export default PageType;

View File

@ -20,10 +20,11 @@ limitations under the License.
* registration code.
*/
import React from "react";
import dis from './dispatcher/dispatcher';
import * as sdk from './index';
import Modal from './Modal';
import { _t } from './languageHandler';
import QuestionDialog from "./components/views/dialogs/QuestionDialog";
// Regex for what a "safe" or "Matrix-looking" localpart would be.
// TODO: Update as needed for https://github.com/matrix-org/matrix-doc/issues/1514
@ -41,9 +42,11 @@ export const SAFE_LOCALPART_REGEX = /^[a-z0-9=_\-./]+$/;
* @param {bool} options.screen_after
* If present the screen to redirect to after a successful login or register.
*/
export async function startAnyRegistrationFlow(options) {
export async function startAnyRegistrationFlow(
// eslint-disable-next-line camelcase
options: { go_home_on_cancel?: boolean, go_welcome_on_cancel?: boolean, screen_after?: boolean},
): Promise<void> {
if (options === undefined) options = {};
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
const modal = Modal.createTrackedDialog('Registration required', '', QuestionDialog, {
hasCancelButton: true,
quitOnly: true,

View File

@ -17,27 +17,31 @@ limitations under the License.
import { MatrixClientPeg } from './MatrixClientPeg';
import { PushProcessor } from 'matrix-js-sdk/src/pushprocessor';
import { NotificationCountType, Room } from "matrix-js-sdk/src/models/room";
import { IAnnotatedPushRule, PushRuleKind } from "matrix-js-sdk/src/@types/PushRules";
export const ALL_MESSAGES_LOUD = 'all_messages_loud';
export const ALL_MESSAGES = 'all_messages';
export const MENTIONS_ONLY = 'mentions_only';
export const MUTE = 'mute';
export enum RoomNotifState {
AllMessagesLoud = 'all_messages_loud',
AllMessages = 'all_messages',
MentionsOnly = 'mentions_only',
Mute = 'mute',
}
export const BADGE_STATES = [ALL_MESSAGES, ALL_MESSAGES_LOUD];
export const MENTION_BADGE_STATES = [...BADGE_STATES, MENTIONS_ONLY];
export const BADGE_STATES = [RoomNotifState.AllMessages, RoomNotifState.AllMessagesLoud];
export const MENTION_BADGE_STATES = [...BADGE_STATES, RoomNotifState.MentionsOnly];
export function shouldShowNotifBadge(roomNotifState) {
export function shouldShowNotifBadge(roomNotifState: RoomNotifState): boolean {
return BADGE_STATES.includes(roomNotifState);
}
export function shouldShowMentionBadge(roomNotifState) {
export function shouldShowMentionBadge(roomNotifState: RoomNotifState): boolean {
return MENTION_BADGE_STATES.includes(roomNotifState);
}
export function aggregateNotificationCount(rooms) {
return rooms.reduce((result, room) => {
export function aggregateNotificationCount(rooms: Room[]): {count: number, highlight: boolean} {
return rooms.reduce<{count: number, highlight: boolean}>((result, room) => {
const roomNotifState = getRoomNotifsState(room.roomId);
const highlight = room.getUnreadNotificationCount('highlight') > 0;
const highlight = room.getUnreadNotificationCount(NotificationCountType.Highlight) > 0;
// use helper method to include highlights in the previous version of the room
const notificationCount = getUnreadNotificationCount(room);
@ -55,9 +59,9 @@ export function aggregateNotificationCount(rooms) {
}, { count: 0, highlight: false });
}
export function getRoomHasBadge(room) {
export function getRoomHasBadge(room: Room): boolean {
const roomNotifState = getRoomNotifsState(room.roomId);
const highlight = room.getUnreadNotificationCount('highlight') > 0;
const highlight = room.getUnreadNotificationCount(NotificationCountType.Highlight) > 0;
const notificationCount = room.getUnreadNotificationCount();
const notifBadges = notificationCount > 0 && shouldShowNotifBadge(roomNotifState);
@ -66,14 +70,14 @@ export function getRoomHasBadge(room) {
return notifBadges || mentionBadges;
}
export function getRoomNotifsState(roomId) {
if (MatrixClientPeg.get().isGuest()) return ALL_MESSAGES;
export function getRoomNotifsState(roomId: string): RoomNotifState {
if (MatrixClientPeg.get().isGuest()) return RoomNotifState.AllMessages;
// look through the override rules for a rule affecting this room:
// if one exists, it will take precedence.
const muteRule = findOverrideMuteRule(roomId);
if (muteRule) {
return MUTE;
return RoomNotifState.Mute;
}
// for everything else, look at the room rule.
@ -89,27 +93,27 @@ export function getRoomNotifsState(roomId) {
// XXX: We have to assume the default is to notify for all messages
// (in particular this will be 'wrong' for one to one rooms because
// they will notify loudly for all messages)
if (!roomRule || !roomRule.enabled) return ALL_MESSAGES;
if (!roomRule || !roomRule.enabled) return RoomNotifState.AllMessages;
// a mute at the room level will still allow mentions
// to notify
if (isMuteRule(roomRule)) return MENTIONS_ONLY;
if (isMuteRule(roomRule)) return RoomNotifState.MentionsOnly;
const actionsObject = PushProcessor.actionListToActionsObject(roomRule.actions);
if (actionsObject.tweaks.sound) return ALL_MESSAGES_LOUD;
if (actionsObject.tweaks.sound) return RoomNotifState.AllMessagesLoud;
return null;
}
export function setRoomNotifsState(roomId, newState) {
if (newState === MUTE) {
export function setRoomNotifsState(roomId: string, newState: RoomNotifState): Promise<void> {
if (newState === RoomNotifState.Mute) {
return setRoomNotifsStateMuted(roomId);
} else {
return setRoomNotifsStateUnmuted(roomId, newState);
}
}
export function getUnreadNotificationCount(room, type=null) {
export function getUnreadNotificationCount(room: Room, type: NotificationCountType = null): number {
let notificationCount = room.getUnreadNotificationCount(type);
// Check notification counts in the old room just in case there's some lost
@ -124,21 +128,21 @@ export function getUnreadNotificationCount(room, type=null) {
// notifying the user for unread messages because they would have extreme
// difficulty changing their notification preferences away from "All Messages"
// and "Noisy".
notificationCount += oldRoom.getUnreadNotificationCount("highlight");
notificationCount += oldRoom.getUnreadNotificationCount(NotificationCountType.Highlight);
}
}
return notificationCount;
}
function setRoomNotifsStateMuted(roomId) {
function setRoomNotifsStateMuted(roomId: string): Promise<any> {
const cli = MatrixClientPeg.get();
const promises = [];
// delete the room rule
const roomRule = cli.getRoomPushRule('global', roomId);
if (roomRule) {
promises.push(cli.deletePushRule('global', 'room', roomRule.rule_id));
promises.push(cli.deletePushRule('global', PushRuleKind.RoomSpecific, roomRule.rule_id));
}
// add/replace an override rule to squelch everything in this room
@ -146,7 +150,7 @@ function setRoomNotifsStateMuted(roomId) {
// is an override rule, not a room rule: it still pertains to this room
// though, so using the room ID as the rule ID is logical and prevents
// duplicate copies of the rule.
promises.push(cli.addPushRule('global', 'override', roomId, {
promises.push(cli.addPushRule('global', PushRuleKind.Override, roomId, {
conditions: [
{
kind: 'event_match',
@ -162,30 +166,30 @@ function setRoomNotifsStateMuted(roomId) {
return Promise.all(promises);
}
function setRoomNotifsStateUnmuted(roomId, newState) {
function setRoomNotifsStateUnmuted(roomId: string, newState: RoomNotifState): Promise<any> {
const cli = MatrixClientPeg.get();
const promises = [];
const overrideMuteRule = findOverrideMuteRule(roomId);
if (overrideMuteRule) {
promises.push(cli.deletePushRule('global', 'override', overrideMuteRule.rule_id));
promises.push(cli.deletePushRule('global', PushRuleKind.Override, overrideMuteRule.rule_id));
}
if (newState === 'all_messages') {
if (newState === RoomNotifState.AllMessages) {
const roomRule = cli.getRoomPushRule('global', roomId);
if (roomRule) {
promises.push(cli.deletePushRule('global', 'room', roomRule.rule_id));
promises.push(cli.deletePushRule('global', PushRuleKind.RoomSpecific, roomRule.rule_id));
}
} else if (newState === 'mentions_only') {
promises.push(cli.addPushRule('global', 'room', roomId, {
} else if (newState === RoomNotifState.MentionsOnly) {
promises.push(cli.addPushRule('global', PushRuleKind.RoomSpecific, roomId, {
actions: [
'dont_notify',
],
}));
// https://matrix.org/jira/browse/SPEC-400
promises.push(cli.setPushRuleEnabled('global', 'room', roomId, true));
} else if ('all_messages_loud') {
promises.push(cli.addPushRule('global', 'room', roomId, {
promises.push(cli.setPushRuleEnabled('global', PushRuleKind.RoomSpecific, roomId, true));
} else if (newState === RoomNotifState.AllMessagesLoud) {
promises.push(cli.addPushRule('global', PushRuleKind.RoomSpecific, roomId, {
actions: [
'notify',
{
@ -195,13 +199,13 @@ function setRoomNotifsStateUnmuted(roomId, newState) {
],
}));
// https://matrix.org/jira/browse/SPEC-400
promises.push(cli.setPushRuleEnabled('global', 'room', roomId, true));
promises.push(cli.setPushRuleEnabled('global', PushRuleKind.RoomSpecific, roomId, true));
}
return Promise.all(promises);
}
function findOverrideMuteRule(roomId) {
function findOverrideMuteRule(roomId: string): IAnnotatedPushRule {
const cli = MatrixClientPeg.get();
if (!cli.pushRules ||
!cli.pushRules['global'] ||
@ -218,7 +222,7 @@ function findOverrideMuteRule(roomId) {
return null;
}
function isRuleForRoom(roomId, rule) {
function isRuleForRoom(roomId: string, rule: IAnnotatedPushRule): boolean {
if (rule.conditions.length !== 1) {
return false;
}
@ -226,6 +230,6 @@ function isRuleForRoom(roomId, rule) {
return (cond.kind === 'event_match' && cond.key === 'room_id' && cond.pattern === roomId);
}
function isMuteRule(rule) {
function isMuteRule(rule: IAnnotatedPushRule): boolean {
return (rule.actions.length === 1 && rule.actions[0] === 'dont_notify');
}

View File

@ -1,24 +0,0 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import {
ALL_MESSAGES,
ALL_MESSAGES_LOUD,
MENTIONS_ONLY,
MUTE,
} from "./RoomNotifs";
export type Volume = ALL_MESSAGES_LOUD | ALL_MESSAGES | MENTIONS_ONLY | MUTE;

View File

@ -247,13 +247,31 @@ import { objectClone } from "./utils/objects";
import { logger } from "matrix-js-sdk/src/logger";
function sendResponse(event, res) {
enum Action {
CloseScalar = "close_scalar",
GetWidgets = "get_widgets",
SetWidgets = "set_widgets",
SetWidget = "set_widget",
JoinRulesState = "join_rules_state",
SetPlumbingState = "set_plumbing_state",
GetMembershipCount = "get_membership_count",
GetRoomEncryptionState = "get_room_enc_state",
CanSendEvent = "can_send_event",
MembershipState = "membership_state",
invite = "invite",
BotOptions = "bot_options",
SetBotOptions = "set_bot_options",
SetBotPower = "set_bot_power",
}
function sendResponse(event: MessageEvent<any>, res: any): void {
const data = objectClone(event.data);
data.response = res;
// @ts-ignore
event.source.postMessage(data, event.origin);
}
function sendError(event, msg, nestedError) {
function sendError(event: MessageEvent<any>, msg: string, nestedError?: Error): void {
console.error("Action:" + event.data.action + " failed with message: " + msg);
const data = objectClone(event.data);
data.response = {
@ -264,10 +282,11 @@ function sendError(event, msg, nestedError) {
if (nestedError) {
data.response.error._error = nestedError;
}
// @ts-ignore
event.source.postMessage(data, event.origin);
}
function inviteUser(event, roomId, userId) {
function inviteUser(event: MessageEvent<any>, roomId: string, userId: string): void {
logger.log(`Received request to invite ${userId} into room ${roomId}`);
const client = MatrixClientPeg.get();
if (!client) {
@ -295,7 +314,7 @@ function inviteUser(event, roomId, userId) {
});
}
function setWidget(event, roomId) {
function setWidget(event: MessageEvent<any>, roomId: string): void {
const widgetId = event.data.widget_id;
let widgetType = event.data.type;
const widgetUrl = event.data.url;
@ -356,7 +375,7 @@ function setWidget(event, roomId) {
}
}
function getWidgets(event, roomId) {
function getWidgets(event: MessageEvent<any>, roomId: string): void {
const client = MatrixClientPeg.get();
if (!client) {
sendError(event, _t('You need to be logged in.'));
@ -382,7 +401,7 @@ function getWidgets(event, roomId) {
sendResponse(event, widgetStateEvents);
}
function getRoomEncState(event, roomId) {
function getRoomEncState(event: MessageEvent<any>, roomId: string): void {
const client = MatrixClientPeg.get();
if (!client) {
sendError(event, _t('You need to be logged in.'));
@ -398,7 +417,7 @@ function getRoomEncState(event, roomId) {
sendResponse(event, roomIsEncrypted);
}
function setPlumbingState(event, roomId, status) {
function setPlumbingState(event: MessageEvent<any>, roomId: string, status: string): void {
if (typeof status !== 'string') {
throw new Error('Plumbing state status should be a string');
}
@ -417,7 +436,7 @@ function setPlumbingState(event, roomId, status) {
});
}
function setBotOptions(event, roomId, userId) {
function setBotOptions(event: MessageEvent<any>, roomId: string, userId: string): void {
logger.log(`Received request to set options for bot ${userId} in room ${roomId}`);
const client = MatrixClientPeg.get();
if (!client) {
@ -433,7 +452,7 @@ function setBotOptions(event, roomId, userId) {
});
}
function setBotPower(event, roomId, userId, level) {
function setBotPower(event: MessageEvent<any>, roomId: string, userId: string, level: number): void {
if (!(Number.isInteger(level) && level >= 0)) {
sendError(event, _t('Power level must be positive integer.'));
return;
@ -464,22 +483,22 @@ function setBotPower(event, roomId, userId, level) {
});
}
function getMembershipState(event, roomId, userId) {
function getMembershipState(event: MessageEvent<any>, roomId: string, userId: string): void {
logger.log(`membership_state of ${userId} in room ${roomId} requested.`);
returnStateEvent(event, roomId, "m.room.member", userId);
}
function getJoinRules(event, roomId) {
function getJoinRules(event: MessageEvent<any>, roomId: string): void {
logger.log(`join_rules of ${roomId} requested.`);
returnStateEvent(event, roomId, "m.room.join_rules", "");
}
function botOptions(event, roomId, userId) {
function botOptions(event: MessageEvent<any>, roomId: string, userId: string): void {
logger.log(`bot_options of ${userId} in room ${roomId} requested.`);
returnStateEvent(event, roomId, "m.room.bot.options", "_" + userId);
}
function getMembershipCount(event, roomId) {
function getMembershipCount(event: MessageEvent<any>, roomId: string): void {
const client = MatrixClientPeg.get();
if (!client) {
sendError(event, _t('You need to be logged in.'));
@ -494,7 +513,7 @@ function getMembershipCount(event, roomId) {
sendResponse(event, count);
}
function canSendEvent(event, roomId) {
function canSendEvent(event: MessageEvent<any>, roomId: string): void {
const evType = "" + event.data.event_type; // force stringify
const isState = Boolean(event.data.is_state);
const client = MatrixClientPeg.get();
@ -528,7 +547,7 @@ function canSendEvent(event, roomId) {
sendResponse(event, true);
}
function returnStateEvent(event, roomId, eventType, stateKey) {
function returnStateEvent(event: MessageEvent<any>, roomId: string, eventType: string, stateKey: string): void {
const client = MatrixClientPeg.get();
if (!client) {
sendError(event, _t('You need to be logged in.'));
@ -547,8 +566,9 @@ function returnStateEvent(event, roomId, eventType, stateKey) {
sendResponse(event, stateEvent.getContent());
}
const onMessage = function(event) {
const onMessage = function(event: MessageEvent<any>): void {
if (!event.origin) { // stupid chrome
// @ts-ignore
event.origin = event.originalEvent.origin;
}
@ -582,8 +602,8 @@ const onMessage = function(event) {
return;
}
if (event.data.action === "close_scalar") {
dis.dispatch({ action: "close_scalar" });
if (event.data.action === Action.CloseScalar) {
dis.dispatch({ action: Action.CloseScalar });
sendResponse(event, null);
return;
}
@ -596,10 +616,10 @@ const onMessage = function(event) {
// Get and set user widgets (not associated with a specific room)
// If roomId is specified, it must be validated, so room-based widgets agreed
// handled further down.
if (event.data.action === "get_widgets") {
if (event.data.action === Action.GetWidgets) {
getWidgets(event, null);
return;
} else if (event.data.action === "set_widget") {
} else if (event.data.action === Action.SetWidgets) {
setWidget(event, null);
return;
} else {
@ -614,28 +634,28 @@ const onMessage = function(event) {
}
// Get and set room-based widgets
if (event.data.action === "get_widgets") {
if (event.data.action === Action.GetWidgets) {
getWidgets(event, roomId);
return;
} else if (event.data.action === "set_widget") {
} else if (event.data.action === Action.SetWidget) {
setWidget(event, roomId);
return;
}
// These APIs don't require userId
if (event.data.action === "join_rules_state") {
if (event.data.action === Action.JoinRulesState) {
getJoinRules(event, roomId);
return;
} else if (event.data.action === "set_plumbing_state") {
} else if (event.data.action === Action.SetPlumbingState) {
setPlumbingState(event, roomId, event.data.status);
return;
} else if (event.data.action === "get_membership_count") {
} else if (event.data.action === Action.GetMembershipCount) {
getMembershipCount(event, roomId);
return;
} else if (event.data.action === "get_room_enc_state") {
} else if (event.data.action === Action.GetRoomEncryptionState) {
getRoomEncState(event, roomId);
return;
} else if (event.data.action === "can_send_event") {
} else if (event.data.action === Action.CanSendEvent) {
canSendEvent(event, roomId);
return;
}
@ -645,19 +665,19 @@ const onMessage = function(event) {
return;
}
switch (event.data.action) {
case "membership_state":
case Action.MembershipState:
getMembershipState(event, roomId, userId);
break;
case "invite":
case Action.invite:
inviteUser(event, roomId, userId);
break;
case "bot_options":
case Action.BotOptions:
botOptions(event, roomId, userId);
break;
case "set_bot_options":
case Action.SetBotOptions:
setBotOptions(event, roomId, userId);
break;
case "set_bot_power":
case Action.SetBotPower:
setBotPower(event, roomId, userId, event.data.level);
break;
default:
@ -667,16 +687,16 @@ const onMessage = function(event) {
};
let listenerCount = 0;
let openManagerUrl = null;
let openManagerUrl: string = null;
export function startListening() {
export function startListening(): void {
if (listenerCount === 0) {
window.addEventListener("message", onMessage, false);
}
listenerCount += 1;
}
export function stopListening() {
export function stopListening(): void {
listenerCount -= 1;
if (listenerCount === 0) {
window.removeEventListener("message", onMessage);
@ -691,6 +711,6 @@ export function stopListening() {
}
}
export function setOpenManagerUrl(url) {
export function setOpenManagerUrl(url: string): void {
openManagerUrl = url;
}

View File

@ -14,12 +14,20 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
class Skinner {
constructor() {
this.components = null;
}
import React from "react";
getComponent(name) {
export interface IComponents {
[key: string]: React.Component;
}
export interface ISkinObject {
components: IComponents;
}
export class Skinner {
public components: IComponents = null;
public getComponent(name: string): React.Component {
if (!name) throw new Error(`Invalid component name: ${name}`);
if (this.components === null) {
throw new Error(
@ -30,7 +38,7 @@ class Skinner {
);
}
const doLookup = (components) => {
const doLookup = (components: IComponents): React.Component => {
if (!components) return null;
let comp = components[name];
// XXX: Temporarily also try 'views.' as we're currently
@ -58,7 +66,7 @@ class Skinner {
return comp;
}
load(skinObject) {
public load(skinObject: ISkinObject): void {
if (this.components !== null) {
throw new Error(
"Attempted to load a skin while a skin is already loaded"+
@ -72,6 +80,7 @@ class Skinner {
}
// Now that we have a skin, load our components too
// eslint-disable-next-line @typescript-eslint/no-var-requires
const idx = require("./component-index");
if (!idx || !idx.components) throw new Error("Invalid react-sdk component index");
for (const c in idx.components) {
@ -79,7 +88,7 @@ class Skinner {
}
}
addComponent(name, comp) {
public addComponent(name: string, comp: any) {
let slot = name;
if (comp.replaces !== undefined) {
if (comp.replaces.indexOf('.') > -1) {
@ -91,7 +100,7 @@ class Skinner {
this.components[slot] = comp;
}
reset() {
public reset(): void {
this.components = null;
}
}
@ -105,8 +114,8 @@ class Skinner {
// See https://derickbailey.com/2016/03/09/creating-a-true-singleton-in-node-js-with-es6-symbols/
// or https://nodejs.org/api/modules.html#modules_module_caching_caveats
// ("Modules are cached based on their resolved filename")
if (global.mxSkinner === undefined) {
global.mxSkinner = new Skinner();
if (window.mxSkinner === undefined) {
window.mxSkinner = new Skinner();
}
export default global.mxSkinner;
export default window.mxSkinner;

View File

@ -14,9 +14,13 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { IThreepid, ThreepidMedium } from "matrix-js-sdk/src/@types/threepids";
import { MatrixClient } from "matrix-js-sdk/src/client";
import IdentityAuthClient from './IdentityAuthClient';
export async function getThreepidsWithBindStatus(client, filterMedium) {
export async function getThreepidsWithBindStatus(
client: MatrixClient, filterMedium?: ThreepidMedium,
): Promise<IThreepid[]> {
const userId = client.getUserId();
let { threepids } = await client.getThreePids();
@ -31,7 +35,7 @@ export async function getThreepidsWithBindStatus(client, filterMedium) {
const identityAccessToken = await authClient.getAccessToken({ check: false });
// Restructure for lookup query
const query = threepids.map(({ medium, address }) => [medium, address]);
const query = threepids.map(({ medium, address }): [string, string] => [medium, address]);
const lookupResults = await client.bulkLookupThreePids(query, identityAccessToken);
// Record which are already bound

View File

@ -42,7 +42,7 @@ import linkifyMatrix from "../../linkify-matrix";
import * as Lifecycle from '../../Lifecycle';
// LifecycleStore is not used but does listen to and dispatch actions
import '../../stores/LifecycleStore';
import PageTypes from '../../PageTypes';
import PageType from '../../PageTypes';
import createRoom, { IOpts } from "../../createRoom";
import { _t, _td, getCurrentLanguage } from '../../languageHandler';
@ -207,7 +207,7 @@ interface IState {
view: Views;
// What the LoggedInView would be showing if visible
// eslint-disable-next-line camelcase
page_type?: PageTypes;
page_type?: PageType;
// The ID of the room we're viewing. This is either populated directly
// in the case where we view a room by ID or by RoomView when it resolves
// what ID an alias points at.
@ -723,7 +723,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
break;
}
case 'view_my_groups':
this.setPage(PageTypes.MyGroups);
this.setPage(PageType.MyGroups);
this.notifyNewScreen('groups');
break;
case 'view_group':
@ -756,7 +756,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
localStorage.setItem("mx_seenSpacesBeta", "1");
// We just dispatch the page change rather than have to worry about
// what the logic is for each of these branches.
if (this.state.page_type === PageTypes.MyGroups) {
if (this.state.page_type === PageType.MyGroups) {
dis.dispatch({ action: 'view_last_screen' });
} else {
dis.dispatch({ action: 'view_my_groups' });
@ -842,7 +842,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
}
};
private setPage(pageType: string) {
private setPage(pageType: PageType) {
this.setState({
page_type: pageType,
});
@ -949,7 +949,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
this.setState({
view: Views.LOGGED_IN,
currentRoomId: roomInfo.room_id || null,
page_type: PageTypes.RoomView,
page_type: PageType.RoomView,
threepidInvite: roomInfo.threepid_invite,
roomOobData: roomInfo.oob_data,
ready: true,
@ -977,7 +977,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
currentGroupId: groupId,
currentGroupIsNew: payload.group_is_new,
});
this.setPage(PageTypes.GroupView);
this.setPage(PageType.GroupView);
this.notifyNewScreen('group/' + groupId);
}
@ -1020,7 +1020,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
justRegistered,
currentRoomId: null,
});
this.setPage(PageTypes.HomePage);
this.setPage(PageType.HomePage);
this.notifyNewScreen('home');
ThemeController.isLogin = false;
this.themeWatcher.recheck();
@ -1038,7 +1038,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
}
this.notifyNewScreen('user/' + userId);
this.setState({ currentUserId: userId });
this.setPage(PageTypes.UserView);
this.setPage(PageType.UserView);
});
}

View File

@ -29,10 +29,9 @@ import { ChevronFace, ContextMenuTooltipButton } from "../../structures/ContextM
import { DefaultTagID, TagID } from "../../../stores/room-list/models";
import { MessagePreviewStore } from "../../../stores/room-list/MessagePreviewStore";
import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar";
import { ALL_MESSAGES, ALL_MESSAGES_LOUD, MENTIONS_ONLY, MUTE } from "../../../RoomNotifs";
import { RoomNotifState } from "../../../RoomNotifs";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import NotificationBadge from "./NotificationBadge";
import { Volume } from "../../../RoomNotifsTypes";
import RoomListStore from "../../../stores/room-list/RoomListStore";
import RoomListActions from "../../../actions/RoomListActions";
import { ActionPayload } from "../../../dispatcher/payloads";
@ -364,7 +363,7 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
this.setState({ generalMenuPosition: null }); // hide the menu
};
private async saveNotifState(ev: ButtonEvent, newState: Volume) {
private async saveNotifState(ev: ButtonEvent, newState: RoomNotifState) {
ev.preventDefault();
ev.stopPropagation();
if (MatrixClientPeg.get().isGuest()) return;
@ -378,10 +377,10 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
}
}
private onClickAllNotifs = ev => this.saveNotifState(ev, ALL_MESSAGES);
private onClickAlertMe = ev => this.saveNotifState(ev, ALL_MESSAGES_LOUD);
private onClickMentions = ev => this.saveNotifState(ev, MENTIONS_ONLY);
private onClickMute = ev => this.saveNotifState(ev, MUTE);
private onClickAllNotifs = ev => this.saveNotifState(ev, RoomNotifState.AllMessages);
private onClickAlertMe = ev => this.saveNotifState(ev, RoomNotifState.AllMessagesLoud);
private onClickMentions = ev => this.saveNotifState(ev, RoomNotifState.MentionsOnly);
private onClickMute = ev => this.saveNotifState(ev, RoomNotifState.Mute);
private renderNotificationsMenu(isActive: boolean): React.ReactElement {
if (MatrixClientPeg.get().isGuest() || this.props.tag === DefaultTagID.Archived ||
@ -404,25 +403,25 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
<IconizedContextMenuOptionList first>
<IconizedContextMenuRadio
label={_t("Use default")}
active={state === ALL_MESSAGES}
active={state === RoomNotifState.AllMessages}
iconClassName="mx_RoomTile_iconBell"
onClick={this.onClickAllNotifs}
/>
<IconizedContextMenuRadio
label={_t("All messages")}
active={state === ALL_MESSAGES_LOUD}
active={state === RoomNotifState.AllMessagesLoud}
iconClassName="mx_RoomTile_iconBellDot"
onClick={this.onClickAlertMe}
/>
<IconizedContextMenuRadio
label={_t("Mentions & Keywords")}
active={state === MENTIONS_ONLY}
active={state === RoomNotifState.MentionsOnly}
iconClassName="mx_RoomTile_iconBellMentions"
onClick={this.onClickMentions}
/>
<IconizedContextMenuRadio
label={_t("None")}
active={state === MUTE}
active={state === RoomNotifState.Mute}
iconClassName="mx_RoomTile_iconBellCrossed"
onClick={this.onClickMute}
/>
@ -432,14 +431,14 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
const classes = classNames("mx_RoomTile_notificationsButton", {
// Show bell icon for the default case too.
mx_RoomTile_iconBell: state === ALL_MESSAGES,
mx_RoomTile_iconBellDot: state === ALL_MESSAGES_LOUD,
mx_RoomTile_iconBellMentions: state === MENTIONS_ONLY,
mx_RoomTile_iconBellCrossed: state === MUTE,
mx_RoomTile_iconBell: state === RoomNotifState.AllMessages,
mx_RoomTile_iconBellDot: state === RoomNotifState.AllMessagesLoud,
mx_RoomTile_iconBellMentions: state === RoomNotifState.MentionsOnly,
mx_RoomTile_iconBellCrossed: state === RoomNotifState.Mute,
// Only show the icon by default if the room is overridden to muted.
// TODO: [FTUE Notifications] Probably need to detect global mute state
mx_RoomTile_notificationsButton_show: state === MUTE,
mx_RoomTile_notificationsButton_show: state === RoomNotifState.Mute,
});
return (

View File

@ -15,20 +15,21 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import Skinner from './Skinner';
import Skinner, { ISkinObject } from './Skinner';
export function loadSkin(skinObject) {
export function loadSkin(skinObject: ISkinObject): void {
Skinner.load(skinObject);
}
export function resetSkin() {
export function resetSkin(): void {
Skinner.reset();
}
export function getComponent(componentName) {
export function getComponent(componentName: string): any {
return Skinner.getComponent(componentName);
}
// Import the js-sdk so the proper `request` object can be set. This does some
// magic with the browser injection to make all subsequent imports work fine.
import "matrix-js-sdk";

View File

@ -22,7 +22,14 @@ import {
tryTransformPermalinkToLocalHref,
} from "./utils/permalinks/Permalinks";
function matrixLinkify(linkify) {
enum Type {
URL = "url",
UserId = "userid",
RoomAlias = "roomalias",
GroupId = "groupid"
}
function matrixLinkify(linkify): void {
// Text tokens
const TT = linkify.scanner.TOKENS;
// Multi tokens
@ -173,11 +180,11 @@ function matrixLinkify(linkify) {
}
// stubs, overwritten in MatrixChat's componentDidMount
matrixLinkify.onUserClick = function(e, userId) { e.preventDefault(); };
matrixLinkify.onAliasClick = function(e, roomAlias) { e.preventDefault(); };
matrixLinkify.onGroupClick = function(e, groupId) { e.preventDefault(); };
matrixLinkify.onUserClick = function(e: MouseEvent, userId: string) { e.preventDefault(); };
matrixLinkify.onAliasClick = function(e: MouseEvent, roomAlias: string) { e.preventDefault(); };
matrixLinkify.onGroupClick = function(e: MouseEvent, groupId: string) { e.preventDefault(); };
const escapeRegExp = function(string) {
const escapeRegExp = function(string): string {
return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
};
@ -196,14 +203,15 @@ matrixLinkify.MATRIXTO_MD_LINK_PATTERN =
matrixLinkify.MATRIXTO_BASE_URL= baseUrl;
matrixLinkify.options = {
events: function(href, type) {
events: function(href: string, type: Type | string): Partial<GlobalEventHandlers> {
switch (type) {
case "url": {
case Type.URL: {
// intercept local permalinks to users and show them like userids (in userinfo of current room)
try {
const permalink = parsePermalink(href);
if (permalink && permalink.userId) {
return {
// @ts-ignore see https://linkify.js.org/docs/options.html
click: function(e) {
matrixLinkify.onUserClick(e, permalink.userId);
},
@ -214,20 +222,23 @@ matrixLinkify.options = {
}
break;
}
case "userid":
case Type.UserId:
return {
// @ts-ignore see https://linkify.js.org/docs/options.html
click: function(e) {
matrixLinkify.onUserClick(e, href);
},
};
case "roomalias":
case Type.RoomAlias:
return {
// @ts-ignore see https://linkify.js.org/docs/options.html
click: function(e) {
matrixLinkify.onAliasClick(e, href);
},
};
case "groupid":
case Type.GroupId:
return {
// @ts-ignore see https://linkify.js.org/docs/options.html
click: function(e) {
matrixLinkify.onGroupClick(e, href);
},
@ -235,11 +246,11 @@ matrixLinkify.options = {
}
},
formatHref: function(href, type) {
formatHref: function(href: string, type: Type | string): string {
switch (type) {
case 'roomalias':
case 'userid':
case 'groupid':
case Type.RoomAlias:
case Type.UserId:
case Type.GroupId:
default: {
return tryTransformEntityToPermalink(href);
}
@ -250,8 +261,8 @@ matrixLinkify.options = {
rel: 'noreferrer noopener',
},
target: function(href, type) {
if (type === 'url') {
target: function(href: string, type: Type | string): string {
if (type === Type.URL) {
try {
const transformed = tryTransformPermalinkToLocalHref(href);
if (transformed !== href || decodeURIComponent(href).match(matrixLinkify.ELEMENT_URL_PATTERN)) {

View File

@ -15,20 +15,17 @@ limitations under the License.
*/
import { GenericEchoChamber, implicitlyReverted, PROPERTY_UPDATED } from "./GenericEchoChamber";
import { getRoomNotifsState, setRoomNotifsState } from "../../RoomNotifs";
import { getRoomNotifsState, RoomNotifState, setRoomNotifsState } from "../../RoomNotifs";
import { RoomEchoContext } from "./RoomEchoContext";
import { _t } from "../../languageHandler";
import { Volume } from "../../RoomNotifsTypes";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
export type CachedRoomValues = Volume;
export enum CachedRoomKey {
NotificationVolume,
}
export class RoomEchoChamber extends GenericEchoChamber<RoomEchoContext, CachedRoomKey, CachedRoomValues> {
private properties = new Map<CachedRoomKey, CachedRoomValues>();
export class RoomEchoChamber extends GenericEchoChamber<RoomEchoContext, CachedRoomKey, RoomNotifState> {
private properties = new Map<CachedRoomKey, RoomNotifState>();
public constructor(context: RoomEchoContext) {
super(context, (k) => this.properties.get(k));
@ -50,8 +47,8 @@ export class RoomEchoChamber extends GenericEchoChamber<RoomEchoContext, CachedR
private onAccountData = (event: MatrixEvent) => {
if (event.getType() === "m.push_rules") {
const currentVolume = this.properties.get(CachedRoomKey.NotificationVolume) as Volume;
const newVolume = getRoomNotifsState(this.context.room.roomId) as Volume;
const currentVolume = this.properties.get(CachedRoomKey.NotificationVolume) as RoomNotifState;
const newVolume = getRoomNotifsState(this.context.room.roomId) as RoomNotifState;
if (currentVolume !== newVolume) {
this.updateNotificationVolume();
}
@ -66,11 +63,11 @@ export class RoomEchoChamber extends GenericEchoChamber<RoomEchoContext, CachedR
// ---- helpers below here ----
public get notificationVolume(): Volume {
public get notificationVolume(): RoomNotifState {
return this.getValue(CachedRoomKey.NotificationVolume);
}
public set notificationVolume(v: Volume) {
public set notificationVolume(v: RoomNotifState) {
this.setValue(_t("Change notification settings"), CachedRoomKey.NotificationVolume, v, async () => {
return setRoomNotifsState(this.context.room.roomId, v);
}, implicitlyReverted);

View File

@ -20,7 +20,7 @@ import { MatrixClientPeg } from "../../MatrixClientPeg";
import { EffectiveMembership, getEffectiveMembership } from "../../utils/membership";
import { readReceiptChangeIsFor } from "../../utils/read-receipts";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { Room } from "matrix-js-sdk/src/models/room";
import { NotificationCountType, Room } from "matrix-js-sdk/src/models/room";
import * as RoomNotifs from '../../RoomNotifs';
import * as Unread from '../../Unread';
import { NotificationState } from "./NotificationState";
@ -91,7 +91,7 @@ export class RoomNotificationState extends NotificationState implements IDestroy
this._color = NotificationColor.Unsent;
this._symbol = "!";
this._count = 1; // not used, technically
} else if (RoomNotifs.getRoomNotifsState(this.room.roomId) === RoomNotifs.MUTE) {
} else if (RoomNotifs.getRoomNotifsState(this.room.roomId) === RoomNotifs.RoomNotifState.Mute) {
// When muted we suppress all notification states, even if we have context on them.
this._color = NotificationColor.None;
this._symbol = null;
@ -101,8 +101,8 @@ export class RoomNotificationState extends NotificationState implements IDestroy
this._symbol = "!";
this._count = 1; // not used, technically
} else {
const redNotifs = RoomNotifs.getUnreadNotificationCount(this.room, 'highlight');
const greyNotifs = RoomNotifs.getUnreadNotificationCount(this.room, 'total');
const redNotifs = RoomNotifs.getUnreadNotificationCount(this.room, NotificationCountType.Highlight);
const greyNotifs = RoomNotifs.getUnreadNotificationCount(this.room, NotificationCountType.Total);
// For a 'true count' we pick the grey notifications first because they include the
// red notifications. If we don't have a grey count for some reason we use the red

View File

@ -17,11 +17,32 @@ limitations under the License.
import { _t } from "./languageHandler";
export const DEFAULT_THEME = "light";
import SettingsStore from "./settings/SettingsStore";
import ThemeWatcher from "./settings/watchers/ThemeWatcher";
export function enumerateThemes() {
export const DEFAULT_THEME = "light";
interface IFontFaces {
src: {
format: string;
url: string;
local: string;
}[];
}
interface ICustomTheme {
colors: {
[key: string]: string;
};
fonts: {
faces: IFontFaces[];
general: string;
monospace: string;
};
is_dark?: boolean; // eslint-disable-line camelcase
}
export function enumerateThemes(): {[key: string]: string} {
const BUILTIN_THEMES = {
"light": _t("Light"),
"dark": _t("Dark"),
@ -34,7 +55,7 @@ export function enumerateThemes() {
return Object.assign({}, customThemeNames, BUILTIN_THEMES);
}
function clearCustomTheme() {
function clearCustomTheme(): void {
// remove all css variables, we assume these are there because of the custom theme
const inlineStyleProps = Object.values(document.body.style);
for (const prop of inlineStyleProps) {
@ -61,7 +82,7 @@ const allowedFontFaceProps = [
"unicode-range",
];
function generateCustomFontFaceCSS(faces) {
function generateCustomFontFaceCSS(faces: IFontFaces[]): string {
return faces.map(face => {
const src = face.src && face.src.map(srcElement => {
let format;
@ -91,7 +112,7 @@ function generateCustomFontFaceCSS(faces) {
}).join("\n");
}
function setCustomThemeVars(customTheme) {
function setCustomThemeVars(customTheme: ICustomTheme): void {
const { style } = document.body;
function setCSSColorVariable(name, hexColor, doPct = true) {
@ -134,7 +155,7 @@ function setCustomThemeVars(customTheme) {
}
}
export function getCustomTheme(themeName) {
export function getCustomTheme(themeName: string): ICustomTheme {
// set css variables
const customThemes = SettingsStore.getValue("custom_themes");
if (!customThemes) {
@ -155,7 +176,7 @@ export function getCustomTheme(themeName) {
*
* @param {string} theme new theme
*/
export async function setTheme(theme) {
export async function setTheme(theme: string): Promise<void> {
if (!theme) {
const themeWatcher = new ThemeWatcher();
theme = themeWatcher.getEffectiveTheme();
@ -200,13 +221,14 @@ export async function setTheme(theme) {
// We could alternatively lock or similar to stop the race, but
// this is probably good enough for now.
styleElements[stylesheetName].disabled = false;
Object.values(styleElements).forEach((a) => {
Object.values(styleElements).forEach((a: HTMLStyleElement) => {
if (a == styleElements[stylesheetName]) return;
a.disabled = true;
});
const bodyStyles = global.getComputedStyle(document.body);
if (bodyStyles.backgroundColor) {
document.querySelector('meta[name="theme-color"]').content = bodyStyles.backgroundColor;
const metaElement: HTMLMetaElement = document.querySelector('meta[name="theme-color"]');
metaElement.content = bodyStyles.backgroundColor;
}
resolve();
};

View File

@ -16,8 +16,8 @@ limitations under the License.
// Returns a promise which resolves when the input promise resolves with its value
// or when the timeout of ms is reached with the value of given timeoutValue
export async function timeout<T>(promise: Promise<T>, timeoutValue: T, ms: number): Promise<T> {
const timeoutPromise = new Promise<T>((resolve) => {
export async function timeout<T, Y>(promise: Promise<T>, timeoutValue: Y, ms: number): Promise<T | Y> {
const timeoutPromise = new Promise<T | Y>((resolve) => {
const timeoutId = setTimeout(resolve, ms, timeoutValue);
promise.then(() => {
clearTimeout(timeoutId);