diff --git a/res/css/_components.scss b/res/css/_components.scss index 8288cf34f6..85e08110ea 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -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"; diff --git a/res/css/views/avatars/_PulsedAvatar.scss b/res/css/views/avatars/_PulsedAvatar.scss new file mode 100644 index 0000000000..ce9e3382ab --- /dev/null +++ b/res/css/views/avatars/_PulsedAvatar.scss @@ -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; + } +} diff --git a/res/css/views/rooms/_RoomTile2.scss b/res/css/views/rooms/_RoomTile2.scss index 7ec7a8b228..1d010eb243 100644 --- a/res/css/views/rooms/_RoomTile2.scss +++ b/res/css/views/rooms/_RoomTile2.scss @@ -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'); } diff --git a/res/css/views/voip/_CallContainer.scss b/res/css/views/voip/_CallContainer.scss new file mode 100644 index 0000000000..e13c851716 --- /dev/null +++ b/res/css/views/voip/_CallContainer.scss @@ -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; + } + } + } +} diff --git a/res/css/views/voip/_CallView2.scss b/res/css/views/voip/_CallView2.scss new file mode 100644 index 0000000000..3b66e7a175 --- /dev/null +++ b/res/css/views/voip/_CallView2.scss @@ -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; + } +} diff --git a/res/img/feather-customised/favourites.svg b/res/img/feather-customised/favourites.svg new file mode 100644 index 0000000000..80f08f6e55 --- /dev/null +++ b/res/img/feather-customised/favourites.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/components/structures/LoggedInView.tsx b/src/components/structures/LoggedInView.tsx index 6354bb8739..88144c45c5 100644 --- a/src/components/structures/LoggedInView.tsx +++ b/src/components/structures/LoggedInView.tsx @@ -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 { + ); } diff --git a/src/components/views/avatars/BaseAvatar.js b/src/components/views/avatars/BaseAvatar.tsx similarity index 78% rename from src/components/views/avatars/BaseAvatar.js rename to src/components/views/avatars/BaseAvatar.tsx index 53e8d0072b..aa2c0ea954 100644 --- a/src/components/views/avatars/BaseAvatar.js +++ b/src/components/views/avatars/BaseAvatar.tsx @@ -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; + className?: string; +} + +const useImageUrl = ({url, urls}): [string, () => void] => { + const [imageUrls, setUrls] = useState([]); + const [urlsIndex, setIndex] = useState(); 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 ( { ); } else { return ( - + { textNode } { imgNode } @@ -140,7 +161,7 @@ const BaseAvatar = (props) => { } } - if (onClick != null) { + if (onClick !== null) { return ( { } }; -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; \ No newline at end of file diff --git a/src/components/views/avatars/GroupAvatar.js b/src/components/views/avatars/GroupAvatar.tsx similarity index 64% rename from src/components/views/avatars/GroupAvatar.js rename to src/components/views/avatars/GroupAvatar.tsx index 0da57bcb99..e55e2e6fac 100644 --- a/src/components/views/avatars/GroupAvatar.js +++ b/src/components/views/avatars/GroupAvatar.tsx @@ -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 { + 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} /> ); - }, -}); + } +} diff --git a/src/components/views/avatars/MemberAvatar.js b/src/components/views/avatars/MemberAvatar.tsx similarity index 64% rename from src/components/views/avatars/MemberAvatar.js rename to src/components/views/avatars/MemberAvatar.tsx index b763129dd8..1d23d85b0f 100644 --- a/src/components/views/avatars/MemberAvatar.js +++ b/src/components/views/avatars/MemberAvatar.tsx @@ -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 { + 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({ ); - }, -}); + } +} diff --git a/src/components/views/avatars/PulsedAvatar.tsx b/src/components/views/avatars/PulsedAvatar.tsx new file mode 100644 index 0000000000..94a6c87687 --- /dev/null +++ b/src/components/views/avatars/PulsedAvatar.tsx @@ -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 = (props) => { + return
+ {props.children} +
; +}; + +export default PulsedAvatar; \ No newline at end of file diff --git a/src/components/views/avatars/RoomAvatar.js b/src/components/views/avatars/RoomAvatar.tsx similarity index 59% rename from src/components/views/avatars/RoomAvatar.js rename to src/components/views/avatars/RoomAvatar.tsx index a72d318b8d..0947157652 100644 --- a/src/components/views/avatars/RoomAvatar.js +++ b/src/components/views/avatars/RoomAvatar.tsx @@ -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 { + 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({ + onClick={this.props.viewAvatarOnClick && !this.state.urls[0] ? this.onRoomAvatarClick : null} + /> ); - }, -}); + } +} diff --git a/src/components/views/rooms/RoomTile2.tsx b/src/components/views/rooms/RoomTile2.tsx index bba8ab15ba..4ecd6bb1ff 100644 --- a/src/components/views/rooms/RoomTile2.tsx +++ b/src/components/views/rooms/RoomTile2.tsx @@ -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 { 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 { // 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 {
this.onTagRoom(e, DefaultTagID.Favourite)} - active={false} // TODO: https://github.com/vector-im/riot-web/issues/14283 - label={_t("Favourite")} + active={isFavorite} + label={favouriteLabel} > - - {_t("Favourite")} + + {favouriteLabel} diff --git a/src/components/views/settings/tabs/room/AdvancedRoomSettingsTab.js b/src/components/views/settings/tabs/room/AdvancedRoomSettingsTab.js index f57d5d3798..2edf3021dc 100644 --- a/src/components/views/settings/tabs/room/AdvancedRoomSettingsTab.js +++ b/src/components/views/settings/tabs/room/AdvancedRoomSettingsTab.js @@ -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")}
+
+ {_t('Make this room low priority')} + +
); } diff --git a/src/components/views/voip/CallContainer.tsx b/src/components/views/voip/CallContainer.tsx new file mode 100644 index 0000000000..0e901fac7d --- /dev/null +++ b/src/components/views/voip/CallContainer.tsx @@ -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 { + public render() { + return
+ + +
; + } +} \ No newline at end of file diff --git a/src/components/views/voip/CallPreview2.tsx b/src/components/views/voip/CallPreview2.tsx new file mode 100644 index 0000000000..1f2caf5ef8 --- /dev/null +++ b/src/components/views/voip/CallPreview2.tsx @@ -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 { + 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 ( + + ); + } + + return ; + } + + return null; + } +} + diff --git a/src/components/views/voip/CallView2.tsx b/src/components/views/voip/CallView2.tsx new file mode 100644 index 0000000000..c80d82d395 --- /dev/null +++ b/src/components/views/voip/CallView2.tsx @@ -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 { + private videoref: React.RefObject; + 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