Merge pull request #6392 from SimonBrandner/task/cleanup-replies

This commit is contained in:
Michael Telatynski 2021-07-18 13:37:44 +01:00 committed by GitHub
commit 4ef4f49e90
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 183 additions and 170 deletions

View file

@ -16,19 +16,16 @@ limitations under the License.
.mx_ReplyThread { .mx_ReplyThread {
margin-top: 0; margin-top: 0;
}
.mx_ReplyThread_show {
cursor: pointer;
}
blockquote.mx_ReplyThread {
margin-left: 0; margin-left: 0;
margin-right: 0; margin-right: 0;
margin-bottom: 8px; margin-bottom: 8px;
padding-left: 10px; padding-left: 10px;
border-left: 4px solid $button-bg-color; border-left: 4px solid $button-bg-color;
.mx_ReplyThread_show {
cursor: pointer;
}
&.mx_ReplyThread_color1 { &.mx_ReplyThread_color1 {
border-left-color: $username-variant1-color; border-left-color: $username-variant1-color;
} }

View file

@ -22,33 +22,34 @@ limitations under the License.
max-height: 50vh; max-height: 50vh;
overflow: auto; overflow: auto;
box-shadow: 0px -16px 32px $composer-shadow-color; box-shadow: 0px -16px 32px $composer-shadow-color;
}
.mx_ReplyPreview_section { .mx_ReplyPreview_section {
border-bottom: 1px solid $primary-hairline-color; border-bottom: 1px solid $primary-hairline-color;
}
.mx_ReplyPreview_header { .mx_ReplyPreview_header {
margin: 8px; margin: 8px;
color: $primary-fg-color; color: $primary-fg-color;
font-weight: 400; font-weight: 400;
opacity: 0.4; opacity: 0.4;
} }
.mx_ReplyPreview_tile { .mx_ReplyPreview_tile {
margin: 0 8px; margin: 0 8px;
} }
.mx_ReplyPreview_title { .mx_ReplyPreview_title {
float: left; float: left;
} }
.mx_ReplyPreview_cancel { .mx_ReplyPreview_cancel {
float: right; float: right;
cursor: pointer; cursor: pointer;
display: flex; display: flex;
}
.mx_ReplyPreview_clear {
clear: both;
}
}
} }
.mx_ReplyPreview_clear {
clear: both;
}

View file

@ -15,10 +15,9 @@ limitations under the License.
*/ */
.mx_ReplyTile { .mx_ReplyTile {
padding-top: 2px;
padding-bottom: 2px;
font-size: $font-14px;
position: relative; position: relative;
padding: 2px 0;
font-size: $font-14px;
line-height: $font-16px; line-height: $font-16px;
&.mx_ReplyTile_audio .mx_MFileBody_info_icon::before { &.mx_ReplyTile_audio .mx_MFileBody_info_icon::before {
@ -38,16 +37,15 @@ limitations under the License.
display: none; display: none;
} }
} }
}
.mx_ReplyTile > a { > a {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
text-decoration: none; text-decoration: none;
color: $primary-fg-color; color: $primary-fg-color;
} }
.mx_ReplyTile .mx_RedactedBody { .mx_RedactedBody {
padding: 4px 0 2px 20px; padding: 4px 0 2px 20px;
&::before { &::before {
@ -55,10 +53,10 @@ limitations under the License.
width: 13px; width: 13px;
top: 5px; top: 5px;
} }
} }
// We do reply size limiting with CSS to avoid duplicating the TextualBody component. // We do reply size limiting with CSS to avoid duplicating the TextualBody component.
.mx_ReplyTile .mx_EventTile_content { .mx_EventTile_content {
$reply-lines: 2; $reply-lines: 2;
$line-height: $font-22px; $line-height: $font-22px;
@ -72,8 +70,7 @@ limitations under the License.
.mx_EventTile_body.mx_EventTile_bigEmoji { .mx_EventTile_body.mx_EventTile_bigEmoji {
line-height: $line-height !important; line-height: $line-height !important;
// Override the big emoji override font-size: $font-14px !important; // Override the big emoji override
font-size: $font-14px !important;
} }
// Hide line numbers // Hide line numbers
@ -100,24 +97,23 @@ limitations under the License.
.markdown-body ul { .markdown-body ul {
margin-bottom: 4px; margin-bottom: 4px;
} }
} }
.mx_ReplyTile.mx_ReplyTile_info { &.mx_ReplyTile_info {
padding-top: 0; padding-top: 0;
} }
.mx_ReplyTile .mx_SenderProfile { .mx_SenderProfile {
color: $primary-fg-color;
font-size: $font-14px; font-size: $font-14px;
display: inline-block; /* anti-zalgo, with overflow hidden */
overflow: hidden;
cursor: pointer;
padding-left: 0; /* left gutter */
padding-bottom: 0;
padding-top: 0;
margin: 0;
line-height: $font-17px; line-height: $font-17px;
/* the next three lines, along with overflow hidden, truncate long display names */
display: inline-block; // anti-zalgo, with overflow hidden
padding: 0;
margin: 0;
// truncate long display names
overflow: hidden;
white-space: nowrap; white-space: nowrap;
text-overflow: ellipsis; text-overflow: ellipsis;
}
} }

