Enable support for image, video and audio files
parent
409213ceb4
commit
59c1b67b7d
|
@ -34,6 +34,7 @@ export default class MAudioBody extends React.Component {
|
||||||
error: null,
|
error: null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
onPlayToggle() {
|
onPlayToggle() {
|
||||||
this.setState({
|
this.setState({
|
||||||
playing: !this.state.playing,
|
playing: !this.state.playing,
|
||||||
|
@ -41,6 +42,7 @@ export default class MAudioBody extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
_getContentUrl() {
|
_getContentUrl() {
|
||||||
|
if (this.props.mediaSrc) return this.props.mediaSrc;
|
||||||
const media = mediaFromContent(this.props.mxEvent.getContent());
|
const media = mediaFromContent(this.props.mxEvent.getContent());
|
||||||
if (media.isEncrypted) {
|
if (media.isEncrypted) {
|
||||||
return this.state.decryptedUrl;
|
return this.state.decryptedUrl;
|
||||||
|
@ -49,6 +51,11 @@ export default class MAudioBody extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getFileBody() {
|
||||||
|
if (this.props.mediaSrc) return null;
|
||||||
|
return <MFileBody {...this.props} decryptedBlob={this.state.decryptedBlob} showGenericPlaceholder={false} />;
|
||||||
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
const content = this.props.mxEvent.getContent();
|
const content = this.props.mxEvent.getContent();
|
||||||
if (content.file !== undefined && this.state.decryptedUrl === null) {
|
if (content.file !== undefined && this.state.decryptedUrl === null) {
|
||||||
|
@ -101,11 +108,11 @@ export default class MAudioBody extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
const contentUrl = this._getContentUrl();
|
const contentUrl = this._getContentUrl();
|
||||||
|
const fileBody = this.getFileBody();
|
||||||
return (
|
return (
|
||||||
<span className="mx_MAudioBody">
|
<span className="mx_MAudioBody">
|
||||||
<audio src={contentUrl} controls />
|
<audio src={contentUrl} controls />
|
||||||
<MFileBody {...this.props} decryptedBlob={this.state.decryptedBlob} showGenericPlaceholder={false} />
|
{ fileBody }
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -242,7 +242,7 @@ export default class MFileBody extends React.Component {
|
||||||
<span className="mx_MFileBody">
|
<span className="mx_MFileBody">
|
||||||
{placeholder}
|
{placeholder}
|
||||||
<div className="mx_MFileBody_download">
|
<div className="mx_MFileBody_download">
|
||||||
<div style={{display: "none"}}>
|
<div style={{ display: "none" }}>
|
||||||
{ /*
|
{ /*
|
||||||
* Add dummy copy of the "a" tag
|
* Add dummy copy of the "a" tag
|
||||||
* We'll use it to learn how the download link
|
* We'll use it to learn how the download link
|
||||||
|
@ -309,7 +309,7 @@ export default class MFileBody extends React.Component {
|
||||||
if (this.props.tileShape === "file_grid") {
|
if (this.props.tileShape === "file_grid") {
|
||||||
return (
|
return (
|
||||||
<span className="mx_MFileBody">
|
<span className="mx_MFileBody">
|
||||||
{placeholder}
|
{ placeholder }
|
||||||
<div className="mx_MFileBody_download">
|
<div className="mx_MFileBody_download">
|
||||||
<a className="mx_MFileBody_downloadLink" {...downloadProps}>
|
<a className="mx_MFileBody_downloadLink" {...downloadProps}>
|
||||||
{ fileName }
|
{ fileName }
|
||||||
|
@ -323,7 +323,7 @@ export default class MFileBody extends React.Component {
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<span className="mx_MFileBody">
|
<span className="mx_MFileBody">
|
||||||
{placeholder}
|
{ placeholder }
|
||||||
<div className="mx_MFileBody_download">
|
<div className="mx_MFileBody_download">
|
||||||
<a {...downloadProps}>
|
<a {...downloadProps}>
|
||||||
<span className="mx_MFileBody_download_icon" />
|
<span className="mx_MFileBody_download_icon" />
|
||||||
|
@ -336,7 +336,7 @@ export default class MFileBody extends React.Component {
|
||||||
} else {
|
} else {
|
||||||
const extra = text ? (': ' + text) : '';
|
const extra = text ? (': ' + text) : '';
|
||||||
return <span className="mx_MFileBody">
|
return <span className="mx_MFileBody">
|
||||||
{placeholder}
|
{ placeholder }
|
||||||
{ _t("Invalid file%(extra)s", { extra: extra }) }
|
{ _t("Invalid file%(extra)s", { extra: extra }) }
|
||||||
</span>;
|
</span>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -90,7 +90,7 @@ export default class MImageBody extends React.Component {
|
||||||
|
|
||||||
showImage() {
|
showImage() {
|
||||||
localStorage.setItem("mx_ShowImage_" + this.props.mxEvent.getId(), "true");
|
localStorage.setItem("mx_ShowImage_" + this.props.mxEvent.getId(), "true");
|
||||||
this.setState({showImage: true});
|
this.setState({ showImage: true });
|
||||||
this._downloadImage();
|
this._downloadImage();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -172,6 +172,7 @@ export default class MImageBody extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
_getContentUrl() {
|
_getContentUrl() {
|
||||||
|
if (this.props.mediaSrc) return this.props.mediaSrc;
|
||||||
const media = mediaFromContent(this.props.mxEvent.getContent());
|
const media = mediaFromContent(this.props.mxEvent.getContent());
|
||||||
if (media.isEncrypted) {
|
if (media.isEncrypted) {
|
||||||
return this.state.decryptedUrl;
|
return this.state.decryptedUrl;
|
||||||
|
@ -296,7 +297,7 @@ export default class MImageBody extends React.Component {
|
||||||
if (showImage) {
|
if (showImage) {
|
||||||
// Don't download anything becaue we don't want to display anything.
|
// Don't download anything becaue we don't want to display anything.
|
||||||
this._downloadImage();
|
this._downloadImage();
|
||||||
this.setState({showImage: true});
|
this.setState({ showImage: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
this._afterComponentDidMount();
|
this._afterComponentDidMount();
|
||||||
|
@ -345,7 +346,7 @@ export default class MImageBody extends React.Component {
|
||||||
imageElement = <HiddenImagePlaceholder />;
|
imageElement = <HiddenImagePlaceholder />;
|
||||||
} else {
|
} else {
|
||||||
imageElement = (
|
imageElement = (
|
||||||
<img style={{display: 'none'}} src={thumbUrl} ref={this._image}
|
<img style={{ display: 'none' }} src={thumbUrl} ref={this._image}
|
||||||
alt={content.body}
|
alt={content.body}
|
||||||
onError={this.onImageError}
|
onError={this.onImageError}
|
||||||
onLoad={this.onImageLoad}
|
onLoad={this.onImageLoad}
|
||||||
|
@ -449,6 +450,7 @@ export default class MImageBody extends React.Component {
|
||||||
|
|
||||||
// Overidden by MStickerBody
|
// Overidden by MStickerBody
|
||||||
getFileBody() {
|
getFileBody() {
|
||||||
|
if (this.props.mediaSrc) return null;
|
||||||
return <MFileBody {...this.props} decryptedBlob={this.state.decryptedBlob} showGenericPlaceholder={false} />;
|
return <MFileBody {...this.props} decryptedBlob={this.state.decryptedBlob} showGenericPlaceholder={false} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -466,7 +468,7 @@ export default class MImageBody extends React.Component {
|
||||||
|
|
||||||
const contentUrl = this._getContentUrl();
|
const contentUrl = this._getContentUrl();
|
||||||
let thumbUrl;
|
let thumbUrl;
|
||||||
if (this._isGif() && SettingsStore.getValue("autoplayGifsAndVideos")) {
|
if ((this._isGif() && SettingsStore.getValue("autoplayGifsAndVideos")) || this.props.mediaSrc) {
|
||||||
thumbUrl = contentUrl;
|
thumbUrl = contentUrl;
|
||||||
} else {
|
} else {
|
||||||
thumbUrl = this._getThumbUrl();
|
thumbUrl = this._getThumbUrl();
|
||||||
|
|
|
@ -29,6 +29,8 @@ interface IProps {
|
||||||
mxEvent: any;
|
mxEvent: any;
|
||||||
/* called when the video has loaded */
|
/* called when the video has loaded */
|
||||||
onHeightChanged: () => void;
|
onHeightChanged: () => void;
|
||||||
|
/* used to refer to the local file while exporting */
|
||||||
|
mediaSrc?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
|
@ -76,6 +78,7 @@ export default class MVideoBody extends React.PureComponent<IProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
private getContentUrl(): string|null {
|
private getContentUrl(): string|null {
|
||||||
|
if (this.props.mediaSrc) return this.props.mediaSrc;
|
||||||
const media = mediaFromContent(this.props.mxEvent.getContent());
|
const media = mediaFromContent(this.props.mxEvent.getContent());
|
||||||
if (media.isEncrypted) {
|
if (media.isEncrypted) {
|
||||||
return this.state.decryptedUrl;
|
return this.state.decryptedUrl;
|
||||||
|
@ -90,6 +93,9 @@ export default class MVideoBody extends React.PureComponent<IProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
private getThumbUrl(): string|null {
|
private getThumbUrl(): string|null {
|
||||||
|
// there's no need of thumbnail when the content is local
|
||||||
|
if (this.props.mediaSrc) return null;
|
||||||
|
|
||||||
const content = this.props.mxEvent.getContent();
|
const content = this.props.mxEvent.getContent();
|
||||||
const media = mediaFromContent(content);
|
const media = mediaFromContent(content);
|
||||||
if (media.isEncrypted) {
|
if (media.isEncrypted) {
|
||||||
|
@ -184,6 +190,11 @@ export default class MVideoBody extends React.PureComponent<IProps, IState> {
|
||||||
this.props.onHeightChanged();
|
this.props.onHeightChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getFileBody = () => {
|
||||||
|
if (this.props.mediaSrc) return null;
|
||||||
|
return <MFileBody {...this.props} decryptedBlob={this.state.decryptedBlob} showGenericPlaceholder={false} />;
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const content = this.props.mxEvent.getContent();
|
const content = this.props.mxEvent.getContent();
|
||||||
const autoplay = SettingsStore.getValue("autoplayGifsAndVideos");
|
const autoplay = SettingsStore.getValue("autoplayGifsAndVideos");
|
||||||
|
@ -197,7 +208,7 @@ export default class MVideoBody extends React.PureComponent<IProps, IState> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Important: If we aren't autoplaying and we haven't decrypred it yet, show a video with a poster.
|
// Important: If we aren't autoplaying and we haven't decrypted it yet, show a video with a poster.
|
||||||
if (content.file !== undefined && this.state.decryptedUrl === null && autoplay) {
|
if (content.file !== undefined && this.state.decryptedUrl === null && autoplay) {
|
||||||
// Need to decrypt the attachment
|
// Need to decrypt the attachment
|
||||||
// The attachment is decrypted in componentDidMount.
|
// The attachment is decrypted in componentDidMount.
|
||||||
|
@ -229,6 +240,8 @@ export default class MVideoBody extends React.PureComponent<IProps, IState> {
|
||||||
preload = "none";
|
preload = "none";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const fileBody = this.getFileBody();
|
||||||
return (
|
return (
|
||||||
<span className="mx_MVideoBody">
|
<span className="mx_MVideoBody">
|
||||||
<video
|
<video
|
||||||
|
@ -246,7 +259,7 @@ export default class MVideoBody extends React.PureComponent<IProps, IState> {
|
||||||
onPlay={this.videoOnPlay}
|
onPlay={this.videoOnPlay}
|
||||||
>
|
>
|
||||||
</video>
|
</video>
|
||||||
<MFileBody {...this.props} decryptedBlob={this.state.decryptedBlob} showGenericPlaceholder={false} />
|
{ fileBody }
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,6 +23,7 @@ import MVoiceMessageBody from "./MVoiceMessageBody";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
mxEvent: MatrixEvent;
|
mxEvent: MatrixEvent;
|
||||||
|
mediaSrc?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.messages.MVoiceOrAudioBody")
|
@replaceableComponent("views.messages.MVoiceOrAudioBody")
|
||||||
|
@ -30,7 +31,7 @@ export default class MVoiceOrAudioBody extends React.PureComponent<IProps> {
|
||||||
public render() {
|
public render() {
|
||||||
const isVoiceMessage = !!this.props.mxEvent.getContent()['org.matrix.msc2516.voice'];
|
const isVoiceMessage = !!this.props.mxEvent.getContent()['org.matrix.msc2516.voice'];
|
||||||
const voiceMessagesEnabled = SettingsStore.getValue("feature_voice_messages");
|
const voiceMessagesEnabled = SettingsStore.getValue("feature_voice_messages");
|
||||||
if (isVoiceMessage && voiceMessagesEnabled) {
|
if (isVoiceMessage && voiceMessagesEnabled && !this.props.mediaSrc) {
|
||||||
return <MVoiceMessageBody {...this.props} />;
|
return <MVoiceMessageBody {...this.props} />;
|
||||||
} else {
|
} else {
|
||||||
return <MAudioBody {...this.props} />;
|
return <MAudioBody {...this.props} />;
|
||||||
|
|
|
@ -44,6 +44,9 @@ export default class MessageEvent extends React.Component {
|
||||||
/* the shape of the tile, used */
|
/* the shape of the tile, used */
|
||||||
tileShape: PropTypes.string,
|
tileShape: PropTypes.string,
|
||||||
|
|
||||||
|
/* to set source to local file path during export */
|
||||||
|
mediaSrc: PropTypes.string,
|
||||||
|
|
||||||
/* the maximum image height to use, if the event is an image */
|
/* the maximum image height to use, if the event is an image */
|
||||||
maxImageHeight: PropTypes.number,
|
maxImageHeight: PropTypes.number,
|
||||||
|
|
||||||
|
@ -120,6 +123,7 @@ export default class MessageEvent extends React.Component {
|
||||||
highlightLink={this.props.highlightLink}
|
highlightLink={this.props.highlightLink}
|
||||||
showUrlPreview={this.props.showUrlPreview}
|
showUrlPreview={this.props.showUrlPreview}
|
||||||
tileShape={this.props.tileShape}
|
tileShape={this.props.tileShape}
|
||||||
|
mediaSrc={this.props.mediaSrc}
|
||||||
maxImageHeight={this.props.maxImageHeight}
|
maxImageHeight={this.props.maxImageHeight}
|
||||||
replacingEventId={this.props.replacingEventId}
|
replacingEventId={this.props.replacingEventId}
|
||||||
editState={this.props.editState}
|
editState={this.props.editState}
|
||||||
|
|
|
@ -103,7 +103,7 @@ for (const evType of ALL_RULE_TYPES) {
|
||||||
stateEventTileTypes[evType] = 'messages.TextualEvent';
|
stateEventTileTypes[evType] = 'messages.TextualEvent';
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getHandlerTile(ev) {
|
export function getHandlerTile(ev: MatrixEvent) {
|
||||||
const type = ev.getType();
|
const type = ev.getType();
|
||||||
|
|
||||||
// don't show verification requests we're not involved in,
|
// don't show verification requests we're not involved in,
|
||||||
|
@ -251,6 +251,9 @@ interface IProps {
|
||||||
|
|
||||||
isExporting?: boolean;
|
isExporting?: boolean;
|
||||||
|
|
||||||
|
// Used while exporting to refer to the local source rather than the online one
|
||||||
|
mediaSrc?: string;
|
||||||
|
|
||||||
// show twelve hour timestamps
|
// show twelve hour timestamps
|
||||||
isTwelveHour?: boolean;
|
isTwelveHour?: boolean;
|
||||||
|
|
||||||
|
@ -342,7 +345,7 @@ export default class EventTile extends React.Component<IProps, IState> {
|
||||||
* or 'sent' receipt, for example.
|
* or 'sent' receipt, for example.
|
||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
private get isEligibleForSpecialReceipt() {
|
private get isEligibleForSpecialReceipt(): boolean {
|
||||||
// First, if there are other read receipts then just short-circuit this.
|
// First, if there are other read receipts then just short-circuit this.
|
||||||
if (this.props.readReceipts && this.props.readReceipts.length > 0) return false;
|
if (this.props.readReceipts && this.props.readReceipts.length > 0) return false;
|
||||||
if (!this.props.mxEvent) return false;
|
if (!this.props.mxEvent) return false;
|
||||||
|
@ -1150,6 +1153,7 @@ export default class EventTile extends React.Component<IProps, IState> {
|
||||||
mxEvent={this.props.mxEvent}
|
mxEvent={this.props.mxEvent}
|
||||||
replacingEventId={this.props.replacingEventId}
|
replacingEventId={this.props.replacingEventId}
|
||||||
editState={this.props.editState}
|
editState={this.props.editState}
|
||||||
|
mediaSrc={this.props.mediaSrc}
|
||||||
highlights={this.props.highlights}
|
highlights={this.props.highlights}
|
||||||
highlightLink={this.props.highlightLink}
|
highlightLink={this.props.highlightLink}
|
||||||
showUrlPreview={this.props.showUrlPreview}
|
showUrlPreview={this.props.showUrlPreview}
|
||||||
|
@ -1332,8 +1336,8 @@ class SentReceipt extends React.PureComponent<ISentReceiptProps, ISentReceiptSta
|
||||||
|
|
||||||
return <span className="mx_EventTile_readAvatars">
|
return <span className="mx_EventTile_readAvatars">
|
||||||
<span className={receiptClasses} onMouseEnter={this.onHoverStart} onMouseLeave={this.onHoverEnd}>
|
<span className={receiptClasses} onMouseEnter={this.onHoverStart} onMouseLeave={this.onHoverEnd}>
|
||||||
{nonCssBadge}
|
{ nonCssBadge }
|
||||||
{tooltip}
|
{ tooltip }
|
||||||
</span>
|
</span>
|
||||||
</span>;
|
</span>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,11 +20,13 @@ import exportJS from "./exportJS";
|
||||||
export default class HTMLExporter extends Exporter {
|
export default class HTMLExporter extends Exporter {
|
||||||
protected zip: JSZip;
|
protected zip: JSZip;
|
||||||
protected avatars: Map<string, boolean>;
|
protected avatars: Map<string, boolean>;
|
||||||
|
protected permalinkCreator: RoomPermalinkCreator;
|
||||||
|
|
||||||
constructor(res: MatrixEvent[], room: Room) {
|
constructor(res: MatrixEvent[], room: Room) {
|
||||||
super(res, room);
|
super(res, room);
|
||||||
this.zip = new JSZip();
|
this.zip = new JSZip();
|
||||||
this.avatars = new Map<string, boolean>();
|
this.avatars = new Map<string, boolean>();
|
||||||
|
this.permalinkCreator = new RoomPermalinkCreator(this.room);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected wrapHTML(content: string, room: Room) {
|
protected wrapHTML(content: string, room: Room) {
|
||||||
|
@ -152,11 +154,12 @@ export default class HTMLExporter extends Exporter {
|
||||||
return wantsDateSeparator(prevEvent.getDate(), event.getDate());
|
return wantsDateSeparator(prevEvent.getDate(), event.getDate());
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async createMessageBody(mxEv: MatrixEvent, joined = false) {
|
|
||||||
const eventTile = <li id={mxEv.getId()}>
|
protected getEventTile(mxEv: MatrixEvent, continuation: boolean, mediaSrc?: string) {
|
||||||
|
return <li id={mxEv.getId()}>
|
||||||
<EventTile
|
<EventTile
|
||||||
mxEvent={mxEv}
|
mxEvent={mxEv}
|
||||||
continuation={joined}
|
continuation={continuation}
|
||||||
isRedacted={mxEv.isRedacted()}
|
isRedacted={mxEv.isRedacted()}
|
||||||
replacingEventId={mxEv.replacingEventId()}
|
replacingEventId={mxEv.replacingEventId()}
|
||||||
isExporting={true}
|
isExporting={true}
|
||||||
|
@ -166,8 +169,9 @@ export default class HTMLExporter extends Exporter {
|
||||||
checkUnmounting={() => false}
|
checkUnmounting={() => false}
|
||||||
isTwelveHour={false}
|
isTwelveHour={false}
|
||||||
last={false}
|
last={false}
|
||||||
|
mediaSrc={mediaSrc}
|
||||||
lastInSection={false}
|
lastInSection={false}
|
||||||
permalinkCreator={new RoomPermalinkCreator(this.room)}
|
permalinkCreator={this.permalinkCreator}
|
||||||
lastSuccessful={false}
|
lastSuccessful={false}
|
||||||
isSelectedEvent={false}
|
isSelectedEvent={false}
|
||||||
getRelationsForEvent={null}
|
getRelationsForEvent={null}
|
||||||
|
@ -177,6 +181,36 @@ export default class HTMLExporter extends Exporter {
|
||||||
showReadReceipts={false}
|
showReadReceipts={false}
|
||||||
/>
|
/>
|
||||||
</li>
|
</li>
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async createMessageBody(mxEv: MatrixEvent, joined = false) {
|
||||||
|
let eventTile: JSX.Element;
|
||||||
|
switch (mxEv.getContent().msgtype) {
|
||||||
|
case "m.image": {
|
||||||
|
const blob = await this.getMediaBlob(mxEv);
|
||||||
|
const filePath = `images/${mxEv.getId()}.${blob.type.replace("image/", "")}`;
|
||||||
|
eventTile = this.getEventTile(mxEv, joined, filePath);
|
||||||
|
this.zip.file(filePath, blob);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "m.video": {
|
||||||
|
const blob = await this.getMediaBlob(mxEv);
|
||||||
|
const filePath = `videos/${mxEv.getId()}.${blob.type.replace("video/", "")}`;
|
||||||
|
eventTile = this.getEventTile(mxEv, joined, filePath);
|
||||||
|
this.zip.file(filePath, blob);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "m.audio": {
|
||||||
|
const blob = await this.getMediaBlob(mxEv);
|
||||||
|
const filePath = `audio/${mxEv.getId()}.${blob.type.replace("audio/", "")}`;
|
||||||
|
eventTile = this.getEventTile(mxEv, joined, filePath);
|
||||||
|
this.zip.file(filePath, blob);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
eventTile = this.getEventTile(mxEv, joined);
|
||||||
|
break;
|
||||||
|
}
|
||||||
return renderToStaticMarkup(eventTile);
|
return renderToStaticMarkup(eventTile);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue