mirror of
https://github.com/element-hq/element-web
synced 2024-11-23 01:35:49 +03:00
Add presence icons; Convert to generic icon component
For https://github.com/vector-im/riot-web/issues/14039
This commit is contained in:
parent
bcebef7e56
commit
e4a51a7c01
7 changed files with 258 additions and 38 deletions
|
@ -189,6 +189,7 @@
|
||||||
@import "./views/rooms/_RoomSublist2.scss";
|
@import "./views/rooms/_RoomSublist2.scss";
|
||||||
@import "./views/rooms/_RoomTile.scss";
|
@import "./views/rooms/_RoomTile.scss";
|
||||||
@import "./views/rooms/_RoomTile2.scss";
|
@import "./views/rooms/_RoomTile2.scss";
|
||||||
|
@import "./views/rooms/_RoomTileIcon.scss";
|
||||||
@import "./views/rooms/_RoomUpgradeWarningBar.scss";
|
@import "./views/rooms/_RoomUpgradeWarningBar.scss";
|
||||||
@import "./views/rooms/_SearchBar.scss";
|
@import "./views/rooms/_SearchBar.scss";
|
||||||
@import "./views/rooms/_SendMessageComposer.scss";
|
@import "./views/rooms/_SendMessageComposer.scss";
|
||||||
|
|
|
@ -34,28 +34,10 @@ limitations under the License.
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
.mx_RoomTile2_publicRoom {
|
.mx_RoomTileIcon {
|
||||||
width: 12px;
|
|
||||||
height: 12px;
|
|
||||||
border-radius: 12px;
|
|
||||||
background-color: $roomlist2-bg-color; // to match the room list itself
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
right: 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');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
69
res/css/views/rooms/_RoomTileIcon.scss
Normal file
69
res/css/views/rooms/_RoomTileIcon.scss
Normal 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;
|
||||||
|
}
|
|
@ -186,6 +186,10 @@ $roomtile2-preview-color: #9e9e9e;
|
||||||
$roomtile2-default-badge-bg-color: #61708b;
|
$roomtile2-default-badge-bg-color: #61708b;
|
||||||
$roomtile2-selected-bg-color: #FFF;
|
$roomtile2-selected-bg-color: #FFF;
|
||||||
|
|
||||||
|
$presence-online: $accent-color;
|
||||||
|
$presence-away: orange; // TODO: Get color
|
||||||
|
$presence-offline: #E3E8F0;
|
||||||
|
|
||||||
// ********************
|
// ********************
|
||||||
|
|
||||||
$roomtile-name-color: #61708b;
|
$roomtile-name-color: #61708b;
|
||||||
|
|
|
@ -21,7 +21,7 @@ import React, { createRef } from "react";
|
||||||
import { Room } from "matrix-js-sdk/src/models/room";
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { RovingTabIndexWrapper } from "../../../accessibility/RovingTabIndex";
|
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 RoomAvatar from "../../views/avatars/RoomAvatar";
|
||||||
import dis from '../../../dispatcher/dispatcher';
|
import dis from '../../../dispatcher/dispatcher';
|
||||||
import { Key } from "../../../Keyboard";
|
import { Key } from "../../../Keyboard";
|
||||||
|
@ -31,6 +31,7 @@ import { _t } from "../../../languageHandler";
|
||||||
import { ContextMenu, ContextMenuButton } from "../../structures/ContextMenu";
|
import { ContextMenu, ContextMenuButton } from "../../structures/ContextMenu";
|
||||||
import { DefaultTagID, TagID } from "../../../stores/room-list/models";
|
import { DefaultTagID, TagID } from "../../../stores/room-list/models";
|
||||||
import { MessagePreviewStore } from "../../../stores/MessagePreviewStore";
|
import { MessagePreviewStore } from "../../../stores/MessagePreviewStore";
|
||||||
|
import RoomTileIcon from "./RoomTileIcon";
|
||||||
|
|
||||||
/*******************************************************************
|
/*******************************************************************
|
||||||
* CAUTION *
|
* CAUTION *
|
||||||
|
@ -86,12 +87,6 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
|
||||||
ActiveRoomObserver.addListener(this.props.room.roomId, this.onActiveRoomUpdate);
|
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() {
|
public componentWillUnmount() {
|
||||||
if (this.props.room) {
|
if (this.props.room) {
|
||||||
ActiveRoomObserver.removeListener(this.props.room.roomId, this.onActiveRoomUpdate);
|
ActiveRoomObserver.removeListener(this.props.room.roomId, this.onActiveRoomUpdate);
|
||||||
|
@ -187,25 +182,25 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
<AccessibleButton onClick={(e) => this.onTagRoom(e, DefaultTagID.Favourite)}>
|
<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>
|
<span>{_t("Favourite")}</span>
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<AccessibleButton onClick={(e) => this.onTagRoom(e, DefaultTagID.LowPriority)}>
|
<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>
|
<span>{_t("Low Priority")}</span>
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<AccessibleButton onClick={(e) => this.onTagRoom(e, DefaultTagID.DM)}>
|
<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>
|
<span>{_t("Direct Chat")}</span>
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<AccessibleButton onClick={this.onOpenRoomSettings}>
|
<AccessibleButton onClick={this.onOpenRoomSettings}>
|
||||||
<span className="mx_IconizedContextMenu_icon mx_RoomTile2_iconSettings" />
|
<span className="mx_IconizedContextMenu_icon mx_RoomTile2_iconSettings"/>
|
||||||
<span>{_t("Settings")}</span>
|
<span>{_t("Settings")}</span>
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
</li>
|
</li>
|
||||||
|
@ -215,7 +210,7 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
|
||||||
<ul>
|
<ul>
|
||||||
<li className="mx_RoomTile2_contextMenu_redRow">
|
<li className="mx_RoomTile2_contextMenu_redRow">
|
||||||
<AccessibleButton onClick={this.onLeaveRoomClick}>
|
<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>
|
<span>{_t("Leave Room")}</span>
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
</li>
|
</li>
|
||||||
|
@ -253,7 +248,7 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
|
||||||
'mx_RoomTile2_minimized': this.props.isMinimized,
|
'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?
|
// TODO: the original RoomTile uses state for the room name. Do we need to?
|
||||||
let name = this.props.room.name;
|
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;
|
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;
|
const avatarSize = 32;
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
|
@ -316,7 +306,7 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
|
||||||
>
|
>
|
||||||
<div className="mx_RoomTile2_avatarContainer">
|
<div className="mx_RoomTile2_avatarContainer">
|
||||||
<RoomAvatar room={this.props.room} width={avatarSize} height={avatarSize}/>
|
<RoomAvatar room={this.props.room} width={avatarSize} height={avatarSize}/>
|
||||||
{globe}
|
<RoomTileIcon room={this.props.room} tag={this.props.tag}/>
|
||||||
</div>
|
</div>
|
||||||
{nameContainer}
|
{nameContainer}
|
||||||
<div className="mx_RoomTile2_badgeContainer">
|
<div className="mx_RoomTile2_badgeContainer">
|
||||||
|
|
148
src/components/views/rooms/RoomTileIcon.tsx
Normal file
148
src/components/views/rooms/RoomTileIcon.tsx
Normal 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
26
src/utils/presence.ts
Normal 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;
|
||||||
|
}
|
Loading…
Reference in a new issue