Compare commits

...
38 Commits
Author SHA1 Message Date
or-else 02e8bd2f74 merge devel 2025-02-08 19:50:34 +03:00
or-else bc8f20977e changed browserslist config, improved browser sleep detection 2025-02-07 20:59:45 +03:00
or-else 12a97cacc8 rebuild for 0.23.0 release 2025-01-16 18:11:49 +03:00
or-else 4052bb72b6 dependencies updated, version bumped 2025-01-16 17:36:36 +03:00
or-else 458b519845 reconnect when PC wakes up from sleep 2024-12-14 20:44:44 +03:00
or-else 07a22564c2 add thai and ukrainian language codes to package.json 2024-05-30 16:12:14 -07:00
or-else 31a95676b7 version bumped to 23.0-rc1, dependencies updated 2024-05-30 16:09:28 -07:00
or-else ba81261ee8 rebuilt with correct sdk 2024-05-18 18:07:14 -07:00
or-else 944cf32bb9 Merge branch 'master' into devel 2024-05-08 15:23:42 -07:00
or-else ed34a46865 make emoji-only messages larger for 1-5 emojis 2024-05-08 13:39:10 -07:00
or-else a4befc4cd3 make font larger for emoji-only messages 2024-05-01 14:36:18 -07:00
or-else 7b15cf1f19 Merge branch 'devel' 2024-04-29 15:53:05 -07:00
or-else df03509029 bump version to 22.13, dependencies updated 2024-04-29 15:17:46 -07:00
or-else 354ce3b8a3 bug fix: do not alter data or blob URLs with [asatt] 2024-02-10 11:33:36 -08:00
or-else 66192a8d53 google decided that assentlins have to be updated 2024-02-08 13:41:11 -08:00
or-else 7f2a89d86c version bumped to 22.12, dependencies updated 2024-02-05 18:51:02 -08:00
or-else 9eaa172030 add QR code display 2024-02-02 16:26:49 -08:00
or-else f932b973f8 ensure asatt=1 is aded to image download URL to set Content-Disposition: attachment 2023-12-21 16:06:19 -08:00
or-else 40916e2fc3 repo url fixed 2023-12-15 15:32:04 -08:00
or-else 5faedc35f3 bump version, update dependencies, and rebuild 2023-12-15 14:27:32 -08:00
or-else de32da49f3 make sure only safe schemes are clickable in URLs 2023-12-15 14:27:05 -08:00
or-else f5714b3b69 fix a security issue with absolute URL appearing as relative du to backslashes 2023-12-15 14:25:44 -08:00
or-else 1155888ced dependencies updated 2023-10-23 17:30:17 -07:00
or-else d128cec90d add Thai and (missing) Ukrainian loaders, mention Thai translation 2023-09-25 14:13:17 -07:00
Gene 3a8ee3e2dd Merge pull request #135 from Bukharney/master
Added Thai translation
2023-09-25 14:05:31 -07:00
Jirapat Lakma 1164d3a5bd added Thai translation 2023-09-25 15:34:10 +07:00
Jirapat Lakma fb2a39280a add Thai translation 2023-09-25 11:25:59 +07:00
or-else a1faba6b0c bump version to 0.22.10 2023-08-16 09:30:40 +03:00
or-else ca24b82f54 mention ukrainian translation 2023-08-14 16:41:12 +03:00
or-else 395edb3e4c rebuilt with ukrainian translations 2023-08-14 16:37:20 +03:00
or-else 691879b2b2 ukrainian translations 2023-08-14 16:36:01 +03:00
or-else 8187f09d26 rebuilt with the fix for #131 2023-08-14 11:15:09 +03:00
or-else e82951722d Merge branch 'devel' of https://github.com/tinode/webapp into devel 2023-08-13 09:59:27 +03:00
or-else 1042b9c39a rebuilt with fix for #129, dependencies updated 2023-08-13 09:59:20 +03:00
Gene 3177b667b8 Merge pull request #130 from tandr3w/master
Fix sending of "read" notifications after backing out of a group on mobile
2023-08-12 09:16:32 +03:00
Andrew 5fb85ce822 Fix sending of "read" notifications after backing out of a group on mobile.
https://groups.google.com/g/tinode/c/DMmeN79dZe0
2023-08-11 15:46:46 -04:00
or-else 500d72702a update dependencies, bump version 2023-07-08 08:44:12 -07:00
or-else df4b9c312a fix height of title panel in image preview 2023-06-27 19:06:29 -07:00
133 changed files with 9381 additions and 9444 deletions
+3 -2
View File
@@ -4,7 +4,7 @@
"namespace": "android_app", "namespace": "android_app",
"package_name": "co.tinode.tindroidx", "package_name": "co.tinode.tindroidx",
"sha256_cert_fingerprints": "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": "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"] ["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"]
} }
}] }
]
+6 -4
View File
@@ -37,18 +37,20 @@ This is _NOT_ a standalone app, this is just a frontend, a client. It requires a
## Internationalization ## 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: The app is currently available in the following languages:
* English * English - default
* Chinese (simplified) * Chinese simplified
* Chinese (traditional) * Chinese traditional
* French * French
* German * German
* Korean * Korean
* Romanian * Romanian
* Russian * Russian
* Spanish * Spanish
* Thai
* Ukrainian
More translations are welcome. See [instructions](https://github.com/tinode/chat/blob/devel/docs/translations.md#webapp). More translations are welcome. See [instructions](https://github.com/tinode/chat/blob/devel/docs/translations.md#webapp).
+31 -4
View File
@@ -1212,7 +1212,7 @@ img.channel {
flex-grow: 0; flex-grow: 0;
padding: 0.5rem 0.75rem; padding: 0.5rem 0.75rem;
line-height: 1.5; line-height: 1.5;
overflow-x: hidden; overflow-x: clip;
} }
/* Make sure padding is only applied at the top level panel */ /* Make sure padding is only applied at the top level panel */
@@ -1420,6 +1420,11 @@ img.channel {
vertical-align: top; vertical-align: top;
} }
/* Div holding QR code */
.qr-code {
align-self: center;
}
/* Scrollable panel where the messages are shown */ /* Scrollable panel where the messages are shown */
#messages-panel { #messages-panel {
position: relative; position: relative;
@@ -1589,8 +1594,10 @@ img.channel {
justify-content: space-between; justify-content: space-between;
background-color: #eceff1; background-color: #eceff1;
box-shadow: 0 1px 2px silver; /* Faint shadow under the panel */ box-shadow: 0 1px 2px silver; /* Faint shadow under the panel */
line-height: 2.5rem; line-height: 2rem;
height: 4.6rem; flex-shrink: 0;
flex-grow: 0;
height: 4rem;
padding: 1rem 0.75rem 0 0.75rem; padding: 1rem 0.75rem 0 0.75rem;
z-index: 3; z-index: 3;
} }
@@ -1620,6 +1627,8 @@ img.channel {
line-height: 1.5rem; line-height: 1.5rem;
background-color: #eceff1; background-color: #eceff1;
box-shadow: 0px -2px 3px -1px rgba(0,0,0,0.1); /* Faint shadow above panel */ 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; z-index: 1;
} }
@@ -2016,6 +2025,24 @@ video {
padding: 0 0.75rem 0 0.75rem; 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 /* The timestamp is a floating element. Need to ensure div.message-content is expanded
* to accomodate it. It's usually called clearfix. */ * to accomodate it. It's usually called clearfix. */
.bubble div.message-content:after { .bubble div.message-content:after {
@@ -3314,7 +3341,7 @@ ul.phone-country-selector span.country {
width: 100%; width: 100%;
} }
/* Bottom sheet rathar than a dialog */ /* Bottom sheet rather than a dialog */
.forward-dialog { .forward-dialog {
border-radius: 0.3rem 0.3rem 0 0; border-radius: 0.3rem 0.3rem 0 0;
margin: auto 0 0 0; margin: auto 0 0 0;
+1 -1
View File
File diff suppressed because one or more lines are too long
+4
View File
@@ -29,6 +29,8 @@
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons" /> <link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons" />
<!-- Styles for local elements --> <!-- Styles for local elements -->
<link rel="stylesheet" href="css/base.css" /> <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 --> <!-- 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@18/umd/react.development.js"></script>
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/react-dom@18/umd/react-dom.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> <script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/react-intl@6/react-intl.iife.js"></script>
<!-- LiveKit for group video calls --> <!-- LiveKit for group video calls -->
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/livekit-client@1/dist/livekit-client.umd.js"></script> <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> </head>
<body> <body>
+3 -1
View File
@@ -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/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="https://fonts.googleapis.com/icon?family=Material+Icons" />
<link rel="stylesheet" href="css/base.min.css" /> <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@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-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/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> </head>
<body> <body>
<noscript><div id="noscript-message"><div id="noscript-title">Javascript support is required.</div> <noscript><div id="noscript-message"><div id="noscript-title">Javascript support is required.</div>
+2937 -7165
View File
File diff suppressed because it is too large Load Diff
+20 -18
View File
@@ -4,7 +4,7 @@
"version": "0.23.0-rc2", "version": "0.23.0-rc2",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://github.com/tinode/webapp.git" "url": "git+https://github.com/tinode/webapp.git"
}, },
"scripts": { "scripts": {
"start": "echo 'Read installation instructions'", "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]'", "extract": "formatjs extract \"src/**/*.{js,jsx}\" --out-file src/i18n/ex/base-en.json --id-interpolation-pattern '[sha512:contenthash:base64:6]'",
"compile": "formatjs compile" "compile": "formatjs compile"
}, },
"browserslist": "> 0.5%, not IE 11, not op_mini all, not and_uc >0", "browserslist": "> 0.5%, last 2 versions, Firefox ESR, not dead",
"files": [ "files": [
"umd/*.js", "umd/*.js",
"umd/*.map", "umd/*.map",
@@ -34,7 +34,6 @@
"index.html", "index.html",
"manifest.json", "manifest.json",
"service-worker.js", "service-worker.js",
"firebase-init.js",
"version.js" "version.js"
], ],
"keywords": [ "keywords": [
@@ -53,6 +52,8 @@
"ko", "ko",
"ro", "ro",
"ru", "ru",
"th",
"uk",
"zh", "zh",
"zh-TW" "zh-TW"
], ],
@@ -65,29 +66,30 @@
"author": "Tinode Authors <info@tinode.co>", "author": "Tinode Authors <info@tinode.co>",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"firebase": "^9.17.2", "firebase": "^10.1.0",
"libphonenumber-js": "^1.10.24", "libphonenumber-js": "^1.10.40",
"qrcodejs": "^1.0.0",
"livekit-client": "^1.10.0", "livekit-client": "^1.10.0",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-intl": "^6.3.0", "react-intl": "^6.4.4",
"tinode-sdk": "^0.23.0-rc1", "tinode-sdk": "^0.23.0",
"webm-duration-fix": "^1.0.4" "webm-duration-fix": "^1.0.4"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.21.3", "@babel/core": "^7.22.10",
"@babel/preset-env": "^7.20.2", "@babel/preset-env": "^7.22.10",
"@babel/preset-react": "^7.18.6", "@babel/preset-react": "^7.22.5",
"@formatjs/cli": "^6.0.4", "@formatjs/cli": "^6.1.3",
"babel-loader": "^9.1.2", "babel-loader": "^9.1.3",
"babel-plugin-formatjs": "^10.4.0", "babel-plugin-formatjs": "^10.5.3",
"browserslist": "^4.21.5", "browserslist": "^4.21.10",
"copy-webpack-plugin": "^11.0.0", "copy-webpack-plugin": "^11.0.0",
"cssnano": "^6.0.1", "cssnano": "^6.0.1",
"glob": "^10.2.7", "glob": "^10.3.3",
"postcss": "^8.4.21", "postcss": "^8.4.27",
"postcss-cli": "^10.1.0", "postcss-cli": "^10.1.0",
"webpack": "^5.76.1", "webpack": "^5.88.2",
"webpack-cli": "^5.0.1" "webpack-cli": "^5.1.4"
} }
} }
+8
View File
@@ -38,6 +38,14 @@ const i18n = {
'new_message': "Новое сообщение", 'new_message': "Новое сообщение",
'new_chat': "Новый чат", 'new_chat': "Новый чат",
}, },
'th': {
'new_message': "ข้อความใหม่",
'new_chat': "แชทใหม่",
},
'uk': {
'new_message': "Нове повідомлення",
'new_chat': "Новий чат",
},
'zh': { 'zh': {
'new_message': "新讯息", 'new_message': "新讯息",
'new_chat': "新聊天", 'new_chat': "新聊天",
+13
View File
@@ -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 // happen until the first message. Setting this option to true enables
// immediate subscription. // immediate subscription.
export const IMMEDIATE_P2P_SUBSCRIPTION = false; 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;
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+7
View File
@@ -1668,5 +1668,12 @@
"description": "Call to action to join a conference call", "description": "Call to action to join a conference call",
"missing": false, "missing": false,
"obsolete": false "obsolete": false
},
"scan_qr_code": {
"translation": "QR-Code scannen",
"defaultMessage": "Scan QR Code",
"description": "Title for scanning QR code",
"missing": false,
"obsolete": false
} }
} }
+7
View File
@@ -1668,5 +1668,12 @@
"description": "Call to action to join a conference call", "description": "Call to action to join a conference call",
"missing": false, "missing": false,
"obsolete": false "obsolete": false
},
"scan_qr_code": {
"translation": "Scan QR Code",
"defaultMessage": "Scan QR Code",
"description": "Title for scanning QR code",
"missing": false,
"obsolete": false
} }
} }
+7
View File
@@ -1668,5 +1668,12 @@
"description": "Call to action to join a conference call", "description": "Call to action to join a conference call",
"missing": false, "missing": false,
"obsolete": 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
} }
} }
+4
View File
@@ -808,6 +808,10 @@
"defaultMessage": "save", "defaultMessage": "save",
"description": "Call to save an attachment" "description": "Call to save an attachment"
}, },
"scan_qr_code": {
"defaultMessage": "Scan QR Code",
"description": "Title for scanning QR code"
},
"search_for_contacts": { "search_for_contacts": {
"defaultMessage": "Use search to find contacts", "defaultMessage": "Use search to find contacts",
"description": "Text shown in contacts view when user entered no search query." "description": "Text shown in contacts view when user entered no search query."
+7
View File
@@ -1668,5 +1668,12 @@
"description": "Call to action to join a conference call", "description": "Call to action to join a conference call",
"missing": false, "missing": false,
"obsolete": 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
} }
} }
+7
View File
@@ -1668,5 +1668,12 @@
"description": "Call to action to join a conference call", "description": "Call to action to join a conference call",
"missing": false, "missing": false,
"obsolete": false "obsolete": false
},
"scan_qr_code": {
"translation": "QR 코드 스캔",
"defaultMessage": "Scan QR Code",
"description": "Title for scanning QR code",
"missing": false,
"obsolete": false
} }
} }
+7
View File
@@ -1668,5 +1668,12 @@
"description": "Call to action to join a conference call", "description": "Call to action to join a conference call",
"missing": false, "missing": false,
"obsolete": 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
} }
} }
+7
View File
@@ -1668,5 +1668,12 @@
"description": "Call to action to join a conference call", "description": "Call to action to join a conference call",
"missing": false, "missing": false,
"obsolete": false "obsolete": false
},
"scan_qr_code": {
"translation": "Отсканируйте QR код",
"defaultMessage": "Scan QR Code",
"description": "Title for scanning QR code",
"missing": false,
"obsolete": false
} }
} }
+1679
View File
File diff suppressed because it is too large Load Diff
+1679
View File
File diff suppressed because it is too large Load Diff
+7
View File
@@ -1668,5 +1668,12 @@
"description": "Call to action to join a conference call", "description": "Call to action to join a conference call",
"missing": false, "missing": false,
"obsolete": false "obsolete": false
},
"scan_qr_code": {
"translation": "掃描二維碼",
"defaultMessage": "Scan QR Code",
"description": "Title for scanning QR code",
"missing": false,
"obsolete": false
} }
} }
+7
View File
@@ -1668,5 +1668,12 @@
"description": "Call to action to join a conference call", "description": "Call to action to join a conference call",
"missing": false, "missing": false,
"obsolete": false "obsolete": false
},
"scan_qr_code": {
"translation": "扫描二维码",
"defaultMessage": "Scan QR Code",
"description": "Title for scanning QR code",
"missing": false,
"obsolete": false
} }
} }
+2
View File
@@ -31,6 +31,8 @@ const messageLoader = {
'ko': _ => import('./i18n.min/ko.json'), 'ko': _ => import('./i18n.min/ko.json'),
'ro': _ => import('./i18n.min/ro.json'), 'ro': _ => import('./i18n.min/ro.json'),
'ru': _ => import('./i18n.min/ru.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': _ => import('./i18n.min/zh.json'),
'zh-TW': _ => import('./i18n.min/zh-TW.json') 'zh-TW': _ => import('./i18n.min/zh-TW.json')
}; };
+9 -2
View File
@@ -10,10 +10,11 @@ import InlineVideo from '../widgets/inline-video.jsx';
import LazyImage from '../widgets/lazy-image.jsx' import LazyImage from '../widgets/lazy-image.jsx'
import UploadingImage from '../widgets/uploading-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 { base64ToBlob, blobToBase64, fitImageSize, imageScaled } from './blob-helpers.js';
import { idToColorClass, secondsToTime, shortenFileName } from './strformat.js'; import { idToColorClass, secondsToTime, shortenFileName } from './strformat.js';
import { cancelablePromise, sanitizeUrlForMime } from './utils.js'; import { cancelablePromise, sanitizeUrl, sanitizeUrlForMime } from './utils.js';
const messages = defineMessages({ const messages = defineMessages({
drafty_form: { 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. // Get text which will be sent back when the button is clicked.
attr['data-title'] = inner.join(''); attr['data-title'] = inner.join('');
break; 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': case 'MN':
// Mention // Mention
attr.className = 'mention' attr.className = 'mention'
+43 -2
View File
@@ -108,9 +108,12 @@ export function asEmail(val) {
// example.html - ok // example.html - ok
// https:example.com - not ok. // https:example.com - not ok.
// http:/example.com - not ok. // http:/example.com - not ok.
// //example.com or \\example.com - not ok.
// ' ↲ https://example.com' - not ok. (↲ means carriage return) // ' ↲ https://example.com' - not ok. (↲ means carriage return)
export function isUrlRelative(url) { 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 // 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. // 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 URLs are safe.
// Relative URL does not start with ':', abcd123: or '//'. // Relative URL does not start with ':', abcd123: or '//'.
@@ -166,6 +172,41 @@ export function sanitizeUrlForMime(url, mimeMajor) {
return null; 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. // Given message's received status, return name and color of a delivery indicator icon.
export function deliveryMarker(received) { export function deliveryMarker(received) {
switch (received) { switch (received) {
+34 -1
View File
@@ -12,6 +12,7 @@ import ErrorPanel from '../widgets/error-panel.jsx';
import GroupManager from '../widgets/group-manager.jsx'; import GroupManager from '../widgets/group-manager.jsx';
import MenuCancel from '../widgets/menu-cancel.jsx'; import MenuCancel from '../widgets/menu-cancel.jsx';
import PermissionsEditor from '../widgets/permissions-editor.jsx'; import PermissionsEditor from '../widgets/permissions-editor.jsx';
import ShowQRCode from '../widgets/show-qrcode.jsx';
import TopicCommonView from './topic-common-view.jsx'; import TopicCommonView from './topic-common-view.jsx';
import TopicSecurity from '../widgets/topic-security.jsx'; import TopicSecurity from '../widgets/topic-security.jsx';
@@ -76,6 +77,11 @@ const messages = defineMessages({
defaultMessage: 'Edit permissions', defaultMessage: 'Edit permissions',
description: 'Menu item [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 { class InfoView extends React.Component {
@@ -120,6 +126,8 @@ class InfoView extends React.Component {
this.handleUnarchive = this.handleUnarchive.bind(this); this.handleUnarchive = this.handleUnarchive.bind(this);
this.handlePermissionsChanged = this.handlePermissionsChanged.bind(this); this.handlePermissionsChanged = this.handlePermissionsChanged.bind(this);
this.handleLaunchPermissionsEditor = this.handleLaunchPermissionsEditor.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.handleShowAddMembers = this.handleShowAddMembers.bind(this);
this.handleMemberUpdateRequest = this.handleMemberUpdateRequest.bind(this); this.handleMemberUpdateRequest = this.handleMemberUpdateRequest.bind(this);
this.handleMemberSelected = this.handleMemberSelected.bind(this); this.handleMemberSelected = this.handleMemberSelected.bind(this);
@@ -368,6 +376,16 @@ class InfoView extends React.Component {
this.props.onNavigate(`perm/${which}`); 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) { handleShowAddMembers(e) {
e.preventDefault(); e.preventDefault();
this.props.onInitFind(); this.props.onInitFind();
@@ -514,6 +532,11 @@ class InfoView extends React.Component {
onLaunchPermissionsEditor={this.handleLaunchPermissionsEditor} onLaunchPermissionsEditor={this.handleLaunchPermissionsEditor}
onNavigate={this.props.onNavigate} /> 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 id="info-view-content" className="scrollable-panel">
<div className="panel-form-column"> <div className="panel-form-column">
<a href="#" className="flat-button float-right" onClick={(e) => {e.preventDefault(); this.props.onNavigate('general');}}> <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> </div>
: null : null
} }
<div className="group"> <div className="panel-form-row">
<div>
<label className="small"><FormattedMessage id="label_user_id" defaultMessage="ID:" <label className="small"><FormattedMessage id="label_user_id" defaultMessage="ID:"
description="Label for user address (ID)" /></label>&nbsp; description="Label for user address (ID)" /></label>&nbsp;
<tt>{this.state.address}</tt> <tt>{this.state.address}</tt>
</div> </div>
<div style={{marginLeft: 'auto'}}>
&nbsp;<a href="#" onClick={this.handleCopyID}>
<i className="material-icons">content_copy</i>
</a>&nbsp;
&nbsp;<a href="#" onClick={this.handleShowQRCode}>
<i className="material-icons">qr_code</i>
</a>&nbsp;
</div>
</div>
<div className="group"> <div className="group">
<BadgeList trustedBadges={this.state.trustedBadges} /> <BadgeList trustedBadges={this.state.trustedBadges} />
</div> </div>
+1 -1
View File
@@ -1685,7 +1685,7 @@ class MessagesView extends React.Component {
<> <>
<div id="topic-caption-panel" className="caption-panel"> <div id="topic-caption-panel" className="caption-panel">
{this.props.displayMobile ? {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> <i className="material-icons">arrow_back</i>
</a> </a>
: :
+1
View File
@@ -113,6 +113,7 @@ class NewTopicView extends React.Component {
onError={this.props.onError} /> : onError={this.props.onError} /> :
this.state.tabSelected === 'byid' ? this.state.tabSelected === 'byid' ?
<NewTopicById <NewTopicById
myURI={Tinode.URI_TOPIC_ID_PREFIX + this.props.tinode.getCurrentUserID()}
onSubmit={this.handleGroupByID} onSubmit={this.handleGroupByID}
onError={this.props.onError} /> : onError={this.props.onError} /> :
<div className="flex-column"> <div className="flex-column">
+15 -4
View File
@@ -19,7 +19,7 @@ import MessagesView from './messages-view.jsx';
import SidepanelView from './sidepanel-view.jsx'; import SidepanelView from './sidepanel-view.jsx';
import { API_KEY, APP_NAME, DEFAULT_P2P_ACCESS_MODE, FORWARDED_PREVIEW_LENGTH, LOGGING_ENABLED, 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, import { CALL_STATE_NONE, CALL_STATE_OUTGOING_INITATED,
CALL_STATE_INCOMING_RECEIVED, CALL_STATE_IN_PROGRESS, CALL_STATE_INCOMING_RECEIVED, CALL_STATE_IN_PROGRESS,
CALL_HEAD_STARTED } from '../constants.js'; 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"); 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. // Window/tab visible or invisible for pausing timers.
document.addEventListener('visibilitychange', this.handleVisibilityEvent); 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. // Parse the hash navigation params.
const parsedNav = HashNavigation.parseUrlHash(window.location.hash); const parsedNav = HashNavigation.parseUrlHash(window.location.hash);
// Maybe navigate to home screen. // Maybe navigate to home screen.
@@ -364,6 +373,8 @@ class TinodeWeb extends React.Component {
window.removeEventListener('online', this.handleOnlineOn); window.removeEventListener('online', this.handleOnlineOn);
window.removeEventListener('offline', this.handleOnlineOff); window.removeEventListener('offline', this.handleOnlineOff);
document.removeEventListener('visibilitychange', this.handleVisibilityEvent); document.removeEventListener('visibilitychange', this.handleVisibilityEvent);
clearInterval(this.wakeUpTicker);
} }
// Setup transport (usually websocket) and server address. This will terminate connection with the server. // Setup transport (usually websocket) and server address. This will terminate connection with the server.
+10 -2
View File
@@ -159,6 +159,7 @@ class BaseChatMessage extends React.PureComponent {
const sideClass = this.props.sequence + ' ' + (this.props.response ? 'left' : 'right'); const sideClass = this.props.sequence + ' ' + (this.props.response ? 'left' : 'right');
const bubbleClass = (this.props.sequence == 'single' || this.props.sequence == 'last') ? 'bubble tip' : 'bubble'; const bubbleClass = (this.props.sequence == 'single' || this.props.sequence == 'last') ? 'bubble tip' : 'bubble';
const avatar = this.props.userAvatar || true; const avatar = this.props.userAvatar || true;
let textSizeClass = 'message-content';
const fullDisplay = (this.props.isGroup && this.props.response && const fullDisplay = (this.props.isGroup && this.props.response &&
(this.props.sequence == 'single' || this.props.sequence == 'last')); (this.props.sequence == 'single' || this.props.sequence == 'last'));
@@ -185,7 +186,14 @@ class BaseChatMessage extends React.PureComponent {
}, this); }, this);
const tree = Drafty.format(content, fullFormatter, this.formatterContext); const tree = Drafty.format(content, fullFormatter, this.formatterContext);
content = React.createElement(React.Fragment, null, tree); 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"> content = <><i className="material-icons gray">warning_amber</i> <i className="gray">
<FormattedMessage id="invalid_content" <FormattedMessage id="invalid_content"
defaultMessage="invalid content" description="Shown when the message is unreadable" /></i></> defaultMessage="invalid content" description="Shown when the message is unreadable" /></i></>
@@ -207,7 +215,7 @@ class BaseChatMessage extends React.PureComponent {
<div> <div>
<div className={bubbleClass}> <div className={bubbleClass}>
<div className="content-meta"> <div className="content-meta">
<div className="message-content"> <div className={textSizeClass}>
{content} {content}
{attachments} {attachments}
</div> </div>
+5 -1
View File
@@ -5,6 +5,7 @@ import SendMessage from './send-message.jsx';
import { REM_SIZE } from '../config.js'; import { REM_SIZE } from '../config.js';
import { fitImageSize } from '../lib/blob-helpers.js'; import { fitImageSize } from '../lib/blob-helpers.js';
import { bytesToHumanSize, shortenFileName } from '../lib/strformat.js'; import { bytesToHumanSize, shortenFileName } from '../lib/strformat.js';
import { urlAsAttachment } from '../lib/utils.js';
export default class ImagePreview extends React.PureComponent { export default class ImagePreview extends React.PureComponent {
constructor(props) { constructor(props) {
@@ -71,13 +72,16 @@ export default class ImagePreview extends React.PureComponent {
const width = this.props.content.width || '-'; const width = this.props.content.width || '-';
const height = this.props.content.height || '-'; const height = this.props.content.height || '-';
const download_url = urlAsAttachment(this.props.content.url);
return ( return (
<div id="image-preview"> <div id="image-preview">
<div id="image-preview-caption-panel"> <div id="image-preview-caption-panel">
{this.props.onSendMessage ? {this.props.onSendMessage ?
<span>{fname}</span> <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 <i className="material-icons">file_download</i> <FormattedMessage
id="download_action" defaultMessage="download" description="Call to action [download]" /> id="download_action" defaultMessage="download" description="Call to action [download]" />
</a> </a>
+20 -2
View File
@@ -1,6 +1,9 @@
import QRCode from 'qrcodejs';
import React from 'react'; import React from 'react';
import { FormattedMessage, defineMessages, injectIntl } from 'react-intl'; import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
import { QRCODE_SIZE } from '../config';
const messages = defineMessages({ const messages = defineMessages({
invalid_id: { invalid_id: {
id: 'error_invalid_id', id: 'error_invalid_id',
@@ -13,6 +16,8 @@ class NewTopicById extends React.PureComponent {
constructor(props) { constructor(props) {
super(props); super(props);
this.qrCodeRef = React.createRef();
this.state = { this.state = {
groupId: '', groupId: '',
}; };
@@ -22,6 +27,14 @@ class NewTopicById extends React.PureComponent {
this.handleSubmit = this.handleSubmit.bind(this); this.handleSubmit = this.handleSubmit.bind(this);
} }
componentDidMount() {
new QRCode(this.qrCodeRef.current, {
text: this.props.myURI,
width: QRCODE_SIZE,
height: QRCODE_SIZE,
});
}
handleChange(e) { handleChange(e) {
this.setState({groupId: e.target.value}); this.setState({groupId: e.target.value});
} }
@@ -36,7 +49,7 @@ class NewTopicById extends React.PureComponent {
e.preventDefault(); e.preventDefault();
if (this.state.groupId) { if (this.state.groupId) {
const name = this.state.groupId.trim(); 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)) { if (name.length > 3 && ['usr', 'grp', 'chn'].includes(prefix)) {
this.props.onSubmit(name); this.props.onSubmit(name);
} else { } else {
@@ -53,7 +66,7 @@ class NewTopicById extends React.PureComponent {
description="Prompt for entering user or group ID">{ description="Prompt for entering user or group ID">{
(prompt) => <input type="text" placeholder={prompt} (prompt) => <input type="text" placeholder={prompt}
value={this.state.groupId} onChange={this.handleChange} value={this.state.groupId} onChange={this.handleChange}
onKeyPress={this.handleKeyPress} required autoFocus /> onKeyDown={this.handleKeyPress} required autoFocus />
}</FormattedMessage> }</FormattedMessage>
</div> </div>
<div className="dialog-buttons"> <div className="dialog-buttons">
@@ -62,6 +75,11 @@ class NewTopicById extends React.PureComponent {
description="Button [Subscribe]" /> description="Button [Subscribe]" />
</button> </button>
</div> </div>
<br />
<div className="panel-form-column">
<label className="small">Scan my ID:</label>
<div className="qr-code" ref={this.qrCodeRef} />
</div>
</div> </div>
); );
} }
+34
View File
@@ -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>
);
}
};
+2
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
View File
File diff suppressed because one or more lines are too long
+2
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
View File
File diff suppressed because one or more lines are too long
+1
View File
File diff suppressed because one or more lines are too long
+1
View File
File diff suppressed because one or more lines are too long
+1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1
View File
File diff suppressed because one or more lines are too long
+2
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2
View File
File diff suppressed because one or more lines are too long
+1
View File
@@ -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\">&nbsp;{c.name}</span>\n <span className=\"dial-code\">&nbsp;+{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":""}
+1
View File
File diff suppressed because one or more lines are too long
+2
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
View File
File diff suppressed because one or more lines are too long
+2
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
View File
File diff suppressed because one or more lines are too long
+1
View File
File diff suppressed because one or more lines are too long
+2
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
View File
File diff suppressed because one or more lines are too long
+1
View File
File diff suppressed because one or more lines are too long
+1
View File
File diff suppressed because one or more lines are too long
+1
View File
File diff suppressed because one or more lines are too long
+1
View File
File diff suppressed because one or more lines are too long
+1
View File
File diff suppressed because one or more lines are too long
+1
View File
File diff suppressed because one or more lines are too long
+1
View File
File diff suppressed because one or more lines are too long
+2
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
View File
File diff suppressed because one or more lines are too long
+2
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
View File
File diff suppressed because one or more lines are too long
+1
View File
File diff suppressed because one or more lines are too long
+1
View File
File diff suppressed because one or more lines are too long
+2
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2
View File
File diff suppressed because one or more lines are too long
+1
View File
@@ -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\">&nbsp;{c.name}</span>\n <span className=\"dial-code\">&nbsp;+{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":""}
+1
View File
File diff suppressed because one or more lines are too long
+1
View File
File diff suppressed because one or more lines are too long
+2
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1009 -644
View File
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