Merge pull request #6205 from robintown/text-for-event-perf

pull/21833/head
Michael Telatynski 2021-07-17 00:08:34 +01:00 committed by GitHub
commit 0f15cee583
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 91 additions and 54 deletions

View File

@ -13,7 +13,6 @@ 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 { MatrixClientPeg } from './MatrixClientPeg'; import { MatrixClientPeg } from './MatrixClientPeg';
import { _t } from './languageHandler'; import { _t } from './languageHandler';
@ -32,7 +31,7 @@ import { MatrixEvent } from "matrix-js-sdk/src/models/event";
// any text to display at all. For this reason they return deferred values // any text to display at all. For this reason they return deferred values
// to avoid the expense of looking up translations when they're not needed. // to avoid the expense of looking up translations when they're not needed.
function textForMemberEvent(ev: MatrixEvent): () => string | null { function textForMemberEvent(ev: MatrixEvent, allowJSX: boolean, showHiddenEvents?: boolean): () => string | null {
// XXX: SYJS-16 "sender is sometimes null for join messages" // XXX: SYJS-16 "sender is sometimes null for join messages"
const senderName = ev.sender ? ev.sender.name : ev.getSender(); const senderName = ev.sender ? ev.sender.name : ev.getSender();
const targetName = ev.target ? ev.target.name : ev.getStateKey(); const targetName = ev.target ? ev.target.name : ev.getStateKey();
@ -84,7 +83,7 @@ function textForMemberEvent(ev: MatrixEvent): () => string | null {
return () => _t('%(senderName)s changed their profile picture', { senderName }); return () => _t('%(senderName)s changed their profile picture', { senderName });
} else if (!prevContent.avatar_url && content.avatar_url) { } else if (!prevContent.avatar_url && content.avatar_url) {
return () => _t('%(senderName)s set a profile picture', { senderName }); return () => _t('%(senderName)s set a profile picture', { senderName });
} else if (SettingsStore.getValue("showHiddenEventsInTimeline")) { } else if (showHiddenEvents ?? SettingsStore.getValue("showHiddenEventsInTimeline")) {
// This is a null rejoin, it will only be visible if using 'show hidden events' (labs) // This is a null rejoin, it will only be visible if using 'show hidden events' (labs)
return () => _t("%(senderName)s made no change", { senderName }); return () => _t("%(senderName)s made no change", { senderName });
} else { } else {
@ -319,7 +318,7 @@ function textForCanonicalAliasEvent(ev: MatrixEvent): () => string | null {
}); });
} }
function textForCallAnswerEvent(event): () => string | null { function textForCallAnswerEvent(event: MatrixEvent): () => string | null {
return () => { return () => {
const senderName = event.sender ? event.sender.name : _t('Someone'); const senderName = event.sender ? event.sender.name : _t('Someone');
const supported = MatrixClientPeg.get().supportsVoip() ? '' : _t('(not supported by this browser)'); const supported = MatrixClientPeg.get().supportsVoip() ? '' : _t('(not supported by this browser)');
@ -327,7 +326,7 @@ function textForCallAnswerEvent(event): () => string | null {
}; };
} }
function textForCallHangupEvent(event): () => string | null { function textForCallHangupEvent(event: MatrixEvent): () => string | null {
const getSenderName = () => event.sender ? event.sender.name : _t('Someone'); const getSenderName = () => event.sender ? event.sender.name : _t('Someone');
const eventContent = event.getContent(); const eventContent = event.getContent();
let getReason = () => ""; let getReason = () => "";
@ -364,14 +363,14 @@ function textForCallHangupEvent(event): () => string | null {
return () => _t('%(senderName)s ended the call.', { senderName: getSenderName() }) + ' ' + getReason(); return () => _t('%(senderName)s ended the call.', { senderName: getSenderName() }) + ' ' + getReason();
} }
function textForCallRejectEvent(event): () => string | null { function textForCallRejectEvent(event: MatrixEvent): () => string | null {
return () => { return () => {
const senderName = event.sender ? event.sender.name : _t('Someone'); const senderName = event.sender ? event.sender.name : _t('Someone');
return _t('%(senderName)s declined the call.', { senderName }); return _t('%(senderName)s declined the call.', { senderName });
}; };
} }
function textForCallInviteEvent(event): () => string | null { function textForCallInviteEvent(event: MatrixEvent): () => string | null {
const getSenderName = () => event.sender ? event.sender.name : _t('Someone'); const getSenderName = () => event.sender ? event.sender.name : _t('Someone');
// FIXME: Find a better way to determine this from the event? // FIXME: Find a better way to determine this from the event?
let isVoice = true; let isVoice = true;
@ -403,7 +402,7 @@ function textForCallInviteEvent(event): () => string | null {
} }
} }
function textForThreePidInviteEvent(event): () => string | null { function textForThreePidInviteEvent(event: MatrixEvent): () => string | null {
const senderName = event.sender ? event.sender.name : event.getSender(); const senderName = event.sender ? event.sender.name : event.getSender();
if (!isValid3pidInvite(event)) { if (!isValid3pidInvite(event)) {
@ -419,7 +418,7 @@ function textForThreePidInviteEvent(event): () => string | null {
}); });
} }
function textForHistoryVisibilityEvent(event): () => string | null { function textForHistoryVisibilityEvent(event: MatrixEvent): () => string | null {
const senderName = event.sender ? event.sender.name : event.getSender(); const senderName = event.sender ? event.sender.name : event.getSender();
switch (event.getContent().history_visibility) { switch (event.getContent().history_visibility) {
case 'invited': case 'invited':
@ -441,7 +440,7 @@ function textForHistoryVisibilityEvent(event): () => string | null {
} }
// Currently will only display a change if a user's power level is changed // Currently will only display a change if a user's power level is changed
function textForPowerEvent(event): () => string | null { function textForPowerEvent(event: MatrixEvent): () => string | null {
const senderName = event.sender ? event.sender.name : event.getSender(); const senderName = event.sender ? event.sender.name : event.getSender();
if (!event.getPrevContent() || !event.getPrevContent().users || if (!event.getPrevContent() || !event.getPrevContent().users ||
!event.getContent() || !event.getContent().users) { !event.getContent() || !event.getContent().users) {
@ -523,7 +522,7 @@ function textForPinnedEvent(event: MatrixEvent, allowJSX: boolean): () => string
return () => _t("%(senderName)s changed the pinned messages for the room.", { senderName }); return () => _t("%(senderName)s changed the pinned messages for the room.", { senderName });
} }
function textForWidgetEvent(event): () => string | null { function textForWidgetEvent(event: MatrixEvent): () => string | null {
const senderName = event.getSender(); const senderName = event.getSender();
const { name: prevName, type: prevType, url: prevUrl } = event.getPrevContent(); const { name: prevName, type: prevType, url: prevUrl } = event.getPrevContent();
const { name, type, url } = event.getContent() || {}; const { name, type, url } = event.getContent() || {};
@ -553,12 +552,12 @@ function textForWidgetEvent(event): () => string | null {
} }
} }
function textForWidgetLayoutEvent(event): () => string | null { function textForWidgetLayoutEvent(event: MatrixEvent): () => string | null {
const senderName = event.sender?.name || event.getSender(); const senderName = event.sender?.name || event.getSender();
return () => _t("%(senderName)s has updated the widget layout", { senderName }); return () => _t("%(senderName)s has updated the widget layout", { senderName });
} }
function textForMjolnirEvent(event): () => string | null { function textForMjolnirEvent(event: MatrixEvent): () => string | null {
const senderName = event.getSender(); const senderName = event.getSender();
const { entity: prevEntity } = event.getPrevContent(); const { entity: prevEntity } = event.getPrevContent();
const { entity, recommendation, reason } = event.getContent(); const { entity, recommendation, reason } = event.getContent();
@ -646,7 +645,9 @@ function textForMjolnirEvent(event): () => string | null {
} }
interface IHandlers { interface IHandlers {
[type: string]: (ev: MatrixEvent, allowJSX?: boolean) => (() => string | JSX.Element | null); [type: string]:
(ev: MatrixEvent, allowJSX: boolean, showHiddenEvents?: boolean) =>
(() => string | JSX.Element | null);
} }
const handlers: IHandlers = { const handlers: IHandlers = {
@ -682,14 +683,27 @@ for (const evType of ALL_RULE_TYPES) {
stateHandlers[evType] = textForMjolnirEvent; stateHandlers[evType] = textForMjolnirEvent;
} }
export function hasText(ev: MatrixEvent): boolean { /**
* Determines whether the given event has text to display.
* @param ev The event
* @param showHiddenEvents An optional cached setting value for showHiddenEventsInTimeline
* to avoid hitting the settings store
*/
export function hasText(ev: MatrixEvent, showHiddenEvents?: boolean): boolean {
const handler = (ev.isState() ? stateHandlers : handlers)[ev.getType()]; const handler = (ev.isState() ? stateHandlers : handlers)[ev.getType()];
return Boolean(handler?.(ev)); return Boolean(handler?.(ev, false, showHiddenEvents));
} }
/**
* Gets the textual content of the given event.
* @param ev The event
* @param allowJSX Whether to output rich JSX content
* @param showHiddenEvents An optional cached setting value for showHiddenEventsInTimeline
* to avoid hitting the settings store
*/
export function textForEvent(ev: MatrixEvent): string; export function textForEvent(ev: MatrixEvent): string;
export function textForEvent(ev: MatrixEvent, allowJSX: true): string | JSX.Element; export function textForEvent(ev: MatrixEvent, allowJSX: true, showHiddenEvents?: boolean): string | JSX.Element;
export function textForEvent(ev: MatrixEvent, allowJSX = false): string | JSX.Element { export function textForEvent(ev: MatrixEvent, allowJSX = false, showHiddenEvents?: boolean): string | JSX.Element {
const handler = (ev.isState() ? stateHandlers : handlers)[ev.getType()]; const handler = (ev.isState() ? stateHandlers : handlers)[ev.getType()];
return handler?.(ev, allowJSX)?.() || ''; return handler?.(ev, allowJSX, showHiddenEvents)?.() || '';
} }

View File

@ -54,7 +54,11 @@ const membershipTypes = [EventType.RoomMember, EventType.RoomThirdPartyInvite, E
// check if there is a previous event and it has the same sender as this event // check if there is a previous event and it has the same sender as this event
// and the types are the same/is in continuedTypes and the time between them is <= CONTINUATION_MAX_INTERVAL // and the types are the same/is in continuedTypes and the time between them is <= CONTINUATION_MAX_INTERVAL
function shouldFormContinuation(prevEvent: MatrixEvent, mxEvent: MatrixEvent): boolean { function shouldFormContinuation(
prevEvent: MatrixEvent,
mxEvent: MatrixEvent,
showHiddenEvents: boolean,
): boolean {
// sanity check inputs // sanity check inputs
if (!prevEvent || !prevEvent.sender || !mxEvent.sender) return false; if (!prevEvent || !prevEvent.sender || !mxEvent.sender) return false;
// check if within the max continuation period // check if within the max continuation period
@ -74,7 +78,7 @@ function shouldFormContinuation(prevEvent: MatrixEvent, mxEvent: MatrixEvent): b
mxEvent.sender.getMxcAvatarUrl() !== prevEvent.sender.getMxcAvatarUrl()) return false; mxEvent.sender.getMxcAvatarUrl() !== prevEvent.sender.getMxcAvatarUrl()) return false;
// if we don't have tile for previous event then it was shown by showHiddenEvents and has no SenderProfile // if we don't have tile for previous event then it was shown by showHiddenEvents and has no SenderProfile
if (!haveTileForEvent(prevEvent)) return false; if (!haveTileForEvent(prevEvent, showHiddenEvents)) return false;
return true; return true;
} }
@ -239,7 +243,8 @@ export default class MessagePanel extends React.Component<IProps, IState> {
}; };
// Cache hidden events setting on mount since Settings is expensive to // Cache hidden events setting on mount since Settings is expensive to
// query, and we check this in a hot code path. // query, and we check this in a hot code path. This is also cached in
// our RoomContext, however we still need a fallback for roomless MessagePanels.
this.showHiddenEventsInTimeline = SettingsStore.getValue("showHiddenEventsInTimeline"); this.showHiddenEventsInTimeline = SettingsStore.getValue("showHiddenEventsInTimeline");
this.showTypingNotificationsWatcherRef = this.showTypingNotificationsWatcherRef =
@ -399,17 +404,21 @@ export default class MessagePanel extends React.Component<IProps, IState> {
return !this.isMounted; return !this.isMounted;
}; };
private get showHiddenEvents(): boolean {
return this.context?.showHiddenEventsInTimeline ?? this.showHiddenEventsInTimeline;
}
// TODO: Implement granular (per-room) hide options // TODO: Implement granular (per-room) hide options
public shouldShowEvent(mxEv: MatrixEvent): boolean { public shouldShowEvent(mxEv: MatrixEvent): boolean {
if (MatrixClientPeg.get().isUserIgnored(mxEv.getSender())) { if (MatrixClientPeg.get().isUserIgnored(mxEv.getSender())) {
return false; // ignored = no show (only happens if the ignore happens after an event was received) return false; // ignored = no show (only happens if the ignore happens after an event was received)
} }
if (this.showHiddenEventsInTimeline) { if (this.showHiddenEvents) {
return true; return true;
} }
if (!haveTileForEvent(mxEv)) { if (!haveTileForEvent(mxEv, this.showHiddenEvents)) {
return false; // no tile = no show return false; // no tile = no show
} }
@ -569,7 +578,7 @@ export default class MessagePanel extends React.Component<IProps, IState> {
if (grouper) { if (grouper) {
if (grouper.shouldGroup(mxEv)) { if (grouper.shouldGroup(mxEv)) {
grouper.add(mxEv); grouper.add(mxEv, this.showHiddenEvents);
continue; continue;
} else { } else {
// not part of group, so get the group tiles, close the // not part of group, so get the group tiles, close the
@ -649,7 +658,8 @@ export default class MessagePanel extends React.Component<IProps, IState> {
} }
// is this a continuation of the previous message? // is this a continuation of the previous message?
const continuation = !wantsDateSeparator && shouldFormContinuation(prevEvent, mxEv); const continuation = !wantsDateSeparator &&
shouldFormContinuation(prevEvent, mxEv, this.showHiddenEvents);
const eventId = mxEv.getId(); const eventId = mxEv.getId();
const highlight = (eventId === this.props.highlightedEventId); const highlight = (eventId === this.props.highlightedEventId);
@ -946,7 +956,7 @@ abstract class BaseGrouper {
} }
public abstract shouldGroup(ev: MatrixEvent): boolean; public abstract shouldGroup(ev: MatrixEvent): boolean;
public abstract add(ev: MatrixEvent): void; public abstract add(ev: MatrixEvent, showHiddenEvents?: boolean): void;
public abstract getTiles(): ReactNode[]; public abstract getTiles(): ReactNode[];
public abstract getNewPrevEvent(): MatrixEvent; public abstract getNewPrevEvent(): MatrixEvent;
} }
@ -1200,10 +1210,10 @@ class MemberGrouper extends BaseGrouper {
return membershipTypes.includes(ev.getType() as EventType); return membershipTypes.includes(ev.getType() as EventType);
} }
public add(ev: MatrixEvent): void { public add(ev: MatrixEvent, showHiddenEvents?: boolean): void {
if (ev.getType() === EventType.RoomMember) { if (ev.getType() === EventType.RoomMember) {
// We can ignore any events that don't actually have a message to display // We can ignore any events that don't actually have a message to display
if (!hasText(ev)) return; if (!hasText(ev, showHiddenEvents)) return;
} }
this.readMarker = this.readMarker || this.panel.readMarkerForEvent( this.readMarker = this.readMarker || this.panel.readMarkerForEvent(
ev.getId(), ev.getId(),

View File

@ -166,6 +166,7 @@ export interface IState {
canReply: boolean; canReply: boolean;
layout: Layout; layout: Layout;
lowBandwidth: boolean; lowBandwidth: boolean;
showHiddenEventsInTimeline: boolean;
showReadReceipts: boolean; showReadReceipts: boolean;
showRedactions: boolean; showRedactions: boolean;
showJoinLeaves: boolean; showJoinLeaves: boolean;
@ -230,6 +231,7 @@ export default class RoomView extends React.Component<IProps, IState> {
canReply: false, canReply: false,
layout: SettingsStore.getValue("layout"), layout: SettingsStore.getValue("layout"),
lowBandwidth: SettingsStore.getValue("lowBandwidth"), lowBandwidth: SettingsStore.getValue("lowBandwidth"),
showHiddenEventsInTimeline: SettingsStore.getValue("showHiddenEventsInTimeline"),
showReadReceipts: true, showReadReceipts: true,
showRedactions: true, showRedactions: true,
showJoinLeaves: true, showJoinLeaves: true,
@ -267,6 +269,9 @@ export default class RoomView extends React.Component<IProps, IState> {
SettingsStore.watchSetting("lowBandwidth", null, () => SettingsStore.watchSetting("lowBandwidth", null, () =>
this.setState({ lowBandwidth: SettingsStore.getValue("lowBandwidth") }), this.setState({ lowBandwidth: SettingsStore.getValue("lowBandwidth") }),
), ),
SettingsStore.watchSetting("showHiddenEventsInTimeline", null, () =>
this.setState({ showHiddenEventsInTimeline: SettingsStore.getValue("showHiddenEventsInTimeline") }),
),
]; ];
} }
@ -1388,7 +1393,7 @@ export default class RoomView extends React.Component<IProps, IState> {
continue; continue;
} }
if (!haveTileForEvent(mxEv)) { if (!haveTileForEvent(mxEv, this.state.showHiddenEventsInTimeline)) {
// XXX: can this ever happen? It will make the result count // XXX: can this ever happen? It will make the result count
// not match the displayed count. // not match the displayed count.
continue; continue;

View File

@ -1337,7 +1337,8 @@ class TimelinePanel extends React.Component<IProps, IState> {
const shouldIgnore = !!ev.status || // local echo const shouldIgnore = !!ev.status || // local echo
(ignoreOwn && ev.getSender() === myUserId); // own message (ignoreOwn && ev.getSender() === myUserId); // own message
const isWithoutTile = !haveTileForEvent(ev) || shouldHideEvent(ev, this.context); const isWithoutTile = !haveTileForEvent(ev, this.context?.showHiddenEventsInTimeline) ||
shouldHideEvent(ev, this.context);
if (isWithoutTile || !node) { if (isWithoutTile || !node) {
// don't start counting if the event should be ignored, // don't start counting if the event should be ignored,

View File

@ -14,9 +14,10 @@ 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 { MatrixEvent } from 'matrix-js-sdk/src/models/event'; import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import RoomContext from "../../../contexts/RoomContext";
import * as TextForEvent from "../../../TextForEvent"; import * as TextForEvent from "../../../TextForEvent";
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
@ -26,11 +27,11 @@ interface IProps {
@replaceableComponent("views.messages.TextualEvent") @replaceableComponent("views.messages.TextualEvent")
export default class TextualEvent extends React.Component<IProps> { export default class TextualEvent extends React.Component<IProps> {
render() { static contextType = RoomContext;
const text = TextForEvent.textForEvent(this.props.mxEvent, true);
if (!text || (text as string).length === 0) return null; public render() {
return ( const text = TextForEvent.textForEvent(this.props.mxEvent, true, this.context?.showHiddenEventsInTimeline);
<div className="mx_TextualEvent">{ text }</div> if (!text) return null;
); return <div className="mx_TextualEvent">{ text }</div>;
} }
} }

View File

@ -1160,7 +1160,7 @@ function isMessageEvent(ev) {
return (messageTypes.includes(ev.getType())); return (messageTypes.includes(ev.getType()));
} }
export function haveTileForEvent(e) { export function haveTileForEvent(e: MatrixEvent, showHiddenEvents?: boolean) {
// Only messages have a tile (black-rectangle) if redacted // Only messages have a tile (black-rectangle) if redacted
if (e.isRedacted() && !isMessageEvent(e)) return false; if (e.isRedacted() && !isMessageEvent(e)) return false;
@ -1170,7 +1170,7 @@ export function haveTileForEvent(e) {
const handler = getHandlerTile(e); const handler = getHandlerTile(e);
if (handler === undefined) return false; if (handler === undefined) return false;
if (handler === 'messages.TextualEvent') { if (handler === 'messages.TextualEvent') {
return hasText(e); return hasText(e, showHiddenEvents);
} else if (handler === 'messages.RoomCreate') { } else if (handler === 'messages.RoomCreate') {
return Boolean(e.getContent()['predecessor']); return Boolean(e.getContent()['predecessor']);
} else { } else {

View File

@ -15,14 +15,15 @@ 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 { SearchResult } from "matrix-js-sdk/src/models/search-result"; import { SearchResult } from "matrix-js-sdk/src/models/search-result";
import EventTile, { haveTileForEvent } from "./EventTile"; import RoomContext from "../../../contexts/RoomContext";
import DateSeparator from '../messages/DateSeparator';
import SettingsStore from "../../../settings/SettingsStore"; import SettingsStore from "../../../settings/SettingsStore";
import { UIFeature } from "../../../settings/UIFeature"; import { UIFeature } from "../../../settings/UIFeature";
import { RoomPermalinkCreator } from '../../../utils/permalinks/Permalinks'; import { RoomPermalinkCreator } from '../../../utils/permalinks/Permalinks';
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import DateSeparator from "../messages/DateSeparator";
import EventTile, { haveTileForEvent } from "./EventTile";
interface IProps { interface IProps {
// a matrix-js-sdk SearchResult containing the details of this result // a matrix-js-sdk SearchResult containing the details of this result
@ -37,6 +38,8 @@ interface IProps {
@replaceableComponent("views.rooms.SearchResultTile") @replaceableComponent("views.rooms.SearchResultTile")
export default class SearchResultTile extends React.Component<IProps> { export default class SearchResultTile extends React.Component<IProps> {
static contextType = RoomContext;
public render() { public render() {
const result = this.props.searchResult; const result = this.props.searchResult;
const mxEv = result.context.getEvent(); const mxEv = result.context.getEvent();
@ -44,7 +47,10 @@ export default class SearchResultTile extends React.Component<IProps> {
const ts1 = mxEv.getTs(); const ts1 = mxEv.getTs();
const ret = [<DateSeparator key={ts1 + "-search"} ts={ts1} />]; const ret = [<DateSeparator key={ts1 + "-search"} ts={ts1} />];
const layout = SettingsStore.getValue("layout");
const isTwelveHour = SettingsStore.getValue("showTwelveHourTimestamps");
const alwaysShowTimestamps = SettingsStore.getValue("alwaysShowTimestamps"); const alwaysShowTimestamps = SettingsStore.getValue("alwaysShowTimestamps");
const enableFlair = SettingsStore.getValue(UIFeature.Flair);
const timeline = result.context.getTimeline(); const timeline = result.context.getTimeline();
for (let j = 0; j < timeline.length; j++) { for (let j = 0; j < timeline.length; j++) {
@ -54,26 +60,25 @@ export default class SearchResultTile extends React.Component<IProps> {
if (!contextual) { if (!contextual) {
highlights = this.props.searchHighlights; highlights = this.props.searchHighlights;
} }
if (haveTileForEvent(ev)) { if (haveTileForEvent(ev, this.context?.showHiddenEventsInTimeline)) {
ret.push(( ret.push(
<EventTile <EventTile
key={`${eventId}+${j}`} key={`${eventId}+${j}`}
mxEvent={ev} mxEvent={ev}
layout={layout}
contextual={contextual} contextual={contextual}
highlights={highlights} highlights={highlights}
permalinkCreator={this.props.permalinkCreator} permalinkCreator={this.props.permalinkCreator}
highlightLink={this.props.resultLink} highlightLink={this.props.resultLink}
onHeightChanged={this.props.onHeightChanged} onHeightChanged={this.props.onHeightChanged}
isTwelveHour={SettingsStore.getValue("showTwelveHourTimestamps")} isTwelveHour={isTwelveHour}
alwaysShowTimestamps={alwaysShowTimestamps} alwaysShowTimestamps={alwaysShowTimestamps}
enableFlair={SettingsStore.getValue(UIFeature.Flair)} enableFlair={enableFlair}
/> />,
)); );
} }
} }
return (
<li data-scroll-tokens={eventId}> return <li data-scroll-tokens={eventId}>{ ret }</li>;
{ ret }
</li>);
} }
} }

View File

@ -41,6 +41,7 @@ const RoomContext = createContext<IState>({
canReply: false, canReply: false,
layout: Layout.Group, layout: Layout.Group,
lowBandwidth: false, lowBandwidth: false,
showHiddenEventsInTimeline: false,
showReadReceipts: true, showReadReceipts: true,
showRedactions: true, showRedactions: true,
showJoinLeaves: true, showJoinLeaves: true,