Compare commits
40
Commits
53d748a7bd
...
12a0660e5b
| 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"],
|
||||
"target": {
|
||||
"namespace": "web",
|
||||
"site": "https://api.tinode.co"
|
||||
"site": "https://web.tinode.co"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -22,4 +22,4 @@
|
||||
"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"]
|
||||
}
|
||||
}]
|
||||
}]
|
||||
|
||||
@@ -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.
|
||||
|
||||
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
|
||||
|
||||
@@ -19,7 +19,7 @@ Try possibly newer or older version live at https://api.tinode.co/. For password
|
||||
|
||||
## 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.
|
||||
|
||||
@@ -32,10 +32,17 @@ As of the time of this writing the following translations exist: English, Russia
|
||||
|
||||
## 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
|
||||
|
||||
|
||||
+16
-14
@@ -461,7 +461,7 @@ li.right:hover .menuTrigger {
|
||||
|
||||
/* Quoted div */
|
||||
.quoted {
|
||||
margin-left: 1.5rem;
|
||||
margin-left: 1rem;
|
||||
}
|
||||
|
||||
/* BEGIN InPlaceEdit styles */
|
||||
@@ -597,7 +597,9 @@ table.permission-editor td.checkbox {
|
||||
.bot-form > div > button {
|
||||
width: auto;
|
||||
}
|
||||
.bot-form button:focus, button:hover, button:active {
|
||||
.bot-form button:focus,
|
||||
.bot-form button:hover,
|
||||
.bot-form button:active {
|
||||
outline: 0;
|
||||
background-color: #e3f2fd;
|
||||
}
|
||||
@@ -1238,8 +1240,12 @@ i.search {
|
||||
}
|
||||
|
||||
#image-preview-container {
|
||||
margin: auto;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#image-preview-footer {
|
||||
@@ -1327,30 +1333,26 @@ i.search {
|
||||
border: 0.62rem solid #c5e1a5;
|
||||
color: #c5e1a5;
|
||||
border-top-color: transparent;
|
||||
border-right-color: inherit;
|
||||
border-bottom-color: inherit;
|
||||
border-left-color: transparent;
|
||||
box-shadow: 0 1.6px 0.5px -0.8px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
/* Right triangle placed bottom right flush. */
|
||||
.chat-box .right .tip::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
content: '';
|
||||
position: absolute;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 0;
|
||||
height: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
left: auto;
|
||||
right: -.8rem;
|
||||
right: -.8rem;
|
||||
top: auto;
|
||||
bottom: 0;
|
||||
border: 0.62rem solid #fafafa;
|
||||
bottom: 0;
|
||||
border: 0.62rem solid #fafafa;
|
||||
color: #fafafa;
|
||||
border-top-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);
|
||||
}
|
||||
|
||||
|
||||
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"?>
|
||||
<!-- 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>
|
||||
<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>
|
||||
|
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">
|
||||
<head>
|
||||
<title>Tinode</title>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="application-name" content="TinodeWeb">
|
||||
<meta name="description" content="Tinode instant messenger">
|
||||
<meta name="application-url" content="https://api.tinode.co/">
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="application-name" content="Tinode Web, version for developers" />
|
||||
<meta name="description" content="Tinode instant messenger, development version" />
|
||||
<meta name="application-url" content="https://web.tinode.co/">
|
||||
<!-- 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 -->
|
||||
<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="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="canonical" href="https://api.tinode.co/" />
|
||||
<link rel="canonical" href="https://web.tinode.co/" />
|
||||
<!-- Google's Roboto font -->
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,400i,500,700,700i" />
|
||||
<!-- 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/react-intl@2/dist/react-intl.js"></script>
|
||||
<!-- 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@5.8.6/firebase-messaging.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@6.4.2/firebase-messaging.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
+17
-13
@@ -2,26 +2,30 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>Tinode</title>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="application-name" content="TinodeWeb">
|
||||
<meta name="description" content="Tinode instant messaging">
|
||||
<meta name="application-url" content="https://api.tinode.co/">
|
||||
<meta name="theme-color" content="#3949AB">
|
||||
<meta name="google" value="notranslate">
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="application-name" content="Tinode Web" />
|
||||
<meta name="description" content="Tinode instant messaging in a browser" />
|
||||
<meta name="application-url" content="https://web.tinode.co/">
|
||||
<meta name="theme-color" content="#3949AB" />
|
||||
<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="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="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/icon?family=Material+Icons" />
|
||||
<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-dom@16.8.4/umd/react-dom.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.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/react-intl@2.8.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@5.8.6/firebase-messaging.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@6.4.2/firebase-app.js"></script>
|
||||
<script crossorigin="anonymous" src="https://unpkg.com/firebase@6.4.2/firebase-messaging.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<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",
|
||||
"description": "Tinode messenger for the web",
|
||||
"version": "0.15.15-rc2",
|
||||
"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"
|
||||
},
|
||||
"version": "0.16.1",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/tinode/example-react-js.git"
|
||||
@@ -49,6 +36,7 @@
|
||||
"img/logo192.png",
|
||||
"img/badge96.png",
|
||||
"img/broken_image.png",
|
||||
"img/og-logo.jpeg",
|
||||
"index-dev.html",
|
||||
"index.html",
|
||||
"manifest.json",
|
||||
@@ -77,10 +65,23 @@
|
||||
"author": "Tinode Authors <info@tinode.co>",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"firebase": "^5.8.6",
|
||||
"react": "^16.8.4",
|
||||
"react-dom": "^16.8.4",
|
||||
"react-intl": "^2.8.0",
|
||||
"tinode-sdk": "^0.15.14"
|
||||
"firebase": "^6.4.2",
|
||||
"react": "^16.9.0",
|
||||
"react-dom": "^16.9.0",
|
||||
"react-intl": "^2.9.0",
|
||||
"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.
|
||||
importScripts('https://unpkg.com/firebase@5.8.1/firebase-app.js');
|
||||
importScripts('https://unpkg.com/firebase@5.8.1/firebase-messaging.js');
|
||||
importScripts('https://unpkg.com/firebase@6.4.2/firebase-app.js');
|
||||
importScripts('https://unpkg.com/firebase@6.4.2/firebase-messaging.js');
|
||||
importScripts('firebase-init.js');
|
||||
|
||||
firebase.initializeApp(FIREBASE_INIT);
|
||||
|
||||
+11
-2
@@ -1,13 +1,13 @@
|
||||
import { PACKAGE_VERSION } from './version.js';
|
||||
|
||||
// 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
|
||||
export const API_KEY = 'AQEAAAABAAD_rAp4DJh05a1HAwFT3A6K';
|
||||
|
||||
// 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.
|
||||
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
|
||||
// hidden under "+X more"
|
||||
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",
|
||||
"email_prompt": "Email, e.g. jdoe@example.com",
|
||||
"button_sign_up": "Sign up",
|
||||
"validate_credential_action": "confirm",
|
||||
"label_your_name": "Your name",
|
||||
"label_password": "Password",
|
||||
"password_unchanged_prompt": "Unchanged",
|
||||
@@ -15,6 +16,10 @@
|
||||
"label_user_id": "Address:",
|
||||
"label_default_access_mode": "Default access mode:",
|
||||
"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",
|
||||
"granted_permissions": "Granted",
|
||||
"menu_item_edit_permissions": "Edit permissions",
|
||||
@@ -133,5 +138,5 @@
|
||||
"cannot_initiate_file_upload": "Cannot initiate file upload.",
|
||||
"tags_not_found": "No tags defined. Add some.",
|
||||
"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": "Сменить пароль",
|
||||
"tags_not_found": "Тагов нет. Добавьте",
|
||||
"tags_editor_no_tags": "Добавьте таги",
|
||||
"title_manage_tags": "Редактор тагов",
|
||||
"title_manage_tags": "Редактировать",
|
||||
"message_sending": "в пути...",
|
||||
"message_sending_failed": "ошибка",
|
||||
"search_placeholder": "Список, напр. email:alice@example.com, tel:+17025550003...",
|
||||
@@ -133,5 +133,10 @@
|
||||
"peers_messaging_disabled": "Чат заблокирован у корреспондента.",
|
||||
"enable_peers_messaging": "Разблокировать.",
|
||||
"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": "添加一些标签",
|
||||
"title_manage_tags": "管理标签",
|
||||
"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) {
|
||||
const dataStart = imageDataUrl.indexOf(',');
|
||||
card.photo = {
|
||||
card.photo = dataStart >= 0 ? {
|
||||
data: imageDataUrl.substring(dataStart+1),
|
||||
type: 'jpg'
|
||||
};
|
||||
} : imageDataUrl;
|
||||
}
|
||||
}
|
||||
return card;
|
||||
@@ -66,7 +66,7 @@ export function arrayEqual(a, b) {
|
||||
// as close to E.164 as possible.
|
||||
export function asPhone(val) {
|
||||
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 null;
|
||||
@@ -80,3 +80,63 @@ export function asEmail(val) {
|
||||
}
|
||||
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",
|
||||
"email_prompt": "Email, e.g. jdoe@example.com",
|
||||
"button_sign_up": "Sign up",
|
||||
"validate_credential_action": "confirm",
|
||||
"label_your_name": "Your name",
|
||||
"label_password": "Password",
|
||||
"password_unchanged_prompt": "Unchanged",
|
||||
@@ -16,6 +17,10 @@
|
||||
"label_user_id": "Address:",
|
||||
"label_default_access_mode": "Default access mode:",
|
||||
"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",
|
||||
"granted_permissions": "Granted",
|
||||
"menu_item_edit_permissions": "Edit permissions",
|
||||
@@ -134,7 +139,7 @@
|
||||
"cannot_initiate_file_upload": "Cannot initiate file upload.",
|
||||
"tags_not_found": "No tags defined. Add some.",
|
||||
"tags_editor_no_tags": "Add some tags",
|
||||
"title_manage_tags": "Manage tags"
|
||||
"title_manage_tags": "Manage"
|
||||
},
|
||||
"ru": {
|
||||
"contacts_not_found": "Чатов нет<br />¯∖_(ツ)_/¯",
|
||||
@@ -178,7 +183,7 @@
|
||||
"sidepanel_title_reset": "Сменить пароль",
|
||||
"tags_not_found": "Тагов нет. Добавьте",
|
||||
"tags_editor_no_tags": "Добавьте таги",
|
||||
"title_manage_tags": "Редактор тагов",
|
||||
"title_manage_tags": "Редактировать",
|
||||
"message_sending": "в пути...",
|
||||
"message_sending_failed": "ошибка",
|
||||
"search_placeholder": "Список, напр. email:alice@example.com, tel:+17025550003...",
|
||||
@@ -271,7 +276,12 @@
|
||||
"peers_messaging_disabled": "Чат заблокирован у корреспондента.",
|
||||
"enable_peers_messaging": "Разблокировать.",
|
||||
"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": {
|
||||
"archived_contacts": "已归档联系人 ({count})",
|
||||
@@ -407,6 +417,12 @@
|
||||
"tags_not_found": "尚未定义标签。添加一些。",
|
||||
"tags_editor_no_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.
|
||||
|
||||
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.state = ContactsView.getDerivedStateFromProps(props, {});
|
||||
this.state = ContactsView.deriveStateFromProps(props);
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps(nextProps, prevState) {
|
||||
static deriveStateFromProps(props) {
|
||||
const contacts = [];
|
||||
let unreadThreads = 0;
|
||||
let archivedCount = 0;
|
||||
nextProps.chatList.map((c) => {
|
||||
props.chatList.map((c) => {
|
||||
if (c.private && c.private.arch) {
|
||||
if (nextProps.archive) {
|
||||
if (props.archive) {
|
||||
contacts.push(c);
|
||||
} else {
|
||||
archivedCount ++;
|
||||
}
|
||||
} else if (!nextProps.archive) {
|
||||
} else if (!props.archive) {
|
||||
contacts.push(c);
|
||||
unreadThreads += c.unread > 0 ? 1 : 0;
|
||||
}
|
||||
@@ -51,14 +51,22 @@ export default class ContactsView extends React.Component {
|
||||
});
|
||||
}
|
||||
|
||||
updateFavicon(unreadThreads);
|
||||
|
||||
return {
|
||||
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) {
|
||||
this.props.onShowArchive();
|
||||
}
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
|
||||
// Edit account parameters.
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import Tinode from 'tinode-sdk';
|
||||
|
||||
import AvatarUpload from '../widgets/avatar-upload.jsx';
|
||||
import CheckBox from '../widgets/checkbox.jsx';
|
||||
import InPlaceEdit from '../widgets/in-place-edit.jsx';
|
||||
import PermissionsEditor from '../widgets/permissions-editor.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 { 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.handleImageChanged = this.handleImageChanged.bind(this);
|
||||
this.handleCheckboxClick = this.handleCheckboxClick.bind(this);
|
||||
this.handleStartAddCred = this.handleStartAddCred.bind(this);
|
||||
this.handleCredChange = this.handleCredChange.bind(this);
|
||||
this.handleCredKeyDown = this.handleCredKeyDown.bind(this);
|
||||
this.handleCredEntered = this.handleCredEntered.bind(this);
|
||||
@@ -65,7 +66,6 @@ export default class EditAccountView extends React.Component {
|
||||
}
|
||||
|
||||
tnCredsUpdated(creds) {
|
||||
console.log("tnCredsUpdated", creds, new Error());
|
||||
this.setState({credentials: creds || []});
|
||||
}
|
||||
|
||||
@@ -81,7 +81,7 @@ export default class EditAccountView extends React.Component {
|
||||
|
||||
handleImageChanged(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) {
|
||||
@@ -92,10 +92,6 @@ export default class EditAccountView extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
handleStartAddCred() {
|
||||
this.setState({addCredActive: true});
|
||||
}
|
||||
|
||||
handleCredChange(e) {
|
||||
this.setState({newCred: e.target.value, addCredInvalid: false});
|
||||
}
|
||||
@@ -165,23 +161,16 @@ export default class EditAccountView extends React.Component {
|
||||
}
|
||||
|
||||
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 = [];
|
||||
this.state.credentials.map((cred) => {
|
||||
credentials.push(<div key={cred.meth + ":" + cred.val}>{cred.meth}: <tt>{cred.val}</tt>
|
||||
{!cred.done ?
|
||||
<a href="javascript:;"
|
||||
onClick={this.props.onCredConfirm.bind(this, cred.meth, cred.val)}>confirm</a>
|
||||
: null} <a href="javascript:;" onClick={this.props.onCredDelete.bind(this, cred.meth, cred.val)}><i
|
||||
className="material-icons gray">delete_outline</i></a></div>);
|
||||
credentials.push(<div key={cred.meth + ":" + cred.val + ":" + cred.done}>{cred.meth}: <tt>{cred.val}</tt>
|
||||
<span > {!cred.done ?
|
||||
<a href="#" onClick={(e) => {e.preventDefault(); this.props.onCredConfirm(cred.meth, cred.val);}}>
|
||||
<FormattedMessage id="validate_credential_action" defaultMessage="confirm"
|
||||
description="Validate credentail call to action" />
|
||||
</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 (
|
||||
@@ -276,7 +265,7 @@ export default class EditAccountView extends React.Component {
|
||||
placeholder="Phone number or email" required="required" autoFocus
|
||||
onChange={this.handleCredChange} onKeyDown={this.handleCredKeyDown} onBlur={this.handleCredEntered} />
|
||||
: 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 className="hr" />
|
||||
@@ -301,11 +290,38 @@ export default class EditAccountView extends React.Component {
|
||||
</div>
|
||||
<div className="hr" />
|
||||
<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"
|
||||
defaultMessage="Logout" description="Button [Logout]" />
|
||||
</a>
|
||||
</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>
|
||||
}</React.Fragment>
|
||||
);
|
||||
|
||||
@@ -238,7 +238,7 @@ class InfoView extends React.Component {
|
||||
|
||||
handleImageChanged(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) {
|
||||
@@ -339,7 +339,8 @@ class InfoView extends React.Component {
|
||||
this.setState({showPermissionEditorFor: undefined});
|
||||
}
|
||||
|
||||
handleShowAddMembers() {
|
||||
handleShowAddMembers(e) {
|
||||
e.preventDefault();
|
||||
this.props.onInitFind();
|
||||
this.setState({showMemberPanel: true});
|
||||
}
|
||||
@@ -353,7 +354,8 @@ class InfoView extends React.Component {
|
||||
this.setState({showMemberPanel: false});
|
||||
}
|
||||
|
||||
handleLeave() {
|
||||
handleLeave(e) {
|
||||
e.preventDefault();
|
||||
this.props.onLeaveTopic(this.props.topic);
|
||||
}
|
||||
|
||||
@@ -594,13 +596,13 @@ class InfoView extends React.Component {
|
||||
</div>
|
||||
<div className="panel-form-row">
|
||||
{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"
|
||||
defaultMessage="Add members" description="Flat button [Add members] (to topic)" />
|
||||
</a>
|
||||
: null}
|
||||
{!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"
|
||||
defaultMessage="Leave" description="Flat button [Leave] (topic)" />
|
||||
</a>
|
||||
@@ -624,7 +626,7 @@ class InfoView extends React.Component {
|
||||
</div>
|
||||
:
|
||||
<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"
|
||||
defaultMessage="Leave" description="Action [Leave] chat" />
|
||||
</a>
|
||||
|
||||
@@ -55,6 +55,8 @@ export default class LoginView extends React.Component {
|
||||
(login_prompt) => <input type="text" id="inputLogin"
|
||||
placeholder={login_prompt}
|
||||
autoComplete="username"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="none"
|
||||
value={this.state.login}
|
||||
onChange={this.handleLoginChange}
|
||||
required autoFocus />
|
||||
|
||||
@@ -73,7 +73,6 @@ class MessagesView extends React.Component {
|
||||
this.handleFormResponse = this.handleFormResponse.bind(this);
|
||||
this.handleContextClick = this.handleContextClick.bind(this);
|
||||
this.handleShowContextMenuMessage = this.handleShowContextMenuMessage.bind(this);
|
||||
this.handleBackNavigation = this.handleBackNavigation.bind(this);
|
||||
this.handleNewChatAcceptance = this.handleNewChatAcceptance.bind(this);
|
||||
this.handleEnablePeer = this.handleEnablePeer.bind(this);
|
||||
}
|
||||
@@ -486,15 +485,12 @@ class MessagesView extends React.Component {
|
||||
this.props.showContextMenu(params, menuItems);
|
||||
}
|
||||
|
||||
handleBackNavigation() {
|
||||
this.props.onHideMessagesView();
|
||||
}
|
||||
|
||||
handleNewChatAcceptance(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);
|
||||
}
|
||||
|
||||
@@ -579,7 +575,7 @@ class MessagesView extends React.Component {
|
||||
<div id="topic-view" className={this.props.hideSelf ? 'nodisplay' : null}>
|
||||
<div id="topic-caption-panel" className="caption-panel">
|
||||
{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>
|
||||
</a>
|
||||
:
|
||||
@@ -605,7 +601,7 @@ class MessagesView extends React.Component {
|
||||
<div id="topic-users" />
|
||||
}
|
||||
<div>
|
||||
<a href="javascript:;" onClick={this.handleContextClick}>
|
||||
<a href="#" onClick={this.handleContextClick}>
|
||||
<i className="material-icons">more_vert</i>
|
||||
</a>
|
||||
</div>
|
||||
@@ -638,7 +634,7 @@ class MessagesView extends React.Component {
|
||||
<div id="peer-messaging-disabled-note">
|
||||
<i className="material-icons secondary">block</i> <FormattedMessage
|
||||
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"
|
||||
defaultMessage="Enable" description="Call to action to enable peer's messaging" /></a>.
|
||||
</div> : null}
|
||||
|
||||
@@ -56,19 +56,19 @@ export default class NewTopicView extends React.Component {
|
||||
<div className="flex-column">
|
||||
<ul className="tabbar">
|
||||
<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"
|
||||
description="Tab title Find" />
|
||||
</a>
|
||||
</li>
|
||||
<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"
|
||||
description="Tab title New Group" />
|
||||
</a>
|
||||
</li>
|
||||
<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"
|
||||
description="Tab title Find topic by ID" />
|
||||
</a>
|
||||
|
||||
@@ -57,7 +57,7 @@ export default class PasswordResetView extends React.PureComponent {
|
||||
onChange={this.handlePasswordChange} />
|
||||
}</FormattedMessage>
|
||||
:
|
||||
<React.Fragment>
|
||||
<>
|
||||
<label htmlFor="inputEmail">
|
||||
<FormattedMessage id="label_reset_password"
|
||||
defaultMessage="Send a password reset email:"
|
||||
@@ -72,7 +72,7 @@ export default class PasswordResetView extends React.PureComponent {
|
||||
onChange={this.handleEmailChange}
|
||||
required autoFocus />
|
||||
}</FormattedMessage>
|
||||
</React.Fragment>
|
||||
</>
|
||||
}
|
||||
<div className="dialog-buttons">
|
||||
<button className="blue" type="submit">{reset ?
|
||||
|
||||
+32
-14
@@ -496,14 +496,13 @@ class TinodeWeb extends React.Component {
|
||||
credCode: undefined,
|
||||
myUserId: this.tinode.getCurrentUserID()
|
||||
});
|
||||
console.log("handleLoginSuccessful");
|
||||
// Subscribe, fetch topic desc, the list of subscriptions. Messages are not fetched.
|
||||
me.subscribe(
|
||||
me.startMetaQuery().
|
||||
withLaterSub().
|
||||
withDesc().
|
||||
withTags().
|
||||
withCreds().
|
||||
withCred().
|
||||
build()
|
||||
).catch((err) => {
|
||||
localStorage.removeItem('auth-token');
|
||||
@@ -532,15 +531,23 @@ class TinodeWeb extends React.Component {
|
||||
} else if (what == 'read') {
|
||||
this.resetContactList();
|
||||
} 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
|
||||
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.
|
||||
if (this.state.topicSelected != cont.topic) {
|
||||
if (this.state.messageSounds) {
|
||||
} else if (this.state.topicSelected != cont.topic) {
|
||||
if (this.state.messageSounds && !archived) {
|
||||
POP_SOUND.play();
|
||||
}
|
||||
this.resetContactList();
|
||||
} else if (document.hidden && this.state.messageSounds) {
|
||||
POP_SOUND.play();
|
||||
}
|
||||
} else if (what == 'recv') {
|
||||
// Explicitly ignoring "recv" -- it causes no visible updates to contact list.
|
||||
@@ -940,12 +947,13 @@ class TinodeWeb extends React.Component {
|
||||
}
|
||||
|
||||
handleCredAdd(method, value) {
|
||||
// TODO: implement
|
||||
console.log("add credential", method, value);
|
||||
const me = this.tinode.getMeTopic();
|
||||
me.setMeta({cred: {meth: method, val: value}}).catch((err) => {
|
||||
this.handleError(err.message, 'err');
|
||||
});
|
||||
}
|
||||
|
||||
handleCredDelete(method, value) {
|
||||
console.log("delete credential", method, value);
|
||||
const me = this.tinode.getMeTopic();
|
||||
me.delCredential(method, value).catch((err) => {
|
||||
this.handleError(err.message, 'err');
|
||||
@@ -953,8 +961,7 @@ class TinodeWeb extends React.Component {
|
||||
}
|
||||
|
||||
handleCredConfirm(method, response) {
|
||||
// TODO: implement
|
||||
console.log("confirm credential", method, response);
|
||||
this.handleCredentialsRequest({cred: [method]});
|
||||
}
|
||||
|
||||
// User clicked Cancel button in Setting or Sign Up panel.
|
||||
@@ -1187,15 +1194,26 @@ class TinodeWeb extends React.Component {
|
||||
}
|
||||
|
||||
handleValidateCredentialsRequest(cred, code) {
|
||||
this.setState({credMethod: cred, credCode: code});
|
||||
this.doLogin(null, null, {meth: cred, resp: code});
|
||||
if (this.tinode.isAuthenticated()) {
|
||||
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) {
|
||||
// If already connected, connnect() will return a resolved promise.
|
||||
this.tinode.connect()
|
||||
.then(() => {
|
||||
return this.tinode.requestResetAuthSecret('basic', method, value)
|
||||
return this.tinode.requestResetAuthSecret('basic', method, value);
|
||||
})
|
||||
.catch((err) => {
|
||||
// Socket error
|
||||
|
||||
@@ -26,6 +26,7 @@ class ValidationView extends React.PureComponent {
|
||||
this.handleChange = this.handleChange.bind(this);
|
||||
this.handleKeyPress = this.handleKeyPress.bind(this);
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
this.handleCancel = this.handleCancel.bind(this);
|
||||
}
|
||||
|
||||
handleChange(e) {
|
||||
@@ -35,6 +36,8 @@ class ValidationView extends React.PureComponent {
|
||||
handleKeyPress(e) {
|
||||
if (e.key === 'Enter') {
|
||||
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() {
|
||||
const { formatMessage } = this.props.intl;
|
||||
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}>
|
||||
<FormattedMessage id="button_confirm" defaultMessage="Confirm" description="Button [Confirm]" />
|
||||
</button>
|
||||
<button className="white" onClick={this.handleCancel}>
|
||||
<FormattedMessage id="button_cancel" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -3,6 +3,7 @@ import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import FileProgress from './file-progress.jsx';
|
||||
import { bytesToHumanSize } from '../lib/strformat.js';
|
||||
import { isUrlRelative, sanitizeUrl } from '../lib/utils.js';
|
||||
|
||||
export default class Attachment extends React.Component {
|
||||
constructor(props) {
|
||||
@@ -53,15 +54,15 @@ export default class Attachment extends React.Component {
|
||||
// If the URL is relative use LargeFileHelper to attach authentication
|
||||
// credentials to the request.
|
||||
let url, helperFunc;
|
||||
if (!this.props.uploader && !this.state.downloader &&
|
||||
!(/^(?:(?:[a-z]+:)?\/\/)/i.test(this.props.downloadUrl))) {
|
||||
if (!this.props.uploader && !this.state.downloader && isUrlRelative(this.props.downloadUrl)) {
|
||||
// Relative URL. Use download helper.
|
||||
url = "javascript:;";
|
||||
url = '#';
|
||||
helperFunc = (e) => {
|
||||
e.preventDefault();
|
||||
this.downloadFile(this.props.downloadUrl, this.props.filename, this.props.mimetype);
|
||||
};
|
||||
} else {
|
||||
url = this.props.downloadUrl;
|
||||
url = sanitizeUrl(this.props.downloadUrl) || 'about:blank';
|
||||
helperFunc = null;
|
||||
}
|
||||
return (
|
||||
|
||||
@@ -14,16 +14,12 @@ export default class AvatarUpload extends React.Component {
|
||||
};
|
||||
|
||||
this.handleFileUpload = this.handleFileUpload.bind(this);
|
||||
this.handleClear = this.handleClear.bind(this);
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps(nextProps, prevState) {
|
||||
if (prevState.dataUrl != nextProps.avatar) {
|
||||
return {
|
||||
dataUrl: nextProps.avatar
|
||||
};
|
||||
componentDidUpdate(prevProps) {
|
||||
if (this.props.avatar != prevProps.avatar) {
|
||||
this.setState({dataUrl: this.props.avatar})
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
handleFileUpload(e) {
|
||||
@@ -42,10 +38,6 @@ export default class AvatarUpload extends React.Component {
|
||||
e.target.value = '';
|
||||
}
|
||||
|
||||
handleClear() {
|
||||
this.props.onImageChanged(null);
|
||||
}
|
||||
|
||||
render() {
|
||||
// Randomize id value in case more than one AvatarUpload is shown
|
||||
// at the same time.
|
||||
@@ -54,7 +46,7 @@ export default class AvatarUpload extends React.Component {
|
||||
<div className="avatar-upload">
|
||||
{this.props.readOnly || !this.state.dataUrl ?
|
||||
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>
|
||||
</a>}
|
||||
{this.state.dataUrl ?
|
||||
|
||||
@@ -6,6 +6,7 @@ import { Drafty } from 'tinode-sdk'
|
||||
import Attachment from './attachment.jsx';
|
||||
import LetterTile from './letter-tile.jsx';
|
||||
import ReceivedMarker from './received-marker.jsx'
|
||||
import { sanitizeImageUrl, sanitizeUrl } from '../lib/utils.js';
|
||||
|
||||
export default class ChatMessage extends React.Component {
|
||||
constructor(props) {
|
||||
@@ -48,7 +49,7 @@ export default class ChatMessage extends React.Component {
|
||||
e.target.dataset.val === undefined ? 1 : '' + e.target.dataset.val;
|
||||
}
|
||||
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';
|
||||
this.props.onFormResponse(e.target.dataset.act, text, data);
|
||||
@@ -97,10 +98,10 @@ export default class ChatMessage extends React.Component {
|
||||
}, this);
|
||||
content = React.createElement('span', null, Drafty.format(content, draftyFormatter, this));
|
||||
} 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"
|
||||
defaultMessage="invalid content" description="Shown when message is unreadable" />
|
||||
</i></span>
|
||||
</i></>
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -125,7 +126,7 @@ export default class ChatMessage extends React.Component {
|
||||
received={this.props.received} />
|
||||
</div>
|
||||
<span className="menuTrigger">
|
||||
<a href="javascript:;" onClick={this.handleContextClick}>
|
||||
<a href="#" onClick={this.handleContextClick}>
|
||||
<i className="material-icons">expand_more</i>
|
||||
</a>
|
||||
</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);
|
||||
dim = dim || {dstWidth: BROKEN_IMAGE_SIZE, dstHeight: BROKEN_IMAGE_SIZE};
|
||||
attr.style = { width: dim.dstWidth + 'px', height: dim.dstHeight + 'px' };
|
||||
attr.src = sanitizeImageUrl(attr.src);
|
||||
if (attr.src) {
|
||||
attr.onClick = this.handlePreviewImage;
|
||||
attr.className += ' image-clickable';
|
||||
|
||||
+17
-12
@@ -9,7 +9,9 @@ export default class ChipInput extends React.Component {
|
||||
constructor(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.removeChipAt = this.removeChipAt.bind(this);
|
||||
@@ -19,20 +21,23 @@ export default class ChipInput extends React.Component {
|
||||
this.handleKeyDown = this.handleKeyDown.bind(this);
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps(nextProps, prevState) {
|
||||
const state = {
|
||||
placeholder: nextProps.chips ? '' : nextProps.prompt,
|
||||
sortedChips: ChipInput.sortChips(nextProps.chips, nextProps.required),
|
||||
chipIndex: ChipInput.indexChips(nextProps.chips),
|
||||
focused: prevState && prevState.focused
|
||||
static deriveStateFromProps(props) {
|
||||
return {
|
||||
placeholder: props.chips ? '' : props.prompt,
|
||||
sortedChips: ChipInput.sortChips(props.chips, props.required),
|
||||
chipIndex: ChipInput.indexChips(props.chips)
|
||||
};
|
||||
}
|
||||
|
||||
if (!prevState || nextProps.chips.length > prevState.sortedChips.length) {
|
||||
// Chip added: clear input.
|
||||
state.input ='';
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
if (prevProps.chips != this.props.chips ||
|
||||
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
|
||||
|
||||
@@ -9,7 +9,8 @@ export default class Chip extends React.PureComponent {
|
||||
this.handleCancel = this.handleCancel.bind(this);
|
||||
}
|
||||
|
||||
handleCancel() {
|
||||
handleCancel(e) {
|
||||
e.preventDefault();
|
||||
this.props.onCancel(this.props.topic, this.props.index);
|
||||
}
|
||||
|
||||
@@ -29,7 +30,7 @@ export default class Chip extends React.PureComponent {
|
||||
}
|
||||
<span>{title}</span>
|
||||
{this.props.onCancel && !this.props.required ?
|
||||
<a href="javascript:;" onClick={this.handleCancel} >×</a>
|
||||
<a href="#" onClick={this.handleCancel} >×</a>
|
||||
: <span className="spacer" />}
|
||||
</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>);
|
||||
}
|
||||
});
|
||||
return <React.Fragment>{badges}</React.Fragment>;
|
||||
return <>{badges}</>;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -70,11 +70,11 @@ export default class Contact extends React.Component {
|
||||
<UnreadBadge count={this.props.unread} /><ContactBadges badges={icon_badges} />
|
||||
</div>
|
||||
{this.props.comment ? <div className="contact-comment">{this.props.comment}</div> : null}
|
||||
<span><ContactBadges badges={badges} /></span>
|
||||
<><ContactBadges badges={badges} /></>
|
||||
</div>
|
||||
{this.props.showContextMenu ?
|
||||
<span className="menuTrigger">
|
||||
<a href="javascript:;" onClick={this.handleContextClick}>
|
||||
<a href="#" onClick={this.handleContextClick}>
|
||||
<i className="material-icons">expand_more</i>
|
||||
</a>
|
||||
</span> : null}
|
||||
|
||||
@@ -13,10 +13,12 @@ export default class ErrorPanel extends React.PureComponent {
|
||||
this.hide = this.hide.bind(this);
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps(nextProps, prevState) {
|
||||
return {
|
||||
show: !(!nextProps.level)
|
||||
};
|
||||
componentDidUpdate(prevProps) {
|
||||
if (prevProps.level !== this.props.level) {
|
||||
this.setState({
|
||||
show: !(!this.props.level)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
hide() {
|
||||
@@ -33,14 +35,14 @@ export default class ErrorPanel extends React.PureComponent {
|
||||
return (
|
||||
<div className={className}>
|
||||
<div className="icon"><i className="material-icons">{level}</i></div>
|
||||
<span>
|
||||
<>
|
||||
<span dangerouslySetInnerHTML={{__html: this.props.text}} />
|
||||
{this.props.action ?
|
||||
<a href="javascript:;" onClick={this.props.action}>
|
||||
<a href="#" onClick={(e) => {e.preventDefault(); this.props.action();}}>
|
||||
{this.props.actionText}
|
||||
</a>
|
||||
: null}
|
||||
</span>
|
||||
</>
|
||||
<div className="cancel"><MenuCancel onCancel={this.hide} /></div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -8,7 +8,7 @@ export default class FileProgress extends React.PureComponent {
|
||||
<div className="uploader">
|
||||
<div><span style={{width: (this.props.progress * 100) + "%"}}></span></div>
|
||||
{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"
|
||||
defaultMessage="cancel" description="Call to action [cancel]" />
|
||||
</a>
|
||||
|
||||
@@ -9,21 +9,27 @@ export default class ImagePreview extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {};
|
||||
this.state = {
|
||||
width: 0,
|
||||
height: 0
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.setState({
|
||||
width: this.container.clientWidth,
|
||||
height: this.container.clientHeight
|
||||
});
|
||||
assignWidth(node) {
|
||||
if (node && !this.state.width) {
|
||||
const bounds = node.getBoundingClientRect();
|
||||
this.setState({
|
||||
width: bounds.width | 0,
|
||||
height: bounds.height | 0
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.props.content) {
|
||||
return null;
|
||||
}
|
||||
const instance = this;
|
||||
|
||||
const dim = fitImageSize(this.props.content.width, this.props.content.height,
|
||||
this.state.width, this.state.height, false);
|
||||
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%';
|
||||
|
||||
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) {
|
||||
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 height = this.props.content.height || '-';
|
||||
@@ -45,9 +52,9 @@ export default class ImagePreview extends React.PureComponent {
|
||||
<i className="material-icons">file_download</i> <FormattedMessage
|
||||
id="download_action" defaultMessage="download" description="Call to action [download]" />
|
||||
</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 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} />
|
||||
</div>
|
||||
<div id="image-preview-footer">
|
||||
|
||||
@@ -21,16 +21,16 @@ export default class InPlaceEdit extends React.Component {
|
||||
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.
|
||||
// Ignore update if in edit mode.
|
||||
if (prevState.initialValue != nextProps.value && !prevState.active) {
|
||||
return {
|
||||
initialValue: nextProps.value || '',
|
||||
value: nextProps.value || ''
|
||||
};
|
||||
const newValue = this.props.value || '';
|
||||
if (prevState.initialValue != newValue && !prevState.active) {
|
||||
this.setState({
|
||||
initialValue: newValue,
|
||||
value: newValue
|
||||
});
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
handeTextChange(e) {
|
||||
|
||||
@@ -2,13 +2,9 @@
|
||||
import React from 'react';
|
||||
|
||||
export default class MenuCancel extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
render() {
|
||||
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() {
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,9 +4,9 @@ export default class MenuStart extends React.PureComponent {
|
||||
render() {
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -36,7 +36,8 @@ export default class SearchContacts extends React.PureComponent {
|
||||
this.props.onSearchContacts(query.length > 0 ? query : Tinode.DEL_CHAR);
|
||||
}
|
||||
|
||||
handleClear() {
|
||||
handleClear(e) {
|
||||
e.preventDefault();
|
||||
if (this.state.edited) {
|
||||
this.props.onSearchContacts(Tinode.DEL_CHAR);
|
||||
}
|
||||
@@ -63,7 +64,7 @@ export default class SearchContacts extends React.PureComponent {
|
||||
value={this.state.search} onChange={this.handleSearchChange}
|
||||
onKeyDown={this.handleKeyDown} required autoFocus />
|
||||
}</FormattedMessage>
|
||||
<a href="javascript:;" onClick={this.handleClear}>
|
||||
<a href="#" onClick={this.handleClear}>
|
||||
<i className="material-icons">close</i>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -149,7 +149,8 @@ class SendMessage extends React.PureComponent {
|
||||
e.target.value = '';
|
||||
}
|
||||
|
||||
handleSend() {
|
||||
handleSend(e) {
|
||||
e.preventDefault();
|
||||
const message = this.state.message.trim();
|
||||
if (message) {
|
||||
this.props.sendMessage(this.state.message.trim());
|
||||
@@ -166,7 +167,7 @@ class SendMessage extends React.PureComponent {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
this.handleSend();
|
||||
this.handleSend(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -193,12 +194,12 @@ class SendMessage extends React.PureComponent {
|
||||
<div id="send-message-panel">
|
||||
{this.props.disabled ?
|
||||
<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>
|
||||
</a>}
|
||||
{this.props.disabled ?
|
||||
<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>
|
||||
</a>}
|
||||
<textarea id="sendMessage" placeholder={prompt}
|
||||
@@ -208,7 +209,7 @@ class SendMessage extends React.PureComponent {
|
||||
autoFocus />
|
||||
{this.props.disabled ?
|
||||
<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>
|
||||
</a>}
|
||||
<input type="file" ref={(ref) => {this.attachFile = ref;}}
|
||||
|
||||
@@ -17,7 +17,6 @@ export default class TagManager extends React.Component {
|
||||
activated: this.props.activated
|
||||
};
|
||||
|
||||
this.handleShowTagManager = this.handleShowTagManager.bind(this);
|
||||
this.handleTagInput = this.handleTagInput.bind(this);
|
||||
this.handleAddTag = this.handleAddTag.bind(this);
|
||||
this.handleRemoveTag = this.handleRemoveTag.bind(this);
|
||||
@@ -32,10 +31,6 @@ export default class TagManager extends React.Component {
|
||||
return null;
|
||||
}
|
||||
|
||||
handleShowTagManager() {
|
||||
this.setState({activated: true});
|
||||
}
|
||||
|
||||
handleTagInput(text) {
|
||||
this.setState({tagInput: text});
|
||||
if (text.length > 0) {
|
||||
@@ -137,12 +132,12 @@ export default class TagManager extends React.Component {
|
||||
: null}
|
||||
</div>
|
||||
:
|
||||
<div>
|
||||
<a href="javascript:;" className="flat-button" onClick={this.handleShowTagManager}>
|
||||
<i className="material-icons">edit</i> <FormattedMessage id="title_manage_tags" defaultMessage="Manage tags"
|
||||
<div className="quoted">
|
||||
<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"
|
||||
description="Section title for the list of tags" />
|
||||
</a>
|
||||
<span>{tags}</span>
|
||||
<>{tags}</>
|
||||
</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 path = require('path');
|
||||
|
||||
module.exports = {
|
||||
entry: [
|
||||
'./src/index.js',
|
||||
path.resolve(__dirname, 'src/index.js'),
|
||||
],
|
||||
devtool: 'source-map',
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /src\/.*\.(js|jsx)?$/,
|
||||
test: /\.jsx?$/,
|
||||
loaders: [
|
||||
'babel-loader',
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user