Compare commits
31
Commits
eac59ea11f
...
d32c1636ce
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d32c1636ce | ||
|
|
a99ad81196 | ||
|
|
eec52a590c | ||
|
|
c7b8ce11e0 | ||
|
|
2156aa0877 | ||
|
|
966dec9654 | ||
|
|
0b2ab61422 | ||
|
|
092b4697a9 | ||
|
|
6c5fe836f4 | ||
|
|
40b99f7332 | ||
|
|
d45a141704 | ||
|
|
ee106a17f6 | ||
|
|
22320808fc | ||
|
|
5006a6fb34 | ||
|
|
df203f48bd | ||
|
|
0e53234d02 | ||
|
|
14a4725c94 | ||
|
|
740cb7faf8 | ||
|
|
384955777d | ||
|
|
8b80f59d3a | ||
|
|
6a6e3ad754 | ||
|
|
d15aa9372d | ||
|
|
5144f581e7 | ||
|
|
8250c5f372 | ||
|
|
ae26e0ba3d | ||
|
|
6a52591e1a | ||
|
|
81afca1efe | ||
|
|
3922db3aa6 | ||
|
|
9cd778534a | ||
|
|
e42de09504 | ||
|
|
5c842ef212 |
@@ -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
@@ -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.
|
||||
*/
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
+3
-3
@@ -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
@@ -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>
|
||||
|
||||
Generated
+2187
-3080
File diff suppressed because it is too large
Load Diff
+21
-20
@@ -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
@@ -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);
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
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.
|
||||
|
||||
export const PACKAGE_VERSION = "0.20.0-rc1";
|
||||
export const PACKAGE_VERSION = "0.21.0-beta1";
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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} />}
|
||||
</>
|
||||
|
||||
@@ -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:"
|
||||
|
||||
@@ -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
@@ -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}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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
@@ -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 */
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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'}} />
|
||||
</>
|
||||
:
|
||||
|
||||
@@ -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});}}>
|
||||
|
||||
@@ -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
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
File diff suppressed because one or more lines are too long
+207
-1369
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
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user