Compare commits

...
36 Commits
Author SHA1 Message Date
or-else ac779ef31b merge devel 2026-03-03 13:36:48 +03:00
or-else e2c32de5c2 update the sdk version 2026-03-03 13:32:29 +03:00
or-else 6bd9afe017 update translations 2026-03-02 14:39:27 +03:00
or-else 91799adb9a rebuilt, new translation modules added to git 2026-03-02 14:11:36 +03:00
or-else 3bfabc90f3 add missing vi translations, rearrange language imports in alpha order 2026-03-02 14:06:40 +03:00
Gene 6b21c1deaa Merge pull request #141 from Antoanyabo/i18n-vi
feat(translation): Add Vietnamese (vi) translation
2026-03-02 13:34:19 +03:00
amit d674deeca6 feat(translation): Add Vietnamese (vi) translation 2026-03-02 15:07:52 +07:00
or-else 573f99993a rebuilt with the sdk bug fix 2026-03-01 19:05:14 +03:00
or-else e6dfde28f6 restore topic subscription after a reconnect 2026-03-01 18:31:24 +03:00
or-else 389ef305ab cleanup 2026-03-01 14:03:18 +03:00
or-else 2aa1b731ee fix for #140, fix for some UI breakage, cosmetic changes 2026-03-01 13:59:59 +03:00
or-else 656aebdb5d rebuild with the correct version of the sdk 2026-02-13 15:43:24 +03:00
or-else e9d6c2e5bf fix #139: use dvh/dvw css units for the top viewport size 2026-02-13 15:38:45 +03:00
or-else e097f134b7 dependencies updated 2026-01-20 13:02:19 +03:00
or-else d296bebe1c bump version to 0.25.2 2026-01-20 13:00:39 +03:00
or-else 234fb48b93 cleanup 2026-01-20 12:58:58 +03:00
or-else a54ae70746 split out a few modules, handle esc key 2026-01-19 15:41:33 +03:00
or-else 5b7565c8f8 debug logging removed 2026-01-17 12:58:36 +03:00
or-else dea781b423 adding TheCard preview 2026-01-17 12:55:45 +03:00
or-else 930f4f0656 TheCard handling in messages 2026-01-16 20:51:57 +03:00
or-else 16489432cf fix wallpaper defaults 2026-01-16 11:51:42 +03:00
or-else 84534dfc63 dependencies updated, browserlist config updated, version changed to 0.25.1 2025-12-26 15:51:21 +03:00
or-else 8c3d273c34 missing translations added 2025-12-26 15:24:34 +03:00
or-else b2bae67ed2 version bumped, dependencies updated 2025-12-12 09:13:31 +03:00
or-else 825a6661f3 dependencies updated, version changed to 0.25.0-rc1 2025-11-26 12:02:48 +03:00
or-else c4fa4c6847 dependencies updated, rebuilt 2025-11-08 12:52:29 +03:00
or-else c17a314640 fix dark mode button color 2025-11-08 10:20:30 +03:00
or-else 7fa83d0d3e merge devel 2025-11-07 19:33:14 +03:00
or-else fb11d0f477 dependencies updated 2025-11-07 15:13:18 +03:00
or-else b11d1f6d39 dependencies updated, version changed to 25.0-beta3, rebuilt 2025-11-07 15:09:26 +03:00
or-else 896505e49d fix incoming call colors in dark mode 2025-11-06 18:39:05 +03:00
or-else ad3c6f5d36 fixed position of controls in incoming call 2025-11-06 13:15:57 +03:00
or-else b4ba9ab7d8 slightly stronger indicator of pinned chat 2025-10-28 12:36:05 +03:00
or-else 4cd3e84178 make subscriber count work with older server 2025-10-28 12:32:52 +03:00
or-else 15b41a9bd8 better screenshots 2025-10-27 19:32:33 +03:00
or-else 369612afa8 rebuild with the sdk fix 2025-10-08 17:36:11 +03:00
169 changed files with 16447 additions and 5323 deletions
+1
View File
@@ -53,6 +53,7 @@ The app is currently available in the following languages:
* Spanish
* Thai
* Ukrainian
* Vietnamese
More translations are welcome. See [instructions](https://github.com/tinode/chat/blob/devel/docs/translations.md#webapp).
+122 -10
View File
@@ -119,9 +119,9 @@
--clr-button-danger-bg: light-dark(#ec645f, #d74742);
--clr-button-danger-text: var(--clr-bright);
--clr-button-danger-hover-bg: light-dark(#f77, #a55);
--clr-button-positive-bg: light-dark(#71c46f, #0d1e0d);
--clr-button-positive-bg: light-dark(#71c46f, #3f983e);
--clr-button-positive-text: var(--clr-bright);
--clr-button-positive-hover-bg: light-dark(#7fed7f, #0a120a);
--clr-button-positive-hover-bg: light-dark(#7fed7f, #6ec26e);
/* Inputs */
--clr-input-text: var(--clr-text-primary);
@@ -130,7 +130,7 @@
--clr-input-shadow: var(--clr-shadow-input-underline);
--clr-input-shadow-focus: var(--clr-primary-accent-focus-shadow);
--clr-input-invalid-border: light-dark(#e57171, #561010);
--clr-input-invalid-shadow: light-dark(rgba(229, 115, 115, 0.25), rgba(26, 12, 12, 0.25));
--clr-input-invalid-shadow: light-dark(#e5737340, #1a0c0c40);
--clr-input-placeholder-text: var(--clr-text-placeholder);
/* Chat Bubbles */
@@ -299,6 +299,8 @@
html, body {
height: 100%;
width: 100%;
height: 100dvh;
width: 100dvw;
font-size: 10pt; /* 13.33 px/rem */
color: var(--clr-text-primary);
overflow: hidden;
@@ -790,6 +792,14 @@ div.trusted-badge {
height: 0.015rem;
}
/* Vertical divider line */
.divider {
display: inline-block;
width: 1px;
margin: 0 0.25rem;
border-left: var(--clr-border-muted) 1.5px groove;
}
.ellipsized {
overflow-x: hidden;
white-space: nowrap;
@@ -816,6 +826,15 @@ div.trusted-badge {
text-decoration: none;
}
.flat-button.disabled,
.flat-button.disabled:hover,
.flat-button.disabled:focus {
color: var(--clr-primary-accent);
cursor: default;
opacity: 0.75;
filter: grayscale(75%);
}
.flat-button.faux {
display: inline-block;
height: 1.4em;
@@ -852,11 +871,11 @@ div.trusted-badge {
.action-button:hover:not([disabled]),
.action-button:active:not([disabled]) {
color: var(--clr-text-tertiary-activated);
background-color: var(--clr-background-light-gray);
background-color: var(--clr-primary-action-hover);
}
.action-button:active {
color: var(--clr-text-tertiary-activated);
background-color: var(--clr-background-extra-light-gray);
background-color: var(--clr-primary-action);
}
.action-button.hidden {
@@ -1338,7 +1357,7 @@ i.search {
background-color: var(--clr-background-blue-gray-100); /* blue-gray 100 highlight */
}
.contact-box > li.tpinned {
border-right: 1px solid var(--clr-secondary-accent);
border-right: 1.5px solid var(--clr-secondary-accent);
background-color: var(--clr-background-blue-gray-75); /* blue-gray 75 highlight */
}
.contact-box > li.action {
@@ -1427,6 +1446,9 @@ i.search {
font-size: 2.4rem;
line-height: 3rem;
}
.contact-box .avatar-box > .checkmark.material-icons {
line-height: unset;
}
.contact-box.small .avatar-box > .material-icons {
font-size: 1.6rem;
line-height: 2rem;
@@ -1961,7 +1983,7 @@ img.channel {
}
#image-preview-caption-panel {
#preview-caption-panel {
display: flex;
justify-content: space-between;
background-color: var(--clr-background-blue-gray-50);
@@ -1971,15 +1993,16 @@ img.channel {
flex-grow: 0;
height: var(--caption-panel-height);
padding: 1rem 0.75rem 0 0.75rem;
font-size: 120%;
z-index: 3;
}
#image-preview-caption-panel a {
#preview-caption-panel a {
color: var(--clr-text-secondary);
text-decoration: none;
}
#image-preview-caption-panel a:hover {
#preview-caption-panel a:hover {
color: var(--clr-dark);
}
@@ -2016,6 +2039,17 @@ img.channel {
background-color: var(--clr-std-ltgray);
padding: 1rem;
border-radius: 0.5rem;
max-width: 30rem;
}
.doc-card .avatar-box {
width: 10rem;
height: 10rem;
font-size: 4.5rem;
}
.doc-card .avatar-box .material-icons {
line-height: 10rem;
}
/* Video-call related styles */
@@ -2267,6 +2301,8 @@ video {
.incoming-call {
position: relative;
display: flex;
flex-direction: column;
background-color: var(--clr-bright);
box-shadow: 0.15rem 0.15rem 1.5rem var(--clr-shadow-dialog);
border-radius: 0.3rem;
@@ -2294,7 +2330,10 @@ video {
border-radius: 0.35rem;
z-index: 1;
text-shadow: 0 0 1px var(--clr-bright);
background-color: rgba(255, 255, 255, 0.7); /* Specific rgba */
background-color: var(--clr-background-off-white);
text-overflow: ellipsis;
overflow-x: hidden;
white-space: nowrap;
}
.call-message {
@@ -3528,6 +3567,79 @@ ul.tabbar li.active > a {
position: relative;
right: 0;
}
/* END of pinned messages */
/* Contact attachments */
.contact-card {
position: relative;
background-color: var(--clr-background-off-white);
border-radius: 0.5rem;
display: flex;
flex-direction: column;
min-width: 14rem;
}
.contact-card .avatar-box {
position: relative;
width: 1.8rem;
height: 1.8rem;
font-size: 1rem;
margin-right: 0.75rem;
margin-bottom: 0;
}
.contact-card .contact-body {
align-items: center;
background-color: var(--clr-background-blue-gray-50);
display: flex;
padding: 0.75rem;
border-radius: 0.5rem 0.5rem 0 0;
border-bottom: none;
}
.contact-card .contact-body .name-box {
display: flex;
flex-direction: column;
overflow: hidden;
}
.contact-card .contact-body .name-box .name {
color: var(--clr-text-primary);
font-weight: 500;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.contact-card .contact-body .name-box .org {
color: var(--clr-text-secondary);
font-size: 90%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-weight: 300;
}
.contact-card .contact-actions {
display: flex;
justify-content: space-evenly;
align-items: center;
padding: 0 0.5rem;
border-radius: 0 0 0.5rem 0.5rem;
border-top: none;
background-color: var(--clr-bright);
}
.contact-card .contact-actions a {
cursor: pointer;
}
.contact-card .contact-actions .divider {
height: 1.8rem;
opacity: 0.5;
}
/* END of contact attachments */
/*
Drag-and-drop area.
+1 -1
View File
File diff suppressed because one or more lines are too long
+327 -514
View File
File diff suppressed because it is too large Load Diff
+10 -4
View File
@@ -1,7 +1,7 @@
{
"name": "tinode-webapp",
"description": "Tinode messenger for the web",
"version": "0.25.0-beta2",
"version": "0.25.2",
"repository": {
"type": "git",
"url": "git+https://github.com/tinode/webapp.git"
@@ -21,7 +21,9 @@
"compile": "formatjs compile",
"test": "jest"
},
"browserslist": "> 0.5%, last 2 versions, Firefox ESR, not dead",
"browserslist": [
"defaults"
],
"files": [
"umd/*.js",
"umd/*.map",
@@ -31,6 +33,9 @@
"img/*.png",
"img/*.svg",
"img/*.jpeg",
"img/bkg/*.jpg",
"img/bkg/*.png",
"img/bkg/*.json",
"index-dev.html",
"index.html",
"manifest.json",
@@ -57,6 +62,7 @@
"ru",
"th",
"uk",
"vi",
"zh",
"zh-TW"
],
@@ -75,7 +81,7 @@
"react": "^18.3.0",
"react-dom": "^18.3.0",
"react-intl": "^7.0.0",
"tinode-sdk": "^0.25.0-beta1",
"tinode-sdk": "^0.25.2",
"webm-duration-fix": "^1.0.4"
},
"devDependencies": {
@@ -88,7 +94,7 @@
"browserslist": "^4.21.10",
"copy-webpack-plugin": "^13.0.0",
"cssnano": "^7.0.6",
"glob": "^11.0.0",
"glob": "^13.0.0",
"jest": "^30.0.0",
"postcss": "^8.4.27",
"postcss-cli": "^11.0.0",
+1 -1
View File
@@ -8,7 +8,7 @@ function writeVersion(fileName, useExport) {
{ flag: 'w' },
(err) => {
if (err) {
return console.log(err);
return console.error(err);
}
}
);
+8 -4
View File
@@ -10,6 +10,10 @@ const webAppChannel = (typeof BroadcastChannel == 'function') ?
// Basic internationalization.
const i18n = {
'ar': {
'new_message': "رسالة جديدة",
'new_chat': "محادثة جديدة",
},
'de': {
'new_message': "Neue Nachricht",
'new_chat': "Neuer Chat",
@@ -50,6 +54,10 @@ const i18n = {
'new_message': "Нове повідомлення",
'new_chat': "Новий чат",
},
'vi': {
'new_message': "Tin nhắn mới",
'new_chat': "Cuộc trò chuyện mới",
},
'zh': {
'new_message': "新讯息",
'new_chat': "新聊天",
@@ -57,10 +65,6 @@ const i18n = {
'zh-TW': {
'new_message': "新訊息",
'new_chat': "新聊天",
},
'ar': {
'new_message': "رسالة جديدة",
'new_chat': "محادثة جديدة",
}
};
+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
+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
+1 -1
View File
File diff suppressed because one or more lines are too long
+66 -3
View File
@@ -259,8 +259,8 @@
"obsolete": false
},
"contacts_not_found": {
"translation": "ليس لديك محادثات<br />¯∖_(ツ)_/¯",
"defaultMessage": "You have no chats<br />¯∖_(ツ)_/¯",
"translation": "ليس لديك محادثات\n¯∖_(ツ)_/¯",
"defaultMessage": "You have no chats\\n¯∖_(ツ)_/¯",
"description": "HTML message shown in ContactList when no contacts are found",
"missing": false,
"obsolete": false
@@ -1225,7 +1225,7 @@
"label_description": {
"translation": "الوصف",
"defaultMessage": "Description",
"description": "Label for editing topic description",
"description": "Label for description of user or topic",
"missing": false,
"obsolete": false
},
@@ -1899,5 +1899,68 @@
"description": "Label adjusting blur amount",
"missing": false,
"obsolete": false
},
"label_scan_id": {
"translation": "مسح معرفي:",
"defaultMessage": "Scan my ID:",
"description": "Label [Scan my ID:]",
"missing": false,
"obsolete": false
},
"unknown_name": {
"translation": "غير معروف",
"defaultMessage": "Unknown",
"description": "Name to show when the name is missing",
"missing": false,
"obsolete": false
},
"save_action": {
"translation": "حفظ",
"defaultMessage": "Save",
"description": "Label for Save button",
"missing": false,
"obsolete": false
},
"cannot_parse_vcard": {
"translation": "تعذّر تحليل ملف vCard.",
"defaultMessage": "Cannot parse vCard file.",
"description": "Error message when vCard file cannot be parsed",
"missing": false,
"obsolete": false
},
"chat_now": {
"translation": "محادثة",
"defaultMessage": "Chat",
"description": "Label for message button in contact card",
"missing": false,
"obsolete": false
},
"find_user": {
"translation": "بحث",
"defaultMessage": "Find",
"description": "Label for search button in contact card",
"missing": false,
"obsolete": false
},
"label_contact_card": {
"translation": "بطاقة الاتصال",
"defaultMessage": "Contact Card",
"description": "Label for contact card preview",
"missing": false,
"obsolete": false
},
"label_contacts": {
"translation": "جهات الاتصال",
"defaultMessage": "Contacts",
"description": "Label for contact information",
"missing": false,
"obsolete": false
},
"add_members_prompt": {
"translation": "إضافة أعضاء",
"defaultMessage": "add members",
"description": "Placeholder in group members input field",
"missing": false,
"obsolete": false
}
}
+66 -3
View File
@@ -259,8 +259,8 @@
"obsolete": false
},
"contacts_not_found": {
"translation": "Sie haben keine Unterhaltungen<br />¯∖_(ツ)_/¯",
"defaultMessage": "You have no chats<br />¯∖_(ツ)_/¯",
"translation": "Sie haben keine Unterhaltungen\n¯∖_(ツ)_/¯",
"defaultMessage": "You have no chats\\n¯∖_(ツ)_/¯",
"description": "HTML message shown in ContactList when no contacts are found",
"missing": false,
"obsolete": false
@@ -1225,7 +1225,7 @@
"label_description": {
"translation": "Beschreibung",
"defaultMessage": "Description",
"description": "Label for editing topic description",
"description": "Label for description of user or topic",
"missing": false,
"obsolete": false
},
@@ -1899,5 +1899,68 @@
"description": "Label adjusting blur amount",
"missing": false,
"obsolete": false
},
"label_scan_id": {
"translation": "Meine ID scannen:",
"defaultMessage": "Scan my ID:",
"description": "Label [Scan my ID:]",
"missing": false,
"obsolete": false
},
"unknown_name": {
"translation": "Unbekannt",
"defaultMessage": "Unknown",
"description": "Name to show when the name is missing",
"missing": false,
"obsolete": false
},
"save_action": {
"translation": "Speichern",
"defaultMessage": "Save",
"description": "Label for Save button",
"missing": false,
"obsolete": false
},
"cannot_parse_vcard": {
"translation": "vCard-Datei konnte nicht verarbeitet werden.",
"defaultMessage": "Cannot parse vCard file.",
"description": "Error message when vCard file cannot be parsed",
"missing": false,
"obsolete": false
},
"chat_now": {
"translation": "Chatten",
"defaultMessage": "Chat",
"description": "Label for message button in contact card",
"missing": false,
"obsolete": false
},
"find_user": {
"translation": "Suchen",
"defaultMessage": "Find",
"description": "Label for search button in contact card",
"missing": false,
"obsolete": false
},
"label_contact_card": {
"translation": "Kontaktkarte",
"defaultMessage": "Contact Card",
"description": "Label for contact card preview",
"missing": false,
"obsolete": false
},
"label_contacts": {
"translation": "Kontakte",
"defaultMessage": "Contacts",
"description": "Label for contact information",
"missing": false,
"obsolete": false
},
"add_members_prompt": {
"translation": "Mitglieder hinzufügen",
"defaultMessage": "add members",
"description": "Placeholder in group members input field",
"missing": false,
"obsolete": false
}
}
+66 -3
View File
@@ -259,8 +259,8 @@
"obsolete": false
},
"contacts_not_found": {
"translation": "You have no chats<br />¯∖_(ツ)_/¯",
"defaultMessage": "You have no chats<br />¯∖_(ツ)_/¯",
"translation": "You have no chats\n¯∖_(ツ)_/¯",
"defaultMessage": "You have no chats\\n¯∖_(ツ)_/¯",
"description": "HTML message shown in ContactList when no contacts are found",
"missing": false,
"obsolete": false
@@ -1225,7 +1225,7 @@
"label_description": {
"translation": "Description",
"defaultMessage": "Description",
"description": "Label for editing topic description",
"description": "Label for description of user or topic",
"missing": false,
"obsolete": false
},
@@ -1899,5 +1899,68 @@
"description": "Label adjusting blur amount",
"missing": false,
"obsolete": false
},
"label_scan_id": {
"translation": "Scan my ID:",
"defaultMessage": "Scan my ID:",
"description": "Label [Scan my ID:]",
"missing": false,
"obsolete": false
},
"unknown_name": {
"translation": "Unknown",
"defaultMessage": "Unknown",
"description": "Name to show when the name is missing",
"missing": false,
"obsolete": false
},
"save_action": {
"translation": "Save",
"defaultMessage": "Save",
"description": "Label for Save button",
"missing": false,
"obsolete": false
},
"cannot_parse_vcard": {
"translation": "Cannot parse vCard file.",
"defaultMessage": "Cannot parse vCard file.",
"description": "Error message when vCard file cannot be parsed",
"missing": false,
"obsolete": false
},
"chat_now": {
"translation": "Chat",
"defaultMessage": "Chat",
"description": "Label for message button in contact card",
"missing": false,
"obsolete": false
},
"find_user": {
"translation": "Find",
"defaultMessage": "Find",
"description": "Label for search button in contact card",
"missing": false,
"obsolete": false
},
"label_contact_card": {
"translation": "Contact Card",
"defaultMessage": "Contact Card",
"description": "Label for contact card preview",
"missing": false,
"obsolete": false
},
"label_contacts": {
"translation": "Contacts",
"defaultMessage": "Contacts",
"description": "Label for contact information",
"missing": false,
"obsolete": false
},
"add_members_prompt": {
"translation": "add members",
"defaultMessage": "add members",
"description": "Placeholder in group members input field",
"missing": false,
"obsolete": false
}
}
+66 -3
View File
@@ -259,8 +259,8 @@
"obsolete": false
},
"contacts_not_found": {
"translation": "No tienes chats<br />¯∖_(ツ)_/¯",
"defaultMessage": "You have no chats<br />¯∖_(ツ)_/¯",
"translation": "No tienes chats\n¯∖_(ツ)_/¯",
"defaultMessage": "You have no chats\\n¯∖_(ツ)_/¯",
"description": "HTML message shown in ContactList when no contacts are found",
"missing": false,
"obsolete": false
@@ -1218,7 +1218,7 @@
"label_description": {
"translation": "Descripción",
"defaultMessage": "Description",
"description": "Label for editing topic description",
"description": "Label for description of user or topic",
"missing": false,
"obsolete": false
},
@@ -1899,5 +1899,68 @@
"description": "Label adjusting blur amount",
"missing": false,
"obsolete": false
},
"label_scan_id": {
"translation": "Escanear mi ID:",
"defaultMessage": "Scan my ID:",
"description": "Label [Scan my ID:]",
"missing": false,
"obsolete": false
},
"unknown_name": {
"translation": "Desconocido",
"defaultMessage": "Unknown",
"description": "Name to show when the name is missing",
"missing": false,
"obsolete": false
},
"save_action": {
"translation": "Guardar",
"defaultMessage": "Save",
"description": "Label for Save button",
"missing": false,
"obsolete": false
},
"cannot_parse_vcard": {
"translation": "No se puede analizar el archivo vCard.",
"defaultMessage": "Cannot parse vCard file.",
"description": "Error message when vCard file cannot be parsed",
"missing": false,
"obsolete": false
},
"chat_now": {
"translation": "Chatear",
"defaultMessage": "Chat",
"description": "Label for message button in contact card",
"missing": false,
"obsolete": false
},
"find_user": {
"translation": "Buscar",
"defaultMessage": "Find",
"description": "Label for search button in contact card",
"missing": false,
"obsolete": false
},
"label_contact_card": {
"translation": "Tarjeta de contacto",
"defaultMessage": "Contact Card",
"description": "Label for contact card preview",
"missing": false,
"obsolete": false
},
"label_contacts": {
"translation": "Contactos",
"defaultMessage": "Contacts",
"description": "Label for contact information",
"missing": false,
"obsolete": false
},
"add_members_prompt": {
"translation": "añadir miembros",
"defaultMessage": "add members",
"description": "Placeholder in group members input field",
"missing": false,
"obsolete": false
}
}
+38 -2
View File
@@ -23,6 +23,10 @@
"defaultMessage": "Report Conversation",
"description": "Flat button [Report Group]"
},
"add_members_prompt": {
"defaultMessage": "add members",
"description": "Placeholder in group members input field"
},
"alias_already_taken": {
"defaultMessage": "(already taken)",
"description": "Error message for alias already taken"
@@ -175,6 +179,10 @@
"defaultMessage": "Cannot initiate file upload.",
"description": "Generic error messagewhen attachment fails"
},
"cannot_parse_vcard": {
"defaultMessage": "Cannot parse vCard file.",
"description": "Error message when vCard file cannot be parsed"
},
"change_email": {
"defaultMessage": "Change email",
"description": "Prompt to change current email"
@@ -207,6 +215,10 @@
"defaultMessage": "Ignore",
"description": "Action [Ignore] for chat invitation."
},
"chat_now": {
"defaultMessage": "Chat",
"description": "Label for message button in contact card"
},
"clear_messages_warning": {
"defaultMessage": "Are you sure you want to clear all messages? It cannot be undone.",
"description": "Alert dialog warning when deleting all messages."
@@ -228,7 +240,7 @@
"description": "Name of the color schema"
},
"contacts_not_found": {
"defaultMessage": "You have no chats<br />¯∖_(ツ)_/¯",
"defaultMessage": "You have no chats\\n¯∖_(ツ)_/¯",
"description": "HTML message shown in ContactList when no contacts are found"
},
"contacts_not_found_short": {
@@ -327,6 +339,10 @@
"defaultMessage": "The file size {size} exceeds the {limit} limit.",
"description": "Error message when attachment is too large"
},
"find_user": {
"defaultMessage": "Find",
"description": "Label for search button in contact card"
},
"forgot_password_link": {
"defaultMessage": "Forgot password?",
"description": "Link to Reset password form"
@@ -422,6 +438,14 @@
"defaultMessage": "Theme:",
"description": "Label for selecting color scheme (dark, light) in Settings"
},
"label_contact_card": {
"defaultMessage": "Contact Card",
"description": "Label for contact card preview"
},
"label_contacts": {
"defaultMessage": "Contacts",
"description": "Label for contact information"
},
"label_content_type": {
"defaultMessage": "Content type:",
"description": "Label for file content type (mime)"
@@ -432,7 +456,7 @@
},
"label_description": {
"defaultMessage": "Description",
"description": "Label for editing topic description"
"description": "Label for description of user or topic"
},
"label_file_name": {
"defaultMessage": "File name:",
@@ -498,6 +522,10 @@
"defaultMessage": "Send SMS to reset password",
"description": "Label for password reset field by phone"
},
"label_scan_id": {
"defaultMessage": "Scan my ID:",
"description": "Label [Scan my ID:]"
},
"label_sdk": {
"defaultMessage": "SDK:"
},
@@ -876,6 +904,10 @@
"defaultMessage": "Requested",
"description": "Title for permissions"
},
"save_action": {
"defaultMessage": "Save",
"description": "Label for Save button"
},
"save_attachment": {
"defaultMessage": "save",
"description": "Call to save an attachment"
@@ -1052,6 +1084,10 @@
"defaultMessage": "Freeform name of the group",
"description": "Prompt for entering topic name"
},
"unknown_name": {
"defaultMessage": "Unknown",
"description": "Name to show when the name is missing"
},
"unnamed_topic": {
"defaultMessage": "Unnamed",
"description": "Title shown when the topic has no name"
+66 -3
View File
@@ -259,8 +259,8 @@
"obsolete": false
},
"contacts_not_found": {
"translation": "Vous n'avez aucune conversation<br />¯∖_(ツ)_/¯",
"defaultMessage": "You have no chats<br />¯∖_(ツ)_/¯",
"translation": "Vous n'avez aucune conversation\n¯∖_(ツ)_/¯",
"defaultMessage": "You have no chats\\n¯∖_(ツ)_/¯",
"description": "HTML message shown in ContactList when no contacts are found",
"missing": false,
"obsolete": false
@@ -1225,7 +1225,7 @@
"label_description": {
"translation": "Description",
"defaultMessage": "Description",
"description": "Label for editing topic description",
"description": "Label for description of user or topic",
"missing": false,
"obsolete": false
},
@@ -1899,5 +1899,68 @@
"description": "Label adjusting blur amount",
"missing": false,
"obsolete": false
},
"label_scan_id": {
"translation": "Scanner mon ID :",
"defaultMessage": "Scan my ID:",
"description": "Label [Scan my ID:]",
"missing": false,
"obsolete": false
},
"unknown_name": {
"translation": "Inconnu",
"defaultMessage": "Unknown",
"description": "Name to show when the name is missing",
"missing": false,
"obsolete": false
},
"save_action": {
"translation": "Enregistrer",
"defaultMessage": "Save",
"description": "Label for Save button",
"missing": false,
"obsolete": false
},
"cannot_parse_vcard": {
"translation": "Impossible d'analyser le fichier vCard.",
"defaultMessage": "Cannot parse vCard file.",
"description": "Error message when vCard file cannot be parsed",
"missing": false,
"obsolete": false
},
"chat_now": {
"translation": "Discuter",
"defaultMessage": "Chat",
"description": "Label for message button in contact card",
"missing": false,
"obsolete": false
},
"find_user": {
"translation": "Rechercher",
"defaultMessage": "Find",
"description": "Label for search button in contact card",
"missing": false,
"obsolete": false
},
"label_contact_card": {
"translation": "Carte de contact",
"defaultMessage": "Contact Card",
"description": "Label for contact card preview",
"missing": false,
"obsolete": false
},
"label_contacts": {
"translation": "Contacts",
"defaultMessage": "Contacts",
"description": "Label for contact information",
"missing": false,
"obsolete": false
},
"add_members_prompt": {
"translation": "ajouter des membres",
"defaultMessage": "add members",
"description": "Placeholder in group members input field",
"missing": false,
"obsolete": false
}
}
+66 -3
View File
@@ -259,8 +259,8 @@
"obsolete": false
},
"contacts_not_found": {
"translation": "Non hai chats<br />¯∖_(ツ)_/¯",
"defaultMessage": "You have no chats<br />¯∖_(ツ)_/¯",
"translation": "Non hai chats\n¯∖_(ツ)_/¯",
"defaultMessage": "You have no chats\\n¯∖_(ツ)_/¯",
"description": "HTML message shown in ContactList when no contacts are found",
"missing": false,
"obsolete": false
@@ -1218,7 +1218,7 @@
"label_description": {
"translation": "Descrizione",
"defaultMessage": "Description",
"description": "Label for editing topic description",
"description": "Label for description of user or topic",
"missing": false,
"obsolete": false
},
@@ -1899,5 +1899,68 @@
"description": "Label adjusting blur amount",
"missing": false,
"obsolete": false
},
"label_scan_id": {
"translation": "Scansiona il mio ID:",
"defaultMessage": "Scan my ID:",
"description": "Label [Scan my ID:]",
"missing": false,
"obsolete": false
},
"unknown_name": {
"translation": "Sconosciuto",
"defaultMessage": "Unknown",
"description": "Name to show when the name is missing",
"missing": false,
"obsolete": false
},
"save_action": {
"translation": "Salva",
"defaultMessage": "Save",
"description": "Label for Save button",
"missing": false,
"obsolete": false
},
"cannot_parse_vcard": {
"translation": "Impossibile analizzare il file vCard.",
"defaultMessage": "Cannot parse vCard file.",
"description": "Error message when vCard file cannot be parsed",
"missing": false,
"obsolete": false
},
"chat_now": {
"translation": "Chatta",
"defaultMessage": "Chat",
"description": "Label for message button in contact card",
"missing": false,
"obsolete": false
},
"find_user": {
"translation": "Cerca",
"defaultMessage": "Find",
"description": "Label for search button in contact card",
"missing": false,
"obsolete": false
},
"label_contact_card": {
"translation": "Scheda contatto",
"defaultMessage": "Contact Card",
"description": "Label for contact card preview",
"missing": false,
"obsolete": false
},
"label_contacts": {
"translation": "Contatti",
"defaultMessage": "Contacts",
"description": "Label for contact information",
"missing": false,
"obsolete": false
},
"add_members_prompt": {
"translation": "aggiungi membri",
"defaultMessage": "add members",
"description": "Placeholder in group members input field",
"missing": false,
"obsolete": false
}
}
+66 -3
View File
@@ -259,8 +259,8 @@
"obsolete": false
},
"contacts_not_found": {
"translation": "채팅이없습니다<br/>¯∖_(ツ)_/¯",
"defaultMessage": "You have no chats<br />¯∖_(ツ)_/¯",
"translation": "채팅이없습니다\n¯∖_(ツ)_/¯",
"defaultMessage": "You have no chats\\n¯∖_(ツ)_/¯",
"description": "HTML message shown in ContactList when no contacts are found",
"missing": false,
"obsolete": false
@@ -1218,7 +1218,7 @@
"label_description": {
"translation": "설명",
"defaultMessage": "Description",
"description": "Label for editing topic description",
"description": "Label for description of user or topic",
"missing": false,
"obsolete": false
},
@@ -1899,5 +1899,68 @@
"description": "Label adjusting blur amount",
"missing": false,
"obsolete": false
},
"label_scan_id": {
"translation": "내 ID 스캔:",
"defaultMessage": "Scan my ID:",
"description": "Label [Scan my ID:]",
"missing": false,
"obsolete": false
},
"unknown_name": {
"translation": "알 수 없음",
"defaultMessage": "Unknown",
"description": "Name to show when the name is missing",
"missing": false,
"obsolete": false
},
"save_action": {
"translation": "저장",
"defaultMessage": "Save",
"description": "Label for Save button",
"missing": false,
"obsolete": false
},
"cannot_parse_vcard": {
"translation": "vCard 파일을 파싱할 수 없습니다.",
"defaultMessage": "Cannot parse vCard file.",
"description": "Error message when vCard file cannot be parsed",
"missing": false,
"obsolete": false
},
"chat_now": {
"translation": "채팅",
"defaultMessage": "Chat",
"description": "Label for message button in contact card",
"missing": false,
"obsolete": false
},
"find_user": {
"translation": "찾기",
"defaultMessage": "Find",
"description": "Label for search button in contact card",
"missing": false,
"obsolete": false
},
"label_contact_card": {
"translation": "연락처 카드",
"defaultMessage": "Contact Card",
"description": "Label for contact card preview",
"missing": false,
"obsolete": false
},
"label_contacts": {
"translation": "연락처",
"defaultMessage": "Contacts",
"description": "Label for contact information",
"missing": false,
"obsolete": false
},
"add_members_prompt": {
"translation": "멤버 추가",
"defaultMessage": "add members",
"description": "Placeholder in group members input field",
"missing": false,
"obsolete": false
}
}
+66 -3
View File
@@ -259,8 +259,8 @@
"obsolete": false
},
"contacts_not_found": {
"translation": "Nu ai contacte salvate<br />¯∖_(ツ)_/¯",
"defaultMessage": "You have no chats<br />¯∖_(ツ)_/¯",
"translation": "Nu ai contacte salvate\n¯∖_(ツ)_/¯",
"defaultMessage": "You have no chats\\n¯∖_(ツ)_/¯",
"description": "HTML message shown in ContactList when no contacts are found",
"missing": false,
"obsolete": false
@@ -1218,7 +1218,7 @@
"label_description": {
"translation": "Descriere",
"defaultMessage": "Description",
"description": "Label for editing topic description",
"description": "Label for description of user or topic",
"missing": false,
"obsolete": false
},
@@ -1899,5 +1899,68 @@
"description": "Label adjusting blur amount",
"missing": false,
"obsolete": false
},
"label_scan_id": {
"translation": "Scanează ID-ul meu:",
"defaultMessage": "Scan my ID:",
"description": "Label [Scan my ID:]",
"missing": false,
"obsolete": false
},
"unknown_name": {
"translation": "Necunoscut",
"defaultMessage": "Unknown",
"description": "Name to show when the name is missing",
"missing": false,
"obsolete": false
},
"save_action": {
"translation": "Salvează",
"defaultMessage": "Save",
"description": "Label for Save button",
"missing": false,
"obsolete": false
},
"cannot_parse_vcard": {
"translation": "Nu se poate procesa fișierul vCard.",
"defaultMessage": "Cannot parse vCard file.",
"description": "Error message when vCard file cannot be parsed",
"missing": false,
"obsolete": false
},
"chat_now": {
"translation": "Conversație",
"defaultMessage": "Chat",
"description": "Label for message button in contact card",
"missing": false,
"obsolete": false
},
"find_user": {
"translation": "Caută",
"defaultMessage": "Find",
"description": "Label for search button in contact card",
"missing": false,
"obsolete": false
},
"label_contact_card": {
"translation": "Card de contact",
"defaultMessage": "Contact Card",
"description": "Label for contact card preview",
"missing": false,
"obsolete": false
},
"label_contacts": {
"translation": "Contacte",
"defaultMessage": "Contacts",
"description": "Label for contact information",
"missing": false,
"obsolete": false
},
"add_members_prompt": {
"translation": "adaugă membri",
"defaultMessage": "add members",
"description": "Placeholder in group members input field",
"missing": false,
"obsolete": false
}
}
+66 -3
View File
@@ -259,8 +259,8 @@
"obsolete": false
},
"contacts_not_found": {
"translation": "Чатов нет<br />¯∖_(ツ)_/¯",
"defaultMessage": "You have no chats<br />¯∖_(ツ)_/¯",
"translation": "Чатов нет\n¯∖_(ツ)_/¯",
"defaultMessage": "You have no chats\\n¯∖_(ツ)_/¯",
"description": "HTML message shown in ContactList when no contacts are found",
"missing": false,
"obsolete": false
@@ -1218,7 +1218,7 @@
"label_description": {
"translation": "Описание",
"defaultMessage": "Description",
"description": "Label for editing topic description",
"description": "Label for description of user or topic",
"missing": false,
"obsolete": false
},
@@ -1899,5 +1899,68 @@
"description": "Label adjusting blur amount",
"missing": false,
"obsolete": false
},
"label_scan_id": {
"translation": "Отсканируйте мой ID:",
"defaultMessage": "Scan my ID:",
"description": "Label [Scan my ID:]",
"missing": false,
"obsolete": false
},
"unknown_name": {
"translation": "Неизвестный",
"defaultMessage": "Unknown",
"description": "Name to show when the name is missing",
"missing": false,
"obsolete": false
},
"save_action": {
"translation": "Сохранить",
"defaultMessage": "Save",
"description": "Label for Save button",
"missing": false,
"obsolete": false
},
"cannot_parse_vcard": {
"translation": "Не удалось обработать файл vCard.",
"defaultMessage": "Cannot parse vCard file.",
"description": "Error message when vCard file cannot be parsed",
"missing": false,
"obsolete": false
},
"chat_now": {
"translation": "Написать",
"defaultMessage": "Chat",
"description": "Label for message button in contact card",
"missing": false,
"obsolete": false
},
"find_user": {
"translation": "Найти",
"defaultMessage": "Find",
"description": "Label for search button in contact card",
"missing": false,
"obsolete": false
},
"label_contact_card": {
"translation": "Карточка контакта",
"defaultMessage": "Contact Card",
"description": "Label for contact card preview",
"missing": false,
"obsolete": false
},
"label_contacts": {
"translation": "Контакты",
"defaultMessage": "Contacts",
"description": "Label for contact information",
"missing": false,
"obsolete": false
},
"add_members_prompt": {
"translation": "добавить участников",
"defaultMessage": "add members",
"description": "Placeholder in group members input field",
"missing": false,
"obsolete": false
}
}
+66 -3
View File
@@ -259,8 +259,8 @@
"obsolete": false
},
"contacts_not_found": {
"translation": "ไม่พบแชท <br />¯∖_(ツ)_/¯",
"defaultMessage": "You have no chats<br />¯∖_(ツ)_/¯",
"translation": "ไม่พบแชท \n¯∖_(ツ)_/¯",
"defaultMessage": "You have no chats\\n¯∖_(ツ)_/¯",
"description": "HTML message shown in ContactList when no contacts are found",
"missing": false,
"obsolete": false
@@ -1225,7 +1225,7 @@
"label_description": {
"translation": "คำอธิบาย",
"defaultMessage": "Description",
"description": "Label for editing topic description",
"description": "Label for description of user or topic",
"missing": false,
"obsolete": false
},
@@ -1899,5 +1899,68 @@
"description": "Label adjusting blur amount",
"missing": false,
"obsolete": false
},
"label_scan_id": {
"translation": "สแกน ID ของฉัน:",
"defaultMessage": "Scan my ID:",
"description": "Label [Scan my ID:]",
"missing": false,
"obsolete": false
},
"unknown_name": {
"translation": "ไม่ทราบ",
"defaultMessage": "Unknown",
"description": "Name to show when the name is missing",
"missing": false,
"obsolete": false
},
"save_action": {
"translation": "บันทึก",
"defaultMessage": "Save",
"description": "Label for Save button",
"missing": false,
"obsolete": false
},
"cannot_parse_vcard": {
"translation": "ไม่สามารถอ่านไฟล์ vCard ได้",
"defaultMessage": "Cannot parse vCard file.",
"description": "Error message when vCard file cannot be parsed",
"missing": false,
"obsolete": false
},
"chat_now": {
"translation": "แชท",
"defaultMessage": "Chat",
"description": "Label for message button in contact card",
"missing": false,
"obsolete": false
},
"find_user": {
"translation": "ค้นหา",
"defaultMessage": "Find",
"description": "Label for search button in contact card",
"missing": false,
"obsolete": false
},
"label_contact_card": {
"translation": "นามบัตร",
"defaultMessage": "Contact Card",
"description": "Label for contact card preview",
"missing": false,
"obsolete": false
},
"label_contacts": {
"translation": "ผู้ติดต่อ",
"defaultMessage": "Contacts",
"description": "Label for contact information",
"missing": false,
"obsolete": false
},
"add_members_prompt": {
"translation": "เพิ่มสมาชิก",
"defaultMessage": "add members",
"description": "Placeholder in group members input field",
"missing": false,
"obsolete": false
}
}
+66 -3
View File
@@ -259,8 +259,8 @@
"obsolete": false
},
"contacts_not_found": {
"translation": "Чатів немає<br />¯∖_(ツ)_/¯",
"defaultMessage": "You have no chats<br />¯∖_(ツ)_/¯",
"translation": "Чатів немає\n¯∖_(ツ)_/¯",
"defaultMessage": "You have no chats\\n¯∖_(ツ)_/¯",
"description": "HTML message shown in ContactList when no contacts are found",
"missing": false,
"obsolete": false
@@ -1218,7 +1218,7 @@
"label_description": {
"translation": "Опис",
"defaultMessage": "Description",
"description": "Label for editing topic description",
"description": "Label for description of user or topic",
"missing": false,
"obsolete": false
},
@@ -1899,5 +1899,68 @@
"description": "Label adjusting blur amount",
"missing": false,
"obsolete": false
},
"label_scan_id": {
"translation": "Сканувати мій ID:",
"defaultMessage": "Scan my ID:",
"description": "Label [Scan my ID:]",
"missing": false,
"obsolete": false
},
"unknown_name": {
"translation": "Невідомий",
"defaultMessage": "Unknown",
"description": "Name to show when the name is missing",
"missing": false,
"obsolete": false
},
"save_action": {
"translation": "Зберегти",
"defaultMessage": "Save",
"description": "Label for Save button",
"missing": false,
"obsolete": false
},
"cannot_parse_vcard": {
"translation": "Не вдалося обробити файл vCard.",
"defaultMessage": "Cannot parse vCard file.",
"description": "Error message when vCard file cannot be parsed",
"missing": false,
"obsolete": false
},
"chat_now": {
"translation": "Написати",
"defaultMessage": "Chat",
"description": "Label for message button in contact card",
"missing": false,
"obsolete": false
},
"find_user": {
"translation": "Знайти",
"defaultMessage": "Find",
"description": "Label for search button in contact card",
"missing": false,
"obsolete": false
},
"label_contact_card": {
"translation": "Картка контакту",
"defaultMessage": "Contact Card",
"description": "Label for contact card preview",
"missing": false,
"obsolete": false
},
"label_contacts": {
"translation": "Контакти",
"defaultMessage": "Contacts",
"description": "Label for contact information",
"missing": false,
"obsolete": false
},
"add_members_prompt": {
"translation": "додати учасників",
"defaultMessage": "add members",
"description": "Placeholder in group members input field",
"missing": false,
"obsolete": false
}
}
+1966
View File
File diff suppressed because it is too large Load Diff
+66 -3
View File
@@ -259,8 +259,8 @@
"obsolete": false
},
"contacts_not_found": {
"translation": "您沒有任何對話<br />¯∖_(ツ)_/¯",
"defaultMessage": "You have no chats<br />¯∖_(ツ)_/¯",
"translation": "您沒有任何對話\n¯∖_(ツ)_/¯",
"defaultMessage": "You have no chats\\n¯∖_(ツ)_/¯",
"description": "HTML message shown in ContactList when no contacts are found",
"missing": false,
"obsolete": false
@@ -1218,7 +1218,7 @@
"label_description": {
"translation": "說明",
"defaultMessage": "Description",
"description": "Label for editing topic description",
"description": "Label for description of user or topic",
"missing": false,
"obsolete": false
},
@@ -1899,5 +1899,68 @@
"description": "Label adjusting blur amount",
"missing": false,
"obsolete": false
},
"label_scan_id": {
"translation": "掃描我的 ID:",
"defaultMessage": "Scan my ID:",
"description": "Label [Scan my ID:]",
"missing": false,
"obsolete": false
},
"unknown_name": {
"translation": "未知",
"defaultMessage": "Unknown",
"description": "Name to show when the name is missing",
"missing": false,
"obsolete": false
},
"save_action": {
"translation": "儲存",
"defaultMessage": "Save",
"description": "Label for Save button",
"missing": false,
"obsolete": false
},
"cannot_parse_vcard": {
"translation": "無法解析 vCard 檔案。",
"defaultMessage": "Cannot parse vCard file.",
"description": "Error message when vCard file cannot be parsed",
"missing": false,
"obsolete": false
},
"chat_now": {
"translation": "聊天",
"defaultMessage": "Chat",
"description": "Label for message button in contact card",
"missing": false,
"obsolete": false
},
"find_user": {
"translation": "尋找",
"defaultMessage": "Find",
"description": "Label for search button in contact card",
"missing": false,
"obsolete": false
},
"label_contact_card": {
"translation": "聯絡人卡片",
"defaultMessage": "Contact Card",
"description": "Label for contact card preview",
"missing": false,
"obsolete": false
},
"label_contacts": {
"translation": "聯絡人",
"defaultMessage": "Contacts",
"description": "Label for contact information",
"missing": false,
"obsolete": false
},
"add_members_prompt": {
"translation": "新增成員",
"defaultMessage": "add members",
"description": "Placeholder in group members input field",
"missing": false,
"obsolete": false
}
}
+66 -3
View File
@@ -259,8 +259,8 @@
"obsolete": false
},
"contacts_not_found": {
"translation": "你尚无会话<br />¯∖_(ツ)_/¯",
"defaultMessage": "You have no chats<br />¯∖_(ツ)_/¯",
"translation": "你尚无会话\n¯∖_(ツ)_/¯",
"defaultMessage": "You have no chats\\n¯∖_(ツ)_/¯",
"description": "HTML message shown in ContactList when no contacts are found",
"missing": false,
"obsolete": false
@@ -1218,7 +1218,7 @@
"label_description": {
"translation": "说明",
"defaultMessage": "Description",
"description": "Label for editing topic description",
"description": "Label for description of user or topic",
"missing": false,
"obsolete": false
},
@@ -1899,5 +1899,68 @@
"description": "Label adjusting blur amount",
"missing": false,
"obsolete": false
},
"label_scan_id": {
"translation": "扫描我的 ID:",
"defaultMessage": "Scan my ID:",
"description": "Label [Scan my ID:]",
"missing": false,
"obsolete": false
},
"unknown_name": {
"translation": "未知",
"defaultMessage": "Unknown",
"description": "Name to show when the name is missing",
"missing": false,
"obsolete": false
},
"save_action": {
"translation": "保存",
"defaultMessage": "Save",
"description": "Label for Save button",
"missing": false,
"obsolete": false
},
"cannot_parse_vcard": {
"translation": "无法解析 vCard 文件。",
"defaultMessage": "Cannot parse vCard file.",
"description": "Error message when vCard file cannot be parsed",
"missing": false,
"obsolete": false
},
"chat_now": {
"translation": "聊天",
"defaultMessage": "Chat",
"description": "Label for message button in contact card",
"missing": false,
"obsolete": false
},
"find_user": {
"translation": "查找",
"defaultMessage": "Find",
"description": "Label for search button in contact card",
"missing": false,
"obsolete": false
},
"label_contact_card": {
"translation": "联系人卡片",
"defaultMessage": "Contact Card",
"description": "Label for contact card preview",
"missing": false,
"obsolete": false
},
"label_contacts": {
"translation": "联系人",
"defaultMessage": "Contacts",
"description": "Label for contact information",
"missing": false,
"obsolete": false
},
"add_members_prompt": {
"translation": "添加成员",
"defaultMessage": "add members",
"description": "Placeholder in group members input field",
"missing": false,
"obsolete": false
}
}
+1
View File
@@ -35,6 +35,7 @@ const messageLoader = {
'ru': _ => import('./i18n.min/ru.json'),
'th': _ => import('./i18n.min/th.json'),
'uk': _ => import('./i18n.min/uk.json'),
'vi': _ => import('./i18n.min/vi.json'),
'zh': _ => import('./i18n.min/zh.json'),
'zh-TW': _ => import('./i18n.min/zh-TW.json')
};
+19
View File
@@ -1,4 +1,5 @@
// File and image helper functions.
import { TheCard } from 'tinode-sdk';
// Supported image MIME types and corresponding file extensions.
export const SUPPORTED_IMAGE_FORMATS = ['image/jpeg', 'image/gif', 'image/png', 'image/svg', 'image/svg+xml'];
@@ -305,3 +306,21 @@ export function base64ToIntArray(b64) {
} catch (err) {}
return null;
}
export function importVCard(fileOrBlob) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onerror = _ => {
reject(reader.error);
};
reader.onload = _ => {
const card = TheCard.importVCard(reader.result);
if (!card) {
reject(new Error("Failed to parse vCard"));
return;
}
resolve(card);
};
reader.readAsText(fileOrBlob);
});
}
+24 -11
View File
@@ -1,13 +1,14 @@
import React from 'react';
import { defineMessages } from 'react-intl';
import { Drafty } from 'tinode-sdk';
import { Drafty, TheCard } from 'tinode-sdk';
import AudioPlayer from '../widgets/audio-player.jsx'
import CallMessage from '../widgets/call-message.jsx'
import CallStatus from '../widgets/call-status.jsx';
import InlineVideo from '../widgets/inline-video.jsx';
import LazyImage from '../widgets/lazy-image.jsx'
import LazyImage from '../widgets/lazy-image.jsx';
import TheCardMini from '../widgets/the-card-mini.jsx';
import UploadingImage from '../widgets/uploading-image.jsx'
import { BROKEN_IMAGE_SIZE, CLICKABLE_URL_SCHEMES, IMAGE_THUMBNAIL_DIM, NO_DIMENSIONS_VIDEO,
@@ -48,10 +49,7 @@ const messages = defineMessages({
// 'this' must contain:
// viewportWidth:
// authorizeURL:
// onImagePreview:
// onVideoPreview:
// onFormButtonClick:
// onQuoteClick:
// onHandleClick:
export function fullFormatter(style, data, values, key, stack) {
if (stack.includes('QQ')) {
return quoteFormatter.call(this, style, data, values, key);
@@ -100,7 +98,7 @@ export function fullFormatter(style, data, values, key, stack) {
break;
case 'BN':
// Button
attr.onClick = this.onFormButtonClick;
attr.onClick = (e) => this.onHandleClick(e, 'form_button');
let inner = React.Children.map(values, (child) => {
return typeof child == 'string' ? child : undefined;
});
@@ -133,7 +131,16 @@ export function fullFormatter(style, data, values, key, stack) {
case 'QQ':
// Quote/citation.
attr.className = 'reply-quote'
attr.onClick = this.onQuoteClick;
attr.onClick = (e) => this.onHandleClick(e, 'quote');
break;
case 'TC':
// TheCard
el = TheCardMini;
attr.content = data;
attr.authorizeURL = this.authorizeURL;
attr.onChatClick = (e) => this.onHandleClick(e, 'contact_chat');
attr.onFindClick = (e) => this.onHandleClick(e, 'contact_find');
values = null;
break;
case 'VC':
el = CallMessage;
@@ -200,7 +207,7 @@ function handleImageData(el, data, attr) {
if (attr.src) {
if (Math.max(data.width || 0, data.height || 0) > IMAGE_THUMBNAIL_DIM) {
// Allow previews for large enough images.
attr.onClick = this.onImagePreview;
attr.onClick = (e) => this.onHandleClick(e, 'image');
attr.className += ' image-clickable';
}
attr.loading = 'lazy';
@@ -241,7 +248,7 @@ function handleVideoData(el, data, attr) {
attr.src = this.authorizeURL(sanitizeUrlForMime(attr.src, 'image'));
attr.alt = data.name;
if (data.ref || data.val) {
attr.onClick = this.onVideoPreview;
attr.onClick = (e) => this.onHandleClick(e, 'video');
attr.loading = 'lazy';
}
el = InlineVideo;
@@ -329,6 +336,12 @@ export function previewFormatter(style, data, values, key) {
el = null;
values = null;
break;
case 'TC':
// TheCard
el = React.Fragment;
values = [<i key="tc" className="material-icons">contact_mail</i>, ' ',
TheCard.getFn(data) || this.formatMessage(messages.drafty_unknown)];
break;
case 'VD':
// Replace image with '[icon] Video'.
el = React.Fragment;
@@ -392,7 +405,7 @@ function inlineVideoAttr(attr, data) {
// formatMessage: this.props.intl.formatMessage
// messages: formatjs messages defined with defineMessages.
// authorizeURL: this.props.tinode.authorizeURL
// onQuoteClick: this.handleQuoteClick (optional)
// onHandleClick: this.handleDraftyClick
function quoteFormatter(style, data, values, key) {
if (['BR', 'EX', 'IM', 'MN', 'VD'].includes(style)) {
let el = Drafty.tagName(style);
+11
View File
@@ -55,6 +55,17 @@ export default class HashNavigation {
return HashNavigation.composeUrlHash(parsed.path, parsed.params);
}
static removeUrlParams(hash, keys) {
if (!Array.isArray(keys)) {
keys = [keys];
}
const parsed = HashNavigation.parseUrlHash(hash);
for (const key of keys) {
delete parsed.params[key];
}
return HashNavigation.composeUrlHash(parsed.path, parsed.params);
}
static setUrlSidePanel(hash, sidepanel) {
const parsed = HashNavigation.parseUrlHash(hash);
parsed.path[0] = sidepanel;
+20
View File
@@ -254,3 +254,23 @@ export function defaultWallpaper() {
const wp = wallpaperIndex[WALLPAPER_DEFAULTS.type][WALLPAPER_DEFAULTS.index];
return {name: `../${WALLPAPER_DEFAULTS.path}${wp.name}`, size: wp.size};
}
export function wallpaperNameFromUrl(wallpaperUrl) {
// Get filename from URL.
const parts = wallpaperUrl.split('/');
return parts[parts.length - 1];
}
export function wallpaperTypeFromUrl(wallpaperUrl) {
// Get filename from URL.
const fname = wallpaperNameFromUrl(wallpaperUrl);
const wallpaperIndex = require('../../img/bkg/index.json');
for (const type in wallpaperIndex) {
for (let i = 0; i < wallpaperIndex[type].length; i++) {
if (wallpaperIndex[type][i].name == fname) {
return type;
}
}
}
return null;
}
+1 -1
View File
@@ -1,3 +1,3 @@
// This is a generated file. Don't edit.
export const PACKAGE_VERSION = "0.25.0-beta2";
export const PACKAGE_VERSION = "0.25.2";
+1 -1
View File
@@ -138,7 +138,7 @@ export default class AccountSettingsView extends React.Component {
<div className="group">
<label className="small">
<FormattedMessage id="label_description" defaultMessage="Description"
description="Label for editing topic description" />
description="Label for description of user or topic" />
</label>
<div className="quoted">{this.state.description}</div>
</div> : null}
+1 -1
View File
@@ -91,7 +91,7 @@ export default class ContactsView extends React.Component {
render() {
return (
<FormattedMessage id="contacts_not_found"
defaultMessage="You have no chats<br />¯∖_(ツ)_/¯"
defaultMessage="You have no chats\n¯∖_(ツ)_/¯"
description="HTML message shown in ContactList when no contacts are found">{
(no_contacts) => <ContactList
tinode={this.props.tinode}
+13 -4
View File
@@ -2,7 +2,7 @@
import React from 'react';
import { FormattedMessage, FormattedNumber, defineMessages, injectIntl } from 'react-intl';
import { AccessMode, Tinode } from 'tinode-sdk';
import { AccessMode, Tinode, TheCard } from 'tinode-sdk';
import AvatarUpload from '../widgets/avatar-upload.jsx';
import BadgeList from '../widgets/badge-list.jsx';
@@ -20,7 +20,6 @@ import { MAX_TITLE_LENGTH, MAX_TOPIC_DESCRIPTION_LENGTH,
NO_ACCESS_MODE, TOAST_DURATION } from '../config.js';
import { makeImageUrl } from '../lib/blob-helpers.js';
import { theCard } from '../lib/utils.js';
import { truncateString } from '../lib/strformat.js';
const messages = defineMessages({
@@ -147,6 +146,7 @@ class InfoView extends React.Component {
this.handleLaunchPermissionsEditor = this.handleLaunchPermissionsEditor.bind(this);
this.handleShowQRCode = this.handleShowQRCode.bind(this);
this.handleCopyToClipboard = this.handleCopyToClipboard.bind(this);
this.handleShare = this.handleShare.bind(this);
this.handleShowAddMembers = this.handleShowAddMembers.bind(this);
this.handleMemberUpdateRequest = this.handleMemberUpdateRequest.bind(this);
this.handleMemberSelected = this.handleMemberSelected.bind(this);
@@ -299,7 +299,7 @@ class InfoView extends React.Component {
handleImageChanged(mime, img) {
this.setState({avatar: img});
this.props.onTopicDescUpdate(this.props.topic, theCard(null, img || Tinode.DEL_CHAR), null);
this.props.onTopicDescUpdate(this.props.topic, new TheCard(null, img || Tinode.DEL_CHAR), null);
}
handleMuted(ignored, checked) {
@@ -421,7 +421,13 @@ class InfoView extends React.Component {
this.setState({toast: this.props.intl.formatMessage(messages.text_copied)});
setTimeout(_ => { this.setState({toast: ''}); }, TOAST_DURATION);
});
}
handleShare(e) {
e.preventDefault();
const card = new TheCard(this.state.fullName, this.state.avatar, null, this.state.description);
TheCard.setTinodeID(card, Tinode.URI_TOPIC_ID_PREFIX + this.props.topic);
this.props.onShareTheCard(card);
}
handleShowAddMembers(e) {
@@ -629,6 +635,9 @@ class InfoView extends React.Component {
&nbsp;<a href="#" onClick={this.handleShowQRCode}>
<i className="material-icons">qr_code</i>
</a>&nbsp;
&nbsp;<a href="#" onClick={this.handleShare}>
<i className="material-icons">share</i>
</a>&nbsp;
</div>
</div>
{alias ?
@@ -673,7 +682,7 @@ class InfoView extends React.Component {
<div className="group">
<label className="small">
<FormattedMessage id="label_description" defaultMessage="Description"
description="Label for editing topic description" />
description="Label for description of user or topic" />
</label>
<div>{this.state.description}</div>
</div> : null}
+113 -26
View File
@@ -1,17 +1,17 @@
// Panel with a chat.
import React from 'react';
import React, { Suspense } from 'react';
import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
import { Drafty, Tinode } from 'tinode-sdk';
import { Drafty, Tinode, TheCard } from 'tinode-sdk';
import CallPanel from '../widgets/call-panel.jsx';
const CallPanel = React.lazy(_ => import('../widgets/call-panel.jsx'));
import ChatMessage from '../widgets/chat-message.jsx';
import ContactBadges from '../widgets/contact-badges.jsx';
import DocPreview from '../widgets/doc-preview.jsx';
const DocPreview = React.lazy(_ => import('../widgets/doc-preview.jsx'));
import ErrorPanel from '../widgets/error-panel.jsx';
import GroupSubs from '../widgets/group-subs.jsx';
import ImagePreview from '../widgets/image-preview.jsx';
const ImagePreview = React.lazy(_ => import('../widgets/image-preview.jsx'));
import Invitation from '../widgets/invitation.jsx';
import LetterTile from '../widgets/letter-tile.jsx';
import LoadSpinner from '../widgets/load-spinner.jsx';
@@ -19,13 +19,14 @@ import LogoView from './logo-view.jsx';
import MetaMessage from '../widgets/meta-message.jsx';
import PinnedMessages from '../widgets/pinned-messages.jsx';
import SendMessage from '../widgets/send-message.jsx';
import VideoPreview from '../widgets/video-preview.jsx';
const TheCardPreview = React.lazy(_ => import('../widgets/the-card-preview.jsx'));
const VideoPreview = React.lazy(_ => import('../widgets/video-preview.jsx'));
import { DEFAULT_P2P_ACCESS_MODE, EDIT_PREVIEW_LENGTH, IMAGE_PREVIEW_DIM, IMMEDIATE_P2P_SUBSCRIPTION,
DRAFTY_FR_MIME_TYPE_LEGACY, KEYPRESS_DELAY, MESSAGES_PAGE, MAX_EXTERN_ATTACHMENT_SIZE, MAX_IMAGE_DIM,
MAX_INBAND_ATTACHMENT_SIZE, READ_DELAY, QUOTED_REPLY_LENGTH, VIDEO_PREVIEW_DIM } from '../config.js';
import { CALL_STATE_OUTGOING_INITATED, CALL_STATE_IN_PROGRESS } from '../constants.js';
import { blobToBase64, fileToBase64, imageScaled, makeImageUrl } from '../lib/blob-helpers.js';
import { blobToBase64, fileToBase64, imageScaled, importVCard, makeImageUrl } from '../lib/blob-helpers.js';
import HashNavigation from '../lib/navigation.js';
import { bytesToHumanSize, relativeDateFormat, shortDateFormat } from '../lib/strformat.js';
@@ -104,6 +105,11 @@ const messages = defineMessages({
'other {{count, number} members}}',
description: 'Count of group topic members'
},
cannot_parse_vcard: {
id: 'cannot_parse_vcard',
defaultMessage: 'Cannot parse vCard file.',
description: 'Error message when vCard file cannot be parsed'
}
});
// Checks if the access permissions are granted but not yet accepted.
@@ -144,6 +150,7 @@ class MessagesView extends React.Component {
this.sendVideoAttachment = this.sendVideoAttachment.bind(this);
this.sendFileAttachment = this.sendFileAttachment.bind(this);
this.sendAudioAttachment = this.sendAudioAttachment.bind(this);
this.sendTheCardAttachment = this.sendTheCardAttachment.bind(this);
this.sendKeyPress = this.sendKeyPress.bind(this);
this.subscribe = this.subscribe.bind(this);
this.handleScrollReference = this.handleScrollReference.bind(this);
@@ -319,6 +326,9 @@ class MessagesView extends React.Component {
} else if (topic.isSubscribed() && this.state.isReader && !prevState.isReader) {
// If reader status has changed and data became available.
topic.getMeta(topic.startMetaQuery().withLaterData().build());
} else if (!topic.isSubscribed() && this.props.ready && !prevProps.ready) {
// Connection restored (e.g. wake from sleep): resubscribe to the currently open topic.
this.subscribe(topic);
}
}
}
@@ -383,11 +393,21 @@ class MessagesView extends React.Component {
};
if (nextProps.forwardMessage) {
if (nextProps.forwardMessage.content) {
const content = nextProps.forwardMessage.content;
const size = content?.length || (content?.size || 0);
nextState.docPreview = {
object: content,
type: nextProps.forwardMessage.type,
size: size
};
} else {
// We are forwarding a message. Show preview.
nextState.reply = {
content: nextProps.forwardMessage.preview,
seq: null
};
}
} else {
nextState.reply = null;
}
@@ -901,7 +921,14 @@ class MessagesView extends React.Component {
if (this.state.videoPreview && this.state.videoPreview.url) {
URL.revokeObjectURL(this.state.videoPreview.url);
}
this.setState({ imagePostview: null, imagePreview: null, docPreview: null, videoPreview: null, videoPostview: null});
this.setState({
imagePostview: null,
imagePreview: null,
docPreview: null,
videoPreview: null,
videoPostview: null,
});
this.props.onCancelForwardMessage();
}
handleFormResponse(action, text, data) {
@@ -1001,7 +1028,7 @@ class MessagesView extends React.Component {
// sendMessage sends the message with an optional subscription to topic first.
sendMessage(msg, uploadCompletionPromise, uploader) {
let head;
if (this.props.forwardMessage) {
if (!msg && this.props.forwardMessage) {
// We are forwarding a message.
msg = this.props.forwardMessage.msg;
head = this.props.forwardMessage.head;
@@ -1039,7 +1066,7 @@ class MessagesView extends React.Component {
});
}
// Send attachment as Drafty message:
// sendFileAttachment sends the file as Drafty message:
// - if file is too large, upload it and send a s link.
// - if file is small enough, just send it in-band.
sendFileAttachment(file) {
@@ -1047,6 +1074,15 @@ class MessagesView extends React.Component {
const maxInbandAttachmentSize = (this.props.tinode.getServerParam('maxMessageSize',
MAX_INBAND_ATTACHMENT_SIZE) * 0.75 - 1024) | 0;
if (TheCard.isFileSupported(file.type, file.name) || file.type == TheCard.contentType) {
// This is a vCard file, try to convert and send as TheCard.
if (this.sendTheCardAttachment(file, maxInbandAttachmentSize)) {
// Successfully sent as TheCard.
return;
}
// Sending as TheCard failed, fall through to sending as generic file attachment.
}
// If the attachment is a JSON file, then use 'application/octet-stream' instead of 'application/json'.
// This is a temporary workaround for the collision with the 'application/json' MIME type of form responses.
// Remove this code in 2026 or so.
@@ -1081,6 +1117,29 @@ class MessagesView extends React.Component {
}
}
// Convert attached vCard to TheCard and send.
sendTheCardAttachment(file, maxInbandAttachmentSize) {
if (file.size > maxInbandAttachmentSize) {
// Cannot convert to TheCard, send as a generic file attachment instead.
return false;
}
if (file.type == TheCard.contentType) {
// Already TheCard, no need to convert.
this.sendMessage(Drafty.appendTheCard(null, file.object));
return true;
}
importVCard(file)
.then(card => {
this.sendMessage(Drafty.appendTheCard(null, card));
})
.catch(err => {
this.props.onError(this.props.intl.formatMessage(messages.cannot_parse_vcard, {error: err.message}), 'err');
});
return true;
}
// handleAttachFile method is called when [Attach file] button is clicked: launch attachment preview.
handleAttachFile(file) {
const maxExternAttachmentSize = this.props.tinode.getServerParam('maxFileUploadSize', MAX_EXTERN_ATTACHMENT_SIZE);
@@ -1488,7 +1547,9 @@ class MessagesView extends React.Component {
// P2P call is an overlay, not a separate component because it cannot be
// re-mounted between full screen/minimized.
const overlay = this.state.rtcPanel ? (
const overlay = this.state.rtcPanel &&
<Suspense fallback={<div><FormattedMessage id="loading_note" defaultMessage="Loading..."
description="Message shown when component is loading"/></div>}>
<CallPanel
topic={this.state.topic}
seq={this.props.callSeq}
@@ -1498,7 +1559,6 @@ class MessagesView extends React.Component {
title={this.state.title}
avatar={this.state.avatar || true}
minimized={this.state.minimizedCallPanel}
onError={this.props.onError}
onHangup={this.handleCallHangup}
onToggleMinimize={this.handleCallPanelToggle}
@@ -1506,20 +1566,21 @@ class MessagesView extends React.Component {
onSendOffer={this.props.onCallSendOffer}
onIceCandidate={this.props.onCallIceCandidate}
onSendAnswer={this.props.onCallSendAnswer} />
) : null;
</Suspense>;
let component;
if (!this.state.topic) {
component = (
<LogoView
component = <LogoView
serverVersion={this.props.serverVersion}
serverAddress={this.props.serverAddress} />
);
serverAddress={this.props.serverAddress} />;
} else {
let component2;
if (this.state.imagePreview) {
// Preview image before sending.
component2 = (
<Suspense fallback={<div><FormattedMessage id="loading_note" defaultMessage="Loading..."
description="Message shown when component is loading"/></div>}>
<ImagePreview
content={this.state.imagePreview}
tinode={this.props.tinode}
@@ -1527,10 +1588,12 @@ class MessagesView extends React.Component {
onCancelReply={this.handleCancelReply}
onClose={this.handleClosePreview}
onSendMessage={this.sendImageAttachment} />
</Suspense>
);
} else if (this.state.videoPreview) {
// Preview video.
component2 = (
component2 = <Suspense fallback={<div><FormattedMessage id="loading_note" defaultMessage="Loading..."
description="Message shown when component is loading"/></div>}>
<VideoPreview
content={this.state.videoPreview}
tinode={this.props.tinode}
@@ -1539,26 +1602,45 @@ class MessagesView extends React.Component {
onCancelReply={this.handleCancelReply}
onClose={this.handleClosePreview}
onSendMessage={this.sendVideoAttachment} />
);
</Suspense>;
} else if (this.state.imagePostview) {
// Expand received image.
component2 = (
component2 = <Suspense fallback={<div><FormattedMessage id="loading_note" defaultMessage="Loading..."
description="Message shown when component is loading"/></div>}>
<ImagePreview
content={this.state.imagePostview}
onClose={this.handleClosePreview} />
);
</Suspense>;
} else if (this.state.videoPostview) {
// Play received video.
component2 = (
component2 = <Suspense fallback={<div><FormattedMessage id="loading_note" defaultMessage="Loading..."
description="Message shown when component is loading"/></div>}>
<VideoPreview
content={this.state.videoPostview}
tinode={this.props.tinode}
onError={this.props.onError}
onClose={this.handleClosePreview} />
);
</Suspense>;
} else if (this.state.docPreview) {
// Preview attachment before sending.
component2 = (
if (TheCard.isFileSupported(this.state.docPreview.type, this.state.docPreview.name) ||
this.state.docPreview.type == TheCard.contentType) {
component2 = <Suspense fallback={<div><FormattedMessage id="loading_note" defaultMessage="Loading..."
description="Message shown when component is loading"/></div>}>
<TheCardPreview
content={this.state.docPreview}
tinode={this.props.tinode}
reply={this.state.reply}
onCancelReply={this.handleCancelReply}
onClose={this.handleClosePreview}
onSendMessage={this.sendFileAttachment} />
</Suspense>;
} else {
component2 = <Suspense fallback={<div><FormattedMessage id="loading_note" defaultMessage="Loading..."
description="Message shown when component is loading"/></div>}>
<DocPreview
content={this.state.docPreview}
tinode={this.props.tinode}
@@ -1566,7 +1648,8 @@ class MessagesView extends React.Component {
onCancelReply={this.handleCancelReply}
onClose={this.handleClosePreview}
onSendMessage={this.sendFileAttachment} />
);
</Suspense>;
}
} else {
const topic = this.props.tinode.getTopic(this.state.topic);
const isChannel = topic.isChannelType() || topic.chan;
@@ -1684,7 +1767,9 @@ class MessagesView extends React.Component {
let lastSeen = null;
if (isChannel) {
if (topic.subcnt) {
lastSeen = formatMessage(messages.subscriber_count, {count: topic.subcnt});
}
} else {
const cont = this.props.tinode.getMeTopic().getContact(this.state.topic);
if (cont) {
@@ -1697,10 +1782,12 @@ class MessagesView extends React.Component {
// TODO: also handle user agent in c.seen.ua
}
} else if (Tinode.isGroupTopicName(cont.topic)) {
if (topic.subcnt) {
lastSeen = formatMessage(messages.member_count, {count: topic.subcnt});
}
}
}
}
const avatar = this.state.avatar || true;
const online = this.state.deleted || topic.isSelfType() ? null :
this.props.online ? 'online' + (this.state.typingIndicator ? ' typing' : '') : 'offline';
@@ -1773,7 +1860,7 @@ class MessagesView extends React.Component {
null}
<div className="avatar-box">
<LetterTile
tinode={this.props.tinode}
authorizeURL={this.props.tinode.authorizeURL}
avatar={avatar}
topic={this.state.topic}
title={this.state.title}
+12 -5
View File
@@ -34,9 +34,13 @@ class NewTopicView extends React.Component {
constructor(props) {
super(props);
const urlParams = HashNavigation.parseUrlHash(window.location.hash).params;
const initialTab = urlParams['tab'] || 'find';
const initialQuery = initialTab == 'find' ? (urlParams['q'] || null) : null;
this.state = {
tabSelected: 'find',
searchQuery: null
tabSelected: initialTab,
searchQuery: initialQuery
};
this.handleTabClick = this.handleTabClick.bind(this);
@@ -83,19 +87,19 @@ class NewTopicView extends React.Component {
handleSearchResultSelected(topicName) {
if (this.state.tabSelected == 'find') {
HashNavigation.navigateTo(HashNavigation.removeUrlParam(window.location.hash, 'tab'));
HashNavigation.navigateTo(HashNavigation.removeUrlParams(window.location.hash, ['tab', 'q']));
this.props.onCreateTopic(topicName);
}
}
handleNewGroupSubmit(name, description, dataUrl, priv, tags, isChannel) {
HashNavigation.navigateTo(HashNavigation.removeUrlParam(window.location.hash, 'tab'));
HashNavigation.navigateTo(HashNavigation.removeUrlParams(window.location.hash, ['tab', 'q']));
this.props.onCreateTopic(undefined,
{public: theCard(name, dataUrl, null, description), private: priv, tags: tags}, isChannel);
}
handleGroupByID(topicName) {
HashNavigation.navigateTo(HashNavigation.removeUrlParam(window.location.hash, 'tab'));
HashNavigation.navigateTo(HashNavigation.removeUrlParams(window.location.hash, ['tab', 'q']));
this.props.onCreateTopic(topicName);
}
@@ -131,14 +135,17 @@ class NewTopicView extends React.Component {
tinode={this.props.tinode}
onSubmit={this.handleNewGroupSubmit}
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">
<SearchContacts
placeholder={search_placeholder}
initialQuery={this.state.searchQuery || ''}
onSearchContacts={this.handleSearchContacts} />
<ContactList
tinode={this.props.tinode}
+3
View File
@@ -254,6 +254,9 @@ class SidepanelView extends React.PureComponent {
<Suspense fallback={<div className="panel-form-row"><FormattedMessage id="loading_note"
defaultMessage="Loading..." description="Message shown when component is loading"/></div>}>
<WallpapersView
wallpaper={this.props.wallpaper}
wallpaperSize={this.props.wallpaperSize}
wallpaperBlur={this.props.wallpaperBlur}
colorSchema={this.props.colorSchema !== 'auto' ? this.props.colorSchema : this.props.systemColorSchema}
onWallpaperSelected={this.props.onWallpaperSelected}
/>
+49 -12
View File
@@ -15,7 +15,7 @@ import CallIncoming from '../widgets/call-incoming.jsx';
const PhoneCountrySelector = React.lazy(_ => import('../widgets/phone-country-selector.jsx'));
const InfoView = React.lazy(_ => import('./info-view.jsx'));
import MessagesView from './messages-view.jsx';
const MessagesView = React.lazy(_ => import('./messages-view.jsx'));
import SidepanelView from './sidepanel-view.jsx';
import { API_KEY, APP_NAME, DEFAULT_COLOR_SCHEME, DEFAULT_P2P_ACCESS_MODE, DEFAULT_TEXT_SIZE, FORWARDED_PREVIEW_LENGTH,
@@ -108,6 +108,7 @@ class TinodeWeb extends React.Component {
this.handleResize = this.handleResize.bind(this);
this.handleHashRoute = this.handleHashRoute.bind(this);
this.handleOnline = this.handleOnline.bind(this);
this.handleEscapeKey = this.handleEscapeKey.bind(this);
this.handleColorSchemeChange = this.handleColorSchemeChange.bind(this);
this.checkForAppUpdate = this.checkForAppUpdate.bind(this);
this.handleVisibilityEvent = this.handleVisibilityEvent.bind(this);
@@ -165,6 +166,7 @@ class TinodeWeb extends React.Component {
this.handleLeaveUnsubRequest = this.handleLeaveUnsubRequest.bind(this);
this.handleBlockTopicRequest = this.handleBlockTopicRequest.bind(this);
this.handleReportTopic = this.handleReportTopic.bind(this);
this.handleShareTheCard = this.handleShareTheCard.bind(this);
this.handleShowContextMenu = this.handleShowContextMenu.bind(this);
this.defaultTopicContextMenu = this.defaultTopicContextMenu.bind(this);
this.handleHideContextMenu = this.handleHideContextMenu.bind(this);
@@ -319,16 +321,15 @@ class TinodeWeb extends React.Component {
// Window/tab visible or invisible for pausing timers.
document.addEventListener('visibilitychange', this.handleVisibilityEvent);
// Listen for Escape key to clear selections.
document.addEventListener('keydown', this.handleEscapeKey);
document.documentElement.style.colorScheme = this.state.colorSchema == 'auto' ? 'light dark' : this.state.colorSchema;
document.documentElement.style.setProperty('--message-text-size', `${this.state.textSize}pt`);
this.applyWallpaperSettings(this.state.wallpaper, this.state.wallpaperSize, this.state.wallpaperBlur,
this.state.colorSchema == 'auto' ? this.state.systemColorSchema : this.state.colorSchema);
console.log("Wallpaper settings:", this.state.wallpaper, this.state.wallpaperSize, this.state.wallpaperBlur);
console.log("Wallpaper type:", this.state.wallpaperSize == 0 ? 'wpimg' : 'wppatt');
console.log("System color scheme:", this.state.systemColorSchema);
// Process background notifications from the service worker.
if (typeof BroadcastChannel == 'function') {
const serviceWorkerChannel = new BroadcastChannel('tinode-sw');
@@ -411,6 +412,7 @@ class TinodeWeb extends React.Component {
window.removeEventListener('offline', this.handleOnlineOff);
window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.handleColorSchemeChange);
document.removeEventListener('visibilitychange', this.handleVisibilityEvent);
document.removeEventListener('keydown', this.handleEscapeKey);
clearInterval(this.wakeUpTicker);
}
@@ -520,6 +522,27 @@ class TinodeWeb extends React.Component {
}
}
handleEscapeKey(e) {
if (e.key === 'Escape' || e.keyCode === 27) {
// Clear selections and navigate to base state
this.setState({
topicSelected: null,
errorText: '',
errorLevel: null,
mobilePanel: 'sidepanel',
topicSelectedOnline: false,
topicSelectedAcs: null,
infoPanel: undefined,
forwardMessage: null,
contextMenuVisible: false,
forwardDialogVisible: false,
alertVisible: false
});
// Navigate to contacts or login view
HashNavigation.navigateTo(HashNavigation.setUrlTopic('', null));
}
}
// Check if a newer version of TinodeWeb app is available at the server.
checkForAppUpdate(reg) {
reg.onupdatefound = _ => {
@@ -830,7 +853,9 @@ class TinodeWeb extends React.Component {
LocalStorageUtil.setObject('auth-token', this.tinode.getAuthToken());
}
const goToTopic = this.state.requestedTopic;
// On reconnect, requestedTopic is undefined but topicSelected may still be set.
// Preserve the open topic so it gets restored after re-login.
const goToTopic = this.state.requestedTopic || this.state.topicSelected;
// Logged in fine, subscribe to 'me' attaching callbacks from the contacts view.
const me = this.tinode.getMeTopic();
me.onMetaDesc = this.tnMeMetaDesc;
@@ -1127,7 +1152,6 @@ class TinodeWeb extends React.Component {
// - head - head dictionary to be attached to the message
handleSendMessage(msg, uploadCompletionPromise, uploader, head) {
const topic = this.tinode.getTopic(this.state.topicSelected);
/* TODO: check if return is required */
return this.sendMessageToTopic(topic, msg, uploadCompletionPromise, uploader, head);
}
@@ -1263,7 +1287,6 @@ class TinodeWeb extends React.Component {
// Manual change of color scheme: 'dark', 'light', 'auto'.
handleChangeColorSchema(schema) {
console.log("User color scheme change:", schema);
this.setState({colorSchema: schema});
LocalStorageUtil.updateObject('settings', {colorSchema: schema});
document.documentElement.style.colorScheme = schema == 'auto' ? 'light dark' : schema;
@@ -1273,14 +1296,12 @@ class TinodeWeb extends React.Component {
// System color scheme has changed.
handleColorSchemeChange(event) {
const systemSchema = event.matches ? 'dark' : 'light';
console.log("System color scheme change:", systemSchema);
this.setState({systemColorSchema: systemSchema});
this.applyColorSchema(this.state.colorSchema, systemSchema, this.state.wallpaperSize);
}
applyColorSchema(schema, systemSchema, size) {
const effectiveSchema = schema == 'auto' ? systemSchema : schema;
console.log("applyColorSchema:", effectiveSchema);
document.documentElement.style.setProperty('--wallpaper-invert', effectiveSchema == 'dark' && size ? '1' : '0');
document.documentElement.style.setProperty('--wallpaper-brightness', effectiveSchema == 'dark' && !size ? '0.5' : '1');
}
@@ -1710,7 +1731,7 @@ class TinodeWeb extends React.Component {
// If the Find panel is active, close it.
this.handleSidepanelCancel();
}
const header = '➦ ' + params.userName;
const header = params.userName ? '➦ ' + params.userName : '';
const content = typeof params.content == 'string' ? Drafty.init(params.content) : Drafty.forwardedContent(params.content);
const preview = Drafty.preview(content, FORWARDED_PREVIEW_LENGTH, true);
const msg = Drafty.append(Drafty.appendLineBreak(Drafty.mention(header, params.userFrom)), content);
@@ -1725,6 +1746,16 @@ class TinodeWeb extends React.Component {
});
}
handleShareTheCard(card) {
this.setState({
forwardDialogVisible: true,
forwardMessage: {
content: card,
type: card.contentType,
}
});
}
defaultTopicContextMenu(topicName) {
const topic = this.tinode.getTopic(topicName);
@@ -2186,6 +2217,9 @@ class TinodeWeb extends React.Component {
reqCredMethod={this.state.reqCredMethod}
textSize={this.state.textSize}
colorSchema={this.state.colorSchema}
wallpaper={this.state.wallpaper}
wallpaperSize={this.state.wallpaperSize}
wallpaperBlur={this.state.wallpaperBlur}
systemColorSchema={this.state.systemColorSchema}
sendOnEnter={this.state.sendOnEnter}
@@ -2232,6 +2266,8 @@ class TinodeWeb extends React.Component {
: null}
{!this.state.displayMobile || (this.state.mobilePanel == 'topic-view' && !this.state.infoPanel) ?
<Suspense fallback={<div><FormattedMessage id="loading_note" defaultMessage="Loading..."
description="Message shown when component is loading"/></div>}>
<MessagesView
tinode={this.tinode}
connected={this.state.connected}
@@ -2282,6 +2318,7 @@ class TinodeWeb extends React.Component {
onNewChat={this.handleNewChatInvitation}
sendMessage={this.handleSendMessage}
onVideoCallClosed={this.handleCallClose} />
</Suspense>
: null}
{this.state.infoPanel ?
@@ -2311,7 +2348,7 @@ class TinodeWeb extends React.Component {
onLeaveTopic={this.handleLeaveUnsubRequest}
onBlockTopic={this.handleBlockTopicRequest}
onReportTopic={this.handleReportTopic}
onAddMember={this.handleManageGroupMembers}
onShareTheCard={this.handleShareTheCard}
onTopicTagsUpdateRequest={this.handleTagsUpdateRequest}
onTopicUnArchive={this.handleUnarchive}
onInitFind={this.tnInitFind}
+21 -6
View File
@@ -3,17 +3,20 @@ import React from 'react';
import { FormattedMessage } from 'react-intl';
import HashNavigation from '../lib/navigation.js';
import { WALLPAPER_DEFAULTS } from '../config.js';
import { wallpaperNameFromUrl, wallpaperTypeFromUrl } from '../lib/utils.js';
import imageIndex from '../../img/bkg/index.json';
export default class WallpapersView extends React.PureComponent {
constructor(props) {
super(props);
const type = wallpaperTypeFromUrl(this.props.wallpaper);
const name = wallpaperNameFromUrl(this.props.wallpaper);
this.state = {
tab: WALLPAPER_DEFAULTS.type, // currently selected tab: 'patt' | 'img'
selectedType: WALLPAPER_DEFAULTS.type, // type of the selected wallpaper.
wallpaper: imageIndex[WALLPAPER_DEFAULTS.type][WALLPAPER_DEFAULTS.index].name,
blur: 0,
tab: type || WALLPAPER_DEFAULTS.type, // currently selected tab: 'patt' | 'img'
selectedType: type || WALLPAPER_DEFAULTS.type, // type of the selected wallpaper.
wallpaper: name || imageIndex[WALLPAPER_DEFAULTS.type][WALLPAPER_DEFAULTS.index].name,
blur: this.props.wallpaperBlur | 0,
};
this.blurValues = [0, 1, 2, 4, 8, 16];
@@ -21,6 +24,7 @@ export default class WallpapersView extends React.PureComponent {
this.handleTabClick = this.handleTabClick.bind(this);
this.handleWallpaperSelected = this.handleWallpaperSelected.bind(this);
this.handleBlurChanged = this.handleBlurChanged.bind(this);
this.hasChanged = this.hasChanged.bind(this);
}
handleTabClick(e) {
@@ -32,6 +36,10 @@ export default class WallpapersView extends React.PureComponent {
handleWallpaperSelected(e) {
let index, type, blur
if (!e) {
if (!this.hasChanged()) {
return;
}
// Restore default.
type = WALLPAPER_DEFAULTS.type;
index = WALLPAPER_DEFAULTS.index;
@@ -58,11 +66,18 @@ export default class WallpapersView extends React.PureComponent {
}
}
hasChanged() {
return this.state.selectedType != WALLPAPER_DEFAULTS.type ||
this.state.wallpaper != imageIndex[WALLPAPER_DEFAULTS.type][WALLPAPER_DEFAULTS.index].name ||
this.state.blur != 0;
}
render() {
return (
<div className="flex-column">
<div className="panel-form-row clean-clickable">
<a className="flat-button" onClick={() => this.handleWallpaperSelected(null)}>
<div className={`panel-form-row${this.hasChanged() ? ' clean-clickable' : ''}`}>
<a className={`flat-button${this.hasChanged() ? '' : ' disabled'}`}
onClick={() => this.handleWallpaperSelected(null)}>
<i className="material-icons">undo</i>&nbsp;
<FormattedMessage id="button_restore" defaultMessage="Restore default" description="Reset setting to default"/>
</a>
+1 -1
View File
@@ -48,7 +48,7 @@ export default class AvatarUpload extends React.Component {
this.props.readOnly && this.props.uid ?
<div className="avatar-box">
<LetterTile
tinode={this.props.tinode}
authorizeURL={this.props.tinode.authorizeURL}
avatar={true}
topic={this.props.uid}
title={this.props.title} />
+1 -1
View File
@@ -122,7 +122,7 @@ export default class CallIncoming extends React.Component {
<div className="caller-card incoming pulse">
<div className="avatar-box">
<LetterTile
tinode={this.props.tinode}
authorizeURL={this.props.tinode.authorizeURL}
avatar={this.state.avatar || true}
topic={this.props.topic}
title={this.state.fullName} />
+3 -3
View File
@@ -346,11 +346,11 @@ class CallPanel extends React.PureComponent {
}
handleDataChannelClose(event) {
console.log('close data channel:', event);
console.info('close data channel:', event);
}
handleDataChannelEvent(event) {
console.log('data channel event:', event);
console.info('data channel event:', event);
const channel = event.channel;
channel.onerror = this.handleDataChannelError;
channel.onmessage = this.handleDataChannelMessage;
@@ -680,7 +680,7 @@ class CallPanel extends React.PureComponent {
<div className={`caller-card${pulseAnimation} draggable`}>
<div className="avatar-box">
<LetterTile
tinode={this.props.tinode}
authorizeURL={this.props.tinode.authorizeURL}
avatar={this.props.avatar}
topic={this.props.topic}
title={this.props.title} />
+46 -8
View File
@@ -2,7 +2,7 @@
import React from 'react';
import { FormattedMessage, injectIntl } from 'react-intl';
import { Drafty, Tinode } from 'tinode-sdk';
import { Drafty, Tinode, TheCard } from 'tinode-sdk';
import Attachment from './attachment.jsx';
import LetterTile from './letter-tile.jsx';
@@ -10,6 +10,7 @@ import ReceivedMarker from './received-marker.jsx'
import { fullFormatter } from '../lib/formatters.js';
import { sanitizeUrl } from '../lib/utils.js';
import HashNavigation from '../lib/navigation.js';
class BaseChatMessage extends React.PureComponent {
constructor(props) {
@@ -26,22 +27,59 @@ class BaseChatMessage extends React.PureComponent {
this.handleExpandImage = this.handleExpandImage.bind(this);
this.handlePlayVideo = this.handlePlayVideo.bind(this);
this.handleFormButtonClick = this.handleFormButtonClick.bind(this);
this.handleQuoteClick = this.handleQuoteClick.bind(this);
this.handleContextClick = this.handleContextClick.bind(this);
this.handleCancelUpload = this.handleCancelUpload.bind(this);
this.handleQuoteClick = this.handleQuoteClick.bind(this);
this.handleDraftyClick = this.handleDraftyClick.bind(this);
this.formatterContext = {
formatMessage: props.intl.formatMessage.bind(props.intl),
viewportWidth: props.viewportWidth,
authorizeURL: props.tinode.authorizeURL.bind(props.tinode),
onImagePreview: this.handleExpandImage,
onVideoPreview: this.handlePlayVideo,
onFormButtonClick: this.handleFormButtonClick,
onQuoteClick: this.handleQuoteClick
onHandleClick: this.handleDraftyClick
};
}
handleDraftyClick = (e, action) => {
switch (action) {
case 'image':
this.handleExpandImage(e);
break;
case 'video':
this.handlePlayVideo(e);
break;
case 'form_button':
this.handleFormButtonClick(e);
break;
case 'quote':
this.handleQuoteClick(e);
break;
case 'contact_chat':
e.preventDefault();
try {
// tinode:topic/usr123abc -> usr123abc
const pathname = new URL(e.target.dataset.val)?.pathname;
const parts = pathname.split('/').filter(Boolean);
HashNavigation.navigateTo(HashNavigation.setUrlTopic('', parts.pop() || ''));
} catch (error) {
console.error("Invalid URL:", error);
}
break;
case 'contact_find':
e.preventDefault();
let hashUrl = HashNavigation.setUrlSidePanel(window.location.hash, 'newtpk');
hashUrl = HashNavigation.addUrlParam(hashUrl, 'q', e.target.dataset.val);
hashUrl = HashNavigation.addUrlParam(hashUrl, 'tab', 'find');
HashNavigation.navigateTo(hashUrl);
break;
default:
// No special handling; let the browser deal with it.
console.info('Unhandled drafty action.', action, e.target.dataset);
break;
}
}
handleExpandImage(e) {
e.preventDefault();
this.props.onExpandMedia({
@@ -99,7 +137,7 @@ class BaseChatMessage extends React.PureComponent {
if (!this.props.response) {
let immutable = false;
Drafty.entities(this.props.content, (_0, _1, tp) => {
immutable = ['AU', 'EX', 'FM', 'IM', 'VC', 'VD'].includes(tp);
immutable = ['AU', 'EX', 'FM', 'IM', 'TC', 'VC', 'VD'].includes(tp);
return immutable;
});
if (!immutable) {
@@ -200,7 +238,7 @@ class BaseChatMessage extends React.PureComponent {
<div className="avatar-box">
{fullDisplay ?
<LetterTile
tinode={this.props.tinode}
authorizeURL={this.props.tinode.authorizeURL}
topic={this.props.userFrom}
title={this.props.userName}
avatar={avatar} /> :
+1 -2
View File
@@ -23,7 +23,6 @@ export default class ChipInput extends React.Component {
static deriveStateFromProps(props) {
return {
placeholder: props.chips ? '' : props.prompt,
sortedChips: ChipInput.sortChips(props.chips, props.staticMembers),
chipIndex: ChipInput.indexChips(props.chips)
};
@@ -137,7 +136,7 @@ export default class ChipInput extends React.Component {
<div className={className}>
{chips}
<input type="text"
placeholder={this.state.placeholder}
placeholder={this.props.prompt}
onChange={this.handleTextInput}
onFocus={this.handleFocusGained}
onBlur={this.handleFocusLost}
+1 -1
View File
@@ -23,7 +23,7 @@ export default class Chip extends React.PureComponent {
<span className="spacer" /> :
<div className="avatar-box">
<LetterTile
tinode={this.props.tinode}
authorizeURL={this.props.tinode.authorizeURL}
avatar={this.props.avatar || true}
topic={this.props.topic}
title={this.props.title} />
+10 -3
View File
@@ -25,7 +25,7 @@ const messages = defineMessages({
}
});
class ContactList extends React.Component {
class ContactList extends React.PureComponent {
render() {
const { formatMessage } = this.props.intl;
const showCheckmark = Array.isArray(this.props.topicSelected);
@@ -53,6 +53,8 @@ class ContactList extends React.Component {
contactsCount ++;
}
if (this.props.contacts && this.props.contacts.length > 0) {
// Used keys to prevent duplicates when the user comes both from the 'fnd' and the list of contacts.
const usedKeys = {};
this.props.contacts.forEach(c => {
if (c.action) {
// Action item
@@ -65,6 +67,10 @@ class ContactList extends React.Component {
} else {
// Normal contact
const key = this.props.showMode ? c.user : (c.topic || c.user);
if (usedKeys[key]) {
return;
}
usedKeys[key] = true;
// If filter function is provided, filter out the items
// which don't satisfy the condition.
if (this.props.filterFunc && this.props.filter) {
@@ -154,8 +160,9 @@ class ContactList extends React.Component {
return (
<div className={this.props.noScroll ? null : "scrollable-panel"}>
{contactsCount == 0 ?
<div className="center-medium-text"
dangerouslySetInnerHTML={{__html: this.props.emptyListMessage}} />
<div className="center-medium-text" style={{whiteSpace: 'pre-line'}}>
{this.props.emptyListMessage}
</div>
:
null
}
+1 -1
View File
@@ -122,7 +122,7 @@ class Contact extends React.Component {
<li className={bgColor} onClick={this.handleClick}>
<div className="avatar-box">
<LetterTile
tinode={this.props.tinode}
authorizeURL={this.props.tinode.authorizeURL}
avatar={avatar}
title={this.props.title}
topic={this.props.item}
+1 -1
View File
@@ -46,7 +46,7 @@ export default class DocPreview extends React.PureComponent {
}
return (
<div id="image-preview">
<div id="image-preview-caption-panel">
<div id="preview-caption-panel">
<span>{this.props.content.name}</span>
<a href="#" onClick={(e) => {e.preventDefault(); this.props.onClose();}}><i className="material-icons gray">close</i></a>
</div>
+6 -1
View File
@@ -15,6 +15,11 @@ const messages = defineMessages({
id: 'contacts_not_found_short',
defaultMessage: "No contacts match ''{query}''",
description: 'Shown in ContactsView when search returned no results'
},
add_members_prompt: {
id: 'add_members_prompt',
defaultMessage: 'add members',
description: 'Placeholder in group members input field'
}
});
@@ -166,7 +171,7 @@ class GroupManager extends React.Component {
tinode={this.props.tinode}
chips={this.state.members}
staticMembers={this.state.staticMembers}
prompt="add members"
prompt={this.props.intl.formatMessage(messages.add_members_prompt)}
filterFunc={this.handleContactFilter}
onChipRemoved={this.handleMemberRemoved} />
</div>
+1 -1
View File
@@ -22,7 +22,7 @@ export default class GroupSubs extends React.Component {
usersOnline.push(
<div className="avatar-box" key={sub.user}>
<LetterTile
tinode={this.props.tinode}
authorizeURL={this.props.tinode.authorizeURL}
topic={sub.user}
avatar={makeImageUrl(sub.public ? sub.public.photo : null) || true}
title={sub.public ? sub.public.fn : null} />
+1 -1
View File
@@ -77,7 +77,7 @@ export default class ImagePreview extends React.PureComponent {
return (
<div id="image-preview">
<div id="image-preview-caption-panel">
<div id="preview-caption-panel">
{this.props.onSendMessage ?
<span>{fname}</span>
:
+1 -1
View File
@@ -27,7 +27,7 @@ export default class LetterTile extends React.PureComponent {
}
}
} else if (this.props.avatar) {
const url = this.props.tinode.authorizeURL(sanitizeUrlForMime(this.props.avatar, 'image'));
const url = this.props.authorizeURL(sanitizeUrlForMime(this.props.avatar, 'image'));
// If avatar image is invalid, show a placeholder.
const className = 'avatar' + (this.props.deleted ? ' deleted' : '');
avatar = <img className={className} alt="avatar" src={url}
+3 -1
View File
@@ -77,7 +77,9 @@ class NewTopicById extends React.PureComponent {
</div>
<br />
<div className="panel-form-column">
<label className="small">Scan my ID:</label>
<label className="small"><FormattedMessage id="label_scan_id"
defaultMessage="Scan my ID:" description="Label [Scan my ID:]" />
</label>
<div className="qr-code" ref={this.qrCodeRef} />
</div>
</div>
+1 -1
View File
@@ -161,7 +161,7 @@ export default class NewTopicGroup extends React.PureComponent {
<div className="group">
<label className="small" htmlFor="new-topic-desc">
<FormattedMessage id="label_description" defaultMessage="Description"
description="Label for editing topic description" />
description="Label for description of user or topic" />
</label>
<FormattedMessage id="description_editing_placeholder" defaultMessage="Description (optional)"
description="Placeholder for editing topic description">{
+3 -2
View File
@@ -8,7 +8,7 @@ export default class SearchContacts extends React.PureComponent {
this.state = {
edited: false,
search: ''
search: this.props.initialQuery || ''
};
this.handleSearchChange = this.handleSearchChange.bind(this);
@@ -47,7 +47,8 @@ export default class SearchContacts extends React.PureComponent {
if (e.key === 'Enter') {
this.handleSearch(e);
} else if (e.key === 'Escape') {
this.handleClear();
e.stopPropagation();
this.handleClear(e);
}
}
+1
View File
@@ -23,6 +23,7 @@ export default class ShowQRCode extends React.PureComponent {
<div className="panel-form-column">
<br />
<div className="qr-code" ref={this.qrCodeRef} />
<div><tt>{this.props.uri}</tt></div>
<div className="dialog-buttons">
<button className="outline" onClick={this.props.onCancel}>
<FormattedMessage id="button_ok" defaultMessage="OK" description="Button [OK]" />
+1 -1
View File
@@ -26,7 +26,7 @@ export default class SideNavbar extends React.PureComponent {
{avatar ?
<div id="self-avatar" className="avatar-box">
<LetterTile
tinode={this.props.tinode}
authorizeURL={this.props.tinode.authorizeURL}
avatar={avatar}
topic={this.props.myUserId}
title={this.props.title} />
+101
View File
@@ -0,0 +1,101 @@
import React from 'react';
import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
import { TheCard } from 'tinode-sdk';
import LetterTile from './letter-tile.jsx';
const messages = defineMessages({
unknown_name: {
id: 'unknown_name',
defaultMessage: 'Unknown',
description: 'Name to show when the name is missing'
}
});
class TheCardMini extends React.PureComponent {
constructor(props) {
super(props);
this.handleDownload = this.handleDownload.bind(this);
this.handleMessage = this.handleMessage.bind(this);
}
handleDownload(e) {
e.preventDefault();
try {
const vcard = TheCard.exportVCard(this.props.content);
const blob = new Blob([vcard], { type: 'text/vcard' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.style.display = 'none';
a.href = url;
a.download = (this.props.content.fn || 'contact') + '.vcf';
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
} catch (err) {
console.error("Failed to download vcard", err);
if (this.props.onError) {
this.props.onError("Failed to download vcard", 'err');
}
}
}
handleMessage(e) {
// This is handled by the parent component via props.onAction which expects a click event.
}
render() {
const card = this.props.content;
if (!card) {
return null;
}
const uid = TheCard.getFirstTinodeID(card);
const contacts = TheCard.getEmails(card).concat(TheCard.getPhones(card));
const org = TheCard.getOrg(card);
return (
<div className="contact-card">
<div className="contact-body">
<div className="avatar-box">
<LetterTile
authorizeURL={this.props.authorizeURL}
topic={uid || 'usr123XXX'}
avatar={TheCard.getPhotoUrl(card) || true}
title={card.fn} />
</div>
<div className="name-box">
<div className="name">{card.fn || <FormattedMessage {...messages.unknown_name} />}</div>
{org && <div className="org">{org}</div>}
</div>
</div>
<div className="contact-actions">
{uid ?
<>
<a className="flat-button" data-val={uid} onClick={this.props.onChatClick}>
<FormattedMessage id="chat_now" defaultMessage="Chat"
description="Label for message button in contact card" />
</a>
<div className="divider"></div>
</>
: contacts.length > 0 ?
<>
<a className="flat-button" data-val={contacts.join(',')} onClick={this.props.onFindClick}>
<FormattedMessage id="find_user" defaultMessage="Find"
description="Label for search button in contact card" />
</a>
<div className="divider"></div>
</>
: null
}
<a className="flat-button" onClick={this.handleDownload}>
<FormattedMessage id="save_action" defaultMessage="Save" description="Label for Save button" />
</a>
</div>
</div>
);
}
};
export default injectIntl(TheCardMini);
+185
View File
@@ -0,0 +1,185 @@
import React from 'react';
import { defineMessages, FormattedMessage } from 'react-intl';
import { parsePhoneNumber } from 'libphonenumber-js/mobile';
import SendMessage from './send-message.jsx';
import LetterTile from './letter-tile.jsx';
import { TheCard } from 'tinode-sdk';
import { importVCard } from '../lib/blob-helpers.js';
const messages = defineMessages({
unknown_name: {
id: 'unknown_name',
defaultMessage: 'Unknown',
description: 'Name to show when the name is missing'
}
});
export default class TheCardPreview extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
content: props.content.object
};
if (!props.content.object) {
importVCard(props.content.file)
.then(card => {
this.setState({ content: card });
});
}
this.handleSendCard = this.handleSendCard.bind(this);
this.handleKeyDown = this.handleKeyDown.bind(this);
}
componentDidMount() {
document.addEventListener('keydown', this.handleKeyDown);
}
componentWillUnmount() {
document.removeEventListener('keydown', this.handleKeyDown);
}
handleKeyDown(e) {
e.preventDefault();
if (e.key === 'Escape') {
this.props.onClose();
}
}
handleSendCard() {
this.props.onClose();
this.props.onSendMessage(this.props.content);
}
render() {
const card = this.state.content;
const fullName = TheCard.getFn(card);
const photoUrl = TheCard.getPhotoUrl(card) || null;
const note = card?.note;
const org = TheCard.getOrg(card);
// Get communication methods
const emails = TheCard.getComm(card, 'email') || [];
const phones = TheCard.getComm(card, 'tel') || [];
const tinodeIds = TheCard.getComm(card, 'tinode') || [];
const urls = TheCard.getComm(card, 'http') || [];
const contacts = [];
// Add emails
emails.forEach((email, idx) => {
const types = Array.isArray(email.des) ? email.des.join(', ') : '';
contacts.push(
<div className="group quoted" key={`email-${idx}`}>
<tt>{email.value}</tt>
<span className="small gray"> {types ? `(${types})` : ''}</span>
</div>
);
});
// Add phones
phones.forEach((phone, idx) => {
let val = phone.value;
try {
const number = parsePhoneNumber(phone.value);
val = number ? number.formatInternational() : phone.value;
} catch (err) {
// Use original value if parsing fails
}
const types = Array.isArray(phone.des) ? phone.des.join(', ') : '';
contacts.push(
<div className="group quoted" key={`tel-${idx}`}>
<tt>{val}</tt>
<span className="small gray"> {types ? `(${types})` : ''}</span>
</div>
);
});
// Add Tinode IDs
tinodeIds.forEach((tid, idx) => {
const types = Array.isArray(tid.des) ? tid.des.join(', ') : '';
contacts.push(
<div className="group quoted" key={`tinode-${idx}`}>
<tt>{tid.value}</tt>
<span className="small gray"> {types ? `(${types})` : ''}</span>
</div>
);
});
// Add URLs
urls.forEach((url, idx) => {
const types = Array.isArray(url.des) ? url.des.join(', ') : '';
contacts.push(
<div className="group quoted" key={`url-${idx}`}>
<tt>{url.value}</tt>
<span className="small gray"> {types ? `(${types})` : ''}</span>
</div>
);
});
return (
<div id="image-preview">
<div id="preview-caption-panel">
<span><FormattedMessage id="label_contact_card" defaultMessage="Contact Card"
description="Label for contact card preview" /></span>
<a href="#" onClick={(e) => {e.preventDefault(); this.props.onClose();}}>
<i className="material-icons gray">close</i>
</a>
</div>
<div id="image-preview-container">
<div className="doc-card">
<div className="panel-form-column">
<center>
<div className="avatar-box">
<LetterTile
authorizeURL={this.props.tinode.authorizeURL}
avatar={photoUrl || true}
topic="usrXXX123"
title={fullName} />
</div>
</center>
<div className="group">
<div className="large ellipsized">{fullName || <FormattedMessage {...messages.unknown_name} />}</div>
</div>
{org ?
<div className="group">
<div>{org}</div>
</div>
: null}
{note ?
<div className="group">
<label className="small">
<FormattedMessage id="label_description" defaultMessage="Description"
description="Label for description of user or topic" />
</label>
<div className="quoted">{note}</div>
</div>
: null}
{contacts.length > 0 ?
<div className="group">
<label className="small">
<FormattedMessage id="label_contacts" defaultMessage="Contacts"
description="Label for contact information" />
</label>
{contacts}
</div>
: null}
</div>
</div>
</div>
<SendMessage
noInput={true}
tinode={this.props.tinode}
reply={this.props.reply}
onCancelReply={this.props.onCancelReply}
onSendMessage={this.handleSendCard}
onError={this.props.onError} />
</div>
);
}
}
+1 -1
View File
@@ -363,7 +363,7 @@ class TopicDescEdit extends React.Component {
<div className="group">
<div><label className="small">
<FormattedMessage id="label_description" defaultMessage="Description"
description="Label for editing topic description" />
description="Label for description of user or topic" />
</label></div>
<div>
<FormattedMessage id="description_editing_placeholder"
+1 -1
View File
@@ -81,7 +81,7 @@ class VideoPreview extends React.PureComponent {
return (
<div id="image-preview">
<div id="image-preview-caption-panel">
<div id="preview-caption-panel">
<span>{this.props.content.filename}</span>
<a href="#" onClick={e => {e.preventDefault(); this.props.onClose();}}><i className="material-icons gray">close</i></a>
</div>
+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
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1 +1 @@
{"version":3,"file":"316.prod.js","mappings":"qkNASA,MAAMA,UAA6BC,IAAAA,cACjCC,WAAAA,CAAYC,GACVC,MAAMD,GAENE,KAAKC,UAAY,GACjB,MAAM,kBAAEC,GAAsBJ,EAAMK,KACpCC,EAAeC,QAAQC,IACPA,EAAGC,KAAKC,MAAM,KACtBH,QAAQI,IACZT,KAAKC,UAAUS,KAAK,CAClBH,KAAME,EAAKE,OACXC,KAAMN,EAAGM,KACTC,MAAMC,EAAAA,EAAAA,IAAUR,EAAGM,MACnBG,KAAMb,EAAkBI,EAAGM,KAAM,CAACI,KAAM,iBAK9ChB,KAAKC,UAAUgB,KAAK,CAACC,EAAGC,IAAMD,EAAEH,KAAKK,cAAcD,EAAEJ,MACvD,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,QAAQ,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,IACzDC,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,UAKxCX,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":""}
{"version":3,"file":"316.prod.js","mappings":"qkNASA,MAAMA,UAA6BC,IAAAA,cACjCC,WAAAA,CAAYC,GACVC,MAAMD,GAENE,KAAKC,UAAY,GACjB,MAAM,kBAAEC,GAAsBJ,EAAMK,KACpCC,EAAeC,QAAQC,IACPA,EAAGC,KAAKC,MAAM,KACtBH,QAAQI,IACZT,KAAKC,UAAUS,KAAK,CAClBH,KAAME,EAAKE,OACXC,KAAMN,EAAGM,KACTC,MAAMC,EAAAA,EAAAA,IAAUR,EAAGM,MACnBG,KAAMb,EAAkBI,EAAGM,KAAM,CAACI,KAAM,iBAK9ChB,KAAKC,UAAUgB,KAAK,CAACC,EAAGC,IAAMD,EAAEH,KAAKK,cAAcD,EAAEJ,MACvD,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,QAAQ,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,IACzDC,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,UAKxCX,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"],"ignoreList":[],"sourceRoot":""}
+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 -1
View File
File diff suppressed because one or more lines are too long
+2
View File
@@ -0,0 +1,2 @@
"use strict";(self.webpackChunktinode_webapp=self.webpackChunktinode_webapp||[]).push([[371],{6371:function(e,t,n){n.r(t);var i=n(1594),o=n.n(i),s=n(8181),r=n(1725),a=n(8589);const l=(0,s.defineMessages)({unrecognized_video_format:{id:"unrecognized_video_format",defaultMessage:[{type:0,value:"Format of this video is not recognized"}]}});class d extends o().PureComponent{constructor(e){super(e),this.videoRef=o().createRef(),this.handleSendVideo=this.handleSendVideo.bind(this),this.handleKeyDown=this.handleKeyDown.bind(this)}componentDidMount(){document.addEventListener("keydown",this.handleKeyDown)}componentWillUnmount(){document.removeEventListener("keydown",this.handleKeyDown)}handleKeyDown(e){this.props.onSendMessage||(e.preventDefault(),"Escape"===e.key&&this.props.onClose())}handleSendVideo(e){this.props.onClose();const t={width:this.videoRef.current.videoWidth,height:this.videoRef.current.videoHeight,duration:1e3*this.videoRef.current.duration|0,mime:this.props.content.mime,name:this.props.content.filename};if(0==t.width||0==t.height)return void this.props.onError(this.props.intl.formatMessage(l.unrecognized_video_format),"err");const n=document.createElement("canvas");n.width=t.width,n.height=t.height;const i=n.getContext("2d");i.drawImage(this.videoRef.current,0,0,n.width,n.height),i.canvas.toBlob(n=>this.props.onSendMessage(e,this.props.content.blob,n,t),"image/jpeg",.75)}render(){if(!this.props.content)return null;const e=this.props.content.width||"-",t=this.props.content.height||"-",n=this.props.onSendMessage?"nodownload":"",i=!this.props.onSendMessage;return o().createElement("div",{id:"image-preview"},o().createElement("div",{id:"preview-caption-panel"},o().createElement("span",null,this.props.content.filename),o().createElement("a",{href:"#",onClick:e=>{e.preventDefault(),this.props.onClose()}},o().createElement("i",{className:"material-icons gray"},"close"))),o().createElement("div",{id:"image-preview-container"},o().createElement("video",{className:"image-preview",controls:!0,controlsList:n,disablePictureInPicture:!0,ref:this.videoRef,autoPlay:i,src:this.props.tinode.authorizeURL(this.props.content.url),poster:this.props.content.preview,alt:this.props.content.filename})),this.props.onSendMessage?o().createElement(r.A,{messagePrompt:"add_image_caption",acceptBlank:!0,tinode:this.props.tinode,reply:this.props.reply,onCancelReply:this.props.onCancelReply,onSendMessage:this.handleSendVideo,onError:this.props.onError}):o().createElement("div",{id:"image-preview-footer"},o().createElement("div",null,o().createElement("div",null,o().createElement("b",null,o().createElement(s.FormattedMessage,{id:"label_file_name",defaultMessage:[{type:0,value:"File name:"}]}))),o().createElement("div",null,o().createElement("span",{title:this.props.content.filename},this.props.content.filename))),o().createElement("div",null,o().createElement("div",null,o().createElement("b",null,o().createElement(s.FormattedMessage,{id:"label_content_type",defaultMessage:[{type:0,value:"Content type:"}]}))),o().createElement("div",null,this.props.content.type)),o().createElement("div",null,o().createElement("div",null,o().createElement("b",null,o().createElement(s.FormattedMessage,{id:"label_size",defaultMessage:[{type:0,value:"Size:"}]}))),o().createElement("div",null,e," × ",t," px; ",(0,a.lX)(this.props.content.size)))))}}t.default=(0,s.injectIntl)(d)}}]);
//# sourceMappingURL=371.prod.js.map
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
+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
@@ -0,0 +1,2 @@
"use strict";(self.webpackChunktinode_webapp=self.webpackChunktinode_webapp||[]).push([[472],{4472:function(e,t,n){n.r(t),n.d(t,{default:function(){return r}});var a=n(1594),l=n.n(a),s=n(8181),o=n(1725),i=n(8589);class r extends l().PureComponent{constructor(e){super(e),this.handleSendDoc=this.handleSendDoc.bind(this),this.handleKeyDown=this.handleKeyDown.bind(this)}componentDidMount(){document.addEventListener("keydown",this.handleKeyDown)}componentWillUnmount(){document.removeEventListener("keydown",this.handleKeyDown)}handleKeyDown(e){e.preventDefault(),"Escape"===e.key&&this.props.onClose()}handleSendDoc(e){this.props.onClose(),this.props.onSendMessage(this.props.content.file)}render(){return this.props.content?l().createElement("div",{id:"image-preview"},l().createElement("div",{id:"preview-caption-panel"},l().createElement("span",null,this.props.content.name),l().createElement("a",{href:"#",onClick:e=>{e.preventDefault(),this.props.onClose()}},l().createElement("i",{className:"material-icons gray"},"close"))),l().createElement("div",{id:"image-preview-container"},l().createElement("div",{className:"doc-card"},l().createElement("i",{className:"material-icons gray"},function(e){const t={default:"insert_drive_file",image:"image",text:"description",video:"theaters"};return t[e]||t[(e||"").split("/")[0]]||t.default}(this.props.content.type)),l().createElement("div",null,l().createElement("b",null,l().createElement(s.FormattedMessage,{id:"label_file_name",defaultMessage:[{type:0,value:"File name:"}]}))," ",(0,i.Tu)(this.props.content.name,24)||"-"),l().createElement("div",null,l().createElement("b",null,l().createElement(s.FormattedMessage,{id:"label_content_type",defaultMessage:[{type:0,value:"Content type:"}]}))," ",this.props.content.type||"application/octet-stream"),l().createElement("div",null,l().createElement("b",null,l().createElement(s.FormattedMessage,{id:"label_size",defaultMessage:[{type:0,value:"Size:"}]}))," ",(0,i.lX)(this.props.content.size)))),l().createElement(o.A,{noInput:!0,tinode:this.props.tinode,reply:this.props.reply,onCancelReply:this.props.onCancelReply,onSendMessage:this.handleSendDoc,onError:this.props.onError})):null}}}}]);
//# sourceMappingURL=472.prod.js.map
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
+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

Some files were not shown because too many files have changed in this diff Show More