Merge branch 'develop' of github.com:matrix-org/matrix-react-sdk into joriks/fix-read-receipts
To fix typespull/21833/head
						commit
						399dd6a225
					
				| 
						 | 
				
			
			@ -131,6 +131,11 @@ $roomListMinimizedWidth: 50px;
 | 
			
		|||
        .mx_LeftPanel2_actualRoomListContainer {
 | 
			
		||||
            flex-grow: 1; // fill the available space
 | 
			
		||||
            overflow-y: auto;
 | 
			
		||||
            width: 100%;
 | 
			
		||||
            max-width: 100%;
 | 
			
		||||
 | 
			
		||||
            // Create a flexbox to trick the layout engine
 | 
			
		||||
            display: flex;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -28,7 +28,6 @@ limitations under the License.
 | 
			
		|||
 | 
			
		||||
    &.mx_NotificationBadge_visible {
 | 
			
		||||
        background-color: $roomtile2-badge-color;
 | 
			
		||||
        margin-right: 14px;
 | 
			
		||||
 | 
			
		||||
        // Create a flexbox to order the count a bit easier
 | 
			
		||||
        display: flex;
 | 
			
		||||
| 
						 | 
				
			
			@ -46,7 +45,6 @@ limitations under the License.
 | 
			
		|||
            width: 6px;
 | 
			
		||||
            height: 6px;
 | 
			
		||||
            border-radius: 6px;
 | 
			
		||||
            margin-right: 8px;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        &.mx_NotificationBadge_2char {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -17,9 +17,11 @@ limitations under the License.
 | 
			
		|||
// TODO: Rename to mx_RoomList during replacement of old component
 | 
			
		||||
 | 
			
		||||
.mx_RoomList2 {
 | 
			
		||||
    width: calc(100% - 16px); // 16px of artificial right-side margin (8px is overflowed from the sublists)
 | 
			
		||||
 | 
			
		||||
    // Create a column-based flexbox for the sublists. That's pretty much all we have to
 | 
			
		||||
    // worry about in this stylesheet.
 | 
			
		||||
    display: flex;
 | 
			
		||||
    flex-direction: column;
 | 
			
		||||
    flex-wrap: wrap;
 | 
			
		||||
    flex-wrap: nowrap; // let the column overflow
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -21,7 +21,7 @@ limitations under the License.
 | 
			
		|||
    display: flex;
 | 
			
		||||
    flex-direction: column;
 | 
			
		||||
 | 
			
		||||
    padding-left: 8px;
 | 
			
		||||
    margin-left: 8px;
 | 
			
		||||
    margin-top: 12px;
 | 
			
		||||
    margin-bottom: 12px;
 | 
			
		||||
    width: 100%;
 | 
			
		||||
| 
						 | 
				
			
			@ -30,16 +30,18 @@ limitations under the License.
 | 
			
		|||
        // Create a flexbox to make ordering easy
 | 
			
		||||
        display: flex;
 | 
			
		||||
        align-items: center;
 | 
			
		||||
        padding-bottom: 8px;
 | 
			
		||||
        height: 24px;
 | 
			
		||||
 | 
			
		||||
        .mx_RoomSublist2_badgeContainer {
 | 
			
		||||
            opacity: 0.8;
 | 
			
		||||
            padding-right: 7px;
 | 
			
		||||
            width: 16px;
 | 
			
		||||
            margin-right: 5px; // aligns with the room tile's badge
 | 
			
		||||
 | 
			
		||||
            // Create another flexbox row because it's super easy to position the badge at
 | 
			
		||||
            // the end this way.
 | 
			
		||||
            // Create another flexbox row because it's super easy to position the badge this way.
 | 
			
		||||
            display: flex;
 | 
			
		||||
            align-items: center;
 | 
			
		||||
            justify-content: flex-end;
 | 
			
		||||
            justify-content: center;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Both of these buttons are hidden by default until the list is hovered
 | 
			
		||||
| 
						 | 
				
			
			@ -77,10 +79,9 @@ limitations under the License.
 | 
			
		|||
            opacity: 0.5;
 | 
			
		||||
            line-height: $font-16px;
 | 
			
		||||
            font-size: $font-12px;
 | 
			
		||||
            padding-bottom: 8px;
 | 
			
		||||
 | 
			
		||||
            width: 100%;
 | 
			
		||||
            flex: 1;
 | 
			
		||||
            max-width: calc(100% - 16px); // 16px is the badge width
 | 
			
		||||
 | 
			
		||||
            // Ellipsize any text overflow
 | 
			
		||||
            text-overflow: ellipsis;
 | 
			
		||||
| 
						 | 
				
			
			@ -158,7 +159,7 @@ limitations under the License.
 | 
			
		|||
            // either side of the list. We define this after the positioning to
 | 
			
		||||
            // trick the browser.
 | 
			
		||||
            margin-left: 4px;
 | 
			
		||||
            margin-right: 8px;
 | 
			
		||||
            margin-right: 4px;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -184,7 +185,7 @@ limitations under the License.
 | 
			
		|||
            &:not(.mx_RoomSublist2_headerContainer_withAux) {
 | 
			
		||||
                // The menu button will be the rightmost button, so make it correctly aligned.
 | 
			
		||||
                .mx_RoomSublist2_menuButton {
 | 
			
		||||
                    margin-right: 16px;
 | 
			
		||||
                    margin-right: 1px; // line it up with the badges on the room tiles
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -218,7 +219,7 @@ limitations under the License.
 | 
			
		|||
            // Show the aux button, but not the list button
 | 
			
		||||
            width: 24px;
 | 
			
		||||
            height: 24px;
 | 
			
		||||
            margin-right: 16px;
 | 
			
		||||
            margin-right: 1px; // line it up with the badges on the room tiles
 | 
			
		||||
            visibility: visible;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -18,9 +18,7 @@ limitations under the License.
 | 
			
		|||
 | 
			
		||||
// Note: the room tile expects to be in a flexbox column container
 | 
			
		||||
.mx_RoomTile2 {
 | 
			
		||||
    width: calc(100% - 21px); // 10px for alignment/inset, 8px for padding on sides, 3px for margin
 | 
			
		||||
    margin-bottom: 4px;
 | 
			
		||||
    margin-right: 3px;
 | 
			
		||||
    padding: 4px;
 | 
			
		||||
 | 
			
		||||
    // The tile is also a flexbox row itself
 | 
			
		||||
| 
						 | 
				
			
			@ -84,7 +82,7 @@ limitations under the License.
 | 
			
		|||
        // the end this way.
 | 
			
		||||
        display: flex;
 | 
			
		||||
        align-items: center;
 | 
			
		||||
        justify-content: flex-end;
 | 
			
		||||
        justify-content: center;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // The menu button is hidden by default
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -265,13 +265,22 @@ function textForServerACLEvent(ev) {
 | 
			
		|||
    return text + changes.join(" ");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function textForMessageEvent(ev) {
 | 
			
		||||
function textForMessageEvent(ev, skipUserPrefix) {
 | 
			
		||||
    const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
 | 
			
		||||
    let message = senderDisplayName + ': ' + ev.getContent().body;
 | 
			
		||||
    if (ev.getContent().msgtype === "m.emote") {
 | 
			
		||||
        message = "* " + senderDisplayName + " " + message;
 | 
			
		||||
    } else if (ev.getContent().msgtype === "m.image") {
 | 
			
		||||
        message = _t('%(senderDisplayName)s sent an image.', {senderDisplayName});
 | 
			
		||||
    if (skipUserPrefix) {
 | 
			
		||||
        message = ev.getContent().body;
 | 
			
		||||
        if (ev.getContent().msgtype === "m.emote") {
 | 
			
		||||
            message = senderDisplayName + " " + message;
 | 
			
		||||
        } else if (ev.getContent().msgtype === "m.image") {
 | 
			
		||||
            message = _t('sent an image.');
 | 
			
		||||
        }
 | 
			
		||||
    } else {
 | 
			
		||||
        if (ev.getContent().msgtype === "m.emote") {
 | 
			
		||||
            message = "* " + senderDisplayName + " " + message;
 | 
			
		||||
        } else if (ev.getContent().msgtype === "m.image") {
 | 
			
		||||
            message = _t('%(senderDisplayName)s sent an image.', {senderDisplayName});
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    return message;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -612,8 +621,8 @@ for (const evType of ALL_RULE_TYPES) {
 | 
			
		|||
    stateHandlers[evType] = textForMjolnirEvent;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function textForEvent(ev) {
 | 
			
		||||
export function textForEvent(ev, skipUserPrefix) {
 | 
			
		||||
    const handler = (ev.isState() ? stateHandlers : handlers)[ev.getType()];
 | 
			
		||||
    if (handler) return handler(ev);
 | 
			
		||||
    if (handler) return handler(ev, skipUserPrefix);
 | 
			
		||||
    return '';
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -31,7 +31,7 @@ import LogoutDialog from "../views/dialogs/LogoutDialog";
 | 
			
		|||
import SettingsStore, {SettingLevel} from "../../settings/SettingsStore";
 | 
			
		||||
import {getCustomTheme} from "../../theme";
 | 
			
		||||
import {getHostingLink} from "../../utils/HostingLink";
 | 
			
		||||
import AccessibleButton from "../views/elements/AccessibleButton";
 | 
			
		||||
import AccessibleButton, {ButtonEvent} from "../views/elements/AccessibleButton";
 | 
			
		||||
 | 
			
		||||
interface IProps {
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -114,7 +114,7 @@ export default class UserMenuButton extends React.Component<IProps, IState> {
 | 
			
		|||
        SettingsStore.setValue("theme", null, SettingLevel.ACCOUNT, newTheme);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    private onSettingsOpen = (ev: React.MouseEvent, tabId: string) => {
 | 
			
		||||
    private onSettingsOpen = (ev: ButtonEvent, tabId: string) => {
 | 
			
		||||
        ev.preventDefault();
 | 
			
		||||
        ev.stopPropagation();
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -123,7 +123,7 @@ export default class UserMenuButton extends React.Component<IProps, IState> {
 | 
			
		|||
        this.setState({menuDisplayed: false}); // also close the menu
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    private onShowArchived = (ev: React.MouseEvent) => {
 | 
			
		||||
    private onShowArchived = (ev: ButtonEvent) => {
 | 
			
		||||
        ev.preventDefault();
 | 
			
		||||
        ev.stopPropagation();
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -131,7 +131,7 @@ export default class UserMenuButton extends React.Component<IProps, IState> {
 | 
			
		|||
        console.log("TODO: Show archived rooms");
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    private onProvideFeedback = (ev: React.MouseEvent) => {
 | 
			
		||||
    private onProvideFeedback = (ev: ButtonEvent) => {
 | 
			
		||||
        ev.preventDefault();
 | 
			
		||||
        ev.stopPropagation();
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -139,7 +139,7 @@ export default class UserMenuButton extends React.Component<IProps, IState> {
 | 
			
		|||
        this.setState({menuDisplayed: false}); // also close the menu
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    private onSignOutClick = (ev: React.MouseEvent) => {
 | 
			
		||||
    private onSignOutClick = (ev: ButtonEvent) => {
 | 
			
		||||
        ev.preventDefault();
 | 
			
		||||
        ev.stopPropagation();
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -19,6 +19,8 @@ import React from 'react';
 | 
			
		|||
import {Key} from '../../../Keyboard';
 | 
			
		||||
import classnames from 'classnames';
 | 
			
		||||
 | 
			
		||||
export type ButtonEvent = React.MouseEvent<Element> | React.KeyboardEvent<Element>
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * children: React's magic prop. Represents all children given to the element.
 | 
			
		||||
 * element:  (optional) The base element type. "div" by default.
 | 
			
		||||
| 
						 | 
				
			
			@ -37,7 +39,7 @@ interface IProps extends React.InputHTMLAttributes<Element> {
 | 
			
		|||
    tabIndex?: number;
 | 
			
		||||
    disabled?: boolean;
 | 
			
		||||
    className?: string;
 | 
			
		||||
    onClick?(e?: React.MouseEvent<Element> | React.KeyboardEvent<Element>): void;
 | 
			
		||||
    onClick?(e?: ButtonEvent): void;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
interface IAccessibleButtonProps extends React.InputHTMLAttributes<Element> {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -237,11 +237,11 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
 | 
			
		|||
                            >
 | 
			
		||||
                                <span>{this.props.label}</span>
 | 
			
		||||
                            </AccessibleButton>
 | 
			
		||||
                            {this.renderMenu()}
 | 
			
		||||
                            {addRoomButton}
 | 
			
		||||
                            <div className="mx_RoomSublist2_badgeContainer">
 | 
			
		||||
                                {badge}
 | 
			
		||||
                            </div>
 | 
			
		||||
                            {this.renderMenu()}
 | 
			
		||||
                            {addRoomButton}
 | 
			
		||||
                        </div>
 | 
			
		||||
                    );
 | 
			
		||||
                }}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -21,7 +21,7 @@ import React, { createRef } from "react";
 | 
			
		|||
import { Room } from "matrix-js-sdk/src/models/room";
 | 
			
		||||
import classNames from "classnames";
 | 
			
		||||
import { RovingTabIndexWrapper } from "../../../accessibility/RovingTabIndex";
 | 
			
		||||
import AccessibleButton from "../../views/elements/AccessibleButton";
 | 
			
		||||
import AccessibleButton, {ButtonEvent} from "../../views/elements/AccessibleButton";
 | 
			
		||||
import RoomAvatar from "../../views/avatars/RoomAvatar";
 | 
			
		||||
import dis from '../../../dispatcher/dispatcher';
 | 
			
		||||
import { Key } from "../../../Keyboard";
 | 
			
		||||
| 
						 | 
				
			
			@ -30,6 +30,7 @@ import NotificationBadge, { INotificationState, NotificationColor, RoomNotificat
 | 
			
		|||
import { _t } from "../../../languageHandler";
 | 
			
		||||
import { ContextMenu, ContextMenuButton } from "../../structures/ContextMenu";
 | 
			
		||||
import { DefaultTagID, TagID } from "../../../stores/room-list/models";
 | 
			
		||||
import { MessagePreviewStore } from "../../../stores/MessagePreviewStore";
 | 
			
		||||
 | 
			
		||||
/*******************************************************************
 | 
			
		||||
 *   CAUTION                                                       *
 | 
			
		||||
| 
						 | 
				
			
			@ -123,7 +124,7 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
 | 
			
		|||
        this.setState({generalMenuDisplayed: false});
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    private onTagRoom = (ev: React.MouseEvent, tagId: TagID) => {
 | 
			
		||||
    private onTagRoom = (ev: ButtonEvent, tagId: TagID) => {
 | 
			
		||||
        ev.preventDefault();
 | 
			
		||||
        ev.stopPropagation();
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -134,7 +135,7 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
 | 
			
		|||
        }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    private onLeaveRoomClick = (ev: React.MouseEvent) => {
 | 
			
		||||
    private onLeaveRoomClick = (ev: ButtonEvent) => {
 | 
			
		||||
        ev.preventDefault();
 | 
			
		||||
        ev.stopPropagation();
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -145,7 +146,7 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
 | 
			
		|||
        this.setState({generalMenuDisplayed: false}); // hide the menu
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    private onOpenRoomSettings = (ev: React.MouseEvent) => {
 | 
			
		||||
    private onOpenRoomSettings = (ev: ButtonEvent) => {
 | 
			
		||||
        ev.preventDefault();
 | 
			
		||||
        ev.stopPropagation();
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -253,8 +254,17 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
 | 
			
		|||
 | 
			
		||||
        let messagePreview = null;
 | 
			
		||||
        if (this.props.showMessagePreview) {
 | 
			
		||||
            // TODO: Actually get the real message preview from state
 | 
			
		||||
            messagePreview = <div className="mx_RoomTile2_messagePreview">I just ate a pie.</div>;
 | 
			
		||||
            // The preview store heavily caches this info, so should be safe to hammer.
 | 
			
		||||
            const text = MessagePreviewStore.instance.getPreviewForRoom(this.props.room);
 | 
			
		||||
 | 
			
		||||
            // Only show the preview if there is one to show.
 | 
			
		||||
            if (text) {
 | 
			
		||||
                messagePreview = (
 | 
			
		||||
                    <div className="mx_RoomTile2_messagePreview">
 | 
			
		||||
                        {text}
 | 
			
		||||
                    </div>
 | 
			
		||||
                );
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const nameClasses = classNames({
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -427,7 +427,9 @@ export default class SendMessageComposer extends React.Component {
 | 
			
		|||
 | 
			
		||||
    _onPaste = (event) => {
 | 
			
		||||
        const {clipboardData} = event;
 | 
			
		||||
        if (clipboardData.files.length) {
 | 
			
		||||
        // Prioritize text on the clipboard over files as Office on macOS puts a bitmap
 | 
			
		||||
        // in the clipboard as well as the content being copied.
 | 
			
		||||
        if (clipboardData.files.length && !clipboardData.types.some(t => t === "text/plain")) {
 | 
			
		||||
            // This actually not so much for 'files' as such (at time of writing
 | 
			
		||||
            // neither chrome nor firefox let you paste a plain file copied
 | 
			
		||||
            // from Finder) but more images copied from a different website
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -246,6 +246,7 @@
 | 
			
		|||
    "%(senderDisplayName)s enabled flair for %(groups)s in this room.": "%(senderDisplayName)s enabled flair for %(groups)s in this room.",
 | 
			
		||||
    "%(senderDisplayName)s disabled flair for %(groups)s in this room.": "%(senderDisplayName)s disabled flair for %(groups)s in this room.",
 | 
			
		||||
    "%(senderDisplayName)s enabled flair for %(newGroups)s and disabled flair for %(oldGroups)s in this room.": "%(senderDisplayName)s enabled flair for %(newGroups)s and disabled flair for %(oldGroups)s in this room.",
 | 
			
		||||
    "sent an image.": "sent an image.",
 | 
			
		||||
    "%(senderDisplayName)s sent an image.": "%(senderDisplayName)s sent an image.",
 | 
			
		||||
    "%(senderName)s set the main address for this room to %(address)s.": "%(senderName)s set the main address for this room to %(address)s.",
 | 
			
		||||
    "%(senderName)s removed the main address for this room.": "%(senderName)s removed the main address for this room.",
 | 
			
		||||
| 
						 | 
				
			
			@ -419,6 +420,7 @@
 | 
			
		|||
    "Restart": "Restart",
 | 
			
		||||
    "Upgrade your Riot": "Upgrade your Riot",
 | 
			
		||||
    "A new version of Riot is available!": "A new version of Riot is available!",
 | 
			
		||||
    "You: %(message)s": "You: %(message)s",
 | 
			
		||||
    "There was an error joining the room": "There was an error joining the room",
 | 
			
		||||
    "Sorry, your homeserver is too old to participate in this room.": "Sorry, your homeserver is too old to participate in this room.",
 | 
			
		||||
    "Please contact your homeserver administrator.": "Please contact your homeserver administrator.",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -42,18 +42,20 @@ export const UPDATE_EVENT = "update";
 | 
			
		|||
 * help prevent lock conflicts.
 | 
			
		||||
 */
 | 
			
		||||
export abstract class AsyncStore<T extends Object> extends EventEmitter {
 | 
			
		||||
    private storeState: T = <T>{};
 | 
			
		||||
    private storeState: T;
 | 
			
		||||
    private lock = new AwaitLock();
 | 
			
		||||
    private readonly dispatcherRef: string;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Creates a new AsyncStore using the given dispatcher.
 | 
			
		||||
     * @param {Dispatcher<ActionPayload>} dispatcher The dispatcher to rely upon.
 | 
			
		||||
     * @param {T} initialState The initial state for the store.
 | 
			
		||||
     */
 | 
			
		||||
    protected constructor(private dispatcher: Dispatcher<ActionPayload>) {
 | 
			
		||||
    protected constructor(private dispatcher: Dispatcher<ActionPayload>, initialState: T = <T>{}) {
 | 
			
		||||
        super();
 | 
			
		||||
 | 
			
		||||
        this.dispatcherRef = dispatcher.register(this.onDispatch.bind(this));
 | 
			
		||||
        this.storeState = initialState;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,135 @@
 | 
			
		|||
/*
 | 
			
		||||
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 { Room } from "matrix-js-sdk/src/models/room";
 | 
			
		||||
import { ActionPayload } from "../dispatcher/payloads";
 | 
			
		||||
import { AsyncStoreWithClient } from "./AsyncStoreWithClient";
 | 
			
		||||
import defaultDispatcher from "../dispatcher/dispatcher";
 | 
			
		||||
import { RoomListStoreTempProxy } from "./room-list/RoomListStoreTempProxy";
 | 
			
		||||
import { textForEvent } from "../TextForEvent";
 | 
			
		||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
 | 
			
		||||
import { _t } from "../languageHandler";
 | 
			
		||||
 | 
			
		||||
const PREVIEWABLE_EVENTS = [
 | 
			
		||||
    // This is the same list from RiotX
 | 
			
		||||
    {type: "m.room.message", isState: false},
 | 
			
		||||
    {type: "m.room.name", isState: true},
 | 
			
		||||
    {type: "m.room.topic", isState: true},
 | 
			
		||||
    {type: "m.room.member", isState: true},
 | 
			
		||||
    {type: "m.room.history_visibility", isState: true},
 | 
			
		||||
    {type: "m.call.invite", isState: false},
 | 
			
		||||
    {type: "m.call.hangup", isState: false},
 | 
			
		||||
    {type: "m.call.answer", isState: false},
 | 
			
		||||
    {type: "m.room.encrypted", isState: false},
 | 
			
		||||
    {type: "m.room.encryption", isState: true},
 | 
			
		||||
    {type: "m.room.third_party_invite", isState: true},
 | 
			
		||||
    {type: "m.sticker", isState: false},
 | 
			
		||||
    {type: "m.room.create", isState: true},
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
// The maximum number of events we're willing to look back on to get a preview.
 | 
			
		||||
const MAX_EVENTS_BACKWARDS = 50;
 | 
			
		||||
 | 
			
		||||
interface IState {
 | 
			
		||||
    [roomId: string]: string | null; // null indicates the preview is empty
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class MessagePreviewStore extends AsyncStoreWithClient<IState> {
 | 
			
		||||
    private static internalInstance = new MessagePreviewStore();
 | 
			
		||||
 | 
			
		||||
    private constructor() {
 | 
			
		||||
        super(defaultDispatcher, {});
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static get instance(): MessagePreviewStore {
 | 
			
		||||
        return MessagePreviewStore.internalInstance;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Gets the pre-translated preview for a given room
 | 
			
		||||
     * @param room The room to get the preview for.
 | 
			
		||||
     * @returns The preview, or null if none present.
 | 
			
		||||
     */
 | 
			
		||||
    public getPreviewForRoom(room: Room): string {
 | 
			
		||||
        if (!room) return null; // invalid room, just return nothing
 | 
			
		||||
 | 
			
		||||
        // It's faster to do a lookup this way than it is to use Object.keys().includes()
 | 
			
		||||
        // We only want to generate a preview if there's one actually missing and not explicitly
 | 
			
		||||
        // set as 'none'.
 | 
			
		||||
        const val = this.state[room.roomId];
 | 
			
		||||
        if (val !== null && typeof(val) !== "string") {
 | 
			
		||||
            this.generatePreview(room);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return this.state[room.roomId];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private generatePreview(room: Room) {
 | 
			
		||||
        const timeline = room.getLiveTimeline();
 | 
			
		||||
        if (!timeline) return; // usually only happens in tests
 | 
			
		||||
        const events = timeline.getEvents();
 | 
			
		||||
 | 
			
		||||
        for (let i = events.length - 1; i >= 0; i--) {
 | 
			
		||||
            if (i === events.length - MAX_EVENTS_BACKWARDS) return; // limit reached
 | 
			
		||||
 | 
			
		||||
            const event = events[i];
 | 
			
		||||
            const preview = this.generatePreviewForEvent(event);
 | 
			
		||||
            if (preview.isPreviewable) {
 | 
			
		||||
                // noinspection JSIgnoredPromiseFromCall - the AsyncStore handles concurrent calls
 | 
			
		||||
                this.updateState({[room.roomId]: preview.preview});
 | 
			
		||||
                return; // break - we found some text
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // if we didn't find anything, subscribe ourselves to an update
 | 
			
		||||
        // noinspection JSIgnoredPromiseFromCall - the AsyncStore handles concurrent calls
 | 
			
		||||
        this.updateState({[room.roomId]: null});
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected async onAction(payload: ActionPayload) {
 | 
			
		||||
        if (!this.matrixClient) return;
 | 
			
		||||
 | 
			
		||||
        // TODO: Remove when new room list is made the default
 | 
			
		||||
        if (!RoomListStoreTempProxy.isUsingNewStore()) return;
 | 
			
		||||
 | 
			
		||||
        if (payload.action === 'MatrixActions.Room.timeline' || payload.action === 'MatrixActions.Event.decrypted') {
 | 
			
		||||
            const event = payload.event; // TODO: Type out the dispatcher
 | 
			
		||||
            if (!Object.keys(this.state).includes(event.getRoomId())) return; // not important
 | 
			
		||||
 | 
			
		||||
            const preview = this.generatePreviewForEvent(event);
 | 
			
		||||
            if (preview.isPreviewable) {
 | 
			
		||||
                await this.updateState({[event.getRoomId()]: preview.preview});
 | 
			
		||||
                return; // break - we found some text
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private generatePreviewForEvent(event: MatrixEvent): { isPreviewable: boolean, preview: string } {
 | 
			
		||||
        if (PREVIEWABLE_EVENTS.some(p => p.type === event.getType() && p.isState === event.isState())) {
 | 
			
		||||
            const isSelf = event.getSender() === this.matrixClient.getUserId();
 | 
			
		||||
            let text = textForEvent(event, /*skipUserPrefix=*/isSelf);
 | 
			
		||||
            if (!text || text.trim().length === 0) text = null; // force null if useless to us
 | 
			
		||||
            if (text && isSelf) {
 | 
			
		||||
                // XXX: i18n doesn't really work here if the language doesn't support prefixing.
 | 
			
		||||
                // We'd ideally somehow route the `You:` bit to the textForEvent call, however
 | 
			
		||||
                // threading that through is non-trivial.
 | 
			
		||||
                text = _t("You: %(message)s", {message: text});
 | 
			
		||||
            }
 | 
			
		||||
            return {isPreviewable: true, preview: text};
 | 
			
		||||
        }
 | 
			
		||||
        return {isPreviewable: false, preview: null};
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
		Reference in New Issue