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/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/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/state/RoomList.js b/src/client/state/RoomList.js index a47bf46..244c735 100644 --- a/src/client/state/RoomList.js +++ b/src/client/state/RoomList.js @@ -386,6 +386,7 @@ class RoomList extends EventEmitter { const lastTimelineEvent = room.timeline[room.timeline.length - 1]; if (lastTimelineEvent.getId() !== event.getId()) return; + if (event.getSender() === this.matrixClient.getUserId()) return; this.emit(cons.events.roomList.EVENT_ARRIVED, room.roomId); }); } diff --git a/src/client/state/cons.js b/src/client/state/cons.js index 7587120..6c00668 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', 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/util/common.js b/src/util/common.js index 78bb349..938ced5 100644 --- a/src/util/common.js +++ b/src/util/common.js @@ -19,3 +19,12 @@ 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, + }; +}