Merge branch 'develop' of github.com:matrix-org/matrix-react-sdk into joriks/fix-read-receipts

To fix types
This commit is contained in:
Jorik Schellekens 2020-06-11 10:55:27 +01:00
commit 399dd6a225
14 changed files with 206 additions and 40 deletions

View file

@ -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;
} }
} }
} }

View file

@ -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 {

View file

@ -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
} }

View file

@ -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;
} }
} }

View file

@ -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

View file

@ -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 '';
} }

View file

@ -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();

View file

@ -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> {

View file

@ -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>
); );
}} }}

View file

@ -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({

View file

@ -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

View file

@ -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.",

View file

@ -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;
} }
/** /**

View 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};
}
}