Add manage pin spaces
authorAjay Bura <ajbura@gmail.com>
Sun, 6 Mar 2022 12:22:04 +0000 (17:52 +0530)
committerAjay Bura <ajbura@gmail.com>
Sun, 6 Mar 2022 12:22:04 +0000 (17:52 +0530)
Signed-off-by: Ajay Bura <ajbura@gmail.com>
src/app/organisms/navigation/SideBar.jsx
src/app/organisms/pw/Dialogs.jsx
src/app/organisms/shortcut-spaces/ShortcutSpaces.jsx [new file with mode: 0644]
src/app/organisms/shortcut-spaces/ShortcutSpaces.scss [new file with mode: 0644]
src/client/action/navigation.js
src/client/state/cons.js
src/client/state/navigation.js

index fe4645d273ab48d8df28cbb118a91b72e78eec30..8ddcd3bc19b36da0b2324d0c31b0609a8a901e8a 100644 (file)
@@ -5,8 +5,8 @@ import initMatrix from '../../../client/initMatrix';
 import cons from '../../../client/state/cons';
 import colorMXID from '../../../util/colorMXID';
 import {
-  selectTab, openInviteList, openSearch,
-  openSettings, openReusableContextMenu,
+  selectTab, openShortcutSpaces, openInviteList,
+  openSearch, openSettings, openReusableContextMenu,
 } from '../../../client/action/navigation';
 import { abbreviateNumber, getEventCords } from '../../../util/common';
 
@@ -16,6 +16,7 @@ import SpaceOptions from '../../molecules/space-options/SpaceOptions';
 
 import HomeIC from '../../../../public/res/ic/outlined/home.svg';
 import UserIC from '../../../../public/res/ic/outlined/user.svg';
+import AddPinIC from '../../../../public/res/ic/outlined/add-pin.svg';
 import SearchIC from '../../../../public/res/ic/outlined/search.svg';
 import InviteIC from '../../../../public/res/ic/outlined/invite.svg';
 
@@ -190,6 +191,11 @@ function SideBar() {
                   );
                 })
               }
+              <SidebarAvatar
+                onClick={() => openShortcutSpaces()}
+                tooltip="Pin spaces"
+                iconSrc={AddPinIC}
+              />
             </div>
           </div>
         </ScrollView>
index ca79818a137dd3c3e10301009e3ce8d160fae0ff..5c5e115cccb9e99f4cecb475c27cf265cfac33d5 100644 (file)
@@ -2,6 +2,7 @@ import React from 'react';
 
 import ReadReceipts from '../read-receipts/ReadReceipts';
 import ProfileViewer from '../profile-viewer/ProfileViewer';
+import ShortcutSpaces from '../shortcut-spaces/ShortcutSpaces';
 import SpaceAddExisting from '../../molecules/space-add-existing/SpaceAddExisting';
 import Search from '../search/Search';
 import ViewSource from '../view-source/ViewSource';
