Merge branch 'dev' into voicemail
This commit is contained in:
commit
6dc7cf9af5
19 changed files with 464 additions and 237 deletions
|
@ -24,11 +24,12 @@ You can serve the application with a webserver of your choosing by simply copyin
|
||||||
Execute the following commands to compile the app from its source code:
|
Execute the following commands to compile the app from its source code:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
npm install # Installs all dependencies
|
npm ci # Installs all dependencies
|
||||||
npm run build # Compiles the app into the dist/ directory
|
npm run build # Compiles the app into the dist/ directory
|
||||||
```
|
```
|
||||||
|
|
||||||
You can then copy the files to a webserver's webroot of your choice.
|
You can then copy the files to a webserver's webroot of your choice.
|
||||||
|
|
||||||
To serve a development version of the app locally for testing, you may also use the command `npm start`.
|
To serve a development version of the app locally for testing, you may also use the command `npm start`.
|
||||||
|
|
||||||
### Running with Docker
|
### Running with Docker
|
||||||
|
|
|
@ -23,12 +23,14 @@ function ReusableContextMenu() {
|
||||||
openerRef.current.style.height = `${cords.height}px`;
|
openerRef.current.style.height = `${cords.height}px`;
|
||||||
openerRef.current.click();
|
openerRef.current.click();
|
||||||
}
|
}
|
||||||
const handleContextMenuOpen = (placement, cords, render) => {
|
const handleContextMenuOpen = (placement, cords, render, afterClose) => {
|
||||||
if (key) {
|
if (key) {
|
||||||
closeMenu();
|
closeMenu();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setData({ placement, cords, render });
|
setData({
|
||||||
|
placement, cords, render, afterClose,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
navigation.on(cons.events.navigation.REUSABLE_CONTEXT_MENU_OPENED, handleContextMenuOpen);
|
navigation.on(cons.events.navigation.REUSABLE_CONTEXT_MENU_OPENED, handleContextMenuOpen);
|
||||||
return () => {
|
return () => {
|
||||||
|
@ -44,6 +46,7 @@ function ReusableContextMenu() {
|
||||||
key = Math.random();
|
key = Math.random();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
data?.afterClose?.();
|
||||||
if (setData) setData(null);
|
if (setData) setData(null);
|
||||||
|
|
||||||
if (key === null) return;
|
if (key === null) return;
|
||||||
|
|
|
@ -303,23 +303,40 @@
|
||||||
|
|
||||||
// markdown formating
|
// markdown formating
|
||||||
.message__body {
|
.message__body {
|
||||||
|
& h1, h2, h3, h4, h5, h6 {
|
||||||
|
margin: 0;
|
||||||
|
margin-bottom: var(--sp-ultra-tight);
|
||||||
|
font-weight: var(--fw-medium);
|
||||||
|
&:first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
& h1,
|
& h1,
|
||||||
& h2 {
|
& h2 {
|
||||||
color: var(--tc-surface-high);
|
color: var(--tc-surface-high);
|
||||||
margin: var(--sp-loose) 0 var(--sp-normal);
|
margin-top: var(--sp-normal);
|
||||||
line-height: var(--lh-h1);
|
font-size: var(--fs-h2);
|
||||||
|
line-height: var(--lh-h2);
|
||||||
|
letter-spacing: var(--ls-h2);
|
||||||
}
|
}
|
||||||
& h3,
|
& h3,
|
||||||
& h4 {
|
& h4 {
|
||||||
color: var(--tc-surface-high);
|
color: var(--tc-surface-high);
|
||||||
margin: var(--sp-normal) 0 var(--sp-tight);
|
margin-top: var(--sp-tight);
|
||||||
line-height: var(--lh-h2);
|
font-size: var(--fs-s1);
|
||||||
|
line-height: var(--lh-s1);
|
||||||
|
letter-spacing: var(--ls-s1);
|
||||||
}
|
}
|
||||||
& h5,
|
& h5,
|
||||||
& h6 {
|
& h6 {
|
||||||
color: var(--tc-surface-high);
|
color: var(--tc-surface-high);
|
||||||
margin: var(--sp-tight) 0 var(--sp-extra-tight);
|
margin-top: var(--sp-extra-tight);
|
||||||
line-height: var(--lh-s1);
|
font-size: var(--fs-b1);
|
||||||
|
line-height: var(--lh-b1);
|
||||||
|
letter-spacing: var(--ls-b1);
|
||||||
}
|
}
|
||||||
& hr {
|
& hr {
|
||||||
border-color: var(--bg-divider);
|
border-color: var(--bg-divider);
|
||||||
|
@ -365,7 +382,7 @@
|
||||||
@include scrollbar.scroll--auto-hide;
|
@include scrollbar.scroll--auto-hide;
|
||||||
}
|
}
|
||||||
& pre {
|
& pre {
|
||||||
display: inline-block;
|
width: fit-content;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
@include scrollbar.scroll;
|
@include scrollbar.scroll;
|
||||||
@include scrollbar.scroll__h;
|
@include scrollbar.scroll__h;
|
||||||
|
@ -376,7 +393,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
& blockquote {
|
& blockquote {
|
||||||
display: inline-block;
|
width: fit-content;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
@include dir.side(border, 4px solid var(--bg-surface-active), 0);
|
@include dir.side(border, 4px solid var(--bg-surface-active), 0);
|
||||||
white-space: initial !important;
|
white-space: initial !important;
|
||||||
|
|
|
@ -11,7 +11,7 @@ function PowerLevelSelector({
|
||||||
value, max, onSelect,
|
value, max, onSelect,
|
||||||
}) {
|
}) {
|
||||||
const handleSubmit = (e) => {
|
const handleSubmit = (e) => {
|
||||||
const powerLevel = e.target.elements['power-level'];
|
const powerLevel = e.target.elements['power-level']?.value;
|
||||||
if (!powerLevel) return;
|
if (!powerLevel) return;
|
||||||
onSelect(Number(powerLevel));
|
onSelect(Number(powerLevel));
|
||||||
};
|
};
|
||||||
|
|
67
src/app/molecules/room-optons/RoomOptions.jsx
Normal file
67
src/app/molecules/room-optons/RoomOptions.jsx
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import { twemojify } from '../../../util/twemojify';
|
||||||
|
|
||||||
|
import initMatrix from '../../../client/initMatrix';
|
||||||
|
import { openInviteUser } from '../../../client/action/navigation';
|
||||||
|
import * as roomActions from '../../../client/action/room';
|
||||||
|
|
||||||
|
import { MenuHeader, MenuItem } from '../../atoms/context-menu/ContextMenu';
|
||||||
|
import RoomNotification from '../room-notification/RoomNotification';
|
||||||
|
|
||||||
|
import TickMarkIC from '../../../../public/res/ic/outlined/tick-mark.svg';
|
||||||
|
import AddUserIC from '../../../../public/res/ic/outlined/add-user.svg';
|
||||||
|
import LeaveArrowIC from '../../../../public/res/ic/outlined/leave-arrow.svg';
|
||||||
|
|
||||||
|
function RoomOptions({ roomId, afterOptionSelect }) {
|
||||||
|
const mx = initMatrix.matrixClient;
|
||||||
|
const room = mx.getRoom(roomId);
|
||||||
|
const canInvite = room?.canInvite(mx.getUserId());
|
||||||
|
|
||||||
|
const handleMarkAsRead = () => {
|
||||||
|
afterOptionSelect();
|
||||||
|
if (!room) return;
|
||||||
|
const events = room.getLiveTimeline().getEvents();
|
||||||
|
mx.sendReadReceipt(events[events.length - 1]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInviteClick = () => {
|
||||||
|
openInviteUser(roomId);
|
||||||
|
afterOptionSelect();
|
||||||
|
};
|
||||||
|
const handleLeaveClick = () => {
|
||||||
|
if (confirm('Are you really want to leave this room?')) {
|
||||||
|
roomActions.leave(roomId);
|
||||||
|
afterOptionSelect();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<MenuHeader>{twemojify(`Options for ${initMatrix.matrixClient.getRoom(roomId)?.name}`)}</MenuHeader>
|
||||||
|
<MenuItem iconSrc={TickMarkIC} onClick={handleMarkAsRead}>Mark as read</MenuItem>
|
||||||
|
<MenuItem
|
||||||
|
iconSrc={AddUserIC}
|
||||||
|
onClick={handleInviteClick}
|
||||||
|
disabled={!canInvite}
|
||||||
|
>
|
||||||
|
Invite
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem iconSrc={LeaveArrowIC} variant="danger" onClick={handleLeaveClick}>Leave</MenuItem>
|
||||||
|
<MenuHeader>Notification</MenuHeader>
|
||||||
|
<RoomNotification roomId={roomId} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
RoomOptions.defaultProps = {
|
||||||
|
afterOptionSelect: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
RoomOptions.propTypes = {
|
||||||
|
roomId: PropTypes.string.isRequired,
|
||||||
|
afterOptionSelect: PropTypes.func,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RoomOptions;
|
|
@ -1,17 +1,22 @@
|
||||||
import React from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import './RoomPermissions.scss';
|
import './RoomPermissions.scss';
|
||||||
|
|
||||||
import initMatrix from '../../../client/initMatrix';
|
import initMatrix from '../../../client/initMatrix';
|
||||||
import { getPowerLabel } from '../../../util/matrixUtil';
|
import { getPowerLabel } from '../../../util/matrixUtil';
|
||||||
|
import { openReusableContextMenu } from '../../../client/action/navigation';
|
||||||
|
import { getEventCords } from '../../../util/common';
|
||||||
|
|
||||||
import Text from '../../atoms/text/Text';
|
import Text from '../../atoms/text/Text';
|
||||||
import Button from '../../atoms/button/Button';
|
import Button from '../../atoms/button/Button';
|
||||||
import { MenuHeader } from '../../atoms/context-menu/ContextMenu';
|
import { MenuHeader } from '../../atoms/context-menu/ContextMenu';
|
||||||
|
import PowerLevelSelector from '../power-level-selector/PowerLevelSelector';
|
||||||
import SettingTile from '../setting-tile/SettingTile';
|
import SettingTile from '../setting-tile/SettingTile';
|
||||||
|
|
||||||
import ChevronBottomIC from '../../../../public/res/ic/outlined/chevron-bottom.svg';
|
import ChevronBottomIC from '../../../../public/res/ic/outlined/chevron-bottom.svg';
|
||||||
|
|
||||||
|
import { useForceUpdate } from '../../hooks/useForceUpdate';
|
||||||
|
|
||||||
const permissionsInfo = {
|
const permissionsInfo = {
|
||||||
users_default: {
|
users_default: {
|
||||||
name: 'Default role',
|
name: 'Default role',
|
||||||
|
@ -23,6 +28,12 @@ const permissionsInfo = {
|
||||||
description: 'Set minimum power level to send messages in room.',
|
description: 'Set minimum power level to send messages in room.',
|
||||||
default: 0,
|
default: 0,
|
||||||
},
|
},
|
||||||
|
'm.reaction': {
|
||||||
|
parent: 'events',
|
||||||
|
name: 'Send reactions',
|
||||||
|
description: 'Set minimum power level to send reactions in room.',
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
redact: {
|
redact: {
|
||||||
name: 'Delete messages sent by others',
|
name: 'Delete messages sent by others',
|
||||||
description: 'Set minimum power level to delete messages in room.',
|
description: 'Set minimum power level to delete messages in room.',
|
||||||
|
@ -130,7 +141,7 @@ const permissionsInfo = {
|
||||||
};
|
};
|
||||||
|
|
||||||
const roomPermsGroups = {
|
const roomPermsGroups = {
|
||||||
'General Permissions': ['users_default', 'events_default', 'redact', 'notifications'],
|
'General Permissions': ['users_default', 'events_default', 'm.reaction', 'redact', 'notifications'],
|
||||||
'Manage members permissions': ['invite', 'kick', 'ban'],
|
'Manage members permissions': ['invite', 'kick', 'ban'],
|
||||||
'Room profile permissions': ['m.room.avatar', 'm.room.name', 'm.room.topic'],
|
'Room profile permissions': ['m.room.avatar', 'm.room.name', 'm.room.topic'],
|
||||||
'Settings permissions': ['state_default', 'm.room.canonical_alias', 'm.room.power_levels', 'm.room.encryption', 'm.room.history_visibility'],
|
'Settings permissions': ['state_default', 'm.room.canonical_alias', 'm.room.power_levels', 'm.room.encryption', 'm.room.history_visibility'],
|
||||||
|
@ -144,13 +155,69 @@ const spacePermsGroups = {
|
||||||
'Settings permissions': ['state_default', 'm.room.canonical_alias', 'm.room.power_levels'],
|
'Settings permissions': ['state_default', 'm.room.canonical_alias', 'm.room.power_levels'],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function useRoomStateUpdate(roomId) {
|
||||||
|
const [, forceUpdate] = useForceUpdate();
|
||||||
|
const mx = initMatrix.matrixClient;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleStateEvent = (event) => {
|
||||||
|
if (event.getRoomId() !== roomId) return;
|
||||||
|
forceUpdate();
|
||||||
|
};
|
||||||
|
|
||||||
|
mx.on('RoomState.events', handleStateEvent);
|
||||||
|
return () => {
|
||||||
|
mx.removeListener('RoomState.events', handleStateEvent);
|
||||||
|
};
|
||||||
|
}, [roomId]);
|
||||||
|
}
|
||||||
|
|
||||||
function RoomPermissions({ roomId }) {
|
function RoomPermissions({ roomId }) {
|
||||||
|
useRoomStateUpdate(roomId);
|
||||||
const mx = initMatrix.matrixClient;
|
const mx = initMatrix.matrixClient;
|
||||||
const room = mx.getRoom(roomId);
|
const room = mx.getRoom(roomId);
|
||||||
const pLEvent = room.currentState.getStateEvents('m.room.power_levels')[0];
|
const pLEvent = room.currentState.getStateEvents('m.room.power_levels')[0];
|
||||||
const permissions = pLEvent.getContent();
|
const permissions = pLEvent.getContent();
|
||||||
const canChangePermission = room.currentState.maySendStateEvent('m.room.power_levels', mx.getUserId());
|
const canChangePermission = room.currentState.maySendStateEvent('m.room.power_levels', mx.getUserId());
|
||||||
|
|
||||||
|
const handlePowerSelector = (e, permKey, parentKey, powerLevel) => {
|
||||||
|
const handlePowerLevelChange = (newPowerLevel) => {
|
||||||
|
if (powerLevel === newPowerLevel) return;
|
||||||
|
|
||||||
|
const newPermissions = { ...permissions };
|
||||||
|
if (parentKey) {
|
||||||
|
newPermissions[parentKey] = {
|
||||||
|
...permissions[parentKey],
|
||||||
|
[permKey]: newPowerLevel,
|
||||||
|
};
|
||||||
|
} else if (permKey === 'notifications') {
|
||||||
|
newPermissions[permKey] = {
|
||||||
|
...permissions[permKey],
|
||||||
|
room: newPowerLevel,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
newPermissions[permKey] = newPowerLevel;
|
||||||
|
}
|
||||||
|
|
||||||
|
mx.sendStateEvent(roomId, 'm.room.power_levels', newPermissions);
|
||||||
|
};
|
||||||
|
|
||||||
|
openReusableContextMenu(
|
||||||
|
'bottom',
|
||||||
|
getEventCords(e, '.btn-surface'),
|
||||||
|
(closeMenu) => (
|
||||||
|
<PowerLevelSelector
|
||||||
|
value={powerLevel}
|
||||||
|
max={100}
|
||||||
|
onSelect={(pl) => {
|
||||||
|
closeMenu();
|
||||||
|
handlePowerLevelChange(pl);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="room-permissions">
|
<div className="room-permissions">
|
||||||
{
|
{
|
||||||
|
@ -182,6 +249,11 @@ function RoomPermissions({ roomId }) {
|
||||||
content={<Text variant="b3">{permInfo.description}</Text>}
|
content={<Text variant="b3">{permInfo.description}</Text>}
|
||||||
options={(
|
options={(
|
||||||
<Button
|
<Button
|
||||||
|
onClick={
|
||||||
|
canChangePermission
|
||||||
|
? (e) => handlePowerSelector(e, permKey, permInfo.parent, powerLevel)
|
||||||
|
: null
|
||||||
|
}
|
||||||
iconSrc={canChangePermission ? ChevronBottomIC : null}
|
iconSrc={canChangePermission ? ChevronBottomIC : null}
|
||||||
>
|
>
|
||||||
<Text variant="b2">
|
<Text variant="b2">
|
||||||
|
|
|
@ -11,7 +11,8 @@ import NotificationBadge from '../../atoms/badge/NotificationBadge';
|
||||||
import { blurOnBubbling } from '../../atoms/button/script';
|
import { blurOnBubbling } from '../../atoms/button/script';
|
||||||
|
|
||||||
function RoomSelectorWrapper({
|
function RoomSelectorWrapper({
|
||||||
isSelected, isUnread, onClick, content, options,
|
isSelected, isUnread, onClick,
|
||||||
|
content, options, onContextMenu,
|
||||||
}) {
|
}) {
|
||||||
let myClass = isUnread ? ' room-selector--unread' : '';
|
let myClass = isUnread ? ' room-selector--unread' : '';
|
||||||
myClass += isSelected ? ' room-selector--selected' : '';
|
myClass += isSelected ? ' room-selector--selected' : '';
|
||||||
|
@ -22,6 +23,7 @@ function RoomSelectorWrapper({
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
onMouseUp={(e) => blurOnBubbling(e, '.room-selector__content')}
|
onMouseUp={(e) => blurOnBubbling(e, '.room-selector__content')}
|
||||||
|
onContextMenu={onContextMenu}
|
||||||
>
|
>
|
||||||
{content}
|
{content}
|
||||||
</button>
|
</button>
|
||||||
|
@ -31,6 +33,7 @@ function RoomSelectorWrapper({
|
||||||
}
|
}
|
||||||
RoomSelectorWrapper.defaultProps = {
|
RoomSelectorWrapper.defaultProps = {
|
||||||
options: null,
|
options: null,
|
||||||
|
onContextMenu: null,
|
||||||
};
|
};
|
||||||
RoomSelectorWrapper.propTypes = {
|
RoomSelectorWrapper.propTypes = {
|
||||||
isSelected: PropTypes.bool.isRequired,
|
isSelected: PropTypes.bool.isRequired,
|
||||||
|
@ -38,12 +41,13 @@ RoomSelectorWrapper.propTypes = {
|
||||||
onClick: PropTypes.func.isRequired,
|
onClick: PropTypes.func.isRequired,
|
||||||
content: PropTypes.node.isRequired,
|
content: PropTypes.node.isRequired,
|
||||||
options: PropTypes.node,
|
options: PropTypes.node,
|
||||||
|
onContextMenu: PropTypes.func,
|
||||||
};
|
};
|
||||||
|
|
||||||
function RoomSelector({
|
function RoomSelector({
|
||||||
name, parentName, roomId, imageSrc, iconSrc,
|
name, parentName, roomId, imageSrc, iconSrc,
|
||||||
isSelected, isUnread, notificationCount, isAlert,
|
isSelected, isUnread, notificationCount, isAlert,
|
||||||
options, onClick,
|
options, onClick, onContextMenu,
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<RoomSelectorWrapper
|
<RoomSelectorWrapper
|
||||||
|
@ -78,6 +82,7 @@ function RoomSelector({
|
||||||
)}
|
)}
|
||||||
options={options}
|
options={options}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
|
onContextMenu={onContextMenu}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -87,6 +92,7 @@ RoomSelector.defaultProps = {
|
||||||
imageSrc: null,
|
imageSrc: null,
|
||||||
iconSrc: null,
|
iconSrc: null,
|
||||||
options: null,
|
options: null,
|
||||||
|
onContextMenu: null,
|
||||||
};
|
};
|
||||||
RoomSelector.propTypes = {
|
RoomSelector.propTypes = {
|
||||||
name: PropTypes.string.isRequired,
|
name: PropTypes.string.isRequired,
|
||||||
|
@ -103,6 +109,7 @@ RoomSelector.propTypes = {
|
||||||
isAlert: PropTypes.bool.isRequired,
|
isAlert: PropTypes.bool.isRequired,
|
||||||
options: PropTypes.node,
|
options: PropTypes.node,
|
||||||
onClick: PropTypes.func.isRequired,
|
onClick: PropTypes.func.isRequired,
|
||||||
|
onContextMenu: PropTypes.func,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default RoomSelector;
|
export default RoomSelector;
|
||||||
|
|
|
@ -4,12 +4,13 @@ import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import initMatrix from '../../../client/initMatrix';
|
import initMatrix from '../../../client/initMatrix';
|
||||||
import navigation from '../../../client/state/navigation';
|
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 { createSpaceShortcut, deleteSpaceShortcut } from '../../../client/action/room';
|
||||||
import { getEventCords, abbreviateNumber } from '../../../util/common';
|
import { getEventCords, abbreviateNumber } from '../../../util/common';
|
||||||
|
|
||||||
import IconButton from '../../atoms/button/IconButton';
|
import IconButton from '../../atoms/button/IconButton';
|
||||||
import RoomSelector from '../../molecules/room-selector/RoomSelector';
|
import RoomSelector from '../../molecules/room-selector/RoomSelector';
|
||||||
|
import RoomOptions from '../../molecules/room-optons/RoomOptions';
|
||||||
|
|
||||||
import HashIC from '../../../../public/res/ic/outlined/hash.svg';
|
import HashIC from '../../../../public/res/ic/outlined/hash.svg';
|
||||||
import HashGlobeIC from '../../../../public/res/ic/outlined/hash-globe.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) => <RoomOptions roomId={roomId} afterOptionSelect={closeMenu} />,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const joinRuleToIconSrc = (joinRule) => ({
|
const joinRuleToIconSrc = (joinRule) => ({
|
||||||
restricted: () => (room.isSpaceRoom() ? SpaceIC : HashIC),
|
restricted: () => (room.isSpaceRoom() ? SpaceIC : HashIC),
|
||||||
invite: () => (room.isSpaceRoom() ? SpaceLockIC : HashLockIC),
|
invite: () => (room.isSpaceRoom() ? SpaceLockIC : HashLockIC),
|
||||||
|
@ -96,13 +106,14 @@ function Selector({
|
||||||
notificationCount={abbreviateNumber(noti.getTotalNoti(roomId))}
|
notificationCount={abbreviateNumber(noti.getTotalNoti(roomId))}
|
||||||
isAlert={noti.getHighlightNoti(roomId) !== 0}
|
isAlert={noti.getHighlightNoti(roomId) !== 0}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
|
onContextMenu={openRoomOptions}
|
||||||
options={(
|
options={(
|
||||||
<IconButton
|
<IconButton
|
||||||
size="extra-small"
|
size="extra-small"
|
||||||
tooltip="Options"
|
tooltip="Options"
|
||||||
tooltipPlacement="right"
|
tooltipPlacement="right"
|
||||||
src={VerticalMenuIC}
|
src={VerticalMenuIC}
|
||||||
onClick={(e) => openRoomOptions(getEventCords(e), roomId)}
|
onClick={openRoomOptions}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -7,26 +7,87 @@ import { twemojify } from '../../../util/twemojify';
|
||||||
import initMatrix from '../../../client/initMatrix';
|
import initMatrix from '../../../client/initMatrix';
|
||||||
import cons from '../../../client/state/cons';
|
import cons from '../../../client/state/cons';
|
||||||
import navigation from '../../../client/state/navigation';
|
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 * as roomActions from '../../../client/action/room';
|
||||||
|
|
||||||
import { getUsername, getUsernameOfRoomMember, getPowerLabel } from '../../../util/matrixUtil';
|
import { getUsername, getUsernameOfRoomMember, getPowerLabel } from '../../../util/matrixUtil';
|
||||||
|
import { getEventCords } from '../../../util/common';
|
||||||
import colorMXID from '../../../util/colorMXID';
|
import colorMXID from '../../../util/colorMXID';
|
||||||
|
|
||||||
import Text from '../../atoms/text/Text';
|
import Text from '../../atoms/text/Text';
|
||||||
import Chip from '../../atoms/chip/Chip';
|
import Chip from '../../atoms/chip/Chip';
|
||||||
import IconButton from '../../atoms/button/IconButton';
|
import IconButton from '../../atoms/button/IconButton';
|
||||||
|
import Input from '../../atoms/input/Input';
|
||||||
import Avatar from '../../atoms/avatar/Avatar';
|
import Avatar from '../../atoms/avatar/Avatar';
|
||||||
import Button from '../../atoms/button/Button';
|
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 Dialog from '../../molecules/dialog/Dialog';
|
||||||
import SettingTile from '../../molecules/setting-tile/SettingTile';
|
|
||||||
|
|
||||||
import ShieldEmptyIC from '../../../../public/res/ic/outlined/shield-empty.svg';
|
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 ChevronBottomIC from '../../../../public/res/ic/outlined/chevron-bottom.svg';
|
||||||
import CrossIC from '../../../../public/res/ic/outlined/cross.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 (
|
||||||
|
<div className="moderation-tools">
|
||||||
|
{canIKick && (
|
||||||
|
<form onSubmit={handleKick}>
|
||||||
|
<Input label="Kick reason" name="kick-reason" />
|
||||||
|
<Button type="submit">Kick</Button>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
{canIBan && (
|
||||||
|
<form onSubmit={handleBan}>
|
||||||
|
<Input label="Ban reason" name="ban-reason" />
|
||||||
|
<Button type="submit">Ban</Button>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
ModerationTools.propTypes = {
|
||||||
|
roomId: PropTypes.string.isRequired,
|
||||||
|
userId: PropTypes.string.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
function SessionInfo({ userId }) {
|
function SessionInfo({ userId }) {
|
||||||
const [devices, setDevices] = useState(null);
|
const [devices, setDevices] = useState(null);
|
||||||
|
const [isVisible, setIsVisible] = useState(false);
|
||||||
const mx = initMatrix.matrixClient;
|
const mx = initMatrix.matrixClient;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -51,10 +112,11 @@ function SessionInfo({ userId }) {
|
||||||
}, [userId]);
|
}, [userId]);
|
||||||
|
|
||||||
function renderSessionChips() {
|
function renderSessionChips() {
|
||||||
|
if (!isVisible) return null;
|
||||||
return (
|
return (
|
||||||
<div className="session-info__chips">
|
<div className="session-info__chips">
|
||||||
{devices === null && <Text variant="b3">Loading sessions...</Text>}
|
{devices === null && <Text variant="b2">Loading sessions...</Text>}
|
||||||
{devices?.length === 0 && <Text variant="b3">No session found.</Text>}
|
{devices?.length === 0 && <Text variant="b2">No session found.</Text>}
|
||||||
{devices !== null && (devices.map((device) => (
|
{devices !== null && (devices.map((device) => (
|
||||||
<Chip
|
<Chip
|
||||||
key={device.deviceId}
|
key={device.deviceId}
|
||||||
|
@ -68,10 +130,13 @@ function SessionInfo({ userId }) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="session-info">
|
<div className="session-info">
|
||||||
<SettingTile
|
<MenuItem
|
||||||
title="Sessions"
|
onClick={() => setIsVisible(!isVisible)}
|
||||||
content={renderSessionChips()}
|
iconSrc={isVisible ? ChevronBottomIC : ChevronRightIC}
|
||||||
/>
|
>
|
||||||
|
<Text variant="b2">{`View ${devices?.length > 0 ? `${devices.length} ` : ''}sessions`}</Text>
|
||||||
|
</MenuItem>
|
||||||
|
{renderSessionChips()}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -98,6 +163,8 @@ function ProfileFooter({ roomId, userId, onRequestClose }) {
|
||||||
const userPL = room.getMember(userId)?.powerLevel || 0;
|
const userPL = room.getMember(userId)?.powerLevel || 0;
|
||||||
const canIKick = room.currentState.hasSufficientPowerLevelFor('kick', myPowerlevel) && userPL < myPowerlevel;
|
const canIKick = room.currentState.hasSufficientPowerLevelFor('kick', myPowerlevel) && userPL < myPowerlevel;
|
||||||
|
|
||||||
|
const isBanned = member?.membership === 'ban';
|
||||||
|
|
||||||
const onCreated = (dmRoomId) => {
|
const onCreated = (dmRoomId) => {
|
||||||
if (isMountedRef.current === false) return;
|
if (isMountedRef.current === false) return;
|
||||||
setIsCreatingDM(false);
|
setIsCreatingDM(false);
|
||||||
|
@ -119,7 +186,7 @@ function ProfileFooter({ roomId, userId, onRequestClose }) {
|
||||||
setIsInviting(false);
|
setIsInviting(false);
|
||||||
}, [userId]);
|
}, [userId]);
|
||||||
|
|
||||||
async function openDM() {
|
const openDM = async () => {
|
||||||
const directIds = [...initMatrix.roomList.directs];
|
const directIds = [...initMatrix.roomList.directs];
|
||||||
|
|
||||||
// Check and open if user already have a DM with userId.
|
// 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;
|
if (isMountedRef.current === false) return;
|
||||||
setIsCreatingDM(false);
|
setIsCreatingDM(false);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
async function toggleIgnore() {
|
const toggleIgnore = async () => {
|
||||||
const ignoredUsers = mx.getIgnoredUsers();
|
const ignoredUsers = mx.getIgnoredUsers();
|
||||||
const uIndex = ignoredUsers.indexOf(userId);
|
const uIndex = ignoredUsers.indexOf(userId);
|
||||||
if (uIndex >= 0) {
|
if (uIndex >= 0) {
|
||||||
|
@ -165,9 +232,9 @@ function ProfileFooter({ roomId, userId, onRequestClose }) {
|
||||||
} catch {
|
} catch {
|
||||||
setIsIgnoring(false);
|
setIsIgnoring(false);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
async function toggleInvite() {
|
const toggleInvite = async () => {
|
||||||
try {
|
try {
|
||||||
setIsInviting(true);
|
setIsInviting(true);
|
||||||
let isInviteSent = false;
|
let isInviteSent = false;
|
||||||
|
@ -182,7 +249,7 @@ function ProfileFooter({ roomId, userId, onRequestClose }) {
|
||||||
} catch {
|
} catch {
|
||||||
setIsInviting(false);
|
setIsInviting(false);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="profile-viewer__buttons">
|
<div className="profile-viewer__buttons">
|
||||||
|
@ -193,7 +260,14 @@ function ProfileFooter({ roomId, userId, onRequestClose }) {
|
||||||
>
|
>
|
||||||
{isCreatingDM ? 'Creating room...' : 'Message'}
|
{isCreatingDM ? 'Creating room...' : 'Message'}
|
||||||
</Button>
|
</Button>
|
||||||
{ member?.membership === 'join' && <Button>Mention</Button>}
|
{ isBanned && canIKick && (
|
||||||
|
<Button
|
||||||
|
variant="positive"
|
||||||
|
onClick={() => roomActions.unban(roomId, userId)}
|
||||||
|
>
|
||||||
|
Unban
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
{ (isInvited ? canIKick : room.canInvite(mx.getUserId())) && isInvitable && (
|
{ (isInvited ? canIKick : room.canInvite(mx.getUserId())) && isInvitable && (
|
||||||
<Button
|
<Button
|
||||||
onClick={toggleInvite}
|
onClick={toggleInvite}
|
||||||
|
@ -226,84 +300,137 @@ ProfileFooter.propTypes = {
|
||||||
onRequestClose: PropTypes.func.isRequired,
|
onRequestClose: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
function ProfileViewer() {
|
function useToggleDialog() {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const [roomId, setRoomId] = useState(null);
|
const [roomId, setRoomId] = useState(null);
|
||||||
const [userId, setUserId] = useState(null);
|
const [userId, setUserId] = useState(null);
|
||||||
|
|
||||||
const mx = initMatrix.matrixClient;
|
|
||||||
const room = roomId ? mx.getRoom(roomId) : null;
|
|
||||||
let username = '';
|
|
||||||
if (room !== null) {
|
|
||||||
const roomMember = room.getMember(userId);
|
|
||||||
if (roomMember) username = getUsernameOfRoomMember(roomMember);
|
|
||||||
else username = getUsername(userId);
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadProfile(uId, rId) {
|
|
||||||
setIsOpen(true);
|
|
||||||
setUserId(uId);
|
|
||||||
setRoomId(rId);
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
const loadProfile = (uId, rId) => {
|
||||||
|
setIsOpen(true);
|
||||||
|
setUserId(uId);
|
||||||
|
setRoomId(rId);
|
||||||
|
};
|
||||||
navigation.on(cons.events.navigation.PROFILE_VIEWER_OPENED, loadProfile);
|
navigation.on(cons.events.navigation.PROFILE_VIEWER_OPENED, loadProfile);
|
||||||
return () => {
|
return () => {
|
||||||
navigation.removeListener(cons.events.navigation.PROFILE_VIEWER_OPENED, loadProfile);
|
navigation.removeListener(cons.events.navigation.PROFILE_VIEWER_OPENED, loadProfile);
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleAfterClose = () => {
|
const closeDialog = () => setIsOpen(false);
|
||||||
|
|
||||||
|
const afterClose = () => {
|
||||||
setUserId(null);
|
setUserId(null);
|
||||||
setRoomId(null);
|
setRoomId(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
function renderProfile() {
|
return [isOpen, roomId, userId, closeDialog, afterClose];
|
||||||
const member = room.getMember(userId) || mx.getUser(userId) || {};
|
}
|
||||||
const avatarMxc = member.getMxcAvatarUrl?.() || member.avatarUrl;
|
|
||||||
const powerLevel = member.powerLevel || 0;
|
function useRerenderOnProfileChange(roomId, userId) {
|
||||||
const canChangeRole = room.currentState.maySendEvent('m.room.power_levels', mx.getUserId());
|
const mx = initMatrix.matrixClient;
|
||||||
|
const [, forceUpdate] = useForceUpdate();
|
||||||
|
useEffect(() => {
|
||||||
|
const handleProfileChange = (mEvent, member) => {
|
||||||
|
if (
|
||||||
|
mEvent.getRoomId() === roomId
|
||||||
|
&& (member.userId === userId || member.userId === mx.getUserId())
|
||||||
|
) {
|
||||||
|
forceUpdate();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
mx.on('RoomMember.powerLevel', handleProfileChange);
|
||||||
|
mx.on('RoomMember.membership', handleProfileChange);
|
||||||
|
return () => {
|
||||||
|
mx.removeListener('RoomMember.powerLevel', handleProfileChange);
|
||||||
|
mx.removeListener('RoomMember.membership', handleProfileChange);
|
||||||
|
};
|
||||||
|
}, [roomId, userId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ProfileViewer() {
|
||||||
|
const [isOpen, roomId, userId, closeDialog, handleAfterClose] = useToggleDialog();
|
||||||
|
useRerenderOnProfileChange(roomId, userId);
|
||||||
|
|
||||||
|
const mx = initMatrix.matrixClient;
|
||||||
|
const room = mx.getRoom(roomId);
|
||||||
|
|
||||||
|
const renderProfile = () => {
|
||||||
|
const roomMember = room.getMember(userId);
|
||||||
|
const username = roomMember ? getUsernameOfRoomMember(roomMember) : getUsername(userId);
|
||||||
|
const avatarMxc = roomMember?.getMxcAvatarUrl?.() || mx.getUser(userId)?.avatarUrl;
|
||||||
|
const avatarUrl = (avatarMxc && avatarMxc !== 'null') ? mx.mxcUrlToHttp(avatarMxc, 80, 80, 'crop') : null;
|
||||||
|
|
||||||
|
const powerLevel = roomMember.powerLevel || 0;
|
||||||
|
const myPowerLevel = room.getMember(mx.getUserId())?.powerLevel || 0;
|
||||||
|
|
||||||
|
const canChangeRole = (
|
||||||
|
room.currentState.maySendEvent('m.room.power_levels', mx.getUserId())
|
||||||
|
&& (powerLevel < myPowerLevel || userId === mx.getUserId())
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleChangePowerLevel = (newPowerLevel) => {
|
||||||
|
if (newPowerLevel === powerLevel) return;
|
||||||
|
if (newPowerLevel === myPowerLevel
|
||||||
|
? confirm('You will not be able to undo this change as you are promoting the user to have the same power level as yourself. Are you sure?')
|
||||||
|
: true
|
||||||
|
) {
|
||||||
|
roomActions.setPowerLevel(roomId, userId, newPowerLevel);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePowerSelector = (e) => {
|
||||||
|
openReusableContextMenu(
|
||||||
|
'bottom',
|
||||||
|
getEventCords(e, '.btn-surface'),
|
||||||
|
(closeMenu) => (
|
||||||
|
<PowerLevelSelector
|
||||||
|
value={powerLevel}
|
||||||
|
max={myPowerLevel}
|
||||||
|
onSelect={(pl) => {
|
||||||
|
closeMenu();
|
||||||
|
handleChangePowerLevel(pl);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="profile-viewer">
|
<div className="profile-viewer">
|
||||||
<div className="profile-viewer__user">
|
<div className="profile-viewer__user">
|
||||||
<Avatar
|
<Avatar imageSrc={avatarUrl} text={username} bgColor={colorMXID(userId)} size="large" />
|
||||||
imageSrc={!avatarMxc ? null : mx.mxcUrlToHttp(avatarMxc, 80, 80, 'crop')}
|
|
||||||
text={username}
|
|
||||||
bgColor={colorMXID(userId)}
|
|
||||||
size="large"
|
|
||||||
/>
|
|
||||||
<div className="profile-viewer__user__info">
|
<div className="profile-viewer__user__info">
|
||||||
<Text variant="s1" weight="medium">{twemojify(username)}</Text>
|
<Text variant="s1" weight="medium">{twemojify(username)}</Text>
|
||||||
<Text variant="b2">{twemojify(userId)}</Text>
|
<Text variant="b2">{twemojify(userId)}</Text>
|
||||||
</div>
|
</div>
|
||||||
<div className="profile-viewer__user__role">
|
<div className="profile-viewer__user__role">
|
||||||
<Text variant="b3">Role</Text>
|
<Text variant="b3">Role</Text>
|
||||||
<Button iconSrc={canChangeRole ? ChevronBottomIC : null}>
|
<Button
|
||||||
|
onClick={canChangeRole ? handlePowerSelector : null}
|
||||||
|
iconSrc={canChangeRole ? ChevronBottomIC : null}
|
||||||
|
>
|
||||||
{`${getPowerLabel(powerLevel) || 'Member'} - ${powerLevel}`}
|
{`${getPowerLabel(powerLevel) || 'Member'} - ${powerLevel}`}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<ModerationTools roomId={roomId} userId={userId} />
|
||||||
<SessionInfo userId={userId} />
|
<SessionInfo userId={userId} />
|
||||||
{ userId !== mx.getUserId() && (
|
{ userId !== mx.getUserId() && (
|
||||||
<ProfileFooter
|
<ProfileFooter roomId={roomId} userId={userId} onRequestClose={closeDialog} />
|
||||||
roomId={roomId}
|
|
||||||
userId={userId}
|
|
||||||
onRequestClose={() => setIsOpen(false)}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog
|
||||||
className="profile-viewer__dialog"
|
className="profile-viewer__dialog"
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
title={`${username} in ${room?.name ?? ''}`}
|
title={room?.name ?? ''}
|
||||||
onAfterClose={handleAfterClose}
|
onAfterClose={handleAfterClose}
|
||||||
onRequestClose={() => setIsOpen(false)}
|
onRequestClose={closeDialog}
|
||||||
contentOptions={<IconButton src={CrossIC} onClick={() => setIsOpen(false)} tooltip="Close" />}
|
contentOptions={<IconButton src={CrossIC} onClick={closeDialog} tooltip="Close" />}
|
||||||
>
|
>
|
||||||
{roomId ? renderProfile() : <div />}
|
{roomId ? renderProfile() : <div />}
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
@use '../../partials/flex';
|
||||||
@use '../../partials/dir';
|
@use '../../partials/dir';
|
||||||
|
|
||||||
.profile-viewer__dialog {
|
.profile-viewer__dialog {
|
||||||
|
@ -15,7 +16,6 @@
|
||||||
&__user {
|
&__user {
|
||||||
display: flex;
|
display: flex;
|
||||||
padding-bottom: var(--sp-normal);
|
padding-bottom: var(--sp-normal);
|
||||||
border-bottom: 1px solid var(--bg-surface-border);
|
|
||||||
|
|
||||||
&__info {
|
&__info {
|
||||||
align-self: flex-end;
|
align-self: flex-end;
|
||||||
|
@ -61,12 +61,47 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.session-info {
|
.profile-viewer__admin-tool {
|
||||||
& .setting-tile__title .text {
|
.setting-tile {
|
||||||
color: var(--tc-surface-high);
|
margin-top: var(--sp-loose);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.moderation-tools {
|
||||||
|
& > form {
|
||||||
|
margin: var(--sp-normal) 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
& .input-container {
|
||||||
|
@extend .cp-fx__item-one;
|
||||||
|
@include dir.side(margin, 0, var(--sp-tight));
|
||||||
|
}
|
||||||
|
& button {
|
||||||
|
height: 46px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-info {
|
||||||
|
box-shadow: var(--bs-surface-border);
|
||||||
|
border-radius: var(--bo-radius);
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
& .context-menu__item button {
|
||||||
|
padding: var(--sp-extra-tight);
|
||||||
|
& .ic-raw {
|
||||||
|
@include dir.side(margin, 0, var(--sp-extra-tight));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&__chips {
|
&__chips {
|
||||||
|
border-top: 1px solid var(--bg-surface-border);
|
||||||
|
padding: var(--sp-tight);
|
||||||
padding-top: var(--sp-ultra-tight);
|
padding-top: var(--sp-ultra-tight);
|
||||||
|
|
||||||
|
& > .text {
|
||||||
|
margin-top: var(--sp-extra-tight);
|
||||||
|
}
|
||||||
& .chip {
|
& .chip {
|
||||||
margin-top: var(--sp-extra-tight);
|
margin-top: var(--sp-extra-tight);
|
||||||
@include dir.side(margin, 0, var(--sp-extra-tight));
|
@include dir.side(margin, 0, var(--sp-extra-tight));
|
||||||
|
|
|
@ -1,125 +0,0 @@
|
||||||
import React, { useEffect, useRef } from 'react';
|
|
||||||
|
|
||||||
import { twemojify } from '../../../util/twemojify';
|
|
||||||
|
|
||||||
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 RoomNotification from '../../molecules/room-notification/RoomNotification';
|
|
||||||
|
|
||||||
import TickMarkIC from '../../../../public/res/ic/outlined/tick-mark.svg';
|
|
||||||
import AddUserIC from '../../../../public/res/ic/outlined/add-user.svg';
|
|
||||||
import LeaveArrowIC from '../../../../public/res/ic/outlined/leave-arrow.svg';
|
|
||||||
|
|
||||||
import { useForceUpdate } from '../../hooks/useForceUpdate';
|
|
||||||
|
|
||||||
let isRoomOptionVisible = false;
|
|
||||||
let roomId = null;
|
|
||||||
function RoomOptions() {
|
|
||||||
const openerRef = useRef(null);
|
|
||||||
const [, forceUpdate] = useForceUpdate();
|
|
||||||
|
|
||||||
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;
|
|
||||||
openerRef.current.click();
|
|
||||||
forceUpdate();
|
|
||||||
}
|
|
||||||
|
|
||||||
const 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 handleMarkAsRead = () => {
|
|
||||||
const mx = initMatrix.matrixClient;
|
|
||||||
const room = mx.getRoom(roomId);
|
|
||||||
if (!room) return;
|
|
||||||
const events = room.getLiveTimeline().getEvents();
|
|
||||||
mx.sendReadReceipt(events[events.length - 1]);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleInviteClick = () => openInviteUser(roomId);
|
|
||||||
const handleLeaveClick = (toggleMenu) => {
|
|
||||||
if (confirm('Are you really want to leave this room?')) {
|
|
||||||
roomActions.leave(roomId);
|
|
||||||
toggleMenu();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const mx = initMatrix.matrixClient;
|
|
||||||
const room = mx.getRoom(roomId);
|
|
||||||
const canInvite = room?.canInvite(mx.getUserId());
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ContextMenu
|
|
||||||
afterToggle={afterRoomOptionsToggle}
|
|
||||||
maxWidth={298}
|
|
||||||
content={(toggleMenu) => (
|
|
||||||
<>
|
|
||||||
<MenuHeader>{twemojify(`Options for ${initMatrix.matrixClient.getRoom(roomId)?.name}`)}</MenuHeader>
|
|
||||||
<MenuItem
|
|
||||||
iconSrc={TickMarkIC}
|
|
||||||
onClick={() => {
|
|
||||||
handleMarkAsRead(); toggleMenu();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Mark as read
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem
|
|
||||||
disabled={!canInvite}
|
|
||||||
iconSrc={AddUserIC}
|
|
||||||
onClick={() => {
|
|
||||||
handleInviteClick(); toggleMenu();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Invite
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem iconSrc={LeaveArrowIC} variant="danger" onClick={() => handleLeaveClick(toggleMenu)}>Leave</MenuItem>
|
|
||||||
<MenuHeader>Notification</MenuHeader>
|
|
||||||
{roomId && <RoomNotification roomId={roomId} />}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
render={(toggleMenu) => (
|
|
||||||
<input
|
|
||||||
ref={openerRef}
|
|
||||||
onClick={toggleMenu}
|
|
||||||
type="button"
|
|
||||||
style={{
|
|
||||||
width: '32px',
|
|
||||||
height: '32px',
|
|
||||||
backgroundColor: 'transparent',
|
|
||||||
position: 'absolute',
|
|
||||||
top: 0,
|
|
||||||
left: 0,
|
|
||||||
padding: 0,
|
|
||||||
border: 'none',
|
|
||||||
visibility: 'hidden',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default RoomOptions;
|
|
|
@ -107,7 +107,7 @@ function PeopleDrawer({ roomId }) {
|
||||||
let isRoomChanged = false;
|
let isRoomChanged = false;
|
||||||
const updateMemberList = (event) => {
|
const updateMemberList = (event) => {
|
||||||
if (isGettingMembers) return;
|
if (isGettingMembers) return;
|
||||||
if (event && event?.event?.room_id !== roomId) return;
|
if (event && event?.getRoomId() !== roomId) return;
|
||||||
setMemberList(
|
setMemberList(
|
||||||
simplyfiMembers(
|
simplyfiMembers(
|
||||||
getMembersWithMembership(membership)
|
getMembersWithMembership(membership)
|
||||||
|
@ -125,6 +125,7 @@ function PeopleDrawer({ roomId }) {
|
||||||
|
|
||||||
asyncSearch.on(asyncSearch.RESULT_SENT, handleSearchData);
|
asyncSearch.on(asyncSearch.RESULT_SENT, handleSearchData);
|
||||||
mx.on('RoomMember.membership', updateMemberList);
|
mx.on('RoomMember.membership', updateMemberList);
|
||||||
|
mx.on('RoomMember.powerLevel', updateMemberList);
|
||||||
return () => {
|
return () => {
|
||||||
isRoomChanged = true;
|
isRoomChanged = true;
|
||||||
setMemberList([]);
|
setMemberList([]);
|
||||||
|
@ -132,6 +133,7 @@ function PeopleDrawer({ roomId }) {
|
||||||
setItemCount(PER_PAGE_MEMBER);
|
setItemCount(PER_PAGE_MEMBER);
|
||||||
asyncSearch.removeListener(asyncSearch.RESULT_SENT, handleSearchData);
|
asyncSearch.removeListener(asyncSearch.RESULT_SENT, handleSearchData);
|
||||||
mx.removeListener('RoomMember.membership', updateMemberList);
|
mx.removeListener('RoomMember.membership', updateMemberList);
|
||||||
|
mx.removeListener('RoomMember.powerLevel', updateMemberList);
|
||||||
};
|
};
|
||||||
}, [roomId, membership]);
|
}, [roomId, membership]);
|
||||||
|
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
&--dropped {
|
&--dropped {
|
||||||
transform: translateY(calc(100% - var(--header-height)));
|
transform: translateY(calc(100% - var(--header-height)));
|
||||||
border-radius: var(--bo-radius) var(--bo-radius) 0 0;
|
border-radius: var(--bo-radius) var(--bo-radius) 0 0;
|
||||||
box-shadow: 0 0 0 1px var(--bg-surface-border);
|
box-shadow: var(--bs-popup);
|
||||||
}
|
}
|
||||||
|
|
||||||
&__content-wrapper {
|
&__content-wrapper {
|
||||||
|
|
|
@ -8,7 +8,7 @@ import { blurOnBubbling } from '../../atoms/button/script';
|
||||||
import initMatrix from '../../../client/initMatrix';
|
import initMatrix from '../../../client/initMatrix';
|
||||||
import cons from '../../../client/state/cons';
|
import cons from '../../../client/state/cons';
|
||||||
import navigation from '../../../client/state/navigation';
|
import navigation from '../../../client/state/navigation';
|
||||||
import { toggleRoomSettings, openRoomOptions } from '../../../client/action/navigation';
|
import { toggleRoomSettings, openReusableContextMenu } from '../../../client/action/navigation';
|
||||||
import { togglePeopleDrawer } from '../../../client/action/settings';
|
import { togglePeopleDrawer } from '../../../client/action/settings';
|
||||||
import colorMXID from '../../../util/colorMXID';
|
import colorMXID from '../../../util/colorMXID';
|
||||||
import { getEventCords } from '../../../util/common';
|
import { getEventCords } from '../../../util/common';
|
||||||
|
@ -18,6 +18,7 @@ import RawIcon from '../../atoms/system-icons/RawIcon';
|
||||||
import IconButton from '../../atoms/button/IconButton';
|
import IconButton from '../../atoms/button/IconButton';
|
||||||
import Header, { TitleWrapper } from '../../atoms/header/Header';
|
import Header, { TitleWrapper } from '../../atoms/header/Header';
|
||||||
import Avatar from '../../atoms/avatar/Avatar';
|
import Avatar from '../../atoms/avatar/Avatar';
|
||||||
|
import RoomOptions from '../../molecules/room-optons/RoomOptions';
|
||||||
|
|
||||||
import UserIC from '../../../../public/res/ic/outlined/user.svg';
|
import UserIC from '../../../../public/res/ic/outlined/user.svg';
|
||||||
import ChevronBottomIC from '../../../../public/res/ic/outlined/chevron-bottom.svg';
|
import ChevronBottomIC from '../../../../public/res/ic/outlined/chevron-bottom.svg';
|
||||||
|
@ -60,6 +61,14 @@ function RoomViewHeader({ roomId }) {
|
||||||
};
|
};
|
||||||
}, [roomId]);
|
}, [roomId]);
|
||||||
|
|
||||||
|
const openRoomOptions = (e) => {
|
||||||
|
openReusableContextMenu(
|
||||||
|
'bottom',
|
||||||
|
getEventCords(e, '.ic-btn'),
|
||||||
|
(closeMenu) => <RoomOptions roomId={roomId} afterOptionSelect={closeMenu} />,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Header>
|
<Header>
|
||||||
<button
|
<button
|
||||||
|
@ -77,7 +86,7 @@ function RoomViewHeader({ roomId }) {
|
||||||
</button>
|
</button>
|
||||||
<IconButton onClick={togglePeopleDrawer} tooltip="People" src={UserIC} />
|
<IconButton onClick={togglePeopleDrawer} tooltip="People" src={UserIC} />
|
||||||
<IconButton
|
<IconButton
|
||||||
onClick={(e) => openRoomOptions(getEventCords(e), roomId)}
|
onClick={openRoomOptions}
|
||||||
tooltip="Options"
|
tooltip="Options"
|
||||||
src={VerticalMenuIC}
|
src={VerticalMenuIC}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -4,11 +4,11 @@ import './Client.scss';
|
||||||
import Text from '../../atoms/text/Text';
|
import Text from '../../atoms/text/Text';
|
||||||
import Spinner from '../../atoms/spinner/Spinner';
|
import Spinner from '../../atoms/spinner/Spinner';
|
||||||
import Navigation from '../../organisms/navigation/Navigation';
|
import Navigation from '../../organisms/navigation/Navigation';
|
||||||
|
import ReusableContextMenu from '../../atoms/context-menu/ReusableContextMenu';
|
||||||
import Room from '../../organisms/room/Room';
|
import Room from '../../organisms/room/Room';
|
||||||
import Windows from '../../organisms/pw/Windows';
|
import Windows from '../../organisms/pw/Windows';
|
||||||
import Dialogs from '../../organisms/pw/Dialogs';
|
import Dialogs from '../../organisms/pw/Dialogs';
|
||||||
import EmojiBoardOpener from '../../organisms/emoji-board/EmojiBoardOpener';
|
import EmojiBoardOpener from '../../organisms/emoji-board/EmojiBoardOpener';
|
||||||
import RoomOptions from '../../organisms/room-optons/RoomOptions';
|
|
||||||
import logout from '../../../client/action/logout';
|
import logout from '../../../client/action/logout';
|
||||||
|
|
||||||
import initMatrix from '../../../client/initMatrix';
|
import initMatrix from '../../../client/initMatrix';
|
||||||
|
@ -65,7 +65,7 @@ function Client() {
|
||||||
<Windows />
|
<Windows />
|
||||||
<Dialogs />
|
<Dialogs />
|
||||||
<EmojiBoardOpener />
|
<EmojiBoardOpener />
|
||||||
<RoomOptions />
|
<ReusableContextMenu />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
export function openAttachmentTypeSelector(params) {
|
||||||
appDispatcher.dispatch({
|
appDispatcher.dispatch({
|
||||||
type: cons.actions.navigation.OPEN_ATTACHMENT_TYPE_SELECTOR,
|
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) {
|
export function openSearch(term) {
|
||||||
appDispatcher.dispatch({
|
appDispatcher.dispatch({
|
||||||
type: cons.actions.navigation.OPEN_SEARCH,
|
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({
|
appDispatcher.dispatch({
|
||||||
type: cons.actions.navigation.OPEN_REUSABLE_CONTEXT_MENU,
|
type: cons.actions.navigation.OPEN_REUSABLE_CONTEXT_MENU,
|
||||||
placement,
|
placement,
|
||||||
cords,
|
cords,
|
||||||
render,
|
render,
|
||||||
|
afterClose,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -192,10 +192,34 @@ async function invite(roomId, userId) {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function kick(roomId, userId) {
|
async function kick(roomId, userId, reason) {
|
||||||
const mx = initMatrix.matrixClient;
|
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;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -215,6 +239,7 @@ function deleteSpaceShortcut(roomId) {
|
||||||
|
|
||||||
export {
|
export {
|
||||||
join, leave,
|
join, leave,
|
||||||
create, invite, kick,
|
create, invite, kick, ban, unban,
|
||||||
|
setPowerLevel,
|
||||||
createSpaceShortcut, deleteSpaceShortcut,
|
createSpaceShortcut, deleteSpaceShortcut,
|
||||||
};
|
};
|
||||||
|
|
|
@ -39,7 +39,6 @@ const cons = {
|
||||||
OPEN_SETTINGS: 'OPEN_SETTINGS',
|
OPEN_SETTINGS: 'OPEN_SETTINGS',
|
||||||
OPEN_EMOJIBOARD: 'OPEN_EMOJIBOARD',
|
OPEN_EMOJIBOARD: 'OPEN_EMOJIBOARD',
|
||||||
OPEN_READRECEIPTS: 'OPEN_READRECEIPTS',
|
OPEN_READRECEIPTS: 'OPEN_READRECEIPTS',
|
||||||
OPEN_ROOMOPTIONS: 'OPEN_ROOMOPTIONS',
|
|
||||||
CLICK_REPLY_TO: 'CLICK_REPLY_TO',
|
CLICK_REPLY_TO: 'CLICK_REPLY_TO',
|
||||||
OPEN_SEARCH: 'OPEN_SEARCH',
|
OPEN_SEARCH: 'OPEN_SEARCH',
|
||||||
OPEN_ATTACHMENT_TYPE_SELECTOR: 'OPEN_ATTACHMENT_TYPE_SELECTOR',
|
OPEN_ATTACHMENT_TYPE_SELECTOR: 'OPEN_ATTACHMENT_TYPE_SELECTOR',
|
||||||
|
@ -77,7 +76,6 @@ const cons = {
|
||||||
PROFILE_VIEWER_OPENED: 'PROFILE_VIEWER_OPENED',
|
PROFILE_VIEWER_OPENED: 'PROFILE_VIEWER_OPENED',
|
||||||
EMOJIBOARD_OPENED: 'EMOJIBOARD_OPENED',
|
EMOJIBOARD_OPENED: 'EMOJIBOARD_OPENED',
|
||||||
READRECEIPTS_OPENED: 'READRECEIPTS_OPENED',
|
READRECEIPTS_OPENED: 'READRECEIPTS_OPENED',
|
||||||
ROOMOPTIONS_OPENED: 'ROOMOPTIONS_OPENED',
|
|
||||||
REPLY_TO_CLICKED: 'REPLY_TO_CLICKED',
|
REPLY_TO_CLICKED: 'REPLY_TO_CLICKED',
|
||||||
OPEN_ATTACHMENT_TYPE_SELECTOR: 'OPEN_ATTACHMENT_TYPE_SELECTOR',
|
OPEN_ATTACHMENT_TYPE_SELECTOR: 'OPEN_ATTACHMENT_TYPE_SELECTOR',
|
||||||
SEARCH_OPENED: 'SEARCH_OPENED',
|
SEARCH_OPENED: 'SEARCH_OPENED',
|
||||||
|
|
|
@ -126,13 +126,6 @@ class Navigation extends EventEmitter {
|
||||||
action.userIds,
|
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]: () => {
|
[cons.actions.navigation.OPEN_ATTACHMENT_TYPE_SELECTOR]: () => {
|
||||||
this.emit(
|
this.emit(
|
||||||
cons.events.navigation.OPEN_ATTACHMENT_TYPE_SELECTOR,
|
cons.events.navigation.OPEN_ATTACHMENT_TYPE_SELECTOR,
|
||||||
|
@ -160,6 +153,7 @@ class Navigation extends EventEmitter {
|
||||||
action.placement,
|
action.placement,
|
||||||
action.cords,
|
action.cords,
|
||||||
action.render,
|
action.render,
|
||||||
|
action.afterClose,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in a new issue