Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion res/css/_components.scss
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,6 @@
@import "./views/rooms/_RoomRecoveryReminder.scss";
@import "./views/rooms/_RoomSublist.scss";
@import "./views/rooms/_RoomTile.scss";
@import "./views/rooms/_RoomTileIcon.scss";
@import "./views/rooms/_RoomUpgradeWarningBar.scss";
@import "./views/rooms/_SearchBar.scss";
@import "./views/rooms/_SendMessageComposer.scss";
Expand Down
45 changes: 42 additions & 3 deletions res/css/views/avatars/_DecoratedRoomAvatar.scss
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,49 @@ limitations under the License.
.mx_DecoratedRoomAvatar, .mx_TemporaryTile {
position: relative;

.mx_RoomTileIcon {
&.mx_DecoratedRoomAvatar_cutout .mx_BaseAvatar {
mask-image: url('$(res)/img/element-icons/roomlist/decorated-avatar-mask.svg');
mask-position: center;
mask-size: contain;
mask-repeat: no-repeat;
}

.mx_DecoratedRoomAvatar_icon {
position: absolute;
bottom: 0;
right: 0;
bottom: -2px;
right: -2px;
margin: 4px;
width: 8px;
height: 8px;
border-radius: 50%;
}

.mx_DecoratedRoomAvatar_icon::before {
content: '';
width: 8px;
height: 8px;
position: absolute;
border-radius: 8px;
}

.mx_DecoratedRoomAvatar_icon_globe::before {
mask-position: center;
mask-size: contain;
mask-repeat: no-repeat;
background: $secondary-fg-color;
mask-image: url('$(res)/img/globe.svg');
}

.mx_DecoratedRoomAvatar_icon_offline::before {
background-color: $presence-offline;
}

.mx_DecoratedRoomAvatar_icon_online::before {
background-color: $presence-online;
}

.mx_DecoratedRoomAvatar_icon_away::before {
background-color: $presence-away;
}

.mx_NotificationBadge, .mx_RoomTile_badgeContainer {
Expand Down
69 changes: 0 additions & 69 deletions res/css/views/rooms/_RoomTileIcon.scss

This file was deleted.

3 changes: 3 additions & 0 deletions res/img/element-icons/roomlist/decorated-avatar-mask.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 1 addition & 4 deletions res/img/globe.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions res/themes/legacy-dark/css/_legacy-dark.scss
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ $room-highlight-color: #343a46;

// typical text (dark-on-white in light skin)
$primary-fg-color: $text-primary-color;
$secondary-fg-color: $primary-fg-color;
$primary-bg-color: $bg-color;
$muted-fg-color: $header-panel-text-primary-color;

Expand Down
1 change: 1 addition & 0 deletions res/themes/legacy-light/css/_legacy-light.scss
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ $header-panel-bg-color: #f3f8fd;

// typical text (dark-on-white in light skin)
$primary-fg-color: #2e2f32;
$secondary-fg-color: $primary-fg-color;
$primary-bg-color: #ffffff;
$muted-fg-color: #61708b; // Commonly used in headings and relevant alt text

Expand Down
143 changes: 139 additions & 4 deletions src/components/views/avatars/DecoratedRoomAvatar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,22 @@ See the License for the specific language governing permissions and
limitations under the License.
*/

import React from 'react';
import React from "react";
import classNames from "classnames";
import { Room } from "matrix-js-sdk/src/models/room";
import { User } from "matrix-js-sdk/src/models/user";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";

import { TagID } from '../../../stores/room-list/models';
import RoomAvatar from "./RoomAvatar";
import RoomTileIcon from "../rooms/RoomTileIcon";
import NotificationBadge from '../rooms/NotificationBadge';
import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore";
import { NotificationState } from "../../../stores/notifications/NotificationState";
import {isPresenceEnabled} from "../../../utils/presence";
import {MatrixClientPeg} from "../../../MatrixClientPeg";
import {_t} from "../../../languageHandler";
import TextWithTooltip from "../elements/TextWithTooltip";
import DMRoomMap from "../../../utils/DMRoomMap";

interface IProps {
room: Room;
Expand All @@ -36,18 +43,134 @@ interface IProps {

interface IState {
notificationState?: NotificationState;
icon: Icon;
}

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",
}

function tooltipText(variant: Icon) {
switch (variant) {
case Icon.Globe:
return _t("This room is public");
case Icon.PresenceOnline:
return _t("Online");
case Icon.PresenceAway:
return _t("Away");
case Icon.PresenceOffline:
return _t("Offline");
}
}

export default class DecoratedRoomAvatar extends React.PureComponent<IProps, IState> {
private _dmUser: User;
private isUnmounted = false;
private isWatchingTimeline = false;

constructor(props: IProps) {
super(props);

this.state = {
notificationState: RoomNotificationStateStore.instance.getRoomState(this.props.room),
icon: this.calculateIcon(),
};
}

public componentWillUnmount() {
this.isUnmounted = true;
if (this.isWatchingTimeline) this.props.room.off('Room.timeline', this.onRoomTimeline);
this.dmUser = null; // clear listeners, if any
}

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

private get dmUser(): User {
return this._dmUser;
}

private set dmUser(val: User) {
const oldUser = this._dmUser;
this._dmUser = val;
if (oldUser && oldUser !== this._dmUser) {
oldUser.off('User.currentlyActive', this.onPresenceUpdate);
oldUser.off('User.presence', this.onPresenceUpdate);
}
if (this._dmUser && oldUser !== this._dmUser) {
this._dmUser.on('User.currentlyActive', this.onPresenceUpdate);
this._dmUser.on('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.calculateIcon()});
}
};

private onPresenceUpdate = () => {
if (this.isUnmounted) return;

let newIcon = this.getPresenceIcon();
if (newIcon !== this.state.icon) this.setState({icon: newIcon});
};

private getPresenceIcon(): Icon {
if (!this.dmUser) return Icon.None;

let icon = Icon.None;

const isOnline = this.dmUser.currentlyActive || this.dmUser.presence === 'online';
if (isOnline) {
icon = Icon.PresenceOnline;
} else if (this.dmUser.presence === 'offline') {
icon = Icon.PresenceOffline;
} else if (this.dmUser.presence === 'unavailable') {
icon = Icon.PresenceAway;
}

return icon;
}

private calculateIcon(): Icon {
let icon = Icon.None;

// We look at the DMRoomMap and not the tag here so that we don't exclude DMs in Favourites
const otherUserId = DMRoomMap.shared().getUserIdForRoomId(this.props.room.roomId);
if (otherUserId && this.props.room.getJoinedMemberCount() === 2) {
// Track presence, if available
if (isPresenceEnabled()) {
if (otherUserId) {
this.dmUser = MatrixClientPeg.get().getUser(otherUserId);
icon = this.getPresenceIcon();
}
}
} else {
// Track publicity
icon = this.isPublicRoom ? Icon.Globe : Icon.None;
if (!this.isWatchingTimeline) {
this.props.room.on('Room.timeline', this.onRoomTimeline);
this.isWatchingTimeline = true;
}
}
return icon;
}

public render(): React.ReactNode {
let badge: React.ReactNode;
if (this.props.displayBadge) {
Expand All @@ -58,15 +181,27 @@ export default class DecoratedRoomAvatar extends React.PureComponent<IProps, ISt
/>;
}

return <div className="mx_DecoratedRoomAvatar">
let icon;
if (this.state.icon !== Icon.None) {
icon = <TextWithTooltip
tooltip={tooltipText(this.state.icon)}
class={`mx_DecoratedRoomAvatar_icon mx_DecoratedRoomAvatar_icon_${this.state.icon.toLowerCase()}`}
/>;
}

const classes = classNames("mx_DecoratedRoomAvatar", {
mx_DecoratedRoomAvatar_cutout: icon,
});

return <div className={classes}>
<RoomAvatar
room={this.props.room}
width={this.props.avatarSize}
height={this.props.avatarSize}
oobData={this.props.oobData}
viewAvatarOnClick={this.props.viewAvatarOnClick}
/>
<RoomTileIcon room={this.props.room} />
{icon}
{badge}
</div>;
}
Expand Down
Loading