diff --git a/res/css/_components.scss b/res/css/_components.scss index 7dd8a2034d..a2d0e1ceb5 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -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"; diff --git a/res/css/views/avatars/_DecoratedRoomAvatar.scss b/res/css/views/avatars/_DecoratedRoomAvatar.scss index 48d72131b5..e0afd9de66 100644 --- a/res/css/views/avatars/_DecoratedRoomAvatar.scss +++ b/res/css/views/avatars/_DecoratedRoomAvatar.scss @@ -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 { diff --git a/res/css/views/rooms/_RoomTileIcon.scss b/res/css/views/rooms/_RoomTileIcon.scss deleted file mode 100644 index 2f3afdd446..0000000000 --- a/res/css/views/rooms/_RoomTileIcon.scss +++ /dev/null @@ -1,69 +0,0 @@ -/* -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: $roomlist-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; -} diff --git a/res/img/element-icons/roomlist/decorated-avatar-mask.svg b/res/img/element-icons/roomlist/decorated-avatar-mask.svg new file mode 100644 index 0000000000..fb09c16bba --- /dev/null +++ b/res/img/element-icons/roomlist/decorated-avatar-mask.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/img/globe.svg b/res/img/globe.svg index cc22bc6e66..635fa91cce 100644 --- a/res/img/globe.svg +++ b/res/img/globe.svg @@ -1,6 +1,3 @@ - - - - + diff --git a/res/themes/legacy-dark/css/_legacy-dark.scss b/res/themes/legacy-dark/css/_legacy-dark.scss index 4268fad030..287723ec9c 100644 --- a/res/themes/legacy-dark/css/_legacy-dark.scss +++ b/res/themes/legacy-dark/css/_legacy-dark.scss @@ -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; diff --git a/res/themes/legacy-light/css/_legacy-light.scss b/res/themes/legacy-light/css/_legacy-light.scss index 5ebb4ccc02..e5ae7f866e 100644 --- a/res/themes/legacy-light/css/_legacy-light.scss +++ b/res/themes/legacy-light/css/_legacy-light.scss @@ -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 diff --git a/src/components/views/avatars/DecoratedRoomAvatar.tsx b/src/components/views/avatars/DecoratedRoomAvatar.tsx index daf28400f2..e6dadf676c 100644 --- a/src/components/views/avatars/DecoratedRoomAvatar.tsx +++ b/src/components/views/avatars/DecoratedRoomAvatar.tsx @@ -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; @@ -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 { + 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) { @@ -58,7 +181,19 @@ export default class DecoratedRoomAvatar extends React.PureComponent; } - return
+ let icon; + if (this.state.icon !== Icon.None) { + icon = ; + } + + const classes = classNames("mx_DecoratedRoomAvatar", { + mx_DecoratedRoomAvatar_cutout: icon, + }); + + return
- + {icon} {badge}
; } diff --git a/src/components/views/rooms/RoomTileIcon.tsx b/src/components/views/rooms/RoomTileIcon.tsx deleted file mode 100644 index 94833ac818..0000000000 --- a/src/components/views/rooms/RoomTileIcon.tsx +++ /dev/null @@ -1,168 +0,0 @@ -/* -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 { Room } from "matrix-js-sdk/src/models/room"; -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 { isPresenceEnabled } from "../../../utils/presence"; -import { _t } from "../../../languageHandler"; -import TextWithTooltip from "../elements/TextWithTooltip"; - -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"); - } -} - -interface IProps { - room: Room; -} - -interface IState { - icon: Icon; -} - -export default class RoomTileIcon extends React.Component { - private _dmUser: User; - private isUnmounted = false; - private isWatchingTimeline = false; - - constructor(props: IProps) { - super(props); - - this.state = { - icon: this.calculateIcon(), - }; - } - - 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); - } - } - - public componentWillUnmount() { - this.isUnmounted = true; - if (this.isWatchingTimeline) this.props.room.off('Room.timeline', this.onRoomTimeline); - this.dmUser = null; // clear listeners, if any - } - - 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.ReactElement { - if (this.state.icon === Icon.None) return null; - - return ; - } -} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 0c6b6ae056..151cfe2ab1 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1188,8 +1188,6 @@ "%(count)s unread messages.|other": "%(count)s unread messages.", "%(count)s unread messages.|one": "1 unread message.", "Unread messages.": "Unread messages.", - "This room is public": "This room is public", - "Away": "Away", "Add a topic": "Add a topic", "Upgrading this room will shut down the current instance of the room and create an upgraded room with the same name.": "Upgrading this room will shut down the current instance of the room and create an upgraded room with the same name.", "This room has already been upgraded.": "This room has already been upgraded.", @@ -1895,6 +1893,8 @@ "Take picture": "Take picture", "Remove for everyone": "Remove for everyone", "Remove for me": "Remove for me", + "This room is public": "This room is public", + "Away": "Away", "User Status": "User Status", "powered by Matrix": "powered by Matrix", "This homeserver would like to make sure you are not a robot.": "This homeserver would like to make sure you are not a robot.",