Add presence icons; Convert to generic icon component

For https://github.com/vector-im/riot-web/issues/14039
This commit is contained in:
Travis Ralston 2020-06-16 14:43:48 -06:00
parent bcebef7e56
commit e4a51a7c01
7 changed files with 258 additions and 38 deletions

View file

@ -189,6 +189,7 @@
@import "./views/rooms/_RoomSublist2.scss";
@import "./views/rooms/_RoomTile.scss";
@import "./views/rooms/_RoomTile2.scss";
@import "./views/rooms/_RoomTileIcon.scss";
@import "./views/rooms/_RoomUpgradeWarningBar.scss";
@import "./views/rooms/_SearchBar.scss";
@import "./views/rooms/_SendMessageComposer.scss";

View file

@ -34,28 +34,10 @@ limitations under the License.
margin-right: 8px;
position: relative;
.mx_RoomTile2_publicRoom {
width: 12px;
height: 12px;
border-radius: 12px;
background-color: $roomlist2-bg-color; // to match the room list itself
.mx_RoomTileIcon {
position: absolute;
bottom: 0;
right: 0;
&::before {
content: '';
width: 8px;
height: 8px;
top: 2px;
left: 2px;
position: absolute;
mask-position: center;
mask-size: contain;
mask-repeat: no-repeat;
background: $primary-fg-color;
mask-image: url('$(res)/img/globe.svg');
}
}
}

View file

