mirror of
https://github.com/element-hq/element-web
synced 2024-11-27 19:56:47 +03:00
Merge branch 'develop' of github.com:matrix-org/matrix-react-sdk into joriks/fix-read-receipts
To fix types
This commit is contained in:
commit
399dd6a225
14 changed files with 206 additions and 40 deletions
|
@ -131,6 +131,11 @@ $roomListMinimizedWidth: 50px;
|
||||||
.mx_LeftPanel2_actualRoomListContainer {
|
.mx_LeftPanel2_actualRoomListContainer {
|
||||||
flex-grow: 1; // fill the available space
|
flex-grow: 1; // fill the available space
|
||||||
overflow-y: auto;
|
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 {
|
&.mx_NotificationBadge_visible {
|
||||||
background-color: $roomtile2-badge-color;
|
background-color: $roomtile2-badge-color;
|
||||||
margin-right: 14px;
|
|
||||||
|
|
||||||
// Create a flexbox to order the count a bit easier
|
// Create a flexbox to order the count a bit easier
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -46,7 +45,6 @@ limitations under the License.
|
||||||
width: 6px;
|
width: 6px;
|
||||||
height: 6px;
|
height: 6px;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
margin-right: 8px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&.mx_NotificationBadge_2char {
|
&.mx_NotificationBadge_2char {
|
||||||
|
|
|
@ -17,9 +17,11 @@ limitations under the License.
|
||||||
// TODO: Rename to mx_RoomList during replacement of old component
|
// TODO: Rename to mx_RoomList during replacement of old component
|
||||||
|
|
||||||
.mx_RoomList2 {
|
.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
|
// Create a column-based flexbox for the sublists. That's pretty much all we have to
|
||||||
// worry about in this stylesheet.
|
// worry about in this stylesheet.
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
flex-wrap: wrap;
|
flex-wrap: nowrap; // let the column overflow
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,7 +21,7 @@ limitations under the License.
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
padding-left: 8px;
|
margin-left: 8px;
|
||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@ -30,16 +30,18 @@ limitations under the License.
|
||||||
// Create a flexbox to make ordering easy
|
// Create a flexbox to make ordering easy
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
height: 24px;
|
||||||
|
|
||||||
.mx_RoomSublist2_badgeContainer {
|
.mx_RoomSublist2_badgeContainer {
|
||||||
opacity: 0.8;
|
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
|
// Create another flexbox row because it's super easy to position the badge this way.
|
||||||
// the end this way.
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: flex-end;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Both of these buttons are hidden by default until the list is hovered
|
// Both of these buttons are hidden by default until the list is hovered
|
||||||
|
@ -77,10 +79,9 @@ limitations under the License.
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
line-height: $font-16px;
|
line-height: $font-16px;
|
||||||
font-size: $font-12px;
|
font-size: $font-12px;
|
||||||
padding-bottom: 8px;
|
|
||||||
|
|
||||||
width: 100%;
|
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
max-width: calc(100% - 16px); // 16px is the badge width
|
||||||
|
|
||||||
// Ellipsize any text overflow
|
// Ellipsize any text overflow
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
|
@ -158,7 +159,7 @@ limitations under the License.
|
||||||
// either side of the list. We define this after the positioning to
|
// either side of the list. We define this after the positioning to
|
||||||
// trick the browser.
|
// trick the browser.
|
||||||
margin-left: 4px;
|
margin-left: 4px;
|
||||||
margin-right: 8px;
|
margin-right: 4px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -184,7 +185,7 @@ limitations under the License.
|
||||||
&:not(.mx_RoomSublist2_headerContainer_withAux) {
|
&:not(.mx_RoomSublist2_headerContainer_withAux) {
|
||||||
// The menu button will be the rightmost button, so make it correctly aligned.
|
// The menu button will be the rightmost button, so make it correctly aligned.
|
||||||
.mx_RoomSublist2_menuButton {
|
.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
|
// Show the aux button, but not the list button
|
||||||
width: 24px;
|
width: 24px;
|
||||||
height: 24px;
|
height: 24px;
|
||||||
margin-right: 16px;
|
margin-right: 1px; // line it up with the badges on the room tiles
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,9 +18,7 @@ limitations under the License.
|
||||||
|
|
||||||
// Note: the room tile expects to be in a flexbox column container
|
// Note: the room tile expects to be in a flexbox column container
|
||||||
.mx_RoomTile2 {
|
.mx_RoomTile2 {
|
||||||
width: calc(100% - 21px); // 10px for alignment/inset, 8px for padding on sides, 3px for margin
|
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
margin-right: 3px;
|
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
|
|
||||||
// The tile is also a flexbox row itself
|
// The tile is also a flexbox row itself
|
||||||
|
@ -84,7 +82,7 @@ limitations under the License.
|
||||||
// the end this way.
|
// the end this way.
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: flex-end;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The menu button is hidden by default
|
// The menu button is hidden by default
|
||||||
|
|
|
@ -265,14 +265,23 @@ function textForServerACLEvent(ev) {
|
||||||
return text + changes.join(" ");
|
return text + changes.join(" ");
|
||||||
}
|
}
|
||||||
|
|
||||||
function textForMessageEvent(ev) {
|
function textForMessageEvent(ev, skipUserPrefix) {
|
||||||
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
|
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
|
||||||
let message = senderDisplayName + ': ' + ev.getContent().body;
|
let message = senderDisplayName + ': ' + ev.getContent().body;
|
||||||
|
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") {
|
if (ev.getContent().msgtype === "m.emote") {
|
||||||
message = "* " + senderDisplayName + " " + message;
|
message = "* " + senderDisplayName + " " + message;
|
||||||
} else if (ev.getContent().msgtype === "m.image") {
|
} else if (ev.getContent().msgtype === "m.image") {
|
||||||
message = _t('%(senderDisplayName)s sent an image.', {senderDisplayName});
|
message = _t('%(senderDisplayName)s sent an image.', {senderDisplayName});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return message;
|
return message;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -612,8 +621,8 @@ for (const evType of ALL_RULE_TYPES) {
|
||||||
stateHandlers[evType] = textForMjolnirEvent;
|
stateHandlers[evType] = textForMjolnirEvent;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function textForEvent(ev) {
|
export function textForEvent(ev, skipUserPrefix) {
|
||||||
const handler = (ev.isState() ? stateHandlers : handlers)[ev.getType()];
|
const handler = (ev.isState() ? stateHandlers : handlers)[ev.getType()];
|
||||||
if (handler) return handler(ev);
|
if (handler) return handler(ev, skipUserPrefix);
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,7 +31,7 @@ import LogoutDialog from "../views/dialogs/LogoutDialog";
|
||||||
import SettingsStore, {SettingLevel} from "../../settings/SettingsStore";
|
import SettingsStore, {SettingLevel} from "../../settings/SettingsStore";
|
||||||
import {getCustomTheme} from "../../theme";
|
import {getCustomTheme} from "../../theme";
|
||||||
import {getHostingLink} from "../../utils/HostingLink";
|
import {getHostingLink} from "../../utils/HostingLink";
|
||||||
import AccessibleButton from "../views/elements/AccessibleButton";
|
import AccessibleButton, {ButtonEvent} from "../views/elements/AccessibleButton";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
}
|
}
|
||||||
|
@ -114,7 +114,7 @@ export default class UserMenuButton extends React.Component<IProps, IState> {
|
||||||
SettingsStore.setValue("theme", null, SettingLevel.ACCOUNT, newTheme);
|
SettingsStore.setValue("theme", null, SettingLevel.ACCOUNT, newTheme);
|
||||||
};
|
};
|
||||||
|
|
||||||
private onSettingsOpen = (ev: React.MouseEvent, tabId: string) => {
|
private onSettingsOpen = (ev: ButtonEvent, tabId: string) => {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
|
|
||||||
|
@ -123,7 +123,7 @@ export default class UserMenuButton extends React.Component<IProps, IState> {
|
||||||
this.setState({menuDisplayed: false}); // also close the menu
|
this.setState({menuDisplayed: false}); // also close the menu
|
||||||
};
|
};
|
||||||
|
|
||||||
private onShowArchived = (ev: React.MouseEvent) => {
|
private onShowArchived = (ev: ButtonEvent) => {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
|
|
||||||
|
@ -131,7 +131,7 @@ export default class UserMenuButton extends React.Component<IProps, IState> {
|
||||||
console.log("TODO: Show archived rooms");
|
console.log("TODO: Show archived rooms");
|
||||||
};
|
};
|
||||||
|
|
||||||
private onProvideFeedback = (ev: React.MouseEvent) => {
|
private onProvideFeedback = (ev: ButtonEvent) => {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
|
|
||||||
|
@ -139,7 +139,7 @@ export default class UserMenuButton extends React.Component<IProps, IState> {
|
||||||
this.setState({menuDisplayed: false}); // also close the menu
|
this.setState({menuDisplayed: false}); // also close the menu
|
||||||
};
|
};
|
||||||
|
|
||||||
private onSignOutClick = (ev: React.MouseEvent) => {
|
private onSignOutClick = (ev: ButtonEvent) => {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
|
|
||||||
|
|
|
@ -19,6 +19,8 @@ import React from 'react';
|
||||||
import {Key} from '../../../Keyboard';
|
import {Key} from '../../../Keyboard';
|
||||||
import classnames from 'classnames';
|
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.
|
* children: React's magic prop. Represents all children given to the element.
|
||||||
* element: (optional) The base element type. "div" by default.
|
* element: (optional) The base element type. "div" by default.
|
||||||
|
@ -37,7 +39,7 @@ interface IProps extends React.InputHTMLAttributes<Element> {
|
||||||
tabIndex?: number;
|
tabIndex?: number;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
onClick?(e?: React.MouseEvent<Element> | React.KeyboardEvent<Element>): void;
|
onClick?(e?: ButtonEvent): void;
|
||||||
};
|
};
|
||||||
|
|
||||||
interface IAccessibleButtonProps extends React.InputHTMLAttributes<Element> {
|
interface IAccessibleButtonProps extends React.InputHTMLAttributes<Element> {
|
||||||
|
|
|
@ -237,11 +237,11 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
||||||
>
|
>
|
||||||
<span>{this.props.label}</span>
|
<span>{this.props.label}</span>
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
|
{this.renderMenu()}
|
||||||
|
{addRoomButton}
|
||||||
<div className="mx_RoomSublist2_badgeContainer">
|
<div className="mx_RoomSublist2_badgeContainer">
|
||||||
{badge}
|
{badge}
|
||||||
</div>
|
</div>
|
||||||
{this.renderMenu()}
|
|
||||||
{addRoomButton}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -21,7 +21,7 @@ import React, { createRef } from "react";
|
||||||
import { Room } from "matrix-js-sdk/src/models/room";
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { RovingTabIndexWrapper } from "../../../accessibility/RovingTabIndex";
|
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 RoomAvatar from "../../views/avatars/RoomAvatar";
|
||||||
import dis from '../../../dispatcher/dispatcher';
|
import dis from '../../../dispatcher/dispatcher';
|
||||||
import { Key } from "../../../Keyboard";
|
import { Key } from "../../../Keyboard";
|
||||||
|
@ -30,6 +30,7 @@ import NotificationBadge, { INotificationState, NotificationColor, RoomNotificat
|
||||||
import { _t } from "../../../languageHandler";
|
import { _t } from "../../../languageHandler";
|
||||||
import { ContextMenu, ContextMenuButton } from "../../structures/ContextMenu";
|
import { ContextMenu, ContextMenuButton } from "../../structures/ContextMenu";
|
||||||
import { DefaultTagID, TagID } from "../../../stores/room-list/models";
|
import { DefaultTagID, TagID } from "../../../stores/room-list/models";
|
||||||
|
import { MessagePreviewStore } from "../../../stores/MessagePreviewStore";
|
||||||
|
|
||||||
/*******************************************************************
|
/*******************************************************************
|
||||||
* CAUTION *
|
* CAUTION *
|
||||||
|
@ -123,7 +124,7 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
|
||||||
this.setState({generalMenuDisplayed: false});
|
this.setState({generalMenuDisplayed: false});
|
||||||
};
|
};
|
||||||
|
|
||||||
private onTagRoom = (ev: React.MouseEvent, tagId: TagID) => {
|
private onTagRoom = (ev: ButtonEvent, tagId: TagID) => {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
ev.stopPropagation();
|
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.preventDefault();
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
|
|
||||||
|
@ -145,7 +146,7 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
|
||||||
this.setState({generalMenuDisplayed: false}); // hide the menu
|
this.setState({generalMenuDisplayed: false}); // hide the menu
|
||||||
};
|
};
|
||||||
|
|
||||||
private onOpenRoomSettings = (ev: React.MouseEvent) => {
|
private onOpenRoomSettings = (ev: ButtonEvent) => {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
|
|
||||||
|
@ -253,8 +254,17 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
let messagePreview = null;
|
let messagePreview = null;
|
||||||
if (this.props.showMessagePreview) {
|
if (this.props.showMessagePreview) {
|
||||||
// TODO: Actually get the real message preview from state
|
// The preview store heavily caches this info, so should be safe to hammer.
|
||||||
messagePreview = <div className="mx_RoomTile2_messagePreview">I just ate a pie.</div>;
|
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({
|
const nameClasses = classNames({
|
||||||
|
|
|
@ -427,7 +427,9 @@ export default class SendMessageComposer extends React.Component {
|
||||||
|
|
||||||
_onPaste = (event) => {
|
_onPaste = (event) => {
|
||||||
const {clipboardData} = 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
|
// This actually not so much for 'files' as such (at time of writing
|
||||||
// neither chrome nor firefox let you paste a plain file copied
|
// neither chrome nor firefox let you paste a plain file copied
|
||||||
// from Finder) but more images copied from a different website
|
// 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 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 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.",
|
"%(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.",
|
"%(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 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.",
|
"%(senderName)s removed the main address for this room.": "%(senderName)s removed the main address for this room.",
|
||||||
|
@ -419,6 +420,7 @@
|
||||||
"Restart": "Restart",
|
"Restart": "Restart",
|
||||||
"Upgrade your Riot": "Upgrade your Riot",
|
"Upgrade your Riot": "Upgrade your Riot",
|
||||||
"A new version of Riot is available!": "A new version of Riot is available!",
|
"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",
|
"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.",
|
"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.",
|
"Please contact your homeserver administrator.": "Please contact your homeserver administrator.",
|
||||||
|
|
|
@ -42,18 +42,20 @@ export const UPDATE_EVENT = "update";
|
||||||
* help prevent lock conflicts.
|
* help prevent lock conflicts.
|
||||||
*/
|
*/
|
||||||
export abstract class AsyncStore<T extends Object> extends EventEmitter {
|
export abstract class AsyncStore<T extends Object> extends EventEmitter {
|
||||||
private storeState: T = <T>{};
|
private storeState: T;
|
||||||
private lock = new AwaitLock();
|
private lock = new AwaitLock();
|
||||||
private readonly dispatcherRef: string;
|
private readonly dispatcherRef: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new AsyncStore using the given dispatcher.
|
* Creates a new AsyncStore using the given dispatcher.
|
||||||
* @param {Dispatcher<ActionPayload>} dispatcher The dispatcher to rely upon.
|
* @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();
|
super();
|
||||||
|
|
||||||
this.dispatcherRef = dispatcher.register(this.onDispatch.bind(this));
|
this.dispatcherRef = dispatcher.register(this.onDispatch.bind(this));
|
||||||
|
this.storeState = initialState;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
135
src/stores/MessagePreviewStore.ts
Normal file
135
src/stores/MessagePreviewStore.ts
Normal file
|
@ -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 a new issue