Merge branch 'dev' into voicemail

This commit is contained in:
C0ffeeCode 2022-01-11 17:37:36 +01:00 committed by GitHub
commit c22e9b008d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 485 additions and 74 deletions

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 18.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 24 24" enable-background="new 0 0 24 24" xml:space="preserve">
<polygon points="9.5,14.1 6,10.6 4.6,12 8.1,15.5 9.5,16.9 19.4,7 18,5.6 "/>
</svg>

After

Width:  |  Height:  |  Size: 520 B

View file

@ -1,4 +1,4 @@
import React, { useState, useEffect } from 'react';
import React from 'react';
import PropTypes from 'prop-types';
import './Avatar.scss';
@ -10,22 +10,16 @@ import RawIcon from '../system-icons/RawIcon';
function Avatar({
text, bgColor, iconSrc, iconColor, imageSrc, size,
}) {
const [image, updateImage] = useState(imageSrc);
let textSize = 's1';
if (size === 'large') textSize = 'h1';
if (size === 'small') textSize = 'b1';
if (size === 'extra-small') textSize = 'b3';
useEffect(() => {
updateImage(imageSrc);
return () => updateImage(null);
}, [imageSrc]);
return (
<div className={`avatar-container avatar-container__${size} noselect`}>
{
image !== null
? <img draggable="false" src={image} onError={() => updateImage(null)} alt="avatar" />
imageSrc !== null
? <img draggable="false" src={imageSrc} alt="avatar" />
: (
<span
style={{ backgroundColor: iconSrc === null ? bgColor : 'transparent' }}

View file

@ -0,0 +1,84 @@
import React, { useState, useEffect, useRef } from 'react';
import cons from '../../../client/state/cons';
import navigation from '../../../client/state/navigation';
import ContextMenu from './ContextMenu';
let key = null;
function ReusableContextMenu() {
const [data, setData] = useState(null);
const openerRef = useRef(null);
const closeMenu = () => {
key = null;
if (data) openerRef.current.click();
};
useEffect(() => {
if (data) {
const { cords } = data;
openerRef.current.style.transform = `translate(${cords.x}px, ${cords.y}px)`;
openerRef.current.style.width = `${cords.width}px`;
openerRef.current.style.height = `${cords.height}px`;
openerRef.current.click();
}
const handleContextMenuOpen = (placement, cords, render) => {
if (key) {
closeMenu();
return;
}
setData({ placement, cords, render });
};
navigation.on(cons.events.navigation.REUSABLE_CONTEXT_MENU_OPENED, handleContextMenuOpen);
return () => {
navigation.removeListener(
cons.events.navigation.REUSABLE_CONTEXT_MENU_OPENED,
handleContextMenuOpen,
);
};
}, [data]);
const handleAfterToggle = (isVisible) => {
if (isVisible) {
key = Math.random();
return;
}
if (setData) setData(null);
if (key === null) return;
const copyKey = key;
setTimeout(() => {
if (key === copyKey) key = null;
}, 200);
};
return (
<ContextMenu
afterToggle={handleAfterToggle}
placement={data?.placement || 'right'}
content={data?.render(closeMenu) ?? ''}
render={(toggleMenu) => (
<input
ref={openerRef}
onClick={toggleMenu}
type="button"
style={{
width: '32px',
height: '32px',
backgroundColor: 'transparent',
position: 'fixed',
top: 0,
left: 0,
padding: 0,
border: 'none',
visibility: 'hidden',
appearance: 'none',
}}
/>
)}
/>
);
}
export default ReusableContextMenu;

View file

@ -0,0 +1,49 @@
import React from 'react';
import PropTypes from 'prop-types';
import './PowerLevelSelector.scss';
import IconButton from '../../atoms/button/IconButton';
import { MenuHeader, MenuItem } from '../../atoms/context-menu/ContextMenu';
import CheckIC from '../../../../public/res/ic/outlined/check.svg';
function PowerLevelSelector({
value, max, onSelect,
}) {
const handleSubmit = (e) => {
const powerLevel = e.target.elements['power-level'];
if (!powerLevel) return;
onSelect(Number(powerLevel));
};
return (
<div className="power-level-selector">
<MenuHeader>Power level selector</MenuHeader>
<form onSubmit={(e) => { e.preventDefault(); handleSubmit(e); }}>
<input
className="input"
defaultValue={value}
type="number"
name="power-level"
placeholder="Power level"
max={max}
autoComplete="off"
required
/>
<IconButton variant="primary" src={CheckIC} type="submit" />
</form>
{max >= 0 && <MenuHeader>Presets</MenuHeader>}
{max >= 100 && <MenuItem variant={value === 100 ? 'positive' : 'surface'} onClick={() => onSelect(100)}>Admin - 100</MenuItem>}
{max >= 50 && <MenuItem variant={value === 50 ? 'positive' : 'surface'} onClick={() => onSelect(50)}>Mod - 50</MenuItem>}
{max >= 0 && <MenuItem variant={value === 0 ? 'positive' : 'surface'} onClick={() => onSelect(0)}>Member - 0</MenuItem>}
</div>
);
}
PowerLevelSelector.propTypes = {
value: PropTypes.number.isRequired,
max: PropTypes.number.isRequired,
onSelect: PropTypes.func.isRequired,
};
export default PowerLevelSelector;

View file

@ -0,0 +1,20 @@
@use '../../partials/flex';
@use '../../partials/dir';
.power-level-selector {
& .context-menu__item .text {
margin: 0 !important;
}
& form {
margin: var(--sp-normal);
display: flex;
& input {
@extend .cp-fx__item-one;
@include dir.side(margin, 0, var(--sp-tight));
width: 148px;
padding: 9px var(--sp-tight);
}
}
}

View file

@ -0,0 +1,208 @@
import React from 'react';
import PropTypes from 'prop-types';
import './RoomPermissions.scss';
import initMatrix from '../../../client/initMatrix';
import { getPowerLabel } from '../../../util/matrixUtil';
import Text from '../../atoms/text/Text';
import Button from '../../atoms/button/Button';
import { MenuHeader } from '../../atoms/context-menu/ContextMenu';
import SettingTile from '../setting-tile/SettingTile';
import ChevronBottomIC from '../../../../public/res/ic/outlined/chevron-bottom.svg';
const permissionsInfo = {
users_default: {
name: 'Default role',
description: 'Set default role for all members.',
default: 0,
},
events_default: {
name: 'Send messages',
description: 'Set minimum power level to send messages in room.',
default: 0,
},
redact: {
name: 'Delete messages sent by others',
description: 'Set minimum power level to delete messages in room.',
default: 50,
},
notifications: {
name: 'Ping room',
description: 'Set minimum power level to ping room.',
default: {
room: 50,
},
},
'm.space.child': {
parent: 'events',
name: 'Manage rooms in space',
description: 'Set minimum power level to manage rooms in space.',
default: 50,
},
invite: {
name: 'Invite',
description: 'Set minimum power level to invite members.',
default: 50,
},
kick: {
name: 'Kick',
description: 'Set minimum power level to kick members.',
default: 50,
},
ban: {
name: 'Ban',
description: 'Set minimum power level to ban members.',
default: 50,
},
'm.room.avatar': {
parent: 'events',
name: 'Change avatar',
description: 'Set minimum power level to change room/space avatar.',
default: 50,
},
'm.room.name': {
parent: 'events',
name: 'Change name',
description: 'Set minimum power level to change room/space name.',
default: 50,
},
'm.room.topic': {
parent: 'events',
name: 'Change topic',
description: 'Set minimum power level to change room/space topic.',
default: 50,
},
state_default: {
name: 'Change settings',
description: 'Set minimum power level to change settings.',
default: 50,
},
'm.room.canonical_alias': {
parent: 'events',
name: 'Change published address',
description: 'Set minimum power level to publish and set main address.',
default: 50,
},
'm.room.power_levels': {
parent: 'events',
name: 'Change permissions',
description: 'Set minimum power level to change permissions.',
default: 50,
},
'm.room.encryption': {
parent: 'events',
name: 'Enable room encryption',
description: 'Set minimum power level to enable room encryption.',
default: 50,
},
'm.room.history_visibility': {
parent: 'events',
name: 'Change history visibility',
description: 'Set minimum power level to change room messages history visibility.',
default: 50,
},
'm.room.tombstone': {
parent: 'events',
name: 'Upgrade room',
description: 'Set minimum power level to upgrade room.',
default: 50,
},
'm.room.pinned_events': {
parent: 'events',
name: 'Pin messages',
description: 'Set minimum power level to pin messages in room.',
default: 50,
},
'm.room.server_acl': {
parent: 'events',
name: 'Change server ACLs',
description: 'Set minimum power level to change server ACLs.',
default: 50,
},
'im.vector.modular.widgets': {
parent: 'events',
name: 'Modify widgets',
description: 'Set minimum power level to modify room widgets.',
default: 50,
},
};
const roomPermsGroups = {
'General Permissions': ['users_default', 'events_default', 'redact', 'notifications'],
'Manage members permissions': ['invite', 'kick', 'ban'],
'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'],
'Other permissions': ['m.room.tombstone', 'm.room.pinned_events', 'm.room.server_acl', 'im.vector.modular.widgets'],
};
const spacePermsGroups = {
'General Permissions': ['users_default', 'm.space.child'],
'Manage members permissions': ['invite', 'kick', 'ban'],
'Space profile permissions': ['m.room.avatar', 'm.room.name', 'm.room.topic'],
'Settings permissions': ['state_default', 'm.room.canonical_alias', 'm.room.power_levels'],
};
function RoomPermissions({ roomId }) {
const mx = initMatrix.matrixClient;
const room = mx.getRoom(roomId);
const pLEvent = room.currentState.getStateEvents('m.room.power_levels')[0];
const permissions = pLEvent.getContent();
const canChangePermission = room.currentState.maySendStateEvent('m.room.power_levels', mx.getUserId());
return (
<div className="room-permissions">
{
Object.keys(roomPermsGroups).map((groupKey) => {
const groupedPermKeys = roomPermsGroups[groupKey];
return (
<div className="room-permissions__card" key={groupKey}>
<MenuHeader>{groupKey}</MenuHeader>
{
groupedPermKeys.map((permKey) => {
const permInfo = permissionsInfo[permKey];
let powerLevel = 0;
let permValue = permInfo.parent
? permissions[permInfo.parent][permKey]
: permissions[permKey];
if (!permValue) permValue = permInfo.default;
if (typeof permValue === 'number') {
powerLevel = permValue;
} else if (permKey === 'notifications') {
powerLevel = permValue.room || 50;
}
return (
<SettingTile
key={permKey}
title={permInfo.name}
content={<Text variant="b3">{permInfo.description}</Text>}
options={(
<Button
iconSrc={canChangePermission ? ChevronBottomIC : null}
>
<Text variant="b2">
{`${getPowerLabel(powerLevel) || 'Member'} - ${powerLevel}`}
</Text>
</Button>
)}
/>
);
})
}
</div>
);
})
}
</div>
);
}
RoomPermissions.propTypes = {
roomId: PropTypes.string.isRequired,
};
export default RoomPermissions;

View file

@ -0,0 +1,11 @@
.room-permissions {
& .setting-tile {
margin: 0 var(--sp-normal);
margin-top: var(--sp-tight);
padding-bottom: var(--sp-tight);
border-bottom: 1px solid var(--bg-surface-border);
&:last-child {
border-bottom: none;
}
}
}

View file

@ -7,13 +7,13 @@ import Text from '../../atoms/text/Text';
function SettingTile({ title, options, content }) {
return (
<div className="setting-tile">
<div className="setting-tile__title__wrapper">
<div className="setting-tile__content">
<div className="setting-tile__title">
<Text variant="b1">{title}</Text>
</div>
{options !== null && <div className="setting-tile__options">{options}</div>}
{content}
</div>
{content !== null && <div className="setting-tile__content">{content}</div>}
{options !== null && <div className="setting-tile__options">{options}</div>}
</div>
);
}

View file

@ -1,13 +1,15 @@
@use '../../partials/dir';
.setting-tile {
&__title__wrapper {
display: flex;
align-items: center;
}
&__title {
display: flex;
&__content {
flex: 1;
min-width: 0;
@include dir.side(margin, 0, var(--sp-normal));
}
&__title {
margin-bottom: var(--sp-ultra-tight);
}
&__options {
@include dir.side(margin, var(--sp-tight), 0);
}
}

View file

@ -188,29 +188,29 @@ function EmojiBoard({ onSelect }) {
const [availableEmojis, setAvailableEmojis] = useState([]);
// This should be called whenever the room changes, so that we can switch out the emoji
// for whatever packs are relevant to this room
function updateAvailableEmoji(selectedRoomId) {
// Retrieve the packs for the new room
const packs = getRelevantPacks(
initMatrix.matrixClient.getRoom(selectedRoomId),
)
// Remove packs that aren't marked as emoji packs
.filter((pack) => pack.usage.indexOf('emoticon') !== -1)
// Remove packs without emojis
.filter((pack) => pack.getEmojis().length !== 0);
// Set an index for each pack so that we know where to jump when the user uses the nav
for (let i = 0; i < packs.length; i += 1) {
packs[i].packIndex = i;
}
// Update the component state
setAvailableEmojis(packs);
}
// Register the above function as an event listener
useEffect(() => {
const updateAvailableEmoji = (selectedRoomId) => {
if (!selectedRoomId) {
setAvailableEmojis([]);
return;
}
// Retrieve the packs for the new room
// Remove packs that aren't marked as emoji packs
// Remove packs without emojis
const packs = getRelevantPacks(
initMatrix.matrixClient.getRoom(selectedRoomId),
)
.filter((pack) => pack.usage.indexOf('emoticon') !== -1)
.filter((pack) => pack.getEmojis().length !== 0);
// Set an index for each pack so that we know where to jump when the user uses the nav
for (let i = 0; i < packs.length; i += 1) {
packs[i].packIndex = i;
}
setAvailableEmojis(packs);
};
navigation.on(cons.events.navigation.ROOM_SELECTED, updateAvailableEmoji);
return () => {
navigation.removeListener(cons.events.navigation.ROOM_SELECTED, updateAvailableEmoji);

View file

@ -261,6 +261,7 @@ function ProfileViewer() {
function renderProfile() {
const member = room.getMember(userId) || mx.getUser(userId) || {};
const avatarMxc = member.getMxcAvatarUrl?.() || member.avatarUrl;
const powerLevel = member.powerLevel || 0;
const canChangeRole = room.currentState.maySendEvent('m.room.power_levels', mx.getUserId());
return (
@ -278,7 +279,9 @@ function ProfileViewer() {
</div>
<div className="profile-viewer__user__role">
<Text variant="b3">Role</Text>
<Button iconSrc={canChangeRole ? ChevronBottomIC : null}>{getPowerLabel(member.powerLevel) || 'Member'}</Button>
<Button iconSrc={canChangeRole ? ChevronBottomIC : null}>
{`${getPowerLabel(powerLevel) || 'Member'} - ${powerLevel}`}
</Button>
</div>
</div>
<SessionInfo userId={userId} />

View file

@ -19,12 +19,12 @@ import RoomVisibility from '../../molecules/room-visibility/RoomVisibility';
import RoomAliases from '../../molecules/room-aliases/RoomAliases';
import RoomHistoryVisibility from '../../molecules/room-history-visibility/RoomHistoryVisibility';
import RoomEncryption from '../../molecules/room-encryption/RoomEncryption';
import RoomPermissions from '../../molecules/room-permissions/RoomPermissions';
import SettingsIC from '../../../../public/res/ic/outlined/settings.svg';
import SearchIC from '../../../../public/res/ic/outlined/search.svg';
import ShieldUserIC from '../../../../public/res/ic/outlined/shield-user.svg';
import LockIC from '../../../../public/res/ic/outlined/lock.svg';
import InfoIC from '../../../../public/res/ic/outlined/info.svg';
import AddUserIC from '../../../../public/res/ic/outlined/add-user.svg';
import LeaveArrowIC from '../../../../public/res/ic/outlined/leave-arrow.svg';
@ -35,7 +35,6 @@ const tabText = {
SEARCH: 'Search',
PERMISSIONS: 'Permissions',
SECURITY: 'Security',
ADVANCED: 'Advanced',
};
const tabItems = [{
@ -54,10 +53,6 @@ const tabItems = [{
iconSrc: LockIC,
text: tabText.SECURITY,
disabled: false,
}, {
iconSrc: InfoIC,
text: tabText.ADVANCED,
disabled: false,
}];
function GeneralSettings({ roomId }) {
@ -156,6 +151,7 @@ function RoomSettings({ roomId }) {
/>
<div className="room-settings__cards-wrapper">
{selectedTab.text === tabText.GENERAL && <GeneralSettings roomId={roomId} />}
{selectedTab.text === tabText.PERMISSIONS && <RoomPermissions roomId={roomId} />}
{selectedTab.text === tabText.SECURITY && <SecuritySettings roomId={roomId} />}
</div>
</div>

View file

@ -32,16 +32,20 @@
padding: 0 var(--sp-normal);
@include dir.side(padding, var(--sp-normal), var(--sp-extra-tight));
}
}
&__card {
margin: var(--sp-normal) 0;
background-color: var(--bg-surface);
border-radius: var(--bo-radius);
box-shadow: var(--bs-surface-border);
overflow: hidden;
.room-settings__card {
margin: var(--sp-normal) 0;
background-color: var(--bg-surface);
border-radius: var(--bo-radius);
box-shadow: var(--bs-surface-border);
overflow: hidden;
& > .context-menu__header:first-child {
margin-top: 2px;
}
& > .context-menu__header:first-child {
margin-top: 2px;
}
}
.room-settings .room-permissions__card {
@extend .room-settings__card;
}

View file

@ -23,9 +23,6 @@
margin-top: var(--sp-normal);
border-bottom: 1px solid var(--bg-surface-border);
padding-bottom: 16px;
&__title__wrapper {
margin-bottom: var(--sp-ultra-tight);
}
}
}

View file

@ -117,3 +117,12 @@ export function openSearch(term) {
term,
});
}
export function openReusableContextMenu(placement, cords, render) {
appDispatcher.dispatch({
type: cons.actions.navigation.OPEN_REUSABLE_CONTEXT_MENU,
placement,
cords,
render,
});
}

View file

@ -43,6 +43,7 @@ const cons = {
CLICK_REPLY_TO: 'CLICK_REPLY_TO',
OPEN_SEARCH: 'OPEN_SEARCH',
OPEN_ATTACHMENT_TYPE_SELECTOR: 'OPEN_ATTACHMENT_TYPE_SELECTOR',
OPEN_REUSABLE_CONTEXT_MENU: 'OPEN_REUSABLE_CONTEXT_MENU',
},
room: {
JOIN: 'JOIN',
@ -80,6 +81,7 @@ const cons = {
REPLY_TO_CLICKED: 'REPLY_TO_CLICKED',
OPEN_ATTACHMENT_TYPE_SELECTOR: 'OPEN_ATTACHMENT_TYPE_SELECTOR',
SEARCH_OPENED: 'SEARCH_OPENED',
REUSABLE_CONTEXT_MENU_OPENED: 'REUSABLE_CONTEXT_MENU_OPENED',
},
roomList: {
ROOMLIST_UPDATED: 'ROOMLIST_UPDATED',

View file

@ -73,6 +73,8 @@ class Navigation extends EventEmitter {
this.emit(cons.events.navigation.SPACE_SELECTED, this.selectedSpaceId);
},
[cons.actions.navigation.SELECT_ROOM]: () => {
if (this.selectedRoomId === action.roomId) return;
const prevSelectedRoomId = this.selectedRoomId;
this.selectedRoomId = action.roomId;
this.removeRecentRoom(prevSelectedRoomId);
@ -152,6 +154,14 @@ class Navigation extends EventEmitter {
action.term,
);
},
[cons.actions.navigation.OPEN_REUSABLE_CONTEXT_MENU]: () => {
this.emit(
cons.events.navigation.REUSABLE_CONTEXT_MENU_OPENED,
action.placement,
action.cords,
action.render,
);
},
};
actions[action.type]?.();
}

View file

@ -48,11 +48,19 @@ class Settings extends EventEmitter {
setTheme(themeIndex) {
const appBody = document.getElementById('appBody');
appBody.classList.remove('system-theme');
this.themes.forEach((themeName) => {
if (themeName === '') return;
appBody.classList.remove(themeName);
});
if (this.themes[themeIndex] !== '') appBody.classList.add(this.themes[themeIndex]);
// If use system theme is enabled
// we will override current theme choice with system theme
if (this.useSystemTheme) {
appBody.classList.add('system-theme');
} else if (this.themes[themeIndex] !== '') {
appBody.classList.add(this.themes[themeIndex]);
}
setSettings('themeIndex', themeIndex);
this.themeIndex = themeIndex;
}
@ -106,19 +114,9 @@ class Settings extends EventEmitter {
const actions = {
[cons.actions.settings.TOGGLE_SYSTEM_THEME]: () => {
this.useSystemTheme = !this.useSystemTheme;
setSettings('useSystemTheme', this.useSystemTheme);
const appBody = document.getElementById('appBody');
if (this.useSystemTheme) {
appBody.classList.add('system-theme');
this.themes.forEach((themeName) => {
if (themeName === '') return;
appBody.classList.remove(themeName);
});
} else {
appBody.classList.remove('system-theme');
this.setTheme(this.themeIndex);
}
setSettings('useSystemTheme', this.useSystemTheme);
this.setTheme(this.themeIndex);
this.emit(cons.events.settings.SYSTEM_THEME_TOGGLED, this.useSystemTheme);
},

View file

@ -21,11 +21,28 @@ export function isInSameDay(dt2, dt1) {
);
}
export function getEventCords(ev) {
const boxInfo = ev.target.getBoundingClientRect();
/**
* @param {Event} ev
* @param {string} [targetSelector] element selector for Element.matches([selector])
*/
export function getEventCords(ev, targetSelector) {
let boxInfo;
const path = ev.nativeEvent.composedPath();
const target = targetSelector
? path.find((element) => element.matches?.(targetSelector))
: null;
if (target) {
boxInfo = target.getBoundingClientRect();
} else {
boxInfo = ev.target.getBoundingClientRect();
}
return {
x: boxInfo.x,
y: boxInfo.y,
width: boxInfo.width,
height: boxInfo.height,
detail: ev.detail,
};
}