Compare commits
51
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d8f7a41bdd | ||
|
|
6c36324ed4 | ||
|
|
e2f3201ddb | ||
|
|
34f17f45b1 | ||
|
|
1cf4722830 | ||
|
|
7c4327ac71 | ||
|
|
48caa605c7 | ||
|
|
edb1383d60 | ||
|
|
2a653f2299 | ||
|
|
eba83fdec8 | ||
|
|
5f173dd395 | ||
|
|
2115f6d638 | ||
|
|
5228d44ade | ||
|
|
ce19ebbbdd | ||
|
|
5e60a5e04d | ||
|
|
9f523b54a4 | ||
|
|
6c13943f1a | ||
|
|
b0bf93a4d9 | ||
|
|
b766e827c9 | ||
|
|
424a8b64bf | ||
|
|
9108efc8c9 | ||
|
|
f94c3612be | ||
|
|
95ddfd21eb | ||
|
|
ae5618c072 | ||
|
|
d083a286df | ||
|
|
e0db170aec | ||
|
|
e9d0f0f1f3 | ||
|
|
08d4c08cbc | ||
|
|
15d24c6607 | ||
|
|
c085ef8ce1 | ||
|
|
2d76befea1 | ||
|
|
20c54fb7e3 | ||
|
|
c5834fad4d | ||
|
|
640704ee2a | ||
|
|
18dd37b402 | ||
|
|
54d6719307 | ||
|
|
255e7fda98 | ||
|
|
1a6e8c8e74 | ||
|
|
b642d05a10 | ||
|
|
920619a988 | ||
|
|
acc24b552d | ||
|
|
2758bb9293 | ||
|
|
4635456c1a | ||
|
|
89f9821255 | ||
|
|
8690ae6a1b | ||
|
|
483d7ef58d | ||
|
|
e1d7ebcc22 | ||
|
|
7d796e3dae | ||
|
|
29cce0a4cc | ||
|
|
c59c3eeb0b | ||
|
|
80d48077c9 |
+106
-16
@@ -4,6 +4,8 @@
|
||||
margin: 0;
|
||||
font-family: Roboto, Arial, sans-serif;
|
||||
text-rendering: optimizeLegibility;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(0,0,0,0.3) rgba(255,255,255,0.1);
|
||||
}
|
||||
|
||||
/* Basic elements defined */
|
||||
@@ -96,6 +98,7 @@ html input[disabled] {
|
||||
cursor: default;
|
||||
opacity: 0.65;
|
||||
box-shadow: none;
|
||||
filter: grayscale(50%);
|
||||
}
|
||||
|
||||
button.round {
|
||||
@@ -117,7 +120,7 @@ button.round.small {
|
||||
box-shadow: 0 2px 0.35rem 0 rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
button:focus, button:hover, button:active {
|
||||
button:hover:not([disabled]), button:active:not([disabled]) {
|
||||
color: #fff;
|
||||
}
|
||||
button:active {
|
||||
@@ -131,7 +134,7 @@ button.primary {
|
||||
background-color: #1e88e5; /* blue 600 */
|
||||
color: white;
|
||||
}
|
||||
button.primary:focus, button.primary:hover, button.primary:active {
|
||||
button.primary:hover:not([disabled]), button.primary:active:not([disabled]) {
|
||||
background-color: #2962ff;
|
||||
}
|
||||
|
||||
@@ -139,7 +142,7 @@ button.secondary {
|
||||
background-color: #FFF;
|
||||
color: #2196f3; /* blue 500 */
|
||||
}
|
||||
button.secondary:focus, button.secondary:hover, button.secondary:active {
|
||||
button.secondary:hover:not([disabled]), button.secondary:active:not([disabled]) {
|
||||
background-color: #e3f2fd;
|
||||
}
|
||||
|
||||
@@ -149,7 +152,7 @@ button.outline {
|
||||
border: 1px solid silver;
|
||||
box-shadow: none;
|
||||
}
|
||||
button.outline:focus, button.outline:hover, button.outline:active {
|
||||
button.outline:hover:not([disabled]), button.outline:active:not([disabled]) {
|
||||
background-color: #e8eaf6; /* indigo 50 */
|
||||
}
|
||||
|
||||
@@ -157,7 +160,7 @@ button.danger {
|
||||
background-color: #ec6560;
|
||||
color: white;
|
||||
}
|
||||
button.danger:focus, button.danger:hover, button.danger:active {
|
||||
button.danger:hover:not([disabled]), button.danger:active:not([disabled]) {
|
||||
background-color: #f77;
|
||||
}
|
||||
|
||||
@@ -165,7 +168,7 @@ button.positive {
|
||||
background-color: #71c46f;
|
||||
color: white;
|
||||
}
|
||||
button.positive:focus, button.positive:hover, button.positive:active {
|
||||
button.positive:hover:not([disabled]), button.positive:active:not([disabled]) {
|
||||
background-color: rgb(127, 237, 127);
|
||||
}
|
||||
|
||||
@@ -184,6 +187,7 @@ input[type="text"],
|
||||
input[type="email"],
|
||||
input[type="password"],
|
||||
input[type="search"],
|
||||
input[type="tel"],
|
||||
textarea {
|
||||
height: 2.5rem;
|
||||
border-bottom: 1px solid #ccc;
|
||||
@@ -194,6 +198,7 @@ textarea {
|
||||
input[type="text"],
|
||||
input[type="email"],
|
||||
input[type="password"],
|
||||
input[type="tel"],
|
||||
textarea {
|
||||
width: 100%;
|
||||
}
|
||||
@@ -202,7 +207,8 @@ textarea:focus,
|
||||
input[type="text"]:focus,
|
||||
input[type="email"]:focus,
|
||||
input[type="password"]:focus,
|
||||
input[type="search"]:focus {
|
||||
input[type="search"]:focus,
|
||||
input[type="tel"]:focus {
|
||||
border-bottom: 1px solid #2196f3;
|
||||
box-shadow: 0 1px 0 rgba(33, 150, 243, 0.25);
|
||||
}
|
||||
@@ -211,7 +217,8 @@ textarea.invalid,
|
||||
input[type="text"].invalid,
|
||||
input[type="email"].invalid,
|
||||
input[type="password"].invalid,
|
||||
input[type="search"].invalid {
|
||||
input[type="search"].invalid,
|
||||
input[type="tel"].invalid {
|
||||
border-bottom: 1px solid #e57373;
|
||||
box-shadow: 0 1px 0 rgba(229, 115, 115, 0.25);
|
||||
}
|
||||
@@ -220,6 +227,7 @@ input[type="text"]::placeholder,
|
||||
input[type="email"]::placeholder,
|
||||
input[type="password"]::placeholder,
|
||||
input[type="search"]::placeholder,
|
||||
input[type="tel"]::placeholder,
|
||||
textarea::placeholder {
|
||||
color: #bbb;
|
||||
font-weight: lighter;
|
||||
@@ -327,13 +335,16 @@ li:hover .menuTrigger a {
|
||||
div.alert-container {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
padding: 1rem;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background: rgba(240,240,240,0.85);
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
div.alert {
|
||||
position: relative;
|
||||
background-color: white;
|
||||
@@ -342,10 +353,15 @@ div.alert {
|
||||
border-radius: 0.3rem;
|
||||
margin: auto 2rem auto 2rem;
|
||||
width: fit-content;
|
||||
height: min-content;
|
||||
min-width: 16rem;
|
||||
max-width: 24rem;
|
||||
max-height: 40rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
div.alert-container .title {
|
||||
position: relative;
|
||||
color: #2196f3;
|
||||
font-size: 1.2rem;
|
||||
font-weight: 500;
|
||||
@@ -354,6 +370,7 @@ div.alert-container .title {
|
||||
}
|
||||
|
||||
div.alert-container .title.with-control {
|
||||
position: relative;
|
||||
display: flex;
|
||||
padding: 0.5rem 0.5rem 0 0.5rem;
|
||||
align-items: center;
|
||||
@@ -362,6 +379,7 @@ div.alert-container .title.with-control {
|
||||
}
|
||||
|
||||
div.alert-container .content {
|
||||
position: relative;
|
||||
padding: 0.5rem 0 0.5rem 0;
|
||||
}
|
||||
/* END Alert */
|
||||
@@ -756,6 +774,7 @@ table.permission-editor td.checkbox {
|
||||
display: flex;
|
||||
padding: 0.5rem;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.dialog-buttons button {
|
||||
@@ -1605,13 +1624,26 @@ img.channel {
|
||||
}
|
||||
|
||||
.controls {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
column-gap: 1rem;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 3.2rem;
|
||||
width: 100%;
|
||||
bottom: 2.5rem;
|
||||
z-index: 1;
|
||||
background-color: #eceff1;
|
||||
box-shadow: 0px -2px 3px -1px rgba(0,0,0,0.1); /* Faint shadow above panel */
|
||||
}
|
||||
|
||||
.incoming-call .controls {
|
||||
bottom: 1rem;
|
||||
}
|
||||
|
||||
.controls button {
|
||||
border-radius: 50%;
|
||||
width: 4rem;
|
||||
height: 4rem;
|
||||
min-width: initial;
|
||||
box-shadow: 0 3px 6px 0 rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
#video-container-panel {
|
||||
@@ -1649,6 +1681,10 @@ img.channel {
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.call-party.self[disabled] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
@@ -1659,6 +1695,10 @@ video {
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
|
||||
.call-party[disabled] video {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.caller-card {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
@@ -1737,7 +1777,7 @@ video {
|
||||
left: initial;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 5rem 3.5rem 2rem 3.5rem;
|
||||
padding: 4rem 3.5rem 5.5rem 3.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -1896,10 +1936,8 @@ video {
|
||||
bottom: 0;
|
||||
background-color: #c5e1a5;
|
||||
clip-path: polygon(100% 0, 0 110%, 100% 110%);
|
||||
-webkit-clip-path: polygon(100% 0, 0 110%, 100% 110%);
|
||||
box-shadow: 0 1px 0.5px rgb(0 0 0 / 12%);
|
||||
border-bottom-left-radius: 0.3rem;
|
||||
-moz-transition: ease-out background-color 0.5s;
|
||||
transition: ease-out background-color 0.5s;
|
||||
transition-delay: 0.3s;
|
||||
}
|
||||
@@ -1918,10 +1956,8 @@ video {
|
||||
bottom: 0;
|
||||
background-color: #fafafa;
|
||||
clip-path: polygon(0 0, 100% 110%, 0 110%);
|
||||
-webkit-clip-path: polygon(0 0, 100% 110%, 0 110%);
|
||||
box-shadow: 0 1px 0.5px rgb(0 0 0 / 12%);
|
||||
border-bottom-right-radius: 0.3rem;
|
||||
-moz-transition: ease-out background-color 0.5s;
|
||||
transition: ease-out background-color 0.5s;
|
||||
transition-delay: 0.3s;
|
||||
}
|
||||
@@ -2997,6 +3033,55 @@ ul.tabbar li.active > a {
|
||||
outline: 0.5rem solid rgba(255,255,255,.95);
|
||||
}
|
||||
|
||||
.country-flag {
|
||||
display: inline-block;
|
||||
font-size: larger;
|
||||
}
|
||||
|
||||
.dial-code {
|
||||
white-space: nowrap;
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
vertical-align: middle;
|
||||
margin-bottom: 0.15rem;
|
||||
}
|
||||
|
||||
ul.phone-country-selector {
|
||||
list-style: none;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
ul.phone-country-selector > li {
|
||||
position: relative;
|
||||
font-weight: lighter;
|
||||
padding: 0.25rem 0.5rem 0.15rem 0.5rem;
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
ul.phone-country-selector > li:hover {
|
||||
background-color: #eceff1; /* blue-gray 50 highlight */
|
||||
}
|
||||
ul.phone-country-selector > li.selected {
|
||||
background-color: #cfd8dc; /* blue-gray 100 highlight */
|
||||
}
|
||||
|
||||
ul.phone-country-selector .dial-code {
|
||||
position: absolute;
|
||||
right: 0.5rem;
|
||||
color: #1e88e5;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
ul.phone-country-selector span {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
ul.phone-country-selector span.country {
|
||||
text-overflow: ellipsis;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/*
|
||||
Very wide monitor.
|
||||
*/
|
||||
@@ -3042,4 +3127,9 @@ ul.tabbar li.active > a {
|
||||
li .menuTrigger {
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
#send-message-input {
|
||||
/* Just an indicator that the screen is in mobile mode */
|
||||
transition-property: -tinode-mobile;
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
+3
-1
@@ -9,5 +9,7 @@ const FIREBASE_INIT = {
|
||||
// Project Settings -> Cloud Messaging -> Web configuration -> Web Push certificates.
|
||||
// This value IS NOT included in the generated js config you get from
|
||||
// the firebase console. It needs to be added separately.
|
||||
messagingVapidKey: "BOgQVPOMzIMXUpsYGpbVkZoEBc0ifKY_f2kSU5DNDGYI6i6CoKqqxDd7w7PJ3FaGRBgVGJffldETumOx831jl58"
|
||||
messagingVapidKey: "BOgQVPOMzIMXUpsYGpbVkZoEBc0ifKY_f2kSU5DNDGYI6i6CoKqqxDd7w7PJ3FaGRBgVGJffldETumOx831jl58",
|
||||
// Google analytics tag. Uncomment and use one you got from https://analytics.google.com/.
|
||||
// measurementId: "G-ABCDEF1234"
|
||||
};
|
||||
|
||||
Generated
+518
-510
File diff suppressed because it is too large
Load Diff
+5
-2
@@ -1,12 +1,13 @@
|
||||
{
|
||||
"name": "tinode-webapp",
|
||||
"description": "Tinode messenger for the web",
|
||||
"version": "0.21.0-beta1",
|
||||
"version": "0.21.0-rc2",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/tinode/example-react-js.git"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "echo 'Read installation instructions'",
|
||||
"build": "npm run vers && npm run build:css && npm run extract && npm run build:i18n && npm run build:prod && npm run build:dev",
|
||||
"clean": "rm umd/* && rm css/base.min.css && rm i18n.min/*",
|
||||
"build:css": "postcss css/base.css > css/base.min.css",
|
||||
@@ -48,6 +49,7 @@
|
||||
"de",
|
||||
"en",
|
||||
"es",
|
||||
"fr",
|
||||
"ko",
|
||||
"ro",
|
||||
"ru",
|
||||
@@ -64,10 +66,11 @@
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"firebase": "^9.14.0",
|
||||
"libphonenumber-js": "^1.10.18",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-intl": "^6.2.1",
|
||||
"tinode-sdk": "^0.21.0-beta1",
|
||||
"tinode-sdk": "^0.21.0-rc2",
|
||||
"webm-duration-fix": "^1.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
+5
-5
@@ -64,16 +64,16 @@ const fbMessaging = firebase.messaging();
|
||||
|
||||
// This method shows the push notifications while the window is in background.
|
||||
fbMessaging.onBackgroundMessage(payload => {
|
||||
if (payload.data.silent == 'true') {
|
||||
// TODO: if this is an 'msg', fetch the new message in the background.
|
||||
return;
|
||||
}
|
||||
|
||||
// Notify webapp that a message was received.
|
||||
if (webAppChannel) {
|
||||
webAppChannel.postMessage(payload.data);
|
||||
}
|
||||
|
||||
if (payload.data.silent == 'true') {
|
||||
// No need to show anything.
|
||||
return;
|
||||
}
|
||||
|
||||
const titles = {'msg': 'new_message', 'sub': 'new_chat'};
|
||||
const pushType = payload.data.what || 'msg';
|
||||
const title = payload.data.title || self.i18nMessage(titles[pushType]);
|
||||
|
||||
+1
-1
@@ -4,5 +4,5 @@ export const CALL_STATE_OUTGOING_INITATED = 1;
|
||||
export const CALL_STATE_INCOMING_RECEIVED = 2;
|
||||
export const CALL_STATE_IN_PROGRESS = 3;
|
||||
|
||||
// Video call drafty headers representing call state.
|
||||
// Video call message headers representing call state.
|
||||
export const CALL_HEAD_STARTED = 'started';
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+52
-3
@@ -358,7 +358,7 @@
|
||||
},
|
||||
"enter_confirmation_code_prompt": {
|
||||
"translation": "Geben Sie den Bestätigungscode ein, der per {method} geschickt wurde:",
|
||||
"defaultMessage": "Enter confirmation code sent to you by {method}:",
|
||||
"defaultMessage": "Confirmation code",
|
||||
"description": "Request to enter confirmation code",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
@@ -533,7 +533,7 @@
|
||||
},
|
||||
"label_reset_password": {
|
||||
"translation": "Passwort per E-Mail wiederherstellen:",
|
||||
"defaultMessage": "Send a password reset email:",
|
||||
"defaultMessage": "Send a password reset email",
|
||||
"description": "Label for password reset field",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
@@ -1308,7 +1308,7 @@
|
||||
},
|
||||
"password_reset_email_sent": {
|
||||
"translation": "Eine E-Mail wurde an {email} gesendet. Folgen Sie den Anweisungen in der E-Mail, um Ihr Passwort zurückzusetzen.",
|
||||
"defaultMessage": "An email has been sent to {email}. Follow the directions in the email to reset your password.",
|
||||
"defaultMessage": "An email with security code has been sent to {email}. Enter the code below.",
|
||||
"description": "Notification that the email with password reset instructions has been sent",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
@@ -1543,11 +1543,60 @@
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"menu_item_audio_call": {
|
||||
"translation": "Anruf",
|
||||
"defaultMessage": "Call",
|
||||
"description": "Start audio call",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"unrecognized_video_format": {
|
||||
"translation": "Das Format dieses Videos wird nicht erkannt",
|
||||
"defaultMessage": "Format of this video is not recognized",
|
||||
"description": "Error message when uploaded video is invalid",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"loading_note": {
|
||||
"translation": "Laden...",
|
||||
"defaultMessage": "Loading...",
|
||||
"description": "Message shown when component is loading",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"password_i_have_code": {
|
||||
"translation": "Ich habe Code",
|
||||
"defaultMessage": "I have code",
|
||||
"description": "Call to open field to enter password reset code",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"label_reset_password_tel": {
|
||||
"translation": "SMS zum Zurücksetzen des Passworts senden",
|
||||
"defaultMessage": "Send SMS to reset password",
|
||||
"description": "Label for password reset field by phone",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"mobile_phone_number": {
|
||||
"translation": "Mobiltelefonnummer",
|
||||
"defaultMessage": "Mobile phone number",
|
||||
"description": "Prompt for entering a mobile phone number",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"password_reset_sms_sent": {
|
||||
"translation": "Eine Textnachricht mit Sicherheitscode wurde an {phone} gesendet. Geben Sie den Code unten ein.",
|
||||
"defaultMessage": "A text message with security code has been sent to {phone}. Enter the code below.",
|
||||
"description": "Notification that the SMS with password reset instructions has been sent",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"mobile_number_required": {
|
||||
"translation": "Handynummer erforderlich",
|
||||
"defaultMessage": "Mobile phone number required",
|
||||
"description": "Error message",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
}
|
||||
}
|
||||
|
||||
+53
-4
@@ -357,8 +357,8 @@
|
||||
"obsolete": false
|
||||
},
|
||||
"enter_confirmation_code_prompt": {
|
||||
"translation": "Enter confirmation code sent to you by {method}:",
|
||||
"defaultMessage": "Enter confirmation code sent to you by {method}:",
|
||||
"translation": "Confirmation code",
|
||||
"defaultMessage": "Confirmation code",
|
||||
"description": "Request to enter confirmation code",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
@@ -533,7 +533,7 @@
|
||||
},
|
||||
"label_reset_password": {
|
||||
"translation": "Send a password reset email:",
|
||||
"defaultMessage": "Send a password reset email:",
|
||||
"defaultMessage": "Send a password reset email",
|
||||
"description": "Label for password reset field",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
@@ -1315,7 +1315,7 @@
|
||||
},
|
||||
"password_reset_email_sent": {
|
||||
"translation": "An email has been sent to {email}. Follow the directions in the email to reset your password.",
|
||||
"defaultMessage": "An email has been sent to {email}. Follow the directions in the email to reset your password.",
|
||||
"defaultMessage": "An email with security code has been sent to {email}. Enter the code below.",
|
||||
"description": "Notification that the email with password reset instructions has been sent",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
@@ -1543,11 +1543,60 @@
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"menu_item_audio_call": {
|
||||
"translation": "Call",
|
||||
"defaultMessage": "Call",
|
||||
"description": "Start audio call",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"unrecognized_video_format": {
|
||||
"translation": "Format of this video is not recognized",
|
||||
"defaultMessage": "Format of this video is not recognized",
|
||||
"description": "Error message when uploaded video is invalid",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"loading_note": {
|
||||
"translation": "Loading...",
|
||||
"defaultMessage": "Loading...",
|
||||
"description": "Message shown when component is loading",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"password_i_have_code": {
|
||||
"translation": "I have code",
|
||||
"defaultMessage": "I have code",
|
||||
"description": "Call to open field to enter password reset code",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"label_reset_password_tel": {
|
||||
"translation": "Send a password reset SMS:",
|
||||
"defaultMessage": "Send SMS to reset password",
|
||||
"description": "Label for password reset field by phone",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"mobile_phone_number": {
|
||||
"translation": "Mobile phone number",
|
||||
"defaultMessage": "Mobile phone number",
|
||||
"description": "Prompt for entering a mobile phone number",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"password_reset_sms_sent": {
|
||||
"translation": "A text message with security code has been sent to {phone}. Enter the code below.",
|
||||
"defaultMessage": "A text message with security code has been sent to {phone}. Enter the code below.",
|
||||
"description": "Notification that the SMS with password reset instructions has been sent",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"mobile_number_required": {
|
||||
"translation": "Mobile phone number required",
|
||||
"defaultMessage": "Mobile phone number required",
|
||||
"description": "Error message",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
}
|
||||
}
|
||||
|
||||
+52
-3
@@ -358,7 +358,7 @@
|
||||
},
|
||||
"enter_confirmation_code_prompt": {
|
||||
"translation": "Introduzca el código de confirmación enviado a tu {method}:",
|
||||
"defaultMessage": "Enter confirmation code sent to you by {method}:",
|
||||
"defaultMessage": "Confirmation code",
|
||||
"description": "Request to enter confirmation code",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
@@ -533,7 +533,7 @@
|
||||
},
|
||||
"label_reset_password": {
|
||||
"translation": "Enviar un correo electrónico de restablecimiento de contraseña:",
|
||||
"defaultMessage": "Send a password reset email:",
|
||||
"defaultMessage": "Send a password reset email",
|
||||
"description": "Label for password reset field",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
@@ -1308,7 +1308,7 @@
|
||||
},
|
||||
"password_reset_email_sent": {
|
||||
"translation": "Se ha enviado un correo electrónico a {email}. Siga las instrucciones del correo electrónico para restablecer su contraseña.",
|
||||
"defaultMessage": "An email has been sent to {email}. Follow the directions in the email to reset your password.",
|
||||
"defaultMessage": "An email with security code has been sent to {email}. Enter the code below.",
|
||||
"description": "Notification that the email with password reset instructions has been sent",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
@@ -1543,11 +1543,60 @@
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"menu_item_audio_call": {
|
||||
"translation": "Llamada",
|
||||
"defaultMessage": "Call",
|
||||
"description": "Start audio call",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"unrecognized_video_format": {
|
||||
"translation": "No se reconoce el formato de este video",
|
||||
"defaultMessage": "Format of this video is not recognized",
|
||||
"description": "Error message when uploaded video is invalid",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"loading_note": {
|
||||
"translation": "Cargando...",
|
||||
"defaultMessage": "Loading...",
|
||||
"description": "Message shown when component is loading",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"password_i_have_code": {
|
||||
"translation": "Tengo código",
|
||||
"defaultMessage": "I have code",
|
||||
"description": "Call to open field to enter password reset code",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"label_reset_password_tel": {
|
||||
"translation": "Enviar SMS para restablecer la contraseña",
|
||||
"defaultMessage": "Send SMS to reset password",
|
||||
"description": "Label for password reset field by phone",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"mobile_phone_number": {
|
||||
"translation": "Número de teléfono móvil",
|
||||
"defaultMessage": "Mobile phone number",
|
||||
"description": "Prompt for entering a mobile phone number",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"password_reset_sms_sent": {
|
||||
"translation": "Se ha enviado un mensaje de texto con un código de seguridad a {phone}. Ingresa el código a continuación.",
|
||||
"defaultMessage": "A text message with security code has been sent to {phone}. Enter the code below.",
|
||||
"description": "Notification that the SMS with password reset instructions has been sent",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"mobile_number_required": {
|
||||
"translation": "Número de teléfono móvil requerido",
|
||||
"defaultMessage": "Mobile phone number required",
|
||||
"description": "Error message",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -268,7 +268,7 @@
|
||||
"description": "Call to action to enable peer's messaging"
|
||||
},
|
||||
"enter_confirmation_code_prompt": {
|
||||
"defaultMessage": "Enter confirmation code sent to you by {method}:",
|
||||
"defaultMessage": "Confirmation code",
|
||||
"description": "Request to enter confirmation code"
|
||||
},
|
||||
"error_invalid_id": {
|
||||
@@ -415,9 +415,13 @@
|
||||
"description": "Label for push notifications switch"
|
||||
},
|
||||
"label_reset_password": {
|
||||
"defaultMessage": "Send a password reset email:",
|
||||
"defaultMessage": "Send a password reset email",
|
||||
"description": "Label for password reset field"
|
||||
},
|
||||
"label_reset_password_tel": {
|
||||
"defaultMessage": "Send SMS to reset password",
|
||||
"description": "Label for password reset field by phone"
|
||||
},
|
||||
"label_sdk": {
|
||||
"defaultMessage": "SDK:"
|
||||
},
|
||||
@@ -492,6 +496,10 @@
|
||||
"defaultMessage": "Terms of Service",
|
||||
"description": "Ancor text for terms of service link"
|
||||
},
|
||||
"loading_note": {
|
||||
"defaultMessage": "Loading...",
|
||||
"description": "Message shown when component is loading"
|
||||
},
|
||||
"login_prompt": {
|
||||
"defaultMessage": "Login",
|
||||
"description": "Placeholer for username/login"
|
||||
@@ -500,6 +508,10 @@
|
||||
"defaultMessage": "Archive",
|
||||
"description": "Move topic from the list of active chats to archive"
|
||||
},
|
||||
"menu_item_audio_call": {
|
||||
"defaultMessage": "Call",
|
||||
"description": "Start audio call"
|
||||
},
|
||||
"menu_item_block": {
|
||||
"defaultMessage": "Block",
|
||||
"description": "Block topic or user"
|
||||
@@ -592,6 +604,14 @@
|
||||
"defaultMessage": "Messaging disabled",
|
||||
"description": "Prompt in SendMessage in read-only topic"
|
||||
},
|
||||
"mobile_number_required": {
|
||||
"defaultMessage": "Mobile phone number required",
|
||||
"description": "Error message"
|
||||
},
|
||||
"mobile_phone_number": {
|
||||
"defaultMessage": "Mobile phone number",
|
||||
"description": "Prompt for entering a mobile phone number"
|
||||
},
|
||||
"more_online_members": {
|
||||
"defaultMessage": "+{overflow} more",
|
||||
"description": "Shown in MessagesView title bar when the number of online subscribers exceeds MAX_ONLINE_IN_TOPIC"
|
||||
@@ -640,14 +660,22 @@
|
||||
"defaultMessage": "Security",
|
||||
"description": "Title for TopicSecirity and AccSecurity."
|
||||
},
|
||||
"password_i_have_code": {
|
||||
"defaultMessage": "I have code",
|
||||
"description": "Call to open field to enter password reset code"
|
||||
},
|
||||
"password_prompt": {
|
||||
"defaultMessage": "Password",
|
||||
"description": "Placeholder/prompt for entering password"
|
||||
},
|
||||
"password_reset_email_sent": {
|
||||
"defaultMessage": "An email has been sent to {email}. Follow the directions in the email to reset your password.",
|
||||
"defaultMessage": "An email with security code has been sent to {email}. Enter the code below.",
|
||||
"description": "Notification that the email with password reset instructions has been sent"
|
||||
},
|
||||
"password_reset_sms_sent": {
|
||||
"defaultMessage": "A text message with security code has been sent to {phone}. Enter the code below.",
|
||||
"description": "Notification that the SMS with password reset instructions has been sent"
|
||||
},
|
||||
"password_unchanged_prompt": {
|
||||
"defaultMessage": "Unchanged",
|
||||
"description": "Message in editor while password is unchanged"
|
||||
|
||||
+52
-3
@@ -358,7 +358,7 @@
|
||||
},
|
||||
"enter_confirmation_code_prompt": {
|
||||
"translation": "Entrez le code de confirmation envoyé par {method}:",
|
||||
"defaultMessage": "Enter confirmation code sent to you by {method}:",
|
||||
"defaultMessage": "Confirmation code",
|
||||
"description": "Request to enter confirmation code",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
@@ -533,7 +533,7 @@
|
||||
},
|
||||
"label_reset_password": {
|
||||
"translation": "Envoyer un mail de remise à zéro de mot de passe :",
|
||||
"defaultMessage": "Send a password reset email:",
|
||||
"defaultMessage": "Send a password reset email",
|
||||
"description": "Label for password reset field",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
@@ -1315,7 +1315,7 @@
|
||||
},
|
||||
"password_reset_email_sent": {
|
||||
"translation": "Un email a été envoyé à {email}. Suivez les instructions du mail pour remettre à zéro votre mot de passe.",
|
||||
"defaultMessage": "An email has been sent to {email}. Follow the directions in the email to reset your password.",
|
||||
"defaultMessage": "An email with security code has been sent to {email}. Enter the code below.",
|
||||
"description": "Notification that the email with password reset instructions has been sent",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
@@ -1543,11 +1543,60 @@
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"menu_item_audio_call": {
|
||||
"translation": "Appel",
|
||||
"defaultMessage": "Call",
|
||||
"description": "Start audio call",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"unrecognized_video_format": {
|
||||
"translation": "Le format de cette vidéo n'est pas reconnu",
|
||||
"defaultMessage": "Format of this video is not recognized",
|
||||
"description": "Error message when uploaded video is invalid",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"loading_note": {
|
||||
"translation": "Chargement...",
|
||||
"defaultMessage": "Loading...",
|
||||
"description": "Message shown when component is loading",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"password_i_have_code": {
|
||||
"translation": "J'ai du code",
|
||||
"defaultMessage": "I have code",
|
||||
"description": "Call to open field to enter password reset code",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"label_reset_password_tel": {
|
||||
"translation": "Envoyer un SMS pour réinitialiser le mot de passe",
|
||||
"defaultMessage": "Send SMS to reset password",
|
||||
"description": "Label for password reset field by phone",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"mobile_phone_number": {
|
||||
"translation": "Numéro de téléphone mobile",
|
||||
"defaultMessage": "Mobile phone number",
|
||||
"description": "Prompt for entering a mobile phone number",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"password_reset_sms_sent": {
|
||||
"translation": "Un SMS avec un code de sécurité a été envoyé à {phone}. Saisissez le code ci-dessous.",
|
||||
"defaultMessage": "A text message with security code has been sent to {phone}. Enter the code below.",
|
||||
"description": "Notification that the SMS with password reset instructions has been sent",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"mobile_number_required": {
|
||||
"translation": "Numéro de téléphone portable requis",
|
||||
"defaultMessage": "Mobile phone number required",
|
||||
"description": "Error message",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
}
|
||||
}
|
||||
|
||||
+52
-3
@@ -358,7 +358,7 @@
|
||||
},
|
||||
"enter_confirmation_code_prompt": {
|
||||
"translation": "{방법}으로보낸확인코드를입력하십시오:",
|
||||
"defaultMessage": "Enter confirmation code sent to you by {method}:",
|
||||
"defaultMessage": "Confirmation code",
|
||||
"description": "Request to enter confirmation code",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
@@ -533,7 +533,7 @@
|
||||
},
|
||||
"label_reset_password": {
|
||||
"translation": "비밀번호재설정이메일보내기: ",
|
||||
"defaultMessage": "Send a password reset email:",
|
||||
"defaultMessage": "Send a password reset email",
|
||||
"description": "Label for password reset field",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
@@ -1308,7 +1308,7 @@
|
||||
},
|
||||
"password_reset_email_sent": {
|
||||
"translation": "이메일이 {email}(으)로 전송되었습니다. 이메일의 지침에 따라 비밀번호를 재설정하세요.",
|
||||
"defaultMessage": "An email has been sent to {email}. Follow the directions in the email to reset your password.",
|
||||
"defaultMessage": "An email with security code has been sent to {email}. Enter the code below.",
|
||||
"description": "Notification that the email with password reset instructions has been sent",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
@@ -1543,11 +1543,60 @@
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"menu_item_audio_call": {
|
||||
"translation": "부르다",
|
||||
"defaultMessage": "Call",
|
||||
"description": "Start audio call",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"unrecognized_video_format": {
|
||||
"translation": "이 동영상의 형식을 인식할 수 없습니다.",
|
||||
"defaultMessage": "Format of this video is not recognized",
|
||||
"description": "Error message when uploaded video is invalid",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"loading_note": {
|
||||
"translation": "로드 중...",
|
||||
"defaultMessage": "Loading...",
|
||||
"description": "Message shown when component is loading",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"password_i_have_code": {
|
||||
"translation": "코드가 있습니다",
|
||||
"defaultMessage": "I have code",
|
||||
"description": "Call to open field to enter password reset code",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"label_reset_password_tel": {
|
||||
"translation": "비밀번호 재설정을 위해 SMS 보내기",
|
||||
"defaultMessage": "Send SMS to reset password",
|
||||
"description": "Label for password reset field by phone",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"mobile_phone_number": {
|
||||
"translation": "휴대전화 번호",
|
||||
"defaultMessage": "Mobile phone number",
|
||||
"description": "Prompt for entering a mobile phone number",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"password_reset_sms_sent": {
|
||||
"translation": "보안 코드가 포함된 문자 메시지가 {phone}로 전송되었습니다. 아래 코드를 입력하세요.",
|
||||
"defaultMessage": "A text message with security code has been sent to {phone}. Enter the code below.",
|
||||
"description": "Notification that the SMS with password reset instructions has been sent",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"mobile_number_required": {
|
||||
"translation": "휴대전화 번호 필요",
|
||||
"defaultMessage": "Mobile phone number required",
|
||||
"description": "Error message",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
}
|
||||
}
|
||||
|
||||
+52
-3
@@ -358,7 +358,7 @@
|
||||
},
|
||||
"enter_confirmation_code_prompt": {
|
||||
"translation": "Introu codul de confirmare trimis pe {method}:",
|
||||
"defaultMessage": "Enter confirmation code sent to you by {method}:",
|
||||
"defaultMessage": "Confirmation code",
|
||||
"description": "Request to enter confirmation code",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
@@ -533,7 +533,7 @@
|
||||
},
|
||||
"label_reset_password": {
|
||||
"translation": "Resetează parola:",
|
||||
"defaultMessage": "Send a password reset email:",
|
||||
"defaultMessage": "Send a password reset email",
|
||||
"description": "Label for password reset field",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
@@ -1308,7 +1308,7 @@
|
||||
},
|
||||
"password_reset_email_sent": {
|
||||
"translation": "Un e-mail a fost trimis către {email}. Urmați instrucțiunile din e-mail pentru a vă reseta parola.",
|
||||
"defaultMessage": "An email has been sent to {email}. Follow the directions in the email to reset your password.",
|
||||
"defaultMessage": "An email with security code has been sent to {email}. Enter the code below.",
|
||||
"description": "Notification that the email with password reset instructions has been sent",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
@@ -1543,11 +1543,60 @@
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"menu_item_audio_call": {
|
||||
"translation": "Apel",
|
||||
"defaultMessage": "Call",
|
||||
"description": "Start audio call",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"unrecognized_video_format": {
|
||||
"translation": "Formatul acestui videoclip nu este recunoscut",
|
||||
"defaultMessage": "Format of this video is not recognized",
|
||||
"description": "Error message when uploaded video is invalid",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"loading_note": {
|
||||
"translation": "Se încarcă...",
|
||||
"defaultMessage": "Loading...",
|
||||
"description": "Message shown when component is loading",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"password_i_have_code": {
|
||||
"translation": "Am cod",
|
||||
"defaultMessage": "I have code",
|
||||
"description": "Call to open field to enter password reset code",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"label_reset_password_tel": {
|
||||
"translation": "Trimite SMS pentru a reseta parola",
|
||||
"defaultMessage": "Send SMS to reset password",
|
||||
"description": "Label for password reset field by phone",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"mobile_phone_number": {
|
||||
"translation": "Număr de telefon mobil",
|
||||
"defaultMessage": "Mobile phone number",
|
||||
"description": "Prompt for entering a mobile phone number",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"password_reset_sms_sent": {
|
||||
"translation": "Un mesaj text cu cod de securitate a fost trimis la {phone}. Introduceți codul de mai jos.",
|
||||
"defaultMessage": "A text message with security code has been sent to {phone}. Enter the code below.",
|
||||
"description": "Notification that the SMS with password reset instructions has been sent",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"mobile_number_required": {
|
||||
"translation": "Este necesar un număr de telefon mobil",
|
||||
"defaultMessage": "Mobile phone number required",
|
||||
"description": "Error message",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
}
|
||||
}
|
||||
|
||||
+52
-3
@@ -358,7 +358,7 @@
|
||||
},
|
||||
"enter_confirmation_code_prompt": {
|
||||
"translation": "Код подтверждения, полученный по {method}:",
|
||||
"defaultMessage": "Enter confirmation code sent to you by {method}:",
|
||||
"defaultMessage": "Confirmation code",
|
||||
"description": "Request to enter confirmation code",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
@@ -533,7 +533,7 @@
|
||||
},
|
||||
"label_reset_password": {
|
||||
"translation": "Отправить емейл для смены пароля:",
|
||||
"defaultMessage": "Send a password reset email:",
|
||||
"defaultMessage": "Send a password reset email",
|
||||
"description": "Label for password reset field",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
@@ -1308,7 +1308,7 @@
|
||||
},
|
||||
"password_reset_email_sent": {
|
||||
"translation": "Сообщение было отправлено на адрес {email}. Следуйте инструкциям в сообщении, чтобы изменить пароль.",
|
||||
"defaultMessage": "An email has been sent to {email}. Follow the directions in the email to reset your password.",
|
||||
"defaultMessage": "An email with security code has been sent to {email}. Enter the code below.",
|
||||
"description": "Notification that the email with password reset instructions has been sent",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
@@ -1543,11 +1543,60 @@
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"menu_item_audio_call": {
|
||||
"translation": "Позвонить",
|
||||
"defaultMessage": "Call",
|
||||
"description": "Start audio call",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"unrecognized_video_format": {
|
||||
"translation": "Неизвестный формат видео файла",
|
||||
"defaultMessage": "Format of this video is not recognized",
|
||||
"description": "Error message when uploaded video is invalid",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"loading_note": {
|
||||
"translation": "Загружается...",
|
||||
"defaultMessage": "Loading...",
|
||||
"description": "Message shown when component is loading",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"password_i_have_code": {
|
||||
"translation": "У меня есть код",
|
||||
"defaultMessage": "I have code",
|
||||
"description": "Call to open field to enter password reset code",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"label_reset_password_tel": {
|
||||
"translation": "Отправить SMS для сброса пароля",
|
||||
"defaultMessage": "Send SMS to reset password",
|
||||
"description": "Label for password reset field by phone",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"mobile_phone_number": {
|
||||
"translation": "Номер мобильного телефона",
|
||||
"defaultMessage": "Mobile phone number",
|
||||
"description": "Prompt for entering a mobile phone number",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"password_reset_sms_sent": {
|
||||
"translation": "SMS с защитным кодом отправлено на {phone}. Введите код ниже.",
|
||||
"defaultMessage": "A text message with security code has been sent to {phone}. Enter the code below.",
|
||||
"description": "Notification that the SMS with password reset instructions has been sent",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"mobile_number_required": {
|
||||
"translation": "Номер мобильного телефона",
|
||||
"defaultMessage": "Mobile phone number required",
|
||||
"description": "Error message",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
}
|
||||
}
|
||||
|
||||
+52
-3
@@ -358,7 +358,7 @@
|
||||
},
|
||||
"enter_confirmation_code_prompt": {
|
||||
"translation": "輸入透過 {method} 傳送給您的確認碼:",
|
||||
"defaultMessage": "Enter confirmation code sent to you by {method}:",
|
||||
"defaultMessage": "Confirmation code",
|
||||
"description": "Request to enter confirmation code",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
@@ -533,7 +533,7 @@
|
||||
},
|
||||
"label_reset_password": {
|
||||
"translation": "傳送重設密碼郵件:",
|
||||
"defaultMessage": "Send a password reset email:",
|
||||
"defaultMessage": "Send a password reset email",
|
||||
"description": "Label for password reset field",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
@@ -1308,7 +1308,7 @@
|
||||
},
|
||||
"password_reset_email_sent": {
|
||||
"translation": "一封電子郵件已發送至 {email}。按照電子郵件中的說明重置密碼。",
|
||||
"defaultMessage": "An email has been sent to {email}. Follow the directions in the email to reset your password.",
|
||||
"defaultMessage": "An email with security code has been sent to {email}. Enter the code below.",
|
||||
"description": "Notification that the email with password reset instructions has been sent",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
@@ -1543,11 +1543,60 @@
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"menu_item_audio_call": {
|
||||
"translation": "稱呼",
|
||||
"defaultMessage": "Call",
|
||||
"description": "Start audio call",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"unrecognized_video_format": {
|
||||
"translation": "無法識別此視頻的格式",
|
||||
"defaultMessage": "Format of this video is not recognized",
|
||||
"description": "Error message when uploaded video is invalid",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"loading_note": {
|
||||
"translation": "加載中...",
|
||||
"defaultMessage": "Loading...",
|
||||
"description": "Message shown when component is loading",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"password_i_have_code": {
|
||||
"translation": "我有代碼",
|
||||
"defaultMessage": "I have code",
|
||||
"description": "Call to open field to enter password reset code",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"label_reset_password_tel": {
|
||||
"translation": "發送短信重置密碼",
|
||||
"defaultMessage": "Send SMS to reset password",
|
||||
"description": "Label for password reset field by phone",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"mobile_phone_number": {
|
||||
"translation": "手機號碼",
|
||||
"defaultMessage": "Mobile phone number",
|
||||
"description": "Prompt for entering a mobile phone number",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"password_reset_sms_sent": {
|
||||
"translation": "帶有安全碼的短信已發送至{phone}。請在下方輸入密碼。",
|
||||
"defaultMessage": "A text message with security code has been sent to {phone}. Enter the code below.",
|
||||
"description": "Notification that the SMS with password reset instructions has been sent",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"mobile_number_required": {
|
||||
"translation": "需要手機號",
|
||||
"defaultMessage": "Mobile phone number required",
|
||||
"description": "Error message",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
}
|
||||
}
|
||||
|
||||
+52
-3
@@ -358,7 +358,7 @@
|
||||
},
|
||||
"enter_confirmation_code_prompt": {
|
||||
"translation": "输入通过{method}发送的验证码:",
|
||||
"defaultMessage": "Enter confirmation code sent to you by {method}:",
|
||||
"defaultMessage": "Confirmation code",
|
||||
"description": "Request to enter confirmation code",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
@@ -533,7 +533,7 @@
|
||||
},
|
||||
"label_reset_password": {
|
||||
"translation": "发送密码重置邮件:",
|
||||
"defaultMessage": "Send a password reset email:",
|
||||
"defaultMessage": "Send a password reset email",
|
||||
"description": "Label for password reset field",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
@@ -1308,7 +1308,7 @@
|
||||
},
|
||||
"password_reset_email_sent": {
|
||||
"translation": "一封电子邮件已发送至 {email}。按照电子邮件中的说明重置密码。",
|
||||
"defaultMessage": "An email has been sent to {email}. Follow the directions in the email to reset your password.",
|
||||
"defaultMessage": "An email with security code has been sent to {email}. Enter the code below.",
|
||||
"description": "Notification that the email with password reset instructions has been sent",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
@@ -1543,11 +1543,60 @@
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"menu_item_audio_call": {
|
||||
"translation": "称呼",
|
||||
"defaultMessage": "Call",
|
||||
"description": "Start audio call",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"unrecognized_video_format": {
|
||||
"translation": "无法识别此视频的格式",
|
||||
"defaultMessage": "Format of this video is not recognized",
|
||||
"description": "Error message when uploaded video is invalid",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"loading_note": {
|
||||
"translation": "加载中...",
|
||||
"defaultMessage": "Loading...",
|
||||
"description": "Message shown when component is loading",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"password_i_have_code": {
|
||||
"translation": "我有代码",
|
||||
"defaultMessage": "I have code",
|
||||
"description": "Call to open field to enter password reset code",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"label_reset_password_tel": {
|
||||
"translation": "发送短信重置密码",
|
||||
"defaultMessage": "Send SMS to reset password",
|
||||
"description": "Label for password reset field by phone",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"mobile_phone_number": {
|
||||
"translation": "手机号",
|
||||
"defaultMessage": "Mobile phone number",
|
||||
"description": "Prompt for entering a mobile phone number",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"password_reset_sms_sent": {
|
||||
"translation": "带有安全码的短信已发送至{phone}。请在下方输入密码。",
|
||||
"defaultMessage": "A text message with security code has been sent to {phone}. Enter the code below.",
|
||||
"description": "Notification that the SMS with password reset instructions has been sent",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"mobile_number_required": {
|
||||
"translation": "需要手机号",
|
||||
"defaultMessage": "Mobile phone number required",
|
||||
"description": "Error message",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,19 @@ import { IntlProvider } from 'react-intl';
|
||||
import TinodeWeb from './views/tinode-web.jsx';
|
||||
import HashNavigation from './lib/navigation.js';
|
||||
|
||||
// Insert google analytics script and tag if configured.
|
||||
if (FIREBASE_INIT && FIREBASE_INIT.measurementId) {
|
||||
const head = document.getElementsByTagName('head')[0];
|
||||
let script = document.createElement('script');
|
||||
script.src = 'https://www.googletagmanager.com/gtag/js?id=' + FIREBASE_INIT.measurementId;
|
||||
script.async = true;
|
||||
head.prepend(script);
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){dataLayer.push(arguments);}
|
||||
gtag('js', new Date());
|
||||
gtag('config', FIREBASE_INIT.measurementId);
|
||||
}
|
||||
|
||||
// Allow loading translation strings for just one language.
|
||||
const messageLoader = {
|
||||
'de': _ => import('./i18n.min/de.json'),
|
||||
|
||||
@@ -99,3 +99,12 @@ export function letterTileColorId(userId) {
|
||||
export function idToColorClass(id, light, fg) {
|
||||
return (light ? 'lt-' : 'dk-') + (fg ? 'fg-' : 'bg-') + letterTileColorId(id);
|
||||
}
|
||||
|
||||
// Takes ISO Alpha-2 country code, like 'US' and converts it to a flag emoji.
|
||||
export function flagEmoji(countryCode) {
|
||||
const codePoints = countryCode
|
||||
.toUpperCase()
|
||||
.split('')
|
||||
.map(char => 0x1F1A5 + char.charCodeAt());
|
||||
return String.fromCodePoint(...codePoints);
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
// This is a generated file. Don't edit.
|
||||
|
||||
export const PACKAGE_VERSION = "0.21.0-beta1";
|
||||
export const PACKAGE_VERSION = "0.21.0-rc2";
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
// Account registration.
|
||||
import React from 'react';
|
||||
import React, { Suspense } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import AvatarCrop from '../widgets/avatar-crop.jsx';
|
||||
import AvatarUpload from '../widgets/avatar-upload.jsx';
|
||||
import CheckBox from '../widgets/checkbox.jsx';
|
||||
const PhoneEdit = React.lazy(_ => import('../widgets/phone-edit.jsx'));
|
||||
import VisiblePassword from '../widgets/visible-password.jsx';
|
||||
|
||||
import LocalStorageUtil from '../lib/local-storage.js';
|
||||
@@ -21,7 +22,9 @@ export default class CreateAccountView extends React.PureComponent {
|
||||
this.state = {
|
||||
login: '',
|
||||
password: '',
|
||||
meth: '',
|
||||
email: '',
|
||||
tel: '',
|
||||
fn: '', // full/formatted name
|
||||
imageUrl: null,
|
||||
uploadUrl: null,
|
||||
@@ -35,6 +38,7 @@ export default class CreateAccountView extends React.PureComponent {
|
||||
this.handleLoginChange = this.handleLoginChange.bind(this);
|
||||
this.handlePasswordChange = this.handlePasswordChange.bind(this);
|
||||
this.handleEmailChange = this.handleEmailChange.bind(this);
|
||||
this.handlePhoneChange = this.handlePhoneChange.bind(this);
|
||||
this.handleFnChange = this.handleFnChange.bind(this);
|
||||
this.handleImageChanged = this.handleImageChanged.bind(this);
|
||||
this.handleToggleSaveToken = this.handleToggleSaveToken.bind(this);
|
||||
@@ -42,6 +46,12 @@ export default class CreateAccountView extends React.PureComponent {
|
||||
this.handleAvatarCropCancel = this.handleAvatarCropCancel.bind(this);
|
||||
this.uploadAvatar = this.uploadAvatar.bind(this);
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
|
||||
// Connection will trigger change by changing the this.props.serverVersion.
|
||||
props.tinode.connect()
|
||||
.catch(err => {
|
||||
this.props.onError(err.message, 'err');
|
||||
});
|
||||
}
|
||||
|
||||
handleLoginChange(e) {
|
||||
@@ -53,7 +63,11 @@ export default class CreateAccountView extends React.PureComponent {
|
||||
}
|
||||
|
||||
handleEmailChange(e) {
|
||||
this.setState({email: e.target.value})
|
||||
this.setState({meth: 'email', email: e.target.value});
|
||||
}
|
||||
|
||||
handlePhoneChange(number) {
|
||||
this.setState({meth: 'tel', tel: number});
|
||||
}
|
||||
|
||||
handleFnChange(e) {
|
||||
@@ -76,7 +90,13 @@ export default class CreateAccountView extends React.PureComponent {
|
||||
this.state.login.trim(),
|
||||
this.state.password.trim(),
|
||||
theCard(this.state.fn.trim().substring(0, MAX_TITLE_LENGTH), this.state.uploadUrl),
|
||||
{'meth': 'email', 'val': this.state.email});
|
||||
{
|
||||
'meth': this.state.meth,
|
||||
'val': this.state.meth == 'email' ?
|
||||
this.state.email :
|
||||
this.state.meth == 'tel' ? this.state.tel : null
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// AvatarCropView calls this method when the user has cropped the image.
|
||||
@@ -124,6 +144,9 @@ export default class CreateAccountView extends React.PureComponent {
|
||||
}
|
||||
|
||||
render() {
|
||||
// "reqCred":{"auth":["email"]}
|
||||
const method = (this.props.tinode.getServerParam('reqCred', {}).auth || [])[0] || 'email';
|
||||
|
||||
if (this.state.newAvatar) {
|
||||
return (
|
||||
<AvatarCrop
|
||||
@@ -143,7 +166,7 @@ export default class CreateAccountView extends React.PureComponent {
|
||||
return (
|
||||
<form className="panel-form-column" onSubmit={this.handleSubmit}>
|
||||
<div className="panel-form-row">
|
||||
<div className="panel-form-column">
|
||||
<div className="umn">
|
||||
<FormattedMessage id="login_prompt" defaultMessage="Login"
|
||||
description="Placeholer for username/login">{
|
||||
(login_prompt) => <input type="text" placeholder={login_prompt} autoComplete="user-name"
|
||||
@@ -169,13 +192,30 @@ export default class CreateAccountView extends React.PureComponent {
|
||||
value={this.state.fn} onChange={this.handleFnChange} required/>
|
||||
}</FormattedMessage>
|
||||
</div>
|
||||
<div className="panel-form-row">
|
||||
<FormattedMessage id="email_prompt" defaultMessage="Email, e.g. jdoe@example.com"
|
||||
description="Input placeholder for email entry">{
|
||||
(email_prompt) => <input type="email" placeholder={email_prompt} autoComplete="email"
|
||||
value={this.state.email} onChange={this.handleEmailChange} required/>
|
||||
}</FormattedMessage>
|
||||
</div>
|
||||
{method == 'email' ?
|
||||
<div className="panel-form-row">
|
||||
<FormattedMessage id="email_prompt" defaultMessage="Email, e.g. jdoe@example.com"
|
||||
description="Input placeholder for email entry">{
|
||||
(email_prompt) => <input type="email" placeholder={email_prompt} autoComplete="email"
|
||||
value={this.state.email} onChange={this.handleEmailChange} required/>
|
||||
}</FormattedMessage>
|
||||
</div>
|
||||
: method == 'tel' ?
|
||||
<><div className="panel-form-row">
|
||||
<label className="small gray"><FormattedMessage id="mobile_phone_number" defaultMessage="Mobile phone number"
|
||||
description="Prompt for entering a mobile phone number" /></label>
|
||||
</div>
|
||||
<div className="panel-form-row">
|
||||
<Suspense fallback={<div><FormattedMessage id="loading_note" defaultMessage="Loading..."
|
||||
description="Message shown when component is loading"/></div>}>
|
||||
<PhoneEdit
|
||||
autoFocus={false}
|
||||
onShowCountrySelector={this.props.onShowCountrySelector}
|
||||
onSubmit={this.handlePhoneChange} />
|
||||
</Suspense>
|
||||
</div></>
|
||||
: null
|
||||
}
|
||||
<div className="panel-form-row">
|
||||
<CheckBox id="save-token" name="save-token" checked={this.state.saveToken}
|
||||
onChange={this.handleToggleSaveToken} />
|
||||
|
||||
@@ -480,7 +480,7 @@ class InfoView extends React.Component {
|
||||
tinode={this.props.tinode}
|
||||
topic={this.props.topic}
|
||||
onCredAdd={this.props.onCredAdd}
|
||||
onTopicTagsUpdateRequest={this.props.onTopicTagsUpdateRequest}
|
||||
onUpdateTagsRequest={this.props.onTopicTagsUpdateRequest}
|
||||
onCredConfirm={this.props.onCredConfirm}
|
||||
onCredDelete={this.props.onCredDelete}
|
||||
onUpdateTopicDesc={this.props.onTopicDescUpdateRequest}
|
||||
|
||||
@@ -93,4 +93,3 @@ export default class LoginView extends React.Component {
|
||||
);
|
||||
}
|
||||
};
|
||||
/* END Login */
|
||||
|
||||
@@ -303,8 +303,6 @@ class MessagesView extends React.Component {
|
||||
scrollPosition: 0,
|
||||
fetchingMessages: false,
|
||||
showGoToLastButton: false,
|
||||
forwardMessage: null,
|
||||
reply: null,
|
||||
contentToEdit: null,
|
||||
dragging: false
|
||||
};
|
||||
@@ -315,6 +313,8 @@ class MessagesView extends React.Component {
|
||||
content: nextProps.forwardMessage.preview,
|
||||
seq: null
|
||||
};
|
||||
} else {
|
||||
nextState.reply = null;
|
||||
}
|
||||
|
||||
if (topic) {
|
||||
@@ -1358,6 +1358,7 @@ class MessagesView extends React.Component {
|
||||
topic={this.state.topic}
|
||||
seq={this.props.callSeq}
|
||||
callState={this.props.callState}
|
||||
callAudioOnly={this.props.callAudioOnly}
|
||||
tinode={this.props.tinode}
|
||||
title={this.state.title}
|
||||
avatar={this.state.avatar || true}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
// A password reset form.
|
||||
import React from 'react';
|
||||
import React, { Suspense } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
const PhoneEdit = React.lazy(_ => import('../widgets/phone-edit.jsx'));
|
||||
import VisiblePassword from '../widgets/visible-password.jsx';
|
||||
|
||||
import HashNavigation from '../lib/navigation.js';
|
||||
@@ -11,14 +12,25 @@ export default class PasswordResetView extends React.PureComponent {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
tel: '',
|
||||
email: '',
|
||||
password: '',
|
||||
sent: false
|
||||
sent: false,
|
||||
haveCode: false,
|
||||
code: ''
|
||||
};
|
||||
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
this.handleEmailChange = this.handleEmailChange.bind(this);
|
||||
this.handlePasswordChange = this.handlePasswordChange.bind(this);
|
||||
this.handleCodeChange = this.handleCodeChange.bind(this);
|
||||
this.handleShowCodeField = this.handleShowCodeField.bind(this);
|
||||
|
||||
// Connection will trigger change by changing the this.props.serverVersion.
|
||||
props.tinode.connect()
|
||||
.catch(err => {
|
||||
this.props.onError(err.message, 'err');
|
||||
});
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
@@ -29,13 +41,24 @@ export default class PasswordResetView extends React.PureComponent {
|
||||
handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
if (this.state.token) {
|
||||
this.props.onReset(this.state.scheme, this.state.password.trim(), this.state.token);
|
||||
} else if (this.state.sent) {
|
||||
this.props.onCancel();
|
||||
// Reset using token.
|
||||
this.props.onReset(this.state.password.trim(), {
|
||||
scheme: 'token',
|
||||
token: this.state.token
|
||||
});
|
||||
} else if (this.state.code && this.method) {
|
||||
// Reset using security code.
|
||||
// The secret is structured as <code>:<cred_method>:<cred_value>, "123456:email:alice@example.com".
|
||||
const cred = this.state.email.trim() || this.state.tel.trim();
|
||||
this.props.onReset(this.state.password.trim(), {
|
||||
scheme: 'code',
|
||||
secret: `${this.state.code}:${this.method}:${cred}`
|
||||
});
|
||||
} else {
|
||||
const email = this.state.email.trim();
|
||||
this.setState({email: email});
|
||||
this.props.onRequest('email', email).then(_ => this.setState({sent: true}));
|
||||
const tel = this.state.tel.trim();
|
||||
this.setState({email: email, tel: tel});
|
||||
this.props.onRequest(this.method, email || tel).then(_ => this.setState({sent: true}));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,63 +70,139 @@ export default class PasswordResetView extends React.PureComponent {
|
||||
this.setState({password: e.target.value});
|
||||
}
|
||||
|
||||
handleCodeChange(e) {
|
||||
this.setState({code: e.target.value.replace(/[^\d]/g, '')});
|
||||
}
|
||||
|
||||
handleShowCodeField(e) {
|
||||
e.preventDefault();
|
||||
this.setState({haveCode: true});
|
||||
}
|
||||
|
||||
render() {
|
||||
// "reqCred":{"auth":["email"]}
|
||||
this.method = (this.props.tinode.getServerParam('reqCred', {}).auth || [])[0] || 'email';
|
||||
|
||||
const showCredentialInput = !(this.state.token && this.state.scheme);
|
||||
const showPasswordInput = !showCredentialInput || this.state.haveCode || this.state.sent;
|
||||
|
||||
const passwordInput = (<>
|
||||
<label className="small gray" htmlFor="new-password">
|
||||
<FormattedMessage id="label_new_password"
|
||||
defaultMessage="New password"
|
||||
description="Label for entering a new password" />
|
||||
</label>
|
||||
<FormattedMessage id="new_password_placeholder" defaultMessage="Enter new password"
|
||||
description="Placeholder for entering new password">{
|
||||
(placeholder) => <VisiblePassword
|
||||
id="new-password"
|
||||
placeholder={placeholder}
|
||||
autoComplete="new-password"
|
||||
value={this.state.password}
|
||||
required={true} autoFocus={true}
|
||||
onChange={this.handlePasswordChange} />
|
||||
}</FormattedMessage>
|
||||
</>);
|
||||
|
||||
const emailInput = (<>
|
||||
{this.state.haveCode ?
|
||||
null :
|
||||
<label htmlFor="inputEmail">
|
||||
<FormattedMessage id="label_reset_password"
|
||||
defaultMessage="Send a password reset email"
|
||||
description="Label for password reset field" />
|
||||
</label>
|
||||
}
|
||||
<FormattedMessage id="credential_email_prompt" defaultMessage="Your registration email"
|
||||
description="Placeholder for entering email">{
|
||||
(placeholder) => <input type="email" id="inputEmail"
|
||||
placeholder={placeholder}
|
||||
autoComplete="email"
|
||||
value={this.state.email}
|
||||
onChange={this.handleEmailChange}
|
||||
required autoFocus />
|
||||
}</FormattedMessage>
|
||||
</>);
|
||||
|
||||
const phoneInput = (<>
|
||||
{this.state.haveCode ?
|
||||
null :
|
||||
<div><label>
|
||||
<FormattedMessage id="label_reset_password_tel"
|
||||
defaultMessage="Send SMS to reset password"
|
||||
description="Label for password reset field by phone" />
|
||||
</label></div>
|
||||
}
|
||||
<div>
|
||||
<label className="small gray">
|
||||
<FormattedMessage id="mobile_phone_number" defaultMessage="Mobile phone number"
|
||||
description="Prompt for entering a mobile phone number" />
|
||||
</label>
|
||||
</div>
|
||||
<div className="panel-form-row">
|
||||
<Suspense fallback={<div className="panel-form-row"><FormattedMessage id="loading_note"
|
||||
defaultMessage="Loading..." description="Message shown when component is loading"/></div>}>
|
||||
<PhoneEdit
|
||||
autoFocus={true}
|
||||
onShowCountrySelector={this.props.onShowCountrySelector}
|
||||
onSubmit={number => this.setState({tel: number})} />
|
||||
</Suspense>
|
||||
</div>
|
||||
</>);
|
||||
|
||||
const sentNotification = (<>{
|
||||
this.method == 'email' ?
|
||||
<FormattedMessage id="password_reset_email_sent"
|
||||
defaultMessage="An email with security code has been sent to {email}. Enter the code below."
|
||||
values={{email: <tt>{this.state.email}</tt>}}
|
||||
description="Notification that the email with password reset instructions has been sent" />
|
||||
: this.method == 'tel' ?
|
||||
<FormattedMessage id="password_reset_sms_sent"
|
||||
defaultMessage="A text message with security code has been sent to {phone}. Enter the code below."
|
||||
values={{phone: <tt>{this.state.tel}</tt>}}
|
||||
description="Notification that the SMS with password reset instructions has been sent" />
|
||||
:
|
||||
null
|
||||
}</>);
|
||||
|
||||
const codeInput = (<>
|
||||
<div>
|
||||
<label className="small gray" htmlFor="enter-confirmation-code">
|
||||
<FormattedMessage id="enter_confirmation_code_prompt"
|
||||
defaultMessage="Confirmation code"
|
||||
description="Request to enter confirmation code" />
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<FormattedMessage id="numeric_confirmation_code_prompt"
|
||||
defaultMessage="Numbers only" description="Prompt for numeric conformation code">{
|
||||
(numbers_only) => <input type="text" id="enter-confirmation-code"
|
||||
placeholder={numbers_only} maxLength={10}
|
||||
value={this.state.code} onChange={this.handleCodeChange} required />
|
||||
}</FormattedMessage>
|
||||
</div>
|
||||
</>);
|
||||
|
||||
const credentialInput = this.method == 'email' ? emailInput : this.method == 'tel' ? phoneInput : null;
|
||||
|
||||
return (
|
||||
<form id="password-reset-form" onSubmit={this.handleSubmit}>
|
||||
{(this.state.token && this.state.scheme) ?
|
||||
<><label htmlFor="new-password">
|
||||
<FormattedMessage id="label_new_password"
|
||||
defaultMessage="New password"
|
||||
description="Label for entering a new password" />
|
||||
</label>
|
||||
<FormattedMessage id="new_password_placeholder" defaultMessage="Enter new password"
|
||||
description="Placeholder for entering new password">{
|
||||
(placeholder) => <VisiblePassword
|
||||
id="new-password"
|
||||
placeholder={placeholder}
|
||||
autoComplete="new-password"
|
||||
value={this.state.password}
|
||||
required={true} autoFocus={true}
|
||||
onChange={this.handlePasswordChange} />
|
||||
}</FormattedMessage></>
|
||||
: this.state.sent ?
|
||||
<>
|
||||
<br/>
|
||||
<center><i className="material-icons huge green">task_alt</i></center>
|
||||
<br/>
|
||||
<center><FormattedMessage id="password_reset_email_sent"
|
||||
defaultMessage="An email has been sent to {email}. Follow the directions in the email to reset your password."
|
||||
values={{ email: <tt>{this.state.email}</tt> }}
|
||||
description="Notification that the email with password reset instructions has been sent" /></center>
|
||||
</>
|
||||
:
|
||||
<>
|
||||
<label htmlFor="inputEmail">
|
||||
<FormattedMessage id="label_reset_password"
|
||||
defaultMessage="Send a password reset email:"
|
||||
description="Label for password reset field" />
|
||||
</label>
|
||||
<FormattedMessage id="credential_email_prompt" defaultMessage="Your registration email"
|
||||
description="Placeholder for entering email">{
|
||||
(placeholder) => <input type="email" id="inputEmail"
|
||||
placeholder={placeholder}
|
||||
autoComplete="email"
|
||||
value={this.state.email}
|
||||
onChange={this.handleEmailChange}
|
||||
required autoFocus />
|
||||
}</FormattedMessage>
|
||||
</>
|
||||
}
|
||||
{this.state.sent ? sentNotification : showCredentialInput ? credentialInput : null}
|
||||
{this.state.haveCode || this.state.sent ? codeInput : null}
|
||||
{showPasswordInput ? passwordInput : null}
|
||||
<div className="dialog-buttons">
|
||||
{this.state.haveCode || this.state.sent ? null :
|
||||
<a href="#" onClick={this.handleShowCodeField} style={{marginRight: 'auto'}}>
|
||||
<FormattedMessage id="password_i_have_code" defaultMessage="I have code"
|
||||
description="Call to open field to enter password reset code" />
|
||||
</a>
|
||||
}
|
||||
<button className="primary" type="submit">{
|
||||
(this.state.token && this.state.scheme) ?
|
||||
<FormattedMessage id="button_reset" defaultMessage="Reset" description="Button [Reset]" />
|
||||
: this.state.sent ?
|
||||
<FormattedMessage id="button_ok" defaultMessage="OK"
|
||||
description="Button [OK]" />
|
||||
showPasswordInput ?
|
||||
<FormattedMessage id="button_reset" defaultMessage="Reset" description="Button [Reset]" />
|
||||
:
|
||||
<FormattedMessage id="button_send_request" defaultMessage="Send request"
|
||||
description="Button [Send request]" />
|
||||
<FormattedMessage id="button_send_request" defaultMessage="Send request"
|
||||
description="Button [Send request]" />
|
||||
}</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -157,6 +157,8 @@ class SidepanelView extends React.PureComponent {
|
||||
view === 'register' ?
|
||||
<CreateAccountView
|
||||
tinode={this.props.tinode}
|
||||
serverVersion={this.props.serverVersion}
|
||||
onShowCountrySelector={this.props.onShowCountrySelector}
|
||||
onCreateAccount={this.props.onCreateAccount}
|
||||
onCancel={this.props.onCancel}
|
||||
onError={this.props.onError} /> :
|
||||
@@ -182,7 +184,7 @@ class SidepanelView extends React.PureComponent {
|
||||
tinode={this.props.tinode}
|
||||
myUserId={this.props.myUserId}
|
||||
onUpdateTopicDesc={this.props.onUpdateAccountDesc}
|
||||
onUpdateTags={this.props.onUpdateAccountTags}
|
||||
onUpdateTagsRequest={this.props.onUpdateAccountTags}
|
||||
onCredAdd={this.props.onCredAdd}
|
||||
onCredDelete={this.props.onCredDelete}
|
||||
onCredConfirm={this.props.onCredConfirm}
|
||||
@@ -245,6 +247,9 @@ class SidepanelView extends React.PureComponent {
|
||||
|
||||
view === 'reset' ?
|
||||
<PasswordResetView
|
||||
tinode={this.props.tinode}
|
||||
serverVersion={this.props.serverVersion}
|
||||
onShowCountrySelector={this.props.onShowCountrySelector}
|
||||
onRequest={this.props.onPasswordResetRequest}
|
||||
onReset={this.props.onResetPassword}
|
||||
onCancel={this.props.onCancel} /> :
|
||||
|
||||
+67
-26
@@ -1,5 +1,5 @@
|
||||
// The top-level class to hold all functionality together.
|
||||
import React from 'react';
|
||||
import React, { Suspense } from 'react';
|
||||
import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
|
||||
|
||||
import { initializeApp as firebaseInitApp } from 'firebase/app';
|
||||
@@ -12,6 +12,7 @@ import Alert from '../widgets/alert.jsx';
|
||||
import ContextMenu from '../widgets/context-menu.jsx';
|
||||
import ForwardDialog from '../widgets/forward-dialog.jsx';
|
||||
import CallIncoming from '../widgets/call-incoming.jsx';
|
||||
const PhoneCountrySelector = React.lazy(_ => import('../widgets/phone-country-selector.jsx'));
|
||||
|
||||
import InfoView from './info-view.jsx';
|
||||
import MessagesView from './messages-view.jsx';
|
||||
@@ -69,6 +70,11 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Info',
|
||||
description: 'Show extended topic information'
|
||||
},
|
||||
menu_item_audio_call: {
|
||||
id: 'menu_item_audio_call',
|
||||
defaultMessage: 'Call',
|
||||
description: 'Start audio call'
|
||||
},
|
||||
menu_item_video_call: {
|
||||
id: 'menu_item_video_call',
|
||||
defaultMessage: 'Video call',
|
||||
@@ -153,11 +159,13 @@ class TinodeWeb extends React.Component {
|
||||
this.handlePasswordResetRequest = this.handlePasswordResetRequest.bind(this);
|
||||
this.handleResetPassword = this.handleResetPassword.bind(this);
|
||||
this.handleContextMenuAction = this.handleContextMenuAction.bind(this);
|
||||
this.handleShowCountrySelector = this.handleShowCountrySelector.bind(this);
|
||||
|
||||
this.handleShowForwardDialog = this.handleShowForwardDialog.bind(this);
|
||||
this.handleHideForwardDialog = this.handleHideForwardDialog.bind(this);
|
||||
|
||||
this.handleStartVideoCall = this.handleStartVideoCall.bind(this);
|
||||
this.handleStartAudioCall = this.handleStartAudioCall.bind(this);
|
||||
this.handleInfoMessage = this.handleInfoMessage.bind(this);
|
||||
this.handleDataMessage = this.handleDataMessage.bind(this);
|
||||
this.handleCallClose = this.handleCallClose.bind(this);
|
||||
@@ -233,6 +241,7 @@ class TinodeWeb extends React.Component {
|
||||
// Video calls.
|
||||
callTopic: undefined,
|
||||
callState: CALL_STATE_NONE,
|
||||
callAudioOnly: undefined,
|
||||
// If true, call state should be transitioned to CALL_STATE_IN_PROGRESS upon
|
||||
// switching to the call topic.
|
||||
callShouldStart: false,
|
||||
@@ -373,16 +382,12 @@ class TinodeWeb extends React.Component {
|
||||
try {
|
||||
this.fcm = firebaseGetMessaging(firebaseInitApp(FIREBASE_INIT, APP_NAME));
|
||||
return navigator.serviceWorker.getRegistration('/service-worker.js').then(reg => {
|
||||
if (reg) {
|
||||
return reg
|
||||
}
|
||||
return navigator.serviceWorker.register('/service-worker.js').then(reg => {
|
||||
return reg || navigator.serviceWorker.register('/service-worker.js').then(reg => {
|
||||
this.checkForAppUpdate(reg);
|
||||
return reg;
|
||||
});
|
||||
}).then(reg => {
|
||||
// Pass locale and version config to the service worker.
|
||||
console.log("SW active:", reg.active, "installing:", reg.installing);
|
||||
(reg.active || reg.installing).postMessage(JSON.stringify({locale: locale, version: PACKAGE_VERSION}));
|
||||
// Request token.
|
||||
return TinodeWeb.requestFCMToken(this.fcm, reg);
|
||||
@@ -402,7 +407,7 @@ class TinodeWeb extends React.Component {
|
||||
// Handhe FCM pushes
|
||||
// (a) for channels always,
|
||||
// (b) pushes when the app is in foreground but has no focus.
|
||||
firebaseOnMessage(this.fcm, (payload) => { this.handlePushMessage(payload); });
|
||||
firebaseOnMessage(this.fcm, payload => { this.handlePushMessage(payload); });
|
||||
}).catch(err => {
|
||||
// SW registration or FCM has failed :(
|
||||
onError(err);
|
||||
@@ -721,20 +726,23 @@ class TinodeWeb extends React.Component {
|
||||
this.handleLoginSuccessful();
|
||||
}
|
||||
}).catch(err => {
|
||||
const autoLogin = err.code >= 500;
|
||||
// Connection or login failed, report error.
|
||||
this.setState({
|
||||
loginDisabled: false,
|
||||
credMethod: undefined,
|
||||
credCode: undefined,
|
||||
loadSpinnerVisible: false,
|
||||
autoLogin: false
|
||||
autoLogin: autoLogin
|
||||
});
|
||||
this.handleError(err.message, 'err');
|
||||
console.warn("Login failed", err);
|
||||
if (token) {
|
||||
this.handleLogout();
|
||||
if (!autoLogin) {
|
||||
if (token) {
|
||||
this.handleLogout();
|
||||
}
|
||||
HashNavigation.navigateTo('');
|
||||
}
|
||||
HashNavigation.navigateTo('');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1162,7 +1170,7 @@ class TinodeWeb extends React.Component {
|
||||
});
|
||||
}
|
||||
|
||||
handleUpdateAccountTagsRequest(tags) {
|
||||
handleUpdateAccountTagsRequest(_, tags) {
|
||||
this.tinode.getMeTopic().setMeta({tags: tags})
|
||||
.catch(err => this.handleError(err.message, 'err'));
|
||||
}
|
||||
@@ -1593,6 +1601,10 @@ class TinodeWeb extends React.Component {
|
||||
title: this.props.intl.formatMessage(messages.menu_item_info),
|
||||
handler: this.handleShowInfoView
|
||||
} : null,
|
||||
subscribed && Tinode.isP2PTopicName(topicName) && webrtc ? {
|
||||
title: this.props.intl.formatMessage(messages.menu_item_audio_call),
|
||||
handler: this.handleStartAudioCall
|
||||
} : null,
|
||||
subscribed && Tinode.isP2PTopicName(topicName) && webrtc ? {
|
||||
title: this.props.intl.formatMessage(messages.menu_item_video_call),
|
||||
handler: this.handleStartVideoCall
|
||||
@@ -1700,13 +1712,13 @@ class TinodeWeb extends React.Component {
|
||||
});
|
||||
}
|
||||
|
||||
handleResetPassword(scheme, newPassword, token) {
|
||||
token = base64ReEncode(token);
|
||||
if (!token) {
|
||||
handleResetPassword(newPassword, tempAuth) {
|
||||
const secret = base64ReEncode(tempAuth.secret);
|
||||
if (!secret || !tempAuth.scheme) {
|
||||
this.handleError(this.props.intl.formatMessage(messages.invalid_security_token), 'err');
|
||||
} else {
|
||||
this.tinode.connect()
|
||||
.then(_ => this.tinode.updateAccountBasic(null, null, newPassword, {token: token}))
|
||||
.then(_ => this.tinode.updateAccountBasic(null, null, newPassword, {scheme: tempAuth.scheme, secret: secret}))
|
||||
.then(_ => HashNavigation.navigateTo(''))
|
||||
.catch(err => {
|
||||
// Socket error
|
||||
@@ -1715,18 +1727,41 @@ class TinodeWeb extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
handleShowCountrySelector(code, dial, selectedCallback) {
|
||||
this.handleShowAlert("Select country",
|
||||
<Suspense fallback={<div><FormattedMessage id="loading_note" defaultMessage="Loading..."
|
||||
description="Message shown when component is loading"/></div>}>
|
||||
<PhoneCountrySelector
|
||||
selected={code}
|
||||
onSubmit={(c, d) => {
|
||||
this.setState({alertVisible: false});
|
||||
selectedCallback(c, d);
|
||||
}} />
|
||||
</Suspense>,
|
||||
null, null, _ => {}, "Cancel");
|
||||
}
|
||||
|
||||
handleStartVideoCall() {
|
||||
this.setState({
|
||||
callTopic: this.state.topicSelected,
|
||||
callState: CALL_STATE_OUTGOING_INITATED
|
||||
callState: CALL_STATE_OUTGOING_INITATED,
|
||||
callAudioOnly: false
|
||||
});
|
||||
}
|
||||
|
||||
handleCallInvite(callTopic, callSeq, callState) {
|
||||
handleStartAudioCall() {
|
||||
this.setState({
|
||||
callTopic: this.state.topicSelected,
|
||||
callState: CALL_STATE_OUTGOING_INITATED,
|
||||
callAudioOnly: true
|
||||
});
|
||||
}
|
||||
|
||||
handleCallInvite(callTopic, callSeq, callState, audioOnly) {
|
||||
switch (callState) {
|
||||
case CALL_STATE_OUTGOING_INITATED:
|
||||
let head = { webrtc: CALL_HEAD_STARTED };
|
||||
this.handleSendMessage(Drafty.videoCall(), undefined, undefined, head)
|
||||
const head = { webrtc: CALL_HEAD_STARTED, aonly: !!audioOnly };
|
||||
this.handleSendMessage(Drafty.videoCall(audioOnly), undefined, undefined, head)
|
||||
.then(ctrl => {
|
||||
if (ctrl.code < 200 || ctrl.code >= 300 || !ctrl.params || !ctrl.params.seq) {
|
||||
this.handleCallClose();
|
||||
@@ -1796,7 +1831,8 @@ class TinodeWeb extends React.Component {
|
||||
}
|
||||
this.setState({
|
||||
callTopic: undefined,
|
||||
callState: CALL_STATE_NONE
|
||||
callState: CALL_STATE_NONE,
|
||||
callAudioOnly: undefined
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1829,7 +1865,8 @@ class TinodeWeb extends React.Component {
|
||||
this.setState({
|
||||
callTopic: null,
|
||||
callState: CALL_STATE_NONE,
|
||||
callSeq: null
|
||||
callSeq: null,
|
||||
callAudioOnly: undefined
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -1862,7 +1899,8 @@ class TinodeWeb extends React.Component {
|
||||
this.setState({
|
||||
callTopic: data.topic,
|
||||
callState: CALL_STATE_INCOMING_RECEIVED,
|
||||
callSeq: data.seq
|
||||
callSeq: data.seq,
|
||||
callAudioOnly: !!msg.head.aonly
|
||||
});
|
||||
} else {
|
||||
// Another call is either in progress or being established.
|
||||
@@ -1918,10 +1956,11 @@ class TinodeWeb extends React.Component {
|
||||
{this.state.callTopic && this.state.callState == CALL_STATE_INCOMING_RECEIVED ?
|
||||
<CallIncoming
|
||||
tinode={this.tinode}
|
||||
onClose={this.handleCallClose}
|
||||
topic={this.state.callTopic}
|
||||
seq={this.state.callSeq}
|
||||
callState={this.state.callState}
|
||||
audioOnly={this.state.callAudioOnly}
|
||||
onClose={this.handleCallClose}
|
||||
onRinging={this.handleCallRinging}
|
||||
onAcceptCall={this.handleCallAccept}
|
||||
onReject={this.handleCallHangup}
|
||||
@@ -1931,12 +1970,12 @@ class TinodeWeb extends React.Component {
|
||||
}
|
||||
{this.state.alertVisible ?
|
||||
<Alert
|
||||
visible={this.state.alertVisible}
|
||||
title={this.state.alertParams.title}
|
||||
content={this.state.alertParams.content}
|
||||
onReject={this.state.alertParams.onReject ? (_ => this.setState({alertVisible: false})) : null}
|
||||
reject={this.state.alertParams.reject}
|
||||
onConfirm={_ => {this.setState({alertVisible: false}); this.state.alertParams.onConfirm();}}
|
||||
onConfirm={this.state.alertParams.onConfirm ?
|
||||
(_ => {this.setState({alertVisible: false}); this.state.alertParams.onConfirm();}) : null}
|
||||
confirm={this.state.alertParams.confirm}
|
||||
/> : null}
|
||||
<SidepanelView
|
||||
@@ -2002,6 +2041,7 @@ class TinodeWeb extends React.Component {
|
||||
onResetPassword={this.handleResetPassword}
|
||||
onShowArchive={this.handleShowArchive}
|
||||
onShowBlocked={this.handleShowBlocked}
|
||||
onShowCountrySelector={this.handleShowCountrySelector}
|
||||
|
||||
onInitFind={this.tnInitFind}
|
||||
searchResults={this.state.searchResults}
|
||||
@@ -2034,6 +2074,7 @@ class TinodeWeb extends React.Component {
|
||||
callTopic={this.state.callTopic}
|
||||
callSeq={this.state.callSeq}
|
||||
callState={this.state.callState}
|
||||
callAudioOnly={this.state.callAudioOnly}
|
||||
onCallHangup={this.handleCallHangup}
|
||||
|
||||
onCallInvite={this.handleCallInvite}
|
||||
|
||||
@@ -87,21 +87,21 @@ class ValidationView extends React.PureComponent {
|
||||
return (
|
||||
<div className="panel-form">
|
||||
<div className="panel-form-row">
|
||||
<label className="small" htmlFor="enter-confirmation-code">
|
||||
<label className="small gray" htmlFor="enter-confirmation-code">
|
||||
<FormattedMessage id="enter_confirmation_code_prompt"
|
||||
defaultMessage="Enter confirmation code sent to you by {method}:"
|
||||
defaultMessage="Confirmation code"
|
||||
description="Request to enter confirmation code"
|
||||
values={{method: method}} />
|
||||
</label>
|
||||
</div>
|
||||
<div className="panel-form-row">
|
||||
<FormattedMessage id="numeric_confirmation_code_prompt"
|
||||
defaultMessage="Numbers only" description="Prompt for numeric conformation code">{
|
||||
(numbers_only) => <input type="text" id="enter-confirmation-code"
|
||||
placeholder={numbers_only}
|
||||
value={this.state.code} onChange={this.handleChange}
|
||||
onKeyPress={this.handleKeyPress} required />
|
||||
}</FormattedMessage>
|
||||
<FormattedMessage id="numeric_confirmation_code_prompt"
|
||||
defaultMessage="Numbers only" description="Prompt for numeric conformation code">{
|
||||
(numbers_only) => <input type="text" id="enter-confirmation-code"
|
||||
placeholder={numbers_only}
|
||||
value={this.state.code} onChange={this.handleChange}
|
||||
onKeyPress={this.handleKeyPress} required />
|
||||
}</FormattedMessage>
|
||||
</div>
|
||||
<div className="dialog-buttons">
|
||||
<button className="secondary" onClick={this.handleCancel}>
|
||||
|
||||
@@ -3,7 +3,7 @@ import { FormattedMessage } from 'react-intl';
|
||||
|
||||
export default class Alert extends React.PureComponent {
|
||||
render() {
|
||||
<div className="alert-container">
|
||||
return (<div className="alert-container">
|
||||
<div className="alert">
|
||||
<div className="title">{this.props.title}</div>
|
||||
<div className="content">{this.props.content}</div>
|
||||
@@ -15,11 +15,15 @@ export default class Alert extends React.PureComponent {
|
||||
:
|
||||
null
|
||||
}
|
||||
<button className="primary" onClick={this.props.onConfirm}>
|
||||
{this.props.confirm || <FormattedMessage id="button_ok" defaultMessage="OK" description="Button [OK]" />}
|
||||
</button>
|
||||
{this.props.onConfirm ?
|
||||
<button className="primary" onClick={this.props.onConfirm}>
|
||||
{this.props.confirm || <FormattedMessage id="button_ok" defaultMessage="OK" description="Button [OK]" />}
|
||||
</button>
|
||||
:
|
||||
null
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>);
|
||||
}
|
||||
};
|
||||
|
||||
+238
-59
@@ -15,6 +15,9 @@ const CALL_ENDED_SOUND = new Audio('audio/call-end.m4a');
|
||||
CALL_ENDED_SOUND.loop = true;
|
||||
const DIALING_SOUND = new Audio('audio/dialing.m4a');
|
||||
|
||||
const VIDEO_MUTED_EVENT = 'video:muted';
|
||||
const VIDEO_UNMUTED_EVENT = 'video:unmuted';
|
||||
|
||||
const messages = defineMessages({
|
||||
already_in_call: {
|
||||
id: 'already_in_call',
|
||||
@@ -29,17 +32,24 @@ class CallPanel extends React.PureComponent {
|
||||
|
||||
this.state = {
|
||||
localStream: undefined,
|
||||
remoteStream: undefined,
|
||||
pc: undefined,
|
||||
dataChannel: undefined,
|
||||
|
||||
previousOnInfo: undefined,
|
||||
waitingForPeer: false,
|
||||
// If true, the client has received a remote SDP from the peer and has sent a local SDP to the peer.
|
||||
callInitialSetupComplete: false
|
||||
callInitialSetupComplete: false,
|
||||
audioOnly: props.callAudioOnly,
|
||||
// Video mute/unmute in progress.
|
||||
videoToggleInProgress: false,
|
||||
// Indicates if the remote peer has informed us that their camera is on.
|
||||
remoteVideoLive: false,
|
||||
};
|
||||
|
||||
this.localStreamConstraints = {
|
||||
audio: true,
|
||||
video: true
|
||||
video: !props.callAudioOnly
|
||||
};
|
||||
this.isOutgoingCall = props.callState == CALL_STATE_OUTGOING_INITATED;
|
||||
|
||||
@@ -73,14 +83,24 @@ class CallPanel extends React.PureComponent {
|
||||
this.handleGetUserMediaError = this.handleGetUserMediaError.bind(this);
|
||||
|
||||
this.stopTracks = this.stopTracks.bind(this);
|
||||
this.disconnectMedia = this.disconnectMedia.bind(this);
|
||||
|
||||
this.handleCloseClick = this.handleCloseClick.bind(this);
|
||||
this.handleToggleCameraClick = this.handleToggleCameraClick.bind(this);
|
||||
this.handleToggleMicClick = this.handleToggleMicClick.bind(this);
|
||||
this.toggleMedia = this.toggleMedia.bind(this);
|
||||
|
||||
this.handleRemoteHangup = this.handleRemoteHangup.bind(this);
|
||||
this.handleVideoCallAccepted = this.handleVideoCallAccepted.bind(this);
|
||||
|
||||
this.muteVideo = this.muteVideo.bind(this);
|
||||
this.unmuteVideo = this.unmuteVideo.bind(this);
|
||||
this.emptyVideoTrack = this.emptyVideoTrack.bind(this);
|
||||
|
||||
this.handleDataChannelEvent = this.handleDataChannelEvent.bind(this);
|
||||
this.handleDataChannelError = this.handleDataChannelError.bind(this);
|
||||
this.handleDataChannelMessage = this.handleDataChannelMessage.bind(this);
|
||||
this.handleDataChannelOpen = this.handleDataChannelOpen.bind(this);
|
||||
this.handleDataChannelClose = this.handleDataChannelClose.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
@@ -105,6 +125,14 @@ class CallPanel extends React.PureComponent {
|
||||
const stream = this.state.localStream;
|
||||
stream.getTracks().forEach(track => {
|
||||
pc.addTrack(track, stream);
|
||||
|
||||
if (track.kind == 'video' && this.state.audioOnly) {
|
||||
// This is an audio-only call.
|
||||
// Remove dummy video track (placeholder remains).
|
||||
track.enabled = false;
|
||||
track.stop();
|
||||
stream.removeTrack(track);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -138,6 +166,16 @@ class CallPanel extends React.PureComponent {
|
||||
}
|
||||
}
|
||||
|
||||
// Creates an empty video track placeholder.
|
||||
emptyVideoTrack() {
|
||||
const width = 640;
|
||||
const height = 480;
|
||||
const canvas = Object.assign(document.createElement("canvas"), {width, height});
|
||||
canvas.getContext('2d').fillRect(0, 0, width, height);
|
||||
const stream = canvas.captureStream(0);
|
||||
return Object.assign(stream.getVideoTracks()[0], {enabled: false});
|
||||
}
|
||||
|
||||
start() {
|
||||
if (this.state.localStream) {
|
||||
this.props.onError(this.props.intl.formatMessage(messages.already_in_call), 'info');
|
||||
@@ -146,7 +184,7 @@ class CallPanel extends React.PureComponent {
|
||||
|
||||
if (this.props.callState == CALL_STATE_IN_PROGRESS) {
|
||||
// We apparently just accepted the call.
|
||||
this.props.onInvite(this.props.topic, this.props.seq, this.props.callState);
|
||||
this.props.onInvite(this.props.topic, this.props.seq, CALL_STATE_IN_PROGRESS, this.props.callAudioOnly);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -154,13 +192,18 @@ class CallPanel extends React.PureComponent {
|
||||
// Start local video.
|
||||
navigator.mediaDevices.getUserMedia(this.localStreamConstraints)
|
||||
.then(stream => {
|
||||
if (!this.localStreamConstraints.video) {
|
||||
// Starting an audio-only call. Create a dummy video track
|
||||
// (so video can be enabled during the call if the user desires).
|
||||
stream.addTrack(this.emptyVideoTrack());
|
||||
}
|
||||
this.setState({localStream: stream, waitingForPeer: true});
|
||||
this.localRef.current.srcObject = stream;
|
||||
|
||||
DIALING_SOUND.play();
|
||||
|
||||
// Send call invitation.
|
||||
this.props.onInvite(this.props.topic, this.props.seq, this.props.callState);
|
||||
this.props.onInvite(this.props.topic, this.props.seq, this.props.callState, this.props.callAudioOnly);
|
||||
})
|
||||
.catch(this.handleGetUserMediaError);
|
||||
}
|
||||
@@ -171,8 +214,10 @@ class CallPanel extends React.PureComponent {
|
||||
RING_SOUND.pause();
|
||||
RING_SOUND.currentTime = 0;
|
||||
|
||||
this.stopTracks(this.localRef.current);
|
||||
this.stopTracks(this.remoteRef.current);
|
||||
this.stopTracks(this.state.localStream);
|
||||
this.stopTracks(this.state.remoteStream);
|
||||
this.disconnectMedia(this.localRef.current);
|
||||
this.disconnectMedia(this.remoteRef.current);
|
||||
if (this.state.pc) {
|
||||
this.state.pc.ontrack = null;
|
||||
this.state.pc.onremovetrack = null;
|
||||
@@ -183,17 +228,26 @@ class CallPanel extends React.PureComponent {
|
||||
this.state.pc.onicegatheringstatechange = null;
|
||||
this.state.pc.onnegotiationneeded = null;
|
||||
this.state.pc.onicecandidateerror = null;
|
||||
this.state.pc.ondatachannel = null;
|
||||
|
||||
if (this.state.dataChannel) {
|
||||
this.state.dataChannel.close();
|
||||
}
|
||||
this.state.pc.close();
|
||||
}
|
||||
this.setState({pc: null, waitingForPeer: false});
|
||||
}
|
||||
|
||||
stopTracks(el) {
|
||||
disconnectMedia(el) {
|
||||
if (!el) {
|
||||
return;
|
||||
}
|
||||
let stream = el.srcObject;
|
||||
|
||||
el.srcObject = null;
|
||||
el.src = '';
|
||||
}
|
||||
|
||||
stopTracks(stream) {
|
||||
if (!stream) {
|
||||
return;
|
||||
}
|
||||
@@ -205,8 +259,42 @@ class CallPanel extends React.PureComponent {
|
||||
track.enabled = false;
|
||||
});
|
||||
}
|
||||
el.srcObject = null;
|
||||
el.src = '';
|
||||
}
|
||||
|
||||
handleDataChannelError(error) {
|
||||
console.error('data channel error', error);
|
||||
}
|
||||
|
||||
handleDataChannelMessage(event) {
|
||||
switch (event.data) {
|
||||
case VIDEO_MUTED_EVENT:
|
||||
this.setState({remoteVideoLive: false}, _ => { this.remoteRef.current.srcObject = this.state.remoteStream; });
|
||||
break;
|
||||
case VIDEO_UNMUTED_EVENT:
|
||||
this.setState({remoteVideoLive: true}, _ => { this.remoteRef.current.srcObject = this.state.remoteStream; });
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
handleDataChannelOpen(event) {
|
||||
if (!this.state.audioOnly) {
|
||||
event.target.send(VIDEO_UNMUTED_EVENT);
|
||||
}
|
||||
}
|
||||
|
||||
handleDataChannelClose() {
|
||||
/* no action */
|
||||
}
|
||||
|
||||
handleDataChannelEvent(event) {
|
||||
const channel = event.channel;
|
||||
channel.onerror = this.handleDataChannelError;
|
||||
channel.onmessage = this.handleDataChannelMessage;
|
||||
channel.onopen = this.handleDataChannelOpen;
|
||||
channel.onclose = this.handleDataChannelClose;
|
||||
this.setState({dataChannel: channel});
|
||||
}
|
||||
|
||||
createPeerConnection() {
|
||||
@@ -220,8 +308,17 @@ class CallPanel extends React.PureComponent {
|
||||
pc.onnegotiationneeded = this.handleNegotiationNeededEvent;
|
||||
pc.onicecandidateerror = this.handleIceCandidateErrorEvent;
|
||||
pc.ontrack = this.handleTrackEvent;
|
||||
pc.ondatachannel = this.handleDataChannelEvent;
|
||||
|
||||
// creating data channel events
|
||||
const channel = pc.createDataChannel("events", {ordered: true});
|
||||
channel.onerror = this.handleDataChannelError;
|
||||
channel.onmessage = this.handleDataChannelMessage;
|
||||
channel.onopen = this.handleDataChannelOpen;
|
||||
channel.onclose = this.handleDataChannelClose;
|
||||
|
||||
this.setState({pc: pc, dataChannel: channel, waitingForPeer: false});
|
||||
|
||||
this.setState({pc: pc, waitingForPeer: false});
|
||||
return pc;
|
||||
}
|
||||
|
||||
@@ -233,7 +330,7 @@ class CallPanel extends React.PureComponent {
|
||||
.then(_ => {
|
||||
this.setState({ callInitialSetupComplete: true }, _ => this.drainRemoteIceCandidatesCache());
|
||||
})
|
||||
.catch(this.reportError);
|
||||
.catch(err => this.reportError(err));
|
||||
}
|
||||
|
||||
reportError(err) {
|
||||
@@ -244,17 +341,18 @@ class CallPanel extends React.PureComponent {
|
||||
return this.isOutgoingCall || this.state.callInitialSetupComplete;
|
||||
}
|
||||
|
||||
handleNegotiationNeededEvent() {
|
||||
handleNegotiationNeededEvent(event) {
|
||||
const pc = event.target;
|
||||
if (!this.canSendOffer()) {
|
||||
return;
|
||||
}
|
||||
this.state.pc.createOffer().then(offer => {
|
||||
return this.state.pc.setLocalDescription(offer);
|
||||
pc.createOffer().then(offer => {
|
||||
return pc.setLocalDescription(offer);
|
||||
})
|
||||
.then(_ => {
|
||||
this.props.onSendOffer(this.props.topic, this.props.seq, this.state.pc.localDescription.toJSON());
|
||||
this.props.onSendOffer(this.props.topic, this.props.seq, pc.localDescription.toJSON());
|
||||
})
|
||||
.catch(this.reportError);
|
||||
.catch(err => this.reportError(err));
|
||||
}
|
||||
|
||||
handleIceCandidateErrorEvent(event) {
|
||||
@@ -271,7 +369,12 @@ class CallPanel extends React.PureComponent {
|
||||
const candidate = new RTCIceCandidate(info.payload);
|
||||
if (this.state.callInitialSetupComplete) {
|
||||
this.state.pc.addIceCandidate(candidate)
|
||||
.catch(this.reportError);
|
||||
.catch(err => {
|
||||
if (candidate.candidate) {
|
||||
this.reportError(err);
|
||||
}
|
||||
console.warn("Error adding new ice candidate", candidate, err);
|
||||
});
|
||||
} else {
|
||||
this.remoteIceCandidatesCache.push(candidate);
|
||||
}
|
||||
@@ -280,13 +383,18 @@ class CallPanel extends React.PureComponent {
|
||||
drainRemoteIceCandidatesCache() {
|
||||
this.remoteIceCandidatesCache.forEach(candidate => {
|
||||
this.state.pc.addIceCandidate(candidate)
|
||||
.catch(this.reportError);
|
||||
.catch(err => {
|
||||
if (candidate.candidate) {
|
||||
this.reportError(err);
|
||||
}
|
||||
console.warn("Error adding cached ice candidate", candidate, err);
|
||||
});
|
||||
});
|
||||
this.remoteIceCandidatesCache = [];
|
||||
}
|
||||
|
||||
handleICEConnectionStateChangeEvent(event) {
|
||||
switch (this.state.pc.iceConnectionState) {
|
||||
switch (event.target.iceConnectionState) {
|
||||
case 'closed':
|
||||
case 'failed':
|
||||
this.handleCloseClick();
|
||||
@@ -295,7 +403,7 @@ class CallPanel extends React.PureComponent {
|
||||
}
|
||||
|
||||
handleSignalingStateChangeEvent(event) {
|
||||
if (this.state.pc.signalingState == 'closed') {
|
||||
if (event.target.signalingState == 'closed') {
|
||||
this.handleCloseClick();
|
||||
}
|
||||
}
|
||||
@@ -307,23 +415,22 @@ class CallPanel extends React.PureComponent {
|
||||
handleTrackEvent(event) {
|
||||
// Remote video becomes available.
|
||||
this.remoteRef.current.srcObject = event.streams[0];
|
||||
// Make sure we display the title (peer's name) over the remote video.
|
||||
this.forceUpdate();
|
||||
this.setState({remoteStream: event.streams[0]});
|
||||
}
|
||||
|
||||
handleGetUserMediaError(e) {
|
||||
console.error("Error opening camera and/or microphone", e);
|
||||
switch(e.name) {
|
||||
case 'NotFoundError':
|
||||
// Cannot start the call b/c no camera and/or microphone found.
|
||||
this.reportError(e.message);
|
||||
this.reportError(e);
|
||||
break;
|
||||
case 'SecurityError':
|
||||
case 'PermissionDeniedError':
|
||||
// Do nothing; this is the same as the user canceling the call.
|
||||
break;
|
||||
default:
|
||||
this.reportError(e.message);
|
||||
console.error("Error opening your camera and/or microphone:", e.message);
|
||||
this.reportError(e);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -334,13 +441,20 @@ class CallPanel extends React.PureComponent {
|
||||
|
||||
handleVideoOfferMsg(info) {
|
||||
let localStream = null;
|
||||
const pc = this.createPeerConnection();
|
||||
const pc = this.state.pc ? this.state.pc : this.createPeerConnection();
|
||||
const desc = new RTCSessionDescription(info.payload);
|
||||
|
||||
pc.setRemoteDescription(desc).then(_ => {
|
||||
return navigator.mediaDevices.getUserMedia(this.localStreamConstraints);
|
||||
})
|
||||
.then(stream => {
|
||||
let dummyVideo;
|
||||
if (!this.localStreamConstraints.video) {
|
||||
// Starting an audio-only call. Create an empty video track so
|
||||
// so the user can enable the video during the call.
|
||||
dummyVideo = this.emptyVideoTrack();
|
||||
stream.addTrack(dummyVideo);
|
||||
}
|
||||
localStream = stream;
|
||||
this.localRef.current.srcObject = stream;
|
||||
this.setState({localStream: stream});
|
||||
@@ -348,6 +462,12 @@ class CallPanel extends React.PureComponent {
|
||||
localStream.getTracks().forEach(track => {
|
||||
pc.addTrack(track, localStream);
|
||||
});
|
||||
|
||||
if (dummyVideo) {
|
||||
dummyVideo.enabled = false;
|
||||
dummyVideo.stop();
|
||||
stream.removeTrack(dummyVideo);
|
||||
}
|
||||
})
|
||||
.then(_ => {
|
||||
return pc.createAnswer();
|
||||
@@ -387,60 +507,119 @@ class CallPanel extends React.PureComponent {
|
||||
this.props.onHangup(this.props.topic, this.props.seq);
|
||||
}
|
||||
|
||||
// Mute/unmute audio/video (specified by kind).
|
||||
toggleMedia(kind) {
|
||||
// Ends video track and turns off the camera.
|
||||
muteVideo() {
|
||||
if (!this.state.pc || !this.state.dataChannel) {
|
||||
return;
|
||||
}
|
||||
|
||||
const stream = this.state.localStream;
|
||||
stream.getTracks().forEach(track => {
|
||||
if (track.kind != kind) {
|
||||
return;
|
||||
const t = stream.getVideoTracks()[0];
|
||||
t.enabled = false;
|
||||
t.stop();
|
||||
|
||||
stream.removeTrack(t);
|
||||
this.state.dataChannel.send(VIDEO_MUTED_EVENT);
|
||||
this.setState({videoToggleInProgress: false});
|
||||
}
|
||||
|
||||
unmuteVideo() {
|
||||
if (!this.state.pc || !this.state.dataChannel) {
|
||||
return;
|
||||
}
|
||||
navigator.mediaDevices.getUserMedia({ video: true })
|
||||
.then(stream => {
|
||||
// Will extract video track from stream and throw stream away,
|
||||
// and replace video track in the media sender.
|
||||
this.localRef.current.srcObject = null;
|
||||
const sender = this.state.pc.getSenders().find(s => s.track.kind == 'video');
|
||||
const track = stream.getVideoTracks()[0];
|
||||
// Remote track from new stream.
|
||||
stream.removeTrack(track);
|
||||
// Add this track to the existing local stream.
|
||||
this.state.localStream.addTrack(track);
|
||||
return sender.replaceTrack(track);
|
||||
})
|
||||
.then(_ => {
|
||||
this.localRef.current.srcObject = this.state.localStream;
|
||||
this.state.dataChannel.send(VIDEO_UNMUTED_EVENT);
|
||||
})
|
||||
.catch(err => this.handleGetUserMediaError(err))
|
||||
.finally(_ => { this.setState({videoToggleInProgress: false}); }); // Make sure we redraw the mute/unmute icons (e.g. camera -> camera_off).
|
||||
}
|
||||
|
||||
handleToggleCameraClick() {
|
||||
if (this.state.videoToggleInProgress) {
|
||||
// Toggle currently in progress.
|
||||
return;
|
||||
}
|
||||
const tracks = this.state.localStream.getVideoTracks();
|
||||
this.setState({videoToggleInProgress: true}, _ => {
|
||||
if (tracks && tracks.length > 0 && tracks[0].enabled && tracks[0].readyState == 'live') {
|
||||
this.muteVideo();
|
||||
} else {
|
||||
this.unmuteVideo();
|
||||
}
|
||||
track.enabled = !track.enabled;
|
||||
this.setState({audioOnly: !this.state.audioOnly});
|
||||
});
|
||||
}
|
||||
|
||||
handleToggleMicClick() {
|
||||
const stream = this.state.localStream;
|
||||
const t = stream.getAudioTracks()[0];
|
||||
t.enabled = !t.enabled;
|
||||
// Make sure we redraw the mute/unmute icons (e.g. mic -> mic_off).
|
||||
this.forceUpdate();
|
||||
}
|
||||
|
||||
handleToggleCameraClick() {
|
||||
this.toggleMedia('video');
|
||||
}
|
||||
|
||||
handleToggleMicClick() {
|
||||
this.toggleMedia('audio');
|
||||
}
|
||||
|
||||
render() {
|
||||
const remoteActive = this.remoteRef.current && this.remoteRef.current.srcObject;
|
||||
const disabled = !(this.state.localStream && this.state.localStream.getTracks());
|
||||
const audioIcon = this.state.localStream && this.state.localStream.getAudioTracks()[0].enabled ? 'mic' : 'mic_off';
|
||||
const videoIcon = this.state.localStream && this.state.localStream.getVideoTracks()[0].enabled ? 'videocam' : 'videocam_off';
|
||||
const audioTracks = this.state.localStream && this.state.localStream.getAudioTracks();
|
||||
const videoTracks = !this.state.audioOnly && this.state.localStream && this.state.localStream.getVideoTracks();
|
||||
const disabled = !this.state.pc || !this.state.dataChannel || !(audioTracks && audioTracks[0]);
|
||||
const audioIcon = audioTracks && audioTracks[0] && audioTracks[0].enabled ? 'mic' : 'mic_off';
|
||||
const videoIcon = videoTracks && videoTracks[0] && videoTracks[0].enabled && videoTracks[0].readyState == 'live' ? 'videocam' : 'videocam_off';
|
||||
const peerTitle = clipStr(this.props.title, MAX_PEER_TITLE_LENGTH);
|
||||
const pulseAnimation = this.state.waitingForPeer ? ' pulse' : '';
|
||||
|
||||
let remoteActive = false;
|
||||
if (this.remoteRef.current && this.remoteRef.current.srcObject && this.state.remoteVideoLive) {
|
||||
const rstream = this.remoteRef.current.srcObject;
|
||||
if (rstream.getVideoTracks().length > 0) {
|
||||
const t = rstream.getVideoTracks()[0];
|
||||
remoteActive = t.enabled && t.readyState == 'live';
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div id="video-container">
|
||||
<div id="video-container-panel">
|
||||
<div className="call-party self">
|
||||
<div className="call-party self" disabled={this.state.audioOnly}>
|
||||
<video ref={this.localRef} autoPlay muted playsInline />
|
||||
<div className="caller-name inactive">
|
||||
<FormattedMessage id="calls_you_label"
|
||||
defaultMessage="You" description="Shown over the local video screen" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="call-party peer">
|
||||
<video ref={this.remoteRef} autoPlay playsInline />
|
||||
<div className="call-party peer" disabled={!remoteActive}>
|
||||
{remoteActive ?
|
||||
<div className="caller-name inactive">{peerTitle}</div> :
|
||||
<div className={`caller-card${pulseAnimation}`}>
|
||||
<div className="avatar-box">
|
||||
<LetterTile
|
||||
tinode={this.props.tinode}
|
||||
avatar={this.props.avatar}
|
||||
topic={this.props.topic}
|
||||
title={this.props.title} />
|
||||
<>
|
||||
<video ref={this.remoteRef} autoPlay playsInline />
|
||||
<div className="caller-name inactive">{peerTitle}</div>
|
||||
</> :
|
||||
<>
|
||||
<audio ref={this.remoteRef} autoPlay />
|
||||
<div className={`caller-card${pulseAnimation}`}>
|
||||
<div className="avatar-box">
|
||||
<LetterTile
|
||||
tinode={this.props.tinode}
|
||||
avatar={this.props.avatar}
|
||||
topic={this.props.topic}
|
||||
title={this.props.title} />
|
||||
</div>
|
||||
<div className="caller-name">{peerTitle}</div>
|
||||
</div>
|
||||
<div className="caller-name">{peerTitle}</div>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -28,7 +28,7 @@ export default class CallStatus extends React.PureComponent {
|
||||
duration = <FormattedMessage id="call_disconnected" defaultMessage="disconnected" description="Label for disconnected call" />;
|
||||
break;
|
||||
}
|
||||
} else if (this.props.callState == 'started' && !this.props.duration) {
|
||||
} else if (['accepted', 'started'].includes(this.props.callState) && !this.props.duration) {
|
||||
duration = <FormattedMessage id="call_in_progress" defaultMessage="in progress" description="Label for call in progress" />
|
||||
} else {
|
||||
duration = <span>{secondsToTime(this.props.duration / 1000)}</span>;
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
// Selector for country dialing code.
|
||||
|
||||
import React from 'react';
|
||||
import { injectIntl } from 'react-intl';
|
||||
|
||||
import * as dcodes from '../dcodes.json';
|
||||
|
||||
import { flagEmoji } from '../lib/strformat';
|
||||
|
||||
class PhoneCountrySelector extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.countries = [];
|
||||
const { formatDisplayName } = props.intl;
|
||||
dcodes.default.forEach(dc => {
|
||||
const parts = dc.dial.split(',');
|
||||
parts.forEach(part => {
|
||||
this.countries.push({
|
||||
dial: part.trim(),
|
||||
code: dc.code,
|
||||
flag: flagEmoji(dc.code),
|
||||
name: formatDisplayName(dc.code, {type: 'region'})
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
this.countries.sort((a, b) => a.name.localeCompare(b.name));
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (this.selectedRef) {
|
||||
this.selectedRef.scrollIntoView({block: 'center', inline: 'nearest'});
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const countries = [];
|
||||
const selected = this.props.selected || 'US';
|
||||
this.countries.forEach((c, idx) => {
|
||||
const style = (c.code == selected ? 'selected ' : '');
|
||||
countries.push(<li className={style} key={idx}
|
||||
ref={ref => {if (c.code == selected) {this.selectedRef = ref}}}
|
||||
onClick={_ => this.props.onSubmit(c.code, c.dial)}>
|
||||
<span className="country-flag">{c.flag}</span>
|
||||
<span className="country"> {c.name}</span>
|
||||
<span className="dial-code"> +{c.dial}</span>
|
||||
</li>);
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="scrollable-panel" style={{height: '30rem'}}>
|
||||
<ul className="phone-country-selector">
|
||||
{countries}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default injectIntl(PhoneCountrySelector);
|
||||
@@ -0,0 +1,109 @@
|
||||
// Editor for a phone number.
|
||||
|
||||
import React from 'react';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import { getExampleNumber, parsePhoneNumber } from 'libphonenumber-js/mobile';
|
||||
import examples from 'libphonenumber-js/mobile/examples'
|
||||
|
||||
import * as dcodes from '../dcodes.json';
|
||||
import { flagEmoji } from '../lib/strformat';
|
||||
|
||||
const messages = defineMessages({
|
||||
mobile_number_required: {
|
||||
id: 'mobile_number_required',
|
||||
defaultMessage: 'Mobile phone number required',
|
||||
description: 'Error message'
|
||||
}
|
||||
});
|
||||
|
||||
class PhoneEdit extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.codeMap = {};
|
||||
dcodes.default.forEach(dc => { this.codeMap[dc.code] = dc.dial; });
|
||||
|
||||
const code = props.countryCode || 'US';
|
||||
const dial = this.codeMap[code];
|
||||
|
||||
this.state = {
|
||||
countryCode: code,
|
||||
dialCode: dial,
|
||||
localNumber: '',
|
||||
placeholderNumber: this.placeholderNumber(code, dial)
|
||||
};
|
||||
|
||||
this.handleChange = this.handleChange.bind(this);
|
||||
this.handleFinished = this.handleFinished.bind(this);
|
||||
this.handleKeyDown = this.handleKeyDown.bind(this);
|
||||
this.showCountrySelector = this.showCountrySelector.bind(this);
|
||||
}
|
||||
|
||||
handleChange(e) {
|
||||
this.setState({localNumber: this.filterNumber(e.target.value)});
|
||||
}
|
||||
|
||||
handleFinished(e) {
|
||||
e.preventDefault();
|
||||
const raw = `${this.state.dialCode}${this.state.localNumber.trim()}`.replace(/[^\d]/g, '');
|
||||
let number = null;
|
||||
try {
|
||||
number = parsePhoneNumber(`+${raw}`);
|
||||
} catch (err) {}
|
||||
|
||||
if (!number || !number.isValid()) {
|
||||
this.inputField.setCustomValidity(this.props.intl.formatMessage(messages.mobile_number_required));
|
||||
return;
|
||||
}
|
||||
this.props.onSubmit(number.format('E.164'));
|
||||
}
|
||||
|
||||
|
||||
handleKeyDown(e) {
|
||||
if (e.key === 'Enter') {
|
||||
this.handleFinished(e);
|
||||
}
|
||||
}
|
||||
|
||||
showCountrySelector() {
|
||||
this.props.onShowCountrySelector(this.state.countryCode, this.state.dialCode,
|
||||
(code, dial) => {
|
||||
this.setState({
|
||||
countryCode: code,
|
||||
dialCode: dial,
|
||||
placeholderNumber: this.placeholderNumber(code, dial)
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
// Filter out characters not permitted in a phone number.
|
||||
filterNumber(number) {
|
||||
if (!number) {
|
||||
return number;
|
||||
}
|
||||
// Leave numbers, space, (, ), -, and .
|
||||
// The + is not allowed: it's handled by the country code portion.
|
||||
return number.replace(/[^-\s().\d]/g, '');
|
||||
}
|
||||
|
||||
placeholderNumber(code, dial) {
|
||||
const sample = getExampleNumber(code, examples);
|
||||
return sample ? sample.formatInternational().substring(dial.length + 1).trim() : '123 0123';
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<>
|
||||
<span className="dial-code" onClick={this.showCountrySelector}>
|
||||
<span className="country-flag">{flagEmoji(this.state.countryCode)} </span>
|
||||
+{this.state.dialCode} </span>
|
||||
<input type="tel" ref={ref => {this.inputField = ref}} placeholder={this.state.placeholderNumber}
|
||||
value={this.state.localNumber} onChange={this.handleChange}
|
||||
maxLength={17} onKeyDown={this.handleKeyDown} onBlur={this.handleFinished}
|
||||
required autoFocus={this.props.autoFocus} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default injectIntl(PhoneEdit);
|
||||
@@ -30,7 +30,7 @@ export default class SearchContacts extends React.PureComponent {
|
||||
|
||||
handleSearch(e) {
|
||||
e.preventDefault();
|
||||
var query = this.state.search.trim();
|
||||
const query = this.state.search.trim();
|
||||
this.setState({edited: (query.length > 0)});
|
||||
this.props.onSearchContacts(query.length > 0 ? query : Tinode.DEL_CHAR);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Send message form.
|
||||
import React, { Suspense } from 'react';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
|
||||
import { Drafty } from 'tinode-sdk';
|
||||
|
||||
// Lazy-loading AudioRecorder because it's quite large due to
|
||||
@@ -90,6 +90,10 @@ class SendMessage extends React.PureComponent {
|
||||
componentDidMount() {
|
||||
if (this.messageEditArea) {
|
||||
this.messageEditArea.addEventListener('paste', this.handlePasteEvent, false);
|
||||
if (window.getComputedStyle(this.messageEditArea).getPropertyValue('transition-property') == 'all') {
|
||||
// Set focus on desktop, but not on mobile: focus causes soft keyboard to pop up.
|
||||
this.messageEditArea.focus();
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({quote: this.formatReply()});
|
||||
@@ -103,7 +107,10 @@ class SendMessage extends React.PureComponent {
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (this.messageEditArea) {
|
||||
this.messageEditArea.focus();
|
||||
if (window.getComputedStyle(this.messageEditArea).getPropertyValue('transition-property') == 'all') {
|
||||
// Set focus on desktop, but not on mobile: focus causes soft keyboard to pop up.
|
||||
this.messageEditArea.focus();
|
||||
}
|
||||
}
|
||||
|
||||
if (prevProps.topicName != this.props.topicName) {
|
||||
@@ -260,17 +267,17 @@ class SendMessage extends React.PureComponent {
|
||||
{this.props.noInput ?
|
||||
(quote || <div className="hr thin" />) :
|
||||
(this.state.audioRec ?
|
||||
(<Suspense fallback={<div>Loading...</div>}>
|
||||
(<Suspense fallback={<div><FormattedMessage id="loading_note" defaultMessage="Loading..."
|
||||
description="Message shown when component is loading"/></div>}>
|
||||
<AudioRecorder
|
||||
onRecordingProgress={_ => this.props.onKeyPress(true)}
|
||||
onDeleted={_ => this.setState({audioRec: false})}
|
||||
onFinished={this.handleAttachAudio}/>
|
||||
</Suspense>) :
|
||||
<textarea id="sendMessage" placeholder={prompt}
|
||||
<textarea id="send-message-input" placeholder={prompt}
|
||||
value={this.state.message} onChange={this.handleMessageTyping}
|
||||
onKeyPress={this.handleKeyPress}
|
||||
ref={(ref) => {this.messageEditArea = ref;}}
|
||||
autoFocus />)}
|
||||
ref={ref => {this.messageEditArea = ref;}} />)}
|
||||
{this.state.message || !audioEnabled ?
|
||||
<a href="#" onClick={this.handleSend} title={formatMessage(messages.icon_title_send)}>
|
||||
<i className="material-icons">{sendIcon}</i>
|
||||
|
||||
@@ -117,7 +117,7 @@ export default class TopicCommon extends React.Component {
|
||||
// Request server to change tags.
|
||||
handleTagsUpdated(tags) {
|
||||
if (!arrayEqual(this.state.tags.slice(0), tags.slice(0))) {
|
||||
this.props.onTopicTagsUpdateRequest(this.props.topic, tags);
|
||||
this.props.onUpdateTagsRequest(this.props.topic, tags);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,10 +2,6 @@
|
||||
import React from 'react';
|
||||
import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
|
||||
|
||||
import InPlaceEdit from './in-place-edit.jsx';
|
||||
|
||||
import { NO_ACCESS_MODE } from '../config.js';
|
||||
|
||||
const messages = defineMessages({
|
||||
clear_messages: {
|
||||
id: 'action_clear_messages',
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"251.prod.js","mappings":"skNAQA,MAAMA,UAAkBC,IAAAA,cACtBC,YAAYC,GACVC,MAAMD,GAENE,KAAKC,QAAU,CAAC,EAChBC,EAAAA,SAAuBC,IAAQH,KAAKC,QAAQE,EAAGC,MAAQD,EAAGE,IAAI,IAE9DL,KAAKM,MAAQ,CACXC,YAAaT,EAAMS,aAAe,KAClCC,SAAUR,KAAKC,QAAQH,EAAMS,aAAe,MAC5CE,YAAa,IAGfT,KAAKU,aAAeV,KAAKU,aAAaC,KAAKX,MAC3CA,KAAKY,eAAiBZ,KAAKY,eAAeD,KAAKX,MAC/CA,KAAKa,cAAgBb,KAAKa,cAAcF,KAAKX,MAC7CA,KAAKc,oBAAsBd,KAAKc,oBAAoBH,KAAKX,KAC3D,CAEAU,aAAaK,GACXf,KAAKgB,SAAS,CAACP,YAAaT,KAAKiB,aAAaF,EAAEG,OAAOC,QACzD,CAEAP,eAAeG,GAEbA,EAAEK,iBACF,MAAMC,EAAU,GAAErB,KAAKM,MAAME,WAAWR,KAAKM,MAAMG,YAAYa,SAASC,QAAQ,SAAU,IAC1FvB,KAAKF,MAAM0B,SAAU,IAAGH,IAC1B,CAGAR,cAAcE,GACE,UAAVA,EAAEU,KACJzB,KAAKY,eAAeG,EAExB,CAEAD,sBACEd,KAAKF,MAAM4B,sBAAsB1B,KAAKM,MAAMC,YAAaP,KAAKM,MAAME,SACtE,CAGAS,aAAaI,GACX,OAAKA,EAKEA,EAAOE,QAAQ,eAAgB,IAJ7BF,CAKX,CAEAM,SACE,OACE,oCACE,0BAAMC,UAAU,YAAYC,QAAS7B,KAAKc,qBACxC,0BAAMc,UAAU,iBAAgBE,EAAAA,EAAAA,IAAU9B,KAAKM,MAAMC,aAAa,KAAa,IAC7EP,KAAKM,MAAME,SAAS,KACxB,2BAAOuB,KAAK,MAAMC,YAAahC,KAAKM,MAAM0B,YACtCb,MAAOnB,KAAKM,MAAMG,YAAawB,SAAUjC,KAAKU,aAC9CwB,UAAW,GAAIC,UAAWnC,KAAKa,cAAeuB,OAAQpC,KAAKY,eAC3DyB,UAAQ,EAACC,UAAWtC,KAAKF,MAAMwC,YAGzC,EAGF,SAAeC,EAAAA,EAAAA,YAAW5C,E","sources":["webpack://tinode-webapp/./src/widgets/phone-edit.jsx"],"sourcesContent":["// Editor for a phone number.\n\nimport React from 'react';\nimport { injectIntl } from 'react-intl';\n\nimport * as dcodes from '../dcodes.json';\nimport { flagEmoji } from '../lib/strformat';\n\nclass PhoneEdit extends React.PureComponent {\n constructor(props) {\n super(props);\n\n this.codeMap = {};\n dcodes.default.forEach(dc => { this.codeMap[dc.code] = dc.dial; });\n\n this.state = {\n countryCode: props.countryCode || 'US',\n dialCode: this.codeMap[props.countryCode || 'US'],\n localNumber: ''\n };\n\n this.handleChange = this.handleChange.bind(this);\n this.handleFinished = this.handleFinished.bind(this);\n this.handleKeyDown = this.handleKeyDown.bind(this);\n this.showCountrySelector = this.showCountrySelector.bind(this);\n }\n\n handleChange(e) {\n this.setState({localNumber: this.filterNumber(e.target.value)});\n }\n\n handleFinished(e) {\n // this.setCustomValidity(\"Mobile phone number required\");\n e.preventDefault();\n const number = `${this.state.dialCode}${this.state.localNumber.trim()}`.replace(/[^\\d]/g, '')\n this.props.onSubmit(`+${number}`);\n }\n\n\n handleKeyDown(e) {\n if (e.key === 'Enter') {\n this.handleFinished(e);\n }\n }\n\n showCountrySelector() {\n this.props.onShowCountrySelector(this.state.countryCode, this.state.dialCode);\n }\n\n // Filter out characters not permitted in a phone number.\n filterNumber(number) {\n if (!number) {\n return number;\n }\n // Leave numbers, space, (, ), -, and .\n // The + is not allowed: it's handled by the country code portion.\n return number.replace(/[^-\\s().\\d]/g, '');\n }\n\n render() {\n return (\n <>\n <span className=\"dial-code\" onClick={this.showCountrySelector}>\n <span className=\"country-flag\">{flagEmoji(this.state.countryCode)} </span>\n +{this.state.dialCode} </span>\n <input type=\"tel\" placeholder={this.state.placeholder}\n value={this.state.localNumber} onChange={this.handleChange}\n maxLength={17} onKeyDown={this.handleKeyDown} onBlur={this.handleFinished}\n required autoFocus={this.props.autoFocus} />\n </>\n );\n }\n}\n\nexport default injectIntl(PhoneEdit);\n"],"names":["PhoneEdit","React","constructor","props","super","this","codeMap","dcodes","dc","code","dial","state","countryCode","dialCode","localNumber","handleChange","bind","handleFinished","handleKeyDown","showCountrySelector","e","setState","filterNumber","target","value","preventDefault","number","trim","replace","onSubmit","key","onShowCountrySelector","render","className","onClick","flagEmoji","type","placeholder","onChange","maxLength","onKeyDown","onBlur","required","autoFocus","injectIntl"],"sourceRoot":""}
|
||||
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"290.prod.js","mappings":"2MASA,MAAMA,UAA6BC,IAAAA,cACjCC,YAAYC,GACVC,MAAMD,GAENE,KAAKC,UAAY,GACjB,MAAM,kBAAEC,GAAsBJ,EAAMK,KACpCC,EAAAA,SAAuBC,IACPA,EAAGC,KAAKC,MAAM,KACtBC,SAAQC,IACZT,KAAKC,UAAUS,KAAK,CAClBJ,KAAMG,EAAKE,OACXC,KAAMP,EAAGO,KACTC,MAAMC,EAAAA,EAAAA,IAAUT,EAAGO,MACnBG,KAAMb,EAAkBG,EAAGO,KAAM,CAACI,KAAM,YACxC,GACF,IAGJhB,KAAKC,UAAUgB,MAAK,CAACC,EAAGC,IAAMD,EAAEH,KAAKK,cAAcD,EAAEJ,OACvD,CAEAM,oBACMrB,KAAKsB,aACPtB,KAAKsB,YAAYC,eAAe,CAACC,MAAO,SAAUC,OAAQ,WAE9D,CAEAC,SACE,MAAMzB,EAAY,GACZ0B,EAAW3B,KAAKF,MAAM6B,UAAY,KAYxC,OAXA3B,KAAKC,UAAUO,SAAQ,CAACoB,EAAGC,KACzB,MAAMC,EAASF,EAAEhB,MAAQe,EAAW,YAAc,GAClD1B,EAAUS,KAAK,wBAAIqB,UAAWD,EAAOE,IAAKH,EACxCI,IAAKA,IAAYL,EAAEhB,MAAQe,IAAW3B,KAAKsB,YAAcW,EAAG,EAC5DC,QAASC,GAAKnC,KAAKF,MAAMsC,SAASR,EAAEhB,KAAMgB,EAAEtB,OAC5C,0BAAMyB,UAAU,gBAAgBH,EAAEf,MAClC,0BAAMkB,UAAU,WAAU,IAAOH,EAAEb,MACnC,0BAAMgB,UAAU,aAAY,KAAQH,EAAEtB,OAClC,IAIN,yBAAKyB,UAAU,mBAAmBD,MAAO,CAACO,OAAQ,UAChD,wBAAIN,UAAU,0BACX9B,GAIT,EAGF,SAAeqC,EAAAA,EAAAA,YAAW3C,E","sources":["webpack://tinode-webapp/./src/widgets/phone-country-selector.jsx"],"sourcesContent":["// Selector for country dialing code.\n\nimport React from 'react';\nimport { injectIntl } from 'react-intl';\n\nimport * as dcodes from '../dcodes.json';\n\nimport { flagEmoji } from '../lib/strformat';\n\nclass PhoneCountrySelector extends React.PureComponent {\n constructor(props) {\n super(props);\n\n this.countries = [];\n const { formatDisplayName } = props.intl;\n dcodes.default.forEach(dc => {\n const parts = dc.dial.split(',');\n parts.forEach(part => {\n this.countries.push({\n dial: part.trim(),\n code: dc.code,\n flag: flagEmoji(dc.code),\n name: formatDisplayName(dc.code, {type: 'region'})\n });\n });\n });\n\n this.countries.sort((a, b) => a.name.localeCompare(b.name));\n }\n\n componentDidMount() {\n if (this.selectedRef) {\n this.selectedRef.scrollIntoView({block: 'center', inline: 'nearest'});\n }\n }\n\n render() {\n const countries = [];\n const selected = this.props.selected || 'US';\n this.countries.forEach((c, idx) => {\n const style = (c.code == selected ? 'selected ' : '');\n countries.push(<li className={style} key={idx}\n ref={ref => {if (c.code == selected) {this.selectedRef = ref}}}\n onClick={_ => this.props.onSubmit(c.code, c.dial)}>\n <span className=\"country-flag\">{c.flag}</span>\n <span className=\"country\"> {c.name}</span>\n <span className=\"dial-code\"> +{c.dial}</span>\n </li>);\n });\n\n return (\n <div className=\"scrollable-panel\" style={{height: '30rem'}}>\n <ul className=\"phone-country-selector\">\n {countries}\n </ul>\n </div>\n );\n }\n}\n\nexport default injectIntl(PhoneCountrySelector);\n"],"names":["PhoneCountrySelector","React","constructor","props","super","this","countries","formatDisplayName","intl","dcodes","dc","dial","split","forEach","part","push","trim","code","flag","flagEmoji","name","type","sort","a","b","localeCompare","componentDidMount","selectedRef","scrollIntoView","block","inline","render","selected","c","idx","style","className","key","ref","onClick","_","onSubmit","height","injectIntl"],"sourceRoot":""}
|
||||
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+721
-295
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"src_widgets_phone-country-selector_jsx.dev.js","mappings":";;;;;;;;;;;;;;;;;;;AAE0B;AACc;AAEC;AAEI;AAE7C,MAAMI,oBAAoB,SAASJ,4DAAmB,CAAC;EACrDM,WAAW,CAACC,KAAK,EAAE;IACjB,KAAK,CAACA,KAAK,CAAC;IAEZ,IAAI,CAACC,SAAS,GAAG,EAAE;IACnB,MAAM;MAAEC;IAAkB,CAAC,GAAGF,KAAK,CAACG,IAAI;IACxCR,iDAAsB,CAACW,EAAE,IAAI;MAC3B,MAAMC,KAAK,GAAGD,EAAE,CAACE,IAAI,CAACC,KAAK,CAAC,GAAG,CAAC;MAChCF,KAAK,CAACF,OAAO,CAACK,IAAI,IAAI;QACpB,IAAI,CAACT,SAAS,CAACU,IAAI,CAAC;UAClBH,IAAI,EAAEE,IAAI,CAACE,IAAI,EAAE;UACjBC,IAAI,EAAEP,EAAE,CAACO,IAAI;UACbC,IAAI,EAAElB,yDAAS,CAACU,EAAE,CAACO,IAAI,CAAC;UACxBE,IAAI,EAAEb,iBAAiB,CAACI,EAAE,CAACO,IAAI,EAAE;YAACG,IAAI,EAAE;UAAQ,CAAC;QACnD,CAAC,CAAC;MACJ,CAAC,CAAC;IACJ,CAAC,CAAC;IAEF,IAAI,CAACf,SAAS,CAACgB,IAAI,CAAC,CAACC,CAAC,EAAEC,CAAC,KAAKD,CAAC,CAACH,IAAI,CAACK,aAAa,CAACD,CAAC,CAACJ,IAAI,CAAC,CAAC;EAC7D;EAEAM,iBAAiB,GAAG;IAClB,IAAI,IAAI,CAACC,WAAW,EAAE;MACpB,IAAI,CAACA,WAAW,CAACC,cAAc,CAAC;QAACC,KAAK,EAAE,QAAQ;QAAEC,MAAM,EAAE;MAAS,CAAC,CAAC;IACvE;EACF;EAEAC,MAAM,GAAG;IACP,MAAMzB,SAAS,GAAG,EAAE;IACpB,MAAM0B,QAAQ,GAAG,IAAI,CAAC3B,KAAK,CAAC2B,QAAQ,IAAI,IAAI;IAC5C,IAAI,CAAC1B,SAAS,CAACI,OAAO,CAAC,CAACuB,CAAC,EAAEC,GAAG,KAAK;MACjC,MAAMC,KAAK,GAAIF,CAAC,CAACf,IAAI,IAAIc,QAAQ,GAAG,WAAW,GAAG,EAAG;MACrD1B,SAAS,CAACU,IAAI,CAAC;QAAI,SAAS,EAAEmB,KAAM;QAAC,GAAG,EAAED,GAAI;QAC5C,GAAG,EAAEE,GAAG,IAAI;UAAC,IAAIH,CAAC,CAACf,IAAI,IAAIc,QAAQ,EAAE;YAAC,IAAI,CAACL,WAAW,GAAGS,GAAG;UAAA;QAAC,CAAE;QAC/D,OAAO,EAAEC,CAAC,IAAI,IAAI,CAAChC,KAAK,CAACiC,QAAQ,CAACL,CAAC,CAACf,IAAI,EAAEe,CAAC,CAACpB,IAAI;MAAE,GAClD;QAAM,SAAS,EAAC;MAAc,GAAEoB,CAAC,CAACd,IAAI,CAAQ,EAC9C;QAAM,SAAS,EAAC;MAAS,GAAC,MAAM,EAACc,CAAC,CAACb,IAAI,CAAQ,EAC/C;QAAM,SAAS,EAAC;MAAW,GAAC,OAAO,EAACa,CAAC,CAACpB,IAAI,CAAQ,CAC/C,CAAC;IACR,CAAC,CAAC;IAEF,OACE;MAAK,SAAS,EAAC,kBAAkB;MAAC,KAAK,EAAE;QAAC0B,MAAM,EAAE;MAAO;IAAE,GACzD;MAAI,SAAS,EAAC;IAAwB,GACnCjC,SAAS,CACP,CACD;EAEV;AACF;AAEA,iEAAeP,sDAAU,CAACG,oBAAoB,CAAC","sources":["webpack://tinode-webapp/./src/widgets/phone-country-selector.jsx"],"sourcesContent":["// Selector for country dialing code.\n\nimport React from 'react';\nimport { injectIntl } from 'react-intl';\n\nimport * as dcodes from '../dcodes.json';\n\nimport { flagEmoji } from '../lib/strformat';\n\nclass PhoneCountrySelector extends React.PureComponent {\n constructor(props) {\n super(props);\n\n this.countries = [];\n const { formatDisplayName } = props.intl;\n dcodes.default.forEach(dc => {\n const parts = dc.dial.split(',');\n parts.forEach(part => {\n this.countries.push({\n dial: part.trim(),\n code: dc.code,\n flag: flagEmoji(dc.code),\n name: formatDisplayName(dc.code, {type: 'region'})\n });\n });\n });\n\n this.countries.sort((a, b) => a.name.localeCompare(b.name));\n }\n\n componentDidMount() {\n if (this.selectedRef) {\n this.selectedRef.scrollIntoView({block: 'center', inline: 'nearest'});\n }\n }\n\n render() {\n const countries = [];\n const selected = this.props.selected || 'US';\n this.countries.forEach((c, idx) => {\n const style = (c.code == selected ? 'selected ' : '');\n countries.push(<li className={style} key={idx}\n ref={ref => {if (c.code == selected) {this.selectedRef = ref}}}\n onClick={_ => this.props.onSubmit(c.code, c.dial)}>\n <span className=\"country-flag\">{c.flag}</span>\n <span className=\"country\"> {c.name}</span>\n <span className=\"dial-code\"> +{c.dial}</span>\n </li>);\n });\n\n return (\n <div className=\"scrollable-panel\" style={{height: '30rem'}}>\n <ul className=\"phone-country-selector\">\n {countries}\n </ul>\n </div>\n );\n }\n}\n\nexport default injectIntl(PhoneCountrySelector);\n"],"names":["React","injectIntl","dcodes","flagEmoji","PhoneCountrySelector","PureComponent","constructor","props","countries","formatDisplayName","intl","default","forEach","dc","parts","dial","split","part","push","trim","code","flag","name","type","sort","a","b","localeCompare","componentDidMount","selectedRef","scrollIntoView","block","inline","render","selected","c","idx","style","ref","_","onSubmit","height"],"sourceRoot":""}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+51
-27
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+2175
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
+1
-1
@@ -1,3 +1,3 @@
|
||||
// This is a generated file. Don't edit.
|
||||
|
||||
const PACKAGE_VERSION = "0.21.0-beta1";
|
||||
const PACKAGE_VERSION = "0.21.0-rc2";
|
||||
|
||||
Reference in New Issue
Block a user