Merge branch 'develop' into element

This commit is contained in:
Bruno Windels 2020-07-08 18:09:38 +02:00
commit 7390baf451
20 changed files with 999 additions and 171 deletions

View file

@ -51,6 +51,7 @@
@import "./views/avatars/_BaseAvatar.scss";
@import "./views/avatars/_DecoratedRoomAvatar.scss";
@import "./views/avatars/_MemberStatusMessageAvatar.scss";
@import "./views/avatars/_PulsedAvatar.scss";
@import "./views/context_menus/_MessageContextMenu.scss";
@import "./views/context_menus/_RoomTileContextMenu.scss";
@import "./views/context_menus/_StatusMessageContextMenu.scss";
@ -225,6 +226,8 @@
@import "./views/settings/tabs/user/_VoiceUserSettingsTab.scss";
@import "./views/terms/_InlineTermsAgreement.scss";
@import "./views/verification/_VerificationShowSas.scss";
@import "./views/voip/_CallContainer.scss";
@import "./views/voip/_CallView.scss";
@import "./views/voip/_CallView2.scss";
@import "./views/voip/_IncomingCallbox.scss";
@import "./views/voip/_VideoView.scss";

View file

@ -0,0 +1,30 @@
/*
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.
*/
.mx_PulsedAvatar {
@keyframes shadow-pulse {
0% {
box-shadow: 0 0 0 0px rgba($accent-color, 0.2);
}
100% {
box-shadow: 0 0 0 6px rgba($accent-color, 0);
}
}
img {
animation: shadow-pulse 1s infinite;
}
}

View file

@ -233,6 +233,10 @@ limitations under the License.
mask-image: url('$(res)/img/element-icons/roomlist/favorite.svg');
}
.mx_RoomTile2_iconFavorite::before {
mask-image: url('$(res)/img/feather-customised/favourites.svg');
}
.mx_RoomTile2_iconArrowDown::before {
mask-image: url('$(res)/img/element-icons/roomlist/low-priority.svg');
}

View file

@ -0,0 +1,89 @@
/*
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.
*/
.mx_CallContainer {
position: absolute;
right: 20px;
bottom: 72px;
border-radius: 8px;
overflow: hidden;
z-index: 100;
box-shadow: 0px 14px 24px rgba(0, 0, 0, 0.08);
cursor: pointer;
.mx_CallPreview {
.mx_VideoView {
width: 350px;
}
.mx_VideoView_localVideoFeed {
border-radius: 8px;
overflow: hidden;
}
}
.mx_IncomingCallBox2 {
min-width: 250px;
background-color: $primary-bg-color;
padding: 8px;
.mx_IncomingCallBox2_CallerInfo {
display: flex;
direction: row;
img {
margin: 8px;
}
> div {
display: flex;
flex-direction: column;
justify-content: center;
}
h1, p {
margin: 0px;
padding: 0px;
font-size: $font-14px;
line-height: $font-16px;
}
h1 {
font-weight: bold;
}
}
.mx_IncomingCallBox2_buttons {
padding: 8px;
display: flex;
flex-direction: row;
> .mx_IncomingCallBox2_spacer {
width: 8px;
}
> * {
flex-shrink: 0;
flex-grow: 1;
margin-right: 0;
font-size: $font-15px;
line-height: $font-24px;
}
}
}
}

View file

@ -0,0 +1,96 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
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.
*/
// TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14231
.mx_CallView2_voice {
background-color: $accent-color;
color: $accent-fg-color;
cursor: pointer;
padding: 6px;
font-weight: bold;
border-radius: 8px;
min-width: 200px;
display: flex;
align-items: center;
img {
margin: 4px;
margin-right: 10px;
}
> div {
display: flex;
flex-direction: column;
// Hacky vertical align
padding-top: 3px;
}
> div > p,
> div > h1 {
padding: 0;
margin: 0;
font-size: $font-13px;
line-height: $font-15px;
}
> div > p {
font-weight: bold;
}
> * {
flex-grow: 0;
flex-shrink: 0;
}
}
.mx_CallView2_hangup {
position: absolute;
right: 8px;
bottom: 10px;
height: 35px;
width: 35px;
border-radius: 35px;
background-color: $notice-primary-color;
z-index: 101;
cursor: pointer;
&::before {
content: '';
position: absolute;
height: 20px;
width: 20px;
top: 6.5px;
left: 7.5px;
mask: url('$(res)/img/hangup.svg');
mask-size: contain;
background-size: contain;
background-color: $primary-fg-color;
}
}

View file

@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.41411 0.432179C7.59217 -0.144061 8.40783 -0.144059 8.58589 0.43218L10.1715 5.56319H15.3856C15.9721 5.56319 16.224 6.30764 15.7578 6.66373L11.5135 9.90611L13.1185 15.1001C13.2948 15.6705 12.6348 16.1309 12.1604 15.7684L8 12.5902L3.83965 15.7684C3.3652 16.1309 2.70521 15.6705 2.88148 15.1001L4.4865 9.90611L0.242159 6.66373C-0.223967 6.30764 0.0278507 5.56319 0.614427 5.56319H5.82854L7.41411 0.432179Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 533 B

