diff --git a/src/app/components/invite-user-prompt/InviteUserPrompt.tsx b/src/app/components/invite-user-prompt/InviteUserPrompt.tsx new file mode 100644 index 00000000..82313c3e --- /dev/null +++ b/src/app/components/invite-user-prompt/InviteUserPrompt.tsx @@ -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(null); + const directUsers = useDirectUsers(); + const [validUserId, setValidUserId] = useState(); + + 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( + 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 = (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 = (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 = (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 ( + }> + + inputRef.current, + clickOutsideDeactivates: true, + onDeactivate: requestClose, + escapeDeactivates: stopPropagation, + }} + > + + +
+ + + Invite + + + + + + + +
+ + + User ID +
+ + {result && result.items.length > 0 && ( + isKeyHotkey('arrowdown', evt), + isKeyBackward: (evt: KeyboardEvent) => isKeyHotkey('arrowup', evt), + escapeDeactivates: stopPropagation, + }} + > + + + +
+ {result.items.map((userId) => { + const username = `${getMxIdLocalPart(userId)}`; + const userServer = getMxIdServer(userId); + + return ( + handleUserId(userId)} + after={ + + {userServer} + + } + disabled={inviting} + > + + + + {queryHighlighRegex + ? highlightText(queryHighlighRegex, [ + username ?? userId, + ]) + : username} + + + + + ); + })} +
+
+
+
+
+ )} +
+
+ + Reason (Optional) +