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
8 changes: 4 additions & 4 deletions sample-apps/react-demo/app/AppSkeleton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ export const AppSkeleton = ({ children }: PropsWithChildren) => {
const unreadCount = notificationStatus?.unread ?? 0;

return (
<div className="min-h-full w-full max-w-7xl mx-auto overflow-x-hidden">
<div className="drawer min-h-full lg:drawer-open">
<div className="min-h-dvh w-full max-w-7xl mx-auto overflow-x-hidden">
<div className="drawer min-h-dvh lg:drawer-open">
<input id="my-drawer" type="checkbox" className="drawer-toggle" />
<div className="drawer-content min-h-full flex flex-col gap-1 items-center">
<div className="drawer-content min-h-dvh flex flex-col gap-1 items-center">
<nav className="hidden md:flex lg:hidden navbar w-full bg-base-100">
<div className="flex-none lg:hidden">
<label
Expand Down Expand Up @@ -54,7 +54,7 @@ const DrawerSide = ({ unreadCount }: { unreadCount: number }) => {
aria-label="close sidebar"
className="drawer-overlay"
></label>
<ul className="menu min-h-full w-60 p-4">
<ul className="menu min-h-dvh w-60 p-4">
<li>
<HomeLink />
</li>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export const Activity = ({
withLink={location === 'timeline' || location === 'profile' || location === 'search' || location === 'foryou'}
withActions={location === 'timeline' || location === 'profile'}
/>
<ActivityContent activity={activity} withoutLinks={location === 'preview'} />
<ActivityContent activity={activity} withoutInteractions={location === 'preview'} />
{activity?.parent ? (location === 'preview' ? <ActivityParent activity={activity} /> : <NavLink className="w-full min-w-0 max-w-full" href={`/activity/${activity.parent?.id}`}><ActivityParent activity={activity} /></NavLink>) : null}
{location !== 'preview' && <ActivityInteractions activity={activity} />}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@ export const ActivityComposer = ({
parent,
onSave,
textareaBorder = true,
portalContainer,
}: {
activity?: ActivityResponse;
parent?: ActivityResponse;
onSave?: () => void;
textareaBorder?: boolean;
portalContainer?: HTMLElement | null;
}) => {
const client = useFeedsClient();
const feed = useFeedContext();
Expand Down Expand Up @@ -77,6 +79,7 @@ export const ActivityComposer = ({
onSubmit={handleSubmit}
textareaBorder={textareaBorder}
allowEmptyText={!!parent}
portalContainer={portalContainer}
/>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { type ActivityResponse } from "@stream-io/feeds-react-sdk";
import { Content } from "../common/Content";

export const ActivityContent = ({ activity, withoutLinks = false }: { activity: ActivityResponse, withoutLinks?: boolean }) => {
export const ActivityContent = ({ activity, withoutInteractions = false }: { activity: ActivityResponse, withoutInteractions?: boolean }) => {
return (
<Content text={activity.text} attachments={activity.attachments} moderation={activity.moderation} location="activity" mentioned_users={activity.mentioned_users} withoutLinks={withoutLinks} />
<Content text={activity.text} attachments={activity.attachments} moderation={activity.moderation} location="activity" mentioned_users={activity.mentioned_users} withoutInteractions={withoutInteractions} />
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export const ActivityActions = ({

return (
<ContentActions canEdit={canEdit} canDelete={canDelete} isModerated={isModerated} onDelete={deleteActivity}>
{(onClose) => <ActivityComposer activity={activity} onSave={onClose} />}
{(onClose, dialogElement) => <ActivityComposer activity={activity} onSave={onClose} portalContainer={dialogElement} />}
</ContentActions>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export const ReplyToActivity = ({ activity }: { activity: ActivityResponse }) =>
</button>
</div>
{isOpen && ownFeed && <StreamFeed feed={ownFeed}>
<ActivityComposer parent={activity} onSave={closeDialog} />
<ActivityComposer parent={activity} onSave={closeDialog} portalContainer={dialogRef.current} />
</StreamFeed>}
</div>
<form method="dialog" className="modal-backdrop">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@ export const CommentComposer = ({
parentComment,
comment,
onSubmitted,
portalContainer,
}: {
activity?: ActivityResponse;
parentComment?: CommentResponse;
comment?: CommentResponse;
onSubmitted?: () => void;
portalContainer?: HTMLElement | null;
}) => {
const client = useFeedsClient();
const [initialText, setInitialText] = useState('');
Expand Down Expand Up @@ -64,6 +66,7 @@ export const CommentComposer = ({
initialMentionedUsers={initialMentionedUsers}
onSubmit={handleSubmit}
submitLabel={isEditing ? 'Save' : 'Reply'}
portalContainer={portalContainer}
/>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export const CommentActions = ({ comment }: { comment: CommentResponse }) => {

return (
<ContentActions canEdit={canEdit} canDelete={canDelete} isModerated={isModerated} onDelete={deleteComment}>
{(onClose) => <CommentComposer comment={comment} onSubmitted={onClose} />}
{(onClose, dialogElement) => <CommentComposer comment={comment} onSubmitted={onClose} portalContainer={dialogElement} />}
</ContentActions>
);
};
14 changes: 7 additions & 7 deletions sample-apps/react-demo/app/components/common/Content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ type ContentProps = {
moderation?: ModerationV2Response;
location: 'comment' | 'activity';
mentioned_users?: UserResponse[];
withoutLinks?: boolean;
withoutInteractions?: boolean;
};

export const Content = ({ text, attachments, moderation, location, mentioned_users = [], withoutLinks = false }: ContentProps) => {
export const Content = ({ text, attachments, moderation, location, mentioned_users = [], withoutInteractions = false }: ContentProps) => {
const { mediaAttachments, ogAttachments } = useMemo(() => {
if (!attachments) {
return { mediaAttachments: [], ogAttachments: [] };
Expand Down Expand Up @@ -120,7 +120,7 @@ export const Content = ({ text, attachments, moderation, location, mentioned_use
const user = userMap.get(username.toLowerCase());

if (user) {
if (withoutLinks) {
if (withoutInteractions) {
parts.push(
<span
key={`mention-${partIndex}-${m.index}`}
Expand All @@ -146,7 +146,7 @@ export const Content = ({ text, attachments, moderation, location, mentioned_use
parts.push(`@${username}`);
}
} else if (m.type === 'url') {
if (withoutLinks) {
if (withoutInteractions) {
parts.push(
<span
key={`url-${partIndex}-${m.index}`}
Expand Down Expand Up @@ -189,7 +189,7 @@ export const Content = ({ text, attachments, moderation, location, mentioned_use
}

return parts;
}, [text, mentioned_users, withoutLinks]);
}, [text, mentioned_users, withoutInteractions]);

if (moderation?.action === 'remove') {
return (
Expand All @@ -207,11 +207,11 @@ export const Content = ({ text, attachments, moderation, location, mentioned_use
{renderedText && <p className="w-full text-md">{renderedText}</p>}
{mediaAttachments.length > 0 && (
<div className="w-full">
<AttachmentList attachments={mediaAttachments} size={mediaAttachmentSize} />
<AttachmentList attachments={mediaAttachments} size={mediaAttachmentSize} disableButtons={withoutInteractions} />
</div>
)}
{ogAttachments.length > 0 && (
<OGAttachmentList attachments={ogAttachments} size={ogAttachmentSize} withoutLinks={withoutLinks} />
<OGAttachmentList attachments={ogAttachments} size={ogAttachmentSize} withoutLinks={withoutInteractions} />
)}
</>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ type ContentActionsProps = {
canEdit: boolean;
canDelete: boolean;
onDelete: () => Promise<any> | undefined;
children: (onClose: () => void) => React.ReactNode;
children: (onClose: () => void, dialogElement: HTMLDialogElement | null) => React.ReactNode;
isModerated: boolean;
};

Expand Down Expand Up @@ -85,7 +85,7 @@ export const ContentActions = ({
</ul>
<ErrorToast error={error} />
{<dialog ref={dialogRef} className="modal">
<div className="modal-box w-[80%] max-w-none sm:w-[40%]">{isEditing ? children(closeDialog) : null}</div>
<div className="modal-box w-[80%] max-w-none sm:w-[40%]">{isEditing ? children(closeDialog, dialogRef.current) : null}</div>
<form method="dialog" className="modal-backdrop">
<button>close</button>
</form>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,23 @@ import {
export type AttachmentListProps = {
attachments: AttachmentType[];
size?: 'small' | 'medium' | 'large';
disableButtons?: boolean;
};

const SWIPE_THRESHOLD = 50;

export const AttachmentList = ({
attachments,
size = 'medium',
disableButtons = false,
}: AttachmentListProps) => {
const [currentIndex, setCurrentIndex] = useState(0);
const [isViewerOpen, setIsViewerOpen] = useState(false);
const [viewerInitialIndex, setViewerInitialIndex] = useState(0);
const touchStartX = useRef<number | null>(null);
const touchStartY = useRef<number | null>(null);
const touchEndX = useRef<number | null>(null);
const touchEndY = useRef<number | null>(null);

const hasMultiple = attachments.length > 1;

Expand Down Expand Up @@ -51,11 +55,20 @@ export const AttachmentList = ({
setCurrentIndex((prev) => (prev === attachments.length - 1 ? 0 : prev + 1));
}, [attachments.length]);

const resetTouchState = useCallback(() => {
touchStartX.current = null;
touchStartY.current = null;
touchEndX.current = null;
touchEndY.current = null;
}, []);

const handleTouchStart = useCallback(
(e: React.TouchEvent) => {
if (!hasMultiple) return;
touchStartX.current = e.touches[0].clientX;
touchStartY.current = e.touches[0].clientY;
touchEndX.current = null;
touchEndY.current = null;
},
[hasMultiple]
);
Expand All @@ -64,28 +77,38 @@ export const AttachmentList = ({
(e: React.TouchEvent) => {
if (!hasMultiple) return;
touchEndX.current = e.touches[0].clientX;
touchEndY.current = e.touches[0].clientY;
},
[hasMultiple]
);

const handleTouchEnd = useCallback(() => {
if (!hasMultiple || touchStartX.current === null || touchEndX.current === null) {
if (
!hasMultiple ||
touchStartX.current === null ||
touchStartY.current === null ||
touchEndX.current === null ||
touchEndY.current === null
) {
resetTouchState();
return;
}

const diff = touchStartX.current - touchEndX.current;
const diffX = touchStartX.current - touchEndX.current;
const diffY = touchStartY.current - touchEndY.current;

if (Math.abs(diff) > SWIPE_THRESHOLD) {
if (diff > 0) {
// Only register as swipe if horizontal movement exceeds vertical
// This prevents scrolling from being interpreted as a swipe
if (Math.abs(diffX) > SWIPE_THRESHOLD && Math.abs(diffX) > Math.abs(diffY)) {
if (diffX > 0) {
goToNext();
} else {
goToPrevious();
}
}

touchStartX.current = null;
touchEndX.current = null;
}, [hasMultiple, goToNext, goToPrevious]);
resetTouchState();
}, [hasMultiple, goToNext, goToPrevious, resetTouchState]);

const handleImageClick = useCallback(() => {
const currentAttachment = attachments[currentIndex];
Expand All @@ -109,8 +132,9 @@ export const AttachmentList = ({
onTouchStart={handleTouchStart}
onTouchMove={handleTouchMove}
onTouchEnd={handleTouchEnd}
onTouchCancel={resetTouchState}
>
{hasMultiple && (
{hasMultiple && !disableButtons && (
<button
className="absolute left-0 top-0 h-full z-10 flex items-center justify-center px-2 cursor-pointer"
onClick={goToPrevious}
Expand All @@ -125,10 +149,10 @@ export const AttachmentList = ({
<Attachment
attachment={currentAttachment}
size={size}
onClick={currentAttachment.type !== 'video' ? handleImageClick : undefined}
onClick={!disableButtons && currentAttachment.type !== 'video' ? handleImageClick : undefined}
/>

{hasMultiple && (
{hasMultiple && !disableButtons && (
<button
className="absolute right-0 top-0 h-full z-10 flex items-center justify-center px-2 cursor-pointer"
onClick={goToNext}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export type ComposerProps = {
onSubmit: (text: string, attachments: Attachment[], mentionedUserIds: string[]) => Promise<void>;
textareaBorder?: boolean;
allowEmptyText?: boolean;
portalContainer?: HTMLElement | null;
};

export const Composer = ({
Expand All @@ -31,6 +32,7 @@ export const Composer = ({
onSubmit,
textareaBorder = true,
allowEmptyText = false,
portalContainer,
}: ComposerProps) => {
const [text, setText] = useState(initialText);
const [completedAttachments, setCompletedAttachments] = useState<Attachment[]>(initialAttachments);
Expand Down Expand Up @@ -165,6 +167,7 @@ export const Composer = ({
caretPosition={caretPosition}
onSelect={handleSelectUser}
onHover={setSelectedIndex}
portalContainer={portalContainer}
/>
)}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export type MentionSuggestionListProps = {
caretPosition: CaretPosition | null;
onSelect: (user: UserResponse) => void;
onHover: (index: number) => void;
portalContainer?: HTMLElement | null;
};

export const MentionSuggestionList = ({
Expand All @@ -52,6 +53,7 @@ export const MentionSuggestionList = ({
caretPosition,
onSelect,
onHover,
portalContainer,
}: MentionSuggestionListProps) => {
const listRef = useRef<HTMLDivElement>(null);
const [dropdownPosition, setDropdownPosition] = useState<DropdownPosition | null>(null);
Expand Down Expand Up @@ -123,6 +125,6 @@ export const MentionSuggestionList = ({
</button>
))}
</div>,
document.body,
portalContainer ?? document.body,
);
};
Loading
Loading