-
+
);
});
SidebarAvatar.defaultProps = {
@@ -52,7 +50,9 @@ SidebarAvatar.defaultProps = {
imageSrc: null,
active: false,
onClick: null,
- notifyCount: null,
+ isUnread: false,
+ notificationCount: 0,
+ isAlert: false,
};
SidebarAvatar.propTypes = {
@@ -63,10 +63,12 @@ SidebarAvatar.propTypes = {
iconSrc: PropTypes.string,
active: PropTypes.bool,
onClick: PropTypes.func,
- notifyCount: PropTypes.oneOfType([
+ isUnread: PropTypes.bool,
+ notificationCount: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
]),
+ isAlert: PropTypes.bool,
};
export default SidebarAvatar;
diff --git a/src/app/molecules/sidebar-avatar/SidebarAvatar.scss b/src/app/molecules/sidebar-avatar/SidebarAvatar.scss
index 6191735..3f445df 100644
--- a/src/app/molecules/sidebar-avatar/SidebarAvatar.scss
+++ b/src/app/molecules/sidebar-avatar/SidebarAvatar.scss
@@ -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 {
position: relative;
display: flex;
justify-content: center;
align-items: center;
- width: 100%;
cursor: pointer;
& .notification-badge {
position: absolute;
- right: var(--sp-extra-tight);
- top: calc(-1 * var(--sp-ultra-tight));
+ right: 0;
+ top: 0;
box-shadow: 0 0 0 2px var(--bg-surface-low);
+ transform: translate(20%, -20%);
+
+ margin: 0 !important;
}
&:focus {
outline: none;
@@ -37,7 +27,7 @@
content: "";
display: block;
position: absolute;
- left: 0;
+ left: -11px;
top: 50%;
transform: translateY(-50%);
@@ -48,7 +38,8 @@
transition: height 200ms linear;
[dir=rtl] & {
- right: 0;
+ left: unset;
+ right: -11px;
border-radius: 4px 0 0 4px;
}
}
diff --git a/src/app/organisms/navigation/Directs.jsx b/src/app/organisms/navigation/Directs.jsx
index a907980..639f4cd 100644
--- a/src/app/organisms/navigation/Directs.jsx
+++ b/src/app/organisms/navigation/Directs.jsx
@@ -12,7 +12,7 @@ import { AtoZ } from './common';
const drawerPostie = new Postie();
function Directs() {
- const { roomList } = initMatrix;
+ const { roomList, notifications } = initMatrix;
const directIds = [...roomList.directs].sort(AtoZ);
const [, forceUpdate] = useState({});
@@ -26,10 +26,11 @@ function Directs() {
drawerPostie.post('selector-change', addresses, selectedRoomId);
}
- function unreadChanged(roomId) {
- if (!drawerPostie.hasTopic('unread-change')) return;
- if (!drawerPostie.hasSubscriber('unread-change', roomId)) return;
- drawerPostie.post('unread-change', roomId);
+ function notiChanged(roomId, total, prevTotal) {
+ if (total === prevTotal) return;
+ if (drawerPostie.hasTopicAndSubscriber('unread-change', roomId)) {
+ drawerPostie.post('unread-change', roomId);
+ }
}
function roomListUpdated() {
@@ -47,13 +48,11 @@ function Directs() {
useEffect(() => {
roomList.on(cons.events.roomList.ROOMLIST_UPDATED, roomListUpdated);
navigation.on(cons.events.navigation.ROOM_SELECTED, selectorChanged);
- roomList.on(cons.events.roomList.MY_RECEIPT_ARRIVED, unreadChanged);
- roomList.on(cons.events.roomList.EVENT_ARRIVED, unreadChanged);
+ notifications.on(cons.events.notifications.NOTI_CHANGED, notiChanged);
return () => {
roomList.removeListener(cons.events.roomList.ROOMLIST_UPDATED, roomListUpdated);
navigation.removeListener(cons.events.navigation.ROOM_SELECTED, selectorChanged);
- roomList.removeListener(cons.events.roomList.MY_RECEIPT_ARRIVED, unreadChanged);
- roomList.removeListener(cons.events.roomList.EVENT_ARRIVED, unreadChanged);
+ notifications.removeListener(cons.events.notifications.NOTI_CHANGED, notiChanged);
};
}, []);
diff --git a/src/app/organisms/navigation/DrawerBreadcrumb.jsx b/src/app/organisms/navigation/DrawerBreadcrumb.jsx
index e784362..7eaae4e 100644
--- a/src/app/organisms/navigation/DrawerBreadcrumb.jsx
+++ b/src/app/organisms/navigation/DrawerBreadcrumb.jsx
@@ -1,4 +1,4 @@
-import React, { useEffect, useRef } from 'react';
+import React, { useState, useEffect, useRef } from 'react';
import PropTypes from 'prop-types';
import './DrawerBreadcrumb.scss';
@@ -6,51 +6,108 @@ import initMatrix from '../../../client/initMatrix';
import cons from '../../../client/state/cons';
import { selectSpace } from '../../../client/action/navigation';
import navigation from '../../../client/state/navigation';
+import { abbreviateNumber } from '../../../util/common';
import Text from '../../atoms/text/Text';
import RawIcon from '../../atoms/system-icons/RawIcon';
import Button from '../../atoms/button/Button';
import ScrollView from '../../atoms/scroll/ScrollView';
+import NotificationBadge from '../../atoms/badge/NotificationBadge';
import ChevronRightIC from '../../../../public/res/ic/outlined/chevron-right.svg';
function DrawerBreadcrumb({ spaceId }) {
+ const [, forceUpdate] = useState({});
const scrollRef = useRef(null);
+ const { roomList, notifications } = initMatrix;
const mx = initMatrix.matrixClient;
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(() => {
requestAnimationFrame(() => {
if (scrollRef?.current === null) return;
scrollRef.current.scrollLeft = scrollRef.current.scrollWidth;
});
+ notifications.on(cons.events.notifications.NOTI_CHANGED, onNotiChanged);
+ return () => {
+ notifications.removeListener(cons.events.notifications.NOTI_CHANGED, onNotiChanged);
+ };
}, [spaceId]);
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 (
{
spacePath.map((id, index) => {
- if (index === 0) {
- return (
-
selectSpace(id)}>
- {id === cons.tabs.HOME ? 'Home' : mx.getRoom(id).name}
-
- );
- }
+ const noti = (id !== cons.tabs.HOME && index < spacePath.length)
+ ? getNotiExcept(id, (index === spacePath.length - 1) ? null : spacePath[index + 1])
+ : getHomeNotiExcept((index === spacePath.length - 1) ? null : spacePath[index + 1]);
+
return (
-
+ { index !== 0 && }
selectSpace(id)}
>
- { mx.getRoom(id).name }
+ {id === cons.tabs.HOME ? 'Home' : mx.getRoom(id).name}
+ { noti !== null && (
+ 0 ? abbreviateNumber(noti.total) : null}
+ />
+ )}
);
diff --git a/src/app/organisms/navigation/DrawerBreadcrumb.scss b/src/app/organisms/navigation/DrawerBreadcrumb.scss
index 80262a9..60cd47f 100644
--- a/src/app/organisms/navigation/DrawerBreadcrumb.scss
+++ b/src/app/organisms/navigation/DrawerBreadcrumb.scss
@@ -51,6 +51,14 @@
overflow: hidden;
text-overflow: ellipsis;
}
+
+ & .notification-badge {
+ margin-left: var(--sp-extra-tight);
+ [dir=rtl] & {
+ margin-left: 0;
+ margin-right: var(--sp-extra-tight);
+ }
+ }
}
&__btn--selected {
diff --git a/src/app/organisms/navigation/Home.jsx b/src/app/organisms/navigation/Home.jsx
index 120ceb7..2c505f7 100644
--- a/src/app/organisms/navigation/Home.jsx
+++ b/src/app/organisms/navigation/Home.jsx
@@ -15,7 +15,7 @@ import { AtoZ } from './common';
const drawerPostie = new Postie();
function Home({ spaceId }) {
const [, forceUpdate] = useState({});
- const { roomList } = initMatrix;
+ const { roomList, notifications } = initMatrix;
let spaceIds = [];
let roomIds = [];
let directIds = [];
@@ -40,10 +40,11 @@ function Home({ spaceId }) {
if (addresses.length === 0) return;
drawerPostie.post('selector-change', addresses, selectedRoomId);
}
- function unreadChanged(roomId) {
- if (!drawerPostie.hasTopic('unread-change')) return;
- if (!drawerPostie.hasSubscriber('unread-change', roomId)) return;
- drawerPostie.post('unread-change', roomId);
+ function notiChanged(roomId, total, prevTotal) {
+ if (total === prevTotal) return;
+ if (drawerPostie.hasTopicAndSubscriber('unread-change', roomId)) {
+ drawerPostie.post('unread-change', roomId);
+ }
}
function roomListUpdated() {
@@ -61,13 +62,11 @@ function Home({ spaceId }) {
useEffect(() => {
roomList.on(cons.events.roomList.ROOMLIST_UPDATED, roomListUpdated);
navigation.on(cons.events.navigation.ROOM_SELECTED, selectorChanged);
- roomList.on(cons.events.roomList.MY_RECEIPT_ARRIVED, unreadChanged);
- roomList.on(cons.events.roomList.EVENT_ARRIVED, unreadChanged);
+ notifications.on(cons.events.notifications.NOTI_CHANGED, notiChanged);
return () => {
roomList.removeListener(cons.events.roomList.ROOMLIST_UPDATED, roomListUpdated);
navigation.removeListener(cons.events.navigation.ROOM_SELECTED, selectorChanged);
- roomList.removeListener(cons.events.roomList.MY_RECEIPT_ARRIVED, unreadChanged);
- roomList.removeListener(cons.events.roomList.EVENT_ARRIVED, unreadChanged);
+ notifications.removeListener(cons.events.notifications.NOTI_CHANGED, notiChanged);
};
}, []);
diff --git a/src/app/organisms/navigation/Selector.jsx b/src/app/organisms/navigation/Selector.jsx
index 0ddc127..7ec7c0b 100644
--- a/src/app/organisms/navigation/Selector.jsx
+++ b/src/app/organisms/navigation/Selector.jsx
@@ -3,9 +3,10 @@ import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import initMatrix from '../../../client/initMatrix';
-import { doesRoomHaveUnread } from '../../../util/matrixUtil';
import navigation from '../../../client/state/navigation';
+import { openRoomOptions } from '../../../client/action/navigation';
import { createSpaceShortcut, deleteSpaceShortcut } from '../../../client/action/room';
+import { getEventCords, abbreviateNumber } from '../../../util/common';
import IconButton from '../../atoms/button/IconButton';
import RoomSelector from '../../molecules/room-selector/RoomSelector';
@@ -16,11 +17,13 @@ import SpaceIC from '../../../../public/res/ic/outlined/space.svg';
import SpaceLockIC from '../../../../public/res/ic/outlined/space-lock.svg';
import StarIC from '../../../../public/res/ic/outlined/star.svg';
import FilledStarIC from '../../../../public/res/ic/filled/star.svg';
+import VerticalMenuIC from '../../../../public/res/ic/outlined/vertical-menu.svg';
function Selector({
roomId, isDM, drawerPostie, onClick,
}) {
const mx = initMatrix.matrixClient;
+ const noti = initMatrix.notifications;
const room = mx.getRoom(roomId);
let imageSrc = room.getAvatarFallbackMember()?.getAvatarUrl(mx.baseUrl, 24, 24, 'crop') || null;
if (imageSrc === null) imageSrc = room.getAvatarUrl(mx.baseUrl, 24, 24, 'crop') || null;
@@ -44,43 +47,56 @@ function Selector({
};
}, []);
+ if (room.isSpaceRoom()) {
+ return (
+
{
+ if (initMatrix.roomList.spaceShortcut.has(roomId)) deleteSpaceShortcut(roomId);
+ else createSpaceShortcut(roomId);
+ forceUpdate({});
+ }}
+ />
+ )}
+ />
+ );
+ }
+
return (
{
- if (room.isSpaceRoom()) {
- return (room.getJoinRule() === 'invite' ? SpaceLockIC : SpaceIC);
- }
- return (room.getJoinRule() === 'invite' ? HashLockIC : HashIC);
- })()
- }
+ // eslint-disable-next-line no-nested-ternary
+ iconSrc={isDM ? null : room.getJoinRule() === 'invite' ? HashLockIC : HashIC}
isSelected={isSelected}
- isUnread={doesRoomHaveUnread(room)}
- notificationCount={room.getUnreadNotificationCount('total') || 0}
- isAlert={room.getUnreadNotificationCount('highlight') !== 0}
+ isUnread={noti.hasNoti(roomId)}
+ notificationCount={abbreviateNumber(noti.getTotalNoti(roomId))}
+ isAlert={noti.getHighlightNoti(roomId) !== 0}
onClick={onClick}
options={(
- !room.isSpaceRoom()
- ? null
- : (
- {
- if (initMatrix.roomList.spaceShortcut.has(roomId)) deleteSpaceShortcut(roomId);
- else createSpaceShortcut(roomId);
- forceUpdate({});
- }}
- />
- )
+ openRoomOptions(getEventCords(e), roomId)}
+ />
)}
/>
);
diff --git a/src/app/organisms/navigation/SideBar.jsx b/src/app/organisms/navigation/SideBar.jsx
index b95636f..cd6de37 100644
--- a/src/app/organisms/navigation/SideBar.jsx
+++ b/src/app/organisms/navigation/SideBar.jsx
@@ -9,6 +9,7 @@ import {
selectTab, openInviteList, openPublicRooms, openSettings,
} from '../../../client/action/navigation';
import navigation from '../../../client/state/navigation';
+import { abbreviateNumber } from '../../../util/common';
import ScrollView from '../../atoms/scroll/ScrollView';
import SidebarAvatar from '../../molecules/sidebar-avatar/SidebarAvatar';
@@ -55,7 +56,7 @@ function ProfileAvatarMenu() {
}
function SideBar() {
- const { roomList } = initMatrix;
+ const { roomList, notifications } = initMatrix;
const mx = initMatrix.matrixClient;
const totalInviteCount = () => roomList.inviteRooms.size
+ roomList.inviteSpaces.size
@@ -74,33 +75,83 @@ function SideBar() {
function onSpaceShortcutUpdated() {
forceUpdate({});
}
+ function onNotificationChanged(roomId, total, prevTotal) {
+ if (total === prevTotal) return;
+ forceUpdate({});
+ }
useEffect(() => {
navigation.on(cons.events.navigation.TAB_SELECTED, onTabSelected);
roomList.on(cons.events.roomList.SPACE_SHORTCUT_UPDATED, onSpaceShortcutUpdated);
- initMatrix.roomList.on(
- cons.events.roomList.INVITELIST_UPDATED,
- onInviteListChange,
- );
+ roomList.on(cons.events.roomList.INVITELIST_UPDATED, onInviteListChange);
+ notifications.on(cons.events.notifications.NOTI_CHANGED, onNotificationChanged);
return () => {
navigation.removeListener(cons.events.navigation.TAB_SELECTED, onTabSelected);
roomList.removeListener(cons.events.roomList.SPACE_SHORTCUT_UPDATED, onSpaceShortcutUpdated);
- initMatrix.roomList.removeListener(
- cons.events.roomList.INVITELIST_UPDATED,
- onInviteListChange,
- );
+ roomList.removeListener(cons.events.roomList.INVITELIST_UPDATED, onInviteListChange);
+ notifications.removeListener(cons.events.notifications.NOTI_CHANGED, onNotificationChanged);
};
}, []);
+ 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 (
- selectTab(cons.tabs.HOME)} tooltip="Home" iconSrc={HomeIC} />
- selectTab(cons.tabs.DIRECTS)} tooltip="People" iconSrc={UserIC} />
+ selectTab(cons.tabs.HOME)}
+ tooltip="Home"
+ iconSrc={HomeIC}
+ isUnread={homeNoti !== null}
+ notificationCount={homeNoti !== null ? abbreviateNumber(homeNoti.total) : 0}
+ isAlert={homeNoti?.highlight > 0}
+ />
+ selectTab(cons.tabs.DIRECTS)}
+ tooltip="People"
+ iconSrc={UserIC}
+ isUnread={dmsNoti !== null}
+ notificationCount={dmsNoti !== null ? abbreviateNumber(dmsNoti.total) : 0}
+ isAlert={dmsNoti?.highlight > 0}
+ />
openPublicRooms()} tooltip="Public rooms" iconSrc={HashSearchIC} />
@@ -117,6 +168,9 @@ function SideBar() {
bgColor={colorMXID(room.roomId)}
imageSrc={room.getAvatarUrl(initMatrix.matrixClient.baseUrl, 42, 42, 'crop') || null}
text={room.name.slice(0, 1)}
+ isUnread={notifications.hasNoti(sRoomId)}
+ notificationCount={abbreviateNumber(notifications.getTotalNoti(sRoomId))}
+ isAlert={notifications.getHighlightNoti(sRoomId) !== 0}
onClick={() => selectTab(shortcut)}
/>
);
@@ -131,7 +185,9 @@ function SideBar() {
{ totalInvites !== 0 && (
openInviteList()}
tooltip="Invites"
iconSrc={InviteIC}
diff --git a/src/app/organisms/room-optons/RoomOptions.jsx b/src/app/organisms/room-optons/RoomOptions.jsx
new file mode 100644
index 0000000..0c89008
--- /dev/null
+++ b/src/app/organisms/room-optons/RoomOptions.jsx
@@ -0,0 +1,229 @@
+import React, { useState, useEffect, useRef } from 'react';
+import './RoomOptions.scss';
+
+import initMatrix from '../../../client/initMatrix';
+import cons from '../../../client/state/cons';
+import navigation from '../../../client/state/navigation';
+import { openInviteUser } from '../../../client/action/navigation';
+import * as roomActions from '../../../client/action/room';
+
+import ContextMenu, { MenuHeader, MenuItem } from '../../atoms/context-menu/ContextMenu';
+
+import BellIC from '../../../../public/res/ic/outlined/bell.svg';
+import BellRingIC from '../../../../public/res/ic/outlined/bell-ring.svg';
+import BellPingIC from '../../../../public/res/ic/outlined/bell-ping.svg';
+import BellOffIC from '../../../../public/res/ic/outlined/bell-off.svg';
+import AddUserIC from '../../../../public/res/ic/outlined/add-user.svg';
+import LeaveArrowIC from '../../../../public/res/ic/outlined/leave-arrow.svg';
+
+function getNotifState(roomId) {
+ const mx = initMatrix.matrixClient;
+ const pushRule = mx.getRoomPushRule('global', roomId);
+
+ if (typeof pushRule === 'undefined') {
+ const overridePushRules = mx.getAccountData('m.push_rules')?.getContent()?.global?.override;
+ if (typeof overridePushRules === 'undefined') return 0;
+
+ const isMuteOverride = overridePushRules.find((rule) => (
+ rule.rule_id === roomId
+ && rule.actions[0] === 'dont_notify'
+ && rule.conditions[0].kind === 'event_match'
+ ));
+
+ return isMuteOverride ? cons.notifs.MUTE : cons.notifs.DEFAULT;
+ }
+ if (pushRule.actions[0] === 'notify') return cons.notifs.ALL_MESSAGES;
+ return cons.notifs.MENTIONS_AND_KEYWORDS;
+}
+
+function setRoomNotifMute(roomId) {
+ const mx = initMatrix.matrixClient;
+ const roomPushRule = mx.getRoomPushRule('global', roomId);
+
+ const promises = [];
+ if (roomPushRule) {
+ promises.push(mx.deletePushRule('global', 'room', roomPushRule.rule_id));
+ }
+
+ promises.push(mx.addPushRule('global', 'override', roomId, {
+ conditions: [
+ {
+ kind: 'event_match',
+ key: 'room_id',
+ pattern: roomId,
+ },
+ ],
+ actions: [
+ 'dont_notify',
+ ],
+ }));
+
+ return Promise.all(promises);
+}
+
+function setRoomNotifsState(newState, roomId) {
+ const mx = initMatrix.matrixClient;
+ const promises = [];
+
+ const oldState = getNotifState(roomId);
+ if (oldState === cons.notifs.MUTE) {
+ promises.push(mx.deletePushRule('global', 'override', roomId));
+ }
+
+ if (newState === cons.notifs.DEFAULT) {
+ const roomPushRule = mx.getRoomPushRule('global', roomId);
+ if (roomPushRule) {
+ promises.push(mx.deletePushRule('global', 'room', roomPushRule.rule_id));
+ }
+ return Promise.all(promises);
+ }
+
+ if (newState === cons.notifs.MENTIONS_AND_KEYWORDS) {
+ promises.push(mx.addPushRule('global', 'room', roomId, {
+ actions: [
+ 'dont_notify',
+ ],
+ }));
+ promises.push(mx.setPushRuleEnabled('global', 'room', roomId, true));
+ return Promise.all(promises);
+ }
+
+ // cons.notifs.ALL_MESSAGES
+ promises.push(mx.addPushRule('global', 'room', roomId, {
+ actions: [
+ 'notify',
+ {
+ set_tweak: 'sound',
+ value: 'default',
+ },
+ ],
+ }));
+
+ promises.push(mx.setPushRuleEnabled('global', 'room', roomId, true));
+
+ return Promise.all(promises);
+}
+
+function setRoomNotifPushRule(notifState, roomId) {
+ if (notifState === cons.notifs.MUTE) {
+ setRoomNotifMute(roomId);
+ return;
+ }
+ setRoomNotifsState(notifState, roomId);
+}
+
+let isRoomOptionVisible = false;
+let roomId = null;
+function RoomOptions() {
+ const openerRef = useRef(null);
+ const [notifState, setNotifState] = useState(cons.notifs.DEFAULT);
+
+ function openRoomOptions(cords, rId) {
+ if (roomId !== null || isRoomOptionVisible) {
+ roomId = null;
+ if (cords.detail === 0) openerRef.current.click();
+ return;
+ }
+ openerRef.current.style.transform = `translate(${cords.x}px, ${cords.y}px)`;
+ roomId = rId;
+ setNotifState(getNotifState(roomId));
+ openerRef.current.click();
+ }
+
+ function afterRoomOptionsToggle(isVisible) {
+ isRoomOptionVisible = isVisible;
+ if (!isVisible) {
+ setTimeout(() => {
+ if (!isRoomOptionVisible) roomId = null;
+ }, 500);
+ }
+ }
+
+ useEffect(() => {
+ navigation.on(cons.events.navigation.ROOMOPTIONS_OPENED, openRoomOptions);
+ return () => {
+ navigation.on(cons.events.navigation.ROOMOPTIONS_OPENED, openRoomOptions);
+ };
+ }, []);
+
+ const handleInviteClick = () => openInviteUser(roomId);
+ const handleLeaveClick = () => {
+ if (confirm('Are you really want to leave this room?')) roomActions.leave(roomId);
+ };
+
+ function setNotif(nState, currentNState) {
+ if (nState === currentNState) return;
+ setRoomNotifPushRule(nState, roomId);
+ setNotifState(nState);
+ }
+
+ return (
+ (
+ <>
+ {`Options for ${initMatrix.matrixClient.getRoom(roomId)?.name}`}
+
+
+ Notification
+
+
+
+
+ >
+ )}
+ render={(toggleMenu) => (
+
+ )}
+ />
+ );
+}
+
+export default RoomOptions;
diff --git a/src/app/organisms/room-optons/RoomOptions.scss b/src/app/organisms/room-optons/RoomOptions.scss
new file mode 100644
index 0000000..ae3f9c3
--- /dev/null
+++ b/src/app/organisms/room-optons/RoomOptions.scss
@@ -0,0 +1,20 @@
+.context-menu__item {
+ position: relative;
+}
+
+.context-menu__item .btn-positive::before {
+ content: '';
+ display: inline-block;
+ width: 3px;
+ height: 12px;
+ background: var(--bg-positive);
+ border-radius: 0 4px 4px 0;
+ position: absolute;
+ left: 0;
+
+ [dir=rtl] & {
+ left: unset;
+ right: 0;
+ border-radius: 4px 0 0 4px;
+ }
+}
\ No newline at end of file
diff --git a/src/app/organisms/room/RoomViewContent.jsx b/src/app/organisms/room/RoomViewContent.jsx
index 18b8d34..57784b6 100644
--- a/src/app/organisms/room/RoomViewContent.jsx
+++ b/src/app/organisms/room/RoomViewContent.jsx
@@ -10,7 +10,7 @@ import cons from '../../../client/state/cons';
import { redactEvent, sendReaction } from '../../../client/action/roomTimeline';
import { getUsername, getUsernameOfRoomMember, doesRoomHaveUnread } from '../../../util/matrixUtil';
import colorMXID from '../../../util/colorMXID';
-import { diffMinutes, isNotInSameDay } from '../../../util/common';
+import { diffMinutes, isNotInSameDay, getEventCords } from '../../../util/common';
import { openEmojiBoard, openReadReceipts } from '../../../client/action/navigation';
import Divider from '../../atoms/divider/Divider';
@@ -176,12 +176,7 @@ function toggleEmoji(roomId, eventId, emojiKey, roomTimeline) {
}
function pickEmoji(e, roomId, eventId, roomTimeline) {
- const boxInfo = e.target.getBoundingClientRect();
- openEmojiBoard({
- x: boxInfo.x,
- y: boxInfo.y,
- detail: e.detail,
- }, (emoji) => {
+ openEmojiBoard(getEventCords(e), (emoji) => {
toggleEmoji(roomId, eventId, emoji.unicode, roomTimeline);
e.target.click();
});
diff --git a/src/app/organisms/room/RoomViewHeader.jsx b/src/app/organisms/room/RoomViewHeader.jsx
index d9b8aa9..e51cbd3 100644
--- a/src/app/organisms/room/RoomViewHeader.jsx
+++ b/src/app/organisms/room/RoomViewHeader.jsx
@@ -2,20 +2,17 @@ import React from 'react';
import PropTypes from 'prop-types';
import initMatrix from '../../../client/initMatrix';
-import { togglePeopleDrawer, openInviteUser } from '../../../client/action/navigation';
-import * as roomActions from '../../../client/action/room';
+import { togglePeopleDrawer, openRoomOptions } from '../../../client/action/navigation';
import colorMXID from '../../../util/colorMXID';
+import { getEventCords } from '../../../util/common';
import Text from '../../atoms/text/Text';
import IconButton from '../../atoms/button/IconButton';
import Header, { TitleWrapper } from '../../atoms/header/Header';
import Avatar from '../../atoms/avatar/Avatar';
-import ContextMenu, { MenuItem, MenuHeader } from '../../atoms/context-menu/ContextMenu';
import UserIC from '../../../../public/res/ic/outlined/user.svg';
import VerticalMenuIC from '../../../../public/res/ic/outlined/vertical-menu.svg';
-import LeaveArrowIC from '../../../../public/res/ic/outlined/leave-arrow.svg';
-import AddUserIC from '../../../../public/res/ic/outlined/add-user.svg';
function RoomViewHeader({ roomId }) {
const mx = initMatrix.matrixClient;
@@ -33,24 +30,10 @@ function RoomViewHeader({ roomId }) {
{ typeof roomTopic !== 'undefined' && {roomTopic}
}
- (
- <>
- Options
- {/* */}
-
-
- >
- )}
- render={(toggleMenu) => }
+ openRoomOptions(getEventCords(e), roomId)}
+ tooltip="Options"
+ src={VerticalMenuIC}
/>
);
diff --git a/src/app/organisms/room/RoomViewInput.jsx b/src/app/organisms/room/RoomViewInput.jsx
index a72f1e3..622d9e2 100644
--- a/src/app/organisms/room/RoomViewInput.jsx
+++ b/src/app/organisms/room/RoomViewInput.jsx
@@ -9,7 +9,7 @@ import initMatrix from '../../../client/initMatrix';
import cons from '../../../client/state/cons';
import settings from '../../../client/state/settings';
import { openEmojiBoard } from '../../../client/action/navigation';
-import { bytesToSize } from '../../../util/common';
+import { bytesToSize, getEventCords } from '../../../util/common';
import { getUsername } from '../../../util/matrixUtil';
import colorMXID from '../../../util/colorMXID';
@@ -327,12 +327,10 @@ function RoomViewInput({
{
- const boxInfo = e.target.getBoundingClientRect();
- openEmojiBoard({
- x: boxInfo.x + (document.dir === 'rtl' ? -80 : 80),
- y: boxInfo.y - 250,
- detail: e.detail,
- }, addEmoji);
+ const cords = getEventCords(e);
+ cords.x += (document.dir === 'rtl' ? -80 : 80);
+ cords.y -= 250;
+ openEmojiBoard(cords, addEmoji);
}}
tooltip="Emoji"
src={EmojiIC}
diff --git a/src/app/templates/client/Client.jsx b/src/app/templates/client/Client.jsx
index 8f89d43..bf7a3e7 100644
--- a/src/app/templates/client/Client.jsx
+++ b/src/app/templates/client/Client.jsx
@@ -8,6 +8,7 @@ import Room from '../../organisms/room/Room';
import Windows from '../../organisms/pw/Windows';
import Dialogs from '../../organisms/pw/Dialogs';
import EmojiBoardOpener from '../../organisms/emoji-board/EmojiBoardOpener';
+import RoomOptions from '../../organisms/room-optons/RoomOptions';
import initMatrix from '../../../client/initMatrix';
@@ -44,6 +45,7 @@ function Client() {
+
);
}
diff --git a/src/client/action/navigation.js b/src/client/action/navigation.js
index 5fa1304..d11aceb 100644
--- a/src/client/action/navigation.js
+++ b/src/client/action/navigation.js
@@ -77,6 +77,14 @@ function openReadReceipts(roomId, eventId) {
});
}
+function openRoomOptions(cords, roomId) {
+ appDispatcher.dispatch({
+ type: cons.actions.navigation.OPEN_ROOMOPTIONS,
+ cords,
+ roomId,
+ });
+}
+
export {
selectTab,
selectSpace,
@@ -89,4 +97,5 @@ export {
openSettings,
openEmojiBoard,
openReadReceipts,
+ openRoomOptions,
};
diff --git a/src/client/initMatrix.js b/src/client/initMatrix.js
index 26d07e6..91a41ea 100644
--- a/src/client/initMatrix.js
+++ b/src/client/initMatrix.js
@@ -4,6 +4,7 @@ import * as sdk from 'matrix-js-sdk';
import { secret } from './state/auth';
import RoomList from './state/RoomList';
import RoomsInput from './state/RoomsInput';
+import Notifications from './state/Notifications';
global.Olm = require('@matrix-org/olm');
@@ -56,6 +57,7 @@ class InitMatrix extends EventEmitter {
if (prevState === null) {
this.roomList = new RoomList(this.matrixClient);
this.roomsInput = new RoomsInput(this.matrixClient);
+ this.notifications = new Notifications(this.roomList);
this.emit('init_loading_finished');
}
},
diff --git a/src/client/state/Notifications.js b/src/client/state/Notifications.js
new file mode 100644
index 0000000..f5ecce2
--- /dev/null
+++ b/src/client/state/Notifications.js
@@ -0,0 +1,162 @@
+import EventEmitter from 'events';
+import cons from './cons';
+
+class Notifications extends EventEmitter {
+ constructor(roomList) {
+ super();
+
+ this.matrixClient = roomList.matrixClient;
+ this.roomList = roomList;
+
+ this.roomIdToNoti = new Map();
+
+ this._initNoti();
+ this._listenEvents();
+
+ // TODO:
+ window.notifications = this;
+ }
+
+ _initNoti() {
+ const addNoti = (roomId) => {
+ const room = this.matrixClient.getRoom(roomId);
+ if (this.doesRoomHaveUnread(room) === false) return;
+ const total = room.getUnreadNotificationCount('total');
+ const highlight = room.getUnreadNotificationCount('highlight');
+ const noti = this.getNoti(room.roomId);
+ this._setNoti(room.roomId, total - noti.total, highlight - noti.highlight);
+ };
+ [...this.roomList.rooms].forEach(addNoti);
+ [...this.roomList.directs].forEach(addNoti);
+ }
+
+ doesRoomHaveUnread(room) {
+ const userId = this.matrixClient.getUserId();
+ const readUpToId = room.getEventReadUpTo(userId);
+ const supportEvents = ['m.room.message', 'm.room.encrypted', 'm.sticker'];
+
+ if (room.timeline.length
+ && room.timeline[room.timeline.length - 1].sender
+ && room.timeline[room.timeline.length - 1].sender.userId === userId
+ && room.timeline[room.timeline.length - 1].getType() !== 'm.room.member') {
+ return false;
+ }
+
+ for (let i = room.timeline.length - 1; i >= 0; i -= 1) {
+ const event = room.timeline[i];
+
+ if (event.getId() === readUpToId) return false;
+
+ if (supportEvents.includes(event.getType())) {
+ return true;
+ }
+ }
+ return true;
+ }
+
+ getNoti(roomId) {
+ 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) {
+ return this.roomIdToNoti.has(roomId);
+ }
+
+ _setNoti(roomId, total, highlight, childId) {
+ const prevTotal = this.roomIdToNoti.get(roomId)?.total ?? null;
+ const noti = this.getNoti(roomId);
+
+ noti.total += total;
+ noti.highlight += highlight;
+ if (childId) {
+ if (noti.from === null) noti.from = new Set();
+ noti.from.add(childId);
+ }
+
+ this.roomIdToNoti.set(roomId, noti);
+ this.emit(cons.events.notifications.NOTI_CHANGED, roomId, noti.total, prevTotal);
+
+ const parentIds = this.roomList.roomIdToParents.get(roomId);
+ if (typeof parentIds === 'undefined') return;
+ [...parentIds].forEach((parentId) => this._setNoti(parentId, total, highlight, roomId));
+ }
+
+ _deleteNoti(roomId, total, highlight, childId) {
+ if (this.roomIdToNoti.has(roomId) === false) return;
+
+ const noti = this.getNoti(roomId);
+ const prevTotal = noti.total;
+ noti.total -= total;
+ noti.highlight -= highlight;
+ if (childId && noti.from !== null) {
+ noti.from.delete(childId);
+ }
+ if (noti.from === null || noti.from.size === 0) {
+ this.roomIdToNoti.delete(roomId);
+ this.emit(cons.events.notifications.FULL_READ, roomId);
+ this.emit(cons.events.notifications.NOTI_CHANGED, roomId, null, prevTotal);
+ } else {
+ this.roomIdToNoti.set(roomId, noti);
+ this.emit(cons.events.notifications.NOTI_CHANGED, roomId, noti.total, prevTotal);
+ }
+
+ const parentIds = this.roomList.roomIdToParents.get(roomId);
+ if (typeof parentIds === 'undefined') return;
+ [...parentIds].forEach((parentId) => this._deleteNoti(parentId, total, highlight, roomId));
+ }
+
+ _listenEvents() {
+ this.matrixClient.on('Room.timeline', (mEvent, room) => {
+ const supportEvents = ['m.room.message', 'm.room.encrypted', 'm.sticker'];
+ if (!supportEvents.includes(mEvent.getType())) return;
+
+ const lastTimelineEvent = room.timeline[room.timeline.length - 1];
+ if (lastTimelineEvent.getId() !== mEvent.getId()) return;
+ if (mEvent.getSender() === this.matrixClient.getUserId()) return;
+
+ const total = room.getUnreadNotificationCount('total');
+ const highlight = room.getUnreadNotificationCount('highlight');
+
+ const noti = this.getNoti(room.roomId);
+ this._setNoti(room.roomId, total - noti.total, highlight - noti.highlight);
+ });
+
+ this.matrixClient.on('Room.receipt', (mEvent, room) => {
+ if (mEvent.getType() === 'm.receipt') {
+ const content = mEvent.getContent();
+ const readedEventId = Object.keys(content)[0];
+ const readerUserId = Object.keys(content[readedEventId]['m.read'])[0];
+ if (readerUserId !== this.matrixClient.getUserId()) return;
+
+ if (this.hasNoti(room.roomId)) {
+ const noti = this.getNoti(room.roomId);
+ this._deleteNoti(room.roomId, noti.total, noti.highlight);
+ }
+ }
+ });
+
+ this.matrixClient.on('Room.myMembership', (room, membership) => {
+ if (membership === 'leave' && this.hasNoti(room.roomId)) {
+ const noti = this.getNoti(room.roomId);
+ this._deleteNoti(room.roomId, noti.total, noti.highlight);
+ }
+ });
+ }
+}
+
+export default Notifications;
diff --git a/src/client/state/RoomList.js b/src/client/state/RoomList.js
index a47bf46..b746a46 100644
--- a/src/client/state/RoomList.js
+++ b/src/client/state/RoomList.js
@@ -2,11 +2,18 @@ import EventEmitter from 'events';
import appDispatcher from '../dispatcher';
import cons from './cons';
+function isMEventSpaceChild(mEvent) {
+ return mEvent.getType() === 'm.space.child' && Object.keys(mEvent.getContent()).length > 0;
+}
+
class RoomList extends EventEmitter {
constructor(matrixClient) {
super();
this.matrixClient = matrixClient;
this.mDirects = this.getMDirects();
+
+ // Contains roomId to parent spaces roomId mapping of all spaces children.
+ // No matter if you have joined those children rooms or not.
this.roomIdToParents = new Map();
this.spaceShortcut = new Set();
@@ -34,14 +41,24 @@ class RoomList extends EventEmitter {
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) {
const space = this.matrixClient.getRoom(roomId);
const mSpaceChild = space?.currentState.getStateEvents('m.space.child');
const children = mSpaceChild?.map((mEvent) => {
- if (Object.keys(mEvent.event.content).length === 0) return null;
- return mEvent.event.state_key;
+ const childId = mEvent.event.state_key;
+ if (isMEventSpaceChild(mEvent)) return childId;
+ return null;
});
- return children?.filter((child) => child !== null);
+ return children?.filter((childId) => childId !== null);
}
addToRoomIdToParents(roomId, parentRoomId) {
@@ -246,22 +263,13 @@ class RoomList extends EventEmitter {
this.matrixClient.on('Room.name', () => {
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) => {
if (mEvent.getType() === 'm.space.child') {
const { event } = mEvent;
- const isRoomAdded = Object.keys(event.content).length > 0;
- if (isRoomAdded) this.addToRoomIdToParents(event.state_key, event.room_id);
- else this.removeFromRoomIdToParents(event.state_key, event.room_id);
+ if (isMEventSpaceChild(mEvent)) {
+ this.addToRoomIdToParents(event.state_key, event.room_id);
+ } else this.removeFromRoomIdToParents(event.state_key, event.room_id);
this.emit(cons.events.roomList.ROOMLIST_UPDATED);
return;
}
@@ -379,15 +387,6 @@ class RoomList extends EventEmitter {
}
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;
- this.emit(cons.events.roomList.EVENT_ARRIVED, room.roomId);
- });
}
}
export default RoomList;
diff --git a/src/client/state/cons.js b/src/client/state/cons.js
index 7587120..fee81b5 100644
--- a/src/client/state/cons.js
+++ b/src/client/state/cons.js
@@ -11,6 +11,12 @@ const cons = {
HOME: 'home',
DIRECTS: 'dm',
},
+ notifs: {
+ DEFAULT: 'default',
+ ALL_MESSAGES: 'all_messages',
+ MENTIONS_AND_KEYWORDS: 'mentions_and_keywords',
+ MUTE: 'mute',
+ },
actions: {
navigation: {
SELECT_TAB: 'SELECT_TAB',
@@ -24,6 +30,7 @@ const cons = {
OPEN_SETTINGS: 'OPEN_SETTINGS',
OPEN_EMOJIBOARD: 'OPEN_EMOJIBOARD',
OPEN_READRECEIPTS: 'OPEN_READRECEIPTS',
+ OPEN_ROOMOPTIONS: 'OPEN_ROOMOPTIONS',
},
room: {
JOIN: 'JOIN',
@@ -52,6 +59,7 @@ const cons = {
SETTINGS_OPENED: 'SETTINGS_OPENED',
EMOJIBOARD_OPENED: 'EMOJIBOARD_OPENED',
READRECEIPTS_OPENED: 'READRECEIPTS_OPENED',
+ ROOMOPTIONS_OPENED: 'ROOMOPTIONS_OPENED',
},
roomList: {
ROOMLIST_UPDATED: 'ROOMLIST_UPDATED',
@@ -59,10 +67,12 @@ const cons = {
ROOM_JOINED: 'ROOM_JOINED',
ROOM_LEAVED: 'ROOM_LEAVED',
ROOM_CREATED: 'ROOM_CREATED',
- MY_RECEIPT_ARRIVED: 'MY_RECEIPT_ARRIVED',
- EVENT_ARRIVED: 'EVENT_ARRIVED',
SPACE_SHORTCUT_UPDATED: 'SPACE_SHORTCUT_UPDATED',
},
+ notifications: {
+ NOTI_CHANGED: 'NOTI_CHANGED',
+ FULL_READ: 'FULL_READ',
+ },
roomTimeline: {
EVENT: 'EVENT',
PAGINATED: 'PAGINATED',
diff --git a/src/client/state/navigation.js b/src/client/state/navigation.js
index 5188aad..d7dabd7 100644
--- a/src/client/state/navigation.js
+++ b/src/client/state/navigation.js
@@ -85,6 +85,13 @@ class Navigation extends EventEmitter {
action.eventId,
);
},
+ [cons.actions.navigation.OPEN_ROOMOPTIONS]: () => {
+ this.emit(
+ cons.events.navigation.ROOMOPTIONS_OPENED,
+ action.cords,
+ action.roomId,
+ );
+ },
};
actions[action.type]?.();
}
diff --git a/src/index.scss b/src/index.scss
index 678bb65..77261e5 100644
--- a/src/index.scss
+++ b/src/index.scss
@@ -32,6 +32,7 @@
--bg-danger-border: rgba(240, 71, 71, 20%);
--bg-tooltip: #353535;
+ --bg-badge: #989898;
/* text color | --tc-[background type]-[priority]: value */
--tc-surface-high: #000000;
@@ -57,6 +58,7 @@
--tc-code: #e62498;
--tc-tooltip: white;
+ --tc-badge: white;
/* system icons | --ic-[background type]-[priority]: value */
@@ -179,6 +181,7 @@
--bg-primary-border: rgba(59, 119, 191, 38%);
--bg-tooltip: #000;
+ --bg-badge: hsl(0, 0%, 75%);
/* text color | --tc-[background type]-[priority]: value */
--tc-surface-high: rgba(255, 255, 255, 94%);
@@ -190,6 +193,7 @@
--tc-primary-low: rgba(255, 255, 255, 0.4);
--tc-code: #e565b1;
+ --tc-badge: black;
/* system icons | --ic-[background type]-[priority]: value */
--ic-surface-normal: rgba(255, 255, 255, 68%);
@@ -216,6 +220,8 @@
--bg-surface-low: hsl(64, 6%, 10%);
--bg-surface-low-transparent: hsla(64, 6%, 14%, 0);
+ --bg-badge: #c4c1ab;
+
/* text color | --tc-[background type]-[priority]: value */
--tc-surface-high: rgb(255, 251, 222, 94%);
diff --git a/src/util/Postie.js b/src/util/Postie.js
index c3bf806..668408d 100644
--- a/src/util/Postie.js
+++ b/src/util/Postie.js
@@ -30,8 +30,8 @@ class Postie {
}
hasTopicAndSubscriber(topic, address) {
- return (this.isTopicExist(topic))
- ? this.isSubscriberExist(topic, address)
+ return (this.hasTopic(topic))
+ ? this.hasSubscriber(topic, address)
: false;
}
diff --git a/src/util/common.js b/src/util/common.js
index 78bb349..f434c5b 100644
--- a/src/util/common.js
+++ b/src/util/common.js
@@ -19,3 +19,17 @@ export function isNotInSameDay(dt2, dt1) {
|| dt2.getYear() !== dt1.getYear()
);
}
+
+export function getEventCords(ev) {
+ const boxInfo = ev.target.getBoundingClientRect();
+ return {
+ x: boxInfo.x,
+ y: boxInfo.y,
+ detail: ev.detail,
+ };
+}
+
+export function abbreviateNumber(number) {
+ if (number > 99) return '99+';
+ return number;
+}