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: 19 additions & 4 deletions src/components/Attachment/Giphy.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,43 @@
import type { Attachment } from 'stream-chat';
import { BaseImage as DefaultBaseImage } from '../BaseImage';
import { toGalleryItemDescriptors } from '../Gallery';
import clsx from 'clsx';
import { useChannelStateContext } from '../../context';
import {
useChannelStateContext,
useComponentContext,
useTranslationContext,
} from '../../context';
import { IconGiphy } from '../Icons';
import { useMemo } from 'react';
import { ImageComponent } from './Image';

export type GiphyAttachmentProps = {
attachment: Attachment;
};

export const Giphy = ({ attachment }: GiphyAttachmentProps) => {
const { giphyVersion: giphyVersionName } = useChannelStateContext();
const { BaseImage = DefaultBaseImage } = useComponentContext();
const { t } = useTranslationContext();
const usesDefaultBaseImage = BaseImage === DefaultBaseImage;

const imageDescriptors = useMemo(
() => toGalleryItemDescriptors(attachment, { giphyVersionName }),
[attachment, giphyVersionName],
);

if (!imageDescriptors) return null;
if (!imageDescriptors?.imageUrl) return null;

const { alt, dimensions, imageUrl, title } = imageDescriptors;

return (
<div className={clsx(`str-chat__message-attachment-giphy`)}>
<ImageComponent {...imageDescriptors} />
<BaseImage
alt={alt ?? title ?? t('User uploaded content')}
height={dimensions?.height}
src={imageUrl}
width={dimensions?.width}
{...(usesDefaultBaseImage ? { showDownloadButtonOnError: false } : {})}
/>
<GiphyBadge />
</div>
);
Expand Down
6 changes: 3 additions & 3 deletions src/components/Attachment/Image.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ import type { GalleryItem } from '../Gallery/GalleryContext';
export type ImageProps = GalleryItem;

/**
* Display image in with option to expand into modal gallery
* Display image with tap-to-expand modal gallery.
*/
export const ImageComponent = (props: ImageProps) => {
export const ImageComponent = (galleryItem: ImageProps) => {
const { ModalGallery = DefaultModalGallery } = useComponentContext();

return <ModalGallery items={[props]} modalClassName='str-chat__image-modal' />;
return <ModalGallery items={[galleryItem]} modalClassName='str-chat__image-modal' />;
};
2 changes: 1 addition & 1 deletion src/components/Attachment/LinkPreview/Card.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react';
import { BaseImage } from '../../Gallery';
import { BaseImage } from '../../BaseImage';
import { SafeAnchor } from '../../SafeAnchor';
import { useChannelStateContext } from '../../../context/ChannelStateContext';

Expand Down
10 changes: 4 additions & 6 deletions src/components/Attachment/ModalGallery.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import React, { useCallback, useState } from 'react';
import clsx from 'clsx';

import type { BaseImageProps, GalleryItem } from '../Gallery';
import {
BaseImage as DefaultBaseImage,
Gallery as DefaultGallery,
GalleryUI,
} from '../Gallery';
import type { BaseImageProps } from '../BaseImage';
import type { GalleryItem } from '../Gallery/GalleryContext';
import { BaseImage as DefaultBaseImage } from '../BaseImage';
import { Gallery as DefaultGallery, GalleryUI } from '../Gallery';
import { LoadingIndicator } from '../Loading';
import { GlobalModal } from '../Modal';
import { useComponentContext, useTranslationContext } from '../../context';
Expand Down
3 changes: 3 additions & 0 deletions src/components/BaseImage/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './BaseImage';
export * from './ImagePlaceholder';
export * from './toBaseImageDescriptors';
1 change: 1 addition & 0 deletions src/components/BaseImage/styling/index.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@use 'ImagePlaceholder';
101 changes: 101 additions & 0 deletions src/components/BaseImage/toBaseImageDescriptors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import {
type Attachment,
isGiphyAttachment,
isImageAttachment,
isLocalImageAttachment,
isLocalVideoAttachment,
isScrapedContent,
isVideoAttachment,
type LinkPreview,
type LocalImageAttachment,
type LocalVideoAttachment,
} from 'stream-chat';
import type { Dimensions } from '../../types/types';

type AttachmentPreviewableInGallery =
| LocalImageAttachment
| LocalVideoAttachment
| LinkPreview
| Attachment;

/** Fields shared with gallery items for image/video preview from an attachment. */
export type BaseImageDescriptor = {
alt?: string;
dimensions?: Dimensions;
imageUrl?: string;
title?: string;
videoThumbnailUrl?: string;
videoUrl?: string;
};

/**
* Maps an attachment (or link preview) to image/video URLs and metadata for {@link BaseImage} or the gallery.
*/
export const toBaseImageDescriptors = (
attachment: AttachmentPreviewableInGallery,
options: { giphyVersionName?: string } = {},
): BaseImageDescriptor | void => {
if (isGiphyAttachment(attachment)) {
const giphyVersion =
options?.giphyVersionName && attachment.giphy
? attachment.giphy[
options.giphyVersionName as keyof NonNullable<Attachment['giphy']>
]
: undefined;

return {
alt: giphyVersion?.url || attachment.thumb_url,
dimensions: giphyVersion
? {
height: giphyVersion.height,
width: giphyVersion.width,
}
: undefined,
imageUrl: attachment.thumb_url,
title: attachment.title || attachment.thumb_url,
};
}

if (isScrapedContent(attachment)) {
const imageUrl = attachment.image_url || attachment.thumb_url;
return {
alt: attachment.title || imageUrl,
imageUrl,
title: attachment.title,
};
}

if (isLocalVideoAttachment(attachment)) {
return {
title: attachment.title,
videoThumbnailUrl: attachment.thumb_url ?? attachment.localMetadata.previewUri,
videoUrl: attachment.asset_url ?? attachment.localMetadata.previewUri,
};
}

if (isVideoAttachment(attachment)) {
return {
title: attachment.title,
videoThumbnailUrl: attachment.thumb_url,
videoUrl: attachment.asset_url,
};
}

if (isLocalImageAttachment(attachment)) {
const imageUrl = attachment.image_url || attachment.localMetadata.previewUri;
return {
alt: attachment.title || imageUrl,
imageUrl,
title: attachment.title,
};
}

if (isImageAttachment(attachment)) {
const imageUrl = attachment.image_url;
return {
alt: attachment.title || imageUrl,
imageUrl,
title: attachment.title,
};
}
};
105 changes: 13 additions & 92 deletions src/components/Gallery/GalleryContext.tsx
Original file line number Diff line number Diff line change
@@ -1,98 +1,8 @@
import { createContext, useContext } from 'react';

import {
type Attachment,
isGiphyAttachment,
isImageAttachment,
isLocalImageAttachment,
isLocalVideoAttachment,
isScrapedContent,
isVideoAttachment,
type LinkPreview,
type LocalImageAttachment,
type LocalVideoAttachment,
} from 'stream-chat';
import { toBaseImageDescriptors } from '../BaseImage';
import type { BaseImageProps } from '../BaseImage';
import type { Dimensions } from '../../types/types';
import type { BaseImageProps } from './BaseImage';

type AttachmentPreviewableInGallery =
| LocalImageAttachment
| LocalVideoAttachment
| LinkPreview
| Attachment;

export const toGalleryItemDescriptors = (
attachment: AttachmentPreviewableInGallery,
options: { giphyVersionName?: string } = {},
): Pick<
GalleryItem,
'alt' | 'dimensions' | 'imageUrl' | 'title' | 'videoThumbnailUrl' | 'videoUrl'
> | void => {
if (isGiphyAttachment(attachment)) {
const giphyVersion =
options?.giphyVersionName && attachment.giphy
? attachment.giphy[
options.giphyVersionName as keyof NonNullable<Attachment['giphy']>
]
: undefined;

return {
alt: giphyVersion?.url || attachment.thumb_url,
dimensions: giphyVersion
? {
height: giphyVersion.height,
width: giphyVersion.width,
}
: undefined,
imageUrl: attachment.thumb_url,
title: attachment.title || attachment.thumb_url,
};
}

if (isScrapedContent(attachment)) {
// LinkPreview + OGAttachment
const imageUrl = attachment.image_url || attachment.thumb_url;
return {
alt: attachment.title || imageUrl,
imageUrl,
title: attachment.title,
};
}

if (isLocalVideoAttachment(attachment)) {
return {
title: attachment.title,
videoThumbnailUrl: attachment.thumb_url ?? attachment.localMetadata.previewUri,
videoUrl: attachment.asset_url ?? attachment.localMetadata.previewUri,
};
}

if (isVideoAttachment(attachment)) {
return {
title: attachment.title,
videoThumbnailUrl: attachment.thumb_url,
videoUrl: attachment.asset_url,
};
}

if (isLocalImageAttachment(attachment)) {
const imageUrl = attachment.image_url || attachment.localMetadata.previewUri;
return {
alt: attachment.title || imageUrl,
imageUrl,
title: attachment.title,
};
}

if (isImageAttachment(attachment)) {
const imageUrl = attachment.image_url;
return {
alt: attachment.title || imageUrl,
imageUrl,
title: attachment.title,
};
}
};

export type GalleryItem = Omit<BaseImageProps, 'src'> & {
dimensions?: Dimensions;
Expand All @@ -101,6 +11,17 @@ export type GalleryItem = Omit<BaseImageProps, 'src'> & {
videoUrl?: string;
};

/**
* Maps an attachment (or link preview) to gallery item fields.
* Delegates to {@link toBaseImageDescriptors}.
*/
export const toGalleryItemDescriptors = (
...args: Parameters<typeof toBaseImageDescriptors>
): Pick<
GalleryItem,
'alt' | 'dimensions' | 'imageUrl' | 'title' | 'videoThumbnailUrl' | 'videoUrl'
> | void => toBaseImageDescriptors(...args);

export type GalleryContextValue = {
/** Whether clicking the empty gallery background should request close */
closeOnBackgroundClick: boolean;
Expand Down
2 changes: 1 addition & 1 deletion src/components/Gallery/GalleryUI.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { useCallback, useContext, useEffect, useRef, useState } from 'react';

import { BaseImage } from './BaseImage';
import { BaseImage } from '../BaseImage';
import { GalleryHeader } from './GalleryHeader';
import { useGalleryContext } from './GalleryContext';
import { Button, type ButtonProps } from '../Button';
Expand Down
2 changes: 0 additions & 2 deletions src/components/Gallery/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
export * from './BaseImage';
export * from './Gallery';
export * from './GalleryContext';
export * from './GalleryUI';
export * from './ImagePlaceholder';
1 change: 0 additions & 1 deletion src/components/Gallery/styling/index.scss
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
@use 'BaseImage';
@use 'Gallery';
@use 'ImagePlaceholder';
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
type LocalVideoAttachment,
} from 'stream-chat';
import { useComponentContext, useTranslationContext } from '../../../context';
import { BaseImage as DefaultBaseImage } from '../../Gallery';
import { BaseImage as DefaultBaseImage } from '../../BaseImage';
import React, {
type KeyboardEvent,
type MouseEvent,
Expand Down
2 changes: 1 addition & 1 deletion src/components/MessageInput/LinkPreviewList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { useStateStore } from '../../store';
import { PopperTooltip } from '../Tooltip';
import { useEnterLeaveHandlers } from '../Tooltip/hooks';
import { useMessageComposer } from './hooks';
import { BaseImage } from '../Gallery';
import { BaseImage } from '../BaseImage';
import { RemoveAttachmentPreviewButton } from './RemoveAttachmentPreviewButton';
import { IconChainLink } from '../Icons';

Expand Down
2 changes: 1 addition & 1 deletion src/components/MessageInput/MessageComposerActions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ export const MessageComposerActions = () => {

if (isCooldownActive) {
content = <CooldownTimer />;
} else if (contentIsEmpty && !editedMessage && recordingEnabled) {
} else if (contentIsEmpty && !editedMessage && !command && recordingEnabled) {
content = <AudioRecordingButtonWithNotification />;
}

Expand Down
Loading
Loading