mirror of
				https://github.com/cinnyapp/cinny.git
				synced 2025-11-04 14:30:29 +03:00 
			
		
		
		
	* add hide activity toggle * stop sending/receiving typing status * send private read receipt when setting toggle is activated * prevent showing read-receipt when feature toggle in on
		
			
				
	
	
		
			336 lines
		
	
	
	
		
			11 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			336 lines
		
	
	
	
		
			11 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
import React, { MouseEventHandler, forwardRef, useMemo, useRef, useState } from 'react';
 | 
						|
import { useNavigate } from 'react-router-dom';
 | 
						|
import {
 | 
						|
  Avatar,
 | 
						|
  Box,
 | 
						|
  Button,
 | 
						|
  Icon,
 | 
						|
  IconButton,
 | 
						|
  Icons,
 | 
						|
  Menu,
 | 
						|
  MenuItem,
 | 
						|
  PopOut,
 | 
						|
  RectCords,
 | 
						|
  Text,
 | 
						|
  config,
 | 
						|
  toRem,
 | 
						|
} from 'folds';
 | 
						|
import { useVirtualizer } from '@tanstack/react-virtual';
 | 
						|
import { useAtom, useAtomValue } from 'jotai';
 | 
						|
import FocusTrap from 'focus-trap-react';
 | 
						|
import { factoryRoomIdByActivity, factoryRoomIdByAtoZ } from '../../../utils/sort';
 | 
						|
import {
 | 
						|
  NavButton,
 | 
						|
  NavCategory,
 | 
						|
  NavCategoryHeader,
 | 
						|
  NavEmptyCenter,
 | 
						|
  NavEmptyLayout,
 | 
						|
  NavItem,
 | 
						|
  NavItemContent,
 | 
						|
  NavLink,
 | 
						|
} from '../../../components/nav';
 | 
						|
import { getExplorePath, getHomeRoomPath, getHomeSearchPath } from '../../pathUtils';
 | 
						|
import { getCanonicalAliasOrRoomId } from '../../../utils/matrix';
 | 
						|
import { useSelectedRoom } from '../../../hooks/router/useSelectedRoom';
 | 
						|
import { useHomeSearchSelected } from '../../../hooks/router/useHomeSelected';
 | 
						|
import { useHomeRooms } from './useHomeRooms';
 | 
						|
import { useMatrixClient } from '../../../hooks/useMatrixClient';
 | 
						|
import { VirtualTile } from '../../../components/virtualizer';
 | 
						|
import { RoomNavCategoryButton, RoomNavItem } from '../../../features/room-nav';
 | 
						|
import { muteChangesAtom } from '../../../state/room-list/mutedRoomList';
 | 
						|
import { makeNavCategoryId } from '../../../state/closedNavCategories';
 | 
						|
import { roomToUnreadAtom } from '../../../state/room/roomToUnread';
 | 
						|
import { useCategoryHandler } from '../../../hooks/useCategoryHandler';
 | 
						|
import { useNavToActivePathMapper } from '../../../hooks/useNavToActivePathMapper';
 | 
						|
import { openCreateRoom, openJoinAlias } from '../../../../client/action/navigation';
 | 
						|
import { PageNav, PageNavHeader, PageNavContent } from '../../../components/page';
 | 
						|
import { useRoomsUnread } from '../../../state/hooks/unread';
 | 
						|
import { markAsRead } from '../../../../client/action/notifications';
 | 
						|
import { useClosedNavCategoriesAtom } from '../../../state/hooks/closedNavCategories';
 | 
						|
import { stopPropagation } from '../../../utils/keyboard';
 | 
						|
import { useSetting } from '../../../state/hooks/settings';
 | 
						|
import { settingsAtom } from '../../../state/settings';
 | 
						|
 | 
						|
type HomeMenuProps = {
 | 
						|
  requestClose: () => void;
 | 
						|
};
 | 
						|
