diff --git a/res/css/views/elements/_ReplyThread.scss b/res/css/views/elements/_ReplyThread.scss index af8ca956ba..44532ea6a7 100644 --- a/res/css/views/elements/_ReplyThread.scss +++ b/res/css/views/elements/_ReplyThread.scss @@ -16,19 +16,16 @@ limitations under the License. .mx_ReplyThread { margin-top: 0; -} - -.mx_ReplyThread_show { - cursor: pointer; -} - -blockquote.mx_ReplyThread { margin-left: 0; margin-right: 0; margin-bottom: 8px; padding-left: 10px; border-left: 4px solid $button-bg-color; + .mx_ReplyThread_show { + cursor: pointer; + } + &.mx_ReplyThread_color1 { border-left-color: $username-variant1-color; } diff --git a/res/css/views/rooms/_ReplyPreview.scss b/res/css/views/rooms/_ReplyPreview.scss index c1fe1d9a8b..60feb39d11 100644 --- a/res/css/views/rooms/_ReplyPreview.scss +++ b/res/css/views/rooms/_ReplyPreview.scss @@ -22,33 +22,34 @@ limitations under the License. max-height: 50vh; overflow: auto; box-shadow: 0px -16px 32px $composer-shadow-color; + + .mx_ReplyPreview_section { + border-bottom: 1px solid $primary-hairline-color; + + .mx_ReplyPreview_header { + margin: 8px; + color: $primary-fg-color; + font-weight: 400; + opacity: 0.4; + } + + .mx_ReplyPreview_tile { + margin: 0 8px; + } + + .mx_ReplyPreview_title { + float: left; + } + + .mx_ReplyPreview_cancel { + float: right; + cursor: pointer; + display: flex; + } + + .mx_ReplyPreview_clear { + clear: both; + } + } } -.mx_ReplyPreview_section { - border-bottom: 1px solid $primary-hairline-color; -} - -.mx_ReplyPreview_header { - margin: 8px; - color: $primary-fg-color; - font-weight: 400; - opacity: 0.4; -} - -.mx_ReplyPreview_tile { - margin: 0 8px; -} - -.mx_ReplyPreview_title { - float: left; -} - -.mx_ReplyPreview_cancel { - float: right; - cursor: pointer; - display: flex; -} - -.mx_ReplyPreview_clear { - clear: both; -} diff --git a/res/css/views/rooms/_ReplyTile.scss b/res/css/views/rooms/_ReplyTile.scss index c8f76ee995..f3e204e415 100644 --- a/res/css/views/rooms/_ReplyTile.scss +++ b/res/css/views/rooms/_ReplyTile.scss @@ -15,10 +15,9 @@ limitations under the License. */ .mx_ReplyTile { - padding-top: 2px; - padding-bottom: 2px; - font-size: $font-14px; position: relative; + padding: 2px 0; + font-size: $font-14px; line-height: $font-16px; &.mx_ReplyTile_audio .mx_MFileBody_info_icon::before { @@ -38,86 +37,83 @@ limitations under the License. display: none; } } -} -.mx_ReplyTile > a { - display: flex; - flex-direction: column; - text-decoration: none; - color: $primary-fg-color; -} - -.mx_ReplyTile .mx_RedactedBody { - padding: 4px 0 2px 20px; - - &::before { - height: 13px; - width: 13px; - top: 5px; - } -} - -// We do reply size limiting with CSS to avoid duplicating the TextualBody component. -.mx_ReplyTile .mx_EventTile_content { - $reply-lines: 2; - $line-height: $font-22px; - - pointer-events: none; - - text-overflow: ellipsis; - display: -webkit-box; - -webkit-box-orient: vertical; - -webkit-line-clamp: $reply-lines; - line-height: $line-height; - - .mx_EventTile_body.mx_EventTile_bigEmoji { - line-height: $line-height !important; - // Override the big emoji override - font-size: $font-14px !important; + > a { + display: flex; + flex-direction: column; + text-decoration: none; + color: $primary-fg-color; } - // Hide line numbers - .mx_EventTile_lineNumbers { - display: none; + .mx_RedactedBody { + padding: 4px 0 2px 20px; + + &::before { + height: 13px; + width: 13px; + top: 5px; + } } - // Hack to cut content in
tags too - .mx_EventTile_pre_container > pre { - overflow: hidden; + // We do reply size limiting with CSS to avoid duplicating the TextualBody component. + .mx_EventTile_content { + $reply-lines: 2; + $line-height: $font-22px; + + pointer-events: none; + text-overflow: ellipsis; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: $reply-lines; - padding: 4px; + line-height: $line-height; + + .mx_EventTile_body.mx_EventTile_bigEmoji { + line-height: $line-height !important; + font-size: $font-14px !important; // Override the big emoji override + } + + // Hide line numbers + .mx_EventTile_lineNumbers { + display: none; + } + + // Hack to cut content intags too + .mx_EventTile_pre_container > pre { + overflow: hidden; + text-overflow: ellipsis; + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: $reply-lines; + padding: 4px; + } + + .markdown-body blockquote, + .markdown-body dl, + .markdown-body ol, + .markdown-body p, + .markdown-body pre, + .markdown-body table, + .markdown-body ul { + margin-bottom: 4px; + } } - .markdown-body blockquote, - .markdown-body dl, - .markdown-body ol, - .markdown-body p, - .markdown-body pre, - .markdown-body table, - .markdown-body ul { - margin-bottom: 4px; + &.mx_ReplyTile_info { + padding-top: 0; + } + + .mx_SenderProfile { + font-size: $font-14px; + line-height: $font-17px; + + display: inline-block; // anti-zalgo, with overflow hidden + padding: 0; + margin: 0; + + // truncate long display names + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; } } - -.mx_ReplyTile.mx_ReplyTile_info { - padding-top: 0; -} - -.mx_ReplyTile .mx_SenderProfile { - color: $primary-fg-color; - 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; - /* the next three lines, along with overflow hidden, truncate long display names */ - white-space: nowrap; - text-overflow: ellipsis; -} diff --git a/src/ActiveRoomObserver.ts b/src/ActiveRoomObserver.ts index 1126dc9496..0be49a24ea 100644 --- a/src/ActiveRoomObserver.ts +++ b/src/ActiveRoomObserver.ts @@ -15,6 +15,7 @@ limitations under the License. */ import RoomViewStore from './stores/RoomViewStore'; +import { EventSubscription } from 'fbemitter'; type Listener = (isActive: boolean) => void; @@ -30,7 +31,7 @@ type Listener = (isActive: boolean) => void; export class ActiveRoomObserver { private listeners: {[key: string]: Listener[]} = {}; private _activeRoomId = RoomViewStore.getRoomId(); - private readonly roomStoreToken: string; + private readonly roomStoreToken: EventSubscription; constructor() { // TODO: We could self-destruct when the last listener goes away, or at least stop listening. diff --git a/src/components/views/elements/ReplyThread.js b/src/components/views/elements/ReplyThread.tsx similarity index 85% rename from src/components/views/elements/ReplyThread.js rename to src/components/views/elements/ReplyThread.tsx index 89427515e2..0eb795e257 100644 --- a/src/components/views/elements/ReplyThread.js +++ b/src/components/views/elements/ReplyThread.tsx @@ -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 limitations under the License. */ + import React from 'react'; import { _t } from '../../../languageHandler'; -import PropTypes from 'prop-types'; import dis from '../../../dispatcher/dispatcher'; import { MatrixEvent } from 'matrix-js-sdk/src/models/event'; import { makeUserPermalink, RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks"; import SettingsStore from "../../../settings/SettingsStore"; -import { LayoutPropType } from "../../../settings/Layout"; +import { Layout } from "../../../settings/Layout"; import escapeHtml from "escape-html"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; import { getUserNameColorClass } from "../../../utils/FormattingUtils"; @@ -32,51 +32,54 @@ import { replaceableComponent } from "../../../utils/replaceableComponent"; import Spinner from './Spinner'; import ReplyTile from "../rooms/ReplyTile"; 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 // 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. @replaceableComponent("views.elements.ReplyThread") -export default class ReplyThread extends React.Component { - 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, - }; - +export default class ReplyThread extends React.Component{ static contextType = MatrixClientContext; + private unmounted = false; + private room: Room; constructor(props, context) { super(props, context); this.state = { - // The loaded events to be rendered as linear-replies events: [], - - // The latest loaded event which has not yet been shown loadedEv: null, - // Whether the component is still loading more events loading: true, - - // Whether as error was encountered fetching a replied to event. err: false, }; - this.unmounted = false; 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; // 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 - static stripPlainReply(body) { + public static stripPlainReply(body: string): string { // Removes lines beginning with `> ` until you reach one that doesn't. const lines = body.split('\n'); while (lines.length && lines[0].startsWith('> ')) lines.shift(); @@ -102,7 +105,7 @@ export default class ReplyThread extends React.Component { } // Part of Replies fallback support - static stripHTMLReply(html) { + public static stripHTMLReply(html: string): string { // Sanitize the original HTML for inclusion in . We allow // 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 @@ -124,7 +127,10 @@ export default class ReplyThread extends React.Component { } // Part of Replies fallback support - static getNestedReplyText(ev, permalinkCreator) { + public static getNestedReplyText( + ev: MatrixEvent, + permalinkCreator: RoomPermalinkCreator, + ): { body: string, html: string } { if (!ev) return null; let { body, formatted_body: html } = ev.getContent(); @@ -200,7 +206,7 @@ export default class ReplyThread extends React.Component { return { body, html }; } - static makeReplyMixIn(ev) { + public static makeReplyMixIn(ev: MatrixEvent) { if (!ev) return {}; return { 'm.relates_to': { @@ -211,10 +217,15 @@ export default class ReplyThread extends React.Component { }; } - static makeThread(parentEv, onHeightChanged, permalinkCreator, ref, layout, alwaysShowTimestamps) { - if (!ReplyThread.getParentEventId(parentEv)) { - return null; - } + public static makeThread( + parentEv: MatrixEvent, + onHeightChanged: () => void, + permalinkCreator: RoomPermalinkCreator, + ref: React.RefObject , + layout: Layout, + alwaysShowTimestamps: boolean, + ): JSX.Element { + if (!ReplyThread.getParentEventId(parentEv)) return null; return { const { parentEv } = this.props; // at time of making this component we checked that props.parentEv has a parentEventId 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 { try { const inReplyToEventId = ReplyThread.getParentEventId(ev); 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 { if (!eventId) return null; const event = this.room.findEventById(eventId); if (event) return event; @@ -282,15 +293,15 @@ export default class ReplyThread extends React.Component { return this.room.findEventById(eventId); } - canCollapse() { + public canCollapse = (): boolean => { return this.state.events.length > 1; - } + }; - collapse() { + public collapse = (): void => { this.initialize(); - } + }; - async onQuoteClick() { + private onQuoteClick = async (): Promise => { const events = [this.state.loadedEv, ...this.state.events]; let loadedEv = null; @@ -304,9 +315,9 @@ export default class ReplyThread extends React.Component { }); dis.fire(Action.FocusSendMessageComposer); - } + }; - getReplyThreadColorClass(ev) { + private getReplyThreadColorClass(ev: MatrixEvent): string { return getUserNameColorClass(ev.getSender()).replace("Username", "ReplyThread"); } diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index e0a924f1e7..1bdcccd77f 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -320,7 +320,7 @@ export default class EventTile extends React.Component { private suppressReadReceiptAnimation: boolean; private isListeningForReceipts: boolean; private tile = React.createRef(); - private replyThread = React.createRef(); + private replyThread = React.createRef (); public readonly ref = createRef (); diff --git a/src/components/views/rooms/ReplyPreview.js b/src/components/views/rooms/ReplyPreview.tsx similarity index 81% rename from src/components/views/rooms/ReplyPreview.js rename to src/components/views/rooms/ReplyPreview.tsx index c7d19e58db..41b3d2460c 100644 --- a/src/components/views/rooms/ReplyPreview.js +++ b/src/components/views/rooms/ReplyPreview.tsx @@ -18,10 +18,11 @@ import React from 'react'; import dis from '../../../dispatcher/dispatcher'; import { _t } from '../../../languageHandler'; import RoomViewStore from '../../../stores/RoomViewStore'; -import PropTypes from "prop-types"; import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks"; import { replaceableComponent } from "../../../utils/replaceableComponent"; import ReplyTile from './ReplyTile'; +import { MatrixEvent } from 'matrix-js-sdk/src/models/event'; +import { EventSubscription } from 'fbemitter'; function cancelQuoting() { dis.dispatch({ @@ -30,41 +31,46 @@ function cancelQuoting() { }); } +interface IProps { + permalinkCreator: RoomPermalinkCreator; +} + +interface IState { + event: MatrixEvent; +} + @replaceableComponent("views.rooms.ReplyPreview") -export default class ReplyPreview extends React.Component { - static propTypes = { - permalinkCreator: PropTypes.instanceOf(RoomPermalinkCreator).isRequired, - }; +export default class ReplyPreview extends React.Component { + private unmounted = false; + private readonly roomStoreToken: EventSubscription; constructor(props) { super(props); - this.unmounted = false; this.state = { event: RoomViewStore.getQuotingEvent(), }; - this._onRoomViewStoreUpdate = this._onRoomViewStoreUpdate.bind(this); - this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate); + this.roomStoreToken = RoomViewStore.addListener(this.onRoomViewStoreUpdate); } componentWillUnmount() { this.unmounted = true; // Remove RoomStore listener - if (this._roomStoreToken) { - this._roomStoreToken.remove(); + if (this.roomStoreToken) { + this.roomStoreToken.remove(); } } - _onRoomViewStoreUpdate() { + private onRoomViewStoreUpdate = (): void => { if (this.unmounted) return; const event = RoomViewStore.getQuotingEvent(); if (this.state.event !== event) { this.setState({ event }); } - } + }; render() { if (!this.state.event) return null; diff --git a/src/components/views/voip/CallPreview.tsx b/src/components/views/voip/CallPreview.tsx index ddcb9057ec..895d9773e4 100644 --- a/src/components/views/voip/CallPreview.tsx +++ b/src/components/views/voip/CallPreview.tsx @@ -30,6 +30,7 @@ import { replaceableComponent } from "../../../utils/replaceableComponent"; import UIStore from '../../../stores/UIStore'; import { lerp } from '../../../utils/AnimationUtils'; import { MarkedExecution } from '../../../utils/MarkedExecution'; +import { EventSubscription } from 'fbemitter'; const PIP_VIEW_WIDTH = 336; const PIP_VIEW_HEIGHT = 232; @@ -108,7 +109,7 @@ function getPrimarySecondaryCalls(calls: MatrixCall[]): [MatrixCall, MatrixCall[ */ @replaceableComponent("views.voip.CallPreview") export default class CallPreview extends React.Component { - private roomStoreToken: any; + private roomStoreToken: EventSubscription; private dispatcherRef: string; private settingsWatcherRef: string; private callViewWrapper = createRef (); @@ -240,7 +241,7 @@ export default class CallPreview extends React.Component { this.scheduledUpdate.mark(); }; - private onRoomViewStoreUpdate = (payload) => { + private onRoomViewStoreUpdate = () => { if (RoomViewStore.getRoomId() === this.state.roomId) return; const roomId = RoomViewStore.getRoomId(); diff --git a/src/stores/RoomViewStore.tsx b/src/stores/RoomViewStore.tsx index 10f42f3166..1a85ff59b1 100644 --- a/src/stores/RoomViewStore.tsx +++ b/src/stores/RoomViewStore.tsx @@ -429,7 +429,7 @@ class RoomViewStore extends Store { } } -let singletonRoomViewStore = null; +let singletonRoomViewStore: RoomViewStore = null; if (!singletonRoomViewStore) { singletonRoomViewStore = new RoomViewStore(); }