cetirizine

An experimental matrix client written in reactjs utilizing tailwind and storybook
git clone git://archive.git.mtrnord.blog/MTRNord/cetirizine.git
Log | Files | Refs | README | LICENSE

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:
Mpackage-lock.json | 9+++++++++
Mpackage.json | 1+
Msrc/app/hooks.ts | 24++----------------------
Msrc/components/avatar/avatar.tsx | 10+++++++---
Msrc/components/button/button.tsx | 9+++++----
Msrc/components/header/header.tsx | 10+++++++---
Msrc/components/input/basic/input.tsx | 9+++++----
Msrc/components/input/chat/input.tsx | 9++++++---
Msrc/components/roomList/roomList.stories.tsx | 57+++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/components/roomList/roomList.tsx | 124+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------
Msrc/components/roomList/roomListItem/roomListItem.tsx | 55+++++++++++++++++++++++++++++++++++++------------------
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