@@ -13,6 +14,7 @@ function Dialogs() {
       <ReadReceipts />
       <ViewSource />
       <ProfileViewer />
+      <ShortcutSpaces />
       <CreateRoom />
       <SpaceAddExisting />
       <Search />
diff --git a/src/app/organisms/shortcut-spaces/ShortcutSpaces.jsx b/src/app/organisms/shortcut-spaces/ShortcutSpaces.jsx
new file mode 100644 (file)
index 0000000..c379b6b
--- /dev/null
@@ -0,0 +1,169 @@
+import React, { useState, useEffect } from 'react';
+import './ShortcutSpaces.scss';
+
+import initMatrix from '../../../client/initMatrix';
+import cons from '../../../client/state/cons';
+import navigation from '../../../client/state/navigation';
+import { createSpaceShortcut, deleteSpaceShortcut } from '../../../client/action/accountData';
+import { joinRuleToIconSrc } from '../../../util/matrixUtil';
+
+import Text from '../../atoms/text/Text';
+import Button from '../../atoms/button/Button';
+import IconButton from '../../atoms/button/IconButton';
+import Checkbox from '../../atoms/button/Checkbox';
+import Spinner from '../../atoms/spinner/Spinner';
+import RoomSelector from '../../molecules/room-selector/RoomSelector';
+import Dialog from '../../molecules/dialog/Dialog';
+
+import PinIC from '../../../../public/res/ic/outlined/pin.svg';
+import PinFilledIC from '../../../../public/res/ic/filled/pin.svg';
+import CrossIC from '../../../../public/res/ic/outlined/cross.svg';
+
+import { useSpaceShortcut } from '../../hooks/useSpaceShortcut';
+import { AtoZ } from '../navigation/common';
+
+function ShortcutSpacesContent() {
+  const mx = initMatrix.matrixClient;
+  const { spaces, roomIdToParents } = initMatrix.roomList;
+
+  const [spaceShortcut] = useSpaceShortcut();
+  const spaceWithoutShortcut = [...spaces].filter(
+    (spaceId) => !spaceShortcut.includes(spaceId),
+  ).sort(AtoZ);
+
+  const [process, setProcess] = useState(null);
+  const [selected, setSelected] = useState([]);
+
+  useEffect(() => {
+    if (process !== null) {
+      setProcess(null);
+      setSelected([]);
+    }
+  }, [spaceShortcut]);
+
+  const toggleSelection = (sId) => {
+    if (process !== null) return;
+    const newSelected = [...selected];
+    const selectedIndex = newSelected.indexOf(sId);
+
+    if (selectedIndex > -1) {
+      newSelected.splice(selectedIndex, 1);
+      setSelected(newSelected);
+      return;
+    }
+    newSelected.push(sId);
+    setSelected(newSelected);
+  };
+
+  const handleAdd = () => {
+    setProcess(`Pinning ${selected.length} spaces...`);
+    createSpaceShortcut(selected);
+  };
+
+  const renderSpace = (spaceId, isShortcut) => {
+    const room = mx.getRoom(spaceId);
+    if (!room) return null;
+
+    const parentSet = roomIdToParents.get(spaceId);
+    const parentNames = parentSet
+      ? [...parentSet].map((parentId) => mx.getRoom(parentId).name)
+      : undefined;
+    const parents = parentNames ? parentNames.join(', ') : null;
+
+    const toggleSelected = () => toggleSelection(spaceId);
+    const deleteShortcut = () => deleteSpaceShortcut(spaceId);
+
+    return (
+      <RoomSelector
+        key={spaceId}
+        name={room.name}
+        parentName={parents}
+        roomId={spaceId}
+        imageSrc={null}
+        iconSrc={joinRuleToIconSrc(room.getJoinRule(), true)}
+        isUnread={false}
+        notificationCount={0}
+        isAlert={false}
+        onClick={isShortcut ? deleteShortcut : toggleSelected}
+        options={isShortcut ? (
+          <IconButton
+            src={isShortcut ? PinFilledIC : PinIC}
+            size="small"
+            onClick={deleteShortcut}
+            disabled={process !== null}
+          />
+        ) : (
+          <Checkbox
+            isActive={selected.includes(spaceId)}
+            variant="positive"
+            onToggle={toggleSelected}
+            tabIndex={-1}
+            disabled={process !== null}
+          />
+        )}
+      />
+    );
+  };
+
+  return (
+    <>
+      <Text className="shortcut-spaces__header" variant="b3" weight="bold">Pinned spaces</Text>
+      {spaceShortcut.length === 0 && <Text>No pinned spaces</Text>}
+      {spaceShortcut.map((spaceId) => renderSpace(spaceId, true))}
+      <Text className="shortcut-spaces__header" variant="b3" weight="bold">Unpinned spaces</Text>
+      {spaceWithoutShortcut.length === 0 && <Text>No unpinned spaces</Text>}
+      {spaceWithoutShortcut.map((spaceId) => renderSpace(spaceId, false))}
+      {selected.length !== 0 && (
+        <div className="shortcut-spaces__footer">
+          {process && <Spinner size="small" />}
+          <Text weight="medium">{process || `${selected.length} spaces selected`}</Text>
+          { !process && (
+            <Button onClick={handleAdd} variant="primary">Pin</Button>
+          )}
+        </div>
+      )}
+    </>
+  );
+}
+
+function useVisibilityToggle() {
+  const [isOpen, setIsOpen] = useState(false);
+
+  useEffect(() => {
+    const handleOpen = () => setIsOpen(true);
+    navigation.on(cons.events.navigation.SHORTCUT_SPACES_OPENED, handleOpen);
+    return () => {
+      navigation.removeListener(cons.events.navigation.SHORTCUT_SPACES_OPENED, handleOpen);
+    };
+  }, []);
+
+  const requestClose = () => setIsOpen(false);
+
+  return [isOpen, requestClose];
+}
+
+function ShortcutSpaces() {
+  const [isOpen, requestClose] = useVisibilityToggle();
+
+  return (
+    <Dialog
+      isOpen={isOpen}
+      className="shortcut-spaces"
+      title={(
+        <Text variant="s1" weight="medium" primary>
+          Pin spaces
+        </Text>
+      )}
+      contentOptions={<IconButton src={CrossIC} onClick={requestClose} tooltip="Close" />}
+      onRequestClose={requestClose}
+    >
+      {
+        isOpen
+          ? <ShortcutSpacesContent />
+          : <div />
+      }
+    </Dialog>
+  );
+}
+
+export default ShortcutSpaces;
diff --git a/src/app/organisms/shortcut-spaces/ShortcutSpaces.scss b/src/app/organisms/shortcut-spaces/ShortcutSpaces.scss
new file mode 100644 (file)
index 0000000..686c8cc
--- /dev/null
@@ -0,0 +1,52 @@
+@use '../../partials/dir';
+@use '../../partials/flex';
+
+.shortcut-spaces {
+  height: 100%;
+  .dialog__content-container {
+    padding: 0;
+    padding-bottom: 80px;
+    @include dir.side(padding, var(--sp-extra-tight), 0);
+
+    & > .text-b1 {
+      padding: 0 var(--sp-extra-tight);
+    }
+  }
+  
+  &__header {
+    margin-top: var(--sp-extra-tight);
+    padding: var(--sp-extra-tight);
+    text-transform: uppercase;
+  }
+
+  .room-selector {
+    margin: 0 var(--sp-extra-tight);
+  }
+  .room-selector__options {
+    display: flex;
+    .checkbox {
+      margin: 0 6px;
+    }
+  }
+
+  &__footer {
+    position: absolute;
+    bottom: 0;
+    left: 0;
+    width: 100%;
+    padding: var(--sp-normal);
+    background-color: var(--bg-surface);
+    border-top: 1px solid var(--bg-surface-border);
+    display: flex;
+    align-items: center;
+  
+    & > .text {
+      @extend .cp-fx__item-one;
+      padding: 0 var(--sp-tight);
+    }
+  
+    & > button {
+      @include dir.side(margin, var(--sp-normal), 0);
+    }
+  }
+}
index 817caddf6ca3712bcd0190ef99f1e43daf65f57b..c145502a7527cfa70c7efc6131aaa8365ff22ec2 100644 (file)
@@ -52,6 +52,12 @@ export function toggleRoomSettings(tabText) {
   });
 }
 
