Merge branch 'develop' of github.com:matrix-org/matrix-react-sdk into joriks/style-fighting
This commit is contained in:
commit
271eeeabee
315 changed files with 8146 additions and 2874 deletions
|
@ -685,6 +685,9 @@ export default createReactClass({
|
|||
mx_EventTile_emote: msgtype === 'm.emote',
|
||||
});
|
||||
|
||||
// If the tile is in the Sending state, don't speak the message.
|
||||
const ariaLive = (this.props.eventSendStatus !== null) ? 'off' : undefined;
|
||||
|
||||
let permalink = "#";
|
||||
if (this.props.permalinkCreator) {
|
||||
permalink = this.props.permalinkCreator.forEvent(this.props.mxEvent.getId());
|
||||
|
@ -819,7 +822,7 @@ export default createReactClass({
|
|||
case 'notif': {
|
||||
const room = this.context.getRoom(this.props.mxEvent.getRoomId());
|
||||
return (
|
||||
<div className={classes}>
|
||||
<div className={classes} aria-live={ariaLive} aria-atomic="true">
|
||||
<div className="mx_EventTile_roomName">
|
||||
<a href={permalink} onClick={this.onPermalinkClicked}>
|
||||
{ room ? room.name : '' }
|
||||
|
@ -845,7 +848,7 @@ export default createReactClass({
|
|||
}
|
||||
case 'file_grid': {
|
||||
return (
|
||||
<div className={classes}>
|
||||
<div className={classes} aria-live={ariaLive} aria-atomic="true">
|
||||
<div className="mx_EventTile_line">
|
||||
<EventTileType ref={this._tile}
|
||||
mxEvent={this.props.mxEvent}
|
||||
|
@ -881,7 +884,7 @@ export default createReactClass({
|
|||
);
|
||||
}
|
||||
return (
|
||||
<div className={classes}>
|
||||
<div className={classes} aria-live={ariaLive} aria-atomic="true">
|
||||
{ ircTimestamp }
|
||||
{ avatar }
|
||||
{ sender }
|
||||
|
@ -911,7 +914,7 @@ export default createReactClass({
|
|||
|
||||
// tab-index=-1 to allow it to be focusable but do not add tab stop for it, primarily for screen readers
|
||||
return (
|
||||
<div className={classes} tabIndex={-1}>
|
||||
<div className={classes} tabIndex={-1} aria-live={ariaLive} aria-atomic="true">
|
||||
{ ircTimestamp }
|
||||
<div className="mx_EventTile_msgOption">
|
||||
{ readAvatars }
|
||||
|
|
|
@ -72,6 +72,7 @@ export default class NotificationBadge extends React.PureComponent<XOR<IProps, I
|
|||
|
||||
public componentWillUnmount() {
|
||||
SettingsStore.unwatchSetting(this.countWatcherRef);
|
||||
this.props.notification.off(NOTIFICATION_STATE_UPDATE, this.onNotificationUpdate);
|
||||
}
|
||||
|
||||
public componentDidUpdate(prevProps: Readonly<IProps>) {
|
||||
|
|
|
@ -73,7 +73,7 @@ export default class ReplyPreview extends React.Component {
|
|||
return <div className="mx_ReplyPreview">
|
||||
<div className="mx_ReplyPreview_section">
|
||||
<div className="mx_ReplyPreview_header mx_ReplyPreview_title">
|
||||
{ '💬 ' + _t('Replying') }
|
||||
{ _t('Replying') }
|
||||
</div>
|
||||
<div className="mx_ReplyPreview_header mx_ReplyPreview_cancel">
|
||||
<img className="mx_filterFlipColor" src={require("../../../../res/img/cancel.svg")} width="18" height="18"
|
||||
|
|
|
@ -31,7 +31,6 @@ import dis from "../../../dispatcher/dispatcher";
|
|||
import defaultDispatcher from "../../../dispatcher/dispatcher";
|
||||
import RoomSublist from "./RoomSublist";
|
||||
import { ActionPayload } from "../../../dispatcher/payloads";
|
||||
import { NameFilterCondition } from "../../../stores/room-list/filters/NameFilterCondition";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import GroupAvatar from "../avatars/GroupAvatar";
|
||||
import TemporaryTile from "./TemporaryTile";
|
||||
|
@ -42,6 +41,8 @@ import { ViewRoomDeltaPayload } from "../../../dispatcher/payloads/ViewRoomDelta
|
|||
import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import CustomRoomTagStore from "../../../stores/CustomRoomTagStore";
|
||||
import { arrayFastClone, arrayHasDiff } from "../../../utils/arrays";
|
||||
import { objectShallowClone, objectWithOnly } from "../../../utils/objects";
|
||||
|
||||
interface IProps {
|
||||
onKeyDown: (ev: React.KeyboardEvent) => void;
|
||||
|
@ -50,7 +51,6 @@ interface IProps {
|
|||
onResize: () => void;
|
||||
resizeNotifier: ResizeNotifier;
|
||||
collapsed: boolean;
|
||||
searchFilter: string;
|
||||
isMinimized: boolean;
|
||||
}
|
||||
|
||||
|
@ -80,7 +80,7 @@ interface ITagAesthetics {
|
|||
sectionLabel: string;
|
||||
sectionLabelRaw?: string;
|
||||
addRoomLabel?: string;
|
||||
onAddRoom?: (dispatcher: Dispatcher<ActionPayload>) => void;
|
||||
onAddRoom?: (dispatcher?: Dispatcher<ActionPayload>) => void;
|
||||
isInvite: boolean;
|
||||
defaultHidden: boolean;
|
||||
}
|
||||
|
@ -104,14 +104,18 @@ const TAG_AESTHETICS: {
|
|||
isInvite: false,
|
||||
defaultHidden: false,
|
||||
addRoomLabel: _td("Start chat"),
|
||||
onAddRoom: (dispatcher: Dispatcher<ActionPayload>) => dispatcher.dispatch({action: 'view_create_chat'}),
|
||||
onAddRoom: (dispatcher?: Dispatcher<ActionPayload>) => {
|
||||
(dispatcher || defaultDispatcher).dispatch({action: 'view_create_chat'});
|
||||
},
|
||||
},
|
||||
[DefaultTagID.Untagged]: {
|
||||
sectionLabel: _td("Rooms"),
|
||||
isInvite: false,
|
||||
defaultHidden: false,
|
||||
addRoomLabel: _td("Create room"),
|
||||
onAddRoom: (dispatcher: Dispatcher<ActionPayload>) => dispatcher.dispatch({action: 'view_create_room'}),
|
||||
onAddRoom: (dispatcher?: Dispatcher<ActionPayload>) => {
|
||||
(dispatcher || defaultDispatcher).dispatch({action: 'view_create_room'})
|
||||
},
|
||||
},
|
||||
[DefaultTagID.LowPriority]: {
|
||||
sectionLabel: _td("Low priority"),
|
||||
|
@ -144,8 +148,7 @@ function customTagAesthetics(tagId: TagID): ITagAesthetics {
|
|||
};
|
||||
}
|
||||
|
||||
export default class RoomList extends React.Component<IProps, IState> {
|
||||
private searchFilter: NameFilterCondition = new NameFilterCondition();
|
||||
export default class RoomList extends React.PureComponent<IProps, IState> {
|
||||
private dispatcherRef;
|
||||
private customTagStoreRef;
|
||||
|
||||
|
@ -159,21 +162,6 @@ export default class RoomList extends React.Component<IProps, IState> {
|
|||
this.dispatcherRef = defaultDispatcher.register(this.onAction);
|
||||
}
|
||||
|
||||
public componentDidUpdate(prevProps: Readonly<IProps>): void {
|
||||
if (prevProps.searchFilter !== this.props.searchFilter) {
|
||||
const hadSearch = !!this.searchFilter.search.trim();
|
||||
const haveSearch = !!this.props.searchFilter.trim();
|
||||
this.searchFilter.search = this.props.searchFilter;
|
||||
if (!hadSearch && haveSearch) {
|
||||
// started a new filter - add the condition
|
||||
RoomListStore.instance.addFilter(this.searchFilter);
|
||||
} else if (hadSearch && !haveSearch) {
|
||||
// cleared a filter - remove the condition
|
||||
RoomListStore.instance.removeFilter(this.searchFilter);
|
||||
} // else the filter hasn't changed enough for us to care here
|
||||
}
|
||||
}
|
||||
|
||||
public componentDidMount(): void {
|
||||
RoomListStore.instance.on(LISTS_UPDATE_EVENT, this.updateLists);
|
||||
this.customTagStoreRef = CustomRoomTagStore.addListener(this.updateLists);
|
||||
|
@ -231,17 +219,46 @@ export default class RoomList extends React.Component<IProps, IState> {
|
|||
console.log("new lists", newLists);
|
||||
}
|
||||
|
||||
this.setState({sublists: newLists}, () => {
|
||||
this.props.onResize();
|
||||
const previousListIds = Object.keys(this.state.sublists);
|
||||
const newListIds = Object.keys(newLists).filter(t => {
|
||||
if (!isCustomTag(t)) return true; // always include non-custom tags
|
||||
|
||||
// if the tag is custom though, only include it if it is enabled
|
||||
return CustomRoomTagStore.getTags()[t];
|
||||
});
|
||||
|
||||
let doUpdate = arrayHasDiff(previousListIds, newListIds);
|
||||
if (!doUpdate) {
|
||||
// so we didn't have the visible sublists change, but did the contents of those
|
||||
// sublists change significantly enough to break the sticky headers? Probably, so
|
||||
// let's check the length of each.
|
||||
for (const tagId of newListIds) {
|
||||
const oldRooms = this.state.sublists[tagId];
|
||||
const newRooms = newLists[tagId];
|
||||
if (oldRooms.length !== newRooms.length) {
|
||||
doUpdate = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (doUpdate) {
|
||||
// We have to break our reference to the room list store if we want to be able to
|
||||
// diff the object for changes, so do that.
|
||||
const newSublists = objectWithOnly(newLists, newListIds);
|
||||
const sublists = objectShallowClone(newSublists, (k, v) => arrayFastClone(v));
|
||||
|
||||
this.setState({sublists}, () => {
|
||||
this.props.onResize();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
private renderCommunityInvites(): React.ReactElement[] {
|
||||
private renderCommunityInvites(): TemporaryTile[] {
|
||||
// TODO: Put community invites in a more sensible place (not in the room list)
|
||||
// See https://github.com/vector-im/riot-web/issues/14456
|
||||
return MatrixClientPeg.get().getGroups().filter(g => {
|
||||
if (g.myMembership !== 'invite') return false;
|
||||
return !this.searchFilter || this.searchFilter.matches(g.name || "");
|
||||
return g.myMembership === 'invite';
|
||||
}).map(g => {
|
||||
const avatar = (
|
||||
<GroupAvatar
|
||||
|
@ -277,8 +294,7 @@ export default class RoomList extends React.Component<IProps, IState> {
|
|||
const tagOrder = TAG_ORDER.reduce((p, c) => {
|
||||
if (c === CUSTOM_TAGS_BEFORE_TAG) {
|
||||
const customTags = Object.keys(this.state.sublists)
|
||||
.filter(t => isCustomTag(t))
|
||||
.filter(t => CustomRoomTagStore.getTags()[t]); // isSelected
|
||||
.filter(t => isCustomTag(t));
|
||||
p.push(...customTags);
|
||||
}
|
||||
p.push(c);
|
||||
|
@ -298,21 +314,18 @@ export default class RoomList extends React.Component<IProps, IState> {
|
|||
: TAG_AESTHETICS[orderedTagId];
|
||||
if (!aesthetics) throw new Error(`Tag ${orderedTagId} does not have aesthetics`);
|
||||
|
||||
const onAddRoomFn = aesthetics.onAddRoom ? () => aesthetics.onAddRoom(dis) : null;
|
||||
components.push(
|
||||
<RoomSublist
|
||||
key={`sublist-${orderedTagId}`}
|
||||
tagId={orderedTagId}
|
||||
forRooms={true}
|
||||
rooms={orderedRooms}
|
||||
startAsHidden={aesthetics.defaultHidden}
|
||||
label={aesthetics.sectionLabelRaw ? aesthetics.sectionLabelRaw : _t(aesthetics.sectionLabel)}
|
||||
onAddRoom={onAddRoomFn}
|
||||
onAddRoom={aesthetics.onAddRoom}
|
||||
addRoomLabel={aesthetics.addRoomLabel ? _t(aesthetics.addRoomLabel) : aesthetics.addRoomLabel}
|
||||
isMinimized={this.props.isMinimized}
|
||||
onResize={this.props.onResize}
|
||||
extraBadTilesThatShouldntExist={extraTiles}
|
||||
isFiltered={!!this.searchFilter.search}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -288,7 +288,6 @@ export default createReactClass({
|
|||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
|
||||
let showSpinner = false;
|
||||
let darkStyle = false;
|
||||
let title;
|
||||
let subTitle;
|
||||
let primaryActionHandler;
|
||||
|
@ -316,7 +315,6 @@ export default createReactClass({
|
|||
break;
|
||||
}
|
||||
case MessageCase.NotLoggedIn: {
|
||||
darkStyle = true;
|
||||
title = _t("Join the conversation with an account");
|
||||
primaryActionLabel = _t("Sign Up");
|
||||
primaryActionHandler = this.onRegisterClick;
|
||||
|
@ -557,7 +555,6 @@ export default createReactClass({
|
|||
const classes = classNames("mx_RoomPreviewBar", "dark-panel", `mx_RoomPreviewBar_${messageCase}`, {
|
||||
"mx_RoomPreviewBar_panel": this.props.canPreview,
|
||||
"mx_RoomPreviewBar_dialog": !this.props.canPreview,
|
||||
"mx_RoomPreviewBar_dark": darkStyle,
|
||||
});
|
||||
|
||||
return (
|
||||
|
|
|
@ -21,7 +21,8 @@ import * as sdk from "../../../index";
|
|||
import { _t } from "../../../languageHandler";
|
||||
import Modal from "../../../Modal";
|
||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import {SettingLevel} from "../../../settings/SettingLevel";
|
||||
|
||||
export default class RoomRecoveryReminder extends React.PureComponent {
|
||||
static propTypes = {
|
||||
|
|
|
@ -32,13 +32,12 @@ import {
|
|||
StyledMenuItemCheckbox,
|
||||
StyledMenuItemRadio,
|
||||
} from "../../structures/ContextMenu";
|
||||
import RoomListStore from "../../../stores/room-list/RoomListStore";
|
||||
import RoomListStore, { LISTS_UPDATE_EVENT } from "../../../stores/room-list/RoomListStore";
|
||||
import { ListAlgorithm, SortAlgorithm } from "../../../stores/room-list/algorithms/models";
|
||||
import { DefaultTagID, TagID } from "../../../stores/room-list/models";
|
||||
import dis from "../../../dispatcher/dispatcher";
|
||||
import defaultDispatcher from "../../../dispatcher/dispatcher";
|
||||
import NotificationBadge from "./NotificationBadge";
|
||||
import { ListNotificationState } from "../../../stores/notifications/ListNotificationState";
|
||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||
import { Key } from "../../../Keyboard";
|
||||
import { ActionPayload } from "../../../dispatcher/payloads";
|
||||
|
@ -47,6 +46,10 @@ import { Direction } from "re-resizable/lib/resizer";
|
|||
import { polyfillTouchEvent } from "../../../@types/polyfill";
|
||||
import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore";
|
||||
import RoomListLayoutStore from "../../../stores/room-list/RoomListLayoutStore";
|
||||
import { arrayFastClone, arrayHasOrderChange } from "../../../utils/arrays";
|
||||
import { objectExcluding, objectHasDiff } from "../../../utils/objects";
|
||||
import TemporaryTile from "./TemporaryTile";
|
||||
import { ListNotificationState } from "../../../stores/notifications/ListNotificationState";
|
||||
|
||||
const SHOW_N_BUTTON_HEIGHT = 28; // As defined by CSS
|
||||
const RESIZE_HANDLE_HEIGHT = 4; // As defined by CSS
|
||||
|
@ -59,7 +62,6 @@ polyfillTouchEvent();
|
|||
|
||||
interface IProps {
|
||||
forRooms: boolean;
|
||||
rooms?: Room[];
|
||||
startAsHidden: boolean;
|
||||
label: string;
|
||||
onAddRoom?: () => void;
|
||||
|
@ -67,11 +69,10 @@ interface IProps {
|
|||
isMinimized: boolean;
|
||||
tagId: TagID;
|
||||
onResize: () => void;
|
||||
isFiltered: boolean;
|
||||
|
||||
// TODO: Don't use this. It's for community invites, and community invites shouldn't be here.
|
||||
// You should feel bad if you use this.
|
||||
extraBadTilesThatShouldntExist?: React.ReactElement[];
|
||||
extraBadTilesThatShouldntExist?: TemporaryTile[];
|
||||
|
||||
// TODO: Account for https://github.com/vector-im/riot-web/issues/14179
|
||||
}
|
||||
|
@ -85,11 +86,12 @@ interface ResizeDelta {
|
|||
type PartialDOMRect = Pick<DOMRect, "left" | "top" | "height">;
|
||||
|
||||
interface IState {
|
||||
notificationState: ListNotificationState;
|
||||
contextMenuPosition: PartialDOMRect;
|
||||
isResizing: boolean;
|
||||
isExpanded: boolean; // used for the for expand of the sublist when the room list is being filtered
|
||||
height: number;
|
||||
rooms: Room[];
|
||||
filteredExtraTiles?: TemporaryTile[];
|
||||
}
|
||||
|
||||
export default class RoomSublist extends React.Component<IProps, IState> {
|
||||
|
@ -98,22 +100,27 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
|||
private dispatcherRef: string;
|
||||
private layout: ListLayout;
|
||||
private heightAtStart: number;
|
||||
private isBeingFiltered: boolean;
|
||||
private notificationState: ListNotificationState;
|
||||
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.layout = RoomListLayoutStore.instance.getLayoutFor(this.props.tagId);
|
||||
this.heightAtStart = 0;
|
||||
const height = this.calculateInitialHeight();
|
||||
this.isBeingFiltered = !!RoomListStore.instance.getFirstNameFilterCondition();
|
||||
this.notificationState = RoomNotificationStateStore.instance.getListState(this.props.tagId);
|
||||
this.state = {
|
||||
notificationState: RoomNotificationStateStore.instance.getListState(this.props.tagId),
|
||||
contextMenuPosition: null,
|
||||
isResizing: false,
|
||||
isExpanded: this.props.isFiltered ? this.props.isFiltered : !this.layout.isCollapsed,
|
||||
height,
|
||||
isExpanded: this.isBeingFiltered ? this.isBeingFiltered : !this.layout.isCollapsed,
|
||||
height: 0, // to be fixed in a moment, we need `rooms` to calculate this.
|
||||
rooms: arrayFastClone(RoomListStore.instance.orderedLists[this.props.tagId] || []),
|
||||
};
|
||||
this.state.notificationState.setRooms(this.props.rooms);
|
||||
// Why Object.assign() and not this.state.height? Because TypeScript says no.
|
||||
this.state = Object.assign(this.state, {height: this.calculateInitialHeight()});
|
||||
this.dispatcherRef = defaultDispatcher.register(this.onAction);
|
||||
RoomListStore.instance.on(LISTS_UPDATE_EVENT, this.onListsUpdated);
|
||||
}
|
||||
|
||||
private calculateInitialHeight() {
|
||||
|
@ -141,12 +148,22 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
|||
return padding;
|
||||
}
|
||||
|
||||
private get numTiles(): number {
|
||||
return RoomSublist.calcNumTiles(this.props);
|
||||
private get extraTiles(): TemporaryTile[] | null {
|
||||
if (this.state.filteredExtraTiles) {
|
||||
return this.state.filteredExtraTiles;
|
||||
}
|
||||
if (this.props.extraBadTilesThatShouldntExist) {
|
||||
return this.props.extraBadTilesThatShouldntExist;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static calcNumTiles(props) {
|
||||
return (props.rooms || []).length + (props.extraBadTilesThatShouldntExist || []).length;
|
||||
private get numTiles(): number {
|
||||
return RoomSublist.calcNumTiles(this.state.rooms, this.extraTiles);
|
||||
}
|
||||
|
||||
private static calcNumTiles(rooms: Room[], extraTiles: any[]) {
|
||||
return (rooms || []).length + (extraTiles || []).length;
|
||||
}
|
||||
|
||||
private get numVisibleTiles(): number {
|
||||
|
@ -154,33 +171,116 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
|||
return Math.min(nVisible, this.numTiles);
|
||||
}
|
||||
|
||||
public componentDidUpdate(prevProps: Readonly<IProps>) {
|
||||
this.state.notificationState.setRooms(this.props.rooms);
|
||||
if (prevProps.isFiltered !== this.props.isFiltered) {
|
||||
if (this.props.isFiltered) {
|
||||
this.setState({isExpanded: true});
|
||||
} else {
|
||||
this.setState({isExpanded: !this.layout.isCollapsed});
|
||||
}
|
||||
}
|
||||
public componentDidUpdate(prevProps: Readonly<IProps>, prevState: Readonly<IState>) {
|
||||
const prevExtraTiles = prevState.filteredExtraTiles || prevProps.extraBadTilesThatShouldntExist;
|
||||
// as the rooms can come in one by one we need to reevaluate
|
||||
// the amount of available rooms to cap the amount of requested visible rooms by the layout
|
||||
if (RoomSublist.calcNumTiles(prevProps) !== this.numTiles) {
|
||||
if (RoomSublist.calcNumTiles(prevState.rooms, prevExtraTiles) !== this.numTiles) {
|
||||
this.setState({height: this.calculateInitialHeight()});
|
||||
}
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
this.state.notificationState.destroy();
|
||||
defaultDispatcher.unregister(this.dispatcherRef);
|
||||
public shouldComponentUpdate(nextProps: Readonly<IProps>, nextState: Readonly<IState>): boolean {
|
||||
if (objectHasDiff(this.props, nextProps)) {
|
||||
// Something we don't care to optimize has updated, so update.
|
||||
return true;
|
||||
}
|
||||
|
||||
// Do the same check used on props for state, without the rooms we're going to no-op
|
||||
const prevStateNoRooms = objectExcluding(this.state, ['rooms']);
|
||||
const nextStateNoRooms = objectExcluding(nextState, ['rooms']);
|
||||
if (objectHasDiff(prevStateNoRooms, nextStateNoRooms)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// If we're supposed to handle extra tiles, take the performance hit and re-render all the
|
||||
// time so we don't have to consider them as part of the visible room optimization.
|
||||
const prevExtraTiles = this.props.extraBadTilesThatShouldntExist || [];
|
||||
const nextExtraTiles = (nextState.filteredExtraTiles || nextProps.extraBadTilesThatShouldntExist) || [];
|
||||
if (prevExtraTiles.length > 0 || nextExtraTiles.length > 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// If we're about to update the height of the list, we don't really care about which rooms
|
||||
// are visible or not for no-op purposes, so ensure that the height calculation runs through.
|
||||
if (RoomSublist.calcNumTiles(nextState.rooms, nextExtraTiles) !== this.numTiles) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Before we go analyzing the rooms, we can see if we're collapsed. If we're collapsed, we don't need
|
||||
// to render anything. We do this after the height check though to ensure that the height gets appropriately
|
||||
// calculated for when/if we become uncollapsed.
|
||||
if (!nextState.isExpanded) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Quickly double check we're not about to break something due to the number of rooms changing.
|
||||
if (this.state.rooms.length !== nextState.rooms.length) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Finally, determine if the room update (as presumably that's all that's left) is within
|
||||
// our visible range. If it is, then do a render. If the update is outside our visible range
|
||||
// then we can skip the update.
|
||||
//
|
||||
// We also optimize for order changing here: if the update did happen in our visible range
|
||||
// but doesn't result in the list re-sorting itself then there's no reason for us to update
|
||||
// on our own.
|
||||
const prevSlicedRooms = this.state.rooms.slice(0, this.numVisibleTiles);
|
||||
const nextSlicedRooms = nextState.rooms.slice(0, this.numVisibleTiles);
|
||||
if (arrayHasOrderChange(prevSlicedRooms, nextSlicedRooms)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Finally, nothing happened so no-op the update
|
||||
return false;
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
defaultDispatcher.unregister(this.dispatcherRef);
|
||||
RoomListStore.instance.off(LISTS_UPDATE_EVENT, this.onListsUpdated);
|
||||
}
|
||||
|
||||
private onListsUpdated = () => {
|
||||
const stateUpdates: IState & any = {}; // &any is to avoid a cast on the initializer
|
||||
|
||||
if (this.props.extraBadTilesThatShouldntExist) {
|
||||
const nameCondition = RoomListStore.instance.getFirstNameFilterCondition();
|
||||
if (nameCondition) {
|
||||
stateUpdates.filteredExtraTiles = this.props.extraBadTilesThatShouldntExist
|
||||
.filter(t => nameCondition.matches(t.props.displayName || ""));
|
||||
} else if (this.state.filteredExtraTiles) {
|
||||
stateUpdates.filteredExtraTiles = null;
|
||||
}
|
||||
}
|
||||
|
||||
const currentRooms = this.state.rooms;
|
||||
const newRooms = arrayFastClone(RoomListStore.instance.orderedLists[this.props.tagId] || []);
|
||||
if (arrayHasOrderChange(currentRooms, newRooms)) {
|
||||
stateUpdates.rooms = newRooms;
|
||||
}
|
||||
|
||||
const isStillBeingFiltered = !!RoomListStore.instance.getFirstNameFilterCondition();
|
||||
if (isStillBeingFiltered !== this.isBeingFiltered) {
|
||||
this.isBeingFiltered = isStillBeingFiltered;
|
||||
if (isStillBeingFiltered) {
|
||||
stateUpdates.isExpanded = true;
|
||||
} else {
|
||||
stateUpdates.isExpanded = !this.layout.isCollapsed;
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(stateUpdates).length > 0) {
|
||||
this.setState(stateUpdates);
|
||||
}
|
||||
};
|
||||
|
||||
private onAction = (payload: ActionPayload) => {
|
||||
if (payload.action === "view_room" && payload.show_room_tile && this.props.rooms) {
|
||||
if (payload.action === "view_room" && payload.show_room_tile && this.state.rooms) {
|
||||
// XXX: we have to do this a tick later because we have incorrect intermediate props during a room change
|
||||
// where we lose the room we are changing from temporarily and then it comes back in an update right after.
|
||||
setImmediate(() => {
|
||||
const roomIndex = this.props.rooms.findIndex((r) => r.roomId === payload.room_id);
|
||||
const roomIndex = this.state.rooms.findIndex((r) => r.roomId === payload.room_id);
|
||||
|
||||
if (!this.state.isExpanded && roomIndex > -1) {
|
||||
this.toggleCollapsed();
|
||||
|
@ -302,12 +402,12 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
|||
let room;
|
||||
if (this.props.tagId === DefaultTagID.Invite) {
|
||||
// switch to first room as that'll be the top of the list for the user
|
||||
room = this.props.rooms && this.props.rooms[0];
|
||||
room = this.state.rooms && this.state.rooms[0];
|
||||
} else {
|
||||
// find the first room with a count of the same colour as the badge count
|
||||
room = this.props.rooms.find((r: Room) => {
|
||||
const notifState = this.state.notificationState.getForRoom(r);
|
||||
return notifState.count > 0 && notifState.color === this.state.notificationState.color;
|
||||
room = this.state.rooms.find((r: Room) => {
|
||||
const notifState = this.notificationState.getForRoom(r);
|
||||
return notifState.count > 0 && notifState.color === this.notificationState.color;
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -399,8 +499,8 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
|||
|
||||
const tiles: React.ReactElement[] = [];
|
||||
|
||||
if (this.props.rooms) {
|
||||
const visibleRooms = this.props.rooms.slice(0, this.numVisibleTiles);
|
||||
if (this.state.rooms) {
|
||||
const visibleRooms = this.state.rooms.slice(0, this.numVisibleTiles);
|
||||
for (const room of visibleRooms) {
|
||||
tiles.push(
|
||||
<RoomTile
|
||||
|
@ -414,8 +514,9 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
|||
}
|
||||
}
|
||||
|
||||
if (this.props.extraBadTilesThatShouldntExist) {
|
||||
tiles.push(...this.props.extraBadTilesThatShouldntExist);
|
||||
if (this.extraTiles) {
|
||||
// HACK: We break typing here, but this 'extra tiles' property shouldn't exist.
|
||||
(tiles as any[]).push(...this.extraTiles);
|
||||
}
|
||||
|
||||
// We only have to do this because of the extra tiles. We do it conditionally
|
||||
|
@ -522,7 +623,7 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
|||
const badge = (
|
||||
<NotificationBadge
|
||||
forceCount={true}
|
||||
notification={this.state.notificationState}
|
||||
notification={this.notificationState}
|
||||
onClick={this.onBadgeClick}
|
||||
tabIndex={tabIndex}
|
||||
aria-label={ariaLabel}
|
||||
|
|
|
@ -17,12 +17,13 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, {createRef} from "react";
|
||||
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, { ButtonEvent } from "../../views/elements/AccessibleButton";
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import defaultDispatcher from '../../../dispatcher/dispatcher';
|
||||
import { Key } from "../../../Keyboard";
|
||||
import ActiveRoomObserver from "../../../ActiveRoomObserver";
|
||||
import { _t } from "../../../languageHandler";
|
||||
|
@ -30,49 +31,41 @@ import {
|
|||
ChevronFace,
|
||||
ContextMenu,
|
||||
ContextMenuTooltipButton,
|
||||
MenuItemRadio,
|
||||
MenuItemCheckbox,
|
||||
MenuItem,
|
||||
MenuItemCheckbox,
|
||||
MenuItemRadio,
|
||||
} from "../../structures/ContextMenu";
|
||||
import { DefaultTagID, TagID } from "../../../stores/room-list/models";
|
||||
import { MessagePreviewStore } from "../../../stores/room-list/MessagePreviewStore";
|
||||
import { MessagePreviewStore, ROOM_PREVIEW_CHANGED } from "../../../stores/room-list/MessagePreviewStore";
|
||||
import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar";
|
||||
import {
|
||||
getRoomNotifsState,
|
||||
setRoomNotifsState,
|
||||
ALL_MESSAGES,
|
||||
ALL_MESSAGES_LOUD,
|
||||
MENTIONS_ONLY,
|
||||
MUTE,
|
||||
} from "../../../RoomNotifs";
|
||||
import { ALL_MESSAGES, ALL_MESSAGES_LOUD, MENTIONS_ONLY, MUTE, } from "../../../RoomNotifs";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import NotificationBadge from "./NotificationBadge";
|
||||
import { Volume } from "../../../RoomNotifsTypes";
|
||||
import RoomListStore from "../../../stores/room-list/RoomListStore";
|
||||
import RoomListActions from "../../../actions/RoomListActions";
|
||||
import defaultDispatcher from "../../../dispatcher/dispatcher";
|
||||
import {ActionPayload} from "../../../dispatcher/payloads";
|
||||
import { ActionPayload } from "../../../dispatcher/payloads";
|
||||
import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore";
|
||||
import { NotificationState } from "../../../stores/notifications/NotificationState";
|
||||
import { NOTIFICATION_STATE_UPDATE, NotificationState } from "../../../stores/notifications/NotificationState";
|
||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||
import { EchoChamber } from "../../../stores/local-echo/EchoChamber";
|
||||
import { CachedRoomKey, RoomEchoChamber } from "../../../stores/local-echo/RoomEchoChamber";
|
||||
import { PROPERTY_UPDATED } from "../../../stores/local-echo/GenericEchoChamber";
|
||||
|
||||
interface IProps {
|
||||
room: Room;
|
||||
showMessagePreview: boolean;
|
||||
isMinimized: boolean;
|
||||
tag: TagID;
|
||||
|
||||
// TODO: Incoming call boxes: https://github.com/vector-im/riot-web/issues/14177
|
||||
}
|
||||
|
||||
type PartialDOMRect = Pick<DOMRect, "left" | "bottom">;
|
||||
|
||||
interface IState {
|
||||
hover: boolean;
|
||||
notificationState: NotificationState;
|
||||
selected: boolean;
|
||||
notificationsMenuPosition: PartialDOMRect;
|
||||
generalMenuPosition: PartialDOMRect;
|
||||
messagePreview?: string;
|
||||
}
|
||||
|
||||
const messagePreviewId = (roomId: string) => `mx_RoomTile_messagePreview_${roomId}`;
|
||||
|
@ -111,25 +104,42 @@ const NotifOption: React.FC<INotifOptionProps> = ({active, onClick, iconClassNam
|
|||
);
|
||||
};
|
||||
|
||||
export default class RoomTile extends React.Component<IProps, IState> {
|
||||
export default class RoomTile extends React.PureComponent<IProps, IState> {
|
||||
private dispatcherRef: string;
|
||||
private roomTileRef = createRef<HTMLDivElement>();
|
||||
private notificationState: NotificationState;
|
||||
private roomProps: RoomEchoChamber;
|
||||
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
hover: false,
|
||||
notificationState: RoomNotificationStateStore.instance.getRoomState(this.props.room),
|
||||
selected: ActiveRoomObserver.activeRoomId === this.props.room.roomId,
|
||||
notificationsMenuPosition: null,
|
||||
generalMenuPosition: null,
|
||||
|
||||
// generatePreview() will return nothing if the user has previews disabled
|
||||
messagePreview: this.generatePreview(),
|
||||
};
|
||||
|
||||
ActiveRoomObserver.addListener(this.props.room.roomId, this.onActiveRoomUpdate);
|
||||
this.dispatcherRef = defaultDispatcher.register(this.onAction);
|
||||
MessagePreviewStore.instance.on(ROOM_PREVIEW_CHANGED, this.onRoomPreviewChanged);
|
||||
this.notificationState = RoomNotificationStateStore.instance.getRoomState(this.props.room);
|
||||
this.notificationState.on(NOTIFICATION_STATE_UPDATE, this.onNotificationUpdate);
|
||||
this.roomProps = EchoChamber.forRoom(this.props.room);
|
||||
this.roomProps.on(PROPERTY_UPDATED, this.onRoomPropertyUpdate);
|
||||
}
|
||||
|
||||
private onNotificationUpdate = () => {
|
||||
this.forceUpdate(); // notification state changed - update
|
||||
};
|
||||
|
||||
private onRoomPropertyUpdate = (property: CachedRoomKey) => {
|
||||
if (property === CachedRoomKey.NotificationVolume) this.onNotificationUpdate();
|
||||
// else ignore - not important for this tile
|
||||
};
|
||||
|
||||
private get showContextMenu(): boolean {
|
||||
return !this.props.isMinimized && this.props.tag !== DefaultTagID.Invite;
|
||||
}
|
||||
|
@ -150,6 +160,8 @@ export default class RoomTile extends React.Component<IProps, IState> {
|
|||
ActiveRoomObserver.removeListener(this.props.room.roomId, this.onActiveRoomUpdate);
|
||||
}
|
||||
defaultDispatcher.unregister(this.dispatcherRef);
|
||||
MessagePreviewStore.instance.off(ROOM_PREVIEW_CHANGED, this.onRoomPreviewChanged);
|
||||
this.notificationState.off(NOTIFICATION_STATE_UPDATE, this.onNotificationUpdate);
|
||||
}
|
||||
|
||||
private onAction = (payload: ActionPayload) => {
|
||||
|
@ -160,6 +172,21 @@ export default class RoomTile extends React.Component<IProps, IState> {
|
|||
}
|
||||
};
|
||||
|
||||
private onRoomPreviewChanged = (room: Room) => {
|
||||
if (this.props.room && room.roomId === this.props.room.roomId) {
|
||||
// generatePreview() will return nothing if the user has previews disabled
|
||||
this.setState({messagePreview: this.generatePreview()});
|
||||
}
|
||||
};
|
||||
|
||||
private generatePreview(): string | null {
|
||||
if (!this.showMessagePreview) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return MessagePreviewStore.instance.getPreviewForRoom(this.props.room, this.props.tag);
|
||||
}
|
||||
|
||||
private scrollIntoView = () => {
|
||||
if (!this.roomTileRef.current) return;
|
||||
this.roomTileRef.current.scrollIntoView({
|
||||
|
@ -168,14 +195,6 @@ export default class RoomTile extends React.Component<IProps, IState> {
|
|||
});
|
||||
};
|
||||
|
||||
private onTileMouseEnter = () => {
|
||||
this.setState({hover: true});
|
||||
};
|
||||
|
||||
private onTileMouseLeave = () => {
|
||||
this.setState({hover: false});
|
||||
};
|
||||
|
||||
private onTileClick = (ev: React.KeyboardEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
@ -292,17 +311,9 @@ export default class RoomTile extends React.Component<IProps, IState> {
|
|||
ev.stopPropagation();
|
||||
if (MatrixClientPeg.get().isGuest()) return;
|
||||
|
||||
// get key before we go async and React discards the nativeEvent
|
||||
const key = (ev as React.KeyboardEvent).key;
|
||||
try {
|
||||
// TODO add local echo - https://github.com/vector-im/riot-web/issues/14280
|
||||
await setRoomNotifsState(this.props.room.roomId, newState);
|
||||
} catch (error) {
|
||||
// TODO: some form of error notification to the user to inform them that their state change failed.
|
||||
// See https://github.com/vector-im/riot-web/issues/14281
|
||||
console.error(error);
|
||||
}
|
||||
this.roomProps.notificationVolume = newState;
|
||||
|
||||
const key = (ev as React.KeyboardEvent).key;
|
||||
if (key === Key.ENTER) {
|
||||
// Implements https://www.w3.org/TR/wai-aria-practices/#keyboard-interaction-12
|
||||
this.setState({notificationsMenuPosition: null}); // hide the menu
|
||||
|
@ -320,7 +331,7 @@ export default class RoomTile extends React.Component<IProps, IState> {
|
|||
return null;
|
||||
}
|
||||
|
||||
const state = getRoomNotifsState(this.props.room.roomId);
|
||||
const state = this.roomProps.notificationVolume;
|
||||
|
||||
let contextMenu = null;
|
||||
if (this.state.notificationsMenuPosition) {
|
||||
|
@ -482,7 +493,7 @@ export default class RoomTile extends React.Component<IProps, IState> {
|
|||
badge = (
|
||||
<div className="mx_RoomTile_badgeContainer" aria-hidden="true">
|
||||
<NotificationBadge
|
||||
notification={this.state.notificationState}
|
||||
notification={this.notificationState}
|
||||
forceCount={false}
|
||||
roomId={this.props.room.roomId}
|
||||
/>
|
||||
|
@ -495,24 +506,18 @@ export default class RoomTile extends React.Component<IProps, IState> {
|
|||
name = name.replace(":", ":\u200b"); // add a zero-width space to allow linewrapping after the colon
|
||||
|
||||
let messagePreview = null;
|
||||
if (this.showMessagePreview) {
|
||||
// The preview store heavily caches this info, so should be safe to hammer.
|
||||
const text = MessagePreviewStore.instance.getPreviewForRoom(this.props.room, this.props.tag);
|
||||
|
||||
// Only show the preview if there is one to show.
|
||||
if (text) {
|
||||
messagePreview = (
|
||||
<div className="mx_RoomTile_messagePreview" id={messagePreviewId(this.props.room.roomId)}>
|
||||
{text}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (this.showMessagePreview && this.state.messagePreview) {
|
||||
messagePreview = (
|
||||
<div className="mx_RoomTile_messagePreview" id={messagePreviewId(this.props.room.roomId)}>
|
||||
{this.state.messagePreview}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const nameClasses = classNames({
|
||||
"mx_RoomTile_name": true,
|
||||
"mx_RoomTile_nameWithPreview": !!messagePreview,
|
||||
"mx_RoomTile_nameHasUnreadEvents": this.state.notificationState.isUnread,
|
||||
"mx_RoomTile_nameHasUnreadEvents": this.notificationState.isUnread,
|
||||
});
|
||||
|
||||
let nameContainer = (
|
||||
|
@ -529,15 +534,15 @@ export default class RoomTile extends React.Component<IProps, IState> {
|
|||
// The following labels are written in such a fashion to increase screen reader efficiency (speed).
|
||||
if (this.props.tag === DefaultTagID.Invite) {
|
||||
// append nothing
|
||||
} else if (this.state.notificationState.hasMentions) {
|
||||
} else if (this.notificationState.hasMentions) {
|
||||
ariaLabel += " " + _t("%(count)s unread messages including mentions.", {
|
||||
count: this.state.notificationState.count,
|
||||
count: this.notificationState.count,
|
||||
});
|
||||
} else if (this.state.notificationState.hasUnreadCount) {
|
||||
} else if (this.notificationState.hasUnreadCount) {
|
||||
ariaLabel += " " + _t("%(count)s unread messages.", {
|
||||
count: this.state.notificationState.count,
|
||||
count: this.notificationState.count,
|
||||
});
|
||||
} else if (this.state.notificationState.isUnread) {
|
||||
} else if (this.notificationState.isUnread) {
|
||||
ariaLabel += " " + _t("Unread messages.");
|
||||
}
|
||||
|
||||
|
@ -560,8 +565,6 @@ export default class RoomTile extends React.Component<IProps, IState> {
|
|||
tabIndex={isActive ? 0 : -1}
|
||||
inputRef={ref}
|
||||
className={classes}
|
||||
onMouseEnter={this.onTileMouseEnter}
|
||||
onMouseLeave={this.onTileMouseLeave}
|
||||
onClick={this.onTileClick}
|
||||
onContextMenu={this.onContextMenu}
|
||||
role="treeitem"
|
||||
|
|
|
@ -29,6 +29,7 @@ import SettingsStore from "../../../settings/SettingsStore";
|
|||
import {ContextMenu} from "../../structures/ContextMenu";
|
||||
import {WidgetType} from "../../../widgets/WidgetType";
|
||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||
import {Action} from "../../../dispatcher/actions";
|
||||
|
||||
// This should be below the dialog level (4000), but above the rest of the UI (1000-2000).
|
||||
// We sit in a context menu, so this should be given to the context menu.
|
||||
|
@ -182,7 +183,7 @@ export default class Stickerpicker extends React.Component {
|
|||
case "stickerpicker_close":
|
||||
this.setState({showStickers: false});
|
||||
break;
|
||||
case "after_right_panel_phase_change":
|
||||
case Action.AfterRightPanelPhaseChange:
|
||||
case "show_left_panel":
|
||||
case "hide_left_panel":
|
||||
this.setState({showStickers: false});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue