diff --git a/public/res/ic/outlined/shield-user.svg b/public/res/ic/outlined/shield-user.svg new file mode 100644 index 0000000..bd5f07c --- /dev/null +++ b/public/res/ic/outlined/shield-user.svg @@ -0,0 +1,11 @@ + + + + + + + + + + diff --git a/src/app/atoms/input/Input.jsx b/src/app/atoms/input/Input.jsx index 5c1d842..b65d521 100644 --- a/src/app/atoms/input/Input.jsx +++ b/src/app/atoms/input/Input.jsx @@ -8,7 +8,7 @@ function Input({ id, label, name, value, placeholder, required, type, onChange, forwardRef, resizable, minHeight, onResize, state, - onKeyDown, + onKeyDown, disabled, }) { return (
@@ -29,6 +29,7 @@ function Input({ onChange={onChange} onResize={onResize} onKeyDown={onKeyDown} + disabled={disabled} /> ) : ( )}
@@ -64,6 +66,7 @@ Input.defaultProps = { onResize: null, state: 'normal', onKeyDown: null, + disabled: false, }; Input.propTypes = { @@ -81,6 +84,7 @@ Input.propTypes = { onResize: PropTypes.func, state: PropTypes.oneOf(['normal', 'success', 'error']), onKeyDown: PropTypes.func, + disabled: PropTypes.bool, }; export default Input; diff --git a/src/app/atoms/input/Input.scss b/src/app/atoms/input/Input.scss index efe7da7..40fe43e 100644 --- a/src/app/atoms/input/Input.scss +++ b/src/app/atoms/input/Input.scss @@ -1,3 +1,5 @@ +@use '../../atoms/scroll/scrollbar'; + .input { display: block; width: 100%; @@ -13,6 +15,11 @@ letter-spacing: var(--ls-b2); line-height: var(--lh-b2); + :disabled { + opacity: 0.4; + cursor: no-drop; + } + &__label { display: inline-block; margin-bottom: var(--sp-ultra-tight); @@ -21,6 +28,10 @@ &--resizable { resize: vertical !important; + overflow-y: auto !important; + @include scrollbar.scroll; + @include scrollbar.scroll__v; + @include scrollbar.scroll--auto-hide; } &--success { border: 1px solid var(--bg-positive); diff --git a/src/app/atoms/modal/RawModal.scss b/src/app/atoms/modal/RawModal.scss index d008cc0..72a64d7 100644 --- a/src/app/atoms/modal/RawModal.scss +++ b/src/app/atoms/modal/RawModal.scss @@ -1,6 +1,6 @@ .ReactModal__Overlay { opacity: 0; - transition: opacity 200ms cubic-bezier(0.13, 0.56, 0.25, 0.99); + transition: opacity 200ms var(--fluid-slide-up); } .ReactModal__Overlay--after-open{ opacity: 1; @@ -11,7 +11,7 @@ .ReactModal__Content { transform: translateY(100%); - transition: transform 200ms cubic-bezier(0.13, 0.56, 0.25, 0.99); + transition: transform 200ms var(--fluid-slide-up); } .ReactModal__Content--after-open{ diff --git a/src/app/organisms/room/Room.jsx b/src/app/organisms/room/Room.jsx index 164e490..b8972d6 100644 --- a/src/app/organisms/room/Room.jsx +++ b/src/app/organisms/room/Room.jsx @@ -9,6 +9,7 @@ import RoomTimeline from '../../../client/state/RoomTimeline'; import Welcome from '../welcome/Welcome'; import RoomView from './RoomView'; +import RoomSettings from './RoomSettings'; import PeopleDrawer from './PeopleDrawer'; function Room() { @@ -42,8 +43,11 @@ function Room() { if (roomTimeline === null) return ; return ( -
- +
+
+ + +
{ isDrawer && }
); diff --git a/src/app/organisms/room/Room.scss b/src/app/organisms/room/Room.scss index cea4bad..975acbd 100644 --- a/src/app/organisms/room/Room.scss +++ b/src/app/organisms/room/Room.scss @@ -1,4 +1,12 @@ -.room-container { - display: flex; +@use '../../partials/flex'; + +.room { + @extend .cp-fx__row; height: 100%; + + &__content { + @extend .cp-fx__item-one; + position: relative; + overflow: hidden; + } } \ No newline at end of file diff --git a/src/app/organisms/room/RoomSettings.jsx b/src/app/organisms/room/RoomSettings.jsx new file mode 100644 index 0000000..82817b5 --- /dev/null +++ b/src/app/organisms/room/RoomSettings.jsx @@ -0,0 +1,85 @@ +import React, { useEffect } from 'react'; +import PropTypes from 'prop-types'; +import './RoomSettings.scss'; + +import cons from '../../../client/state/cons'; +import navigation from '../../../client/state/navigation'; + +import Text from '../../atoms/text/Text'; +import Header, { TitleWrapper } from '../../atoms/header/Header'; +import ScrollView from '../../atoms/scroll/ScrollView'; +import Tabs from '../../atoms/tabs/Tabs'; +import RoomProfile from '../../molecules/room-profile/RoomProfile'; + +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 { useForceUpdate } from '../../hooks/useForceUpdate'; + +const tabItems = [{ + iconSrc: SettingsIC, + text: 'General', + disabled: false, +}, { + iconSrc: SearchIC, + text: 'Search', + disabled: false, +}, { + iconSrc: ShieldUserIC, + text: 'Permissions', + disabled: false, +}, { + iconSrc: LockIC, + text: 'Security', + disabled: false, +}, { + iconSrc: InfoIC, + text: 'Advanced', + disabled: false, +}]; + +function RoomSettings({ roomId }) { + const [, forceUpdate] = useForceUpdate(); + + useEffect(() => { + const settingsToggle = (isVisible) => { + if (isVisible) forceUpdate(); + else setTimeout(() => forceUpdate(), 200); + }; + navigation.on(cons.events.navigation.ROOM_SETTINGS_TOGGLED, settingsToggle); + return () => { + navigation.removeListener(cons.events.navigation.ROOM_SETTINGS_TOGGLED, settingsToggle); + }; + }, []); + + if (!navigation.isRoomSettings) return null; + + return ( +
+ +
+
+ + Room settings + +
+ + false} /> +
+ {/*
+
*/} +
+
+
+
+ ); +} + +RoomSettings.propTypes = { + roomId: PropTypes.string.isRequired, +}; + +export default RoomSettings; diff --git a/src/app/organisms/room/RoomSettings.scss b/src/app/organisms/room/RoomSettings.scss new file mode 100644 index 0000000..fd78f6b --- /dev/null +++ b/src/app/organisms/room/RoomSettings.scss @@ -0,0 +1,39 @@ +@use '../../partials/dir'; + +.room-settings { + height: 100%; + + &__content { + padding-bottom: calc(2 * var(--sp-extra-loose)); + position: relative; + + & .room-profile { + margin: var(--sp-extra-loose); + } + } + + & .tabs { + position: sticky; + top: 0; + width: 100%; + background-color: var(--bg-surface-low); + box-shadow: 0 -4px 0 var(--bg-surface-low), + inset 0 -1px 0 var(--bg-surface-border); + + &__content { + padding: 0 var(--sp-normal); + } + } + + &__cards-wrapper { + padding: var(--sp-normal); + @include dir.side(padding, var(--sp-normal), var(--sp-extra-tight)); + } + + &__card { + background-color: var(--bg-surface); + border-radius: var(--bo-radius); + box-shadow: var(--bs-surface-border); + padding: 16px; + } +} \ No newline at end of file diff --git a/src/app/organisms/room/RoomView.jsx b/src/app/organisms/room/RoomView.jsx index 7b75127..7851f25 100644 --- a/src/app/organisms/room/RoomView.jsx +++ b/src/app/organisms/room/RoomView.jsx @@ -1,9 +1,12 @@ -import React from 'react'; +import React, { useEffect, useRef } from 'react'; import PropTypes from 'prop-types'; import './RoomView.scss'; import EventEmitter from 'events'; +import cons from '../../../client/state/cons'; +import navigation from '../../../client/state/navigation'; + import RoomViewHeader from './RoomViewHeader'; import RoomViewContent from './RoomViewContent'; import RoomViewFloating from './RoomViewFloating'; @@ -13,11 +16,27 @@ import RoomViewCmdBar from './RoomViewCmdBar'; const viewEvent = new EventEmitter(); function RoomView({ roomTimeline, eventId }) { + const roomViewRef = useRef(null); // eslint-disable-next-line react/prop-types const { roomId } = roomTimeline; + useEffect(() => { + const settingsToggle = (isVisible) => { + const roomView = roomViewRef.current; + roomView.classList.toggle('room-view--dropped'); + + const roomViewContent = roomView.children[1]; + if (isVisible) setTimeout(() => { roomViewContent.style.visibility = 'hidden'; }, 200); + else roomViewContent.style.visibility = 'visible'; + }; + navigation.on(cons.events.navigation.ROOM_SETTINGS_TOGGLED, settingsToggle); + return () => { + navigation.removeListener(cons.events.navigation.ROOM_SETTINGS_TOGGLED, settingsToggle); + }; + }, []); + return ( -
+
diff --git a/src/app/organisms/room/RoomView.scss b/src/app/organisms/room/RoomView.scss index 9e1c8b4..73aac89 100644 --- a/src/app/organisms/room/RoomView.scss +++ b/src/app/organisms/room/RoomView.scss @@ -1,8 +1,23 @@ @use '../../partials/flex'; .room-view { - @extend .cp-fx__item-one; @extend .cp-fx__column; + background-color: var(--bg-surface); + height: 100%; + width: 100%; + position: absolute; + top: 0; + z-index: 99; + box-shadow: none; + + transition: transform 200ms var(--fluid-slide-down), + box-shadow 200ms var(--fluid-slide-down); + + &--dropped { + transform: translateY(calc(100% - var(--header-height))); + border-radius: var(--bo-radius) var(--bo-radius) 0 0; + box-shadow: var(--bs-popup); + } &__content-wrapper { @extend .cp-fx__item-one; diff --git a/src/app/organisms/room/RoomViewHeader.jsx b/src/app/organisms/room/RoomViewHeader.jsx index e9153f5..284b306 100644 --- a/src/app/organisms/room/RoomViewHeader.jsx +++ b/src/app/organisms/room/RoomViewHeader.jsx @@ -1,20 +1,26 @@ -import React from 'react'; +import React, { useEffect, useRef } from 'react'; import PropTypes from 'prop-types'; +import './RoomViewHeader.scss'; import { twemojify } from '../../../util/twemojify'; +import { blurOnBubbling } from '../../atoms/button/script'; import initMatrix from '../../../client/initMatrix'; -import { openRoomOptions } from '../../../client/action/navigation'; +import cons from '../../../client/state/cons'; +import navigation from '../../../client/state/navigation'; +import { toggleRoomSettings, openRoomOptions } from '../../../client/action/navigation'; import { togglePeopleDrawer } from '../../../client/action/settings'; import colorMXID from '../../../util/colorMXID'; import { getEventCords } from '../../../util/common'; import Text from '../../atoms/text/Text'; +import RawIcon from '../../atoms/system-icons/RawIcon'; import IconButton from '../../atoms/button/IconButton'; import Header, { TitleWrapper } from '../../atoms/header/Header'; import Avatar from '../../atoms/avatar/Avatar'; import UserIC from '../../../../public/res/ic/outlined/user.svg'; +import ChevronBottomIC from '../../../../public/res/ic/outlined/chevron-bottom.svg'; import VerticalMenuIC from '../../../../public/res/ic/outlined/vertical-menu.svg'; function RoomViewHeader({ roomId }) { @@ -23,15 +29,35 @@ function RoomViewHeader({ roomId }) { let avatarSrc = mx.getRoom(roomId).getAvatarUrl(mx.baseUrl, 36, 36, 'crop'); avatarSrc = isDM ? mx.getRoom(roomId).getAvatarFallbackMember()?.getAvatarUrl(mx.baseUrl, 36, 36, 'crop') : avatarSrc; const roomName = mx.getRoom(roomId).name; - const roomTopic = mx.getRoom(roomId).currentState.getStateEvents('m.room.topic')[0]?.getContent().topic; + const roomHeaderBtnRef = useRef(null); + useEffect(() => { + const settingsToggle = (isVisibile) => { + const rawIcon = roomHeaderBtnRef.current.lastElementChild; + rawIcon.style.transform = isVisibile + ? 'rotateX(180deg)' + : 'rotateX(0deg)'; + }; + navigation.on(cons.events.navigation.ROOM_SETTINGS_TOGGLED, settingsToggle); + return () => { + navigation.removeListener(cons.events.navigation.ROOM_SETTINGS_TOGGLED, settingsToggle); + }; + }, []); return (
- - - {twemojify(roomName)} - { typeof roomTopic !== 'undefined' &&

{twemojify(roomTopic)}

} -
+ openRoomOptions(getEventCords(e), roomId)} diff --git a/src/app/organisms/room/RoomViewHeader.scss b/src/app/organisms/room/RoomViewHeader.scss new file mode 100644 index 0000000..ddfab6c --- /dev/null +++ b/src/app/organisms/room/RoomViewHeader.scss @@ -0,0 +1,27 @@ +@use '../../partials/flex'; +@use '../../partials/dir'; + +.room-header__btn { + min-width: 0; + @extend .cp-fx__row--s-c; + @include dir.side(margin, 0, auto); + border-radius: var(--bo-radius); + cursor: pointer; + + & .ic-raw { + @include dir.side(margin, 0, var(--sp-extra-tight)); + transition: transform 200ms ease-in-out; + } + @media (hover:hover) { + &:hover { + background-color: var(--bg-surface-hover); + box-shadow: var(--bs-surface-outline); + } + } + &:focus, + &:active { + background-color: var(--bg-surface-active); + box-shadow: var(--bs-surface-outline); + outline: none; + } +} \ No newline at end of file diff --git a/src/app/organisms/room/RoomViewInput.jsx b/src/app/organisms/room/RoomViewInput.jsx index 87d3b5b..e8d5d39 100644 --- a/src/app/organisms/room/RoomViewInput.jsx +++ b/src/app/organisms/room/RoomViewInput.jsx @@ -151,7 +151,6 @@ function RoomViewInput({ navigation.on(cons.events.navigation.REPLY_TO_CLICKED, setUpReply); if (textAreaRef?.current !== null) { isTyping = false; - focusInput(); textAreaRef.current.value = roomsInput.getMessage(roomId); setAttachment(roomsInput.getAttachment(roomId)); setReplyTo(roomsInput.getReplyTo(roomId)); diff --git a/src/app/organisms/welcome/Welcome.scss b/src/app/organisms/welcome/Welcome.scss index 7f242c9..e55bb8e 100644 --- a/src/app/organisms/welcome/Welcome.scss +++ b/src/app/organisms/welcome/Welcome.scss @@ -3,6 +3,7 @@ .app-welcome { width: 100%; height: 100%; + background-color: var(--bg-surface); & > div { @extend .cp-fx__column--c-c; diff --git a/src/app/templates/client/Client.scss b/src/app/templates/client/Client.scss index 546c151..45a5052 100644 --- a/src/app/templates/client/Client.scss +++ b/src/app/templates/client/Client.scss @@ -9,7 +9,6 @@ .room__wrapper { flex: 1; min-width: 0; - background-color: var(--bg-surface); } diff --git a/src/client/action/navigation.js b/src/client/action/navigation.js index 693ff7a..02ee13a 100644 --- a/src/client/action/navigation.js +++ b/src/client/action/navigation.js @@ -23,6 +23,13 @@ export function selectRoom(roomId, eventId) { }); } +export function toggleRoomSettings(roomId) { + appDispatcher.dispatch({ + type: cons.actions.navigation.TOGGLE_ROOM_SETTINGS, + roomId, + }); +} + export function openInviteList() { appDispatcher.dispatch({ type: cons.actions.navigation.OPEN_INVITE_LIST, diff --git a/src/client/event/hotkeys.js b/src/client/event/hotkeys.js index 41c93e9..ab94635 100644 --- a/src/client/event/hotkeys.js +++ b/src/client/event/hotkeys.js @@ -1,4 +1,4 @@ -import { openSearch } from '../action/navigation'; +import { openSearch, toggleRoomSettings } from '../action/navigation'; import navigation from '../state/navigation'; function listenKeyboard(event) { @@ -11,12 +11,24 @@ function listenKeyboard(event) { openSearch(); } } + if (!event.ctrlKey && !event.altKey) { + if (event.keyCode === 38 && navigation.isRoomSettings) { + // close room settings + toggleRoomSettings(); + return; + } + if (event.keyCode === 40 && !navigation.isRoomSettings) { + // open room settings + toggleRoomSettings(); + return; + } + if (navigation.isRawModalVisible) return; if (['text', 'textarea'].includes(document.activeElement.type)) { return; } - if (event.keyCode < 48 + if ((event.keyCode !== 8 && event.keyCode < 48) || (event.keyCode >= 91 && event.keyCode <= 93) || (event.keyCode >= 112 && event.keyCode <= 183)) { return; diff --git a/src/client/state/cons.js b/src/client/state/cons.js index f15bd3b..9d34c39 100644 --- a/src/client/state/cons.js +++ b/src/client/state/cons.js @@ -30,7 +30,7 @@ const cons = { SELECT_TAB: 'SELECT_TAB', SELECT_SPACE: 'SELECT_SPACE', SELECT_ROOM: 'SELECT_ROOM', - TOGGLE_PEOPLE_DRAWER: 'TOGGLE_PEOPLE_DRAWER', + TOGGLE_ROOM_SETTINGS: 'TOGGLE_ROOM_SETTINGS', OPEN_INVITE_LIST: 'OPEN_INVITE_LIST', OPEN_PUBLIC_ROOMS: 'OPEN_PUBLIC_ROOMS', OPEN_CREATE_ROOM: 'OPEN_CREATE_ROOM', @@ -65,7 +65,7 @@ const cons = { TAB_SELECTED: 'TAB_SELECTED', SPACE_SELECTED: 'SPACE_SELECTED', ROOM_SELECTED: 'ROOM_SELECTED', - PEOPLE_DRAWER_TOGGLED: 'PEOPLE_DRAWER_TOGGLED', + ROOM_SETTINGS_TOGGLED: 'ROOM_SETTINGS_TOGGLED', INVITE_LIST_OPENED: 'INVITE_LIST_OPENED', PUBLIC_ROOMS_OPENED: 'PUBLIC_ROOMS_OPENED', CREATE_ROOM_OPENED: 'CREATE_ROOM_OPENED', diff --git a/src/client/state/navigation.js b/src/client/state/navigation.js index 44681ee..8ebf404 100644 --- a/src/client/state/navigation.js +++ b/src/client/state/navigation.js @@ -11,6 +11,7 @@ class Navigation extends EventEmitter { this.selectedSpacePath = [cons.tabs.HOME]; this.selectedRoomId = null; + this.isRoomSettings = false; this.recentRooms = []; this.isRawModalVisible = false; @@ -77,6 +78,10 @@ class Navigation extends EventEmitter { this.removeRecentRoom(prevSelectedRoomId); this.addRecentRoom(prevSelectedRoomId); this.removeRecentRoom(this.selectedRoomId); + if (this.isRoomSettings) { + this.isRoomSettings = !this.isRoomSettings; + this.emit(cons.events.navigation.ROOM_SETTINGS_TOGGLED, this.isRoomSettings); + } this.emit( cons.events.navigation.ROOM_SELECTED, this.selectedRoomId, @@ -84,6 +89,10 @@ class Navigation extends EventEmitter { action.eventId, ); }, + [cons.actions.navigation.TOGGLE_ROOM_SETTINGS]: () => { + this.isRoomSettings = !this.isRoomSettings; + this.emit(cons.events.navigation.ROOM_SETTINGS_TOGGLED, this.isRoomSettings); + }, [cons.actions.navigation.OPEN_INVITE_LIST]: () => { this.emit(cons.events.navigation.INVITE_LIST_OPENED); }, diff --git a/src/index.scss b/src/index.scss index 3e78805..6807a20 100644 --- a/src/index.scss +++ b/src/index.scss @@ -68,6 +68,7 @@ /* system icons | --ic-[background type]-[priority]: value */ + --ic-surface-high: #272727; --ic-surface-normal: #626262; --ic-surface-low: #7c7c7c; --ic-primary-normal: #ffffff; @@ -177,6 +178,8 @@ /* transition curves */ --fluid-push: cubic-bezier(0, 0.8, 0.67, 0.97); + --fluid-slide-down: cubic-bezier(0.02, 0.82, 0.4, 0.96); + --fluid-slide-up: cubic-bezier(0.13, 0.56, 0.25, 0.99); --font-primary: 'Roboto', sans-serif; --font-secondary: 'Roboto', sans-serif; @@ -218,6 +221,7 @@ /* text color | --tc-[background type]-[priority]: value */ + --ic-surface-high: rgb(255, 255, 255);; --tc-surface-high: rgba(255, 255, 255, 98%); --tc-surface-normal: rgba(255, 255, 255, 94%); --tc-surface-normal-low: rgba(255, 255, 255, 60%); @@ -299,7 +303,8 @@ /* system icons | --ic-[background type]-[priority]: value */ - --ic-surface-normal: rgb(255, 251, 222, 84%); + --ic-surface-high: rgb(255, 251, 222); + --ic-surface-normal: rgba(255, 251, 222, 84%); --ic-surface-low: rgba(255, 251, 222, 64%); }