+export function openShortcutSpaces() {
+  appDispatcher.dispatch({
+    type: cons.actions.navigation.OPEN_SHORTCUT_SPACES,
+  });
+}
+
 export function openInviteList() {
   appDispatcher.dispatch({
     type: cons.actions.navigation.OPEN_INVITE_LIST,
index aae179f92f96043abffd240051d072fea8cb32e6..141cb1fc24cb135fd44f05d2e12a7e1fc41f8692 100644 (file)
@@ -34,6 +34,7 @@ const cons = {
       OPEN_SPACE_MANAGE: 'OPEN_SPACE_MANAGE',
       OPEN_SPACE_ADDEXISTING: 'OPEN_SPACE_ADDEXISTING',
       TOGGLE_ROOM_SETTINGS: 'TOGGLE_ROOM_SETTINGS',
+      OPEN_SHORTCUT_SPACES: 'OPEN_SHORTCUT_SPACES',
       OPEN_INVITE_LIST: 'OPEN_INVITE_LIST',
       OPEN_PUBLIC_ROOMS: 'OPEN_PUBLIC_ROOMS',
       OPEN_CREATE_ROOM: 'OPEN_CREATE_ROOM',
@@ -76,6 +77,7 @@ const cons = {
       SPACE_MANAGE_OPENED: 'SPACE_MANAGE_OPENED',
       SPACE_ADDEXISTING_OPENED: 'SPACE_ADDEXISTING_OPENED',
       ROOM_SETTINGS_TOGGLED: 'ROOM_SETTINGS_TOGGLED',
+      SHORTCUT_SPACES_OPENED: 'SHORTCUT_SPACES_OPENED',
       INVITE_LIST_OPENED: 'INVITE_LIST_OPENED',
       PUBLIC_ROOMS_OPENED: 'PUBLIC_ROOMS_OPENED',
       CREATE_ROOM_OPENED: 'CREATE_ROOM_OPENED',
index ee8d06c28eaefe8f2132fd67f40d7c004fe53875..88de35fbb6962f93c42552c6d0007c28b9820250 100644 (file)
@@ -106,6 +106,9 @@ class Navigation extends EventEmitter {
           action.tabText,
         );
       },
+      [cons.actions.navigation.OPEN_SHORTCUT_SPACES]: () => {
+        this.emit(cons.events.navigation.SHORTCUT_SPACES_OPENED);
+      },
       [cons.actions.navigation.OPEN_INVITE_LIST]: () => {
         this.emit(cons.events.navigation.INVITE_LIST_OPENED);
       },