commit d0c94063c9c8767b479e2b9ae7b52ba5281240ee
parent be3705f5fba2cd36cc5b27bab14ecb94c10ce789
Author: MTRNord <mtrnord1@gmail.com>
Date: Sun, 23 Apr 2023 23:58:00 +0200
feat: Add first iteration of the roomlist
Diffstat:
11 files changed, 251 insertions(+), 66 deletions(-)
diff --git a/package-lock.json b/package-lock.json
@@ -24,6 +24,7 @@
"olm": "https://packages.matrix.org/npm/olm/olm-3.2.1.tgz",
"react": "^18.2.0",
"react-dom": "^18.2.0",
+ "react-intersection-observer": "^9.4.3",
"react-redux": "^8.0.5",
"react-router-dom": "^6.10.0",
"redux-saga": "^1.2.3",
@@ -12296,6 +12297,14 @@
"react": "^16.8.4 || ^17.0.0 || ^18.0.0"
}
},
+ "node_modules/react-intersection-observer": {
+ "version": "9.4.3",
+ "resolved": "https://registry.npmjs.org/react-intersection-observer/-/react-intersection-observer-9.4.3.tgz",
+ "integrity": "sha512-WNRqMQvKpupr6MzecAQI0Pj0+JQong307knLP4g/nBex7kYfIaZsPpXaIhKHR+oV8z+goUbH9e10j6lGRnTzlQ==",
+ "peerDependencies": {
+ "react": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0"
+ }
+ },
"node_modules/react-is": {
"version": "17.0.2",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
diff --git a/package.json b/package.json
@@ -20,6 +20,7 @@
"olm": "https://packages.matrix.org/npm/olm/olm-3.2.1.tgz",
"react": "^18.2.0",
"react-dom": "^18.2.0",
+ "react-intersection-observer": "^9.4.3",
"react-redux": "^8.0.5",
"react-router-dom": "^6.10.0",
"redux-saga": "^1.2.3",
diff --git a/src/app/hooks.ts b/src/app/hooks.ts
@@ -1,27 +1,6 @@
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
import type { RootState, AppDispatch } from './store';
-import { RefObject, useEffect, useMemo, useState } from 'react';
// Use throughout your app instead of plain `useDispatch` and `useSelector`
export const useAppDispatch = () => useDispatch<AppDispatch>();
-export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
-
-export default function useOnScreen(ref: RefObject<HTMLElement>) {
- const [isIntersecting, setIntersecting] = useState(false)
-
- const observer = useMemo(() => new IntersectionObserver(
- ([entry]) => setIntersecting(entry.isIntersecting),
- {
- // TODO: This is probably too much or too less
- rootMargin: "70px"
- }
- ), [ref])
-
-
- useEffect(() => {
- observer.observe(ref.current as Element)
- return () => observer.disconnect()
- }, [])
-
- return isIntersecting
-}
+export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
+\ No newline at end of file
diff --git a/src/components/avatar/avatar.tsx b/src/components/avatar/avatar.tsx
@@ -1,3 +1,5 @@
+import { FC } from "react";
+
type AvatarProps = {
/**
* The URL of the Avatar image
@@ -17,7 +19,7 @@ type AvatarProps = {
online: boolean
};
-export default function Avatar({ avatarUrl, displayname, dm = false, online = false }: AvatarProps) {
+const Avatar: FC<AvatarProps> = ({ avatarUrl, displayname, dm = false, online = false }: AvatarProps) => {
if (avatarUrl) {
return (
<div className="flex relative w-8 h-8 justify-center items-center m-1 mr-2 text-xl rounded-full text-white">
@@ -48,4 +50,6 @@ export default function Avatar({ avatarUrl, displayname, dm = false, online = fa
}
</div>
);
-}
-\ No newline at end of file
+}
+
+export default Avatar;
+\ No newline at end of file
diff --git a/src/components/button/button.tsx b/src/components/button/button.tsx
@@ -1,4 +1,4 @@
-import { memo } from "react";
+import { FC, memo } from "react";
import "./button.scss";
type ButtonProps = {
@@ -25,7 +25,7 @@ type ButtonProps = {
readonly: boolean
};
-export default memo(function Button({ type = "button", style = "primary", onClick, children, readonly }: ButtonProps) {
+const Button: FC<ButtonProps> = ({ type = "button", style = "primary", onClick, children, readonly }: ButtonProps) => {
if (style === "secondary") {
return <button disabled={readonly} onClick={onClick} className="button bg-orange-400 hover:bg-orange-500 ease-out duration-150 disabled:bg-slate-200 disabled:cursor-not-allowed" type={type}>{children}</button>;
} else if (style === "abort") {
@@ -33,4 +33,5 @@ export default memo(function Button({ type = "button", style = "primary", onClic
} else {
return <button disabled={readonly} onClick={onClick} className="button bg-green-400 hover:bg-green-500 ease-out duration-150 disabled:bg-slate-200 disabled:cursor-not-allowed" type={type}>{children}</button>;
}
-});
-\ No newline at end of file
+}
+export default memo(Button);
+\ No newline at end of file
diff --git a/src/components/header/header.tsx b/src/components/header/header.tsx
@@ -1,3 +1,5 @@
+import { FC } from "react";
+
type HeaderProps = {
/**
* The HeaderText
@@ -5,6 +7,8 @@ type HeaderProps = {
children: string
};
-export default function Header({ children }: HeaderProps) {
+const Header: FC<HeaderProps> = ({ children }: HeaderProps) => {
return <h1 className='text-black font-bold text-xl'>{children}</h1>;
-}
-\ No newline at end of file
+}
+
+export default Header
+\ No newline at end of file
diff --git a/src/components/input/basic/input.tsx b/src/components/input/basic/input.tsx
@@ -1,4 +1,4 @@
-import { ChangeEvent, memo } from 'react';
+import { ChangeEvent, FC, memo } from 'react';
type InputProps = {
/**
@@ -27,7 +27,7 @@ type InputProps = {
onChange: (e: ChangeEvent<HTMLInputElement>) => void;
};
-export default memo(function Input({ placeholder, password = false, autoFocus = false, value, readonly, onChange }: InputProps) {
+const Input: FC<InputProps> = ({ placeholder, password = false, autoFocus = false, value, readonly, onChange }: InputProps) => {
return (
<input
disabled={readonly}
@@ -39,4 +39,5 @@ export default memo(function Input({ placeholder, password = false, autoFocus =
onChange={onChange}
/>
);
-});
-\ No newline at end of file
+}
+export default memo(Input);
+\ No newline at end of file
diff --git a/src/components/input/chat/input.tsx b/src/components/input/chat/input.tsx
@@ -21,6 +21,7 @@ import CodeHighlightPlugin from './plugins/CodeHighlightPlugin';
import EditorTheme from './theme';
import './input.scss';
+import { FC } from 'react';
type ChatInputProps = {
/**
@@ -41,7 +42,7 @@ function Placeholder() {
return <div className="editor-placeholder" id="editor-placeholder">Enter message...</div>;
}
-export default function ChatInput({ namespace, onChange, onError }: ChatInputProps) {
+const ChatInput: FC<ChatInputProps> = ({ namespace, onChange, onError }: ChatInputProps) => {
const initialConfig: InitialConfigType = {
namespace: namespace,
theme: EditorTheme,
@@ -80,4 +81,6 @@ export default function ChatInput({ namespace, onChange, onError }: ChatInputPro
</div>
</LexicalComposer>
);
-}
-\ No newline at end of file
+}
+
+export default ChatInput;
+\ No newline at end of file
diff --git a/src/components/roomList/roomList.stories.tsx b/src/components/roomList/roomList.stories.tsx
@@ -30,5 +30,62 @@ type Story = StoryObj<typeof RoomList>;
export const RootList: Story = {
args: {
+ rooms: [
+ {
+ roomID: "1",
+ displayname: "test",
+ avatarUrl: "https://randomuser.me/api/portraits/men/62.jpg",
+ dm: true,
+ online: true,
+ },
+ {
+ roomID: "2",
+ displayname: "test1",
+ dm: false,
+ online: false,
+ },
+ {
+ roomID: "3",
+ displayname: "test2",
+ dm: false,
+ online: false,
+ }
+ ],
+ sections: [
+ {
+ sectionName: "Test Section",
+ rooms: [
+ {
+ roomID: "2",
+ displayname: "test1",
+ dm: false,
+ online: false,
+ },
+ {
+ roomID: "3",
+ displayname: "test2",
+ dm: false,
+ online: false,
+ }
+ ],
+ roomID: "12",
+ subsections: [
+ {
+ sectionName: "Test Subsection",
+ rooms: [
+ {
+ roomID: "1",
+ displayname: "test",
+ avatarUrl: "https://randomuser.me/api/portraits/men/62.jpg",
+ dm: true,
+ online: true,
+ }
+ ],
+ roomID: "14",
+ subsections: []
+ }
+ ]
+ }
+ ]
}
};
\ No newline at end of file
diff --git a/src/components/roomList/roomList.tsx b/src/components/roomList/roomList.tsx
@@ -1,3 +1,7 @@
+import { FC, useState } from "react";
+import RoomListItem from "./roomListItem/roomListItem";
+import { ChevronDown, ChevronRight } from "lucide-react";
+
type Room = {
/**
* The URL of the Avatar image
@@ -16,10 +20,6 @@ type Room = {
*/
online: boolean
/**
- * Wether the current room is selected
- */
- active: boolean
- /**
* The roomid of the Room
*/
roomID: string
@@ -38,6 +38,10 @@ type Section = {
* The Subsections of the Section
*/
subsections: Section[]
+ /**
+ * The roomid of the Space
+ */
+ roomID: string
}
type RoomListProps = {
@@ -51,10 +55,112 @@ type RoomListProps = {
rooms: Room[]
};
-export default function RoomList({ sections, rooms }: RoomListProps) {
+type RoomListRoomsProps = {
+ /**
+ * The roomid of the Space
+ */
+ sectionID: string
+ /**
+ * Rooms
+ */
+ rooms: Room[]
+ /**
+ * The onClick handler
+ */
+ onClick: (roomID: string) => void;
+ /**
+ * The activeRoom
+ */
+ activeRoom?: string;
+ /**
+ * If rooms are hidden
+ */
+ hidden: boolean
+};
+
+const RoomListRooms: FC<RoomListRoomsProps> = ({ sectionID, rooms, onClick, activeRoom, hidden }: RoomListRoomsProps) => {
+ const roomsRendered = rooms.map(room => {
+ return (
+ <RoomListItem
+ hidden={hidden}
+ key={room.roomID + sectionID}
+ avatarUrl={room.avatarUrl}
+ displayname={room.displayname}
+ dm={room.dm}
+ online={room.online}
+ active={room.roomID === activeRoom}
+ onClick={() => { onClick(room.roomID) }}
+ />
+ );
+ });
+ return (
+ <>
+ {roomsRendered}
+ </>
+ );
+}
+
+const RoomSection: FC<{ section: Section, onRoomClick: (roomID: string) => void, activeRoom: string | undefined }> = ({ section, onRoomClick, activeRoom }: { section: Section, onRoomClick: (roomID: string) => void, activeRoom: string | undefined }) => {
+ const [hidden, setHidden] = useState<boolean>(false);
+ return (
+ <div key={section.roomID} className="flex flex-col gap-1">
+ <div className="flex flex-row gap-2 p-1 px-2 bg-gray-300 items-center justify-between cursor-pointer" onClick={() => setHidden(prev => !prev)}>
+ <span className='text-black font-normal text-xl capitalize'>{section.sectionName}</span>
+ {hidden ? <ChevronRight size={20} /> : <ChevronDown size={20} />}
+ </div >
+ {!hidden && (<RoomListRooms
+ hidden={hidden}
+ sectionID={section.roomID}
+ rooms={section.rooms}
+ onClick={onRoomClick}
+ activeRoom={activeRoom}
+ />)}
+ {!hidden && (
+ section.subsections.map(section => {
+ return (
+ <RoomSection
+ key={section.roomID}
+ section={section}
+ onRoomClick={onRoomClick}
+ activeRoom={activeRoom}
+ />
+ );
+ })
+ )}
+ </div>
+ );
+}
+
+const RoomList: FC<RoomListProps> = ({ sections, rooms }: RoomListProps) => {
+ const [activeRoom, setActiveRoom] = useState<string | undefined>(undefined);
+
return (
- <div>
-
+ <div className="flex flex-col gap-1">
+ {
+ sections.map(section => {
+ return (
+ <RoomSection
+ key={section.roomID}
+ section={section}
+ onRoomClick={(roomID: string) => { setActiveRoom(roomID) }}
+ activeRoom={activeRoom}
+ />
+ );
+ })
+ }
+ <RoomSection
+ section={{
+ sectionName: "Others",
+ roomID: "other",
+ subsections: [],
+ rooms: rooms
+ }}
+ onRoomClick={(roomID: string) => { setActiveRoom(roomID) }}
+ activeRoom={activeRoom}
+ />
+
</div>
);
-}
-\ No newline at end of file
+}
+
+export default RoomList
+\ No newline at end of file
diff --git a/src/components/roomList/roomListItem/roomListItem.tsx b/src/components/roomList/roomListItem/roomListItem.tsx
@@ -1,4 +1,6 @@
+import { useInView } from "react-intersection-observer";
import Avatar from "../../avatar/avatar";
+import { FC } from "react";
type RoomListItemProps = {
/**
@@ -21,22 +23,39 @@ type RoomListItemProps = {
* Wether the current room is selected
*/
active: boolean
+ /**
+ * The onClick handler
+ */
+ onClick: () => void;
+ /**
+ * If room is hidden
+ */
+ hidden: boolean
};
-export default function RoomListItem({ avatarUrl, displayname, dm = false, online = false, active = false }: RoomListItemProps) {
- if (!active) {
- return (
- <div className="flex flex-row gap-2 p-1 hover:bg-gray-300 hover:rounded duration-150 ease-in items-center">
- <Avatar avatarUrl={avatarUrl} displayname={displayname} dm={dm} online={online} />
- <span className='text-black font-normal text-xl capitalize'>{displayname}</span>
- </div>
- );
- } else {
- return (
- <div className="flex flex-row gap-2 p-1 bg-gray-300 rounded duration-150 ease-in items-center">
- <Avatar avatarUrl={avatarUrl} displayname={displayname} dm={dm} online={online} />
- <span className='text-black font-normal text-xl capitalize'>{displayname}</span>
- </div>
- );
- }
-}
-\ No newline at end of file
+const RoomListItem: FC<RoomListItemProps> = ({ avatarUrl, displayname, dm = false, online = false, active = false, onClick, hidden }: RoomListItemProps) => {
+ const { ref, inView } = useInView({
+ triggerOnce: true,
+ rootMargin: '200px 0px',
+ skip: hidden,
+ });
+ return (
+ <div ref={ref} onClick={onClick}>
+ {
+ inView && (active ? (
+ <div className="flex flex-row gap-2 p-1 bg-gray-300 hover:bg-gray-400 rounded duration-200 ease-in-out items-center">
+ <Avatar avatarUrl={avatarUrl} displayname={displayname} dm={dm} online={online} />
+ <span className='text-black font-normal text-xl capitalize'>{displayname}</span>
+ </div>
+ ) : (
+ <div className="flex flex-row gap-2 p-1 hover:bg-gray-300 hover:rounded duration-200 ease-in-out items-center">
+ <Avatar avatarUrl={avatarUrl} displayname={displayname} dm={dm} online={online} />
+ <span className='text-black font-normal text-xl capitalize'>{displayname}</span>
+ </div>
+ ))
+ }
+ </div>
+ );
+}
+
+export default RoomListItem
+\ No newline at end of file