Skip to content

Commit 58995b4

Browse files
author
Nathan Zylbersztejn
authored
Merge pull request #65 from mrbot-ai/doc-viewer
Doc viewer
2 parents 839a4c9 + a8fdeca commit 58995b4

File tree

13 files changed

+298
-17
lines changed

13 files changed

+298
-17
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
## 0.5.5
2+
- new prop, `docViewer`, if this props is true, this will treat every link in received messages as a document and will open it in a popup using docs.google.com/viewer (note: this is an experimental feature and should be used with caution)
3+
14
## 0.5.4
25
- When reconnecting to an existing chat session, the bot will send a message contained in the localStorage key specified by the `NEXT_MESSAGE` constant. The message should be stringified JSON with a `message` property describing the message and an `expiry` property set to a UNIX timestamp in milliseconds after which this message should not be sent.
36

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,11 @@ emit('bot_uttered', message, room=socket_id)
212212

213213
When reconnecting to an existing chat session, the bot will send a message contained in the localStorage key specified by the `NEXT_MESSAGE` constant. The message should be stringified JSON with a `message` property describing the message and an `expiry` property set to a UNIX timestamp in milliseconds after which this message should not be sent. This is useful if you would like your bot to be able to offer your user to navigate around the site.
214214

215+
### docViewer
216+
217+
**Note :** this is an **experimental** feature
218+
219+
If you add this prop to the component or to the init script, `docViewer=true` , this will treat links in received messages as links to a document ( `.pdf .doc .xlsx` etc. ) and will open them in a popup using `https://docs.google.com/viewer` service
215220

216221
## API
217222

