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 b37d90d882cc9163bdd1265b9b6fe8e3e65241ce
parent b9885ac7504d2dbb228143fc858a91fef0caddf3
Author: MTRNord <mtrnord1@gmail.com>
Date:   Mon, 21 Aug 2023 22:38:14 +0200

Start working on a darkmode and add room filtering and some stuff I probably forgot. This was on here for months without push

Diffstat:
Achangelog.d/avatars.fix | 2++
Achangelog.d/search.feature | 2++
Achangelog.d/settings.feature | 2++
Msrc/app/sdk/client.ts | 4++--
Msrc/app/sdk/room.ts | 2+-
Asrc/components/avatar/avatar.scss | 54++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/components/avatar/avatar.tsx | 34+++++++++-------------------------
Msrc/components/button/button.scss | 45++++++++++++++++++++++++++++++++++++++++++++-
Msrc/components/button/button.tsx | 6+++---
Msrc/components/events/messageEvent.scss | 10++++++++--
Msrc/components/events/wrapper.tsx | 2+-
Asrc/components/header/header.scss | 6++++++
Msrc/components/header/header.tsx | 3++-
Msrc/components/input/basic/input.tsx | 2+-
Msrc/components/input/chat/input.scss | 20++++++++++++++------
Msrc/components/login/login.tsx | 2+-
Msrc/components/roomList/roomList.tsx | 31++++++++++++++++---------------
Asrc/components/roomList/roomListItem/roomListItem.scss | 19+++++++++++++++++++
Msrc/components/roomList/roomListItem/roomListItem.tsx | 10+++++++++-
Msrc/index.scss | 80+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Msrc/main.tsx | 3+++
Msrc/pages/MainPage.scss | 133+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
Msrc/pages/MainPage.tsx | 166+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------
Mtailwind.config.js | 1-
24 files changed, 554 insertions(+), 85 deletions(-)

diff --git a/changelog.d/avatars.fix b/changelog.d/avatars.fix @@ -0,0 +1 @@ +Ensure that the placeholder is always present until the image loads by doing it within CSS. +\ No newline at end of file diff --git a/changelog.d/search.feature b/changelog.d/search.feature @@ -0,0 +1 @@ +Add a way to filter the roomlist for specific terms. +\ No newline at end of file diff --git a/changelog.d/settings.feature b/changelog.d/settings.feature @@ -0,0 +1 @@ +Add a App Settings menu. +\ No newline at end of file diff --git a/src/app/sdk/client.ts b/src/app/sdk/client.ts @@ -93,7 +93,7 @@ export class MatrixClient extends EventEmitter { public spacesInView: string[] = []; public spaceOpen: string[] = []; public database?: IDBPDatabase<MatrixDB>; - private profileInfo?: IProfileInfo; + public profileInfo?: IProfileInfo; public currentRoom?: string; private user: OwnUser = new OwnUser(this); private sync: MatrixSlidingSync = new MatrixSlidingSync(this, this.user); @@ -489,7 +489,7 @@ export function useSpaces() { export function useProfile() { const client = useContext(MatrixContext); - const [profile, setProfile] = useState<IProfileInfo>({ + const [profile, setProfile] = useState<IProfileInfo>(client.profileInfo || { displayname: client.mxid || "Unknown", }); diff --git a/src/app/sdk/room.ts b/src/app/sdk/room.ts @@ -223,7 +223,7 @@ export class Room extends EventEmitter { return name; } - public getMemberAvatar(userID: string, size: number = 32): string | undefined { + public getMemberAvatar(userID: string, size: number = 64): string | undefined { let avatarURL: string | undefined = undefined; this.stateEvents.forEach((event) => { if (event.type === "m.room.member" && event.state_key === userID && event.content.membership == "join") { diff --git a/src/components/avatar/avatar.scss b/src/components/avatar/avatar.scss @@ -0,0 +1,53 @@ +.avatar { + position: relative; + overflow: hidden; + + img { + width: 100%; + height: 100%; + border-radius: 50%; + color: transparent; + object-fit: cover; + + &::after { + border-radius: 50%; + content: attr(alt); + display: flex; + align-items: center; + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + color: white; + background-color: var(--orange-500); + white-space: pre; + padding-left: calc(50% - 0.5ch); + font-family: monospace; + letter-spacing: 2rem; + } + } + + .online, + .offline, + .unknown { + width: 0.75rem; + height: 0.75rem; + position: absolute; + bottom: 0; + right: 0; + border-radius: 50%; + } + + .online { + background-color: var(--green-500); + } + + .offline { + background-color: var(--red-500); + } + + .unknown { + background-color: var(--gray-500); + } +} +\ No newline at end of file diff --git a/src/components/avatar/avatar.tsx b/src/components/avatar/avatar.tsx @@ -1,5 +1,6 @@ -import { FC, memo, useState } from "react"; +import { FC, memo } from "react"; import { OnlineState } from "../../app/sdk/api/otherEnums"; +import './avatar.scss'; type AvatarProps = { /** @@ -23,38 +24,21 @@ type AvatarProps = { * @default false */ isBot: boolean + size?: string }; -const Avatar: FC<AvatarProps> = memo(({ avatarUrl, displayname, dm = false, online = OnlineState.Unknown }: AvatarProps) => { - const [error, setError] = useState(false); +const Avatar: FC<AvatarProps> = memo(({ avatarUrl, displayname, dm = false, online = OnlineState.Unknown, size = "2rem" }: AvatarProps) => { - if (avatarUrl && !error) { - return ( - <div className="flex relative min-w-[2rem] min-h-[2rem] w-[2rem] h-[2rem] justify-center items-center text-xl rounded-full text-white"> - <img className="rounded-full w-8 h-8 object-cover" alt={displayname} src={avatarUrl} onError={() => { setError(true) }} /> - { - dm ? - ( - online === OnlineState.Online ? - <div className="bg-green-500 rounded-full w-3 h-3 absolute right-0 bottom-0"></div> : - (online === OnlineState.Offline ? <div className="bg-red-500 rounded-full w-3 h-3 absolute right-0 bottom-0"></div> : - <div className="bg-gray-500 rounded-full w-3 h-3 absolute right-0 bottom-0"></div>) - ) : - <></> - } - </div> - ); - } return ( - <div className="flex relative min-w-[2rem] min-h-[2rem] w-[2rem] h-[2rem] bg-orange-500 justify-center items-center m-0 mr-3 text-xl rounded-full text-white"> - {displayname.replace('@', '').replace('!', '').replace('#', '')[0].toUpperCase()} + <div style={{ minWidth: size, minHeight: size, width: size, height: size }} className="avatar"> + <img alt={displayname} src={avatarUrl} /> { dm ? ( online === OnlineState.Online ? - <div className="bg-green-500 rounded-full w-3 h-3 absolute right-0 bottom-0"></div> : - (online === OnlineState.Offline ? <div className="bg-red-500 rounded-full w-3 h-3 absolute right-0 bottom-0"></div> : - <div className="bg-gray-500 rounded-full w-3 h-3 absolute right-0 bottom-0"></div>) + <div className="online"></div> : + (online === OnlineState.Offline ? <div className="offline"></div> : + <div className="unknown"></div>) ) : <></> } diff --git a/src/components/button/button.scss b/src/components/button/button.scss @@ -1,3 +1,46 @@ .button { - @apply block uppercase mx-auto shadow text-black text-xs py-3 px-10 rounded font-medium; + display: block; + text-transform: uppercase; + margin-left: auto; + margin-right: auto; + box-shadow: var(--box-shadow); + color: black; + font-size: 0.694rem; + padding-top: 0.75rem; + padding-bottom: 0.75rem; + padding-left: 2.5rem; + padding-right: 2.5rem; + border-radius: var(--base-rounding); + font-weight: var(--medium-font); + transition-duration: 150ms; + transition-timing-function: cubic-bezier(0, 0, 0.2, 1); + + &:disabled { + cursor: not-allowed; + background-color: var(--slate-200); + } +} + +.secondary { + background-color: var(--orange-400); + + &:hover { + background-color: var(--orange-500); + } +} + +.abort { + background-color: var(--red-400); + + &:hover { + background-color: var(--red-500); + } +} + +.primary { + background-color: var(--green-400); + + &:hover { + background-color: var(--green-500); + } } \ No newline at end of file diff --git a/src/components/button/button.tsx b/src/components/button/button.tsx @@ -27,11 +27,11 @@ type ButtonProps = { const Button: FC<ButtonProps> = memo(({ 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>; + return <button disabled={readonly} onClick={onClick} className="button secondary" type={type}>{children}</button>; } else if (style === "abort") { - return <button disabled={readonly} onClick={onClick} className="button bg-red-400 hover:bg-red-500 ease-out duration-150 disabled:bg-slate-200 disabled:cursor-not-allowed" type={type}>{children}</button>; + return <button disabled={readonly} onClick={onClick} className="button abort" type={type}>{children}</button>; } 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>; + return <button disabled={readonly} onClick={onClick} className="button primary" type={type}>{children}</button>; } }) export default Button; \ No newline at end of file diff --git a/src/components/events/messageEvent.scss b/src/components/events/messageEvent.scss @@ -1,14 +1,20 @@ #event { #text-event { + color: var(--text-color); + h1 { - @apply text-2xl text-black font-bold; + font-size: var(--font-size-2xl); + font-weight: var(--bold-font); + color: var(--text-color); margin: 0; margin-bottom: 12px; padding: 0; } h2 { - @apply text-xl text-black font-bold; + font-size: var(--font-size-xl); + font-weight: var(--bold-font); + color: var(--text-color); margin: 0; margin-top: 10px; padding: 0; diff --git a/src/components/events/wrapper.tsx b/src/components/events/wrapper.tsx @@ -37,7 +37,7 @@ export type MessageWrapperProps = { export const MessageWrapper: FC<MessageWrapperProps> = memo(({ hasPreviousEvent, displayname, avatar_url, onlineState, isBot, dm, children }) => { return ( - <div className={!hasPreviousEvent ? "flex flex-row gap-4 p-2 pb-1 hover:bg-gray-200 rounded-md duration-200 ease-in-out items-start" : "flex flex-row p-2 pb-1 pt-1 hover:bg-gray-200 rounded-md duration-200 ease-in-out"}> + <div className={!hasPreviousEvent ? "flex flex-row gap-4 p-2 pb-1 hover:bg-gray-200 dark:hover:bg-gray-700 rounded-md duration-200 ease-in-out items-start" : "flex flex-row p-2 pb-1 pt-1 hover:bg-gray-200 dark:hover:bg-gray-700 rounded-md duration-200 ease-in-out"}> {!hasPreviousEvent && <Avatar displayname={displayname} avatarUrl={avatar_url} diff --git a/src/components/header/header.scss b/src/components/header/header.scss @@ -0,0 +1,5 @@ +.header { + color: var(--text-color); + font-weight: var(--bold-font); + font-size: var(--font-size-xl); +} +\ No newline at end of file diff --git a/src/components/header/header.tsx b/src/components/header/header.tsx @@ -1,4 +1,5 @@ import { FC, memo } from "react"; +import "./header.scss" type HeaderProps = { /** @@ -8,7 +9,7 @@ type HeaderProps = { }; const Header: FC<HeaderProps> = memo(({ children }: HeaderProps) => { - return <h1 className='text-black font-bold text-xl'>{children}</h1>; + return <h1 className='header'>{children}</h1>; }) 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 @@ -31,7 +31,7 @@ const Input: FC<InputProps> = memo(({ placeholder, password = false, autoFocus = return ( <input disabled={readonly} - className='text-base form-input rounded-lg disabled:bg-slate-200 disabled:cursor-not-allowed transition-colors ease-in-out delay-150' + className='text-base form-input rounded-lg disabled:bg-slate-200 disabled:cursor-not-allowed transition-colors ease-in-out delay-150 text-black' value={value} type={password ? "password" : "text"} autoFocus={autoFocus} diff --git a/src/components/input/chat/input.scss b/src/components/input/chat/input.scss @@ -1,12 +1,15 @@ .editor-container { - @apply text-black relative font-normal; + position: relative; + font-weight: var(--normal-font); line-height: 20px; text-align: left; height: 100%; } .editor-inner { - @apply bg-white relative overflow-y-auto; + overflow-y: auto; + position: relative; + background-color: var(--background-color); scrollbar-width: thin; max-height: 50lvh; } @@ -34,15 +37,20 @@ tab-size: 1; outline: 0; padding: 15px 10px; - caret-color: #444; } .toolbar { display: flex; margin-bottom: 1px; - background: #fff; + background: var(--background-color); padding: 4px; vertical-align: middle; + + button { + svg { + stroke: var(--text-color); + } + } } .toolbar button.toolbar-item { @@ -87,7 +95,7 @@ } .toolbar .toolbar-item:hover:not([disabled]) { - background-color: #eee; + background-color: var(--background-color-hover); } .toolbar .divider { @@ -120,7 +128,7 @@ width: 200px; vertical-align: middle; font-size: 14px; - color: #777; + color: var(--text-color); text-overflow: ellipsis; width: 70px; overflow: hidden; diff --git a/src/components/login/login.tsx b/src/components/login/login.tsx @@ -27,7 +27,7 @@ const Login = memo(() => { setLoginPending(false); } return ( - <form className="flex flex-col rounded-md shadow p-4 bg-white gap-2 min-w-[30rem]" onSubmit={(e) => { + <form className="flex flex-col rounded-md shadow p-4 bg-white dark:bg-slate-900 gap-2 min-w-[30rem]" onSubmit={(e) => { e.preventDefault(); startLogin(); }}> diff --git a/src/components/roomList/roomList.tsx b/src/components/roomList/roomList.tsx @@ -170,19 +170,20 @@ const RoomList: FC<RoomListProps> = memo(({ sections, rooms, dmRooms }: RoomList return ( <div id='roomlist'> - <RoomSection - section={{ - sectionName: "DMs", - roomID: "dms", - subsections: [], - rooms: dmRooms - }} - onRoomClick={(roomID: string) => { - setActiveRoom(roomID); - navigate(`/${encodeURIComponent(roomID)}`); - }} - activeRoom={activeRoom} - /> + {dmRooms.length == 0 ? <></> : + <RoomSection + section={{ + sectionName: "DMs", + roomID: "dms", + subsections: [], + rooms: dmRooms + }} + onRoomClick={(roomID: string) => { + setActiveRoom(roomID); + navigate(`/${encodeURIComponent(roomID)}`); + }} + activeRoom={activeRoom} + />} { sections.map(section => { return ( @@ -198,7 +199,7 @@ const RoomList: FC<RoomListProps> = memo(({ sections, rooms, dmRooms }: RoomList ); }) } - <RoomSection + {rooms.length > 0 ? <RoomSection section={{ sectionName: "Others", roomID: "other", @@ -210,7 +211,7 @@ const RoomList: FC<RoomListProps> = memo(({ sections, rooms, dmRooms }: RoomList navigate(`/${encodeURIComponent(roomID)}`); }} activeRoom={activeRoom} - /> + /> : <></>} </div> ); diff --git a/src/components/roomList/roomListItem/roomListItem.scss b/src/components/roomList/roomListItem/roomListItem.scss @@ -0,0 +1,18 @@ +.notificationCount { + .higlight { + background-color: red; + } + + // Make round + border-radius: 50%; + background-color: var(--slate-200); + $size: 25px; + min-width: $size; + min-height: $size; + width: $size; + height: $size; + display: flex; + align-items: center; + justify-content: center; + color:black; +} +\ No newline at end of file diff --git a/src/components/roomList/roomListItem/roomListItem.tsx b/src/components/roomList/roomListItem/roomListItem.tsx @@ -1,8 +1,9 @@ import { useInView } from "react-intersection-observer"; import Avatar from "../../avatar/avatar"; import { FC, memo, useContext } from "react"; -import { MatrixContext } from "../../../app/sdk/client"; +import { MatrixContext, useRoom } from "../../../app/sdk/client"; import { OnlineState } from "../../../app/sdk/api/otherEnums"; +import "./roomListItem.scss"; type RoomListItemProps = { /** @@ -41,6 +42,7 @@ type RoomListItemProps = { const RoomListItem: FC<RoomListItemProps> = memo(({ roomId, avatarUrl, displayname, dm = false, online = OnlineState.Unknown, active = false, onClick, hidden }: RoomListItemProps) => { const matrixClient = useContext(MatrixContext); + const room = useRoom(roomId); const { ref, inView } = useInView({ triggerOnce: true, skip: hidden, @@ -60,11 +62,17 @@ const RoomListItem: FC<RoomListItemProps> = memo(({ roomId, avatarUrl, displayna <div className="flex flex-row gap-2 p-1 bg-gray-300 hover:bg-gray-400 rounded-lg duration-200 ease-in-out items-center"> <Avatar avatarUrl={avatarUrl} displayname={displayname} dm={dm} online={online} isBot={false} /> <span title={displayname} className='text-slate-900 font-normal text-base max-w-[32ch] overflow-hidden text-ellipsis w-full whitespace-nowrap'>{displayname}</span> + {(room?.getNotificationHighlightCount() !== 0 || room?.getNotificationCount() !== 0) && (room?.getNotificationHighlightCount() === 0 ? + <div className="notificationCount">{room?.getNotificationCount()}</div> + : <div className="notificationCount higlight">{room?.getNotificationCount() ?? 0 + (room?.getNotificationHighlightCount() ?? 0)}</div>)} </div> ) : ( <div className="flex flex-row gap-2 p-1 hover:bg-gray-300 rounded-lg duration-200 ease-in-out items-center"> <Avatar avatarUrl={avatarUrl} displayname={displayname} dm={dm} online={online} isBot={false} /> <span title={displayname} className='text-slate-900 font-normal text-base max-w-[32ch] overflow-hidden text-ellipsis w-full whitespace-nowrap'>{displayname}</span> + {(room?.getNotificationHighlightCount() !== 0 || room?.getNotificationCount() !== 0) && (room?.getNotificationHighlightCount() === 0 ? + <div className="notificationCount">{room?.getNotificationCount()}</div> + : <div className="notificationCount higlight">{room?.getNotificationCount() ?? 0 + (room?.getNotificationHighlightCount() ?? 0)}</div>)} </div> )) } diff --git a/src/index.scss b/src/index.scss @@ -2,6 +2,59 @@ @tailwind components; @tailwind utilities; +:root { + // Colors + --bg-opacity: 1; + + --orange-400: rgb(251 146 60 / var(--bg-opacity)); + --orange-500: rgb(249 115 22 / var(--bg-opacity)); + + --green-400: rgb(74 222 128 / var(--bg-opacity)); + --green-500: rgb(34 197 94 / var(--bg-opacity)); + + --red-400: rgb(248 113 113 / var(--bg-opacity)); + --red-500: rgb(239 68 68 / var(--bg-opacity)); + + --gray-300: rgb(209 213 219 / var(--bg-opacity)); + --gray-500: rgb(107 114 128 / var(--bg-opacity)); + + --slate-100: rgb(226 232 240 / var(--bg-opacity)); + --slate-200: rgb(241 245 249 / var(--bg-opacity)); + --slate-500: rgb(100 116 139 / var(--bg-opacity)); + --slate-600: rgb(71 85 105 / var(--bg-opacity)); + --slate-900: rgb(15 23 42 / var(--bg-opacity)); + + --background-color: white; + --background-color-hover: var(--slate-100); + --blockquote-color: var(--slate-100); + + --text-color: black; + + // Shadows + --shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1); + --shadow-colored: 0 1px 3px 0 var(--shadow-color), 0 1px 2px -1px var(--shadow-color); + --box-shadow: var(--ring-offset-shadow, 0 0 #0000), + var(--ring-shadow, 0 0 #0000), + var(--shadow); + + // Rounding + --base-rounding: 0.25rem; + + // Typography + --bold-font: 700; + --medium-font: 500; + --normal-font: 400; + --semibold-font: 600; + + --font-size-base: 1rem; + --font-size-xl: 1.44rem; + --font-size-2xl: 1.2rem; +} + +html { + color-scheme: light dark; +} + body, html { margin: 0; @@ -29,7 +82,11 @@ a { } code { - @apply rounded font-normal text-red-500 bg-slate-100 p-0.5; + border-radius: var(--base-rounding); + font-weight: var(--normal-font); + color: var(--red-500); + background-color: var(--slate-100); + padding: 0.125rem; } pre { @@ -44,6 +101,25 @@ pre { * Blockquote should be light grey with a thick left border */ blockquote { - @apply border-l-4 border-gray-300 p-4 py-0 bg-slate-100; border-radius: 0 0.375rem 0.375rem 0; + border-left-width: 4px; + border-color: var(--gray-300); + padding: 1rem; + padding-top: 0.25rem; + padding-bottom: 0px; + background-color: var(--blockquote-color); +} + +body { + background-color: var(--background-color); + color: var(--text-color); +} + +@media (prefers-color-scheme: dark) { + :root { + --background-color: var(--slate-900); + --text-color: white; + --blockquote-color: var(--slate-600); + --background-color-hover: var(--slate-600); + } } \ No newline at end of file diff --git a/src/main.tsx b/src/main.tsx @@ -2,6 +2,7 @@ import { initAsync, start } from "@mtrnord/matrix-sdk-crypto-js"; import React from 'react' import { createRoot } from 'react-dom/client'; import './index.scss'; +import ReactModal from "react-modal"; initAsync().then(() => { // if (Tracing.isAvailable()) { @@ -19,6 +20,8 @@ initAsync().then(() => { const container = document.getElementById('root')!; const root = createRoot(container); + ReactModal.setAppElement('#root'); + root.render( <React.StrictMode> <MatrixContext.Provider value={defaultMatrixClient}> diff --git a/src/pages/MainPage.scss b/src/pages/MainPage.scss @@ -15,7 +15,7 @@ @apply bg-gradient-to-br from-slate-100 via-gray-200 to-orange-200 border-r-[1px] border-slate-300; display: grid; grid-auto-flow: column; - grid-template-rows: 60px auto; + grid-template-rows: min-content auto; grid-template-areas: "user-info" "roomlist"; @@ -24,13 +24,13 @@ #user-info { @apply p-3 items-center border-b-2; grid-area: user-info; - column-gap: 0.5rem; + gap: 1rem; display: grid; - grid-auto-flow: row; grid-template-columns: auto 1fr; + grid-template-rows: min-content min-content; grid-template-areas: - "avatar username"; - flex-direction: row; + "avatar username" + "search search"; align-items: center; justify-self: flex-start; width: 100%; @@ -41,6 +41,21 @@ flex-direction: row; align-items: center; justify-content: space-between; + + span { + color: black; + font-size: 1rem; + font-weight: var(--semibold-font); + } + } + + #search-container { + grid-area: search; + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + width: 100%; } } } @@ -54,9 +69,19 @@ display: flex; flex-direction: row; align-items: center; - justify-content: start; + justify-content: flex-start; gap: 1rem; grid-area: header; + + @media (prefers-color-scheme: dark) { + @apply border-slate-700; + } + + #room-name { + color: var(--text-color); + font-size: var(--font-size-base); + font-weight: var(--semibold-font); + } } #chat-view { @@ -66,4 +91,100 @@ #chat-editor { grid-area: editor; } +} + +#modal { + display: grid; + grid-template-areas: + "sidebar main-area x" + "sidebar main-area main-area2"; + grid-template-columns: 20ch auto 25px; + grid-template-rows: 25px auto; + gap: 0.75rem; + width: 70%; + margin: 0 auto; + background-color: var(--background-color) !important; + + #modal-sidebar { + grid-area: sidebar; + display: flex; + flex-direction: column; + gap: 0.25rem; + } + + #modal-content { + grid-row-start: main-area; + grid-row-end: main-area; + grid-column-start: main-area; + grid-column-end: main-area2; + z-index: 0; + + #profile-view { + display: grid; + grid-template-areas: + "title avatar" + "inputs avatar" + "inputs other"; + gap: 1rem; + row-gap: 0.5rem; + align-items: flex-start; + + #title { + grid-area: title; + @apply text-xl font-bold text-black flex items-center; + } + + label { + @apply flex relative flex-row items-center rounded-lg justify-center; + + input { + &::placeholder { + @apply text-slate-600; + // TODO: This is not aligned and I cant get it working :( + } + + &:hover { + @apply bg-slate-400/25 outline; + } + + @apply py-2 rounded-lg pl-10 outline-slate-700 w-full outline-2 bg-slate-400/50; + } + } + + #inputs { + grid-area: inputs; + display: flex; + flex-direction: column; + gap: 0.25rem; + + #matrixid { + input:hover { + outline: none; + } + } + } + + #avatar { + grid-area: avatar; + justify-self: flex-end; + align-self: flex-end; + margin: 1rem; + @apply border-slate-700 rounded-full border-2; + } + + + } + } + + #modal-cross { + &:hover { + @apply bg-slate-300; + } + + @apply stroke-slate-600 rounded-full p-1 cursor-pointer; + grid-area: x; + justify-self: center; + align-self: center; + z-index: 1; + } } \ No newline at end of file diff --git a/src/pages/MainPage.tsx b/src/pages/MainPage.tsx @@ -1,4 +1,4 @@ -import { Settings } from 'lucide-react'; +import { Brush, Pencil, PersonStanding, Search, Settings, User, X } from 'lucide-react'; import Avatar from '../components/avatar/avatar'; import ChatInput from '../components/input/chat/input'; import RoomList, { Section } from '../components/roomList/roomList'; @@ -15,6 +15,7 @@ import { IRoomEvent, IRoomMemberEvent } from '../app/sdk/api/events'; import Linkify from 'linkify-react'; import { OnlineState } from '../app/sdk/api/otherEnums'; import { Virtuoso } from 'react-virtuoso'; +import ReactModal from 'react-modal'; type ChatViewProps = { /** @@ -314,6 +315,9 @@ const MainPage = memo(() => { const room = useRoom(decodeURIComponent(params.roomIdOrAlias || "")); client.setCurrentRoom(params.roomIdOrAlias ? decodeURIComponent(params.roomIdOrAlias) : undefined) + const [showSettings, setShowSettings] = useState(false); + const [searchTerm, setSearchTerm] = useState<string | null>(null); + // Filter toplevel spaces. // A toplevel space is a space that is not a child of another space. // We can not rely only on the parent. We need to check in both directions. @@ -390,6 +394,12 @@ const MainPage = memo(() => { // If the room is in the sync list, we can compare the indexes. return a_index - b_index; + }).filter(room => { + if (searchTerm) { + return room.getName().toLowerCase().includes(searchTerm.toLowerCase()) + } else { + return true; + } }).map(room => { return { roomID: room.roomID, @@ -403,7 +413,13 @@ const MainPage = memo(() => { const generateSubsections = (subspace: Room): Section | undefined => { const subspaceMeta = [...spacesWithRooms].find(space => space.spaceRoom.roomID === subspace.roomID); if (subspaceMeta) { - const rooms = [...subspaceMeta?.children].filter(room => !room.isSpace() && !room.isTombstoned() && !room.isDM()).map(room => { + const rooms = [...subspaceMeta?.children].filter(room => !room.isSpace() && !room.isTombstoned() && !room.isDM()).filter(room => { + if (searchTerm) { + return room.getName().toLowerCase().includes(searchTerm.toLowerCase()) + } else { + return true; + } + }).map(room => { return { roomID: room.roomID, displayname: room.getName(), @@ -413,16 +429,24 @@ const MainPage = memo(() => { } }); + const subsections = [...subspaceMeta?.children] + .filter(room => room.isSpace() && !room.isTombstoned()).map(generateSubsections) + .filter(section => section !== undefined) as Section[]; + if (searchTerm && rooms.length === 0 && subsections.length == 0) { + return undefined; + } + return { sectionName: subspace.getName(), rooms: rooms, roomID: subspace.roomID, - subsections: [...subspaceMeta?.children] - .filter(room => room.isSpace() && !room.isTombstoned()).map(generateSubsections) - .filter(section => section !== undefined) as Section[], + subsections: subsections, } } } + if (searchTerm && rooms.length === 0) { + return undefined; + } // Its a little weird sicne there are no children attached to the room object. Only to spacesWithRooms. // Each subsection can have further subsections and rooms. @@ -432,12 +456,18 @@ const MainPage = memo(() => { roomID: space.spaceRoom.roomID, subsections: [...space.children] .filter(room => room.isSpace()) - .map(generateSubsections), + .map(generateSubsections).filter(section => section !== undefined), } as Section; - }); + }).filter(section => section !== undefined) as Section[]; // Add the toplevel section "Other" to the end of the list. - const otherRooms = leftOverRooms.filter(room => !room.isSpace() && !room.isDM()).map(room => { + const otherRooms = leftOverRooms.filter(room => !room.isSpace() && !room.isDM()).filter(room => { + if (searchTerm) { + return room.getName().toLowerCase().includes(searchTerm.toLowerCase()) + } else { + return true; + } + }).map(room => { return { roomID: room.roomID, displayname: room.getName(), @@ -447,7 +477,13 @@ const MainPage = memo(() => { } }); - const dmRooms = leftOverRooms.filter(room => !room.isSpace() && room.isDM()).map(room => { + const dmRooms = leftOverRooms.filter(room => !room.isSpace() && room.isDM()).filter(room => { + if (searchTerm) { + return room.getName().toLowerCase().includes(searchTerm.toLowerCase()) + } else { + return true; + } + }).map(room => { return { roomID: room.roomID, displayname: room.getName(), @@ -477,8 +513,23 @@ const MainPage = memo(() => { <div id="user-info"> <Avatar displayname={profile.displayname || client.mxid!} avatarUrl={profile?.avatar_url} dm={false} online={OnlineState.Unknown} isBot={false} /> <div id='username-container'> - <span className='text-base font-semibold'>{profile?.displayname}</span> - <Settings size={28} stroke='unset' className='stroke-slate-600 rounded-full hover:bg-slate-300 p-1 cursor-pointer' /> + + <span>{profile?.displayname}</span> + <Settings size={28} stroke='unset' className='stroke-slate-600 rounded-full hover:bg-slate-300 p-1 cursor-pointer' onClick={() => { setShowSettings(true) }} /> + </div> + <div id='search-container'> + <label className='flex relative flex-row items-center justify-start hover:bg-slate-400/25 bg-slate-400/50 flex-1 rounded-lg group'> + <Search size={20} stroke='unset' className='stroke-slate-600 absolute top-2 left-2' /> + <input className='placeholder:text-slate-600 bg-transparent py-2 rounded-lg pl-8 group-hover:outline outline-slate-700 w-full outline-2 text-black' placeholder='Search' onInput={(e) => { + e.preventDefault(); + const value = e.currentTarget.value; + if (value === "") { + setSearchTerm(null); + } else { + setSearchTerm(value); + } + }} /> + </label> </div> </div> <RoomList sections={sections} rooms={otherRooms} dmRooms={dmRooms} /> @@ -489,15 +540,98 @@ const MainPage = memo(() => { <div id='room-info'> <Avatar displayname={room.getName()} avatarUrl={room.getAvatarURL()} dm={room.isDM()} online={room.presence} isBot={false} /> <div className='flex mr-2'> - <h1 className='text-black font-semibold text-lg'>{room.getName()}</h1> - <Linkify options={linkifyOptions} as='p' className="ml-2 text-slate-700 font-normal text-base line-clamp-2 text-ellipsis">{room.getTopic()}</Linkify> + <h1 id='room-name'>{room.getName()}</h1> + <Linkify options={linkifyOptions} as='p' className="ml-2 text-slate-700 dark:text-slate-400 font-normal text-base line-clamp-2 text-ellipsis">{room.getTopic()}</Linkify> </div> </div> <ChatView id='chat-view' room={room} /> <ChatInput id='chat-editor' namespace='Editor' room={room} /> </> : <></> } - </div> + + <ReactModal + isOpen={showSettings} + id="modal" + overlayClassName="fixed top-0 bottom-0 right-0 left-0 bg-slate-700/75" + onRequestClose={() => { setShowSettings(false) }} + > + <SettingsView onClose={() => { setShowSettings(false) }} /> + </ReactModal> + </div > }) -export default MainPage; -\ No newline at end of file +export default MainPage; + +type SettingsViewProps = { + onClose: () => void +} + +const SettingsView: FC<SettingsViewProps> = memo(({ onClose }) => { + const [currentSettingsView, setCurrentSettingsView] = useState("profile"); + return ( + <> + <X id="modal-cross" size={28} stroke='unset' onClick={onClose}>Close Modal</X> + <nav id="modal-sidebar"> + <button className={'p-1 bg-white w-full text-left hover:bg-slate-200 rounded flex items-center gap-1 ' + (currentSettingsView === "profile" ? '!bg-slate-200' : '')} onClick={() => { setCurrentSettingsView("profile") }}> + <User size={28} stroke='unset' className='stroke-slate-600 p-1' /> Profile + </button> + <button className={'p-1 bg-white w-full text-left hover:bg-slate-200 rounded flex items-center gap-1 ' + (currentSettingsView === "design" ? '!bg-slate-200' : '')} onClick={() => { setCurrentSettingsView("design") }}> + <Brush size={28} stroke='unset' className='stroke-slate-600 p-1' /> Design + </button> + </nav> + <div id='modal-content'> + { + currentSettingsView === "profile" ? + <SettingsProfileView /> : + <SettingsDesignView /> + } + + </div> + </> + ); +}); + + +const SettingsProfileView = memo(() => { + const client = useContext(MatrixContext); + const profile = useProfile(); + const [displayname, setDisplayname] = useState(profile.displayname); + + return ( + <div id="profile-view"> + <h1 id="title"><User size={28} stroke='unset' className='stroke-slate-600 p-1' /> Profile</h1> + + <div id="inputs"> + <div id='displayname-container'> + <span className='text-slate-700 font-semibold'>Displayname</span> + <label> + <Pencil size={20} stroke='unset' className='stroke-slate-600 absolute top-2 left-2' /> + <input value={displayname} onInput={(e) => { + e.preventDefault(); + const value = e.currentTarget.value; + setDisplayname(value); + }} /> + </label> + </div> + <div id='matrixid'> + <span className='text-slate-700 font-semibold'>MatrixID</span> + <label> + <PersonStanding size={20} stroke='unset' className='stroke-slate-600 absolute top-2 left-2' /> + <input disabled placeholder={client.mxid} /> + </label> + </div> + </div> + <div id="avatar"> + <Avatar size='14rem' displayname={profile.displayname || client.mxid!} avatarUrl={profile?.avatar_url} dm={false} online={OnlineState.Unknown} isBot={false} /> + </div> + </div> + ) +}) + +const SettingsDesignView = memo(() => { + return ( + <> + <h1 className='text-xl font-bold text-black flex items-center'><Brush size={28} stroke='unset' className='stroke-slate-600 p-1' /> Design</h1> + </> + ) +}) +\ No newline at end of file diff --git a/tailwind.config.js b/tailwind.config.js @@ -7,7 +7,6 @@ export default { "./index.html", "./src/**/*.{ts,tsx}", ], - darkMode: ['class', '[data-mode="dark"]'], theme: { extend: { fontFamily: {