Skip to content

Commit a8aa524

Browse files
authoredApr 8, 2024
feat: support size_limit in upload config (#2301)
# Submit a pull request ### 🎯 Goal Support the newly introduced upload size limit setting. ### 🎨 UI Changes ![image](https://github.com/GetStream/stream-chat-react/assets/975978/777a93ee-3e90-4e47-b012-4e1cbd35e5ba) ### To-Do - [x] Bump LLC (GetStream/stream-chat-js#1244) - [x] Translation
1 parent af739c8 commit a8aa524

File tree

21 files changed

+103
-23
lines changed

21 files changed

+103
-23
lines changed
 

‎package.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,6 @@
7575
"lodash.throttle": "^4.1.1",
7676
"lodash.uniqby": "^4.7.0",
7777
"nanoid": "^3.3.4",
78-
"pretty-bytes": "^5.4.1",
7978
"prop-types": "^15.7.2",
8079
"react-dropzone": "^14.2.3",
8180
"react-fast-compare": "^3.2.2",
@@ -223,7 +222,7 @@
223222
"rollup-plugin-url": "^3.0.1",
224223
"rollup-plugin-visualizer": "^4.2.0",
225224
"semantic-release": "^19.0.5",
226-
"stream-chat": "^8.21.0",
225+
"stream-chat": "^8.25.1",
227226
"style-loader": "^2.0.0",
228227
"ts-jest": "^28.0.8",
229228
"typescript": "^4.7.4",

‎rollup.config.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,6 @@ const externalDependencies = [
4949
'lodash.throttle',
5050
'lodash.uniqby',
5151
'mml-react',
52-
'pretty-bytes',
5352
'prop-types',
5453
'react-fast-compare',
5554
'react-images',

‎src/components/Attachment/__tests__/Audio.test.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import React from 'react';
2-
import prettybytes from 'pretty-bytes';
32
import { act, cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react';
43
import '@testing-library/jest-dom';
54

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

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

1212
const AUDIO = generateAudioAttachment();
1313

@@ -53,7 +53,7 @@ describe('Audio', () => {
5353
});
5454

5555
expect(getByText(AUDIO.title)).toBeInTheDocument();
56-
expect(getByText(prettybytes(AUDIO.file_size))).toBeInTheDocument();
56+
expect(getByText(prettifyFileSize(AUDIO.file_size))).toBeInTheDocument();
5757
expect(container.querySelector('img')).not.toBeInTheDocument();
5858
});
5959

‎src/components/Attachment/components/FileSizeIndicator.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React from 'react';
2-
import prettybytes from 'pretty-bytes';
2+
import { prettifyFileSize } from '../../MessageInput/hooks/utils';
33

44
type FileSizeIndicatorProps = {
55
/** file size in byte */
@@ -19,7 +19,7 @@ export const FileSizeIndicator = ({ fileSize, maximumFractionDigits }: FileSizeI
1919
className='str-chat__message-attachment-file--item-size'
2020
data-testid='file-size-indicator'
2121
>
22-
{prettybytes(fileSize, { maximumFractionDigits })}
22+
{prettifyFileSize(fileSize, maximumFractionDigits)}
2323
</span>
2424
);
2525
};

‎src/components/MessageInput/__tests__/MessageInput.test.js

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,13 @@ import {
2525
generateMessage,
2626
generateUser,
2727
getOrCreateChannelApi,
28+
getTestClient,
2829
getTestClientWithUser,
2930
useMockedApis,
3031
} from '../../../mock-builders';
3132

3233
expect.extend(toHaveNoViolations);
3334

34-
jest.mock('../../Channel/utils', () => ({ makeAddNotifications: jest.fn }));
35-
3635
let chatClient;
3736
let channel;
3837

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

7776
const submitMock = jest.fn();
7877
const editMock = jest.fn();
78+
const mockAddNotification = jest.fn();
79+
80+
jest.mock('../../Channel/utils', () => ({
81+
makeAddNotifications: () => mockAddNotification,
82+
}));
7983

8084
const defaultMessageContextValue = {
8185
getMessageActions: () => ['delete', 'edit', 'quote'],
@@ -509,6 +513,55 @@ function axeNoViolations(container) {
509513
await axeNoViolations(container);
510514
});
511515

516+
it('should show notification if size limit is exceeded', async () => {
517+
chatClient = getTestClient({
518+
getAppSettings: () => ({
519+
app: {
520+
file_upload_config: { size_limit: 1 },
521+
image_upload_config: { size_limit: 1 },
522+
},
523+
}),
524+
});
525+
await renderComponent({
526+
messageInputProps: {
527+
doFileUploadRequest: mockUploadApi(),
528+
},
529+
});
530+
const formElement = await screen.findByPlaceholderText(inputPlaceholder);
531+
const file = getFile(filename1);
532+
act(() => dropFile(file, formElement));
533+
await waitFor(() => expect(screen.queryByText(filename1)).toBeInTheDocument());
534+
535+
expect(mockAddNotification).toHaveBeenCalledTimes(1);
536+
expect(mockAddNotification.mock.calls[0][0]).toContain('File is too large');
537+
});
538+
539+
it('should apply separate limits to files and images', async () => {
540+
chatClient = getTestClient({
541+
getAppSettings: () => ({
542+
app: {
543+
file_upload_config: { size_limit: 100 },
544+
image_upload_config: { size_limit: 1 },
545+
},
546+
}),
547+
});
548+
const doImageUploadRequest = mockUploadApi();
549+
await renderComponent({
550+
messageInputProps: {
551+
doImageUploadRequest,
552+
},
553+
});
554+
const formElement = await screen.findByPlaceholderText(inputPlaceholder);
555+
const file = getImage();
556+
await act(() => {
557+
dropFile(file, formElement);
558+
});
559+
await waitFor(() => {
560+
expect(mockAddNotification).toHaveBeenCalledTimes(1);
561+
expect(mockAddNotification.mock.calls[0][0]).toContain('File is too large');
562+
});
563+
});
564+
512565
// TODO: Check if pasting plaintext is not prevented -> tricky because recreating exact event is hard
513566
// TODO: Remove image/file -> difficult because there is no easy selector and components are in react-file-utils
514567
});

‎src/components/MessageInput/hooks/utils.ts

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type { ChannelActionContextValue } from '../../../context/ChannelActionCo
55
import type { ChatContextValue } from '../../../context/ChatContext';
66
import type { TranslationContextValue } from '../../../context/TranslationContext';
77
import type { DefaultStreamChatGenerics } from '../../../types/types';
8+
import { DEFAULT_UPLOAD_SIZE_LIMIT_BYTES } from '../../../constants/limits';
89

910
export const accentsMap: { [key: string]: string } = {
1011
a: 'á|à|ã|â|À|Á|Ã|Â',
@@ -136,12 +137,13 @@ export const checkUploadPermissions = async <
136137
allowed_mime_types,
137138
blocked_file_extensions,
138139
blocked_mime_types,
140+
size_limit,
139141
} =
140142
((uploadType === 'image'
141143
? appSettings?.app?.image_upload_config
142144
: appSettings?.app?.file_upload_config) as FileUploadConfig) || {};
143145

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

155157
if (!allowed) {
156-
sendErrorNotification();
158+
sendNotAllowedErrorNotification();
157159
return false;
158160
}
159161
}
@@ -164,7 +166,7 @@ export const checkUploadPermissions = async <
164166
);
165167

166168
if (blocked) {
167-
sendErrorNotification();
169+
sendNotAllowedErrorNotification();
168170
return false;
169171
}
170172
}
@@ -175,7 +177,7 @@ export const checkUploadPermissions = async <
175177
);
176178

177179
if (!allowed) {
178-
sendErrorNotification();
180+
sendNotAllowedErrorNotification();
179181
return false;
180182
}
181183
}
@@ -186,10 +188,29 @@ export const checkUploadPermissions = async <
186188
);
187189

188190
if (blocked) {
189-
sendErrorNotification();
191+
sendNotAllowedErrorNotification();
190192
return false;
191193
}
192194
}
193195

196+
const sizeLimit = size_limit || DEFAULT_UPLOAD_SIZE_LIMIT_BYTES;
197+
if (file.size && file.size > sizeLimit) {
198+
addNotification(
199+
t('File is too large: {{ size }}, maximum upload size is {{ limit }}', {
200+
limit: prettifyFileSize(sizeLimit),
201+
size: prettifyFileSize(file.size),
202+
}),
203+
'error',
204+
);
205+
return false;
206+
}
207+
194208
return true;
195209
};
210+
211+
export function prettifyFileSize(bytes: number, precision = 3) {
212+
const units = ['B', 'kB', 'MB', 'GB'];
213+
const exponent = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1);
214+
const mantissa = bytes / 1024 ** exponent;
215+
return `${mantissa.toPrecision(precision)} ${units[exponent]}`;
216+
}

‎src/constants/limits.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ export const DEFAULT_NEXT_CHANNEL_PAGE_SIZE = 100;
33
export const DEFAULT_JUMP_TO_PAGE_SIZE = 100;
44
export const DEFAULT_THREAD_PAGE_SIZE = 50;
55
export const DEFAULT_LOAD_PAGE_SCROLL_THRESHOLD = 250;
6+
export const DEFAULT_UPLOAD_SIZE_LIMIT_BYTES = 100 * 1024 * 1024; // 100 MB

