Compare commits
40
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
12a0660e5b | ||
|
|
2502460143 | ||
|
|
c31631b69f | ||
|
|
711cf7c690 | ||
|
|
3669ed7645 | ||
|
|
41ebf3c705 | ||
|
|
6f359be64d | ||
|
|
d472a321de | ||
|
|
eda9f0f7f9 | ||
|
|
d9c729d9ab | ||
|
|
78b0909f02 | ||
|
|
61a9b6c56f | ||
|
|
a6bc2a1f8f | ||
|
|
503c9128d4 | ||
|
|
1b4bba051e | ||
|
|
c14fb857bc | ||
|
|
be528ff282 | ||
|
|
fb9461834a | ||
|
|
0217d84721 | ||
|
|
f1744c3f69 | ||
|
|
a79373c8cd | ||
|
|
d2975f0c20 | ||
|
|
d782fb8eab | ||
|
|
bdf152e5d6 | ||
|
|
40edad1c99 | ||
|
|
7239aae8aa | ||
|
|
81c789b713 | ||
|
|
744da99f30 | ||
|
|
112ce08fde | ||
|
|
3fd01648a8 | ||
|
|
619ec15920 | ||
|
|
a6a097b15f | ||
|
|
450a2f6975 | ||
|
|
e006043290 | ||
|
|
34db50280e | ||
|
|
6268fb68f8 | ||
|
|
9a310a0438 | ||
|
|
821ab6a16f | ||
|
|
f9e78bf280 | ||
|
|
83488ca878 |
@@ -11,7 +11,7 @@
|
|||||||
"relation": ["delegate_permission/common.get_login_creds"],
|
"relation": ["delegate_permission/common.get_login_creds"],
|
||||||
"target": {
|
"target": {
|
||||||
"namespace": "web",
|
"namespace": "web",
|
||||||
"site": "https://api.tinode.co"
|
"site": "https://web.tinode.co"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -22,4 +22,4 @@
|
|||||||
"sha256_cert_fingerprints":
|
"sha256_cert_fingerprints":
|
||||||
["AA:0A:FF:C4:F3:1F:58:83:F3:8E:69:03:2E:B9:AA:1A:C4:62:65:F1:1E:50:F9:7E:AD:35:56:55:32:B7:1A:C6"]
|
["AA:0A:FF:C4:F3:1F:58:83:F3:8E:69:03:2E:B9:AA:1A:C4:62:65:F1:1E:50:F9:7E:AD:35:56:55:32:B7:1A:C6"]
|
||||||
}
|
}
|
||||||
}]
|
}]
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ The app is about 200KB minified and gzipped. That includes all dependencies (Rea
|
|||||||
|
|
||||||
Although the app is generally usable, keep in mind that this is work in progress. Some bugs probably exist. The app was tested in the latest Chrome & Firefox only. NPM package is available at https://www.npmjs.com/package/tinode-webapp.
|
Although the app is generally usable, keep in mind that this is work in progress. Some bugs probably exist. The app was tested in the latest Chrome & Firefox only. NPM package is available at https://www.npmjs.com/package/tinode-webapp.
|
||||||
|
|
||||||
Try possibly newer or older version live at https://api.tinode.co/. For passwords for demo accounts and other instructions [see here](https://github.com/tinode/chat/#demo).
|
Try possibly newer or older version live at https://web.tinode.co/. For passwords for demo accounts and other instructions [see here](https://github.com/tinode/chat/#demo).
|
||||||
|
|
||||||
## Getting support
|
## Getting support
|
||||||
|
|
||||||
@@ -19,7 +19,7 @@ Try possibly newer or older version live at https://api.tinode.co/. For password
|
|||||||
|
|
||||||
## Internationalization
|
## Internationalization
|
||||||
|
|
||||||
The app is fully internationalized using [React-Intl](https://github.com/yahoo/react-intl). The UI language is selected automatically from the language specified by the browser. A specific language can be forced by adding `hl=XX` parameter to the URL, i.e. https://api.tinode.co/#?hl=ru.
|
The app is fully internationalized using [React-Intl](https://github.com/yahoo/react-intl). The UI language is selected automatically from the language specified by the browser. A specific language can be forced by adding `hl=XX` parameter to the URL, i.e. https://web.tinode.co/#?hl=ru.
|
||||||
|
|
||||||
As of the time of this writing the following translations exist: English, Russian, Simplified Chinese. More translations are welcome. Send a pull request with a json file with translated strings. Take a look at [English](/src/i18n/en.json), [Russian](/src/i18n/ru.json), or [Simplified Chinese](/src/i18n/zh-CN.json) translations for guidance.
|
As of the time of this writing the following translations exist: English, Russian, Simplified Chinese. More translations are welcome. Send a pull request with a json file with translated strings. Take a look at [English](/src/i18n/en.json), [Russian](/src/i18n/ru.json), or [Simplified Chinese](/src/i18n/zh-CN.json) translations for guidance.
|
||||||
|
|
||||||
@@ -32,10 +32,17 @@ As of the time of this writing the following translations exist: English, Russia
|
|||||||
|
|
||||||
## Other
|
## Other
|
||||||
|
|
||||||
Demo avatars and some other graphics are from https://www.pexels.com/ under [CC0 license](https://www.pexels.com/photo-license/).
|
* Demo avatars and some other graphics are from https://www.pexels.com/ under [CC0 license](https://www.pexels.com/photo-license/).
|
||||||
|
* Background patterns from http://subtlepatterns.com/, commercial and non-commercial use allowed with attribution.
|
||||||
|
|
||||||
Background patterns from http://subtlepatterns.com/, commercial and non-commercial use allowed with attribution.
|
## Push notifications
|
||||||
|
|
||||||
|
If you want to use the app with your own server and want web push notification to work you have to set them up:
|
||||||
|
|
||||||
|
* Register at https://firebase.google.com/, set up the project if you have not done so already.
|
||||||
|
* Open https://console.firebase.google.com/, navigate to your project the to `Cloud Messaging`.
|
||||||
|
* Locate `firebase-init.js` in the root folder of your copy of this web app. Get Sender ID and Web Push certificate from https://console.firebase.google.com/: copy `Sender ID` (Project Settings -> Cloud Messaging, "Sender ID") to `messagingSenderId` field, copy `Web Push certificate` (Project Settings -> Cloud Messaging -> Web configuration -> Web Push certificates) to `messagingVapidKey` field.
|
||||||
|
* Copy Google-provided server key to `tinode.conf`, see details [here](https://github.com/tinode/chat/blob/master/docs/faq.md#q-how-to-setup-fcm-push-notifications).
|
||||||
|
|
||||||
## Responsive design
|
## Responsive design
|
||||||
|
|
||||||
|
|||||||
+16
-14
@@ -461,7 +461,7 @@ li.right:hover .menuTrigger {
|
|||||||
|
|
||||||
/* Quoted div */
|
/* Quoted div */
|
||||||
.quoted {
|
.quoted {
|
||||||
margin-left: 1.5rem;
|
margin-left: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* BEGIN InPlaceEdit styles */
|
/* BEGIN InPlaceEdit styles */
|
||||||
@@ -597,7 +597,9 @@ table.permission-editor td.checkbox {
|
|||||||
.bot-form > div > button {
|
.bot-form > div > button {
|
||||||
width: auto;
|
width: auto;
|
||||||
}
|
}
|
||||||
.bot-form button:focus, button:hover, button:active {
|
.bot-form button:focus,
|
||||||
|
.bot-form button:hover,
|
||||||
|
.bot-form button:active {
|
||||||
outline: 0;
|
outline: 0;
|
||||||
background-color: #e3f2fd;
|
background-color: #e3f2fd;
|
||||||
}
|
}
|
||||||
@@ -1238,8 +1240,12 @@ i.search {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#image-preview-container {
|
#image-preview-container {
|
||||||
margin: auto;
|
display: flex;
|
||||||
|
justify-content: space-around;
|
||||||
|
align-items: center;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
#image-preview-footer {
|
#image-preview-footer {
|
||||||
@@ -1327,30 +1333,26 @@ i.search {
|
|||||||
border: 0.62rem solid #c5e1a5;
|
border: 0.62rem solid #c5e1a5;
|
||||||
color: #c5e1a5;
|
color: #c5e1a5;
|
||||||
border-top-color: transparent;
|
border-top-color: transparent;
|
||||||
border-right-color: inherit;
|
|
||||||
border-bottom-color: inherit;
|
|
||||||
border-left-color: transparent;
|
border-left-color: transparent;
|
||||||
box-shadow: 0 1.6px 0.5px -0.8px rgba(0, 0, 0, 0.12);
|
box-shadow: 0 1.6px 0.5px -0.8px rgba(0, 0, 0, 0.12);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Right triangle placed bottom right flush. */
|
/* Right triangle placed bottom right flush. */
|
||||||
.chat-box .right .tip::before {
|
.chat-box .right .tip::before {
|
||||||
content: '';
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
width: 0;
|
width: 0;
|
||||||
height: 0;
|
height: 0;
|
||||||
left: auto;
|
left: auto;
|
||||||
right: -.8rem;
|
right: -.8rem;
|
||||||
top: auto;
|
top: auto;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
border: 0.62rem solid #fafafa;
|
border: 0.62rem solid #fafafa;
|
||||||
color: #fafafa;
|
color: #fafafa;
|
||||||
border-top-color: transparent;
|
border-top-color: transparent;
|
||||||
border-right-color: transparent;
|
border-right-color: transparent;
|
||||||
border-bottom-color: inherit;
|
|
||||||
border-left-color: inherit;
|
|
||||||
box-shadow: 0 1.6px 0.5px -0.8px rgba(0, 0, 0, 0.12);
|
box-shadow: 0 1.6px 0.5px -0.8px rgba(0, 0, 0, 0.12);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
+1
-305
@@ -1,305 +1 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="192" height="192"><defs><filter id="f" color-interpolation-filters="sRGB"><feFlood flood-opacity=".251" flood-color="#000" result="flood"/><feComposite in="flood" in2="SourceGraphic" operator="in" result="composite1"/><feGaussianBlur in="composite1" result="blur"/><feOffset dy="-1" result="offset"/><feComposite in="SourceGraphic" in2="offset" result="fbSourceGraphic"/><feColorMatrix result="fbSourceGraphicAlpha" in="fbSourceGraphic" values="0 0 0 -1 0 0 0 0 -1 0 0 0 0 -1 0 0 0 0 1 0"/><feFlood flood-opacity=".251" flood-color="#000" result="flood"/><feComposite in2="fbSourceGraphic" in="flood" operator="in" result="composite1"/><feGaussianBlur in="composite1" result="blur"/><feOffset dy="-3" result="offset"/><feComposite in2="offset" in="fbSourceGraphic" result="composite2"/></filter><filter id="d" color-interpolation-filters="sRGB"><feFlood flood-opacity=".196" flood-color="#FFF" result="flood"/><feComposite in="flood" in2="SourceGraphic" operator="out" result="composite1"/><feGaussianBlur in="composite1" result="blur"/><feOffset dy="2" result="offset"/><feComposite in="offset" in2="SourceGraphic" operator="atop" result="composite2"/></filter><filter id="b" color-interpolation-filters="sRGB"><feFlood flood-opacity=".196" flood-color="#000" result="flood"/><feComposite in="flood" in2="SourceGraphic" operator="out" result="composite1"/><feGaussianBlur in="composite1" result="blur"/><feOffset dy="-2" result="offset"/><feComposite in="offset" in2="SourceGraphic" operator="atop" result="composite2"/></filter><filter id="c" color-interpolation-filters="sRGB"><feFlood flood-opacity=".196" flood-color="#000" result="flood"/><feComposite in="flood" in2="SourceGraphic" operator="out" result="composite1"/><feGaussianBlur in="composite1" result="blur"/><feOffset dy="-2" result="offset"/><feComposite in="offset" in2="SourceGraphic" operator="atop" result="composite2"/></filter><linearGradient id="a"><stop offset="0" stop-opacity=".251"/><stop offset="1" stop-color="#fff" stop-opacity="0"/></linearGradient><linearGradient xlink:href="#a" id="e" x1="102.814" y1="97.076" x2="219.019" y2="170.481" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1.29306 0 0 1.00596 -43.959 .807)"/></defs><g fill-rule="evenodd"><path d="M14.102 64A88 87.925 0 008 95.924c-.012 44.455 18.485 70.138 1.863 79.822-12.733 7.419 43.153 8.258 70.137 8.248V64H14.102z" fill="#fc5d3e" filter="url(#b)"/><path d="M80 64v119.994c8.24-.003 14.055-.08 14.055-.08L96 183.85a88 87.925 0 0088-87.926A88 87.925 0 00177.99 64H80z" fill="#45ae49" filter="url(#c)"/><path d="M96 8A88 87.925 0 009.52 80h172.978A88 87.925 0 0096 8z" fill="#2d526b" filter="url(#d)"/></g><path d="M88.002 40.975l-8.6 112.808 39.791 26.94c34.169-9.369 59.381-38.298 63.971-73.4z" fill="url(#e)" fill-rule="evenodd" opacity=".5"/><text y="153.195" x="53.125" style="line-height:125%;-inkscape-font-specification:'Arial, Bold';text-align:start" font-weight="700" font-size="160" font-family="Arial" letter-spacing="0" word-spacing="0"><tspan style="line-height:125%;-inkscape-font-specification:'Arial, Bold';text-align:start" y="153.195" x="53.125" fill="#fff">t</tspan></text></svg>
|
||||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
|
||||||
|
|
||||||
<svg
|
|
||||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
|
||||||
xmlns:cc="http://creativecommons.org/ns#"
|
|
||||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
|
||||||
xmlns:svg="http://www.w3.org/2000/svg"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
|
||||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
|
||||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
|
||||||
width="192"
|
|
||||||
height="192"
|
|
||||||
viewBox="0 0 192 192"
|
|
||||||
id="svg2"
|
|
||||||
version="1.1"
|
|
||||||
inkscape:version="0.91 r13725"
|
|
||||||
sodipodi:docname="logo.svg"
|
|
||||||
inkscape:export-xdpi="120"
|
|
||||||
inkscape:export-ydpi="120">
|
|
||||||
<defs
|
|
||||||
id="defs4">
|
|
||||||
<linearGradient
|
|
||||||
inkscape:collect="always"
|
|
||||||
id="linearGradient16239">
|
|
||||||
<stop
|
|
||||||
style="stop-color:#000000;stop-opacity:0.25098041"
|
|
||||||
offset="0"
|
|
||||||
id="stop16241" />
|
|
||||||
<stop
|
|
||||||
style="stop-color:#ffffff;stop-opacity:0;"
|
|
||||||
offset="1"
|
|
||||||
id="stop16243" />
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient
|
|
||||||
inkscape:collect="always"
|
|
||||||
xlink:href="#linearGradient16239"
|
|
||||||
id="linearGradient16249"
|
|
||||||
x1="102.81445"
|
|
||||||
y1="97.075966"
|
|
||||||
x2="219.01871"
|
|
||||||
y2="170.4807"
|
|
||||||
gradientUnits="userSpaceOnUse"
|
|
||||||
gradientTransform="matrix(1.293059,0,0,1.0059594,-43.958501,0.80696392)" />
|
|
||||||
<filter
|
|
||||||
style="color-interpolation-filters:sRGB;"
|
|
||||||
inkscape:label="Drop Shadow"
|
|
||||||
id="filter16588">
|
|
||||||
<feFlood
|
|
||||||
flood-opacity="0.25098"
|
|
||||||
flood-color="rgb(0,0,0)"
|
|
||||||
result="flood"
|
|
||||||
id="feFlood16590" />
|
|
||||||
<feComposite
|
|
||||||
in="flood"
|
|
||||||
in2="SourceGraphic"
|
|
||||||
operator="in"
|
|
||||||
result="composite1"
|
|
||||||
id="feComposite16592" />
|
|
||||||
<feGaussianBlur
|
|
||||||
in="composite1"
|
|
||||||
stdDeviation="0"
|
|
||||||
result="blur"
|
|
||||||
id="feGaussianBlur16594" />
|
|
||||||
<feOffset
|
|
||||||
dx="-0"
|
|
||||||
dy="-1"
|
|
||||||
result="offset"
|
|
||||||
id="feOffset16596" />
|
|
||||||
<feComposite
|
|
||||||
in="SourceGraphic"
|
|
||||||
in2="offset"
|
|
||||||
operator="over"
|
|
||||||
result="fbSourceGraphic"
|
|
||||||
id="feComposite16598" />
|
|
||||||
<feColorMatrix
|
|
||||||
result="fbSourceGraphicAlpha"
|
|
||||||
in="fbSourceGraphic"
|
|
||||||
values="0 0 0 -1 0 0 0 0 -1 0 0 0 0 -1 0 0 0 0 1 0"
|
|
||||||
id="feColorMatrix4300" />
|
|
||||||
<feFlood
|
|
||||||
id="feFlood4302"
|
|
||||||
flood-opacity="0.25098"
|
|
||||||
flood-color="rgb(0,0,0)"
|
|
||||||
result="flood"
|
|
||||||
in="fbSourceGraphic" />
|
|
||||||
<feComposite
|
|
||||||
id="feComposite4304"
|
|
||||||
in2="fbSourceGraphic"
|
|
||||||
in="flood"
|
|
||||||
operator="in"
|
|
||||||
result="composite1" />
|
|
||||||
<feGaussianBlur
|
|
||||||
id="feGaussianBlur4306"
|
|
||||||
in="composite1"
|
|
||||||
stdDeviation="0"
|
|
||||||
result="blur" />
|
|
||||||
<feOffset
|
|
||||||
id="feOffset4308"
|
|
||||||
dx="0"
|
|
||||||
dy="-3"
|
|
||||||
result="offset" />
|
|
||||||
<feComposite
|
|
||||||
id="feComposite4310"
|
|
||||||
in2="offset"
|
|
||||||
in="fbSourceGraphic"
|
|
||||||
operator="over"
|
|
||||||
result="composite2" />
|
|
||||||
</filter>
|
|
||||||
<filter
|
|
||||||
style="color-interpolation-filters:sRGB;"
|
|
||||||
inkscape:label="Drop Shadow"
|
|
||||||
id="filter4539">
|
|
||||||
<feFlood
|
|
||||||
flood-opacity="0.196078"
|
|
||||||
flood-color="rgb(255,255,255)"
|
|
||||||
result="flood"
|
|
||||||
id="feFlood4541" />
|
|
||||||
<feComposite
|
|
||||||
in="flood"
|
|
||||||
in2="SourceGraphic"
|
|
||||||
operator="out"
|
|
||||||
result="composite1"
|
|
||||||
id="feComposite4543" />
|
|
||||||
<feGaussianBlur
|
|
||||||
in="composite1"
|
|
||||||
stdDeviation="0"
|
|
||||||
result="blur"
|
|
||||||
id="feGaussianBlur4545" />
|
|
||||||
<feOffset
|
|
||||||
dx="0"
|
|
||||||
dy="2"
|
|
||||||
result="offset"
|
|
||||||
id="feOffset4547" />
|
|
||||||
<feComposite
|
|
||||||
in="offset"
|
|
||||||
in2="SourceGraphic"
|
|
||||||
operator="atop"
|
|
||||||
result="composite2"
|
|
||||||
id="feComposite4549" />
|
|
||||||
</filter>
|
|
||||||
<filter
|
|
||||||
style="color-interpolation-filters:sRGB;"
|
|
||||||
inkscape:label="Drop Shadow"
|
|
||||||
id="filter4779">
|
|
||||||
<feFlood
|
|
||||||
flood-opacity="0.196078"
|
|
||||||
flood-color="rgb(0,0,0)"
|
|
||||||
result="flood"
|
|
||||||
id="feFlood4781" />
|
|
||||||
<feComposite
|
|
||||||
in="flood"
|
|
||||||
in2="SourceGraphic"
|
|
||||||
operator="out"
|
|
||||||
result="composite1"
|
|
||||||
id="feComposite4783" />
|
|
||||||
<feGaussianBlur
|
|
||||||
in="composite1"
|
|
||||||
stdDeviation="0"
|
|
||||||
result="blur"
|
|
||||||
id="feGaussianBlur4785" />
|
|
||||||
<feOffset
|
|
||||||
dx="0"
|
|
||||||
dy="-2"
|
|
||||||
result="offset"
|
|
||||||
id="feOffset4787" />
|
|
||||||
<feComposite
|
|
||||||
in="offset"
|
|
||||||
in2="SourceGraphic"
|
|
||||||
operator="atop"
|
|
||||||
result="composite2"
|
|
||||||
id="feComposite4789" />
|
|
||||||
</filter>
|
|
||||||
<filter
|
|
||||||
style="color-interpolation-filters:sRGB;"
|
|
||||||
inkscape:label="Drop Shadow"
|
|
||||||
id="filter4791">
|
|
||||||
<feFlood
|
|
||||||
flood-opacity="0.196078"
|
|
||||||
flood-color="rgb(0,0,0)"
|
|
||||||
result="flood"
|
|
||||||
id="feFlood4793" />
|
|
||||||
<feComposite
|
|
||||||
in="flood"
|
|
||||||
in2="SourceGraphic"
|
|
||||||
operator="out"
|
|
||||||
result="composite1"
|
|
||||||
id="feComposite4795" />
|
|
||||||
<feGaussianBlur
|
|
||||||
in="composite1"
|
|
||||||
stdDeviation="0"
|
|
||||||
result="blur"
|
|
||||||
id="feGaussianBlur4797" />
|
|
||||||
<feOffset
|
|
||||||
dx="0"
|
|
||||||
dy="-2"
|
|
||||||
result="offset"
|
|
||||||
id="feOffset4799" />
|
|
||||||
<feComposite
|
|
||||||
in="offset"
|
|
||||||
in2="SourceGraphic"
|
|
||||||
operator="atop"
|
|
||||||
result="composite2"
|
|
||||||
id="feComposite4801" />
|
|
||||||
</filter>
|
|
||||||
</defs>
|
|
||||||
<sodipodi:namedview
|
|
||||||
id="base"
|
|
||||||
pagecolor="#ffffff"
|
|
||||||
bordercolor="#666666"
|
|
||||||
borderopacity="1.0"
|
|
||||||
inkscape:pageopacity="0"
|
|
||||||
inkscape:pageshadow="2"
|
|
||||||
inkscape:zoom="4.1822917"
|
|
||||||
inkscape:cx="39.691158"
|
|
||||||
inkscape:cy="95.079953"
|
|
||||||
inkscape:document-units="px"
|
|
||||||
inkscape:current-layer="layer6"
|
|
||||||
showgrid="false"
|
|
||||||
units="px"
|
|
||||||
inkscape:window-width="1680"
|
|
||||||
inkscape:window-height="1005"
|
|
||||||
inkscape:window-x="0"
|
|
||||||
inkscape:window-y="1"
|
|
||||||
inkscape:window-maximized="1" />
|
|
||||||
<metadata
|
|
||||||
id="metadata7">
|
|
||||||
<rdf:RDF>
|
|
||||||
<cc:Work
|
|
||||||
rdf:about="">
|
|
||||||
<dc:format>image/svg+xml</dc:format>
|
|
||||||
<dc:type
|
|
||||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
|
||||||
<dc:title></dc:title>
|
|
||||||
</cc:Work>
|
|
||||||
</rdf:RDF>
|
|
||||||
</metadata>
|
|
||||||
<g
|
|
||||||
style="display:inline"
|
|
||||||
inkscape:label="Colors copy"
|
|
||||||
id="g16151"
|
|
||||||
inkscape:groupmode="layer">
|
|
||||||
<path
|
|
||||||
style="opacity:1;fill:#fc5d3e;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;filter:url(#filter4779)"
|
|
||||||
d="M 14.101562,64 A 87.999993,87.924608 0 0 0 8,95.923828 C 7.98792,140.37931 26.484895,166.06204 9.8632812,175.74609 -2.8697567,183.16463 53.015901,184.00441 80,183.99414 L 80,64 14.101562,64 Z"
|
|
||||||
id="rect16153"
|
|
||||||
inkscape:connector-curvature="0" />
|
|
||||||
<path
|
|
||||||
style="opacity:1;fill:#45ae49;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;filter:url(#filter4791)"
|
|
||||||
d="m 80,64 0,119.99414 c 8.240743,-0.003 14.054688,-0.0801 14.054688,-0.0801 L 96,183.84961 A 87.999993,87.924608 0 0 0 184,95.923828 87.999993,87.924608 0 0 0 177.99023,64 L 80,64 Z"
|
|
||||||
id="rect16157"
|
|
||||||
inkscape:connector-curvature="0" />
|
|
||||||
<path
|
|
||||||
style="opacity:1;fill:#2d526b;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;filter:url(#filter4539)"
|
|
||||||
d="M 96,8 A 87.999993,87.924608 0 0 0 9.5195312,80 L 182.49805,80 A 87.999993,87.924608 0 0 0 96,8 Z"
|
|
||||||
id="rect16159"
|
|
||||||
inkscape:connector-curvature="0" />
|
|
||||||
</g>
|
|
||||||
<g
|
|
||||||
inkscape:label="Letter t"
|
|
||||||
id="g16167"
|
|
||||||
inkscape:groupmode="layer"
|
|
||||||
style="display:inline">
|
|
||||||
<g
|
|
||||||
inkscape:groupmode="layer"
|
|
||||||
id="layer6"
|
|
||||||
inkscape:label="Shadow"
|
|
||||||
style="display:inline;opacity:0.5">
|
|
||||||
<path
|
|
||||||
style="opacity:1;fill:url(#linearGradient16249);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
|
|
||||||
d="M 88.001953,40.974609 79.402344,153.7832 119.19336,180.72266 c 34.16846,-9.36841 59.38095,-38.29738 63.9707,-73.40039 z"
|
|
||||||
id="rect16225"
|
|
||||||
inkscape:connector-curvature="0"
|
|
||||||
sodipodi:nodetypes="ccccc" />
|
|
||||||
</g>
|
|
||||||
<text
|
|
||||||
sodipodi:linespacing="125%"
|
|
||||||
id="text16169"
|
|
||||||
y="153.19533"
|
|
||||||
x="53.125"
|
|
||||||
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:160.00001526px;line-height:125%;font-family:Arial;-inkscape-font-specification:'Arial, Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
|
||||||
xml:space="preserve"><tspan
|
|
||||||
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:160.00001526px;line-height:125%;font-family:Arial;-inkscape-font-specification:'Arial, Bold';text-align:start;writing-mode:lr-tb;text-anchor:start;fill:#ffffff;fill-opacity:1"
|
|
||||||
y="153.19533"
|
|
||||||
x="53.125"
|
|
||||||
id="tspan16171"
|
|
||||||
sodipodi:role="line">t</tspan></text>
|
|
||||||
</g>
|
|
||||||
<g
|
|
||||||
inkscape:label="Bubble"
|
|
||||||
inkscape:groupmode="layer"
|
|
||||||
id="layer1"
|
|
||||||
transform="translate(0,-860.36222)"
|
|
||||||
style="display:none">
|
|
||||||
<path
|
|
||||||
style="opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;filter:url(#filter16588)"
|
|
||||||
d="M 0,0 0,192 192,192 192,0 0,0 Z m 96,8 a 87.999998,87.924613 0 0 1 88,87.923828 87.999998,87.924613 0 0 1 -88,87.925782 l -1.945312,0.0644 c 0,0 -100.813021,1.51613 -84.1914068,-8.16797 C 26.484896,166.06199 7.98792,140.37931 8,95.923828 A 87.999998,87.924613 0 0 1 96,8 Z"
|
|
||||||
transform="translate(0,860.36222)"
|
|
||||||
id="rect16110"
|
|
||||||
inkscape:connector-curvature="0"
|
|
||||||
inkscape:export-xdpi="120.00001"
|
|
||||||
inkscape:export-ydpi="120.00001" />
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 3.2 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 28 KiB |
+14
-10
@@ -3,20 +3,24 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<title>Tinode</title>
|
<title>Tinode</title>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<meta name="application-name" content="TinodeWeb">
|
<meta name="application-name" content="Tinode Web, version for developers" />
|
||||||
<meta name="description" content="Tinode instant messenger">
|
<meta name="description" content="Tinode instant messenger, development version" />
|
||||||
<meta name="application-url" content="https://api.tinode.co/">
|
<meta name="application-url" content="https://web.tinode.co/">
|
||||||
<!-- Browser styling for Chrome, FF, Opera -->
|
<!-- Browser styling for Chrome, FF, Opera -->
|
||||||
<meta name="theme-color" content="#3949AB">
|
<meta name="theme-color" content="#3949AB" />
|
||||||
<!-- Ask Google not to offer page translation -->
|
<!-- Ask Google not to offer page translation -->
|
||||||
<meta name="google" value="notranslate">
|
<meta name="google" value="notranslate" />
|
||||||
|
<!-- Open Graph description -->
|
||||||
|
<meta name="og:url" content="https://web.tinode.co/">
|
||||||
|
<meta name="og:title" content="Tinode Web Development">
|
||||||
|
<meta name="og:image" content="img/og-logo.jpeg">
|
||||||
|
|
||||||
<link rel="manifest" href="manifest.json" />
|
<link rel="manifest" href="manifest.json" />
|
||||||
<link rel="shortcut icon" id="shortcut-icon" href="img/logo32x32.png" type="image/png" />
|
<link rel="shortcut icon" id="shortcut-icon" href="img/logo32x32.png" type="image/png" />
|
||||||
<link rel="apple-touch-icon" sizes="192x192" href="img/logo192.png" type="image/png" />
|
<link rel="apple-touch-icon" sizes="192x192" href="img/logo192.png" type="image/png" />
|
||||||
<link rel="canonical" href="https://api.tinode.co/" />
|
<link rel="canonical" href="https://web.tinode.co/" />
|
||||||
<!-- Google's Roboto font -->
|
<!-- Google's Roboto font -->
|
||||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,400i,500,700,700i" />
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,400i,500,700,700i" />
|
||||||
<!-- Google's material design icons -->
|
<!-- Google's material design icons -->
|
||||||
@@ -30,8 +34,8 @@
|
|||||||
<script crossorigin="anonymous" src="https://unpkg.com/prop-types@15/prop-types.js"></script>
|
<script crossorigin="anonymous" src="https://unpkg.com/prop-types@15/prop-types.js"></script>
|
||||||
<script crossorigin="anonymous" src="https://unpkg.com/react-intl@2/dist/react-intl.js"></script>
|
<script crossorigin="anonymous" src="https://unpkg.com/react-intl@2/dist/react-intl.js"></script>
|
||||||
<!-- Firebase scripts -->
|
<!-- Firebase scripts -->
|
||||||
<script crossorigin="anonymous" src="https://unpkg.com/firebase@5.8.6/firebase-app.js"></script>
|
<script crossorigin="anonymous" src="https://unpkg.com/firebase@6.4.2/firebase-app.js"></script>
|
||||||
<script crossorigin="anonymous" src="https://unpkg.com/firebase@5.8.6/firebase-messaging.js"></script>
|
<script crossorigin="anonymous" src="https://unpkg.com/firebase@6.4.2/firebase-messaging.js"></script>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
+17
-13
@@ -2,26 +2,30 @@
|
|||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<title>Tinode</title>
|
<title>Tinode</title>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<meta name="application-name" content="TinodeWeb">
|
<meta name="application-name" content="Tinode Web" />
|
||||||
<meta name="description" content="Tinode instant messaging">
|
<meta name="description" content="Tinode instant messaging in a browser" />
|
||||||
<meta name="application-url" content="https://api.tinode.co/">
|
<meta name="application-url" content="https://web.tinode.co/">
|
||||||
<meta name="theme-color" content="#3949AB">
|
<meta name="theme-color" content="#3949AB" />
|
||||||
<meta name="google" value="notranslate">
|
<meta name="google" value="notranslate" />
|
||||||
|
<meta name="format-detection" content="telephone=no" />
|
||||||
|
<meta name="og:url" content="https://web.tinode.co/">
|
||||||
|
<meta name="og:title" content="Tinode Web">
|
||||||
|
<meta name="og:image" content="img/og-logo.jpeg">
|
||||||
<link rel="manifest" href="manifest.json" />
|
<link rel="manifest" href="manifest.json" />
|
||||||
<link rel="shortcut icon" id="shortcut-icon" href="img/logo32x32.png" type="image/png" />
|
<link rel="shortcut icon" id="shortcut-icon" href="img/logo32x32.png" type="image/png" />
|
||||||
<link rel="apple-touch-icon" sizes="192x192" href="img/logo192.png" type="image/png" />
|
<link rel="apple-touch-icon" sizes="192x192" href="img/logo192.png" type="image/png" />
|
||||||
<link rel="canonical" href="https://api.tinode.co/" />
|
<link rel="canonical" href="https://web.tinode.co/" />
|
||||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,400i,500,700,700i" />
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,400i,500,700,700i" />
|
||||||
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons" />
|
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons" />
|
||||||
<link rel="stylesheet" href="css/base.min.css" />
|
<link rel="stylesheet" href="css/base.min.css" />
|
||||||
<script crossorigin="anonymous" src="https://unpkg.com/react@16.8.4/umd/react.production.min.js"></script>
|
<script crossorigin="anonymous" src="https://unpkg.com/react@16.9.0/umd/react.production.min.js"></script>
|
||||||
<script crossorigin="anonymous" src="https://unpkg.com/react-dom@16.8.4/umd/react-dom.production.min.js"></script>
|
<script crossorigin="anonymous" src="https://unpkg.com/react-dom@16.9.0/umd/react-dom.production.min.js"></script>
|
||||||
<script crossorigin="anonymous" src="https://unpkg.com/prop-types@15.7.2/prop-types.min.js"></script>
|
<script crossorigin="anonymous" src="https://unpkg.com/prop-types@15.7.2/prop-types.min.js"></script>
|
||||||
<script crossorigin="anonymous" src="https://unpkg.com/react-intl@2.8.0/dist/react-intl.min.js"></script>
|
<script crossorigin="anonymous" src="https://unpkg.com/react-intl@2.9.0/dist/react-intl.min.js"></script>
|
||||||
<script crossorigin="anonymous" src="https://unpkg.com/firebase@5.8.6/firebase-app.js"></script>
|
<script crossorigin="anonymous" src="https://unpkg.com/firebase@6.4.2/firebase-app.js"></script>
|
||||||
<script crossorigin="anonymous" src="https://unpkg.com/firebase@5.8.6/firebase-messaging.js"></script>
|
<script crossorigin="anonymous" src="https://unpkg.com/firebase@6.4.2/firebase-messaging.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<noscript><div id="noscript-alertbox"><div id="noscript-title">Javascript support is required.</div>
|
<noscript><div id="noscript-alertbox"><div id="noscript-title">Javascript support is required.</div>
|
||||||
|
|||||||
Generated
+2037
-762
File diff suppressed because it is too large
Load Diff
+20
-19
@@ -1,20 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "tinode-webapp",
|
"name": "tinode-webapp",
|
||||||
"description": "Tinode messenger for the web",
|
"description": "Tinode messenger for the web",
|
||||||
"version": "0.15.15-rc2",
|
"version": "0.16.1",
|
||||||
"devDependencies": {
|
|
||||||
"@babel/core": "^7.3.4",
|
|
||||||
"@babel/preset-env": "^7.3.4",
|
|
||||||
"@babel/preset-react": "^7.0.0",
|
|
||||||
"babel-loader": "^8.0.5",
|
|
||||||
"babel-plugin-react-intl": "^3.0.1",
|
|
||||||
"browserslist": "^4.4.1",
|
|
||||||
"cssnano": "^4.1.10",
|
|
||||||
"postcss": "^7.0.17",
|
|
||||||
"uglifyjs-webpack-plugin": "^2.1.2",
|
|
||||||
"webpack": "^4.29.6",
|
|
||||||
"webpack-cli": "^3.2.3"
|
|
||||||
},
|
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/tinode/example-react-js.git"
|
"url": "https://github.com/tinode/example-react-js.git"
|
||||||
@@ -49,6 +36,7 @@
|
|||||||
"img/logo192.png",
|
"img/logo192.png",
|
||||||
"img/badge96.png",
|
"img/badge96.png",
|
||||||
"img/broken_image.png",
|
"img/broken_image.png",
|
||||||
|
"img/og-logo.jpeg",
|
||||||
"index-dev.html",
|
"index-dev.html",
|
||||||
"index.html",
|
"index.html",
|
||||||
"manifest.json",
|
"manifest.json",
|
||||||
@@ -77,10 +65,23 @@
|
|||||||
"author": "Tinode Authors <info@tinode.co>",
|
"author": "Tinode Authors <info@tinode.co>",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"firebase": "^5.8.6",
|
"firebase": "^6.4.2",
|
||||||
"react": "^16.8.4",
|
"react": "^16.9.0",
|
||||||
"react-dom": "^16.8.4",
|
"react-dom": "^16.9.0",
|
||||||
"react-intl": "^2.8.0",
|
"react-intl": "^2.9.0",
|
||||||
"tinode-sdk": "^0.15.14"
|
"tinode-sdk": "^0.16.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@babel/core": "^7.5.4",
|
||||||
|
"@babel/preset-env": "^7.5.4",
|
||||||
|
"@babel/preset-react": "^7.0.0",
|
||||||
|
"babel-loader": "^8.0.6",
|
||||||
|
"babel-plugin-react-intl": "^4.1.0",
|
||||||
|
"browserslist": "^4.6.6",
|
||||||
|
"cssnano": "^4.1.10",
|
||||||
|
"postcss": "^7.0.17",
|
||||||
|
"uglifyjs-webpack-plugin": "^2.1.3",
|
||||||
|
"webpack": "^4.35.3",
|
||||||
|
"webpack-cli": "^3.3.6"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
// Must be located at the root.
|
// Must be located at the root.
|
||||||
importScripts('https://unpkg.com/firebase@5.8.1/firebase-app.js');
|
importScripts('https://unpkg.com/firebase@6.4.2/firebase-app.js');
|
||||||
importScripts('https://unpkg.com/firebase@5.8.1/firebase-messaging.js');
|
importScripts('https://unpkg.com/firebase@6.4.2/firebase-messaging.js');
|
||||||
importScripts('firebase-init.js');
|
importScripts('firebase-init.js');
|
||||||
|
|
||||||
firebase.initializeApp(FIREBASE_INIT);
|
firebase.initializeApp(FIREBASE_INIT);
|
||||||
|
|||||||
+11
-2
@@ -1,13 +1,13 @@
|
|||||||
import { PACKAGE_VERSION } from './version.js';
|
import { PACKAGE_VERSION } from './version.js';
|
||||||
|
|
||||||
// Name of this application, used in the User-Agent.
|
// Name of this application, used in the User-Agent.
|
||||||
export const APP_NAME = 'TinodeWeb/' + (PACKAGE_VERSION || '0.15');
|
export const APP_NAME = 'TinodeWeb/' + (PACKAGE_VERSION || '0.16');
|
||||||
|
|
||||||
// API key. Use https://github.com/tinode/chat/tree/master/keygen to generate your own
|
// API key. Use https://github.com/tinode/chat/tree/master/keygen to generate your own
|
||||||
export const API_KEY = 'AQEAAAABAAD_rAp4DJh05a1HAwFT3A6K';
|
export const API_KEY = 'AQEAAAABAAD_rAp4DJh05a1HAwFT3A6K';
|
||||||
|
|
||||||
// The array of possible hosts to connect to.
|
// The array of possible hosts to connect to.
|
||||||
export const KNOWN_HOSTS = {hosted: 'api.tinode.co', local: 'localhost:6060'};
|
export const KNOWN_HOSTS = {hosted: 'web.tinode.co', local: 'localhost:6060'};
|
||||||
|
|
||||||
// Default host name and port to connect to.
|
// Default host name and port to connect to.
|
||||||
export const DEFAULT_HOST = KNOWN_HOSTS.hosted;
|
export const DEFAULT_HOST = KNOWN_HOSTS.hosted;
|
||||||
@@ -63,3 +63,12 @@ export const MAX_IMAGE_DIM = 768;
|
|||||||
// Maximum number of online users to be shown in a topic title bar. Others will be
|
// Maximum number of online users to be shown in a topic title bar. Others will be
|
||||||
// hidden under "+X more"
|
// hidden under "+X more"
|
||||||
export const MAX_ONLINE_IN_TOPIC = 4;
|
export const MAX_ONLINE_IN_TOPIC = 4;
|
||||||
|
|
||||||
|
// Link for "Contact Us".
|
||||||
|
export const LINK_CONTACT_US = 'email:info@tinode.co';
|
||||||
|
|
||||||
|
// Link to Privacy Policy.
|
||||||
|
export const LINK_PRIVACY_POLICY = 'https://tinode.co/privacy.htnl';
|
||||||
|
|
||||||
|
// Link to Terms of Service.
|
||||||
|
export const LINK_TERMS_OF_SERVICE = 'https://tinode.co/terms.html';
|
||||||
|
|||||||
+6
-1
@@ -4,6 +4,7 @@
|
|||||||
"full_name_prompt": "Full name, e.g. John Doe",
|
"full_name_prompt": "Full name, e.g. John Doe",
|
||||||
"email_prompt": "Email, e.g. jdoe@example.com",
|
"email_prompt": "Email, e.g. jdoe@example.com",
|
||||||
"button_sign_up": "Sign up",
|
"button_sign_up": "Sign up",
|
||||||
|
"validate_credential_action": "confirm",
|
||||||
"label_your_name": "Your name",
|
"label_your_name": "Your name",
|
||||||
"label_password": "Password",
|
"label_password": "Password",
|
||||||
"password_unchanged_prompt": "Unchanged",
|
"password_unchanged_prompt": "Unchanged",
|
||||||
@@ -15,6 +16,10 @@
|
|||||||
"label_user_id": "Address:",
|
"label_user_id": "Address:",
|
||||||
"label_default_access_mode": "Default access mode:",
|
"label_default_access_mode": "Default access mode:",
|
||||||
"button_logout": "Logout",
|
"button_logout": "Logout",
|
||||||
|
"label_legal": "Legal",
|
||||||
|
"link_contact_us": "Contact Us",
|
||||||
|
"link_terms_of_service": "Terms of Service",
|
||||||
|
"link_privacy_policy": "Privacy Policy",
|
||||||
"requested_permissions": "Requested",
|
"requested_permissions": "Requested",
|
||||||
"granted_permissions": "Granted",
|
"granted_permissions": "Granted",
|
||||||
"menu_item_edit_permissions": "Edit permissions",
|
"menu_item_edit_permissions": "Edit permissions",
|
||||||
@@ -133,5 +138,5 @@
|
|||||||
"cannot_initiate_file_upload": "Cannot initiate file upload.",
|
"cannot_initiate_file_upload": "Cannot initiate file upload.",
|
||||||
"tags_not_found": "No tags defined. Add some.",
|
"tags_not_found": "No tags defined. Add some.",
|
||||||
"tags_editor_no_tags": "Add some tags",
|
"tags_editor_no_tags": "Add some tags",
|
||||||
"title_manage_tags": "Manage tags"
|
"title_manage_tags": "Manage"
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-2
@@ -40,7 +40,7 @@
|
|||||||
"sidepanel_title_reset": "Сменить пароль",
|
"sidepanel_title_reset": "Сменить пароль",
|
||||||
"tags_not_found": "Тагов нет. Добавьте",
|
"tags_not_found": "Тагов нет. Добавьте",
|
||||||
"tags_editor_no_tags": "Добавьте таги",
|
"tags_editor_no_tags": "Добавьте таги",
|
||||||
"title_manage_tags": "Редактор тагов",
|
"title_manage_tags": "Редактировать",
|
||||||
"message_sending": "в пути...",
|
"message_sending": "в пути...",
|
||||||
"message_sending_failed": "ошибка",
|
"message_sending_failed": "ошибка",
|
||||||
"search_placeholder": "Список, напр. email:alice@example.com, tel:+17025550003...",
|
"search_placeholder": "Список, напр. email:alice@example.com, tel:+17025550003...",
|
||||||
@@ -133,5 +133,10 @@
|
|||||||
"peers_messaging_disabled": "Чат заблокирован у корреспондента.",
|
"peers_messaging_disabled": "Чат заблокирован у корреспондента.",
|
||||||
"enable_peers_messaging": "Разблокировать.",
|
"enable_peers_messaging": "Разблокировать.",
|
||||||
"more_online_members": "+еще {overflow}",
|
"more_online_members": "+еще {overflow}",
|
||||||
"label_user_contacts": "Конакты"
|
"label_user_contacts": "Конакты:",
|
||||||
|
"validate_credential_action": "подтвердить",
|
||||||
|
"label_legal": "Прочее",
|
||||||
|
"link_contact_us": "Связаться с нами",
|
||||||
|
"link_terms_of_service": "Условия сервиса",
|
||||||
|
"link_privacy_policy": "Политика конфиденциальности"
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-1
@@ -133,5 +133,10 @@
|
|||||||
"tags_editor_no_tags": "添加一些标签",
|
"tags_editor_no_tags": "添加一些标签",
|
||||||
"title_manage_tags": "管理标签",
|
"title_manage_tags": "管理标签",
|
||||||
"more_online_members": "还有{overflow}个",
|
"more_online_members": "还有{overflow}个",
|
||||||
"label_user_contacts": "往来"
|
"label_user_contacts": "往来:",
|
||||||
|
"validate_credential_action": "确认",
|
||||||
|
"label_legal": "法律东西",
|
||||||
|
"link_contact_us": "联系我们",
|
||||||
|
"link_terms_of_service": "条款和条件",
|
||||||
|
"link_privacy_policy": "隐私政策"
|
||||||
}
|
}
|
||||||
|
|||||||
+63
-3
@@ -28,10 +28,10 @@ export function vcard(fn, imageDataUrl) {
|
|||||||
}
|
}
|
||||||
if (imageDataUrl) {
|
if (imageDataUrl) {
|
||||||
const dataStart = imageDataUrl.indexOf(',');
|
const dataStart = imageDataUrl.indexOf(',');
|
||||||
card.photo = {
|
card.photo = dataStart >= 0 ? {
|
||||||
data: imageDataUrl.substring(dataStart+1),
|
data: imageDataUrl.substring(dataStart+1),
|
||||||
type: 'jpg'
|
type: 'jpg'
|
||||||
};
|
} : imageDataUrl;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return card;
|
return card;
|
||||||
@@ -66,7 +66,7 @@ export function arrayEqual(a, b) {
|
|||||||
// as close to E.164 as possible.
|
// as close to E.164 as possible.
|
||||||
export function asPhone(val) {
|
export function asPhone(val) {
|
||||||
val = val.trim();
|
val = val.trim();
|
||||||
if (/^\s*(?:\+?(\d{1,3}))?[- (.]*(\d{3})[- ).]*(\d{3})[- .]*(\d{2})[- .]*(\d{2})?\s*$/.test(val)) {
|
if (/^(?:\+?(\d{1,3}))?[- (.]*(\d{3})[- ).]*(\d{3})[- .]*(\d{2})[- .]*(\d{2})?$/.test(val)) {
|
||||||
return val.replace(/[- ().]*/, '');
|
return val.replace(/[- ().]*/, '');
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
@@ -80,3 +80,63 @@ export function asEmail(val) {
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Checks if URL is a relative url, i.e. has no 'scheme://', including the case of missing scheme '//'.
|
||||||
|
// The scheme is expected to be RFC-compliant, e.g. [a-z][a-z0-9+.-]*
|
||||||
|
// example.html - ok
|
||||||
|
// https:example.com - not ok.
|
||||||
|
// http:/example.com - not ok.
|
||||||
|
// ' ↲ https://example.com' - not ok. (↲ means carriage return)
|
||||||
|
export function isUrlRelative(url) {
|
||||||
|
return !/^\s*([a-z][a-z0-9+.-]*:|\/\/)/im.test(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure URL does not present an XSS risk. Optional allowedSchemes may contain an array of
|
||||||
|
// strings with permitted URL schemes, such as ['ftp', 'ftps']; otherwise accept http and https only.
|
||||||
|
export function sanitizeUrl(url, allowedSchemes) {
|
||||||
|
if (!url) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strip control characters and whitespace. They are not valid URL characters anyway.
|
||||||
|
url = url.replace(/[^\x20-\x7E]/gmi, '').trim();
|
||||||
|
|
||||||
|
// Relative URLs are safe.
|
||||||
|
if (!/^([a-z][a-z0-9+.-]*:|\/\/)/i.test(url)) {
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
// Blob URLs are safe.
|
||||||
|
if (/^blob:http/.test(url)) {
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Absolute URL. Accept only safe schemes, or no scheme.
|
||||||
|
const schemes = Array.isArray(allowedSchemes) ? allowedSchemes.join('|') : 'http|https';
|
||||||
|
const re = new RegExp('^((' + schemes + '):|//)', 'i');
|
||||||
|
if (!re.test(url)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure URL is suitable for <img src="url"> field: the URL must be a relative URL or
|
||||||
|
// have http:, https:, or data: scheme. In case of data: scheme, the URL must start with
|
||||||
|
// a 'data:image/XXXX;base64,'.
|
||||||
|
export function sanitizeImageUrl(url) {
|
||||||
|
if (!url) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sanitizedUrl = sanitizeUrl(url);
|
||||||
|
if (sanitizedUrl) {
|
||||||
|
return sanitizedUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Is this a data: URL of an image?
|
||||||
|
if (/data:image\/[a-z0-9.-]+;base64,/i.test(url.trim())) {
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|||||||
+20
-4
@@ -5,6 +5,7 @@
|
|||||||
"full_name_prompt": "Full name, e.g. John Doe",
|
"full_name_prompt": "Full name, e.g. John Doe",
|
||||||
"email_prompt": "Email, e.g. jdoe@example.com",
|
"email_prompt": "Email, e.g. jdoe@example.com",
|
||||||
"button_sign_up": "Sign up",
|
"button_sign_up": "Sign up",
|
||||||
|
"validate_credential_action": "confirm",
|
||||||
"label_your_name": "Your name",
|
"label_your_name": "Your name",
|
||||||
"label_password": "Password",
|
"label_password": "Password",
|
||||||
"password_unchanged_prompt": "Unchanged",
|
"password_unchanged_prompt": "Unchanged",
|
||||||
@@ -16,6 +17,10 @@
|
|||||||
"label_user_id": "Address:",
|
"label_user_id": "Address:",
|
||||||
"label_default_access_mode": "Default access mode:",
|
"label_default_access_mode": "Default access mode:",
|
||||||
"button_logout": "Logout",
|
"button_logout": "Logout",
|
||||||
|
"label_legal": "Legal",
|
||||||
|
"link_contact_us": "Contact Us",
|
||||||
|
"link_terms_of_service": "Terms of Service",
|
||||||
|
"link_privacy_policy": "Privacy Policy",
|
||||||
"requested_permissions": "Requested",
|
"requested_permissions": "Requested",
|
||||||
"granted_permissions": "Granted",
|
"granted_permissions": "Granted",
|
||||||
"menu_item_edit_permissions": "Edit permissions",
|
"menu_item_edit_permissions": "Edit permissions",
|
||||||
@@ -134,7 +139,7 @@
|
|||||||
"cannot_initiate_file_upload": "Cannot initiate file upload.",
|
"cannot_initiate_file_upload": "Cannot initiate file upload.",
|
||||||
"tags_not_found": "No tags defined. Add some.",
|
"tags_not_found": "No tags defined. Add some.",
|
||||||
"tags_editor_no_tags": "Add some tags",
|
"tags_editor_no_tags": "Add some tags",
|
||||||
"title_manage_tags": "Manage tags"
|
"title_manage_tags": "Manage"
|
||||||
},
|
},
|
||||||
"ru": {
|
"ru": {
|
||||||
"contacts_not_found": "Чатов нет<br />¯∖_(ツ)_/¯",
|
"contacts_not_found": "Чатов нет<br />¯∖_(ツ)_/¯",
|
||||||
@@ -178,7 +183,7 @@
|
|||||||
"sidepanel_title_reset": "Сменить пароль",
|
"sidepanel_title_reset": "Сменить пароль",
|
||||||
"tags_not_found": "Тагов нет. Добавьте",
|
"tags_not_found": "Тагов нет. Добавьте",
|
||||||
"tags_editor_no_tags": "Добавьте таги",
|
"tags_editor_no_tags": "Добавьте таги",
|
||||||
"title_manage_tags": "Редактор тагов",
|
"title_manage_tags": "Редактировать",
|
||||||
"message_sending": "в пути...",
|
"message_sending": "в пути...",
|
||||||
"message_sending_failed": "ошибка",
|
"message_sending_failed": "ошибка",
|
||||||
"search_placeholder": "Список, напр. email:alice@example.com, tel:+17025550003...",
|
"search_placeholder": "Список, напр. email:alice@example.com, tel:+17025550003...",
|
||||||
@@ -271,7 +276,12 @@
|
|||||||
"peers_messaging_disabled": "Чат заблокирован у корреспондента.",
|
"peers_messaging_disabled": "Чат заблокирован у корреспондента.",
|
||||||
"enable_peers_messaging": "Разблокировать.",
|
"enable_peers_messaging": "Разблокировать.",
|
||||||
"more_online_members": "+еще {overflow}",
|
"more_online_members": "+еще {overflow}",
|
||||||
"label_user_contacts": "Конакты"
|
"label_user_contacts": "Конакты:",
|
||||||
|
"validate_credential_action": "подтвердить",
|
||||||
|
"label_legal": "Прочее",
|
||||||
|
"link_contact_us": "Связаться с нами",
|
||||||
|
"link_terms_of_service": "Условия сервиса",
|
||||||
|
"link_privacy_policy": "Политика конфиденциальности"
|
||||||
},
|
},
|
||||||
"zh-CN": {
|
"zh-CN": {
|
||||||
"archived_contacts": "已归档联系人 ({count})",
|
"archived_contacts": "已归档联系人 ({count})",
|
||||||
@@ -407,6 +417,12 @@
|
|||||||
"tags_not_found": "尚未定义标签。添加一些。",
|
"tags_not_found": "尚未定义标签。添加一些。",
|
||||||
"tags_editor_no_tags": "添加一些标签",
|
"tags_editor_no_tags": "添加一些标签",
|
||||||
"title_manage_tags": "管理标签",
|
"title_manage_tags": "管理标签",
|
||||||
"more_online_members": "还有{overflow}个"
|
"more_online_members": "还有{overflow}个",
|
||||||
|
"label_user_contacts": "往来:",
|
||||||
|
"validate_credential_action": "确认",
|
||||||
|
"label_legal": "法律东西",
|
||||||
|
"link_contact_us": "联系我们",
|
||||||
|
"link_terms_of_service": "条款和条件",
|
||||||
|
"link_privacy_policy": "隐私政策"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -1,3 +1,3 @@
|
|||||||
// This is a generated file. Don't edit.
|
// This is a generated file. Don't edit.
|
||||||
|
|
||||||
export const PACKAGE_VERSION = "0.15.15-rc2";
|
export const PACKAGE_VERSION = "0.16.1";
|
||||||
|
|||||||
@@ -20,21 +20,21 @@ export default class ContactsView extends React.Component {
|
|||||||
|
|
||||||
this.handleAction = this.handleAction.bind(this);
|
this.handleAction = this.handleAction.bind(this);
|
||||||
|
|
||||||
this.state = ContactsView.getDerivedStateFromProps(props, {});
|
this.state = ContactsView.deriveStateFromProps(props);
|
||||||
}
|
}
|
||||||
|
|
||||||
static getDerivedStateFromProps(nextProps, prevState) {
|
static deriveStateFromProps(props) {
|
||||||
const contacts = [];
|
const contacts = [];
|
||||||
let unreadThreads = 0;
|
let unreadThreads = 0;
|
||||||
let archivedCount = 0;
|
let archivedCount = 0;
|
||||||
nextProps.chatList.map((c) => {
|
props.chatList.map((c) => {
|
||||||
if (c.private && c.private.arch) {
|
if (c.private && c.private.arch) {
|
||||||
if (nextProps.archive) {
|
if (props.archive) {
|
||||||
contacts.push(c);
|
contacts.push(c);
|
||||||
} else {
|
} else {
|
||||||
archivedCount ++;
|
archivedCount ++;
|
||||||
}
|
}
|
||||||
} else if (!nextProps.archive) {
|
} else if (!props.archive) {
|
||||||
contacts.push(c);
|
contacts.push(c);
|
||||||
unreadThreads += c.unread > 0 ? 1 : 0;
|
unreadThreads += c.unread > 0 ? 1 : 0;
|
||||||
}
|
}
|
||||||
@@ -51,14 +51,22 @@ export default class ContactsView extends React.Component {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
updateFavicon(unreadThreads);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
contactList: contacts,
|
contactList: contacts,
|
||||||
archivedCount: archivedCount
|
unreadThreads: unreadThreads
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps, prevState) {
|
||||||
|
if (prevProps.chatList != this.props.chatList) {
|
||||||
|
const newState = ContactsView.deriveStateFromProps(this.props);
|
||||||
|
this.setState(newState);
|
||||||
|
if (newState.unreadThreads != prevState.unreadThreads) {
|
||||||
|
updateFavicon(newState.unreadThreads);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
handleAction(action_ignored) {
|
handleAction(action_ignored) {
|
||||||
this.props.onShowArchive();
|
this.props.onShowArchive();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
|
|
||||||
// Edit account parameters.
|
// Edit account parameters.
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
import Tinode from 'tinode-sdk';
|
||||||
|
|
||||||
import AvatarUpload from '../widgets/avatar-upload.jsx';
|
import AvatarUpload from '../widgets/avatar-upload.jsx';
|
||||||
import CheckBox from '../widgets/checkbox.jsx';
|
import CheckBox from '../widgets/checkbox.jsx';
|
||||||
import InPlaceEdit from '../widgets/in-place-edit.jsx';
|
import InPlaceEdit from '../widgets/in-place-edit.jsx';
|
||||||
import PermissionsEditor from '../widgets/permissions-editor.jsx';
|
import PermissionsEditor from '../widgets/permissions-editor.jsx';
|
||||||
import TagManager from '../widgets/tag-manager.jsx';
|
import TagManager from '../widgets/tag-manager.jsx';
|
||||||
|
|
||||||
|
import { LINK_CONTACT_US, LINK_PRIVACY_POLICY, LINK_TERMS_OF_SERVICE } from '../config.js';
|
||||||
import { makeImageUrl } from '../lib/blob-helpers.js';
|
import { makeImageUrl } from '../lib/blob-helpers.js';
|
||||||
import { arrayEqual, asEmail, asPhone, vcard } from '../lib/utils.js';
|
import { arrayEqual, asEmail, asPhone, vcard } from '../lib/utils.js';
|
||||||
|
|
||||||
@@ -38,7 +40,6 @@ export default class EditAccountView extends React.Component {
|
|||||||
this.handlePasswordUpdate = this.handlePasswordUpdate.bind(this);
|
this.handlePasswordUpdate = this.handlePasswordUpdate.bind(this);
|
||||||
this.handleImageChanged = this.handleImageChanged.bind(this);
|
this.handleImageChanged = this.handleImageChanged.bind(this);
|
||||||
this.handleCheckboxClick = this.handleCheckboxClick.bind(this);
|
this.handleCheckboxClick = this.handleCheckboxClick.bind(this);
|
||||||
this.handleStartAddCred = this.handleStartAddCred.bind(this);
|
|
||||||
this.handleCredChange = this.handleCredChange.bind(this);
|
this.handleCredChange = this.handleCredChange.bind(this);
|
||||||
this.handleCredKeyDown = this.handleCredKeyDown.bind(this);
|
this.handleCredKeyDown = this.handleCredKeyDown.bind(this);
|
||||||
this.handleCredEntered = this.handleCredEntered.bind(this);
|
this.handleCredEntered = this.handleCredEntered.bind(this);
|
||||||
@@ -65,7 +66,6 @@ export default class EditAccountView extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
tnCredsUpdated(creds) {
|
tnCredsUpdated(creds) {
|
||||||
console.log("tnCredsUpdated", creds, new Error());
|
|
||||||
this.setState({credentials: creds || []});
|
this.setState({credentials: creds || []});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,7 +81,7 @@ export default class EditAccountView extends React.Component {
|
|||||||
|
|
||||||
handleImageChanged(img) {
|
handleImageChanged(img) {
|
||||||
this.setState({avatar: img});
|
this.setState({avatar: img});
|
||||||
this.props.onUpdateAccount(undefined, vcard(null, img));
|
this.props.onUpdateAccount(undefined, vcard(null, img || Tinode.DEL_CHAR));
|
||||||
}
|
}
|
||||||
|
|
||||||
handleCheckboxClick(what, checked) {
|
handleCheckboxClick(what, checked) {
|
||||||
@@ -92,10 +92,6 @@ export default class EditAccountView extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleStartAddCred() {
|
|
||||||
this.setState({addCredActive: true});
|
|
||||||
}
|
|
||||||
|
|
||||||
handleCredChange(e) {
|
handleCredChange(e) {
|
||||||
this.setState({newCred: e.target.value, addCredInvalid: false});
|
this.setState({newCred: e.target.value, addCredInvalid: false});
|
||||||
}
|
}
|
||||||
@@ -165,23 +161,16 @@ export default class EditAccountView extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
/*
|
|
||||||
let tags = [];
|
|
||||||
this.state.tags.map((tag) => {
|
|
||||||
tags.push(<span className="badge" key={tags.length}>{tag}</span>);
|
|
||||||
});
|
|
||||||
if (tags.length == 0) {
|
|
||||||
tags = <i><FormattedMessage id="tags_not_found" /></i>;
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
let credentials = [];
|
let credentials = [];
|
||||||
this.state.credentials.map((cred) => {
|
this.state.credentials.map((cred) => {
|
||||||
credentials.push(<div key={cred.meth + ":" + cred.val}>{cred.meth}: <tt>{cred.val}</tt>
|
credentials.push(<div key={cred.meth + ":" + cred.val + ":" + cred.done}>{cred.meth}: <tt>{cred.val}</tt>
|
||||||
{!cred.done ?
|
<span > {!cred.done ?
|
||||||
<a href="javascript:;"
|
<a href="#" onClick={(e) => {e.preventDefault(); this.props.onCredConfirm(cred.meth, cred.val);}}>
|
||||||
onClick={this.props.onCredConfirm.bind(this, cred.meth, cred.val)}>confirm</a>
|
<FormattedMessage id="validate_credential_action" defaultMessage="confirm"
|
||||||
: null} <a href="javascript:;" onClick={this.props.onCredDelete.bind(this, cred.meth, cred.val)}><i
|
description="Validate credentail call to action" />
|
||||||
className="material-icons gray">delete_outline</i></a></div>);
|
</a>
|
||||||
|
: null} <a href="#" onClick={(e) => {e.preventDefault(); this.props.onCredDelete(cred.meth, cred.val);}}><i
|
||||||
|
className="material-icons gray">delete_outline</i></a></span></div>);
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -276,7 +265,7 @@ export default class EditAccountView extends React.Component {
|
|||||||
placeholder="Phone number or email" required="required" autoFocus
|
placeholder="Phone number or email" required="required" autoFocus
|
||||||
onChange={this.handleCredChange} onKeyDown={this.handleCredKeyDown} onBlur={this.handleCredEntered} />
|
onChange={this.handleCredChange} onKeyDown={this.handleCredKeyDown} onBlur={this.handleCredEntered} />
|
||||||
: null}
|
: null}
|
||||||
<div><a href="javascript:;" onClick={this.handleStartAddCred}>+ Add another</a></div>
|
<div><a href="#" onClick={(e) => {e.preventDefault(); this.setState({addCredActive: true});}}>+ Add another</a></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="hr" />
|
<div className="hr" />
|
||||||
@@ -301,11 +290,38 @@ export default class EditAccountView extends React.Component {
|
|||||||
</div>
|
</div>
|
||||||
<div className="hr" />
|
<div className="hr" />
|
||||||
<div className="panel-form-column">
|
<div className="panel-form-column">
|
||||||
<a href="javascript:;" className="red flat-button" onClick={this.props.onLogout}>
|
<a href="#" className="red flat-button" onClick={(e) => {e.preventDefault(); this.props.onLogout();}}>
|
||||||
<i className="material-icons">exit_to_app</i> <FormattedMessage id="button_logout"
|
<i className="material-icons">exit_to_app</i> <FormattedMessage id="button_logout"
|
||||||
defaultMessage="Logout" description="Button [Logout]" />
|
defaultMessage="Logout" description="Button [Logout]" />
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="hr" />
|
||||||
|
<div className="panel-form-column">
|
||||||
|
<div>
|
||||||
|
<label className="small">
|
||||||
|
<FormattedMessage id="label_legal" defaultMessage="Legal"
|
||||||
|
description="Section label" />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className="quoted">
|
||||||
|
<a href={LINK_CONTACT_US} className="flat-button" target="_blank">
|
||||||
|
<i className="material-icons">email</i> <FormattedMessage id="link_contact_us"
|
||||||
|
defaultMessage="Contact Us" description="Ancor text for contacting us by email" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div className="quoted">
|
||||||
|
<a href={LINK_TERMS_OF_SERVICE} className="flat-button" target="_blank">
|
||||||
|
<i className="material-icons">description</i> <FormattedMessage id="link_terms_of_service"
|
||||||
|
defaultMessage="Terms of Service" description="Ancor text for terms of service link" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div className="quoted">
|
||||||
|
<a href={LINK_PRIVACY_POLICY} className="flat-button" target="_blank">
|
||||||
|
<i className="material-icons">policy</i> <FormattedMessage id="link_privacy_policy"
|
||||||
|
defaultMessage="Privacy Policy" description="Ancor text for privacy policy link" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}</React.Fragment>
|
}</React.Fragment>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -238,7 +238,7 @@ class InfoView extends React.Component {
|
|||||||
|
|
||||||
handleImageChanged(img) {
|
handleImageChanged(img) {
|
||||||
this.setState({avatar: img});
|
this.setState({avatar: img});
|
||||||
this.props.onTopicDescUpdate(this.props.topic, vcard(null, img), null);
|
this.props.onTopicDescUpdate(this.props.topic, vcard(null, img || Tinode.DEL_CHAR), null);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleMuted(ignored, checked) {
|
handleMuted(ignored, checked) {
|
||||||
@@ -339,7 +339,8 @@ class InfoView extends React.Component {
|
|||||||
this.setState({showPermissionEditorFor: undefined});
|
this.setState({showPermissionEditorFor: undefined});
|
||||||
}
|
}
|
||||||
|
|
||||||
handleShowAddMembers() {
|
handleShowAddMembers(e) {
|
||||||
|
e.preventDefault();
|
||||||
this.props.onInitFind();
|
this.props.onInitFind();
|
||||||
this.setState({showMemberPanel: true});
|
this.setState({showMemberPanel: true});
|
||||||
}
|
}
|
||||||
@@ -353,7 +354,8 @@ class InfoView extends React.Component {
|
|||||||
this.setState({showMemberPanel: false});
|
this.setState({showMemberPanel: false});
|
||||||
}
|
}
|
||||||
|
|
||||||
handleLeave() {
|
handleLeave(e) {
|
||||||
|
e.preventDefault();
|
||||||
this.props.onLeaveTopic(this.props.topic);
|
this.props.onLeaveTopic(this.props.topic);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -594,13 +596,13 @@ class InfoView extends React.Component {
|
|||||||
</div>
|
</div>
|
||||||
<div className="panel-form-row">
|
<div className="panel-form-row">
|
||||||
{this.state.sharer ?
|
{this.state.sharer ?
|
||||||
<a href="javascript:;" className="flat-button" onClick={this.handleShowAddMembers}>
|
<a href="#" className="flat-button" onClick={this.handleShowAddMembers}>
|
||||||
<i className="material-icons">person_add</i> <FormattedMessage id="button_add_members"
|
<i className="material-icons">person_add</i> <FormattedMessage id="button_add_members"
|
||||||
defaultMessage="Add members" description="Flat button [Add members] (to topic)" />
|
defaultMessage="Add members" description="Flat button [Add members] (to topic)" />
|
||||||
</a>
|
</a>
|
||||||
: null}
|
: null}
|
||||||
{!this.state.owner ?
|
{!this.state.owner ?
|
||||||
<a href="javascript:;" className="red flat-button" onClick={this.handleLeave}>
|
<a href="#" className="red flat-button" onClick={this.handleLeave}>
|
||||||
<i className="material-icons">exit_to_app</i> <FormattedMessage id="button_leave"
|
<i className="material-icons">exit_to_app</i> <FormattedMessage id="button_leave"
|
||||||
defaultMessage="Leave" description="Flat button [Leave] (topic)" />
|
defaultMessage="Leave" description="Flat button [Leave] (topic)" />
|
||||||
</a>
|
</a>
|
||||||
@@ -624,7 +626,7 @@ class InfoView extends React.Component {
|
|||||||
</div>
|
</div>
|
||||||
:
|
:
|
||||||
<div className="panel-form-row">
|
<div className="panel-form-row">
|
||||||
<a href="javascript:;" className="red flat-button" onClick={this.handleLeave}>
|
<a href="#" className="red flat-button" onClick={this.handleLeave}>
|
||||||
<i className="material-icons">exit_to_app</i> <FormattedMessage id="action_leave_chat"
|
<i className="material-icons">exit_to_app</i> <FormattedMessage id="action_leave_chat"
|
||||||
defaultMessage="Leave" description="Action [Leave] chat" />
|
defaultMessage="Leave" description="Action [Leave] chat" />
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -55,6 +55,8 @@ export default class LoginView extends React.Component {
|
|||||||
(login_prompt) => <input type="text" id="inputLogin"
|
(login_prompt) => <input type="text" id="inputLogin"
|
||||||
placeholder={login_prompt}
|
placeholder={login_prompt}
|
||||||
autoComplete="username"
|
autoComplete="username"
|
||||||
|
autoCorrect="off"
|
||||||
|
autoCapitalize="none"
|
||||||
value={this.state.login}
|
value={this.state.login}
|
||||||
onChange={this.handleLoginChange}
|
onChange={this.handleLoginChange}
|
||||||
required autoFocus />
|
required autoFocus />
|
||||||
|
|||||||
@@ -73,7 +73,6 @@ class MessagesView extends React.Component {
|
|||||||
this.handleFormResponse = this.handleFormResponse.bind(this);
|
this.handleFormResponse = this.handleFormResponse.bind(this);
|
||||||
this.handleContextClick = this.handleContextClick.bind(this);
|
this.handleContextClick = this.handleContextClick.bind(this);
|
||||||
this.handleShowContextMenuMessage = this.handleShowContextMenuMessage.bind(this);
|
this.handleShowContextMenuMessage = this.handleShowContextMenuMessage.bind(this);
|
||||||
this.handleBackNavigation = this.handleBackNavigation.bind(this);
|
|
||||||
this.handleNewChatAcceptance = this.handleNewChatAcceptance.bind(this);
|
this.handleNewChatAcceptance = this.handleNewChatAcceptance.bind(this);
|
||||||
this.handleEnablePeer = this.handleEnablePeer.bind(this);
|
this.handleEnablePeer = this.handleEnablePeer.bind(this);
|
||||||
}
|
}
|
||||||
@@ -486,15 +485,12 @@ class MessagesView extends React.Component {
|
|||||||
this.props.showContextMenu(params, menuItems);
|
this.props.showContextMenu(params, menuItems);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleBackNavigation() {
|
|
||||||
this.props.onHideMessagesView();
|
|
||||||
}
|
|
||||||
|
|
||||||
handleNewChatAcceptance(action) {
|
handleNewChatAcceptance(action) {
|
||||||
this.props.onNewChat(this.state.topic, action);
|
this.props.onNewChat(this.state.topic, action);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleEnablePeer() {
|
handleEnablePeer(e) {
|
||||||
|
e.preventDefault();
|
||||||
this.props.onChangePermissions(this.state.topic, DEFAULT_ACCESS_MODE, this.state.topic);
|
this.props.onChangePermissions(this.state.topic, DEFAULT_ACCESS_MODE, this.state.topic);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -579,7 +575,7 @@ class MessagesView extends React.Component {
|
|||||||
<div id="topic-view" className={this.props.hideSelf ? 'nodisplay' : null}>
|
<div id="topic-view" className={this.props.hideSelf ? 'nodisplay' : null}>
|
||||||
<div id="topic-caption-panel" className="caption-panel">
|
<div id="topic-caption-panel" className="caption-panel">
|
||||||
{this.props.displayMobile ?
|
{this.props.displayMobile ?
|
||||||
<a href="javascript:;" id="hide-message-view" onClick={this.handleBackNavigation}>
|
<a href="#" id="hide-message-view" onClick={(e) => {e.preventDefault(); this.props.onHideMessagesView();}}>
|
||||||
<i className="material-icons">arrow_back</i>
|
<i className="material-icons">arrow_back</i>
|
||||||
</a>
|
</a>
|
||||||
:
|
:
|
||||||
@@ -605,7 +601,7 @@ class MessagesView extends React.Component {
|
|||||||
<div id="topic-users" />
|
<div id="topic-users" />
|
||||||
}
|
}
|
||||||
<div>
|
<div>
|
||||||
<a href="javascript:;" onClick={this.handleContextClick}>
|
<a href="#" onClick={this.handleContextClick}>
|
||||||
<i className="material-icons">more_vert</i>
|
<i className="material-icons">more_vert</i>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -638,7 +634,7 @@ class MessagesView extends React.Component {
|
|||||||
<div id="peer-messaging-disabled-note">
|
<div id="peer-messaging-disabled-note">
|
||||||
<i className="material-icons secondary">block</i> <FormattedMessage
|
<i className="material-icons secondary">block</i> <FormattedMessage
|
||||||
id="peers_messaging_disabled" defaultMessage="Peer's messaging is disabled."
|
id="peers_messaging_disabled" defaultMessage="Peer's messaging is disabled."
|
||||||
description="Shown when the p2p peer's messaging is disabled" /> <a href="javascript:;"
|
description="Shown when the p2p peer's messaging is disabled" /> <a href="#"
|
||||||
onClick={this.handleEnablePeer}><FormattedMessage id="enable_peers_messaging"
|
onClick={this.handleEnablePeer}><FormattedMessage id="enable_peers_messaging"
|
||||||
defaultMessage="Enable" description="Call to action to enable peer's messaging" /></a>.
|
defaultMessage="Enable" description="Call to action to enable peer's messaging" /></a>.
|
||||||
</div> : null}
|
</div> : null}
|
||||||
|
|||||||
@@ -56,19 +56,19 @@ export default class NewTopicView extends React.Component {
|
|||||||
<div className="flex-column">
|
<div className="flex-column">
|
||||||
<ul className="tabbar">
|
<ul className="tabbar">
|
||||||
<li className={this.state.tabSelected === "p2p" ? "active" : null}>
|
<li className={this.state.tabSelected === "p2p" ? "active" : null}>
|
||||||
<a href="javascript:;" data-id="p2p" onClick={this.handleTabClick}>
|
<a href="#" data-id="p2p" onClick={this.handleTabClick}>
|
||||||
<FormattedMessage id="tabtitle_find_user" defaultMessage="find"
|
<FormattedMessage id="tabtitle_find_user" defaultMessage="find"
|
||||||
description="Tab title Find" />
|
description="Tab title Find" />
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li className={this.state.tabSelected === "grp" ? "active" : null}>
|
<li className={this.state.tabSelected === "grp" ? "active" : null}>
|
||||||
<a href="javascript:;" data-id="grp" onClick={this.handleTabClick}>
|
<a href="#" data-id="grp" onClick={this.handleTabClick}>
|
||||||
<FormattedMessage id="tabtitle_new_group" defaultMessage="new group"
|
<FormattedMessage id="tabtitle_new_group" defaultMessage="new group"
|
||||||
description="Tab title New Group" />
|
description="Tab title New Group" />
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li className={this.state.tabSelected === "byid" ? "active" : null}>
|
<li className={this.state.tabSelected === "byid" ? "active" : null}>
|
||||||
<a href="javascript:;" data-id="byid" onClick={this.handleTabClick}>
|
<a href="#" data-id="byid" onClick={this.handleTabClick}>
|
||||||
<FormattedMessage id="tabtitle_group_by_id" defaultMessage="by id"
|
<FormattedMessage id="tabtitle_group_by_id" defaultMessage="by id"
|
||||||
description="Tab title Find topic by ID" />
|
description="Tab title Find topic by ID" />
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ export default class PasswordResetView extends React.PureComponent {
|
|||||||
onChange={this.handlePasswordChange} />
|
onChange={this.handlePasswordChange} />
|
||||||
}</FormattedMessage>
|
}</FormattedMessage>
|
||||||
:
|
:
|
||||||
<React.Fragment>
|
<>
|
||||||
<label htmlFor="inputEmail">
|
<label htmlFor="inputEmail">
|
||||||
<FormattedMessage id="label_reset_password"
|
<FormattedMessage id="label_reset_password"
|
||||||
defaultMessage="Send a password reset email:"
|
defaultMessage="Send a password reset email:"
|
||||||
@@ -72,7 +72,7 @@ export default class PasswordResetView extends React.PureComponent {
|
|||||||
onChange={this.handleEmailChange}
|
onChange={this.handleEmailChange}
|
||||||
required autoFocus />
|
required autoFocus />
|
||||||
}</FormattedMessage>
|
}</FormattedMessage>
|
||||||
</React.Fragment>
|
</>
|
||||||
}
|
}
|
||||||
<div className="dialog-buttons">
|
<div className="dialog-buttons">
|
||||||
<button className="blue" type="submit">{reset ?
|
<button className="blue" type="submit">{reset ?
|
||||||
|
|||||||
+32
-14
@@ -496,14 +496,13 @@ class TinodeWeb extends React.Component {
|
|||||||
credCode: undefined,
|
credCode: undefined,
|
||||||
myUserId: this.tinode.getCurrentUserID()
|
myUserId: this.tinode.getCurrentUserID()
|
||||||
});
|
});
|
||||||
console.log("handleLoginSuccessful");
|
|
||||||
// Subscribe, fetch topic desc, the list of subscriptions. Messages are not fetched.
|
// Subscribe, fetch topic desc, the list of subscriptions. Messages are not fetched.
|
||||||
me.subscribe(
|
me.subscribe(
|
||||||
me.startMetaQuery().
|
me.startMetaQuery().
|
||||||
withLaterSub().
|
withLaterSub().
|
||||||
withDesc().
|
withDesc().
|
||||||
withTags().
|
withTags().
|
||||||
withCreds().
|
withCred().
|
||||||
build()
|
build()
|
||||||
).catch((err) => {
|
).catch((err) => {
|
||||||
localStorage.removeItem('auth-token');
|
localStorage.removeItem('auth-token');
|
||||||
@@ -532,15 +531,23 @@ class TinodeWeb extends React.Component {
|
|||||||
} else if (what == 'read') {
|
} else if (what == 'read') {
|
||||||
this.resetContactList();
|
this.resetContactList();
|
||||||
} else if (what == 'msg') {
|
} else if (what == 'msg') {
|
||||||
|
// Check if the topic is archived. If so, don't play a sound.
|
||||||
|
const topic = this.tinode.getTopic(cont.topic);
|
||||||
|
const archived = topic && topic.isArchived();
|
||||||
|
|
||||||
// New message received
|
// New message received
|
||||||
|
if (document.hidden) {
|
||||||
|
if (this.state.messageSounds && !archived) {
|
||||||
|
POP_SOUND.play();
|
||||||
|
}
|
||||||
|
this.resetContactList();
|
||||||
|
|
||||||
// Skip update if the topic is currently open, otherwise the badge will annoyingly flash.
|
// Skip update if the topic is currently open, otherwise the badge will annoyingly flash.
|
||||||
if (this.state.topicSelected != cont.topic) {
|
} else if (this.state.topicSelected != cont.topic) {
|
||||||
if (this.state.messageSounds) {
|
if (this.state.messageSounds && !archived) {
|
||||||
POP_SOUND.play();
|
POP_SOUND.play();
|
||||||
}
|
}
|
||||||
this.resetContactList();
|
this.resetContactList();
|
||||||
} else if (document.hidden && this.state.messageSounds) {
|
|
||||||
POP_SOUND.play();
|
|
||||||
}
|
}
|
||||||
} else if (what == 'recv') {
|
} else if (what == 'recv') {
|
||||||
// Explicitly ignoring "recv" -- it causes no visible updates to contact list.
|
// Explicitly ignoring "recv" -- it causes no visible updates to contact list.
|
||||||
@@ -940,12 +947,13 @@ class TinodeWeb extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleCredAdd(method, value) {
|
handleCredAdd(method, value) {
|
||||||
// TODO: implement
|
const me = this.tinode.getMeTopic();
|
||||||
console.log("add credential", method, value);
|
me.setMeta({cred: {meth: method, val: value}}).catch((err) => {
|
||||||
|
this.handleError(err.message, 'err');
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
handleCredDelete(method, value) {
|
handleCredDelete(method, value) {
|
||||||
console.log("delete credential", method, value);
|
|
||||||
const me = this.tinode.getMeTopic();
|
const me = this.tinode.getMeTopic();
|
||||||
me.delCredential(method, value).catch((err) => {
|
me.delCredential(method, value).catch((err) => {
|
||||||
this.handleError(err.message, 'err');
|
this.handleError(err.message, 'err');
|
||||||
@@ -953,8 +961,7 @@ class TinodeWeb extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleCredConfirm(method, response) {
|
handleCredConfirm(method, response) {
|
||||||
// TODO: implement
|
this.handleCredentialsRequest({cred: [method]});
|
||||||
console.log("confirm credential", method, response);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// User clicked Cancel button in Setting or Sign Up panel.
|
// User clicked Cancel button in Setting or Sign Up panel.
|
||||||
@@ -1187,15 +1194,26 @@ class TinodeWeb extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleValidateCredentialsRequest(cred, code) {
|
handleValidateCredentialsRequest(cred, code) {
|
||||||
this.setState({credMethod: cred, credCode: code});
|
if (this.tinode.isAuthenticated()) {
|
||||||
this.doLogin(null, null, {meth: cred, resp: code});
|
const me = this.tinode.getMeTopic();
|
||||||
|
me.setMeta({cred: {meth: cred, resp: code}})
|
||||||
|
.then(() => {
|
||||||
|
HashNavigation.navigateTo('');
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
this.handleError(err.message, 'err');
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.setState({credMethod: cred, credCode: code});
|
||||||
|
this.doLogin(null, null, {meth: cred, resp: code});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handlePasswordResetRequest(method, value) {
|
handlePasswordResetRequest(method, value) {
|
||||||
// If already connected, connnect() will return a resolved promise.
|
// If already connected, connnect() will return a resolved promise.
|
||||||
this.tinode.connect()
|
this.tinode.connect()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
return this.tinode.requestResetAuthSecret('basic', method, value)
|
return this.tinode.requestResetAuthSecret('basic', method, value);
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
// Socket error
|
// Socket error
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ class ValidationView extends React.PureComponent {
|
|||||||
this.handleChange = this.handleChange.bind(this);
|
this.handleChange = this.handleChange.bind(this);
|
||||||
this.handleKeyPress = this.handleKeyPress.bind(this);
|
this.handleKeyPress = this.handleKeyPress.bind(this);
|
||||||
this.handleSubmit = this.handleSubmit.bind(this);
|
this.handleSubmit = this.handleSubmit.bind(this);
|
||||||
|
this.handleCancel = this.handleCancel.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleChange(e) {
|
handleChange(e) {
|
||||||
@@ -35,6 +36,8 @@ class ValidationView extends React.PureComponent {
|
|||||||
handleKeyPress(e) {
|
handleKeyPress(e) {
|
||||||
if (e.key === 'Enter') {
|
if (e.key === 'Enter') {
|
||||||
this.handleSubmit(e);
|
this.handleSubmit(e);
|
||||||
|
} else if (e.key == 'Escape') {
|
||||||
|
this.handleCancel(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,6 +48,12 @@ class ValidationView extends React.PureComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleCancel(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
this.props.onCancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { formatMessage } = this.props.intl;
|
const { formatMessage } = this.props.intl;
|
||||||
const methods = {'email': formatMessage(messages.email), 'tel': formatMessage(messages.phone)};
|
const methods = {'email': formatMessage(messages.email), 'tel': formatMessage(messages.phone)};
|
||||||
@@ -72,6 +81,9 @@ class ValidationView extends React.PureComponent {
|
|||||||
<button className="blue" onClick={this.handleSubmit}>
|
<button className="blue" onClick={this.handleSubmit}>
|
||||||
<FormattedMessage id="button_confirm" defaultMessage="Confirm" description="Button [Confirm]" />
|
<FormattedMessage id="button_confirm" defaultMessage="Confirm" description="Button [Confirm]" />
|
||||||
</button>
|
</button>
|
||||||
|
<button className="white" onClick={this.handleCancel}>
|
||||||
|
<FormattedMessage id="button_cancel" />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { FormattedMessage } from 'react-intl';
|
|||||||
|
|
||||||
import FileProgress from './file-progress.jsx';
|
import FileProgress from './file-progress.jsx';
|
||||||
import { bytesToHumanSize } from '../lib/strformat.js';
|
import { bytesToHumanSize } from '../lib/strformat.js';
|
||||||
|
import { isUrlRelative, sanitizeUrl } from '../lib/utils.js';
|
||||||
|
|
||||||
export default class Attachment extends React.Component {
|
export default class Attachment extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
@@ -53,15 +54,15 @@ export default class Attachment extends React.Component {
|
|||||||
// If the URL is relative use LargeFileHelper to attach authentication
|
// If the URL is relative use LargeFileHelper to attach authentication
|
||||||
// credentials to the request.
|
// credentials to the request.
|
||||||
let url, helperFunc;
|
let url, helperFunc;
|
||||||
if (!this.props.uploader && !this.state.downloader &&
|
if (!this.props.uploader && !this.state.downloader && isUrlRelative(this.props.downloadUrl)) {
|
||||||
!(/^(?:(?:[a-z]+:)?\/\/)/i.test(this.props.downloadUrl))) {
|
|
||||||
// Relative URL. Use download helper.
|
// Relative URL. Use download helper.
|
||||||
url = "javascript:;";
|
url = '#';
|
||||||
helperFunc = (e) => {
|
helperFunc = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
this.downloadFile(this.props.downloadUrl, this.props.filename, this.props.mimetype);
|
this.downloadFile(this.props.downloadUrl, this.props.filename, this.props.mimetype);
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
url = this.props.downloadUrl;
|
url = sanitizeUrl(this.props.downloadUrl) || 'about:blank';
|
||||||
helperFunc = null;
|
helperFunc = null;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -14,16 +14,12 @@ export default class AvatarUpload extends React.Component {
|
|||||||
};
|
};
|
||||||
|
|
||||||
this.handleFileUpload = this.handleFileUpload.bind(this);
|
this.handleFileUpload = this.handleFileUpload.bind(this);
|
||||||
this.handleClear = this.handleClear.bind(this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static getDerivedStateFromProps(nextProps, prevState) {
|
componentDidUpdate(prevProps) {
|
||||||
if (prevState.dataUrl != nextProps.avatar) {
|
if (this.props.avatar != prevProps.avatar) {
|
||||||
return {
|
this.setState({dataUrl: this.props.avatar})
|
||||||
dataUrl: nextProps.avatar
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleFileUpload(e) {
|
handleFileUpload(e) {
|
||||||
@@ -42,10 +38,6 @@ export default class AvatarUpload extends React.Component {
|
|||||||
e.target.value = '';
|
e.target.value = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
handleClear() {
|
|
||||||
this.props.onImageChanged(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
// Randomize id value in case more than one AvatarUpload is shown
|
// Randomize id value in case more than one AvatarUpload is shown
|
||||||
// at the same time.
|
// at the same time.
|
||||||
@@ -54,7 +46,7 @@ export default class AvatarUpload extends React.Component {
|
|||||||
<div className="avatar-upload">
|
<div className="avatar-upload">
|
||||||
{this.props.readOnly || !this.state.dataUrl ?
|
{this.props.readOnly || !this.state.dataUrl ?
|
||||||
null :
|
null :
|
||||||
<a href="javascript:;" className="clear-avatar" onClick={this.handleClear}>
|
<a href="#" className="clear-avatar" onClick={(e) => {e.preventDefault(); this.props.onImageChanged(null);}}>
|
||||||
<i className="material-icons">clear</i>
|
<i className="material-icons">clear</i>
|
||||||
</a>}
|
</a>}
|
||||||
{this.state.dataUrl ?
|
{this.state.dataUrl ?
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { Drafty } from 'tinode-sdk'
|
|||||||
import Attachment from './attachment.jsx';
|
import Attachment from './attachment.jsx';
|
||||||
import LetterTile from './letter-tile.jsx';
|
import LetterTile from './letter-tile.jsx';
|
||||||
import ReceivedMarker from './received-marker.jsx'
|
import ReceivedMarker from './received-marker.jsx'
|
||||||
|
import { sanitizeImageUrl, sanitizeUrl } from '../lib/utils.js';
|
||||||
|
|
||||||
export default class ChatMessage extends React.Component {
|
export default class ChatMessage extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
@@ -48,7 +49,7 @@ export default class ChatMessage extends React.Component {
|
|||||||
e.target.dataset.val === undefined ? 1 : '' + e.target.dataset.val;
|
e.target.dataset.val === undefined ? 1 : '' + e.target.dataset.val;
|
||||||
}
|
}
|
||||||
if (e.target.dataset.act == 'url') {
|
if (e.target.dataset.act == 'url') {
|
||||||
data.ref = '' + e.target.dataset.ref;
|
data.ref = sanitizeUrl(e.target.dataset.ref) || 'about:blank';
|
||||||
}
|
}
|
||||||
const text = e.target.dataset.title || 'unknown';
|
const text = e.target.dataset.title || 'unknown';
|
||||||
this.props.onFormResponse(e.target.dataset.act, text, data);
|
this.props.onFormResponse(e.target.dataset.act, text, data);
|
||||||
@@ -97,10 +98,10 @@ export default class ChatMessage extends React.Component {
|
|||||||
}, this);
|
}, this);
|
||||||
content = React.createElement('span', null, Drafty.format(content, draftyFormatter, this));
|
content = React.createElement('span', null, Drafty.format(content, draftyFormatter, this));
|
||||||
} else if (typeof content != 'string') {
|
} else if (typeof content != 'string') {
|
||||||
content = <span><i className="material-icons">error_outline</i> <i>
|
content = <><i className="material-icons">error_outline</i> <i>
|
||||||
<FormattedMessage id="invalid_content"
|
<FormattedMessage id="invalid_content"
|
||||||
defaultMessage="invalid content" description="Shown when message is unreadable" />
|
defaultMessage="invalid content" description="Shown when message is unreadable" />
|
||||||
</i></span>
|
</i></>
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -125,7 +126,7 @@ export default class ChatMessage extends React.Component {
|
|||||||
received={this.props.received} />
|
received={this.props.received} />
|
||||||
</div>
|
</div>
|
||||||
<span className="menuTrigger">
|
<span className="menuTrigger">
|
||||||
<a href="javascript:;" onClick={this.handleContextClick}>
|
<a href="#" onClick={this.handleContextClick}>
|
||||||
<i className="material-icons">expand_more</i>
|
<i className="material-icons">expand_more</i>
|
||||||
</a>
|
</a>
|
||||||
</span>
|
</span>
|
||||||
@@ -163,6 +164,7 @@ function draftyFormatter(style, data, values, key) {
|
|||||||
Math.min(this.props.viewportWidth - REM_SIZE * 4, REM_SIZE * 36), REM_SIZE * 24, false);
|
Math.min(this.props.viewportWidth - REM_SIZE * 4, REM_SIZE * 36), REM_SIZE * 24, false);
|
||||||
dim = dim || {dstWidth: BROKEN_IMAGE_SIZE, dstHeight: BROKEN_IMAGE_SIZE};
|
dim = dim || {dstWidth: BROKEN_IMAGE_SIZE, dstHeight: BROKEN_IMAGE_SIZE};
|
||||||
attr.style = { width: dim.dstWidth + 'px', height: dim.dstHeight + 'px' };
|
attr.style = { width: dim.dstWidth + 'px', height: dim.dstHeight + 'px' };
|
||||||
|
attr.src = sanitizeImageUrl(attr.src);
|
||||||
if (attr.src) {
|
if (attr.src) {
|
||||||
attr.onClick = this.handlePreviewImage;
|
attr.onClick = this.handlePreviewImage;
|
||||||
attr.className += ' image-clickable';
|
attr.className += ' image-clickable';
|
||||||
|
|||||||
+17
-12
@@ -9,7 +9,9 @@ export default class ChipInput extends React.Component {
|
|||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = ChipInput.getDerivedStateFromProps(props);
|
this.state = ChipInput.deriveStateFromProps(props);
|
||||||
|
this.state.input = '';
|
||||||
|
this.state.focused = false;
|
||||||
|
|
||||||
this.handleTextInput = this.handleTextInput.bind(this);
|
this.handleTextInput = this.handleTextInput.bind(this);
|
||||||
this.removeChipAt = this.removeChipAt.bind(this);
|
this.removeChipAt = this.removeChipAt.bind(this);
|
||||||
@@ -19,20 +21,23 @@ export default class ChipInput extends React.Component {
|
|||||||
this.handleKeyDown = this.handleKeyDown.bind(this);
|
this.handleKeyDown = this.handleKeyDown.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
static getDerivedStateFromProps(nextProps, prevState) {
|
static deriveStateFromProps(props) {
|
||||||
const state = {
|
return {
|
||||||
placeholder: nextProps.chips ? '' : nextProps.prompt,
|
placeholder: props.chips ? '' : props.prompt,
|
||||||
sortedChips: ChipInput.sortChips(nextProps.chips, nextProps.required),
|
sortedChips: ChipInput.sortChips(props.chips, props.required),
|
||||||
chipIndex: ChipInput.indexChips(nextProps.chips),
|
chipIndex: ChipInput.indexChips(props.chips)
|
||||||
focused: prevState && prevState.focused
|
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (!prevState || nextProps.chips.length > prevState.sortedChips.length) {
|
componentDidUpdate(prevProps, prevState) {
|
||||||
// Chip added: clear input.
|
if (prevProps.chips != this.props.chips ||
|
||||||
state.input ='';
|
prevProps.required != this.props.required ||
|
||||||
|
prevProps.prompt != this.props.prompt) {
|
||||||
|
this.setState(ChipInput.deriveStateFromProps(this.props));
|
||||||
|
}
|
||||||
|
if (!prevState || this.props.chips.length > prevState.sortedChips.length) {
|
||||||
|
this.setState({input: ''});
|
||||||
}
|
}
|
||||||
|
|
||||||
return state;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Map chip index to user name
|
// Map chip index to user name
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ export default class Chip extends React.PureComponent {
|
|||||||
this.handleCancel = this.handleCancel.bind(this);
|
this.handleCancel = this.handleCancel.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleCancel() {
|
handleCancel(e) {
|
||||||
|
e.preventDefault();
|
||||||
this.props.onCancel(this.props.topic, this.props.index);
|
this.props.onCancel(this.props.topic, this.props.index);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,7 +30,7 @@ export default class Chip extends React.PureComponent {
|
|||||||
}
|
}
|
||||||
<span>{title}</span>
|
<span>{title}</span>
|
||||||
{this.props.onCancel && !this.props.required ?
|
{this.props.onCancel && !this.props.required ?
|
||||||
<a href="javascript:;" onClick={this.handleCancel} >×</a>
|
<a href="#" onClick={this.handleCancel} >×</a>
|
||||||
: <span className="spacer" />}
|
: <span className="spacer" />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export default class ContactBadges extends React.PureComponent {
|
|||||||
badges.push(<span className={style} key={b.key || b.name}>{b.name}</span>);
|
badges.push(<span className={style} key={b.key || b.name}>{b.name}</span>);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return <React.Fragment>{badges}</React.Fragment>;
|
return <>{badges}</>;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,11 +70,11 @@ export default class Contact extends React.Component {
|
|||||||
<UnreadBadge count={this.props.unread} /><ContactBadges badges={icon_badges} />
|
<UnreadBadge count={this.props.unread} /><ContactBadges badges={icon_badges} />
|
||||||
</div>
|
</div>
|
||||||
{this.props.comment ? <div className="contact-comment">{this.props.comment}</div> : null}
|
{this.props.comment ? <div className="contact-comment">{this.props.comment}</div> : null}
|
||||||
<span><ContactBadges badges={badges} /></span>
|
<><ContactBadges badges={badges} /></>
|
||||||
</div>
|
</div>
|
||||||
{this.props.showContextMenu ?
|
{this.props.showContextMenu ?
|
||||||
<span className="menuTrigger">
|
<span className="menuTrigger">
|
||||||
<a href="javascript:;" onClick={this.handleContextClick}>
|
<a href="#" onClick={this.handleContextClick}>
|
||||||
<i className="material-icons">expand_more</i>
|
<i className="material-icons">expand_more</i>
|
||||||
</a>
|
</a>
|
||||||
</span> : null}
|
</span> : null}
|
||||||
|
|||||||
@@ -13,10 +13,12 @@ export default class ErrorPanel extends React.PureComponent {
|
|||||||
this.hide = this.hide.bind(this);
|
this.hide = this.hide.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
static getDerivedStateFromProps(nextProps, prevState) {
|
componentDidUpdate(prevProps) {
|
||||||
return {
|
if (prevProps.level !== this.props.level) {
|
||||||
show: !(!nextProps.level)
|
this.setState({
|
||||||
};
|
show: !(!this.props.level)
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
hide() {
|
hide() {
|
||||||
@@ -33,14 +35,14 @@ export default class ErrorPanel extends React.PureComponent {
|
|||||||
return (
|
return (
|
||||||
<div className={className}>
|
<div className={className}>
|
||||||
<div className="icon"><i className="material-icons">{level}</i></div>
|
<div className="icon"><i className="material-icons">{level}</i></div>
|
||||||
<span>
|
<>
|
||||||
<span dangerouslySetInnerHTML={{__html: this.props.text}} />
|
<span dangerouslySetInnerHTML={{__html: this.props.text}} />
|
||||||
{this.props.action ?
|
{this.props.action ?
|
||||||
<a href="javascript:;" onClick={this.props.action}>
|
<a href="#" onClick={(e) => {e.preventDefault(); this.props.action();}}>
|
||||||
{this.props.actionText}
|
{this.props.actionText}
|
||||||
</a>
|
</a>
|
||||||
: null}
|
: null}
|
||||||
</span>
|
</>
|
||||||
<div className="cancel"><MenuCancel onCancel={this.hide} /></div>
|
<div className="cancel"><MenuCancel onCancel={this.hide} /></div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export default class FileProgress extends React.PureComponent {
|
|||||||
<div className="uploader">
|
<div className="uploader">
|
||||||
<div><span style={{width: (this.props.progress * 100) + "%"}}></span></div>
|
<div><span style={{width: (this.props.progress * 100) + "%"}}></span></div>
|
||||||
{this.props.progress < 0.999 ?
|
{this.props.progress < 0.999 ?
|
||||||
<a href="javascript:;" onClick={this.props.onCancel}>
|
<a href="#" onClick={(e) => {e.preventDefault(); this.props.onCancel();}}>
|
||||||
<i className="material-icons">close</i> <FormattedMessage id="action_cancel"
|
<i className="material-icons">close</i> <FormattedMessage id="action_cancel"
|
||||||
defaultMessage="cancel" description="Call to action [cancel]" />
|
defaultMessage="cancel" description="Call to action [cancel]" />
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -9,21 +9,27 @@ export default class ImagePreview extends React.PureComponent {
|
|||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {};
|
this.state = {
|
||||||
|
width: 0,
|
||||||
|
height: 0
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
assignWidth(node) {
|
||||||
this.setState({
|
if (node && !this.state.width) {
|
||||||
width: this.container.clientWidth,
|
const bounds = node.getBoundingClientRect();
|
||||||
height: this.container.clientHeight
|
this.setState({
|
||||||
});
|
width: bounds.width | 0,
|
||||||
|
height: bounds.height | 0
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
if (!this.props.content) {
|
if (!this.props.content) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const instance = this;
|
|
||||||
const dim = fitImageSize(this.props.content.width, this.props.content.height,
|
const dim = fitImageSize(this.props.content.width, this.props.content.height,
|
||||||
this.state.width, this.state.height, false);
|
this.state.width, this.state.height, false);
|
||||||
const size = dim ? { width: dim.dstWidth + 'px', height: dim.dstHeight + 'px' } :
|
const size = dim ? { width: dim.dstWidth + 'px', height: dim.dstHeight + 'px' } :
|
||||||
@@ -32,9 +38,10 @@ export default class ImagePreview extends React.PureComponent {
|
|||||||
size.maxHeight = '100%';
|
size.maxHeight = '100%';
|
||||||
|
|
||||||
let filename = this.props.content.filename;
|
let filename = this.props.content.filename;
|
||||||
const maxlength = (this.props.content.width / REM_SIZE) | 0;
|
// Averate font aspect ratio is ~0.5; File name takes 1/3 of the viewport width.
|
||||||
|
const maxlength = Math.max(((this.state.width / REM_SIZE / 1.5) | 0) - 2, 12);
|
||||||
if (filename.length > maxlength) {
|
if (filename.length > maxlength) {
|
||||||
filename = filename.slice(0, maxlength-2) + '...' + filename.slice(2-maxlength);
|
filename = filename.slice(0, maxlength/2 - 1) + '...' + filename.slice(1 - maxlength/2);
|
||||||
}
|
}
|
||||||
const width = this.props.content.width || '-';
|
const width = this.props.content.width || '-';
|
||||||
const height = this.props.content.height || '-';
|
const height = this.props.content.height || '-';
|
||||||
@@ -45,9 +52,9 @@ export default class ImagePreview extends React.PureComponent {
|
|||||||
<i className="material-icons">file_download</i> <FormattedMessage
|
<i className="material-icons">file_download</i> <FormattedMessage
|
||||||
id="download_action" defaultMessage="download" description="Call to action [download]" />
|
id="download_action" defaultMessage="download" description="Call to action [download]" />
|
||||||
</a>
|
</a>
|
||||||
<a href="javascript:;" onClick={this.props.onClose}><i className="material-icons gray">close</i></a>
|
<a href="#" onClick={(e) => {e.preventDefault(); this.props.onClose();}}><i className="material-icons gray">close</i></a>
|
||||||
</div>
|
</div>
|
||||||
<div id="image-preview-container" ref={function(ref) {instance.container = ref;}}>
|
<div id="image-preview-container" ref={(node) => this.assignWidth(node)}>
|
||||||
<img src={this.props.content.url} style={size} />
|
<img src={this.props.content.url} style={size} />
|
||||||
</div>
|
</div>
|
||||||
<div id="image-preview-footer">
|
<div id="image-preview-footer">
|
||||||
|
|||||||
@@ -21,16 +21,16 @@ export default class InPlaceEdit extends React.Component {
|
|||||||
this.handlePasswordFinished = this.handlePasswordFinished.bind(this);
|
this.handlePasswordFinished = this.handlePasswordFinished.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
static getDerivedStateFromProps(nextProps, prevState) {
|
componentDidUpdate(prevProps, prevState) {
|
||||||
// If text has changed while in read mode, update text and discard changes.
|
// If text has changed while in read mode, update text and discard changes.
|
||||||
// Ignore update if in edit mode.
|
// Ignore update if in edit mode.
|
||||||
if (prevState.initialValue != nextProps.value && !prevState.active) {
|
const newValue = this.props.value || '';
|
||||||
return {
|
if (prevState.initialValue != newValue && !prevState.active) {
|
||||||
initialValue: nextProps.value || '',
|
this.setState({
|
||||||
value: nextProps.value || ''
|
initialValue: newValue,
|
||||||
};
|
value: newValue
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handeTextChange(e) {
|
handeTextChange(e) {
|
||||||
|
|||||||
@@ -2,13 +2,9 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
export default class MenuCancel extends React.PureComponent {
|
export default class MenuCancel extends React.PureComponent {
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<a href="javascript:;" onClick={this.props.onCancel}><i className="material-icons">close</i></a>
|
<a href="#" onClick={(e) => {e.preventDefault(); this.props.onCancel();}}><i className="material-icons">close</i></a>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ export default class MenuContacts extends React.PureComponent {
|
|||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<a href="javascript:;" onClick={this.props.onNewTopic}><i className="material-icons">chat</i></a>
|
<a href="#" onClick={(e) => {e.preventDefault(); this.props.onNewTopic();}}><i className="material-icons">chat</i></a>
|
||||||
|
|
||||||
<a href="javascript:;" onClick={this.props.onSettings}><i className="material-icons">settings</i></a>
|
<a href="#" onClick={(e) => {e.preventDefault(); this.props.onSettings();}}><i className="material-icons">settings</i></a>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ export default class MenuStart extends React.PureComponent {
|
|||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<a href="javascript:;" onClick={this.props.onSignUp}><i className="material-icons">person_add</i></a>
|
<a href="#" onClick={(e) => {e.preventDefault(); this.props.onSignUp();}}><i className="material-icons">person_add</i></a>
|
||||||
|
|
||||||
<a href="javascript:;" onClick={this.props.onSettings}><i className="material-icons">settings</i></a>
|
<a href="#" onClick={(e) => {e.preventDefault(); this.props.onSettings();}}><i className="material-icons">settings</i></a>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,7 +36,8 @@ export default class SearchContacts extends React.PureComponent {
|
|||||||
this.props.onSearchContacts(query.length > 0 ? query : Tinode.DEL_CHAR);
|
this.props.onSearchContacts(query.length > 0 ? query : Tinode.DEL_CHAR);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleClear() {
|
handleClear(e) {
|
||||||
|
e.preventDefault();
|
||||||
if (this.state.edited) {
|
if (this.state.edited) {
|
||||||
this.props.onSearchContacts(Tinode.DEL_CHAR);
|
this.props.onSearchContacts(Tinode.DEL_CHAR);
|
||||||
}
|
}
|
||||||
@@ -63,7 +64,7 @@ export default class SearchContacts extends React.PureComponent {
|
|||||||
value={this.state.search} onChange={this.handleSearchChange}
|
value={this.state.search} onChange={this.handleSearchChange}
|
||||||
onKeyDown={this.handleKeyDown} required autoFocus />
|
onKeyDown={this.handleKeyDown} required autoFocus />
|
||||||
}</FormattedMessage>
|
}</FormattedMessage>
|
||||||
<a href="javascript:;" onClick={this.handleClear}>
|
<a href="#" onClick={this.handleClear}>
|
||||||
<i className="material-icons">close</i>
|
<i className="material-icons">close</i>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -149,7 +149,8 @@ class SendMessage extends React.PureComponent {
|
|||||||
e.target.value = '';
|
e.target.value = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSend() {
|
handleSend(e) {
|
||||||
|
e.preventDefault();
|
||||||
const message = this.state.message.trim();
|
const message = this.state.message.trim();
|
||||||
if (message) {
|
if (message) {
|
||||||
this.props.sendMessage(this.state.message.trim());
|
this.props.sendMessage(this.state.message.trim());
|
||||||
@@ -166,7 +167,7 @@ class SendMessage extends React.PureComponent {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
this.handleSend();
|
this.handleSend(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -193,12 +194,12 @@ class SendMessage extends React.PureComponent {
|
|||||||
<div id="send-message-panel">
|
<div id="send-message-panel">
|
||||||
{this.props.disabled ?
|
{this.props.disabled ?
|
||||||
<i className="material-icons disabled">photo</i> :
|
<i className="material-icons disabled">photo</i> :
|
||||||
<a href="javascript:;" onClick={(e) => {this.attachImage.click();}} title="Add image">
|
<a href="#" onClick={(e) => {e.preventDefault(); this.attachImage.click();}} title="Add image">
|
||||||
<i className="material-icons secondary">photo</i>
|
<i className="material-icons secondary">photo</i>
|
||||||
</a>}
|
</a>}
|
||||||
{this.props.disabled ?
|
{this.props.disabled ?
|
||||||
<i className="material-icons disabled">attach_file</i> :
|
<i className="material-icons disabled">attach_file</i> :
|
||||||
<a href="javascript:;" onClick={(e) => {this.attachFile.click();}} title="Attach file">
|
<a href="#" onClick={(e) => {e.preventDefault(); this.attachFile.click();}} title="Attach file">
|
||||||
<i className="material-icons secondary">attach_file</i>
|
<i className="material-icons secondary">attach_file</i>
|
||||||
</a>}
|
</a>}
|
||||||
<textarea id="sendMessage" placeholder={prompt}
|
<textarea id="sendMessage" placeholder={prompt}
|
||||||
@@ -208,7 +209,7 @@ class SendMessage extends React.PureComponent {
|
|||||||
autoFocus />
|
autoFocus />
|
||||||
{this.props.disabled ?
|
{this.props.disabled ?
|
||||||
<i className="material-icons disabled">send</i> :
|
<i className="material-icons disabled">send</i> :
|
||||||
<a href="javascript:;" onClick={this.handleSend} title="Send">
|
<a href="#" onClick={this.handleSend} title="Send">
|
||||||
<i className="material-icons">send</i>
|
<i className="material-icons">send</i>
|
||||||
</a>}
|
</a>}
|
||||||
<input type="file" ref={(ref) => {this.attachFile = ref;}}
|
<input type="file" ref={(ref) => {this.attachFile = ref;}}
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ export default class TagManager extends React.Component {
|
|||||||
activated: this.props.activated
|
activated: this.props.activated
|
||||||
};
|
};
|
||||||
|
|
||||||
this.handleShowTagManager = this.handleShowTagManager.bind(this);
|
|
||||||
this.handleTagInput = this.handleTagInput.bind(this);
|
this.handleTagInput = this.handleTagInput.bind(this);
|
||||||
this.handleAddTag = this.handleAddTag.bind(this);
|
this.handleAddTag = this.handleAddTag.bind(this);
|
||||||
this.handleRemoveTag = this.handleRemoveTag.bind(this);
|
this.handleRemoveTag = this.handleRemoveTag.bind(this);
|
||||||
@@ -32,10 +31,6 @@ export default class TagManager extends React.Component {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
handleShowTagManager() {
|
|
||||||
this.setState({activated: true});
|
|
||||||
}
|
|
||||||
|
|
||||||
handleTagInput(text) {
|
handleTagInput(text) {
|
||||||
this.setState({tagInput: text});
|
this.setState({tagInput: text});
|
||||||
if (text.length > 0) {
|
if (text.length > 0) {
|
||||||
@@ -137,12 +132,12 @@ export default class TagManager extends React.Component {
|
|||||||
: null}
|
: null}
|
||||||
</div>
|
</div>
|
||||||
:
|
:
|
||||||
<div>
|
<div className="quoted">
|
||||||
<a href="javascript:;" className="flat-button" onClick={this.handleShowTagManager}>
|
<a href="#" className="flat-button" onClick={(e) => {e.preventDefault(); this.setState({activated: true});}}>
|
||||||
<i className="material-icons">edit</i> <FormattedMessage id="title_manage_tags" defaultMessage="Manage tags"
|
<i className="material-icons">edit</i> <FormattedMessage id="title_manage_tags" defaultMessage="Manage"
|
||||||
description="Section title for the list of tags" />
|
description="Section title for the list of tags" />
|
||||||
</a>
|
</a>
|
||||||
<span>{tags}</span>
|
<>{tags}</>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+3042
-324
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+3
-2
@@ -1,14 +1,15 @@
|
|||||||
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
|
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
entry: [
|
entry: [
|
||||||
'./src/index.js',
|
path.resolve(__dirname, 'src/index.js'),
|
||||||
],
|
],
|
||||||
devtool: 'source-map',
|
devtool: 'source-map',
|
||||||
module: {
|
module: {
|
||||||
rules: [
|
rules: [
|
||||||
{
|
{
|
||||||
test: /src\/.*\.(js|jsx)?$/,
|
test: /\.jsx?$/,
|
||||||
loaders: [
|
loaders: [
|
||||||
'babel-loader',
|
'babel-loader',
|
||||||
],
|
],
|
||||||
|
|||||||
Reference in New Issue
Block a user