merge devel
This commit is contained in:
@@ -4,7 +4,7 @@
|
||||
"namespace": "android_app",
|
||||
"package_name": "co.tinode.tindroidx",
|
||||
"sha256_cert_fingerprints":
|
||||
["5B:BE:6A:C4:A7:66:A8:BB:4B:C1:52:66:E5:A4:BD:66:3C:33:50:04:11:CB:17:44:45:89:5E:3C:8D:55:77:7A"]
|
||||
["25:9A:71:EE:BD:2E:9F:D3:2D:64:1E:90:49:33:BD:F3:E3:42:BD:C4:DD:FA:BA:68:EE:D2:EB:76:65:33:DC:92"]
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -22,4 +22,5 @@
|
||||
"sha256_cert_fingerprints":
|
||||
["5B:BE:6A:C4:A7:66:A8:BB:4B:C1:52:66:E5:A4:BD:66:3C:33:50:04:11:CB:17:44:45:89:5E:3C:8D:55:77:7A"]
|
||||
}
|
||||
}]
|
||||
}
|
||||
]
|
||||
|
||||
@@ -37,18 +37,20 @@ This is _NOT_ a standalone app, this is just a frontend, a client. It requires a
|
||||
|
||||
## 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.
|
||||
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=fr.
|
||||
|
||||
The app is currently available in the following languages:
|
||||
* English
|
||||
* Chinese (simplified)
|
||||
* Chinese (traditional)
|
||||
* English - default
|
||||
* Chinese simplified
|
||||
* Chinese traditional
|
||||
* French
|
||||
* German
|
||||
* Korean
|
||||
* Romanian
|
||||
* Russian
|
||||
* Spanish
|
||||
* Thai
|
||||
* Ukrainian
|
||||
|
||||
More translations are welcome. See [instructions](https://github.com/tinode/chat/blob/devel/docs/translations.md#webapp).
|
||||
|
||||
|
||||
+31
-4
@@ -1212,7 +1212,7 @@ img.channel {
|
||||
flex-grow: 0;
|
||||
padding: 0.5rem 0.75rem;
|
||||
line-height: 1.5;
|
||||
overflow-x: hidden;
|
||||
overflow-x: clip;
|
||||
}
|
||||
|
||||
/* Make sure padding is only applied at the top level panel */
|
||||
@@ -1420,6 +1420,11 @@ img.channel {
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
/* Div holding QR code */
|
||||
.qr-code {
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
/* Scrollable panel where the messages are shown */
|
||||
#messages-panel {
|
||||
position: relative;
|
||||
@@ -1589,8 +1594,10 @@ img.channel {
|
||||
justify-content: space-between;
|
||||
background-color: #eceff1;
|
||||
box-shadow: 0 1px 2px silver; /* Faint shadow under the panel */
|
||||
line-height: 2.5rem;
|
||||
height: 4.6rem;
|
||||
line-height: 2rem;
|
||||
flex-shrink: 0;
|
||||
flex-grow: 0;
|
||||
height: 4rem;
|
||||
padding: 1rem 0.75rem 0 0.75rem;
|
||||
z-index: 3;
|
||||
}
|
||||
@@ -1620,6 +1627,8 @@ img.channel {
|
||||
line-height: 1.5rem;
|
||||
background-color: #eceff1;
|
||||
box-shadow: 0px -2px 3px -1px rgba(0,0,0,0.1); /* Faint shadow above panel */
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
@@ -2016,6 +2025,24 @@ video {
|
||||
padding: 0 0.75rem 0 0.75rem;
|
||||
}
|
||||
|
||||
/* Increase font size when the message contains only emojis.
|
||||
Font from 3x to almost normal, from 1 to 5 emojis in a message */
|
||||
.bubble div.message-content.emoji-1 {
|
||||
font-size: 300%;
|
||||
}
|
||||
.bubble div.message-content.emoji-2 {
|
||||
font-size: 241%;
|
||||
}
|
||||
.bubble div.message-content.emoji-3 {
|
||||
font-size: 193%;
|
||||
}
|
||||
.bubble div.message-content.emoji-4 {
|
||||
font-size: 155%;
|
||||
}
|
||||
.bubble div.message-content.emoji-5 {
|
||||
font-size: 125%;
|
||||
}
|
||||
|
||||
/* The timestamp is a floating element. Need to ensure div.message-content is expanded
|
||||
* to accomodate it. It's usually called clearfix. */
|
||||
.bubble div.message-content:after {
|
||||
@@ -3314,7 +3341,7 @@ ul.phone-country-selector span.country {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Bottom sheet rathar than a dialog */
|
||||
/* Bottom sheet rather than a dialog */
|
||||
.forward-dialog {
|
||||
border-radius: 0.3rem 0.3rem 0 0;
|
||||
margin: auto 0 0 0;
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
@@ -29,6 +29,8 @@
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons" />
|
||||
<!-- Styles for local elements -->
|
||||
<link rel="stylesheet" href="css/base.css" />
|
||||
<!-- Polyfill Intl.Segmenter for Firefox 124 and earlier. This can be safely removed in the end of 2024. -->
|
||||
<script crossorigin="anonymous" src="https://polyfill-fastly.io/v3/polyfill.min.js?features=Intl.Segmenter"></script>
|
||||
<!-- ReactJS -->
|
||||
<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>
|
||||
@@ -36,6 +38,8 @@
|
||||
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/react-intl@6/react-intl.iife.js"></script>
|
||||
<!-- LiveKit for group video calls -->
|
||||
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/livekit-client@1/dist/livekit-client.umd.js"></script>
|
||||
<!-- Displaying QR code with ID of the topic -->
|
||||
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/qrcodejs@1.0.0/qrcode.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
+3
-1
@@ -23,10 +23,12 @@
|
||||
<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://polyfill-fastly.io/v3/polyfill.min.js?features=Intl.Segmenter"></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.3.2/react-intl.iife.min.js"></script>
|
||||
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/livekit-client@1.10.0/dist/livekit-client.umd.js"></script>
|
||||
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/react-intl@6.4.4/react-intl.iife.min.js"></script>
|
||||
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/qrcodejs@1.0.0/qrcode.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<noscript><div id="noscript-message"><div id="noscript-title">Javascript support is required.</div>
|
||||
|
||||
Generated
+2937
-7165
File diff suppressed because it is too large
Load Diff
+20
-18
@@ -4,7 +4,7 @@
|
||||
"version": "0.23.0-rc2",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/tinode/webapp.git"
|
||||
"url": "git+https://github.com/tinode/webapp.git"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "echo 'Read installation instructions'",
|
||||
@@ -20,7 +20,7 @@
|
||||
"extract": "formatjs extract \"src/**/*.{js,jsx}\" --out-file src/i18n/ex/base-en.json --id-interpolation-pattern '[sha512:contenthash:base64:6]'",
|
||||
"compile": "formatjs compile"
|
||||
},
|
||||
"browserslist": "> 0.5%, not IE 11, not op_mini all, not and_uc >0",
|
||||
"browserslist": "> 0.5%, last 2 versions, Firefox ESR, not dead",
|
||||
"files": [
|
||||
"umd/*.js",
|
||||
"umd/*.map",
|
||||
@@ -34,7 +34,6 @@
|
||||
"index.html",
|
||||
"manifest.json",
|
||||
"service-worker.js",
|
||||
"firebase-init.js",
|
||||
"version.js"
|
||||
],
|
||||
"keywords": [
|
||||
@@ -53,6 +52,8 @@
|
||||
"ko",
|
||||
"ro",
|
||||
"ru",
|
||||
"th",
|
||||
"uk",
|
||||
"zh",
|
||||
"zh-TW"
|
||||
],
|
||||
@@ -65,29 +66,30 @@
|
||||
"author": "Tinode Authors <info@tinode.co>",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"firebase": "^9.17.2",
|
||||
"libphonenumber-js": "^1.10.24",
|
||||
"firebase": "^10.1.0",
|
||||
"libphonenumber-js": "^1.10.40",
|
||||
"qrcodejs": "^1.0.0",
|
||||
"livekit-client": "^1.10.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-intl": "^6.3.0",
|
||||
"tinode-sdk": "^0.23.0-rc1",
|
||||
"react-intl": "^6.4.4",
|
||||
"tinode-sdk": "^0.23.0",
|
||||
"webm-duration-fix": "^1.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.21.3",
|
||||
"@babel/preset-env": "^7.20.2",
|
||||
"@babel/preset-react": "^7.18.6",
|
||||
"@formatjs/cli": "^6.0.4",
|
||||
"babel-loader": "^9.1.2",
|
||||
"babel-plugin-formatjs": "^10.4.0",
|
||||
"browserslist": "^4.21.5",
|
||||
"@babel/core": "^7.22.10",
|
||||
"@babel/preset-env": "^7.22.10",
|
||||
"@babel/preset-react": "^7.22.5",
|
||||
"@formatjs/cli": "^6.1.3",
|
||||
"babel-loader": "^9.1.3",
|
||||
"babel-plugin-formatjs": "^10.5.3",
|
||||
"browserslist": "^4.21.10",
|
||||
"copy-webpack-plugin": "^11.0.0",
|
||||
"cssnano": "^6.0.1",
|
||||
"glob": "^10.2.7",
|
||||
"postcss": "^8.4.21",
|
||||
"glob": "^10.3.3",
|
||||
"postcss": "^8.4.27",
|
||||
"postcss-cli": "^10.1.0",
|
||||
"webpack": "^5.76.1",
|
||||
"webpack-cli": "^5.0.1"
|
||||
"webpack": "^5.88.2",
|
||||
"webpack-cli": "^5.1.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,6 +38,14 @@ const i18n = {
|
||||
'new_message': "Новое сообщение",
|
||||
'new_chat': "Новый чат",
|
||||
},
|
||||
'th': {
|
||||
'new_message': "ข้อความใหม่",
|
||||
'new_chat': "แชทใหม่",
|
||||
},
|
||||
'uk': {
|
||||
'new_message': "Нове повідомлення",
|
||||
'new_chat': "Новий чат",
|
||||
},
|
||||
'zh': {
|
||||
'new_message': "新讯息",
|
||||
'new_chat': "新聊天",
|
||||
|
||||
@@ -126,3 +126,16 @@ export const LINK_TERMS_OF_SERVICE = 'https://tinode.co/terms.html';
|
||||
// happen until the first message. Setting this option to true enables
|
||||
// immediate subscription.
|
||||
export const IMMEDIATE_P2P_SUBSCRIPTION = false;
|
||||
|
||||
// URL schemes permitted to appear in links (i.e. no javascript: scheme).
|
||||
export const CLICKABLE_URL_SCHEMES = ['http', 'https', 'ftp', 'ftps'];
|
||||
|
||||
// Size of the QR code in the app.
|
||||
export const QRCODE_SIZE = 128;
|
||||
|
||||
// How much time has to pass to consider computer awoken from sleep (80 sec).
|
||||
// Chrome throttles ticker events in inactive tabs to once per minute. This
|
||||
// value ensures that these once-a-minute events do not trigger a reconnect.
|
||||
export const WAKE_UP_TIMEOUT = 80000;
|
||||
// Timer interval between checks if computer woke up from sleep (1 sec).
|
||||
export const WAKE_UP_TICK = 1000;
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1668,5 +1668,12 @@
|
||||
"description": "Call to action to join a conference call",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"scan_qr_code": {
|
||||
"translation": "QR-Code scannen",
|
||||
"defaultMessage": "Scan QR Code",
|
||||
"description": "Title for scanning QR code",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1668,5 +1668,12 @@
|
||||
"description": "Call to action to join a conference call",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"scan_qr_code": {
|
||||
"translation": "Scan QR Code",
|
||||
"defaultMessage": "Scan QR Code",
|
||||
"description": "Title for scanning QR code",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1668,5 +1668,12 @@
|
||||
"description": "Call to action to join a conference call",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"scan_qr_code": {
|
||||
"translation": "Escanear código QR",
|
||||
"defaultMessage": "Scan QR Code",
|
||||
"description": "Title for scanning QR code",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -808,6 +808,10 @@
|
||||
"defaultMessage": "save",
|
||||
"description": "Call to save an attachment"
|
||||
},
|
||||
"scan_qr_code": {
|
||||
"defaultMessage": "Scan QR Code",
|
||||
"description": "Title for scanning QR code"
|
||||
},
|
||||
"search_for_contacts": {
|
||||
"defaultMessage": "Use search to find contacts",
|
||||
"description": "Text shown in contacts view when user entered no search query."
|
||||
|
||||
@@ -1668,5 +1668,12 @@
|
||||
"description": "Call to action to join a conference call",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"scan_qr_code": {
|
||||
"translation": "Scanner le code QR",
|
||||
"defaultMessage": "Scan QR Code",
|
||||
"description": "Title for scanning QR code",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1668,5 +1668,12 @@
|
||||
"description": "Call to action to join a conference call",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"scan_qr_code": {
|
||||
"translation": "QR 코드 스캔",
|
||||
"defaultMessage": "Scan QR Code",
|
||||
"description": "Title for scanning QR code",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1668,5 +1668,12 @@
|
||||
"description": "Call to action to join a conference call",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"scan_qr_code": {
|
||||
"translation": "Scanați codul QR",
|
||||
"defaultMessage": "Scan QR Code",
|
||||
"description": "Title for scanning QR code",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1668,5 +1668,12 @@
|
||||
"description": "Call to action to join a conference call",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"scan_qr_code": {
|
||||
"translation": "Отсканируйте QR код",
|
||||
"defaultMessage": "Scan QR Code",
|
||||
"description": "Title for scanning QR code",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
}
|
||||
}
|
||||
|
||||
+1679
File diff suppressed because it is too large
Load Diff
+1679
File diff suppressed because it is too large
Load Diff
@@ -1668,5 +1668,12 @@
|
||||
"description": "Call to action to join a conference call",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"scan_qr_code": {
|
||||
"translation": "掃描二維碼",
|
||||
"defaultMessage": "Scan QR Code",
|
||||
"description": "Title for scanning QR code",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1668,5 +1668,12 @@
|
||||
"description": "Call to action to join a conference call",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"scan_qr_code": {
|
||||
"translation": "扫描二维码",
|
||||
"defaultMessage": "Scan QR Code",
|
||||
"description": "Title for scanning QR code",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,8 @@ const messageLoader = {
|
||||
'ko': _ => import('./i18n.min/ko.json'),
|
||||
'ro': _ => import('./i18n.min/ro.json'),
|
||||
'ru': _ => import('./i18n.min/ru.json'),
|
||||
'th': _ => import('./i18n.min/th.json'),
|
||||
'uk': _ => import('./i18n.min/uk.json'),
|
||||
'zh': _ => import('./i18n.min/zh.json'),
|
||||
'zh-TW': _ => import('./i18n.min/zh-TW.json')
|
||||
};
|
||||
|
||||
@@ -10,10 +10,11 @@ import InlineVideo from '../widgets/inline-video.jsx';
|
||||
import LazyImage from '../widgets/lazy-image.jsx'
|
||||
import UploadingImage from '../widgets/uploading-image.jsx'
|
||||
|
||||
import { BROKEN_IMAGE_SIZE, IMAGE_THUMBNAIL_DIM, NO_DIMENSIONS_VIDEO, REM_SIZE, VIDEO_THUMBNAIL_WIDTH } from '../config.js';
|
||||
import { BROKEN_IMAGE_SIZE, CLICKABLE_URL_SCHEMES, IMAGE_THUMBNAIL_DIM, NO_DIMENSIONS_VIDEO, REM_SIZE,
|
||||
VIDEO_THUMBNAIL_WIDTH } from '../config.js';
|
||||
import { base64ToBlob, blobToBase64, fitImageSize, imageScaled } from './blob-helpers.js';
|
||||
import { idToColorClass, secondsToTime, shortenFileName } from './strformat.js';
|
||||
import { cancelablePromise, sanitizeUrlForMime } from './utils.js';
|
||||
import { cancelablePromise, sanitizeUrl, sanitizeUrlForMime } from './utils.js';
|
||||
|
||||
const messages = defineMessages({
|
||||
drafty_form: {
|
||||
@@ -109,6 +110,12 @@ export function fullFormatter(style, data, values, key, stack) {
|
||||
// Get text which will be sent back when the button is clicked.
|
||||
attr['data-title'] = inner.join('');
|
||||
break;
|
||||
case 'LN':
|
||||
if (attr) {
|
||||
// Ensure only safe URL schemes are clickable.
|
||||
attr.href = typeof attr.href == 'string' ? sanitizeUrl(attr.href, CLICKABLE_URL_SCHEMES) : '';
|
||||
}
|
||||
break;
|
||||
case 'MN':
|
||||
// Mention
|
||||
attr.className = 'mention'
|
||||
|
||||
+43
-2
@@ -108,9 +108,12 @@ export function asEmail(val) {
|
||||
// example.html - ok
|
||||
// https:example.com - not ok.
|
||||
// http:/example.com - not ok.
|
||||
// //example.com or \\example.com - not ok.
|
||||
// ' ↲ https://example.com' - not ok. (↲ means carriage return)
|
||||
export function isUrlRelative(url) {
|
||||
return url && !/^\s*([a-z][a-z0-9+.-]*:|\/\/)/im.test(url);
|
||||
// Replacing backslashes with forward slashes to mimic JS URL parser then testing for
|
||||
// 'scheme:' and '//' with optional space at the start.
|
||||
return url && !/^\s*([a-z][a-z0-9+.-]*:|\/\/)/im.test(url.replace(/\\/g, '/'));
|
||||
}
|
||||
|
||||
// Ensure URL does not present an XSS risk. Optional allowedSchemes may contain an array of
|
||||
@@ -121,7 +124,10 @@ export function sanitizeUrl(url, allowedSchemes) {
|
||||
}
|
||||
|
||||
// Strip control characters and whitespace. They are not valid URL characters anyway.
|
||||
url = url.replace(/[^\x20-\x7E]/gmi, '').trim();
|
||||
url = url.replace(/[^\x21-\x7E]/gmi, '').trim();
|
||||
|
||||
// Replace backslashes with forward slashes. They will be replaced in the URL parser anyway.
|
||||
url = url.replace(/\\/g, '/');
|
||||
|
||||
// Relative URLs are safe.
|
||||
// Relative URL does not start with ':', abcd123: or '//'.
|
||||
@@ -166,6 +172,41 @@ export function sanitizeUrlForMime(url, mimeMajor) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Append query parameter 'asatt=1' to the URL.
|
||||
// It will cause Tinode server to add 'Content-Disposition: attachment' header when serving it.
|
||||
// The URL is a string. The URL here is always absolute.
|
||||
export function urlAsAttachment(url) {
|
||||
// TODO: check if URL is local or remote, i.e. compare to window.location.origin.
|
||||
/*
|
||||
if (typeof window.location == 'object') {
|
||||
if (!url.startsWith(window.location.origin)) {
|
||||
return url;
|
||||
}
|
||||
}
|
||||
*/
|
||||
// Check if the URL is a data or blob. Do not alter such URLs.
|
||||
if (url.startsWith('data:') || url.startsWith('blob:')) {
|
||||
return url;
|
||||
}
|
||||
|
||||
// Split URL into host+path, query, fragment.
|
||||
let query = '', fragment = '';
|
||||
const idxF = url.indexOf('#');
|
||||
if (idxF > 0) {
|
||||
fragment = url.substring(idxF+1);
|
||||
url = url.substring(0, idxF);
|
||||
}
|
||||
const idxQ = url.indexOf('?');
|
||||
if (idxQ > 0) {
|
||||
query = url.substring(idxQ+1);
|
||||
url = url.substring(0, idxQ);
|
||||
}
|
||||
// Add parameter and reassemble.
|
||||
const params = new URLSearchParams(query);
|
||||
params.append('asatt', '1');
|
||||
return `${url}?${params.toString()}` + (fragment ? `#${fragment}` : '');
|
||||
}
|
||||
|
||||
// Given message's received status, return name and color of a delivery indicator icon.
|
||||
export function deliveryMarker(received) {
|
||||
switch (received) {
|
||||
|
||||
+34
-1
@@ -12,6 +12,7 @@ import ErrorPanel from '../widgets/error-panel.jsx';
|
||||
import GroupManager from '../widgets/group-manager.jsx';
|
||||
import MenuCancel from '../widgets/menu-cancel.jsx';
|
||||
import PermissionsEditor from '../widgets/permissions-editor.jsx';
|
||||
import ShowQRCode from '../widgets/show-qrcode.jsx';
|
||||
import TopicCommonView from './topic-common-view.jsx';
|
||||
import TopicSecurity from '../widgets/topic-security.jsx';
|
||||
|
||||
@@ -76,6 +77,11 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Edit permissions',
|
||||
description: 'Menu item [Edit permissions]'
|
||||
},
|
||||
qrcode: {
|
||||
id: 'scan_qr_code',
|
||||
defaultMessage: 'Scan QR Code',
|
||||
description: 'Title for scanning QR code'
|
||||
},
|
||||
});
|
||||
|
||||
class InfoView extends React.Component {
|
||||
@@ -120,6 +126,8 @@ class InfoView extends React.Component {
|
||||
this.handleUnarchive = this.handleUnarchive.bind(this);
|
||||
this.handlePermissionsChanged = this.handlePermissionsChanged.bind(this);
|
||||
this.handleLaunchPermissionsEditor = this.handleLaunchPermissionsEditor.bind(this);
|
||||
this.handleCopyID = this.handleCopyID.bind(this);
|
||||
this.handleShowQRCode = this.handleShowQRCode.bind(this);
|
||||
this.handleShowAddMembers = this.handleShowAddMembers.bind(this);
|
||||
this.handleMemberUpdateRequest = this.handleMemberUpdateRequest.bind(this);
|
||||
this.handleMemberSelected = this.handleMemberSelected.bind(this);
|
||||
@@ -368,6 +376,16 @@ class InfoView extends React.Component {
|
||||
this.props.onNavigate(`perm/${which}`);
|
||||
}
|
||||
|
||||
handleCopyID(e) {
|
||||
e.preventDefault();
|
||||
navigator.clipboard.writeText(this.props.myUserId);
|
||||
}
|
||||
|
||||
handleShowQRCode(e) {
|
||||
e.preventDefault();
|
||||
this.props.onNavigate('qrcode');
|
||||
}
|
||||
|
||||
handleShowAddMembers(e) {
|
||||
e.preventDefault();
|
||||
this.props.onInitFind();
|
||||
@@ -514,6 +532,11 @@ class InfoView extends React.Component {
|
||||
onLaunchPermissionsEditor={this.handleLaunchPermissionsEditor}
|
||||
onNavigate={this.props.onNavigate} />
|
||||
:
|
||||
view == 'qrcode' ?
|
||||
<ShowQRCode
|
||||
uri={Tinode.URI_TOPIC_ID_PREFIX + this.props.tinode.myUserId}
|
||||
onCancel={this.handleBackNavigate} />
|
||||
:
|
||||
<div id="info-view-content" className="scrollable-panel">
|
||||
<div className="panel-form-column">
|
||||
<a href="#" className="flat-button float-right" onClick={(e) => {e.preventDefault(); this.props.onNavigate('general');}}>
|
||||
@@ -546,11 +569,21 @@ class InfoView extends React.Component {
|
||||
</div>
|
||||
: null
|
||||
}
|
||||
<div className="group">
|
||||
<div className="panel-form-row">
|
||||
<div>
|
||||
<label className="small"><FormattedMessage id="label_user_id" defaultMessage="ID:"
|
||||
description="Label for user address (ID)" /></label>
|
||||
<tt>{this.state.address}</tt>
|
||||
</div>
|
||||
<div style={{marginLeft: 'auto'}}>
|
||||
<a href="#" onClick={this.handleCopyID}>
|
||||
<i className="material-icons">content_copy</i>
|
||||
</a>
|
||||
<a href="#" onClick={this.handleShowQRCode}>
|
||||
<i className="material-icons">qr_code</i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div className="group">
|
||||
<BadgeList trustedBadges={this.state.trustedBadges} />
|
||||
</div>
|
||||
|
||||
@@ -1685,7 +1685,7 @@ class MessagesView extends React.Component {
|
||||
<>
|
||||
<div id="topic-caption-panel" className="caption-panel">
|
||||
{this.props.displayMobile ?
|
||||
<a href="#" id="hide-message-view" onClick={(e) => {e.preventDefault(); this.props.onHideMessagesView();}}>
|
||||
<a href="#" id="hide-message-view" onClick={(e) => {e.preventDefault(); this.leave(this.state.topic); this.props.onHideMessagesView();}}>
|
||||
<i className="material-icons">arrow_back</i>
|
||||
</a>
|
||||
:
|
||||
|
||||
@@ -113,6 +113,7 @@ class NewTopicView extends React.Component {
|
||||
onError={this.props.onError} /> :
|
||||
this.state.tabSelected === 'byid' ?
|
||||
<NewTopicById
|
||||
myURI={Tinode.URI_TOPIC_ID_PREFIX + this.props.tinode.getCurrentUserID()}
|
||||
onSubmit={this.handleGroupByID}
|
||||
onError={this.props.onError} /> :
|
||||
<div className="flex-column">
|
||||
|
||||
@@ -19,7 +19,7 @@ import MessagesView from './messages-view.jsx';
|
||||
import SidepanelView from './sidepanel-view.jsx';
|
||||
|
||||
import { API_KEY, APP_NAME, DEFAULT_P2P_ACCESS_MODE, FORWARDED_PREVIEW_LENGTH, LOGGING_ENABLED,
|
||||
MEDIA_BREAKPOINT } from '../config.js';
|
||||
MEDIA_BREAKPOINT, WAKE_UP_TICK, WAKE_UP_TIMEOUT } from '../config.js';
|
||||
import { CALL_STATE_NONE, CALL_STATE_OUTGOING_INITATED,
|
||||
CALL_STATE_INCOMING_RECEIVED, CALL_STATE_IN_PROGRESS,
|
||||
CALL_HEAD_STARTED } from '../constants.js';
|
||||
@@ -298,6 +298,18 @@ class TinodeWeb extends React.Component {
|
||||
console.warn("Your browser does not support BroadcastChannel. Some features will not be available");
|
||||
}
|
||||
|
||||
// Detect if computer just woke up from sleep. Checks how much time has passed since the last tick.
|
||||
// If too much time has passed then the computer was asleep.
|
||||
this.lastWakeUpCheck = new Date().getTime();
|
||||
this.wakeUpTicker = setInterval(_ => {
|
||||
const now = new Date().getTime();
|
||||
if (now - this.lastWakeUpCheck > WAKE_UP_TIMEOUT) {
|
||||
// Trigger reconnect.
|
||||
this.handleOnlineOn();
|
||||
}
|
||||
this.lastWakeUpCheck = now;
|
||||
}, WAKE_UP_TICK);
|
||||
|
||||
// Window/tab visible or invisible for pausing timers.
|
||||
document.addEventListener('visibilitychange', this.handleVisibilityEvent);
|
||||
|
||||
@@ -339,9 +351,6 @@ class TinodeWeb extends React.Component {
|
||||
});
|
||||
}
|
||||
|
||||
this.readTimer = null;
|
||||
this.readTimerCallback = null;
|
||||
|
||||
// Parse the hash navigation params.
|
||||
const parsedNav = HashNavigation.parseUrlHash(window.location.hash);
|
||||
// Maybe navigate to home screen.
|
||||
@@ -364,6 +373,8 @@ class TinodeWeb extends React.Component {
|
||||
window.removeEventListener('online', this.handleOnlineOn);
|
||||
window.removeEventListener('offline', this.handleOnlineOff);
|
||||
document.removeEventListener('visibilitychange', this.handleVisibilityEvent);
|
||||
|
||||
clearInterval(this.wakeUpTicker);
|
||||
}
|
||||
|
||||
// Setup transport (usually websocket) and server address. This will terminate connection with the server.
|
||||
|
||||
@@ -159,6 +159,7 @@ class BaseChatMessage extends React.PureComponent {
|
||||
const sideClass = this.props.sequence + ' ' + (this.props.response ? 'left' : 'right');
|
||||
const bubbleClass = (this.props.sequence == 'single' || this.props.sequence == 'last') ? 'bubble tip' : 'bubble';
|
||||
const avatar = this.props.userAvatar || true;
|
||||
let textSizeClass = 'message-content';
|
||||
const fullDisplay = (this.props.isGroup && this.props.response &&
|
||||
(this.props.sequence == 'single' || this.props.sequence == 'last'));
|
||||
|
||||
@@ -185,7 +186,14 @@ class BaseChatMessage extends React.PureComponent {
|
||||
}, this);
|
||||
const tree = Drafty.format(content, fullFormatter, this.formatterContext);
|
||||
content = React.createElement(React.Fragment, null, tree);
|
||||
} else if (typeof content != 'string') {
|
||||
} else if (typeof content == 'string') {
|
||||
// Make font bigger for emoji-only messages.
|
||||
// Must use 'new RegExp(...)' because React does not like /{1-5}/.
|
||||
if (new RegExp('^\\p{RGI_Emoji}{1,5}$', 'v').test(content || '')) {
|
||||
// Content consists of 1-5 emoji characters. Count how many and use it to increase the font size.
|
||||
textSizeClass += ' emoji-' + (content || '').match(/\p{RGI_Emoji}/vg).length;
|
||||
}
|
||||
} else {
|
||||
content = <><i className="material-icons gray">warning_amber</i> <i className="gray">
|
||||
<FormattedMessage id="invalid_content"
|
||||
defaultMessage="invalid content" description="Shown when the message is unreadable" /></i></>
|
||||
@@ -207,7 +215,7 @@ class BaseChatMessage extends React.PureComponent {
|
||||
<div>
|
||||
<div className={bubbleClass}>
|
||||
<div className="content-meta">
|
||||
<div className="message-content">
|
||||
<div className={textSizeClass}>
|
||||
{content}
|
||||
{attachments}
|
||||
</div>
|
||||
|
||||
@@ -5,6 +5,7 @@ import SendMessage from './send-message.jsx';
|
||||
import { REM_SIZE } from '../config.js';
|
||||
import { fitImageSize } from '../lib/blob-helpers.js';
|
||||
import { bytesToHumanSize, shortenFileName } from '../lib/strformat.js';
|
||||
import { urlAsAttachment } from '../lib/utils.js';
|
||||
|
||||
export default class ImagePreview extends React.PureComponent {
|
||||
constructor(props) {
|
||||
@@ -71,13 +72,16 @@ export default class ImagePreview extends React.PureComponent {
|
||||
|
||||
const width = this.props.content.width || '-';
|
||||
const height = this.props.content.height || '-';
|
||||
|
||||
const download_url = urlAsAttachment(this.props.content.url);
|
||||
|
||||
return (
|
||||
<div id="image-preview">
|
||||
<div id="image-preview-caption-panel">
|
||||
{this.props.onSendMessage ?
|
||||
<span>{fname}</span>
|
||||
:
|
||||
<a href={this.props.content.url} download={this.props.content.filename}>
|
||||
<a href={download_url} download={this.props.content.filename}>
|
||||
<i className="material-icons">file_download</i> <FormattedMessage
|
||||
id="download_action" defaultMessage="download" description="Call to action [download]" />
|
||||
</a>
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import QRCode from 'qrcodejs';
|
||||
import React from 'react';
|
||||
import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
|
||||
|
||||
import { QRCODE_SIZE } from '../config';
|
||||
|
||||
const messages = defineMessages({
|
||||
invalid_id: {
|
||||
id: 'error_invalid_id',
|
||||
@@ -13,6 +16,8 @@ class NewTopicById extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.qrCodeRef = React.createRef();
|
||||
|
||||
this.state = {
|
||||
groupId: '',
|
||||
};
|
||||
@@ -22,6 +27,14 @@ class NewTopicById extends React.PureComponent {
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
new QRCode(this.qrCodeRef.current, {
|
||||
text: this.props.myURI,
|
||||
width: QRCODE_SIZE,
|
||||
height: QRCODE_SIZE,
|
||||
});
|
||||
}
|
||||
|
||||
handleChange(e) {
|
||||
this.setState({groupId: e.target.value});
|
||||
}
|
||||
@@ -36,7 +49,7 @@ class NewTopicById extends React.PureComponent {
|
||||
e.preventDefault();
|
||||
if (this.state.groupId) {
|
||||
const name = this.state.groupId.trim();
|
||||
const prefix = name.substr(0, 3);
|
||||
const prefix = name.substring(0, 3);
|
||||
if (name.length > 3 && ['usr', 'grp', 'chn'].includes(prefix)) {
|
||||
this.props.onSubmit(name);
|
||||
} else {
|
||||
@@ -53,7 +66,7 @@ class NewTopicById extends React.PureComponent {
|
||||
description="Prompt for entering user or group ID">{
|
||||
(prompt) => <input type="text" placeholder={prompt}
|
||||
value={this.state.groupId} onChange={this.handleChange}
|
||||
onKeyPress={this.handleKeyPress} required autoFocus />
|
||||
onKeyDown={this.handleKeyPress} required autoFocus />
|
||||
}</FormattedMessage>
|
||||
</div>
|
||||
<div className="dialog-buttons">
|
||||
@@ -62,6 +75,11 @@ class NewTopicById extends React.PureComponent {
|
||||
description="Button [Subscribe]" />
|
||||
</button>
|
||||
</div>
|
||||
<br />
|
||||
<div className="panel-form-column">
|
||||
<label className="small">Scan my ID:</label>
|
||||
<div className="qr-code" ref={this.qrCodeRef} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import QRCode from 'qrcodejs';
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import { QRCODE_SIZE } from '../config';
|
||||
|
||||
export default class ShowQRCode extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.qrCodeRef = React.createRef();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
new QRCode(this.qrCodeRef.current, {
|
||||
text: this.props.uri,
|
||||
width: QRCODE_SIZE,
|
||||
height: QRCODE_SIZE,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="panel-form-column">
|
||||
<br />
|
||||
<div className="qr-code" ref={this.qrCodeRef} />
|
||||
<div className="dialog-buttons">
|
||||
<button className="outline" onClick={this.props.onCancel}>
|
||||
<FormattedMessage id="button_ok" defaultMessage="OK" description="Button [OK]" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"316.prod.js","mappings":"8KASA,MAAMA,UAA6BC,IAAAA,cACjCC,WAAAA,CAAYC,GACVC,MAAMD,GAENE,KAAKC,UAAY,GACjB,MAAM,kBAAEC,GAAsBJ,EAAMK,KACpCC,EAAeC,SAAQC,IACPA,EAAGC,KAAKC,MAAM,KACtBH,SAAQI,IACZT,KAAKC,UAAUS,KAAK,CAClBH,KAAME,EAAKE,OACXC,KAAMN,EAAGM,KACTC,MAAMC,EAAAA,EAAAA,IAAUR,EAAGM,MACnBG,KAAMb,EAAkBI,EAAGM,KAAM,CAACI,KAAM,YACxC,GACF,IAGJhB,KAAKC,UAAUgB,MAAK,CAACC,EAAGC,IAAMD,EAAEH,KAAKK,cAAcD,EAAEJ,OACvD,CAEAM,iBAAAA,GACMrB,KAAKsB,aACPtB,KAAKsB,YAAYC,eAAe,CAACC,MAAO,SAAUC,OAAQ,WAE9D,CAEAC,MAAAA,GACE,MAAMzB,EAAY,GACZ0B,EAAW3B,KAAKF,MAAM6B,UAAY,KAYxC,OAXA3B,KAAKC,UAAUI,SAAQ,CAACuB,EAAGC,KACzB,MAAMC,EAASF,EAAEhB,MAAQe,EAAW,YAAc,GAClD1B,EAAUS,KAAKd,IAAAA,cAAA,MAAImC,UAAWD,EAAOE,IAAKH,EACxCI,IAAKA,IAAYL,EAAEhB,MAAQe,IAAW3B,KAAKsB,YAAcW,EAAG,EAC5DC,QAASC,GAAKnC,KAAKF,MAAMsC,SAASR,EAAEhB,KAAMgB,EAAErB,OAC5CX,IAAAA,cAAA,QAAMmC,UAAU,gBAAgBH,EAAEf,MAClCjB,IAAAA,cAAA,QAAMmC,UAAU,WAAU,IAAOH,EAAEb,MACnCnB,IAAAA,cAAA,QAAMmC,UAAU,aAAY,KAAQH,EAAErB,OAClC,IAINX,IAAAA,cAAA,OAAKmC,UAAU,mBAAmBD,MAAO,CAACO,OAAQ,UAChDzC,IAAAA,cAAA,MAAImC,UAAU,0BACX9B,GAIT,EAGF,WAAeqC,EAAAA,EAAAA,YAAW3C,E","sources":["webpack://tinode-webapp/./src/widgets/phone-country-selector.jsx"],"sourcesContent":["// Selector for country dialing code.\n\nimport React from 'react';\nimport { injectIntl } from 'react-intl';\n\nimport * as dcodes from '../dcodes.json';\n\nimport { flagEmoji } from '../lib/strformat';\n\nclass PhoneCountrySelector extends React.PureComponent {\n constructor(props) {\n super(props);\n\n this.countries = [];\n const { formatDisplayName } = props.intl;\n dcodes.default.forEach(dc => {\n const parts = dc.dial.split(',');\n parts.forEach(part => {\n this.countries.push({\n dial: part.trim(),\n code: dc.code,\n flag: flagEmoji(dc.code),\n name: formatDisplayName(dc.code, {type: 'region'})\n });\n });\n });\n\n this.countries.sort((a, b) => a.name.localeCompare(b.name));\n }\n\n componentDidMount() {\n if (this.selectedRef) {\n this.selectedRef.scrollIntoView({block: 'center', inline: 'nearest'});\n }\n }\n\n render() {\n const countries = [];\n const selected = this.props.selected || 'US';\n this.countries.forEach((c, idx) => {\n const style = (c.code == selected ? 'selected ' : '');\n countries.push(<li className={style} key={idx}\n ref={ref => {if (c.code == selected) {this.selectedRef = ref}}}\n onClick={_ => this.props.onSubmit(c.code, c.dial)}>\n <span className=\"country-flag\">{c.flag}</span>\n <span className=\"country\"> {c.name}</span>\n <span className=\"dial-code\"> +{c.dial}</span>\n </li>);\n });\n\n return (\n <div className=\"scrollable-panel\" style={{height: '30rem'}}>\n <ul className=\"phone-country-selector\">\n {countries}\n </ul>\n </div>\n );\n }\n}\n\nexport default injectIntl(PhoneCountrySelector);\n"],"names":["PhoneCountrySelector","React","constructor","props","super","this","countries","formatDisplayName","intl","dcodes","forEach","dc","dial","split","part","push","trim","code","flag","flagEmoji","name","type","sort","a","b","localeCompare","componentDidMount","selectedRef","scrollIntoView","block","inline","render","selected","c","idx","style","className","key","ref","onClick","_","onSubmit","height","injectIntl"],"sourceRoot":""}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"884.prod.js","mappings":"4MASA,MAAMA,UAA6BC,IAAAA,cACjCC,WAAAA,CAAYC,GACVC,MAAMD,GAENE,KAAKC,UAAY,GACjB,MAAM,kBAAEC,GAAsBJ,EAAMK,KACpCC,EAAeC,SAAQC,IACPA,EAAGC,KAAKC,MAAM,KACtBH,SAAQI,IACZT,KAAKC,UAAUS,KAAK,CAClBH,KAAME,EAAKE,OACXC,KAAMN,EAAGM,KACTC,MAAMC,EAAAA,EAAAA,IAAUR,EAAGM,MACnBG,KAAMb,EAAkBI,EAAGM,KAAM,CAACI,KAAM,YACxC,GACF,IAGJhB,KAAKC,UAAUgB,MAAK,CAACC,EAAGC,IAAMD,EAAEH,KAAKK,cAAcD,EAAEJ,OACvD,CAEAM,iBAAAA,GACMrB,KAAKsB,aACPtB,KAAKsB,YAAYC,eAAe,CAACC,MAAO,SAAUC,OAAQ,WAE9D,CAEAC,MAAAA,GACE,MAAMzB,EAAY,GACZ0B,EAAW3B,KAAKF,MAAM6B,UAAY,KAYxC,OAXA3B,KAAKC,UAAUI,SAAQ,CAACuB,EAAGC,KACzB,MAAMC,EAASF,EAAEhB,MAAQe,EAAW,YAAc,GAClD1B,EAAUS,KAAKd,IAAAA,cAAA,MAAImC,UAAWD,EAAOE,IAAKH,EACxCI,IAAKA,IAAYL,EAAEhB,MAAQe,IAAW3B,KAAKsB,YAAcW,EAAG,EAC5DC,QAASC,GAAKnC,KAAKF,MAAMsC,SAASR,EAAEhB,KAAMgB,EAAErB,OAC5CX,IAAAA,cAAA,QAAMmC,UAAU,gBAAgBH,EAAEf,MAClCjB,IAAAA,cAAA,QAAMmC,UAAU,WAAU,IAAOH,EAAEb,MACnCnB,IAAAA,cAAA,QAAMmC,UAAU,aAAY,KAAQH,EAAErB,OAClC,IAINX,IAAAA,cAAA,OAAKmC,UAAU,mBAAmBD,MAAO,CAACO,OAAQ,UAChDzC,IAAAA,cAAA,MAAImC,UAAU,0BACX9B,GAIT,EAGF,SAAeqC,EAAAA,EAAAA,YAAW3C,E","sources":["webpack://tinode-webapp/./src/widgets/phone-country-selector.jsx"],"sourcesContent":["// Selector for country dialing code.\n\nimport React from 'react';\nimport { injectIntl } from 'react-intl';\n\nimport * as dcodes from '../dcodes.json';\n\nimport { flagEmoji } from '../lib/strformat';\n\nclass PhoneCountrySelector extends React.PureComponent {\n constructor(props) {\n super(props);\n\n this.countries = [];\n const { formatDisplayName } = props.intl;\n dcodes.default.forEach(dc => {\n const parts = dc.dial.split(',');\n parts.forEach(part => {\n this.countries.push({\n dial: part.trim(),\n code: dc.code,\n flag: flagEmoji(dc.code),\n name: formatDisplayName(dc.code, {type: 'region'})\n });\n });\n });\n\n this.countries.sort((a, b) => a.name.localeCompare(b.name));\n }\n\n componentDidMount() {\n if (this.selectedRef) {\n this.selectedRef.scrollIntoView({block: 'center', inline: 'nearest'});\n }\n }\n\n render() {\n const countries = [];\n const selected = this.props.selected || 'US';\n this.countries.forEach((c, idx) => {\n const style = (c.code == selected ? 'selected ' : '');\n countries.push(<li className={style} key={idx}\n ref={ref => {if (c.code == selected) {this.selectedRef = ref}}}\n onClick={_ => this.props.onSubmit(c.code, c.dial)}>\n <span className=\"country-flag\">{c.flag}</span>\n <span className=\"country\"> {c.name}</span>\n <span className=\"dial-code\"> +{c.dial}</span>\n </li>);\n });\n\n return (\n <div className=\"scrollable-panel\" style={{height: '30rem'}}>\n <ul className=\"phone-country-selector\">\n {countries}\n </ul>\n </div>\n );\n }\n}\n\nexport default injectIntl(PhoneCountrySelector);\n"],"names":["PhoneCountrySelector","React","constructor","props","super","this","countries","formatDisplayName","intl","dcodes","forEach","dc","dial","split","part","push","trim","code","flag","flagEmoji","name","type","sort","a","b","localeCompare","componentDidMount","selectedRef","scrollIntoView","block","inline","render","selected","c","idx","style","className","key","ref","onClick","_","onSubmit","height","injectIntl"],"sourceRoot":""}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1009
-644
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user