Added unread symbol for Spaces, DMs and Home (#82)

This commit is contained in:
Ajay Bura 2021-09-12 20:44:13 +05:30
parent fc0dc8aea0
commit b07c50e580
14 changed files with 229 additions and 108 deletions

View file

@ -12,7 +12,7 @@
} }
&--alert { &--alert {
background-color: var(--bg-danger); background-color: var(--bg-positive);
& .text { color: white } & .text { color: white }
} }

View file

@ -84,7 +84,10 @@ RoomSelector.propTypes = {
iconSrc: PropTypes.string, iconSrc: PropTypes.string,
isSelected: PropTypes.bool, isSelected: PropTypes.bool,
isUnread: PropTypes.bool.isRequired, isUnread: PropTypes.bool.isRequired,
notificationCount: PropTypes.number.isRequired, notificationCount: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
]).isRequired,
isAlert: PropTypes.bool.isRequired, isAlert: PropTypes.bool.isRequired,
options: PropTypes.node, options: PropTypes.node,
onClick: PropTypes.func.isRequired, onClick: PropTypes.func.isRequired,

View file

@ -2,29 +2,22 @@ import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import './SidebarAvatar.scss'; import './SidebarAvatar.scss';
import Tippy from '@tippyjs/react';
import Avatar from '../../atoms/avatar/Avatar'; import Avatar from '../../atoms/avatar/Avatar';
import Text from '../../atoms/text/Text'; import Text from '../../atoms/text/Text';
import Tooltip from '../../atoms/tooltip/Tooltip';
import NotificationBadge from '../../atoms/badge/NotificationBadge'; import NotificationBadge from '../../atoms/badge/NotificationBadge';
import { blurOnBubbling } from '../../atoms/button/script'; import { blurOnBubbling } from '../../atoms/button/script';
const SidebarAvatar = React.forwardRef(({ const SidebarAvatar = React.forwardRef(({
tooltip, text, bgColor, imageSrc, tooltip, text, bgColor, imageSrc,
iconSrc, active, onClick, notifyCount, iconSrc, active, onClick, isUnread, notificationCount, isAlert,
}, ref) => { }, ref) => {
let activeClass = ''; let activeClass = '';
if (active) activeClass = ' sidebar-avatar--active'; if (active) activeClass = ' sidebar-avatar--active';
return ( return (
<Tippy <Tooltip
content={<Text variant="b1">{tooltip}</Text>} content={<Text variant="b1">{tooltip}</Text>}
className="sidebar-avatar-tippy"
touch="hold"
arrow={false}
placement="right" placement="right"
maxWidth={200}
delay={[0, 0]}
duration={[100, 0]}
offset={[0, 0]}
> >
<button <button
ref={ref} ref={ref}
@ -40,9 +33,14 @@ const SidebarAvatar = React.forwardRef(({
iconSrc={iconSrc} iconSrc={iconSrc}
size="normal" size="normal"
/> />
{ notifyCount !== null && <NotificationBadge alert content={notifyCount} /> } { isUnread && (
<NotificationBadge
alert={isAlert}
content={notificationCount !== 0 ? notificationCount : null}
/>
)}
</button> </button>
</Tippy> </Tooltip>
); );
}); });
SidebarAvatar.defaultProps = { SidebarAvatar.defaultProps = {
@ -52,7 +50,9 @@ SidebarAvatar.defaultProps = {
imageSrc: null, imageSrc: null,
active: false, active: false,
onClick: null, onClick: null,
notifyCount: null, isUnread: false,
notificationCount: 0,
isAlert: false,
}; };
SidebarAvatar.propTypes = { SidebarAvatar.propTypes = {
@ -63,10 +63,12 @@ SidebarAvatar.propTypes = {
iconSrc: PropTypes.string, iconSrc: PropTypes.string,
active: PropTypes.bool, active: PropTypes.bool,
onClick: PropTypes.func, onClick: PropTypes.func,
notifyCount: PropTypes.oneOfType([ isUnread: PropTypes.bool,
notificationCount: PropTypes.oneOfType([
PropTypes.string, PropTypes.string,
PropTypes.number, PropTypes.number,
]), ]),
isAlert: PropTypes.bool,
}; };
export default SidebarAvatar; export default SidebarAvatar;

View file

@ -1,28 +1,18 @@
.sidebar-avatar-tippy {
padding: var(--sp-extra-tight) var(--sp-normal);
background-color: var(--bg-tooltip);
border-radius: var(--bo-radius);
box-shadow: var(--bs-popup);
.text {
color: var(--tc-tooltip);
}
}
.sidebar-avatar { .sidebar-avatar {
position: relative; position: relative;
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
width: 100%;
cursor: pointer; cursor: pointer;
& .notification-badge { & .notification-badge {
position: absolute; position: absolute;
right: var(--sp-extra-tight); right: 0;
top: calc(-1 * var(--sp-ultra-tight)); top: 0;
box-shadow: 0 0 0 2px var(--bg-surface-low); box-shadow: 0 0 0 2px var(--bg-surface-low);
transform: translate(20%, -20%);
margin: 0 !important;
} }
&:focus { &:focus {
outline: none; outline: none;
@ -37,7 +27,7 @@
content: ""; content: "";
display: block; display: block;
position: absolute; position: absolute;
left: 0; left: -11px;
top: 50%; top: 50%;
transform: translateY(-50%); transform: translateY(-50%);
@ -48,7 +38,8 @@
transition: height 200ms linear; transition: height 200ms linear;
[dir=rtl] & { [dir=rtl] & {
right: 0; left: unset;
right: -11px;
border-radius: 4px 0 0 4px; border-radius: 4px 0 0 4px;
} }
} }

View file

@ -12,7 +12,7 @@ import { AtoZ } from './common';
const drawerPostie = new Postie(); const drawerPostie = new Postie();
function Directs() { function Directs() {
const { roomList } = initMatrix; const { roomList, notifications } = initMatrix;
const directIds = [...roomList.directs].sort(AtoZ); const directIds = [...roomList.directs].sort(AtoZ);
const [, forceUpdate] = useState({}); const [, forceUpdate] = useState({});
@ -26,11 +26,12 @@ function Directs() {
drawerPostie.post('selector-change', addresses, selectedRoomId); drawerPostie.post('selector-change', addresses, selectedRoomId);
} }
function unreadChanged(roomId) { function notiChanged(roomId, total, prevTotal) {
if (!drawerPostie.hasTopic('unread-change')) return; if (total === prevTotal) return;
if (!drawerPostie.hasSubscriber('unread-change', roomId)) return; if (drawerPostie.hasTopicAndSubscriber('unread-change', roomId)) {
drawerPostie.post('unread-change', roomId); drawerPostie.post('unread-change', roomId);
} }
}
function roomListUpdated() { function roomListUpdated() {
const { spaces, rooms, directs } = initMatrix.roomList; const { spaces, rooms, directs } = initMatrix.roomList;
@ -47,13 +48,11 @@ function Directs() {
useEffect(() => { useEffect(() => {
roomList.on(cons.events.roomList.ROOMLIST_UPDATED, roomListUpdated); roomList.on(cons.events.roomList.ROOMLIST_UPDATED, roomListUpdated);
navigation.on(cons.events.navigation.ROOM_SELECTED, selectorChanged); navigation.on(cons.events.navigation.ROOM_SELECTED, selectorChanged);
roomList.on(cons.events.roomList.MY_RECEIPT_ARRIVED, unreadChanged); notifications.on(cons.events.notifications.NOTI_CHANGED, notiChanged);
roomList.on(cons.events.roomList.EVENT_ARRIVED, unreadChanged);
return () => { return () => {
roomList.removeListener(cons.events.roomList.ROOMLIST_UPDATED, roomListUpdated); roomList.removeListener(cons.events.roomList.ROOMLIST_UPDATED, roomListUpdated);
navigation.removeListener(cons.events.navigation.ROOM_SELECTED, selectorChanged); navigation.removeListener(cons.events.navigation.ROOM_SELECTED, selectorChanged);
roomList.removeListener(cons.events.roomList.MY_RECEIPT_ARRIVED, unreadChanged); notifications.removeListener(cons.events.notifications.NOTI_CHANGED, notiChanged);
roomList.removeListener(cons.events.roomList.EVENT_ARRIVED, unreadChanged);
}; };
}, []); }, []);

View file

@ -1,4 +1,4 @@
import React, { useEffect, useRef } from 'react'; import React, { useState, useEffect, useRef } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import './DrawerBreadcrumb.scss'; import './DrawerBreadcrumb.scss';
@ -6,51 +6,108 @@ import initMatrix from '../../../client/initMatrix';
import cons from '../../../client/state/cons'; import cons from '../../../client/state/cons';
import { selectSpace } from '../../../client/action/navigation'; import { selectSpace } from '../../../client/action/navigation';
import navigation from '../../../client/state/navigation'; import navigation from '../../../client/state/navigation';
import { abbreviateNumber } from '../../../util/common';
import Text from '../../atoms/text/Text'; import Text from '../../atoms/text/Text';
import RawIcon from '../../atoms/system-icons/RawIcon'; import RawIcon from '../../atoms/system-icons/RawIcon';
import Button from '../../atoms/button/Button'; import Button from '../../atoms/button/Button';
import ScrollView from '../../atoms/scroll/ScrollView'; import ScrollView from '../../atoms/scroll/ScrollView';
import NotificationBadge from '../../atoms/badge/NotificationBadge';
import ChevronRightIC from '../../../../public/res/ic/outlined/chevron-right.svg'; import ChevronRightIC from '../../../../public/res/ic/outlined/chevron-right.svg';
function DrawerBreadcrumb({ spaceId }) { function DrawerBreadcrumb({ spaceId }) {
const [, forceUpdate] = useState({});
const scrollRef = useRef(null); const scrollRef = useRef(null);
const { roomList, notifications } = initMatrix;
const mx = initMatrix.matrixClient; const mx = initMatrix.matrixClient;
const spacePath = navigation.selectedSpacePath; const spacePath = navigation.selectedSpacePath;
function onNotiChanged(roomId, total, prevTotal) {
if (total === prevTotal) return;
if (navigation.selectedSpacePath.includes(roomId)) {
forceUpdate({});
}
if (navigation.selectedSpacePath[0] === cons.tabs.HOME) {
if (!roomList.isOrphan(roomId)) return;
if (roomList.directs.has(roomId)) return;
forceUpdate({});
}
}
useEffect(() => { useEffect(() => {
requestAnimationFrame(() => { requestAnimationFrame(() => {
if (scrollRef?.current === null) return; if (scrollRef?.current === null) return;
scrollRef.current.scrollLeft = scrollRef.current.scrollWidth; scrollRef.current.scrollLeft = scrollRef.current.scrollWidth;
}); });
notifications.on(cons.events.notifications.NOTI_CHANGED, onNotiChanged);
return () => {
notifications.removeListener(cons.events.notifications.NOTI_CHANGED, onNotiChanged);
};
}, [spaceId]); }, [spaceId]);
if (spacePath.length === 1) return null; if (spacePath.length === 1) return null;
function getHomeNotiExcept(childId) {
const orphans = roomList.getOrphans();
const childIndex = orphans.indexOf(childId);
if (childId !== -1) orphans.splice(childIndex, 1);
let noti = null;
orphans.forEach((roomId) => {
if (!notifications.hasNoti(roomId)) return;
if (noti === null) noti = { total: 0, highlight: 0 };
const childNoti = notifications.getNoti(roomId);
noti.total += childNoti.total;
noti.highlight += childNoti.highlight;
});
return noti;
}
function getNotiExcept(roomId, childId) {
if (!notifications.hasNoti(roomId)) return null;
const noti = notifications.getNoti(roomId);
if (!notifications.hasNoti(childId)) return noti;
if (noti.from === null) return noti;
if (noti.from.has(childId) && noti.from.size === 1) return null;
const childNoti = notifications.getNoti(childId);
return {
total: noti.total - childNoti.total,
highlight: noti.highlight - childNoti.highlight,
};
}
return ( return (
<div className="breadcrumb__wrapper"> <div className="breadcrumb__wrapper">
<ScrollView ref={scrollRef} horizontal vertical={false} invisible> <ScrollView ref={scrollRef} horizontal vertical={false} invisible>
<div className="breadcrumb"> <div className="breadcrumb">
{ {
spacePath.map((id, index) => { spacePath.map((id, index) => {
if (index === 0) { const noti = (id !== cons.tabs.HOME && index < spacePath.length)
return ( ? getNotiExcept(id, (index === spacePath.length - 1) ? null : spacePath[index + 1])
<Button key={id} onClick={() => selectSpace(id)}> : getHomeNotiExcept((index === spacePath.length - 1) ? null : spacePath[index + 1]);
<Text variant="b2">{id === cons.tabs.HOME ? 'Home' : mx.getRoom(id).name}</Text>
</Button>
);
}
return ( return (
<React.Fragment <React.Fragment
key={id} key={id}
> >
<RawIcon size="extra-small" src={ChevronRightIC} /> { index !== 0 && <RawIcon size="extra-small" src={ChevronRightIC} />}
<Button <Button
className={index === spacePath.length - 1 ? 'breadcrumb__btn--selected' : ''} className={index === spacePath.length - 1 ? 'breadcrumb__btn--selected' : ''}
onClick={() => selectSpace(id)} onClick={() => selectSpace(id)}
> >
<Text variant="b2">{ mx.getRoom(id).name }</Text> <Text variant="b2">{id === cons.tabs.HOME ? 'Home' : mx.getRoom(id).name}</Text>
{ noti !== null && (
<NotificationBadge
alert={noti.highlight !== 0}
content={noti.total > 0 ? abbreviateNumber(noti.total) : null}
/>
)}
</Button> </Button>
</React.Fragment> </React.Fragment>
); );

View file

@ -51,6 +51,14 @@
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
& .notification-badge {
margin-left: var(--sp-extra-tight);
[dir=rtl] & {
margin-left: 0;
margin-right: var(--sp-extra-tight);
}
}
} }
&__btn--selected { &__btn--selected {

View file

@ -15,7 +15,7 @@ import { AtoZ } from './common';
const drawerPostie = new Postie(); const drawerPostie = new Postie();
function Home({ spaceId }) { function Home({ spaceId }) {
const [, forceUpdate] = useState({}); const [, forceUpdate] = useState({});
const { roomList } = initMatrix; const { roomList, notifications } = initMatrix;
let spaceIds = []; let spaceIds = [];
let roomIds = []; let roomIds = [];
let directIds = []; let directIds = [];
@ -40,11 +40,12 @@ function Home({ spaceId }) {
if (addresses.length === 0) return; if (addresses.length === 0) return;
drawerPostie.post('selector-change', addresses, selectedRoomId); drawerPostie.post('selector-change', addresses, selectedRoomId);
} }
function unreadChanged(roomId) { function notiChanged(roomId, total, prevTotal) {
if (!drawerPostie.hasTopic('unread-change')) return; if (total === prevTotal) return;
if (!drawerPostie.hasSubscriber('unread-change', roomId)) return; if (drawerPostie.hasTopicAndSubscriber('unread-change', roomId)) {
drawerPostie.post('unread-change', roomId); drawerPostie.post('unread-change', roomId);
} }
}
function roomListUpdated() { function roomListUpdated() {
const { spaces, rooms, directs } = initMatrix.roomList; const { spaces, rooms, directs } = initMatrix.roomList;
@ -61,13 +62,11 @@ function Home({ spaceId }) {
useEffect(() => { useEffect(() => {
roomList.on(cons.events.roomList.ROOMLIST_UPDATED, roomListUpdated); roomList.on(cons.events.roomList.ROOMLIST_UPDATED, roomListUpdated);
navigation.on(cons.events.navigation.ROOM_SELECTED, selectorChanged); navigation.on(cons.events.navigation.ROOM_SELECTED, selectorChanged);
roomList.on(cons.events.roomList.MY_RECEIPT_ARRIVED, unreadChanged); notifications.on(cons.events.notifications.NOTI_CHANGED, notiChanged);
roomList.on(cons.events.roomList.EVENT_ARRIVED, unreadChanged);
return () => { return () => {
roomList.removeListener(cons.events.roomList.ROOMLIST_UPDATED, roomListUpdated); roomList.removeListener(cons.events.roomList.ROOMLIST_UPDATED, roomListUpdated);
navigation.removeListener(cons.events.navigation.ROOM_SELECTED, selectorChanged); navigation.removeListener(cons.events.navigation.ROOM_SELECTED, selectorChanged);
roomList.removeListener(cons.events.roomList.MY_RECEIPT_ARRIVED, unreadChanged); notifications.removeListener(cons.events.notifications.NOTI_CHANGED, notiChanged);
roomList.removeListener(cons.events.roomList.EVENT_ARRIVED, unreadChanged);
}; };
}, []); }, []);

View file

@ -3,11 +3,10 @@ import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import initMatrix from '../../../client/initMatrix'; import initMatrix from '../../../client/initMatrix';
import { doesRoomHaveUnread } from '../../../util/matrixUtil';
import navigation from '../../../client/state/navigation'; import navigation from '../../../client/state/navigation';
import { openRoomOptions } from '../../../client/action/navigation'; import { openRoomOptions } from '../../../client/action/navigation';
import { createSpaceShortcut, deleteSpaceShortcut } from '../../../client/action/room'; import { createSpaceShortcut, deleteSpaceShortcut } from '../../../client/action/room';
import { getEventCords } from '../../../util/common'; import { getEventCords, abbreviateNumber } from '../../../util/common';
import IconButton from '../../atoms/button/IconButton'; import IconButton from '../../atoms/button/IconButton';
import RoomSelector from '../../molecules/room-selector/RoomSelector'; import RoomSelector from '../../molecules/room-selector/RoomSelector';
@ -24,6 +23,7 @@ function Selector({
roomId, isDM, drawerPostie, onClick, roomId, isDM, drawerPostie, onClick,
}) { }) {
const mx = initMatrix.matrixClient; const mx = initMatrix.matrixClient;
const noti = initMatrix.notifications;
const room = mx.getRoom(roomId); const room = mx.getRoom(roomId);
let imageSrc = room.getAvatarFallbackMember()?.getAvatarUrl(mx.baseUrl, 24, 24, 'crop') || null; let imageSrc = room.getAvatarFallbackMember()?.getAvatarUrl(mx.baseUrl, 24, 24, 'crop') || null;
if (imageSrc === null) imageSrc = room.getAvatarUrl(mx.baseUrl, 24, 24, 'crop') || null; if (imageSrc === null) imageSrc = room.getAvatarUrl(mx.baseUrl, 24, 24, 'crop') || null;
@ -54,9 +54,9 @@ function Selector({
name={room.name} name={room.name}
roomId={roomId} roomId={roomId}
iconSrc={room.getJoinRule() === 'invite' ? SpaceLockIC : SpaceIC} iconSrc={room.getJoinRule() === 'invite' ? SpaceLockIC : SpaceIC}
isUnread={doesRoomHaveUnread(room)} isUnread={noti.hasNoti(roomId)}
notificationCount={room.getUnreadNotificationCount('total') || 0} notificationCount={abbreviateNumber(noti.getTotalNoti(roomId))}
isAlert={room.getUnreadNotificationCount('highlight') !== 0} isAlert={noti.getHighlightNoti(roomId) !== 0}
onClick={onClick} onClick={onClick}
options={( options={(
<IconButton <IconButton
@ -85,9 +85,9 @@ function Selector({
// eslint-disable-next-line no-nested-ternary // eslint-disable-next-line no-nested-ternary
iconSrc={isDM ? null : room.getJoinRule() === 'invite' ? HashLockIC : HashIC} iconSrc={isDM ? null : room.getJoinRule() === 'invite' ? HashLockIC : HashIC}
isSelected={isSelected} isSelected={isSelected}
isUnread={doesRoomHaveUnread(room)} isUnread={noti.hasNoti(roomId)}
notificationCount={room.getUnreadNotificationCount('total') || 0} notificationCount={abbreviateNumber(noti.getTotalNoti(roomId))}
isAlert={room.getUnreadNotificationCount('highlight') !== 0} isAlert={noti.getHighlightNoti(roomId) !== 0}
onClick={onClick} onClick={onClick}
options={( options={(
<IconButton <IconButton

View file

@ -9,6 +9,7 @@ import {
selectTab, openInviteList, openPublicRooms, openSettings, selectTab, openInviteList, openPublicRooms, openSettings,
} from '../../../client/action/navigation'; } from '../../../client/action/navigation';
import navigation from '../../../client/state/navigation'; import navigation from '../../../client/state/navigation';
import { abbreviateNumber } from '../../../util/common';
import ScrollView from '../../atoms/scroll/ScrollView'; import ScrollView from '../../atoms/scroll/ScrollView';
import SidebarAvatar from '../../molecules/sidebar-avatar/SidebarAvatar'; import SidebarAvatar from '../../molecules/sidebar-avatar/SidebarAvatar';
@ -55,7 +56,7 @@ function ProfileAvatarMenu() {
} }
function SideBar() { function SideBar() {
const { roomList } = initMatrix; const { roomList, notifications } = initMatrix;
const mx = initMatrix.matrixClient; const mx = initMatrix.matrixClient;
const totalInviteCount = () => roomList.inviteRooms.size const totalInviteCount = () => roomList.inviteRooms.size
+ roomList.inviteSpaces.size + roomList.inviteSpaces.size
@ -74,33 +75,83 @@ function SideBar() {
function onSpaceShortcutUpdated() { function onSpaceShortcutUpdated() {
forceUpdate({}); forceUpdate({});
} }
function onNotificationChanged(roomId, total, prevTotal) {
if (total === prevTotal) return;
forceUpdate({});
}
useEffect(() => { useEffect(() => {
navigation.on(cons.events.navigation.TAB_SELECTED, onTabSelected); navigation.on(cons.events.navigation.TAB_SELECTED, onTabSelected);
roomList.on(cons.events.roomList.SPACE_SHORTCUT_UPDATED, onSpaceShortcutUpdated); roomList.on(cons.events.roomList.SPACE_SHORTCUT_UPDATED, onSpaceShortcutUpdated);
initMatrix.roomList.on( roomList.on(cons.events.roomList.INVITELIST_UPDATED, onInviteListChange);
cons.events.roomList.INVITELIST_UPDATED, notifications.on(cons.events.notifications.NOTI_CHANGED, onNotificationChanged);
onInviteListChange,
);
return () => { return () => {
navigation.removeListener(cons.events.navigation.TAB_SELECTED, onTabSelected); navigation.removeListener(cons.events.navigation.TAB_SELECTED, onTabSelected);
roomList.removeListener(cons.events.roomList.SPACE_SHORTCUT_UPDATED, onSpaceShortcutUpdated); roomList.removeListener(cons.events.roomList.SPACE_SHORTCUT_UPDATED, onSpaceShortcutUpdated);
initMatrix.roomList.removeListener( roomList.removeListener(cons.events.roomList.INVITELIST_UPDATED, onInviteListChange);
cons.events.roomList.INVITELIST_UPDATED, notifications.removeListener(cons.events.notifications.NOTI_CHANGED, onNotificationChanged);
onInviteListChange,
);
}; };
}, []); }, []);
function getHomeNoti() {
const orphans = roomList.getOrphans();
let noti = null;
orphans.forEach((roomId) => {
if (!notifications.hasNoti(roomId)) return;
if (noti === null) noti = { total: 0, highlight: 0 };
const childNoti = notifications.getNoti(roomId);
noti.total += childNoti.total;
noti.highlight += childNoti.highlight;
});
return noti;
}
function getDMsNoti() {
if (roomList.directs.size === 0) return null;
let noti = null;
[...roomList.directs].forEach((roomId) => {
if (!notifications.hasNoti(roomId)) return;
if (noti === null) noti = { total: 0, highlight: 0 };
const childNoti = notifications.getNoti(roomId);
noti.total += childNoti.total;
noti.highlight += childNoti.highlight;
});
return noti;
}
// TODO: bellow operations are heavy.
// refactor this component into more smaller components.
const dmsNoti = getDMsNoti();
const homeNoti = getHomeNoti();
return ( return (
<div className="sidebar"> <div className="sidebar">
<div className="sidebar__scrollable"> <div className="sidebar__scrollable">
<ScrollView invisible> <ScrollView invisible>
<div className="scrollable-content"> <div className="scrollable-content">
<div className="featured-container"> <div className="featured-container">
<SidebarAvatar active={selectedTab === cons.tabs.HOME} onClick={() => selectTab(cons.tabs.HOME)} tooltip="Home" iconSrc={HomeIC} /> <SidebarAvatar
<SidebarAvatar active={selectedTab === cons.tabs.DIRECTS} onClick={() => selectTab(cons.tabs.DIRECTS)} tooltip="People" iconSrc={UserIC} /> active={selectedTab === cons.tabs.HOME}
onClick={() => selectTab(cons.tabs.HOME)}
tooltip="Home"
iconSrc={HomeIC}
isUnread={homeNoti !== null}
notificationCount={homeNoti !== null ? abbreviateNumber(homeNoti.total) : 0}
isAlert={homeNoti?.highlight > 0}
/>
<SidebarAvatar
active={selectedTab === cons.tabs.DIRECTS}
onClick={() => selectTab(cons.tabs.DIRECTS)}
tooltip="People"
iconSrc={UserIC}
isUnread={dmsNoti !== null}
notificationCount={dmsNoti !== null ? abbreviateNumber(dmsNoti.total) : 0}
isAlert={dmsNoti?.highlight > 0}
/>
<SidebarAvatar onClick={() => openPublicRooms()} tooltip="Public rooms" iconSrc={HashSearchIC} /> <SidebarAvatar onClick={() => openPublicRooms()} tooltip="Public rooms" iconSrc={HashSearchIC} />
</div> </div>
<div className="sidebar-divider" /> <div className="sidebar-divider" />
@ -117,6 +168,9 @@ function SideBar() {
bgColor={colorMXID(room.roomId)} bgColor={colorMXID(room.roomId)}
imageSrc={room.getAvatarUrl(initMatrix.matrixClient.baseUrl, 42, 42, 'crop') || null} imageSrc={room.getAvatarUrl(initMatrix.matrixClient.baseUrl, 42, 42, 'crop') || null}
text={room.name.slice(0, 1)} text={room.name.slice(0, 1)}
isUnread={notifications.hasNoti(sRoomId)}
notificationCount={abbreviateNumber(notifications.getTotalNoti(sRoomId))}
isAlert={notifications.getHighlightNoti(sRoomId) !== 0}
onClick={() => selectTab(shortcut)} onClick={() => selectTab(shortcut)}
/> />
); );
@ -131,7 +185,9 @@ function SideBar() {
<div className="sticky-container"> <div className="sticky-container">
{ totalInvites !== 0 && ( { totalInvites !== 0 && (
<SidebarAvatar <SidebarAvatar
notifyCount={totalInvites} isUnread
notificationCount={totalInvites}
isAlert
onClick={() => openInviteList()} onClick={() => openInviteList()}
tooltip="Invites" tooltip="Invites"
iconSrc={InviteIC} iconSrc={InviteIC}

View file

@ -58,11 +58,27 @@ class Notifications extends EventEmitter {
return this.roomIdToNoti.get(roomId) || { total: 0, highlight: 0, from: null }; return this.roomIdToNoti.get(roomId) || { total: 0, highlight: 0, from: null };
} }
getTotalNoti(roomId) {
const { total } = this.getNoti(roomId);
return total;
}
getHighlightNoti(roomId) {
const { highlight } = this.getNoti(roomId);
return highlight;
}
getFromNoti(roomId) {
const { from } = this.getNoti(roomId);
return from;
}
hasNoti(roomId) { hasNoti(roomId) {
return this.roomIdToNoti.has(roomId); return this.roomIdToNoti.has(roomId);
} }
_setNoti(roomId, total, highlight, childId) { _setNoti(roomId, total, highlight, childId) {
const prevTotal = this.roomIdToNoti.get(roomId)?.total ?? null;
const noti = this.getNoti(roomId); const noti = this.getNoti(roomId);
noti.total += total; noti.total += total;
@ -73,7 +89,7 @@ class Notifications extends EventEmitter {
} }
this.roomIdToNoti.set(roomId, noti); this.roomIdToNoti.set(roomId, noti);
this.emit(cons.events.notification.NOTI_CHANGED, roomId); this.emit(cons.events.notifications.NOTI_CHANGED, roomId, noti.total, prevTotal);
const parentIds = this.roomList.roomIdToParents.get(roomId); const parentIds = this.roomList.roomIdToParents.get(roomId);
if (typeof parentIds === 'undefined') return; if (typeof parentIds === 'undefined') return;
@ -84,6 +100,7 @@ class Notifications extends EventEmitter {
if (this.roomIdToNoti.has(roomId) === false) return; if (this.roomIdToNoti.has(roomId) === false) return;
const noti = this.getNoti(roomId); const noti = this.getNoti(roomId);
const prevTotal = noti.total;
noti.total -= total; noti.total -= total;
noti.highlight -= highlight; noti.highlight -= highlight;
if (childId && noti.from !== null) { if (childId && noti.from !== null) {
@ -91,10 +108,11 @@ class Notifications extends EventEmitter {
} }
if (noti.from === null || noti.from.size === 0) { if (noti.from === null || noti.from.size === 0) {
this.roomIdToNoti.delete(roomId); this.roomIdToNoti.delete(roomId);
this.emit(cons.events.notification.FULL_READ, roomId); this.emit(cons.events.notifications.FULL_READ, roomId);
this.emit(cons.events.notifications.NOTI_CHANGED, roomId, null, prevTotal);
} else { } else {
this.roomIdToNoti.set(roomId, noti); this.roomIdToNoti.set(roomId, noti);
this.emit(cons.events.notification.NOTI_CHANGED, roomId); this.emit(cons.events.notifications.NOTI_CHANGED, roomId, noti.total, prevTotal);
} }
const parentIds = this.roomList.roomIdToParents.get(roomId); const parentIds = this.roomList.roomIdToParents.get(roomId);
@ -120,8 +138,6 @@ class Notifications extends EventEmitter {
this.matrixClient.on('Room.receipt', (mEvent, room) => { this.matrixClient.on('Room.receipt', (mEvent, room) => {
if (mEvent.getType() === 'm.receipt') { if (mEvent.getType() === 'm.receipt') {
if (typeof mEvent.event.room_id === 'string') return;
const content = mEvent.getContent(); const content = mEvent.getContent();
const readedEventId = Object.keys(content)[0]; const readedEventId = Object.keys(content)[0];
const readerUserId = Object.keys(content[readedEventId]['m.read'])[0]; const readerUserId = Object.keys(content[readedEventId]['m.read'])[0];

View file

@ -41,6 +41,15 @@ class RoomList extends EventEmitter {
this.matrixClient.setAccountData(cons['in.cinny.spaces'], spaceContent); this.matrixClient.setAccountData(cons['in.cinny.spaces'], spaceContent);
} }
isOrphan(roomId) {
return !this.roomIdToParents.has(roomId);
}
getOrphans() {
const rooms = [...this.spaces].concat([...this.rooms]);
return rooms.filter((roomId) => !this.roomIdToParents.has(roomId));
}
getSpaceChildren(roomId) { getSpaceChildren(roomId) {
const space = this.matrixClient.getRoom(roomId); const space = this.matrixClient.getRoom(roomId);
const mSpaceChild = space?.currentState.getStateEvents('m.space.child'); const mSpaceChild = space?.currentState.getStateEvents('m.space.child');
@ -254,15 +263,6 @@ class RoomList extends EventEmitter {
this.matrixClient.on('Room.name', () => { this.matrixClient.on('Room.name', () => {
this.emit(cons.events.roomList.ROOMLIST_UPDATED); this.emit(cons.events.roomList.ROOMLIST_UPDATED);
}); });
this.matrixClient.on('Room.receipt', (event, room) => {
if (event.getType() === 'm.receipt') {
const content = event.getContent();
const userReadEventId = Object.keys(content)[0];
const eventReaderUserId = Object.keys(content[userReadEventId]['m.read'])[0];
if (eventReaderUserId !== this.matrixClient.getUserId()) return;
this.emit(cons.events.roomList.MY_RECEIPT_ARRIVED, room.roomId);
}
});
this.matrixClient.on('RoomState.events', (mEvent) => { this.matrixClient.on('RoomState.events', (mEvent) => {
if (mEvent.getType() === 'm.space.child') { if (mEvent.getType() === 'm.space.child') {
@ -387,16 +387,6 @@ class RoomList extends EventEmitter {
} }
this.emit(cons.events.roomList.ROOMLIST_UPDATED); this.emit(cons.events.roomList.ROOMLIST_UPDATED);
}); });
this.matrixClient.on('Room.timeline', (event, room) => {
const supportEvents = ['m.room.message', 'm.room.encrypted', 'm.sticker'];
if (!supportEvents.includes(event.getType())) return;
const lastTimelineEvent = room.timeline[room.timeline.length - 1];
if (lastTimelineEvent.getId() !== event.getId()) return;
if (event.getSender() === this.matrixClient.getUserId()) return;
this.emit(cons.events.roomList.EVENT_ARRIVED, room.roomId);
});
} }
} }
export default RoomList; export default RoomList;

View file

@ -67,11 +67,9 @@ const cons = {
ROOM_JOINED: 'ROOM_JOINED', ROOM_JOINED: 'ROOM_JOINED',
ROOM_LEAVED: 'ROOM_LEAVED', ROOM_LEAVED: 'ROOM_LEAVED',
ROOM_CREATED: 'ROOM_CREATED', ROOM_CREATED: 'ROOM_CREATED',
MY_RECEIPT_ARRIVED: 'MY_RECEIPT_ARRIVED',
EVENT_ARRIVED: 'EVENT_ARRIVED',
SPACE_SHORTCUT_UPDATED: 'SPACE_SHORTCUT_UPDATED', SPACE_SHORTCUT_UPDATED: 'SPACE_SHORTCUT_UPDATED',
}, },
notification: { notifications: {
NOTI_CHANGED: 'NOTI_CHANGED', NOTI_CHANGED: 'NOTI_CHANGED',
FULL_READ: 'FULL_READ', FULL_READ: 'FULL_READ',
}, },

View file

@ -220,6 +220,8 @@
--bg-surface-low: hsl(64, 6%, 10%); --bg-surface-low: hsl(64, 6%, 10%);
--bg-surface-low-transparent: hsla(64, 6%, 14%, 0); --bg-surface-low-transparent: hsla(64, 6%, 14%, 0);
--bg-badge: #c4c1ab;
/* text color | --tc-[background type]-[priority]: value */ /* text color | --tc-[background type]-[priority]: value */
--tc-surface-high: rgb(255, 251, 222, 94%); --tc-surface-high: rgb(255, 251, 222, 94%);