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

MainPage.tsx (28007B)


      1 import { Brush, Pencil, PersonStanding, Search, Settings, User, X } from 'lucide-react';
      2 import Avatar from '../components/avatar/avatar';
      3 import ChatInput from '../components/input/chat/input';
      4 import RoomList, { Section } from '../components/roomList/roomList';
      5 import './MainPage.scss';
      6 import { useProfile, useRoom, useRooms, useSpaces } from '../app/sdk/client';
      7 import { Room } from '../app/sdk/room';
      8 import { FC, memo, useCallback, useContext, useEffect, useState } from 'react';
      9 import { MatrixContext } from '../app/sdk/client';
     10 import { useLocation, useParams } from 'react-router-dom';
     11 import MessageEvent from '../components/events/messageEvent';
     12 import UnknownEvent, { RedactedEvent, UndecryptableEvent } from '../components/events/unknownEvent';
     13 import MemberEvent from '../components/events/memberEvent';
     14 import { IRoomEvent, IRoomMemberEvent } from '../app/sdk/api/events';
     15 import Linkify from 'linkify-react';
     16 import { OnlineState } from '../app/sdk/api/otherEnums';
     17 import { Virtuoso } from 'react-virtuoso';
     18 import ReactModal from 'react-modal';
     19 import { SDKError } from '../app/sdk/utils';
     20 
     21 type ChatViewProps = {
     22     /**
     23      * The roomID of the room to display
     24      * If the roomID is empty, the ChatView will be empty
     25      * If the roomID is invalid, the ChatView will be empty
     26      * If the roomID is valid, the ChatView will display the room
     27      * If the roomID is valid, but the room is not joined, the ChatView will display a placeholder
     28      * If the roomID is valid, but the room is not loaded, the ChatView will display a placeholder
     29      */
     30     room?: Room,
     31     id?: string
     32 };
     33 
     34 const ChatView: FC<ChatViewProps> = memo(({ room, id }) => {
     35     const client = useContext(MatrixContext);
     36     const [events, setEvents] = useState<IRoomEvent[]>([]);
     37     const [eventsFull, setEventsFull] = useState<IRoomEvent[]>([]);
     38     const { pathname } = useLocation();
     39     const [previousPathname, setPreviousPathname] = useState<string | undefined>(undefined);
     40     const [firstItemIndex, setFirstItemIndex] = useState<number | undefined>(undefined)
     41 
     42     const decryptEvents = async (index: number, event: IRoomEvent, eventsFull: IRoomEvent[]) => {
     43         let previousEvent = eventsFull?.[index - 1];
     44         const previousEventIsFromSameSender = previousEvent?.sender === event.sender;
     45         // Check if event is redacted
     46         const redaction = eventsFull?.find((e) => {
     47             return e.type === "m.room.redaction" && e.redacts === event.event_id;
     48         });
     49         const redacted = redaction !== undefined;
     50         let redacted_because = undefined;
     51         let redaction_id = undefined;
     52         if (redacted) {
     53             redaction_id = redaction?.event_id;
     54             if (redaction.content.reason) {
     55                 redacted_because = redaction.content.reason;
     56             }
     57         }
     58 
     59         // Decrypt the event if it is encrypted
     60         if (event.type === "m.room.encrypted" && room?.roomID) {
     61             try {
     62                 const decrypted_event = await client.decryptRoomEvent(room.roomID, event);
     63                 if (decrypted_event instanceof SDKError) {
     64                     // TODO: Show proper error instead
     65                     return {
     66                         ...event,
     67                         unsigned: {
     68                             ...event.unsigned,
     69                             undecryptable: true,
     70                             key: event.event_id,
     71                             hasPreviousEvent: previousEventIsFromSameSender
     72                         }
     73                     }
     74                 }
     75                 if (decrypted_event) {
     76                     event = JSON.parse(decrypted_event.event) as IRoomEvent;
     77                     if (event.content["m.new_content"]) {
     78                         event.content.body = event.content["m.new_content"].body;
     79                         event.content.formatted_body = event.content["m.new_content"].formatted_body;
     80                         event.content.format = event.content["m.new_content"].format;
     81                     }
     82                 } else {
     83                     if (redacted) {
     84                         return {
     85                             ...event,
     86                             unsigned: {
     87                                 ...event.unsigned,
     88                                 redacted: true,
     89                                 redacted_because: redacted_because,
     90                                 hasPreviousEvent: previousEventIsFromSameSender,
     91                                 redaction_id: redaction_id
     92                             }
     93                         }
     94                     }
     95                     return {
     96                         ...event,
     97                         unsigned: {
     98                             ...event.unsigned,
     99                             undecryptable: true,
    100                             key: event.event_id,
    101                             hasPreviousEvent: previousEventIsFromSameSender
    102                         }
    103                     }
    104                 }
    105             } catch (e: any) {
    106                 if (redacted) {
    107                     return {
    108                         ...event,
    109                         unsigned: {
    110                             ...event.unsigned,
    111                             redacted: true,
    112                             redacted_because: redacted_because,
    113                             hasPreviousEvent: previousEventIsFromSameSender,
    114                             redaction_id: redaction_id
    115                         }
    116                     }
    117                 }
    118                 return {
    119                     ...event,
    120                     unsigned: {
    121                         ...event.unsigned,
    122                         undecryptable: true,
    123                         hasPreviousEvent: previousEventIsFromSameSender
    124                     }
    125                 }
    126             }
    127         }
    128 
    129         return event;
    130     }
    131 
    132     useEffect(() => {
    133         if (previousPathname !== pathname) {
    134             console.log("Resetting events because we changed rooms");
    135             setEvents([]);
    136             setEventsFull([]);
    137             setFirstItemIndex(0);
    138             setPreviousPathname(pathname);
    139         }
    140 
    141         if ((eventsFull.length === 0) && room) {
    142             const eventsAll = room?.getEvents().filter((event, index, self) => {
    143                 return self.findIndex(e => e.event_id === event.event_id) === index;
    144             }).sort((a, b) => {
    145                 return b.origin_server_ts - a.origin_server_ts;
    146             }).reverse();
    147 
    148             Promise.all(eventsAll.map(async (event, index) => {
    149                 return await decryptEvents(index, event, eventsAll);
    150             })).then((eventsRaw: IRoomEvent[]) => {
    151                 const no_relations = eventsRaw.filter(event => event.type !== "m.reaction" &&
    152                     event.type !== "m.room.redaction" &&
    153                     event.content["m.relates_to"]?.["rel_type"] !== "m.replace"
    154                 );
    155                 if (no_relations.length > 0) {
    156                     const events = [...no_relations];
    157                     setEventsFull(() => [...eventsRaw]);
    158                     setEvents(() => events);
    159                     setFirstItemIndex(events.length);
    160                 }
    161             })
    162         }
    163     }, [room, events, setEvents, eventsFull, setEventsFull, pathname, previousPathname, firstItemIndex, setFirstItemIndex])
    164 
    165 
    166     useEffect(() => {
    167         if (room) {
    168             // Listen for event updates
    169             const listenForEvents = (eventsListened: IRoomEvent[]) => {
    170                 const eventsAll = eventsListened.filter((event, index, self) => {
    171                     return self.findIndex(e => e.event_id === event.event_id) === index;
    172                 }).sort((a, b) => {
    173                     return b.origin_server_ts - a.origin_server_ts;
    174                 }).reverse();
    175 
    176                 Promise.all(eventsAll.map(async (event, index) => {
    177                     return await decryptEvents(index, event, eventsAll);
    178                 })).then((eventsRaw: IRoomEvent[]) => {
    179                     const no_relations = eventsRaw.filter(event => event.type !== "m.reaction" &&
    180                         event.type !== "m.room.redaction" &&
    181                         event.content["m.relates_to"]?.["rel_type"] !== "m.replace"
    182                     );
    183 
    184 
    185                     // If there are no events or no changes, do nothing
    186                     if (no_relations.length > 0 && events.length > 0) {
    187                         const newEvents = [...no_relations];
    188                         if (eventsRaw.length !== eventsFull.length || newEvents.length !== events.length) {
    189                             setEventsFull(() => [...eventsRaw]);
    190                             setEvents(() => newEvents);
    191                         }
    192                     }
    193                 })
    194             };
    195             console.log("Adding listener for events")
    196             room.on("events", listenForEvents);
    197             return () => {
    198                 console.log("Removing listener for events")
    199                 room.off("events", listenForEvents);
    200             }
    201         }
    202     }, [room, eventsFull, events, setEventsFull, setEvents, pathname])
    203 
    204     const renderEventPure = useCallback((_index: number, event: IRoomEvent) => {
    205         const index = eventsFull.findIndex(e => e.event_id === event.event_id);
    206         if (event.unsigned?.redacted) {
    207             return (<RedactedEvent event={event} redacted_because={event.unsigned.redacted_because} key={event.unsigned.redaction_id} room={room} hasPreviousEvent={event.unsigned.hasPreviousEvent} />)
    208         }
    209 
    210         if (event.unsigned?.undecryptable) {
    211             return (<UndecryptableEvent key={event.event_id} event={event} hasPreviousEvent={event.unsigned.hasPreviousEvent} room={room} />)
    212         }
    213 
    214         let previousEvent = eventsFull?.[index - 1];
    215         const previousEventIsFromSameSender = previousEvent?.sender === event.sender;
    216 
    217         let previousEventType = previousEvent?.type;
    218 
    219         // Make a list of events which are reactions for the current event we want to render
    220         const reactions = eventsFull?.filter((e) => {
    221             return e.type === "m.reaction" && e.content["m.relates_to"].event_id === event.event_id;
    222         });
    223 
    224         // Check if event is redacted
    225         const redaction = eventsFull?.find((e) => {
    226             return e.type === "m.room.redaction" && e.redacts === event.event_id;
    227         });
    228         const redacted = redaction !== undefined;
    229         let redacted_because = undefined;
    230         let redaction_id = undefined;
    231         if (redacted) {
    232             redaction_id = redaction?.event_id;
    233             if (redaction.content.reason) {
    234                 redacted_because = redaction.content.reason;
    235             }
    236         }
    237 
    238         // Check if there is an edit (m.relates_to with rel_type of "m.replace")
    239         const edit = eventsFull?.find((e) => {
    240             if (!e.content["m.relates_to"]) {
    241                 return false;
    242             }
    243             return e.content["m.relates_to"].rel_type === "m.replace" && e.content["m.relates_to"].event_id === event.event_id;
    244         });
    245 
    246         // If there is an edit, use the edited event instead of the original event
    247         if (edit) {
    248             event = edit;
    249             if (edit.content["m.new_content"]) {
    250                 event.content.body = edit.content["m.new_content"].body;
    251                 event.content.formatted_body = edit.content["m.new_content"].formatted_body;
    252                 event.content.format = edit.content["m.new_content"].format;
    253             }
    254         }
    255 
    256         return (
    257             <div className='max-w-[130ch]'>
    258                 {renderEvent(event, previousEventIsFromSameSender, previousEventType, reactions, redacted, redacted_because, redaction_id, room)}
    259             </div>
    260         )
    261     }, [eventsFull])
    262 
    263 
    264     if (events.length === 0 || !room || eventsFull.length === 0) {
    265         return (
    266             <></>
    267         )
    268     }
    269 
    270     return (
    271         <Virtuoso
    272             id={id}
    273             alignToBottom
    274             className='flex overflow-y-auto overflow-x-hidden scrollbarSmall'
    275             data={events}
    276             firstItemIndex={firstItemIndex}
    277             initialTopMostItemIndex={events.length - 1}
    278             overscan={200}
    279             itemContent={renderEventPure}
    280             components={{ Header }}
    281             followOutput={(isAtBottom: boolean) => {
    282                 if (isAtBottom) {
    283                     return 'smooth'
    284                 } else {
    285                     return false
    286                 }
    287             }}
    288         />
    289     );
    290 });
    291 
    292 
    293 const Header = () => {
    294     return (
    295         <div
    296             style={{
    297                 padding: '2rem',
    298                 display: 'flex',
    299                 justifyContent: 'center',
    300             }}
    301         >
    302             Loading...
    303         </div>
    304     )
    305 }
    306 
    307 // Render events based on the event type and content
    308 const renderEvent = (event: IRoomEvent, previousEventIsFromSameSender: boolean, previousEventType: string, reactions: IRoomEvent[], redacted: boolean, redacted_because: string, redaction_id?: string, room?: Room,) => {
    309     if (redacted) {
    310         return (<RedactedEvent event={event} redacted_because={redacted_because} room={room} hasPreviousEvent={previousEventIsFromSameSender} key={redaction_id} />)
    311     }
    312     switch (event.type) {
    313         case "m.room.message":
    314             return <MessageEvent reactions={reactions} event={event} room={room} key={event.event_id} hasPreviousEvent={previousEventIsFromSameSender && previousEventType === "m.room.message"} />
    315         case "m.room.member":
    316             return <MemberEvent event={event as IRoomMemberEvent} key={event.event_id} />
    317         default:
    318             return <UnknownEvent event={event} key={event.event_id} />
    319     }
    320 }
    321 
    322 const MainPage = memo(() => {
    323     const profile = useProfile();
    324     const spacesWithRooms = useSpaces();
    325     const rooms = useRooms();
    326     const client = useContext(MatrixContext);
    327     const params = useParams();
    328     const room = useRoom(decodeURIComponent(params.roomIdOrAlias || ""));
    329     client.setCurrentRoom(params.roomIdOrAlias ? decodeURIComponent(params.roomIdOrAlias) : undefined)
    330 
    331     const [showSettings, setShowSettings] = useState(false);
    332     const [searchTerm, setSearchTerm] = useState<string | null>(null);
    333 
    334     // Filter toplevel spaces.
    335     // A toplevel space is a space that is not a child of another space.
    336     // We can not rely only on the parent. We need to check in both directions.
    337     const toplevelSpaces = [...spacesWithRooms].filter(({ spaceRoom }) => {
    338         const not_tombstoned = !spaceRoom.isTombstoned();
    339         const not_a_child = ![...spacesWithRooms].some(({ children: otherChildren }) => {
    340             return [...otherChildren].some(room => room.roomID === spaceRoom.roomID);
    341         });
    342         // Also check if there are no parents set
    343         const no_parents = spaceRoom.getSpaceParentIDs().length === 0;
    344         return not_a_child && no_parents && not_tombstoned;
    345     });
    346 
    347     // Filter rooms which are not part of any space and are not a space.
    348     // A room is not part of any space if it is not a child of any space.
    349     // A room is not a space if it has not any space as parent.
    350     const leftOverRooms = [...rooms].filter(room => {
    351         const not_tombstoned = !room.isTombstoned();
    352         const not_a_child = ![...spacesWithRooms].some(({ children }) => {
    353             return [...children].some(otherRoom => otherRoom.roomID === room.roomID);
    354         });
    355         const no_parents = room.getSpaceParentIDs().length === 0;
    356         const not_a_space = !room.isSpace();
    357         return not_a_child && no_parents && not_a_space && not_tombstoned;
    358     }).sort((a, b) => {
    359         // Sort rooms by sliding sync list order of overview list,
    360 
    361         // Get the index of the room in the sync list.
    362         const a_index = a.windowPos["overview"];
    363         const b_index = b.windowPos["overview"];
    364 
    365         // If the room is not in the sync list, it will be at the end of the list.
    366         // This is the same as the index being -1.
    367         // So we need to check for that.
    368         if (a_index === -1) {
    369             return 1;
    370         }
    371         if (b_index === -1) {
    372             return -1;
    373         }
    374 
    375         // If the room is in the sync list, we can compare the indexes.
    376         return a_index - b_index;
    377     });
    378 
    379 
    380     // Generate a list of sections.
    381     // Each section apart from special toplevel ones is a space.
    382     // Each space has a list of rooms and subsections.
    383     // Each subsection has a list of rooms and subsections.
    384     // Subsections can nest infinitely.
    385     // Rooms are always within a section.
    386     // A section represents a space.
    387     // If a room is not within a space it is in the toplevel section "Other" which is at the end of the list.
    388     // The toplevel section "Other" is always present.
    389     // The toplevel section "Other" is always the last section.
    390     const sections = toplevelSpaces.map(space => {
    391         const rooms = [...space.children].filter(room => !room.isSpace() && !room.isTombstoned() && !room.isDM()).sort((a, b) => {
    392             // Sort rooms by sliding sync list order of the spaces list,
    393 
    394             // Get the index of the room in the sync list.
    395             const a_index = a.windowPos[space.spaceRoom.roomID];
    396             const b_index = b.windowPos[space.spaceRoom.roomID];
    397 
    398             // If the room is not in the sync list, it will be at the end of the list.
    399             // This is the same as the index being -1.
    400             // So we need to check for that.
    401             if (a_index === -1) {
    402                 return 1;
    403             }
    404             if (b_index === -1) {
    405                 return -1;
    406             }
    407 
    408             // If the room is in the sync list, we can compare the indexes.
    409             return a_index - b_index;
    410         }).filter(room => {
    411             if (searchTerm) {
    412                 return room.getName().toLowerCase().includes(searchTerm.toLowerCase())
    413             } else {
    414                 return true;
    415             }
    416         }).map(room => {
    417             return {
    418                 roomID: room.roomID,
    419                 displayname: room.getName(),
    420                 avatarUrl: room.getAvatarURL(),
    421                 dm: room.isDM(),
    422                 online: room.presence,
    423             }
    424         });
    425 
    426         const generateSubsections = (subspace: Room): Section | undefined => {
    427             const subspaceMeta = [...spacesWithRooms].find(space => space.spaceRoom.roomID === subspace.roomID);
    428             if (subspaceMeta) {
    429                 const rooms = [...subspaceMeta?.children].filter(room => !room.isSpace() && !room.isTombstoned() && !room.isDM()).filter(room => {
    430                     if (searchTerm) {
    431                         return room.getName().toLowerCase().includes(searchTerm.toLowerCase())
    432                     } else {
    433                         return true;
    434                     }
    435                 }).map(room => {
    436                     return {
    437                         roomID: room.roomID,
    438                         displayname: room.getName(),
    439                         avatarUrl: room.getAvatarURL(),
    440                         dm: room.isDM(),
    441                         online: room.presence,
    442                     }
    443                 });
    444 
    445                 const subsections = [...subspaceMeta?.children]
    446                     .filter(room => room.isSpace() && !room.isTombstoned()).map(generateSubsections)
    447                     .filter(section => section !== undefined) as Section[];
    448                 if (searchTerm && rooms.length === 0 && subsections.length == 0) {
    449                     return undefined;
    450                 }
    451 
    452                 return {
    453                     sectionName: subspace.getName(),
    454                     rooms: rooms,
    455                     roomID: subspace.roomID,
    456                     subsections: subsections,
    457                 }
    458             }
    459         }
    460         if (searchTerm && rooms.length === 0) {
    461             return undefined;
    462         }
    463 
    464         // Its a little weird sicne there are no children attached to the room object. Only to spacesWithRooms.
    465         // Each subsection can have further subsections and rooms.
    466         return {
    467             sectionName: space.spaceRoom.getName(),
    468             rooms: rooms,
    469             roomID: space.spaceRoom.roomID,
    470             subsections: [...space.children]
    471                 .filter(room => room.isSpace())
    472                 .map(generateSubsections).filter(section => section !== undefined),
    473         } as Section;
    474     }).filter(section => section !== undefined) as Section[];
    475 
    476     // Add the toplevel section "Other" to the end of the list.
    477     const otherRooms = leftOverRooms.filter(room => !room.isSpace() && !room.isDM()).filter(room => {
    478         if (searchTerm) {
    479             return room.getName().toLowerCase().includes(searchTerm.toLowerCase())
    480         } else {
    481             return true;
    482         }
    483     }).map(room => {
    484         return {
    485             roomID: room.roomID,
    486             displayname: room.getName(),
    487             avatarUrl: room.getAvatarURL(),
    488             dm: room.isDM(),
    489             online: room.presence,
    490         }
    491     });
    492 
    493     const dmRooms = leftOverRooms.filter(room => !room.isSpace() && room.isDM()).filter(room => {
    494         if (searchTerm) {
    495             return room.getName().toLowerCase().includes(searchTerm.toLowerCase())
    496         } else {
    497             return true;
    498         }
    499     }).map(room => {
    500         return {
    501             roomID: room.roomID,
    502             displayname: room.getName(),
    503             avatarUrl: room.getAvatarURL(),
    504             dm: room.isDM(),
    505             online: room.presence,
    506         }
    507     });
    508 
    509 
    510     // Check and print if otherRooms has duplicates.
    511     const otherRoomsIDs = otherRooms.map(room => room.roomID);
    512     const otherRoomsDuplicates = otherRoomsIDs.filter((id, index) => otherRoomsIDs.indexOf(id) !== index);
    513     if (otherRoomsDuplicates.length > 0) {
    514         console.error('otherRooms has duplicates', otherRoomsDuplicates);
    515     }
    516 
    517     const linkifyOptions = {
    518         defaultProtocol: "https",
    519         rel: "noopener",
    520         target: "_blank",
    521         className: "text-blue-500 hover:text-blue-700 active:text-blue-700 visited:text-blue-500"
    522     }
    523 
    524     return <div id='main-container'>
    525         <div id='sidebar'>
    526             <div id="user-info">
    527                 <Avatar displayname={profile.displayname || client.mxid!} avatarUrl={profile?.avatar_url} dm={false} online={OnlineState.Unknown} isBot={false} />
    528                 <div id='username-container'>
    529 
    530                     <span>{profile?.displayname}</span>
    531                     <Settings size={28} stroke='unset' className='stroke-slate-600 rounded-full hover:bg-slate-300 p-1 cursor-pointer' onClick={() => { setShowSettings(true) }} />
    532                 </div>
    533                 <div id='search-container'>
    534                     <label className='flex relative flex-row items-center justify-start hover:bg-slate-400/25 bg-slate-400/50 flex-1 rounded-lg group'>
    535                         <Search size={20} stroke='unset' className='stroke-slate-600 absolute top-2 left-2' />
    536                         <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) => {
    537                             e.preventDefault();
    538                             const value = e.currentTarget.value;
    539                             if (value === "") {
    540                                 setSearchTerm(null);
    541                             } else {
    542                                 setSearchTerm(value);
    543                             }
    544                         }} />
    545                     </label>
    546                 </div>
    547             </div>
    548             <RoomList sections={sections} rooms={otherRooms} dmRooms={dmRooms} />
    549         </div>
    550         {
    551             room ? <>
    552                 <div className="room-wrapper"></div>
    553                 <div id='room-info'>
    554                     <Avatar displayname={room.getName()} avatarUrl={room.getAvatarURL()} dm={room.isDM()} online={room.presence} isBot={false} />
    555                     <div className='flex mr-2'>
    556                         <h1 id='room-name'>{room.getName()}</h1>
    557                         <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>
    558                     </div>
    559                 </div>
    560                 <ChatView id='chat-view' room={room} />
    561                 <ChatInput id='chat-editor' namespace='Editor' room={room} />
    562             </> : <></>
    563         }
    564 
    565         <ReactModal
    566             isOpen={showSettings}
    567             id="modal"
    568             overlayClassName="fixed top-0 bottom-0 right-0 left-0 bg-slate-700/75"
    569             onRequestClose={() => { setShowSettings(false) }}
    570         >
    571             <SettingsView onClose={() => { setShowSettings(false) }} />
    572         </ReactModal>
    573     </div >
    574 })
    575 
    576 export default MainPage;
    577 
    578 type SettingsViewProps = {
    579     onClose: () => void
    580 }
    581 
    582 const SettingsView: FC<SettingsViewProps> = memo(({ onClose }) => {
    583     const [currentSettingsView, setCurrentSettingsView] = useState("profile");
    584     return (
    585         <>
    586             <X id="modal-cross" size={28} stroke='unset' onClick={onClose}>Close Modal</X>
    587             <nav id="modal-sidebar">
    588                 <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") }}>
    589                     <User size={28} stroke='unset' className='stroke-slate-600 p-1' /> Profile
    590                 </button>
    591                 <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") }}>
    592                     <Brush size={28} stroke='unset' className='stroke-slate-600 p-1' /> Design
    593                 </button>
    594             </nav>
    595             <div id='modal-content'>
    596                 {
    597                     currentSettingsView === "profile" ?
    598                         <SettingsProfileView /> :
    599                         <SettingsDesignView />
    600                 }
    601 
    602             </div>
    603         </>
    604     );
    605 });
    606 
    607 
    608 const SettingsProfileView = memo(() => {
    609     const client = useContext(MatrixContext);
    610     const profile = useProfile();
    611     const [displayname, setDisplayname] = useState(profile.displayname);
    612 
    613     return (
    614         <div id="profile-view">
    615             <h1 id="title"><User size={28} stroke='unset' className='stroke-slate-600 p-1' /> Profile</h1>
    616 
    617             <div id="inputs">
    618                 <div id='displayname-container'>
    619                     <span className='text-slate-700 font-semibold'>Displayname</span>
    620                     <label>
    621                         <Pencil size={20} stroke='unset' className='stroke-slate-600 absolute top-2 left-2' />
    622                         <input value={displayname} onInput={(e) => {
    623                             e.preventDefault();
    624                             const value = e.currentTarget.value;
    625                             setDisplayname(value);
    626                         }} />
    627                     </label>
    628                 </div>
    629                 <div id='matrixid'>
    630                     <span className='text-slate-700 font-semibold'>MatrixID</span>
    631                     <label>
    632                         <PersonStanding size={20} stroke='unset' className='stroke-slate-600 absolute top-2 left-2' />
    633                         <input disabled placeholder={client.mxid} />
    634                     </label>
    635                 </div>
    636             </div>
    637             <div id="avatar">
    638                 <Avatar size='14rem' displayname={profile.displayname || client.mxid!} avatarUrl={profile?.avatar_url} dm={false} online={OnlineState.Unknown} isBot={false} />
    639             </div>
    640         </div>
    641     )
    642 })
    643 
    644 const SettingsDesignView = memo(() => {
    645     return (
    646         <>
    647             <h1 className='text-xl font-bold text-black flex items-center'><Brush size={28} stroke='unset' className='stroke-slate-600 p-1' /> Design</h1>
    648         </>
    649     )
    650 })