Rewrite: Message Attachments (#20106)
parent
9d4d7733cc
commit
9b33bbe5d0
@ -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,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; |
||||
} |
||||
} |
||||
@ -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; |
||||
Loading…
Reference in new issue