Skip to content

feat: support size_limit in upload config #2301

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Apr 8, 2024
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
3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,6 @@
"lodash.throttle": "^4.1.1",
"lodash.uniqby": "^4.7.0",
"nanoid": "^3.3.4",
"pretty-bytes": "^5.4.1",
"prop-types": "^15.7.2",
"react-dropzone": "^14.2.3",
"react-fast-compare": "^3.2.2",
Expand Down Expand Up @@ -223,7 +222,7 @@
"rollup-plugin-url": "^3.0.1",
"rollup-plugin-visualizer": "^4.2.0",
"semantic-release": "^19.0.5",
"stream-chat": "^8.21.0",
"stream-chat": "^8.25.1",
"style-loader": "^2.0.0",
"ts-jest": "^28.0.8",
"typescript": "^4.7.4",
Expand Down
1 change: 0 additions & 1 deletion rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@ const externalDependencies = [
'lodash.throttle',
'lodash.uniqby',
'mml-react',
'pretty-bytes',
'prop-types',
'react-fast-compare',
'react-images',
Expand Down
4 changes: 2 additions & 2 deletions src/components/Attachment/__tests__/Audio.test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import React from 'react';
import prettybytes from 'pretty-bytes';
import { act, cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react';
import '@testing-library/jest-dom';

Expand All @@ -8,6 +7,7 @@ import { Audio } from '../Audio';
import { ChannelActionProvider, ChatContext } from '../../../context';

import { generateAudioAttachment } from 'mock-builders';
import { prettifyFileSize } from '../../MessageInput/hooks/utils';

const AUDIO = generateAudioAttachment();

Expand Down Expand Up @@ -53,7 +53,7 @@ describe('Audio', () => {
});

expect(getByText(AUDIO.title)).toBeInTheDocument();
expect(getByText(prettybytes(AUDIO.file_size))).toBeInTheDocument();
expect(getByText(prettifyFileSize(AUDIO.file_size))).toBeInTheDocument();
expect(container.querySelector('img')).not.toBeInTheDocument();
});

Expand Down
4 changes: 2 additions & 2 deletions src/components/Attachment/components/FileSizeIndicator.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react';
import prettybytes from 'pretty-bytes';
import { prettifyFileSize } from '../../MessageInput/hooks/utils';

type FileSizeIndicatorProps = {
/** file size in byte */
Expand All @@ -19,7 +19,7 @@ export const FileSizeIndicator = ({ fileSize, maximumFractionDigits }: FileSizeI
className='str-chat__message-attachment-file--item-size'
data-testid='file-size-indicator'
>
{prettybytes(fileSize, { maximumFractionDigits })}
{prettifyFileSize(fileSize, maximumFractionDigits)}
</span>
);
};
57 changes: 55 additions & 2 deletions src/components/MessageInput/__tests__/MessageInput.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,13 @@ import {
generateMessage,
generateUser,
getOrCreateChannelApi,
getTestClient,
getTestClientWithUser,
useMockedApis,
} from '../../../mock-builders';

expect.extend(toHaveNoViolations);

jest.mock('../../Channel/utils', () => ({ makeAddNotifications: jest.fn }));

let chatClient;
let channel;

Expand Down Expand Up @@ -76,6 +75,11 @@ const mockFaultyUploadApi = (cause) => jest.fn().mockImplementation(() => Promis

const submitMock = jest.fn();
const editMock = jest.fn();
const mockAddNotification = jest.fn();

jest.mock('../../Channel/utils', () => ({
makeAddNotifications: () => mockAddNotification,
}));

const defaultMessageContextValue = {
getMessageActions: () => ['delete', 'edit', 'quote'],
Expand Down Expand Up @@ -509,6 +513,55 @@ function axeNoViolations(container) {
await axeNoViolations(container);
});

it('should show notification if size limit is exceeded', async () => {
chatClient = getTestClient({
getAppSettings: () => ({
app: {
file_upload_config: { size_limit: 1 },
image_upload_config: { size_limit: 1 },
},
}),
});
await renderComponent({
messageInputProps: {
doFileUploadRequest: mockUploadApi(),
},
});
const formElement = await screen.findByPlaceholderText(inputPlaceholder);
const file = getFile(filename1);
act(() => dropFile(file, formElement));
await waitFor(() => expect(screen.queryByText(filename1)).toBeInTheDocument());

expect(mockAddNotification).toHaveBeenCalledTimes(1);
expect(mockAddNotification.mock.calls[0][0]).toContain('File is too large');
});

it('should apply separate limits to files and images', async () => {
chatClient = getTestClient({
getAppSettings: () => ({
app: {
file_upload_config: { size_limit: 100 },
image_upload_config: { size_limit: 1 },
},
}),
});
const doImageUploadRequest = mockUploadApi();
await renderComponent({
messageInputProps: {
doImageUploadRequest,
},
});
const formElement = await screen.findByPlaceholderText(inputPlaceholder);
const file = getImage();
await act(() => {
dropFile(file, formElement);
});
await waitFor(() => {
expect(mockAddNotification).toHaveBeenCalledTimes(1);
expect(mockAddNotification.mock.calls[0][0]).toContain('File is too large');
});
});

// TODO: Check if pasting plaintext is not prevented -> tricky because recreating exact event is hard
// TODO: Remove image/file -> difficult because there is no easy selector and components are in react-file-utils
});
Expand Down
31 changes: 26 additions & 5 deletions src/components/MessageInput/hooks/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type { ChannelActionContextValue } from '../../../context/ChannelActionCo
import type { ChatContextValue } from '../../../context/ChatContext';
import type { TranslationContextValue } from '../../../context/TranslationContext';
import type { DefaultStreamChatGenerics } from '../../../types/types';
import { DEFAULT_UPLOAD_SIZE_LIMIT_BYTES } from '../../../constants/limits';

export const accentsMap: { [key: string]: string } = {
a: 'á|à|ã|â|À|Á|Ã|Â',
Expand Down Expand Up @@ -136,12 +137,13 @@ export const checkUploadPermissions = async <
allowed_mime_types,
blocked_file_extensions,
blocked_mime_types,
size_limit,
} =
((uploadType === 'image'
? appSettings?.app?.image_upload_config
: appSettings?.app?.file_upload_config) as FileUploadConfig) || {};

const sendErrorNotification = () =>
const sendNotAllowedErrorNotification = () =>
addNotification(
t(`Upload type: "{{ type }}" is not allowed`, { type: file.type || 'unknown type' }),
'error',
Expand All @@ -153,7 +155,7 @@ export const checkUploadPermissions = async <
);

if (!allowed) {
sendErrorNotification();
sendNotAllowedErrorNotification();
return false;
}
}
Expand All @@ -164,7 +166,7 @@ export const checkUploadPermissions = async <
);

if (blocked) {
sendErrorNotification();
sendNotAllowedErrorNotification();
return false;
}
}
Expand All @@ -175,7 +177,7 @@ export const checkUploadPermissions = async <
);

if (!allowed) {
sendErrorNotification();
sendNotAllowedErrorNotification();
return false;
}
}
Expand All @@ -186,10 +188,29 @@ export const checkUploadPermissions = async <
);

if (blocked) {
sendErrorNotification();
sendNotAllowedErrorNotification();
return false;
}
}

const sizeLimit = size_limit || DEFAULT_UPLOAD_SIZE_LIMIT_BYTES;
if (file.size && file.size > sizeLimit) {
addNotification(
t('File is too large: {{ size }}, maximum upload size is {{ limit }}', {
limit: prettifyFileSize(sizeLimit),
size: prettifyFileSize(file.size),
}),
'error',
);
return false;
}

return true;
};

export function prettifyFileSize(bytes: number, precision = 3) {
const units = ['B', 'kB', 'MB', 'GB'];
const exponent = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1);
const mantissa = bytes / 1024 ** exponent;
return `${mantissa.toPrecision(precision)} ${units[exponent]}`;
}
1 change: 1 addition & 0 deletions src/constants/limits.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ export const DEFAULT_NEXT_CHANNEL_PAGE_SIZE = 100;
export const DEFAULT_JUMP_TO_PAGE_SIZE = 100;
export const DEFAULT_THREAD_PAGE_SIZE = 50;
export const DEFAULT_LOAD_PAGE_SCROLL_THRESHOLD = 250;
export const DEFAULT_UPLOAD_SIZE_LIMIT_BYTES = 100 * 1024 * 1024; // 100 MB
1 change: 1 addition & 0 deletions src/i18n/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"Error: {{ errorMessage }}": "Fehler: {{ errorMessage }}",
"Failed to jump to the first unread message": "Fehler beim Springen zur ersten ungelesenen Nachricht",
"Failed to mark channel as read": "Fehler beim Markieren des Kanals als gelesen",
"File is too large: {{ size }}, maximum upload size is {{ limit }}": "Datei ist zu groß: {{ size }}, maximale Upload-Größe beträgt {{ limit }}",
"Flag": "Meldung",
"Latest Messages": "Neueste Nachrichten",
"Load more": "Mehr laden",
Expand Down
1 change: 1 addition & 0 deletions src/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"Error: {{ errorMessage }}": "Error: {{ errorMessage }}",
"Failed to jump to the first unread message": "Failed to jump to the first unread message",
"Failed to mark channel as read": "Failed to mark channel as read",
"File is too large: {{ size }}, maximum upload size is {{ limit }}": "File is too large: {{ size }}, maximum upload size is {{ limit }}",
"Flag": "Flag",
"Latest Messages": "Latest Messages",
"Load more": "Load more",
Expand Down
1 change: 1 addition & 0 deletions src/i18n/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"Error: {{ errorMessage }}": "Error: {{ errorMessage }}",
"Failed to jump to the first unread message": "Error al saltar al primer mensaje no leído",
"Failed to mark channel as read": "Error al marcar el canal como leído",
"File is too large: {{ size }}, maximum upload size is {{ limit }}": "El archivo es demasiado grande: {{ size }}, el tamaño máximo de carga es de {{ limit }}",
"Flag": "Bandera",
"Latest Messages": "Últimos mensajes",
"Load more": "Cargar más",
Expand Down
1 change: 1 addition & 0 deletions src/i18n/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"Error: {{ errorMessage }}": "Erreur : {{ errorMessage }}",
"Failed to jump to the first unread message": "Échec de saut vers le premier message non lu",
"Failed to mark channel as read": "Échec de la marque du canal comme lu",
"File is too large: {{ size }}, maximum upload size is {{ limit }}": "Le fichier est trop volumineux : {{ size }}, la taille de téléchargement maximale est de {{ limit }}",
"Flag": "Signaler",
"Latest Messages": "Derniers messages",
"Load more": "Charger plus",
Expand Down
1 change: 1 addition & 0 deletions src/i18n/hi.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"Error: {{ errorMessage }}": "फेल: {{ errorMessage }}",
"Failed to jump to the first unread message": "पहले अपठित संदेश पर जाने में विफल",
"Failed to mark channel as read": "चैनल को पढ़ा हुआ चिह्नित करने में विफल।",
"File is too large: {{ size }}, maximum upload size is {{ limit }}": "फ़ाइल बहुत बड़ी है: {{ size }}, अधिकतम अपलोड साइज़ {{ limit }} है",
"Flag": "फ्लैग करे",
"Latest Messages": "नवीनतम संदेश",
"Load more": "और लोड करें",
Expand Down
1 change: 1 addition & 0 deletions src/i18n/it.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"Error: {{ errorMessage }}": "Errore: {{ errorMessage }}",
"Failed to jump to the first unread message": "Impossibile passare al primo messaggio non letto",
"Failed to mark channel as read": "Impossibile contrassegnare il canale come letto",
"File is too large: {{ size }}, maximum upload size is {{ limit }}": "Il file è troppo grande: {{ size }}, la dimensione massima di caricamento è {{ limit }}",
"Flag": "Segnala",
"Latest Messages": "Ultimi messaggi",
"Load more": "Carica di più",
Expand Down
1 change: 1 addition & 0 deletions src/i18n/ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"Error: {{ errorMessage }}": "エラー: {{ errorMessage }}",
"Failed to jump to the first unread message": "最初の未読メッセージにジャンプできませんでした",
"Failed to mark channel as read": "チャンネルを既読にすることができませんでした",
"File is too large: {{ size }}, maximum upload size is {{ limit }}": "ファイルが大きすぎます:{{ size }}、最大アップロードサイズは{{ limit }}です",
"Flag": "フラグ",
"Latest Messages": "最新のメッセージ",
"Load more": "もっと読み込む",
Expand Down
1 change: 1 addition & 0 deletions src/i18n/ko.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"Error: {{ errorMessage }}": "오류: {{ errorMessage }}",
"Failed to jump to the first unread message": "첫 번째 읽지 않은 메시지로 이동하지 못했습니다",
"Failed to mark channel as read": "채널을 읽음으로 표시하는 데 실패했습니다",
"File is too large: {{ size }}, maximum upload size is {{ limit }}": "파일이 너무 큽니다: {{ size }}, 최대 업로드 크기는 {{ limit }}입니다",
"Flag": "플래그",
"Latest Messages": "최신 메시지",
"Load more": "더 불러오기",
Expand Down
1 change: 1 addition & 0 deletions src/i18n/nl.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"Error: {{ errorMessage }}": "Error: {{ errorMessage }}",
"Failed to jump to the first unread message": "Niet gelukt om naar het eerste ongelezen bericht te springen",
"Failed to mark channel as read": "Kanaal kon niet als gelezen worden gemarkeerd",
"File is too large: {{ size }}, maximum upload size is {{ limit }}": "Bestand is te groot: {{ size }}, maximale uploadgrootte is {{ limit }}",
"Flag": "Markeer",
"Latest Messages": "Laatste berichten",
"Load more": "Meer laden",
Expand Down
1 change: 1 addition & 0 deletions src/i18n/pt.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"Error: {{ errorMessage }}": "Erro: {{ errorMessage }}",
"Failed to jump to the first unread message": "Falha ao pular para a primeira mensagem não lida",
"Failed to mark channel as read": "Falha ao marcar o canal como lido",
"File is too large: {{ size }}, maximum upload size is {{ limit }}": "O arquivo é muito grande: {{ size }}, o tamanho máximo de upload é {{ limit }}",
"Flag": "Reportar",
"Latest Messages": "Mensagens mais recentes",
"Load more": "Carregar mais",
Expand Down
1 change: 1 addition & 0 deletions src/i18n/ru.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"Error: {{ errorMessage }}": "Ошибка: {{ errorMessage }}",
"Failed to jump to the first unread message": "Не удалось перейти к первому непрочитанному сообщению",
"Failed to mark channel as read": "Не удалось пометить канал как прочитанный",
"File is too large: {{ size }}, maximum upload size is {{ limit }}": "Файл слишком большой: {{ size }}, максимальный размер загрузки составляет {{ limit }}",
"Flag": "Пожаловаться",
"Latest Messages": "Последние сообщения",
"Load more": "Загрузить больше",
Expand Down
1 change: 1 addition & 0 deletions src/i18n/tr.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"Error: {{ errorMessage }}": "Hata: {{ errorMessage }}",
"Failed to jump to the first unread message": "İlk okunmamış mesaja atlamada hata oluştu",
"Failed to mark channel as read": "Kanalı okundu olarak işaretleme başarısız oldu",
"File is too large: {{ size }}, maximum upload size is {{ limit }}": "Dosya çok büyük: {{ size }}, maksimum yükleme boyutu {{ limit }}",
"Flag": "Bayrak",
"Latest Messages": "Son Mesajlar",
"Load more": "Daha fazla yükle",
Expand Down
6 changes: 3 additions & 3 deletions src/mock-builders/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,11 @@ const connectUser = (client, user) =>
resolve();
});

function mockClient(client) {
function mockClient(client, mocks = {}) {
jest.spyOn(client, '_setToken').mockImplementation();
jest.spyOn(client, '_setupConnection').mockImplementation();
jest.spyOn(client, '_setupConnection').mockImplementation();
jest.spyOn(client, 'getAppSettings').mockImplementation();
jest.spyOn(client, 'getAppSettings').mockImplementation(mocks.getAppSettings);
client.tokenManager = {
getToken: jest.fn(() => token),
tokenReady: jest.fn(() => true),
Expand All @@ -31,7 +31,7 @@ function mockClient(client) {
return client;
}

export const getTestClient = () => mockClient(new StreamChat(apiKey));
export const getTestClient = (mocks) => mockClient(new StreamChat(apiKey), mocks);

export const getTestClientWithUser = async (user = { id: nanoid() }) => {
const client = mockClient(new StreamChat(apiKey));
Expand Down
7 changes: 1 addition & 6 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -11623,11 +11623,6 @@ prettier@^2.2.0:
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.2.1.tgz#795a1a78dd52f073da0cd42b21f9c91381923ff5"
integrity sha512-PqyhM2yCjg/oKkFPtTGUojv7gnZAoG80ttl45O6x2Ug/rMJw4wcc9k6aaf2hibP7BGVCCM33gZoGjyvt9mm16Q==

pretty-bytes@^5.4.1:
version "5.6.0"
resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb"
integrity sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==

pretty-format@^26.6.2:
version "26.6.2"
resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-26.6.2.tgz#e35c2705f14cb7fe2fe94fa078345b444120fc93"
Expand Down Expand Up @@ -13310,7 +13305,7 @@ stream-browserify@^2.0.1:
inherits "~2.0.1"
readable-stream "^2.0.2"

stream-chat@^8.21.0:
stream-chat@^8.25.1:
version "8.25.1"
resolved "https://registry.yarnpkg.com/stream-chat/-/stream-chat-8.25.1.tgz#5898098cb25e81ac537dd6adbbd9742b8a508fbf"
integrity sha512-9U4aVbLmRrRumxMGq4Fr/Y6vR30JSTxC9BkU3uGf3q+QZam3t4XA6TzsTJdJ6c34+QBSXL/k1hlsyA/InxKOLQ==
Expand Down
Loading