mirror of
				https://github.com/cinnyapp/cinny.git
				synced 2025-11-04 06:20:28 +03:00 
			
		
		
		
	New invite user to room dialog (#2460)
* fix 0 displayed in invite with no timestamp * support displaying invite reason for receiver * show invite reason as compact message * remove unused import * revert: show invite reason as compact message * remove unused import * add new invite prompt
This commit is contained in:
		
							parent
							
								
									c881b59957
								
							
						
					
					
						commit
						13cdcbcdb1
					
				
					 10 changed files with 434 additions and 56 deletions
				
			
		
							
								
								
									
										291
									
								
								src/app/components/invite-user-prompt/InviteUserPrompt.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										291
									
								
								src/app/components/invite-user-prompt/InviteUserPrompt.tsx
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,291 @@
 | 
				
			||||||
 | 
					import React, {
 | 
				
			||||||
 | 
					  ChangeEventHandler,
 | 
				
			||||||
 | 
					  FormEventHandler,
 | 
				
			||||||
 | 
					  KeyboardEventHandler,
 | 
				
			||||||
 | 
					  useCallback,
 | 
				
			||||||
 | 
					  useMemo,
 | 
				
			||||||
 | 
					  useRef,
 | 
				
			||||||
 | 
					  useState,
 | 
				
			||||||
 | 
					} from 'react';
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  Overlay,
 | 
				
			||||||
 | 
					  OverlayBackdrop,
 | 
				
			||||||
 | 
					  OverlayCenter,
 | 
				
			||||||
 | 
					  Box,
 | 
				
			||||||
 | 
					  Header,
 | 
				
			||||||
 | 
					  config,
 | 
				
			||||||
 | 
					  Text,
 | 
				
			||||||
 | 
					  IconButton,
 | 
				
			||||||
 | 
					  Icon,
 | 
				
			||||||
 | 
					  Icons,
 | 
				
			||||||
 | 
					  Input,
 | 
				
			||||||
 | 
					  Button,
 | 
				
			||||||
 | 
					  Spinner,
 | 
				
			||||||
 | 
					  color,
 | 
				
			||||||
 | 
					  TextArea,
 | 
				
			||||||
 | 
					  Dialog,
 | 
				
			||||||
 | 
					  Menu,
 | 
				
			||||||
 | 
					  toRem,
 | 
				
			||||||
 | 
					  Scroll,
 | 
				
			||||||
 | 
					  MenuItem,
 | 
				
			||||||
 | 
					} from 'folds';
 | 
				
			||||||
 | 
					import { Room } from 'matrix-js-sdk';
 | 
				
			||||||
 | 
					import { isKeyHotkey } from 'is-hotkey';
 | 
				
			||||||
 | 
					import FocusTrap from 'focus-trap-react';
 | 
				
			||||||
 | 
					import { stopPropagation } from '../../utils/keyboard';
 | 
				
			||||||
 | 
					import { useDirectUsers } from '../../hooks/useDirectUsers';
 | 
				
			||||||
 | 
					import { getMxIdLocalPart, getMxIdServer, isUserId } from '../../utils/matrix';
 | 
				
			||||||
 | 
					import { Membership } from '../../../types/matrix/room';
 | 
				
			||||||
 | 
					import { useAsyncSearch, UseAsyncSearchOptions } from '../../hooks/useAsyncSearch';
 | 
				
			||||||
 | 
					import { highlightText, makeHighlightRegex } from '../../plugins/react-custom-html-parser';
 | 
				
			||||||
 | 
					import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
 | 
				
			||||||
 | 
					import { useMatrixClient } from '../../hooks/useMatrixClient';
 | 
				
			||||||
 | 
					import { BreakWord } from '../../styles/Text.css';
 | 
				
			||||||
 | 
					import { useAlive } from '../../hooks/useAlive';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const SEARCH_OPTIONS: UseAsyncSearchOptions = {
 | 
				
			||||||
 | 
					  limit: 1000,
 | 
				
			||||||
 | 
					  matchOptions: {
 | 
				
			||||||
 | 
					    contain: true,
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					const getUserIdString = (userId: string) => getMxIdLocalPart(userId) ?? userId;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type InviteUserProps = {
 | 
				
			||||||
 | 
					  room: Room;
 | 
				
			||||||
 | 
					  requestClose: () => void;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					export function InviteUserPrompt({ room, requestClose }: InviteUserProps) {
 | 
				
			||||||
 | 
					  const mx = useMatrixClient();
 | 
				
			||||||
 | 
					  const alive = useAlive();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const inputRef = useRef<HTMLInputElement>(null);
 | 
				
			||||||
 | 
					  const directUsers = useDirectUsers();
 | 
				
			||||||
 | 
					  const [validUserId, setValidUserId] = useState<string>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const filteredUsers = useMemo(
 | 
				
			||||||
 | 
					    () =>
 | 
				
			||||||
 | 
					      directUsers.filter((userId) => {
 | 
				
			||||||
 | 
					        const membership = room.getMember(userId)?.membership;
 | 
				
			||||||
 | 
					        return membership !== Membership.Join;
 | 
				
			||||||
 | 
					      }),
 | 
				
			||||||
 | 
					    [directUsers, room]
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					  const [result, search, resetSearch] = useAsyncSearch(
 | 
				
			||||||
 | 
					    filteredUsers,
 | 
				
			||||||
 | 
					    getUserIdString,
 | 
				
			||||||
 | 
					    SEARCH_OPTIONS
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					  const queryHighlighRegex = result?.query
 | 
				
			||||||
 | 
					    ? makeHighlightRegex(result.query.split(' '))
 | 
				
			||||||
 | 
					    : undefined;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const [inviteState, invite] = useAsyncCallback<void, Error, [string, string | undefined]>(
 | 
				
			||||||
 | 
					    useCallback(
 | 
				
			||||||
 | 
					      async (userId, reason) => {
 | 
				
			||||||
 | 
					        await mx.invite(room.roomId, userId, reason);
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      [mx, room]
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const inviting = inviteState.status === AsyncStatus.Loading;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleReset = () => {
 | 
				
			||||||
 | 
					    if (inputRef.current) inputRef.current.value = '';
 | 
				
			||||||
 | 
					    setValidUserId(undefined);
 | 
				
			||||||
 | 
					    resetSearch();
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleSubmit: FormEventHandler<HTMLFormElement> = (evt) => {
 | 
				
			||||||
 | 
					    evt.preventDefault();
 | 
				
			||||||
 | 
					    const target = evt.target as HTMLFormElement | undefined;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (inviting || !validUserId) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const reasonInput = target?.reasonInput as HTMLTextAreaElement | undefined;
 | 
				
			||||||
 | 
					    const reason = reasonInput?.value.trim();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    invite(validUserId, reason || undefined).then(() => {
 | 
				
			||||||
 | 
					      if (alive()) {
 | 
				
			||||||
 | 
					        handleReset();
 | 
				
			||||||
 | 
					        if (reasonInput) reasonInput.value = '';
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleSearchChange: ChangeEventHandler<HTMLInputElement> = (evt) => {
 | 
				
			||||||
 | 
					    const value = evt.currentTarget.value.trim();
 | 
				
			||||||
 | 
					    if (isUserId(value)) {
 | 
				
			||||||
 | 
					      setValidUserId(value);
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      setValidUserId(undefined);
 | 
				
			||||||
 | 
					      const term = getMxIdLocalPart(value) ?? (value.startsWith('@') ? value.slice(1) : value);
 | 
				
			||||||
 | 
					      if (term) {
 | 
				
			||||||
 | 
					        search(term);
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        resetSearch();
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleUserId = (userId: string) => {
 | 
				
			||||||
 | 
					    if (inputRef.current) {
 | 
				
			||||||
 | 
					      inputRef.current.value = userId;
 | 
				
			||||||
 | 
					      setValidUserId(userId);
 | 
				
			||||||
 | 
					      resetSearch();
 | 
				
			||||||
 | 
					      inputRef.current.focus();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleKeyDown: KeyboardEventHandler<HTMLInputElement> = (evt) => {
 | 
				
			||||||
 | 
					    if (isKeyHotkey('escape', evt)) {
 | 
				
			||||||
 | 
					      resetSearch();
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (isKeyHotkey('tab', evt) && result && result.items.length > 0) {
 | 
				
			||||||
 | 
					      evt.preventDefault();
 | 
				
			||||||
 | 
					      const userId = result.items[0];
 | 
				
			||||||
 | 
					      handleUserId(userId);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <Overlay open backdrop={<OverlayBackdrop />}>
 | 
				
			||||||
 | 
					      <OverlayCenter>
 | 
				
			||||||
 | 
					        <FocusTrap
 | 
				
			||||||
 | 
					          focusTrapOptions={{
 | 
				
			||||||
 | 
					            initialFocus: () => inputRef.current,
 | 
				
			||||||
 | 
					            clickOutsideDeactivates: true,
 | 
				
			||||||
 | 
					            onDeactivate: requestClose,
 | 
				
			||||||
 | 
					            escapeDeactivates: stopPropagation,
 | 
				
			||||||
 | 
					          }}
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          <Dialog>
 | 
				
			||||||
 | 
					            <Box grow="Yes" direction="Column">
 | 
				
			||||||
 | 
					              <Header
 | 
				
			||||||
 | 
					                size="500"
 | 
				
			||||||
 | 
					                style={{ padding: `0 ${config.space.S200} 0 ${config.space.S400}` }}
 | 
				
			||||||
 | 
					              >
 | 
				
			||||||
 | 
					                <Box grow="Yes">
 | 
				
			||||||
 | 
					                  <Text size="H4" truncate>
 | 
				
			||||||
 | 
					                    Invite
 | 
				
			||||||
 | 
					                  </Text>
 | 
				
			||||||
 | 
					                </Box>
 | 
				
			||||||
 | 
					                <Box shrink="No">
 | 
				
			||||||
 | 
					                  <IconButton size="300" radii="300" onClick={requestClose}>
 | 
				
			||||||
 | 
					                    <Icon src={Icons.Cross} />
 | 
				
			||||||
 | 
					                  </IconButton>
 | 
				
			||||||
 | 
					                </Box>
 | 
				
			||||||
 | 
					              </Header>
 | 
				
			||||||
 | 
					              <Box
 | 
				
			||||||
 | 
					                as="form"
 | 
				
			||||||
 | 
					                onSubmit={handleSubmit}
 | 
				
			||||||
 | 
					                shrink="No"
 | 
				
			||||||
 | 
					                style={{ padding: config.space.S400 }}
 | 
				
			||||||
 | 
					                direction="Column"
 | 
				
			||||||
 | 
					                gap="400"
 | 
				
			||||||
 | 
					              >
 | 
				
			||||||
 | 
					                <Box direction="Column" gap="100">
 | 
				
			||||||
 | 
					                  <Text size="L400">User ID</Text>
 | 
				
			||||||
 | 
					                  <div>
 | 
				
			||||||
 | 
					                    <Input
 | 
				
			||||||
 | 
					                      size="500"
 | 
				
			||||||
 | 
					                      ref={inputRef}
 | 
				
			||||||
 | 
					                      onChange={handleSearchChange}
 | 
				
			||||||
 | 
					                      onKeyDown={handleKeyDown}
 | 
				
			||||||
 | 
					                      placeholder="@john:server"
 | 
				
			||||||
 | 
					                      name="userIdInput"
 | 
				
			||||||
 | 
					                      variant="Background"
 | 
				
			||||||
 | 
					                      disabled={inviting}
 | 
				
			||||||
 | 
					                      autoComplete="off"
 | 
				
			||||||
 | 
					                      required
 | 
				
			||||||
 | 
					                    />
 | 
				
			||||||
 | 
					                    {result && result.items.length > 0 && (
 | 
				
			||||||
 | 
					                      <FocusTrap
 | 
				
			||||||
 | 
					                        focusTrapOptions={{
 | 
				
			||||||
 | 
					                          initialFocus: false,
 | 
				
			||||||
 | 
					                          onDeactivate: resetSearch,
 | 
				
			||||||
 | 
					                          returnFocusOnDeactivate: false,
 | 
				
			||||||
 | 
					                          clickOutsideDeactivates: true,
 | 
				
			||||||
 | 
					                          allowOutsideClick: true,
 | 
				
			||||||
 | 
					                          isKeyForward: (evt: KeyboardEvent) => isKeyHotkey('arrowdown', evt),
 | 
				
			||||||
 | 
					                          isKeyBackward: (evt: KeyboardEvent) => isKeyHotkey('arrowup', evt),
 | 
				
			||||||
 | 
					                          escapeDeactivates: stopPropagation,
 | 
				
			||||||
 | 
					                        }}
 | 
				
			||||||
 | 
					                      >
 | 
				
			||||||
 | 
					                        <Box style={{ position: 'relative' }}>
 | 
				
			||||||
 | 
					                          <Menu style={{ position: 'absolute', top: 0, zIndex: 1, width: '100%' }}>
 | 
				
			||||||
 | 
					                            <Scroll size="300" style={{ maxHeight: toRem(100) }}>
 | 
				
			||||||
 | 
					                              <div style={{ padding: config.space.S100 }}>
 | 
				
			||||||
 | 
					                                {result.items.map((userId) => {
 | 
				
			||||||
 | 
					                                  const username = `${getMxIdLocalPart(userId)}`;
 | 
				
			||||||
 | 
					                                  const userServer = getMxIdServer(userId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                                  return (
 | 
				
			||||||
 | 
					                                    <MenuItem
 | 
				
			||||||
 | 
					                                      key={userId}
 | 
				
			||||||
 | 
					                                      type="button"
 | 
				
			||||||
 | 
					                                      size="300"
 | 
				
			||||||
 | 
					                                      variant="Surface"
 | 
				
			||||||
 | 
					                                      radii="300"
 | 
				
			||||||
 | 
					                                      onClick={() => handleUserId(userId)}
 | 
				
			||||||
 | 
					                                      after={
 | 
				
			||||||
 | 
					                                        <Text size="T200" truncate>
 | 
				
			||||||
 | 
					                                          {userServer}
 | 
				
			||||||
 | 
					                                        </Text>
 | 
				
			||||||
 | 
					                                      }
 | 
				
			||||||
 | 
					                                      disabled={inviting}
 | 
				
			||||||
 | 
					                                    >
 | 
				
			||||||
 | 
					                                      <Box grow="Yes">
 | 
				
			||||||
 | 
					                                        <Text size="T300" truncate>
 | 
				
			||||||
 | 
					                                          <b>
 | 
				
			||||||
 | 
					                                            {queryHighlighRegex
 | 
				
			||||||
 | 
					                                              ? highlightText(queryHighlighRegex, [
 | 
				
			||||||
 | 
					                                                  username ?? userId,
 | 
				
			||||||
 | 
					                                                ])
 | 
				
			||||||
 | 
					                                              : username}
 | 
				
			||||||
 | 
					                                          </b>
 | 
				
			||||||
 | 
					                                        </Text>
 | 
				
			||||||
 | 
					                                      </Box>
 | 
				
			||||||
 | 
					                                    </MenuItem>
 | 
				
			||||||
 | 
					                                  );
 | 
				
			||||||
 | 
					                                })}
 | 
				
			||||||
 | 
					                              </div>
 | 
				
			||||||
 | 
					                            </Scroll>
 | 
				
			||||||
 | 
					                          </Menu>
 | 
				
			||||||
 | 
					                        </Box>
 | 
				
			||||||
 | 
					                      </FocusTrap>
 | 
				
			||||||
 | 
					                    )}
 | 
				
			||||||
 | 
					                  </div>
 | 
				
			||||||
 | 
					                </Box>
 | 
				
			||||||
 | 
					                <Box direction="Column" gap="100">
 | 
				
			||||||
 | 
					                  <Text size="L400">Reason (Optional)</Text>
 | 
				
			||||||
 | 
					                  <TextArea
 | 
				
			||||||
 | 
					                    size="500"
 | 
				
			||||||
 | 
					                    name="reasonInput"
 | 
				
			||||||
 | 
					                    variant="Background"
 | 
				
			||||||
 | 
					                    rows={4}
 | 
				
			||||||
 | 
					                    resize="None"
 | 
				
			||||||
 | 
					                  />
 | 
				
			||||||
 | 
					                </Box>
 | 
				
			||||||
 | 
					                {inviteState.status === AsyncStatus.Error && (
 | 
				
			||||||
 | 
					                  <Text size="T200" style={{ color: color.Critical.Main }} className={BreakWord}>
 | 
				
			||||||
 | 
					                    <b>{inviteState.error.message}</b>
 | 
				
			||||||
 | 
					                  </Text>
 | 
				
			||||||
 | 
					                )}
 | 
				
			||||||
 | 
					                <Button
 | 
				
			||||||
 | 
					                  type="submit"
 | 
				
			||||||
 | 
					                  disabled={!validUserId || inviting}
 | 
				
			||||||
 | 
					                  before={inviting && <Spinner size="200" variant="Primary" fill="Solid" />}
 | 
				
			||||||
 | 
					                >
 | 
				
			||||||
 | 
					                  <Text size="B400">Invite</Text>
 | 
				
			||||||
 | 
					                </Button>
 | 
				
			||||||
 | 
					              </Box>
 | 
				
			||||||
 | 
					            </Box>
 | 
				
			||||||
 | 
					          </Dialog>
 | 
				
			||||||
 | 
					        </FocusTrap>
 | 
				
			||||||
 | 
					      </OverlayCenter>
 | 
				
			||||||
 | 
					    </Overlay>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										1
									
								
								src/app/components/invite-user-prompt/index.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								src/app/components/invite-user-prompt/index.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1 @@
 | 
				
			||||||
 | 
					export * from './InviteUserPrompt';
 | 
				
			||||||
| 
						 | 
					@ -1,8 +1,7 @@
 | 
				
			||||||
import React, { useCallback } from 'react';
 | 
					import React, { useCallback, useState } from 'react';
 | 
				
			||||||
import { Avatar, Box, Button, Spinner, Text, as } from 'folds';
 | 
					import { Avatar, Box, Button, Spinner, Text, as } from 'folds';
 | 
				
			||||||
import { Room } from 'matrix-js-sdk';
 | 
					import { Room } from 'matrix-js-sdk';
 | 
				
			||||||
import { useAtomValue } from 'jotai';
 | 
					import { useAtomValue } from 'jotai';
 | 
				
			||||||
import { openInviteUser } from '../../../client/action/navigation';
 | 
					 | 
				
			||||||
import { IRoomCreateContent, Membership, StateEvent } from '../../../types/matrix/room';
 | 
					import { IRoomCreateContent, Membership, StateEvent } from '../../../types/matrix/room';
 | 
				
			||||||
import { getMemberDisplayName, getStateEvent } from '../../utils/room';
 | 
					import { getMemberDisplayName, getStateEvent } from '../../utils/room';
 | 
				
			||||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
 | 
					import { useMatrixClient } from '../../hooks/useMatrixClient';
 | 
				
			||||||
| 
						 | 
					@ -17,6 +16,7 @@ import { mDirectAtom } from '../../state/mDirectList';
 | 
				
			||||||
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
 | 
					import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
 | 
				
			||||||
import { useSetting } from '../../state/hooks/settings';
 | 
					import { useSetting } from '../../state/hooks/settings';
 | 
				
			||||||
import { settingsAtom } from '../../state/settings';
 | 
					import { settingsAtom } from '../../state/settings';
 | 
				
			||||||
 | 
					import { InviteUserPrompt } from '../invite-user-prompt';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export type RoomIntroProps = {
 | 
					export type RoomIntroProps = {
 | 
				
			||||||
  room: Room;
 | 
					  room: Room;
 | 
				
			||||||
| 
						 | 
					@ -27,6 +27,7 @@ export const RoomIntro = as<'div', RoomIntroProps>(({ room, ...props }, ref) =>
 | 
				
			||||||
  const useAuthentication = useMediaAuthentication();
 | 
					  const useAuthentication = useMediaAuthentication();
 | 
				
			||||||
  const { navigateRoom } = useRoomNavigate();
 | 
					  const { navigateRoom } = useRoomNavigate();
 | 
				
			||||||
  const mDirects = useAtomValue(mDirectAtom);
 | 
					  const mDirects = useAtomValue(mDirectAtom);
 | 
				
			||||||
 | 
					  const [invitePrompt, setInvitePrompt] = useState(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const createEvent = getStateEvent(room, StateEvent.RoomCreate);
 | 
					  const createEvent = getStateEvent(room, StateEvent.RoomCreate);
 | 
				
			||||||
  const avatarMxc = useRoomAvatar(room, mDirects.has(room.roomId));
 | 
					  const avatarMxc = useRoomAvatar(room, mDirects.has(room.roomId));
 | 
				
			||||||
| 
						 | 
					@ -76,14 +77,13 @@ export const RoomIntro = as<'div', RoomIntroProps>(({ room, ...props }, ref) =>
 | 
				
			||||||
          )}
 | 
					          )}
 | 
				
			||||||
        </Box>
 | 
					        </Box>
 | 
				
			||||||
        <Box gap="200" wrap="Wrap">
 | 
					        <Box gap="200" wrap="Wrap">
 | 
				
			||||||
          <Button
 | 
					          <Button onClick={() => setInvitePrompt(true)} variant="Secondary" size="300" radii="300">
 | 
				
			||||||
            onClick={() => openInviteUser(room.roomId)}
 | 
					 | 
				
			||||||
            variant="Secondary"
 | 
					 | 
				
			||||||
            size="300"
 | 
					 | 
				
			||||||
            radii="300"
 | 
					 | 
				
			||||||
          >
 | 
					 | 
				
			||||||
            <Text size="B300">Invite Member</Text>
 | 
					            <Text size="B300">Invite Member</Text>
 | 
				
			||||||
          </Button>
 | 
					          </Button>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          {invitePrompt && (
 | 
				
			||||||
 | 
					            <InviteUserPrompt room={room} requestClose={() => setInvitePrompt(false)} />
 | 
				
			||||||
 | 
					          )}
 | 
				
			||||||
          {typeof prevRoomId === 'string' &&
 | 
					          {typeof prevRoomId === 'string' &&
 | 
				
			||||||
            (mx.getRoom(prevRoomId)?.getMyMembership() === Membership.Join ? (
 | 
					            (mx.getRoom(prevRoomId)?.getMyMembership() === Membership.Join ? (
 | 
				
			||||||
              <Button
 | 
					              <Button
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -18,7 +18,6 @@ import {
 | 
				
			||||||
import { HierarchyItem } from '../../hooks/useSpaceHierarchy';
 | 
					import { HierarchyItem } from '../../hooks/useSpaceHierarchy';
 | 
				
			||||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
 | 
					import { useMatrixClient } from '../../hooks/useMatrixClient';
 | 
				
			||||||
import { MSpaceChildContent, StateEvent } from '../../../types/matrix/room';
 | 
					import { MSpaceChildContent, StateEvent } from '../../../types/matrix/room';
 | 
				
			||||||
import { openInviteUser } from '../../../client/action/navigation';
 | 
					 | 
				
			||||||
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
 | 
					import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
 | 
				
			||||||
import { UseStateProvider } from '../../components/UseStateProvider';
 | 
					import { UseStateProvider } from '../../components/UseStateProvider';
 | 
				
			||||||
import { LeaveSpacePrompt } from '../../components/leave-space-prompt';
 | 
					import { LeaveSpacePrompt } from '../../components/leave-space-prompt';
 | 
				
			||||||
| 
						 | 
					@ -30,6 +29,7 @@ import { useOpenSpaceSettings } from '../../state/hooks/spaceSettings';
 | 
				
			||||||
import { IPowerLevels } from '../../hooks/usePowerLevels';
 | 
					import { IPowerLevels } from '../../hooks/usePowerLevels';
 | 
				
			||||||
import { getRoomCreatorsForRoomId } from '../../hooks/useRoomCreators';
 | 
					import { getRoomCreatorsForRoomId } from '../../hooks/useRoomCreators';
 | 
				
			||||||
import { getRoomPermissionsAPI } from '../../hooks/useRoomPermissions';
 | 
					import { getRoomPermissionsAPI } from '../../hooks/useRoomPermissions';
 | 
				
			||||||
 | 
					import { InviteUserPrompt } from '../../components/invite-user-prompt';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type HierarchyItemWithParent = HierarchyItem & {
 | 
					type HierarchyItemWithParent = HierarchyItem & {
 | 
				
			||||||
  parentId: string;
 | 
					  parentId: string;
 | 
				
			||||||
| 
						 | 
					@ -126,24 +126,39 @@ function InviteMenuItem({
 | 
				
			||||||
  requestClose: () => void;
 | 
					  requestClose: () => void;
 | 
				
			||||||
  disabled?: boolean;
 | 
					  disabled?: boolean;
 | 
				
			||||||
}) {
 | 
					}) {
 | 
				
			||||||
 | 
					  const mx = useMatrixClient();
 | 
				
			||||||
 | 
					  const room = mx.getRoom(item.roomId);
 | 
				
			||||||
 | 
					  const [invitePrompt, setInvitePrompt] = useState(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const handleInvite = () => {
 | 
					  const handleInvite = () => {
 | 
				
			||||||
    openInviteUser(item.roomId);
 | 
					    setInvitePrompt(true);
 | 
				
			||||||
    requestClose();
 | 
					 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <MenuItem
 | 
					    <>
 | 
				
			||||||
      onClick={handleInvite}
 | 
					      <MenuItem
 | 
				
			||||||
      size="300"
 | 
					        onClick={handleInvite}
 | 
				
			||||||
      radii="300"
 | 
					        size="300"
 | 
				
			||||||
      variant="Primary"
 | 
					        radii="300"
 | 
				
			||||||
      fill="None"
 | 
					        variant="Primary"
 | 
				
			||||||
      disabled={disabled}
 | 
					        fill="None"
 | 
				
			||||||
    >
 | 
					        aria-pressed={invitePrompt}
 | 
				
			||||||
      <Text as="span" size="T300" truncate>
 | 
					        disabled={disabled || !room}
 | 
				
			||||||
        Invite
 | 
					      >
 | 
				
			||||||
      </Text>
 | 
					        <Text as="span" size="T300" truncate>
 | 
				
			||||||
    </MenuItem>
 | 
					          Invite
 | 
				
			||||||
 | 
					        </Text>
 | 
				
			||||||
 | 
					      </MenuItem>
 | 
				
			||||||
 | 
					      {invitePrompt && room && (
 | 
				
			||||||
 | 
					        <InviteUserPrompt
 | 
				
			||||||
 | 
					          room={room}
 | 
				
			||||||
 | 
					          requestClose={() => {
 | 
				
			||||||
 | 
					            setInvitePrompt(false);
 | 
				
			||||||
 | 
					            requestClose();
 | 
				
			||||||
 | 
					          }}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					      )}
 | 
				
			||||||
 | 
					    </>
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -26,7 +26,6 @@ import { useMatrixClient } from '../../hooks/useMatrixClient';
 | 
				
			||||||
import { RoomAvatar } from '../../components/room-avatar';
 | 
					import { RoomAvatar } from '../../components/room-avatar';
 | 
				
			||||||
import { nameInitials } from '../../utils/common';
 | 
					import { nameInitials } from '../../utils/common';
 | 
				
			||||||
import * as css from './LobbyHeader.css';
 | 
					import * as css from './LobbyHeader.css';
 | 
				
			||||||
import { openInviteUser } from '../../../client/action/navigation';
 | 
					 | 
				
			||||||
import { IPowerLevels } from '../../hooks/usePowerLevels';
 | 
					import { IPowerLevels } from '../../hooks/usePowerLevels';
 | 
				
			||||||
import { UseStateProvider } from '../../components/UseStateProvider';
 | 
					import { UseStateProvider } from '../../components/UseStateProvider';
 | 
				
			||||||
import { LeaveSpacePrompt } from '../../components/leave-space-prompt';
 | 
					import { LeaveSpacePrompt } from '../../components/leave-space-prompt';
 | 
				
			||||||
| 
						 | 
					@ -38,6 +37,7 @@ import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
 | 
				
			||||||
import { useOpenSpaceSettings } from '../../state/hooks/spaceSettings';
 | 
					import { useOpenSpaceSettings } from '../../state/hooks/spaceSettings';
 | 
				
			||||||
import { useRoomCreators } from '../../hooks/useRoomCreators';
 | 
					import { useRoomCreators } from '../../hooks/useRoomCreators';
 | 
				
			||||||
import { useRoomPermissions } from '../../hooks/useRoomPermissions';
 | 
					import { useRoomPermissions } from '../../hooks/useRoomPermissions';
 | 
				
			||||||
 | 
					import { InviteUserPrompt } from '../../components/invite-user-prompt';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type LobbyMenuProps = {
 | 
					type LobbyMenuProps = {
 | 
				
			||||||
  powerLevels: IPowerLevels;
 | 
					  powerLevels: IPowerLevels;
 | 
				
			||||||
| 
						 | 
					@ -53,9 +53,10 @@ const LobbyMenu = forwardRef<HTMLDivElement, LobbyMenuProps>(
 | 
				
			||||||
    const canInvite = permissions.action('invite', mx.getSafeUserId());
 | 
					    const canInvite = permissions.action('invite', mx.getSafeUserId());
 | 
				
			||||||
    const openSpaceSettings = useOpenSpaceSettings();
 | 
					    const openSpaceSettings = useOpenSpaceSettings();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const [invitePrompt, setInvitePrompt] = useState(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const handleInvite = () => {
 | 
					    const handleInvite = () => {
 | 
				
			||||||
      openInviteUser(space.roomId);
 | 
					      setInvitePrompt(true);
 | 
				
			||||||
      requestClose();
 | 
					 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const handleRoomSettings = () => {
 | 
					    const handleRoomSettings = () => {
 | 
				
			||||||
| 
						 | 
					@ -65,6 +66,15 @@ const LobbyMenu = forwardRef<HTMLDivElement, LobbyMenuProps>(
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
      <Menu ref={ref} style={{ maxWidth: toRem(160), width: '100vw' }}>
 | 
					      <Menu ref={ref} style={{ maxWidth: toRem(160), width: '100vw' }}>
 | 
				
			||||||
 | 
					        {invitePrompt && (
 | 
				
			||||||
 | 
					          <InviteUserPrompt
 | 
				
			||||||
 | 
					            room={space}
 | 
				
			||||||
 | 
					            requestClose={() => {
 | 
				
			||||||
 | 
					              setInvitePrompt(false);
 | 
				
			||||||
 | 
					              requestClose();
 | 
				
			||||||
 | 
					            }}
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					        )}
 | 
				
			||||||
        <Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
 | 
					        <Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
 | 
				
			||||||
          <MenuItem
 | 
					          <MenuItem
 | 
				
			||||||
            onClick={handleInvite}
 | 
					            onClick={handleInvite}
 | 
				
			||||||
| 
						 | 
					@ -73,6 +83,7 @@ const LobbyMenu = forwardRef<HTMLDivElement, LobbyMenuProps>(
 | 
				
			||||||
            size="300"
 | 
					            size="300"
 | 
				
			||||||
            after={<Icon size="100" src={Icons.UserPlus} />}
 | 
					            after={<Icon size="100" src={Icons.UserPlus} />}
 | 
				
			||||||
            radii="300"
 | 
					            radii="300"
 | 
				
			||||||
 | 
					            aria-pressed={invitePrompt}
 | 
				
			||||||
            disabled={!canInvite}
 | 
					            disabled={!canInvite}
 | 
				
			||||||
          >
 | 
					          >
 | 
				
			||||||
            <Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
 | 
					            <Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -30,7 +30,6 @@ import { roomToUnreadAtom } from '../../state/room/roomToUnread';
 | 
				
			||||||
import { usePowerLevels } from '../../hooks/usePowerLevels';
 | 
					import { usePowerLevels } from '../../hooks/usePowerLevels';
 | 
				
			||||||
import { copyToClipboard } from '../../utils/dom';
 | 
					import { copyToClipboard } from '../../utils/dom';
 | 
				
			||||||
import { markAsRead } from '../../../client/action/notifications';
 | 
					import { markAsRead } from '../../../client/action/notifications';
 | 
				
			||||||
import { openInviteUser } from '../../../client/action/navigation';
 | 
					 | 
				
			||||||
import { UseStateProvider } from '../../components/UseStateProvider';
 | 
					import { UseStateProvider } from '../../components/UseStateProvider';
 | 
				
			||||||
import { LeaveRoomPrompt } from '../../components/leave-room-prompt';
 | 
					import { LeaveRoomPrompt } from '../../components/leave-room-prompt';
 | 
				
			||||||
import { useRoomTypingMember } from '../../hooks/useRoomTypingMembers';
 | 
					import { useRoomTypingMember } from '../../hooks/useRoomTypingMembers';
 | 
				
			||||||
| 
						 | 
					@ -51,6 +50,7 @@ import {
 | 
				
			||||||
import { RoomNotificationModeSwitcher } from '../../components/RoomNotificationSwitcher';
 | 
					import { RoomNotificationModeSwitcher } from '../../components/RoomNotificationSwitcher';
 | 
				
			||||||
import { useRoomCreators } from '../../hooks/useRoomCreators';
 | 
					import { useRoomCreators } from '../../hooks/useRoomCreators';
 | 
				
			||||||
import { useRoomPermissions } from '../../hooks/useRoomPermissions';
 | 
					import { useRoomPermissions } from '../../hooks/useRoomPermissions';
 | 
				
			||||||
 | 
					import { InviteUserPrompt } from '../../components/invite-user-prompt';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type RoomNavItemMenuProps = {
 | 
					type RoomNavItemMenuProps = {
 | 
				
			||||||
  room: Room;
 | 
					  room: Room;
 | 
				
			||||||
| 
						 | 
					@ -70,14 +70,15 @@ const RoomNavItemMenu = forwardRef<HTMLDivElement, RoomNavItemMenuProps>(
 | 
				
			||||||
    const openRoomSettings = useOpenRoomSettings();
 | 
					    const openRoomSettings = useOpenRoomSettings();
 | 
				
			||||||
    const space = useSpaceOptionally();
 | 
					    const space = useSpaceOptionally();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const [invitePrompt, setInvitePrompt] = useState(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const handleMarkAsRead = () => {
 | 
					    const handleMarkAsRead = () => {
 | 
				
			||||||
      markAsRead(mx, room.roomId, hideActivity);
 | 
					      markAsRead(mx, room.roomId, hideActivity);
 | 
				
			||||||
      requestClose();
 | 
					      requestClose();
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const handleInvite = () => {
 | 
					    const handleInvite = () => {
 | 
				
			||||||
      openInviteUser(room.roomId);
 | 
					      setInvitePrompt(true);
 | 
				
			||||||
      requestClose();
 | 
					 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const handleCopyLink = () => {
 | 
					    const handleCopyLink = () => {
 | 
				
			||||||
| 
						 | 
					@ -94,6 +95,15 @@ const RoomNavItemMenu = forwardRef<HTMLDivElement, RoomNavItemMenuProps>(
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
      <Menu ref={ref} style={{ maxWidth: toRem(160), width: '100vw' }}>
 | 
					      <Menu ref={ref} style={{ maxWidth: toRem(160), width: '100vw' }}>
 | 
				
			||||||
 | 
					        {invitePrompt && room && (
 | 
				
			||||||
 | 
					          <InviteUserPrompt
 | 
				
			||||||
 | 
					            room={room}
 | 
				
			||||||
 | 
					            requestClose={() => {
 | 
				
			||||||
 | 
					              setInvitePrompt(false);
 | 
				
			||||||
 | 
					              requestClose();
 | 
				
			||||||
 | 
					            }}
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					        )}
 | 
				
			||||||
        <Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
 | 
					        <Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
 | 
				
			||||||
          <MenuItem
 | 
					          <MenuItem
 | 
				
			||||||
            onClick={handleMarkAsRead}
 | 
					            onClick={handleMarkAsRead}
 | 
				
			||||||
| 
						 | 
					@ -137,6 +147,7 @@ const RoomNavItemMenu = forwardRef<HTMLDivElement, RoomNavItemMenuProps>(
 | 
				
			||||||
            size="300"
 | 
					            size="300"
 | 
				
			||||||
            after={<Icon size="100" src={Icons.UserPlus} />}
 | 
					            after={<Icon size="100" src={Icons.UserPlus} />}
 | 
				
			||||||
            radii="300"
 | 
					            radii="300"
 | 
				
			||||||
 | 
					            aria-pressed={invitePrompt}
 | 
				
			||||||
            disabled={!canInvite}
 | 
					            disabled={!canInvite}
 | 
				
			||||||
          >
 | 
					          >
 | 
				
			||||||
            <Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
 | 
					            <Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -45,7 +45,6 @@ import { useRoomUnread } from '../../state/hooks/unread';
 | 
				
			||||||
import { usePowerLevelsContext } from '../../hooks/usePowerLevels';
 | 
					import { usePowerLevelsContext } from '../../hooks/usePowerLevels';
 | 
				
			||||||
import { markAsRead } from '../../../client/action/notifications';
 | 
					import { markAsRead } from '../../../client/action/notifications';
 | 
				
			||||||
import { roomToUnreadAtom } from '../../state/room/roomToUnread';
 | 
					import { roomToUnreadAtom } from '../../state/room/roomToUnread';
 | 
				
			||||||
import { openInviteUser } from '../../../client/action/navigation';
 | 
					 | 
				
			||||||
import { copyToClipboard } from '../../utils/dom';
 | 
					import { copyToClipboard } from '../../utils/dom';
 | 
				
			||||||
import { LeaveRoomPrompt } from '../../components/leave-room-prompt';
 | 
					import { LeaveRoomPrompt } from '../../components/leave-room-prompt';
 | 
				
			||||||
import { useRoomAvatar, useRoomName, useRoomTopic } from '../../hooks/useRoomMeta';
 | 
					import { useRoomAvatar, useRoomName, useRoomTopic } from '../../hooks/useRoomMeta';
 | 
				
			||||||
| 
						 | 
					@ -69,6 +68,7 @@ import { JumpToTime } from './jump-to-time';
 | 
				
			||||||
import { useRoomNavigate } from '../../hooks/useRoomNavigate';
 | 
					import { useRoomNavigate } from '../../hooks/useRoomNavigate';
 | 
				
			||||||
import { useRoomCreators } from '../../hooks/useRoomCreators';
 | 
					import { useRoomCreators } from '../../hooks/useRoomCreators';
 | 
				
			||||||
import { useRoomPermissions } from '../../hooks/useRoomPermissions';
 | 
					import { useRoomPermissions } from '../../hooks/useRoomPermissions';
 | 
				
			||||||
 | 
					import { InviteUserPrompt } from '../../components/invite-user-prompt';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type RoomMenuProps = {
 | 
					type RoomMenuProps = {
 | 
				
			||||||
  room: Room;
 | 
					  room: Room;
 | 
				
			||||||
| 
						 | 
					@ -87,14 +87,15 @@ const RoomMenu = forwardRef<HTMLDivElement, RoomMenuProps>(({ room, requestClose
 | 
				
			||||||
  const notificationMode = getRoomNotificationMode(notificationPreferences, room.roomId);
 | 
					  const notificationMode = getRoomNotificationMode(notificationPreferences, room.roomId);
 | 
				
			||||||
  const { navigateRoom } = useRoomNavigate();
 | 
					  const { navigateRoom } = useRoomNavigate();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const [invitePrompt, setInvitePrompt] = useState(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const handleMarkAsRead = () => {
 | 
					  const handleMarkAsRead = () => {
 | 
				
			||||||
    markAsRead(mx, room.roomId, hideActivity);
 | 
					    markAsRead(mx, room.roomId, hideActivity);
 | 
				
			||||||
    requestClose();
 | 
					    requestClose();
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const handleInvite = () => {
 | 
					  const handleInvite = () => {
 | 
				
			||||||
    openInviteUser(room.roomId);
 | 
					    setInvitePrompt(true);
 | 
				
			||||||
    requestClose();
 | 
					 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const handleCopyLink = () => {
 | 
					  const handleCopyLink = () => {
 | 
				
			||||||
| 
						 | 
					@ -113,6 +114,15 @@ const RoomMenu = forwardRef<HTMLDivElement, RoomMenuProps>(({ room, requestClose
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <Menu ref={ref} style={{ maxWidth: toRem(160), width: '100vw' }}>
 | 
					    <Menu ref={ref} style={{ maxWidth: toRem(160), width: '100vw' }}>
 | 
				
			||||||
 | 
					      {invitePrompt && (
 | 
				
			||||||
 | 
					        <InviteUserPrompt
 | 
				
			||||||
 | 
					          room={room}
 | 
				
			||||||
 | 
					          requestClose={() => {
 | 
				
			||||||
 | 
					            setInvitePrompt(false);
 | 
				
			||||||
 | 
					            requestClose();
 | 
				
			||||||
 | 
					          }}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					      )}
 | 
				
			||||||
      <Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
 | 
					      <Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
 | 
				
			||||||
        <MenuItem
 | 
					        <MenuItem
 | 
				
			||||||
          onClick={handleMarkAsRead}
 | 
					          onClick={handleMarkAsRead}
 | 
				
			||||||
| 
						 | 
					@ -156,6 +166,7 @@ const RoomMenu = forwardRef<HTMLDivElement, RoomMenuProps>(({ room, requestClose
 | 
				
			||||||
          size="300"
 | 
					          size="300"
 | 
				
			||||||
          after={<Icon size="100" src={Icons.UserPlus} />}
 | 
					          after={<Icon size="100" src={Icons.UserPlus} />}
 | 
				
			||||||
          radii="300"
 | 
					          radii="300"
 | 
				
			||||||
 | 
					          aria-pressed={invitePrompt}
 | 
				
			||||||
          disabled={!canInvite}
 | 
					          disabled={!canInvite}
 | 
				
			||||||
        >
 | 
					        >
 | 
				
			||||||
          <Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
 | 
					          <Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -81,6 +81,7 @@ type InviteData = {
 | 
				
			||||||
  senderId: string;
 | 
					  senderId: string;
 | 
				
			||||||
  senderName: string;
 | 
					  senderName: string;
 | 
				
			||||||
  inviteTs?: number;
 | 
					  inviteTs?: number;
 | 
				
			||||||
 | 
					  reason?: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  isSpace: boolean;
 | 
					  isSpace: boolean;
 | 
				
			||||||
  isDirect: boolean;
 | 
					  isDirect: boolean;
 | 
				
			||||||
| 
						 | 
					@ -102,11 +103,17 @@ const makeInviteData = (mx: MatrixClient, room: Room, useAuthentication: boolean
 | 
				
			||||||
  const member = room.getMember(userId);
 | 
					  const member = room.getMember(userId);
 | 
				
			||||||
  const memberEvent = member?.events.member;
 | 
					  const memberEvent = member?.events.member;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const content = memberEvent?.getContent();
 | 
				
			||||||
  const senderId = memberEvent?.getSender();
 | 
					  const senderId = memberEvent?.getSender();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const senderName = senderId
 | 
					  const senderName = senderId
 | 
				
			||||||
    ? getMemberDisplayName(room, senderId) ?? getMxIdLocalPart(senderId) ?? senderId
 | 
					    ? getMemberDisplayName(room, senderId) ?? getMxIdLocalPart(senderId) ?? senderId
 | 
				
			||||||
    : undefined;
 | 
					    : undefined;
 | 
				
			||||||
  const inviteTs = memberEvent?.getTs() ?? 0;
 | 
					  const inviteTs = memberEvent?.getTs();
 | 
				
			||||||
 | 
					  const reason =
 | 
				
			||||||
 | 
					    content && 'reason' in content && typeof content.reason === 'string'
 | 
				
			||||||
 | 
					      ? content.reason
 | 
				
			||||||
 | 
					      : undefined;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return {
 | 
					  return {
 | 
				
			||||||
    room,
 | 
					    room,
 | 
				
			||||||
| 
						 | 
					@ -119,6 +126,7 @@ const makeInviteData = (mx: MatrixClient, room: Room, useAuthentication: boolean
 | 
				
			||||||
    senderId: senderId ?? 'Unknown',
 | 
					    senderId: senderId ?? 'Unknown',
 | 
				
			||||||
    senderName: senderName ?? 'Unknown',
 | 
					    senderName: senderName ?? 'Unknown',
 | 
				
			||||||
    inviteTs,
 | 
					    inviteTs,
 | 
				
			||||||
 | 
					    reason,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    isSpace: isSpace(room),
 | 
					    isSpace: isSpace(room),
 | 
				
			||||||
    isDirect: direct,
 | 
					    isDirect: direct,
 | 
				
			||||||
| 
						 | 
					@ -130,7 +138,8 @@ const hasBadWords = (invite: InviteData): boolean =>
 | 
				
			||||||
  testBadWords(invite.roomName) ||
 | 
					  testBadWords(invite.roomName) ||
 | 
				
			||||||
  testBadWords(invite.roomTopic ?? '') ||
 | 
					  testBadWords(invite.roomTopic ?? '') ||
 | 
				
			||||||
  testBadWords(invite.senderName) ||
 | 
					  testBadWords(invite.senderName) ||
 | 
				
			||||||
  testBadWords(invite.senderId);
 | 
					  testBadWords(invite.senderId) ||
 | 
				
			||||||
 | 
					  testBadWords(invite.reason || '');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type NavigateHandler = (roomId: string, space: boolean) => void;
 | 
					type NavigateHandler = (roomId: string, space: boolean) => void;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -184,7 +193,7 @@ function InviteCard({
 | 
				
			||||||
      variant="SurfaceVariant"
 | 
					      variant="SurfaceVariant"
 | 
				
			||||||
      direction="Column"
 | 
					      direction="Column"
 | 
				
			||||||
      gap="300"
 | 
					      gap="300"
 | 
				
			||||||
      style={{ padding: `${config.space.S400} ${config.space.S400} ${config.space.S200}` }}
 | 
					      style={{ padding: config.space.S400 }}
 | 
				
			||||||
    >
 | 
					    >
 | 
				
			||||||
      {(invite.isEncrypted || invite.isDirect || invite.isSpace) && (
 | 
					      {(invite.isEncrypted || invite.isDirect || invite.isSpace) && (
 | 
				
			||||||
        <Box gap="200" alignItems="Center">
 | 
					        <Box gap="200" alignItems="Center">
 | 
				
			||||||
| 
						 | 
					@ -298,22 +307,29 @@ function InviteCard({
 | 
				
			||||||
          </Box>
 | 
					          </Box>
 | 
				
			||||||
        </Box>
 | 
					        </Box>
 | 
				
			||||||
      </Box>
 | 
					      </Box>
 | 
				
			||||||
      <Box gap="200" alignItems="Baseline">
 | 
					      <Box direction="Column">
 | 
				
			||||||
        <Box grow="Yes">
 | 
					        <Box gap="200" alignItems="Baseline">
 | 
				
			||||||
          <Text size="T200" priority="300">
 | 
					          <Box grow="Yes">
 | 
				
			||||||
            From: <b>{invite.senderId}</b>
 | 
					            <Text size="T200" priority="300">
 | 
				
			||||||
          </Text>
 | 
					              From: <b>{invite.senderId}</b>
 | 
				
			||||||
        </Box>
 | 
					            </Text>
 | 
				
			||||||
        {invite.inviteTs && (
 | 
					 | 
				
			||||||
          <Box shrink="No">
 | 
					 | 
				
			||||||
            <Time
 | 
					 | 
				
			||||||
              size="T200"
 | 
					 | 
				
			||||||
              ts={invite.inviteTs}
 | 
					 | 
				
			||||||
              hour24Clock={hour24Clock}
 | 
					 | 
				
			||||||
              dateFormatString={dateFormatString}
 | 
					 | 
				
			||||||
              priority="300"
 | 
					 | 
				
			||||||
            />
 | 
					 | 
				
			||||||
          </Box>
 | 
					          </Box>
 | 
				
			||||||
 | 
					          {typeof invite.inviteTs === 'number' && invite.inviteTs !== 0 && (
 | 
				
			||||||
 | 
					            <Box shrink="No">
 | 
				
			||||||
 | 
					              <Time
 | 
				
			||||||
 | 
					                size="T200"
 | 
				
			||||||
 | 
					                ts={invite.inviteTs}
 | 
				
			||||||
 | 
					                hour24Clock={hour24Clock}
 | 
				
			||||||
 | 
					                dateFormatString={dateFormatString}
 | 
				
			||||||
 | 
					                priority="300"
 | 
				
			||||||
 | 
					              />
 | 
				
			||||||
 | 
					            </Box>
 | 
				
			||||||
 | 
					          )}
 | 
				
			||||||
 | 
					        </Box>
 | 
				
			||||||
 | 
					        {invite.reason && (
 | 
				
			||||||
 | 
					          <Text size="T200" priority="300">
 | 
				
			||||||
 | 
					            Reason: {invite.reason}
 | 
				
			||||||
 | 
					          </Text>
 | 
				
			||||||
        )}
 | 
					        )}
 | 
				
			||||||
      </Box>
 | 
					      </Box>
 | 
				
			||||||
    </SequenceCard>
 | 
					    </SequenceCard>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -82,7 +82,6 @@ import { useRoomsUnread } from '../../../state/hooks/unread';
 | 
				
			||||||
import { roomToUnreadAtom } from '../../../state/room/roomToUnread';
 | 
					import { roomToUnreadAtom } from '../../../state/room/roomToUnread';
 | 
				
			||||||
import { markAsRead } from '../../../../client/action/notifications';
 | 
					import { markAsRead } from '../../../../client/action/notifications';
 | 
				
			||||||
import { copyToClipboard } from '../../../utils/dom';
 | 
					import { copyToClipboard } from '../../../utils/dom';
 | 
				
			||||||
import { openInviteUser } from '../../../../client/action/navigation';
 | 
					 | 
				
			||||||
import { stopPropagation } from '../../../utils/keyboard';
 | 
					import { stopPropagation } from '../../../utils/keyboard';
 | 
				
			||||||
import { getMatrixToRoom } from '../../../plugins/matrix-to';
 | 
					import { getMatrixToRoom } from '../../../plugins/matrix-to';
 | 
				
			||||||
import { getViaServers } from '../../../plugins/via-servers';
 | 
					import { getViaServers } from '../../../plugins/via-servers';
 | 
				
			||||||
| 
						 | 
					@ -93,6 +92,7 @@ import { settingsAtom } from '../../../state/settings';
 | 
				
			||||||
import { useOpenSpaceSettings } from '../../../state/hooks/spaceSettings';
 | 
					import { useOpenSpaceSettings } from '../../../state/hooks/spaceSettings';
 | 
				
			||||||
import { useRoomCreators } from '../../../hooks/useRoomCreators';
 | 
					import { useRoomCreators } from '../../../hooks/useRoomCreators';
 | 
				
			||||||
import { useRoomPermissions } from '../../../hooks/useRoomPermissions';
 | 
					import { useRoomPermissions } from '../../../hooks/useRoomPermissions';
 | 
				
			||||||
 | 
					import { InviteUserPrompt } from '../../../components/invite-user-prompt';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type SpaceMenuProps = {
 | 
					type SpaceMenuProps = {
 | 
				
			||||||
  room: Room;
 | 
					  room: Room;
 | 
				
			||||||
| 
						 | 
					@ -111,6 +111,8 @@ const SpaceMenu = forwardRef<HTMLDivElement, SpaceMenuProps>(
 | 
				
			||||||
    const canInvite = permissions.action('invite', mx.getSafeUserId());
 | 
					    const canInvite = permissions.action('invite', mx.getSafeUserId());
 | 
				
			||||||
    const openSpaceSettings = useOpenSpaceSettings();
 | 
					    const openSpaceSettings = useOpenSpaceSettings();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const [invitePrompt, setInvitePrompt] = useState(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const allChild = useSpaceChildren(
 | 
					    const allChild = useSpaceChildren(
 | 
				
			||||||
      allRoomsAtom,
 | 
					      allRoomsAtom,
 | 
				
			||||||
      room.roomId,
 | 
					      room.roomId,
 | 
				
			||||||
| 
						 | 
					@ -136,8 +138,7 @@ const SpaceMenu = forwardRef<HTMLDivElement, SpaceMenuProps>(
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const handleInvite = () => {
 | 
					    const handleInvite = () => {
 | 
				
			||||||
      openInviteUser(room.roomId);
 | 
					      setInvitePrompt(true);
 | 
				
			||||||
      requestClose();
 | 
					 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const handleRoomSettings = () => {
 | 
					    const handleRoomSettings = () => {
 | 
				
			||||||
| 
						 | 
					@ -147,6 +148,15 @@ const SpaceMenu = forwardRef<HTMLDivElement, SpaceMenuProps>(
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
      <Menu ref={ref} style={{ maxWidth: toRem(160), width: '100vw' }}>
 | 
					      <Menu ref={ref} style={{ maxWidth: toRem(160), width: '100vw' }}>
 | 
				
			||||||
 | 
					        {invitePrompt && room && (
 | 
				
			||||||
 | 
					          <InviteUserPrompt
 | 
				
			||||||
 | 
					            room={room}
 | 
				
			||||||
 | 
					            requestClose={() => {
 | 
				
			||||||
 | 
					              setInvitePrompt(false);
 | 
				
			||||||
 | 
					              requestClose();
 | 
				
			||||||
 | 
					            }}
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					        )}
 | 
				
			||||||
        <Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
 | 
					        <Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
 | 
				
			||||||
          <MenuItem
 | 
					          <MenuItem
 | 
				
			||||||
            onClick={handleMarkAsRead}
 | 
					            onClick={handleMarkAsRead}
 | 
				
			||||||
| 
						 | 
					@ -181,6 +191,7 @@ const SpaceMenu = forwardRef<HTMLDivElement, SpaceMenuProps>(
 | 
				
			||||||
            size="300"
 | 
					            size="300"
 | 
				
			||||||
            after={<Icon size="100" src={Icons.UserPlus} />}
 | 
					            after={<Icon size="100" src={Icons.UserPlus} />}
 | 
				
			||||||
            radii="300"
 | 
					            radii="300"
 | 
				
			||||||
 | 
					            aria-pressed={invitePrompt}
 | 
				
			||||||
            disabled={!canInvite}
 | 
					            disabled={!canInvite}
 | 
				
			||||||
          >
 | 
					          >
 | 
				
			||||||
            <Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
 | 
					            <Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -57,7 +57,6 @@ import { useSpaceJoinedHierarchy } from '../../../hooks/useSpaceHierarchy';
 | 
				
			||||||
import { allRoomsAtom } from '../../../state/room-list/roomList';
 | 
					import { allRoomsAtom } from '../../../state/room-list/roomList';
 | 
				
			||||||
import { PageNav, PageNavContent, PageNavHeader } from '../../../components/page';
 | 
					import { PageNav, PageNavContent, PageNavHeader } from '../../../components/page';
 | 
				
			||||||
import { usePowerLevels } from '../../../hooks/usePowerLevels';
 | 
					import { usePowerLevels } from '../../../hooks/usePowerLevels';
 | 
				
			||||||
import { openInviteUser } from '../../../../client/action/navigation';
 | 
					 | 
				
			||||||
import { useRecursiveChildScopeFactory, useSpaceChildren } from '../../../state/hooks/roomList';
 | 
					import { useRecursiveChildScopeFactory, useSpaceChildren } from '../../../state/hooks/roomList';
 | 
				
			||||||
import { roomToParentsAtom } from '../../../state/room/roomToParents';
 | 
					import { roomToParentsAtom } from '../../../state/room/roomToParents';
 | 
				
			||||||
import { markAsRead } from '../../../../client/action/notifications';
 | 
					import { markAsRead } from '../../../../client/action/notifications';
 | 
				
			||||||
| 
						 | 
					@ -84,6 +83,7 @@ import { useRoomPermissions } from '../../../hooks/useRoomPermissions';
 | 
				
			||||||
import { ContainerColor } from '../../../styles/ContainerColor.css';
 | 
					import { ContainerColor } from '../../../styles/ContainerColor.css';
 | 
				
			||||||
import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
 | 
					import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
 | 
				
			||||||
import { BreakWord } from '../../../styles/Text.css';
 | 
					import { BreakWord } from '../../../styles/Text.css';
 | 
				
			||||||
 | 
					import { InviteUserPrompt } from '../../../components/invite-user-prompt';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type SpaceMenuProps = {
 | 
					type SpaceMenuProps = {
 | 
				
			||||||
  room: Room;
 | 
					  room: Room;
 | 
				
			||||||
| 
						 | 
					@ -102,6 +102,8 @@ const SpaceMenu = forwardRef<HTMLDivElement, SpaceMenuProps>(({ room, requestClo
 | 
				
			||||||
  const openSpaceSettings = useOpenSpaceSettings();
 | 
					  const openSpaceSettings = useOpenSpaceSettings();
 | 
				
			||||||
  const { navigateRoom } = useRoomNavigate();
 | 
					  const { navigateRoom } = useRoomNavigate();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const [invitePrompt, setInvitePrompt] = useState(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const allChild = useSpaceChildren(
 | 
					  const allChild = useSpaceChildren(
 | 
				
			||||||
    allRoomsAtom,
 | 
					    allRoomsAtom,
 | 
				
			||||||
    room.roomId,
 | 
					    room.roomId,
 | 
				
			||||||
| 
						 | 
					@ -122,8 +124,7 @@ const SpaceMenu = forwardRef<HTMLDivElement, SpaceMenuProps>(({ room, requestClo
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const handleInvite = () => {
 | 
					  const handleInvite = () => {
 | 
				
			||||||
    openInviteUser(room.roomId);
 | 
					    setInvitePrompt(true);
 | 
				
			||||||
    requestClose();
 | 
					 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const handleRoomSettings = () => {
 | 
					  const handleRoomSettings = () => {
 | 
				
			||||||
| 
						 | 
					@ -139,6 +140,15 @@ const SpaceMenu = forwardRef<HTMLDivElement, SpaceMenuProps>(({ room, requestClo
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <Menu ref={ref} style={{ maxWidth: toRem(160), width: '100vw' }}>
 | 
					    <Menu ref={ref} style={{ maxWidth: toRem(160), width: '100vw' }}>
 | 
				
			||||||
      <Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
 | 
					      <Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
 | 
				
			||||||
 | 
					        {invitePrompt && room && (
 | 
				
			||||||
 | 
					          <InviteUserPrompt
 | 
				
			||||||
 | 
					            room={room}
 | 
				
			||||||
 | 
					            requestClose={() => {
 | 
				
			||||||
 | 
					              setInvitePrompt(false);
 | 
				
			||||||
 | 
					              requestClose();
 | 
				
			||||||
 | 
					            }}
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					        )}
 | 
				
			||||||
        <MenuItem
 | 
					        <MenuItem
 | 
				
			||||||
          onClick={handleMarkAsRead}
 | 
					          onClick={handleMarkAsRead}
 | 
				
			||||||
          size="300"
 | 
					          size="300"
 | 
				
			||||||
| 
						 | 
					@ -160,6 +170,7 @@ const SpaceMenu = forwardRef<HTMLDivElement, SpaceMenuProps>(({ room, requestClo
 | 
				
			||||||
          size="300"
 | 
					          size="300"
 | 
				
			||||||
          after={<Icon size="100" src={Icons.UserPlus} />}
 | 
					          after={<Icon size="100" src={Icons.UserPlus} />}
 | 
				
			||||||
          radii="300"
 | 
					          radii="300"
 | 
				
			||||||
 | 
					          aria-pressed={invitePrompt}
 | 
				
			||||||
          disabled={!canInvite}
 | 
					          disabled={!canInvite}
 | 
				
			||||||
        >
 | 
					        >
 | 
				
			||||||
          <Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
 | 
					          <Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue