Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions examples/vite/src/AppSettings/ActionsMenu/ActionsMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ import {
AttachmentPromptDialog,
attachmentPromptDialogId,
} from './AttachmentPromptDialog';
import {
WebSocketEventPromptDialog,
webSocketEventPromptDialogId,
} from './WebSocketEventPromptDialog';

const actionsMenuDialogId = 'app-actions-menu';

Expand Down Expand Up @@ -91,6 +95,20 @@ function TriggerAttachmentAction({ onTrigger }: { onTrigger: () => void }) {
);
}

function TriggerWebSocketEventAction({ onTrigger }: { onTrigger: () => void }) {
const { closeMenu } = useContextMenuContext();

return (
<ContextMenuButton
label='Trigger WS Event'
onClick={() => {
closeMenu();
onTrigger();
}}
/>
);
}

const ActionsMenuInner = ({ iconOnly }: { iconOnly: boolean }) => {
const [menuButtonElement, setMenuButtonElement] = useState<HTMLButtonElement | null>(
null,
Expand All @@ -104,6 +122,9 @@ const ActionsMenuInner = ({ iconOnly }: { iconOnly: boolean }) => {
const { dialog: attachmentDialog } = useDialogOnNearestManager({
id: attachmentPromptDialogId,
});
const { dialog: webSocketEventDialog } = useDialogOnNearestManager({
id: webSocketEventPromptDialogId,
});
const menuIsOpen = useDialogIsOpen(actionsMenuDialogId, dialogManager?.id);

return (
Expand All @@ -127,9 +148,11 @@ const ActionsMenuInner = ({ iconOnly }: { iconOnly: boolean }) => {
>
<TriggerNotificationAction onTrigger={notificationDialog.open} />
<TriggerAttachmentAction onTrigger={attachmentDialog.open} />
<TriggerWebSocketEventAction onTrigger={webSocketEventDialog.open} />
</ContextMenu>
<NotificationPromptDialog referenceElement={menuButtonElement} />
<AttachmentPromptDialog referenceElement={menuButtonElement} />
<WebSocketEventPromptDialog referenceElement={menuButtonElement} />
</div>
);
};
278 changes: 87 additions & 191 deletions examples/vite/src/AppSettings/ActionsMenu/AttachmentPromptDialog.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,16 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import type { PointerEvent as ReactPointerEvent } from 'react';
import { useCallback, useEffect, useState } from 'react';
import type { LocalAttachment } from 'stream-chat';
import {
DialogAnchor,
Prompt,
useChatContext,
useDialogIsOpen,
useDialogOnNearestManager,
} from 'stream-chat-react';
import { DraggableDialog } from './DraggableDialog';

export const attachmentPromptDialogId = 'app-attachment-prompt-dialog';
type AttachmentEditorTab = 'unsupported-file' | 'unsupported-object';

const VIEWPORT_MARGIN = 8;
const defaultUnsupportedAttachment = {
asset_url: 'https://example.com/unsupported.bin',
file_size: 128000,
Expand Down Expand Up @@ -43,11 +41,6 @@ const initialUnsupportedObjectValue = JSON.stringify(
2,
);

const clamp = (value: number, min: number, max: number) => {
if (max < min) return min;
return Math.min(Math.max(value, min), max);
};

export const AttachmentPromptDialog = ({
referenceElement,
}: {
Expand All @@ -61,8 +54,6 @@ export const AttachmentPromptDialog = ({
initialUnsupportedObjectValue,
);
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const [dragOffset, setDragOffset] = useState({ x: 0, y: 0 });
const shellRef = useRef<HTMLDivElement | null>(null);
const { channel } = useChatContext();
const { dialog, dialogManager } = useDialogOnNearestManager({
id: attachmentPromptDialogId,
Expand All @@ -75,41 +66,6 @@ export const AttachmentPromptDialog = ({
setUnsupportedFileInput(initialUnsupportedFileValue);
setUnsupportedObjectInput(initialUnsupportedObjectValue);
setErrorMessage(null);
setDragOffset({ x: 0, y: 0 });
}, [dialogIsOpen]);

useEffect(() => {
if (!dialogIsOpen) return;

const clampToViewport = () => {
const shell = shellRef.current;
if (!shell) return;

const rect = shell.getBoundingClientRect();
const nextLeft = clamp(
rect.left,
VIEWPORT_MARGIN,
window.innerWidth - rect.width - VIEWPORT_MARGIN,
);
const nextTop = clamp(
rect.top,
VIEWPORT_MARGIN,
window.innerHeight - rect.height - VIEWPORT_MARGIN,
);

if (nextLeft === rect.left && nextTop === rect.top) return;

setDragOffset((current) => ({
x: current.x + (nextLeft - rect.left),
y: current.y + (nextTop - rect.top),
}));
};

window.addEventListener('resize', clampToViewport);

return () => {
window.removeEventListener('resize', clampToViewport);
};
}, [dialogIsOpen]);

const closeDialog = useCallback(() => {
Expand Down Expand Up @@ -145,159 +101,99 @@ export const AttachmentPromptDialog = ({
[channel, closeDialog, unsupportedFileInput, unsupportedObjectInput],
);

const handleHeaderPointerDown = useCallback(
(event: ReactPointerEvent<HTMLDivElement>) => {
if (event.button !== 0) return;
if (!(event.target instanceof HTMLElement)) return;
if (event.target.closest('button')) return;

const shell = shellRef.current;
if (!shell) return;

event.preventDefault();

const startClientX = event.clientX;
const startClientY = event.clientY;
const startOffset = dragOffset;
const startRect = shell.getBoundingClientRect();

const handlePointerMove = (moveEvent: PointerEvent) => {
const nextLeft = clamp(
startRect.left + (moveEvent.clientX - startClientX),
VIEWPORT_MARGIN,
window.innerWidth - startRect.width - VIEWPORT_MARGIN,
);
const nextTop = clamp(
startRect.top + (moveEvent.clientY - startClientY),
VIEWPORT_MARGIN,
window.innerHeight - startRect.height - VIEWPORT_MARGIN,
);

setDragOffset({
x: startOffset.x + (nextLeft - startRect.left),
y: startOffset.y + (nextTop - startRect.top),
});
};

const handlePointerUp = () => {
window.removeEventListener('pointermove', handlePointerMove);
window.removeEventListener('pointerup', handlePointerUp);
};

window.addEventListener('pointermove', handlePointerMove);
window.addEventListener('pointerup', handlePointerUp);
},
[dragOffset],
);

const shellStyle = {
transform: `translate(${dragOffset.x}px, ${dragOffset.y}px)`,
};

return (
<DialogAnchor
allowFlip
className='app__attachment-dialog'
<DraggableDialog
dialogClassName='app__attachment-dialog'
dialogId={attachmentPromptDialogId}
dialogIsOpen={dialogIsOpen}
dialogManagerId={dialogManager?.id}
id={attachmentPromptDialogId}
placement='right-start'
dragHandleClassName='app__attachment-dialog__drag-handle'
onClose={closeDialog}
promptClassName='app__attachment-dialog__prompt'
referenceElement={referenceElement}
tabIndex={-1}
trapFocus
updatePositionOnContentResize
shellClassName='app__attachment-dialog__shell'
title='Message Composer'
>
<div className='app__attachment-dialog__shell' ref={shellRef} style={shellStyle}>
<Prompt.Root className='app__attachment-dialog__prompt'>
<Prompt.Body className='app__attachment-dialog__body'>
<div className='app__attachment-dialog__subsection'>
<h3 className='app__attachment-dialog__subsection-title'>
Attach Unsupported Attachment
</h3>
<div
className='app__attachment-dialog__drag-handle'
onPointerDown={handleHeaderPointerDown}
aria-label='Attachment type'
className='app__attachment-dialog__tabs'
role='tablist'
>
<Prompt.Header close={closeDialog} title='Message Composer' />
<button
aria-selected={activeTab === 'unsupported-file'}
className='app__attachment-dialog__tab'
onClick={() => {
setActiveTab('unsupported-file');
if (errorMessage) setErrorMessage(null);
}}
role='tab'
type='button'
>
Unsupported file
</button>
<button
aria-selected={activeTab === 'unsupported-object'}
className='app__attachment-dialog__tab'
onClick={() => {
setActiveTab('unsupported-object');
if (errorMessage) setErrorMessage(null);
}}
role='tab'
type='button'
>
Unsupported object
</button>
</div>
<Prompt.Body className='app__attachment-dialog__body'>
<div className='app__attachment-dialog__subsection'>
<h3 className='app__attachment-dialog__subsection-title'>
Attach Unsupported Attachment
</h3>
<div
aria-label='Attachment type'
className='app__attachment-dialog__tabs'
role='tablist'
<label className='app__attachment-dialog__field'>
<span className='app__attachment-dialog__field-label'>Attachment JSON</span>
<textarea
className='app__attachment-dialog__textarea'
onChange={(event) => {
if (activeTab === 'unsupported-file') {
setUnsupportedFileInput(event.target.value);
} else {
setUnsupportedObjectInput(event.target.value);
}
if (errorMessage) setErrorMessage(null);
}}
rows={12}
spellCheck={false}
value={
activeTab === 'unsupported-file'
? unsupportedFileInput
: unsupportedObjectInput
}
/>
</label>
<div className='app__attachment-dialog__subsection-actions'>
{activeTab === 'unsupported-file' ? (
<Prompt.FooterControlsButtonPrimary
size='sm'
onClick={() => attachToComposer('unsupported-file')}
>
Attach Unsupported file
</Prompt.FooterControlsButtonPrimary>
) : (
<Prompt.FooterControlsButtonPrimary
size='sm'
onClick={() => attachToComposer('unsupported-object')}
>
<button
aria-selected={activeTab === 'unsupported-file'}
className='app__attachment-dialog__tab'
onClick={() => {
setActiveTab('unsupported-file');
if (errorMessage) setErrorMessage(null);
}}
role='tab'
type='button'
>
Unsupported file
</button>
<button
aria-selected={activeTab === 'unsupported-object'}
className='app__attachment-dialog__tab'
onClick={() => {
setActiveTab('unsupported-object');
if (errorMessage) setErrorMessage(null);
}}
role='tab'
type='button'
>
Unsupported object
</button>
</div>
<label className='app__attachment-dialog__field'>
<span className='app__attachment-dialog__field-label'>
Attachment JSON
</span>
<textarea
className='app__attachment-dialog__textarea'
onChange={(event) => {
if (activeTab === 'unsupported-file') {
setUnsupportedFileInput(event.target.value);
} else {
setUnsupportedObjectInput(event.target.value);
}
if (errorMessage) setErrorMessage(null);
}}
rows={12}
spellCheck={false}
value={
activeTab === 'unsupported-file'
? unsupportedFileInput
: unsupportedObjectInput
}
/>
</label>
<div className='app__attachment-dialog__subsection-actions'>
{activeTab === 'unsupported-file' ? (
<Prompt.FooterControlsButtonPrimary
size='sm'
onClick={() => attachToComposer('unsupported-file')}
>
Attach Unsupported file
</Prompt.FooterControlsButtonPrimary>
) : (
<Prompt.FooterControlsButtonPrimary
size='sm'
onClick={() => attachToComposer('unsupported-object')}
>
Attach Unsupported object
</Prompt.FooterControlsButtonPrimary>
)}
</div>
{errorMessage && (
<div className='app__attachment-dialog__error' role='alert'>
{errorMessage}
</div>
)}
Attach Unsupported object
</Prompt.FooterControlsButtonPrimary>
)}
</div>
{errorMessage && (
<div className='app__attachment-dialog__error' role='alert'>
{errorMessage}
</div>
</Prompt.Body>
</Prompt.Root>
</div>
</DialogAnchor>
)}
</div>
</Prompt.Body>
</DraggableDialog>
);
};
Loading