@@ -292,3 +297,4 @@ RUN mkdir -p /root/.npm
292297
[@TheoTomalty](https://github.com/TheoTomalty)
293298
[@Hub4IT](https://github.com/Hub4IT)
294299
[@dliuproduction](https://github.com/dliuproduction)
300+
[@MatthieuJnon](https://github.com/MatthieuJnon)

dev/src/index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
title: "DEV Test",
1717
inputTextFieldHint: "Type a message...",
1818
connectingText: "Waiting for server...",
19+
docViewer: false,
1920
params: {
2021
images: {
2122
dims: {

index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import { Widget, toggleChat, openChat, closeChat, showChat, hideChat, isOpen, is
44

55
const plugin = {
66
init: (args) => {
7-
87
ReactDOM.render(
98
<Widget
109
socketUrl={args.socketUrl}
@@ -25,6 +24,7 @@ const plugin = {
2524
embedded={args.embedded}
2625
openLauncherImage={args.openLauncherImage}
2726
closeImage={args.closeImage}
27+
docViewer={args.docViewer}
2828
/>, document.querySelector(args.selector)
2929
);
3030
}

package-lock.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "rasa-webchat",
3-
"version": "0.5.3",
3+
"version": "0.5.5",
44
"description": "Chat web widget for React apps and Rasa Core chatbots",
55
"main": "lib/index.js",
66
"repository": "git@https://github.com/mrbot-ai/rasa-webchat.git",
Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,34 @@
11
import React, { PureComponent } from 'react';
22
import ReactMarkdown from 'react-markdown';
3-
import { PROP_TYPES } from 'constants';
3+
import { connect } from 'react-redux';
4+
import PropTypes from 'prop-types';
45

6+
import { PROP_TYPES } from 'constants';
7+
import DocViewer from '../docViewer';
58
import './styles.scss';
69

710
class Message extends PureComponent {
811
render() {
12+
const { docViewer } = this.props;
913
const sender = this.props.message.get('sender');
1014
const text = this.props.message.get('text');
1115
return (
1216
<div className={sender}>
13-
<div className="message-text" >
17+
<div className="message-text">
1418
{sender === 'response' ? (
15-
<ReactMarkdown className={'markdown'} source={text} linkTarget={(url) => { if (!url.startsWith('mailto') && !url.startsWith('javascript')) return '_blank'; }} transformLinkUri={null} />
19+
<ReactMarkdown
20+
className={'markdown'}
21+
source={text}
22+
linkTarget={(url) => {
23+
if (!url.startsWith('mailto') && !url.startsWith('javascript')) return '_blank';
24+
return undefined;
25+
}}
26+
transformLinkUri={null}
27+
renderers={{
28+
link: props =>
29+
docViewer ? <DocViewer src={props.href} /> : <a href={props.href}>{props.href}</a>
30+
}}
31+
/>
1632
) : (
1733
text
1834
)}
@@ -23,7 +39,12 @@ class Message extends PureComponent {
2339
}
2440

2541
Message.propTypes = {
26-
message: PROP_TYPES.MESSAGE
42+
message: PROP_TYPES.MESSAGE,
43+
docViewer: PropTypes.bool.isRequired
2744
};
2845

29-
export default Message;
46+
const mapStateToProps = state => ({
47+
docViewer: state.behavior.get('docViewer')
48+
});
49+
50+
export default connect(mapStateToProps)(Message);
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import React, { Component } from 'react';
2+
import PropTypes from 'prop-types';
3+
4+
import Portal from 'utils/portal';
5+
import './style.scss';
6+
7+
class DocViewer extends Component {
8+
constructor() {
9+
super();
10+
this.iframeLoaded = this.iframeLoaded.bind(this);
11+
this.updateIframeSrc = this.updateIframeSrc.bind(this);
12+
this.handleOpenModal = this.handleOpenModal.bind(this);
13+
this.handleCloseModal = this.handleCloseModal.bind(this);
14+
this.state = {
15+
openedModal: false,
16+
iFrameLoading: true
17+
};
18+
}
19+
20+
getIframeLink() {
21+
return `https://docs.google.com/viewer?url=${this.props.src}&embedded=true`;
22+
}
23+
24+
iframeLoaded() {
25+
clearInterval(this.iframeTimeoutId);
26+
this.setState({ iFrameLoading: false });
27+
}
28+
29+
bindActions() {
30+
this.iframeLoaded = this.iframeLoaded.bind(this);
31+
}
32+
33+
updateIframeSrc() {
34+
if (this.iframe) this.iframe.src = this.getIframeLink();
35+
else clearInterval(this.iframeTimeoutId);
36+
}
37+
38+
handleOpenModal() {
39+
this.setState({ openedModal: true });
40+
this.iframeTimeoutId = setInterval(this.updateIframeSrc, 1000 * 4);
41+
}
42+
43+
handleCloseModal() {
44+
this.setState({ openedModal: false, iFrameLoading: true });
45+
}
46+
47+
render() {
48+
const iframeSrc = this.getIframeLink();
49+
50+
return (
51+
<span>
52+
<button onClick={this.handleOpenModal} className="doc-viewer-open-modal-link">
53+
{this.props.src}
54+
</button>
55+
{this.state.openedModal && (
56+
<Portal>
57+
<div className="doc-viewer-modal-fade" aria-hidden="true" onClick={this.handleCloseModal} />
58+
<div className="doc-viewer-modal">
59+
<div className="doc-viewer-modal-body">
60+
{this.state.iFrameLoading && <div className="doc-viewer-spinner" />}
61+
<iframe
62+
src={iframeSrc}
63+
title="viewer"
64+
className="doc-viewer-modal-iframe"
65+
onLoad={this.iframeLoaded}
66+
onError={this.updateIframeSrc}
67+
ref={(iframe) => {
68+
this.iframe = iframe;
69+
}}
70+
/>
71+
</div>
72+
<div className="doc-viewer-modal-footer">
73+
<button type="button" className="doc-viewer-close-modal" onClick={this.handleCloseModal}>
74+
X
75+
</button>
76+
</div>
77+
</div>
78+
</Portal>
79+
)}
80+
</span>
81+
);
82+
}
83+
}
84+
85+
DocViewer.propTypes = {
86+
src: PropTypes.string.isRequired
87+
};
88+
89+
export default DocViewer;
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
@import "variables.scss";
2+
3+
.doc-viewer-modal {
4+
position: fixed;
5+
top: 50%;
6+
left: 50%;
7+
transform: translate(-50%, -50%);
8+
z-index: 20041997;
9+
display: block;
10+
}
11+
12+
.doc-viewer-modal-body {
13+
overflow-y: auto;
14+
border-radius: 5px;
15+
width: 80vw;
16+
max-width: 900px;
17+
position: relative;
18+
height: 90vh;
19+
top: 0;
20+
background: white;
21+
animation: doc-viewer-slide-down 0.2s ease;
22+
}
23+
24+
.doc-viewer-open-modal-link {
25+
color: $blue-1;
26+
text-decoration: underline;
27+
cursor: pointer;
28+
display: inline;
29+
background: none !important;
30+
border: none;
31+
padding: 0 !important;
32+
font: inherit;
33+
}
34+
35+
.doc-viewer-modal-iframe {
36+
height: 100%;
37+
width: 100%;
38+
border: none;
39+
}
40+
41+
.doc-viewer-modal-fade {
42+
opacity: 0.5;
43+
position: fixed;
44+
top: 0;
45+
right: 0;
46+
bottom: 0;
47+
left: 0;
48+
z-index: 10000; // to get on top of the chat widget
49+
background-color: #000;
50+
animation: appear 0.2s ease;
51+
}
52+
53+
.doc-viewer-modal-footer {
54+
flex: 0 0 auto;
55+
border: none;
56+
text-align: center;
57+
margin-top: 2vh;
58+
}
59+
60+
.doc-viewer-close-modal {
61+
border-radius: 50%;
62+
background: #89919b;
63+
color: white;
64+
font-size: 15px;
65+
width: 45px;
66+
height: 45px;
67+
padding: 0;
68+
text-align: center;
69+
cursor: pointer;
70+
touch-action: manipulation;
71+
border: 1px solid transparent;
72+
font-weight: 100;
73+
}
74+
75+
.doc-viewer-spinner {
76+
display: inline-block;
77+
width: 64px;
78+
height: 64px;
79+
position: fixed;
80+
top: 50%;
81+
left: 50%;
82+
transform: translate(-32px, -32px);
83+
animation: appear .6s ease-in;
84+
}
85+
.doc-viewer-spinner:after {
86+
content: " ";
87+
display: block;
88+
width: 46px;
89+
height: 46px;
90+
margin: 1px;
91+
border-radius: 50%;
92+
border: 5px solid $blue-1;
93+
border-color: $blue-1 transparent $blue-1 transparent;
94+
will-change: transform;
95+
animation: doc-viewer-spinner 1.2s linear infinite;
96+
}
97+
@keyframes doc-viewer-spinner {
98+
0% {
99+
transform: rotate(0deg);
100+
}
101+
100% {
102+
transform: rotate(360deg);
103+
}
104+
}
105+
106+
@keyframes appear {
107+
0% {
108+
opacity: 0;
109+
}
110+
111+
100% {
112+
opacity: 0.5;
113+
}
114+
}
115+
116+
@keyframes doc-viewer-slide-down {
117+
0% {
118+
opacity: 0;
119+
transform: translateY(-100px);
120+
}
121+
122+
100% {
123+
opacity: 1;
124+
transform: translateY(0);
125+
}
126+
}

src/index.js

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ const ConnectedWidget = (props) => {
1313
props.inputTextFieldHint,
1414
props.connectingText,
1515
sock,
16-
storage
16+
storage,
17+
props.docViewer,
1718
);
1819
return (<Provider store={store}>
1920
<Widget
@@ -57,7 +58,8 @@ ConnectedWidget.propTypes = {
5758
embedded: PropTypes.bool,
5859
params: PropTypes.object,
5960
openLauncherImage: PropTypes.string,
60-
closeImage: PropTypes.string
61+
closeImage: PropTypes.string,
62+
docViewer: PropTypes.bool
6163
};
6264

6365
ConnectedWidget.defaultProps = {
@@ -72,8 +74,9 @@ ConnectedWidget.defaultProps = {
7274
badge: 0,
7375
embedded: false,
7476
params: {
75-
storage: "local"
76-
}
77+
storage: 'local'
78+
},
79+
docViewer: false
7780
};
7881

7982
export default ConnectedWidget;

0 commit comments

Comments
 (0)