Compare commits

...
40 Commits
Author SHA1 Message Date
or-else 12a0660e5b shorthand replacements for React.Fragment and <span> 2019-09-22 21:18:31 +03:00
or-else 2502460143 translations added, minor visual tweaks, version bumped 2019-09-20 14:37:29 +03:00
or-else c31631b69f ToS added, small bug in CSS fixed 2019-09-19 12:57:42 +03:00
or-else 711cf7c690 fix: crash on leaving the topic 2019-09-04 20:12:15 +03:00
or-else 3669ed7645 bump version and rebuild 2019-09-03 14:55:07 +03:00
or-else 41ebf3c705 fix: #37; correct case for auto* in LoginView 2019-09-02 18:49:31 +03:00
or-else 6f359be64d make webpack path universal / vs \\ 2019-08-29 20:03:57 +03:00
or-else d472a321de no autocorrection for username 2019-08-26 12:26:10 +03:00
or-else eda9f0f7f9 fix: exception in send-message.jsx on Enter 2019-08-24 18:04:14 +03:00
or-else d9c729d9ab update dependencies and rebuild 2019-08-24 14:31:55 +03:00
or-else 78b0909f02 add open graph headers 2019-08-24 14:13:33 +03:00
or-else 61a9b6c56f minify logo.svg and bump version 2019-08-22 11:55:23 +03:00
or-else a6bc2a1f8f migrate the app from api.. to web.. 2019-08-22 10:46:14 +03:00
or-else 503c9128d4 fix: wrong bubble tip color in Safari 2019-08-21 17:12:47 +03:00
or-else 1b4bba051e fix blob: URLS, corractly calculate image preview viwport size 2019-08-17 20:07:15 +03:00
or-else c14fb857bc fix for possible xss exploits 2019-08-17 16:56:15 +03:00
or-else be528ff282 bum version and rebuild 2019-08-15 11:24:49 +03:00
or-else fb9461834a refactored getDerivedStateFromProps in in-place-edit 2019-08-15 10:49:24 +03:00
or-else 0217d84721 refactoring due to facebook/react/pull/15047 2019-08-15 10:30:29 +03:00
or-else f1744c3f69 getDerivedStateFromProps refactored in contacts-view and chips-input, #35 2019-08-14 20:24:07 +03:00
or-else a79373c8cd replace getDerivedStateFromProps with componentDidUpdate 2019-08-12 11:51:44 +03:00
or-else d2975f0c20 fix avatar not being updated while edited 2019-08-06 17:18:28 +03:00
or-else d782fb8eab bump version and rebuild 2019-08-02 09:06:46 +03:00
or-else bdf152e5d6 fix: unable to delete profile picture 2019-08-01 09:53:41 +03:00
or-else 40edad1c99 fixed: unable to remove topic avatar 2019-08-01 09:49:26 +03:00
or-else 7239aae8aa fix typo, bump versin and rebuild 2019-07-26 11:48:19 +03:00
or-else 81c789b713 push notification instructions updated 2019-07-20 12:12:26 +03:00
or-else 744da99f30 don't play new message sound for achived topics 2019-07-18 13:35:39 +03:00
or-else 112ce08fde updated dependencies and rebuilt 2019-07-17 13:51:59 +03:00
or-else 3fd01648a8 bump version and rebuild 2019-07-08 17:29:00 +03:00
or-else 619ec15920 update one translation 2019-06-26 16:08:54 -07:00
or-else a6a097b15f translations 2019-06-20 20:26:43 -07:00
or-else 450a2f6975 translation tweaks 2019-06-16 12:09:47 -07:00
or-else e006043290 rebuilt with possible fix for https://github.com/tinode/chat/issues/242 2019-06-15 09:46:39 -07:00
or-else 34db50280e rebuild 2019-06-13 09:41:31 -07:00
or-else 6268fb68f8 minor fixes 2019-06-12 18:27:24 -07:00
or-else 9a310a0438 handle credential confirmation from EditAccountView 2019-06-12 11:03:21 -07:00
or-else 821ab6a16f dependencies updated 2019-06-11 17:01:11 -07:00
or-else f9e78bf280 credential management 2019-06-09 19:57:41 -07:00
or-else 83488ca878 rebuild 2019-06-07 16:27:26 -07:00
49 changed files with 5495 additions and 1630 deletions
+1 -1
View File
@@ -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"
} }
}, },
{ {
+11 -4
View File
@@ -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
+9 -7
View File
@@ -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,8 +1333,6 @@ 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);
} }
@@ -1349,8 +1353,6 @@ i.search {
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);
} }
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -305
View File
@@ -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

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

