Merge branch 'develop' of github.com:matrix-org/matrix-react-sdk into joriks/room-list-auto-expand-on-search
This commit is contained in:
commit
9b79de7fe7
63 changed files with 2300 additions and 799 deletions
|
@ -18,7 +18,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React, {useCallback, useContext, useEffect, useMemo, useState} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import * as AvatarLogic from '../../../Avatar';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
|
@ -26,9 +26,25 @@ import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
|||
import {useEventEmitter} from "../../../hooks/useEventEmitter";
|
||||
import {toPx} from "../../../utils/units";
|
||||
|
||||
const useImageUrl = ({url, urls}) => {
|
||||
const [imageUrls, setUrls] = useState([]);
|
||||
const [urlsIndex, setIndex] = useState();
|
||||
interface IProps {
|
||||
name: string; // The name (first initial used as default)
|
||||
idName?: string; // ID for generating hash colours
|
||||
title?: string; // onHover title text
|
||||
url?: string; // highest priority of them all, shortcut to set in urls[0]
|
||||
urls?: string[]; // [highest_priority, ... , lowest_priority]
|
||||
width?: number;
|
||||
height?: number;
|
||||
// XXX: resizeMethod not actually used.
|
||||
resizeMethod?: string;
|
||||
defaultToInitialLetter?: boolean; // true to add default url
|
||||
onClick?: React.MouseEventHandler;
|
||||
inputRef?: React.RefObject<HTMLImageElement & HTMLSpanElement>;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const useImageUrl = ({url, urls}): [string, () => void] => {
|
||||
const [imageUrls, setUrls] = useState<string[]>([]);
|
||||
const [urlsIndex, setIndex] = useState<number>();
|
||||
|
||||
const onError = useCallback(() => {
|
||||
setIndex(i => i + 1); // try the next one
|
||||
|
@ -70,17 +86,17 @@ const useImageUrl = ({url, urls}) => {
|
|||
return [imageUrl, onError];
|
||||
};
|
||||
|
||||
const BaseAvatar = (props) => {
|
||||
const BaseAvatar = (props: IProps) => {
|
||||
const {
|
||||
name,
|
||||
idName,
|
||||
title,
|
||||
url,
|
||||
urls,
|
||||
width=40,
|
||||
height=40,
|
||||
resizeMethod="crop", // eslint-disable-line no-unused-vars
|
||||
defaultToInitialLetter=true,
|
||||
width = 40,
|
||||
height = 40,
|
||||
resizeMethod = "crop", // eslint-disable-line no-unused-vars
|
||||
defaultToInitialLetter = true,
|
||||
onClick,
|
||||
inputRef,
|
||||
...otherProps
|
||||
|
@ -117,7 +133,7 @@ const BaseAvatar = (props) => {
|
|||
aria-hidden="true" />
|
||||
);
|
||||
|
||||
if (onClick != null) {
|
||||
if (onClick !== null) {
|
||||
return (
|
||||
<AccessibleButton
|
||||
{...otherProps}
|
||||
|
@ -132,7 +148,12 @@ const BaseAvatar = (props) => {
|
|||
);
|
||||
} else {
|
||||
return (
|
||||
<span className="mx_BaseAvatar" ref={inputRef} {...otherProps} role="presentation">
|
||||
<span
|
||||
className="mx_BaseAvatar"
|
||||
ref={inputRef}
|
||||
{...otherProps}
|
||||
role="presentation"
|
||||
>
|
||||
{ textNode }
|
||||
{ imgNode }
|
||||
</span>
|
||||
|
@ -140,7 +161,7 @@ const BaseAvatar = (props) => {
|
|||
}
|
||||
}
|
||||
|
||||
if (onClick != null) {
|
||||
if (onClick !== null) {
|
||||
return (
|
||||
<AccessibleButton
|
||||
className="mx_BaseAvatar mx_BaseAvatar_image"
|
||||
|
@ -173,26 +194,5 @@ const BaseAvatar = (props) => {
|
|||
}
|
||||
};
|
||||
|
||||
BaseAvatar.displayName = "BaseAvatar";
|
||||
|
||||
BaseAvatar.propTypes = {
|
||||
name: PropTypes.string.isRequired, // The name (first initial used as default)
|
||||
idName: PropTypes.string, // ID for generating hash colours
|
||||
title: PropTypes.string, // onHover title text
|
||||
url: PropTypes.string, // highest priority of them all, shortcut to set in urls[0]
|
||||
urls: PropTypes.array, // [highest_priority, ... , lowest_priority]
|
||||
width: PropTypes.number,
|
||||
height: PropTypes.number,
|
||||
// XXX resizeMethod not actually used.
|
||||
resizeMethod: PropTypes.string,
|
||||
defaultToInitialLetter: PropTypes.bool, // true to add default url
|
||||
onClick: PropTypes.func,
|
||||
inputRef: PropTypes.oneOfType([
|
||||
// Either a function
|
||||
PropTypes.func,
|
||||
// Or the instance of a DOM native element
|
||||
PropTypes.shape({ current: PropTypes.instanceOf(Element) }),
|
||||
]),
|
||||
};
|
||||
|
||||
export default BaseAvatar;
|
||||
export type BaseAvatarType = React.FC<IProps>;
|
|
@ -21,8 +21,8 @@ import { TagID } from '../../../stores/room-list/models';
|
|||
import RoomAvatar from "./RoomAvatar";
|
||||
import RoomTileIcon from "../rooms/RoomTileIcon";
|
||||
import NotificationBadge from '../rooms/NotificationBadge';
|
||||
import { INotificationState } from "../../../stores/notifications/INotificationState";
|
||||
import { TagSpecificNotificationState } from "../../../stores/notifications/TagSpecificNotificationState";
|
||||
import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore";
|
||||
import { NotificationState } from "../../../stores/notifications/NotificationState";
|
||||
|
||||
interface IProps {
|
||||
room: Room;
|
||||
|
@ -33,7 +33,7 @@ interface IProps {
|
|||
}
|
||||
|
||||
interface IState {
|
||||
notificationState?: INotificationState;
|
||||
notificationState?: NotificationState;
|
||||
}
|
||||
|
||||
export default class DecoratedRoomAvatar extends React.PureComponent<IProps, IState> {
|
||||
|
@ -42,7 +42,7 @@ export default class DecoratedRoomAvatar extends React.PureComponent<IProps, ISt
|
|||
super(props);
|
||||
|
||||
this.state = {
|
||||
notificationState: new TagSpecificNotificationState(this.props.room, this.props.tag),
|
||||
notificationState: RoomNotificationStateStore.instance.getRoomState(this.props.room, this.props.tag),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -15,43 +15,36 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import createReactClass from 'create-react-class';
|
||||
import * as sdk from '../../../index';
|
||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import BaseAvatar from './BaseAvatar';
|
||||
|
||||
export default createReactClass({
|
||||
displayName: 'GroupAvatar',
|
||||
export interface IProps {
|
||||
groupId?: string;
|
||||
groupName?: string;
|
||||
groupAvatarUrl?: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
resizeMethod?: string;
|
||||
onClick?: React.MouseEventHandler;
|
||||
}
|
||||
|
||||
propTypes: {
|
||||
groupId: PropTypes.string,
|
||||
groupName: PropTypes.string,
|
||||
groupAvatarUrl: PropTypes.string,
|
||||
width: PropTypes.number,
|
||||
height: PropTypes.number,
|
||||
resizeMethod: PropTypes.string,
|
||||
onClick: PropTypes.func,
|
||||
},
|
||||
export default class GroupAvatar extends React.Component<IProps> {
|
||||
public static defaultProps = {
|
||||
width: 36,
|
||||
height: 36,
|
||||
resizeMethod: 'crop',
|
||||
};
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
width: 36,
|
||||
height: 36,
|
||||
resizeMethod: 'crop',
|
||||
};
|
||||
},
|
||||
|
||||
getGroupAvatarUrl: function() {
|
||||
getGroupAvatarUrl() {
|
||||
return MatrixClientPeg.get().mxcUrlToHttp(
|
||||
this.props.groupAvatarUrl,
|
||||
this.props.width,
|
||||
this.props.height,
|
||||
this.props.resizeMethod,
|
||||
);
|
||||
},
|
||||
}
|
||||
|
||||
render: function() {
|
||||
const BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
|
||||
render() {
|
||||
// extract the props we use from props so we can pass any others through
|
||||
// should consider adding this as a global rule in js-sdk?
|
||||
/*eslint no-unused-vars: ["error", { "ignoreRestSiblings": true }]*/
|
||||
|
@ -65,5 +58,5 @@ export default createReactClass({
|
|||
{...otherProps}
|
||||
/>
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
|
@ -16,48 +16,50 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import createReactClass from 'create-react-class';
|
||||
import * as sdk from "../../../index";
|
||||
import dis from "../../../dispatcher/dispatcher";
|
||||
import {Action} from "../../../dispatcher/actions";
|
||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
import BaseAvatar from "./BaseAvatar";
|
||||
|
||||
export default createReactClass({
|
||||
displayName: 'MemberAvatar',
|
||||
interface IProps {
|
||||
// TODO: replace with correct type
|
||||
member: any;
|
||||
fallbackUserId: string;
|
||||
width: number;
|
||||
height: number;
|
||||
resizeMethod: string;
|
||||
// The onClick to give the avatar
|
||||
onClick: React.MouseEventHandler;
|
||||
// Whether the onClick of the avatar should be overriden to dispatch `Action.ViewUser`
|
||||
viewUserOnClick: boolean;
|
||||
title: string;
|
||||
}
|
||||
|
||||
propTypes: {
|
||||
member: PropTypes.object,
|
||||
fallbackUserId: PropTypes.string,
|
||||
width: PropTypes.number,
|
||||
height: PropTypes.number,
|
||||
resizeMethod: PropTypes.string,
|
||||
// The onClick to give the avatar
|
||||
onClick: PropTypes.func,
|
||||
// Whether the onClick of the avatar should be overriden to dispatch `Action.ViewUser`
|
||||
viewUserOnClick: PropTypes.bool,
|
||||
title: PropTypes.string,
|
||||
},
|
||||
interface IState {
|
||||
name: string;
|
||||
title: string;
|
||||
imageUrl?: string;
|
||||
}
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
width: 40,
|
||||
height: 40,
|
||||
resizeMethod: 'crop',
|
||||
viewUserOnClick: false,
|
||||
};
|
||||
},
|
||||
export default class MemberAvatar extends React.Component<IProps, IState> {
|
||||
public static defaultProps = {
|
||||
width: 40,
|
||||
height: 40,
|
||||
resizeMethod: 'crop',
|
||||
viewUserOnClick: false,
|
||||
};
|
||||
|
||||
getInitialState: function() {
|
||||
return this._getState(this.props);
|
||||
},
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
||||
UNSAFE_componentWillReceiveProps: function(nextProps) {
|
||||
this.setState(this._getState(nextProps));
|
||||
},
|
||||
this.state = MemberAvatar.getState(props);
|
||||
}
|
||||
|
||||
_getState: function(props) {
|
||||
public static getDerivedStateFromProps(nextProps: IProps): IState {
|
||||
return MemberAvatar.getState(nextProps);
|
||||
}
|
||||
|
||||
private static getState(props: IProps): IState {
|
||||
if (props.member && props.member.name) {
|
||||
return {
|
||||
name: props.member.name,
|
||||
|
@ -79,11 +81,9 @@ export default createReactClass({
|
|||
} else {
|
||||
console.error("MemberAvatar called somehow with null member or fallbackUserId");
|
||||
}
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
|
||||
}
|
||||
|
||||
render() {
|
||||
let {member, fallbackUserId, onClick, viewUserOnClick, ...otherProps} = this.props;
|
||||
const userId = member ? member.userId : fallbackUserId;
|
||||
|
||||
|
@ -100,5 +100,5 @@ export default createReactClass({
|
|||
<BaseAvatar {...otherProps} name={this.state.name} title={this.state.title}
|
||||
idName={userId} url={this.state.imageUrl} onClick={onClick} />
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
28
src/components/views/avatars/PulsedAvatar.tsx
Normal file
28
src/components/views/avatars/PulsedAvatar.tsx
Normal file
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
Copyright 2020 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.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
interface IProps {
|
||||
}
|
||||
|
||||
const PulsedAvatar: React.FC<IProps> = (props) => {
|
||||
return <div className="mx_PulsedAvatar">
|
||||
{props.children}
|
||||
</div>;
|
||||
};
|
||||
|
||||
export default PulsedAvatar;
|
|
@ -13,90 +13,96 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
import React from "react";
|
||||
import PropTypes from 'prop-types';
|
||||
import createReactClass from 'create-react-class';
|
||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
import React from 'react';
|
||||
import Room from 'matrix-js-sdk/src/models/room';
|
||||
import {getHttpUriForMxc} from 'matrix-js-sdk/src/content-repo';
|
||||
|
||||
import BaseAvatar from './BaseAvatar';
|
||||
import ImageView from '../elements/ImageView';
|
||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import Modal from '../../../Modal';
|
||||
import * as sdk from "../../../index";
|
||||
import * as Avatar from '../../../Avatar';
|
||||
import {getHttpUriForMxc} from "matrix-js-sdk/src/content-repo";
|
||||
|
||||
export default createReactClass({
|
||||
displayName: 'RoomAvatar',
|
||||
|
||||
interface IProps {
|
||||
// Room may be left unset here, but if it is,
|
||||
// oobData.avatarUrl should be set (else there
|
||||
// would be nowhere to get the avatar from)
|
||||
propTypes: {
|
||||
room: PropTypes.object,
|
||||
oobData: PropTypes.object,
|
||||
width: PropTypes.number,
|
||||
height: PropTypes.number,
|
||||
resizeMethod: PropTypes.string,
|
||||
viewAvatarOnClick: PropTypes.bool,
|
||||
},
|
||||
room?: Room;
|
||||
// TODO: type when js-sdk has types
|
||||
oobData?: any;
|
||||
width?: number;
|
||||
height?: number;
|
||||
resizeMethod?: string;
|
||||
viewAvatarOnClick?: boolean;
|
||||
}
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
width: 36,
|
||||
height: 36,
|
||||
resizeMethod: 'crop',
|
||||
oobData: {},
|
||||
interface IState {
|
||||
urls: string[];
|
||||
}
|
||||
|
||||
export default class RoomAvatar extends React.Component<IProps, IState> {
|
||||
public static defaultProps = {
|
||||
width: 36,
|
||||
height: 36,
|
||||
resizeMethod: 'crop',
|
||||
oobData: {},
|
||||
};
|
||||
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
urls: RoomAvatar.getImageUrls(this.props),
|
||||
};
|
||||
},
|
||||
}
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
urls: this.getImageUrls(this.props),
|
||||
};
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
public componentDidMount() {
|
||||
MatrixClientPeg.get().on("RoomState.events", this.onRoomStateEvents);
|
||||
},
|
||||
}
|
||||
|
||||
componentWillUnmount: function() {
|
||||
public componentWillUnmount() {
|
||||
const cli = MatrixClientPeg.get();
|
||||
if (cli) {
|
||||
cli.removeListener("RoomState.events", this.onRoomStateEvents);
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
||||
UNSAFE_componentWillReceiveProps: function(newProps) {
|
||||
this.setState({
|
||||
urls: this.getImageUrls(newProps),
|
||||
});
|
||||
},
|
||||
public static getDerivedStateFromProps(nextProps: IProps): IState {
|
||||
return {
|
||||
urls: RoomAvatar.getImageUrls(nextProps),
|
||||
};
|
||||
}
|
||||
|
||||
onRoomStateEvents: function(ev) {
|
||||
// TODO: type when js-sdk has types
|
||||
private onRoomStateEvents = (ev: any) => {
|
||||
if (!this.props.room ||
|
||||
ev.getRoomId() !== this.props.room.roomId ||
|
||||
ev.getType() !== 'm.room.avatar'
|
||||
) return;
|
||||
|
||||
this.setState({
|
||||
urls: this.getImageUrls(this.props),
|
||||
urls: RoomAvatar.getImageUrls(this.props),
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
getImageUrls: function(props) {
|
||||
private static getImageUrls(props: IProps): string[] {
|
||||
return [
|
||||
getHttpUriForMxc(
|
||||
MatrixClientPeg.get().getHomeserverUrl(),
|
||||
// Default props don't play nicely with getDerivedStateFromProps
|
||||
//props.oobData !== undefined ? props.oobData.avatarUrl : {},
|
||||
props.oobData.avatarUrl,
|
||||
Math.floor(props.width * window.devicePixelRatio),
|
||||
Math.floor(props.height * window.devicePixelRatio),
|
||||
props.resizeMethod,
|
||||
), // highest priority
|
||||
this.getRoomAvatarUrl(props),
|
||||
RoomAvatar.getRoomAvatarUrl(props),
|
||||
].filter(function(url) {
|
||||
return (url != null && url != "");
|
||||
return (url !== null && url !== "");
|
||||
});
|
||||
},
|
||||
}
|
||||
|
||||
getRoomAvatarUrl: function(props) {
|
||||
private static getRoomAvatarUrl(props: IProps): string {
|
||||
if (!props.room) return null;
|
||||
|
||||
return Avatar.avatarUrlForRoom(
|
||||
|
@ -105,24 +111,21 @@ export default createReactClass({
|
|||
Math.floor(props.height * window.devicePixelRatio),
|
||||
props.resizeMethod,
|
||||
);
|
||||
},
|
||||
}
|
||||
|
||||
onRoomAvatarClick: function() {
|
||||
private onRoomAvatarClick = () => {
|
||||
const avatarUrl = this.props.room.getAvatarUrl(
|
||||
MatrixClientPeg.get().getHomeserverUrl(),
|
||||
null, null, null, false);
|
||||
const ImageView = sdk.getComponent("elements.ImageView");
|
||||
const params = {
|
||||
src: avatarUrl,
|
||||
name: this.props.room.name,
|
||||
};
|
||||
|
||||
Modal.createDialog(ImageView, params, "mx_Dialog_lightbox");
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
|
||||
};
|
||||
|
||||
public render() {
|
||||
/*eslint no-unused-vars: ["error", { "ignoreRestSiblings": true }]*/
|
||||
const {room, oobData, viewAvatarOnClick, ...otherProps} = this.props;
|
||||
|
||||
|
@ -132,8 +135,8 @@ export default createReactClass({
|
|||
<BaseAvatar {...otherProps} name={roomName}
|
||||
idName={room ? room.roomId : null}
|
||||
urls={this.state.urls}
|
||||
onClick={this.props.viewAvatarOnClick ? this.onRoomAvatarClick : null}
|
||||
disabled={!this.state.urls[0]} />
|
||||
onClick={this.props.viewAvatarOnClick && !this.state.urls[0] ? this.onRoomAvatarClick : null}
|
||||
/>
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
|
@ -64,7 +64,6 @@ export default function AccessibleButton({
|
|||
className,
|
||||
...restProps
|
||||
}: IProps) {
|
||||
|
||||
const newProps: IAccessibleButtonProps = restProps;
|
||||
if (!disabled) {
|
||||
newProps.onClick = onClick;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
@ -23,6 +23,7 @@ import { _t } from '../../../languageHandler';
|
|||
import { formatCommaSeparatedList } from '../../../utils/FormattingUtils';
|
||||
import * as sdk from "../../../index";
|
||||
import {MatrixEvent} from "matrix-js-sdk";
|
||||
import {isValid3pidInvite} from "../../../RoomInvite";
|
||||
|
||||
export default createReactClass({
|
||||
displayName: 'MemberEventListSummary',
|
||||
|
@ -284,6 +285,9 @@ export default createReactClass({
|
|||
_getTransition: function(e) {
|
||||
if (e.mxEvent.getType() === 'm.room.third_party_invite') {
|
||||
// Handle 3pid invites the same as invites so they get bundled together
|
||||
if (!isValid3pidInvite(e.mxEvent)) {
|
||||
return 'invite_withdrawal';
|
||||
}
|
||||
return 'invited';
|
||||
}
|
||||
|
||||
|
|
|
@ -16,13 +16,18 @@ limitations under the License.
|
|||
|
||||
import { _t } from '../../../languageHandler';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import classNames from 'classnames';
|
||||
|
||||
export default (props) => {
|
||||
const className = classNames({
|
||||
'mx_JumpToBottomButton': true,
|
||||
'mx_JumpToBottomButton_highlight': props.highlight,
|
||||
});
|
||||
let badge;
|
||||
if (props.numUnreadMessages) {
|
||||
badge = (<div className="mx_JumpToBottomButton_badge">{props.numUnreadMessages}</div>);
|
||||
}
|
||||
return (<div className="mx_JumpToBottomButton">
|
||||
return (<div className={className}>
|
||||
<AccessibleButton className="mx_JumpToBottomButton_scrollDown"
|
||||
title={_t("Scroll to most recent messages")}
|
||||
onClick={props.onScrollToBottomClick}>
|
||||
|
|
|
@ -22,11 +22,10 @@ import { DefaultTagID, TagID } from "../../../stores/room-list/models";
|
|||
import { readReceiptChangeIsFor } from "../../../utils/read-receipts";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import { XOR } from "../../../@types/common";
|
||||
import { INotificationState, NOTIFICATION_STATE_UPDATE } from "../../../stores/notifications/INotificationState";
|
||||
import { NotificationColor } from "../../../stores/notifications/NotificationColor";
|
||||
import { NOTIFICATION_STATE_UPDATE, NotificationState } from "../../../stores/notifications/NotificationState";
|
||||
|
||||
interface IProps {
|
||||
notification: INotificationState;
|
||||
notification: NotificationState;
|
||||
|
||||
/**
|
||||
* If true, the badge will show a count if at all possible. This is typically
|
||||
|
@ -97,19 +96,17 @@ export default class NotificationBadge extends React.PureComponent<XOR<IProps, I
|
|||
const {notification, forceCount, roomId, onClick, ...props} = this.props;
|
||||
|
||||
// Don't show a badge if we don't need to
|
||||
if (notification.color <= NotificationColor.None) return null;
|
||||
if (notification.isIdle) return null;
|
||||
|
||||
// TODO: Update these booleans for FTUE Notifications: https://github.com/vector-im/riot-web/issues/14261
|
||||
// As of writing, that is "if red, show count always" and "optionally show counts instead of dots".
|
||||
// See git diff for what that boolean state looks like.
|
||||
// XXX: We ignore this.state.showCounts (the setting which controls counts vs dots).
|
||||
const hasNotif = notification.color >= NotificationColor.Red;
|
||||
const hasCount = notification.color >= NotificationColor.Grey;
|
||||
const hasAnySymbol = notification.symbol || notification.count > 0;
|
||||
let isEmptyBadge = !hasAnySymbol || !hasCount;
|
||||
let isEmptyBadge = !hasAnySymbol || !notification.hasUnreadCount;
|
||||
if (forceCount) {
|
||||
isEmptyBadge = false;
|
||||
if (!hasCount) return null; // Can't render a badge
|
||||
if (!notification.hasUnreadCount) return null; // Can't render a badge
|
||||
}
|
||||
|
||||
let symbol = notification.symbol || formatMinimalBadgeCount(notification.count);
|
||||
|
@ -117,8 +114,8 @@ export default class NotificationBadge extends React.PureComponent<XOR<IProps, I
|
|||
|
||||
const classes = classNames({
|
||||
'mx_NotificationBadge': true,
|
||||
'mx_NotificationBadge_visible': isEmptyBadge ? true : hasCount,
|
||||
'mx_NotificationBadge_highlighted': hasNotif,
|
||||
'mx_NotificationBadge_visible': isEmptyBadge ? true : notification.hasUnreadCount,
|
||||
'mx_NotificationBadge_highlighted': notification.hasMentions,
|
||||
'mx_NotificationBadge_dot': isEmptyBadge,
|
||||
'mx_NotificationBadge_2char': symbol.length > 0 && symbol.length < 3,
|
||||
'mx_NotificationBadge_3char': symbol.length > 2,
|
||||
|
|
|
@ -16,7 +16,6 @@ limitations under the License.
|
|||
|
||||
import React from "react";
|
||||
import { BreadcrumbsStore } from "../../../stores/BreadcrumbsStore";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
|
@ -92,9 +91,6 @@ export default class RoomBreadcrumbs2 extends React.PureComponent<IProps, IState
|
|||
};
|
||||
|
||||
public render(): React.ReactElement {
|
||||
// TODO: Decorate crumbs with icons: https://github.com/vector-im/riot-web/issues/14040
|
||||
// TODO: Scrolling: https://github.com/vector-im/riot-web/issues/14040
|
||||
// TODO: Tooltips: https://github.com/vector-im/riot-web/issues/14040
|
||||
const tiles = BreadcrumbsStore.instance.rooms.map((r, i) => {
|
||||
const roomTags = RoomListStore.instance.getTagsForRoom(r);
|
||||
const roomTag = roomTags.includes(DefaultTagID.DM) ? DefaultTagID.DM : roomTags[0];
|
||||
|
|
|
@ -32,15 +32,14 @@ import defaultDispatcher from "../../../dispatcher/dispatcher";
|
|||
import RoomSublist2 from "./RoomSublist2";
|
||||
import { ActionPayload } from "../../../dispatcher/payloads";
|
||||
import { NameFilterCondition } from "../../../stores/room-list/filters/NameFilterCondition";
|
||||
import { ListLayout } from "../../../stores/room-list/ListLayout";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import GroupAvatar from "../avatars/GroupAvatar";
|
||||
import TemporaryTile from "./TemporaryTile";
|
||||
import { StaticNotificationState } from "../../../stores/notifications/StaticNotificationState";
|
||||
import { NotificationColor } from "../../../stores/notifications/NotificationColor";
|
||||
import { TagSpecificNotificationState } from "../../../stores/notifications/TagSpecificNotificationState";
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
import { ViewRoomDeltaPayload } from "../../../dispatcher/payloads/ViewRoomDeltaPayload";
|
||||
import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore";
|
||||
|
||||
// TODO: Remove banner on launch: https://github.com/vector-im/riot-web/issues/14231
|
||||
// TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14231
|
||||
|
@ -66,7 +65,6 @@ interface IProps {
|
|||
|
||||
interface IState {
|
||||
sublists: ITagMap;
|
||||
layouts: Map<TagID, ListLayout>;
|
||||
}
|
||||
|
||||
const TAG_ORDER: TagID[] = [
|
||||
|
@ -151,7 +149,6 @@ export default class RoomList2 extends React.Component<IProps, IState> {
|
|||
|
||||
this.state = {
|
||||
sublists: {},
|
||||
layouts: new Map<TagID, ListLayout>(),
|
||||
};
|
||||
|
||||
this.dispatcherRef = defaultDispatcher.register(this.onAction);
|
||||
|
@ -215,14 +212,11 @@ export default class RoomList2 extends React.Component<IProps, IState> {
|
|||
let listRooms = lists[t];
|
||||
|
||||
if (unread) {
|
||||
// TODO Be smarter and not spin up a bunch of wasted listeners just to kill them 4 lines later
|
||||
// https://github.com/vector-im/riot-web/issues/14035
|
||||
const notificationStates = rooms.map(r => new TagSpecificNotificationState(r, t));
|
||||
// filter to only notification rooms (and our current active room so we can index properly)
|
||||
listRooms = notificationStates.filter(state => {
|
||||
return state.room.roomId === roomId || state.color >= NotificationColor.Bold;
|
||||
listRooms = listRooms.filter(r => {
|
||||
const state = RoomNotificationStateStore.instance.getRoomState(r, t);
|
||||
return state.room.roomId === roomId || state.isUnread;
|
||||
});
|
||||
notificationStates.forEach(state => state.destroy());
|
||||
}
|
||||
|
||||
rooms.push(...listRooms);
|
||||
|
@ -238,12 +232,7 @@ export default class RoomList2 extends React.Component<IProps, IState> {
|
|||
const newLists = RoomListStore.instance.orderedLists;
|
||||
console.log("new lists", newLists);
|
||||
|
||||
const layoutMap = new Map<TagID, ListLayout>();
|
||||
for (const tagId of Object.keys(newLists)) {
|
||||
layoutMap.set(tagId, new ListLayout(tagId));
|
||||
}
|
||||
|
||||
this.setState({sublists: newLists, layouts: layoutMap}, () => {
|
||||
this.setState({sublists: newLists}, () => {
|
||||
this.props.onResize();
|
||||
});
|
||||
};
|
||||
|
@ -312,8 +301,6 @@ export default class RoomList2 extends React.Component<IProps, IState> {
|
|||
label={_t(aesthetics.sectionLabel)}
|
||||
onAddRoom={onAddRoomFn}
|
||||
addRoomLabel={aesthetics.addRoomLabel}
|
||||
isInvite={aesthetics.isInvite}
|
||||
layout={this.state.layouts.get(orderedTagId)}
|
||||
isMinimized={this.props.isMinimized}
|
||||
onResize={this.props.onResize}
|
||||
extraBadTilesThatShouldntExist={extraTiles}
|
||||
|
|
|
@ -24,9 +24,9 @@ import {RovingAccessibleButton, RovingTabIndexWrapper} from "../../../accessibil
|
|||
import { _t } from "../../../languageHandler";
|
||||
import AccessibleButton from "../../views/elements/AccessibleButton";
|
||||
import RoomTile2 from "./RoomTile2";
|
||||
import { ResizableBox, ResizeCallbackData } from "react-resizable";
|
||||
import { ListLayout } from "../../../stores/room-list/ListLayout";
|
||||
import {
|
||||
ChevronFace,
|
||||
ContextMenu,
|
||||
ContextMenuButton,
|
||||
StyledMenuItemCheckbox,
|
||||
|
@ -40,7 +40,13 @@ import NotificationBadge from "./NotificationBadge";
|
|||
import { ListNotificationState } from "../../../stores/notifications/ListNotificationState";
|
||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||
import { Key } from "../../../Keyboard";
|
||||
import StyledCheckbox from "../elements/StyledCheckbox";
|
||||
import defaultDispatcher from "../../../dispatcher/dispatcher";
|
||||
import {ActionPayload} from "../../../dispatcher/payloads";
|
||||
import { Enable, Resizable } from "re-resizable";
|
||||
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";
|
||||
|
||||
// TODO: Remove banner on launch: https://github.com/vector-im/riot-web/issues/14231
|
||||
// TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14231
|
||||
|
@ -55,9 +61,13 @@ import StyledCheckbox from "../elements/StyledCheckbox";
|
|||
|
||||
const SHOW_N_BUTTON_HEIGHT = 32; // As defined by CSS
|
||||
const RESIZE_HANDLE_HEIGHT = 4; // As defined by CSS
|
||||
export const HEADER_HEIGHT = 32; // As defined by CSS
|
||||
|
||||
const MAX_PADDING_HEIGHT = SHOW_N_BUTTON_HEIGHT + RESIZE_HANDLE_HEIGHT;
|
||||
|
||||
// HACK: We really shouldn't have to do this.
|
||||
polyfillTouchEvent();
|
||||
|
||||
interface IProps {
|
||||
forRooms: boolean;
|
||||
rooms?: Room[];
|
||||
|
@ -65,8 +75,6 @@ interface IProps {
|
|||
label: string;
|
||||
onAddRoom?: () => void;
|
||||
addRoomLabel: string;
|
||||
isInvite: boolean;
|
||||
layout?: ListLayout;
|
||||
isMinimized: boolean;
|
||||
tagId: TagID;
|
||||
onResize: () => void;
|
||||
|
@ -89,16 +97,21 @@ interface IState {
|
|||
export default class RoomSublist2 extends React.Component<IProps, IState> {
|
||||
private headerButton = createRef<HTMLDivElement>();
|
||||
private sublistRef = createRef<HTMLDivElement>();
|
||||
private dispatcherRef: string;
|
||||
private layout: ListLayout;
|
||||
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.layout = RoomListLayoutStore.instance.getLayoutFor(this.props.tagId);
|
||||
|
||||
this.state = {
|
||||
notificationState: new ListNotificationState(this.props.isInvite, this.props.tagId),
|
||||
notificationState: RoomNotificationStateStore.instance.getListState(this.props.tagId),
|
||||
contextMenuPosition: null,
|
||||
isResizing: false,
|
||||
};
|
||||
this.state.notificationState.setRooms(this.props.rooms);
|
||||
this.dispatcherRef = defaultDispatcher.register(this.onAction);
|
||||
}
|
||||
|
||||
private get numTiles(): number {
|
||||
|
@ -106,8 +119,7 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
private get numVisibleTiles(): number {
|
||||
if (!this.props.layout) return 0;
|
||||
const nVisible = Math.floor(this.props.layout.visibleTiles);
|
||||
const nVisible = Math.floor(this.layout.visibleTiles);
|
||||
return Math.min(nVisible, this.numTiles);
|
||||
}
|
||||
|
||||
|
@ -117,17 +129,53 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
|||
|
||||
public componentWillUnmount() {
|
||||
this.state.notificationState.destroy();
|
||||
defaultDispatcher.unregister(this.dispatcherRef);
|
||||
}
|
||||
|
||||
private onAction = (payload: ActionPayload) => {
|
||||
if (payload.action === "view_room" && payload.show_room_tile && this.props.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 isCollapsed = this.layout.isCollapsed;
|
||||
const roomIndex = this.props.rooms.findIndex((r) => r.roomId === payload.room_id);
|
||||
|
||||
if (isCollapsed && roomIndex > -1) {
|
||||
this.toggleCollapsed();
|
||||
}
|
||||
// extend the visible section to include the room if it is entirely invisible
|
||||
if (roomIndex >= this.numVisibleTiles) {
|
||||
this.layout.visibleTiles = this.layout.tilesWithPadding(roomIndex + 1, MAX_PADDING_HEIGHT);
|
||||
this.forceUpdate(); // because the layout doesn't trigger a re-render
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
private onAddRoom = (e) => {
|
||||
e.stopPropagation();
|
||||
if (this.props.onAddRoom) this.props.onAddRoom();
|
||||
};
|
||||
|
||||
private onResize = (e: React.MouseEvent, data: ResizeCallbackData) => {
|
||||
const direction = e.movementY < 0 ? -1 : +1;
|
||||
const tileDiff = this.props.layout.pixelsToTiles(Math.abs(e.movementY)) * direction;
|
||||
this.props.layout.setVisibleTilesWithin(tileDiff, this.numTiles);
|
||||
private onResize = (
|
||||
e: MouseEvent | TouchEvent,
|
||||
travelDirection: Direction,
|
||||
refToElement: HTMLDivElement,
|
||||
delta: { width: number, height: number }, // TODO: Use re-resizer's NumberSize when it is exposed as the type
|
||||
) => {
|
||||
// Do some sanity checks, but in reality we shouldn't need these.
|
||||
if (travelDirection !== "bottom") return;
|
||||
if (delta.height === 0) return; // something went wrong, so just ignore it.
|
||||
|
||||
// NOTE: the movement in the MouseEvent (not present on a TouchEvent) is inaccurate
|
||||
// for our purposes. The delta provided by the library is also a change *from when
|
||||
// resizing started*, meaning it is fairly useless for us. This is why we just use
|
||||
// the client height and run with it.
|
||||
|
||||
const heightBefore = this.layout.visibleTiles;
|
||||
const heightInTiles = this.layout.pixelsToTiles(refToElement.clientHeight);
|
||||
this.layout.setVisibleTilesWithin(heightInTiles, this.numTiles);
|
||||
if (heightBefore === this.layout.visibleTiles) return; // no-op
|
||||
this.forceUpdate(); // because the layout doesn't trigger a re-render
|
||||
};
|
||||
|
||||
|
@ -141,13 +189,13 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
|||
|
||||
private onShowAllClick = () => {
|
||||
const numVisibleTiles = this.numVisibleTiles;
|
||||
this.props.layout.visibleTiles = this.props.layout.tilesWithPadding(this.numTiles, MAX_PADDING_HEIGHT);
|
||||
this.layout.visibleTiles = this.layout.tilesWithPadding(this.numTiles, MAX_PADDING_HEIGHT);
|
||||
this.forceUpdate(); // because the layout doesn't trigger a re-render
|
||||
setImmediate(this.focusRoomTile, numVisibleTiles); // focus the tile after the current bottom one
|
||||
};
|
||||
|
||||
private onShowLessClick = () => {
|
||||
this.props.layout.visibleTiles = this.props.layout.defaultVisibleTiles;
|
||||
this.layout.visibleTiles = this.layout.defaultVisibleTiles;
|
||||
this.forceUpdate(); // because the layout doesn't trigger a re-render
|
||||
// focus will flow to the show more button here
|
||||
};
|
||||
|
@ -161,7 +209,7 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
|||
}
|
||||
};
|
||||
|
||||
private onOpenMenuClick = (ev: InputEvent) => {
|
||||
private onOpenMenuClick = (ev: React.MouseEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
const target = ev.target as HTMLButtonElement;
|
||||
|
@ -195,7 +243,7 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
|||
};
|
||||
|
||||
private onMessagePreviewChanged = () => {
|
||||
this.props.layout.showPreviews = !this.props.layout.showPreviews;
|
||||
this.layout.showPreviews = !this.layout.showPreviews;
|
||||
this.forceUpdate(); // because the layout doesn't trigger a re-render
|
||||
};
|
||||
|
||||
|
@ -233,7 +281,11 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
|||
|
||||
const possibleSticky = target.parentElement;
|
||||
const sublist = possibleSticky.parentElement.parentElement;
|
||||
if (possibleSticky.classList.contains('mx_RoomSublist2_headerContainer_sticky')) {
|
||||
const list = sublist.parentElement.parentElement;
|
||||
// the scrollTop is capped at the height of the header in LeftPanel2
|
||||
const isAtTop = list.scrollTop <= HEADER_HEIGHT;
|
||||
const isSticky = possibleSticky.classList.contains('mx_RoomSublist2_headerContainer_sticky');
|
||||
if (isSticky && !isAtTop) {
|
||||
// is sticky - jump to list
|
||||
sublist.scrollIntoView({behavior: 'smooth'});
|
||||
} else {
|
||||
|
@ -243,13 +295,13 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
|||
};
|
||||
|
||||
private toggleCollapsed = () => {
|
||||
this.props.layout.isCollapsed = !this.props.layout.isCollapsed;
|
||||
this.layout.isCollapsed = !this.layout.isCollapsed;
|
||||
this.forceUpdate(); // because the layout doesn't trigger an update
|
||||
setImmediate(() => this.props.onResize()); // needs to happen when the DOM is updated
|
||||
};
|
||||
|
||||
private onHeaderKeyDown = (ev: React.KeyboardEvent) => {
|
||||
const isCollapsed = this.props.layout && this.props.layout.isCollapsed;
|
||||
const isCollapsed = this.layout && this.layout.isCollapsed;
|
||||
switch (ev.key) {
|
||||
case Key.ARROW_LEFT:
|
||||
ev.stopPropagation();
|
||||
|
@ -289,7 +341,7 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
|||
};
|
||||
|
||||
private renderVisibleTiles(): React.ReactElement[] {
|
||||
if (this.props.layout && this.props.layout.isCollapsed) {
|
||||
if (this.layout && this.layout.isCollapsed) {
|
||||
// don't waste time on rendering
|
||||
return [];
|
||||
}
|
||||
|
@ -303,7 +355,7 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
|||
<RoomTile2
|
||||
room={room}
|
||||
key={`room-${room.roomId}`}
|
||||
showMessagePreview={this.props.layout.showPreviews}
|
||||
showMessagePreview={this.layout.showPreviews}
|
||||
isMinimized={this.props.isMinimized}
|
||||
tag={this.props.tagId}
|
||||
/>
|
||||
|
@ -354,7 +406,7 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
|||
<StyledMenuItemCheckbox
|
||||
onClose={this.onCloseMenu}
|
||||
onChange={this.onMessagePreviewChanged}
|
||||
checked={this.props.layout.showPreviews}
|
||||
checked={this.layout.showPreviews}
|
||||
>
|
||||
{_t("Message preview")}
|
||||
</StyledMenuItemCheckbox>
|
||||
|
@ -365,7 +417,7 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
|||
|
||||
contextMenu = (
|
||||
<ContextMenu
|
||||
chevronFace="none"
|
||||
chevronFace={ChevronFace.None}
|
||||
left={this.state.contextMenuPosition.left}
|
||||
top={this.state.contextMenuPosition.top + this.state.contextMenuPosition.height}
|
||||
onFinished={this.onCloseMenu}
|
||||
|
@ -446,7 +498,7 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
|||
|
||||
const collapseClasses = classNames({
|
||||
'mx_RoomSublist2_collapseBtn': true,
|
||||
'mx_RoomSublist2_collapseBtn_collapsed': this.props.layout && this.props.layout.isCollapsed,
|
||||
'mx_RoomSublist2_collapseBtn_collapsed': this.layout && this.layout.isCollapsed,
|
||||
});
|
||||
|
||||
const classes = classNames({
|
||||
|
@ -474,7 +526,7 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
|||
tabIndex={tabIndex}
|
||||
className="mx_RoomSublist2_headerText"
|
||||
role="treeitem"
|
||||
aria-expanded={!this.props.layout || !this.props.layout.isCollapsed}
|
||||
aria-expanded={!this.layout.isCollapsed}
|
||||
aria-level={1}
|
||||
onClick={this.onHeaderClick}
|
||||
onContextMenu={this.onContextMenu}
|
||||
|
@ -508,12 +560,10 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
|||
|
||||
let content = null;
|
||||
if (visibleTiles.length > 0) {
|
||||
const layout = this.props.layout; // to shorten calls
|
||||
const layout = this.layout; // to shorten calls
|
||||
|
||||
const maxTilesFactored = layout.tilesWithResizerBoxFactor(this.numTiles);
|
||||
const showMoreBtnClasses = classNames({
|
||||
'mx_RoomSublist2_showNButton': true,
|
||||
'mx_RoomSublist2_isCutting': this.state.isResizing && layout.visibleTiles < maxTilesFactored,
|
||||
});
|
||||
|
||||
// If we're hiding rooms, show a 'show more' button to the user. This button
|
||||
|
@ -537,7 +587,7 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
|||
{showMoreText}
|
||||
</RovingAccessibleButton>
|
||||
);
|
||||
} else if (this.numTiles <= visibleTiles.length && this.numTiles > this.props.layout.defaultVisibleTiles) {
|
||||
} else if (this.numTiles <= visibleTiles.length && this.numTiles > this.layout.defaultVisibleTiles) {
|
||||
// we have all tiles visible - add a button to show less
|
||||
let showLessText = (
|
||||
<span className='mx_RoomSublist2_showNButtonText'>
|
||||
|
@ -556,9 +606,19 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
// Figure out if we need a handle
|
||||
let handles = ['s'];
|
||||
const handles: Enable = {
|
||||
bottom: true, // the only one we need, but the others must be explicitly false
|
||||
bottomLeft: false,
|
||||
bottomRight: false,
|
||||
left: false,
|
||||
right: false,
|
||||
top: false,
|
||||
topLeft: false,
|
||||
topRight: false,
|
||||
};
|
||||
if (layout.visibleTiles >= this.numTiles && this.numTiles <= layout.minVisibleTiles) {
|
||||
handles = []; // no handles, we're at a minimum
|
||||
// we're at a minimum, don't have a bottom handle
|
||||
handles.bottom = false;
|
||||
}
|
||||
|
||||
// We have to account for padding so we can accommodate a 'show more' button and
|
||||
|
@ -582,22 +642,33 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
|||
const tilesWithoutPadding = Math.min(relativeTiles, layout.visibleTiles);
|
||||
const tilesPx = layout.calculateTilesToPixelsMin(relativeTiles, tilesWithoutPadding, padding);
|
||||
|
||||
// Now that we know our padding constraints, let's find out if we need to chop off the
|
||||
// last rendered visible tile so it doesn't collide with the 'show more' button
|
||||
let visibleUnpaddedTiles = Math.round(layout.visibleTiles - layout.pixelsToTiles(padding));
|
||||
if (visibleUnpaddedTiles === visibleTiles.length - 1) {
|
||||
const placeholder = <div className="mx_RoomSublist2_placeholder" key='placeholder' />;
|
||||
visibleTiles.splice(visibleUnpaddedTiles, 1, placeholder);
|
||||
}
|
||||
|
||||
const dimensions = {
|
||||
height: tilesPx,
|
||||
};
|
||||
content = (
|
||||
<ResizableBox
|
||||
width={-1}
|
||||
height={tilesPx}
|
||||
axis="y"
|
||||
minConstraints={[-1, minTilesPx]}
|
||||
maxConstraints={[-1, maxTilesPx]}
|
||||
resizeHandles={handles}
|
||||
onResize={this.onResize}
|
||||
className="mx_RoomSublist2_resizeBox"
|
||||
<Resizable
|
||||
size={dimensions as any}
|
||||
minHeight={minTilesPx}
|
||||
maxHeight={maxTilesPx}
|
||||
onResizeStart={this.onResizeStart}
|
||||
onResizeStop={this.onResizeStop}
|
||||
onResize={this.onResize}
|
||||
handleWrapperClass="mx_RoomSublist2_resizerHandles"
|
||||
handleClasses={{bottom: "mx_RoomSublist2_resizerHandle"}}
|
||||
className="mx_RoomSublist2_resizeBox"
|
||||
enable={handles}
|
||||
>
|
||||
{visibleTiles}
|
||||
{showNButton}
|
||||
</ResizableBox>
|
||||
</Resizable>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React 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";
|
||||
|
@ -27,6 +27,7 @@ import { Key } from "../../../Keyboard";
|
|||
import ActiveRoomObserver from "../../../ActiveRoomObserver";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import {
|
||||
ChevronFace,
|
||||
ContextMenu,
|
||||
ContextMenuButton,
|
||||
MenuItemRadio,
|
||||
|
@ -45,11 +46,14 @@ import {
|
|||
MUTE,
|
||||
} from "../../../RoomNotifs";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import { TagSpecificNotificationState } from "../../../stores/notifications/TagSpecificNotificationState";
|
||||
import { INotificationState } from "../../../stores/notifications/INotificationState";
|
||||
import NotificationBadge from "./NotificationBadge";
|
||||
import { NotificationColor } from "../../../stores/notifications/NotificationColor";
|
||||
import { Volume } from "../../../RoomNotifsTypes";
|
||||
import RoomListStore from "../../../stores/room-list/RoomListStore2";
|
||||
import RoomListActions from "../../../actions/RoomListActions";
|
||||
import defaultDispatcher from "../../../dispatcher/dispatcher";
|
||||
import {ActionPayload} from "../../../dispatcher/payloads";
|
||||
import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore";
|
||||
import { NotificationState } from "../../../stores/notifications/NotificationState";
|
||||
|
||||
// TODO: Remove banner on launch: https://github.com/vector-im/riot-web/issues/14231
|
||||
// TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14231
|
||||
|
@ -75,7 +79,7 @@ type PartialDOMRect = Pick<DOMRect, "left" | "bottom">;
|
|||
|
||||
interface IState {
|
||||
hover: boolean;
|
||||
notificationState: INotificationState;
|
||||
notificationState: NotificationState;
|
||||
selected: boolean;
|
||||
notificationsMenuPosition: PartialDOMRect;
|
||||
generalMenuPosition: PartialDOMRect;
|
||||
|
@ -87,7 +91,7 @@ const contextMenuBelow = (elementRect: PartialDOMRect) => {
|
|||
// align the context menu's icons with the icon which opened the context menu
|
||||
const left = elementRect.left + window.pageXOffset - 9;
|
||||
const top = elementRect.bottom + window.pageYOffset + 17;
|
||||
const chevronFace = "none";
|
||||
const chevronFace = ChevronFace.None;
|
||||
return {left, top, chevronFace};
|
||||
};
|
||||
|
||||
|
@ -118,18 +122,23 @@ const NotifOption: React.FC<INotifOptionProps> = ({active, onClick, iconClassNam
|
|||
};
|
||||
|
||||
export default class RoomTile2 extends React.Component<IProps, IState> {
|
||||
private dispatcherRef: string;
|
||||
private roomTileRef = createRef<HTMLDivElement>();
|
||||
// TODO: a11y: https://github.com/vector-im/riot-web/issues/14180
|
||||
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
hover: false,
|
||||
notificationState: new TagSpecificNotificationState(this.props.room, this.props.tag),
|
||||
notificationState: RoomNotificationStateStore.instance.getRoomState(this.props.room, this.props.tag),
|
||||
selected: ActiveRoomObserver.activeRoomId === this.props.room.roomId,
|
||||
notificationsMenuPosition: null,
|
||||
generalMenuPosition: null,
|
||||
};
|
||||
|
||||
ActiveRoomObserver.addListener(this.props.room.roomId, this.onActiveRoomUpdate);
|
||||
this.dispatcherRef = defaultDispatcher.register(this.onAction);
|
||||
}
|
||||
|
||||
private get showContextMenu(): boolean {
|
||||
|
@ -140,12 +149,36 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
|
|||
return !this.props.isMinimized && this.props.showMessagePreview;
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
// when we're first rendered (or our sublist is expanded) make sure we are visible if we're active
|
||||
if (this.state.selected) {
|
||||
this.scrollIntoView();
|
||||
}
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
if (this.props.room) {
|
||||
ActiveRoomObserver.removeListener(this.props.room.roomId, this.onActiveRoomUpdate);
|
||||
}
|
||||
defaultDispatcher.unregister(this.dispatcherRef);
|
||||
}
|
||||
|
||||
private onAction = (payload: ActionPayload) => {
|
||||
if (payload.action === "view_room" && payload.room_id === this.props.room.roomId && payload.show_room_tile) {
|
||||
setImmediate(() => {
|
||||
this.scrollIntoView();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
private scrollIntoView = () => {
|
||||
if (!this.roomTileRef.current) return;
|
||||
this.roomTileRef.current.scrollIntoView({
|
||||
block: "nearest",
|
||||
behavior: "auto",
|
||||
});
|
||||
};
|
||||
|
||||
private onTileMouseEnter = () => {
|
||||
this.setState({hover: true});
|
||||
};
|
||||
|
@ -159,7 +192,6 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
|
|||
ev.stopPropagation();
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
// TODO: Support show_room_tile in new room list: https://github.com/vector-im/riot-web/issues/14233
|
||||
show_room_tile: true, // make sure the room is visible in the list
|
||||
room_id: this.props.room.roomId,
|
||||
clear_search: (ev && (ev.key === Key.ENTER || ev.key === Key.SPACE)),
|
||||
|
@ -170,7 +202,7 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
|
|||
this.setState({selected: isActive});
|
||||
};
|
||||
|
||||
private onNotificationsMenuOpenClick = (ev: InputEvent) => {
|
||||
private onNotificationsMenuOpenClick = (ev: React.MouseEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
const target = ev.target as HTMLButtonElement;
|
||||
|
@ -181,7 +213,7 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
|
|||
this.setState({notificationsMenuPosition: null});
|
||||
};
|
||||
|
||||
private onGeneralMenuOpenClick = (ev: InputEvent) => {
|
||||
private onGeneralMenuOpenClick = (ev: React.MouseEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
const target = ev.target as HTMLButtonElement;
|
||||
|
@ -210,8 +242,22 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
|
|||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
// TODO: Support tagging: https://github.com/vector-im/riot-web/issues/14211
|
||||
// TODO: XOR favourites and low priority: https://github.com/vector-im/riot-web/issues/14210
|
||||
if (tagId === DefaultTagID.Favourite) {
|
||||
const roomTags = RoomListStore.instance.getTagsForRoom(this.props.room);
|
||||
const isFavourite = roomTags.includes(DefaultTagID.Favourite);
|
||||
const removeTag = isFavourite ? DefaultTagID.Favourite : DefaultTagID.LowPriority;
|
||||
const addTag = isFavourite ? null : DefaultTagID.Favourite;
|
||||
dis.dispatch(RoomListActions.tagRoom(
|
||||
MatrixClientPeg.get(),
|
||||
this.props.room,
|
||||
removeTag,
|
||||
addTag,
|
||||
undefined,
|
||||
0
|
||||
));
|
||||
} else {
|
||||
console.log(`Unexpected tag ${tagId} applied to ${this.props.room.room_id}`);
|
||||
}
|
||||
|
||||
if ((ev as React.KeyboardEvent).key === Key.ENTER) {
|
||||
// Implements https://www.w3.org/TR/wai-aria-practices/#keyboard-interaction-12
|
||||
|
@ -343,6 +389,13 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
|
|||
|
||||
// TODO: We could do with a proper invite context menu, unlike what showContextMenu suggests
|
||||
|
||||
const roomTags = RoomListStore.instance.getTagsForRoom(this.props.room);
|
||||
|
||||
const isFavorite = roomTags.includes(DefaultTagID.Favourite);
|
||||
const favouriteIconClassName = isFavorite ? "mx_RoomTile2_iconFavorite" : "mx_RoomTile2_iconStar";
|
||||
const favouriteLabelClassName = isFavorite ? "mx_RoomTile2_contextMenu_activeRow" : "";
|
||||
const favouriteLabel = isFavorite ? _t("Favourited") : _t("Favourite");
|
||||
|
||||
let contextMenu = null;
|
||||
if (this.state.generalMenuPosition) {
|
||||
contextMenu = (
|
||||
|
@ -350,12 +403,13 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
|
|||
<div className="mx_IconizedContextMenu mx_IconizedContextMenu_compact mx_RoomTile2_contextMenu">
|
||||
<div className="mx_IconizedContextMenu_optionList">
|
||||
<MenuItemCheckbox
|
||||
className={favouriteLabelClassName}
|
||||
onClick={(e) => this.onTagRoom(e, DefaultTagID.Favourite)}
|
||||
active={false} // TODO: https://github.com/vector-im/riot-web/issues/14283
|
||||
label={_t("Favourite")}
|
||||
active={isFavorite}
|
||||
label={favouriteLabel}
|
||||
>
|
||||
<span className="mx_IconizedContextMenu_icon mx_RoomTile2_iconStar" />
|
||||
<span className="mx_IconizedContextMenu_label">{_t("Favourite")}</span>
|
||||
<span className={classNames("mx_IconizedContextMenu_icon", favouriteIconClassName)} />
|
||||
<span className="mx_IconizedContextMenu_label">{favouriteLabel}</span>
|
||||
</MenuItemCheckbox>
|
||||
<MenuItem onClick={this.onOpenRoomSettings} label={_t("Settings")}>
|
||||
<span className="mx_IconizedContextMenu_icon mx_RoomTile2_iconSettings" />
|
||||
|
@ -437,11 +491,10 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
|
|||
}
|
||||
}
|
||||
|
||||
const notificationColor = this.state.notificationState.color;
|
||||
const nameClasses = classNames({
|
||||
"mx_RoomTile2_name": true,
|
||||
"mx_RoomTile2_nameWithPreview": !!messagePreview,
|
||||
"mx_RoomTile2_nameHasUnreadEvents": notificationColor >= NotificationColor.Bold,
|
||||
"mx_RoomTile2_nameHasUnreadEvents": this.state.notificationState.isUnread,
|
||||
});
|
||||
|
||||
let nameContainer = (
|
||||
|
@ -458,15 +511,15 @@ export default class RoomTile2 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 (notificationColor >= NotificationColor.Red) {
|
||||
} else if (this.state.notificationState.hasMentions) {
|
||||
ariaLabel += " " + _t("%(count)s unread messages including mentions.", {
|
||||
count: this.state.notificationState.count,
|
||||
});
|
||||
} else if (notificationColor >= NotificationColor.Grey) {
|
||||
} else if (this.state.notificationState.hasUnreadCount) {
|
||||
ariaLabel += " " + _t("%(count)s unread messages.", {
|
||||
count: this.state.notificationState.count,
|
||||
});
|
||||
} else if (notificationColor >= NotificationColor.Bold) {
|
||||
} else if (this.state.notificationState.isUnread) {
|
||||
ariaLabel += " " + _t("Unread messages.");
|
||||
}
|
||||
|
||||
|
@ -477,7 +530,7 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
|
|||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<RovingTabIndexWrapper>
|
||||
<RovingTabIndexWrapper inputRef={this.roomTileRef}>
|
||||
{({onFocus, isActive, ref}) =>
|
||||
<AccessibleButton
|
||||
onFocus={onFocus}
|
||||
|
|
|
@ -18,16 +18,15 @@ import React from "react";
|
|||
import classNames from "classnames";
|
||||
import { RovingTabIndexWrapper } from "../../../accessibility/RovingTabIndex";
|
||||
import AccessibleButton from "../../views/elements/AccessibleButton";
|
||||
import { INotificationState } from "../../../stores/notifications/INotificationState";
|
||||
import NotificationBadge from "./NotificationBadge";
|
||||
import { NotificationColor } from "../../../stores/notifications/NotificationColor";
|
||||
import { NotificationState } from "../../../stores/notifications/NotificationState";
|
||||
|
||||
interface IProps {
|
||||
isMinimized: boolean;
|
||||
isSelected: boolean;
|
||||
displayName: string;
|
||||
avatar: React.ReactElement;
|
||||
notificationState: INotificationState;
|
||||
notificationState: NotificationState;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
|
@ -74,7 +73,7 @@ export default class TemporaryTile extends React.Component<IProps, IState> {
|
|||
|
||||
const nameClasses = classNames({
|
||||
"mx_RoomTile2_name": true,
|
||||
"mx_RoomTile2_nameHasUnreadEvents": this.props.notificationState.color >= NotificationColor.Bold,
|
||||
"mx_RoomTile2_nameHasUnreadEvents": this.props.notificationState.isUnread,
|
||||
});
|
||||
|
||||
let nameContainer = (
|
||||
|
|
|
@ -22,6 +22,10 @@ import * as sdk from "../../../../..";
|
|||
import AccessibleButton from "../../../elements/AccessibleButton";
|
||||
import Modal from "../../../../../Modal";
|
||||
import dis from "../../../../../dispatcher/dispatcher";
|
||||
import RoomListStore from "../../../../../stores/room-list/RoomListStore2";
|
||||
import RoomListActions from "../../../../../actions/RoomListActions";
|
||||
import { DefaultTagID } from '../../../../../stores/room-list/models';
|
||||
import LabelledToggleSwitch from '../../../elements/LabelledToggleSwitch';
|
||||
|
||||
export default class AdvancedRoomSettingsTab extends React.Component {
|
||||
static propTypes = {
|
||||
|
@ -29,12 +33,16 @@ export default class AdvancedRoomSettingsTab extends React.Component {
|
|||
closeSettingsFn: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
const room = MatrixClientPeg.get().getRoom(props.roomId);
|
||||
const roomTags = RoomListStore.instance.getTagsForRoom(room);
|
||||
|
||||
this.state = {
|
||||
// This is eventually set to the value of room.getRecommendedVersion()
|
||||
upgradeRecommendation: null,
|
||||
isLowPriorityRoom: roomTags.includes(DefaultTagID.LowPriority),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -86,6 +94,25 @@ export default class AdvancedRoomSettingsTab extends React.Component {
|
|||
this.props.closeSettingsFn();
|
||||
};
|
||||
|
||||
_onToggleLowPriorityTag = (e) => {
|
||||
this.setState({
|
||||
isLowPriorityRoom: !this.state.isLowPriorityRoom,
|
||||
});
|
||||
|
||||
const removeTag = this.state.isLowPriorityRoom ? DefaultTagID.LowPriority : DefaultTagID.Favourite;
|
||||
const addTag = this.state.isLowPriorityRoom ? null : DefaultTagID.LowPriority;
|
||||
const client = MatrixClientPeg.get();
|
||||
|
||||
dis.dispatch(RoomListActions.tagRoom(
|
||||
client,
|
||||
client.getRoom(this.props.roomId),
|
||||
removeTag,
|
||||
addTag,
|
||||
undefined,
|
||||
0,
|
||||
));
|
||||
}
|
||||
|
||||
render() {
|
||||
const client = MatrixClientPeg.get();
|
||||
const room = client.getRoom(this.props.roomId);
|
||||
|
@ -156,6 +183,17 @@ export default class AdvancedRoomSettingsTab extends React.Component {
|
|||
{_t("Open Devtools")}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
<div className='mx_SettingsTab_section mx_SettingsTab_subsectionText'>
|
||||
<span className='mx_SettingsTab_subheading'>{_t('Make this room low priority')}</span>
|
||||
<LabelledToggleSwitch
|
||||
value={this.state.isLowPriorityRoom}
|
||||
onChange={this._onToggleLowPriorityTag}
|
||||
label={_t(
|
||||
"Low priority rooms show up at the bottom of your room list" +
|
||||
" in a dedicated section at the bottom of your room list",
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -402,6 +402,12 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
|
|||
useCheckbox={true}
|
||||
disabled={this.state.useIRCLayout}
|
||||
/>
|
||||
<SettingsFlag
|
||||
name="useIRCLayout"
|
||||
level={SettingLevel.DEVICE}
|
||||
useCheckbox={true}
|
||||
onChange={(checked) => this.setState({useIRCLayout: checked})}
|
||||
/>
|
||||
<SettingsFlag
|
||||
name="useSystemFont"
|
||||
level={SettingLevel.DEVICE}
|
||||
|
@ -440,7 +446,6 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
|
|||
</div>
|
||||
{this.renderThemeSection()}
|
||||
{SettingsStore.isFeatureEnabled("feature_font_scaling") ? this.renderFontSection() : null}
|
||||
{SettingsStore.isFeatureEnabled("feature_irc_ui") ? this.renderLayoutSection() : null}
|
||||
{this.renderAdvancedSection()}
|
||||
</div>
|
||||
);
|
||||
|
|
37
src/components/views/voip/CallContainer.tsx
Normal file
37
src/components/views/voip/CallContainer.tsx
Normal file
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
Copyright 2020 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.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import IncomingCallBox2 from './IncomingCallBox2';
|
||||
import CallPreview from './CallPreview2';
|
||||
import * as VectorConferenceHandler from '../../../VectorConferenceHandler';
|
||||
|
||||
interface IProps {
|
||||
|
||||
}
|
||||
|
||||
interface IState {
|
||||
|
||||
}
|
||||
|
||||
export default class CallContainer extends React.PureComponent<IProps, IState> {
|
||||
public render() {
|
||||
return <div className="mx_CallContainer">
|
||||
<IncomingCallBox2 />
|
||||
<CallPreview ConferenceHandler={VectorConferenceHandler} />
|
||||
</div>;
|
||||
}
|
||||
}
|
129
src/components/views/voip/CallPreview2.tsx
Normal file
129
src/components/views/voip/CallPreview2.tsx
Normal file
|
@ -0,0 +1,129 @@
|
|||
/*
|
||||
Copyright 2017, 2018 New Vector Ltd
|
||||
Copyright 2019, 2020 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.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14231
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import CallView from "./CallView2";
|
||||
import RoomViewStore from '../../../stores/RoomViewStore';
|
||||
import CallHandler from '../../../CallHandler';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import { ActionPayload } from '../../../dispatcher/payloads';
|
||||
import PersistentApp from "../elements/PersistentApp";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
|
||||
interface IProps {
|
||||
// A Conference Handler implementation
|
||||
// Must have a function signature:
|
||||
// getConferenceCallForRoom(roomId: string): MatrixCall
|
||||
ConferenceHandler: any;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
roomId: string;
|
||||
activeCall: any;
|
||||
newRoomListActive: boolean;
|
||||
}
|
||||
|
||||
export default class CallPreview extends React.Component<IProps, IState> {
|
||||
private roomStoreToken: any;
|
||||
private dispatcherRef: string;
|
||||
private settingsWatcherRef: string;
|
||||
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
roomId: RoomViewStore.getRoomId(),
|
||||
activeCall: CallHandler.getAnyActiveCall(),
|
||||
newRoomListActive: SettingsStore.getValue("feature_new_room_list"),
|
||||
};
|
||||
|
||||
this.settingsWatcherRef = SettingsStore.watchSetting("feature_new_room_list", null, (name, roomId, level, valAtLevel, newVal) => this.setState({
|
||||
newRoomListActive: newVal,
|
||||
}));
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
this.roomStoreToken = RoomViewStore.addListener(this.onRoomViewStoreUpdate);
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
if (this.roomStoreToken) {
|
||||
this.roomStoreToken.remove();
|
||||
}
|
||||
dis.unregister(this.dispatcherRef);
|
||||
SettingsStore.unwatchSetting(this.settingsWatcherRef);
|
||||
}
|
||||
|
||||
private onRoomViewStoreUpdate = (payload) => {
|
||||
if (RoomViewStore.getRoomId() === this.state.roomId) return;
|
||||
this.setState({
|
||||
roomId: RoomViewStore.getRoomId(),
|
||||
});
|
||||
};
|
||||
|
||||
private onAction = (payload: ActionPayload) => {
|
||||
switch (payload.action) {
|
||||
// listen for call state changes to prod the render method, which
|
||||
// may hide the global CallView if the call it is tracking is dead
|
||||
case 'call_state':
|
||||
this.setState({
|
||||
activeCall: CallHandler.getAnyActiveCall(),
|
||||
});
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
private onCallViewClick = () => {
|
||||
const call = CallHandler.getAnyActiveCall();
|
||||
if (call) {
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
room_id: call.groupRoomId || call.roomId,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
public render() {
|
||||
if (this.state.newRoomListActive) {
|
||||
const callForRoom = CallHandler.getCallForRoom(this.state.roomId);
|
||||
const showCall = (
|
||||
this.state.activeCall &&
|
||||
this.state.activeCall.call_state === 'connected' &&
|
||||
!callForRoom
|
||||
);
|
||||
|
||||
if (showCall) {
|
||||
return (
|
||||
<CallView
|
||||
className="mx_CallPreview" onClick={this.onCallViewClick}
|
||||
ConferenceHandler={this.props.ConferenceHandler}
|
||||
showHangup={true}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <PersistentApp />;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
200
src/components/views/voip/CallView2.tsx
Normal file
200
src/components/views/voip/CallView2.tsx
Normal file
|
@ -0,0 +1,200 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2019, 2020 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.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14231
|
||||
|
||||
import React, {createRef} from 'react';
|
||||
import Room from 'matrix-js-sdk/src/models/room';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import CallHandler from '../../../CallHandler';
|
||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import VideoView from "./VideoView";
|
||||
import RoomAvatar from "../avatars/RoomAvatar";
|
||||
import PulsedAvatar from '../avatars/PulsedAvatar';
|
||||
|
||||
interface IProps {
|
||||
// js-sdk room object. If set, we will only show calls for the given
|
||||
// room; if not, we will show any active call.
|
||||
room?: Room;
|
||||
|
||||
// A Conference Handler implementation
|
||||
// Must have a function signature:
|
||||
// getConferenceCallForRoom(roomId: string): MatrixCall
|
||||
ConferenceHandler?: any;
|
||||
|
||||
// maxHeight style attribute for the video panel
|
||||
maxVideoHeight?: number;
|
||||
|
||||
// a callback which is called when the user clicks on the video div
|
||||
onClick?: React.MouseEventHandler;
|
||||
|
||||
// a callback which is called when the content in the callview changes
|
||||
// in a way that is likely to cause a resize.
|
||||
onResize?: any;
|
||||
|
||||
// classname applied to view,
|
||||
className?: string;
|
||||
|
||||
// Whether to show the hang up icon:W
|
||||
showHangup?: boolean;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
call: any;
|
||||
}
|
||||
|
||||
export default class CallView extends React.Component<IProps, IState> {
|
||||
private videoref: React.RefObject<any>;
|
||||
private dispatcherRef: string;
|
||||
public call: any;
|
||||
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
// the call this view is displaying (if any)
|
||||
call: null,
|
||||
};
|
||||
|
||||
this.videoref = createRef();
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
this.showCall();
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
dis.unregister(this.dispatcherRef);
|
||||
}
|
||||
|
||||
private onAction = (payload) => {
|
||||
// don't filter out payloads for room IDs other than props.room because
|
||||
// we may be interested in the conf 1:1 room
|
||||
if (payload.action !== 'call_state') {
|
||||
return;
|
||||
}
|
||||
this.showCall();
|
||||
};
|
||||
|
||||
private showCall() {
|
||||
let call;
|
||||
|
||||
if (this.props.room) {
|
||||
const roomId = this.props.room.roomId;
|
||||
call = CallHandler.getCallForRoom(roomId) ||
|
||||
(this.props.ConferenceHandler ?
|
||||
this.props.ConferenceHandler.getConferenceCallForRoom(roomId) :
|
||||
null
|
||||
);
|
||||
|
||||
if (this.call) {
|
||||
this.setState({ call: call });
|
||||
}
|
||||
} else {
|
||||
call = CallHandler.getAnyActiveCall();
|
||||
// Ignore calls if we can't get the room associated with them.
|
||||
// I think the underlying problem is that the js-sdk sends events
|
||||
// for calls before it has made the rooms available in the store,
|
||||
// although this isn't confirmed.
|
||||
if (MatrixClientPeg.get().getRoom(call.roomId) === null) {
|
||||
call = null;
|
||||
}
|
||||
this.setState({ call: call });
|
||||
}
|
||||
|
||||
if (call) {
|
||||
call.setLocalVideoElement(this.getVideoView().getLocalVideoElement());
|
||||
call.setRemoteVideoElement(this.getVideoView().getRemoteVideoElement());
|
||||
// always use a separate element for audio stream playback.
|
||||
// this is to let us move CallView around the DOM without interrupting remote audio
|
||||
// during playback, by having the audio rendered by a top-level <audio/> element.
|
||||
// rather than being rendered by the main remoteVideo <video/> element.
|
||||
call.setRemoteAudioElement(this.getVideoView().getRemoteAudioElement());
|
||||
}
|
||||
if (call && call.type === "video" && call.call_state !== "ended" && call.call_state !== "ringing") {
|
||||
// if this call is a conf call, don't display local video as the
|
||||
// conference will have us in it
|
||||
this.getVideoView().getLocalVideoElement().style.display = (
|
||||
call.confUserId ? "none" : "block"
|
||||
);
|
||||
this.getVideoView().getRemoteVideoElement().style.display = "block";
|
||||
} else {
|
||||
this.getVideoView().getLocalVideoElement().style.display = "none";
|
||||
this.getVideoView().getRemoteVideoElement().style.display = "none";
|
||||
dis.dispatch({action: 'video_fullscreen', fullscreen: false});
|
||||
}
|
||||
|
||||
if (this.props.onResize) {
|
||||
this.props.onResize();
|
||||
}
|
||||
}
|
||||
|
||||
private getVideoView() {
|
||||
return this.videoref.current;
|
||||
}
|
||||
|
||||
public render() {
|
||||
let view: React.ReactNode;
|
||||
if (this.state.call && this.state.call.type === "voice") {
|
||||
const client = MatrixClientPeg.get();
|
||||
const callRoom = client.getRoom(this.state.call.roomId);
|
||||
|
||||
view = <AccessibleButton className="mx_CallView2_voice" onClick={this.props.onClick}>
|
||||
<PulsedAvatar>
|
||||
<RoomAvatar
|
||||
room={callRoom}
|
||||
height={35}
|
||||
width={35}
|
||||
/>
|
||||
</PulsedAvatar>
|
||||
<div>
|
||||
<h1>{callRoom.name}</h1>
|
||||
<p>{ _t("Active call") }</p>
|
||||
</div>
|
||||
</AccessibleButton>;
|
||||
} else {
|
||||
view = <VideoView
|
||||
ref={this.videoref}
|
||||
onClick={this.props.onClick}
|
||||
onResize={this.props.onResize}
|
||||
maxHeight={this.props.maxVideoHeight}
|
||||
/>;
|
||||
}
|
||||
|
||||
let hangup: React.ReactNode;
|
||||
if (this.props.showHangup) {
|
||||
hangup = <div
|
||||
className="mx_CallView2_hangup"
|
||||
onClick={() => {
|
||||
dis.dispatch({
|
||||
action: 'hangup',
|
||||
room_id: this.state.call.roomId,
|
||||
});
|
||||
}}
|
||||
/>;
|
||||
}
|
||||
|
||||
return <div className={this.props.className}>
|
||||
{view}
|
||||
{hangup}
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
141
src/components/views/voip/IncomingCallBox2.tsx
Normal file
141
src/components/views/voip/IncomingCallBox2.tsx
Normal file
|
@ -0,0 +1,141 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2019, 2020 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.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14231
|
||||
|
||||
import React from 'react';
|
||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { ActionPayload } from '../../../dispatcher/payloads';
|
||||
import CallHandler from '../../../CallHandler';
|
||||
import PulsedAvatar from '../avatars/PulsedAvatar';
|
||||
import RoomAvatar from '../avatars/RoomAvatar';
|
||||
import FormButton from '../elements/FormButton';
|
||||
|
||||
interface IProps {
|
||||
}
|
||||
|
||||
interface IState {
|
||||
incomingCall: any;
|
||||
}
|
||||
|
||||
export default class IncomingCallBox2 extends React.Component<IProps, IState> {
|
||||
private dispatcherRef: string;
|
||||
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
this.state = {
|
||||
incomingCall: null,
|
||||
};
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
dis.unregister(this.dispatcherRef);
|
||||
}
|
||||
|
||||
private onAction = (payload: ActionPayload) => {
|
||||
switch (payload.action) {
|
||||
case 'call_state':
|
||||
const call = CallHandler.getCall(payload.room_id);
|
||||
if (call && call.call_state === 'ringing') {
|
||||
this.setState({
|
||||
incomingCall: call,
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
incomingCall: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private onAnswerClick: React.MouseEventHandler = (e) => {
|
||||
e.stopPropagation();
|
||||
dis.dispatch({
|
||||
action: 'answer',
|
||||
room_id: this.state.incomingCall.roomId,
|
||||
});
|
||||
};
|
||||
|
||||
private onRejectClick: React.MouseEventHandler = (e) => {
|
||||
e.stopPropagation();
|
||||
dis.dispatch({
|
||||
action: 'hangup',
|
||||
room_id: this.state.incomingCall.roomId,
|
||||
});
|
||||
};
|
||||
|
||||
public render() {
|
||||
if (!this.state.incomingCall) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let room = null;
|
||||
if (this.state.incomingCall) {
|
||||
room = MatrixClientPeg.get().getRoom(this.state.incomingCall.roomId);
|
||||
}
|
||||
|
||||
const caller = room ? room.name : _t("Unknown caller");
|
||||
|
||||
let incomingCallText = null;
|
||||
if (this.state.incomingCall) {
|
||||
if (this.state.incomingCall.type === "voice") {
|
||||
incomingCallText = _t("Incoming voice call");
|
||||
} else if (this.state.incomingCall.type === "video") {
|
||||
incomingCallText = _t("Incoming video call");
|
||||
} else {
|
||||
incomingCallText = _t("Incoming call");
|
||||
}
|
||||
}
|
||||
|
||||
return <div className="mx_IncomingCallBox2">
|
||||
<div className="mx_IncomingCallBox2_CallerInfo">
|
||||
<PulsedAvatar>
|
||||
<RoomAvatar
|
||||
room={room}
|
||||
height={32}
|
||||
width={32}
|
||||
/>
|
||||
</PulsedAvatar>
|
||||
<div>
|
||||
<h1>{caller}</h1>
|
||||
<p>{incomingCallText}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mx_IncomingCallBox2_buttons">
|
||||
<FormButton
|
||||
className={"mx_IncomingCallBox2_decline"}
|
||||
onClick={this.onRejectClick}
|
||||
kind="danger"
|
||||
label={_t("Decline")}
|
||||
/>
|
||||
<div className="mx_IncomingCallBox2_spacer" />
|
||||
<FormButton
|
||||
className={"mx_IncomingCallBox2_accept"}
|
||||
onClick={this.onAnswerClick}
|
||||
kind="primary"
|
||||
label={_t("Accept")}
|
||||
/>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
Loading…
Add table
Add a link
Reference in a new issue