const HomeMenu = forwardRef<HTMLDivElement, HomeMenuProps>(({ requestClose }, ref) => {
 | 
						|
  const orphanRooms = useHomeRooms();
 | 
						|
  const [hideActivity] = useSetting(settingsAtom, 'hideActivity');
 | 
						|
  const unread = useRoomsUnread(orphanRooms, roomToUnreadAtom);
 | 
						|
  const mx = useMatrixClient();
 | 
						|
 | 
						|
  const handleMarkAsRead = () => {
 | 
						|
    if (!unread) return;
 | 
						|
    orphanRooms.forEach((rId) => markAsRead(mx, rId, hideActivity));
 | 
						|
    requestClose();
 | 
						|
  };
 | 
						|
 | 
						|
  const handleJoinAddress = () => {
 | 
						|
    openJoinAlias();
 | 
						|
    requestClose();
 | 
						|
  };
 | 
						|
 | 
						|
  return (
 | 
						|
    <Menu ref={ref} style={{ maxWidth: toRem(160), width: '100vw' }}>
 | 
						|
      <Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
 | 
						|
        <MenuItem
 | 
						|
          onClick={handleMarkAsRead}
 | 
						|
          size="300"
 | 
						|
          after={<Icon size="100" src={Icons.CheckTwice} />}
 | 
						|
          radii="300"
 | 
						|
          aria-disabled={!unread}
 | 
						|
        >
 | 
						|
          <Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
 | 
						|
            Mark as Read
 | 
						|
          </Text>
 | 
						|
        </MenuItem>
 | 
						|
        <MenuItem
 | 
						|
          onClick={handleJoinAddress}
 | 
						|
          size="300"
 | 
						|
          radii="300"
 | 
						|
          after={<Icon size="100" src={Icons.Link} />}
 | 
						|
        >
 | 
						|
          <Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
 | 
						|
            Join with Address
 | 
						|
          </Text>
 | 
						|
        </MenuItem>
 | 
						|
      </Box>
 | 
						|
    </Menu>
 | 
						|
  );
 | 
						|
});
 | 
						|
 | 
						|
function HomeHeader() {
 | 
						|
  const [menuAnchor, setMenuAnchor] = useState<RectCords>();
 | 
						|
 | 
						|
  const handleOpenMenu: MouseEventHandler<HTMLButtonElement> = (evt) => {
 | 
						|
    const cords = evt.currentTarget.getBoundingClientRect();
 | 
						|
    setMenuAnchor((currentState) => {
 | 
						|
      if (currentState) return undefined;
 | 
						|
      return cords;
 | 
						|
    });
 | 
						|
  };
 | 
						|
 | 
						|
  return (
 | 
						|
    <>
 | 
						|
      <PageNavHeader>
 | 
						|
        <Box alignItems="Center" grow="Yes" gap="300">
 | 
						|
          <Box grow="Yes">
 | 
						|
            <Text size="H4" truncate>
 | 
						|
              Home
 | 
						|
            </Text>
 | 
						|
          </Box>
 | 
						|
          <Box>
 | 
						|
            <IconButton aria-pressed={!!menuAnchor} variant="Background" onClick={handleOpenMenu}>
 | 
						|
              <Icon src={Icons.VerticalDots} size="200" />
 | 
						|
            </IconButton>
 | 
						|
          </Box>
 | 
						|
        </Box>
 | 
						|
      </PageNavHeader>
 | 
						|
      <PopOut
 | 
						|
        anchor={menuAnchor}
 | 
						|
        position="Bottom"
 | 
						|
        align="End"
 | 
						|
        offset={6}
 | 
						|
        content={
 | 
						|
          <FocusTrap
 | 
						|
            focusTrapOptions={{
 | 
						|
              initialFocus: false,
 | 
						|
              returnFocusOnDeactivate: false,
 | 
						|
              onDeactivate: () => setMenuAnchor(undefined),
 | 
						|
              clickOutsideDeactivates: true,
 | 
						|
              isKeyForward: (evt: KeyboardEvent) => evt.key === 'ArrowDown',
 | 
						|
              isKeyBackward: (evt: KeyboardEvent) => evt.key === 'ArrowUp',
 | 
						|
              escapeDeactivates: stopPropagation,
 | 
						|
            }}
 | 
						|
          >
 | 
						|
            <HomeMenu requestClose={() => setMenuAnchor(undefined)} />
 | 
						|
          </FocusTrap>
 | 
						|
        }
 | 
						|
      />
 | 
						|
    </>
 | 
						|
  );
 | 
						|
}
 | 
						|
 | 
						|
