// Action surfaces rendered alongside the user-profile card after // the Dawn redesign: // // - `UserActionsMenu` — single 3-dot pill anchored to the // top-right of the card. Hosts every imperative action we have // (start DM in groups, block / unblock, kick / ban / invite for // mods). Replaces the old bottom actions row, which on mobile // overflowed past the rail height — the user couldn't reach // Block without discovering a hidden scroll. // - `IgnoredUserAlert` — banner shown when the viewer has the // profile target on their ignore list. Pure information, no // action of its own. // // The legacy chip-strip components (Server / Share / MutualRooms) // were lifted into `UserInfoRows.tsx` as Fleet-style attribute rows // and are intentionally not re-exported here. import React, { MouseEventHandler, useCallback, useState } from 'react'; import FocusTrap from 'focus-trap-react'; import { isKeyHotkey } from 'is-hotkey'; import { Box, Icon, IconButton, Icons, Line, Menu, MenuItem, PopOut, RectCords, Spinner, Text, config, toRem, } from 'folds'; import { useTranslation } from 'react-i18next'; import { useMatrixClient } from '../../hooks/useMatrixClient'; import { stopPropagation } from '../../utils/keyboard'; import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback'; import { useIgnoredUsers } from '../../hooks/useIgnoredUsers'; import { CutoutCard } from '../cutout-card'; import { SettingTile } from '../setting-tile'; import { UserModeration } from './UserModeration'; export function IgnoredUserAlert() { const { t } = useTranslation(); return ( {t('User.blocked_title')} {t('User.blocked_description')} ); } type UserActionsMenuProps = { userId: string; // Whether to surface «Написать» (start a private DM) — true only // in group rooms when the target isn't the viewer themselves; in // 1:1 the user is already in the conversation, so the action is a // no-op. showStartDm?: boolean; onStartDm?: () => void; // Room-moderation gates. Each is independently true/false; if all // three are false the moderation block is suppressed entirely so // the menu doesn't carry a dangling separator. canKick?: boolean; canBan?: boolean; canInvite?: boolean; }; // 3-dot pill containing every imperative action for the profile // target. Anchored top-right of the card by the parent layout. // // Block / Unblock lives here too — earlier I tried promoting it to // a standalone chip in a bottom actions row, but on mobile the rail // is ~42vh and that row scrolled below the visible rail edge, so // users couldn't reach it. Putting Block back into the menu keeps // the action accessible at the same vertical position regardless of // content length. export function UserActionsMenu({ userId, showStartDm, onStartDm, canKick, canBan, canInvite, }: UserActionsMenuProps) { const { t } = useTranslation(); const mx = useMatrixClient(); const [cords, setCords] = useState(); const open: MouseEventHandler = (evt) => { // Anchor the popout to the trigger's TOP edge with zero height // — `position="Bottom"` then drops the menu starting from that // edge, so it visually covers the 3-dot button instead of // floating below it. Looks tighter on mobile where space is // tight; the trigger naturally re-emerges when the menu closes. const rect = evt.currentTarget.getBoundingClientRect(); setCords({ x: rect.x, y: rect.y, width: rect.width, height: 0 }); }; const close = () => setCords(undefined); const ignoredUsers = useIgnoredUsers(); const ignored = ignoredUsers.includes(userId); const [ignoreState, toggleIgnore] = useAsyncCallback( useCallback(async () => { const next = ignoredUsers.filter((u) => u !== userId); if (!ignored) next.push(userId); await mx.setIgnoredUsers(next); }, [mx, ignoredUsers, userId, ignored]) ); const ignoring = ignoreState.status === AsyncStatus.Loading; const moderationVisible = !!(canKick || canBan || canInvite); return ( isKeyHotkey('arrowdown', evt), isKeyBackward: (evt: KeyboardEvent) => isKeyHotkey('arrowup', evt), }} > {showStartDm && onStartDm && ( { onStartDm(); close(); }} before={} > {t('User.message')} )} {showStartDm && onStartDm && } { toggleIgnore(); close(); }} before={ ignoring ? ( ) : ( ) } disabled={ignoring} > {ignored ? t('User.unblock') : t('User.block')} {moderationVisible && ( <> > )} } > ); }