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
+2 -2
View File
@@ -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"]
}
}]
}]
+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.
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
View File
@@ -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);
}
+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"?>
<!-- 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

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

+14 -10
View File
@@ -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
View File
@@ -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>
+2037 -762
View File
File diff suppressed because it is too large Load Diff
+20 -19
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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";
+16 -8
View File
@@ -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();
}
+41 -25
View File
@@ -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> &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>
}</React.Fragment>
);
+8 -6
View File
@@ -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>
+2
View File
@@ -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 />
+5 -9
View File
@@ -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}
+3 -3
View File
@@ -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>
+2 -2
View File
@@ -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
View File
@@ -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
+12
View File
@@ -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>
);
+5 -4
View File
@@ -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 (
+4 -12
View File
@@ -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 -4
View File
@@ -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
View File
@@ -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
+3 -2
View File
@@ -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} >&times;</a>
<a href="#" onClick={this.handleCancel} >&times;</a>
: <span className="spacer" />}
</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>);
}
});
return <React.Fragment>{badges}</React.Fragment>;
return <>{badges}</>;
}
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} />
</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}
+9 -7
View File
@@ -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>
);
+1 -1
View File
@@ -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>
+18 -11
View File
@@ -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">
+7 -7
View File
@@ -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) {
+1 -5
View File
@@ -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>
);
}
}
+2 -2
View File
@@ -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>
&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>
);
}
+2 -2
View File
@@ -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>
&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>
);
}
+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);
}
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>
+6 -5
View File
@@ -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;}}
+4 -9
View File
@@ -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> &nbsp;<FormattedMessage id="title_manage_tags" defaultMessage="Manage"
description="Section title for the list of tags" />
</a>
<span>{tags}</span>
<>{tags}</>
</div>
}
</div>
+3042 -324
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 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',
],