merge devel

This commit is contained in:
or-else
2026-03-03 13:36:48 +03:00
149 changed files with 15975 additions and 4765 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).
+110 -3
View File
@@ -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;
@@ -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 */
@@ -3533,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
+3 -2
View File
@@ -1,7 +1,7 @@
{
"name": "tinode-webapp",
"description": "Tinode messenger for the web",
"version": "0.25.1",
"version": "0.25.2",
"repository": {
"type": "git",
"url": "git+https://github.com/tinode/webapp.git"
@@ -62,6 +62,7 @@
"ru",
"th",
"uk",
"vi",
"zh",
"zh-TW"
],
@@ -80,7 +81,7 @@
"react": "^18.3.0",
"react-dom": "^18.3.0",
"react-intl": "^7.0.0",
"tinode-sdk": "^0.25.1",
"tinode-sdk": "^0.25.2",
"webm-duration-fix": "^1.0.4"
},
"devDependencies": {
+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
+59 -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
},
@@ -1906,5 +1906,61 @@
"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
}
}
+59 -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
},
@@ -1906,5 +1906,61 @@
"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
}
}
+59 -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
},
@@ -1906,5 +1906,61 @@
"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
}
}
+59 -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
},
@@ -1906,5 +1906,61 @@
"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
}
}
+34 -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:",
@@ -880,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"
@@ -1056,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"
+59 -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
},
@@ -1906,5 +1906,61 @@
"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
}
}
+59 -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
},
@@ -1906,5 +1906,61 @@
"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
}
}
+59 -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
},
@@ -1906,5 +1906,61 @@
"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
}
}
+59 -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
},
@@ -1906,5 +1906,61 @@
"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
}
}
+59 -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
},
@@ -1906,5 +1906,61 @@
"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
}
}
+59 -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
},
@@ -1906,5 +1906,61 @@
"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
}
}
+59 -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
},
@@ -1906,5 +1906,61 @@
"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
+59 -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
},
@@ -1906,5 +1906,61 @@
"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
}
}
+59 -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
},
@@ -1906,5 +1906,61 @@
"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.1";
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}
+160 -77
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) {
// We are forwarding a message. Show preview.
nextState.reply = {
content: nextProps.forwardMessage.preview,
seq: null
};
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,85 +1547,109 @@ 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 ? (
<CallPanel
topic={this.state.topic}
seq={this.props.callSeq}
callState={this.props.callState}
callAudioOnly={this.props.callAudioOnly}
tinode={this.props.tinode}
title={this.state.title}
avatar={this.state.avatar || true}
minimized={this.state.minimizedCallPanel}
onError={this.props.onError}
onHangup={this.handleCallHangup}
onToggleMinimize={this.handleCallPanelToggle}
onInvite={this.props.onCallInvite}
onSendOffer={this.props.onCallSendOffer}
onIceCandidate={this.props.onCallIceCandidate}
onSendAnswer={this.props.onCallSendAnswer} />
) : null;
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}
callState={this.props.callState}
callAudioOnly={this.props.callAudioOnly}
tinode={this.props.tinode}
title={this.state.title}
avatar={this.state.avatar || true}
minimized={this.state.minimizedCallPanel}
onError={this.props.onError}
onHangup={this.handleCallHangup}
onToggleMinimize={this.handleCallPanelToggle}
onInvite={this.props.onCallInvite}
onSendOffer={this.props.onCallSendOffer}
onIceCandidate={this.props.onCallIceCandidate}
onSendAnswer={this.props.onCallSendAnswer} />
</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 = (
<ImagePreview
content={this.state.imagePreview}
tinode={this.props.tinode}
reply={this.state.reply}
onCancelReply={this.handleCancelReply}
onClose={this.handleClosePreview}
onSendMessage={this.sendImageAttachment} />
<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}
reply={this.state.reply}
onCancelReply={this.handleCancelReply}
onClose={this.handleClosePreview}
onSendMessage={this.sendImageAttachment} />
</Suspense>
);
} else if (this.state.videoPreview) {
// Preview video.
component2 = (
<VideoPreview
content={this.state.videoPreview}
tinode={this.props.tinode}
reply={this.state.reply}
onError={this.props.onError}
onCancelReply={this.handleCancelReply}
onClose={this.handleClosePreview}
onSendMessage={this.sendVideoAttachment} />
);
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}
reply={this.state.reply}
onError={this.props.onError}
onCancelReply={this.handleCancelReply}
onClose={this.handleClosePreview}
onSendMessage={this.sendVideoAttachment} />
</Suspense>;
} else if (this.state.imagePostview) {
// Expand received image.
component2 = (
<ImagePreview
content={this.state.imagePostview}
onClose={this.handleClosePreview} />
);
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 = (
<VideoPreview
content={this.state.videoPostview}
tinode={this.props.tinode}
onError={this.props.onError}
onClose={this.handleClosePreview} />
);
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 = (
<DocPreview
content={this.state.docPreview}
tinode={this.props.tinode}
reply={this.state.reply}
onCancelReply={this.handleCancelReply}
onClose={this.handleClosePreview}
onSendMessage={this.sendFileAttachment} />
);
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}
reply={this.state.reply}
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;
@@ -1777,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}
+32 -25
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,26 +135,29 @@ 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}
onSearchContacts={this.handleSearchContacts} />
<ContactList
tinode={this.props.tinode}
contacts={this.props.searchResults}
myUserId={this.props.myUserId}
emptyListMessage={no_contacts_placeholder}
showSelfTopic={!this.state.searchQuery}
showOnline={false}
showUnread={false}
showContextMenu={false}
onTopicSelected={this.handleSearchResultSelected} />
</div>}
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}
contacts={this.props.searchResults}
myUserId={this.props.myUserId}
emptyListMessage={no_contacts_placeholder}
showSelfTopic={!this.state.searchQuery}
showOnline={false}
showUnread={false}
showContextMenu={false}
onTopicSelected={this.handleSearchResultSelected} />
</div>}
</div>
);
}
+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}
/>
+93 -49
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,6 +321,9 @@ 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`);
@@ -407,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);
}
@@ -516,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 = _ => {
@@ -826,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;
@@ -1123,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);
}
@@ -1703,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);
@@ -1718,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);
@@ -2179,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}
@@ -2225,56 +2266,59 @@ class TinodeWeb extends React.Component {
: null}
{!this.state.displayMobile || (this.state.mobilePanel == 'topic-view' && !this.state.infoPanel) ?
<MessagesView
tinode={this.tinode}
connected={this.state.connected}
ready={this.state.ready}
online={this.state.topicSelectedOnline}
acs={this.state.topicSelectedAcs}
displayMobile={this.state.displayMobile}
viewportWidth={this.state.viewportWidth}
viewportHeight={this.state.viewportHeight}
topic={this.state.topicSelected}
myUserId={this.state.myUserId}
// User public.fn.
myUserName={this.state.sidePanelTitle}
serverVersion={this.state.serverVersion}
serverAddress={this.state.serverAddress}
applicationVisible={this.state.applicationVisible}
colorSchema={colorSchema}
sendOnEnter={this.state.sendOnEnter}
wallpaper={this.state.wallpaper}
<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}
ready={this.state.ready}
online={this.state.topicSelectedOnline}
acs={this.state.topicSelectedAcs}
displayMobile={this.state.displayMobile}
viewportWidth={this.state.viewportWidth}
viewportHeight={this.state.viewportHeight}
topic={this.state.topicSelected}
myUserId={this.state.myUserId}
// User public.fn.
myUserName={this.state.sidePanelTitle}
serverVersion={this.state.serverVersion}
serverAddress={this.state.serverAddress}
applicationVisible={this.state.applicationVisible}
colorSchema={colorSchema}
sendOnEnter={this.state.sendOnEnter}
wallpaper={this.state.wallpaper}
forwardMessage={this.state.forwardMessage}
onCancelForwardMessage={this.handleHideForwardDialog}
forwardMessage={this.state.forwardMessage}
onCancelForwardMessage={this.handleHideForwardDialog}
callTopic={this.state.callTopic}
callSeq={this.state.callSeq}
callState={this.state.callState}
callAudioOnly={this.state.callAudioOnly}
onCallHangup={this.handleCallHangup}
onAcceptCall={this.handleCallAccept}
callTopic={this.state.callTopic}
callSeq={this.state.callSeq}
callState={this.state.callState}
callAudioOnly={this.state.callAudioOnly}
onCallHangup={this.handleCallHangup}
onAcceptCall={this.handleCallAccept}
onCallInvite={this.handleCallInvite}
onCallSendOffer={this.handleCallSendOffer}
onCallIceCandidate={this.handleCallIceCandidate}
onCallSendAnswer={this.handleCallSendAnswer}
onCallInvite={this.handleCallInvite}
onCallSendOffer={this.handleCallSendOffer}
onCallIceCandidate={this.handleCallIceCandidate}
onCallSendAnswer={this.handleCallSendAnswer}
errorText={this.state.errorText}
errorLevel={this.state.errorLevel}
errorAction={this.state.errorAction}
errorActionText={this.state.errorActionText}
errorText={this.state.errorText}
errorLevel={this.state.errorLevel}
errorAction={this.state.errorAction}
errorActionText={this.state.errorActionText}
newTopicParams={this.state.newTopicParams}
newTopicParams={this.state.newTopicParams}
onHideMessagesView={this.handleHideMessagesView}
onError={this.handleError}
onNewTopicCreated={this.handleNewTopicCreated}
showContextMenu={this.handleShowContextMenu}
onChangePermissions={this.handleChangePermissions}
onNewChat={this.handleNewChatInvitation}
sendMessage={this.handleSendMessage}
onVideoCallClosed={this.handleCallClose} />
onHideMessagesView={this.handleHideMessagesView}
onError={this.handleError}
onNewTopicCreated={this.handleNewTopicCreated}
showContextMenu={this.handleShowContextMenu}
onChangePermissions={this.handleChangePermissions}
onNewChat={this.handleNewChatInvitation}
sendMessage={this.handleSendMessage}
onVideoCallClosed={this.handleCallClose} />
</Suspense>
: null}
{this.state.infoPanel ?
@@ -2304,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}
+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
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([[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
+1 -1
View File
@@ -1,2 +1,2 @@
"use strict";(self.webpackChunktinode_webapp=self.webpackChunktinode_webapp||[]).push([[827],{3827:function(e,t,a){a.r(t),a.d(t,{default:function(){return d}});var l=a(1594),s=a.n(l),r=a(8181),n=a(7151),i=a(9027),c=a(1781);class d extends s().PureComponent{constructor(e){super(e),this.state={tab:i.S.type,selectedType:i.S.type,wallpaper:c[i.S.type][i.S.index].name,blur:0},this.blurValues=[0,1,2,4,8,16],this.handleTabClick=this.handleTabClick.bind(this),this.handleWallpaperSelected=this.handleWallpaperSelected.bind(this),this.handleBlurChanged=this.handleBlurChanged.bind(this)}handleTabClick(e){e.preventDefault(),n.A.navigateTo(n.A.addUrlParam(window.location.hash,"tab",e.currentTarget.dataset.id)),this.setState({tab:e.currentTarget.dataset.id})}handleWallpaperSelected(e){let t,a,l;e?(e.preventDefault(),a=this.state.tab,t=e.currentTarget.dataset.id,l="patt"==a?0:this.state.blur):(a=i.S.type,t=i.S.index,l=0);const s=c[a][t].name,r="patt"==a?c[a][t].size:0;this.setState({tab:a,wallpaper:s,selectedType:a,blur:l}),this.props.onWallpaperSelected(`../${i.S.path}${s}`,r,this.blurValues[l])}handleBlurChanged(e){e.preventDefault();const t=e.currentTarget.value;this.setState({blur:t}),"img"==this.state.selectedType&&this.props.onWallpaperSelected(`../${i.S.path}${this.state.wallpaper}`,0,this.blurValues[t])}render(){return s().createElement("div",{className:"flex-column"},s().createElement("div",{className:"panel-form-row clean-clickable"},s().createElement("a",{className:"flat-button",onClick:()=>this.handleWallpaperSelected(null)},s().createElement("i",{className:"material-icons"},"undo")," ",s().createElement(r.FormattedMessage,{id:"button_restore",defaultMessage:[{type:0,value:"Restore default"}]}))),s().createElement("ul",{className:"tabbar"},s().createElement("li",{className:"patt"===this.state.tab?"active":null},s().createElement("a",{href:"#","data-id":"patt",onClick:this.handleTabClick},s().createElement(r.FormattedMessage,{id:"tabtitle_pattern",defaultMessage:[{type:0,value:"pattern"}]}))),s().createElement("li",{className:"img"===this.state.tab?"active":null},s().createElement("a",{href:"#","data-id":"img",onClick:this.handleTabClick},s().createElement(r.FormattedMessage,{id:"tabtitle_image",defaultMessage:[{type:0,value:"image"}]})))),s().createElement("div",{id:"settings-form",className:"scrollable-panel"},"patt"===this.state.tab?s().createElement("div",{className:"image-grid"},c.patt.map((e,t)=>{const a=this.state.wallpaper===e.name?" selected":"",l="dark"==this.props.colorSchema?"inverted":null;return s().createElement("div",{key:t,"data-id":t,className:`image-grid-cell${a}`,onClick:this.handleWallpaperSelected},s().createElement("img",{src:i.S.path+e.name,alt:e.name,className:l,style:{width:`${e.size}px`,height:`${e.size}px`}}))})):s().createElement(s().Fragment,null,s().createElement("div",{className:"panel-form-column"},s().createElement("div",{className:"panel-form-row"},s().createElement("label",{className:"small"},s().createElement(r.FormattedMessage,{id:"label_blur_wallpaper",defaultMessage:[{type:0,value:"Blur:"}]}))),s().createElement("div",{className:"panel-form-row"},s().createElement("input",{type:"range",id:"blur",name:"blur",min:"0",max:"5",step:"1",value:this.state.blur,onChange:this.handleBlurChanged}))),s().createElement("div",{className:"image-grid"},c.img.map((e,t)=>{const a=this.state.wallpaper===e.name?" selected":"";return s().createElement("div",{key:t,"data-id":t,className:`image-grid-cell${a}`,onClick:this.handleWallpaperSelected},s().createElement("img",{src:i.S.path+e.pr,alt:e.name,style:{width:"100%",height:"100%"}}))})))))}}}}]);
"use strict";(self.webpackChunktinode_webapp=self.webpackChunktinode_webapp||[]).push([[827],{3827:function(e,t,a){a.r(t),a.d(t,{default:function(){return p}});var l=a(1594),s=a.n(l),r=a(8181),n=a(7151),i=a(9027),d=a(3540),c=a(1781);class p extends s().PureComponent{constructor(e){super(e);const t=(0,d.MC)(this.props.wallpaper),a=(0,d.t7)(this.props.wallpaper);this.state={tab:t||i.S.type,selectedType:t||i.S.type,wallpaper:a||c[i.S.type][i.S.index].name,blur:0|this.props.wallpaperBlur},this.blurValues=[0,1,2,4,8,16],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){e.preventDefault(),n.A.navigateTo(n.A.addUrlParam(window.location.hash,"tab",e.currentTarget.dataset.id)),this.setState({tab:e.currentTarget.dataset.id})}handleWallpaperSelected(e){let t,a,l;if(e)e.preventDefault(),a=this.state.tab,t=e.currentTarget.dataset.id,l="patt"==a?0:this.state.blur;else{if(!this.hasChanged())return;a=i.S.type,t=i.S.index,l=0}const s=c[a][t].name,r="patt"==a?c[a][t].size:0;this.setState({tab:a,wallpaper:s,selectedType:a,blur:l}),this.props.onWallpaperSelected(`../${i.S.path}${s}`,r,this.blurValues[l])}handleBlurChanged(e){e.preventDefault();const t=e.currentTarget.value;this.setState({blur:t}),"img"==this.state.selectedType&&this.props.onWallpaperSelected(`../${i.S.path}${this.state.wallpaper}`,0,this.blurValues[t])}hasChanged(){return this.state.selectedType!=i.S.type||this.state.wallpaper!=c[i.S.type][i.S.index].name||0!=this.state.blur}render(){return s().createElement("div",{className:"flex-column"},s().createElement("div",{className:"panel-form-row"+(this.hasChanged()?" clean-clickable":"")},s().createElement("a",{className:"flat-button"+(this.hasChanged()?"":" disabled"),onClick:()=>this.handleWallpaperSelected(null)},s().createElement("i",{className:"material-icons"},"undo")," ",s().createElement(r.FormattedMessage,{id:"button_restore",defaultMessage:[{type:0,value:"Restore default"}]}))),s().createElement("ul",{className:"tabbar"},s().createElement("li",{className:"patt"===this.state.tab?"active":null},s().createElement("a",{href:"#","data-id":"patt",onClick:this.handleTabClick},s().createElement(r.FormattedMessage,{id:"tabtitle_pattern",defaultMessage:[{type:0,value:"pattern"}]}))),s().createElement("li",{className:"img"===this.state.tab?"active":null},s().createElement("a",{href:"#","data-id":"img",onClick:this.handleTabClick},s().createElement(r.FormattedMessage,{id:"tabtitle_image",defaultMessage:[{type:0,value:"image"}]})))),s().createElement("div",{id:"settings-form",className:"scrollable-panel"},"patt"===this.state.tab?s().createElement("div",{className:"image-grid"},c.patt.map((e,t)=>{const a=this.state.wallpaper===e.name?" selected":"",l="dark"==this.props.colorSchema?"inverted":null;return s().createElement("div",{key:t,"data-id":t,className:`image-grid-cell${a}`,onClick:this.handleWallpaperSelected},s().createElement("img",{src:i.S.path+e.name,alt:e.name,className:l,style:{width:`${e.size}px`,height:`${e.size}px`}}))})):s().createElement(s().Fragment,null,s().createElement("div",{className:"panel-form-column"},s().createElement("div",{className:"panel-form-row"},s().createElement("label",{className:"small"},s().createElement(r.FormattedMessage,{id:"label_blur_wallpaper",defaultMessage:[{type:0,value:"Blur:"}]}))),s().createElement("div",{className:"panel-form-row"},s().createElement("input",{type:"range",id:"blur",name:"blur",min:"0",max:"5",step:"1",value:this.state.blur,onChange:this.handleBlurChanged}))),s().createElement("div",{className:"image-grid"},c.img.map((e,t)=>{const a=this.state.wallpaper===e.name?" selected":"";return s().createElement("div",{key:t,"data-id":t,className:`image-grid-cell${a}`,onClick:this.handleWallpaperSelected},s().createElement("img",{src:i.S.path+e.pr,alt:e.name,style:{width:"100%",height:"100%"}}))})))))}}}}]);
//# sourceMappingURL=827.prod.js.map
+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([[889],{8889:function(e,t,n){n.r(t),n.d(t,{default:function(){return d}});var s=n(1594),i=n.n(s),a=n(8181),l=n(1725),o=n(9027),r=n(6110),p=n(8589),h=n(3540);class d extends i().PureComponent{constructor(e){super(e),this.state={width:0,height:0},this.handleSendImage=this.handleSendImage.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())}assignWidth(e){if(e&&!this.state.width){const t=e.getBoundingClientRect();this.setState({width:0|t.width,height:0|t.height})}}handleSendImage(e){this.props.onClose(),this.props.onSendMessage(e,this.props.content.blob)}render(){if(!this.props.content)return null;const e=(0,r.ae)(this.props.content.width,this.props.content.height,this.state.width,this.state.height,!1),t=e?{width:e.dstWidth+"px",height:e.dstHeight+"px"}:this.props.content.width>this.props.content.height?{width:"100%"}:{height:"100%"};t.maxWidth="100%",t.maxHeight="100%";const n=Math.max((this.state.width/o.JS/1.5|0)-2,12),s=(0,p.Tu)(this.props.content.filename,n)||"-",d=this.props.content.width||"-",c=this.props.content.height||"-",m=(0,h.FH)(this.props.content.url);return i().createElement("div",{id:"image-preview"},i().createElement("div",{id:"preview-caption-panel"},this.props.onSendMessage?i().createElement("span",null,s):i().createElement("a",{href:m,download:this.props.content.filename},i().createElement("i",{className:"material-icons"},"file_download")," ",i().createElement(a.FormattedMessage,{id:"download_action",defaultMessage:[{type:0,value:"download"}]})),i().createElement("a",{href:"#",onClick:e=>{e.preventDefault(),this.props.onClose()}},i().createElement("i",{className:"material-icons gray"},"close"))),i().createElement("div",{id:"image-preview-container",ref:e=>this.assignWidth(e)},i().createElement("img",{src:this.props.content.url,style:t,className:"image-preview",alt:this.props.content.filename})),this.props.onSendMessage?i().createElement(l.A,{messagePrompt:"add_image_caption",acceptBlank:!0,tinode:this.props.tinode,reply:this.props.reply,onCancelReply:this.props.onCancelReply,onSendMessage:this.handleSendImage,onError:this.props.onError}):i().createElement("div",{id:"image-preview-footer"},i().createElement("div",null,i().createElement("div",null,i().createElement("b",null,i().createElement(a.FormattedMessage,{id:"label_file_name",defaultMessage:[{type:0,value:"File name:"}]}))),i().createElement("div",null,i().createElement("span",{title:this.props.content.filename},s))),i().createElement("div",null,i().createElement("div",null,i().createElement("b",null,i().createElement(a.FormattedMessage,{id:"label_content_type",defaultMessage:[{type:0,value:"Content type:"}]}))),i().createElement("div",null,this.props.content.type)),i().createElement("div",null,i().createElement("div",null,i().createElement("b",null,i().createElement(a.FormattedMessage,{id:"label_size",defaultMessage:[{type:0,value:"Size:"}]}))),i().createElement("div",null,d," × ",c," px; ",(0,p.lX)(this.props.content.size)))))}}}}]);
//# sourceMappingURL=889.prod.js.map

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