Rewrite: Message Attachments (#20106)

pull/20123/head
Guilherme Gazzo 5 years ago committed by GitHub
parent 9d4d7733cc
commit 9b33bbe5d0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 6
      KNOWN_ISSUES.md
  2. 3
      app/message-action/client/index.js
  3. 31
      app/message-action/client/messageAction.html
  4. 13
      app/message-action/client/messageAction.js
  5. 64
      app/message-action/client/stylesheets/messageAction.css
  6. 1
      app/message-action/index.js
  7. 5
      app/message-attachments/client/index.js
  8. 178
      app/message-attachments/client/messageAttachment.html
  9. 150
      app/message-attachments/client/messageAttachment.js
  10. 20
      app/message-attachments/client/renderField.html
  11. 54
      app/message-attachments/client/renderField.js
  12. 163
      app/message-attachments/client/stylesheets/messageAttachments.css
  13. 4
      app/theme/client/imports/general/base.css
  14. 8
      app/ui-message/client/message.html
  15. 2
      app/ui-message/client/messageBox/messageBoxReplyPreview.html
  16. 7
      app/ui-message/client/messageBox/messageBoxReplyPreview.js
  17. 3
      client/adapters.js
  18. 30
      client/components/Message/Attachments/ActionAttachtment.tsx
  19. 95
      client/components/Message/Attachments/Attachment.tsx
  20. 104
      client/components/Message/Attachments/Attachments.stories.js
  21. 19
      client/components/Message/Attachments/FieldsAttachment.tsx
  22. 42
      client/components/Message/Attachments/Files/AudioAttachment.tsx
  23. 39
      client/components/Message/Attachments/Files/GenericFileAttachment.tsx
  24. 49
      client/components/Message/Attachments/Files/ImageAttachment.tsx
  25. 38
      client/components/Message/Attachments/Files/PDFAttachment.tsx
  26. 42
      client/components/Message/Attachments/Files/VideoAttachment.tsx
  27. 26
      client/components/Message/Attachments/Files/index.tsx
  28. 34
      client/components/Message/Attachments/QuoteAttachment.tsx
  29. 11
      client/components/Message/Attachments/components/Image.stories.js
  30. 95
      client/components/Message/Attachments/components/Image.tsx
  31. 35
      client/components/Message/Attachments/context/AttachmentContext.tsx
  32. 11
      client/components/Message/Attachments/hooks/useCollapse.tsx
  33. 8
      client/components/Message/Attachments/hooks/useLoadImage.tsx
  34. 87
      client/components/Message/Attachments/index.tsx
  35. 29
      client/components/Message/Attachments/providers/AttachmentProvider.tsx
  36. 1
      client/importPackages.js
  37. 6
      client/providers/MeteorProvider.js
  38. 9
      client/types/fuselage.d.ts
  39. 2
      packages/rocketchat-i18n/i18n/en.i18n.json

@ -0,0 +1,6 @@
## `registerFieldTemplate` is deprecated
hmm it's true :(, we don't encourage this type of customization anymore, it ends up opening some security holes, we prefer the use of UIKit. If you feel any difficulty let us know
## `attachment.actions` is deprecated
same reason above
## `attachment PDF preview` is no longer being rendered
it is temporarily disabled, nowadays is huge effort render the previews and requires the download of the entire file on the client. We are working to improve this :)

@ -1,3 +0,0 @@
import './messageAction.html';
import './messageAction';
import './stylesheets/messageAction.css';