‎src/i18n/de.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
"Error: {{ errorMessage }}": "Fehler: {{ errorMessage }}",
3232
"Failed to jump to the first unread message": "Fehler beim Springen zur ersten ungelesenen Nachricht",
3333
"Failed to mark channel as read": "Fehler beim Markieren des Kanals als gelesen",
34+
"File is too large: {{ size }}, maximum upload size is {{ limit }}": "Datei ist zu groß: {{ size }}, maximale Upload-Größe beträgt {{ limit }}",
3435
"Flag": "Meldung",
3536
"Latest Messages": "Neueste Nachrichten",
3637
"Load more": "Mehr laden",

‎src/i18n/en.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
"Error: {{ errorMessage }}": "Error: {{ errorMessage }}",
3232
"Failed to jump to the first unread message": "Failed to jump to the first unread message",
3333
"Failed to mark channel as read": "Failed to mark channel as read",
34+
"File is too large: {{ size }}, maximum upload size is {{ limit }}": "File is too large: {{ size }}, maximum upload size is {{ limit }}",
3435
"Flag": "Flag",
3536
"Latest Messages": "Latest Messages",
3637
"Load more": "Load more",

‎src/i18n/es.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
"Error: {{ errorMessage }}": "Error: {{ errorMessage }}",
3232
"Failed to jump to the first unread message": "Error al saltar al primer mensaje no leído",
3333
"Failed to mark channel as read": "Error al marcar el canal como leído",
34+
"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 }}",
3435
"Flag": "Bandera",
3536
"Latest Messages": "Últimos mensajes",
3637
"Load more": "Cargar más",

‎src/i18n/fr.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
"Error: {{ errorMessage }}": "Erreur : {{ errorMessage }}",
3232
"Failed to jump to the first unread message": "Échec de saut vers le premier message non lu",
3333
"Failed to mark channel as read": "Échec de la marque du canal comme lu",
34+
"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 }}",
3435
"Flag": "Signaler",
3536
"Latest Messages": "Derniers messages",
3637
"Load more": "Charger plus",

‎src/i18n/hi.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
"Error: {{ errorMessage }}": "फेल: {{ errorMessage }}",
3333
"Failed to jump to the first unread message": "पहले अपठित संदेश पर जाने में विफल",
3434
"Failed to mark channel as read": "चैनल को पढ़ा हुआ चिह्नित करने में विफल।",
35+
"File is too large: {{ size }}, maximum upload size is {{ limit }}": "फ़ाइल बहुत बड़ी है: {{ size }}, अधिकतम अपलोड साइज़ {{ limit }} है",
3536
"Flag": "फ्लैग करे",
3637
"Latest Messages": "नवीनतम संदेश",
3738
"Load more": "और लोड करें",

‎src/i18n/it.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
"Error: {{ errorMessage }}": "Errore: {{ errorMessage }}",
3232
"Failed to jump to the first unread message": "Impossibile passare al primo messaggio non letto",
3333
"Failed to mark channel as read": "Impossibile contrassegnare il canale come letto",
34+
"File is too large: {{ size }}, maximum upload size is {{ limit }}": "Il file è troppo grande: {{ size }}, la dimensione massima di caricamento è {{ limit }}",
3435
"Flag": "Segnala",
3536
"Latest Messages": "Ultimi messaggi",
3637
"Load more": "Carica di più",

‎src/i18n/ja.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
"Error: {{ errorMessage }}": "エラー: {{ errorMessage }}",
3232
"Failed to jump to the first unread message": "最初の未読メッセージにジャンプできませんでした",
3333
"Failed to mark channel as read": "チャンネルを既読にすることができませんでした",
34+
"File is too large: {{ size }}, maximum upload size is {{ limit }}": "ファイルが大きすぎます:{{ size }}、最大アップロードサイズは{{ limit }}です",
3435
"Flag": "フラグ",
3536
"Latest Messages": "最新のメッセージ",
3637
"Load more": "もっと読み込む",

‎src/i18n/ko.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
"Error: {{ errorMessage }}": "오류: {{ errorMessage }}",
3232
"Failed to jump to the first unread message": "첫 번째 읽지 않은 메시지로 이동하지 못했습니다",
3333
"Failed to mark channel as read": "채널을 읽음으로 표시하는 데 실패했습니다",
34+
"File is too large: {{ size }}, maximum upload size is {{ limit }}": "파일이 너무 큽니다: {{ size }}, 최대 업로드 크기는 {{ limit }}입니다",
3435
"Flag": "플래그",
3536
"Latest Messages": "최신 메시지",
3637
"Load more": "더 불러오기",

