Merge branch 'develop' into travis/skinning/pt3-easy-comps
This commit is contained in:
commit
e5180a472f
39 changed files with 2446 additions and 960 deletions
|
@ -500,6 +500,9 @@ export default class MessagePanel extends React.Component {
|
|||
|
||||
let prevEvent = null; // the last event we showed
|
||||
|
||||
// Note: the EventTile might still render a "sent/sending receipt" independent of
|
||||
// this information. When not providing read receipt information, the tile is likely
|
||||
// to assume that sent receipts are to be shown more often.
|
||||
this._readReceiptsByEvent = {};
|
||||
if (this.props.showReadReceipts) {
|
||||
this._readReceiptsByEvent = this._getReadReceiptsByShownEvent();
|
||||
|
@ -536,10 +539,17 @@ export default class MessagePanel extends React.Component {
|
|||
const nextEvent = i < this.props.events.length - 1
|
||||
? this.props.events[i + 1]
|
||||
: null;
|
||||
|
||||
// The next event with tile is used to to determine the 'last successful' flag
|
||||
// when rendering the tile. The shouldShowEvent function is pretty quick at what
|
||||
// it does, so this should have no significant cost even when a room is used for
|
||||
// not-chat purposes.
|
||||
const nextTile = this.props.events.slice(i + 1).find(e => this._shouldShowEvent(e));
|
||||
|
||||
// make sure we unpack the array returned by _getTilesForEvent,
|
||||
// otherwise react will auto-generate keys and we will end up
|
||||
// replacing all of the DOM elements every time we paginate.
|
||||
ret.push(...this._getTilesForEvent(prevEvent, mxEv, last, nextEvent));
|
||||
ret.push(...this._getTilesForEvent(prevEvent, mxEv, last, nextEvent, nextTile));
|
||||
prevEvent = mxEv;
|
||||
}
|
||||
|
||||
|
@ -555,7 +565,7 @@ export default class MessagePanel extends React.Component {
|
|||
return ret;
|
||||
}
|
||||
|
||||
_getTilesForEvent(prevEvent, mxEv, last, nextEvent) {
|
||||
_getTilesForEvent(prevEvent, mxEv, last, nextEvent, nextEventWithTile) {
|
||||
const TileErrorBoundary = sdk.getComponent('messages.TileErrorBoundary');
|
||||
const EventTile = sdk.getComponent('rooms.EventTile');
|
||||
const DateSeparator = sdk.getComponent('messages.DateSeparator');
|
||||
|
@ -600,12 +610,23 @@ export default class MessagePanel extends React.Component {
|
|||
let isLastSuccessful = false;
|
||||
const isSentState = s => !s || s === 'sent';
|
||||
const isSent = isSentState(mxEv.getAssociatedStatus());
|
||||
if (!nextEvent && isSent) {
|
||||
const hasNextEvent = nextEvent && this._shouldShowEvent(nextEvent);
|
||||
if (!hasNextEvent && isSent) {
|
||||
isLastSuccessful = true;
|
||||
} else if (nextEvent && isSent && !isSentState(nextEvent.getAssociatedStatus())) {
|
||||
} else if (hasNextEvent && isSent && !isSentState(nextEvent.getAssociatedStatus())) {
|
||||
isLastSuccessful = true;
|
||||
}
|
||||
|
||||
// This is a bit nuanced, but if our next event is hidden but a future event is not
|
||||
// hidden then we're not the last successful.
|
||||
if (
|
||||
nextEventWithTile &&
|
||||
nextEventWithTile !== nextEvent &&
|
||||
isSentState(nextEventWithTile.getAssociatedStatus())
|
||||
) {
|
||||
isLastSuccessful = false;
|
||||
}
|
||||
|
||||
// We only want to consider "last successful" if the event is sent by us, otherwise of course
|
||||
// it's successful: we received it.
|
||||
isLastSuccessful = isLastSuccessful && mxEv.getSender() === MatrixClientPeg.get().getUserId();
|
||||
|
|
|
@ -64,6 +64,7 @@ export interface ISpaceSummaryEvent {
|
|||
state_key: string;
|
||||
content: {
|
||||
order?: string;
|
||||
suggested?: boolean;
|
||||
auto_join?: boolean;
|
||||
via?: string;
|
||||
};
|
||||
|
@ -91,7 +92,7 @@ const SubSpace: React.FC<ISubspaceProps> = ({
|
|||
const name = space.name || space.canonical_alias || space.aliases?.[0] || _t("Unnamed Space");
|
||||
|
||||
const evContent = event?.getContent();
|
||||
const [autoJoin, _setAutoJoin] = useState(evContent?.auto_join);
|
||||
const [suggested, _setSuggested] = useState(evContent?.suggested);
|
||||
const [removed, _setRemoved] = useState(!evContent?.via);
|
||||
|
||||
const cli = MatrixClientPeg.get();
|
||||
|
@ -102,12 +103,12 @@ const SubSpace: React.FC<ISubspaceProps> = ({
|
|||
let actions;
|
||||
if (editing && queueAction) {
|
||||
if (event && cli.getRoom(event.getRoomId())?.currentState.maySendStateEvent(event.getType(), cli.getUserId())) {
|
||||
const setAutoJoin = () => {
|
||||
_setAutoJoin(v => {
|
||||
const setSuggested = () => {
|
||||
_setSuggested(v => {
|
||||
queueAction({
|
||||
event,
|
||||
removed,
|
||||
autoJoin: !v,
|
||||
suggested: !v,
|
||||
});
|
||||
return !v;
|
||||
});
|
||||
|
@ -118,7 +119,7 @@ const SubSpace: React.FC<ISubspaceProps> = ({
|
|||
queueAction({
|
||||
event,
|
||||
removed: !v,
|
||||
autoJoin,
|
||||
suggested,
|
||||
});
|
||||
return !v;
|
||||
});
|
||||
|
@ -131,7 +132,7 @@ const SubSpace: React.FC<ISubspaceProps> = ({
|
|||
} else {
|
||||
actions = <React.Fragment>
|
||||
<FormButton kind="danger" onClick={setRemoved} label={_t("Remove from Space")} />
|
||||
<StyledCheckbox checked={autoJoin} onChange={setAutoJoin} />
|
||||
<StyledCheckbox checked={suggested} onChange={setSuggested} />
|
||||
</React.Fragment>;
|
||||
}
|
||||
} else {
|
||||
|
@ -182,8 +183,8 @@ const SubSpace: React.FC<ISubspaceProps> = ({
|
|||
|
||||
interface IAction {
|
||||
event: MatrixEvent;
|
||||
suggested: boolean;
|
||||
removed: boolean;
|
||||
autoJoin: boolean;
|
||||
}
|
||||
|
||||
interface IRoomTileProps {
|
||||
|
@ -199,7 +200,7 @@ const RoomTile = ({ room, event, editing, queueAction, onPreviewClick, onJoinCli
|
|||
const name = room.name || room.canonical_alias || room.aliases?.[0] || _t("Unnamed Room");
|
||||
|
||||
const evContent = event?.getContent();
|
||||
const [autoJoin, _setAutoJoin] = useState(evContent?.auto_join);
|
||||
const [suggested, _setSuggested] = useState(evContent?.suggested);
|
||||
const [removed, _setRemoved] = useState(!evContent?.via);
|
||||
|
||||
const cli = MatrixClientPeg.get();
|
||||
|
@ -209,12 +210,12 @@ const RoomTile = ({ room, event, editing, queueAction, onPreviewClick, onJoinCli
|
|||
let actions;
|
||||
if (editing && queueAction) {
|
||||
if (event && cli.getRoom(event.getRoomId())?.currentState.maySendStateEvent(event.getType(), cli.getUserId())) {
|
||||
const setAutoJoin = () => {
|
||||
_setAutoJoin(v => {
|
||||
const setSuggested = () => {
|
||||
_setSuggested(v => {
|
||||
queueAction({
|
||||
event,
|
||||
removed,
|
||||
autoJoin: !v,
|
||||
suggested: !v,
|
||||
});
|
||||
return !v;
|
||||
});
|
||||
|
@ -225,7 +226,7 @@ const RoomTile = ({ room, event, editing, queueAction, onPreviewClick, onJoinCli
|
|||
queueAction({
|
||||
event,
|
||||
removed: !v,
|
||||
autoJoin,
|
||||
suggested,
|
||||
});
|
||||
return !v;
|
||||
});
|
||||
|
@ -238,7 +239,7 @@ const RoomTile = ({ room, event, editing, queueAction, onPreviewClick, onJoinCli
|
|||
} else {
|
||||
actions = <React.Fragment>
|
||||
<FormButton kind="danger" onClick={setRemoved} label={_t("Remove from Space")} />
|
||||
<StyledCheckbox checked={autoJoin} onChange={setAutoJoin} />
|
||||
<StyledCheckbox checked={suggested} onChange={setSuggested} />
|
||||
</React.Fragment>;
|
||||
}
|
||||
} else {
|
||||
|
@ -445,10 +446,10 @@ const SpaceRoomDirectory: React.FC<IProps> = ({ space, initialText = "", onFinis
|
|||
|
||||
const onSaveButtonClicked = () => {
|
||||
// TODO setBusy
|
||||
pendingActions.current.forEach(({event, autoJoin, removed}) => {
|
||||
pendingActions.current.forEach(({event, suggested, removed}) => {
|
||||
const content = {
|
||||
...event.getContent(),
|
||||
auto_join: autoJoin,
|
||||
suggested,
|
||||
};
|
||||
|
||||
if (removed) {
|
||||
|
@ -463,7 +464,7 @@ const SpaceRoomDirectory: React.FC<IProps> = ({ space, initialText = "", onFinis
|
|||
if (isEditing) {
|
||||
adminButton = <React.Fragment>
|
||||
<FormButton label={_t("Save changes")} onClick={onSaveButtonClicked} />
|
||||
<span>{ _t("All users join by default") }</span>
|
||||
<span>{ _t("Promoted to users") }</span>
|
||||
</React.Fragment>;
|
||||
} else {
|
||||
adminButton = <FormButton label={_t("Manage rooms")} onClick={onManageButtonClicked} />;
|
||||
|
|
|
@ -557,7 +557,7 @@ export default class SpaceRoomView extends React.PureComponent<IProps, IState> {
|
|||
case Phase.PublicCreateRooms:
|
||||
return <SpaceSetupFirstRooms
|
||||
space={this.props.space}
|
||||
title={_t("What discussions do you want to have?")}
|
||||
title={_t("What are some things you want to discuss?")}
|
||||
description={_t("We'll create rooms for each topic.")}
|
||||
onFinished={() => this.setState({ phase: Phase.PublicShare })}
|
||||
/>;
|
||||
|
|
|
@ -27,7 +27,7 @@ export default class InfoDialog extends React.Component {
|
|||
className: PropTypes.string,
|
||||
title: PropTypes.string,
|
||||
description: PropTypes.node,
|
||||
button: PropTypes.oneOfType(PropTypes.string, PropTypes.bool),
|
||||
button: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
|
||||
onFinished: PropTypes.func,
|
||||
hasCloseButton: PropTypes.bool,
|
||||
onKeyDown: PropTypes.func,
|
||||
|
|
|
@ -49,7 +49,6 @@ import {replaceableComponent} from "../../../utils/replaceableComponent";
|
|||
|
||||
export const KIND_DM = "dm";
|
||||
export const KIND_INVITE = "invite";
|
||||
export const KIND_SPACE_INVITE = "space_invite";
|
||||
export const KIND_CALL_TRANSFER = "call_transfer";
|
||||
|
||||
const INITIAL_ROOMS_SHOWN = 3; // Number of rooms to show at first
|
||||
|
@ -311,7 +310,7 @@ interface IInviteDialogProps {
|
|||
// not provided.
|
||||
kind: string,
|
||||
|
||||
// The room ID this dialog is for. Only required for KIND_INVITE and KIND_SPACE_INVITE.
|
||||
// The room ID this dialog is for. Only required for KIND_INVITE.
|
||||
roomId: string,
|
||||
|
||||
// The call to transfer. Only required for KIND_CALL_TRANSFER.
|
||||
|
@ -351,8 +350,8 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
if ((props.kind === KIND_INVITE || props.kind === KIND_SPACE_INVITE) && !props.roomId) {
|
||||
throw new Error("When using KIND_INVITE or KIND_SPACE_INVITE a roomId is required for an InviteDialog");
|
||||
if ((props.kind === KIND_INVITE) && !props.roomId) {
|
||||
throw new Error("When using KIND_INVITE a roomId is required for an InviteDialog");
|
||||
} else if (props.kind === KIND_CALL_TRANSFER && !props.call) {
|
||||
throw new Error("When using KIND_CALL_TRANSFER a call is required for an InviteDialog");
|
||||
}
|
||||
|
@ -1029,7 +1028,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
sectionSubname = _t("May include members not in %(communityName)s", {communityName});
|
||||
}
|
||||
|
||||
if (this.props.kind === KIND_INVITE || this.props.kind === KIND_SPACE_INVITE) {
|
||||
if (this.props.kind === KIND_INVITE) {
|
||||
sectionName = kind === 'recents' ? _t("Recently Direct Messaged") : _t("Suggestions");
|
||||
}
|
||||
|
||||
|
@ -1250,25 +1249,31 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
}
|
||||
buttonText = _t("Go");
|
||||
goButtonFn = this._startDm;
|
||||
} else if (this.props.kind === KIND_INVITE || this.props.kind === KIND_SPACE_INVITE) {
|
||||
title = this.props.kind === KIND_INVITE ? _t("Invite to this room") : _t("Invite to this space");
|
||||
} else if (this.props.kind === KIND_INVITE) {
|
||||
const room = MatrixClientPeg.get()?.getRoom(this.props.roomId);
|
||||
const isSpace = room?.isSpaceRoom();
|
||||
title = isSpace
|
||||
? _t("Invite to %(spaceName)s", {
|
||||
spaceName: room.name || _t("Unnamed Space"),
|
||||
})
|
||||
: _t("Invite to this room");
|
||||
|
||||
let helpTextUntranslated;
|
||||
if (this.props.kind === KIND_INVITE) {
|
||||
if (isSpace) {
|
||||
if (identityServersEnabled) {
|
||||
helpTextUntranslated = _td("Invite someone using their name, email address, username " +
|
||||
"(like <userId/>) or <a>share this room</a>.");
|
||||
"(like <userId/>) or <a>share this space</a>.");
|
||||
} else {
|
||||
helpTextUntranslated = _td("Invite someone using their name, username " +
|
||||
"(like <userId/>) or <a>share this room</a>.");
|
||||
"(like <userId/>) or <a>share this space</a>.");
|
||||
}
|
||||
} else { // KIND_SPACE_INVITE
|
||||
} else {
|
||||
if (identityServersEnabled) {
|
||||
helpTextUntranslated = _td("Invite someone using their name, email address, username " +
|
||||
"(like <userId/>) or <a>share this space</a>.");
|
||||
"(like <userId/>) or <a>share this room</a>.");
|
||||
} else {
|
||||
helpTextUntranslated = _td("Invite someone using their name, username " +
|
||||
"(like <userId/>) or <a>share this space</a>.");
|
||||
"(like <userId/>) or <a>share this room</a>.");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -40,6 +40,7 @@ import RoomAvatar from "../avatars/RoomAvatar";
|
|||
import {WIDGET_LAYOUT_EVENT_TYPE} from "../../../stores/widgets/WidgetLayoutStore";
|
||||
import {objectHasDiff} from "../../../utils/objects";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import Tooltip from "../elements/Tooltip";
|
||||
|
||||
const eventTileTypes = {
|
||||
'm.room.message': 'messages.MessageEvent',
|
||||
|
@ -569,11 +570,8 @@ export default class EventTile extends React.Component {
|
|||
};
|
||||
|
||||
getReadAvatars() {
|
||||
if (this._shouldShowSentReceipt) {
|
||||
return <span className="mx_EventTile_readAvatars"><span className='mx_EventTile_receiptSent' /></span>;
|
||||
}
|
||||
if (this._shouldShowSendingReceipt) {
|
||||
return <span className="mx_EventTile_readAvatars"><span className='mx_EventTile_receiptSending' /></span>;
|
||||
if (this._shouldShowSentReceipt || this._shouldShowSendingReceipt) {
|
||||
return <SentReceipt messageState={this.props.mxEvent.getAssociatedStatus()} />;
|
||||
}
|
||||
|
||||
// return early if there are no read receipts
|
||||
|
@ -1182,7 +1180,6 @@ class E2ePadlock extends React.Component {
|
|||
render() {
|
||||
let tooltip = null;
|
||||
if (this.state.hover) {
|
||||
const Tooltip = sdk.getComponent("elements.Tooltip");
|
||||
tooltip = <Tooltip className="mx_EventTile_e2eIcon_tooltip" label={this.props.title} dir="auto" />;
|
||||
}
|
||||
|
||||
|
@ -1197,3 +1194,56 @@ class E2ePadlock extends React.Component {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
interface ISentReceiptProps {
|
||||
messageState: string; // TODO: Types for message sending state
|
||||
}
|
||||
|
||||
interface ISentReceiptState {
|
||||
hover: boolean;
|
||||
}
|
||||
|
||||
class SentReceipt extends React.PureComponent<ISentReceiptProps, ISentReceiptState> {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.state = {
|
||||
hover: false,
|
||||
};
|
||||
}
|
||||
|
||||
onHoverStart = () => {
|
||||
this.setState({hover: true});
|
||||
};
|
||||
|
||||
onHoverEnd = () => {
|
||||
this.setState({hover: false});
|
||||
};
|
||||
|
||||
render() {
|
||||
const isSent = !this.props.messageState || this.props.messageState === 'sent';
|
||||
const receiptClasses = classNames({
|
||||
'mx_EventTile_receiptSent': isSent,
|
||||
'mx_EventTile_receiptSending': !isSent,
|
||||
});
|
||||
|
||||
let tooltip = null;
|
||||
if (this.state.hover) {
|
||||
let label = _t("Sending your message...");
|
||||
if (this.props.messageState === 'encrypting') {
|
||||
label = _t("Encrypting your message...");
|
||||
} else if (isSent) {
|
||||
label = _t("Your message was sent");
|
||||
}
|
||||
// The yOffset is somewhat arbitrary - it just brings the tooltip down to be more associated
|
||||
// with the read receipt.
|
||||
tooltip = <Tooltip className="mx_EventTile_readAvatars_receiptTooltip" label={label} yOffset={20} />;
|
||||
}
|
||||
|
||||
return <span className="mx_EventTile_readAvatars">
|
||||
<span className={receiptClasses} onMouseEnter={this.onHoverStart} onMouseLeave={this.onHoverEnd}>
|
||||
{tooltip}
|
||||
</span>
|
||||
</span>;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2020, 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -22,14 +22,13 @@ import {
|
|||
} from "../../../accessibility/RovingTabIndex";
|
||||
import NotificationBadge from "./NotificationBadge";
|
||||
import { NotificationState } from "../../../stores/notifications/NotificationState";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
|
||||
interface IProps {
|
||||
isMinimized: boolean;
|
||||
isSelected: boolean;
|
||||
displayName: string;
|
||||
avatar: React.ReactElement;
|
||||
notificationState: NotificationState;
|
||||
notificationState?: NotificationState;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
|
@ -37,9 +36,7 @@ interface IState {
|
|||
hover: boolean;
|
||||
}
|
||||
|
||||
// TODO: Remove with community invites in the room list: https://github.com/vector-im/element-web/issues/14456
|
||||
@replaceableComponent("views.rooms.TemporaryTile")
|
||||
export default class TemporaryTile extends React.Component<IProps, IState> {
|
||||
export default class ExtraTile extends React.Component<IProps, IState> {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
|
@ -59,18 +56,21 @@ export default class TemporaryTile extends React.Component<IProps, IState> {
|
|||
public render(): React.ReactElement {
|
||||
// XXX: We copy classes because it's easier
|
||||
const classes = classNames({
|
||||
'mx_ExtraTile': true,
|
||||
'mx_RoomTile': true,
|
||||
'mx_TemporaryTile': true,
|
||||
'mx_RoomTile_selected': this.props.isSelected,
|
||||
'mx_RoomTile_minimized': this.props.isMinimized,
|
||||
});
|
||||
|
||||
const badge = (
|
||||
<NotificationBadge
|
||||
notification={this.props.notificationState}
|
||||
forceCount={false}
|
||||
/>
|
||||
);
|
||||
let badge;
|
||||
if (this.props.notificationState) {
|
||||
badge = (
|
||||
<NotificationBadge
|
||||
notification={this.props.notificationState}
|
||||
forceCount={false}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
let name = this.props.displayName;
|
||||
if (typeof name !== 'string') name = '';
|
||||
|
@ -78,7 +78,7 @@ export default class TemporaryTile extends React.Component<IProps, IState> {
|
|||
|
||||
const nameClasses = classNames({
|
||||
"mx_RoomTile_name": true,
|
||||
"mx_RoomTile_nameHasUnreadEvents": this.props.notificationState.isUnread,
|
||||
"mx_RoomTile_nameHasUnreadEvents": this.props.notificationState?.isUnread,
|
||||
});
|
||||
|
||||
let nameContainer = (
|
|
@ -16,9 +16,10 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import * as React from "react";
|
||||
import React, { ReactComponentElement } from "react";
|
||||
import { Dispatcher } from "flux";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import * as fbEmitter from "fbemitter";
|
||||
|
||||
import { _t, _td } from "../../../languageHandler";
|
||||
import { RovingTabIndexProvider } from "../../../accessibility/RovingTabIndex";
|
||||
|
@ -33,7 +34,7 @@ import RoomSublist from "./RoomSublist";
|
|||
import { ActionPayload } from "../../../dispatcher/payloads";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import GroupAvatar from "../avatars/GroupAvatar";
|
||||
import TemporaryTile from "./TemporaryTile";
|
||||
import ExtraTile from "./ExtraTile";
|
||||
import { StaticNotificationState } from "../../../stores/notifications/StaticNotificationState";
|
||||
import { NotificationColor } from "../../../stores/notifications/NotificationColor";
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
|
@ -47,10 +48,12 @@ import { IconizedContextMenuOption, IconizedContextMenuOptionList } from "../con
|
|||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import { CommunityPrototypeStore } from "../../../stores/CommunityPrototypeStore";
|
||||
import CallHandler from "../../../CallHandler";
|
||||
import SpaceStore from "../../../stores/SpaceStore";
|
||||
import SpaceStore, { SUGGESTED_ROOMS } from "../../../stores/SpaceStore";
|
||||
import { showAddExistingRooms, showCreateNewRoom } from "../../../utils/space";
|
||||
import { EventType } from "matrix-js-sdk/src/@types/event";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import RoomAvatar from "../avatars/RoomAvatar";
|
||||
import { ISpaceSummaryRoom } from "../../structures/SpaceRoomDirectory";
|
||||
|
||||
interface IProps {
|
||||
onKeyDown: (ev: React.KeyboardEvent) => void;
|
||||
|
@ -64,6 +67,8 @@ interface IProps {
|
|||
interface IState {
|
||||
sublists: ITagMap;
|
||||
isNameFiltering: boolean;
|
||||
currentRoomId?: string;
|
||||
suggestedRooms: ISpaceSummaryRoom[];
|
||||
}
|
||||
|
||||
const TAG_ORDER: TagID[] = [
|
||||
|
@ -76,6 +81,7 @@ const TAG_ORDER: TagID[] = [
|
|||
|
||||
DefaultTagID.LowPriority,
|
||||
DefaultTagID.ServerNotice,
|
||||
DefaultTagID.Suggested,
|
||||
DefaultTagID.Archived,
|
||||
];
|
||||
const CUSTOM_TAGS_BEFORE_TAG = DefaultTagID.LowPriority;
|
||||
|
@ -243,6 +249,12 @@ const TAG_AESTHETICS: ITagAestheticsMap = {
|
|||
isInvite: false,
|
||||
defaultHidden: true,
|
||||
},
|
||||
|
||||
[DefaultTagID.Suggested]: {
|
||||
sectionLabel: _td("Suggested Rooms"),
|
||||
isInvite: false,
|
||||
defaultHidden: false,
|
||||
},
|
||||
};
|
||||
|
||||
function customTagAesthetics(tagId: TagID): ITagAesthetics {
|
||||
|
@ -262,6 +274,7 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
|
|||
private dispatcherRef;
|
||||
private customTagStoreRef;
|
||||
private tagAesthetics: ITagAestheticsMap;
|
||||
private roomStoreToken: fbEmitter.EventSubscription;
|
||||
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
@ -269,6 +282,7 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
|
|||
this.state = {
|
||||
sublists: {},
|
||||
isNameFiltering: !!RoomListStore.instance.getFirstNameFilterCondition(),
|
||||
suggestedRooms: SpaceStore.instance.suggestedRooms,
|
||||
};
|
||||
|
||||
// shallow-copy from the template as we need to make modifications to it
|
||||
|
@ -276,20 +290,30 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
|
|||
this.updateDmAddRoomAction();
|
||||
|
||||
this.dispatcherRef = defaultDispatcher.register(this.onAction);
|
||||
this.roomStoreToken = RoomViewStore.addListener(this.onRoomViewStoreUpdate);
|
||||
}
|
||||
|
||||
public componentDidMount(): void {
|
||||
SpaceStore.instance.on(SUGGESTED_ROOMS, this.updateSuggestedRooms);
|
||||
RoomListStore.instance.on(LISTS_UPDATE_EVENT, this.updateLists);
|
||||
this.customTagStoreRef = CustomRoomTagStore.addListener(this.updateLists);
|
||||
this.updateLists(); // trigger the first update
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
SpaceStore.instance.off(SUGGESTED_ROOMS, this.updateSuggestedRooms);
|
||||
RoomListStore.instance.off(LISTS_UPDATE_EVENT, this.updateLists);
|
||||
defaultDispatcher.unregister(this.dispatcherRef);
|
||||
if (this.customTagStoreRef) this.customTagStoreRef.remove();
|
||||
if (this.roomStoreToken) this.roomStoreToken.remove();
|
||||
}
|
||||
|
||||
private onRoomViewStoreUpdate = () => {
|
||||
this.setState({
|
||||
currentRoomId: RoomViewStore.getRoomId(),
|
||||
});
|
||||
};
|
||||
|
||||
private updateDmAddRoomAction() {
|
||||
const dmTagAesthetics = objectShallowClone(TAG_AESTHETICS[DefaultTagID.DM]);
|
||||
if (CallHandler.sharedInstance().getSupportsPstnProtocol()) {
|
||||
|
@ -321,7 +345,7 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
|
|||
|
||||
private getRoomDelta = (roomId: string, delta: number, unread = false) => {
|
||||
const lists = RoomListStore.instance.orderedLists;
|
||||
const rooms: Room = [];
|
||||
const rooms: Room[] = [];
|
||||
TAG_ORDER.forEach(t => {
|
||||
let listRooms = lists[t];
|
||||
|
||||
|
@ -342,6 +366,10 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
|
|||
return room;
|
||||
};
|
||||
|
||||
private updateSuggestedRooms = (suggestedRooms: ISpaceSummaryRoom[]) => {
|
||||
this.setState({ suggestedRooms });
|
||||
};
|
||||
|
||||
private updateLists = () => {
|
||||
const newLists = RoomListStore.instance.orderedLists;
|
||||
if (SettingsStore.getValue("advancedRoomListLogging")) {
|
||||
|
@ -396,7 +424,44 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
|
|||
dis.dispatch({ action: Action.ViewRoomDirectory, initialText });
|
||||
};
|
||||
|
||||
private renderCommunityInvites(): TemporaryTile[] {
|
||||
private renderSuggestedRooms(): ReactComponentElement<typeof ExtraTile>[] {
|
||||
return this.state.suggestedRooms.map(room => {
|
||||
const name = room.name || room.canonical_alias || room.aliases.pop() || _t("Empty room");
|
||||
const avatar = (
|
||||
<RoomAvatar
|
||||
oobData={{
|
||||
name,
|
||||
avatarUrl: room.avatar_url,
|
||||
}}
|
||||
width={32}
|
||||
height={32}
|
||||
resizeMethod="crop"
|
||||
/>
|
||||
);
|
||||
const viewRoom = () => {
|
||||
defaultDispatcher.dispatch({
|
||||
action: "view_room",
|
||||
room_id: room.room_id,
|
||||
oobData: {
|
||||
avatarUrl: room.avatar_url,
|
||||
name,
|
||||
},
|
||||
});
|
||||
};
|
||||
return (
|
||||
<ExtraTile
|
||||
isMinimized={this.props.isMinimized}
|
||||
isSelected={this.state.currentRoomId === room.room_id}
|
||||
displayName={name}
|
||||
avatar={avatar}
|
||||
onClick={viewRoom}
|
||||
key={`suggestedRoomTile_${room.room_id}`}
|
||||
/>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
private renderCommunityInvites(): ReactComponentElement<typeof ExtraTile>[] {
|
||||
// TODO: Put community invites in a more sensible place (not in the room list)
|
||||
// See https://github.com/vector-im/element-web/issues/14456
|
||||
return MatrixClientPeg.get().getGroups().filter(g => {
|
||||
|
@ -417,7 +482,7 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
|
|||
});
|
||||
};
|
||||
return (
|
||||
<TemporaryTile
|
||||
<ExtraTile
|
||||
isMinimized={this.props.isMinimized}
|
||||
isSelected={false}
|
||||
displayName={g.name}
|
||||
|
@ -449,7 +514,14 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
|
|||
|
||||
for (const orderedTagId of tagOrder) {
|
||||
const orderedRooms = this.state.sublists[orderedTagId] || [];
|
||||
const extraTiles = orderedTagId === DefaultTagID.Invite ? this.renderCommunityInvites() : null;
|
||||
|
||||
let extraTiles = null;
|
||||
if (orderedTagId === DefaultTagID.Invite) {
|
||||
extraTiles = this.renderCommunityInvites();
|
||||
} else if (orderedTagId === DefaultTagID.Suggested) {
|
||||
extraTiles = this.renderSuggestedRooms();
|
||||
}
|
||||
|
||||
const totalTiles = orderedRooms.length + (extraTiles ? extraTiles.length : 0);
|
||||
if (totalTiles === 0 && !ALWAYS_VISIBLE_TAGS.includes(orderedTagId)) {
|
||||
continue; // skip tag - not needed
|
||||
|
@ -472,7 +544,7 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
|
|||
isMinimized={this.props.isMinimized}
|
||||
onResize={this.props.onResize}
|
||||
showSkeleton={showSkeleton}
|
||||
extraBadTilesThatShouldntExist={extraTiles}
|
||||
extraTiles={extraTiles}
|
||||
/>);
|
||||
}
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import * as React from "react";
|
||||
import {createRef} from "react";
|
||||
import { createRef, ReactComponentElement } from "react";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import classNames from 'classnames';
|
||||
import { RovingAccessibleButton, RovingTabIndexWrapper } from "../../../accessibility/RovingTabIndex";
|
||||
|
@ -48,7 +48,7 @@ import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNo
|
|||
import RoomListLayoutStore from "../../../stores/room-list/RoomListLayoutStore";
|
||||
import { arrayFastClone, arrayHasOrderChange } from "../../../utils/arrays";
|
||||
import { objectExcluding, objectHasDiff } from "../../../utils/objects";
|
||||
import TemporaryTile from "./TemporaryTile";
|
||||
import ExtraTile from "./ExtraTile";
|
||||
import { ListNotificationState } from "../../../stores/notifications/ListNotificationState";
|
||||
import IconizedContextMenu from "../context_menus/IconizedContextMenu";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
|
@ -74,9 +74,7 @@ interface IProps {
|
|||
onResize: () => void;
|
||||
showSkeleton?: 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?: TemporaryTile[];
|
||||
extraTiles?: ReactComponentElement<typeof ExtraTile>[];
|
||||
|
||||
// TODO: Account for https://github.com/vector-im/element-web/issues/14179
|
||||
}
|
||||
|
@ -96,7 +94,7 @@ interface IState {
|
|||
isExpanded: boolean; // used for the for expand of the sublist when the room list is being filtered
|
||||
height: number;
|
||||
rooms: Room[];
|
||||
filteredExtraTiles?: TemporaryTile[];
|
||||
filteredExtraTiles?: ReactComponentElement<typeof ExtraTile>[];
|
||||
}
|
||||
|
||||
@replaceableComponent("views.rooms.RoomSublist")
|
||||
|
@ -155,12 +153,12 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
|||
return padding;
|
||||
}
|
||||
|
||||
private get extraTiles(): TemporaryTile[] | null {
|
||||
private get extraTiles(): ReactComponentElement<typeof ExtraTile>[] | null {
|
||||
if (this.state.filteredExtraTiles) {
|
||||
return this.state.filteredExtraTiles;
|
||||
}
|
||||
if (this.props.extraBadTilesThatShouldntExist) {
|
||||
return this.props.extraBadTilesThatShouldntExist;
|
||||
if (this.props.extraTiles) {
|
||||
return this.props.extraTiles;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
@ -179,7 +177,7 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
public componentDidUpdate(prevProps: Readonly<IProps>, prevState: Readonly<IState>) {
|
||||
const prevExtraTiles = prevState.filteredExtraTiles || prevProps.extraBadTilesThatShouldntExist;
|
||||
const prevExtraTiles = prevState.filteredExtraTiles || prevProps.extraTiles;
|
||||
// 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(prevState.rooms, prevExtraTiles) !== this.numTiles) {
|
||||
|
@ -202,8 +200,8 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
|||
|
||||
// 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) || [];
|
||||
const prevExtraTiles = this.props.extraTiles || [];
|
||||
const nextExtraTiles = (nextState.filteredExtraTiles || nextProps.extraTiles) || [];
|
||||
if (prevExtraTiles.length > 0 || nextExtraTiles.length > 0) {
|
||||
return true;
|
||||
}
|
||||
|
@ -251,10 +249,10 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
|||
private onListsUpdated = () => {
|
||||
const stateUpdates: IState & any = {}; // &any is to avoid a cast on the initializer
|
||||
|
||||
if (this.props.extraBadTilesThatShouldntExist) {
|
||||
if (this.props.extraTiles) {
|
||||
const nameCondition = RoomListStore.instance.getFirstNameFilterCondition();
|
||||
if (nameCondition) {
|
||||
stateUpdates.filteredExtraTiles = this.props.extraBadTilesThatShouldntExist
|
||||
stateUpdates.filteredExtraTiles = this.props.extraTiles
|
||||
.filter(t => nameCondition.matches(t.props.displayName || ""));
|
||||
} else if (this.state.filteredExtraTiles) {
|
||||
stateUpdates.filteredExtraTiles = null;
|
||||
|
|
|
@ -107,7 +107,8 @@ const SpaceCreateMenu = ({ onFinished }) => {
|
|||
if (visibility === null) {
|
||||
body = <React.Fragment>
|
||||
<h2>{ _t("Create a space") }</h2>
|
||||
<p>{ _t("Organise rooms into spaces, for just you or anyone") }</p>
|
||||
<p>{ _t("Spaces are new ways to group rooms and people. " +
|
||||
"To join an existing space you’ll need an invite") }</p>
|
||||
|
||||
<SpaceCreateMenuType
|
||||
title={_t("Public")}
|
||||
|
@ -117,12 +118,12 @@ const SpaceCreateMenu = ({ onFinished }) => {
|
|||
/>
|
||||
<SpaceCreateMenuType
|
||||
title={_t("Private")}
|
||||
description={_t("Invite only space, best for yourself or teams")}
|
||||
description={_t("Invite only, best for yourself or teams")}
|
||||
className="mx_SpaceCreateMenuType_private"
|
||||
onClick={() => setVisibility(Visibility.Private)}
|
||||
/>
|
||||
|
||||
{/*<p>{ _t("Looking to join an existing space?") }</p>*/}
|
||||
<p>{ _t("You can change this later") }</p>
|
||||
</React.Fragment>;
|
||||
} else {
|
||||
body = <React.Fragment>
|
||||
|
@ -134,9 +135,7 @@ const SpaceCreateMenu = ({ onFinished }) => {
|
|||
|
||||
<h2>
|
||||
{
|
||||
visibility === Visibility.Public
|
||||
? _t("Personalise your public space")
|
||||
: _t("Personalise your private space")
|
||||
visibility === Visibility.Public ? _t("Your public space") : _t("Your private space")
|
||||
}
|
||||
</h2>
|
||||
<p>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue