diff --git a/.github/workflows/docker.yaml b/.github/workflows/docker.yaml index 04fc5c6..bada578 100644 --- a/.github/workflows/docker.yaml +++ b/.github/workflows/docker.yaml @@ -1,9 +1,8 @@ name: Publish Docker image on: - push: - branches: - - master + release: + types: [published] jobs: push_to_registry: diff --git a/.github/workflows/netlify-dev.yaml b/.github/workflows/netlify-dev.yaml index b2174a7..b08a5c8 100644 --- a/.github/workflows/netlify-dev.yaml +++ b/.github/workflows/netlify-dev.yaml @@ -1,4 +1,4 @@ -name: 'Deploy to Netlify' +name: 'Deploy to Netlify (dev)' on: push: diff --git a/.github/workflows/netlify-prod.yaml b/.github/workflows/netlify-prod.yaml index 7de6e11..5d7f789 100644 --- a/.github/workflows/netlify-prod.yaml +++ b/.github/workflows/netlify-prod.yaml @@ -1,9 +1,8 @@ -name: 'Deploy to Netlify' +name: 'Deploy to Netlify (prod)' on: - push: - branches: - - master + release: + types: [published] jobs: deploy: diff --git a/README.md b/README.md index 8187d65..e4995d9 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,8 @@ Cinny is a [Matrix](https://matrix.org) client focusing primarily on simple, elegant and secure interface. +![preview](https://github.com/ajbura/cinny-site/blob/master/assets/preview-light.png) + ## Building and Running ### Building from source diff --git a/public/res/ic/outlined/bell-off.svg b/public/res/ic/outlined/bell-off.svg new file mode 100644 index 0000000..79ce8a3 --- /dev/null +++ b/public/res/ic/outlined/bell-off.svg @@ -0,0 +1,12 @@ + + + + + + + + + + diff --git a/public/res/ic/outlined/bell-ping.svg b/public/res/ic/outlined/bell-ping.svg new file mode 100644 index 0000000..3431bea --- /dev/null +++ b/public/res/ic/outlined/bell-ping.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + diff --git a/public/res/ic/outlined/bell-ring.svg b/public/res/ic/outlined/bell-ring.svg new file mode 100644 index 0000000..57fc267 --- /dev/null +++ b/public/res/ic/outlined/bell-ring.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + diff --git a/public/res/ic/outlined/bell.svg b/public/res/ic/outlined/bell.svg index d3d2f6d..43d470b 100644 --- a/public/res/ic/outlined/bell.svg +++ b/public/res/ic/outlined/bell.svg @@ -4,8 +4,7 @@ - - + + diff --git a/src/app/atoms/badge/NotificationBadge.jsx b/src/app/atoms/badge/NotificationBadge.jsx index 897a201..c92815b 100644 --- a/src/app/atoms/badge/NotificationBadge.jsx +++ b/src/app/atoms/badge/NotificationBadge.jsx @@ -8,7 +8,7 @@ function NotificationBadge({ alert, content }) { const notificationClass = alert ? ' notification-badge--alert' : ''; return (
- {content && {content}} + {content !== null && {content}}
); } diff --git a/src/app/atoms/badge/NotificationBadge.scss b/src/app/atoms/badge/NotificationBadge.scss index d408500..c672b11 100644 --- a/src/app/atoms/badge/NotificationBadge.scss +++ b/src/app/atoms/badge/NotificationBadge.scss @@ -2,17 +2,18 @@ min-width: 16px; min-height: 8px; padding: 0 var(--sp-ultra-tight); - background-color: var(--tc-surface-low); + background-color: var(--bg-badge); border-radius: var(--bo-radius); .text { - color: white; + color: var(--tc-badge); text-align: center; font-weight: 700; } &--alert { - background-color: var(--bg-danger); + background-color: var(--bg-positive); + & .text { color: white } } &:empty { diff --git a/src/app/atoms/button/Button.jsx b/src/app/atoms/button/Button.jsx index bfe06f6..bebca86 100644 --- a/src/app/atoms/button/Button.jsx +++ b/src/app/atoms/button/Button.jsx @@ -40,7 +40,7 @@ Button.defaultProps = { Button.propTypes = { id: PropTypes.string, className: PropTypes.string, - variant: PropTypes.oneOf(['surface', 'primary', 'caution', 'danger']), + variant: PropTypes.oneOf(['surface', 'primary', 'positive', 'caution', 'danger']), iconSrc: PropTypes.string, type: PropTypes.oneOf(['button', 'submit']), onClick: PropTypes.func, diff --git a/src/app/atoms/button/Button.scss b/src/app/atoms/button/Button.scss index 224c634..1426500 100644 --- a/src/app/atoms/button/Button.scss +++ b/src/app/atoms/button/Button.scss @@ -2,6 +2,7 @@ .btn-surface, .btn-primary, +.btn-positive, .btn-caution, .btn-danger { display: inline-flex; @@ -67,6 +68,13 @@ @include state.focus(var(--bs-primary-outline)); @include state.active(var(--bg-primary-active)); } +.btn-positive { + box-shadow: var(--bs-positive-border); + @include color(var(--tc-positive-high), var(--ic-positive-normal)); + @include state.hover(var(--bg-positive-hover)); + @include state.focus(var(--bs-positive-outline)); + @include state.active(var(--bg-positive-active)); +} .btn-caution { box-shadow: var(--bs-caution-border); @include color(var(--tc-caution-high), var(--ic-caution-normal)); diff --git a/src/app/atoms/context-menu/ContextMenu.jsx b/src/app/atoms/context-menu/ContextMenu.jsx index b525e22..023ee38 100644 --- a/src/app/atoms/context-menu/ContextMenu.jsx +++ b/src/app/atoms/context-menu/ContextMenu.jsx @@ -93,7 +93,7 @@ MenuItem.defaultProps = { }; MenuItem.propTypes = { - variant: PropTypes.oneOf(['surface', 'caution', 'danger']), + variant: PropTypes.oneOf(['surface', 'positive', 'caution', 'danger']), iconSrc: PropTypes.string, type: PropTypes.oneOf(['button', 'submit']), onClick: PropTypes.func.isRequired, diff --git a/src/app/atoms/context-menu/ContextMenu.scss b/src/app/atoms/context-menu/ContextMenu.scss index fd6ca07..4a8cc2a 100644 --- a/src/app/atoms/context-menu/ContextMenu.scss +++ b/src/app/atoms/context-menu/ContextMenu.scss @@ -30,6 +30,9 @@ .text { color: var(--tc-surface-low); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } &:not(:first-child) { diff --git a/src/app/molecules/room-selector/RoomSelector.jsx b/src/app/molecules/room-selector/RoomSelector.jsx index 01e2ffc..47201a6 100644 --- a/src/app/molecules/room-selector/RoomSelector.jsx +++ b/src/app/molecules/room-selector/RoomSelector.jsx @@ -10,10 +10,12 @@ import NotificationBadge from '../../atoms/badge/NotificationBadge'; import { blurOnBubbling } from '../../atoms/button/script'; function RoomSelectorWrapper({ - isSelected, onClick, content, options, + isSelected, isUnread, onClick, content, options, }) { + let myClass = isUnread ? ' room-selector--unread' : ''; + myClass += isSelected ? ' room-selector--selected' : ''; return ( -
+
- + ); }); 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 ( - - ); - } + 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 && } ); 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}`} + { + handleInviteClick(); toggleMenu(); + }} + > + Invite + + Leave + Notification + setNotif(cons.notifs.DEFAULT, notifState)} + > + Default + + setNotif(cons.notifs.ALL_MESSAGES, notifState)} + > + All messages + + setNotif(cons.notifs.MENTIONS_AND_KEYWORDS, notifState)} + > + Mentions & Keywords + + setNotif(cons.notifs.MUTE, notifState)} + > + Mute + + + )} + 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 - {/* */} - { - openInviteUser(roomId); toogleMenu(); - }} - > - Invite - - roomActions.leave(roomId)}>Leave - - )} - 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; +}