‎src/i18n/nl.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
"Error: {{ errorMessage }}": "Error: {{ errorMessage }}",
3232
"Failed to jump to the first unread message": "Niet gelukt om naar het eerste ongelezen bericht te springen",
3333
"Failed to mark channel as read": "Kanaal kon niet als gelezen worden gemarkeerd",
34+
"File is too large: {{ size }}, maximum upload size is {{ limit }}": "Bestand is te groot: {{ size }}, maximale uploadgrootte is {{ limit }}",
3435
"Flag": "Markeer",
3536
"Latest Messages": "Laatste berichten",
3637
"Load more": "Meer laden",

‎src/i18n/pt.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
"Error: {{ errorMessage }}": "Erro: {{ errorMessage }}",
3232
"Failed to jump to the first unread message": "Falha ao pular para a primeira mensagem não lida",
3333
"Failed to mark channel as read": "Falha ao marcar o canal como lido",
34+
"File is too large: {{ size }}, maximum upload size is {{ limit }}": "O arquivo é muito grande: {{ size }}, o tamanho máximo de upload é {{ limit }}",
3435
"Flag": "Reportar",
3536
"Latest Messages": "Mensagens mais recentes",
3637
"Load more": "Carregar mais",

‎src/i18n/ru.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
"Error: {{ errorMessage }}": "Ошибка: {{ errorMessage }}",
3232
"Failed to jump to the first unread message": "Не удалось перейти к первому непрочитанному сообщению",
3333
"Failed to mark channel as read": "Не удалось пометить канал как прочитанный",
34+
"File is too large: {{ size }}, maximum upload size is {{ limit }}": "Файл слишком большой: {{ size }}, максимальный размер загрузки составляет {{ limit }}",
3435
"Flag": "Пожаловаться",
3536
"Latest Messages": "Последние сообщения",
3637
"Load more": "Загрузить больше",

‎src/i18n/tr.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
"Error: {{ errorMessage }}": "Hata: {{ errorMessage }}",
3232
"Failed to jump to the first unread message": "İlk okunmamış mesaja atlamada hata oluştu",
3333
"Failed to mark channel as read": "Kanalı okundu olarak işaretleme başarısız oldu",
34+
"File is too large: {{ size }}, maximum upload size is {{ limit }}": "Dosya çok büyük: {{ size }}, maksimum yükleme boyutu {{ limit }}",
3435
"Flag": "Bayrak",
3536
"Latest Messages": "Son Mesajlar",
3637
"Load more": "Daha fazla yükle",

‎src/mock-builders/index.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,11 @@ const connectUser = (client, user) =>
1818
resolve();
1919
});
2020

21-
function mockClient(client) {
21+
function mockClient(client, mocks = {}) {
2222
jest.spyOn(client, '_setToken').mockImplementation();
2323
jest.spyOn(client, '_setupConnection').mockImplementation();
2424
jest.spyOn(client, '_setupConnection').mockImplementation();
25-
jest.spyOn(client, 'getAppSettings').mockImplementation();
25+
jest.spyOn(client, 'getAppSettings').mockImplementation(mocks.getAppSettings);
2626
client.tokenManager = {
2727
getToken: jest.fn(() => token),
2828
tokenReady: jest.fn(() => true),
@@ -31,7 +31,7 @@ function mockClient(client) {
3131
return client;
3232
}
3333

34-
export const getTestClient = () => mockClient(new StreamChat(apiKey));
34+
export const getTestClient = (mocks) => mockClient(new StreamChat(apiKey), mocks);
3535

3636
export const getTestClientWithUser = async (user = { id: nanoid() }) => {
3737
const client = mockClient(new StreamChat(apiKey));

‎yarn.lock

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11623,11 +11623,6 @@ prettier@^2.2.0:
1162311623
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.2.1.tgz#795a1a78dd52f073da0cd42b21f9c91381923ff5"
1162411624
integrity sha512-PqyhM2yCjg/oKkFPtTGUojv7gnZAoG80ttl45O6x2Ug/rMJw4wcc9k6aaf2hibP7BGVCCM33gZoGjyvt9mm16Q==
1162511625

11626-
pretty-bytes@^5.4.1:
11627-
version "5.6.0"
11628-
resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb"
11629-
integrity sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==
11630-
1163111626
pretty-format@^26.6.2:
1163211627
version "26.6.2"
1163311628
resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-26.6.2.tgz#e35c2705f14cb7fe2fe94fa078345b444120fc93"
@@ -13310,7 +13305,7 @@ stream-browserify@^2.0.1:
1331013305
inherits "~2.0.1"
1331113306
readable-stream "^2.0.2"
1331213307

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

0 commit comments

Comments
 (0)