@ -1,31 +0,0 @@
<template name="messageAction">
<div class="action {{#if areButtonsHorizontal}}horizontal-buttons{{/if}}">
{{#if isButton}}
{{#if image_url}}
{{#if url}}
<a href="{{url}}" target="_blank" type="button" rel="noopener noreferrer">
<img class="image-button" src="{{image_url}}"/>
</a>
{{/if}}
{{#if msg_in_chat_window}}
<button class="{{jsActionButtonClassname msg_processing_type}}" value="{{msg}}">
<img class="image-button" src="{{image_url}}" />
</button>
{{/if}}
{{/if}}
{{#if text}}
{{#if url}}
<a class="text-button" href="{{url}}" target="_blank" type="button" rel="noopener noreferrer">
<span class="overflow-ellipsis">{{text}}</span>
</a>
{{/if}}
{{#if msg_in_chat_window}}
<button class="text-button {{jsActionButtonClassname msg_processing_type}}" value="{{msg}}">
<span class="overflow-ellipsis">{{text}}</span>
</button>
{{/if}}
{{/if}}
{{/if}}
</div>
</template>

@ -1,13 +0,0 @@
import { Template } from 'meteor/templating';
Template.messageAction.helpers({
isButton() {
return this.type === 'button';
},
areButtonsHorizontal() {
return Template.parentData(1).button_alignment === 'horizontal';
},
jsActionButtonClassname(processingType) {
return `js-actionButton-${ processingType || 'sendMessage' }`;
},
});

@ -1,64 +0,0 @@
.attachment {
& .action {
margin-top: 2px;
}
& .text-button {
position: relative;
display: inline-flex;
min-width: 0;
max-width: 220px;
height: 28px;
margin: 2px 2px 2px 0;
padding: 0 10px;
cursor: pointer;
user-select: none;
text-align: center;
vertical-align: middle;
white-space: nowrap;
text-decoration: none;
color: #2c2d30;
border: 2px solid lightgray;
border-radius: 4px;
outline: none;
background: rgb(250, 250, 250);
font-size: 13px;
font-weight: 500;
align-items: center;
-webkit-appearance: none;
justify-content: center;
-webkit-tap-highlight-color: transparent;
}
& .overflow-ellipsis {
display: block;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
& .image-button {
max-height: 200px;
}
& .horizontal-buttons {
display: inline;
}
}

@ -1 +0,0 @@
export * from './client/index';

@ -1,6 +1 @@
import './messageAttachment.html';
import './messageAttachment';
import './renderField.html';
import './stylesheets/messageAttachments.css';
export { registerFieldTemplate } from './renderField';

@ -1,178 +0,0 @@
<template name="messageAttachment">
<div class="attachment">
<!-- <div>fallback: {{fallback}}</div> -->
{{#if markdownInPretext}}
{{{parsedPretext}}}
{{else}}
{{pretext}}
{{/if}}
<div class="color-primary-font-color {{# unless $eq color 'none'}}attachment-block{{/unless}}">
<div class="attachment-block-border background-info-font-color" style="background-color: {{color}}"></div>
{{#if author_name}}
{{#if author_link}}
<div class="attachment-author">
{{#if author_icon}}
<img src="{{author_icon}}">
{{/if}}
<a href="{{author_link}}" target="_blank" rel="noopener noreferrer">{{author_name}}</a>
{{#if ts}}
{{#if message_link}}
<span class="time-link">
<a href="{{message_link}}" rel="noopener noreferrer">{{time}}</a>
</span>
{{else}}
{{#unless time}}
<span class="time">
{{time}}
</span>
{{/unless}}
{{/if}}
{{/if}}
</div>
{{else}}
<div class="attachment-author">
{{#if author_icon}}
<img src="{{author_icon}}">
{{/if}}
{{author_name}}
{{#if ts}}
{{#if message_link}}
<span class="time-link">
<a href="{{message_link}}" rel="noopener noreferrer">{{time}}</a>
</span>
{{else}}
{{#unless time}}
<span class="time">
{{time}}
</span>
{{/unless}}
{{/if}}
{{/if}}
</div>
{{/if}}
{{/if}}
{{#if title}}
<div class="attachment-title">
{{#if title_link}}
<a href="{{getURL title_link}}" target="_blank" rel="noopener noreferrer">{{title}}</a>
{{#if title_link_download}}
<a class="attachment-download-icon" title="{{_ 'Download'}}" href="{{getURL title_link}}?download" target="_blank" download="{{title}}" rel="noopener noreferrer">{{> icon icon="download"}}</a>
{{/if}}
{{else}}
{{title}}
{{/if}}
{{> collapseArrow collapsedMedia=collapsedMediaVar}}
</div>
{{/if}}
{{#unless collapsed}}
<div class="attachment-flex">
{{#if thumb_url}}
<div class="attachment-thumb">
<img src="{{thumb_url}}">
</div>
{{/if}}
{{#if text}}
<div class="attachment-flex-column-grow attachment-text">
{{{parsedText}}}
</div>
{{/if}}
</div>
{{/unless}}
{{#if image_url}}
{{#unless collapsed}}
<div class="attachment-image inline-image">
{{#if loadImage}}
<figure>
{{> lazyloadImage src=(getURL image_url) preview=image_preview height=(getImageHeight image_dimensions.height) class="gallery-item" title=title description=description}}
{{#if labels}}
<div class="image-labels">
{{#each labels}}
<span class="image-label primary-background-color color-content-background-color" style="background-color: {{bgColor}}; color: {{fontColor}};">{{label}}</span>
{{/each}}
</div>
{{/if}}
{{#if description}}
<figcaption class="attachment-description">{{description}}</figcaption>
{{/if}}
</figure>
{{else}}
<div class="image-to-download" data-url="{{image_url}}">
<i class="icon-picture rc-input__wrapper"></i>
<div>{{_ "Click_to_load"}}</div>
</div>
{{/if}}
</div>
{{/unless}}
{{/if}}
{{#if audio_url}}
{{#unless collapsed}}
<div class="attachment-audio">
<audio controls>
<source src="{{ getURL audio_url}}" type="{{audio_type}}" data-description="{{description}}">
{{_ "Browser_does_not_support_audio_element"}}
</audio>
</div>
{{/unless}}
{{/if}}
{{#if video_url}}
{{#unless collapsed}}
<div class="attachment-video">
<video controls class="inline-video">
<source src="{{ getURL video_url}}" type="{{video_type}}" data-description="{{description}}">
{{_ "Browser_does_not_support_video_element"}}
</video>
</div>
{{/unless}}
{{/if}}
{{#if isPDF}}
{{#unless collapsed}}
<div id="js-loading-{{fileId}}" class="attachment-pdf-loading">
{{> icon block="rc-input__icon-svg" icon="loading"}}
</div>
<canvas id="{{fileId}}" class="attachment-canvas"></canvas>
{{/unless}}
{{/if}}
{{#if fields}}
{{#unless collapsed}}
<div class="attachment-fields">
{{#each field in fields}}
{{> renderField field=field}}
{{/each}}
</div>
{{/unless}}
{{/if}}
{{#unless image_url}}
{{#if description}}
<div class="attachment-description">{{description}}</div>
{{/if}}
{{/unless}}
{{#if actions}}
{{#unless collapsed}}
<div class="actions-container">
{{#each actions}}
{{> messageAction}}
{{/each}}
</div>
{{/unless}}
{{/if}}
{{#each attachments}}
{{injectMessage . ../msg}}
{{injectSettings . ../settings}}
{{injectIndex . ../index @index}}
{{> messageAttachment (injectCollapsedMedia ..)}}
{{/each}}
</div>
</div>
</template>

@ -1,150 +0,0 @@
import { Meteor } from 'meteor/meteor';
import { Template } from 'meteor/templating';
import { DateFormat } from '../../lib';
import { getURL } from '../../utils/client';
import { createCollapseable } from '../../ui-utils';
import { renderMessageBody } from '../../../client/lib/renderMessageBody';
const colors = {
good: '#35AC19',
warning: '#FCB316',
danger: '#D30230',
};
async function renderPdfToCanvas(canvasId, pdfLink) {
const isSafari = /constructor/i.test(window.HTMLElement)
|| ((p) => p.toString() === '[object SafariRemoteNotification]')(!window.safari
|| (typeof window.safari !== 'undefined' && window.safari.pushNotification));
if (isSafari) {
const [, version] = /Version\/([0-9]+)/.exec(navigator.userAgent) || [null, 0];
if (version <= 12) {
return;
}
}
if (!pdfLink || !/\.pdf$/i.test(pdfLink)) {
return;
}
pdfLink = getURL(pdfLink);
const canvas = document.getElementById(canvasId);
if (!canvas) {
return;
}
const pdfjsLib = await import('pdfjs-dist');
pdfjsLib.GlobalWorkerOptions.workerSrc = `${ Meteor.absoluteUrl() }pdf.worker.min.js`;
const loader = document.getElementById(`js-loading-${ canvasId }`);
if (loader) {
loader.style.display = 'block';
}
const pdf = await pdfjsLib.getDocument(pdfLink).promise;
const page = await pdf.getPage(1);
const scale = 0.5;
const viewport = page.getViewport({ scale });
const context = canvas.getContext('2d');
canvas.height = viewport.height;
canvas.width = viewport.width;
await page.render({
canvasContext: context,
viewport,
}).promise;
if (loader) {
loader.style.display = 'none';
}
canvas.style.maxWidth = '-webkit-fill-available';
canvas.style.maxWidth = '-moz-available';
canvas.style.display = 'block';
}
createCollapseable(Template.messageAttachment, (instance) => (instance.data && (instance.data.collapsed || (instance.data.settings && instance.data.settings.collapseMediaByDefault))) || false);
Template.messageAttachment.helpers({
parsedText() {
return renderMessageBody({
msg: this.text,
});
},
markdownInPretext() {
return this.mrkdwn_in && this.mrkdwn_in.includes('pretext');
},
parsedPretext() {
return renderMessageBody({
msg: this.pretext,
});
},
loadImage() {
if (this.downloadImages) {
return true;
}
if (this.settings.autoImageLoad === false) {
return false;
}
if (this.settings.saveMobileBandwidth === true) {
return false;
}
return true;
},
getImageHeight(height = 200) {
return height;
},
color() {
return colors[this.color] || this.color;
},
time() {
const messageDate = new Date(this.ts);
const today = new Date();
if (messageDate.toDateString() === today.toDateString()) {
return DateFormat.formatTime(this.ts);
}
return DateFormat.formatDateAndTime(this.ts);
},
injectIndex(data, previousIndex, index) {
data.index = `${ previousIndex }.attachments.${ index }`;
},
injectSettings(data, settings) {
data.settings = settings;
},
injectMessage(data, { rid, _id }) {
data.msg = { _id, rid };
},
injectCollapsedMedia(data) {
const { collapsedMedia } = data;
Object.assign(this, { collapsedMedia });
return this;
},
isFile() {
return this.type === 'file';
},
isPDF() {
if (
this.type === 'file'
&& this.title_link.endsWith('.pdf')
&& Template.parentData(1).msg.file
) {
this.fileId = Template.parentData(1).msg.file._id;
return true;
}
return false;
},
getURL,
});
Template.messageAttachment.onRendered(function() {
const { msg } = Template.parentData(1);
this.autorun(() => {
if (msg && msg.file && msg.file.type === 'application/pdf' && !this.collapsedMedia.get()) {
Meteor.defer(() => { renderPdfToCanvas(msg.file._id, msg.attachments[0].title_link); });
}
});
});

@ -1,20 +0,0 @@
<template name="renderField">
{{#if field.type}}
<!-- a custom rendering is requested -->
<div class="field">
{{{specializedRendering field=field message=../..}}}
</div>
{{else}}
{{#if field.short}}
<div class="attachment-field attachment-field-short">
<div class="attachment-field-title">{{field.title}}</div>
{{{markdown field.value}}}
</div>
{{else}}
<div class="attachment-field">
<div class="attachment-field-title">{{field.title}}</div>
{{{markdown field.value}}}
</div>
{{/if}}
{{/if}}
</template>

@ -1,11 +1,3 @@
import { Template } from 'meteor/templating';
import { Blaze } from 'meteor/blaze';
import { Markdown } from '../../markdown/client';
import { escapeHTML } from '../../../lib/escapeHTML';
const renderers = {};
/**
* The field templates will be rendered non-reactive for all messages by the messages-list (@see rocketchat-nrr)
* Thus, we cannot provide helpers or events to the template, but we need to register this interactivity at the parent
@ -15,48 +7,6 @@ const renderers = {};
* @param helpers
* @param events
*/
export function registerFieldTemplate(fieldType, templateName, events) {
renderers[fieldType] = templateName;
// propagate helpers and events to the room template, changing the selectors
// loop at events. For each event (like 'click .accept'), copy the function to a function of the room events.
// While doing that, add the fieldType as class selector to the events function in order to avoid naming clashes
if (events != null) {
const uniqueEvents = {};
// rename the event handlers so they are unique in the "parent" template to which the events bubble
for (const property in events) {
if (events.hasOwnProperty(property)) {
const event = property.substr(0, property.indexOf(' '));
const selector = property.substr(property.indexOf(' ') + 1);
Object.defineProperty(uniqueEvents,
`${ event } .${ fieldType } ${ selector }`,
{
value: events[property],
enumerable: true, // assign as a own property
});
}
}
Template.roomOld.events(uniqueEvents);
}
export function registerFieldTemplate() {
console.warn('registerFieldTemplate DEPRECATED');
}
// onRendered is not being executed (no idea why). Consequently, we cannot use Blaze.renderWithData(), since we don't
// have access to the DOM outside onRendered. Therefore, we can only translate the content of the field to HTML and
// embed it non-reactively.
// This in turn means that onRendered of the field template will not be processed either.
// I guess it may have someting to do with rocketchat-nrr
Template.renderField.helpers({
specializedRendering({ hash: { field, message } }) {
let html = '';
if (field.type && renderers[field.type]) {
html = Blaze.toHTMLWithData(Template[renderers[field.type]], { field, message });
} else {
// consider the value already formatted as html
html = escapeHTML(field.value);
}
return `<div class="${ escapeHTML(field.type) }">${ html }</div>`;
},
markdown(text) {
return Markdown.parse(text);
},
});

@ -1,163 +0,0 @@
html.rtl .attachment {
direction: rtl;
& .attachment-block {
padding-right: 15px;
padding-left: 0;
& .attachment-block-border {
right: 0;
left: auto;
}
}
& .attachment-thumb {
padding-top: 10px;
padding-right: 5px;
}
& .attachment-download-icon {
margin-right: 5px;
margin-left: auto;
}
}
.attachment {
& .attachment-block {
position: relative;
margin: 5px 0;
padding-left: 15px;
& .attachment-block-border {
position: absolute;
top: 0;
bottom: 0;
left: 0;
width: 2px;
border-radius: 8px;
}
}
& .attachment-author {
font-size: 0.95rem;
font-weight: 600;
line-height: 1.2rem;
& > a {
font-weight: 600;
}
& img {
max-width: 16px;
max-height: 16px;
margin-right: 2px;
margin-bottom: -2px;
}
& .time,
& .time-link {
font-size: 0.8em;
font-weight: normal;
}
}
& .attachment-title {
color: #1d74f5;
font-size: 1.02rem;
font-weight: 500;
line-height: 1.5rem;
}
& .attachment-text {
padding: 3px 0;
line-height: 1rem;
}
& .attachment-image {
margin-top: 4px;
line-height: 0;
}
& .attachment-fields {
display: flex;
margin-top: 4px;
align-items: center;
flex-wrap: wrap;
& .attachment-field {
flex: 1 0 100%;
padding-top: 5px;
padding-bottom: 5px;
&.attachment-field-short {
display: inline-block;
flex: 1 1;
margin-right: 12px;
}
& .attachment-field-title {
font-weight: 600;
line-height: 1rem;
}
}
}
& .attachment-thumb {
padding-top: 5px;
padding-right: 10px;
line-height: 0;
& img {
max-width: 100px;
}
}
& .attachment-flex {
display: flex;
align-items: flex-start;
& .attachment-flex-column-grow {
word-break: break-word;
flex-grow: 1;
}
}
& .attachment-small-content {
max-width: 700px;
}
& .attachment-download-icon {
padding: 0 5px;
}
& .attachment-canvas {
display: none;
}
& .attachment-pdf-loading {
display: none;
font-size: 1.5rem;
svg {
animation: spin 1s linear infinite;
}
}
& .actions-container {
margin-top: 6px;
}
}

@ -246,3 +246,7 @@ button {
display: none !important;
}
}
.gallery-item {
cursor: pointer;
}

@ -115,13 +115,7 @@
{{> oembedBaseWidget}}
{{/each}}
{{/if}}
{{#each msg.attachments}}
{{injectMessage . ../msg}}
{{injectSettings . ../settings}}
{{injectIndex . @index}}
{{> messageAttachment}}
{{/each}}
{{> reactAttachments attachments=msg.attachments file=msg.file }}
{{#if msg.drid}}
{{> DiscussionMetric count=msg.dcount drid=msg.drid lm=msg.dlm openDiscussion=actions.openDiscussion }}
{{/if}}

@ -1,7 +1,7 @@
<template name="messageBoxReplyPreview" args="input replyMessageData">
<div class="reply-preview message-popup">
<div class="message">
{{> messageAttachment text=replyMessageData.msg author_name=replyMessageData.u.username}}
{{> reactAttachments attachments=attachments}}
</div>
<div class="rc-message-box__icon cancel-reply" data-mid="{{replyMessageData._id}}">
{{> icon block="rc-input__icon-svg" icon="cross"}}

@ -2,6 +2,13 @@ import { Template } from 'meteor/templating';
import './messageBoxReplyPreview.html';
Template.messageBoxReplyPreview.helpers({
attachments() {
const { replyMessageData } = this;
return [{ text: replyMessageData.msg, author_name: replyMessageData.u.username }];
},
});
Template.messageBoxReplyPreview.events({
'click .cancel-reply'(event) {
event.preventDefault();

@ -1,5 +1,6 @@
const { createTemplateForComponent } = require('./reactAdapters');
import { createTemplateForComponent } from './reactAdapters';
createTemplateForComponent('reactAttachments', () => import('./components/Message/Attachments'));
createTemplateForComponent('ThreadMetric', () => import('./components/Message/Metrics/Thread'));
createTemplateForComponent('DiscussionMetric', () => import('./components/Message/Metrics/Discussion'));
createTemplateForComponent('BroadCastMetric', () => import('./components/Message/Metrics/Broadcast'));

@ -0,0 +1,30 @@
import { Box, Button, ButtonGroup } from '@rocket.chat/fuselage';
import React, { FC } from 'react';
import { AttachmentProps } from '.';
// DEPRECATED
type Action = {
type: 'button';
text: string;
msg?: string;
url?: string;
image_url?: string;
is_webview?: true;
msg_in_chat_window?: true;
msg_processing_type?: 'sendMessage' | 'respondWithMessage';
};
export type ActionAttachmentProps = {
button_alignment: 'horizontal'| 'vertical';
actions: Array<Action>;
} & AttachmentProps;
export const ActionAttachment: FC<ActionAttachmentProps> = ({ actions }) => <ButtonGroup mb='x4' {...{ small: true }}>{
actions.filter(({ type, msg_in_chat_window: msgInChatWindow, url, image_url: image, text }) => type === 'button' && (image || text) && (url || msgInChatWindow)).map(({ text, url, msg, msg_processing_type: processingType = 'sendMessage', image_url: image }, index) => {
const content = image ? <Box is='img' src={image} maxHeight={200}/> : text;
if (url) {
return <Button is='a' href={url} target='_blank' rel='noopener noreferrer' key={index} small>{content}</Button>;
}
return <Button className={`js-actionButton-${ processingType }`} key={index} small value={msg}>{content}</Button>;
})}</ButtonGroup>;

@ -0,0 +1,95 @@
import React, { FC, memo } from 'react';
import { ActionButton, Box, BoxProps, ButtonProps, Avatar } from '@rocket.chat/fuselage';
import { useFormatMemorySize } from '../../../hooks/useFormatMemorySize';
import Image, { ImageProps } from './components/Image';
import { useAttachmentDimensions } from './context/AttachmentContext';
import { useTranslation } from '../../../contexts/TranslationContext';
export type AttachmentPropsBase = {
title?: string;
ts: Date;
collapsed?: boolean;
description?: string;
title_link?: string;
title_link_download: boolean;
};
const Row: FC<BoxProps> = (props) => <Box mi='neg-x2' mbe='x2' rcx-message-attachment display='flex' alignItems='center' {...props}/>;
const Title: FC<BoxProps> = (props) => <Box withTruncatedText mi='x2' fontScale='c1' color='hint' {...props}></Box>;
const Text: FC<BoxProps> = (props) => <Box withTruncatedText mbe='x4' mi='x2' fontScale='p1' color='default' {...props}></Box>;
const Size: FC<BoxProps & { size: number }> = ({ size, ...props }) => {
const format = useFormatMemorySize();
return <Title flexShrink={0} {...props}>({format(size)})</Title>;
};
const Action: FC<ButtonProps & { icon: string }> = (props) => <ActionButton mi='x2' mini ghost {...props} />;
const Collapse: FC<ButtonProps & { collapsed?: boolean }> = ({ collapsed = false, ...props }) => {
const t = useTranslation();
return <Action title={collapsed ? t('Uncollapse') : t('Collapse')}icon={ !collapsed ? 'chevron-down' : 'chevron-left' }{...props} />;
};
const Download: FC<ButtonProps & { href: string }> = (props) => {
const t = useTranslation();
return <Action icon='download' title={t('Download')} is='a' target='_blank' {...props} />;
};
const Content: FC<BoxProps> = ({ ...props }) => <Box mb='x4' {...props} />;
const Details: FC<BoxProps> = ({ ...props }) => <Box fontScale='p1' color='info' bg='neutral-100' pi='x16' pb='x16' {...props}/>;
const Inner: FC<BoxProps> = ({ ...props }) => <Box mis='x16' {...props}/>;
const Block: FC<{ pre?: JSX.Element | string; color?: string }> = ({ pre, color = 'neutral-600', children }) => <Attachment>{pre}<Box display='flex' flexDirection='row' pis='x16' borderRadius='x2' borderInlineStartStyle='solid' borderInlineStartWidth='x2' borderInlineStartColor={color} children={children}></Box></Attachment>;
const Author: FC<{}> = (props) => <Box display='flex' flexDirection='row' alignItems='center' mbe='x4' {...props}/>;
const AuthorAvatar: FC<{ url: string }> = ({ url }) => <Avatar { ...{ url, size: 'x24' } as any} />;
const AuthorName: FC<BoxProps> = (props) => <Box withTruncatedText fontScale='p2' mi='x8' {...props}/>;
const Thumb: FC<{ url: string }> = memo(({ url }) => <Box mis='x8'><Avatar { ...{ url, size: 'x48' } as any} /></Box>);
export const Attachment: FC<BoxProps> & {
Row: FC<BoxProps>;
Title: FC<BoxProps>;
Text: FC<BoxProps>;
Size: FC<BoxProps & { size: number }>;
Collapse: FC<ButtonProps & { collapsed?: boolean }>;
Content: FC<BoxProps>;
Details: FC<BoxProps>;
Inner: FC<BoxProps>;
Block: FC<{ pre?: JSX.Element | string; color?: string }>;
Author: FC<{}>;
AuthorAvatar: FC<{ url: string }>;
AuthorName: FC<BoxProps>;
Image: FC<ImageProps>;
Thumb: FC<{ url: string }>;
Download: FC<ButtonProps & { href: string }>;
} = (props) => {
const { width } = useAttachmentDimensions();
return <Box rcx-message-attachment mb='x4' maxWidth={width} width='full' display='flex' overflow='hidden' flexDirection='column' {...props}/>;
};
Attachment.Image = Image;
Attachment.Row = Row;
Attachment.Title = Title;
Attachment.Text = Text;
Attachment.Size = Size;
Attachment.Thumb = Thumb;
Attachment.Collapse = Collapse;
Attachment.Download = Download;
Attachment.Content = Content;
Attachment.Details = Details;
Attachment.Inner = Inner;
Attachment.Block = Block;
Attachment.Author = Author;
Attachment.AuthorAvatar = AuthorAvatar;
Attachment.AuthorName = AuthorName;

@ -0,0 +1,104 @@
import React from 'react';
import Attachments from '.';
export default {
title: 'Message/Attachments',
component: Attachments,
};
const field = {
color: '#ff0000',
text: 'Yay for gruggy!',
pretext: 'Pre Text',
mrkdwn_in: ['fields'],
title: 'Attachment Example',
title_link: 'https://youtube.com',
fields: [{
short: true,
title: 'Test1 ',
value: 'Testing out something or other Testing out something or other Testing out something or other Testing out something or other Testing out something or other Testing out something or other Testing out something or other Testing out something or other Testing out something or other Testing out something or other Testing out something or other Testing out something or other',
}, {
short: true,
title: 'Another Test 2',
value: '[Link](https://google.com/) something and this and that.',
}, {
title: 'Another Test 3',
value: '[Link](https://google.com/) something and this and that.',
}, {
short: true,
title: 'Another Test 4',
value: '[Link](https://google.com/) something and this and that.',
}, {
short: true,
title: 'Another Test 5',
value: '[Link](https://google.com/) something and this and that.',
}],
};
const image = {
ts: '2016-12-09T16:53:06.761Z',
collapsed: false,
title: 'Attachment Image Example',
title_link: 'https://youtube.com',
title_link_download: true,
image_url: 'https://rocket.chat/wp-content/uploads/2020/07/devices-screens-768x433.png.webp',
type: 'file',
};
const video = {
ts: '2016-12-09T16:53:06.761Z',
collapsed: false,
title: 'Attachment Video Example',
title_link: 'https://youtube.com',
title_link_download: true,
video_url: 'http://www.w3schools.com/tags/movie.mp4',
type: 'file',
};
const audio = {
ts: '2016-12-09T16:53:06.761Z',
collapsed: false,
title: 'Attachment Audio Example',
title_link: 'https://youtube.com',
title_link_download: true,
audio_url: 'http://www.w3schools.com/tags/horse.mp3',
type: 'file',
};
const message = {
_id: '12312321',
rid: 'GENERAL',
msg: 'Sample message',
alias: 'Gruggy',
emoji: ':smirk:',
avatar: 'https://avatars2.githubusercontent.com/u/5263975?s=60&v=3',
attachments: [field, image, video, audio],
};
window.__meteor_runtime_config__ = { ROOT_URL_PATH_PREFIX: '' };
export const Default = () => <Attachments
attachments={message.attachments}
/>;
export const Fields = () => <Attachments
attachments={[field]}
/>;
export const FailingImage = () => <Attachments
attachments={[{ ...image, image_url: 'invalid.url' }]}
/>;
export const Image = () => <Attachments
attachments={[image]}
/>;
export const Video = () => <Attachments
attachments={[video]}
/>;
export const Audio = () => <Attachments
attachments={[audio]}
/>;

@ -0,0 +1,19 @@
import React, { FC } from 'react';
import { Box, BoxProps } from '@rocket.chat/fuselage';
type FieldProp = {
short?: boolean;
title: string;
value: JSX.Element | string;
}
const Field: FC<BoxProps | FieldProp> = ({ title, value, ...props }) => <Box mb='x4' pi='x4' width='full' flexBasis={100} flexShrink={0} {...props}>
<Box fontScale='p2' color='default'>{title}</Box>
{value}
</Box>;
const ShortField: FC<FieldProp> = (props) => <Field {...props} flexGrow={1} width='50%' flexBasis={1}/>;
export type FieldsAttachmentProps = Array<FieldProp>;
export const FieldsAttachment: FC<{ fields: FieldsAttachmentProps }> = ({ fields }): any => <Box flexWrap='wrap' display='flex' mb='x4' mi='neg-x4'>{fields.map((field, index) => (field.short ? <ShortField {...field} key={index}/> : <Field {...field} key={index}/>))} </Box>;

@ -0,0 +1,42 @@
import React, { FC } from 'react';
import { useCollapse } from '../hooks/useCollapse';
import { Attachment, AttachmentPropsBase } from '../Attachment';
import { FileProp } from '..';
import { useMediaUrl } from '../context/AttachmentContext';
export type AudioAttachmentProps = {
audio_url: string;
audio_type: string;
audio_size?: number;
file: FileProp;
} & AttachmentPropsBase;
export const AudioAttachment: FC<AudioAttachmentProps> = ({
title,
audio_url: url,
audio_type: type,
collapsed: collapsedDefault = false,
audio_size: size,
description,
title_link: link,
title_link_download: hasDownload,
}) => {
const [collapsed, collapse] = useCollapse(collapsedDefault);
// useTranslation();
const getURL = useMediaUrl();
return <Attachment>
<Attachment.Row>
<Attachment.Title>{title}</Attachment.Title>
{size && <Attachment.Size size={size}/>}
{collapse}
{hasDownload && link && <Attachment.Download href={link}/>}
</Attachment.Row>
{ !collapsed && <Attachment.Content border='none'>
<audio controls>
<source src={getURL(url)} type={type}/>
</audio>
{description && <Attachment.Details is='figcaption'>{description}</Attachment.Details>}
</Attachment.Content> }
</Attachment>;
};

@ -0,0 +1,39 @@
import React, { FC } from 'react';
import { Attachment, AttachmentPropsBase } from '../Attachment';
import MarkdownText from '../../../MarkdownText';
import { FileProp } from '..';
export type GenericFileAttachmentProps = {
file: FileProp;
} & AttachmentPropsBase;
export const GenericFileAttachment: FC<GenericFileAttachmentProps> = ({
title,
// collapsed: collapsedDefault = false,
description,
title_link: link,
title_link_download: hasDownload,
file: {
size,
// format,
// name,
},
}) =>
// const [collapsed, collapse] = useCollapse(collapsedDefault);
<Attachment>
{ description && <MarkdownText withRichContent={undefined} content={description} /> }
<Attachment.Row>
<Attachment.Title { ...hasDownload && link && { is: 'a', href: link, color: undefined } } >{title}</Attachment.Title>
{size && <Attachment.Size size={size}/>}
{/* {collapse} */}
{hasDownload && link && <Attachment.Download href={link}/>}
</Attachment.Row>
{/* { !collapsed && <Attachment.Content>
<Attachment.Details>
{hasDownload && link && <Attachment.Download href={link}/>}
<Attachment.Row><Attachment.Title { ...hasDownload && link && { is: 'a', href: link } } >{name}</Attachment.Title></Attachment.Row>
<Attachment.Row>{size && <Attachment.Size size={size}/>}<Attachment.Title>{format && size && ' | '}{format}</Attachment.Title></Attachment.Row>
</Attachment.Details>
</Attachment.Content> } */}
</Attachment>;

@ -0,0 +1,49 @@
import React, { FC } from 'react';
import { useCollapse } from '../hooks/useCollapse';
import { Attachment, AttachmentPropsBase } from '../Attachment';
import Image, { Dimensions } from '../components/Image';
import MarkdownText from '../../../MarkdownText';
import { FileProp } from '..';
import { useMediaUrl } from '../context/AttachmentContext';
import { useLoadImage } from '../hooks/useLoadImage';
export type ImageAttachmentProps = {
image_dimensions: Dimensions;
image_preview?: string;
image_url: string;
image_type: string;
image_size?: number;
file: FileProp;
} & AttachmentPropsBase;
export const ImageAttachment: FC<ImageAttachmentProps> = ({
title,
image_url: url,
image_preview: imagePreview,
collapsed: collapsedDefault = false,
image_size: size,
image_dimensions: imageDimensions = {
height: 360,
width: 480,
},
description,
title_link: link,
title_link_download: hasDownload,
}) => {
const [loadImage, setLoadImage] = useLoadImage();
const [collapsed, collapse] = useCollapse(collapsedDefault);
const getURL = useMediaUrl();
return <Attachment>
<MarkdownText withRichContent={undefined} content={description} />
<Attachment.Row>
<Attachment.Title>{title}</Attachment.Title>
{size && <Attachment.Size size={size}/>}
{collapse}
{hasDownload && link && <Attachment.Download href={getURL(link)}/>}
</Attachment.Row>
{ !collapsed && <Attachment.Content>
<Image {...imageDimensions } loadImage={loadImage} setLoadImage={setLoadImage} src={ url} previewUrl={`data:image/png;base64,${ imagePreview }`} />
</Attachment.Content> }
</Attachment>;
};

@ -0,0 +1,38 @@
import React, { FC } from 'react';
import { useCollapse } from '../hooks/useCollapse';
import { Attachment, AttachmentPropsBase } from '../Attachment';
import MarkdownText from '../../../MarkdownText';
import { useTranslation } from '../../../../contexts/TranslationContext';
import { FileProp } from '..';
export type PDFAttachmentProps = {
file: FileProp;
} & AttachmentPropsBase;
export const PDFAttachment: FC<PDFAttachmentProps> = ({
collapsed: collapsedDefault = false,
description,
title_link: link,
title_link_download: hasDownload,
file,
}) => {
const t = useTranslation();
const [collapsed, collapse] = useCollapse(collapsedDefault);
return <Attachment>
<MarkdownText withRichContent={undefined} content={description} />
<Attachment.Row>
<Attachment.Title>{t('PDF')}</Attachment.Title>
{collapse}
{hasDownload && link && <Attachment.Download href={link}/>}
</Attachment.Row>
{ !collapsed && <Attachment.Content>
<canvas id={file._id} className='attachment-canvas'></canvas>
{/* <div id="js-loading-{{fileId}}" class="attachment-pdf-loading">
<Attachment.Title>{title}</Attachment.Title>
{file.size && <Attachment.Size size={file.size}/>}
{{> icon block="rc-input__icon-svg" icon="loading"}}
</div>*/}
</Attachment.Content> }
</Attachment>;
};

@ -0,0 +1,42 @@
import React, { FC } from 'react';
import { Box } from '@rocket.chat/fuselage';
import { useCollapse } from '../hooks/useCollapse';
import { Attachment, AttachmentPropsBase } from '../Attachment';
import { FileProp } from '..';
import { useMediaUrl } from '../context/AttachmentContext';
export type VideoAttachmentProps = {
video_url: string;
video_type: string;
video_size: number;
file: FileProp;
} & AttachmentPropsBase;
export const VideoAttachment: FC<VideoAttachmentProps> = ({ title,
video_url: url,
video_type: type,
collapsed: collapsedDefault = false,
video_size: size,
description,
title_link: link,
title_link_download: hasDownload,
}) => {
const [collapsed, collapse] = useCollapse(collapsedDefault);
// useTranslation();
const getURL = useMediaUrl();
return <Attachment>
<Attachment.Row>
<Attachment.Title>{title}</Attachment.Title>
{size && <Attachment.Size size={size}/>}
{collapse}
{hasDownload && link && <Attachment.Download href={link}/>}
</Attachment.Row>
{ !collapsed && <Attachment.Content width='full'>
<Box is='video' width='full' controls>
<source src={getURL(url)} type={type}/>
</Box>
{description && <Attachment.Details is='figcaption'>{description}</Attachment.Details>}
</Attachment.Content>}
</Attachment>;
};

@ -0,0 +1,26 @@
import React, { FC } from 'react';
import { AttachmentPropsGeneric, FileProp } from '..';
import { AudioAttachment, AudioAttachmentProps } from './AudioAttachment';
import { GenericFileAttachment } from './GenericFileAttachment';
import { ImageAttachment, ImageAttachmentProps } from './ImageAttachment';
// import { PDFAttachment } from './PDFAttachment';
import { VideoAttachment, VideoAttachmentProps } from './VideoAttachment';
export type FileAttachmentProps = { type: 'file'; file: FileProp } & (VideoAttachmentProps | ImageAttachmentProps | AudioAttachmentProps);
const isFileImageAttachment = (attachment: FileAttachmentProps): attachment is ImageAttachmentProps & { type: 'file' } => 'image_url' in attachment;
const isFileAudioAttachment = (attachment: FileAttachmentProps): attachment is AudioAttachmentProps & { type: 'file' } => 'audio_url' in attachment;
const isFileVideoAttachment = (attachment: FileAttachmentProps): attachment is VideoAttachmentProps & { type: 'file' } => 'video_url' in attachment;
// const isFilePDFAttachment = (attachment: FileAttachmentProps): attachment is VideoAttachmentProps & { type: 'file' } => attachment?.file?.type.endsWith('pdf');
export const FileAttachment: FC<FileAttachmentProps> = (attachment) => {
if (isFileImageAttachment(attachment)) { return <ImageAttachment {...attachment} />; }
if (isFileAudioAttachment(attachment)) { return <AudioAttachment {...attachment} />; }
if (isFileVideoAttachment(attachment)) { return <VideoAttachment {...attachment} />; }
// if (isFilePDFAttachment(attachment)) { return <PDFAttachment {...attachment} />; }
return <GenericFileAttachment {...attachment}/>;
};
export const isFileAttachment = (attachment: AttachmentPropsGeneric): attachment is FileAttachmentProps => 'type' in attachment && attachment.type === 'file';

@ -0,0 +1,34 @@
import { Box } from '@rocket.chat/fuselage';
import React, { FC } from 'react';
import { Attachment, AttachmentPropsBase } from './Attachment';
import { useTimeAgo } from '../../../hooks/useTimeAgo';
import MarkdownText from '../../MarkdownText';
import Attachments from '.';
export type QuoteAttachmentProps = {
author_name: string;
author_link: string;
author_icon: string;
message_link?: string;
text: string;
attachments?: Array<QuoteAttachmentProps>;
} & AttachmentPropsBase;
export const QuoteAttachment: FC<QuoteAttachmentProps> = ({ author_icon: url, author_name: name, author_link: authorLink, message_link: messageLink, ts, text, attachments }) => {
const format = useTimeAgo();
return <>
<Attachment.Content maxWidth='480px' width='full' borderRadius='x2' borderWidth='x2' borderStyle='solid' borderColor='neutral-200' >
<Attachment.Details is='blockquote'>
<Attachment.Author>
<Attachment.AuthorAvatar url={url} />
<Attachment.AuthorName {...authorLink && { is: 'a', href: authorLink, target: '_blank', color: undefined }}>{name}</Attachment.AuthorName>
<Box fontScale='c1' {...messageLink ? { is: 'a', href: messageLink } : { color: 'hint' }}>{format(ts)}</Box>
</Attachment.Author>
<MarkdownText content={text} />
{attachments && <Attachment.Inner><Attachments attachments={attachments} /></Attachment.Inner>}
</Attachment.Details>
</Attachment.Content>
</>;
};

@ -0,0 +1,11 @@
import React from 'react';
import { Retry } from './Image';
export default {
title: 'Image',
component: Image,
};
// export const Default = () => <Image />;
export const RetryImage = () => <Retry />;

@ -0,0 +1,95 @@
import React, { memo, FC, useState, useMemo } from 'react';
import { Box, Icon, BoxProps } from '@rocket.chat/fuselage';
import { css } from '@rocket.chat/css-in-js';
import colors from '@rocket.chat/fuselage-tokens/colors';
import { useTranslation } from '../../../../contexts/TranslationContext';
import { useAttachmentDimensions } from '../context/AttachmentContext';
export type Dimensions = {
width: number;
height: number;
};
export type ImageProps = {
previewUrl?: string;
src: string;
loadImage?: boolean;
setLoadImage: () => void;
} & Dimensions & ({ loadImage: true } | { loadImage: false; setLoadImage: () => void });
const ImageBox: FC<BoxProps> = (props) => <Box display='flex' maxWidth='full' flexDirection='column' justifyContent='center' alignItems='center' alignContent='center' borderRadius='x2' borderWidth='x2' borderStyle='solid' borderColor='neutral-200' {...props}/>;
export const Retry: FC<BoxProps & { retry: () => void }> = ({ retry, ...props }) => {
const t = useTranslation();
const clickable = css`
cursor: pointer;
background: var(--rxc-color-neutral-100, ${ colors.n100 }) !important;
&:hover,
&:focus {
background: var(--rxc-color-neutral-300, ${ colors.n300 }) !important;
}
`;
return <ImageBox className={clickable} {...props} onClick={retry}>
<Icon name='refresh' color='neutral-700' size='x64'/>
<Box fontScale='h1' color='default'>{t('Retry')}</Box>
</ImageBox>;
};
export const Load: FC<BoxProps & { load: () => void }> = ({ load, ...props }) => {
const t = useTranslation();
const clickable = css`
cursor: pointer;
background: var(--rxc-color-neutral-100, ${ colors.n100 }) !important;
&:hover,
&:focus {
background: var(--rxc-color-neutral-300, ${ colors.n300 }) !important;
}
`;
return <ImageBox className={clickable} {...props} onClick={load}>
<Icon name='image' color='neutral-700' size='x64'/>
<Box fontScale='h1' color='default'>{t('Click_to_load')}</Box>
</ImageBox>;
};
const getDimensions = (width: Dimensions['width'], height: Dimensions['height'], limits: { width: number; height: number }): { width: 'auto' | number; height: 'auto' | number } => {
const ratio = height / width;
if (height >= width || Math.min(width, limits.width) * ratio > limits.height) {
return { width: width * Math.min(height, limits.height) / height, height: Math.min(height, limits.height) };
}
return { width: Math.min(width, limits.width), height: height * Math.min(width, limits.width) / width };
};
const Image: FC<ImageProps> = ({ previewUrl, loadImage = true, setLoadImage, src, ...size }) => {
const limits = useAttachmentDimensions();
const { width = limits.width, height = limits.height } = size;
const [error, setError] = useState(false);
const { setHasError, setHasNoError } = useMemo(() => ({
setHasError: (): void => setError(true),
setHasNoError: (): void => setError(false),
}), []);
const dimensions = getDimensions(width, height, limits);
const background = previewUrl && `url(${ previewUrl }) center center / cover no-repeat fixed`;
if (!loadImage) {
return <Load { ...limits } load={setLoadImage}/>;
}
if (error) {
return <Retry { ...dimensions } retry={setHasNoError}/>;
}
return <ImageBox className='gallery-item' onError={setHasError} {...previewUrl && { style: { background } } as any } { ...dimensions } src={src} is='img'/>;
};
export default memo(Image);

@ -0,0 +1,35 @@
import { createContext, useContext } from 'react';
export type AttachmentContextValue = {
getURL: (url: string) => string;
dimensions: {
width: number;
height: number;
};
collapsedByDefault: boolean;
autoLoadEmbedMedias: boolean;
};
export const AttachmentContext = createContext<AttachmentContextValue>({
getURL: (url: string) => url,
dimensions: {
width: 480,
height: 360,
},
collapsedByDefault: false,
autoLoadEmbedMedias: true,
});
export const useMediaUrl = (): (path: string) => string => {
const { getURL } = useContext(AttachmentContext);
return getURL;
};
export const useAttachmentDimensions = (): {
width: number;
height: number;
} => useContext(AttachmentContext).dimensions;
export const useAttachmentIsCollapsedByDefault = (): boolean => useContext(AttachmentContext).collapsedByDefault;
export const useAttachmentAutoLoadEmbedMedia = (): boolean => useContext(AttachmentContext).autoLoadEmbedMedias;

@ -0,0 +1,11 @@
import React from 'react';
import { useToggle } from '@rocket.chat/fuselage-hooks';
import { Attachment } from '../Attachment';
import { useAttachmentIsCollapsedByDefault } from '../context/AttachmentContext';
export const useCollapse = (attachmentCollapsed: boolean): [boolean, JSX.Element] => {
const collpaseByDefault = useAttachmentIsCollapsedByDefault();
const [collapsed, toogleCollapsed] = useToggle(collpaseByDefault || attachmentCollapsed);
return [collapsed, <Attachment.Collapse collapsed={collapsed} onClick={toogleCollapsed as any} />];
};

@ -0,0 +1,8 @@
import { useCallback, useState } from 'react';
import { useAttachmentAutoLoadEmbedMedia } from '../context/AttachmentContext';
export const useLoadImage = (): [boolean, () => void] => {
const [loadImage, setLoadImage] = useState(useAttachmentAutoLoadEmbedMedia());
return [loadImage, useCallback(() => setLoadImage(true), [])];
};

@ -0,0 +1,87 @@
import React, { FC, memo } from 'react';
import { FieldsAttachment, FieldsAttachmentProps } from './FieldsAttachment';
import { QuoteAttachment, QuoteAttachmentProps } from './QuoteAttachment';
import { Attachment } from './Attachment';
import { FileAttachmentProps, isFileAttachment, FileAttachment } from './Files';
import MarkdownText from '../../MarkdownText';
import { Dimensions } from './components/Image';
import { ActionAttachment, ActionAttachmentProps } from './ActionAttachtment';
type PossibleMarkdownFields = 'text' | 'pretext' | 'fields';
export type FileProp = {
_id: string;
name: string;
type: string;
format: string;
size: number;
};
export type AttachmentProps = {
author_icon?: string;
author_link?: string;
author_name?: string;
fields: FieldsAttachmentProps;
// footer
// footer_icon
image_url?: string;
image_dimensions?: Dimensions;
mrkdwn_in?: Array<PossibleMarkdownFields>;
pretext?: string;
text? : string;
thumb_url?: string;
title?: string;
title_link?: string;
ts?: Date;
color?: string;
}
export type AttachmentPropsGeneric = AttachmentProps | FileAttachmentProps | QuoteAttachmentProps | ActionAttachmentProps;
const isQuoteAttachment = (attachment: AttachmentPropsGeneric): attachment is QuoteAttachmentProps => 'message_link' in attachment;
const isActionAttachment = (attachment: AttachmentPropsGeneric): attachment is ActionAttachmentProps => 'actions' in attachment;
const applyMarkdownIfRequires = (list: AttachmentProps['mrkdwn_in']) => (key: PossibleMarkdownFields, text: string): JSX.Element | string => (list?.includes(key) ? <MarkdownText withRichContent={null} content={text}/> : text);
const Item: FC<{attachment: AttachmentPropsGeneric; file?: FileProp }> = memo(({ attachment, file = null }) => {
if (isFileAttachment(attachment)) {
return file && <FileAttachment {...attachment} file={file}/>;
}
if (isQuoteAttachment(attachment)) {
return <QuoteAttachment {...attachment}/>;
}
const applyMardownFor = applyMarkdownIfRequires(attachment.mrkdwn_in);
return <Attachment.Block color={attachment.color} pre={attachment.pretext && <Attachment.Text>{applyMardownFor('pretext', attachment.pretext)}</Attachment.Text>}>
<Attachment.Content>
{attachment.author_name && <Attachment.Author>
{ attachment.author_icon && <Attachment.AuthorAvatar url={attachment.author_icon } />}
<Attachment.AuthorName {...attachment.author_link && { is: 'a', href: attachment.author_link, target: '_blank', color: undefined }}>{attachment.author_name}</Attachment.AuthorName>
</Attachment.Author> }
{attachment.title && <Attachment.Title {...attachment.title_link && { is: 'a', href: attachment.title_link, target: '_blank', color: undefined }}>{attachment.title}</Attachment.Title> }
{attachment.text && <Attachment.Text>{applyMardownFor('text', attachment.text)}</Attachment.Text>}
{attachment.fields && <FieldsAttachment fields={attachment.mrkdwn_in?.includes('fields') ? attachment.fields.map(({ value, ...rest }) => ({ ...rest, value: <MarkdownText withRichContent={null} content={value} /> })) : attachment.fields} />}
{attachment.image_url && <Attachment.Image {...attachment.image_dimensions as any} src={attachment.image_url} />}
{/* DEPRECATED */}
{isActionAttachment(attachment) && <ActionAttachment {...attachment} />}
</Attachment.Content>
{attachment.thumb_url && <Attachment.Thumb url={attachment.thumb_url} /> }
</Attachment.Block>;
});
const Attachments: FC<{ attachments: Array<AttachmentPropsGeneric>; file?: FileProp}> = ({ attachments = null, file }): any => attachments && attachments.map((attachment, index) => <Item key={index} file={file} attachment={attachment} />);
export default Attachments;

@ -0,0 +1,29 @@
import React, { useMemo, FC } from 'react';
import { usePrefersReducedData } from '@rocket.chat/fuselage-hooks';
import { AttachmentContext, AttachmentContextValue } from '../context/AttachmentContext';
import { useUserPreference } from '../../../../contexts/UserContext';
import { getURL } from '../../../../../app/utils/client';
import { useLayout } from '../../../../contexts/LayoutContext';
const AttachmentProvider: FC<{}> = ({ children }) => {
const { isMobile } = useLayout();
const reducedData = usePrefersReducedData();
const collapsedByDefault = !!useUserPreference<boolean>('collapseMediaByDefault');
const autoLoadEmbedMedias = !!useUserPreference<boolean>('autoImageLoad');
const saveMobileBandwidth = !!useUserPreference<boolean>('saveMobileBandwidth');
const contextValue: AttachmentContextValue = useMemo(() => ({
getURL: getURL as (url: string) => string,
collapsedByDefault,
autoLoadEmbedMedias: !reducedData && autoLoadEmbedMedias && (!saveMobileBandwidth || !isMobile),
dimensions: {
width: 480,
height: 360,
},
}), [autoLoadEmbedMedias, collapsedByDefault, saveMobileBandwidth, reducedData]);
return <AttachmentContext.Provider children={children} value={contextValue} />;
};
export default AttachmentProvider;

@ -33,7 +33,6 @@ import '../app/logger';
import '../app/token-login/client';
import '../app/markdown/client';
import '../app/mentions-flextab/client';
import '../app/message-action';
import '../app/message-attachments';
import '../app/message-mark-as-unread/client';
import '../app/message-pin/client';

@ -1,5 +1,6 @@
import React from 'react';
import AttachmentProvider from '../components/Message/Attachments/providers/AttachmentProvider';
import AuthorizationProvider from './AuthorizationProvider';
import AvatarUrlProvider from './AvatarUrlProvider';
import ConnectionStatusProvider from './ConnectionStatusProvider';
@ -33,7 +34,10 @@ function MeteorProvider({ children }) {
<AuthorizationProvider>
<OmniChannelProvider>
<ModalProvider>
{children}
{/* TODO move to RoomContext */}
<AttachmentProvider>
{children}
</AttachmentProvider>
</ModalProvider>
</OmniChannelProvider>
</AuthorizationProvider>

@ -1,3 +1,12 @@
declare module '@rocket.chat/fuselage-tokens/colors' {
type ColorsType = {
[key: string]: string;
};
const Colors: ColorsType;
export default Colors;
}
declare module '@rocket.chat/fuselage' {
import { css } from '@rocket.chat/css-in-js';
import { Placements } from '@rocket.chat/fuselage-hooks';

@ -825,6 +825,7 @@
"Cloud_workspace_disconnect": "If you no longer wish to utilize cloud services you can disconnect your workspace from Rocket.Chat Cloud.",
"Cloud_workspace_support": "If you have trouble with a cloud service, please try to sync first. Should the issue persist, please open a support ticket in the Cloud Console.",
"Collaborative": "Collaborative",
"Collapse": "Collapse",
"Collapse_Embedded_Media_By_Default": "Collapse Embedded Media by Default",
"color": "Color",
"Color": "Color",
@ -3832,6 +3833,7 @@
"Unavailable": "Unavailable",
"Unblock_User": "Unblock User",
"Uncheck_All": "Uncheck All",
"Uncollapse": "Uncollapse",
"Undefined": "Undefined",
"Unfavorite": "Unfavorite",
"Unfollow_message": "Unfollow Message",

Loading…
Cancel
Save