function HomeEmpty() {
 | 
						|
  const navigate = useNavigate();
 | 
						|
 | 
						|
  return (
 | 
						|
    <NavEmptyCenter>
 | 
						|
      <NavEmptyLayout
 | 
						|
        icon={<Icon size="600" src={Icons.Hash} />}
 | 
						|
        title={
 | 
						|
          <Text size="H5" align="Center">
 | 
						|
            No Rooms
 | 
						|
          </Text>
 | 
						|
        }
 | 
						|
        content={
 | 
						|
          <Text size="T300" align="Center">
 | 
						|
            You do not have any rooms yet.
 | 
						|
          </Text>
 | 
						|
        }
 | 
						|
        options={
 | 
						|
          <>
 | 
						|
            <Button onClick={() => openCreateRoom()} variant="Secondary" size="300">
 | 
						|
              <Text size="B300" truncate>
 | 
						|
                Create Room
 | 
						|
              </Text>
 | 
						|
            </Button>
 | 
						|
            <Button
 | 
						|
              onClick={() => navigate(getExplorePath())}
 | 
						|
              variant="Secondary"
 | 
						|
              fill="Soft"
 | 
						|
              size="300"
 | 
						|
            >
 | 
						|
              <Text size="B300" truncate>
 | 
						|
                Explore Community Rooms
 | 
						|
              </Text>
 | 
						|
            </Button>
 | 
						|
          </>
 | 
						|
        }
 | 
						|
      />
 | 
						|
    </NavEmptyCenter>
 | 
						|
  );
 | 
						|
}
 | 
						|
 | 
						|
const DEFAULT_CATEGORY_ID = makeNavCategoryId('home', 'room');
 | 
						|
