mirror of
https://github.com/element-hq/element-web
synced 2024-11-23 17:56:01 +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 {
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
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