@ -0,0 +1,69 @@
/*
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_RoomTileIcon {
width: 12px;
height: 12px;
border-radius: 12px;
background-color: $roomlist2-bg-color; // to match the room list itself
}
.mx_RoomTileIcon_globe::before {
content: '';
width: 8px;
height: 8px;
top: 2px;
left: 2px;
position: absolute;
mask-position: center;
mask-size: contain;
mask-repeat: no-repeat;
background: $primary-fg-color;
mask-image: url('$(res)/img/globe.svg');
}
.mx_RoomTileIcon_offline::before {
content: '';
width: 8px;
height: 8px;
top: 2px;
left: 2px;
position: absolute;
border-radius: 8px;
background-color: $presence-offline;
}
.mx_RoomTileIcon_online::before {
content: '';
width: 8px;
height: 8px;
top: 2px;
left: 2px;
position: absolute;
border-radius: 8px;
background-color: $presence-online;
}
.mx_RoomTileIcon_away::before {
content: '';
width: 8px;
height: 8px;
top: 2px;
left: 2px;
position: absolute;
border-radius: 8px;
background-color: $presence-away;
}

View file

@ -186,6 +186,10 @@ $roomtile2-preview-color: #9e9e9e;
$roomtile2-default-badge-bg-color: #61708b;
$roomtile2-selected-bg-color: #FFF;
$presence-online: $accent-color;
$presence-away: orange; // TODO: Get color
$presence-offline: #E3E8F0;
// ********************
$roomtile-name-color: #61708b;

View file

@ -21,7 +21,7 @@ import React, { createRef } from "react";
import { Room } from "matrix-js-sdk/src/models/room";
import classNames from "classnames";
import { RovingTabIndexWrapper } from "../../../accessibility/RovingTabIndex";
import AccessibleButton, {ButtonEvent} from "../../views/elements/AccessibleButton";
import AccessibleButton, { ButtonEvent } from "../../views/elements/AccessibleButton";
import RoomAvatar from "../../views/avatars/RoomAvatar";
import dis from '../../../dispatcher/dispatcher';
import { Key } from "../../../Keyboard";
@ -31,6 +31,7 @@ import { _t } from "../../../languageHandler";
import { ContextMenu, ContextMenuButton } from "../../structures/ContextMenu";
import { DefaultTagID, TagID } from "../../../stores/room-list/models";
import { MessagePreviewStore } from "../../../stores/MessagePreviewStore";
import RoomTileIcon from "./RoomTileIcon";
/*******************************************************************
* CAUTION *
@ -86,12 +87,6 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
ActiveRoomObserver.addListener(this.props.room.roomId, this.onActiveRoomUpdate);
}
private get isPublicRoom(): boolean {
const joinRules = this.props.room.currentState.getStateEvents("m.room.join_rules", "");
const joinRule = joinRules && joinRules.getContent().join_rule;
return joinRule === 'public';
}
public componentWillUnmount() {
if (this.props.room) {
ActiveRoomObserver.removeListener(this.props.room.roomId, this.onActiveRoomUpdate);
@ -187,25 +182,25 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
<ul>
<li>
<AccessibleButton onClick={(e) => this.onTagRoom(e, DefaultTagID.Favourite)}>
<span className="mx_IconizedContextMenu_icon mx_RoomTile2_iconStar" />
<span className="mx_IconizedContextMenu_icon mx_RoomTile2_iconStar"/>
<span>{_t("Favourite")}</span>
</AccessibleButton>
</li>
<li>
<AccessibleButton onClick={(e) => this.onTagRoom(e, DefaultTagID.LowPriority)}>
<span className="mx_IconizedContextMenu_icon mx_RoomTile2_iconArrowDown" />
<span className="mx_IconizedContextMenu_icon mx_RoomTile2_iconArrowDown"/>
<span>{_t("Low Priority")}</span>
</AccessibleButton>
</li>
<li>
<AccessibleButton onClick={(e) => this.onTagRoom(e, DefaultTagID.DM)}>
<span className="mx_IconizedContextMenu_icon mx_RoomTile2_iconUser" />
<span className="mx_IconizedContextMenu_icon mx_RoomTile2_iconUser"/>
<span>{_t("Direct Chat")}</span>
</AccessibleButton>
</li>
<li>
<AccessibleButton onClick={this.onOpenRoomSettings}>
<span className="mx_IconizedContextMenu_icon mx_RoomTile2_iconSettings" />
<span className="mx_IconizedContextMenu_icon mx_RoomTile2_iconSettings"/>
<span>{_t("Settings")}</span>
</AccessibleButton>
</li>
@ -215,7 +210,7 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
<ul>
<li className="mx_RoomTile2_contextMenu_redRow">
<AccessibleButton onClick={this.onLeaveRoomClick}>
<span className="mx_IconizedContextMenu_icon mx_RoomTile2_iconSignOut" />
<span className="mx_IconizedContextMenu_icon mx_RoomTile2_iconSignOut"/>
<span>{_t("Leave Room")}</span>
</AccessibleButton>
</li>
@ -253,7 +248,7 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
'mx_RoomTile2_minimized': this.props.isMinimized,
});
const badge = <NotificationBadge notification={this.state.notificationState} allowNoCount={true} />;
const badge = <NotificationBadge notification={this.state.notificationState} allowNoCount={true}/>;
// TODO: the original RoomTile uses state for the room name. Do we need to?
let name = this.props.room.name;
@ -294,11 +289,6 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
);
if (this.props.isMinimized) nameContainer = null;
let globe = null;
if (this.isPublicRoom && this.props.tag !== DefaultTagID.DM) {
globe = <span className='mx_RoomTile2_publicRoom' />; // sizing and such set by CSS
}
const avatarSize = 32;
return (
<React.Fragment>
@ -316,7 +306,7 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
>
<div className="mx_RoomTile2_avatarContainer">
<RoomAvatar room={this.props.room} width={avatarSize} height={avatarSize}/>
{globe}
<RoomTileIcon room={this.props.room} tag={this.props.tag}/>
</div>
{nameContainer}
<div className="mx_RoomTile2_badgeContainer">

View file

@ -0,0 +1,148 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 New Vector Ltd
Copyright 2018 Michael Telatynski <7t3chguy@gmail.com>
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.
*/
import React from "react";
import { Room } from "matrix-js-sdk/src/models/room";
import { RovingTabIndexWrapper } from "../../../accessibility/RovingTabIndex";
import AccessibleButton from "../../views/elements/AccessibleButton";
import RoomAvatar from "../../views/avatars/RoomAvatar";
import ActiveRoomObserver from "../../../ActiveRoomObserver";
import { DefaultTagID, TagID } from "../../../stores/room-list/models";
import { User } from "matrix-js-sdk/src/models/user";
import {MatrixEvent} from "matrix-js-sdk/src/models/event";
import DMRoomMap from "../../../utils/DMRoomMap";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import SdkConfig from "../../../SdkConfig";
import { isPresenceEnabled } from "../../../utils/presence";
enum Icon {
// Note: the names here are used in CSS class names
None = "NONE", // ... except this one
Globe = "GLOBE",
PresenceOnline = "ONLINE",
PresenceAway = "AWAY",
PresenceOffline = "OFFLINE",
}
interface IProps {
room: Room;
tag: TagID;
}
interface IState {
icon: Icon;
}
export default class RoomTileIcon extends React.Component<IProps, IState> {
private isUnmounted = false;
private dmUser: User;
private isWatchingTimeline = false;
constructor(props: IProps) {
super(props);
this.state = {
icon: this.getIcon(),
};
}
private get isPublicRoom(): boolean {
const joinRules = this.props.room.currentState.getStateEvents("m.room.join_rules", "");
const joinRule = joinRules && joinRules.getContent().join_rule;
return joinRule === 'public';
}
public componentWillUnmount() {
this.isUnmounted = true;
if (this.isWatchingTimeline) this.props.room.off('Room.timeline', this.onRoomTimeline);
this.unsubscribePresence();
}
private unsubscribePresence() {
if (this.dmUser) {
this.dmUser.off('User.currentlyActive', this.onPresenceUpdate);
this.dmUser.off('User.presence', this.onPresenceUpdate);
}
}
private onRoomTimeline = (ev: MatrixEvent, room: Room) => {
if (this.isUnmounted) return;
// apparently these can happen?
if (!room) return;
if (this.props.room.roomId !== room.roomId) return;
if (ev.getType() === 'm.room.join_rules' || ev.getType() === 'm.room.member') {
this.setState({icon: this.getIcon()});
}
};
private onPresenceUpdate = () => {
if (this.isUnmounted) return;
let newIcon = this.getPresenceIcon();
if (newIcon !== this.state.icon) this.setState({icon: newIcon});
};
private getPresenceIcon(): Icon {
let newIcon = Icon.None;
const isOnline = this.dmUser.currentlyActive || this.dmUser.presence === 'online';
if (isOnline) {
newIcon = Icon.PresenceOnline;
} else if (this.dmUser.presence === 'offline') {
newIcon = Icon.PresenceOffline;
} else if (this.dmUser.presence === 'unavailable') {
newIcon = Icon.PresenceAway;
}
return newIcon;
}
private getIcon(): Icon {
let defaultIcon = Icon.None;
this.unsubscribePresence();
if (this.props.tag === DefaultTagID.DM && this.props.room.getJoinedMemberCount() === 2) {
// Track presence, if available
if (isPresenceEnabled()) {
const otherUserId = DMRoomMap.shared().getUserIdForRoomId(this.props.room.roomId);
if (otherUserId) {
this.dmUser = MatrixClientPeg.get().getUser(otherUserId);
if (this.dmUser) {
this.dmUser.on('User.currentlyActive', this.onPresenceUpdate);
this.dmUser.on('User.presence', this.onPresenceUpdate);
defaultIcon = this.getPresenceIcon();
}
}
}
} else {
// Track publicity
defaultIcon = this.isPublicRoom ? Icon.Globe : Icon.None;
this.props.room.on('Room.timeline', this.onRoomTimeline);
this.isWatchingTimeline = true;
}
return defaultIcon;
}
public render(): React.ReactElement {
if (this.state.icon === Icon.None) return null;
return <span className={`mx_RoomTileIcon mx_RoomTileIcon_${this.state.icon.toLowerCase()}`} />;
}
}

26
src/utils/presence.ts Normal file
View file

@ -0,0 +1,26 @@
/*
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 { MatrixClientPeg } from "../MatrixClientPeg";
import SdkConfig from "../SdkConfig";
export function isPresenceEnabled() {
const hsUrl = MatrixClientPeg.get().baseUrl;
const urls = SdkConfig.get()['enable_presence_by_hs_url'];
if (!urls) return true;
if (urls[hsUrl] || urls[hsUrl] === undefined) return true;
return false;
}