+14 -10
View File
@@ -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
View File
@@ -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>
+2033 -758
View File
File diff suppressed because it is too large Load Diff
+20 -19
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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";
+16 -8
View File
@@ -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();
} }
+41 -25
View File
@@ -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> &nbsp;<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> &nbsp;<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> &nbsp;<FormattedMessage id="link_privacy_policy"
defaultMessage="Privacy Policy" description="Ancor text for privacy policy link" />
</a>
</div>
</div>
</div> </div>
}</React.Fragment> }</React.Fragment>
); );
+8 -6
View File
@@ -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>
+2
View File
@@ -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 />
+5 -9
View File
@@ -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}
+3 -3
View File
@@ -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>
+2 -2
View File
@@ -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 ?
+30 -12
View File
@@ -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,16 +531,24 @@ 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
// Skip update if the topic is currently open, otherwise the badge will annoyingly flash. if (document.hidden) {
if (this.state.topicSelected != cont.topic) { if (this.state.messageSounds && !archived) {
if (this.state.messageSounds) {
POP_SOUND.play(); POP_SOUND.play();
} }
this.resetContactList(); this.resetContactList();
} else if (document.hidden && this.state.messageSounds) {
// Skip update if the topic is currently open, otherwise the badge will annoyingly flash.
} else if (this.state.topicSelected != cont.topic) {
if (this.state.messageSounds && !archived) {
POP_SOUND.play(); POP_SOUND.play();
} }
this.resetContactList();
}
} 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.
} else if (what == 'gone' || what == 'unsub') { } else if (what == 'gone' || what == 'unsub') {
@@ -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) {
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.setState({credMethod: cred, credCode: code});
this.doLogin(null, null, {meth: cred, resp: 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
+12
View File
@@ -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>
); );
+5 -4
View File
@@ -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 (
+4 -12
View File
@@ -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 -4
View File
@@ -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
View File
@@ -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) {
// Chip added: clear input.
state.input ='';
} }
return state; 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: ''});
}
} }
// Map chip index to user name // Map chip index to user name
+3 -2
View File
@@ -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} >&times;</a> <a href="#" onClick={this.handleCancel} >&times;</a>
: <span className="spacer" />} : <span className="spacer" />}
</div> </div>
); );
+1 -1
View File
@@ -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;
} }
+2 -2
View File
@@ -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}
+9 -7
View File
@@ -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>
); );
+1 -1
View File
@@ -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>
+16 -9
View File
@@ -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) {
if (node && !this.state.width) {
const bounds = node.getBoundingClientRect();
this.setState({ this.setState({
width: this.container.clientWidth, width: bounds.width | 0,
height: this.container.clientHeight 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">
+7 -7
View File
@@ -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) {
+1 -5
View File
@@ -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>
); );
} }
} }
+2 -2
View File
@@ -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>
&nbsp; &nbsp;
<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>
); );
} }
+2 -2
View File
@@ -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>
&nbsp; &nbsp;
<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>
); );
} }
+3 -2
View File
@@ -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>
+6 -5
View File
@@ -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;}}
+4 -9
View File
@@ -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> &nbsp;<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>
+3031 -313
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+3 -2
View File
@@ -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',
], ],