diff --git a/public/res/ic/outlined/bell-off.svg b/public/res/ic/outlined/bell-off.svg
new file mode 100644
index 00000000..79ce8a33
--- /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 00000000..3431bea1
--- /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 00000000..57fc2679
--- /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 d3d2f6db..43d470b5 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 00000000..0c890085
--- /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 00000000..ae3f9c3f
--- /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 5fa13040..d11aceb7 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 a47bf469..244c7352 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 75871201..6c00668a 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 5188aad8..d7dabd78 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 78bb349b..938ced54 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,
+ };
+}