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/_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";
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
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-selected-bg-color: #FFF;
|
||||
|
||||
$presence-online: $accent-color;
|
||||
$presence-away: orange; // TODO: Get color
|
||||
$presence-offline: #E3E8F0;
|
||||
|
||||
// ********************
|
||||
|
||||
$roomtile-name-color: #61708b;
|
||||
|
|
|
@ -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">
|
||||
|
|
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