View file

@ -15,6 +15,7 @@ limitations under the License.
*/ */
import RoomViewStore from './stores/RoomViewStore'; import RoomViewStore from './stores/RoomViewStore';
import { EventSubscription } from 'fbemitter';
type Listener = (isActive: boolean) => void; type Listener = (isActive: boolean) => void;
@ -30,7 +31,7 @@ type Listener = (isActive: boolean) => void;
export class ActiveRoomObserver { export class ActiveRoomObserver {
private listeners: {[key: string]: Listener[]} = {}; private listeners: {[key: string]: Listener[]} = {};
private _activeRoomId = RoomViewStore.getRoomId(); private _activeRoomId = RoomViewStore.getRoomId();
private readonly roomStoreToken: string; private readonly roomStoreToken: EventSubscription;
constructor() { constructor() {
// TODO: We could self-destruct when the last listener goes away, or at least stop listening. // TODO: We could self-destruct when the last listener goes away, or at least stop listening.

View file

@ -14,14 +14,14 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import PropTypes from 'prop-types';
import dis from '../../../dispatcher/dispatcher'; import dis from '../../../dispatcher/dispatcher';
import { MatrixEvent } from 'matrix-js-sdk/src/models/event'; import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
import { makeUserPermalink, RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks"; import { makeUserPermalink, RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks";
import SettingsStore from "../../../settings/SettingsStore"; import SettingsStore from "../../../settings/SettingsStore";
import { LayoutPropType } from "../../../settings/Layout"; import { Layout } from "../../../settings/Layout";
import escapeHtml from "escape-html"; import escapeHtml from "escape-html";
import MatrixClientContext from "../../../contexts/MatrixClientContext"; import MatrixClientContext from "../../../contexts/MatrixClientContext";
import { getUserNameColorClass } from "../../../utils/FormattingUtils"; import { getUserNameColorClass } from "../../../utils/FormattingUtils";
@ -32,51 +32,54 @@ import { replaceableComponent } from "../../../utils/replaceableComponent";
import Spinner from './Spinner'; import Spinner from './Spinner';
import ReplyTile from "../rooms/ReplyTile"; import ReplyTile from "../rooms/ReplyTile";
import Pill from './Pill'; import Pill from './Pill';
import { Room } from 'matrix-js-sdk/src/models/room';
interface IProps {
// the latest event in this chain of replies
parentEv?: MatrixEvent;
// called when the ReplyThread contents has changed, including EventTiles thereof
onHeightChanged: () => void;
permalinkCreator: RoomPermalinkCreator;
// Specifies which layout to use.
layout?: Layout;
// Whether to always show a timestamp
alwaysShowTimestamps?: boolean;
}
interface IState {
// The loaded events to be rendered as linear-replies
events: MatrixEvent[];
// The latest loaded event which has not yet been shown
loadedEv: MatrixEvent;
// Whether the component is still loading more events
loading: boolean;
// Whether as error was encountered fetching a replied to event.
err: boolean;
}
// This component does no cycle detection, simply because the only way to make such a cycle would be to // This component does no cycle detection, simply because the only way to make such a cycle would be to
// craft event_id's, using a homeserver that generates predictable event IDs; even then the impact would // craft event_id's, using a homeserver that generates predictable event IDs; even then the impact would
// be low as each event being loaded (after the first) is triggered by an explicit user action. // be low as each event being loaded (after the first) is triggered by an explicit user action.
@replaceableComponent("views.elements.ReplyThread") @replaceableComponent("views.elements.ReplyThread")
export default class ReplyThread extends React.Component { export default class ReplyThread extends React.Component<IProps, IState> {
static propTypes = {
// the latest event in this chain of replies
parentEv: PropTypes.instanceOf(MatrixEvent),
// called when the ReplyThread contents has changed, including EventTiles thereof
onHeightChanged: PropTypes.func.isRequired,
permalinkCreator: PropTypes.instanceOf(RoomPermalinkCreator).isRequired,
// Specifies which layout to use.
layout: LayoutPropType,
// Whether to always show a timestamp
alwaysShowTimestamps: PropTypes.bool,
};
static contextType = MatrixClientContext; static contextType = MatrixClientContext;
private unmounted = false;
private room: Room;
constructor(props, context) { constructor(props, context) {
super(props, context); super(props, context);
this.state = { this.state = {
// The loaded events to be rendered as linear-replies
events: [], events: [],
// The latest loaded event which has not yet been shown
loadedEv: null, loadedEv: null,
// Whether the component is still loading more events
loading: true, loading: true,
// Whether as error was encountered fetching a replied to event.
err: false, err: false,
}; };
this.unmounted = false;
this.room = this.context.getRoom(this.props.parentEv.getRoomId()); this.room = this.context.getRoom(this.props.parentEv.getRoomId());
this.onQuoteClick = this.onQuoteClick.bind(this);
this.canCollapse = this.canCollapse.bind(this);
this.collapse = this.collapse.bind(this);
} }
static getParentEventId(ev) { public static getParentEventId(ev: MatrixEvent): string {
if (!ev || ev.isRedacted()) return; if (!ev || ev.isRedacted()) return;
// XXX: For newer relations (annotations, replacements, etc.), we now // XXX: For newer relations (annotations, replacements, etc.), we now
@ -92,7 +95,7 @@ export default class ReplyThread extends React.Component {
} }
// Part of Replies fallback support // Part of Replies fallback support
static stripPlainReply(body) { public static stripPlainReply(body: string): string {
// Removes lines beginning with `> ` until you reach one that doesn't. // Removes lines beginning with `> ` until you reach one that doesn't.
const lines = body.split('\n'); const lines = body.split('\n');
while (lines.length && lines[0].startsWith('> ')) lines.shift(); while (lines.length && lines[0].startsWith('> ')) lines.shift();
@ -102,7 +105,7 @@ export default class ReplyThread extends React.Component {
} }
// Part of Replies fallback support // Part of Replies fallback support
static stripHTMLReply(html) { public static stripHTMLReply(html: string): string {
// Sanitize the original HTML for inclusion in <mx-reply>. We allow // Sanitize the original HTML for inclusion in <mx-reply>. We allow
// any HTML, since the original sender could use special tags that we // any HTML, since the original sender could use special tags that we
// don't recognize, but want to pass along to any recipients who do // don't recognize, but want to pass along to any recipients who do
@ -124,7 +127,10 @@ export default class ReplyThread extends React.Component {
} }
// Part of Replies fallback support // Part of Replies fallback support
static getNestedReplyText(ev, permalinkCreator) { public static getNestedReplyText(
ev: MatrixEvent,
permalinkCreator: RoomPermalinkCreator,
): { body: string, html: string } {
if (!ev) return null; if (!ev) return null;
let { body, formatted_body: html } = ev.getContent(); let { body, formatted_body: html } = ev.getContent();
@ -200,7 +206,7 @@ export default class ReplyThread extends React.Component {
return { body, html }; return { body, html };
} }
static makeReplyMixIn(ev) { public static makeReplyMixIn(ev: MatrixEvent) {
if (!ev) return {}; if (!ev) return {};
return { return {
'm.relates_to': { 'm.relates_to': {
@ -211,10 +217,15 @@ export default class ReplyThread extends React.Component {
}; };
} }
static makeThread(parentEv, onHeightChanged, permalinkCreator, ref, layout, alwaysShowTimestamps) { public static makeThread(
if (!ReplyThread.getParentEventId(parentEv)) { parentEv: MatrixEvent,
return null; onHeightChanged: () => void,
} permalinkCreator: RoomPermalinkCreator,
ref: React.RefObject<ReplyThread>,
layout: Layout,
alwaysShowTimestamps: boolean,
): JSX.Element {
if (!ReplyThread.getParentEventId(parentEv)) return null;
return <ReplyThread return <ReplyThread
parentEv={parentEv} parentEv={parentEv}
onHeightChanged={onHeightChanged} onHeightChanged={onHeightChanged}
@ -237,7 +248,7 @@ export default class ReplyThread extends React.Component {
this.unmounted = true; this.unmounted = true;
} }
async initialize() { private async initialize(): Promise<void> {
const { parentEv } = this.props; const { parentEv } = this.props;
// at time of making this component we checked that props.parentEv has a parentEventId // at time of making this component we checked that props.parentEv has a parentEventId
const ev = await this.getEvent(ReplyThread.getParentEventId(parentEv)); const ev = await this.getEvent(ReplyThread.getParentEventId(parentEv));
@ -256,7 +267,7 @@ export default class ReplyThread extends React.Component {
} }
} }
async getNextEvent(ev) { private async getNextEvent(ev: MatrixEvent): Promise<MatrixEvent> {
try { try {
const inReplyToEventId = ReplyThread.getParentEventId(ev); const inReplyToEventId = ReplyThread.getParentEventId(ev);
return await this.getEvent(inReplyToEventId); return await this.getEvent(inReplyToEventId);
@ -265,7 +276,7 @@ export default class ReplyThread extends React.Component {
} }
} }
async getEvent(eventId) { private async getEvent(eventId: string): Promise<MatrixEvent> {
if (!eventId) return null; if (!eventId) return null;
const event = this.room.findEventById(eventId); const event = this.room.findEventById(eventId);
if (event) return event; if (event) return event;
@ -282,15 +293,15 @@ export default class ReplyThread extends React.Component {
return this.room.findEventById(eventId); return this.room.findEventById(eventId);
} }
canCollapse() { public canCollapse = (): boolean => {
return this.state.events.length > 1; return this.state.events.length > 1;
} };
collapse() { public collapse = (): void => {
this.initialize(); this.initialize();
} };
async onQuoteClick() { private onQuoteClick = async (): Promise<void> => {
const events = [this.state.loadedEv, ...this.state.events]; const events = [this.state.loadedEv, ...this.state.events];
let loadedEv = null; let loadedEv = null;
@ -304,9 +315,9 @@ export default class ReplyThread extends React.Component {
}); });
dis.fire(Action.FocusSendMessageComposer); dis.fire(Action.FocusSendMessageComposer);
} };
getReplyThreadColorClass(ev) { private getReplyThreadColorClass(ev: MatrixEvent): string {
return getUserNameColorClass(ev.getSender()).replace("Username", "ReplyThread"); return getUserNameColorClass(ev.getSender()).replace("Username", "ReplyThread");
} }

View file

@ -320,7 +320,7 @@ export default class EventTile extends React.Component<IProps, IState> {
private suppressReadReceiptAnimation: boolean; private suppressReadReceiptAnimation: boolean;
private isListeningForReceipts: boolean; private isListeningForReceipts: boolean;
private tile = React.createRef(); private tile = React.createRef();
private replyThread = React.createRef(); private replyThread = React.createRef<ReplyThread>();
public readonly ref = createRef<HTMLElement>(); public readonly ref = createRef<HTMLElement>();

View file

@ -18,10 +18,11 @@ import React from 'react';
import dis from '../../../dispatcher/dispatcher'; import dis from '../../../dispatcher/dispatcher';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import RoomViewStore from '../../../stores/RoomViewStore'; import RoomViewStore from '../../../stores/RoomViewStore';
import PropTypes from "prop-types";
import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks"; import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks";
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import ReplyTile from './ReplyTile'; import ReplyTile from './ReplyTile';
import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
import { EventSubscription } from 'fbemitter';
function cancelQuoting() { function cancelQuoting() {
dis.dispatch({ dis.dispatch({
@ -30,41 +31,46 @@ function cancelQuoting() {
}); });
} }
interface IProps {
permalinkCreator: RoomPermalinkCreator;
}
interface IState {
event: MatrixEvent;
}
@replaceableComponent("views.rooms.ReplyPreview") @replaceableComponent("views.rooms.ReplyPreview")
export default class ReplyPreview extends React.Component { export default class ReplyPreview extends React.Component<IProps, IState> {
static propTypes = { private unmounted = false;
permalinkCreator: PropTypes.instanceOf(RoomPermalinkCreator).isRequired, private readonly roomStoreToken: EventSubscription;
};
constructor(props) { constructor(props) {
super(props); super(props);
this.unmounted = false;
this.state = { this.state = {
event: RoomViewStore.getQuotingEvent(), event: RoomViewStore.getQuotingEvent(),
}; };
this._onRoomViewStoreUpdate = this._onRoomViewStoreUpdate.bind(this); this.roomStoreToken = RoomViewStore.addListener(this.onRoomViewStoreUpdate);
this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate);
} }
componentWillUnmount() { componentWillUnmount() {
this.unmounted = true; this.unmounted = true;
// Remove RoomStore listener // Remove RoomStore listener
if (this._roomStoreToken) { if (this.roomStoreToken) {
this._roomStoreToken.remove(); this.roomStoreToken.remove();
} }
} }
_onRoomViewStoreUpdate() { private onRoomViewStoreUpdate = (): void => {
if (this.unmounted) return; if (this.unmounted) return;
const event = RoomViewStore.getQuotingEvent(); const event = RoomViewStore.getQuotingEvent();
if (this.state.event !== event) { if (this.state.event !== event) {
this.setState({ event }); this.setState({ event });
} }
} };
render() { render() {
if (!this.state.event) return null; if (!this.state.event) return null;

View file

@ -30,6 +30,7 @@ import { replaceableComponent } from "../../../utils/replaceableComponent";
import UIStore from '../../../stores/UIStore'; import UIStore from '../../../stores/UIStore';
import { lerp } from '../../../utils/AnimationUtils'; import { lerp } from '../../../utils/AnimationUtils';
import { MarkedExecution } from '../../../utils/MarkedExecution'; import { MarkedExecution } from '../../../utils/MarkedExecution';
import { EventSubscription } from 'fbemitter';
const PIP_VIEW_WIDTH = 336; const PIP_VIEW_WIDTH = 336;
const PIP_VIEW_HEIGHT = 232; const PIP_VIEW_HEIGHT = 232;
@ -108,7 +109,7 @@ function getPrimarySecondaryCalls(calls: MatrixCall[]): [MatrixCall, MatrixCall[
*/ */
@replaceableComponent("views.voip.CallPreview") @replaceableComponent("views.voip.CallPreview")
export default class CallPreview extends React.Component<IProps, IState> { export default class CallPreview extends React.Component<IProps, IState> {
private roomStoreToken: any; private roomStoreToken: EventSubscription;
private dispatcherRef: string; private dispatcherRef: string;
private settingsWatcherRef: string; private settingsWatcherRef: string;
private callViewWrapper = createRef<HTMLDivElement>(); private callViewWrapper = createRef<HTMLDivElement>();
@ -240,7 +241,7 @@ export default class CallPreview extends React.Component<IProps, IState> {
this.scheduledUpdate.mark(); this.scheduledUpdate.mark();
}; };
private onRoomViewStoreUpdate = (payload) => { private onRoomViewStoreUpdate = () => {
if (RoomViewStore.getRoomId() === this.state.roomId) return; if (RoomViewStore.getRoomId() === this.state.roomId) return;
const roomId = RoomViewStore.getRoomId(); const roomId = RoomViewStore.getRoomId();

View file

@ -429,7 +429,7 @@ class RoomViewStore extends Store<ActionPayload> {
} }
} }
let singletonRoomViewStore = null; let singletonRoomViewStore: RoomViewStore = null;
if (!singletonRoomViewStore) { if (!singletonRoomViewStore) {
singletonRoomViewStore = new RoomViewStore(); singletonRoomViewStore = new RoomViewStore();
} }