export function Home() {
 | 
						|
  const mx = useMatrixClient();
 | 
						|
  useNavToActivePathMapper('home');
 | 
						|
  const scrollRef = useRef<HTMLDivElement>(null);
 | 
						|
  const rooms = useHomeRooms();
 | 
						|
  const muteChanges = useAtomValue(muteChangesAtom);
 | 
						|
  const mutedRooms = muteChanges.added;
 | 
						|
  const roomToUnread = useAtomValue(roomToUnreadAtom);
 | 
						|
 | 
						|
  const selectedRoomId = useSelectedRoom();
 | 
						|
  const searchSelected = useHomeSearchSelected();
 | 
						|
  const noRoomToDisplay = rooms.length === 0;
 | 
						|
  const [closedCategories, setClosedCategories] = useAtom(useClosedNavCategoriesAtom());
 | 
						|
 | 
						|
  const sortedRooms = useMemo(() => {
 | 
						|
    const items = Array.from(rooms).sort(
 | 
						|
      closedCategories.has(DEFAULT_CATEGORY_ID)
 | 
						|
        ? factoryRoomIdByActivity(mx)
 | 
						|
        : factoryRoomIdByAtoZ(mx)
 | 
						|
    );
 | 
						|
    if (closedCategories.has(DEFAULT_CATEGORY_ID)) {
 | 
						|
      return items.filter((rId) => roomToUnread.has(rId) || rId === selectedRoomId);
 | 
						|
    }
 | 
						|
    return items;
 | 
						|
  }, [mx, rooms, closedCategories, roomToUnread, selectedRoomId]);
 | 
						|
 | 
						|
  const virtualizer = useVirtualizer({
 | 
						|
    count: sortedRooms.length,
 | 
						|
    getScrollElement: () => scrollRef.current,
 | 
						|
    estimateSize: () => 38,
 | 
						|
    overscan: 10,
 | 
						|
  });
 | 
						|
 | 
						|
  const handleCategoryClick = useCategoryHandler(setClosedCategories, (categoryId) =>
 | 
						|
    closedCategories.has(categoryId)
 | 
						|
  );
 | 
						|
 | 
						|
  return (
 | 
						|
    <PageNav>
 | 
						|
      <HomeHeader />
 | 
						|
      {noRoomToDisplay ? (
 | 
						|
        <HomeEmpty />
 | 
						|
      ) : (
 | 
						|
        <PageNavContent scrollRef={scrollRef}>
 | 
						|
          <Box direction="Column" gap="300">
 | 
						|
            <NavCategory>
 | 
						|
              <NavItem variant="Background" radii="400">
 | 
						|
                <NavButton onClick={() => openCreateRoom()}>
 | 
						|
                  <NavItemContent>
 | 
						|
                    <Box as="span" grow="Yes" alignItems="Center" gap="200">
 | 
						|
                      <Avatar size="200" radii="400">
 | 
						|
                        <Icon src={Icons.Plus} size="100" />
 | 
						|
                      </Avatar>
 | 
						|
                      <Box as="span" grow="Yes">
 | 
						|
                        <Text as="span" size="Inherit" truncate>
 | 
						|
                          Create Room
 | 
						|
                        </Text>
 | 
						|
                      </Box>
 | 
						|
                    </Box>
 | 
						|
                  </NavItemContent>
 | 
						|
                </NavButton>
 | 
						|
              </NavItem>
 | 
						|
              <NavItem variant="Background" radii="400">
 | 
						|
                <NavButton onClick={() => openJoinAlias()}>
 | 
						|
                  <NavItemContent>
 | 
						|
                    <Box as="span" grow="Yes" alignItems="Center" gap="200">
 | 
						|
                      <Avatar size="200" radii="400">
 | 
						|
                        <Icon src={Icons.Link} size="100" />
 | 
						|
                      </Avatar>
 | 
						|
                      <Box as="span" grow="Yes">
 | 
						|
                        <Text as="span" size="Inherit" truncate>
 | 
						|
                          Join with Address
 | 
						|
                        </Text>
 | 
						|
                      </Box>
 | 
						|
                    </Box>
 | 
						|
                  </NavItemContent>
 | 
						|
                </NavButton>
 | 
						|
              </NavItem>
 | 
						|
              <NavItem variant="Background" radii="400" aria-selected={searchSelected}>
 | 
						|
                <NavLink to={getHomeSearchPath()}>
 | 
						|
                  <NavItemContent>
 | 
						|
                    <Box as="span" grow="Yes" alignItems="Center" gap="200">
 | 
						|
                      <Avatar size="200" radii="400">
 | 
						|
                        <Icon src={Icons.Search} size="100" filled={searchSelected} />
 | 
						|
                      </Avatar>
 | 
						|
                      <Box as="span" grow="Yes">
 | 
						|
                        <Text as="span" size="Inherit" truncate>
 | 
						|
                          Message Search
 | 
						|
                        </Text>
 | 
						|
                      </Box>
 | 
						|
                    </Box>
 | 
						|
                  </NavItemContent>
 | 
						|
                </NavLink>
 | 
						|
              </NavItem>
 | 
						|
            </NavCategory>
 | 
						|
            <NavCategory>
 | 
						|
              <NavCategoryHeader>
 | 
						|
                <RoomNavCategoryButton
 | 
						|
                  closed={closedCategories.has(DEFAULT_CATEGORY_ID)}
 | 
						|
                  data-category-id={DEFAULT_CATEGORY_ID}
 | 
						|
                  onClick={handleCategoryClick}
 | 
						|
                >
 | 
						|
                  Rooms
 | 
						|
                </RoomNavCategoryButton>
 | 
						|
              </NavCategoryHeader>
 | 
						|
              <div
 | 
						|
                style={{
 | 
						|
                  position: 'relative',
 | 
						|
                  height: virtualizer.getTotalSize(),
 | 
						|
                }}
 | 
						|
              >
 | 
						|
                {virtualizer.getVirtualItems().map((vItem) => {
 | 
						|
                  const roomId = sortedRooms[vItem.index];
 | 
						|
                  const room = mx.getRoom(roomId);
 | 
						|
                  if (!room) return null;
 | 
						|
                  const selected = selectedRoomId === roomId;
 | 
						|
 | 
						|
                  return (
 | 
						|
                    <VirtualTile
 | 
						|
                      virtualItem={vItem}
 | 
						|
                      key={vItem.index}
 | 
						|
                      ref={virtualizer.measureElement}
 | 
						|
                    >
 | 
						|
                      <RoomNavItem
 | 
						|
                        room={room}
 | 
						|
                        selected={selected}
 | 
						|
                        linkPath={getHomeRoomPath(getCanonicalAliasOrRoomId(mx, roomId))}
 | 
						|
                        muted={mutedRooms.includes(roomId)}
 | 
						|
                      />
 | 
						|
                    </VirtualTile>
 | 
						|
                  );
 | 
						|
                })}
 | 
						|
              </div>
 | 
						|
            </NavCategory>
 | 
						|
          </Box>
 | 
						|
        </PageNavContent>
 | 
						|
      )}
 | 
						|
    </PageNav>
 | 
						|
  );
 | 
						|
}
 |