merge devel
@@ -72,11 +72,11 @@ If you want to use the app with your own server and want web push notification t
|
|||||||
### Desktop screenshot
|
### Desktop screenshot
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="web-desktop-2.png" alt="Desktop web: full app" width=810 />
|
<img src="web-desktop.jpg" alt="Desktop web: full app" width=810 />
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
### Mobile screenshots
|
### Mobile screenshots
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="web-mob-contacts-1.png" alt="Mobile web: contacts" width=250 /> <img src="web-mob-chat-1.png" alt="Mobile web: chat" width=250 /> <img src="web-mob-info-1.png" alt="Mobile web: topic info" width=250 /> <img src="web-mob-new-chat-1.png" alt="Mobile web: start new chat" width=250 /> <img src="web-mob-acc-settings.png" alt="Mobile web: account settings" width=250 />
|
<kbd><img src="web-mob-contacts.png" alt="Mobile web: contacts" width=250 /></kbd> <kbd><img src="web-mob-chat.png" alt="Mobile web: chat" width=250 /></kbd> <kbd><img src="web-mob-info.png" alt="Mobile web: topic info" width=250 /></kbd> <kbd><img src="web-mob-new-chat.png" alt="Mobile web: start new chat" width=250 /></kbd> <kbd><img src="web-mob-acc-settings.png" alt="Mobile web: account settings" width=250 /></kbd>
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -32,8 +32,11 @@ a:focus {
|
|||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
a.red:hover,
|
a.danger {
|
||||||
a.red:focus {
|
color: #D32F2F;
|
||||||
|
}
|
||||||
|
a.danger:hover,
|
||||||
|
a.danger:focus {
|
||||||
color: #F44336;
|
color: #F44336;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,14 +63,16 @@ a.gray:focus {
|
|||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.flat-button .faux {
|
.flat-button.faux {
|
||||||
height: 1.5em;
|
display: inline-block;
|
||||||
line-height: 1.5em;
|
height: 1.4em;
|
||||||
|
line-height: 1.4em;
|
||||||
text-transform: none;
|
text-transform: none;
|
||||||
padding-left: .5rem;
|
padding-left: .35rem;
|
||||||
padding-right: .5rem;
|
padding-right: .35rem;
|
||||||
border: 1px solid gray;
|
border: 1px solid #999;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
|
background-color: #eee;
|
||||||
}
|
}
|
||||||
|
|
||||||
form {
|
form {
|
||||||
@@ -110,7 +115,7 @@ button {
|
|||||||
padding-left: 1.2rem;
|
padding-left: 1.2rem;
|
||||||
padding-right: 1.2rem;
|
padding-right: 1.2rem;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
box-shadow: 0 2px 3px 0 rgba(0, 0, 0, 0.25);
|
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.25);
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
min-width: 5rem;
|
min-width: 5rem;
|
||||||
margin: 0 0.35rem 0 0.35rem;
|
margin: 0 0.35rem 0 0.35rem;
|
||||||
@@ -141,7 +146,6 @@ button.round.small {
|
|||||||
line-height: 2rem;
|
line-height: 2rem;
|
||||||
box-shadow: 0 2px 0.35rem 0 rgba(0, 0, 0, 0.25);
|
box-shadow: 0 2px 0.35rem 0 rgba(0, 0, 0, 0.25);
|
||||||
}
|
}
|
||||||
|
|
||||||
button:focus, button:hover, button:active {
|
button:focus, button:hover, button:active {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
@@ -152,27 +156,19 @@ button:active.round.small {
|
|||||||
box-shadow: inset 0 2px 0.35rem rgba(0, 0, 0, 0.125);
|
box-shadow: inset 0 2px 0.35rem rgba(0, 0, 0, 0.125);
|
||||||
}
|
}
|
||||||
|
|
||||||
button.blue {
|
button.primary {
|
||||||
background-color: #1e88e5; /* blue 600 */
|
background-color: #1e88e5; /* blue 600 */
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
button.blue:focus, button.blue:hover, button.blue:active {
|
button.primary:focus, button.primary:hover, button.primary:active {
|
||||||
background-color: #1976d2;
|
|
||||||
}
|
|
||||||
|
|
||||||
button.blue {
|
|
||||||
background-color: #1e88e5; /* blue 600 */
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
button.blue:focus, button.blue:hover, button.blue:active {
|
|
||||||
background-color: #2962ff;
|
background-color: #2962ff;
|
||||||
}
|
}
|
||||||
|
|
||||||
button.white {
|
button.secondary {
|
||||||
background-color: #FFF;
|
background-color: #FFF;
|
||||||
color: #2196f3; /* blue 500 */
|
color: #2196f3; /* blue 500 */
|
||||||
}
|
}
|
||||||
button.white:focus, button.white:hover, button.blue:active {
|
button.secondary:focus, button.secondary:hover, button.secondary:active {
|
||||||
background-color: #e3f2fd;
|
background-color: #e3f2fd;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -258,16 +254,16 @@ textarea {
|
|||||||
|
|
||||||
/* Chrome & other webkit browsers */
|
/* Chrome & other webkit browsers */
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
width: 0.6rem;
|
width: 0.6rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-track {
|
::-webkit-scrollbar-track {
|
||||||
-webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.2);
|
-webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.2);
|
||||||
background-color: rgba(255,255,255,0.1);
|
background-color: rgba(255,255,255,0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb {
|
::-webkit-scrollbar-thumb {
|
||||||
background-color: rgba(0,0,0,0.3);
|
background-color: rgba(0,0,0,0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Firefox only */
|
/* Firefox only */
|
||||||
@@ -331,11 +327,9 @@ li:hover .menuTrigger a {
|
|||||||
}
|
}
|
||||||
|
|
||||||
li.left:hover .menuTrigger {
|
li.left:hover .menuTrigger {
|
||||||
-text-shadow: #c5e1a5 0 0 15px, #c5e1a5 0 0 5px;
|
|
||||||
background-image: radial-gradient(#c5e1a5 40%, transparent 75%);
|
background-image: radial-gradient(#c5e1a5 40%, transparent 75%);
|
||||||
}
|
}
|
||||||
li.right:hover .menuTrigger {
|
li.right:hover .menuTrigger {
|
||||||
-text-shadow: #fafafa 0 0 15px, #fafafa 0 0 5px;
|
|
||||||
background-image: radial-gradient(#fafafa 40%, transparent 75%);
|
background-image: radial-gradient(#fafafa 40%, transparent 75%);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -343,13 +337,12 @@ li.right:hover .menuTrigger {
|
|||||||
|
|
||||||
/* Begin Alert: modal window with message and buttons. */
|
/* Begin Alert: modal window with message and buttons. */
|
||||||
div.alert-container {
|
div.alert-container {
|
||||||
|
position: absolute;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
position: absolute;
|
|
||||||
-x-backdrop-filter: blur(4px);
|
|
||||||
background: rgba(0,0,0,0.33);
|
background: rgba(0,0,0,0.33);
|
||||||
z-index: 3;
|
z-index: 3;
|
||||||
}
|
}
|
||||||
@@ -491,8 +484,8 @@ div.alert .content {
|
|||||||
|
|
||||||
#topic-view {
|
#topic-view {
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow-x: hidden;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
|
overflow-x: hidden;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@@ -551,7 +544,7 @@ div.alert .content {
|
|||||||
margin-left: 1rem;
|
margin-left: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Extra spece on the right */
|
/* Extra space on the right */
|
||||||
.space-right {
|
.space-right {
|
||||||
margin-right: 0.25rem;
|
margin-right: 0.25rem;
|
||||||
}
|
}
|
||||||
@@ -837,8 +830,8 @@ i.search {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.contact-box > li {
|
.contact-box > li {
|
||||||
display: flex;
|
|
||||||
position: relative;
|
position: relative;
|
||||||
|
display: flex;
|
||||||
padding: 0.35rem 0.25rem 0.5rem 0.35rem;
|
padding: 0.35rem 0.25rem 0.5rem 0.35rem;
|
||||||
min-height: 3.75rem;
|
min-height: 3.75rem;
|
||||||
flex-grow: 0;
|
flex-grow: 0;
|
||||||
@@ -879,10 +872,18 @@ i.search {
|
|||||||
font-weight: lighter;
|
font-weight: lighter;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.contact-box .contact-comment b {
|
||||||
|
font-weight: bolder;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-box .contact-comment .material-icons {
|
||||||
|
margin-bottom: .2rem;
|
||||||
|
}
|
||||||
|
|
||||||
/* Avatar and online-offline indicator */
|
/* Avatar and online-offline indicator */
|
||||||
.contact-box .avatar-box {
|
.contact-box .avatar-box {
|
||||||
margin-right: 0.35rem;
|
|
||||||
position: relative;
|
position: relative;
|
||||||
|
margin-right: 0.35rem;
|
||||||
width: 3rem;
|
width: 3rem;
|
||||||
min-width: 3rem;
|
min-width: 3rem;
|
||||||
height: 3rem;
|
height: 3rem;
|
||||||
@@ -926,14 +927,13 @@ i.search {
|
|||||||
|
|
||||||
/* Online-offline indicator positioned inside avatar-box */
|
/* Online-offline indicator positioned inside avatar-box */
|
||||||
.avatar-box .online, .offline {
|
.avatar-box .online, .offline {
|
||||||
display: inline-block;
|
position: absolute;
|
||||||
width: 0.75rem;
|
width: 0.75rem;
|
||||||
min-width: 0.75rem;
|
min-width: 0.75rem;
|
||||||
height: 0.75rem;
|
height: 0.75rem;
|
||||||
min-height: 0.75rem;
|
min-height: 0.75rem;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
position: absolute;
|
|
||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0.1rem;
|
bottom: 0.1rem;
|
||||||
}
|
}
|
||||||
@@ -958,7 +958,6 @@ i.search {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.contact-box .checkmark.material-icons {
|
.contact-box .checkmark.material-icons {
|
||||||
display: inline-block;
|
|
||||||
width: 1.5rem;
|
width: 1.5rem;
|
||||||
height: 1.5rem;
|
height: 1.5rem;
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
@@ -1018,8 +1017,8 @@ i.search {
|
|||||||
|
|
||||||
/* Styling of avatar upload control */
|
/* Styling of avatar upload control */
|
||||||
.avatar-upload {
|
.avatar-upload {
|
||||||
display: flex;
|
|
||||||
position: relative;
|
position: relative;
|
||||||
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
width: 7rem;
|
width: 7rem;
|
||||||
@@ -1115,7 +1114,7 @@ i.search {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.avatar-upload .inputfile:focus + label.round {
|
.avatar-upload .inputfile:focus + label.round {
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.avatar-upload .inputfile:focus + label,
|
.avatar-upload .inputfile:focus + label,
|
||||||
@@ -1413,12 +1412,12 @@ i.search {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.chat-box .bubble {
|
.chat-box .bubble {
|
||||||
display: flex;
|
|
||||||
position: relative;
|
position: relative;
|
||||||
max-width: 40rem;
|
display: flex;
|
||||||
|
max-width: 40rem;
|
||||||
min-width: 9rem;
|
min-width: 9rem;
|
||||||
height: auto;
|
height: auto;
|
||||||
padding: 0.75rem 0.5rem 0.75rem 1.25rem;
|
padding: 0.75rem 0.5rem 0.75rem 0.5rem;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
@@ -1447,10 +1446,12 @@ i.search {
|
|||||||
|
|
||||||
.chat-box .left .bubble {
|
.chat-box .left .bubble {
|
||||||
background-color: #c5e1a5;
|
background-color: #c5e1a5;
|
||||||
|
margin-right: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-box .right .bubble {
|
.chat-box .right .bubble {
|
||||||
background-color: #fafafa;
|
background-color: #fafafa;
|
||||||
|
margin-left: 1.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-box .center .bubble {
|
.chat-box .center .bubble {
|
||||||
@@ -1505,12 +1506,21 @@ i.search {
|
|||||||
margin-right: 0.8rem;
|
margin-right: 0.8rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Message text + timestamp and delivery marker */
|
||||||
|
.chat-box .bubble div.content-meta {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.chat-box .bubble div.message-content {
|
.chat-box .bubble div.message-content {
|
||||||
|
position: relative;
|
||||||
color: #212121;
|
color: #212121;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
width: 100%;
|
||||||
max-width: 36rem;
|
max-width: 36rem;
|
||||||
overflow-wrap: break-word;
|
overflow-wrap: break-word;
|
||||||
|
padding: 0 0.75rem 0 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* The timestamp is a floating element. Need to ensure div.message-content is expanded
|
/* The timestamp is a floating element. Need to ensure div.message-content is expanded
|
||||||
* to accomodate it. It's usually called clearfix. */
|
* to accomodate it. It's usually called clearfix. */
|
||||||
.chat-box .bubble div.message-content:after {
|
.chat-box .bubble div.message-content:after {
|
||||||
@@ -1521,29 +1531,21 @@ i.search {
|
|||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-box .timestamp {
|
.chat-box .timestamp-padding {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
width: 4rem;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-box .timestamp {
|
||||||
position: relative;
|
position: relative;
|
||||||
font-size: 80%;
|
font-size: 80%;
|
||||||
color: #777;
|
color: #777;
|
||||||
padding-left: 1rem;
|
top: 0.35rem;
|
||||||
top: 0.5rem;
|
|
||||||
float: right;
|
float: right;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Typing notification bubble */
|
|
||||||
.chat-box .typing {
|
|
||||||
display: inline-block;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
width: 2rem;
|
|
||||||
padding: 0.25rem 0.35rem;
|
|
||||||
vertical-align: middle;
|
|
||||||
text-align: center;
|
|
||||||
color: #aaa;
|
|
||||||
margin-top: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* The name of the person who sent the message
|
/* The name of the person who sent the message
|
||||||
* (group chat only)
|
* (group chat only)
|
||||||
*/
|
*/
|
||||||
@@ -1557,7 +1559,6 @@ i.search {
|
|||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border-radius: 0.1rem;
|
border-radius: 0.1rem;
|
||||||
display: block;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.inline-image {
|
.inline-image {
|
||||||
@@ -1577,11 +1578,11 @@ i.search {
|
|||||||
|
|
||||||
/* Broken image graphics which replaces the stock graphics. */
|
/* Broken image graphics which replaces the stock graphics. */
|
||||||
img.inline-image::before, img.image-preview::before {
|
img.inline-image::before, img.image-preview::before {
|
||||||
|
position: absolute;
|
||||||
|
display: flex;
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
font-weight: lighter;
|
font-weight: lighter;
|
||||||
color: #666;
|
color: #666;
|
||||||
display:flex;
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -1672,7 +1673,6 @@ ul.tabbar > li > a {
|
|||||||
padding-top: 0.75rem;
|
padding-top: 0.75rem;
|
||||||
padding-left: 1rem;
|
padding-left: 1rem;
|
||||||
padding-right: 1rem;
|
padding-right: 1rem;
|
||||||
display: block;
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
line-height: 2rem;
|
line-height: 2rem;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
@@ -1689,11 +1689,10 @@ ul.tabbar li.active > a {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ul.tabbar li.active > a:after {
|
ul.tabbar li.active > a:after {
|
||||||
height:0.15rem;
|
height: 0.15rem;
|
||||||
display:block;
|
content: "";
|
||||||
content:"";
|
bottom: 0;
|
||||||
bottom:0;
|
left: 0;
|
||||||
left:0;
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
background: #2196f3;
|
background: #2196f3;
|
||||||
animation: tabbar-border-expand 0.2s cubic-bezier(0.4, 0.0, 0.4, 1) 0s alternate forwards;
|
animation: tabbar-border-expand 0.2s cubic-bezier(0.4, 0.0, 0.4, 1) 0s alternate forwards;
|
||||||
@@ -1701,12 +1700,12 @@ ul.tabbar li.active > a:after {
|
|||||||
|
|
||||||
@keyframes tabbar-border-expand {
|
@keyframes tabbar-border-expand {
|
||||||
0% {
|
0% {
|
||||||
opacity:0;
|
opacity: 0;
|
||||||
width:0;
|
width: 0;
|
||||||
}
|
}
|
||||||
100% {
|
100% {
|
||||||
opacity:1;
|
opacity: 1;
|
||||||
width:100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/* END Tabs */
|
/* END Tabs */
|
||||||
@@ -1788,10 +1787,10 @@ ul.tabbar li.active > a:after {
|
|||||||
|
|
||||||
/* Div with letter tile content */
|
/* Div with letter tile content */
|
||||||
.lettertile > div {
|
.lettertile > div {
|
||||||
|
position: absolute;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
flex-grow: 0;
|
flex-grow: 0;
|
||||||
position: absolute;
|
|
||||||
top: 0; bottom: 0; left: 0; right: 0;
|
top: 0; bottom: 0; left: 0; right: 0;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -34,8 +34,8 @@
|
|||||||
<!-- React-Intl for translations -->
|
<!-- React-Intl for translations -->
|
||||||
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/react-intl@5/react-intl.umd.js"></script>
|
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/react-intl@5/react-intl.umd.js"></script>
|
||||||
<!-- Firebase for push notifications -->
|
<!-- Firebase for push notifications -->
|
||||||
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/firebase@7/firebase-app.js"></script>
|
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/firebase@8/firebase-app.js"></script>
|
||||||
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/firebase@7/firebase-messaging.js"></script>
|
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/firebase@8/firebase-messaging.js"></script>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -24,9 +24,9 @@
|
|||||||
<link rel="stylesheet" href="css/base.min.css" />
|
<link rel="stylesheet" href="css/base.min.css" />
|
||||||
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/react@17.0.1/umd/react.production.min.js"></script>
|
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/react@17.0.1/umd/react.production.min.js"></script>
|
||||||
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/react-dom@17.0.1/umd/react-dom.production.min.js"></script>
|
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/react-dom@17.0.1/umd/react-dom.production.min.js"></script>
|
||||||
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/react-intl@5.10.14/react-intl.umd.min.js"></script>
|
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/react-intl@5.12.5/react-intl.umd.min.js"></script>
|
||||||
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/firebase@7.24.0/firebase-app.js"></script>
|
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/firebase@8.2.9/firebase-app.js"></script>
|
||||||
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/firebase@7.24.0/firebase-messaging.js"></script>
|
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/firebase@8.2.9/firebase-messaging.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<noscript><div id="noscript-message"><div id="noscript-title">Javascript support is required.</div>
|
<noscript><div id="noscript-message"><div id="noscript-title">Javascript support is required.</div>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "tinode-webapp",
|
"name": "tinode-webapp",
|
||||||
"description": "Tinode messenger for the web",
|
"description": "Tinode messenger for the web",
|
||||||
"version": "0.17.0",
|
"version": "0.17.1-alpha1",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/tinode/example-react-js.git"
|
"url": "https://github.com/tinode/example-react-js.git"
|
||||||
@@ -13,17 +13,11 @@
|
|||||||
"build:prod": "webpack --mode production",
|
"build:prod": "webpack --mode production",
|
||||||
"build:i18n": "node scripts/pack-messages.js",
|
"build:i18n": "node scripts/pack-messages.js",
|
||||||
"vers": "node scripts/gen-version.js",
|
"vers": "node scripts/gen-version.js",
|
||||||
"extract": "formatjs extract 'src/**/*.jsx' --out-file src/i18n/ex/base-en.json --id-interpolation-pattern '[sha512:contenthash:base64:6]'",
|
"extract": "formatjs extract src/**/*.jsx --out-file src/i18n/ex/base-en.json --id-interpolation-pattern '[sha512:contenthash:base64:6]'",
|
||||||
"compile": "formatjs compile"
|
"compile": "formatjs compile"
|
||||||
},
|
},
|
||||||
"browserslist": "> 0.5%, not IE 11",
|
"browserslist": "> 0.5%, not IE 11",
|
||||||
"files": [
|
"files": [
|
||||||
"src/index.js",
|
|
||||||
"src/config.js",
|
|
||||||
"src/views/*.jsx",
|
|
||||||
"src/widgets/*.jsx",
|
|
||||||
"src/lib/*.js",
|
|
||||||
"src/i18n/*.json",
|
|
||||||
"umd/index.prod.js",
|
"umd/index.prod.js",
|
||||||
"umd/index.prod.js.map",
|
"umd/index.prod.js.map",
|
||||||
"umd/index.dev.js",
|
"umd/index.dev.js",
|
||||||
@@ -40,7 +34,8 @@
|
|||||||
"index.html",
|
"index.html",
|
||||||
"manifest.json",
|
"manifest.json",
|
||||||
"service-worker.js",
|
"service-worker.js",
|
||||||
"firebase-init.js"
|
"firebase-init.js",
|
||||||
|
"version.js"
|
||||||
],
|
],
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"chat",
|
"chat",
|
||||||
@@ -68,25 +63,27 @@
|
|||||||
"author": "Tinode Authors <info@tinode.co>",
|
"author": "Tinode Authors <info@tinode.co>",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"firebase": "^7.24.0",
|
"firebase": "^8.2.9",
|
||||||
"react": "^17.0.1",
|
"react": "^17.0.1",
|
||||||
"react-dom": "^17.0.1",
|
"react-dom": "^17.0.1",
|
||||||
"react-intl": "^5.10.14",
|
"react-intl": "^5.12.5",
|
||||||
"tinode-sdk": "^0.16.10"
|
"tinode-sdk": "^0.17.1-alpha1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.12.10",
|
"@babel/core": "^7.13.8",
|
||||||
"@babel/preset-env": "^7.12.11",
|
"@babel/preset-env": "^7.13.8",
|
||||||
"@babel/preset-react": "^7.12.10",
|
"@babel/preset-react": "^7.12.13",
|
||||||
"@formatjs/cli": "^3.0.5",
|
"@formatjs/cli": "^4.2.6",
|
||||||
"babel-loader": "^8.2.2",
|
"babel-loader": "^8.2.2",
|
||||||
"babel-plugin-formatjs": "^9.0.4",
|
"babel-plugin-formatjs": "^10.2.6",
|
||||||
"browserslist": "^4.16.1",
|
"browserslist": "^4.16.3",
|
||||||
"copy-webpack-plugin": "^6.4.1",
|
"copy-webpack-plugin": "^8.1.0",
|
||||||
"cssnano": "^4.1.10",
|
"cssnano": "^5.0.1",
|
||||||
"postcss": "^8.2.4",
|
"glob": "^7.1.6",
|
||||||
|
"mkdirp": "^1.0.4",
|
||||||
|
"postcss": "^8.2.6",
|
||||||
"postcss-cli": "^8.3.1",
|
"postcss-cli": "^8.3.1",
|
||||||
"webpack": "^5.13.0",
|
"webpack": "^5.24.2",
|
||||||
"webpack-cli": "^4.3.1"
|
"webpack-cli": "^4.5.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,18 @@
|
|||||||
var fs = require('fs');
|
const fs = require('fs');
|
||||||
var version = require('../package.json').version;
|
const version = require('../package.json').version;
|
||||||
|
|
||||||
fs.writeFile('./src/version.js',
|
function writeVersion(fileName, useExport) {
|
||||||
'// This is a generated file. Don\'t edit.\n\n'+
|
fs.writeFile(fileName,
|
||||||
'export const PACKAGE_VERSION = "'+version+'";\n',
|
'// This is a generated file. Don\'t edit.\n\n'+
|
||||||
{ flag: 'w' },
|
`${useExport ? 'export ' : ''}const PACKAGE_VERSION = "${version}";\n`,
|
||||||
function(err) {
|
{ flag: 'w' },
|
||||||
if (err) {
|
(err) => {
|
||||||
return console.log(err);
|
if (err) {
|
||||||
|
return console.log(err);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
);
|
||||||
);
|
}
|
||||||
|
|
||||||
|
writeVersion('./src/version.js', true);
|
||||||
|
writeVersion('./version.js', false);
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
// Must be located at the root.
|
// Must be located at the root.
|
||||||
importScripts('https://cdn.jsdelivr.net/npm/firebase@7.24.0/firebase-app.js');
|
importScripts('https://cdn.jsdelivr.net/npm/firebase@8.2.9/firebase-app.js');
|
||||||
importScripts('https://cdn.jsdelivr.net/npm/firebase@7.24.0/firebase-messaging.js');
|
importScripts('https://cdn.jsdelivr.net/npm/firebase@8.2.9/firebase-messaging.js');
|
||||||
importScripts('firebase-init.js');
|
importScripts('firebase-init.js');
|
||||||
|
importScripts('version.js');
|
||||||
|
|
||||||
firebase.initializeApp(FIREBASE_INIT);
|
// Channel to notify the webapp.
|
||||||
|
const webAppChannel = new BroadcastChannel('tinode-sw');
|
||||||
|
|
||||||
// Basic internationalization.
|
// Basic internationalization.
|
||||||
const i18n = {
|
const i18n = {
|
||||||
@@ -44,12 +46,18 @@ self.i18nMessage = function(id) {
|
|||||||
return lang[id] || i18n['en'][id] || id;
|
return lang[id] || i18n['en'][id] || id;
|
||||||
}
|
}
|
||||||
|
|
||||||
// This method shows the push notifications.
|
firebase.initializeApp(FIREBASE_INIT);
|
||||||
firebase.messaging().setBackgroundMessageHandler((payload) => {
|
const fbMessaging = firebase.messaging();
|
||||||
|
|
||||||
|
// This method shows the push notifications while the window is in background.
|
||||||
|
fbMessaging.onBackgroundMessage((payload) => {
|
||||||
if (payload.data.silent == 'true') {
|
if (payload.data.silent == 'true') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Notify webapp that a message was received.
|
||||||
|
webAppChannel.postMessage(payload.data);
|
||||||
|
|
||||||
const pushType = payload.data.what || 'msg';
|
const pushType = payload.data.what || 'msg';
|
||||||
const title = payload.data.title || self.i18nMessage(pushType == 'msg' ? 'new_message' : 'new_chat');
|
const title = payload.data.title || self.i18nMessage(pushType == 'msg' ? 'new_message' : 'new_chat');
|
||||||
const options = {
|
const options = {
|
||||||
@@ -113,23 +121,34 @@ self.addEventListener('notificationclick', event => {
|
|||||||
|
|
||||||
// This is needed for 'Add to Home Screen'.
|
// This is needed for 'Add to Home Screen'.
|
||||||
self.addEventListener('fetch', event => {
|
self.addEventListener('fetch', event => {
|
||||||
event.respondWith(
|
if (event.request.method != 'GET') {
|
||||||
// Try to find response in cache.
|
return;
|
||||||
caches.match(event.request)
|
}
|
||||||
.then((resp) => {
|
|
||||||
// If response is found in cache, return it. Otherwise fetch.
|
event.respondWith((async () => {
|
||||||
return resp || fetch(event.request);
|
// Try to find the response in the cache.
|
||||||
})
|
const cache = await caches.open(PACKAGE_VERSION);
|
||||||
.catch((err) => {
|
const cachedResponse = await cache.match(event.request);
|
||||||
// Something went wrong.
|
if (cachedResponse) {
|
||||||
console.log("Service worker Fetch:", err);
|
return cachedResponse;
|
||||||
})
|
}
|
||||||
);
|
// Not found in cache.
|
||||||
|
const response = await fetch(event.request);
|
||||||
|
if (!response || response.status != 200 || response.type != 'basic') {
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
if (event.request.url && (event.request.url.startsWith('http://') || event.request.url.startsWith('https://'))) {
|
||||||
|
await cache.put(event.request, response.clone());
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
})());
|
||||||
});
|
});
|
||||||
|
|
||||||
// This code get the human language from the webapp for localization of strings.
|
// This code gets the human language from the webapp.
|
||||||
self.addEventListener('message', event => {
|
self.addEventListener('message', event => {
|
||||||
const data = JSON.parse(event.data);
|
const data = JSON.parse(event.data);
|
||||||
|
|
||||||
|
// The locale is used for selecting strings in an appropriate language.
|
||||||
self.locale = data.locale || '';
|
self.locale = data.locale || '';
|
||||||
self.baseLocale = self.locale.toLowerCase().split(/[-_]/)[0];
|
self.baseLocale = self.locale.toLowerCase().split(/[-_]/)[0];
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
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.16');
|
export const APP_NAME = 'TinodeWeb/' + (PACKAGE_VERSION || '0.17');
|
||||||
|
|
||||||
// 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';
|
||||||
@@ -22,11 +22,11 @@ export const RECEIVED_DELAY = 500;
|
|||||||
// Delay before sending a read notification, milliseconds.
|
// Delay before sending a read notification, milliseconds.
|
||||||
export const READ_DELAY = 1000;
|
export const READ_DELAY = 1000;
|
||||||
|
|
||||||
// The shortest allowed tag length. Matches the value on the server.
|
// The default shortest allowed tag length. Matches the value on the server.
|
||||||
export const MIN_TAG_LENGTH = 2;
|
export const MIN_TAG_LENGTH = 2;
|
||||||
// The greatest allowed tag length. Matches the value on the server.
|
// The default greatest allowed tag length. Matches the value on the server.
|
||||||
export const MAX_TAG_LENGTH = 96;
|
export const MAX_TAG_LENGTH = 96;
|
||||||
// The maximum number of tags allowed. Matches the value on the server.
|
// The default maximum number of tags allowed. Matches the value on the server.
|
||||||
export const MAX_TAG_COUNT = 16;
|
export const MAX_TAG_COUNT = 16;
|
||||||
|
|
||||||
// Access mode for P2P subscriptions initiated by the current user.
|
// Access mode for P2P subscriptions initiated by the current user.
|
||||||
@@ -56,17 +56,18 @@ export const BROKEN_IMAGE_SIZE = 32;
|
|||||||
export const MESSAGES_PAGE = 24;
|
export const MESSAGES_PAGE = 24;
|
||||||
|
|
||||||
// Default maximum in-band (included directly into the message) attachment size which fits into
|
// Default maximum in-band (included directly into the message) attachment size which fits into
|
||||||
// a message of 256K in size. The actual binary size of the attachment should be smaller due to
|
// a message of 256K in size. Used when the server-provided value is unavailable. The actual
|
||||||
// base64 encoding expansion and some overhead, for instance 1024 bytes.
|
// binary size of the attachment should be smaller due to base64 encoding expansion and some overhead,
|
||||||
|
// for instance 1024 bytes.
|
||||||
// Increase this limit to a greater value in production, if desired. Also increase
|
// Increase this limit to a greater value in production, if desired. Also increase
|
||||||
// max_message_size in the server config.
|
// max_message_size in the server config.
|
||||||
export const MAX_INBAND_ATTACHMENT_SIZE = 262144;
|
export const MAX_INBAND_ATTACHMENT_SIZE = 262144;
|
||||||
|
|
||||||
// Absolute maximum attachment size to be used with the server = 8MB. Increase to
|
// Default absolute maximum attachment size to be used with the server = 8MB.
|
||||||
// something like 100MB in production.
|
// Used when the server-provided value is unavailable. Increase to something like 100MB in production.
|
||||||
export const MAX_EXTERN_ATTACHMENT_SIZE = 1 << 23;
|
export const MAX_EXTERN_ATTACHMENT_SIZE = 1 << 23;
|
||||||
|
|
||||||
// Maximum allowed linear dimension of an inline image in pixels. Larer images will be shrunk
|
// Maximum allowed linear dimension (pixels) of an image sent inline. Larger images will be shrunk
|
||||||
// to make the larger dimension fit under this size. You may want to adjust it to 1600 or
|
// to make the larger dimension fit under this size. You may want to adjust it to 1600 or
|
||||||
// 2400 for production.
|
// 2400 for production.
|
||||||
export const MAX_IMAGE_DIM = 1024;
|
export const MAX_IMAGE_DIM = 1024;
|
||||||
@@ -85,7 +86,7 @@ export const MAX_TITLE_LENGTH = 60;
|
|||||||
export const MESSAGE_PREVIEW_LENGTH = 80;
|
export const MESSAGE_PREVIEW_LENGTH = 80;
|
||||||
|
|
||||||
// Link for "Contact Us".
|
// Link for "Contact Us".
|
||||||
export const LINK_CONTACT_US = 'email:info@tinode.co';
|
export const LINK_CONTACT_US = 'email:support@tinode.co';
|
||||||
|
|
||||||
// Link to Privacy Policy.
|
// Link to Privacy Policy.
|
||||||
export const LINK_PRIVACY_POLICY = 'https://tinode.co/privacy.html';
|
export const LINK_PRIVACY_POLICY = 'https://tinode.co/privacy.html';
|
||||||
|
|||||||
@@ -172,5 +172,8 @@
|
|||||||
"tags_editor_no_tags": "Schlagworte hinzufügen",
|
"tags_editor_no_tags": "Schlagworte hinzufügen",
|
||||||
"title_manage_tags": "Verwalten",
|
"title_manage_tags": "Verwalten",
|
||||||
"channel_prompt": "Dies ist ein Sender",
|
"channel_prompt": "Dies ist ein Sender",
|
||||||
"channel": "Sender"
|
"channel": "Sender",
|
||||||
|
"drafty_attachment": "Dateianhang",
|
||||||
|
"drafty_form": "Formular:",
|
||||||
|
"drafty_image": "Bild"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,6 +43,9 @@
|
|||||||
"delete_messages_warning": "Are you sure you want to delete all messages for everyone? It cannot be undone.",
|
"delete_messages_warning": "Are you sure you want to delete all messages for everyone? It cannot be undone.",
|
||||||
"deleted_content": "content deleted",
|
"deleted_content": "content deleted",
|
||||||
"download_action": "download",
|
"download_action": "download",
|
||||||
|
"drafty_attachment": "Attachment",
|
||||||
|
"drafty_form": "Form:",
|
||||||
|
"drafty_image": "Picture",
|
||||||
"email_dative": "email",
|
"email_dative": "email",
|
||||||
"email_prompt": "Email, e.g. jdoe@example.com",
|
"email_prompt": "Email, e.g. jdoe@example.com",
|
||||||
"enable_peers_messaging": "Enable",
|
"enable_peers_messaging": "Enable",
|
||||||
|
|||||||
@@ -48,7 +48,7 @@
|
|||||||
"private_editing_placeholder": "Sólo visible para tí",
|
"private_editing_placeholder": "Sólo visible para tí",
|
||||||
"label_muting_topic": "Silenciado:",
|
"label_muting_topic": "Silenciado:",
|
||||||
"action_more": "Más",
|
"action_more": "Más",
|
||||||
"label_your_permissions": "Tus permisoss:",
|
"label_your_permissions": "Tus permisos:",
|
||||||
"label_permissions": "Permisos:",
|
"label_permissions": "Permisos:",
|
||||||
"label_you": "Tú:",
|
"label_you": "Tú:",
|
||||||
"label_default_access": "Modo de acceso predeterminado:",
|
"label_default_access": "Modo de acceso predeterminado:",
|
||||||
@@ -105,7 +105,7 @@
|
|||||||
"code_doesnot_match": "El código no coincide",
|
"code_doesnot_match": "El código no coincide",
|
||||||
"phone_dative": "teléfono",
|
"phone_dative": "teléfono",
|
||||||
"email_dative": "correo electrónico",
|
"email_dative": "correo electrónico",
|
||||||
"enter_confirmation_code_prompt": "Intruduzca el código de confirmación enviado a tu {method}:",
|
"enter_confirmation_code_prompt": "Introduzca el código de confirmación enviado a tu {method}:",
|
||||||
"numeric_confirmation_code_prompt": "Sólo números",
|
"numeric_confirmation_code_prompt": "Sólo números",
|
||||||
"button_confirm": "Confirmar",
|
"button_confirm": "Confirmar",
|
||||||
"save_attachment": "guardar",
|
"save_attachment": "guardar",
|
||||||
@@ -172,5 +172,8 @@
|
|||||||
"tags_editor_no_tags": "Añadir etiquetas",
|
"tags_editor_no_tags": "Añadir etiquetas",
|
||||||
"title_manage_tags": "Gestionar",
|
"title_manage_tags": "Gestionar",
|
||||||
"channel_prompt": "Este es un canal",
|
"channel_prompt": "Este es un canal",
|
||||||
"channel": "canal"
|
"channel": "canal",
|
||||||
|
"drafty_attachment": "Archivo",
|
||||||
|
"drafty_form": "Formulario:",
|
||||||
|
"drafty_image": "Imagen"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -175,6 +175,18 @@
|
|||||||
"defaultMessage": "download",
|
"defaultMessage": "download",
|
||||||
"description": "Call to action [download]"
|
"description": "Call to action [download]"
|
||||||
},
|
},
|
||||||
|
"drafty_attachment": {
|
||||||
|
"defaultMessage": "Attachment",
|
||||||
|
"description": "Comment for attachment in drafty preview"
|
||||||
|
},
|
||||||
|
"drafty_form": {
|
||||||
|
"defaultMessage": "Form:",
|
||||||
|
"description": "Comment for form in drafty preview"
|
||||||
|
},
|
||||||
|
"drafty_image": {
|
||||||
|
"defaultMessage": "Picture",
|
||||||
|
"description": "Comment for embedded images in drafty preview"
|
||||||
|
},
|
||||||
"email_dative": {
|
"email_dative": {
|
||||||
"defaultMessage": "email",
|
"defaultMessage": "email",
|
||||||
"description": "Dative case of 'email', i.e. 'email' in 'by email'"
|
"description": "Dative case of 'email', i.e. 'email' in 'by email'"
|
||||||
|
|||||||
@@ -175,5 +175,8 @@
|
|||||||
"messages_not_readable": "메시지에 액세스 할 수 없습니다",
|
"messages_not_readable": "메시지에 액세스 할 수 없습니다",
|
||||||
"deleted_content": "내용이 삭제되었습니다",
|
"deleted_content": "내용이 삭제되었습니다",
|
||||||
"channel_prompt": "이 채널입니다",
|
"channel_prompt": "이 채널입니다",
|
||||||
"channel": "채널"
|
"channel": "채널",
|
||||||
|
"drafty_attachment": "부착",
|
||||||
|
"drafty_form": "형태:",
|
||||||
|
"drafty_image": "이미지"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -172,5 +172,8 @@
|
|||||||
"cannot_initiate_file_upload": "Nu se poate face încărcarea.",
|
"cannot_initiate_file_upload": "Nu se poate face încărcarea.",
|
||||||
"tags_not_found": "Niciun tag definit. Adaugă.",
|
"tags_not_found": "Niciun tag definit. Adaugă.",
|
||||||
"tags_editor_no_tags": "Adaugă tag-uri",
|
"tags_editor_no_tags": "Adaugă tag-uri",
|
||||||
"title_manage_tags": "Administrează"
|
"title_manage_tags": "Administrează",
|
||||||
|
"drafty_attachment": "Atasament",
|
||||||
|
"drafty_form": "Formă:",
|
||||||
|
"drafty_image": "Imagine"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -172,5 +172,8 @@
|
|||||||
"no_connection": "Нет связи",
|
"no_connection": "Нет связи",
|
||||||
"code_doesnot_match": "Код не совпадает",
|
"code_doesnot_match": "Код не совпадает",
|
||||||
"channel_prompt": "Создать канал",
|
"channel_prompt": "Создать канал",
|
||||||
"channel": "канал"
|
"channel": "канал",
|
||||||
|
"drafty_attachment": "Аттачмент",
|
||||||
|
"drafty_form": "Форма:",
|
||||||
|
"drafty_image": "Картинка"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -172,5 +172,8 @@
|
|||||||
"no_connection": "无连接",
|
"no_connection": "无连接",
|
||||||
"code_doesnot_match": "代码不匹配",
|
"code_doesnot_match": "代码不匹配",
|
||||||
"channel_prompt": "这是一个频道",
|
"channel_prompt": "这是一个频道",
|
||||||
"channel": "频道"
|
"channel": "频道",
|
||||||
|
"drafty_attachment": "附件",
|
||||||
|
"drafty_form": "形式:",
|
||||||
|
"drafty_image": "图像"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,10 +6,15 @@ import { bytesToHumanSize } from './strformat.js'
|
|||||||
export const SUPPORTED_IMAGE_FORMATS = ['image/jpeg', 'image/gif', 'image/png', 'image/svg', 'image/svg+xml'];
|
export const SUPPORTED_IMAGE_FORMATS = ['image/jpeg', 'image/gif', 'image/png', 'image/svg', 'image/svg+xml'];
|
||||||
export const MIME_EXTENSIONS = ['jpg', 'gif', 'png', 'svg', 'svg'];
|
export const MIME_EXTENSIONS = ['jpg', 'gif', 'png', 'svg', 'svg'];
|
||||||
|
|
||||||
// Make a data URL from public.photo
|
// Make a data: URL from public.photo
|
||||||
export function makeImageUrl(photo) {
|
export function makeImageDataUrl(photo) {
|
||||||
return (photo && photo.type && photo.data) ?
|
if (photo) {
|
||||||
'data:image/' + photo.type + ';base64,' + photo.data : null;
|
if (photo.data && photo.type) {
|
||||||
|
return 'data:image/' + photo.type + ';base64,' + photo.data;
|
||||||
|
}
|
||||||
|
return photo.ref;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate linear dimensions for scaling image down to fit under a certain size.
|
// Calculate linear dimensions for scaling image down to fit under a certain size.
|
||||||
|
|||||||
@@ -19,23 +19,35 @@ export function updateFavicon(count) {
|
|||||||
document.title = (count > 0 ? '('+count+') ' : '') + 'Tinode';
|
document.title = (count > 0 ? '('+count+') ' : '') + 'Tinode';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create VCard which represents topic 'public' info
|
// Create theCard which represents user's or topic's "public" info.
|
||||||
export function vcard(fn, imageDataUrl) {
|
export function theCard(fn, imageUrl, imageMimeType) {
|
||||||
let card = null;
|
let card = null;
|
||||||
|
fn = fn && fn.trim();
|
||||||
|
|
||||||
if ((fn && fn.trim()) || imageDataUrl) {
|
if (fn) {
|
||||||
card = {};
|
card = {
|
||||||
if (fn) {
|
fn: fn
|
||||||
card.fn = fn.trim();
|
};
|
||||||
}
|
|
||||||
if (imageDataUrl) {
|
|
||||||
const dataStart = imageDataUrl.indexOf(',');
|
|
||||||
card.photo = dataStart >= 0 ? {
|
|
||||||
data: imageDataUrl.substring(dataStart+1),
|
|
||||||
type: 'jpg'
|
|
||||||
} : imageDataUrl;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (imageUrl) {
|
||||||
|
card = card || {};
|
||||||
|
let mimeType = imageMimeType;
|
||||||
|
// Is this a data URL "data:[<mediatype>][;base64],<data>"?
|
||||||
|
const matches = /^data:(image\/[-a-z0-9+.]+)(;base64)?,/i.exec(imageUrl);
|
||||||
|
if (matches) {
|
||||||
|
mimeType = matches[1];
|
||||||
|
card.photo = {
|
||||||
|
data: imageUrl.substring(imageUrl.indexOf(',') + 1)
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
card.photo = {
|
||||||
|
ref: imageUrl
|
||||||
|
};
|
||||||
|
}
|
||||||
|
card.photo.type = (mimeType || 'image/jpeg').substring('image/'.length);
|
||||||
|
}
|
||||||
|
|
||||||
return card;
|
return card;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -173,7 +173,10 @@
|
|||||||
"tags_editor_no_tags": "Schlagworte hinzufügen",
|
"tags_editor_no_tags": "Schlagworte hinzufügen",
|
||||||
"title_manage_tags": "Verwalten",
|
"title_manage_tags": "Verwalten",
|
||||||
"channel_prompt": "Dies ist ein Sender",
|
"channel_prompt": "Dies ist ein Sender",
|
||||||
"channel": "Sender"
|
"channel": "Sender",
|
||||||
|
"drafty_attachment": "Dateianhang",
|
||||||
|
"drafty_form": "Formular:",
|
||||||
|
"drafty_image": "Bild"
|
||||||
},
|
},
|
||||||
"en": {
|
"en": {
|
||||||
"action_block_contact": "Block Contact",
|
"action_block_contact": "Block Contact",
|
||||||
@@ -220,6 +223,9 @@
|
|||||||
"delete_messages_warning": "Are you sure you want to delete all messages for everyone? It cannot be undone.",
|
"delete_messages_warning": "Are you sure you want to delete all messages for everyone? It cannot be undone.",
|
||||||
"deleted_content": "content deleted",
|
"deleted_content": "content deleted",
|
||||||
"download_action": "download",
|
"download_action": "download",
|
||||||
|
"drafty_attachment": "Attachment",
|
||||||
|
"drafty_form": "Form:",
|
||||||
|
"drafty_image": "Picture",
|
||||||
"email_dative": "email",
|
"email_dative": "email",
|
||||||
"email_prompt": "Email, e.g. jdoe@example.com",
|
"email_prompt": "Email, e.g. jdoe@example.com",
|
||||||
"enable_peers_messaging": "Enable",
|
"enable_peers_messaging": "Enable",
|
||||||
@@ -401,7 +407,7 @@
|
|||||||
"private_editing_placeholder": "Sólo visible para tí",
|
"private_editing_placeholder": "Sólo visible para tí",
|
||||||
"label_muting_topic": "Silenciado:",
|
"label_muting_topic": "Silenciado:",
|
||||||
"action_more": "Más",
|
"action_more": "Más",
|
||||||
"label_your_permissions": "Tus permisoss:",
|
"label_your_permissions": "Tus permisos:",
|
||||||
"label_permissions": "Permisos:",
|
"label_permissions": "Permisos:",
|
||||||
"label_you": "Tú:",
|
"label_you": "Tú:",
|
||||||
"label_default_access": "Modo de acceso predeterminado:",
|
"label_default_access": "Modo de acceso predeterminado:",
|
||||||
@@ -458,7 +464,7 @@
|
|||||||
"code_doesnot_match": "El código no coincide",
|
"code_doesnot_match": "El código no coincide",
|
||||||
"phone_dative": "teléfono",
|
"phone_dative": "teléfono",
|
||||||
"email_dative": "correo electrónico",
|
"email_dative": "correo electrónico",
|
||||||
"enter_confirmation_code_prompt": "Intruduzca el código de confirmación enviado a tu {method}:",
|
"enter_confirmation_code_prompt": "Introduzca el código de confirmación enviado a tu {method}:",
|
||||||
"numeric_confirmation_code_prompt": "Sólo números",
|
"numeric_confirmation_code_prompt": "Sólo números",
|
||||||
"button_confirm": "Confirmar",
|
"button_confirm": "Confirmar",
|
||||||
"save_attachment": "guardar",
|
"save_attachment": "guardar",
|
||||||
@@ -525,7 +531,10 @@
|
|||||||
"tags_editor_no_tags": "Añadir etiquetas",
|
"tags_editor_no_tags": "Añadir etiquetas",
|
||||||
"title_manage_tags": "Gestionar",
|
"title_manage_tags": "Gestionar",
|
||||||
"channel_prompt": "Este es un canal",
|
"channel_prompt": "Este es un canal",
|
||||||
"channel": "canal"
|
"channel": "canal",
|
||||||
|
"drafty_attachment": "Archivo",
|
||||||
|
"drafty_form": "Formulario:",
|
||||||
|
"drafty_image": "Imagen"
|
||||||
},
|
},
|
||||||
"ko": {
|
"ko": {
|
||||||
"validate_credential_action": "확인",
|
"validate_credential_action": "확인",
|
||||||
@@ -701,7 +710,10 @@
|
|||||||
"messages_not_readable": "메시지에 액세스 할 수 없습니다",
|
"messages_not_readable": "메시지에 액세스 할 수 없습니다",
|
||||||
"deleted_content": "내용이 삭제되었습니다",
|
"deleted_content": "내용이 삭제되었습니다",
|
||||||
"channel_prompt": "이 채널입니다",
|
"channel_prompt": "이 채널입니다",
|
||||||
"channel": "채널"
|
"channel": "채널",
|
||||||
|
"drafty_attachment": "부착",
|
||||||
|
"drafty_form": "형태:",
|
||||||
|
"drafty_image": "이미지"
|
||||||
},
|
},
|
||||||
"ro": {
|
"ro": {
|
||||||
"validate_credential_action": "confirmă",
|
"validate_credential_action": "confirmă",
|
||||||
@@ -877,7 +889,10 @@
|
|||||||
"cannot_initiate_file_upload": "Nu se poate face încărcarea.",
|
"cannot_initiate_file_upload": "Nu se poate face încărcarea.",
|
||||||
"tags_not_found": "Niciun tag definit. Adaugă.",
|
"tags_not_found": "Niciun tag definit. Adaugă.",
|
||||||
"tags_editor_no_tags": "Adaugă tag-uri",
|
"tags_editor_no_tags": "Adaugă tag-uri",
|
||||||
"title_manage_tags": "Administrează"
|
"title_manage_tags": "Administrează",
|
||||||
|
"drafty_attachment": "Atasament",
|
||||||
|
"drafty_form": "Formă:",
|
||||||
|
"drafty_image": "Imagine"
|
||||||
},
|
},
|
||||||
"ru": {
|
"ru": {
|
||||||
"contacts_not_found": "Чатов нет<br />¯∖_(ツ)_/¯",
|
"contacts_not_found": "Чатов нет<br />¯∖_(ツ)_/¯",
|
||||||
@@ -1053,7 +1068,10 @@
|
|||||||
"no_connection": "Нет связи",
|
"no_connection": "Нет связи",
|
||||||
"code_doesnot_match": "Код не совпадает",
|
"code_doesnot_match": "Код не совпадает",
|
||||||
"channel_prompt": "Создать канал",
|
"channel_prompt": "Создать канал",
|
||||||
"channel": "канал"
|
"channel": "канал",
|
||||||
|
"drafty_attachment": "Аттачмент",
|
||||||
|
"drafty_form": "Форма:",
|
||||||
|
"drafty_image": "Картинка"
|
||||||
},
|
},
|
||||||
"zh": {
|
"zh": {
|
||||||
"archived_contacts": "已归档联系人 ({count})",
|
"archived_contacts": "已归档联系人 ({count})",
|
||||||
@@ -1229,6 +1247,9 @@
|
|||||||
"no_connection": "无连接",
|
"no_connection": "无连接",
|
||||||
"code_doesnot_match": "代码不匹配",
|
"code_doesnot_match": "代码不匹配",
|
||||||
"channel_prompt": "这是一个频道",
|
"channel_prompt": "这是一个频道",
|
||||||
"channel": "频道"
|
"channel": "频道",
|
||||||
|
"drafty_attachment": "附件",
|
||||||
|
"drafty_form": "形式:",
|
||||||
|
"drafty_image": "图像"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.17.0";
|
export const PACKAGE_VERSION = "0.17.1-alpha1";
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ import InPlaceEdit from '../widgets/in-place-edit.jsx';
|
|||||||
import TagManager from '../widgets/tag-manager.jsx';
|
import TagManager from '../widgets/tag-manager.jsx';
|
||||||
|
|
||||||
import { MAX_TITLE_LENGTH } from '../config.js';
|
import { MAX_TITLE_LENGTH } from '../config.js';
|
||||||
import { makeImageUrl } from '../lib/blob-helpers.js';
|
import { makeImageDataUrl } from '../lib/blob-helpers.js';
|
||||||
import { arrayEqual, asEmail, asPhone, vcard } from '../lib/utils.js';
|
import { arrayEqual, asEmail, asPhone, theCard } from '../lib/utils.js';
|
||||||
|
|
||||||
export default class AccGeneralView extends React.Component {
|
export default class AccGeneralView extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
@@ -19,7 +19,7 @@ export default class AccGeneralView extends React.Component {
|
|||||||
const me = this.props.tinode.getMeTopic();
|
const me = this.props.tinode.getMeTopic();
|
||||||
this.state = {
|
this.state = {
|
||||||
fullName: me.public ? me.public.fn : undefined,
|
fullName: me.public ? me.public.fn : undefined,
|
||||||
avatar: makeImageUrl(me.public ? me.public.photo : null),
|
avatar: makeImageDataUrl(me.public ? me.public.photo : null),
|
||||||
tags: me.tags(),
|
tags: me.tags(),
|
||||||
credentials: me.getCredentials() || [],
|
credentials: me.getCredentials() || [],
|
||||||
addCredActive: false,
|
addCredActive: false,
|
||||||
@@ -62,13 +62,13 @@ export default class AccGeneralView extends React.Component {
|
|||||||
fn = fn.trim().substring(0, MAX_TITLE_LENGTH);
|
fn = fn.trim().substring(0, MAX_TITLE_LENGTH);
|
||||||
if (fn) {
|
if (fn) {
|
||||||
this.setState({fullName: fn});
|
this.setState({fullName: fn});
|
||||||
this.props.onUpdateAccount(undefined, vcard(fn, null));
|
this.props.onUpdateAccount(undefined, theCard(fn, null));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleImageChanged(img) {
|
handleImageChanged(img) {
|
||||||
this.setState({avatar: img});
|
this.setState({avatar: img});
|
||||||
this.props.onUpdateAccount(undefined, vcard(null, img || Tinode.DEL_CHAR));
|
this.props.onUpdateAccount(undefined, theCard(null, img || Tinode.DEL_CHAR));
|
||||||
}
|
}
|
||||||
|
|
||||||
handleCredChange(e) {
|
handleCredChange(e) {
|
||||||
|
|||||||
@@ -110,11 +110,11 @@ class AccSecurityView extends React.Component {
|
|||||||
</div>
|
</div>
|
||||||
<div className="hr" />
|
<div className="hr" />
|
||||||
<div className="panel-form-column">
|
<div className="panel-form-column">
|
||||||
<a href="#" className="red flat-button" onClick={(e) => {e.preventDefault(); this.props.onLogout();}}>
|
<a href="#" className="danger 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>
|
||||||
<a href="#" className="red flat-button" onClick={(e) => {this.handleDeleteAccount(e);}}>
|
<a href="#" className="danger flat-button" onClick={(e) => {this.handleDeleteAccount(e);}}>
|
||||||
<i className="material-icons">delete</i> <FormattedMessage id="button_delete_account"
|
<i className="material-icons">delete</i> <FormattedMessage id="button_delete_account"
|
||||||
defaultMessage="Delete account" description="Button [Delete account]" />
|
defaultMessage="Delete account" description="Button [Delete account]" />
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import CheckBox from '../widgets/checkbox.jsx';
|
|||||||
import VisiblePassword from '../widgets/visible-password.jsx';
|
import VisiblePassword from '../widgets/visible-password.jsx';
|
||||||
|
|
||||||
import LocalStorageUtil from '../lib/local-storage.js';
|
import LocalStorageUtil from '../lib/local-storage.js';
|
||||||
import { vcard } from '../lib/utils.js';
|
import { theCard } from '../lib/utils.js';
|
||||||
|
|
||||||
import { MAX_TITLE_LENGTH } from '../config.js';
|
import { MAX_TITLE_LENGTH } from '../config.js';
|
||||||
|
|
||||||
@@ -65,12 +65,12 @@ export default class CreateAccountView extends React.PureComponent {
|
|||||||
this.props.onCreateAccount(
|
this.props.onCreateAccount(
|
||||||
this.state.login.trim(),
|
this.state.login.trim(),
|
||||||
this.state.password.trim(),
|
this.state.password.trim(),
|
||||||
vcard(this.state.fn.trim().substring(0, MAX_TITLE_LENGTH), this.state.imageDataUrl),
|
theCard(this.state.fn.trim().substring(0, MAX_TITLE_LENGTH), this.state.imageDataUrl),
|
||||||
{'meth': 'email', 'val': this.state.email});
|
{'meth': 'email', 'val': this.state.email});
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let submitClasses = 'blue';
|
let submitClasses = 'primary';
|
||||||
if (this.props.disabled) {
|
if (this.props.disabled) {
|
||||||
submitClasses += ' disabled';
|
submitClasses += ' disabled';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { FormattedMessage } from 'react-intl';
|
|||||||
|
|
||||||
import AvatarUpload from '../widgets/avatar-upload.jsx';
|
import AvatarUpload from '../widgets/avatar-upload.jsx';
|
||||||
|
|
||||||
import { makeImageUrl } from '../lib/blob-helpers.js';
|
import { makeImageDataUrl } from '../lib/blob-helpers.js';
|
||||||
|
|
||||||
export default class EditAccountView extends React.Component {
|
export default class EditAccountView extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
@@ -13,7 +13,7 @@ export default class EditAccountView extends React.Component {
|
|||||||
const me = this.props.tinode.getMeTopic();
|
const me = this.props.tinode.getMeTopic();
|
||||||
this.state = {
|
this.state = {
|
||||||
fullName: me.public ? me.public.fn : undefined,
|
fullName: me.public ? me.public.fn : undefined,
|
||||||
avatar: makeImageUrl(me.public ? me.public.photo : null)
|
avatar: makeImageDataUrl(me.public ? me.public.photo : null)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ import TagManager from '../widgets/tag-manager.jsx';
|
|||||||
|
|
||||||
import { MAX_TITLE_LENGTH, NO_ACCESS_MODE } from '../config.js';
|
import { MAX_TITLE_LENGTH, NO_ACCESS_MODE } from '../config.js';
|
||||||
|
|
||||||
import { makeImageUrl } from '../lib/blob-helpers.js';
|
import { makeImageDataUrl } from '../lib/blob-helpers.js';
|
||||||
import { arrayEqual, vcard } from '../lib/utils.js';
|
import { arrayEqual, theCard } from '../lib/utils.js';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
requested: {
|
requested: {
|
||||||
@@ -227,7 +227,7 @@ class InfoView extends React.Component {
|
|||||||
muted: acs && acs.isMuted(),
|
muted: acs && acs.isMuted(),
|
||||||
|
|
||||||
fullName: topic.public ? topic.public.fn : undefined,
|
fullName: topic.public ? topic.public.fn : undefined,
|
||||||
avatar: makeImageUrl(topic.public ? topic.public.photo : null),
|
avatar: makeImageDataUrl(topic.public ? topic.public.photo : null),
|
||||||
private: topic.private ? topic.private.comment : null,
|
private: topic.private ? topic.private.comment : null,
|
||||||
address: topic.name,
|
address: topic.name,
|
||||||
groupTopic: topic.isGroupType(),
|
groupTopic: topic.isGroupType(),
|
||||||
@@ -282,7 +282,7 @@ class InfoView extends React.Component {
|
|||||||
fn = fn.trim().substring(0, MAX_TITLE_LENGTH);
|
fn = fn.trim().substring(0, MAX_TITLE_LENGTH);
|
||||||
if (this.state.fullName !== fn) {
|
if (this.state.fullName !== fn) {
|
||||||
this.setState({fullName: fn});
|
this.setState({fullName: fn});
|
||||||
this.props.onTopicDescUpdate(this.props.topic, vcard(fn, null), null);
|
this.props.onTopicDescUpdate(this.props.topic, theCard(fn, null), null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -296,7 +296,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 || Tinode.DEL_CHAR), null);
|
this.props.onTopicDescUpdate(this.props.topic, theCard(null, img || Tinode.DEL_CHAR), null);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleMuted(ignored, checked) {
|
handleMuted(ignored, checked) {
|
||||||
@@ -717,18 +717,18 @@ class InfoView extends React.Component {
|
|||||||
:
|
:
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
<a href="#" className="red flat-button" onClick={this.handleLeave}>
|
<a href="#" className="danger flat-button" onClick={this.handleLeave}>
|
||||||
<i className="material-icons">exit_to_app</i> {formatMessage(messages.leave_chat)}
|
<i className="material-icons">exit_to_app</i> {formatMessage(messages.leave_chat)}
|
||||||
</a>
|
</a>
|
||||||
{!this.state.groupTopic ?
|
{!this.state.groupTopic ?
|
||||||
<a href="#" className="red flat-button" onClick={this.handleBlock}>
|
<a href="#" className="danger flat-button" onClick={this.handleBlock}>
|
||||||
<i className="material-icons">block</i> {formatMessage(messages.block_contact)}
|
<i className="material-icons">block</i> {formatMessage(messages.block_contact)}
|
||||||
</a>
|
</a>
|
||||||
:
|
:
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
{!this.state.owner ?
|
{!this.state.owner ?
|
||||||
<a href="#" className="red flat-button" onClick={this.handleReport}>
|
<a href="#" className="danger flat-button" onClick={this.handleReport}>
|
||||||
<i className="material-icons">report</i> {formatMessage(messages.report_chat)}
|
<i className="material-icons">report</i> {formatMessage(messages.report_chat)}
|
||||||
</a>
|
</a>
|
||||||
:
|
:
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ export default class LoginView extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
var submitClasses = 'blue';
|
let submitClasses = 'primary';
|
||||||
if (this.props.disabled) {
|
if (this.props.disabled) {
|
||||||
submitClasses += ' disabled';
|
submitClasses += ' disabled';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import SendMessage from '../widgets/send-message.jsx';
|
|||||||
import { DEFAULT_P2P_ACCESS_MODE, IMAGE_PREVIEW_DIM, KEYPRESS_DELAY, MESSAGES_PAGE,
|
import { DEFAULT_P2P_ACCESS_MODE, IMAGE_PREVIEW_DIM, KEYPRESS_DELAY, MESSAGES_PAGE,
|
||||||
MAX_EXTERN_ATTACHMENT_SIZE, MAX_IMAGE_DIM, MAX_INBAND_ATTACHMENT_SIZE, READ_DELAY } from '../config.js';
|
MAX_EXTERN_ATTACHMENT_SIZE, MAX_IMAGE_DIM, MAX_INBAND_ATTACHMENT_SIZE, READ_DELAY } from '../config.js';
|
||||||
import { SUPPORTED_IMAGE_FORMATS, blobToBase64, filePasted, fileToBase64,
|
import { SUPPORTED_IMAGE_FORMATS, blobToBase64, filePasted, fileToBase64,
|
||||||
imageScaled, makeImageUrl } from '../lib/blob-helpers.js';
|
imageScaled, makeImageDataUrl } from '../lib/blob-helpers.js';
|
||||||
import HashNavigation from '../lib/navigation.js';
|
import HashNavigation from '../lib/navigation.js';
|
||||||
import { bytesToHumanSize, shortDateFormat } from '../lib/strformat.js';
|
import { bytesToHumanSize, shortDateFormat } from '../lib/strformat.js';
|
||||||
|
|
||||||
@@ -133,7 +133,7 @@ class MessagesView extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const topic = this.props.tinode.getTopic(this.state.topic);
|
const topic = this.props.tinode ? this.props.tinode.getTopic(this.state.topic) : undefined;
|
||||||
if (this.state.topic != prevState.topic) {
|
if (this.state.topic != prevState.topic) {
|
||||||
if (prevState.topic && !Tinode.isNewGroupTopicName(prevState.topic)) {
|
if (prevState.topic && !Tinode.isNewGroupTopicName(prevState.topic)) {
|
||||||
this.leave(prevState.topic);
|
this.leave(prevState.topic);
|
||||||
@@ -261,7 +261,7 @@ class MessagesView extends React.Component {
|
|||||||
if (topic.public) {
|
if (topic.public) {
|
||||||
Object.assign(nextState, {
|
Object.assign(nextState, {
|
||||||
title: topic.public.fn,
|
title: topic.public.fn,
|
||||||
avatar: makeImageUrl(topic.public.photo)
|
avatar: makeImageDataUrl(topic.public.photo)
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
Object.assign(nextState, {
|
Object.assign(nextState, {
|
||||||
@@ -391,7 +391,7 @@ class MessagesView extends React.Component {
|
|||||||
if (desc.public) {
|
if (desc.public) {
|
||||||
this.setState({
|
this.setState({
|
||||||
title: desc.public.fn,
|
title: desc.public.fn,
|
||||||
avatar: makeImageUrl(desc.public.photo)
|
avatar: makeImageDataUrl(desc.public.photo)
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.setState({
|
this.setState({
|
||||||
@@ -857,7 +857,7 @@ class MessagesView extends React.Component {
|
|||||||
const user = topic.userDesc(thisFrom);
|
const user = topic.userDesc(thisFrom);
|
||||||
if (user && user.public) {
|
if (user && user.public) {
|
||||||
userName = user.public.fn;
|
userName = user.public.fn;
|
||||||
userAvatar = makeImageUrl(user.public.photo);
|
userAvatar = makeImageDataUrl(user.public.photo);
|
||||||
}
|
}
|
||||||
userFrom = thisFrom;
|
userFrom = thisFrom;
|
||||||
chatBoxClass='chat-box group';
|
chatBoxClass='chat-box group';
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import NewTopicGroup from '../widgets/new-topic-group.jsx';
|
|||||||
import SearchContacts from '../widgets/search-contacts.jsx';
|
import SearchContacts from '../widgets/search-contacts.jsx';
|
||||||
|
|
||||||
import HashNavigation from '../lib/navigation.js';
|
import HashNavigation from '../lib/navigation.js';
|
||||||
import { vcard } from '../lib/utils.js';
|
import { theCard } from '../lib/utils.js';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
search_for_contacts: {
|
search_for_contacts: {
|
||||||
@@ -65,7 +65,7 @@ class NewTopicView extends React.Component {
|
|||||||
|
|
||||||
handleNewGroupSubmit(name, dataUrl, priv, tags, isChannel) {
|
handleNewGroupSubmit(name, dataUrl, priv, tags, isChannel) {
|
||||||
HashNavigation.navigateTo(HashNavigation.removeUrlParam(window.location.hash, 'tab'));
|
HashNavigation.navigateTo(HashNavigation.removeUrlParam(window.location.hash, 'tab'));
|
||||||
this.props.onCreateTopic(undefined, vcard(name, dataUrl), priv, tags, isChannel);
|
this.props.onCreateTopic(undefined, theCard(name, dataUrl), priv, tags, isChannel);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleGroupByID(topicName) {
|
handleGroupByID(topicName) {
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ export default class PasswordResetView extends React.PureComponent {
|
|||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
<div className="dialog-buttons">
|
<div className="dialog-buttons">
|
||||||
<button className="blue" type="submit">{reset ?
|
<button className="primary" type="submit">{reset ?
|
||||||
<FormattedMessage id="button_reset" defaultMessage="Reset" description="Button [Reset]" /> :
|
<FormattedMessage id="button_reset" defaultMessage="Reset" description="Button [Reset]" /> :
|
||||||
<FormattedMessage id="button_send_request" defaultMessage="Send request"
|
<FormattedMessage id="button_send_request" defaultMessage="Send request"
|
||||||
description="Button [Send request]" />
|
description="Button [Send request]" />
|
||||||
|
|||||||
@@ -36,16 +36,15 @@ export default class SettingsView extends React.PureComponent {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
const names = {def: "default", ws: "websocket", lp: "long polling"};
|
const names = {def: "default", ws: "websocket", lp: "long polling"};
|
||||||
var transportOptions = [];
|
const transportOptions = [];
|
||||||
var instance = this;
|
['def', 'ws', 'lp'].map((item) => {
|
||||||
['def', 'ws', 'lp'].map(function(item) {
|
const id = 'transport-' + item;
|
||||||
var id = 'transport-' + item;
|
const name = names[item];
|
||||||
var name = names[item];
|
|
||||||
transportOptions.push(
|
transportOptions.push(
|
||||||
<li key={item}>
|
<li key={item}>
|
||||||
<input type="radio" id={id} name="transport-select" value={item}
|
<input type="radio" id={id} name="transport-select" value={item}
|
||||||
checked={instance.state.transport === item}
|
checked={this.state.transport === item}
|
||||||
onChange={instance.handleTransportSelected} />
|
onChange={this.handleTransportSelected} />
|
||||||
<label htmlFor={id}>{name}</label>
|
<label htmlFor={id}>{name}</label>
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
@@ -73,7 +72,7 @@ export default class SettingsView extends React.PureComponent {
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div className="dialog-buttons">
|
<div className="dialog-buttons">
|
||||||
<button type="submit" className="blue">
|
<button type="submit" className="primary">
|
||||||
<FormattedMessage id="button_update" defaultMessage="Update"
|
<FormattedMessage id="button_update" defaultMessage="Update"
|
||||||
description="Button [Update]" />
|
description="Button [Update]" />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -17,7 +17,8 @@ import SidepanelView from './sidepanel-view.jsx';
|
|||||||
|
|
||||||
import { API_KEY, APP_NAME, DEFAULT_P2P_ACCESS_MODE, LOGGING_ENABLED,
|
import { API_KEY, APP_NAME, DEFAULT_P2P_ACCESS_MODE, LOGGING_ENABLED,
|
||||||
MEDIA_BREAKPOINT, RECEIVED_DELAY } from '../config.js';
|
MEDIA_BREAKPOINT, RECEIVED_DELAY } from '../config.js';
|
||||||
import { base64ReEncode, makeImageUrl } from '../lib/blob-helpers.js';
|
import { PACKAGE_VERSION } from '../version.js';
|
||||||
|
import { base64ReEncode, makeImageDataUrl } from '../lib/blob-helpers.js';
|
||||||
import { detectServerAddress, isLocalHost, isSecureConnection } from '../lib/host-name.js';
|
import { detectServerAddress, isLocalHost, isSecureConnection } from '../lib/host-name.js';
|
||||||
import LocalStorageUtil from '../lib/local-storage.js';
|
import LocalStorageUtil from '../lib/local-storage.js';
|
||||||
import HashNavigation from '../lib/navigation.js';
|
import HashNavigation from '../lib/navigation.js';
|
||||||
@@ -109,9 +110,10 @@ class TinodeWeb extends React.Component {
|
|||||||
this.handleCredAdd = this.handleCredAdd.bind(this);
|
this.handleCredAdd = this.handleCredAdd.bind(this);
|
||||||
this.handleCredDelete = this.handleCredDelete.bind(this);
|
this.handleCredDelete = this.handleCredDelete.bind(this);
|
||||||
this.handleCredConfirm = this.handleCredConfirm.bind(this);
|
this.handleCredConfirm = this.handleCredConfirm.bind(this);
|
||||||
this.initDesktopAlerts = this.initDesktopAlerts.bind(this);
|
this.initFCMessaging = this.initFCMessaging.bind(this);
|
||||||
this.togglePushToken = this.togglePushToken.bind(this);
|
this.togglePushToken = this.togglePushToken.bind(this);
|
||||||
this.requestPushToken = this.requestPushToken.bind(this);
|
this.requestPushToken = this.requestPushToken.bind(this);
|
||||||
|
this.handlePushMessage = this.handlePushMessage.bind(this);
|
||||||
this.handleSidepanelCancel = this.handleSidepanelCancel.bind(this);
|
this.handleSidepanelCancel = this.handleSidepanelCancel.bind(this);
|
||||||
this.handleStartTopicRequest = this.handleStartTopicRequest.bind(this);
|
this.handleStartTopicRequest = this.handleStartTopicRequest.bind(this);
|
||||||
this.handleNewTopicCreated = this.handleNewTopicCreated.bind(this);
|
this.handleNewTopicCreated = this.handleNewTopicCreated.bind(this);
|
||||||
@@ -139,6 +141,7 @@ class TinodeWeb extends React.Component {
|
|||||||
|
|
||||||
getBlankState() {
|
getBlankState() {
|
||||||
const settings = LocalStorageUtil.getObject('settings') || {};
|
const settings = LocalStorageUtil.getObject('settings') || {};
|
||||||
|
const persist = !!LocalStorageUtil.getObject('keep-logged-in');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
connected: false,
|
connected: false,
|
||||||
@@ -152,11 +155,11 @@ class TinodeWeb extends React.Component {
|
|||||||
// "On" is the default, so saving the "off" state.
|
// "On" is the default, so saving the "off" state.
|
||||||
messageSounds: !settings.messageSoundsOff,
|
messageSounds: !settings.messageSoundsOff,
|
||||||
incognitoMode: false,
|
incognitoMode: false,
|
||||||
desktopAlerts: settings.desktopAlerts,
|
desktopAlerts: persist && settings.desktopAlerts,
|
||||||
desktopAlertsEnabled: (isSecureConnection() || isLocalHost()) &&
|
desktopAlertsEnabled: (isSecureConnection() || isLocalHost()) &&
|
||||||
(typeof firebase != 'undefined') && (typeof navigator != 'undefined') &&
|
(typeof firebase != 'undefined') && (typeof navigator != 'undefined') &&
|
||||||
(typeof FIREBASE_INIT != 'undefined'),
|
(typeof FIREBASE_INIT != 'undefined'),
|
||||||
firebaseToken: LocalStorageUtil.getObject('firebase-token'),
|
firebaseToken: persist ? LocalStorageUtil.getObject('firebase-token') : null,
|
||||||
|
|
||||||
applicationVisible: !document.hidden,
|
applicationVisible: !document.hidden,
|
||||||
|
|
||||||
@@ -200,7 +203,9 @@ class TinodeWeb extends React.Component {
|
|||||||
// Merged results of a search query and p2p chats.
|
// Merged results of a search query and p2p chats.
|
||||||
searchableContacts: [],
|
searchableContacts: [],
|
||||||
credMethod: undefined,
|
credMethod: undefined,
|
||||||
credCode: undefined
|
credCode: undefined,
|
||||||
|
// Topic to go to after login.
|
||||||
|
requestedTopic: undefined
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -209,6 +214,10 @@ class TinodeWeb extends React.Component {
|
|||||||
window.addEventListener('online', (e) => { this.handleOnline(true); });
|
window.addEventListener('online', (e) => { this.handleOnline(true); });
|
||||||
window.addEventListener('offline', (e) => { this.handleOnline(false); });
|
window.addEventListener('offline', (e) => { this.handleOnline(false); });
|
||||||
window.addEventListener('hashchange', this.handleHashRoute);
|
window.addEventListener('hashchange', this.handleHashRoute);
|
||||||
|
// Process background notifications from the service worker.
|
||||||
|
const serviceWorkerChannel = new BroadcastChannel('tinode-sw');
|
||||||
|
serviceWorkerChannel.addEventListener('message', this.handlePushMessage);
|
||||||
|
|
||||||
// Window/tab visible or invisible for pausing timers.
|
// Window/tab visible or invisible for pausing timers.
|
||||||
document.addEventListener('visibilitychange', this.handleVisibilityEvent);
|
document.addEventListener('visibilitychange', this.handleVisibilityEvent);
|
||||||
|
|
||||||
@@ -219,44 +228,36 @@ class TinodeWeb extends React.Component {
|
|||||||
|
|
||||||
const keepLoggedIn = LocalStorageUtil.getObject('keep-logged-in');
|
const keepLoggedIn = LocalStorageUtil.getObject('keep-logged-in');
|
||||||
|
|
||||||
const {formatMessage, locale} = this.props.intl;
|
|
||||||
new Promise((resolve, reject) => {
|
new Promise((resolve, reject) => {
|
||||||
this.tinode = TinodeWeb.tnSetup(this.state.serverAddress, this.state.transport, locale, keepLoggedIn, resolve);
|
this.tinode = TinodeWeb.tnSetup(this.state.serverAddress, this.state.transport,
|
||||||
|
this.props.intl.locale, keepLoggedIn, resolve);
|
||||||
this.tinode.onConnect = this.handleConnected;
|
this.tinode.onConnect = this.handleConnected;
|
||||||
this.tinode.onDisconnect = this.handleDisconnect;
|
this.tinode.onDisconnect = this.handleDisconnect;
|
||||||
this.tinode.onAutoreconnectIteration = this.handleAutoreconnectIteration;
|
this.tinode.onAutoreconnectIteration = this.handleAutoreconnectIteration;
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
// Initialize desktop alerts.
|
// Initialize desktop alerts.
|
||||||
if (this.state.desktopAlertsEnabled) {
|
if (this.state.desktopAlertsEnabled) {
|
||||||
try {
|
this.initFCMessaging().then(() => {
|
||||||
this.fbPush = firebase.initializeApp(FIREBASE_INIT, APP_NAME).messaging();
|
if (this.state.desktopAlerts) {
|
||||||
this.fbPush.usePublicVapidKey(FIREBASE_INIT.messagingVapidKey);
|
if (!this.state.firebaseToken) {
|
||||||
navigator.serviceWorker.register('/service-worker.js').then((reg) => {
|
this.togglePushToken(true);
|
||||||
this.checkForAppUpdate(reg);
|
} else {
|
||||||
this.fbPush.useServiceWorker(reg);
|
this.tinode.setDeviceToken(this.state.firebaseToken);
|
||||||
reg.active.postMessage(JSON.stringify({locale: locale}));
|
|
||||||
this.initDesktopAlerts();
|
|
||||||
if (this.state.desktopAlerts) {
|
|
||||||
if (!this.state.firebaseToken) {
|
|
||||||
this.togglePushToken(true);
|
|
||||||
} else {
|
|
||||||
this.tinode.setDeviceToken(this.state.firebaseToken, true);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}).catch((err) => {
|
}
|
||||||
// registration failed :(
|
}).catch(() => {
|
||||||
console.log("Failed to register service worker:", err);
|
// do nothing: handled earlier.
|
||||||
});
|
// catch needed to pervent unnecessary logging of error.
|
||||||
} catch (err) {
|
});
|
||||||
this.handleError(formatMessage(messages.push_init_failed), 'err');
|
|
||||||
console.log("Failed to initialize push notifications", err);
|
|
||||||
this.setState({desktopAlertsEnabled: false});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const token = keepLoggedIn ? LocalStorageUtil.getObject('auth-token') : undefined;
|
// Parse and save the hash navigation params.
|
||||||
|
|
||||||
const parsedNav = HashNavigation.parseUrlHash(window.location.hash);
|
const parsedNav = HashNavigation.parseUrlHash(window.location.hash);
|
||||||
|
|
||||||
|
// Read contacts from cache.
|
||||||
|
this.resetContactList();
|
||||||
|
|
||||||
|
const token = keepLoggedIn ? LocalStorageUtil.getObject('auth-token') : undefined;
|
||||||
if (token) {
|
if (token) {
|
||||||
this.setState({autoLogin: true});
|
this.setState({autoLogin: true});
|
||||||
|
|
||||||
@@ -272,6 +273,8 @@ class TinodeWeb extends React.Component {
|
|||||||
parsedNav.path[0] = '';
|
parsedNav.path[0] = '';
|
||||||
HashNavigation.navigateTo(HashNavigation.composeUrlHash(parsedNav.path, parsedNav.params));
|
HashNavigation.navigateTo(HashNavigation.composeUrlHash(parsedNav.path, parsedNav.params));
|
||||||
} else if (!parsedNav.params.token) {
|
} else if (!parsedNav.params.token) {
|
||||||
|
// No token, save possible topic name and navigating to blank state.
|
||||||
|
this.setState({requestedTopic: parsedNav.path[1]});
|
||||||
HashNavigation.navigateTo('');
|
HashNavigation.navigateTo('');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -297,6 +300,53 @@ class TinodeWeb extends React.Component {
|
|||||||
return tinode;
|
return tinode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Notifiy Tinode that a push message was received from the server.
|
||||||
|
handlePushMessage(data) {
|
||||||
|
if (data.what == 'msg' && Tinode.isChannelTopicName(data.topic)) {
|
||||||
|
// The last argument is a fake user Id: otherwise the update is seen as one from the current user.
|
||||||
|
this.tinode.oobNotification(data.topic, data.seq, 'fake-uid');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
initFCMessaging() {
|
||||||
|
const {formatMessage, locale} = this.props.intl;
|
||||||
|
const onError = (msg, err) => {
|
||||||
|
console.log(msg, err);
|
||||||
|
this.handleError(formatMessage(messages.push_init_failed), 'err');
|
||||||
|
this.setState({desktopAlertsEnabled: false});
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.fbPush = firebase.initializeApp(FIREBASE_INIT, APP_NAME).messaging();
|
||||||
|
this.fbPush.usePublicVapidKey(FIREBASE_INIT.messagingVapidKey);
|
||||||
|
|
||||||
|
return navigator.serviceWorker.register('/service-worker.js').then((reg) => {
|
||||||
|
this.checkForAppUpdate(reg);
|
||||||
|
this.fbPush.useServiceWorker(reg);
|
||||||
|
reg.active.postMessage(JSON.stringify({locale: locale, version: PACKAGE_VERSION}));
|
||||||
|
|
||||||
|
// Google could not be bothered to mention that onTokenRefresh is never called.
|
||||||
|
this.fbPush.onTokenRefresh(() => {
|
||||||
|
this.requestPushToken();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handhe FCM pushes
|
||||||
|
// (a) for channels always,
|
||||||
|
// (b) pushes when the app is in foreground but has not focus.
|
||||||
|
this.fbPush.onMessage(payload => { this.handlePushMessage(payload.data); });
|
||||||
|
|
||||||
|
return reg;
|
||||||
|
}).catch((err) => {
|
||||||
|
// registration failed :(
|
||||||
|
onError("Failed to register service worker:", err);
|
||||||
|
return Promise.reject(err);
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
onError("Failed to initialize push notifications", err);
|
||||||
|
return Promise.reject(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
handleResize() {
|
handleResize() {
|
||||||
const mobile = document.documentElement.clientWidth <= MEDIA_BREAKPOINT;
|
const mobile = document.documentElement.clientWidth <= MEDIA_BREAKPOINT;
|
||||||
this.setState({
|
this.setState({
|
||||||
@@ -341,10 +391,16 @@ class TinodeWeb extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Topic for MessagesView selector.
|
// Topic for MessagesView selector.
|
||||||
if (hash.path.length > 1 && hash.path[1] != this.state.topicSelected) {
|
const topicName = hash.path[1];
|
||||||
this.setState({
|
if (Tinode.topicType(topicName) && topicName != this.state.topicSelected) {
|
||||||
topicSelected: Tinode.topicType(hash.path[1]) ? hash.path[1] : null
|
const newState = {
|
||||||
});
|
topicSelected: topicName
|
||||||
|
};
|
||||||
|
const acs = this.tinode.getTopicAccessMode(topicName);
|
||||||
|
if (acs) {
|
||||||
|
newState.topicSelectedAcs = acs;
|
||||||
|
}
|
||||||
|
this.setState(newState);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Empty hashpath
|
// Empty hashpath
|
||||||
@@ -367,8 +423,12 @@ class TinodeWeb extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleOnline(online) {
|
handleOnline(online) {
|
||||||
|
console.log("handleOnline", online);
|
||||||
|
|
||||||
if (online) {
|
if (online) {
|
||||||
this.handleError();
|
this.handleError();
|
||||||
|
clearInterval(this.reconnectCountdown);
|
||||||
|
this.tinode.reconnect();
|
||||||
} else {
|
} else {
|
||||||
this.handleError(this.props.intl.formatMessage(messages.no_connection), 'warn');
|
this.handleError(this.props.intl.formatMessage(messages.no_connection), 'warn');
|
||||||
}
|
}
|
||||||
@@ -407,6 +467,10 @@ class TinodeWeb extends React.Component {
|
|||||||
|
|
||||||
// Connection succeeded.
|
// Connection succeeded.
|
||||||
handleConnected() {
|
handleConnected() {
|
||||||
|
// Just to be sure.
|
||||||
|
clearInterval(this.reconnectCountdown);
|
||||||
|
this.handleError();
|
||||||
|
|
||||||
const params = this.tinode.getServerInfo();
|
const params = this.tinode.getServerInfo();
|
||||||
this.setState({
|
this.setState({
|
||||||
serverVersion: params.ver + ' ' + (params.build ? params.build : 'none')
|
serverVersion: params.ver + ' ' + (params.build ? params.build : 'none')
|
||||||
@@ -534,6 +598,8 @@ class TinodeWeb extends React.Component {
|
|||||||
if (LocalStorageUtil.getObject('keep-logged-in')) {
|
if (LocalStorageUtil.getObject('keep-logged-in')) {
|
||||||
LocalStorageUtil.setObject('auth-token', this.tinode.getAuthToken());
|
LocalStorageUtil.setObject('auth-token', this.tinode.getAuthToken());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const goToTopic = this.state.requestedTopic;
|
||||||
// Logged in fine, subscribe to 'me' attaching callbacks from the contacts view.
|
// Logged in fine, subscribe to 'me' attaching callbacks from the contacts view.
|
||||||
const me = this.tinode.getMeTopic();
|
const me = this.tinode.getMeTopic();
|
||||||
me.onMetaDesc = this.tnMeMetaDesc;
|
me.onMetaDesc = this.tnMeMetaDesc;
|
||||||
@@ -545,6 +611,7 @@ class TinodeWeb extends React.Component {
|
|||||||
credCode: undefined,
|
credCode: undefined,
|
||||||
myUserId: this.tinode.getCurrentUserID(),
|
myUserId: this.tinode.getCurrentUserID(),
|
||||||
autoLogin: true,
|
autoLogin: true,
|
||||||
|
requestedTopic: undefined,
|
||||||
});
|
});
|
||||||
// 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(
|
||||||
@@ -562,7 +629,11 @@ class TinodeWeb extends React.Component {
|
|||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
this.setState({loadSpinnerVisible: false});
|
this.setState({loadSpinnerVisible: false});
|
||||||
});
|
});
|
||||||
HashNavigation.navigateTo(HashNavigation.setUrlSidePanel(window.location.hash, 'contacts'));
|
let urlHash = HashNavigation.setUrlSidePanel(window.location.hash, 'contacts');
|
||||||
|
if (goToTopic) {
|
||||||
|
urlHash = HashNavigation.setUrlTopic(urlHash, goToTopic);
|
||||||
|
}
|
||||||
|
HashNavigation.navigateTo(urlHash);
|
||||||
}
|
}
|
||||||
|
|
||||||
tnMeMetaDesc(desc) {
|
tnMeMetaDesc(desc) {
|
||||||
@@ -570,7 +641,7 @@ class TinodeWeb extends React.Component {
|
|||||||
if (desc.public) {
|
if (desc.public) {
|
||||||
this.setState({
|
this.setState({
|
||||||
sidePanelTitle: desc.public.fn,
|
sidePanelTitle: desc.public.fn,
|
||||||
sidePanelAvatar: makeImageUrl(desc.public.photo)
|
sidePanelAvatar: makeImageDataUrl(desc.public.photo)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (desc.acs) {
|
if (desc.acs) {
|
||||||
@@ -674,6 +745,10 @@ class TinodeWeb extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.tinode.getMeTopic().contacts((c) => {
|
this.tinode.getMeTopic().contacts((c) => {
|
||||||
|
if (!c.topic && !c.user) {
|
||||||
|
// Contacts expect c.topic to be set.
|
||||||
|
c.topic = c.name;
|
||||||
|
}
|
||||||
newState.chatList.push(c);
|
newState.chatList.push(c);
|
||||||
if (this.state.topicSelected == c.topic) {
|
if (this.state.topicSelected == c.topic) {
|
||||||
newState.topicSelectedOnline = c.online;
|
newState.topicSelectedOnline = c.online;
|
||||||
@@ -966,42 +1041,37 @@ class TinodeWeb extends React.Component {
|
|||||||
this.state.myUserId ? 'blocked' : ''));
|
this.state.myUserId ? 'blocked' : ''));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize desktop alerts = push notifications.
|
|
||||||
initDesktopAlerts() {
|
|
||||||
// Google could not be bothered to mention that
|
|
||||||
// onTokenRefresh is never called.
|
|
||||||
this.fbPush.onTokenRefresh(() => {
|
|
||||||
this.requestPushToken();
|
|
||||||
});
|
|
||||||
|
|
||||||
this.fbPush.onMessage((payload) => {
|
|
||||||
// No need to do anything about it.
|
|
||||||
// All the magic happends in the service worker.
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
togglePushToken(enabled) {
|
togglePushToken(enabled) {
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
if (!this.state.firebaseToken) {
|
if (!this.state.firebaseToken) {
|
||||||
this.fbPush.requestPermission().then(() => {
|
const fcm = this.fbPush ?
|
||||||
|
Promise.resolve() :
|
||||||
|
this.initFCMessaging();
|
||||||
|
fcm.then(() => {
|
||||||
|
return this.fbPush.requestPermission();
|
||||||
|
}).then(() => {
|
||||||
this.requestPushToken();
|
this.requestPushToken();
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
|
console.log("Failed to get notification permission.", err);
|
||||||
this.handleError(err.message, 'err');
|
this.handleError(err.message, 'err');
|
||||||
this.setState({desktopAlerts: false, firebaseToken: null});
|
this.setState({desktopAlerts: false, firebaseToken: null});
|
||||||
LocalStorageUtil.updateObject('settings', {desktopAlerts: false});
|
LocalStorageUtil.updateObject('settings', {desktopAlerts: false});
|
||||||
console.log("Failed to get permission to notify.", err);
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.setState({desktopAlerts: true});
|
this.setState({desktopAlerts: true});
|
||||||
LocalStorageUtil.updateObject('settings', {desktopAlerts: true});
|
if (LocalStorageUtil.getObject('keep-logged-in')) {
|
||||||
|
LocalStorageUtil.updateObject('settings', {desktopAlerts: true});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else if (this.state.firebaseToken) {
|
} else if (this.state.firebaseToken && this.fbPush) {
|
||||||
this.fbPush.deleteToken(this.state.firebaseToken).catch((err) => {
|
this.fbPush.deleteToken(this.state.firebaseToken).catch((err) => {
|
||||||
console.log("Unable to delete token.", err);
|
console.log("Unable to delete token.", err);
|
||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
LocalStorageUtil.updateObject('settings', {desktopAlerts: false});
|
LocalStorageUtil.updateObject('settings', {desktopAlerts: false});
|
||||||
localStorage.removeItem('firebase-token');
|
localStorage.removeItem('firebase-token');
|
||||||
this.setState({desktopAlerts: false, firebaseToken: null});
|
this.setState({desktopAlerts: false, firebaseToken: null});
|
||||||
|
// Inform the server that the token was deleted.
|
||||||
|
this.tinode.setDeviceToken(null);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.setState({desktopAlerts: false, firebaseToken: null});
|
this.setState({desktopAlerts: false, firebaseToken: null});
|
||||||
@@ -1011,12 +1081,17 @@ class TinodeWeb extends React.Component {
|
|||||||
|
|
||||||
requestPushToken() {
|
requestPushToken() {
|
||||||
this.fbPush.getToken().then((refreshedToken) => {
|
this.fbPush.getToken().then((refreshedToken) => {
|
||||||
|
const persist = LocalStorageUtil.getObject('keep-logged-in');
|
||||||
if (refreshedToken != this.state.firebaseToken) {
|
if (refreshedToken != this.state.firebaseToken) {
|
||||||
this.tinode.setDeviceToken(refreshedToken, true);
|
this.tinode.setDeviceToken(refreshedToken);
|
||||||
LocalStorageUtil.setObject('firebase-token', refreshedToken);
|
if (persist) {
|
||||||
|
LocalStorageUtil.setObject('firebase-token', refreshedToken);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
this.setState({firebaseToken: refreshedToken, desktopAlerts: true});
|
this.setState({firebaseToken: refreshedToken, desktopAlerts: true});
|
||||||
LocalStorageUtil.updateObject('settings', {desktopAlerts: true});
|
if (persist) {
|
||||||
|
LocalStorageUtil.updateObject('settings', {desktopAlerts: true});
|
||||||
|
}
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
this.handleError(err.message, 'err');
|
this.handleError(err.message, 'err');
|
||||||
console.log("Failed to retrieve firebase token", err);
|
console.log("Failed to retrieve firebase token", err);
|
||||||
|
|||||||
@@ -78,10 +78,10 @@ class ValidationView extends React.PureComponent {
|
|||||||
}</FormattedMessage>
|
}</FormattedMessage>
|
||||||
</div>
|
</div>
|
||||||
<div className="dialog-buttons">
|
<div className="dialog-buttons">
|
||||||
<button className="blue" onClick={this.handleSubmit}>
|
<button className="primary" 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}>
|
<button className="secondary" onClick={this.handleCancel}>
|
||||||
<FormattedMessage id="button_cancel" defaultMessage="Cancel" description="Button [Cancel]" />
|
<FormattedMessage id="button_cancel" defaultMessage="Cancel" description="Button [Cancel]" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export default class Alert extends React.PureComponent {
|
|||||||
:
|
:
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
<button className="blue" onClick={this.props.onConfirm}>
|
<button className="primary" onClick={this.props.onConfirm}>
|
||||||
{this.props.confirm || <FormattedMessage id="button_ok" defaultMessage="OK" description="Button [OK]" />}
|
{this.props.confirm || <FormattedMessage id="button_ok" defaultMessage="OK" description="Button [OK]" />}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import React from 'react';
|
|||||||
import LetterTile from './letter-tile.jsx';
|
import LetterTile from './letter-tile.jsx';
|
||||||
|
|
||||||
import { AVATAR_SIZE, MAX_EXTERN_ATTACHMENT_SIZE } from '../config.js';
|
import { AVATAR_SIZE, MAX_EXTERN_ATTACHMENT_SIZE } from '../config.js';
|
||||||
import { imageScaled, blobToBase64, makeImageUrl } from '../lib/blob-helpers.js';
|
import { imageScaled, blobToBase64, makeImageDataUrl } from '../lib/blob-helpers.js';
|
||||||
|
|
||||||
export default class AvatarUpload extends React.Component {
|
export default class AvatarUpload extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
@@ -28,7 +28,7 @@ export default class AvatarUpload extends React.Component {
|
|||||||
(mime, blob) => {
|
(mime, blob) => {
|
||||||
// Convert blob to base64-encoded bits.
|
// Convert blob to base64-encoded bits.
|
||||||
blobToBase64(blob, (unused, base64bits) => {
|
blobToBase64(blob, (unused, base64bits) => {
|
||||||
const du = makeImageUrl({data: base64bits, type: mime});
|
const du = makeImageDataUrl({data: base64bits, type: mime});
|
||||||
this.setState({dataUrl: du});
|
this.setState({dataUrl: du});
|
||||||
this.props.onImageChanged(du);
|
this.props.onImageChanged(du);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ export default class ChatMessage extends React.Component {
|
|||||||
(this.props.sequence + ' ' + (this.props.response ? 'left' : 'right'));
|
(this.props.sequence + ' ' + (this.props.response ? 'left' : 'right'));
|
||||||
const bubbleClass = (this.props.sequence == 'single' || this.props.sequence == 'last') ? 'bubble tip' : 'bubble';
|
const bubbleClass = (this.props.sequence == 'single' || this.props.sequence == 'last') ? 'bubble tip' : 'bubble';
|
||||||
const avatar = this.props.deleted ? null : (this.props.userAvatar || true);
|
const avatar = this.props.deleted ? null : (this.props.userAvatar || true);
|
||||||
const fullDisplay = (this.props.userFrom && this.props.response &&
|
const fullDisplay = (this.props.userFrom && this.props.response && !this.props.deleted &&
|
||||||
(this.props.sequence == 'single' || this.props.sequence == 'last'));
|
(this.props.sequence == 'single' || this.props.sequence == 'last'));
|
||||||
|
|
||||||
let content = this.props.content;
|
let content = this.props.content;
|
||||||
@@ -128,9 +128,11 @@ export default class ChatMessage extends React.Component {
|
|||||||
null}
|
null}
|
||||||
<div>
|
<div>
|
||||||
<div className={bubbleClass}>
|
<div className={bubbleClass}>
|
||||||
<div className="message-content">
|
<div className="content-meta">
|
||||||
{content}
|
<div className="message-content">
|
||||||
{attachments}
|
{content}
|
||||||
|
{attachments}
|
||||||
|
</div>
|
||||||
{this.props.timestamp ?
|
{this.props.timestamp ?
|
||||||
<ReceivedMarker
|
<ReceivedMarker
|
||||||
timestamp={this.props.timestamp}
|
timestamp={this.props.timestamp}
|
||||||
@@ -187,9 +189,15 @@ function draftyFormatter(style, data, values, key) {
|
|||||||
if (data) {
|
if (data) {
|
||||||
attr.className = 'inline-image';
|
attr.className = 'inline-image';
|
||||||
const dim = fitImageSize(data.width, data.height,
|
const dim = fitImageSize(data.width, data.height,
|
||||||
Math.min(this.props.viewportWidth - REM_SIZE * 4, REM_SIZE * 36), REM_SIZE * 24, false) ||
|
Math.min(this.props.viewportWidth - REM_SIZE * 6.5, REM_SIZE * 34.5), REM_SIZE * 24, false) ||
|
||||||
{dstWidth: BROKEN_IMAGE_SIZE, dstHeight: BROKEN_IMAGE_SIZE};
|
{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',
|
||||||
|
// Looks like a Chrome bug: broken image does not respect 'width' and 'height'.
|
||||||
|
minWidth: dim.dstWidth + 'px',
|
||||||
|
minHeight: dim.dstHeight + 'px'
|
||||||
|
};
|
||||||
if (!Drafty.isProcessing(data)) {
|
if (!Drafty.isProcessing(data)) {
|
||||||
attr.src = this.props.tinode.authorizeURL(sanitizeImageUrl(attr.src));
|
attr.src = this.props.tinode.authorizeURL(sanitizeImageUrl(attr.src));
|
||||||
attr.alt = data.name;
|
attr.alt = data.name;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import React from 'react';
|
|||||||
|
|
||||||
import Chip from './chip.jsx';
|
import Chip from './chip.jsx';
|
||||||
|
|
||||||
import { makeImageUrl } from '../lib/blob-helpers.js';
|
import { makeImageDataUrl } from '../lib/blob-helpers.js';
|
||||||
|
|
||||||
/* BEGIN ChipInput: group membership widget */
|
/* BEGIN ChipInput: group membership widget */
|
||||||
export default class ChipInput extends React.Component {
|
export default class ChipInput extends React.Component {
|
||||||
@@ -119,7 +119,7 @@ export default class ChipInput extends React.Component {
|
|||||||
chips.push(
|
chips.push(
|
||||||
<Chip
|
<Chip
|
||||||
onCancel={this.handleChipCancel}
|
onCancel={this.handleChipCancel}
|
||||||
avatar={makeImageUrl(item.public ? item.public.photo : null)}
|
avatar={makeImageDataUrl(item.public ? item.public.photo : null)}
|
||||||
title={item.public ? item.public.fn : undefined}
|
title={item.public ? item.public.fn : undefined}
|
||||||
noAvatar={this.props.avatarDisabled}
|
noAvatar={this.props.avatarDisabled}
|
||||||
topic={item.user}
|
topic={item.user}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import Tinode from 'tinode-sdk';
|
|||||||
import Contact from './contact.jsx';
|
import Contact from './contact.jsx';
|
||||||
import ContactAction from './contact-action.jsx';
|
import ContactAction from './contact-action.jsx';
|
||||||
|
|
||||||
import { makeImageUrl } from '../lib/blob-helpers.js';
|
import { makeImageDataUrl } from '../lib/blob-helpers.js';
|
||||||
|
|
||||||
import { MESSAGE_PREVIEW_LENGTH } from '../config.js';
|
import { MESSAGE_PREVIEW_LENGTH } from '../config.js';
|
||||||
|
|
||||||
@@ -48,14 +48,14 @@ class ContactList extends React.Component {
|
|||||||
// If filter function is provided, filter out the items
|
// If filter function is provided, filter out the items
|
||||||
// which don't satisfy the condition.
|
// which don't satisfy the condition.
|
||||||
if (this.props.filterFunc && this.props.filter) {
|
if (this.props.filterFunc && this.props.filter) {
|
||||||
let content = [key];
|
const filterOn = [key];
|
||||||
if (c.private && c.private.comment) {
|
if (c.private && c.private.comment) {
|
||||||
content.push(('' + c.private.comment).toLowerCase());
|
filterOn.push(('' + c.private.comment).toLowerCase());
|
||||||
}
|
}
|
||||||
if (c.public && c.public.fn) {
|
if (c.public && c.public.fn) {
|
||||||
content.push(('' + c.public.fn).toLowerCase());
|
filterOn.push(('' + c.public.fn).toLowerCase());
|
||||||
}
|
}
|
||||||
if (!this.props.filterFunc(this.props.filter, content)) {
|
if (!this.props.filterFunc(this.props.filter, filterOn)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -78,7 +78,7 @@ class ContactList extends React.Component {
|
|||||||
c.private.join(',') : (c.private ? c.private.comment : null);
|
c.private.join(',') : (c.private ? c.private.comment : null);
|
||||||
let preview;
|
let preview;
|
||||||
let deliveryStatus;
|
let deliveryStatus;
|
||||||
if (!this.props.showMode) {
|
if (!this.props.showMode && c.latestMessage) {
|
||||||
const msg = c.latestMessage();
|
const msg = c.latestMessage();
|
||||||
if (msg) {
|
if (msg) {
|
||||||
deliveryStatus = msg._status || c.msgStatus(msg, true);
|
deliveryStatus = msg._status || c.msgStatus(msg, true);
|
||||||
@@ -90,7 +90,7 @@ class ContactList extends React.Component {
|
|||||||
contactNodes.push(
|
contactNodes.push(
|
||||||
<Contact
|
<Contact
|
||||||
title={c.public ? c.public.fn : null}
|
title={c.public ? c.public.fn : null}
|
||||||
avatar={makeImageUrl(c.public ? c.public.photo : null)}
|
avatar={makeImageDataUrl(c.public ? c.public.photo : null)}
|
||||||
comment={comment}
|
comment={comment}
|
||||||
preview={preview}
|
preview={preview}
|
||||||
received={deliveryStatus}
|
received={deliveryStatus}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// A single topic or user.
|
// A single topic or user.
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
|
||||||
|
|
||||||
import ContactBadges from './contact-badges.jsx';
|
import ContactBadges from './contact-badges.jsx';
|
||||||
import LetterTile from './letter-tile.jsx';
|
import LetterTile from './letter-tile.jsx';
|
||||||
@@ -11,7 +11,25 @@ import { Drafty } from 'tinode-sdk';
|
|||||||
|
|
||||||
import { deliveryMarker } from '../lib/utils.js';
|
import { deliveryMarker } from '../lib/utils.js';
|
||||||
|
|
||||||
export default class Contact extends React.Component {
|
const messages = defineMessages({
|
||||||
|
drafty_image: {
|
||||||
|
id: 'drafty_image',
|
||||||
|
defaultMessage: 'Picture',
|
||||||
|
description: 'Comment for embedded images in drafty preview'
|
||||||
|
},
|
||||||
|
drafty_form: {
|
||||||
|
id: 'drafty_form',
|
||||||
|
defaultMessage: 'Form: ',
|
||||||
|
description: 'Comment for form in drafty preview'
|
||||||
|
},
|
||||||
|
drafty_attachment: {
|
||||||
|
id: 'drafty_attachment',
|
||||||
|
defaultMessage: 'Attachment',
|
||||||
|
description: 'Comment for attachment in drafty preview'
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
class Contact extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
@@ -65,7 +83,8 @@ export default class Contact extends React.Component {
|
|||||||
this.props.comment;
|
this.props.comment;
|
||||||
|
|
||||||
const icon = deliveryMarker(this.props.received);
|
const icon = deliveryMarker(this.props.received);
|
||||||
const marker = icon ? <i className={'material-icons small space-right ' + icon.color}>{icon.name}</i> : null;
|
const marker = icon ? <i className={'material-icons small space-right' +
|
||||||
|
(icon.color ? ' ' + icon.color : '')}>{icon.name}</i> : null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li className={!this.props.showCheckmark && this.props.selected ? 'selected' : null} onClick={this.handleClick}>
|
<li className={!this.props.showCheckmark && this.props.selected ? 'selected' : null} onClick={this.handleClick}>
|
||||||
@@ -84,8 +103,9 @@ export default class Contact extends React.Component {
|
|||||||
{this.props.isChannel ? <img src="/img/channel.png" className="channel" alt="channel" /> : null}
|
{this.props.isChannel ? <img src="/img/channel.png" className="channel" alt="channel" /> : null}
|
||||||
<UnreadBadge count={this.props.unread} /><ContactBadges badges={icon_badges} />
|
<UnreadBadge count={this.props.unread} /><ContactBadges badges={icon_badges} />
|
||||||
</div>
|
</div>
|
||||||
<div className="contact-comment">{marker}{subtitle || '\u00A0'}</div>
|
{this.props.showMode ?
|
||||||
<span><ContactBadges badges={badges} /></span>
|
<span><ContactBadges badges={badges} /></span> :
|
||||||
|
<div className="contact-comment">{marker}{subtitle || '\u00A0'}</div>}
|
||||||
</div>
|
</div>
|
||||||
{this.props.showContextMenu ?
|
{this.props.showContextMenu ?
|
||||||
<span className="menuTrigger">
|
<span className="menuTrigger">
|
||||||
@@ -104,7 +124,13 @@ function draftyFormatter(style, data, values, key) {
|
|||||||
let el = Drafty.tagName(style);
|
let el = Drafty.tagName(style);
|
||||||
const attr = { key: key };
|
const attr = { key: key };
|
||||||
if (el) {
|
if (el) {
|
||||||
|
const { formatMessage } = this.props.intl;
|
||||||
switch (style) {
|
switch (style) {
|
||||||
|
case 'BR':
|
||||||
|
// Replace new line with a space.
|
||||||
|
el = React.Fragment;
|
||||||
|
values = [' '];
|
||||||
|
break;
|
||||||
case 'HL':
|
case 'HL':
|
||||||
// Make highlight less prominent in preview.
|
// Make highlight less prominent in preview.
|
||||||
attr.className = 'highlight preview';
|
attr.className = 'highlight preview';
|
||||||
@@ -116,7 +142,7 @@ function draftyFormatter(style, data, values, key) {
|
|||||||
case 'IM':
|
case 'IM':
|
||||||
// Replace image with '[icon] Image'.
|
// Replace image with '[icon] Image'.
|
||||||
el = React.Fragment;
|
el = React.Fragment;
|
||||||
values = [<i className="material-icons">photo</i>, 'Picture'];
|
values = [<i key="im" className="material-icons">photo</i>, formatMessage(messages.drafty_image)];
|
||||||
break;
|
break;
|
||||||
case 'BN':
|
case 'BN':
|
||||||
el = 'span';
|
el = 'span';
|
||||||
@@ -124,19 +150,24 @@ function draftyFormatter(style, data, values, key) {
|
|||||||
break;
|
break;
|
||||||
case 'FM':
|
case 'FM':
|
||||||
el = React.Fragment;
|
el = React.Fragment;
|
||||||
values = [<i className="material-icons">dashboard</i>, 'Form:'];
|
values = [<i key="fm" className="material-icons">dashboard</i>,
|
||||||
|
formatMessage(messages.drafty_form)].concat(values || []);
|
||||||
break;
|
break;
|
||||||
case 'RW':
|
case 'RW':
|
||||||
el = React.Fragment;
|
el = React.Fragment;
|
||||||
break;
|
break;
|
||||||
case 'EX':
|
case 'EX':
|
||||||
|
if (data && data.mime == 'application/json') {
|
||||||
|
// Ignore JSON attachments: they are form response payloads.
|
||||||
|
return null;
|
||||||
|
}
|
||||||
el = React.Fragment;
|
el = React.Fragment;
|
||||||
values = [<i className="material-icons">attachment</i>, 'Attachment'];
|
values = [<i key="ex" className="material-icons">attachment</i>, formatMessage(messages.drafty_attachment)];
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
if (el == '_UNKN') {
|
if (el == '_UNKN') {
|
||||||
el = React.Fragment;
|
el = React.Fragment;
|
||||||
values = [<i className="material-icons">extension</i>];
|
values = [<i key="unkn" className="material-icons">extension</i>];
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -145,3 +176,5 @@ function draftyFormatter(style, data, values, key) {
|
|||||||
return values;
|
return values;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export default injectIntl(Contact);
|
||||||
|
|||||||
@@ -122,7 +122,7 @@ class ContextMenu extends React.Component {
|
|||||||
title: formatMessage(messages.clear_messages),
|
title: formatMessage(messages.clear_messages),
|
||||||
handler: (params, errorHandler) => {
|
handler: (params, errorHandler) => {
|
||||||
return props.onShowAlert(
|
return props.onShowAlert(
|
||||||
formatMessage(messages.menu_item_clear_messages), // title
|
formatMessage(messages.clear_messages), // title
|
||||||
formatMessage(messages.clear_messages_warning), // content
|
formatMessage(messages.clear_messages_warning), // content
|
||||||
(() => { this.deleteMessages(true, false, params, errorHandler); }), // onConfirm
|
(() => { this.deleteMessages(true, false, params, errorHandler); }), // onConfirm
|
||||||
null, // "OK"
|
null, // "OK"
|
||||||
@@ -136,7 +136,7 @@ class ContextMenu extends React.Component {
|
|||||||
title: formatMessage(messages.clear_for_all),
|
title: formatMessage(messages.clear_for_all),
|
||||||
handler: (params, errorHandler) => {
|
handler: (params, errorHandler) => {
|
||||||
return props.onShowAlert(
|
return props.onShowAlert(
|
||||||
formatMessage(messages.menu_item_clear_messages_for_all), // title
|
formatMessage(messages.clear_for_all), // title
|
||||||
formatMessage(message.delete_messages_warning), // content
|
formatMessage(message.delete_messages_warning), // content
|
||||||
(() => { return this.deleteMessages(true, true, params, errorHandler); }),
|
(() => { return this.deleteMessages(true, true, params, errorHandler); }),
|
||||||
null, // "OK"
|
null, // "OK"
|
||||||
@@ -205,7 +205,7 @@ class ContextMenu extends React.Component {
|
|||||||
title: formatMessage(messages.topic_delete),
|
title: formatMessage(messages.topic_delete),
|
||||||
handler: (params, errorHandler) => {
|
handler: (params, errorHandler) => {
|
||||||
return props.onShowAlert(
|
return props.onShowAlert(
|
||||||
formatMessage(messages.menu_item_delete_topic), // title
|
formatMessage(messages.topic_delete), // title
|
||||||
formatMessage(messages.topic_delete_warning), // content
|
formatMessage(messages.topic_delete_warning), // content
|
||||||
(() => {
|
(() => {
|
||||||
const topic = this.props.tinode.getTopic(params.topicName);
|
const topic = this.props.tinode.getTopic(params.topicName);
|
||||||
|
|||||||
@@ -188,10 +188,10 @@ class GroupManager extends React.Component {
|
|||||||
showUnread={false}
|
showUnread={false}
|
||||||
onTopicSelected={this.handleContactSelected} />
|
onTopicSelected={this.handleContactSelected} />
|
||||||
<div id="group-manager-buttons" className="panel-form-row">
|
<div id="group-manager-buttons" className="panel-form-row">
|
||||||
<button className="blue" onClick={this.handleSubmit}>
|
<button className="primary" onClick={this.handleSubmit}>
|
||||||
<FormattedMessage id="button_ok" defaultMessage="OK" description="Button [OK]" />
|
<FormattedMessage id="button_ok" defaultMessage="OK" description="Button [OK]" />
|
||||||
</button>
|
</button>
|
||||||
<button className="white" onClick={this.handleCancel}>
|
<button className="secondary" onClick={this.handleCancel}>
|
||||||
<FormattedMessage id="button_cancel" defaultMessage="Cancel" description="Button [Cancel]" />
|
<FormattedMessage id="button_cancel" defaultMessage="Cancel" description="Button [Cancel]" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { FormattedMessage } from 'react-intl';
|
|||||||
import { MAX_ONLINE_IN_TOPIC } from '../config.js'
|
import { MAX_ONLINE_IN_TOPIC } from '../config.js'
|
||||||
|
|
||||||
import LetterTile from './letter-tile.jsx';
|
import LetterTile from './letter-tile.jsx';
|
||||||
import { makeImageUrl } from '../lib/blob-helpers.js';
|
import { makeImageDataUrl } from '../lib/blob-helpers.js';
|
||||||
|
|
||||||
export default class GroupSubs extends React.Component {
|
export default class GroupSubs extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
@@ -23,7 +23,7 @@ export default class GroupSubs extends React.Component {
|
|||||||
<div className="avatar-box" key={sub.user}>
|
<div className="avatar-box" key={sub.user}>
|
||||||
<LetterTile
|
<LetterTile
|
||||||
topic={sub.user}
|
topic={sub.user}
|
||||||
avatar={makeImageUrl(sub.public ? sub.public.photo : null) || true}
|
avatar={makeImageDataUrl(sub.public ? sub.public.photo : null) || true}
|
||||||
title={sub.public ? sub.public.fn : null} />
|
title={sub.public ? sub.public.fn : null} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -23,15 +23,15 @@ export default class Invitation extends React.PureComponent {
|
|||||||
description="New chat invitation message: [Accept] [Ignore] [Block]." />
|
description="New chat invitation message: [Accept] [Ignore] [Block]." />
|
||||||
</div>
|
</div>
|
||||||
<div className="footer">
|
<div className="footer">
|
||||||
<button className="blue" onClick={event => { this.handleButtonAction(event, "accept"); }}>
|
<button className="primary" onClick={event => { this.handleButtonAction(event, "accept"); }}>
|
||||||
<FormattedMessage id="chat_invitation_accept"
|
<FormattedMessage id="chat_invitation_accept"
|
||||||
defaultMessage="Accept" description="Action [Accept] for chat invitation." />
|
defaultMessage="Accept" description="Action [Accept] for chat invitation." />
|
||||||
</button>
|
</button>
|
||||||
<button className="white" onClick={event => { this.handleButtonAction(event, "delete"); }}>
|
<button className="secondary" onClick={event => { this.handleButtonAction(event, "delete"); }}>
|
||||||
<FormattedMessage id="chat_invitation_ignore"
|
<FormattedMessage id="chat_invitation_ignore"
|
||||||
defaultMessage="Ignore" description="Action [Ignore] for chat invitation." />
|
defaultMessage="Ignore" description="Action [Ignore] for chat invitation." />
|
||||||
</button>
|
</button>
|
||||||
<button className="white" onClick={event => { this.handleButtonAction(event, "block"); }}>
|
<button className="secondary" onClick={event => { this.handleButtonAction(event, "block"); }}>
|
||||||
<FormattedMessage id="chat_invitation_block"
|
<FormattedMessage id="chat_invitation_block"
|
||||||
defaultMessage="Block" description="Action [Block] for chat invitation." />
|
defaultMessage="Block" description="Action [Block] for chat invitation." />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -35,8 +35,9 @@ class NewTopicById extends React.PureComponent {
|
|||||||
handleSubmit(e) {
|
handleSubmit(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (this.state.groupId) {
|
if (this.state.groupId) {
|
||||||
var name = this.state.groupId.trim();
|
const name = this.state.groupId.trim();
|
||||||
if (name.length > 3 && (name.substr(0, 3) == 'usr' || name.substr(0, 3) == 'grp')) {
|
const prefix = name.substr(0, 3);
|
||||||
|
if (name.length > 3 && ['usr', 'grp', 'chn'].includes(prefix)) {
|
||||||
this.props.onSubmit(name);
|
this.props.onSubmit(name);
|
||||||
} else {
|
} else {
|
||||||
this.props.onError(this.props.intl.formatMessage(messages.invalid_id), 'err');
|
this.props.onError(this.props.intl.formatMessage(messages.invalid_id), 'err');
|
||||||
@@ -56,7 +57,7 @@ class NewTopicById extends React.PureComponent {
|
|||||||
}</FormattedMessage>
|
}</FormattedMessage>
|
||||||
</div>
|
</div>
|
||||||
<div className="dialog-buttons">
|
<div className="dialog-buttons">
|
||||||
<button className="blue" onClick={this.handleSubmit}>
|
<button className="primary" onClick={this.handleSubmit}>
|
||||||
<FormattedMessage id="button_subscribe" defaultMessage="Subscribe"
|
<FormattedMessage id="button_subscribe" defaultMessage="Subscribe"
|
||||||
description="Button [Subscribe]" />
|
description="Button [Subscribe]" />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ export default class NewTopicGroup extends React.PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
var submitClasses = 'blue';
|
let submitClasses = 'primary';
|
||||||
if (this.props.disabled) {
|
if (this.props.disabled) {
|
||||||
submitClasses += ' disabled';
|
submitClasses += ' disabled';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
|
|||||||
import CheckBox from './checkbox.jsx';
|
import CheckBox from './checkbox.jsx';
|
||||||
import Contact from './contact.jsx';
|
import Contact from './contact.jsx';
|
||||||
|
|
||||||
import { makeImageUrl } from '../lib/blob-helpers.js';
|
import { makeImageDataUrl } from '../lib/blob-helpers.js';
|
||||||
|
|
||||||
// Translatable permission names.
|
// Translatable permission names.
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
@@ -135,7 +135,7 @@ class PermissionsEditor extends React.Component {
|
|||||||
<ul className="contact-box"><Contact
|
<ul className="contact-box"><Contact
|
||||||
item={this.props.item}
|
item={this.props.item}
|
||||||
title={this.props.userTitle}
|
title={this.props.userTitle}
|
||||||
avatar={makeImageUrl(this.props.userAvatar ? this.props.userAvatar : null)} /></ul> : null}
|
avatar={makeImageDataUrl(this.props.userAvatar ? this.props.userAvatar : null)} /></ul> : null}
|
||||||
<label className="small"><FormattedMessage id="title_permissions"
|
<label className="small"><FormattedMessage id="title_permissions"
|
||||||
defaultMessage="Permissions" description="Section title"/></label>
|
defaultMessage="Permissions" description="Section title"/></label>
|
||||||
<table className="permission-editor">
|
<table className="permission-editor">
|
||||||
@@ -153,7 +153,7 @@ class PermissionsEditor extends React.Component {
|
|||||||
<button className="outline" onClick={this.handleCancel}>
|
<button className="outline" onClick={this.handleCancel}>
|
||||||
<FormattedMessage id="button_cancel" defaultMessage="Cancel" description="Button [Cancel]" />
|
<FormattedMessage id="button_cancel" defaultMessage="Cancel" description="Button [Cancel]" />
|
||||||
</button>
|
</button>
|
||||||
<button className="blue" onClick={this.handleSubmit}>
|
<button className="primary" onClick={this.handleSubmit}>
|
||||||
<FormattedMessage id="button_ok" defaultMessage="OK" description="Button [OK]" />
|
<FormattedMessage id="button_ok" defaultMessage="OK" description="Button [OK]" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ export default class TagManager extends React.Component {
|
|||||||
<button className="outline" onClick={this.handleCancel}>
|
<button className="outline" onClick={this.handleCancel}>
|
||||||
<FormattedMessage id="button_cancel" defaultMessage="Cancel" description="Button [Cancel]" />
|
<FormattedMessage id="button_cancel" defaultMessage="Cancel" description="Button [Cancel]" />
|
||||||
</button>
|
</button>
|
||||||
<button className="blue" onClick={this.handleSubmit}>
|
<button className="primary" onClick={this.handleSubmit}>
|
||||||
<FormattedMessage id="button_ok" defaultMessage="OK" description="Button [OK]" />
|
<FormattedMessage id="button_ok" defaultMessage="OK" description="Button [OK]" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
// This is a generated file. Don't edit.
|
||||||
|
|
||||||
|
const PACKAGE_VERSION = "0.17.1-alpha1";
|
||||||
|
Before Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 473 KiB |
|
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 82 KiB |
|
Before Width: | Height: | Size: 499 KiB |
|
After Width: | Height: | Size: 592 KiB |
|
Before Width: | Height: | Size: 105 KiB |
|
After Width: | Height: | Size: 164 KiB |
|
Before Width: | Height: | Size: 116 KiB |
|
After Width: | Height: | Size: 145 KiB |
|
Before Width: | Height: | Size: 100 KiB |
|
After Width: | Height: | Size: 144 KiB |