View file

@ -52,6 +52,7 @@ import {
} from "../../toasts/ServerLimitToast";
import { Action } from "../../dispatcher/actions";
import LeftPanel2 from "./LeftPanel2";
import CallContainer from '../views/voip/CallContainer';
import { ViewRoomDeltaPayload } from "../../dispatcher/payloads/ViewRoomDeltaPayload";
// We need to fetch each pinned message individually (if we don't already have it)
@ -696,6 +697,7 @@ class LoggedInView extends React.Component<IProps, IState> {
</div>
</DragDropContext>
</div>
<CallContainer />
</MatrixClientContext.Provider>
);
}

View file

@ -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>;

View file

@ -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}
/>
);
},
});
}
}

View file

@ -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} />
);
},
});
}
}

View 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;

View file

@ -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}
/>
);
},
});
}
}

View file

@ -51,6 +51,8 @@ import { INotificationState } from "../../../stores/notifications/INotificationS
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";
@ -241,8 +243,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
@ -374,6 +390,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 = (
@ -381,12 +404,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" />

View file

@ -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>
);
}

View 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>;
}
}

View 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;
}
}

View 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>;
}
}

View 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>;
}
}

View file

@ -559,12 +559,17 @@
"My Ban List": "My Ban List",
"This is your list of users/servers you have blocked - don't leave the room!": "This is your list of users/servers you have blocked - don't leave the room!",
"Active call (%(roomName)s)": "Active call (%(roomName)s)",
"Active call": "Active call",
"unknown caller": "unknown caller",
"Incoming voice call from %(name)s": "Incoming voice call from %(name)s",
"Incoming video call from %(name)s": "Incoming video call from %(name)s",
"Incoming call from %(name)s": "Incoming call from %(name)s",
"Decline": "Decline",
"Accept": "Accept",
"Unknown caller": "Unknown caller",
"Incoming voice call": "Incoming voice call",
"Incoming video call": "Incoming video call",
"Incoming call": "Incoming call",
"The other party cancelled the verification.": "The other party cancelled the verification.",
"Verified!": "Verified!",
"You've successfully verified this user.": "You've successfully verified this user.",
@ -967,6 +972,8 @@
"Room version:": "Room version:",
"Developer options": "Developer options",
"Open Devtools": "Open Devtools",
"Make this room low priority": "Make this room low priority",
"Low priority rooms show up at the bottom of your room list in a dedicated section at the bottom of your room list": "Low priority rooms show up at the bottom of your room list in a dedicated section at the bottom of your room list",
"This room is bridging messages to the following platforms. <a>Learn more.</a>": "This room is bridging messages to the following platforms. <a>Learn more.</a>",
"This room isnt bridging messages to any platforms. <a>Learn more.</a>": "This room isnt bridging messages to any platforms. <a>Learn more.</a>",
"Bridges": "Bridges",
@ -1225,6 +1232,7 @@
"All messages": "All messages",
"Mentions & Keywords": "Mentions & Keywords",
"Notification options": "Notification options",
"Favourited": "Favourited",
"Favourite": "Favourite",
"Leave Room": "Leave Room",
"Room options": "Room options",
@ -2103,7 +2111,6 @@
"%(count)s <resendText>Resend all</resendText> or <cancelText>cancel all</cancelText> now. You can also select individual messages to resend or cancel.|one": "<resendText>Resend message</resendText> or <cancelText>cancel message</cancelText> now.",
"Connectivity to the server has been lost.": "Connectivity to the server has been lost.",
"Sent messages will be stored until your connection has returned.": "Sent messages will be stored until your connection has returned.",
"Active call": "Active call",
"There's no one else here! Would you like to <inviteText>invite others</inviteText> or <nowarnText>stop warning about the empty room</nowarnText>?": "There's no one else here! Would you like to <inviteText>invite others</inviteText> or <nowarnText>stop warning about the empty room</nowarnText>?",
"You seem to be uploading files, are you sure you want to quit?": "You seem to be uploading files, are you sure you want to quit?",
"You seem to be in a call, are you sure you want to quit?": "You seem to be in a call, are you sure you want to quit?",

View file

@ -205,8 +205,9 @@ describe("<TextualBody />", () => {
expect(content.html()).toBe('<span class="mx_EventTile_body markdown-body" dir="auto">' +
'Hey <span>' +
'<a class="mx_Pill mx_UserPill" title="@user:server">' +
'<img class="mx_BaseAvatar mx_BaseAvatar_image" src="mxc://avatar.url/image.png" ' +
'style="width: 16px; height: 16px;" title="@member:domain.bla" alt="" aria-hidden="true">Member</a>' +
'<img src="mxc://avatar.url/image.png" style="width: 16px; height: 16px;" ' +
'title="@member:domain.bla" alt="" aria-hidden="true" role="button" tabindex="0" ' +
'class="mx_AccessibleButton mx_BaseAvatar mx_BaseAvatar_image">Member</a>' +
'</span></span>');
});
});