{
@@ -182,6 +249,11 @@ function RoomPermissions({ roomId }) {
content={
{permInfo.description}}
options={(
@@ -31,6 +33,7 @@ function RoomSelectorWrapper({
}
RoomSelectorWrapper.defaultProps = {
options: null,
+ onContextMenu: null,
};
RoomSelectorWrapper.propTypes = {
isSelected: PropTypes.bool.isRequired,
@@ -38,12 +41,13 @@ RoomSelectorWrapper.propTypes = {
onClick: PropTypes.func.isRequired,
content: PropTypes.node.isRequired,
options: PropTypes.node,
+ onContextMenu: PropTypes.func,
};
function RoomSelector({
name, parentName, roomId, imageSrc, iconSrc,
isSelected, isUnread, notificationCount, isAlert,
- options, onClick,
+ options, onClick, onContextMenu,
}) {
return (
);
}
@@ -87,6 +92,7 @@ RoomSelector.defaultProps = {
imageSrc: null,
iconSrc: null,
options: null,
+ onContextMenu: null,
};
RoomSelector.propTypes = {
name: PropTypes.string.isRequired,
@@ -103,6 +109,7 @@ RoomSelector.propTypes = {
isAlert: PropTypes.bool.isRequired,
options: PropTypes.node,
onClick: PropTypes.func.isRequired,
+ onContextMenu: PropTypes.func,
};
export default RoomSelector;
diff --git a/src/app/organisms/navigation/Selector.jsx b/src/app/organisms/navigation/Selector.jsx
index 80a03574..e321db82 100644
--- a/src/app/organisms/navigation/Selector.jsx
+++ b/src/app/organisms/navigation/Selector.jsx
@@ -4,12 +4,13 @@ import PropTypes from 'prop-types';
import initMatrix from '../../../client/initMatrix';
import navigation from '../../../client/state/navigation';
-import { openRoomOptions } from '../../../client/action/navigation';
+import { openReusableContextMenu } 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';
+import RoomOptions from '../../molecules/room-optons/RoomOptions';
import HashIC from '../../../../public/res/ic/outlined/hash.svg';
import HashGlobeIC from '../../../../public/res/ic/outlined/hash-globe.svg';
@@ -49,6 +50,15 @@ function Selector({
};
}, []);
+ const openRoomOptions = (e) => {
+ e.preventDefault();
+ openReusableContextMenu(
+ 'right',
+ getEventCords(e, '.room-selector'),
+ (closeMenu) =>
,
+ );
+ };
+
const joinRuleToIconSrc = (joinRule) => ({
restricted: () => (room.isSpaceRoom() ? SpaceIC : HashIC),
invite: () => (room.isSpaceRoom() ? SpaceLockIC : HashLockIC),
@@ -96,13 +106,14 @@ function Selector({
notificationCount={abbreviateNumber(noti.getTotalNoti(roomId))}
isAlert={noti.getHighlightNoti(roomId) !== 0}
onClick={onClick}
+ onContextMenu={openRoomOptions}
options={(
openRoomOptions(getEventCords(e), roomId)}
+ onClick={openRoomOptions}
/>
)}
/>
diff --git a/src/app/organisms/profile-viewer/ProfileViewer.jsx b/src/app/organisms/profile-viewer/ProfileViewer.jsx
index 73b722e5..23890125 100644
--- a/src/app/organisms/profile-viewer/ProfileViewer.jsx
+++ b/src/app/organisms/profile-viewer/ProfileViewer.jsx
@@ -7,26 +7,87 @@ import { twemojify } from '../../../util/twemojify';
import initMatrix from '../../../client/initMatrix';
import cons from '../../../client/state/cons';
import navigation from '../../../client/state/navigation';
-import { selectRoom } from '../../../client/action/navigation';
+import { selectRoom, openReusableContextMenu } from '../../../client/action/navigation';
import * as roomActions from '../../../client/action/room';
import { getUsername, getUsernameOfRoomMember, getPowerLabel } from '../../../util/matrixUtil';
+import { getEventCords } from '../../../util/common';
import colorMXID from '../../../util/colorMXID';
import Text from '../../atoms/text/Text';
import Chip from '../../atoms/chip/Chip';
import IconButton from '../../atoms/button/IconButton';
+import Input from '../../atoms/input/Input';
import Avatar from '../../atoms/avatar/Avatar';
import Button from '../../atoms/button/Button';
+import { MenuItem } from '../../atoms/context-menu/ContextMenu';
+import PowerLevelSelector from '../../molecules/power-level-selector/PowerLevelSelector';
import Dialog from '../../molecules/dialog/Dialog';
-import SettingTile from '../../molecules/setting-tile/SettingTile';
import ShieldEmptyIC from '../../../../public/res/ic/outlined/shield-empty.svg';
+import ChevronRightIC from '../../../../public/res/ic/outlined/chevron-right.svg';
import ChevronBottomIC from '../../../../public/res/ic/outlined/chevron-bottom.svg';
import CrossIC from '../../../../public/res/ic/outlined/cross.svg';
+import { useForceUpdate } from '../../hooks/useForceUpdate';
+
+function ModerationTools({
+ roomId, userId,
+}) {
+ const mx = initMatrix.matrixClient;
+ const room = mx.getRoom(roomId);
+ const roomMember = room.getMember(userId);
+
+ const myPowerLevel = room.getMember(mx.getUserId()).powerLevel;
+ const powerLevel = roomMember?.powerLevel || 0;
+ const canIKick = (
+ roomMember?.membership === 'join'
+ && room.currentState.hasSufficientPowerLevelFor('kick', myPowerLevel)
+ && powerLevel < myPowerLevel
+ );
+ const canIBan = (
+ ['join', 'leave'].includes(roomMember?.membership)
+ && room.currentState.hasSufficientPowerLevelFor('ban', myPowerLevel)
+ && powerLevel < myPowerLevel
+ );
+
+ const handleKick = (e) => {
+ e.preventDefault();
+ const kickReason = e.target.elements['kick-reason']?.value.trim();
+ roomActions.kick(roomId, userId, kickReason !== '' ? kickReason : undefined);
+ };
+
+ const handleBan = (e) => {
+ e.preventDefault();
+ const banReason = e.target.elements['ban-reason']?.value.trim();
+ roomActions.ban(roomId, userId, banReason !== '' ? banReason : undefined);
+ };
+
+ return (
+
+ {canIKick && (
+
+ )}
+ {canIBan && (
+
+ )}
+
+ );
+}
+ModerationTools.propTypes = {
+ roomId: PropTypes.string.isRequired,
+ userId: PropTypes.string.isRequired,
+};
+
function SessionInfo({ userId }) {
const [devices, setDevices] = useState(null);
+ const [isVisible, setIsVisible] = useState(false);
const mx = initMatrix.matrixClient;
useEffect(() => {
@@ -51,10 +112,11 @@ function SessionInfo({ userId }) {
}, [userId]);
function renderSessionChips() {
+ if (!isVisible) return null;
return (
- {devices === null && Loading sessions...}
- {devices?.length === 0 && No session found.}
+ {devices === null && Loading sessions...}
+ {devices?.length === 0 && No session found.}
{devices !== null && (devices.map((device) => (
-
+
+ {renderSessionChips()}
);
}
@@ -98,6 +163,8 @@ function ProfileFooter({ roomId, userId, onRequestClose }) {
const userPL = room.getMember(userId)?.powerLevel || 0;
const canIKick = room.currentState.hasSufficientPowerLevelFor('kick', myPowerlevel) && userPL < myPowerlevel;
+ const isBanned = member?.membership === 'ban';
+
const onCreated = (dmRoomId) => {
if (isMountedRef.current === false) return;
setIsCreatingDM(false);
@@ -119,7 +186,7 @@ function ProfileFooter({ roomId, userId, onRequestClose }) {
setIsInviting(false);
}, [userId]);
- async function openDM() {
+ const openDM = async () => {
const directIds = [...initMatrix.roomList.directs];
// Check and open if user already have a DM with userId.
@@ -145,9 +212,9 @@ function ProfileFooter({ roomId, userId, onRequestClose }) {
if (isMountedRef.current === false) return;
setIsCreatingDM(false);
}
- }
+ };
- async function toggleIgnore() {
+ const toggleIgnore = async () => {
const ignoredUsers = mx.getIgnoredUsers();
const uIndex = ignoredUsers.indexOf(userId);
if (uIndex >= 0) {
@@ -165,9 +232,9 @@ function ProfileFooter({ roomId, userId, onRequestClose }) {
} catch {
setIsIgnoring(false);
}
- }
+ };
- async function toggleInvite() {
+ const toggleInvite = async () => {
try {
setIsInviting(true);
let isInviteSent = false;
@@ -182,7 +249,7 @@ function ProfileFooter({ roomId, userId, onRequestClose }) {
} catch {
setIsInviting(false);
}
- }
+ };
return (
@@ -193,7 +260,14 @@ function ProfileFooter({ roomId, userId, onRequestClose }) {
>
{isCreatingDM ? 'Creating room...' : 'Message'}
- { member?.membership === 'join' &&
}
+ { isBanned && canIKick && (
+
+ )}
{ (isInvited ? canIKick : room.canInvite(mx.getUserId())) && isInvitable && (
);
}
diff --git a/src/client/action/navigation.js b/src/client/action/navigation.js
index 763e2036..44fcf173 100644
--- a/src/client/action/navigation.js
+++ b/src/client/action/navigation.js
@@ -87,14 +87,6 @@ export function openReadReceipts(roomId, userIds) {
});
}
-export function openRoomOptions(cords, roomId) {
- appDispatcher.dispatch({
- type: cons.actions.navigation.OPEN_ROOMOPTIONS,
- cords,
- roomId,
- });
-}
-
export function openAttachmentTypeSelector(params) {
appDispatcher.dispatch({
type: cons.actions.navigation.OPEN_ATTACHMENT_TYPE_SELECTOR,
@@ -102,15 +94,6 @@ export function openAttachmentTypeSelector(params) {
});
}
-export function replyTo(userId, eventId, body) {
- appDispatcher.dispatch({
- type: cons.actions.navigation.CLICK_REPLY_TO,
- userId,
- eventId,
- body,
- });
-}
-
export function openSearch(term) {
appDispatcher.dispatch({
type: cons.actions.navigation.OPEN_SEARCH,
@@ -118,11 +101,12 @@ export function openSearch(term) {
});
}
-export function openReusableContextMenu(placement, cords, render) {
+export function openReusableContextMenu(placement, cords, render, afterClose) {
appDispatcher.dispatch({
type: cons.actions.navigation.OPEN_REUSABLE_CONTEXT_MENU,
placement,
cords,
render,
+ afterClose,
});
}
diff --git a/src/client/action/room.js b/src/client/action/room.js
index 5fe5faac..9849b2e0 100644
--- a/src/client/action/room.js
+++ b/src/client/action/room.js
@@ -192,10 +192,34 @@ async function invite(roomId, userId) {
return result;
}
-async function kick(roomId, userId) {
+async function kick(roomId, userId, reason) {
const mx = initMatrix.matrixClient;
- const result = await mx.kick(roomId, userId);
+ const result = await mx.kick(roomId, userId, reason);
+ return result;
+}
+
+async function ban(roomId, userId, reason) {
+ const mx = initMatrix.matrixClient;
+
+ const result = await mx.ban(roomId, userId, reason);
+ return result;
+}
+
+async function unban(roomId, userId) {
+ const mx = initMatrix.matrixClient;
+
+ const result = await mx.unban(roomId, userId);
+ return result;
+}
+
+async function setPowerLevel(roomId, userId, powerLevel) {
+ const mx = initMatrix.matrixClient;
+ const room = mx.getRoom(roomId);
+
+ const powerlevelEvent = room.currentState.getStateEvents('m.room.power_levels')[0];
+
+ const result = await mx.setPowerLevel(roomId, userId, powerLevel, powerlevelEvent);
return result;
}
@@ -215,6 +239,7 @@ function deleteSpaceShortcut(roomId) {
export {
join, leave,
- create, invite, kick,
+ create, invite, kick, ban, unban,
+ setPowerLevel,
createSpaceShortcut, deleteSpaceShortcut,
};
diff --git a/src/client/state/cons.js b/src/client/state/cons.js
index 15696255..b0dee9c4 100644
--- a/src/client/state/cons.js
+++ b/src/client/state/cons.js
@@ -39,7 +39,6 @@ const cons = {
OPEN_SETTINGS: 'OPEN_SETTINGS',
OPEN_EMOJIBOARD: 'OPEN_EMOJIBOARD',
OPEN_READRECEIPTS: 'OPEN_READRECEIPTS',
- OPEN_ROOMOPTIONS: 'OPEN_ROOMOPTIONS',
CLICK_REPLY_TO: 'CLICK_REPLY_TO',
OPEN_SEARCH: 'OPEN_SEARCH',
OPEN_ATTACHMENT_TYPE_SELECTOR: 'OPEN_ATTACHMENT_TYPE_SELECTOR',
@@ -77,7 +76,6 @@ const cons = {
PROFILE_VIEWER_OPENED: 'PROFILE_VIEWER_OPENED',
EMOJIBOARD_OPENED: 'EMOJIBOARD_OPENED',
READRECEIPTS_OPENED: 'READRECEIPTS_OPENED',
- ROOMOPTIONS_OPENED: 'ROOMOPTIONS_OPENED',
REPLY_TO_CLICKED: 'REPLY_TO_CLICKED',
OPEN_ATTACHMENT_TYPE_SELECTOR: 'OPEN_ATTACHMENT_TYPE_SELECTOR',
SEARCH_OPENED: 'SEARCH_OPENED',
diff --git a/src/client/state/navigation.js b/src/client/state/navigation.js
index 0acdc09b..5a34f07e 100644
--- a/src/client/state/navigation.js
+++ b/src/client/state/navigation.js
@@ -126,13 +126,6 @@ class Navigation extends EventEmitter {
action.userIds,
);
},
- [cons.actions.navigation.OPEN_ROOMOPTIONS]: () => {
- this.emit(
- cons.events.navigation.ROOMOPTIONS_OPENED,
- action.cords,
- action.roomId,
- );
- },
[cons.actions.navigation.OPEN_ATTACHMENT_TYPE_SELECTOR]: () => {
this.emit(
cons.events.navigation.OPEN_ATTACHMENT_TYPE_SELECTOR,
@@ -160,6 +153,7 @@ class Navigation extends EventEmitter {
action.placement,
action.cords,
action.render,
+ action.afterClose,
);
},
};