stream-chat-angular 3.0.0-beta.8 → 3.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/README.md +44 -12
  2. package/assets/version.d.ts +1 -1
  3. package/bundles/stream-chat-angular.umd.js +89 -35
  4. package/bundles/stream-chat-angular.umd.js.map +1 -1
  5. package/esm2015/assets/version.js +2 -2
  6. package/esm2015/lib/attachment-list/attachment-list.component.js +9 -2
  7. package/esm2015/lib/attachment-preview-list/attachment-preview-list.component.js +2 -2
  8. package/esm2015/lib/attachment.service.js +20 -5
  9. package/esm2015/lib/channel.service.js +42 -17
  10. package/esm2015/lib/message/message.component.js +7 -2
  11. package/esm2015/lib/message-input/message-input.component.js +8 -3
  12. package/esm2015/lib/types.js +1 -1
  13. package/fesm2015/stream-chat-angular.js +83 -26
  14. package/fesm2015/stream-chat-angular.js.map +1 -1
  15. package/lib/attachment-list/attachment-list.component.d.ts +1 -0
  16. package/lib/channel.service.d.ts +28 -2
  17. package/lib/types.d.ts +1 -1
  18. package/package.json +2 -2
  19. package/src/assets/styles/css/index.css +1 -1
  20. package/src/assets/styles/css/index.css.map +1 -1
  21. package/src/assets/styles/scss/ActionsBox.scss +2 -2
  22. package/src/assets/styles/scss/Attachment.scss +1 -1
  23. package/src/assets/styles/scss/ChannelList.scss +6 -0
  24. package/src/assets/styles/scss/ChannelSearch.scss +12 -1
  25. package/src/assets/styles/scss/Message.scss +104 -93
  26. package/src/assets/styles/scss/MessageInput.scss +8 -2
  27. package/src/assets/styles/scss/MessageInputFlat.scss +6 -0
  28. package/src/assets/styles/scss/Thread.scss +15 -2
  29. package/src/assets/styles/scss/VirtualMessage.scss +1 -0
  30. package/src/assets/styles/scss/_base.scss +4 -0
  31. package/src/assets/version.ts +1 -1
@@ -17,7 +17,7 @@ import transliterate from '@stream-io/transliterate';
17
17
  import * as i7 from 'angular-mentions';
18
18
  import { MentionModule } from 'angular-mentions';
19
19
 
20
- const version = '3.0.0-beta.8';
20
+ const version = '3.0.1';
21
21
 
22
22
  /**
23
23
  * The `NotificationService` can be used to add or remove notifications. By default the [`NotificationList`](../components/NotificationListComponent.mdx) component displays the currently active notifications.
@@ -253,7 +253,7 @@ const getReadBy = (message, channel) => {
253
253
  };
254
254
 
255
255
  /**
256
- * The `ChannelService` provides data and interaction for the channel list and message list. TEST
256
+ * The `ChannelService` provides data and interaction for the channel list and message list.
257
257
  */
