Compare commits

...
31 Commits
Author SHA1 Message Date
or-else d32c1636ce editing once works, editing twice does not 2022-11-19 16:21:24 -08:00
or-else a99ad81196 all dependencies updated to latest, including breaking changes 2022-11-17 11:43:28 -08:00
or-else eec52a590c bumped version to 0.20.2, dependencies updated 2022-11-16 09:33:33 -08:00
or-else c7b8ce11e0 force reconnect if countdown is negative (computer woke up from sleep) 2022-11-14 17:20:07 -08:00
or-else 2156aa0877 wrap long text in <tt> 2022-11-06 12:08:02 -08:00
or-else 966dec9654 rebuilt 2022-11-02 17:48:08 -07:00
or-else 0b2ab61422 dependencies updated, version bumped to 0.20.1 2022-11-02 17:46:21 -07:00
or-else 092b4697a9 releasing 0.20.0 2022-10-11 11:27:02 +03:00
or-else 6c5fe836f4 rebuilt with fix for #101 2022-09-22 14:45:43 +03:00
or-else 40b99f7332 update dependencies, bump version to 0.20.0-rc2, rebuild 2022-09-06 15:22:27 -07:00
or-else d45a141704 fix avatar upload on signup 2022-09-01 11:12:09 -07:00
or-else ee106a17f6 incomplete fix for authentication after credential validation, #100 2022-08-29 18:37:10 -07:00
or-else 22320808fc fix frozen validation input field 2022-08-27 12:51:52 -07:00
or-else 5006a6fb34 partial fix for avatar uploads 2022-08-25 18:50:11 -10:00
or-else df203f48bd fix display of a validation success message 2022-08-25 15:02:11 -10:00
or-else 0e53234d02 add "success" message when credential was validated, #100 2022-08-25 13:45:37 -10:00
or-else 14a4725c94 Merge branch 'next' of https://github.com/tinode/webapp into next 2022-08-25 13:03:44 -10:00
or-else 740cb7faf8 partial fix for #100 2022-08-25 13:03:31 -10:00
aforge 384955777d Update host when user hits Enter in host selector. 2022-08-24 12:36:10 +03:00
or-else 8b80f59d3a fix for a crash on a typo in a name of a variable 2022-08-23 18:12:51 -10:00
or-else 6a6e3ad754 restrict app width on very wide monitors 2022-08-22 12:11:18 -10:00
or-else d15aa9372d minor visual tweaks and missing translation 2022-08-22 12:05:32 -10:00
or-else 5144f581e7 better handling of non-default hosts, like connecting to sandbox from localhost 2022-08-15 15:06:33 -07:00
or-else 8250c5f372 enable connections to sandbox from localhost 2022-08-14 16:01:09 -07:00
or-else ae26e0ba3d added "Helping out" section 2022-08-09 18:04:32 -07:00
or-else 6a52591e1a updated dependencies and bumped version to -rc1, rebuilt 2022-08-09 16:02:19 -07:00
or-else 81afca1efe merge with remote 2022-08-09 15:16:43 -07:00
aforge 3922db3aa6 Update call status text. 2022-08-02 13:43:57 +03:00
aforge 9cd778534a Add onIceCandidateError handler for video calls. 2022-07-26 01:07:26 +03:00
or-else e42de09504 rebuilt with fixed sdk 2022-07-02 15:09:51 -07:00
or-else 5c842ef212 bump version and rebuild 2022-06-30 11:29:04 -07:00
49 changed files with 4608 additions and 6393 deletions
+10
View File
@@ -25,6 +25,16 @@ This is _NOT_ a standalone app, this is just a frontend, a client. It requires a
* For bugs and feature requests [open an issue](https://github.com/tinode/webchat/issues/new).
* Use https://tinode.co/contact for commercial inquiries.
## Helping out
* If you appreciate our work, please help spread the word! Sharing on Reddit, HN, and other communities helps more than you think.
* Consider buying paid support: https://tinode.co/support.html
* If you are a software developer, send us your pull requests with bug fixes and new features.
* If you use the app and discover bugs or missing features, let us know by filing bug reports and feature requests. Vote for existing [feature requests](https://github.com/tinode/chat/issues?q=is%3Aissue+is%3Aopen+sort%3Areactions-%2B1-desc+label%3A%22feature+request%22) you find most valuable.
* If you speak a language other than English, [translate](https://github.com/tinode/chat/blob/devel/docs/translations.md#webapp) the apps into your language. You may also review and improve existing translations.
* If you are a UI/UX expert, help us polish the app UI.
* Use it: install it for your colleagues or friends at work or at home.
## Internationalization
The app is fully internationalized using [React-Intl](https://github.com/formatjs/react-intl). The UI language is selected automatically from the language specified by the browser. A specific language can be forced by adding `#?hl=XX` parameter to the URL when starting the app, e.g. https://web.tinode.co/#?hl=ru.
+14 -3
View File
@@ -20,7 +20,6 @@ html, body {
tt {
font-family: 'Roboto Mono', Courier, monospace;
white-space: nowrap;
}
a {
@@ -284,6 +283,7 @@ ul.menu {
ul.menu > li {
padding: 0.75rem 0.75rem 0.75rem 1.25rem;
cursor: pointer;
}
ul.menu > li:hover {
@@ -463,6 +463,8 @@ div.trusted-badge {
touch-action: manipulation;
white-space: nowrap;
text-transform: uppercase;
background-color: white;
z-index: 1;
}
.flat-button:hover,
.flat-button:focus {
@@ -533,7 +535,7 @@ div.trusted-badge {
#mountPoint {
height: 100%;
padding-top: 1rem;
padding-bottom: 3rem;
padding-bottom: 1rem;
}
/* Top-level application layout style: sidepanel + main view */
@@ -839,7 +841,7 @@ table.permission-editor td.checkbox {
}
input#host-name {
width: 15rem;
width: 100%;
}
/* Input with a search icon on the left */
@@ -2921,6 +2923,15 @@ ul.tabbar li.active > a {
color: #666C;
}
/*
Very wide monitor.
*/
@media (min-width: 1440px){
#app-container {
max-width: 114rem;
}
}
/*
Mobile styles.
*/
+1 -1
View File
File diff suppressed because one or more lines are too long
+3 -3
View File
@@ -30,10 +30,10 @@
<!-- Styles for local elements -->
<link rel="stylesheet" href="css/base.css" />
<!-- ReactJS -->
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/react@17/umd/react.development.js"></script>
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/react-dom@17/umd/react-dom.development.js"></script>
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/react@18/umd/react.development.js"></script>
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/react-dom@18/umd/react-dom.development.js"></script>
<!-- React-Intl for translations -->
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/react-intl@5/react-intl.iife.js"></script>
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/react-intl@6/react-intl.iife.js"></script>
</head>
<body>
+3 -3
View File
@@ -23,9 +23,9 @@
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,400i,500,700,700i" />
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons" />
<link rel="stylesheet" href="css/base.min.css" />
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/react@17.0.2/umd/react.production.min.js"></script>
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/react-dom@17.0.2/umd/react-dom.production.min.js"></script>
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/react-intl@5.20.7/react-intl.iife.min.js"></script>
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/react@18.2.0/umd/react.production.min.js"></script>
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/react-dom@18.2.0/umd/react-dom.production.min.js"></script>
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/react-intl@6.2.1/react-intl.iife.min.js"></script>
</head>
<body>
<noscript><div id="noscript-message"><div id="noscript-title">Javascript support is required.</div>
+2187 -3080
View File
File diff suppressed because it is too large Load Diff
+21 -20
View File
@@ -1,7 +1,8 @@
{
"name": "tinode-webapp",
"description": "Tinode messenger for the web",
"version": "0.20.0-rc1",
"version": "0.21.0-beta1",
"repository": {
"type": "git",
"url": "https://github.com/tinode/example-react-js.git"
@@ -66,29 +67,29 @@
"author": "Tinode Authors <info@tinode.co>",
"license": "Apache-2.0",
"dependencies": {
"firebase": "^9.6.7",
"firebase": "^9.14.0",
"fix-webm-duration": "^1.0.5",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-intl": "^5.20.2",
"tinode-sdk": "^0.20.0-rc1",
"webrtc-adapter": "^8.1.1"
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-intl": "^6.2.1",
"tinode-sdk": "^0.21.0-beta1",
"webrtc-adapter": "^8.2.0"
},
"devDependencies": {
"@babel/core": "^7.14.5",
"@babel/preset-env": "^7.14.5",
"@babel/preset-react": "^7.14.5",
"@formatjs/cli": "^4.2.20",
"babel-loader": "^8.2.2",
"babel-plugin-formatjs": "^10.2.19",
"browserslist": "^4.16.5",
"copy-webpack-plugin": "^10.0.0",
"cssnano": "^5.0.6",
"@babel/core": "^7.20.2",
"@babel/preset-env": "^7.20.2",
"@babel/preset-react": "^7.18.6",
"@formatjs/cli": "^5.1.3",
"babel-loader": "^9.1.0",
"babel-plugin-formatjs": "^10.3.31",
"browserslist": "^4.21.4",
"copy-webpack-plugin": "^11.0.0",
"cssnano": "^5.1.14",
"glob": "^8.0.3",
"mkdirp": "^1.0.4",
"postcss": "^8.3.2",
"postcss-cli": "^9.0.2",
"webpack": "^5.38.1",
"webpack-cli": "^4.7.2"
"postcss": "^8.4.19",
"postcss-cli": "^10.0.0",
"webpack": "^5.75.0",
"webpack-cli": "^5.0.0"
}
}
+6 -7
View File
@@ -1,7 +1,6 @@
// Must be located at the root.
// FIXME: update Firebase version.
importScripts('https://cdn.jsdelivr.net/npm/firebase@8.10.1/firebase-app.js');
importScripts('https://cdn.jsdelivr.net/npm/firebase@8.10.1/firebase-messaging.js');
importScripts('https://cdn.jsdelivr.net/npm/firebase@9.14.0/firebase-app-compat.js');
importScripts('https://cdn.jsdelivr.net/npm/firebase@9.14.0/firebase-messaging-compat.js');
importScripts('firebase-init.js');
importScripts('version.js');
@@ -64,7 +63,7 @@ firebase.initializeApp(FIREBASE_INIT);
const fbMessaging = firebase.messaging();
// This method shows the push notifications while the window is in background.
fbMessaging.onBackgroundMessage((payload) => {
fbMessaging.onBackgroundMessage(payload => {
if (payload.data.silent == 'true') {
// TODO: if this is an 'msg', fetch the new message in the background.
return;
@@ -94,7 +93,7 @@ fbMessaging.onBackgroundMessage((payload) => {
// Update service worker immediately for both the current client
// and all other active clients.
self.addEventListener('install', event => {
self.addEventListener('install', _ => {
self.skipWaiting();
});
@@ -113,7 +112,7 @@ self.addEventListener('notificationclick', event => {
event.waitUntil(self.clients.matchAll({
type: 'window',
includeUncontrolled: true
}).then((windowClients) => {
}).then(windowClients => {
let anyClient = null;
for (let i = 0; i < windowClients.length; i++) {
const url = new URL(windowClients[i].url);
@@ -149,7 +148,7 @@ self.addEventListener('fetch', event => {
return;
}
event.respondWith((async () => {
event.respondWith((async _ => {
// Try to find the response in the cache.
const cache = await caches.open(PACKAGE_VERSION);
+2
View File
@@ -97,6 +97,8 @@ export const MESSAGE_PREVIEW_LENGTH = 80;
export const QUOTED_REPLY_LENGTH = 30;
// Length of a preview of a forwarded message.
export const FORWARDED_PREVIEW_LENGTH = 84;
// Length of preview for a message being edited.
export const EDIT_PREVIEW_LENGTH = 60;
// Minimum duration of an audio recording in milliseconds (2 sec).
export const MIN_DURATION = 2_000;
+50 -1
View File
@@ -1420,7 +1420,56 @@
"call_declined": {
"translation": "abgelehnt",
"defaultMessage": "declined",
"description": "Label for declined incoming call",
"description": "Label for declined call",
"missing": false,
"obsolete": false
},
"call_disconnected": {
"translation": "getrennt",
"defaultMessage": "disconnected",
"description": "Label for disconnected call",
"missing": false,
"obsolete": false
},
"label_use_secure_connection": {
"translation": "Verwenden Sie eine sichere Verbindung",
"defaultMessage": "Use secure connection",
"description": "Label for WS/WSS connection type in SettingsView",
"missing": false,
"obsolete": false
},
"phone_or_email_prompt": {
"translation": "Telefonnummer oder E-Mail",
"defaultMessage": "Phone number or email",
"description": "Placeholer for phone or email",
"missing": false,
"obsolete": false
},
"cred_confirmed_successfully": {
"translation": "Erfolgreich bestätigt",
"defaultMessage": "Confirmed successfully",
"description": "Message explaining that the credential was successfully validated.",
"missing": false,
"obsolete": false
},
"menu_item_edit": {
"translation": "Bearbeiten",
"defaultMessage": "Edit",
"description": "Edit message",
"missing": false,
"obsolete": false
},
"editing_message": {
"translation": "Bearbeitung",
"defaultMessage": "Editing",
"description": "Title over message editing preview",
"missing": false,
"obsolete": false
},
"message_edited_marker": {
"translation": ", bearbeitet",
"defaultMessage": ", edited",
"description": "Marker indicating that the message was edited",
"missing": false,
"obsolete": false
}
+50 -1
View File
@@ -1420,7 +1420,56 @@
"call_declined": {
"translation": "declined",
"defaultMessage": "declined",
"description": "Label for declined incoming call",
"description": "Label for declined call",
"missing": false,
"obsolete": false
},
"call_disconnected": {
"translation": "disconnected",
"defaultMessage": "disconnected",
"description": "Label for disconnected call",
"missing": false,
"obsolete": false
},
"label_use_secure_connection": {
"translation": "Use secure connection",
"defaultMessage": "Use secure connection",
"description": "Label for WS/WSS connection type in SettingsView",
"missing": false,
"obsolete": false
},
"phone_or_email_prompt": {
"translation": "Phone number or email",
"defaultMessage": "Phone number or email",
"description": "Placeholer for phone or email",
"missing": false,
"obsolete": false
},
"cred_confirmed_successfully": {
"translation": "Confirmed successfully",
"defaultMessage": "Confirmed successfully",
"description": "Message explaining that the credential was successfully validated.",
"missing": false,
"obsolete": false
},
"menu_item_edit": {
"translation": "Edit",
"defaultMessage": "Edit",
"description": "Edit message",
"missing": false,
"obsolete": false
},
"editing_message": {
"translation": "Editing",
"defaultMessage": "Editing",
"description": "Title over message editing preview",
"missing": false,
"obsolete": false
},
"message_edited_marker": {
"translation": ", edited",
"defaultMessage": ", edited",
"description": "Marker indicating that the message was edited",
"missing": false,
"obsolete": false
}
+50 -1
View File
@@ -1420,7 +1420,56 @@
"call_declined": {
"translation": "rechazada",
"defaultMessage": "declined",
"description": "Label for declined incoming call",
"description": "Label for declined call",
"missing": false,
"obsolete": false
},
"call_disconnected": {
"translation": "desconectada",
"defaultMessage": "disconnected",
"description": "Label for disconnected call",
"missing": false,
"obsolete": false
},
"label_use_secure_connection": {
"translation": "Usar conexión segura",
"defaultMessage": "Use secure connection",
"description": "Label for WS/WSS connection type in SettingsView",
"missing": false,
"obsolete": false
},
"phone_or_email_prompt": {
"translation": "Número de teléfono o correo electrónico",
"defaultMessage": "Phone number or email",
"description": "Placeholer for phone or email",
"missing": false,
"obsolete": false
},
"cred_confirmed_successfully": {
"translation": "Confirmado con éxito",
"defaultMessage": "Confirmed successfully",
"description": "Message explaining that the credential was successfully validated.",
"missing": false,
"obsolete": false
},
"menu_item_edit": {
"translation": "Editar",
"defaultMessage": "Edit",
"description": "Edit message",
"missing": false,
"obsolete": false
},
"editing_message": {
"translation": "Edición",
"defaultMessage": "Editing",
"description": "Title over message editing preview",
"missing": false,
"obsolete": false
},
"message_edited_marker": {
"translation": ", editado",
"defaultMessage": ", edited",
"description": "Marker indicating that the message was edited",
"missing": false,
"obsolete": false
}
+29 -1
View File
@@ -129,7 +129,11 @@
},
"call_declined": {
"defaultMessage": "declined",
"description": "Label for declined incoming call"
"description": "Label for declined call"
},
"call_disconnected": {
"defaultMessage": "disconnected",
"description": "Label for disconnected call"
},
"call_missed": {
"defaultMessage": "missed",
@@ -191,6 +195,10 @@
"defaultMessage": "No contacts match ''{query}''",
"description": "Shown in ContactsView when search returned no results"
},
"cred_confirmed_successfully": {
"defaultMessage": "Confirmed successfully",
"description": "Message explaining that the credential was successfully validated."
},
"credential_email_prompt": {
"defaultMessage": "Your registration email",
"description": "Placeholder for entering email"
@@ -235,6 +243,10 @@
"defaultMessage": "Unsupported",
"description": "Unsupported entity in drafty"
},
"editing_message": {
"defaultMessage": "Editing",
"description": "Title over message editing preview"
},
"email_dative": {
"defaultMessage": "email",
"description": "Dative case of 'email', i.e. 'email' in 'by email'"
@@ -392,6 +404,10 @@
"defaultMessage": "Archived:",
"description": "Label for unarchiving the topic"
},
"label_use_secure_connection": {
"defaultMessage": "Use secure connection",
"description": "Label for WS/WSS connection type in SettingsView"
},
"label_user_contacts": {
"defaultMessage": "Contacts:",
"description": "Label for user contacts"
@@ -468,6 +484,10 @@
"defaultMessage": "Delete",
"description": "Delete entire topic"
},
"menu_item_edit": {
"defaultMessage": "Edit",
"description": "Edit message"
},
"menu_item_edit_permissions": {
"defaultMessage": "Edit permissions",
"description": "Menu item [Edit permissions]"
@@ -512,6 +532,10 @@
"defaultMessage": "Video call",
"description": "Start video call"
},
"message_edited_marker": {
"defaultMessage": ", edited",
"description": "Marker indicating that the message was edited"
},
"message_sending": {
"defaultMessage": "sending...",
"description": "Message being sent, in place of time stamp"
@@ -640,6 +664,10 @@
"defaultMessage": "phone",
"description": "Dative case of 'phone', i.e. 'phone' in 'by phone'"
},
"phone_or_email_prompt": {
"defaultMessage": "Phone number or email",
"description": "Placeholer for phone or email"
},
"private_editing_placeholder": {
"defaultMessage": "Visible to you only",
"description": "Placeholder for editing 'private'"
+66 -17
View File
@@ -1369,59 +1369,108 @@
"obsolete": false
},
"already_in_call": {
"translation": "",
"translation": "Vous êtes déjà dans un appel en cours!",
"defaultMessage": "You already in an ongoing call!",
"description": "Error message when the user tried to accept a new call without finishing pervious one",
"missing": true,
"missing": false,
"obsolete": false
},
"call_cancelled": {
"translation": "",
"translation": "annulé",
"defaultMessage": "cancelled",
"description": "Label for cancelled outgoing call",
"missing": true,
"missing": false,
"obsolete": false
},
"call_declined": {
"translation": "",
"translation": "refusé",
"defaultMessage": "declined",
"description": "Label for declined incoming call",
"missing": true,
"description": "Label for declined call",
"missing": false,
"obsolete": false
},
"call_missed": {
"translation": "",
"translation": "manqué",
"defaultMessage": "missed",
"description": "Label for missed incoming call",
"missing": true,
"missing": false,
"obsolete": false
},
"calls_incoming": {
"translation": "",
"translation": "Appel entrant",
"defaultMessage": "Incoming call",
"description": "Incoming call label",
"missing": true,
"missing": false,
"obsolete": false
},
"calls_outgoing": {
"translation": "",
"translation": "Appel sortant",
"defaultMessage": "Outgoing call",
"description": "Outgoing call label",
"missing": true,
"missing": false,
"obsolete": false
},
"calls_you_label": {
"translation": "",
"translation": "Tu",
"defaultMessage": "You",
"description": "Shown over the local video screen",
"missing": true,
"missing": false,
"obsolete": false
},
"menu_item_video_call": {
"translation": "",
"translation": "Appel vidéo",
"defaultMessage": "Video call",
"description": "Start video call",
"missing": true,
"missing": false,
"obsolete": false
},
"call_disconnected": {
"translation": "débranché",
"defaultMessage": "disconnected",
"description": "Label for disconnected call",
"missing": false,
"obsolete": false
},
"label_use_secure_connection": {
"translation": "Utiliser une connexion sécurisée",
"defaultMessage": "Use secure connection",
"description": "Label for WS/WSS connection type in SettingsView",
"missing": false,
"obsolete": false
},
"phone_or_email_prompt": {
"translation": "Numéro de téléphone ou email",
"defaultMessage": "Phone number or email",
"description": "Placeholer for phone or email",
"missing": false,
"obsolete": false
},
"cred_confirmed_successfully": {
"translation": "Confirmé avec succès",
"defaultMessage": "Confirmed successfully",
"description": "Message explaining that the credential was successfully validated.",
"missing": false,
"obsolete": false
},
"menu_item_edit": {
"translation": "Modifier",
"defaultMessage": "Edit",
"description": "Edit message",
"missing": false,
"obsolete": false
},
"editing_message": {
"translation": "Édition",
"defaultMessage": "Editing",
"description": "Title over message editing preview",
"missing": false,
"obsolete": false
},
"message_edited_marker": {
"translation": ", édité",
"defaultMessage": ", edited",
"description": "Marker indicating that the message was edited",
"missing": false,
"obsolete": false
}
}
+50 -1
View File
@@ -1420,7 +1420,56 @@
"call_declined": {
"translation": "통화 거부",
"defaultMessage": "declined",
"description": "Label for declined incoming call",
"description": "Label for declined call",
"missing": false,
"obsolete": false
},
"call_disconnected": {
"translation": "연결 끊김",
"defaultMessage": "disconnected",
"description": "Label for disconnected call",
"missing": false,
"obsolete": false
},
"label_use_secure_connection": {
"translation": "보안 연결 사용",
"defaultMessage": "Use secure connection",
"description": "Label for WS/WSS connection type in SettingsView",
"missing": false,
"obsolete": false
},
"phone_or_email_prompt": {
"translation": "전화번호 또는 이메일",
"defaultMessage": "Phone number or email",
"description": "Placeholer for phone or email",
"missing": false,
"obsolete": false
},
"cred_confirmed_successfully": {
"translation": "성공적으로 확인됨",
"defaultMessage": "Confirmed successfully",
"description": "Message explaining that the credential was successfully validated.",
"missing": false,
"obsolete": false
},
"menu_item_edit": {
"translation": "편집",
"defaultMessage": "Edit",
"description": "Edit message",
"missing": false,
"obsolete": false
},
"editing_message": {
"translation": "교정하다",
"defaultMessage": "Editing",
"description": "Title over message editing preview",
"missing": false,
"obsolete": false
},
"message_edited_marker": {
"translation": " 수정됨",
"defaultMessage": ", edited",
"description": "Marker indicating that the message was edited",
"missing": false,
"obsolete": false
}
+50 -1
View File
@@ -1420,7 +1420,56 @@
"call_declined": {
"translation": "refuzat",
"defaultMessage": "declined",
"description": "Label for declined incoming call",
"description": "Label for declined call",
"missing": false,
"obsolete": false
},
"call_disconnected": {
"translation": "deconectat",
"defaultMessage": "disconnected",
"description": "Label for disconnected call",
"missing": false,
"obsolete": false
},
"label_use_secure_connection": {
"translation": "Utilizați conexiune securizată",
"defaultMessage": "Use secure connection",
"description": "Label for WS/WSS connection type in SettingsView",
"missing": false,
"obsolete": false
},
"phone_or_email_prompt": {
"translation": "Număr de telefon sau e-mail",
"defaultMessage": "Phone number or email",
"description": "Placeholer for phone or email",
"missing": false,
"obsolete": false
},
"cred_confirmed_successfully": {
"translation": "Confirmat cu succes",
"defaultMessage": "Confirmed successfully",
"description": "Message explaining that the credential was successfully validated.",
"missing": false,
"obsolete": false
},
"menu_item_edit": {
"translation": "Editează",
"defaultMessage": "Edit",
"description": "Edit message",
"missing": false,
"obsolete": false
},
"editing_message": {
"translation": "Editare",
"defaultMessage": "Editing",
"description": "Title over message editing preview",
"missing": false,
"obsolete": false
},
"message_edited_marker": {
"translation": ", editat",
"defaultMessage": ", edited",
"description": "Marker indicating that the message was edited",
"missing": false,
"obsolete": false
}
+51 -2
View File
@@ -140,7 +140,7 @@
"obsolete": false
},
"button_edit": {
"translation": "Редактировать",
"translation": "Изменить",
"defaultMessage": "Edit",
"description": "Call to action [Edit]",
"missing": false,
@@ -1420,7 +1420,56 @@
"call_declined": {
"translation": "отклонен",
"defaultMessage": "declined",
"description": "Label for declined incoming call",
"description": "Label for declined call",
"missing": false,
"obsolete": false
},
"call_disconnected": {
"translation": "разъединен",
"defaultMessage": "disconnected",
"description": "Label for disconnected call",
"missing": false,
"obsolete": false
},
"label_use_secure_connection": {
"translation": "Безопасное соединение",
"defaultMessage": "Use secure connection",
"description": "Label for WS/WSS connection type in SettingsView",
"missing": false,
"obsolete": false
},
"phone_or_email_prompt": {
"translation": "Телефон или емейл",
"defaultMessage": "Phone number or email",
"description": "Placeholer for phone or email",
"missing": false,
"obsolete": false
},
"cred_confirmed_successfully": {
"translation": "Подтверждено успешно",
"defaultMessage": "Confirmed successfully",
"description": "Message explaining that the credential was successfully validated.",
"missing": false,
"obsolete": false
},
"menu_item_edit": {
"translation": "Изменить",
"defaultMessage": "Edit",
"description": "Edit message",
"missing": false,
"obsolete": false
},
"editing_message": {
"translation": "Редактирование",
"defaultMessage": "Editing",
"description": "Title over message editing preview",
"missing": false,
"obsolete": false
},
"message_edited_marker": {
"translation": ", изменено",
"defaultMessage": ", edited",
"description": "Marker indicating that the message was edited",
"missing": false,
"obsolete": false
}
+50 -1
View File
@@ -1420,7 +1420,56 @@
"call_declined": {
"translation": "被拒",
"defaultMessage": "declined",
"description": "Label for declined incoming call",
"description": "Label for declined call",
"missing": false,
"obsolete": false
},
"call_disconnected": {
"translation": "斷開連接",
"defaultMessage": "disconnected",
"description": "Label for disconnected call",
"missing": false,
"obsolete": false
},
"label_use_secure_connection": {
"translation": "使用安全連接",
"defaultMessage": "Use secure connection",
"description": "Label for WS/WSS connection type in SettingsView",
"missing": false,
"obsolete": false
},
"phone_or_email_prompt": {
"translation": "電話號碼或電子郵件",
"defaultMessage": "Phone number or email",
"description": "Placeholer for phone or email",
"missing": false,
"obsolete": false
},
"cred_confirmed_successfully": {
"translation": "確認成功",
"defaultMessage": "Confirmed successfully",
"description": "Message explaining that the credential was successfully validated.",
"missing": false,
"obsolete": false
},
"menu_item_edit": {
"translation": "編輯",
"defaultMessage": "Edit",
"description": "Edit message",
"missing": false,
"obsolete": false
},
"editing_message": {
"translation": "編輯",
"defaultMessage": "Editing",
"description": "Title over message editing preview",
"missing": false,
"obsolete": false
},
"message_edited_marker": {
"translation": ",編輯",
"defaultMessage": ", edited",
"description": "Marker indicating that the message was edited",
"missing": false,
"obsolete": false
}
+50 -1
View File
@@ -1420,7 +1420,56 @@
"call_declined": {
"translation": "被拒",
"defaultMessage": "declined",
"description": "Label for declined incoming call",
"description": "Label for declined call",
"missing": false,
"obsolete": false
},
"call_disconnected": {
"translation": "断开连接",
"defaultMessage": "disconnected",
"description": "Label for disconnected call",
"missing": false,
"obsolete": false
},
"label_use_secure_connection": {
"translation": "使用安全连接",
"defaultMessage": "Use secure connection",
"description": "Label for WS/WSS connection type in SettingsView",
"missing": false,
"obsolete": false
},
"phone_or_email_prompt": {
"translation": "电话号码或电子邮件",
"defaultMessage": "Phone number or email",
"description": "Placeholer for phone or email",
"missing": false,
"obsolete": false
},
"cred_confirmed_successfully": {
"translation": "确认成功",
"defaultMessage": "Confirmed successfully",
"description": "Message explaining that the credential was successfully validated.",
"missing": false,
"obsolete": false
},
"menu_item_edit": {
"translation": "编辑",
"defaultMessage": "Edit",
"description": "Edit message",
"missing": false,
"obsolete": false
},
"editing_message": {
"translation": "编辑",
"defaultMessage": "Editing",
"description": "Title over message editing preview",
"missing": false,
"obsolete": false
},
"message_edited_marker": {
"translation": ",编辑",
"defaultMessage": ", edited",
"description": "Marker indicating that the message was edited",
"missing": false,
"obsolete": false
}
+5 -4
View File
@@ -2,7 +2,7 @@
// Used to generate umd/index.prod.js
import React from 'react';
import ReactDOM from 'react-dom';
import { createRoot } from 'react-dom/client';
import { IntlProvider } from 'react-intl';
@@ -29,11 +29,12 @@ const messages = allMessages[htmlLang];
// Set lang attribute of the HTML element: <html lang="XX">
document.getElementsByTagName('html')[0].setAttribute('lang', htmlLang);
ReactDOM.render(
// Render the app.
const root = createRoot(document.getElementById('mountPoint'));
root.render(
<React.StrictMode>
<IntlProvider locale={language} messages={messages} textComponent={React.Fragment}>
<TinodeWeb />
</IntlProvider>
</React.StrictMode>,
document.getElementById('mountPoint')
</React.StrictMode>
);
+18 -17
View File
@@ -1,8 +1,8 @@
// Utility class for hash navigation.
// Parse hash as in http://www.example.com/path#hash as if it were
// path and arguments.
export default class HashNavigation {
// Parse hash as in http://www.example.com/path#hash as if it were
// path and arguments.
static parseUrlHash(hash) {
// Split path from args, path -> parts[0], args->path[1]
const parts = hash.split('?', 2);
@@ -12,10 +12,11 @@ export default class HashNavigation {
path = parts[0].replace('#', '').split('/');
}
if (parts[1]) {
parts[1].split('&').forEach(function(part) {
const item = part.split('=');
if (item[0]) {
params[decodeURIComponent(item[0])] = decodeURIComponent(item[1]);
parts[1].split('&').forEach((arg) => {
// Can't use split() because the value may contain '='.
const eq = arg.indexOf('=');
if (eq > 0) {
params[arg.slice(0, eq)] = decodeURIComponent(arg.slice(eq + 1));
}
});
}
@@ -31,7 +32,7 @@ export default class HashNavigation {
const args = [];
for (const key in params) {
if (params.hasOwnProperty(key)) {
args.push(key + '=' + params[key]);
args.push(key + '=' + encodeURIComponent(params[key]));
}
}
if (args.length > 0) {
@@ -41,38 +42,38 @@ export default class HashNavigation {
}
static addUrlParam(hash, key, value) {
const parsed = this.parseUrlHash(hash);
const parsed = HashNavigation.parseUrlHash(hash);
parsed.params[key] = value;
return this.composeUrlHash(parsed.path, parsed.params);
return HashNavigation.composeUrlHash(parsed.path, parsed.params);
}
static removeUrlParam(hash, key) {
const parsed = this.parseUrlHash(hash);
const parsed = HashNavigation.parseUrlHash(hash);
delete parsed.params[key];
return this.composeUrlHash(parsed.path, parsed.params);
return HashNavigation.composeUrlHash(parsed.path, parsed.params);
}
static setUrlSidePanel(hash, sidepanel) {
const parsed = this.parseUrlHash(hash);
const parsed = HashNavigation.parseUrlHash(hash);
parsed.path[0] = sidepanel;
return this.composeUrlHash(parsed.path, parsed.params);
return HashNavigation.composeUrlHash(parsed.path, parsed.params);
}
static setUrlInfoPanel(hash, infopanel) {
const parsed = this.parseUrlHash(hash);
const parsed = HashNavigation.parseUrlHash(hash);
if (infopanel) {
parsed.params.info = infopanel;
} else {
delete parsed.params.info;
}
return this.composeUrlHash(parsed.path, parsed.params);
return HashNavigation.composeUrlHash(parsed.path, parsed.params);
}
static setUrlTopic(hash, topic) {
const parsed = this.parseUrlHash(hash);
const parsed = HashNavigation.parseUrlHash(hash);
parsed.path[1] = topic;
// Close InfoView on topic change.
delete parsed.params.info;
return this.composeUrlHash(parsed.path, parsed.params);
return HashNavigation.composeUrlHash(parsed.path, parsed.params);
}
}
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1,3 +1,3 @@
// This is a generated file. Don't edit.
export const PACKAGE_VERSION = "0.20.0-rc1";
export const PACKAGE_VERSION = "0.21.0-beta1";
+73 -7
View File
@@ -2,14 +2,17 @@
import React 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';
import VisiblePassword from '../widgets/visible-password.jsx';
import LocalStorageUtil from '../lib/local-storage.js';
import { imageScaled, blobToBase64, makeImageUrl } from '../lib/blob-helpers.js';
import { theCard } from '../lib/utils.js';
import { MAX_TITLE_LENGTH } from '../config.js';
import { AVATAR_SIZE, MAX_AVATAR_BYTES, MAX_EXTERN_ATTACHMENT_SIZE,
MAX_TITLE_LENGTH } from '../config.js';
export default class CreateAccountView extends React.PureComponent {
constructor(props) {
@@ -20,8 +23,12 @@ export default class CreateAccountView extends React.PureComponent {
password: '',
email: '',
fn: '', // full/formatted name
imageDataUrl: null,
imageUrl: null,
uploadUrl: null,
newAvatar: null,
newAvatarMime: null,
errorCleared: false,
buttonDisabled: false,
saveToken: LocalStorageUtil.getObject('keep-logged-in')
};
@@ -31,6 +38,9 @@ export default class CreateAccountView extends React.PureComponent {
this.handleFnChange = this.handleFnChange.bind(this);
this.handleImageChanged = this.handleImageChanged.bind(this);
this.handleToggleSaveToken = this.handleToggleSaveToken.bind(this);
this.handleAvatarCropped = this.handleAvatarCropped.bind(this);
this.handleAvatarCropCancel = this.handleAvatarCropCancel.bind(this);
this.uploadAvatar = this.uploadAvatar.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
@@ -50,8 +60,8 @@ export default class CreateAccountView extends React.PureComponent {
this.setState({fn: e.target.value});
}
handleImageChanged(img) {
this.setState({imageDataUrl: img});
handleImageChanged(mime, img) {
this.setState({newAvatar: img, newAvatarMime: mime});
}
handleToggleSaveToken() {
@@ -65,11 +75,66 @@ export default class CreateAccountView extends React.PureComponent {
this.props.onCreateAccount(
this.state.login.trim(),
this.state.password.trim(),
theCard(this.state.fn.trim().substring(0, MAX_TITLE_LENGTH), this.state.imageDataUrl),
theCard(this.state.fn.trim().substring(0, MAX_TITLE_LENGTH), this.state.uploadUrl),
{'meth': 'email', 'val': this.state.email});
}
// AvatarCropView calls this method when the user has cropped the image.
handleAvatarCropped(mime, blob, width, height) {
const url = blob ? URL.createObjectURL(blob) : null;
this.setState({avatar: url, newAvatar: null, newAvatarMime: null});
if (blob) {
this.uploadAvatar(mime, blob, width, height);
}
}
handleAvatarCropCancel() {
this.setState({newAvatar: null, newAvatarMime: null});
}
// Utility method for converting cropped avatar blob to bytes for sending inband or
// for uploading it to the server out of band.
uploadAvatar(mime, blob, width, height) {
const readyToUpload = (image) => {
let {mime, blob} = image;
this.setState({imageUrl: URL.createObjectURL(blob), buttonDisabled: true});
if (blob.size > MAX_AVATAR_BYTES) {
// Too large to send inband - uploading out of band and sending as a link.
const uploader = this.props.tinode.getLargeFileHelper();
uploader.upload(blob, 'newacc')
.then(url => this.setState({uploadUrl: url}))
.catch(err => this.props.onError(err.message, 'err'))
.finally(_ => this.setState({buttonDisabled: false}));
} else {
// Convert blob to base64-encoded bits.
blobToBase64(blob)
.then(b64 => this.setState({uploadUrl: makeImageUrl({data: b64.bits, type: mime})}))
.finally(_ => this.setState({buttonDisabled: false}));
}
};
if (width > AVATAR_SIZE || height > AVATAR_SIZE || width != height) {
// Avatar is not square or too large even after cropping. Shrink it and make square.
imageScaled(blob, AVATAR_SIZE, AVATAR_SIZE, MAX_EXTERN_ATTACHMENT_SIZE, true)
.then(scaled => readyToUpload(scaled))
.catch(err => this.props.onError(err.message, 'err'));
} else {
readyToUpload({mime: mime, blob: blob, width: width, height: height});
}
}
render() {
if (this.state.newAvatar) {
return (
<AvatarCrop
avatar={this.state.newAvatar}
mime={this.state.newAvatarMime}
onSubmit={this.handleAvatarCropped}
onCancel={this.handleAvatarCropCancel}
onError={this.props.onError} />
);
}
let submitClasses = 'primary';
if (this.props.disabled) {
submitClasses += ' disabled';
@@ -93,7 +158,8 @@ export default class CreateAccountView extends React.PureComponent {
</div>
<AvatarUpload
tinode={this.props.tinode}
onImageChanged={this.handleImageChanged}
avatar={this.state.imageUrl}
onImageUpdated={this.handleImageChanged}
onError={this.props.onError} />
</div>
<div className="panel-form-row">
@@ -119,7 +185,7 @@ export default class CreateAccountView extends React.PureComponent {
}</FormattedMessage>
</div>
<div className="dialog-buttons">
<button className={submitClasses} type="submit">
<button className={submitClasses} type="submit" disabled={this.state.buttonDisabled}>
<FormattedMessage id="button_sign_up" defaultMessage="Sign up"
description="Create account button [Sign Up]" />
</button>
+1 -1
View File
@@ -255,7 +255,7 @@ class InfoView extends React.Component {
}
}
handleImageChanged(img) {
handleImageChanged(mime, img) {
this.setState({avatar: img});
this.props.onTopicDescUpdate(this.props.topic, theCard(null, img || Tinode.DEL_CHAR), null);
}
+61 -19
View File
@@ -19,7 +19,7 @@ import LogoView from './logo-view.jsx';
import MetaMessage from '../widgets/meta-message.jsx';
import SendMessage from '../widgets/send-message.jsx';
import { DEFAULT_P2P_ACCESS_MODE, IMAGE_PREVIEW_DIM, KEYPRESS_DELAY,
import { DEFAULT_P2P_ACCESS_MODE, EDIT_PREVIEW_LENGTH, IMAGE_PREVIEW_DIM, KEYPRESS_DELAY,
MESSAGES_PAGE, MAX_EXTERN_ATTACHMENT_SIZE, MAX_IMAGE_DIM, MAX_INBAND_ATTACHMENT_SIZE,
READ_DELAY, QUOTED_REPLY_LENGTH } from '../config.js';
import { CALL_STATE_OUTGOING_INITATED, CALL_STATE_IN_PROGRESS } from '../constants.js';
@@ -63,6 +63,11 @@ const messages = defineMessages({
defaultMessage: 'invalid content',
description: 'Shown when the message is unreadable'
},
editing_message: {
id: 'editing_message',
defaultMessage: 'Editing',
description: 'Title over message editing preview'
}
});
// Checks if the access permissions are granted but not yet accepted.
@@ -125,6 +130,7 @@ class MessagesView extends React.Component {
this.goToLatestMessage = this.goToLatestMessage.bind(this);
this.handlePickReply = this.handlePickReply.bind(this);
this.handleEditMessage = this.handleEditMessage.bind(this);
this.handleCancelReply = this.handleCancelReply.bind(this);
this.handleQuoteClick = this.handleQuoteClick.bind(this);
this.handleCallHangup = this.handleCallHangup.bind(this);
@@ -237,6 +243,7 @@ class MessagesView extends React.Component {
peerMessagingDisabled: false,
channel: false,
reply: null,
contentToEdit: null,
showGoToLastButton: false
};
} else if (nextProps.topic != prevState.topic) {
@@ -635,8 +642,6 @@ class MessagesView extends React.Component {
if (status >= Tinode.MESSAGE_STATUS_SENT && msg.from != this.props.myUserId) {
this.postReadNotification(msg.seq);
}
// This will send "received" notifications right away.
this.props.onData(msg);
}
handleAllMessagesReceived(count) {
@@ -755,14 +760,19 @@ class MessagesView extends React.Component {
msg = this.props.forwardMessage.msg;
head = this.props.forwardMessage.head;
this.handleCancelReply();
} else if (this.state.reply && this.state.reply.content) {
// We are replying to a message in this topic.
head = {reply: '' + this.state.reply.seq};
// Turn it into Drafty so we can make a quoted Drafty object later.
if (typeof msg == 'string') {
msg = Drafty.parse(msg);
} else if (this.state.reply) {
if (this.state.reply.editing) {
// Editing an existing message.
head = {replace: ':' + this.state.reply.seq};
} else if (this.state.reply.content) {
// Replying to a message in this topic.
// Turn it into Drafty so we can make a quoted Drafty object later.
head = {reply: '' + this.state.reply.seq};
if (typeof msg == 'string') {
msg = Drafty.parse(msg);
}
msg = Drafty.append(Drafty.sanitizeEntities(this.state.reply.content), msg);
}
msg = Drafty.append(Drafty.sanitizeEntities(this.state.reply.content), msg);
this.handleCancelReply();
}
this.props.sendMessage(msg, uploadCompletionPromise, uploader, head);
@@ -771,7 +781,7 @@ class MessagesView extends React.Component {
// Retry sending a message.
retrySend(pub) {
this.props.sendMessage(pub.content, undefined, undefined, pub.head)
.then(() => {
.then(_ => {
// All good. Remove the original message draft from the cache.
const topic = this.props.tinode.getTopic(this.state.topic);
topic.delMessagesList([pub.seq], true);
@@ -979,9 +989,8 @@ class MessagesView extends React.Component {
// senderId: UID of the sender of the source message.
// senderName: full name of the sender of the original message.
handlePickReply(seq, content, senderId, senderName) {
this.setState({reply: null});
if (!seq || !content) {
this.setState({reply: null});
return;
}
@@ -1003,8 +1012,38 @@ class MessagesView extends React.Component {
this.props.onCancelForwardMessage();
}
// seq: seq ID of the message to edit.
// context: message content.
handleEditMessage(seq, content) {
if (!seq || !content) {
this.setState({reply: null});
return;
}
content = typeof content == 'string' ? Drafty.init(content) : content;
const editable = Drafty.toMarkdown(content);
if (Drafty.isValid(content)) {
content = Drafty.replyContent(content, EDIT_PREVIEW_LENGTH);
} else {
// /!\ invalid content.
content = Drafty.append(Drafty.init('\u26A0 '),
Drafty.wrapInto(this.props.intl.formatMessage(messages.invalid_content), 'EM'));
}
this.setState({
reply: {
content: Drafty.quote(this.props.intl.formatMessage(messages.message_editing), null, content),
seq: seq,
editing: true
},
contentToEdit: editable
});
this.props.onCancelForwardMessage();
}
handleCancelReply() {
this.setState({reply: null});
this.setState({reply: null, contentToEdit: null});
this.props.onCancelForwardMessage();
}
@@ -1013,7 +1052,7 @@ class MessagesView extends React.Component {
if (ref && ref.current) {
ref.current.scrollIntoView({block: "center", behavior: "smooth"});
ref.current.classList.add('flash');
setTimeout(() => { ref.current.classList.remove('flash') } , 1000);
setTimeout(_ => {ref.current.classList.remove('flash')} , 1000);
} else {
console.error("Unresolved message ref", replyToSeq);
}
@@ -1159,6 +1198,8 @@ class MessagesView extends React.Component {
tinode={this.props.tinode}
content={msg.content}
mimeType={msg.head && msg.head.mime}
replyToSeq={replyToSeq}
edited={msg.head && msg.head.replace && !msg.head.webrtc}
timestamp={msg.ts}
response={isReply}
seq={msg.seq}
@@ -1170,17 +1211,17 @@ class MessagesView extends React.Component {
sequence={sequence}
received={deliveryStatus}
uploader={msg._uploader}
userIsWriter={this.state.isWriter}
viewportWidth={this.props.viewportWidth} // Used by `formatter`.
showContextMenu={this.handleShowMessageContextMenu}
onImagePreview={this.handleImagePostview}
onFormResponse={this.handleFormResponse}
onError={this.props.onError}
onCancelUpload={this.handleCancelUpload}
pickReply={this.handlePickReply}
replyToSeq={replyToSeq}
editMessage={this.handleEditMessage}
onQuoteClick={this.handleQuoteClick}
onError={this.props.onError}
ref={ref}
userIsWriter={this.state.isWriter}
key={msg.seq} />
);
}
@@ -1289,13 +1330,14 @@ class MessagesView extends React.Component {
topicName={this.state.topic}
noInput={!!this.props.forwardMessage}
disabled={!this.state.isWriter || this.state.deleted}
reply={this.state.reply}
initMessage={this.state.contentToEdit}
onKeyPress={this.sendKeyPress}
onSendMessage={this.sendMessage}
onAttachFile={this.props.forwardMessage ? null : this.handleAttachFile}
onAttachImage={this.props.forwardMessage ? null : this.handleAttachImage}
onAttachAudio={this.props.forwardMessage ? null : this.sendAudioAttachment}
onError={this.props.onError}
reply={this.state.reply}
onQuoteClick={this.handleQuoteClick}
onCancelReply={this.handleCancelReply} />}
</>
+18 -3
View File
@@ -2,6 +2,8 @@
import React from 'react';
import { FormattedMessage } from 'react-intl';
import CheckBox from '../widgets/checkbox.jsx';
import HostSelector from '../widgets/host-selector.jsx';
export default class SettingsView extends React.PureComponent {
@@ -11,18 +13,21 @@ export default class SettingsView extends React.PureComponent {
this.state = {
transport: props.transport || 'def',
serverAddress: props.serverAddress,
secureConnection: props.secureConnection,
};
this.handleSubmit = this.handleSubmit.bind(this);
this.handleTransportSelected = this.handleTransportSelected.bind(this);
this.handleServerAddressChange = this.handleServerAddressChange.bind(this);
this.handleToggleSecure = this.handleToggleSecure.bind(this);
}
handleSubmit(e) {
e.preventDefault();
this.props.onUpdate({
transport: this.state.transport,
serverAddress: this.state.serverAddress
serverAddress: this.state.serverAddress,
secureConnection: this.state.secureConnection,
});
}
@@ -34,6 +39,10 @@ export default class SettingsView extends React.PureComponent {
this.setState({serverAddress: name});
}
handleToggleSecure(e) {
this.setState({secureConnection: !this.state.secureConnection});
}
render() {
const names = {def: "default", ws: "websocket", lp: "long polling"};
const transportOptions = [];
@@ -58,8 +67,14 @@ export default class SettingsView extends React.PureComponent {
description="Label for server selector in SettingsView" />
</label>
</div>
<HostSelector serverAddress={this.state.serverAddress}
onServerAddressChange={this.handleServerAddressChange} />
<HostSelector serverAddress={this.state.serverAddress} onServerAddressChange={this.handleServerAddressChange} />
<div className="panel-form-row">
<CheckBox id="secure-connection" name="secure-connection" checked={this.state.secureConnection}
className="quoted" onChange={this.handleToggleSecure} /><label htmlFor="secure-connection">
<FormattedMessage id="label_use_secure_connection" defaultMessage="Use secure connection"
description="Label for WS/WSS connection type in SettingsView" />
</label>
</div>
<div className="panel-form-row">
<label className="small">
<FormattedMessage id="label_wire_transport" defaultMessage="Wire transport:"
+4 -3
View File
@@ -92,7 +92,7 @@ const messages = defineMessages({
}
});
class SidepanelView extends React.Component {
class SidepanelView extends React.PureComponent {
constructor(props) {
super(props);
@@ -165,6 +165,7 @@ class SidepanelView extends React.Component {
<SettingsView
transport={this.props.transport}
serverAddress={this.props.serverAddress}
secureConnection={this.props.secureConnection}
onCancel={this.props.onCancel}
onUpdate={this.props.onGlobalSettings} /> :
@@ -238,9 +239,9 @@ class SidepanelView extends React.Component {
<ValidationView
credCode={this.props.credCode}
credMethod={this.props.credMethod}
credToken={this.props.credToken}
onSubmit={this.props.onValidateCredentials}
onCancel={this.props.onCancel}
onError={this.props.onError} /> :
onCancel={this.props.onCancel} /> :
view === 'reset' ?
<PasswordResetView
+142 -98
View File
@@ -73,6 +73,11 @@ const messages = defineMessages({
id: 'menu_item_video_call',
defaultMessage: 'Video call',
description: 'Start video call'
},
cred_confirmed_successfully: {
id: 'cred_confirmed_successfully',
defaultMessage: 'Confirmed successfully',
description: 'Message explaining that the credential was successfully validated.'
}
});
@@ -95,14 +100,12 @@ class TinodeWeb extends React.Component {
this.handleConnected = this.handleConnected.bind(this);
this.handleAutoreconnectIteration = this.handleAutoreconnectIteration.bind(this);
this.doLogin = this.doLogin.bind(this);
this.handleCredentialsRequest = this.handleCredentialsRequest.bind(this);
this.handleLoginSuccessful = this.handleLoginSuccessful.bind(this);
this.handleDisconnect = this.handleDisconnect.bind(this);
this.tnMeMetaDesc = this.tnMeMetaDesc.bind(this);
this.tnMeContactUpdate = this.tnMeContactUpdate.bind(this);
this.tnMeSubsUpdated = this.tnMeSubsUpdated.bind(this);
this.resetContactList = this.resetContactList.bind(this);
this.tnData = this.tnData.bind(this);
this.tnInitFind = this.tnInitFind.bind(this);
this.tnFndSubsUpdated = this.tnFndSubsUpdated.bind(this);
this.handleSearchContacts = this.handleSearchContacts.bind(this);
@@ -186,6 +189,8 @@ class TinodeWeb extends React.Component {
autoLogin: false,
transport: settings.transport || null,
serverAddress: settings.serverAddress || detectServerAddress(),
secureConnection: settings.secureConnection === undefined ?
isSecureConnection() : settings.secureConnection,
serverVersion: "no connection",
// "On" is the default, so saving the "off" state.
messageSounds: !settings.messageSoundsOff,
@@ -251,8 +256,10 @@ class TinodeWeb extends React.Component {
searchResults: [],
// Merged results of a search query and p2p chats.
searchableContacts: [],
// Credential validation.
credMethod: undefined,
credCode: undefined,
credToken: undefined,
// Topic to go to after login.
requestedTopic: undefined
};
@@ -282,17 +289,17 @@ class TinodeWeb extends React.Component {
});
new Promise((resolve, reject) => {
this.tinode = TinodeWeb.tnSetup(this.state.serverAddress, this.state.transport,
this.tinode = TinodeWeb.tnSetup(this.state.serverAddress, isSecureConnection(), this.state.transport,
this.props.intl.locale, this.state.persist, resolve);
this.tinode.onConnect = this.handleConnected;
this.tinode.onDisconnect = this.handleDisconnect;
this.tinode.onAutoreconnectIteration = this.handleAutoreconnectIteration;
this.tinode.onInfoMessage = this.handleInfoMessage;
this.tinode.onDataMessage = this.handleDataMessage;
}).then(() => {
}).then(_ => {
// Initialize desktop alerts.
if (this.state.desktopAlertsEnabled) {
this.initFCMessaging().then(() => {
this.initFCMessaging().then(_ => {
if (this.state.desktopAlerts) {
this.tinode.setDeviceToken(this.state.firebaseToken);
}
@@ -321,17 +328,20 @@ class TinodeWeb extends React.Component {
});
}
this.readTimer = null;
this.readTimerCallback = null;
// Maybe navigate to home screen.
if (!['cred', 'reset', 'register'].includes(parsedNav.path[0])) {
// Save possible topic name.
this.setState({requestedTopic: parsedNav.path[1]});
HashNavigation.navigateTo('');
const path = parsedNav.params && parsedNav.params.cred_done ?
HashNavigation.addUrlParam('', 'cred_done', parsedNav.params.cred_done):
'';
HashNavigation.navigateTo(path);
} else {
this.handleHashRoute();
}
this.readTimer = null;
this.readTimerCallback = null;
this.handleHashRoute();
});
}
@@ -342,9 +352,9 @@ class TinodeWeb extends React.Component {
}
// Setup transport (usually websocket) and server address. This will terminate connection with the server.
static tnSetup(serverAddress, transport, locale, persistentCache, onSetupCompleted) {
static tnSetup(serverAddress, secureConnection, transport, locale, persistentCache, onSetupCompleted) {
const tinode = new Tinode({appName: APP_NAME, host: serverAddress, apiKey: API_KEY, transport: transport,
secure: isSecureConnection(), persist: persistentCache}, onSetupCompleted);
secure: secureConnection, persist: persistentCache}, onSetupCompleted);
tinode.setHumanLanguage(locale);
tinode.enableLogging(LOGGING_ENABLED, true);
return tinode;
@@ -462,14 +472,20 @@ class TinodeWeb extends React.Component {
}
}
// Handle for hashchange event: display appropriate panels.
// Handle for hash navigation (hashchange) event: update state.
handleHashRoute() {
const hash = HashNavigation.parseUrlHash(window.location.hash);
// Start with panel parameters.
const newState = {
infoPanel: hash.params.info,
newTopicTabSelected: hash.params.tab
};
if (hash.path && hash.path.length > 0) {
// Left-side panel selector.
if (['register','settings','edit','notif','security','support','general','crop',
'cred','reset','newtpk','archive','blocked','contacts',''].includes(hash.path[0])) {
this.setState({sidePanelSelected: hash.path[0]});
newState.sidePanelSelected = hash.path[0];
} else {
console.warn("Unknown sidepanel view", hash.path[0]);
}
@@ -481,29 +497,34 @@ class TinodeWeb extends React.Component {
// Clear invalid topic name.
topicName = null;
}
this.setState({
Object.assign(newState, {
topicSelected: topicName,
topicSelectedAcs: this.tinode.getTopicAccessMode(topicName)
});
}
} else {
// Empty hashpath
this.setState({sidePanelSelected: '', topicSelected: null});
Object.assign(newState, {sidePanelSelected: '', topicSelected: null});
}
// Save validation credentials, if available.
// Save credential validation parameters, if available.
if (hash.params.method) {
this.setState({ credMethod: hash.params.method });
newState.credMethod = hash.params.method;
}
if (hash.params.code) {
this.setState({ credCode: hash.params.code });
newState.credCode = hash.params.code;
}
if (hash.params.token) {
newState.credToken = hash.params.token;
}
// Additional parameters of panels.
this.setState({
infoPanel: hash.params.info,
newTopicTabSelected: hash.params.tab
});
// Show a message if validation was successful.
if (hash.params.cred_done) {
Object.assign(newState,
TinodeWeb.stateForError(this.props.intl.formatMessage(messages.cred_confirmed_successfully), 'info'));
}
this.setState(newState);
}
handleOnline(online) {
@@ -521,14 +542,18 @@ class TinodeWeb extends React.Component {
this.setState({applicationVisible: !document.hidden});
}
handleError(err, level, action, actionText) {
this.setState({
static stateForError(err, level, action, actionText) {
return {
errorText: err,
errorLevel: level,
errorAction: action,
errorActionText: actionText,
callShouldStart: false,
});
};
}
handleError(err, level, action, actionText) {
this.setState(TinodeWeb.stateForError(err, level, action, actionText));
}
// User clicked Login button in the side panel.
@@ -543,7 +568,7 @@ class TinodeWeb extends React.Component {
this.handleError('', null);
if (this.tinode.isConnected()) {
this.doLogin(login, password, {meth: this.state.credMethod, resp: this.state.credCode});
this.doLogin(login, password, null, {meth: this.state.credMethod, resp: this.state.credCode});
} else {
this.tinode.connect().catch((err) => {
// Socket error
@@ -580,7 +605,8 @@ class TinodeWeb extends React.Component {
});
if (this.state.autoLogin) {
this.doLogin(this.state.login, this.state.password, {meth: this.state.credMethod, resp: this.state.credCode});
this.doLogin(this.state.login, this.state.password, null,
{meth: this.state.credMethod, resp: this.state.credCode});
}
}
@@ -596,7 +622,7 @@ class TinodeWeb extends React.Component {
if (prom) {
// Reconnecting now
prom.then(() => {
prom.then(_ => {
// Reconnected: clear error
this.handleError();
}).catch((err) => {
@@ -608,10 +634,17 @@ class TinodeWeb extends React.Component {
const {formatMessage} = this.props.intl;
let count = sec / 1000;
count = count | count;
this.reconnectCountdown = setInterval(() => {
const timeLeft = (count > 99) ? secondsToTime(count) : count;
this.reconnectCountdown = setInterval(_ => {
if (count < -10) {
// Count could go to negative if computer woke up from sleep.
clearInterval(this.reconnectCountdown);
this.tinode.reconnect();
return;
}
const fmtTime = (count > 99) ? secondsToTime(count) : count;
this.handleError(
formatMessage(messages.reconnect_countdown, {seconds: timeLeft}),
formatMessage(messages.reconnect_countdown, {seconds: fmtTime}),
'warn',
_ => {
clearInterval(this.reconnectCountdown);
@@ -638,63 +671,69 @@ class TinodeWeb extends React.Component {
});
}
doLogin(login, password, cred) {
doLogin(login, password, tmpToken, cred) {
if (this.tinode.isAuthenticated()) {
// Already logged in. Go to default screen.
HashNavigation.navigateTo('');
return;
}
// Sanitize and package credentail.
cred = Tinode.credential(cred);
// Try to login with login/password. If they are not available, try token. If no token, ask for login/password.
let promise = null;
let token = this.tinode.getAuthToken();
if (login && password) {
token = null;
this.setState({password: null});
promise = this.tinode.loginBasic(login, password, cred);
} else if (token) {
promise = this.tinode.loginToken(token.token, cred);
}
if (promise) {
promise.then((ctrl) => {
if (ctrl.code >= 300 && ctrl.text === 'validate credentials') {
this.setState({loadSpinnerVisible: false});
if (cred) {
this.handleError(this.props.intl.formatMessage(messages.code_doesnot_match), 'warn');
}
this.handleCredentialsRequest(ctrl.params);
} else {
this.handleLoginSuccessful();
}
}).catch((err) => {
// Login failed, report error.
this.setState({
loginDisabled: false,
credMethod: undefined,
credCode: undefined,
loadSpinnerVisible: false,
autoLogin: false
});
this.handleError(err.message, 'err');
if (token) {
this.handleLogout();
}
HashNavigation.navigateTo('');
});
} else {
let token = tmpToken || (this.tinode.getAuthToken() || {}).token;
if (!(login && password) && !token) {
// No login credentials provided.
// Make sure we are on the login page.
HashNavigation.navigateTo('');
this.setState({loginDisabled: false});
return;
}
// Sanitize and package credentail.
cred = Tinode.credential(cred);
// May be disconnected.
let connectionPromise = this.tinode.isConnected() ? Promise.resolve() : this.tinode.connect();
// Try to login with login/password. If they are not available, try token; if no token, ask for login/password.
let loginPromise;
if (login && password) {
token = null;
this.setState({password: null});
loginPromise = connectionPromise.then(_ => this.tinode.loginBasic(login, password, cred));
} else {
loginPromise = connectionPromise.then(_ => this.tinode.loginToken(token, cred));
}
loginPromise.then((ctrl) => {
if (ctrl.code >= 300 && ctrl.text === 'validate credentials') {
this.setState({loadSpinnerVisible: false});
if (cred) {
this.handleError(this.props.intl.formatMessage(messages.code_doesnot_match), 'warn');
}
TinodeWeb.navigateToCredentialsView(ctrl.params);
} else {
this.handleLoginSuccessful();
}
}).catch((err) => {
// Connection or login failed, report error.
this.setState({
loginDisabled: false,
credMethod: undefined,
credCode: undefined,
loadSpinnerVisible: false,
autoLogin: false
});
this.handleError(err.message, 'err');
console.warn("Login failed", err);
if (token) {
this.handleLogout();
}
HashNavigation.navigateTo('');
});
}
handleCredentialsRequest(params) {
static navigateToCredentialsView(params) {
const parsed = HashNavigation.parseUrlHash(window.location.hash);
parsed.path[0] = 'cred';
parsed.params['method'] = params.cred[0];
parsed.params['token'] = params.token;
HashNavigation.navigateTo(HashNavigation.composeUrlHash(parsed.path, parsed.params));
}
@@ -716,6 +755,7 @@ class TinodeWeb extends React.Component {
connected: true,
credMethod: undefined,
credCode: undefined,
credToken: undefined,
myUserId: this.tinode.getCurrentUserID(),
autoLogin: true,
requestedTopic: undefined,
@@ -887,9 +927,6 @@ class TinodeWeb extends React.Component {
this.setState(newState);
}
tnData(data) {
}
/* Fnd topic: find contacts by tokens */
tnInitFind() {
const fnd = this.tinode.getFndTopic();
@@ -1095,16 +1132,20 @@ class TinodeWeb extends React.Component {
this.handleError();
this.tinode.connect(this.state.serverAddress)
.then(() => {
.then(_ => {
let attachments;
if (public_ && public_.photo && public_.photo.ref) {
attachments = [public_.photo.ref];
}
return this.tinode.createAccountBasic(login_, password_,
{public: public_, tags: tags_, cred: Tinode.credential(cred_)});
}).then((ctrl) => {
{public: public_, tags: tags_, cred: Tinode.credential(cred_), attachments: attachments});
}).then(ctrl => {
if (ctrl.code >= 300 && ctrl.text == 'validate credentials') {
this.handleCredentialsRequest(ctrl.params);
TinodeWeb.navigateToCredentialsView(ctrl.params);
} else {
this.handleLoginSuccessful(this);
}
}).catch((err) => {
}).catch(err => {
this.handleError(err.message, 'err');
});
}
@@ -1141,13 +1182,15 @@ class TinodeWeb extends React.Component {
handleGlobalSettings(settings) {
const serverAddress = settings.serverAddress || this.state.serverAddress;
const transport = settings.transport || this.state.transport;
const secureConnection = settings.secureConnection === undefined ?
this.state.secureConnection : settings.secureConnection;
if (this.tinode) {
this.tinode.clearStorage();
this.tinode.onDisconnect = undefined;
this.tinode.disconnect();
}
this.tinode = TinodeWeb.tnSetup(serverAddress, transport, this.props.intl.locale,
LocalStorageUtil.getObject('keep-logged-in'));
this.tinode = TinodeWeb.tnSetup(serverAddress, secureConnection, transport,
this.props.intl.locale, LocalStorageUtil.getObject('keep-logged-in'));
this.tinode.onConnect = this.handleConnected;
this.tinode.onDisconnect = this.handleDisconnect;
this.tinode.onAutoreconnectIteration = this.handleAutoreconnectIteration;
@@ -1156,11 +1199,13 @@ class TinodeWeb extends React.Component {
this.setState({
serverAddress: serverAddress,
transport: transport
transport: transport,
secureConnection: secureConnection,
});
LocalStorageUtil.setObject('settings', {
serverAddress: serverAddress,
transport: transport
transport: transport,
secureConnection: secureConnection,
});
HashNavigation.navigateTo(HashNavigation.setUrlSidePanel(window.location.hash, ''));
@@ -1227,7 +1272,7 @@ class TinodeWeb extends React.Component {
}
handleCredConfirm(method, response) {
this.handleCredentialsRequest({cred: [method]});
TinodeWeb.navigateToCredentialsView({cred: [method]});
}
// User clicked Cancel button in Setting or Sign Up panel.
@@ -1407,7 +1452,7 @@ class TinodeWeb extends React.Component {
this.setState(this.getBlankState());
cleared.then(() => {
this.tinode = TinodeWeb.tnSetup(this.state.serverAddress,
this.tinode = TinodeWeb.tnSetup(this.state.serverAddress, isSecureConnection(),
this.state.transport, this.props.intl.locale, LocalStorageUtil.getObject('keep-logged-in'), () => {
this.tinode.onConnect = this.handleConnected;
this.tinode.onDisconnect = this.handleDisconnect;
@@ -1647,19 +1692,17 @@ class TinodeWeb extends React.Component {
}
}
handleValidateCredentialsRequest(cred, code) {
handleValidateCredentialsRequest(cred, code, token) {
if (this.tinode.isAuthenticated()) {
const me = this.tinode.getMeTopic();
me.setMeta({cred: {meth: cred, resp: code}})
.then(() => {
HashNavigation.navigateTo('');
})
// Adding new email or phone number in account setting.
this.tinode.getMeTopic().setMeta({cred: {meth: cred, resp: code}})
.catch((err) => {
this.handleError(err.message, 'err');
});
} else {
this.setState({credMethod: cred, credCode: code});
this.doLogin(null, null, {meth: cred, resp: code});
// Credential validation on signup.
this.setState({credMethod: cred, credCode: code, credToken: token});
this.doLogin(null, null, token, {meth: cred, resp: code});
}
}
@@ -1941,6 +1984,7 @@ class TinodeWeb extends React.Component {
chatList={this.state.chatList}
credMethod={this.state.credMethod}
credCode={this.state.credCode}
credToken={this.state.credToken}
transport={this.state.transport}
messageSounds={this.state.messageSounds}
@@ -1948,6 +1992,7 @@ class TinodeWeb extends React.Component {
desktopAlertsEnabled={this.state.desktopAlertsEnabled}
incognitoMode={this.state.incognitoMode}
serverAddress={this.state.serverAddress}
secureConnection={this.state.secureConnection}
serverVersion={this.state.serverVersion}
onGlobalSettings={this.handleGlobalSettings}
@@ -2025,7 +2070,6 @@ class TinodeWeb extends React.Component {
newTopicParams={this.state.newTopicParams}
onHideMessagesView={this.handleHideMessagesView}
onData={this.tnData}
onError={this.handleError}
onNewTopicCreated={this.handleNewTopicCreated}
showContextMenu={this.handleShowContextMenu}
+31 -5
View File
@@ -20,7 +20,8 @@ class ValidationView extends React.PureComponent {
super(props);
this.state = {
code: props.credCode || ''
code: props.credCode || '',
codeReceived: props.credCode
};
this.handleChange = this.handleChange.bind(this);
@@ -29,8 +30,34 @@ class ValidationView extends React.PureComponent {
this.handleCancel = this.handleCancel.bind(this);
}
static getDerivedStateFromProps(nextProps, prevState) {
if (nextProps.credCode != prevState.codeReceived) {
return {
code: nextProps.credCode || '',
codeReceived: nextProps.credCode
};
}
return prevState;
}
componentDidMount() {
// Submit code automatically if it's received from the parent.
if (this.props.credCode) {
this.props.onSubmit(this.props.credMethod, this.props.credCode, this.props.credToken);
}
}
componentDidUpdate(prevProps, prevState) {
// Submit code automatically if it's received from the parent.
if (this.state.codeReceived && this.state.code && this.state.code != prevState.code) {
this.props.onSubmit(this.props.credMethod, this.state.code, this.props.credToken);
}
}
handleChange(e) {
this.setState({code: e.target.value});
this.setState({code: e.target.value.trim()});
}
handleKeyPress(e) {
@@ -44,7 +71,7 @@ class ValidationView extends React.PureComponent {
handleSubmit(e) {
e.preventDefault();
if (this.state.code && this.state.code.trim()) {
this.props.onSubmit(this.props.credMethod, this.state.code.trim());
this.props.onSubmit(this.props.credMethod, this.state.code.trim(), this.props.credToken);
}
}
@@ -53,11 +80,10 @@ class ValidationView extends React.PureComponent {
this.props.onCancel();
}
render() {
const { formatMessage } = this.props.intl;
const methods = {'email': formatMessage(messages.email), 'tel': formatMessage(messages.phone)};
let method = methods[this.props.credMethod] || this.props.credMethod;
const method = methods[this.props.credMethod] || this.props.credMethod;
return (
<div className="panel-form">
<div className="panel-form-row">
+18 -7
View File
@@ -12,13 +12,24 @@ export default class CallMessage extends React.PureComponent {
const callIcon = this.props.incoming ?
(isCallDropped ? 'call_missed' : 'call_received') :
(isCallDropped ? 'call_missed_outgoing' : 'call_made');
const duration = isCallDropped ? (
this.props.incoming ? (this.props.callState == 'declined' ?
<FormattedMessage id="call_declined" defaultMessage="declined" description="Label for declined incoming call" /> :
<FormattedMessage id="call_missed" defaultMessage="missed" description="Label for missed incoming call" />
) :
<FormattedMessage id="call_cancelled" defaultMessage="cancelled" description="Label for cancelled outgoing call" />
) : <span>{secondsToTime(this.props.duration / 1000)}</span>;
let duration;
if (isCallDropped) {
switch (this.props.callState) {
case 'declined':
duration = <FormattedMessage id="call_declined" defaultMessage="declined" description="Label for declined call" />;
break;
case 'missed':
duration = this.props.incoming ?
<FormattedMessage id="call_missed" defaultMessage="missed" description="Label for missed incoming call" /> :
<FormattedMessage id="call_cancelled" defaultMessage="cancelled" description="Label for cancelled outgoing call" />;
break;
default:
duration = <FormattedMessage id="call_disconnected" defaultMessage="disconnected" description="Label for disconnected call" />;
break;
}
} else {
duration = <span>{secondsToTime(this.props.duration / 1000)}</span>;
}
return <div className="call-message">
<div><i className="material-icons big gray">call</i></div>
<div className="flex-column narrow">
+8 -1
View File
@@ -62,6 +62,7 @@ class CallPanel extends React.PureComponent {
this.handleICEConnectionStateChangeEvent = this.handleICEConnectionStateChangeEvent.bind(this);
this.handleSignalingStateChangeEvent = this.handleSignalingStateChangeEvent.bind(this);
this.handleICEGatheringStateChangeEvent = this.handleICEGatheringStateChangeEvent.bind(this);
this.handleIceCandidateErrorEvent = this.handleIceCandidateErrorEvent.bind(this);
this.handleTrackEvent = this.handleTrackEvent.bind(this);
this.handleVideoOfferMsg = this.handleVideoOfferMsg.bind(this);
@@ -181,6 +182,7 @@ class CallPanel extends React.PureComponent {
this.state.pc.onsignalingstatechange = null;
this.state.pc.onicegatheringstatechange = null;
this.state.pc.onnegotiationneeded = null;
this.state.pc.onicecandidateerror = null;
this.state.pc.close();
}
@@ -216,6 +218,7 @@ class CallPanel extends React.PureComponent {
pc.onicegatheringstatechange = this.handleICEGatheringStateChangeEvent;
pc.onsignalingstatechange = this.handleSignalingStateChangeEvent;
pc.onnegotiationneeded = this.handleNegotiationNeededEvent;
pc.onicecandidateerror = this.handleIceCandidateErrorEvent;
pc.ontrack = this.handleTrackEvent;
this.setState({pc: pc, waitingForPeer: false});
@@ -254,6 +257,10 @@ class CallPanel extends React.PureComponent {
.catch(this.reportError);
}
handleIceCandidateErrorEvent(event) {
console.warn("ICE candidate error:", event);
}
handleICECandidateEvent(event) {
if (event.candidate) {
this.props.onIceCandidate(this.props.topic, this.props.seq, event.candidate.toJSON());
@@ -294,7 +301,7 @@ class CallPanel extends React.PureComponent {
}
handleICEGatheringStateChangeEvent(event) {
// console.log('ICE gathering change state: ', event);
// ICE gathering change state
}
handleTrackEvent(event) {
+18 -7
View File
@@ -13,13 +13,24 @@ export default class CallStatus extends React.PureComponent {
const icon2 = this.props.incoming ?
(isCallDropped ? 'call_missed' : 'call_received') :
(isCallDropped ? 'call_missed_outgoing' : 'call_made');
const duration = isCallDropped ? (
this.props.incoming ? (this.props.callState == 'declined' ?
<FormattedMessage id="call_declined" defaultMessage="declined" description="Label for declined incoming call" /> :
<FormattedMessage id="call_missed" defaultMessage="missed" description="Label for missed incoming call" />
) :
<FormattedMessage id="call_cancelled" defaultMessage="cancelled" description="Label for cancelled outgoing call" />
) : <span>{secondsToTime(this.props.duration / 1000)}</span>;
let duration;
if (isCallDropped) {
switch (this.props.callState) {
case 'declined':
duration = <FormattedMessage id="call_declined" defaultMessage="declined" description="Label for declined call" />;
break;
case 'missed':
duration = this.props.incoming ?
<FormattedMessage id="call_missed" defaultMessage="missed" description="Label for missed incoming call" /> :
<FormattedMessage id="call_cancelled" defaultMessage="cancelled" description="Label for cancelled outgoing call" />;
break;
default:
duration = <FormattedMessage id="call_disconnected" defaultMessage="disconnected" description="Label for disconnected call" />;
break;
}
} else {
duration = <span>{secondsToTime(this.props.duration / 1000)}</span>;
}
return (
<>
<div className="composed-material">
+6 -1
View File
@@ -80,6 +80,9 @@ class BaseChatMessage extends React.PureComponent {
this.props.received > Tinode.MESSAGE_STATUS_FAILED &&
this.props.received < Tinode.MESSAGE_STATUS_DEL_RANGE) {
menuItems.push('menu_item_reply');
if (!this.props.response) {
menuItems.push('menu_item_edit');
}
}
menuItems.push('menu_item_forward');
@@ -90,7 +93,8 @@ class BaseChatMessage extends React.PureComponent {
userName: this.props.userName,
y: e.pageY,
x: e.pageX,
pickReply: this.props.pickReply
pickReply: this.props.pickReply,
editMessage: this.props.editMessage,
}, menuItems);
}
@@ -175,6 +179,7 @@ class BaseChatMessage extends React.PureComponent {
</div>
{this.props.timestamp ?
<ReceivedMarker
edited={this.props.edited}
timestamp={this.props.timestamp}
received={this.props.received} />
: null}
+34 -15
View File
@@ -1,6 +1,6 @@
import React from 'react';
/* BEGIN CheckBox: styled checkbox */
/* CheckBox: styled three-state checkbox, either clickable or static */
export default class CheckBox extends React.PureComponent {
constructor(props) {
super(props);
@@ -13,19 +13,38 @@ export default class CheckBox extends React.PureComponent {
}
render() {
return (
this.props.onChange ? (
this.props.checked === true ?
<i className="material-icons blue clickable" onClick={this.handleChange}>check_box</i> :
this.props.checked === false ?
<i className="material-icons blue clickable" onClick={this.handleChange}>check_box_outline_blank</i> :
<i className="material-icons lt-blue">indeterminate_check_box</i>
) : (
this.props.checked ?
<i className="material-icons">check_box</i> :
<i className="material-icons">check_box_outline_blank</i>
)
);
let classList = ['material-icons'];
let iconName;
if (Array.isArray(this.props.className)) {
classList.push(...this.props.className);
} else if (this.props.className) {
classList.push(this.props.className);
}
if (this.props.onChange) {
if (this.props.checked) {
classList.push('blue', 'clickable');
iconName = 'check_box';
} else if (this.props.checked === false) {
classList.push('blue', 'clickable');
iconName = 'check_box_outline_blank';
} else {
classList.push('lt-blue');
iconName = 'indeterminate_check_box';
}
} else {
if (this.props.checked) {
iconName = 'check_box';
} else {
iconName = 'check_box_outline_blank';
}
}
let attrs = {
className: classList.join(' '),
id: this.props.id,
};
if (this.props.onChange) {
attrs.onClick = this.handleChange;
}
return React.createElement('i', attrs, iconName);
}
}
/* END CheckBox */
+19 -3
View File
@@ -55,6 +55,11 @@ const messages = defineMessages({
defaultMessage: 'Forward',
description: 'Forward message'
},
edit: {
id: 'menu_item_edit',
defaultMessage: 'Edit',
description: 'Edit message'
},
topic_delete: {
id: 'menu_item_delete_topic',
defaultMessage: 'Delete',
@@ -139,7 +144,7 @@ class ContextMenu extends React.Component {
return props.onShowAlert(
formatMessage(messages.clear_messages), // title
formatMessage(messages.clear_messages_warning), // content
(() => { this.deleteMessages(true, false, params, errorHandler); }), // onConfirm
(_ => { this.deleteMessages(true, false, params, errorHandler); }), // onConfirm
null, // "OK"
true, // Show Reject button
null // "Cancel"
@@ -152,8 +157,8 @@ class ContextMenu extends React.Component {
handler: (params, errorHandler) => {
return props.onShowAlert(
formatMessage(messages.clear_for_all), // title
formatMessage(message.delete_messages_warning), // content
(() => { return this.deleteMessages(true, true, params, errorHandler); }),
formatMessage(messages.delete_messages_warning), // content
(_ => { return this.deleteMessages(true, true, params, errorHandler); }),
null, // "OK"
true, // Show Reject button
null // "Cancel"
@@ -193,6 +198,13 @@ class ContextMenu extends React.Component {
title: formatMessage(messages.forward),
handler: () => {} /* the action is taken directly in tinode-web */
},
'menu_item_edit': {
id: 'menu_item_edit',
title: formatMessage(messages.edit),
handler: (params, errorHandler) => {
return this.editMessage(params, errorHandler);
}
},
'topic_unmute': {
id: 'topic_unmute',
title: formatMessage(messages.unmute),
@@ -437,6 +449,10 @@ class ContextMenu extends React.Component {
params.pickReply(params.seq, params.content, params.userFrom, params.userName, errorHandler);
}
editMessage(params, errorHandler) {
params.editMessage(params.seq, params.content, errorHandler);
}
render() {
const menu = [];
let count = 0;
+9 -1
View File
@@ -15,6 +15,7 @@ export default class HostSelector extends React.PureComponent {
this.handleHostNameChange = this.handleHostNameChange.bind(this);
this.handleEditingFinished = this.handleEditingFinished.bind(this);
this.handleKeyDown = this.handleKeyDown.bind(this);
}
handleHostNameChange(e) {
@@ -28,6 +29,12 @@ export default class HostSelector extends React.PureComponent {
}
}
handleKeyDown(e) {
if (e.key == 'Enter') {
this.handleEditingFinished();
}
}
render() {
var hostOptions = [];
for (let key in KNOWN_HOSTS) {
@@ -40,7 +47,8 @@ export default class HostSelector extends React.PureComponent {
<div className="panel-form-row">
<input type="search" id="host-name" placeholder={this.props.hostName} list="known-hosts"
className="quoted" value={this.state.hostName} onChange={this.handleHostNameChange}
onBlur={this.handleEditingFinished} required />
onBlur={this.handleEditingFinished}
onKeyDown={this.handleKeyDown} required />
<datalist id="known-hosts">
{hostOptions}
</datalist>
+2 -2
View File
@@ -37,7 +37,7 @@ export default class NewTopicGroup extends React.PureComponent {
this.setState({[name]: e.target.value || ''});
}
handleImageChanged(img) {
handleImageChanged(mime, img) {
this.setState({imageDataUrl: img});
}
@@ -72,7 +72,7 @@ export default class NewTopicGroup extends React.PureComponent {
<AvatarUpload
tinode={this.props.tinode}
onError={this.props.onError}
onImageChanged={this.handleImageChanged} />
onImageUpdated={this.handleImageChanged} />
</center>
<div className="group">
<label className="small" htmlFor="new-topic-fn">
+7 -1
View File
@@ -16,6 +16,11 @@ const messages = defineMessages({
id: 'message_sending_failed',
defaultMessage: 'failed',
description: 'Failed to send message, in place of time stamp'
},
message_edited_marker: {
id: 'message_edited_marker',
defaultMessage: ', edited',
description: 'Marker indicating that the message was edited'
}
});
@@ -33,10 +38,11 @@ class ReceivedMarker extends React.PureComponent {
const icon = deliveryMarker(this.props.received);
const marker = icon ? <i className={'material-icons small ' + icon.color}>{icon.name}</i> : null;
const edited = this.props.edited ? formatMessage(messages.message_edited_marker) : null;
return (
<span className="timestamp">
{timestamp}{'\u00a0'}{marker}
{timestamp}{edited}{'\u00a0'}{marker}
</span>
);
}
+19 -8
View File
@@ -83,7 +83,14 @@ class SendMessage extends React.PureComponent {
}
if (prevProps.topicName != this.props.topicName) {
this.setState({message: '', audioRec: false, quote: null});
this.setState({message: this.props.initMessage || '', audioRec: false, quote: null});
} else if (prevProps.initMessage != this.props.initMessage) {
const msg = this.props.initMessage || '';
this.setState({message: msg}, _ => {
// If there is text, scroll to bottom and set caret to the end.
this.messageEditArea.scrollTop = this.messageEditArea.scrollHeight;
this.messageEditArea.setSelectionRange(msg.length, msg.length);
});
}
if (prevProps.reply != this.props.reply) {
this.setState({quote: this.formatReply()});
@@ -193,10 +200,13 @@ class SendMessage extends React.PureComponent {
formatMessage(messages[this.props.messagePrompt]) :
formatMessage(messages.type_new_message));
const sendIcon = (this.props.reply && this.props.reply.editing) ?
'check_circle' : 'send';
const quote = this.state.quote ?
(<div id="reply-quote-preview">
<div className="cancel">
<a href="#" onClick={(e) => {e.preventDefault(); this.props.onCancelReply();}}><i className="material-icons gray">close</i></a>
<a href="#" onClick={e => {e.preventDefault(); this.props.onCancelReply();}}><i className="material-icons gray">close</i></a>
</div>
{this.state.quote}
</div>) : null;
@@ -209,10 +219,10 @@ class SendMessage extends React.PureComponent {
<>
{this.props.onAttachFile && !this.state.audioRec ?
<>
<a href="#" onClick={(e) => {e.preventDefault(); this.attachImage.click();}} title="Add image">
<a href="#" onClick={e => {e.preventDefault(); this.attachImage.click();}} title="Add image">
<i className="material-icons secondary">photo</i>
</a>
<a href="#" onClick={(e) => {e.preventDefault(); this.attachFile.click();}} title="Attach file">
<a href="#" onClick={e => {e.preventDefault(); this.attachFile.click();}} title="Attach file">
<i className="material-icons secondary">attach_file</i>
</a>
</>
@@ -225,13 +235,14 @@ class SendMessage extends React.PureComponent {
onDeleted={_ => this.setState({audioRec: false})}
onFinished={this.handleAttachAudio}/> :
<textarea id="sendMessage" placeholder={prompt}
value={this.state.message} onChange={this.handleMessageTyping}
value={this.state.message}
onChange={this.handleMessageTyping}
onKeyPress={this.handleKeyPress}
ref={(ref) => {this.messageEditArea = ref;}}
autoFocus />)}
{this.state.message || !audioEnabled ?
<a href="#" onClick={this.handleSend} title="Send">
<i className="material-icons">send</i>
<i className="material-icons">{sendIcon}</i>
</a> :
!this.state.audioRec ?
<a href="#" onClick={e => {e.preventDefault(); this.setState({audioRec: true})}} title="Voice">
@@ -239,9 +250,9 @@ class SendMessage extends React.PureComponent {
</a> :
null
}
<input type="file" ref={(ref) => {this.attachFile = ref;}}
<input type="file" ref={ref => {this.attachFile = ref}}
onChange={this.handleAttachFile} style={{display: 'none'}} />
<input type="file" ref={(ref) => {this.attachImage = ref;}} accept="image/*"
<input type="file" ref={ref => {this.attachImage = ref}} accept="image/*"
onChange={this.handleAttachImage} style={{display: 'none'}} />
</>
:
+7 -5
View File
@@ -3,7 +3,6 @@
import React from 'react';
import { FormattedMessage } from 'react-intl';
import AvatarUpload from '../widgets/avatar-upload.jsx';
import TopicDescEdit from '../widgets/topic-desc-edit.jsx';
import { arrayEqual, asEmail, asPhone } from '../lib/utils.js';
@@ -159,10 +158,13 @@ export default class TopicCommon extends React.Component {
<div className="quoted">
{credentials}
{this.state.addCredActive ?
<input type="text" value={this.state.value}
className={this.state.addCredInvalid ? 'invalid' : null}
placeholder="Phone number or email" required="required" autoFocus
onChange={this.handleCredChange} onKeyDown={this.handleCredKeyDown} onBlur={this.handleCredEntered} />
<FormattedMessage id="phone_or_email_prompt" defaultMessage="Phone number or email"
description="Placeholer for phone or email">{(phone_or_email_prompt) =>
<input type="text" value={this.state.value}
className={this.state.addCredInvalid ? 'invalid' : null}
placeholder={phone_or_email_prompt} required="required" autoFocus
onChange={this.handleCredChange} onKeyDown={this.handleCredKeyDown} onBlur={this.handleCredEntered} />
}</FormattedMessage>
: null}
<div>
<a href="#" onClick={(e) => {e.preventDefault(); this.setState({addCredActive: true});}}>
+6 -17
View File
@@ -39,10 +39,10 @@ export default class TopicDescEdit extends React.Component {
this.handleFullNameUpdate = this.handleFullNameUpdate.bind(this);
this.handleImageUpdated = this.handleImageUpdated.bind(this);
this.handleAvatarCropped = this.handleAvatarCropped.bind(this);
this.handleAvatarCropCancel = this.handleAvatarCropCancel.bind(this);
this.uploadAvatar = this.uploadAvatar.bind(this);
this.handlePrivateUpdate = this.handlePrivateUpdate.bind(this);
this.handleDescriptionUpdate = this.handleDescriptionUpdate.bind(this);
this.uploadAvatar = this.uploadAvatar.bind(this);
this.handleAvatarCropCancel = this.handleAvatarCropCancel.bind(this);
this.handleTagsUpdated = this.handleTagsUpdated.bind(this);
}
@@ -113,27 +113,16 @@ export default class TopicDescEdit extends React.Component {
if (blob.size > MAX_AVATAR_BYTES) {
// Too large to send inband - uploading out of band and sending as a link.
const uploader = this.props.tinode.getLargeFileHelper();
this.setState({uploading: true});
uploader.upload(blob)
.then((url) => {
this.props.onUpdateTopicDesc(this.props.topic, theCard(null, url));
})
.catch((err) => {
this.props.onError(err, 'err');
})
.finally(() => {
this.setState({uploading: false});
});
.then(url => this.props.onUpdateTopicDesc(this.props.topic, theCard(null, url)))
.catch(err => this.props.onError(err.message, 'err'));
} else {
this.setState({uploading: true});
// Convert blob to base64-encoded bits.
blobToBase64(blob)
.then(b64 => {
const du = makeImageUrl({data: b64.bits, type: mime});
this.setState({source: du});
this.props.onUpdateTopicDesc(this.props.topic, theCard(null, du));
this.setState({uploading: false});
});
}
};
@@ -142,13 +131,13 @@ export default class TopicDescEdit extends React.Component {
// Avatar is not square or too large even after cropping. Shrink it and make square.
imageScaled(blob, AVATAR_SIZE, AVATAR_SIZE, MAX_EXTERN_ATTACHMENT_SIZE, true)
.then(scaled => readyToUpload(scaled))
.catch(err => this.props.onError(err, 'err'));
.catch(err => this.props.onError(err.message, 'err'));
} else {
readyToUpload({mime: mime, blob: blob, width: width, height: height});
}
}
handleAvatarCropCancel(img) {
handleAvatarCropCancel() {
this.setState({newAvatar: null, newAvatarMime: null});
}
+1145 -1648
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+207 -1369
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1,3 +1,3 @@
// This is a generated file. Don't edit.
const PACKAGE_VERSION = "0.20.0-rc1";
const PACKAGE_VERSION = "0.21.0-beta1";