diff --git a/src/app/molecules/channel-selector/ChannelSelector.jsx b/src/app/molecules/channel-selector/ChannelSelector.jsx index 631dd578..076b5fe9 100644 --- a/src/app/molecules/channel-selector/ChannelSelector.jsx +++ b/src/app/molecules/channel-selector/ChannelSelector.jsx @@ -18,7 +18,7 @@ function ChannelSelectorWrapper({ className="channel-selector__content" type="button" onClick={onClick} - onMouseUp={(e) => blurOnBubbling(e, '.channel-selector__wrapper')} + onMouseUp={(e) => blurOnBubbling(e, '.channel-selector')} > {content} diff --git a/src/app/organisms/navigation/Directs.jsx b/src/app/organisms/navigation/Directs.jsx new file mode 100644 index 00000000..9c347e96 --- /dev/null +++ b/src/app/organisms/navigation/Directs.jsx @@ -0,0 +1,69 @@ +import React, { useState, useEffect } from 'react'; + +import initMatrix from '../../../client/initMatrix'; +import cons from '../../../client/state/cons'; +import navigation from '../../../client/state/navigation'; +import { selectRoom } from '../../../client/action/navigation'; +import Postie from '../../../util/Postie'; + +import Selector from './Selector'; + +import { AtoZ } from './common'; + +const drawerPostie = new Postie(); +function Directs() { + const { roomList } = initMatrix; + const directIds = [...roomList.directs].sort(AtoZ); + + const [, forceUpdate] = useState({}); + + function selectorChanged(activeRoomID, prevActiveRoomId) { + if (!drawerPostie.hasTopic('selector-change')) return; + const addresses = []; + if (drawerPostie.hasSubscriber('selector-change', activeRoomID)) addresses.push(activeRoomID); + if (drawerPostie.hasSubscriber('selector-change', prevActiveRoomId)) addresses.push(prevActiveRoomId); + if (addresses.length === 0) return; + drawerPostie.post('selector-change', addresses, activeRoomID); + } + + function unreadChanged(roomId) { + if (!drawerPostie.hasTopic('unread-change')) return; + if (!drawerPostie.hasSubscriber('unread-change', roomId)) return; + drawerPostie.post('unread-change', roomId); + } + + function roomListUpdated() { + const { spaces, rooms, directs } = initMatrix.roomList; + if (!( + spaces.has(navigation.getActiveRoomId()) + || rooms.has(navigation.getActiveRoomId()) + || directs.has(navigation.getActiveRoomId())) + ) { + selectRoom(null); + } + forceUpdate({}); + } + + 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); + 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); + }; + }, []); + + return directIds.map((id) => ( + + )); +} + +export default Directs; diff --git a/src/app/organisms/navigation/Drawer.jsx b/src/app/organisms/navigation/Drawer.jsx index e439d604..92c192a2 100644 --- a/src/app/organisms/navigation/Drawer.jsx +++ b/src/app/organisms/navigation/Drawer.jsx @@ -1,86 +1,14 @@ import React, { useState, useEffect } from 'react'; -import PropTypes from 'prop-types'; import './Drawer.scss'; -import initMatrix from '../../../client/initMatrix'; import cons from '../../../client/state/cons'; -import { doesRoomHaveUnread } from '../../../util/matrixUtil'; -import { - selectRoom, openPublicChannels, openCreateChannel, openInviteUser, -} from '../../../client/action/navigation'; import navigation from '../../../client/state/navigation'; -import Header, { TitleWrapper } from '../../atoms/header/Header'; -import Text from '../../atoms/text/Text'; -import IconButton from '../../atoms/button/IconButton'; import ScrollView from '../../atoms/scroll/ScrollView'; -import ContextMenu, { MenuItem, MenuHeader } from '../../atoms/context-menu/ContextMenu'; -import ChannelSelector from '../../molecules/channel-selector/ChannelSelector'; -import PlusIC from '../../../../public/res/ic/outlined/plus.svg'; -// import VerticalMenuIC from '../../../../public/res/ic/outlined/vertical-menu.svg'; -import HashIC from '../../../../public/res/ic/outlined/hash.svg'; -import HashLockIC from '../../../../public/res/ic/outlined/hash-lock.svg'; -import HashPlusIC from '../../../../public/res/ic/outlined/hash-plus.svg'; -import HashSearchIC from '../../../../public/res/ic/outlined/hash-search.svg'; -import SpaceIC from '../../../../public/res/ic/outlined/space.svg'; -import SpaceLockIC from '../../../../public/res/ic/outlined/space-lock.svg'; - -function AtoZ(aId, bId) { - let aName = initMatrix.matrixClient.getRoom(aId).name; - let bName = initMatrix.matrixClient.getRoom(bId).name; - - // remove "#" from the room name - // To ignore it in sorting - aName = aName.replaceAll('#', ''); - bName = bName.replaceAll('#', ''); - - if (aName.toLowerCase() < bName.toLowerCase()) { - return -1; - } - if (aName.toLowerCase() > bName.toLowerCase()) { - return 1; - } - return 0; -} - -function DrawerHeader({ activeTab }) { - return ( -
- - {(activeTab === 'home' ? 'Home' : 'Direct messages')} - - {(activeTab === 'dm') - ? openInviteUser()} tooltip="Start DM" src={PlusIC} size="normal" /> - : ( - ( - <> - Add channel - { hideMenu(); openCreateChannel(); }} - > - Create new channel - - { hideMenu(); openPublicChannels(); }} - > - Add Public channel - - - )} - render={(toggleMenu) => ()} - /> - )} - {/* ''} tooltip="Menu" src={VerticalMenuIC} size="normal" /> */} -
- ); -} -DrawerHeader.propTypes = { - activeTab: PropTypes.string.isRequired, -}; +import DrawerHeader from './DrawerHeader'; +import Home from './Home'; +import Directs from './Directs'; function DrawerBradcrumb() { return ( @@ -94,111 +22,6 @@ function DrawerBradcrumb() { ); } -function renderSelector(room, roomId, isSelected, isDM) { - const mx = initMatrix.matrixClient; - let imageSrc = room.getAvatarFallbackMember()?.getAvatarUrl(mx.baseUrl, 24, 24, 'crop'); - if (typeof imageSrc === 'undefined') imageSrc = null; - - return ( - { - if (room.isSpaceRoom()) { - return (room.getJoinRule() === 'invite' ? SpaceLockIC : SpaceIC); - } - return (room.getJoinRule() === 'invite' ? HashLockIC : HashIC); - })() - } - isSelected={isSelected} - isUnread={doesRoomHaveUnread(room)} - notificationCount={room.getUnreadNotificationCount('total')} - isAlert={room.getUnreadNotificationCount('highlight') !== 0} - onClick={() => selectRoom(roomId)} - /> - ); -} - -function Directs({ selectedRoomId }) { - const mx = initMatrix.matrixClient; - const directIds = [...initMatrix.roomList.directs].sort(AtoZ); - - return directIds.map((id) => renderSelector(mx.getRoom(id), id, selectedRoomId === id, true)); -} -Directs.defaultProps = { selectedRoomId: null }; -Directs.propTypes = { selectedRoomId: PropTypes.string }; - -function Home({ selectedRoomId }) { - const mx = initMatrix.matrixClient; - const spaceIds = [...initMatrix.roomList.spaces].sort(AtoZ); - const roomIds = [...initMatrix.roomList.rooms].sort(AtoZ); - - return ( - <> - { spaceIds.length !== 0 && Spaces } - { spaceIds.map((id) => renderSelector(mx.getRoom(id), id, selectedRoomId === id, false)) } - { roomIds.length !== 0 && Channels } - { roomIds.map((id) => renderSelector(mx.getRoom(id), id, selectedRoomId === id, false)) } - - ); -} -Home.defaultProps = { selectedRoomId: null }; -Home.propTypes = { selectedRoomId: PropTypes.string }; - -function Channels({ activeTab }) { - const [selectedRoomId, changeSelectedRoomId] = useState(null); - const [, updateState] = useState(); - - const selectHandler = (roomId) => changeSelectedRoomId(roomId); - const handleDataChanges = () => updateState({}); - - const onRoomListChange = () => { - const { spaces, rooms, directs } = initMatrix.roomList; - if (!( - spaces.has(selectedRoomId) - || rooms.has(selectedRoomId) - || directs.has(selectedRoomId)) - ) { - selectRoom(null); - } - }; - - useEffect(() => { - navigation.on(cons.events.navigation.ROOM_SELECTED, selectHandler); - initMatrix.roomList.on(cons.events.roomList.ROOMLIST_UPDATED, handleDataChanges); - - return () => { - navigation.removeListener(cons.events.navigation.ROOM_SELECTED, selectHandler); - initMatrix.roomList.removeListener(cons.events.roomList.ROOMLIST_UPDATED, handleDataChanges); - }; - }, []); - useEffect(() => { - initMatrix.roomList.on(cons.events.roomList.ROOMLIST_UPDATED, onRoomListChange); - - return () => { - initMatrix.roomList.removeListener(cons.events.roomList.ROOMLIST_UPDATED, onRoomListChange); - }; - }, [selectedRoomId]); - - return ( -
- { - activeTab === 'home' - ? - : - } -
- ); -} -Channels.propTypes = { - activeTab: PropTypes.string.isRequired, -}; - function Drawer() { const [activeTab, setActiveTab] = useState('home'); @@ -219,7 +42,13 @@ function Drawer() {
- +
+ { + activeTab === 'home' + ? + : + } +
diff --git a/src/app/organisms/navigation/DrawerHeader.jsx b/src/app/organisms/navigation/DrawerHeader.jsx new file mode 100644 index 00000000..c86b09b3 --- /dev/null +++ b/src/app/organisms/navigation/DrawerHeader.jsx @@ -0,0 +1,55 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import { + openPublicChannels, openCreateChannel, openInviteUser, +} from '../../../client/action/navigation'; + +import Text from '../../atoms/text/Text'; +import Header, { TitleWrapper } from '../../atoms/header/Header'; +import IconButton from '../../atoms/button/IconButton'; +import ContextMenu, { MenuItem, MenuHeader } from '../../atoms/context-menu/ContextMenu'; + +import PlusIC from '../../../../public/res/ic/outlined/plus.svg'; +import HashPlusIC from '../../../../public/res/ic/outlined/hash-plus.svg'; +import HashSearchIC from '../../../../public/res/ic/outlined/hash-search.svg'; + +function DrawerHeader({ activeTab }) { + return ( +
+ + {(activeTab === 'home' ? 'Home' : 'Direct messages')} + + {(activeTab === 'dm') + ? openInviteUser()} tooltip="Start DM" src={PlusIC} size="normal" /> + : ( + ( + <> + Add channel + { hideMenu(); openCreateChannel(); }} + > + Create new channel + + { hideMenu(); openPublicChannels(); }} + > + Add Public channel + + + )} + render={(toggleMenu) => ()} + /> + )} + {/* ''} tooltip="Menu" src={VerticalMenuIC} size="normal" /> */} +
+ ); +} +DrawerHeader.propTypes = { + activeTab: PropTypes.string.isRequired, +}; + +export default DrawerHeader; diff --git a/src/app/organisms/navigation/Home.jsx b/src/app/organisms/navigation/Home.jsx new file mode 100644 index 00000000..80cd3c0b --- /dev/null +++ b/src/app/organisms/navigation/Home.jsx @@ -0,0 +1,86 @@ +import React, { useState, useEffect } from 'react'; + +import initMatrix from '../../../client/initMatrix'; +import cons from '../../../client/state/cons'; +import navigation from '../../../client/state/navigation'; +import { selectRoom } from '../../../client/action/navigation'; +import Postie from '../../../util/Postie'; + +import Text from '../../atoms/text/Text'; +import Selector from './Selector'; + +import { AtoZ } from './common'; + +const drawerPostie = new Postie(); +function Home() { + const { roomList } = initMatrix; + const spaceIds = [...roomList.spaces].sort(AtoZ); + const roomIds = [...roomList.rooms].sort(AtoZ); + + const [, forceUpdate] = useState({}); + + function selectorChanged(activeRoomID, prevActiveRoomId) { + if (!drawerPostie.hasTopic('selector-change')) return; + const addresses = []; + if (drawerPostie.hasSubscriber('selector-change', activeRoomID)) addresses.push(activeRoomID); + if (drawerPostie.hasSubscriber('selector-change', prevActiveRoomId)) addresses.push(prevActiveRoomId); + if (addresses.length === 0) return; + drawerPostie.post('selector-change', addresses, activeRoomID); + } + function unreadChanged(roomId) { + if (!drawerPostie.hasTopic('unread-change')) return; + if (!drawerPostie.hasSubscriber('unread-change', roomId)) return; + drawerPostie.post('unread-change', roomId); + } + + function roomListUpdated() { + const { spaces, rooms, directs } = initMatrix.roomList; + if (!( + spaces.has(navigation.getActiveRoomId()) + || rooms.has(navigation.getActiveRoomId()) + || directs.has(navigation.getActiveRoomId())) + ) { + selectRoom(null); + } + forceUpdate({}); + } + + 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); + 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); + }; + }, []); + + return ( + <> + { spaceIds.length !== 0 && Spaces } + { spaceIds.map((id) => ( + + ))} + + { roomIds.length !== 0 && Channels } + { roomIds.map((id) => ( + + )) } + + ); +} + +export default Home; diff --git a/src/app/organisms/navigation/Selector.jsx b/src/app/organisms/navigation/Selector.jsx new file mode 100644 index 00000000..c90fc854 --- /dev/null +++ b/src/app/organisms/navigation/Selector.jsx @@ -0,0 +1,76 @@ +/* eslint-disable react/prop-types */ +import React, { useState, useEffect } from 'react'; +import PropTypes from 'prop-types'; + +import initMatrix from '../../../client/initMatrix'; +import { doesRoomHaveUnread } from '../../../util/matrixUtil'; +import { selectRoom } from '../../../client/action/navigation'; +import navigation from '../../../client/state/navigation'; + +import ChannelSelector from '../../molecules/channel-selector/ChannelSelector'; + +import HashIC from '../../../../public/res/ic/outlined/hash.svg'; +import HashLockIC from '../../../../public/res/ic/outlined/hash-lock.svg'; +import SpaceIC from '../../../../public/res/ic/outlined/space.svg'; +import SpaceLockIC from '../../../../public/res/ic/outlined/space-lock.svg'; + +function Selector({ roomId, isDM, drawerPostie }) { + const mx = initMatrix.matrixClient; + const room = mx.getRoom(roomId); + const imageSrc = room.getAvatarFallbackMember()?.getAvatarUrl(mx.baseUrl, 24, 24, 'crop') || null; + + const [isSelected, setIsSelected] = useState(navigation.getActiveRoomId() === roomId); + const [, forceUpdate] = useState({}); + + function selectorChanged(activeRoomId) { + setIsSelected(activeRoomId === roomId); + } + function changeNotificationBadge() { + forceUpdate({}); + } + + useEffect(() => { + drawerPostie.subscribe('selector-change', roomId, selectorChanged); + drawerPostie.subscribe('unread-change', roomId, changeNotificationBadge); + return () => { + drawerPostie.unsubscribe('selector-change', roomId); + drawerPostie.unsubscribe('unread-change', roomId); + }; + }, []); + + return ( + { + if (room.isSpaceRoom()) { + return (room.getJoinRule() === 'invite' ? SpaceLockIC : SpaceIC); + } + return (room.getJoinRule() === 'invite' ? HashLockIC : HashIC); + })() + } + isSelected={isSelected} + isUnread={doesRoomHaveUnread(room)} + notificationCount={room.getUnreadNotificationCount('total') || 0} + isAlert={room.getUnreadNotificationCount('highlight') !== 0} + onClick={() => selectRoom(roomId)} + /> + ); +} + +Selector.defaultProps = { + isDM: true, +}; + +Selector.propTypes = { + roomId: PropTypes.string.isRequired, + isDM: PropTypes.bool, + drawerPostie: PropTypes.shape({}).isRequired, +}; + +export default Selector; diff --git a/src/app/organisms/navigation/common.js b/src/app/organisms/navigation/common.js new file mode 100644 index 00000000..27cc676c --- /dev/null +++ b/src/app/organisms/navigation/common.js @@ -0,0 +1,21 @@ +import initMatrix from '../../../client/initMatrix'; + +function AtoZ(aId, bId) { + let aName = initMatrix.matrixClient.getRoom(aId).name; + let bName = initMatrix.matrixClient.getRoom(bId).name; + + // remove "#" from the room name + // To ignore it in sorting + aName = aName.replaceAll('#', ''); + bName = bName.replaceAll('#', ''); + + if (aName.toLowerCase() < bName.toLowerCase()) { + return -1; + } + if (aName.toLowerCase() > bName.toLowerCase()) { + return 1; + } + return 0; +} + +export { AtoZ }; diff --git a/src/client/state/RoomList.js b/src/client/state/RoomList.js index c9f3ca5e..428d1040 100644 --- a/src/client/state/RoomList.js +++ b/src/client/state/RoomList.js @@ -155,12 +155,13 @@ class RoomList extends EventEmitter { this.matrixClient.on('Room.name', () => { this.emit(cons.events.roomList.ROOMLIST_UPDATED); }); - this.matrixClient.on('Room.receipt', (event) => { + this.matrixClient.on('Room.receipt', (event, room) => { if (event.getType() === 'm.receipt') { - const evContent = event.getContent(); - const userId = Object.keys(evContent[Object.keys(evContent)[0]]['m.read'])[0]; - if (userId !== this.matrixClient.getUserId()) return; - this.emit(cons.events.roomList.ROOMLIST_UPDATED); + 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); } }); @@ -280,8 +281,13 @@ class RoomList extends EventEmitter { this.emit(cons.events.roomList.ROOMLIST_UPDATED); }); - this.matrixClient.on('Room.timeline', () => { - 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); }); } } diff --git a/src/client/state/cons.js b/src/client/state/cons.js index 2ff8c5e2..b5de3d66 100644 --- a/src/client/state/cons.js +++ b/src/client/state/cons.js @@ -50,6 +50,8 @@ const cons = { ROOM_JOINED: 'ROOM_JOINED', ROOM_LEAVED: 'ROOM_LEAVED', ROOM_CREATED: 'ROOM_CREATED', + MY_RECEIPT_ARRIVED: 'MY_RECEIPT_ARRIVED', + EVENT_ARRIVED: 'EVENT_ARRIVED', }, roomTimeline: { EVENT: 'EVENT', diff --git a/src/client/state/navigation.js b/src/client/state/navigation.js index 6dcf39fd..1aa6c0c2 100644 --- a/src/client/state/navigation.js +++ b/src/client/state/navigation.js @@ -6,7 +6,7 @@ class Navigation extends EventEmitter { constructor() { super(); - this.activeTab = 'channels'; + this.activeTab = 'home'; this.activeRoomId = null; this.isPeopleDrawerVisible = true; } @@ -26,8 +26,9 @@ class Navigation extends EventEmitter { this.emit(cons.events.navigation.TAB_CHANGED, this.activeTab); }, [cons.actions.navigation.SELECT_ROOM]: () => { + const prevActiveRoomId = this.activeRoomId; this.activeRoomId = action.roomId; - this.emit(cons.events.navigation.ROOM_SELECTED, this.activeRoomId); + this.emit(cons.events.navigation.ROOM_SELECTED, this.activeRoomId, prevActiveRoomId); }, [cons.actions.navigation.TOGGLE_PEOPLE_DRAWER]: () => { this.isPeopleDrawerVisible = !this.isPeopleDrawerVisible;