258
258
  class ChannelService {
259
259
  constructor(chatClientService, ngZone) {
@@ -346,6 +346,21 @@ class ChannelService {
346
346
  this.activeThreadMessagesSubject.next([]);
347
347
  this.messageToQuoteSubject.next(undefined);
348
348
  }
349
+ /**
350
+ * Deselects the currently active (if any) channel
351
+ */
352
+ deselectActiveChannel() {
353
+ const activeChannel = this.activeChannelSubject.getValue();
354
+ if (!activeChannel) {
355
+ return;
356
+ }
357
+ this.activeChannelMessagesSubject.next([]);
358
+ this.activeChannelSubject.next(undefined);
359
+ this.activeParentMessageIdSubject.next(undefined);
360
+ this.activeThreadMessagesSubject.next([]);
361
+ this.latestMessageDateByUserByChannelsSubject.next({});
362
+ this.selectMessageToQuote(undefined);
363
+ }
349
364
  /**
350
365
  * Sets the given `message` as an active parent message. If `undefined` is provided, it will deleselect the current parent message.
351
366
  * @param message
@@ -416,8 +431,10 @@ class ChannelService {
416
431
  * @param filters
417
432
  * @param sort
418
433
  * @param options
434
+ * @param shouldSetActiveChannel Decides if the first channel in the result should be made as an active channel, or no channel should be marked as active
435
+ * @returns the list of channels found by the query
419
436
  */
420
- init(filters, sort, options) {
437
+ init(filters, sort, options, shouldSetActiveChannel = true) {
421
438
  return __awaiter(this, void 0, void 0, function* () {
422
439
  this.filters = filters;
423
440
  this.options = options || {
@@ -429,21 +446,17 @@ class ChannelService {
429
446
  message_limit: this.messagePageSize,
430
447
  };
431
448
  this.sort = sort || { last_message_at: -1, updated_at: -1 };
432
- yield this.queryChannels();
449
+ const result = yield this.queryChannels(shouldSetActiveChannel);
433
450
  this.chatClientService.events$.subscribe((notification) => void this.handleNotification(notification));
451
+ return result;
434
452
  });
435
453
  }
436
454
  /**
437
455
  * Resets the `activeChannel$`, `channels$` and `activeChannelMessages$` Observables. Useful when disconnecting a chat user, use in combination with [`disconnectUser`](./ChatClientService.mdx/#disconnectuser).
438
456
  */
439
457
  reset() {
440
- this.activeChannelMessagesSubject.next([]);
441
- this.activeChannelSubject.next(undefined);
442
- this.activeParentMessageIdSubject.next(undefined);
443
- this.activeThreadMessagesSubject.next([]);
458
+ this.deselectActiveChannel();
444
459
  this.channelsSubject.next(undefined);
445
- this.latestMessageDateByUserByChannelsSubject.next({});
446
- this.selectMessageToQuote(undefined);
447
460
  }
448
461
  /**
449
462
  * Loads the next page of channels. The page size can be set in the [query option](https://getstream.io/chat/docs/javascript/query_channels/?language=javascript#query-options) object.
@@ -451,7 +464,7 @@ class ChannelService {
451
464
  loadMoreChannels() {
452
465
  return __awaiter(this, void 0, void 0, function* () {
453
466
  this.options.offset = this.channels.length;
454
- yield this.queryChannels();
467
+ yield this.queryChannels(false);
455
468
  });
456
469
  }
457
470
  /**
@@ -535,8 +548,12 @@ class ChannelService {
535
548
  const result = [];
536
549
  const channel = this.activeChannelSubject.getValue();
537
550
  const uploadResults = yield Promise.allSettled(uploads.map((upload) => upload.type === 'image'
538
- ? channel.sendImage(upload.file)
539
- : channel.sendFile(upload.file)));
551
+ ? this.customImageUploadRequest
552
+ ? this.customImageUploadRequest(upload.file, channel)
553
+ : channel.sendImage(upload.file)
554
+ : this.customFileUploadRequest
555
+ ? this.customFileUploadRequest(upload.file, channel)
556
+ : channel.sendFile(upload.file)));
540
557
  uploadResults.forEach((uploadResult, i) => {
541
558
  const file = uploads[i].file;
542
559
  const type = uploads[i].type;
@@ -563,8 +580,12 @@ class ChannelService {
563
580
  return __awaiter(this, void 0, void 0, function* () {
564
581
  const channel = this.activeChannelSubject.getValue();
565
582
  yield (attachmentUpload.type === 'image'
566
- ? channel.deleteImage(attachmentUpload.url)
567
- : channel.deleteFile(attachmentUpload.url));
583
+ ? this.customImageDeleteRequest
584
+ ? this.customImageDeleteRequest(attachmentUpload.url, channel)
585
+ : channel.deleteImage(attachmentUpload.url)
586
+ : this.customFileDeleteRequest
587
+ ? this.customFileDeleteRequest(attachmentUpload.url, channel)
588
+ : channel.deleteFile(attachmentUpload.url));
568
589
  });
569
590
  }
570
591
  /**
@@ -865,20 +886,24 @@ class ChannelService {
865
886
  this.activeChannelSubscriptions.forEach((s) => s.unsubscribe());
866
887
  this.activeChannelSubscriptions = [];
867
888
  }
868
- queryChannels() {
889
+ queryChannels(shouldSetActiveChannel) {
869
890
  return __awaiter(this, void 0, void 0, function* () {
870
891
  try {
871
892
  const channels = yield this.chatClientService.chatClient.queryChannels(this.filters, this.sort, this.options);
872
893
  channels.forEach((c) => this.watchForChannelEvents(c));
873
894
  const prevChannels = this.channelsSubject.getValue() || [];
874
895
  this.channelsSubject.next([...prevChannels, ...channels]);
875
- if (channels.length > 0 && !this.activeChannelSubject.getValue()) {
896
+ if (channels.length > 0 &&
897
+ !this.activeChannelSubject.getValue() &&
898
+ shouldSetActiveChannel) {
876
899
  this.setAsActiveChannel(channels[0]);
877
900
  }
878
901
  this.hasMoreChannelsSubject.next(channels.length >= this.options.limit);
902
+ return channels;
879
903
  }
880
904
  catch (error) {
881
905
  this.channelsSubject.error(error);
906
+ throw error;
882
907
  }
883
908
  });
884
909
  }
@@ -1244,10 +1269,14 @@ class AttachmentService {
1244
1269
  }
1245
1270
  const imageFiles = [];
1246
1271
  const dataFiles = [];
1272
+ const videoFiles = [];
1247
1273
  Array.from(fileList).forEach((file) => {
1248
1274
  if (isImageFile(file)) {
1249
1275
  imageFiles.push(file);
1250
1276
  }
1277
+ else if (file.type.startsWith('video/')) {
1278
+ videoFiles.push(file);
1279
+ }
1251
1280
  else {
1252
1281
  dataFiles.push(file);
1253
1282
  }
@@ -1259,6 +1288,11 @@ class AttachmentService {
1259
1288
  state: 'uploading',
1260
1289
  type: 'image',
1261
1290
  })),
1291
+ ...videoFiles.map((file) => ({
1292
+ file,
1293
+ state: 'uploading',
1294
+ type: 'video',
1295
+ })),
1262
1296
  ...dataFiles.map((file) => ({
1263
1297
  file,
1264
1298
  state: 'uploading',
@@ -1296,19 +1330,25 @@ class AttachmentService {
1296
1330
  deleteAttachment(upload) {
1297
1331
  return __awaiter(this, void 0, void 0, function* () {
1298
1332
  const attachmentUploads = this.attachmentUploadsSubject.getValue();
1333
+ let result;
1299
1334
  if (upload.state === 'success') {
1300
1335
  try {
1301
1336
  yield this.channelService.deleteAttachment(upload);
1302
- attachmentUploads.splice(attachmentUploads.indexOf(upload), 1);
1337
+ result = [...attachmentUploads];
1338
+ const index = attachmentUploads.indexOf(upload);
1339
+ result.splice(index, 1);
1303
1340
  }
1304
1341
  catch (error) {
1342
+ result = attachmentUploads;
1305
1343
  this.notificationService.addTemporaryNotification('streamChat.Error deleting attachment');
1306
1344
  }
1307
1345
  }
1308
1346
  else {
1309
- attachmentUploads.splice(attachmentUploads.indexOf(upload), 1);
1347
+ result = [...attachmentUploads];
1348
+ const index = attachmentUploads.indexOf(upload);
1349
+ result.splice(index, 1);
1310
1350
  }
1311
- this.attachmentUploadsSubject.next([...attachmentUploads]);
1351
+ this.attachmentUploadsSubject.next([...result]);
1312
1352
  });
1313
1353
  }
1314
1354
  /**
@@ -1390,9 +1430,9 @@ class AttachmentService {
1390
1430
  }
1391
1431
  uploadAttachments(uploads) {
1392
1432
  return __awaiter(this, void 0, void 0, function* () {
1393
- const attachmentUploads = this.attachmentUploadsSubject.getValue();
1394
1433
  this.attachmentUploadInProgressCounterSubject.next(this.attachmentUploadInProgressCounterSubject.getValue() + 1);
1395
1434
  const result = yield this.channelService.uploadAttachments(uploads);
1435
+ const attachmentUploads = this.attachmentUploadsSubject.getValue();
1396
1436
  result.forEach((r) => {
1397
1437
  const upload = attachmentUploads.find((upload) => upload.file === r.file);
1398
1438
  if (!upload) {
@@ -2094,6 +2134,7 @@ class AttachmentListComponent {
2094
2134
  const containsGallery = images.length >= 2;
2095
2135
  this.orderedAttachments = [
2096
2136
  ...(containsGallery ? this.createGallery(images) : images),
2137
+ ...this.attachments.filter((a) => this.isVideo(a)),
2097
2138
  ...this.attachments.filter((a) => this.isFile(a)),
2098
2139
  ...this.attachments.filter((a) => this.isCard(a)),
2099
2140
  ];
@@ -2110,6 +2151,12 @@ class AttachmentListComponent {
2110
2151
  isGallery(attachment) {
2111
2152
  return attachment.type === 'gallery';
2112
2153
  }
2154
+ isVideo(attachment) {
2155
+ return (attachment.type === 'video' &&
2156
+ attachment.asset_url &&
2157
+ !attachment.og_scrape_url // links from video share services (such as YouTube or Facebook) are can't be played
2158
+ );
2159
+ }
2113
2160
  isCard(attachment) {
2114
2161
  return (!attachment.type ||
2115
2162
  (attachment.type === 'image' && !this.isImage(attachment)) ||
@@ -2177,7 +2224,7 @@ class AttachmentListComponent {
2177
2224
  }
2178
2225
  }
2179
2226
  AttachmentListComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "12.2.5", ngImport: i0, type: AttachmentListComponent, deps: [{ token: CustomTemplatesService }, { token: ImageLoadService }, { token: ChannelService }], target: i0.ɵɵFactoryTarget.Component });
2180
- AttachmentListComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "12.2.5", type: AttachmentListComponent, selector: "stream-attachment-list", inputs: { messageId: "messageId", attachments: "attachments" }, viewQueries: [{ propertyName: "modalContent", first: true, predicate: ["modalContent"], descendants: true, static: true }], usesOnChanges: true, ngImport: i0, template: "<ng-container *ngFor=\"let attachment of orderedAttachments; trackBy: trackById\">\n <div\n data-testclass=\"attachment-container\"\n class=\"str-chat__message-attachment str-chat__message-attachment--{{\n attachment.type\n }}\"\n [class.str-chat__message-attachment--card]=\"isCard(attachment)\"\n [class.str-chat-angular__message-attachment-file-single]=\"\n isFile(attachment)\n \"\n >\n <img\n *ngIf=\"isImage(attachment)\"\n class=\"str-chat__message-attachment--img\"\n data-testclass=\"image\"\n [src]=\"attachment.img_url || attachment.thumb_url || attachment.image_url\"\n [alt]=\"attachment?.fallback\"\n (load)=\"imageLoaded()\"\n (click)=\"openImageModal([attachment])\"\n (keyup.enter)=\"openImageModal([attachment])\"\n />\n <div\n class=\"str-chat__gallery\"\n data-testid=\"image-gallery\"\n *ngIf=\"isGallery(attachment)\"\n [class.str-chat__gallery--square]=\"(attachment?.images)!.length > 3\"\n >\n <ng-container\n *ngFor=\"\n let galleryImage of attachment.images;\n let index = index;\n let isLast = last;\n trackBy: trackByImageUrl\n \"\n >\n <button\n *ngIf=\"index < 3 || (index === 3 && isLast)\"\n class=\"str-chat__gallery-image\"\n data-testclass=\"gallery-image\"\n (click)=\"openImageModal(attachment.images!, index)\"\n (keyup.enter)=\"openImageModal(attachment.images!, index)\"\n >\n <img\n [src]=\"\n galleryImage.img_url ||\n galleryImage.thumb_url ||\n galleryImage.image_url\n \"\n [alt]=\"galleryImage.fallback\"\n (load)=\"imageLoaded()\"\n />\n </button>\n <button\n *ngIf=\"index === 3 && !isLast\"\n class=\"str-chat__gallery-placeholder\"\n data-testclass=\"gallery-image\"\n (click)=\"openImageModal(attachment.images!, index)\"\n (keyup.enter)=\"openImageModal(attachment.images!, index)\"\n [ngStyle]=\"{\n 'background-image':\n 'url(' +\n (galleryImage.img_url ||\n galleryImage.thumb_url ||\n galleryImage.image_url) +\n ')'\n }\"\n >\n <p\n [innerHTML]=\"\n 'streamChat.{{ imageCount }} more'\n | translate: { imageCount: attachment!.images!.length - 4 }\n \"\n ></p>\n </button>\n </ng-container>\n </div>\n <div\n *ngIf=\"isFile(attachment)\"\n class=\"\n str-chat__message-attachment-file--item\n str-chat-angular__message-attachment-file-single\n \"\n >\n <stream-icon-placeholder\n icon=\"file\"\n [size]=\"30\"\n ></stream-icon-placeholder>\n <div class=\"str-chat__message-attachment-file--item-text\">\n <a\n data-testclass=\"file-link\"\n download\n href=\"{{ attachment.asset_url }}\"\n target=\"_blank\"\n >\n {{ attachment.title }}\n </a>\n <span data-testclass=\"size\" *ngIf=\"hasFileSize(attachment)\">{{\n getFileSize(attachment)\n }}</span>\n </div>\n </div>\n <div\n *ngIf=\"isCard(attachment)\"\n class=\"str-chat__message-attachment-card str-chat__message-attachment-card--{{\n attachment.type\n }}\"\n >\n <div\n *ngIf=\"attachment.image_url || attachment.thumb_url\"\n class=\"str-chat__message-attachment-card--header\"\n >\n <img\n data-testclass=\"card-img\"\n alt=\"{{ attachment.image_url || attachment.thumb_url }}\"\n src=\"{{ attachment.image_url || attachment.thumb_url }}\"\n />\n </div>\n <div class=\"str-chat__message-attachment-card--content\">\n <div class=\"str-chat__message-attachment-card--flex\">\n <div\n *ngIf=\"attachment.title\"\n data-testclass=\"card-title\"\n class=\"str-chat__message-attachment-card--title\"\n >\n {{ attachment.title }}\n </div>\n <div\n *ngIf=\"attachment.text\"\n class=\"str-chat__message-attachment-card--text\"\n data-testclass=\"card-text\"\n >\n {{ attachment.text }}\n </div>\n <a\n class=\"str-chat__message-attachment-card--url\"\n *ngIf=\"attachment.title_link || attachment.og_scrape_url\"\n data-testclass=\"url-link\"\n noopener\n noreferrer\n href=\"{{ attachment.title_link || attachment.og_scrape_url }}\"\n target=\"_blank\"\n >\n {{ trimUrl(attachment.title_link || attachment.og_scrape_url) }}\n </a>\n </div>\n </div>\n </div>\n <div\n class=\"str-chat__message-attachment-actions\"\n *ngIf=\"attachment.actions && attachment.actions.length > 0\"\n >\n <div class=\"str-chat__message-attachment-actions-form\">\n <button\n *ngFor=\"let action of attachment.actions; trackBy: trackByActionValue\"\n class=\"str-chat__message-attachment-actions-button str-chat__message-attachment-actions-button--{{\n action.style\n }}\"\n data-testclass=\"attachment-action\"\n (click)=\"sendAction(action)\"\n (keyup.enter)=\"sendAction(action)\"\n >\n {{ action.text }}\n </button>\n </div>\n </div>\n </div>\n</ng-container>\n\n<ng-container *ngIf=\"imagesToView && imagesToView.length > 0\">\n <ng-container\n *ngTemplateOutlet=\"\n (customTemplatesService.modalTemplate$ | async) || defaultModal;\n context: getModalContext()\n \"\n ></ng-container>\n</ng-container>\n\n<ng-template\n #defaultModal\n let-isOpen=\"isOpen\"\n let-isOpenChangeHandler=\"isOpenChangeHandler\"\n let-content=\"content\"\n>\n <stream-modal\n [isOpen]=\"isOpen\"\n (isOpenChange)=\"isOpenChangeHandler($event)\"\n [content]=\"content\"\n >\n </stream-modal>\n</ng-template>\n\n<ng-template #modalContent>\n <div class=\"stream-chat-angular__image-modal\">\n <button\n class=\"stream-chat-angular__image-modal-stepper\"\n [ngStyle]=\"{\n visibility: isImageModalPrevButtonVisible ? 'visible' : 'hidden'\n }\"\n data-testid=\"image-modal-prev\"\n type=\"button\"\n (click)=\"stepImages(-1)\"\n (keyup.enter)=\"stepImages(-1)\"\n >\n <stream-icon-placeholder icon=\"arrow-left\"></stream-icon-placeholder>\n </button>\n <img\n class=\"stream-chat-angular__image-modal-image\"\n data-testid=\"modal-image\"\n [src]=\"\n imagesToView[imagesToViewCurrentIndex].img_url ||\n imagesToView[imagesToViewCurrentIndex].thumb_url ||\n imagesToView[imagesToViewCurrentIndex].image_url\n \"\n [alt]=\"imagesToView[imagesToViewCurrentIndex].fallback\"\n />\n <button\n class=\"stream-chat-angular__image-modal-stepper\"\n type=\"button\"\n [ngStyle]=\"{\n visibility: isImageModalNextButtonVisible ? 'visible' : 'hidden'\n }\"\n data-testid=\"image-modal-next\"\n (click)=\"stepImages(1)\"\n (keyup.enter)=\"stepImages(1)\"\n >\n <stream-icon-placeholder icon=\"arrow-right\"></stream-icon-placeholder>\n </button>\n </div>\n</ng-template>\n", components: [{ type: IconPlaceholderComponent, selector: "stream-icon-placeholder", inputs: ["icon", "size"] }, { type: ModalComponent, selector: "stream-modal", inputs: ["isOpen", "content"], outputs: ["isOpenChange"] }], directives: [{ type: i3.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { type: i3.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { type: i3.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { type: i3.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet"] }], pipes: { "translate": i2.TranslatePipe, "async": i3.AsyncPipe } });
2227
+ AttachmentListComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "12.2.5", type: AttachmentListComponent, selector: "stream-attachment-list", inputs: { messageId: "messageId", attachments: "attachments" }, viewQueries: [{ propertyName: "modalContent", first: true, predicate: ["modalContent"], descendants: true, static: true }], usesOnChanges: true, ngImport: i0, template: "<ng-container *ngFor=\"let attachment of orderedAttachments; trackBy: trackById\">\n <div\n data-testclass=\"attachment-container\"\n class=\"str-chat__message-attachment str-chat__message-attachment--{{\n attachment.type\n }}\"\n [class.str-chat__message-attachment--card]=\"isCard(attachment)\"\n [class.str-chat-angular__message-attachment-file-single]=\"\n isFile(attachment)\n \"\n >\n <img\n *ngIf=\"isImage(attachment)\"\n class=\"str-chat__message-attachment--img\"\n data-testclass=\"image\"\n [src]=\"attachment.img_url || attachment.thumb_url || attachment.image_url\"\n [alt]=\"attachment?.fallback\"\n (load)=\"imageLoaded()\"\n (click)=\"openImageModal([attachment])\"\n (keyup.enter)=\"openImageModal([attachment])\"\n />\n <div\n class=\"str-chat__gallery\"\n data-testid=\"image-gallery\"\n *ngIf=\"isGallery(attachment)\"\n [class.str-chat__gallery--square]=\"(attachment?.images)!.length > 3\"\n >\n <ng-container\n *ngFor=\"\n let galleryImage of attachment.images;\n let index = index;\n let isLast = last;\n trackBy: trackByImageUrl\n \"\n >\n <button\n *ngIf=\"index < 3 || (index === 3 && isLast)\"\n class=\"str-chat__gallery-image\"\n data-testclass=\"gallery-image\"\n (click)=\"openImageModal(attachment.images!, index)\"\n (keyup.enter)=\"openImageModal(attachment.images!, index)\"\n >\n <img\n [src]=\"\n galleryImage.img_url ||\n galleryImage.thumb_url ||\n galleryImage.image_url\n \"\n [alt]=\"galleryImage.fallback\"\n (load)=\"imageLoaded()\"\n />\n </button>\n <button\n *ngIf=\"index === 3 && !isLast\"\n class=\"str-chat__gallery-placeholder\"\n data-testclass=\"gallery-image\"\n (click)=\"openImageModal(attachment.images!, index)\"\n (keyup.enter)=\"openImageModal(attachment.images!, index)\"\n [ngStyle]=\"{\n 'background-image':\n 'url(' +\n (galleryImage.img_url ||\n galleryImage.thumb_url ||\n galleryImage.image_url) +\n ')'\n }\"\n >\n <p\n [innerHTML]=\"\n 'streamChat.{{ imageCount }} more'\n | translate: { imageCount: attachment!.images!.length - 4 }\n \"\n ></p>\n </button>\n </ng-container>\n </div>\n <video\n *ngIf=\"isVideo(attachment)\"\n controls\n data-testclass=\"video-attachment\"\n [src]=\"attachment.asset_url\"\n style=\"\n width: 100%;\n max-width: 400px;\n height: 300px;\n border-radius: inherit;\n \"\n ></video>\n <div\n *ngIf=\"isFile(attachment)\"\n class=\"\n str-chat__message-attachment-file--item\n str-chat-angular__message-attachment-file-single\n \"\n >\n <stream-icon-placeholder\n icon=\"file\"\n [size]=\"30\"\n ></stream-icon-placeholder>\n <div class=\"str-chat__message-attachment-file--item-text\">\n <a\n data-testclass=\"file-link\"\n download\n href=\"{{ attachment.asset_url }}\"\n target=\"_blank\"\n >\n {{ attachment.title }}\n </a>\n <span data-testclass=\"size\" *ngIf=\"hasFileSize(attachment)\">{{\n getFileSize(attachment)\n }}</span>\n </div>\n </div>\n <div\n *ngIf=\"isCard(attachment)\"\n class=\"str-chat__message-attachment-card str-chat__message-attachment-card--{{\n attachment.type\n }}\"\n >\n <div\n *ngIf=\"attachment.image_url || attachment.thumb_url\"\n class=\"str-chat__message-attachment-card--header\"\n >\n <img\n data-testclass=\"card-img\"\n alt=\"{{ attachment.image_url || attachment.thumb_url }}\"\n src=\"{{ attachment.image_url || attachment.thumb_url }}\"\n />\n </div>\n <div class=\"str-chat__message-attachment-card--content\">\n <div class=\"str-chat__message-attachment-card--flex\">\n <div\n *ngIf=\"attachment.title\"\n data-testclass=\"card-title\"\n class=\"str-chat__message-attachment-card--title\"\n >\n {{ attachment.title }}\n </div>\n <div\n *ngIf=\"attachment.text\"\n class=\"str-chat__message-attachment-card--text\"\n data-testclass=\"card-text\"\n >\n {{ attachment.text }}\n </div>\n <a\n class=\"str-chat__message-attachment-card--url\"\n *ngIf=\"attachment.title_link || attachment.og_scrape_url\"\n data-testclass=\"url-link\"\n noopener\n noreferrer\n href=\"{{ attachment.title_link || attachment.og_scrape_url }}\"\n target=\"_blank\"\n >\n {{ trimUrl(attachment.title_link || attachment.og_scrape_url) }}\n </a>\n </div>\n </div>\n </div>\n <div\n class=\"str-chat__message-attachment-actions\"\n *ngIf=\"attachment.actions && attachment.actions.length > 0\"\n >\n <div class=\"str-chat__message-attachment-actions-form\">\n <button\n *ngFor=\"let action of attachment.actions; trackBy: trackByActionValue\"\n class=\"str-chat__message-attachment-actions-button str-chat__message-attachment-actions-button--{{\n action.style\n }}\"\n data-testclass=\"attachment-action\"\n (click)=\"sendAction(action)\"\n (keyup.enter)=\"sendAction(action)\"\n >\n {{ action.text }}\n </button>\n </div>\n </div>\n </div>\n</ng-container>\n\n<ng-container *ngIf=\"imagesToView && imagesToView.length > 0\">\n <ng-container\n *ngTemplateOutlet=\"\n (customTemplatesService.modalTemplate$ | async) || defaultModal;\n context: getModalContext()\n \"\n ></ng-container>\n</ng-container>\n\n<ng-template\n #defaultModal\n let-isOpen=\"isOpen\"\n let-isOpenChangeHandler=\"isOpenChangeHandler\"\n let-content=\"content\"\n>\n <stream-modal\n [isOpen]=\"isOpen\"\n (isOpenChange)=\"isOpenChangeHandler($event)\"\n [content]=\"content\"\n >\n </stream-modal>\n</ng-template>\n\n<ng-template #modalContent>\n <div class=\"stream-chat-angular__image-modal\">\n <button\n class=\"stream-chat-angular__image-modal-stepper\"\n [ngStyle]=\"{\n visibility: isImageModalPrevButtonVisible ? 'visible' : 'hidden'\n }\"\n data-testid=\"image-modal-prev\"\n type=\"button\"\n (click)=\"stepImages(-1)\"\n (keyup.enter)=\"stepImages(-1)\"\n >\n <stream-icon-placeholder icon=\"arrow-left\"></stream-icon-placeholder>\n </button>\n <img\n class=\"stream-chat-angular__image-modal-image\"\n data-testid=\"modal-image\"\n [src]=\"\n imagesToView[imagesToViewCurrentIndex].img_url ||\n imagesToView[imagesToViewCurrentIndex].thumb_url ||\n imagesToView[imagesToViewCurrentIndex].image_url\n \"\n [alt]=\"imagesToView[imagesToViewCurrentIndex].fallback\"\n />\n <button\n class=\"stream-chat-angular__image-modal-stepper\"\n type=\"button\"\n [ngStyle]=\"{\n visibility: isImageModalNextButtonVisible ? 'visible' : 'hidden'\n }\"\n data-testid=\"image-modal-next\"\n (click)=\"stepImages(1)\"\n (keyup.enter)=\"stepImages(1)\"\n >\n <stream-icon-placeholder icon=\"arrow-right\"></stream-icon-placeholder>\n </button>\n </div>\n</ng-template>\n", components: [{ type: IconPlaceholderComponent, selector: "stream-icon-placeholder", inputs: ["icon", "size"] }, { type: ModalComponent, selector: "stream-modal", inputs: ["isOpen", "content"], outputs: ["isOpenChange"] }], directives: [{ type: i3.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { type: i3.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { type: i3.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { type: i3.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet"] }], pipes: { "translate": i2.TranslatePipe, "async": i3.AsyncPipe } });
2181
2228
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "12.2.5", ngImport: i0, type: AttachmentListComponent, decorators: [{
2182
2229
  type: Component,
2183
2230
  args: [{
@@ -2219,7 +2266,7 @@ class AttachmentPreviewListComponent {
2219
2266
  }
2220
2267
  }
2221
2268
  AttachmentPreviewListComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "12.2.5", ngImport: i0, type: AttachmentPreviewListComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2222
- AttachmentPreviewListComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "12.2.5", type: AttachmentPreviewListComponent, selector: "stream-attachment-preview-list", inputs: { attachmentUploads$: "attachmentUploads$" }, outputs: { retryAttachmentUpload: "retryAttachmentUpload", deleteAttachment: "deleteAttachment" }, ngImport: i0, template: "<div class=\"rfu-image-previewer\" *ngIf=\"(attachmentUploads$ | async)?.length\">\n <ng-container\n *ngFor=\"\n let attachmentUpload of attachmentUploads$ | async;\n trackBy: trackByFile\n \"\n >\n <div\n *ngIf=\"attachmentUpload.type === 'image'\"\n class=\"rfu-image-previewer__image\"\n [class.rfu-image-previewer__image--loaded]=\"\n attachmentUpload.state === 'success'\n \"\n data-testclass=\"attachment-image-preview\"\n >\n <div\n *ngIf=\"attachmentUpload.state === 'error'\"\n class=\"rfu-image-previewer__retry\"\n (click)=\"attachmentUploadRetried(attachmentUpload.file)\"\n (keyup.enter)=\"attachmentUploadRetried(attachmentUpload.file)\"\n data-testclass=\"upload-error\"\n >\n <stream-icon-placeholder icon=\"retry\"></stream-icon-placeholder>\n </div>\n <div class=\"rfu-thumbnail__wrapper\" style=\"width: 100; height: 100\">\n <div class=\"rfu-thumbnail__overlay\">\n <div\n class=\"rfu-icon-button\"\n data-testclass=\"delete-attachment\"\n role=\"button\"\n (click)=\"attachmentDeleted(attachmentUpload)\"\n (keyup.enter)=\"attachmentDeleted(attachmentUpload)\"\n >\n <stream-icon-placeholder icon=\"close\"></stream-icon-placeholder>\n </div>\n </div>\n <img\n *ngIf=\"attachmentUpload.url || attachmentUpload.previewUri\"\n src=\"{{\n attachmentUpload.url\n ? attachmentUpload.url\n : attachmentUpload.previewUri\n }}\"\n alt=\"attachmentUpload.file.name\"\n class=\"rfu-thumbnail__image\"\n data-testclass=\"attachment-image\"\n />\n </div>\n <stream-loading-indicator-placeholder\n data-testclass=\"loading-indicator\"\n color=\"rgba(255,255,255,0.7)\"\n *ngIf=\"attachmentUpload.state === 'uploading'\"\n ></stream-loading-indicator-placeholder>\n </div>\n <div\n class=\"rfu-file-previewer\"\n *ngIf=\"attachmentUpload.type === 'file'\"\n data-testclass=\"attachment-file-preview\"\n >\n <ol>\n <li\n class=\"rfu-file-previewer__file\"\n [class.rfu-file-previewer__file--uploading]=\"\n attachmentUpload.state === 'uploading'\n \"\n [class.rfu-file-previewer__file--failed]=\"\n attachmentUpload.state === 'error'\n \"\n >\n <stream-icon-placeholder icon=\"file\"></stream-icon-placeholder>\n\n <a\n data-testclass=\"file-download-link\"\n href=\"{{ attachmentUpload.url }}\"\n (click)=\"attachmentUpload.url ? null : $event.preventDefault()\"\n (keyup.enter)=\"\n attachmentUpload.url ? null : $event.preventDefault()\n \"\n download\n >\n {{ attachmentUpload.file.name }}\n <ng-container *ngIf=\"attachmentUpload.state === 'error'\">\n <div\n data-testclass=\"file-upload-retry\"\n class=\"rfu-file-previewer__failed\"\n (click)=\"attachmentUploadRetried(attachmentUpload.file)\"\n (keyup.enter)=\"attachmentUploadRetried(attachmentUpload.file)\"\n translate\n >\n streamChat.failed\n </div>\n <div\n class=\"rfu-file-previewer__retry\"\n (click)=\"attachmentUploadRetried(attachmentUpload.file)\"\n (keyup.enter)=\"attachmentUploadRetried(attachmentUpload.file)\"\n translate\n >\n streamChat.retry\n </div>\n </ng-container>\n </a>\n\n <span\n data-testclass=\"file-delete\"\n class=\"rfu-file-previewer__close-button\"\n (click)=\"attachmentDeleted(attachmentUpload)\"\n (keyup.enter)=\"attachmentDeleted(attachmentUpload)\"\n >\n \u2718\n </span>\n <div\n *ngIf=\"attachmentUpload.state === 'uploading'\"\n class=\"rfu-file-previewer__loading-indicator\"\n >\n <stream-loading-indicator-placeholder></stream-loading-indicator-placeholder>\n </div>\n </li>\n </ol>\n </div>\n </ng-container>\n</div>\n", components: [{ type: IconPlaceholderComponent, selector: "stream-icon-placeholder", inputs: ["icon", "size"] }, { type: LoadingIndicatorPlaceholderComponent, selector: "stream-loading-indicator-placeholder", inputs: ["size", "color"] }], directives: [{ type: i3.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { type: i3.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { type: i2.TranslateDirective, selector: "[translate],[ngx-translate]", inputs: ["translate", "translateParams"] }], pipes: { "async": i3.AsyncPipe } });
2269
+ AttachmentPreviewListComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "12.2.5", type: AttachmentPreviewListComponent, selector: "stream-attachment-preview-list", inputs: { attachmentUploads$: "attachmentUploads$" }, outputs: { retryAttachmentUpload: "retryAttachmentUpload", deleteAttachment: "deleteAttachment" }, ngImport: i0, template: "<div class=\"rfu-image-previewer\" *ngIf=\"(attachmentUploads$ | async)?.length\">\n <ng-container\n *ngFor=\"\n let attachmentUpload of attachmentUploads$ | async;\n trackBy: trackByFile\n \"\n >\n <div\n *ngIf=\"attachmentUpload.type === 'image'\"\n class=\"rfu-image-previewer__image\"\n [class.rfu-image-previewer__image--loaded]=\"\n attachmentUpload.state === 'success'\n \"\n data-testclass=\"attachment-image-preview\"\n >\n <div\n *ngIf=\"attachmentUpload.state === 'error'\"\n class=\"rfu-image-previewer__retry\"\n (click)=\"attachmentUploadRetried(attachmentUpload.file)\"\n (keyup.enter)=\"attachmentUploadRetried(attachmentUpload.file)\"\n data-testclass=\"upload-error\"\n >\n <stream-icon-placeholder icon=\"retry\"></stream-icon-placeholder>\n </div>\n <div class=\"rfu-thumbnail__wrapper\" style=\"width: 100; height: 100\">\n <div class=\"rfu-thumbnail__overlay\">\n <div\n class=\"rfu-icon-button\"\n data-testclass=\"delete-attachment\"\n role=\"button\"\n (click)=\"attachmentDeleted(attachmentUpload)\"\n (keyup.enter)=\"attachmentDeleted(attachmentUpload)\"\n >\n <stream-icon-placeholder icon=\"close\"></stream-icon-placeholder>\n </div>\n </div>\n <img\n *ngIf=\"attachmentUpload.url || attachmentUpload.previewUri\"\n src=\"{{\n attachmentUpload.url\n ? attachmentUpload.url\n : attachmentUpload.previewUri\n }}\"\n alt=\"attachmentUpload.file.name\"\n class=\"rfu-thumbnail__image\"\n data-testclass=\"attachment-image\"\n />\n </div>\n <stream-loading-indicator-placeholder\n data-testclass=\"loading-indicator\"\n color=\"rgba(255,255,255,0.7)\"\n *ngIf=\"attachmentUpload.state === 'uploading'\"\n ></stream-loading-indicator-placeholder>\n </div>\n <div\n class=\"rfu-file-previewer\"\n *ngIf=\"\n attachmentUpload.type === 'file' || attachmentUpload.type === 'video'\n \"\n data-testclass=\"attachment-file-preview\"\n >\n <ol>\n <li\n class=\"rfu-file-previewer__file\"\n [class.rfu-file-previewer__file--uploading]=\"\n attachmentUpload.state === 'uploading'\n \"\n [class.rfu-file-previewer__file--failed]=\"\n attachmentUpload.state === 'error'\n \"\n >\n <stream-icon-placeholder icon=\"file\"></stream-icon-placeholder>\n\n <a\n data-testclass=\"file-download-link\"\n href=\"{{ attachmentUpload.url }}\"\n (click)=\"attachmentUpload.url ? null : $event.preventDefault()\"\n (keyup.enter)=\"\n attachmentUpload.url ? null : $event.preventDefault()\n \"\n download\n >\n {{ attachmentUpload.file.name }}\n <ng-container *ngIf=\"attachmentUpload.state === 'error'\">\n <div\n data-testclass=\"file-upload-retry\"\n class=\"rfu-file-previewer__failed\"\n (click)=\"attachmentUploadRetried(attachmentUpload.file)\"\n (keyup.enter)=\"attachmentUploadRetried(attachmentUpload.file)\"\n translate\n >\n streamChat.failed\n </div>\n <div\n class=\"rfu-file-previewer__retry\"\n (click)=\"attachmentUploadRetried(attachmentUpload.file)\"\n (keyup.enter)=\"attachmentUploadRetried(attachmentUpload.file)\"\n translate\n >\n streamChat.retry\n </div>\n </ng-container>\n </a>\n\n <span\n data-testclass=\"file-delete\"\n class=\"rfu-file-previewer__close-button\"\n (click)=\"attachmentDeleted(attachmentUpload)\"\n (keyup.enter)=\"attachmentDeleted(attachmentUpload)\"\n >\n \u2718\n </span>\n <div\n *ngIf=\"attachmentUpload.state === 'uploading'\"\n class=\"rfu-file-previewer__loading-indicator\"\n >\n <stream-loading-indicator-placeholder></stream-loading-indicator-placeholder>\n </div>\n </li>\n </ol>\n </div>\n </ng-container>\n</div>\n", components: [{ type: IconPlaceholderComponent, selector: "stream-icon-placeholder", inputs: ["icon", "size"] }, { type: LoadingIndicatorPlaceholderComponent, selector: "stream-loading-indicator-placeholder", inputs: ["size", "color"] }], directives: [{ type: i3.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { type: i3.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { type: i2.TranslateDirective, selector: "[translate],[ngx-translate]", inputs: ["translate", "translateParams"] }], pipes: { "async": i3.AsyncPipe } });
2223
2270
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "12.2.5", ngImport: i0, type: AttachmentPreviewListComponent, decorators: [{
2224
2271
  type: Component,
2225
2272
  args: [{
@@ -2388,10 +2435,15 @@ class MessageInputComponent {
2388
2435
  return;
2389
2436
  }
2390
2437
  const attachments = this.attachmentService.mapToAttachments();
2391
- const text = this.textareaValue;
2392
- if (!text && (!attachments || attachments.length === 0)) {
2438
+ let text = this.textareaValue;
2439
+ const textContainsOnlySpaceChars = !text.replace(/ /g, ''); //spcae
2440
+ if ((!text || textContainsOnlySpaceChars) &&
2441
+ (!attachments || attachments.length === 0)) {
2393
2442
  return;
2394
2443
  }
2444
+ if (textContainsOnlySpaceChars) {
2445
+ text = '';
2446
+ }
2395
2447
  if (this.containsLinks && !this.canSendLinks) {
2396
2448
  this.notificationService.addTemporaryNotification('streamChat.Sending links is not allowed in this conversation');
2397
2449
  return;
@@ -3513,7 +3565,12 @@ class MessageComponent {
3513
3565
  this.message.mentioned_users.length === 0) {
3514
3566
  // Wrap emojis in span to display emojis correctly in Chrome https://bugs.chromium.org/p/chromium/issues/detail?id=596223
3515
3567
  const regex = new RegExp(emojiRegex(), 'g');
3516
- content = content.replace(regex, (match) => `<span class="str-chat__emoji-display-fix">${match}</span>`);
3568
+ // Based on this: https://stackoverflow.com/questions/4565112/javascript-how-to-find-out-if-the-user-browser-is-chrome
3569
+ /* eslint-disable @typescript-eslint/no-unsafe-member-access */
3570
+ const isChrome = !!window.chrome &&
3571
+ typeof window.opr === 'undefined';
3572
+ /* eslint-enable @typescript-eslint/no-unsafe-member-access */
3573
+ content = content.replace(regex, (match) => `<span ${isChrome ? 'class="str-chat__emoji-display-fix"' : ''}>${match}</span>`);
3517
3574
  this.messageTextParts = [{ content, type: 'text' }];
3518
3575
  }
3519
3576
  else {