Compare commits
55
Commits
abd94d6858
...
e05e75ed28
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e05e75ed28 | ||
|
|
4d450e6b85 | ||
|
|
b014592381 | ||
|
|
1d1c9400f8 | ||
|
|
97d4bcfba9 | ||
|
|
ed19ed750c | ||
|
|
93e7b69f88 | ||
|
|
af58db914b | ||
|
|
0c9e6379d6 | ||
|
|
311b36b977 | ||
|
|
d12fd0e16f | ||
|
|
a7998a1d29 | ||
|
|
231635f86d | ||
|
|
5574085a01 | ||
|
|
5d53899f89 | ||
|
|
97c8114fb7 | ||
|
|
28d825c681 | ||
|
|
cede800726 | ||
|
|
53c1f9fd0b | ||
|
|
796f16a003 | ||
|
|
738d84045d | ||
|
|
de1028e141 | ||
|
|
f9d622a8d8 | ||
|
|
fbdf1df9d0 | ||
|
|
09baee8d73 | ||
|
|
84d0ec5e9c | ||
|
|
4d9bc435fc | ||
|
|
7d08e65e64 | ||
|
|
6e19bf6add | ||
|
|
96ad18a902 | ||
|
|
58983588a4 | ||
|
|
9401a8d483 | ||
|
|
6019303f2c | ||
|
|
28fd6a7236 | ||
|
|
e8d48eb5f0 | ||
|
|
d91534236f | ||
|
|
4d3682c0ef | ||
|
|
45c6cf22ff | ||
|
|
c56b30337f | ||
|
|
ab128ba6bb | ||
|
|
b2bbe75f6d | ||
|
|
0f095ceeca | ||
|
|
331dea37cf | ||
|
|
af72df8a4f | ||
|
|
4f56dfc276 | ||
|
|
2817d83c56 | ||
|
|
571b876626 | ||
|
|
60867671aa | ||
|
|
1d46599a1f | ||
|
|
04734a7dd9 | ||
|
|
37351f9fdf | ||
|
|
79fba3a402 | ||
|
|
7c9cc230ac | ||
|
|
ff2f6b035a | ||
|
|
b16d538942 |
@@ -0,0 +1,38 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve TinodeWeb
|
||||
title: ''
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
If you ant to ask a question, please post to https://groups.google.com/d/forum/tinode instead.
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Environment (please complete the following information):**
|
||||
- OS: [e.g. Mac OS, Windows 10 Home]
|
||||
- Browser [e.g. Chrome, Safari]
|
||||
- TinodeWeb Version [e.g. 0.16.6]
|
||||
|
||||
**Console log**
|
||||
Please attach or insert console log which illustrates the problem.
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
@@ -0,0 +1,23 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: feature request
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
If you ant to ask a question, please post to https://groups.google.com/d/forum/tinode instead.
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
@@ -1,8 +1,10 @@
|
||||
# Single-page chat with Tinode
|
||||
# Tinode Web
|
||||
|
||||
## Briefing
|
||||
|
||||
Single-page web chat application for [Tinode](https://github.com/tinode/chat/). The app is built on
|
||||
[React](https://reactjs.org/). The [Tinode javascript SDK](https://github.com/tinode/tinode-js/) has no external dependencies.
|
||||
Overall it's a lot like open source WhatsApp or Telegram web apps.
|
||||
Overall, it's a lot like open source WhatsApp or Telegram web apps.
|
||||
|
||||
Although the app is generally usable, keep in mind that this is work in progress. Some bugs probably exist, some features are missing. The app was tested in the latest Chrome & Firefox only. NPM package is [available](https://www.npmjs.com/package/tinode-webapp).
|
||||
|
||||
@@ -29,7 +31,9 @@ The app is fully internationalized using [React-Intl](https://github.com/formatj
|
||||
As of the time of this writing the following translations exist:
|
||||
* English
|
||||
* Chinese (simplified)
|
||||
* German
|
||||
* Korean
|
||||
* Romanian
|
||||
* Russian
|
||||
* Spanish
|
||||
|
||||
|
||||
+63
-7
@@ -541,6 +541,15 @@ div.alert .content {
|
||||
margin-left: 1rem;
|
||||
}
|
||||
|
||||
/* Container with whitish background and rounded corners */
|
||||
.rounded-container {
|
||||
position: relative;
|
||||
background-color: #fafafa;
|
||||
border-radius: 0.1rem;
|
||||
padding: 0.25rem;
|
||||
margin: 0.25rem;
|
||||
}
|
||||
|
||||
/* BEGIN InPlaceEdit styles */
|
||||
.in-place-edit {
|
||||
display: inline-block;
|
||||
@@ -819,12 +828,13 @@ i.search {
|
||||
min-height: 3.5rem;
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
border-bottom: 1px solid #eee;
|
||||
border-collapse: collapse;
|
||||
/* partial bottom border */
|
||||
background: linear-gradient(to right, transparent 3.5rem, #eee 1px, #eee 100%) 0 100% / 100% 1px no-repeat, #fff;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
.contact-box > li:last-child {
|
||||
border-bottom: none; /* remove border from the last contact */
|
||||
/* remove border from the last contact */
|
||||
background: #fff;
|
||||
}
|
||||
.contact-box > li:hover {
|
||||
background-color: #eceff1; /* blue-gray 50 highlight */
|
||||
@@ -892,6 +902,13 @@ i.search {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.contact-box img.channel {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
vertical-align: middle;
|
||||
margin-left: 0.3rem;
|
||||
}
|
||||
|
||||
/* Online-offline indicator positioned inside avatar-box */
|
||||
.avatar-box .online, .offline {
|
||||
display: inline-block;
|
||||
@@ -1520,15 +1537,54 @@ i.search {
|
||||
color:#777;
|
||||
}
|
||||
|
||||
/* Inline image styles */
|
||||
.inline-image {
|
||||
max-width: 36rem;
|
||||
max-height: 24rem;
|
||||
/* Inline and preview image styles */
|
||||
.inline-image, .image-preview {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border-radius: 0.1rem;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.inline-image {
|
||||
max-width: 36rem;
|
||||
max-height: 24rem;
|
||||
}
|
||||
|
||||
/* Container with upload progress */
|
||||
.inline-image > .rounded-container {
|
||||
position: relative;
|
||||
margin: 0.25rem auto;
|
||||
bottom: 2rem;
|
||||
/* Imitating minimum margins on the left and right */
|
||||
border-left: 0.25rem solid transparent;
|
||||
border-right: 0.25rem solid transparent;
|
||||
}
|
||||
|
||||
/* Broken image graphics which replaces the stock graphics. */
|
||||
img.inline-image::before, img.image-preview::before {
|
||||
font-size: 0.9rem;
|
||||
font-weight: lighter;
|
||||
color: #666;
|
||||
display:flex;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
justify-content: center;
|
||||
background: center no-repeat #ccc url("../img/broken_image.png");
|
||||
}
|
||||
|
||||
img.inline-image::before {
|
||||
content: attr(alt);
|
||||
background-size: 2rem 2rem;
|
||||
}
|
||||
|
||||
img.image-preview::before {
|
||||
content: " ";
|
||||
background-size: 8rem 8rem;
|
||||
}
|
||||
|
||||
/* Attachment styles */
|
||||
.attachment {
|
||||
display: flex;
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Binary file not shown.
|
After Width: | Height: | Size: 2.1 KiB |
+3
-3
@@ -32,10 +32,10 @@
|
||||
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/react@16/umd/react.development.js"></script>
|
||||
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/react-dom@16/umd/react-dom.development.js"></script>
|
||||
<!-- React-Intl for translations -->
|
||||
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/react-intl@3/dist/react-intl.js"></script>
|
||||
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/react-intl@4/dist/react-intl.js"></script>
|
||||
<!-- Firebase for push notifications -->
|
||||
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/firebase@7.14.6/firebase-app.js"></script>
|
||||
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/firebase@7.14.6/firebase-messaging.js"></script>
|
||||
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/firebase@7.20.0/firebase-app.js"></script>
|
||||
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/firebase@7.20.0/firebase-messaging.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
+3
-3
@@ -24,9 +24,9 @@
|
||||
<link rel="stylesheet" href="css/base.min.css" />
|
||||
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/react@16.13.1/umd/react.production.min.js"></script>
|
||||
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/react-dom@16.13.1/umd/react-dom.production.min.js"></script>
|
||||
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/react-intl@3.12.1/dist/react-intl.min.js"></script>
|
||||
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/firebase@7.14.6/firebase-app.js"></script>
|
||||
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/firebase@7.14.6/firebase-messaging.js"></script>
|
||||
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/react-intl@4.7.6/dist/react-intl.min.js"></script>
|
||||
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/firebase@7.20.0/firebase-app.js"></script>
|
||||
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/firebase@7.20.0/firebase-messaging.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<noscript><div id="noscript-message"><div id="noscript-title">Javascript support is required.</div>
|
||||
|
||||
Generated
+1920
-2461
File diff suppressed because it is too large
Load Diff
+18
-18
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "tinode-webapp",
|
||||
"description": "Tinode messenger for the web",
|
||||
"version": "0.16.6",
|
||||
"version": "0.16.9-rc1",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/tinode/example-react-js.git"
|
||||
@@ -68,25 +68,25 @@
|
||||
"author": "Tinode Authors <info@tinode.co>",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"firebase": "^7.14.6",
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.1",
|
||||
"react-intl": "^4.6.9",
|
||||
"tinode-sdk": "^0.16.6"
|
||||
"firebase": "^7.24.0",
|
||||
"react": "^16.14.0",
|
||||
"react-dom": "^16.14.0",
|
||||
"react-intl": "^4.7.6",
|
||||
"tinode-sdk": "^0.16.9-rc2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.10.2",
|
||||
"@babel/preset-env": "^7.10.2",
|
||||
"@babel/preset-react": "^7.10.1",
|
||||
"babel-loader": "^8.1.0",
|
||||
"babel-plugin-react-intl": "^7.5.20",
|
||||
"browserslist": "^4.12.0",
|
||||
"copy-webpack-plugin": "^6.0.2",
|
||||
"@babel/core": "^7.12.10",
|
||||
"@babel/preset-env": "^7.12.10",
|
||||
"@babel/preset-react": "^7.12.10",
|
||||
"babel-loader": "^8.2.2",
|
||||
"babel-plugin-react-intl": "^7.9.4",
|
||||
"browserslist": "^4.16.0",
|
||||
"copy-webpack-plugin": "^6.4.0",
|
||||
"cssnano": "^4.1.10",
|
||||
"postcss": "^7.0.32",
|
||||
"postcss-cli": "^7.1.1",
|
||||
"terser-webpack-plugin": "^3.0.3",
|
||||
"webpack": "^4.43.0",
|
||||
"webpack-cli": "^3.3.11"
|
||||
"postcss": "^7.0.35",
|
||||
"postcss-cli": "^7.1.2",
|
||||
"terser-webpack-plugin": "^3.1.0",
|
||||
"webpack": "^4.44.2",
|
||||
"webpack-cli": "^3.3.12"
|
||||
}
|
||||
}
|
||||
|
||||
+12
-3
@@ -1,12 +1,16 @@
|
||||
// Must be located at the root.
|
||||
importScripts('https://cdn.jsdelivr.net/npm/firebase@7.14.6/firebase-app.js');
|
||||
importScripts('https://cdn.jsdelivr.net/npm/firebase@7.14.6/firebase-messaging.js');
|
||||
importScripts('https://cdn.jsdelivr.net/npm/firebase@7.24.0/firebase-app.js');
|
||||
importScripts('https://cdn.jsdelivr.net/npm/firebase@7.24.0/firebase-messaging.js');
|
||||
importScripts('firebase-init.js');
|
||||
|
||||
firebase.initializeApp(FIREBASE_INIT);
|
||||
|
||||
// Basic internationalization.
|
||||
const i18n = {
|
||||
'de': {
|
||||
'new_message': "Neue Nachricht",
|
||||
'new_chat': "Neuer Chat",
|
||||
},
|
||||
'en': {
|
||||
'new_message': "New message",
|
||||
'new_chat': "New chat",
|
||||
@@ -19,6 +23,10 @@ const i18n = {
|
||||
'new_message': "새로운 메시지",
|
||||
'new_chat': "새로운 채팅",
|
||||
},
|
||||
'ro': {
|
||||
'new_message': "Mesaj nou",
|
||||
'new_chat': "Chat nou",
|
||||
},
|
||||
'ru': {
|
||||
'new_message': "Новое сообщение",
|
||||
'new_chat': "Новый чат",
|
||||
@@ -31,7 +39,8 @@ const i18n = {
|
||||
self.i18nMessage = function(id) {
|
||||
// Choose translations: given something like 'de-CH', try 'de-CH' then 'de' then 'en'.
|
||||
const lang = i18n[self.locale] || i18n[self.baseLocale] || i18n['en'];
|
||||
// Try id in the specified language, if missing try English, otherwise use id as the last resort.
|
||||
// Try finding string by id in the specified language, if missing try English, otherwise use the id itself
|
||||
// as the last resort.
|
||||
return lang[id] || i18n['en'][id] || id;
|
||||
}
|
||||
|
||||
|
||||
+17
-10
@@ -23,7 +23,9 @@ export const RECEIVED_DELAY = 500;
|
||||
export const READ_DELAY = 1000;
|
||||
|
||||
// The shortest allowed tag length. Matches the value on the server.
|
||||
export const MIN_TAG_LENGTH = 4;
|
||||
export const MIN_TAG_LENGTH = 2;
|
||||
// The greatest allowed tag length. Matches the value on the server.
|
||||
export const MAX_TAG_LENGTH = 96;
|
||||
// The maximum number of tags allowed. Matches the value on the server.
|
||||
export const MAX_TAG_COUNT = 16;
|
||||
|
||||
@@ -31,6 +33,8 @@ export const MAX_TAG_COUNT = 16;
|
||||
export const DEFAULT_P2P_ACCESS_MODE = 'JRWPS';
|
||||
// Access mode for new group topics created by the current user.
|
||||
export const NEW_GRP_ACCESS_MODE = 'JRWPSAO';
|
||||
// Access mode for a channel.
|
||||
export const CHANNEL_ACCESS_MODE = 'JR';
|
||||
|
||||
// Access mode for no access.
|
||||
export const NO_ACCESS_MODE = 'N';
|
||||
@@ -51,21 +55,24 @@ export const BROKEN_IMAGE_SIZE = 32;
|
||||
// Number of chat messages to fetch in one call.
|
||||
export const MESSAGES_PAGE = 24;
|
||||
|
||||
// Maximum in-band (included directly into the message) attachment size which fits into
|
||||
// a message of 256K in size, assuming base64 encoding and 1024 bytes of overhead.
|
||||
// This is size of an object *before* base64 encoding is applied.
|
||||
// Default maximum in-band (included directly into the message) attachment size which fits into
|
||||
// a message of 256K in size. The actual binary size of the attachment should be smaller due to
|
||||
// base64 encoding expansion and some overhead, for instance 1024 bytes.
|
||||
// Increase this limit to a greater value in production, if desired. Also increase
|
||||
// max_message_size in server config.
|
||||
// MAX_INBAND_ATTACHMENT_SIZE = base64DecodedLen(max_message_size) - 1024;
|
||||
export const MAX_INBAND_ATTACHMENT_SIZE = 195584;
|
||||
// max_message_size in the server config.
|
||||
export const MAX_INBAND_ATTACHMENT_SIZE = 262144;
|
||||
|
||||
// Absolute maximum attachment size to be used with the server = 8MB. Increase to
|
||||
// something like 100MB in production.
|
||||
export const MAX_EXTERN_ATTACHMENT_SIZE = 1 << 23;
|
||||
|
||||
// Maximum allowed linear dimension of an inline image in pixels. You may want
|
||||
// to adjust it to 1600 or 2400 for production.
|
||||
export const MAX_IMAGE_DIM = 768;
|
||||
// Maximum allowed linear dimension of an inline image in pixels. Larer images will be shrunk
|
||||
// to make the larger dimension fit under this size. You may want to adjust it to 1600 or
|
||||
// 2400 for production.
|
||||
export const MAX_IMAGE_DIM = 1024;
|
||||
|
||||
// Linear dimensions of image preview: shrink image under this size for previews.
|
||||
export const IMAGE_PREVIEW_DIM = 64;
|
||||
|
||||
// Maximum number of online users to be shown in a topic title bar. Others will be
|
||||
// hidden under "+X more"
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
{
|
||||
"validate_credential_action": "bestätigen",
|
||||
"title_tag_manager": "Schlagworte (Nutzer entdecken)",
|
||||
"label_user_contacts": "Kontakte:",
|
||||
"button_add_another": "Hinzufügen",
|
||||
"label_message_sound": "Benachrichtigungston:",
|
||||
"label_push_notifications": "Benachrichtigungsmeldungen:",
|
||||
"label_push_notifications_disabled": "Benachrichtigungsmeldungen (erfordert HTTPS):",
|
||||
"label_incognito_mode": "inkognito-Modus:",
|
||||
"delete_account": "Konto löschen",
|
||||
"delete_account_arning": "Möchten Sie das Konto wirklich löschen? Das kann nicht rückgängig gemacht werden",
|
||||
"label_password": "Passwort",
|
||||
"password_unchanged_prompt": "unverändert",
|
||||
"button_logout": "Abmelden",
|
||||
"button_delete_account": "Konto löschen",
|
||||
"label_default_access_mode": "Standard Zugriffsmodus:",
|
||||
"blocked_contacts_link": "Blockierte Kontakte ({count})",
|
||||
"link_contact_us": "Kontakt",
|
||||
"link_terms_of_service": "Nutzungsbedingungen",
|
||||
"link_privacy_policy": "Datenschutzerklärung",
|
||||
"label_sdk": "SDK:",
|
||||
"label_server_address": "Server Adresse:",
|
||||
"archived_contacts": "Archivierte Kontakte ({count})",
|
||||
"contacts_not_found": "Sie haben keine Unterhaltungen<br />¯∖_(ツ)_/¯",
|
||||
"full_name_prompt": "Vollständiger Name, z.B. Lisa Musterfrau",
|
||||
"email_prompt": "E-Mail, z.B. jdoe@beispiel.de",
|
||||
"button_sign_up": "Anmelden",
|
||||
"label_your_name": "Ihr Name",
|
||||
"label_user_id": "ID:",
|
||||
"button_edit": "Bearbeiten",
|
||||
"requested_permissions": "Angefordert",
|
||||
"granted_permissions": "Erteilt",
|
||||
"menu_item_edit_permissions": "Berechtigungen ändern",
|
||||
"label_other_user": "Andere",
|
||||
"action_clear_messages": "Nachrichten leeren",
|
||||
"clear_messages_warning": "Sind Sie sicher, dass Sie diese Unterhaltung für alle leeren wollen? Das kann nicht rückgängig gemacht werden.",
|
||||
"action_delete_messages": "Nachrichten für alle löschen",
|
||||
"delete_messages_warning": "Sind Sie sicher, dass Sie diese Unterhaltung für alle löschen wollen? Das kann nicht rückgängig gemacht werden.",
|
||||
"action_leave_chat": "Unterhaltung verlassen",
|
||||
"leave_chat_warning": "Möchten Sie diese Unterhaltung wirklich verlassen?",
|
||||
"action_block_contact": "Kontakt blockieren",
|
||||
"block_contact_warning": "Möchten Sie diesen Kontakt wirklich blockieren?",
|
||||
"action_report_chat": "Unterhaltung melden",
|
||||
"report_chat_warning": "Möchten Sie diese Unterhaltung wirklich blockieren und melden?",
|
||||
"title_info": "Info",
|
||||
"label_topic_name": "Name",
|
||||
"label_private": "Privater Kommentar",
|
||||
"private_editing_placeholder": "Nur für Sie sichtbar",
|
||||
"label_muting_topic": "Stumm geschaltet:",
|
||||
"action_more": "Mehr",
|
||||
"label_your_permissions": "Ihre Berechtigungen:",
|
||||
"label_permissions": "Berechtigungen:",
|
||||
"label_you": "Sie:",
|
||||
"label_default_access": "Standard Zugriffsmodus:",
|
||||
"label_group_members": "Gruppenmitglieder:",
|
||||
"button_add_members": "Mitglieder hinzufügen",
|
||||
"group_has_no_members": "Keine Mitglieder",
|
||||
"login_prompt": "Anmelden",
|
||||
"password_prompt": "Passwort",
|
||||
"stay_logged_in": "Angemeldet bleiben",
|
||||
"forgot_password_link": "Passwort vergessen?",
|
||||
"button_sign_in": "Anmelden",
|
||||
"label_client": "Client:",
|
||||
"label_server": "Server:",
|
||||
"online_now": "jetzt online",
|
||||
"last_seen_timestamp": "Zuletzt gesehen",
|
||||
"title_not_found": "Nicht gefunden",
|
||||
"unnamed_topic": "Unbenannt",
|
||||
"messages_not_readable": "Neue Nachrichten können nicht gelesen werden",
|
||||
"peers_messaging_disabled": "Gruppennachrichten sind deaktiviert",
|
||||
"enable_peers_messaging": "Aktivieren",
|
||||
"search_for_contacts": "Nutzen Sie die Suche um Kontakte zu finden",
|
||||
"search_no_results": "Die Suche hatte keine Ergebnisse",
|
||||
"tabtitle_find_user": "Suchen",
|
||||
"tabtitle_new_group": "Neue Gruppe",
|
||||
"tabtitle_group_by_id": "nach ID",
|
||||
"new_password_placeholder": "Geben Sie ein neues Passwort ein",
|
||||
"label_reset_password": "Passwort per E-Mail wiederherstellen:",
|
||||
"credential_email_prompt": "E-Mail Adresse für Registrierung",
|
||||
"button_reset": "Zurücksetzen",
|
||||
"button_send_request": "Anfrage senden",
|
||||
"label_server_to_use": "Server verwenden:",
|
||||
"label_wire_transport": "Übertragung per Kabel:",
|
||||
"button_update": "Aktualisieren",
|
||||
"sidepanel_title_login": "Anmelden",
|
||||
"sidepanel_title_register": "Konto erstellen",
|
||||
"sidepanel_title_settings": "Einstellungen",
|
||||
"sidepanel_title_account_settings": "Konto-Einstellungen",
|
||||
"sidepanel_title_acc_general": "Allgemein",
|
||||
"sidepanel_title_acc_security": "Sicherheit",
|
||||
"sidepanel_title_acc_notifications": "Benachrichtigungen",
|
||||
"sidepanel_title_acc_support": "Unterstützung",
|
||||
"sidepanel_title_newtpk": "Neue Unterhaltung starten",
|
||||
"sidepanel_title_cred": "Anmeldeinformationen bestätigen",
|
||||
"sidepanel_title_reset": "Passwort zurücksetzen",
|
||||
"sidepanel_title_archive": "Archivierte Unterhaltungen",
|
||||
"sidepanel_title_blocked": "Blockierte Unterhaltungen",
|
||||
"reconnect_countdown": "Getrennt. Wiederverbinden in {seconds}…",
|
||||
"reconnect_now": "Jetzt probieren",
|
||||
"push_init_failed": "Initialisierung von Push-Benachrichtigungen fehlgeschlagen",
|
||||
"invalid_security_token": "Ungültiger Sicherheitsschlüssel",
|
||||
"no_connection": "Keine Verbindung",
|
||||
"code_doesnot_match": "Code stimmt nicht überein",
|
||||
"update_available": "Aktualisierung verfügbar.",
|
||||
"reload_update": "Neu laden",
|
||||
"phone_dative": "Telefon",
|
||||
"email_dative": "E-Mail",
|
||||
"enter_confirmation_code_prompt": "Geben Sie den Bestätigungscode ein, der per {method} geschickt wurde:",
|
||||
"numeric_confirmation_code_prompt": "Nur zahlen",
|
||||
"button_confirm": "Bestätigen",
|
||||
"save_attachment": "Speichern",
|
||||
"deleted_content": "Inhalt gelöscht",
|
||||
"invalid_content": "ungültiger Inhalt",
|
||||
"user_not_found": "Nicht gefunden",
|
||||
"badge_you": "Sie",
|
||||
"badge_owner": "Besitzer",
|
||||
"menu_item_info": "Info",
|
||||
"menu_item_clear_messages": "Nachrichten leeren",
|
||||
"menu_item_clear_messages_for_all": "Für alle leeren",
|
||||
"menu_item_delete": "Löschen",
|
||||
"menu_item_delete_for_all": "Für alle löschen",
|
||||
"menu_item_send_retry": "Wiederholen",
|
||||
"menu_item_mute": "Unterhaltung stumm schalten",
|
||||
"menu_item_unmute": "Stumm schalten beenden",
|
||||
"menu_item_delete_topic": "Entfernen",
|
||||
"topic_delete_warning": "Möchten Sie diese Unterhaltung wirklich löschen?",
|
||||
"menu_item_unblock": "Blockierung aufheben",
|
||||
"menu_item_block": "Blockieren",
|
||||
"topic_block_warning": "Möchten Sie diese Unterhaltung wirklich blockieren?",
|
||||
"menu_item_member_delete": "Entfernen",
|
||||
"menu_item_archive_topic": "Archivieren",
|
||||
"action_cancel": "Abbrechen",
|
||||
"upload_finishing": "wird abgeschlossen...",
|
||||
"no_contacts": "Sie haben keine Kontakte :-(",
|
||||
"contacts_not_found_short": "Keine Kontakte für ''{query}''",
|
||||
"title_group_members": "Gruppenmitglieder",
|
||||
"title_all_contacts": "Alle Kontakte",
|
||||
"button_ok": "OK",
|
||||
"button_cancel": "Abbrechen",
|
||||
"more_online_members": "+{overflow} mehr",
|
||||
"download_action": "herunterladen",
|
||||
"label_file_name": "Dateiname:",
|
||||
"label_content_type": "Inhaltsart:",
|
||||
"label_size": "Größe:",
|
||||
"chat_invitation": "Sie sind eingeladen, an einem neuen Chat teilzunehmen. Was möchten Sie tun?",
|
||||
"chat_invitation_accept": "Akzeptieren",
|
||||
"chat_invitation_ignore": "Ignorieren",
|
||||
"chat_invitation_block": "Blockieren",
|
||||
"error_invalid_id": "ungültige ID",
|
||||
"group_user_id_prompt": "Gruppe oder Benutzer ID",
|
||||
"button_subscribe": "Abbonieren",
|
||||
"topic_name_editing_placeholder": "Freiform Name der Gruppe",
|
||||
"button_create": "Erstellen",
|
||||
"permission_join": "Beitreten ({val})",
|
||||
"permission_read": "Lesen ({val})",
|
||||
"permission_write": "Schreiben ({val})",
|
||||
"permission_pres": "Benachrichtigt werden ({val})",
|
||||
"permission_admin": "Bestätigen ({val})",
|
||||
"permission_share": "Teilen ({val})",
|
||||
"permission_delete": "Entfernen ({val})",
|
||||
"permission_owner": "Besitzer ({val})",
|
||||
"title_permissions": "Berechtigungen",
|
||||
"message_sending": "wird gesendet...",
|
||||
"message_sending_failed": "fehlgeschlagen",
|
||||
"search_placeholder": "Liste: email:lisa@beispiel.de, tel:17025550003...",
|
||||
"messaging_disabled_prompt": "Nachrichtenübermittlung deaktiviert",
|
||||
"new_message_prompt": "Neue Nachricht",
|
||||
"image_caption_prompt": "Bildunterschrift",
|
||||
"file_attachment_too_large": "Die Dateigröße {size} überschreitet das Limit von {limit}.",
|
||||
"cannot_initiate_file_upload": "Datei kann nicht hochgeladen werden.",
|
||||
"tags_not_found": "Keine Schlagworte definiert. Erstellen Sie welche.",
|
||||
"tags_editor_no_tags": "Schlagworte hinzufügen",
|
||||
"title_manage_tags": "Verwalten",
|
||||
"channel_prompt": "Dies ist ein Sender",
|
||||
"channel": "Sender"
|
||||
}
|
||||
@@ -65,6 +65,7 @@
|
||||
"online_now": "online now",
|
||||
"last_seen_timestamp": "Last seen",
|
||||
"title_not_found": "Not found",
|
||||
"channel": "channel",
|
||||
"unnamed_topic": "Unnamed",
|
||||
"messages_not_readable": "no access to messages",
|
||||
"peers_messaging_disabled": "Peer's messaging is disabled.",
|
||||
@@ -150,6 +151,7 @@
|
||||
"group_user_id_prompt": "Group or User ID",
|
||||
"button_subscribe": "Subscribe",
|
||||
"topic_name_editing_placeholder": "Freeform name of the group",
|
||||
"channel_prompt": "This is a channel",
|
||||
"button_create": "Create",
|
||||
"permission_join": "Join ({val})",
|
||||
"permission_read": "Read ({val})",
|
||||
|
||||
+3
-1
@@ -170,5 +170,7 @@
|
||||
"cannot_initiate_file_upload": "No se pudo iniciar la carga del archivo.",
|
||||
"tags_not_found": "No hay etiquetas definidas. Añade unas.",
|
||||
"tags_editor_no_tags": "Añadir etiquetas",
|
||||
"title_manage_tags": "Gestionar"
|
||||
"title_manage_tags": "Gestionar",
|
||||
"channel_prompt": "Este es un canal",
|
||||
"channel": "canal"
|
||||
}
|
||||
|
||||
+5
-3
@@ -171,7 +171,9 @@
|
||||
"tags_not_found": "태그가정의되지않았습니다.일부를추가하십시오.",
|
||||
"tags_editor_no_tags": "일부태그추가",
|
||||
"title_manage_tags": "관리",
|
||||
"granted_permissions": ":!![MISSING]!!:Granted",
|
||||
"messages_not_readable": ":!![MISSING]!!:no access to messages",
|
||||
"deleted_content": ":!![MISSING]!!:content deleted"
|
||||
"granted_permissions": "승낙하다",
|
||||
"messages_not_readable": "메시지에 액세스 할 수 없습니다",
|
||||
"deleted_content": "내용이 삭제되었습니다",
|
||||
"channel_prompt": "이 채널입니다",
|
||||
"channel": "채널"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
{
|
||||
"validate_credential_action": "confirmă",
|
||||
"title_tag_manager": "Tag-uri (user discovery)",
|
||||
"label_user_contacts": "Contacte:",
|
||||
"button_add_another": "Adaugă",
|
||||
"label_message_sound": "Sunet de mesaj:",
|
||||
"label_push_notifications": "Alerte de notificare:",
|
||||
"label_push_notifications_disabled": "Alerte de notificare (necesită HTTPS):",
|
||||
"label_incognito_mode": "Mod incognito:",
|
||||
"delete_account": "Șterge contul",
|
||||
"delete_account_arning": "Ești sigur că dorești să ștergi contul? Acesta nu va putea fi recuperat.",
|
||||
"label_password": "Parola",
|
||||
"password_unchanged_prompt": "Neschimbată",
|
||||
"button_logout": "Deconectare",
|
||||
"button_delete_account": "Șterge cont",
|
||||
"label_default_access_mode": "Default access mode:",
|
||||
"blocked_contacts_link": "Contact blocate ({count})",
|
||||
"link_contact_us": "Contactează-ne",
|
||||
"link_terms_of_service": "Termenii Serviciului",
|
||||
"link_privacy_policy": "Politica de Confidențialitate",
|
||||
"label_sdk": "SDK:",
|
||||
"label_server_address": "Adresa serverului:",
|
||||
"archived_contacts": "Contacte arhivate ({count})",
|
||||
"contacts_not_found": "Nu ai contacte salvate<br />¯∖_(ツ)_/¯",
|
||||
"full_name_prompt": "Numele tău",
|
||||
"email_prompt": "Adresa de e-mail",
|
||||
"button_sign_up": "Creează cont",
|
||||
"label_your_name": "Numele tău",
|
||||
"label_user_id": "ID:",
|
||||
"button_edit": "Editează",
|
||||
"requested_permissions": "Necesare",
|
||||
"granted_permissions": "Oferite",
|
||||
"menu_item_edit_permissions": "Editează permisiuni",
|
||||
"label_other_user": "Altele",
|
||||
"action_clear_messages": "Șterge mesajele",
|
||||
"clear_messages_warning": "Ești sigur că dorești să ștergi mesajele? Acestea nu pot fi recuperate.",
|
||||
"action_delete_messages": "Șterge toate mesajele",
|
||||
"delete_messages_warning": "Ești sigur că dorești să șterge mesajele din toate conversațiile? Acestea nu pot fi recuperate.",
|
||||
"action_leave_chat": "Ieși din conversație",
|
||||
"leave_chat_warning": "Ești sigur că dorești să ieși din conersație?",
|
||||
"action_block_contact": "Blochează contact",
|
||||
"block_contact_warning": "Ești sigur că dorești să blochezi acest contact?",
|
||||
"action_report_chat": "Raportează conversația",
|
||||
"report_chat_warning": "Ești sigur că dorești să blochezi și să raportezi această conversație?",
|
||||
"title_info": "Info",
|
||||
"label_topic_name": "Nume",
|
||||
"label_private": "Comentariu privat",
|
||||
"private_editing_placeholder": "Vizibil doar ție",
|
||||
"label_muting_topic": "Mod silențios (muted):",
|
||||
"action_more": "Mai multe",
|
||||
"label_your_permissions": "Permisiuniile tale:",
|
||||
"label_permissions": "Permisiuni:",
|
||||
"label_you": "Tu:",
|
||||
"label_default_access": "Modul default:",
|
||||
"label_group_members": "Membrii grupului:",
|
||||
"button_add_members": "Adaugă persoane",
|
||||
"group_has_no_members": "În acest grup nu se află persoane",
|
||||
"login_prompt": "Conectează-te",
|
||||
"password_prompt": "Parola",
|
||||
"stay_logged_in": "Rămâi conectat",
|
||||
"forgot_password_link": "Ți-ai uitat parola?",
|
||||
"button_sign_in": "Conectează-te",
|
||||
"label_client": "Client:",
|
||||
"label_server": "Server:",
|
||||
"online_now": "online acum",
|
||||
"last_seen_timestamp": "Văzut ultima dată",
|
||||
"title_not_found": "Nu a fost găsit",
|
||||
"channel": "canal",
|
||||
"unnamed_topic": "Nedenumit",
|
||||
"messages_not_readable": "Mesajele nu pot fi citite",
|
||||
"peers_messaging_disabled": "Mesageria de tip Peer este dezactivată.",
|
||||
"enable_peers_messaging": "Activează",
|
||||
"search_for_contacts": "Caută în contacte",
|
||||
"search_no_results": "Nu s-au găsit rezultate",
|
||||
"tabtitle_find_user": "caută",
|
||||
"tabtitle_new_group": "grup nou",
|
||||
"tabtitle_group_by_id": "după id",
|
||||
"new_password_placeholder": "Introdu parolă",
|
||||
"label_reset_password": "Resetează parola:",
|
||||
"credential_email_prompt": "Adresa de e-mail",
|
||||
"button_reset": "Resetează",
|
||||
"button_send_request": "Trimite cerere",
|
||||
"label_server_to_use": "Server de utilizat:",
|
||||
"label_wire_transport": "Transportul legat:",
|
||||
"button_update": "Actualizează",
|
||||
"sidepanel_title_login": "Conectează-te",
|
||||
"sidepanel_title_register": "Creează Cont",
|
||||
"sidepanel_title_settings": "Setări",
|
||||
"sidepanel_title_account_settings": "Setările Contului",
|
||||
"sidepanel_title_acc_general": "Generale",
|
||||
"sidepanel_title_acc_security": "Securitate",
|
||||
"sidepanel_title_acc_notifications": "Notificări",
|
||||
"sidepanel_title_acc_support": "Ajutor",
|
||||
"sidepanel_title_newtpk": "Creeză un nou Chat",
|
||||
"sidepanel_title_cred": "Confirmă credențiale",
|
||||
"sidepanel_title_reset": "Resetează Parola",
|
||||
"sidepanel_title_archive": "Conversații Arhivate",
|
||||
"sidepanel_title_blocked": "Conversații Blocate",
|
||||
"reconnect_countdown": "Deconectat. Se încearcă conectarea în {seconds}…",
|
||||
"reconnect_now": "Încearcă din nou",
|
||||
"push_init_failed": "Nu s-a reușit inițializarea tip push notifications",
|
||||
"invalid_security_token": "Codul de securitate este invalid",
|
||||
"no_connection": "Nu există conexiune",
|
||||
"code_doesnot_match": "Codul furnizat nu este valid",
|
||||
"update_available": "Actualizare disponibilă.",
|
||||
"reload_update": "Reîncarcă",
|
||||
"phone_dative": "telefon",
|
||||
"email_dative": "email",
|
||||
"enter_confirmation_code_prompt": "Introu codul de confirmare trimis pe {method}:",
|
||||
"numeric_confirmation_code_prompt": "Doar cifre",
|
||||
"button_confirm": "Confirmă",
|
||||
"save_attachment": "salvează",
|
||||
"deleted_content": "conținut șters",
|
||||
"invalid_content": "conținut invalid",
|
||||
"user_not_found": "Utilizatorul nu a fost găsit",
|
||||
"badge_you": "tu",
|
||||
"badge_owner": "owner",
|
||||
"menu_item_info": "Info",
|
||||
"menu_item_clear_messages": "Șterge mesajele",
|
||||
"menu_item_clear_messages_for_all": "Ștergele pe toate",
|
||||
"menu_item_delete": "Șterge",
|
||||
"menu_item_delete_for_all": "Ștergele pe toate",
|
||||
"menu_item_send_retry": "Reîncearcă",
|
||||
"menu_item_mute": "Mute",
|
||||
"menu_item_unmute": "Unmute",
|
||||
"menu_item_delete_topic": "Șterge",
|
||||
"topic_delete_warning": "Ești sigur că dorești să șterge această conversație?",
|
||||
"menu_item_unblock": "Deblochează",
|
||||
"menu_item_block": "Blochează",
|
||||
"topic_block_warning": "Ești sigur că dorești să blochezi această conversație?",
|
||||
"menu_item_member_delete": "Șterge persoană",
|
||||
"menu_item_archive_topic": "Arhivează",
|
||||
"action_cancel": "anulează",
|
||||
"upload_finishing": "se încarcă...",
|
||||
"no_contacts": "Nu ai contacte adăugate :-(",
|
||||
"contacts_not_found_short": "Niciun contact găsit după criteriile ''{query}''",
|
||||
"title_group_members": "Persoanele din grup",
|
||||
"title_all_contacts": "Toate Contactele",
|
||||
"button_ok": "OK",
|
||||
"button_cancel": "Anulează",
|
||||
"more_online_members": "+{overflow} mai mult",
|
||||
"download_action": "download",
|
||||
"label_file_name": "Nume fișier:",
|
||||
"label_content_type": "Tip conținut:",
|
||||
"label_size": "Size:",
|
||||
"chat_invitation": "Ai primit invitație într-o conversație. Accepți?",
|
||||
"chat_invitation_accept": "Da, accept",
|
||||
"chat_invitation_ignore": "Ignoră",
|
||||
"chat_invitation_block": "Blochează",
|
||||
"error_invalid_id": "ID Invalid",
|
||||
"group_user_id_prompt": "Grup sau user ID",
|
||||
"button_subscribe": "Subscribe",
|
||||
"topic_name_editing_placeholder": "Numele grupului",
|
||||
"channel_prompt": "Acesta este un canal",
|
||||
"button_create": "Creează",
|
||||
"permission_join": "Participare ({val})",
|
||||
"permission_read": "Citire ({val})",
|
||||
"permission_write": "Scriere ({val})",
|
||||
"permission_pres": "Notificare ({val})",
|
||||
"permission_admin": "Aprobare ({val})",
|
||||
"permission_share": "Distribuire ({val})",
|
||||
"permission_delete": "Ștergere ({val})",
|
||||
"permission_owner": "Deținător ({val})",
|
||||
"title_permissions": "Permisiuni",
|
||||
"message_sending": "se trimite...",
|
||||
"message_sending_failed": "eroare",
|
||||
"search_placeholder": "Caută după e-mail sau număr telefon...",
|
||||
"messaging_disabled_prompt": "Mesageria este dezactivată",
|
||||
"new_message_prompt": "Mesaj nou",
|
||||
"image_caption_prompt": "Titlul imaginii",
|
||||
"file_attachment_too_large": "Dimensiunea fișiterului {size} depășește limita de {limit}.",
|
||||
"cannot_initiate_file_upload": "Nu se poate face încărcarea.",
|
||||
"tags_not_found": "Niciun tag definit. Adaugă.",
|
||||
"tags_editor_no_tags": "Adaugă tag-uri",
|
||||
"title_manage_tags": "Administrează"
|
||||
}
|
||||
+3
-1
@@ -170,5 +170,7 @@
|
||||
"push_init_failed": "Ошибка инициализации пуш уведомлений",
|
||||
"invalid_security_token": "Токен некорректен",
|
||||
"no_connection": "Нет связи",
|
||||
"code_doesnot_match": "Код не совпадает"
|
||||
"code_doesnot_match": "Код не совпадает",
|
||||
"channel_prompt": "Создать канал",
|
||||
"channel": "канал"
|
||||
}
|
||||
|
||||
+3
-1
@@ -170,5 +170,7 @@
|
||||
"push_init_failed": "初始化推送通知失败",
|
||||
"invalid_security_token": "无效的安全令牌",
|
||||
"no_connection": "无连接",
|
||||
"code_doesnot_match": "代码不匹配"
|
||||
"code_doesnot_match": "代码不匹配",
|
||||
"channel_prompt": "这是一个频道",
|
||||
"channel": "频道"
|
||||
}
|
||||
|
||||
+52
-64
@@ -1,5 +1,6 @@
|
||||
// File and image helper functions.
|
||||
import { MAX_INBAND_ATTACHMENT_SIZE, MAX_IMAGE_DIM } from '../config.js';
|
||||
import { bytesToHumanSize } from './strformat.js'
|
||||
|
||||
// Supported image MIME types and corresponding file extensions.
|
||||
export const SUPPORTED_IMAGE_FORMATS = ['image/jpeg', 'image/gif', 'image/png', 'image/svg', 'image/svg+xml'];
|
||||
@@ -55,105 +56,92 @@ export function fitImageSize(width, height, maxWidth, maxHeight, forceSquare) {
|
||||
|
||||
// Ensure file's extension matches mime content type
|
||||
export function fileNameForMime(fname, mime) {
|
||||
var idx = SUPPORTED_IMAGE_FORMATS.indexOf(mime);
|
||||
var ext = MIME_EXTENSIONS[idx];
|
||||
const idx = SUPPORTED_IMAGE_FORMATS.indexOf(mime);
|
||||
if (idx < 0 || !fname) {
|
||||
// Unknown mime or empty name.
|
||||
return fname;
|
||||
}
|
||||
const ext = MIME_EXTENSIONS[idx];
|
||||
|
||||
var at = fname.lastIndexOf('.');
|
||||
const at = fname.lastIndexOf('.');
|
||||
if (at >= 0) {
|
||||
fname = fname.substring(0, at);
|
||||
}
|
||||
return fname + '.' + ext;
|
||||
}
|
||||
|
||||
// Convert uploaded image into a base64-encoded string possibly scaling
|
||||
// linear dimensions or constraining to a square.
|
||||
export function imageFileScaledToBase64(file, width, height, forceSquare, onSuccess, onError) {
|
||||
var img = new Image();
|
||||
// Scale uploaded image to fit under certain dimensions and byte size, optionally constraining to a square.
|
||||
// On success calls onSuccess callback with the scaled image as Blob.
|
||||
export function imageScaled(fileOrBlob, maxWidth, maxHeight, maxSize, forceSquare, onSuccess, onError) {
|
||||
const img = new Image();
|
||||
img.crossOrigin = 'Anonymous';
|
||||
img.onerror = function(err) {
|
||||
onError("Image format unrecognized");
|
||||
}
|
||||
img.onload = function() {
|
||||
var dim = fitImageSize(this.width, this.height, width, height, forceSquare);
|
||||
img.onload = async function() {
|
||||
// Once the image is loaded, the URL is no longer needed.
|
||||
URL.revokeObjectURL(img.src);
|
||||
|
||||
// Calculate the desired image dimensions.
|
||||
const dim = fitImageSize(this.width, this.height, maxWidth, maxHeight, forceSquare);
|
||||
if (!dim) {
|
||||
onError("Invalid image");
|
||||
return;
|
||||
}
|
||||
var canvas = document.createElement('canvas');
|
||||
let canvas = document.createElement('canvas');
|
||||
canvas.width = dim.dstWidth;
|
||||
canvas.height = dim.dstHeight;
|
||||
var ctx = canvas.getContext('2d');
|
||||
let ctx = canvas.getContext('2d');
|
||||
ctx.imageSmoothingEnabled = true;
|
||||
ctx.drawImage(this, dim.xoffset, dim.yoffset, dim.srcWidth, dim.srcHeight,
|
||||
0, 0, dim.dstWidth, dim.dstHeight);
|
||||
|
||||
var mime = (this.width != dim.dstWidth ||
|
||||
this.height != dim.dstHeight ||
|
||||
SUPPORTED_IMAGE_FORMATS.indexOf(file.type) < 0) ? 'image/jpeg' : file.type;
|
||||
var imageBits = canvas.toDataURL(mime);
|
||||
var parts = imageBits.split(',');
|
||||
// Get actual image type: 'data:image/png;base64,'
|
||||
mime = getMimeType(parts[0]);
|
||||
if (!mime) {
|
||||
const mime = SUPPORTED_IMAGE_FORMATS.indexOf(fileOrBlob.type) < 0 ? 'image/jpeg' : fileOrBlob.type;
|
||||
// Generate blob to check size of the image.
|
||||
let blob = await new Promise(resolve => canvas.toBlob(resolve, mime));
|
||||
if (!blob) {
|
||||
onError("Unsupported image format");
|
||||
return;
|
||||
}
|
||||
// Ensure the image is not too large
|
||||
var quality = 0.78;
|
||||
if (base64DecodedLen(imageBits.length) > MAX_INBAND_ATTACHMENT_SIZE) {
|
||||
mime = 'image/jpeg';
|
||||
}
|
||||
if (mime == 'image/jpeg') {
|
||||
// Reduce size of the jpeg by reducing image quality
|
||||
while (base64DecodedLen(imageBits.length) > MAX_INBAND_ATTACHMENT_SIZE && quality > 0.45) {
|
||||
imageBits = canvas.toDataURL(mime, quality);
|
||||
quality *= 0.84;
|
||||
}
|
||||
}
|
||||
if (base64DecodedLen(imageBits.length) > MAX_INBAND_ATTACHMENT_SIZE) {
|
||||
onError("The image size " + bytesToHumanSize(base64DecodedLen(imageBits.length)) +
|
||||
" exceeds the " + bytesToHumanSize(MAX_INBAND_ATTACHMENT_SIZE) + " limit.", "err");
|
||||
return;
|
||||
|
||||
// Ensure the image is not too large. Shrink the image keeping the aspect ratio.
|
||||
// Do nothing if maxsize is <= 0.
|
||||
while (maxSize > 0 && blob.length > maxSize) {
|
||||
dim.dstWidth = (dim.dstWidth * 0.70710678118) | 0;
|
||||
dim.dstHeight = (dim.dstHeight * 0.70710678118) | 0;
|
||||
canvas.width = dim.dstWidth;
|
||||
canvas.height = dim.dstHeight;
|
||||
ctx = canvas.getContext('2d');
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
ctx.drawImage(this, dim.xoffset, dim.yoffset, dim.srcWidth, dim.srcHeight,
|
||||
0, 0, dim.dstWidth, dim.dstHeight);
|
||||
blob = await new Promise(resolve => canvas.toBlob(resolve, mime));
|
||||
}
|
||||
|
||||
canvas = null;
|
||||
onSuccess(imageBits.split(',')[1], mime, dim.dstWidth, dim.dstHeight, fileNameForMime(file.name, mime));
|
||||
onSuccess(blob, mime, dim.dstWidth, dim.dstHeight, fileNameForMime(fileOrBlob.name, mime));
|
||||
};
|
||||
img.src = URL.createObjectURL(file);
|
||||
img.src = URL.createObjectURL(fileOrBlob);
|
||||
}
|
||||
|
||||
// Convert uploaded image file to base64-encoded string without scaling/converting the image
|
||||
export function imageFileToBase64(file, onSuccess, onError) {
|
||||
var reader = new FileReader();
|
||||
reader.addEventListener('load', function() {
|
||||
var parts = reader.result.split(',');
|
||||
var mime = getMimeType(parts[0]);
|
||||
if (!mime) {
|
||||
onError("Failed to process image file");
|
||||
return;
|
||||
}
|
||||
|
||||
// Get image size.
|
||||
var img = new Image();
|
||||
img.crossOrigin = 'Anonymous';
|
||||
img.onload = function() {
|
||||
onSuccess(parts[1], mime, this.width, this.height, fileNameForMime(file.name, mime));
|
||||
}
|
||||
img.onerror = function(err) {
|
||||
onError("Image format not recognized");
|
||||
}
|
||||
img.src = URL.createObjectURL(file);
|
||||
}, false);
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
|
||||
export function fileToBase64(file, onSuccess, onError) {
|
||||
var reader = new FileReader();
|
||||
// Convert File to base64 string.
|
||||
export function fileToBase64(file, onSuccess) {
|
||||
const reader = new FileReader();
|
||||
reader.addEventListener('load', function() {
|
||||
onSuccess(file.type, reader.result.split(',')[1], file.name);
|
||||
});
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
|
||||
// Convert Blob to base64 string.
|
||||
export function blobToBase64(blob, onSuccess) {
|
||||
const reader = new FileReader();
|
||||
reader.addEventListener('load', function() {
|
||||
onSuccess(blob.type, reader.result.split(',')[1]);
|
||||
});
|
||||
reader.readAsDataURL(blob);
|
||||
}
|
||||
|
||||
// File pasted from the clipboard. It's either an inline image or a file attachment.
|
||||
// FIXME: handle large files out of band.
|
||||
export function filePasted(event, onImageSuccess, onAttachmentSuccess, onError) {
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
export default class HashNavigation {
|
||||
static parseUrlHash(hash) {
|
||||
// Split path from args, path -> parts[0], args->path[1]
|
||||
let parts = hash.split('?', 2);
|
||||
let params = {};
|
||||
const parts = hash.split('?', 2);
|
||||
const params = {};
|
||||
let path = [];
|
||||
if (parts[0]) {
|
||||
path = parts[0].substr(1).split('/');
|
||||
@@ -27,9 +27,9 @@ export default class HashNavigation {
|
||||
}
|
||||
|
||||
static composeUrlHash(path, params) {
|
||||
var url = path.join('/');
|
||||
var args = [];
|
||||
for (var key in params) {
|
||||
let url = path.join('/');
|
||||
const args = [];
|
||||
for (const key in params) {
|
||||
if (params.hasOwnProperty(key)) {
|
||||
args.push(key + '=' + params[key]);
|
||||
}
|
||||
@@ -41,25 +41,25 @@ export default class HashNavigation {
|
||||
}
|
||||
|
||||
static addUrlParam(hash, key, value) {
|
||||
var parsed = this.parseUrlHash(hash);
|
||||
const parsed = this.parseUrlHash(hash);
|
||||
parsed.params[key] = value;
|
||||
return this.composeUrlHash(parsed.path, parsed.params);
|
||||
}
|
||||
|
||||
static removeUrlParam(hash, key) {
|
||||
var parsed = this.parseUrlHash(hash);
|
||||
const parsed = this.parseUrlHash(hash);
|
||||
delete parsed.params[key];
|
||||
return this.composeUrlHash(parsed.path, parsed.params);
|
||||
}
|
||||
|
||||
static setUrlSidePanel(hash, sidepanel) {
|
||||
var parsed = this.parseUrlHash(hash);
|
||||
const parsed = this.parseUrlHash(hash);
|
||||
parsed.path[0] = sidepanel;
|
||||
return this.composeUrlHash(parsed.path, parsed.params);
|
||||
}
|
||||
|
||||
static setUrlTopic(hash, topic) {
|
||||
var parsed = this.parseUrlHash(hash);
|
||||
const parsed = this.parseUrlHash(hash);
|
||||
parsed.path[1] = topic;
|
||||
// Close InfoView on topic change.
|
||||
delete parsed.params.info;
|
||||
|
||||
+7
-3
@@ -88,11 +88,12 @@ export function asEmail(val) {
|
||||
// http:/example.com - not ok.
|
||||
// ' ↲ https://example.com' - not ok. (↲ means carriage return)
|
||||
export function isUrlRelative(url) {
|
||||
return !/^\s*([a-z][a-z0-9+.-]*:|\/\/)/im.test(url);
|
||||
return url && !/^\s*([a-z][a-z0-9+.-]*:|\/\/)/im.test(url);
|
||||
}
|
||||
|
||||
// Ensure URL does not present an XSS risk. Optional allowedSchemes may contain an array of
|
||||
// strings with permitted URL schemes, such as ['ftp', 'ftps']; otherwise accept http and https only.
|
||||
// If apikey and/or token is defined, it's appended to relative URL.
|
||||
export function sanitizeUrl(url, allowedSchemes) {
|
||||
if (!url) {
|
||||
return null;
|
||||
@@ -102,9 +103,11 @@ export function sanitizeUrl(url, allowedSchemes) {
|
||||
url = url.replace(/[^\x20-\x7E]/gmi, '').trim();
|
||||
|
||||
// Relative URLs are safe.
|
||||
// Relative URL does not start with ':', abcd123: or '//'.
|
||||
if (!/^([a-z][a-z0-9+.-]*:|\/\/)/i.test(url)) {
|
||||
return url;
|
||||
}
|
||||
|
||||
// Blob URLs are safe.
|
||||
if (/^blob:http/.test(url)) {
|
||||
return url;
|
||||
@@ -121,8 +124,9 @@ export function sanitizeUrl(url, allowedSchemes) {
|
||||
}
|
||||
|
||||
// Ensure URL is suitable for <img src="url"> field: the URL must be a relative URL or
|
||||
// have http:, https:, or data: scheme. In case of data: scheme, the URL must start with
|
||||
// a 'data:image/XXXX;base64,'.
|
||||
// have http:, https:, blob: or data: scheme.
|
||||
// The relative URL is appended with the API key and/or auth token.
|
||||
// In case of data: scheme, the URL must start with a 'data:image/XXXX;base64,'.
|
||||
export function sanitizeImageUrl(url) {
|
||||
if (!url) {
|
||||
return null;
|
||||
|
||||
+369
-4
@@ -1,4 +1,180 @@
|
||||
{
|
||||
"de": {
|
||||
"validate_credential_action": "bestätigen",
|
||||
"title_tag_manager": "Schlagworte (Nutzer entdecken)",
|
||||
"label_user_contacts": "Kontakte:",
|
||||
"button_add_another": "Hinzufügen",
|
||||
"label_message_sound": "Benachrichtigungston:",
|
||||
"label_push_notifications": "Benachrichtigungsmeldungen:",
|
||||
"label_push_notifications_disabled": "Benachrichtigungsmeldungen (erfordert HTTPS):",
|
||||
"label_incognito_mode": "inkognito-Modus:",
|
||||
"delete_account": "Konto löschen",
|
||||
"delete_account_arning": "Möchten Sie das Konto wirklich löschen? Das kann nicht rückgängig gemacht werden",
|
||||
"label_password": "Passwort",
|
||||
"password_unchanged_prompt": "unverändert",
|
||||
"button_logout": "Abmelden",
|
||||
"button_delete_account": "Konto löschen",
|
||||
"label_default_access_mode": "Standard Zugriffsmodus:",
|
||||
"blocked_contacts_link": "Blockierte Kontakte ({count})",
|
||||
"link_contact_us": "Kontakt",
|
||||
"link_terms_of_service": "Nutzungsbedingungen",
|
||||
"link_privacy_policy": "Datenschutzerklärung",
|
||||
"label_sdk": "SDK:",
|
||||
"label_server_address": "Server Adresse:",
|
||||
"archived_contacts": "Archivierte Kontakte ({count})",
|
||||
"contacts_not_found": "Sie haben keine Unterhaltungen<br />¯∖_(ツ)_/¯",
|
||||
"full_name_prompt": "Vollständiger Name, z.B. Lisa Musterfrau",
|
||||
"email_prompt": "E-Mail, z.B. jdoe@beispiel.de",
|
||||
"button_sign_up": "Anmelden",
|
||||
"label_your_name": "Ihr Name",
|
||||
"label_user_id": "ID:",
|
||||
"button_edit": "Bearbeiten",
|
||||
"requested_permissions": "Angefordert",
|
||||
"granted_permissions": "Erteilt",
|
||||
"menu_item_edit_permissions": "Berechtigungen ändern",
|
||||
"label_other_user": "Andere",
|
||||
"action_clear_messages": "Nachrichten leeren",
|
||||
"clear_messages_warning": "Sind Sie sicher, dass Sie diese Unterhaltung für alle leeren wollen? Das kann nicht rückgängig gemacht werden.",
|
||||
"action_delete_messages": "Nachrichten für alle löschen",
|
||||
"delete_messages_warning": "Sind Sie sicher, dass Sie diese Unterhaltung für alle löschen wollen? Das kann nicht rückgängig gemacht werden.",
|
||||
"action_leave_chat": "Unterhaltung verlassen",
|
||||
"leave_chat_warning": "Möchten Sie diese Unterhaltung wirklich verlassen?",
|
||||
"action_block_contact": "Kontakt blockieren",
|
||||
"block_contact_warning": "Möchten Sie diesen Kontakt wirklich blockieren?",
|
||||
"action_report_chat": "Unterhaltung melden",
|
||||
"report_chat_warning": "Möchten Sie diese Unterhaltung wirklich blockieren und melden?",
|
||||
"title_info": "Info",
|
||||
"label_topic_name": "Name",
|
||||
"label_private": "Privater Kommentar",
|
||||
"private_editing_placeholder": "Nur für Sie sichtbar",
|
||||
"label_muting_topic": "Stumm geschaltet:",
|
||||
"action_more": "Mehr",
|
||||
"label_your_permissions": "Ihre Berechtigungen:",
|
||||
"label_permissions": "Berechtigungen:",
|
||||
"label_you": "Sie:",
|
||||
"label_default_access": "Standard Zugriffsmodus:",
|
||||
"label_group_members": "Gruppenmitglieder:",
|
||||
"button_add_members": "Mitglieder hinzufügen",
|
||||
"group_has_no_members": "Keine Mitglieder",
|
||||
"login_prompt": "Anmelden",
|
||||
"password_prompt": "Passwort",
|
||||
"stay_logged_in": "Angemeldet bleiben",
|
||||
"forgot_password_link": "Passwort vergessen?",
|
||||
"button_sign_in": "Anmelden",
|
||||
"label_client": "Client:",
|
||||
"label_server": "Server:",
|
||||
"online_now": "jetzt online",
|
||||
"last_seen_timestamp": "Zuletzt gesehen",
|
||||
"title_not_found": "Nicht gefunden",
|
||||
"unnamed_topic": "Unbenannt",
|
||||
"messages_not_readable": "Neue Nachrichten können nicht gelesen werden",
|
||||
"peers_messaging_disabled": "Gruppennachrichten sind deaktiviert",
|
||||
"enable_peers_messaging": "Aktivieren",
|
||||
"search_for_contacts": "Nutzen Sie die Suche um Kontakte zu finden",
|
||||
"search_no_results": "Die Suche hatte keine Ergebnisse",
|
||||
"tabtitle_find_user": "Suchen",
|
||||
"tabtitle_new_group": "Neue Gruppe",
|
||||
"tabtitle_group_by_id": "nach ID",
|
||||
"new_password_placeholder": "Geben Sie ein neues Passwort ein",
|
||||
"label_reset_password": "Passwort per E-Mail wiederherstellen:",
|
||||
"credential_email_prompt": "E-Mail Adresse für Registrierung",
|
||||
"button_reset": "Zurücksetzen",
|
||||
"button_send_request": "Anfrage senden",
|
||||
"label_server_to_use": "Server verwenden:",
|
||||
"label_wire_transport": "Übertragung per Kabel:",
|
||||
"button_update": "Aktualisieren",
|
||||
"sidepanel_title_login": "Anmelden",
|
||||
"sidepanel_title_register": "Konto erstellen",
|
||||
"sidepanel_title_settings": "Einstellungen",
|
||||
"sidepanel_title_account_settings": "Konto-Einstellungen",
|
||||
"sidepanel_title_acc_general": "Allgemein",
|
||||
"sidepanel_title_acc_security": "Sicherheit",
|
||||
"sidepanel_title_acc_notifications": "Benachrichtigungen",
|
||||
"sidepanel_title_acc_support": "Unterstützung",
|
||||
"sidepanel_title_newtpk": "Neue Unterhaltung starten",
|
||||
"sidepanel_title_cred": "Anmeldeinformationen bestätigen",
|
||||
"sidepanel_title_reset": "Passwort zurücksetzen",
|
||||
"sidepanel_title_archive": "Archivierte Unterhaltungen",
|
||||
"sidepanel_title_blocked": "Blockierte Unterhaltungen",
|
||||
"reconnect_countdown": "Getrennt. Wiederverbinden in {seconds}…",
|
||||
"reconnect_now": "Jetzt probieren",
|
||||
"push_init_failed": "Initialisierung von Push-Benachrichtigungen fehlgeschlagen",
|
||||
"invalid_security_token": "Ungültiger Sicherheitsschlüssel",
|
||||
"no_connection": "Keine Verbindung",
|
||||
"code_doesnot_match": "Code stimmt nicht überein",
|
||||
"update_available": "Aktualisierung verfügbar.",
|
||||
"reload_update": "Neu laden",
|
||||
"phone_dative": "Telefon",
|
||||
"email_dative": "E-Mail",
|
||||
"enter_confirmation_code_prompt": "Geben Sie den Bestätigungscode ein, der per {method} geschickt wurde:",
|
||||
"numeric_confirmation_code_prompt": "Nur zahlen",
|
||||
"button_confirm": "Bestätigen",
|
||||
"save_attachment": "Speichern",
|
||||
"deleted_content": "Inhalt gelöscht",
|
||||
"invalid_content": "ungültiger Inhalt",
|
||||
"user_not_found": "Nicht gefunden",
|
||||
"badge_you": "Sie",
|
||||
"badge_owner": "Besitzer",
|
||||
"menu_item_info": "Info",
|
||||
"menu_item_clear_messages": "Nachrichten leeren",
|
||||
"menu_item_clear_messages_for_all": "Für alle leeren",
|
||||
"menu_item_delete": "Löschen",
|
||||
"menu_item_delete_for_all": "Für alle löschen",
|
||||
"menu_item_send_retry": "Wiederholen",
|
||||
"menu_item_mute": "Unterhaltung stumm schalten",
|
||||
"menu_item_unmute": "Stumm schalten beenden",
|
||||
"menu_item_delete_topic": "Entfernen",
|
||||
"topic_delete_warning": "Möchten Sie diese Unterhaltung wirklich löschen?",
|
||||
"menu_item_unblock": "Blockierung aufheben",
|
||||
"menu_item_block": "Blockieren",
|
||||
"topic_block_warning": "Möchten Sie diese Unterhaltung wirklich blockieren?",
|
||||
"menu_item_member_delete": "Entfernen",
|
||||
"menu_item_archive_topic": "Archivieren",
|
||||
"action_cancel": "Abbrechen",
|
||||
"upload_finishing": "wird abgeschlossen...",
|
||||
"no_contacts": "Sie haben keine Kontakte :-(",
|
||||
"contacts_not_found_short": "Keine Kontakte für ''{query}''",
|
||||
"title_group_members": "Gruppenmitglieder",
|
||||
"title_all_contacts": "Alle Kontakte",
|
||||
"button_ok": "OK",
|
||||
"button_cancel": "Abbrechen",
|
||||
"more_online_members": "+{overflow} mehr",
|
||||
"download_action": "herunterladen",
|
||||
"label_file_name": "Dateiname:",
|
||||
"label_content_type": "Inhaltsart:",
|
||||
"label_size": "Größe:",
|
||||
"chat_invitation": "Sie sind eingeladen, an einem neuen Chat teilzunehmen. Was möchten Sie tun?",
|
||||
"chat_invitation_accept": "Akzeptieren",
|
||||
"chat_invitation_ignore": "Ignorieren",
|
||||
"chat_invitation_block": "Blockieren",
|
||||
"error_invalid_id": "ungültige ID",
|
||||
"group_user_id_prompt": "Gruppe oder Benutzer ID",
|
||||
"button_subscribe": "Abbonieren",
|
||||
"topic_name_editing_placeholder": "Freiform Name der Gruppe",
|
||||
"button_create": "Erstellen",
|
||||
"permission_join": "Beitreten ({val})",
|
||||
"permission_read": "Lesen ({val})",
|
||||
"permission_write": "Schreiben ({val})",
|
||||
"permission_pres": "Benachrichtigt werden ({val})",
|
||||
"permission_admin": "Bestätigen ({val})",
|
||||
"permission_share": "Teilen ({val})",
|
||||
"permission_delete": "Entfernen ({val})",
|
||||
"permission_owner": "Besitzer ({val})",
|
||||
"title_permissions": "Berechtigungen",
|
||||
"message_sending": "wird gesendet...",
|
||||
"message_sending_failed": "fehlgeschlagen",
|
||||
"search_placeholder": "Liste: email:lisa@beispiel.de, tel:17025550003...",
|
||||
"messaging_disabled_prompt": "Nachrichtenübermittlung deaktiviert",
|
||||
"new_message_prompt": "Neue Nachricht",
|
||||
"image_caption_prompt": "Bildunterschrift",
|
||||
"file_attachment_too_large": "Die Dateigröße {size} überschreitet das Limit von {limit}.",
|
||||
"cannot_initiate_file_upload": "Datei kann nicht hochgeladen werden.",
|
||||
"tags_not_found": "Keine Schlagworte definiert. Erstellen Sie welche.",
|
||||
"tags_editor_no_tags": "Schlagworte hinzufügen",
|
||||
"title_manage_tags": "Verwalten",
|
||||
"channel_prompt": "Dies ist ein Sender",
|
||||
"channel": "Sender"
|
||||
},
|
||||
"en": {
|
||||
"validate_credential_action": "confirm",
|
||||
"title_tag_manager": "Tags (user discovery)",
|
||||
@@ -66,6 +242,7 @@
|
||||
"online_now": "online now",
|
||||
"last_seen_timestamp": "Last seen",
|
||||
"title_not_found": "Not found",
|
||||
"channel": "channel",
|
||||
"unnamed_topic": "Unnamed",
|
||||
"messages_not_readable": "no access to messages",
|
||||
"peers_messaging_disabled": "Peer's messaging is disabled.",
|
||||
@@ -151,6 +328,7 @@
|
||||
"group_user_id_prompt": "Group or User ID",
|
||||
"button_subscribe": "Subscribe",
|
||||
"topic_name_editing_placeholder": "Freeform name of the group",
|
||||
"channel_prompt": "This is a channel",
|
||||
"button_create": "Create",
|
||||
"permission_join": "Join ({val})",
|
||||
"permission_read": "Read ({val})",
|
||||
@@ -345,7 +523,9 @@
|
||||
"cannot_initiate_file_upload": "No se pudo iniciar la carga del archivo.",
|
||||
"tags_not_found": "No hay etiquetas definidas. Añade unas.",
|
||||
"tags_editor_no_tags": "Añadir etiquetas",
|
||||
"title_manage_tags": "Gestionar"
|
||||
"title_manage_tags": "Gestionar",
|
||||
"channel_prompt": "Este es un canal",
|
||||
"channel": "canal"
|
||||
},
|
||||
"ko": {
|
||||
"validate_credential_action": "확인",
|
||||
@@ -516,7 +696,188 @@
|
||||
"cannot_initiate_file_upload": "파일업로드를시작할수없습니다.",
|
||||
"tags_not_found": "태그가정의되지않았습니다.일부를추가하십시오.",
|
||||
"tags_editor_no_tags": "일부태그추가",
|
||||
"title_manage_tags": "관리"
|
||||
"title_manage_tags": "관리",
|
||||
"granted_permissions": "승낙하다",
|
||||
"messages_not_readable": "메시지에 액세스 할 수 없습니다",
|
||||
"deleted_content": "내용이 삭제되었습니다",
|
||||
"channel_prompt": "이 채널입니다",
|
||||
"channel": "채널"
|
||||
},
|
||||
"ro": {
|
||||
"validate_credential_action": "confirmă",
|
||||
"title_tag_manager": "Tag-uri (user discovery)",
|
||||
"label_user_contacts": "Contacte:",
|
||||
"button_add_another": "Adaugă",
|
||||
"label_message_sound": "Sunet de mesaj:",
|
||||
"label_push_notifications": "Alerte de notificare:",
|
||||
"label_push_notifications_disabled": "Alerte de notificare (necesită HTTPS):",
|
||||
"label_incognito_mode": "Mod incognito:",
|
||||
"delete_account": "Șterge contul",
|
||||
"delete_account_arning": "Ești sigur că dorești să ștergi contul? Acesta nu va putea fi recuperat.",
|
||||
"label_password": "Parola",
|
||||
"password_unchanged_prompt": "Neschimbată",
|
||||
"button_logout": "Deconectare",
|
||||
"button_delete_account": "Șterge cont",
|
||||
"label_default_access_mode": "Default access mode:",
|
||||
"blocked_contacts_link": "Contact blocate ({count})",
|
||||
"link_contact_us": "Contactează-ne",
|
||||
"link_terms_of_service": "Termenii Serviciului",
|
||||
"link_privacy_policy": "Politica de Confidențialitate",
|
||||
"label_sdk": "SDK:",
|
||||
"label_server_address": "Adresa serverului:",
|
||||
"archived_contacts": "Contacte arhivate ({count})",
|
||||
"contacts_not_found": "Nu ai contacte salvate<br />¯∖_(ツ)_/¯",
|
||||
"full_name_prompt": "Numele tău",
|
||||
"email_prompt": "Adresa de e-mail",
|
||||
"button_sign_up": "Creează cont",
|
||||
"label_your_name": "Numele tău",
|
||||
"label_user_id": "ID:",
|
||||
"button_edit": "Editează",
|
||||
"requested_permissions": "Necesare",
|
||||
"granted_permissions": "Oferite",
|
||||
"menu_item_edit_permissions": "Editează permisiuni",
|
||||
"label_other_user": "Altele",
|
||||
"action_clear_messages": "Șterge mesajele",
|
||||
"clear_messages_warning": "Ești sigur că dorești să ștergi mesajele? Acestea nu pot fi recuperate.",
|
||||
"action_delete_messages": "Șterge toate mesajele",
|
||||
"delete_messages_warning": "Ești sigur că dorești să șterge mesajele din toate conversațiile? Acestea nu pot fi recuperate.",
|
||||
"action_leave_chat": "Ieși din conversație",
|
||||
"leave_chat_warning": "Ești sigur că dorești să ieși din conersație?",
|
||||
"action_block_contact": "Blochează contact",
|
||||
"block_contact_warning": "Ești sigur că dorești să blochezi acest contact?",
|
||||
"action_report_chat": "Raportează conversația",
|
||||
"report_chat_warning": "Ești sigur că dorești să blochezi și să raportezi această conversație?",
|
||||
"title_info": "Info",
|
||||
"label_topic_name": "Nume",
|
||||
"label_private": "Comentariu privat",
|
||||
"private_editing_placeholder": "Vizibil doar ție",
|
||||
"label_muting_topic": "Mod silențios (muted):",
|
||||
"action_more": "Mai multe",
|
||||
"label_your_permissions": "Permisiuniile tale:",
|
||||
"label_permissions": "Permisiuni:",
|
||||
"label_you": "Tu:",
|
||||
"label_default_access": "Modul default:",
|
||||
"label_group_members": "Membrii grupului:",
|
||||
"button_add_members": "Adaugă persoane",
|
||||
"group_has_no_members": "În acest grup nu se află persoane",
|
||||
"login_prompt": "Conectează-te",
|
||||
"password_prompt": "Parola",
|
||||
"stay_logged_in": "Rămâi conectat",
|
||||
"forgot_password_link": "Ți-ai uitat parola?",
|
||||
"button_sign_in": "Conectează-te",
|
||||
"label_client": "Client:",
|
||||
"label_server": "Server:",
|
||||
"online_now": "online acum",
|
||||
"last_seen_timestamp": "Văzut ultima dată",
|
||||
"title_not_found": "Nu a fost găsit",
|
||||
"channel": "canal",
|
||||
"unnamed_topic": "Nedenumit",
|
||||
"messages_not_readable": "Mesajele nu pot fi citite",
|
||||
"peers_messaging_disabled": "Mesageria de tip Peer este dezactivată.",
|
||||
"enable_peers_messaging": "Activează",
|
||||
"search_for_contacts": "Caută în contacte",
|
||||
"search_no_results": "Nu s-au găsit rezultate",
|
||||
"tabtitle_find_user": "caută",
|
||||
"tabtitle_new_group": "grup nou",
|
||||
"tabtitle_group_by_id": "după id",
|
||||
"new_password_placeholder": "Introdu parolă",
|
||||
"label_reset_password": "Resetează parola:",
|
||||
"credential_email_prompt": "Adresa de e-mail",
|
||||
"button_reset": "Resetează",
|
||||
"button_send_request": "Trimite cerere",
|
||||
"label_server_to_use": "Server de utilizat:",
|
||||
"label_wire_transport": "Transportul legat:",
|
||||
"button_update": "Actualizează",
|
||||
"sidepanel_title_login": "Conectează-te",
|
||||
"sidepanel_title_register": "Creează Cont",
|
||||
"sidepanel_title_settings": "Setări",
|
||||
"sidepanel_title_account_settings": "Setările Contului",
|
||||
"sidepanel_title_acc_general": "Generale",
|
||||
"sidepanel_title_acc_security": "Securitate",
|
||||
"sidepanel_title_acc_notifications": "Notificări",
|
||||
"sidepanel_title_acc_support": "Ajutor",
|
||||
"sidepanel_title_newtpk": "Creeză un nou Chat",
|
||||
"sidepanel_title_cred": "Confirmă credențiale",
|
||||
"sidepanel_title_reset": "Resetează Parola",
|
||||
"sidepanel_title_archive": "Conversații Arhivate",
|
||||
"sidepanel_title_blocked": "Conversații Blocate",
|
||||
"reconnect_countdown": "Deconectat. Se încearcă conectarea în {seconds}…",
|
||||
"reconnect_now": "Încearcă din nou",
|
||||
"push_init_failed": "Nu s-a reușit inițializarea tip push notifications",
|
||||
"invalid_security_token": "Codul de securitate este invalid",
|
||||
"no_connection": "Nu există conexiune",
|
||||
"code_doesnot_match": "Codul furnizat nu este valid",
|
||||
"update_available": "Actualizare disponibilă.",
|
||||
"reload_update": "Reîncarcă",
|
||||
"phone_dative": "telefon",
|
||||
"email_dative": "email",
|
||||
"enter_confirmation_code_prompt": "Introu codul de confirmare trimis pe {method}:",
|
||||
"numeric_confirmation_code_prompt": "Doar cifre",
|
||||
"button_confirm": "Confirmă",
|
||||
"save_attachment": "salvează",
|
||||
"deleted_content": "conținut șters",
|
||||
"invalid_content": "conținut invalid",
|
||||
"user_not_found": "Utilizatorul nu a fost găsit",
|
||||
"badge_you": "tu",
|
||||
"badge_owner": "owner",
|
||||
"menu_item_info": "Info",
|
||||
"menu_item_clear_messages": "Șterge mesajele",
|
||||
"menu_item_clear_messages_for_all": "Ștergele pe toate",
|
||||
"menu_item_delete": "Șterge",
|
||||
"menu_item_delete_for_all": "Ștergele pe toate",
|
||||
"menu_item_send_retry": "Reîncearcă",
|
||||
"menu_item_mute": "Mute",
|
||||
"menu_item_unmute": "Unmute",
|
||||
"menu_item_delete_topic": "Șterge",
|
||||
"topic_delete_warning": "Ești sigur că dorești să șterge această conversație?",
|
||||
"menu_item_unblock": "Deblochează",
|
||||
"menu_item_block": "Blochează",
|
||||
"topic_block_warning": "Ești sigur că dorești să blochezi această conversație?",
|
||||
"menu_item_member_delete": "Șterge persoană",
|
||||
"menu_item_archive_topic": "Arhivează",
|
||||
"action_cancel": "anulează",
|
||||
"upload_finishing": "se încarcă...",
|
||||
"no_contacts": "Nu ai contacte adăugate :-(",
|
||||
"contacts_not_found_short": "Niciun contact găsit după criteriile ''{query}''",
|
||||
"title_group_members": "Persoanele din grup",
|
||||
"title_all_contacts": "Toate Contactele",
|
||||
"button_ok": "OK",
|
||||
"button_cancel": "Anulează",
|
||||
"more_online_members": "+{overflow} mai mult",
|
||||
"download_action": "download",
|
||||
"label_file_name": "Nume fișier:",
|
||||
"label_content_type": "Tip conținut:",
|
||||
"label_size": "Size:",
|
||||
"chat_invitation": "Ai primit invitație într-o conversație. Accepți?",
|
||||
"chat_invitation_accept": "Da, accept",
|
||||
"chat_invitation_ignore": "Ignoră",
|
||||
"chat_invitation_block": "Blochează",
|
||||
"error_invalid_id": "ID Invalid",
|
||||
"group_user_id_prompt": "Grup sau user ID",
|
||||
"button_subscribe": "Subscribe",
|
||||
"topic_name_editing_placeholder": "Numele grupului",
|
||||
"channel_prompt": "Acesta este un canal",
|
||||
"button_create": "Creează",
|
||||
"permission_join": "Participare ({val})",
|
||||
"permission_read": "Citire ({val})",
|
||||
"permission_write": "Scriere ({val})",
|
||||
"permission_pres": "Notificare ({val})",
|
||||
"permission_admin": "Aprobare ({val})",
|
||||
"permission_share": "Distribuire ({val})",
|
||||
"permission_delete": "Ștergere ({val})",
|
||||
"permission_owner": "Deținător ({val})",
|
||||
"title_permissions": "Permisiuni",
|
||||
"message_sending": "se trimite...",
|
||||
"message_sending_failed": "eroare",
|
||||
"search_placeholder": "Caută după e-mail sau număr telefon...",
|
||||
"messaging_disabled_prompt": "Mesageria este dezactivată",
|
||||
"new_message_prompt": "Mesaj nou",
|
||||
"image_caption_prompt": "Titlul imaginii",
|
||||
"file_attachment_too_large": "Dimensiunea fișiterului {size} depășește limita de {limit}.",
|
||||
"cannot_initiate_file_upload": "Nu se poate face încărcarea.",
|
||||
"tags_not_found": "Niciun tag definit. Adaugă.",
|
||||
"tags_editor_no_tags": "Adaugă tag-uri",
|
||||
"title_manage_tags": "Administrează"
|
||||
},
|
||||
"ru": {
|
||||
"contacts_not_found": "Чатов нет<br />¯∖_(ツ)_/¯",
|
||||
@@ -690,7 +1051,9 @@
|
||||
"push_init_failed": "Ошибка инициализации пуш уведомлений",
|
||||
"invalid_security_token": "Токен некорректен",
|
||||
"no_connection": "Нет связи",
|
||||
"code_doesnot_match": "Код не совпадает"
|
||||
"code_doesnot_match": "Код не совпадает",
|
||||
"channel_prompt": "Создать канал",
|
||||
"channel": "канал"
|
||||
},
|
||||
"zh": {
|
||||
"archived_contacts": "已归档联系人 ({count})",
|
||||
@@ -864,6 +1227,8 @@
|
||||
"push_init_failed": "初始化推送通知失败",
|
||||
"invalid_security_token": "无效的安全令牌",
|
||||
"no_connection": "无连接",
|
||||
"code_doesnot_match": "代码不匹配"
|
||||
"code_doesnot_match": "代码不匹配",
|
||||
"channel_prompt": "这是一个频道",
|
||||
"channel": "频道"
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
// This is a generated file. Don't edit.
|
||||
|
||||
export const PACKAGE_VERSION = "0.16.6";
|
||||
export const PACKAGE_VERSION = "0.16.9-rc1";
|
||||
|
||||
@@ -157,6 +157,7 @@ export default class AccGeneralView extends React.Component {
|
||||
title={title_tag_manager}
|
||||
activated={false}
|
||||
tags={this.state.tags}
|
||||
tinode={this.props.tinode}
|
||||
onSubmit={this.handleTagsUpdated} />
|
||||
}</FormattedMessage>
|
||||
<div className="hr" />
|
||||
|
||||
+20
-7
@@ -106,6 +106,7 @@ class InfoView extends React.Component {
|
||||
muted: false,
|
||||
address: null,
|
||||
groupTopic: undefined,
|
||||
channel: undefined,
|
||||
fullName: undefined,
|
||||
avatar: null,
|
||||
private: null,
|
||||
@@ -230,6 +231,7 @@ class InfoView extends React.Component {
|
||||
private: topic.private ? topic.private.comment : null,
|
||||
address: topic.name,
|
||||
groupTopic: (topic.getType() == 'grp'),
|
||||
channel: topic.isChannel(),
|
||||
showMemberPanel: false,
|
||||
access: acs ? acs.getMode() : undefined,
|
||||
modeGiven: acs ? acs.getGiven() : undefined,
|
||||
@@ -335,9 +337,17 @@ class InfoView extends React.Component {
|
||||
case 'want':
|
||||
toEdit = this.state.modeWant;
|
||||
toCompare = this.state.modeGiven;
|
||||
toSkip = this.state.owner ? 'O' : // Don't allow owner to unset 'O' permission.
|
||||
if (this.state.owner) {
|
||||
// Don't allow owner to unset 'O' permission. All other permissions are OK.
|
||||
toSkip = 'O';
|
||||
} else {
|
||||
// Allow accepting any of 'ASDO' permissions but don't allow asking for them.
|
||||
Tinode.AccessMode.encode(Tinode.AccessMode.diff('ASDO', this.state.modeGiven));
|
||||
toSkip = Tinode.AccessMode.encode(Tinode.AccessMode.diff('ASDO', this.state.modeGiven));
|
||||
if (this.state.channel) {
|
||||
// Channels are read-only.
|
||||
toSkip += 'W';
|
||||
}
|
||||
}
|
||||
titleEdit = formatMessage(messages.requested);
|
||||
titleCompare = formatMessage(messages.granted);
|
||||
break;
|
||||
@@ -357,11 +367,11 @@ class InfoView extends React.Component {
|
||||
toSkip = 'O';
|
||||
break;
|
||||
case 'user': {
|
||||
let topic = this.props.tinode.getTopic(this.props.topic);
|
||||
const topic = this.props.tinode.getTopic(this.props.topic);
|
||||
if (!topic) {
|
||||
return;
|
||||
}
|
||||
var user = topic.subscriber(uid);
|
||||
const user = topic.subscriber(uid);
|
||||
if (!user || !user.acs) {
|
||||
return;
|
||||
}
|
||||
@@ -687,6 +697,7 @@ class InfoView extends React.Component {
|
||||
title={tags}
|
||||
tags={this.state.tags}
|
||||
activated={false}
|
||||
tinode={this.props.tinode}
|
||||
onSubmit={this.handleTagsUpdated} />
|
||||
}</FormattedMessage>
|
||||
<div className="hr" />
|
||||
@@ -695,11 +706,15 @@ class InfoView extends React.Component {
|
||||
null
|
||||
}
|
||||
<div className="panel-form-column">
|
||||
{!this.state.channel ?
|
||||
<a href="#" className="flat-button" onClick={this.handleDeleteMessages}>
|
||||
<i className="material-icons">delete_outline</i> {
|
||||
formatMessage(this.state.deleter ? messages.delete_messages : messages.clear_messages)
|
||||
}
|
||||
</a>
|
||||
:
|
||||
null
|
||||
}
|
||||
<a href="#" className="red flat-button" onClick={this.handleLeave}>
|
||||
<i className="material-icons">exit_to_app</i> {formatMessage(messages.leave_chat)}
|
||||
</a>
|
||||
@@ -718,7 +733,7 @@ class InfoView extends React.Component {
|
||||
null
|
||||
}
|
||||
</div>
|
||||
{this.state.groupTopic ?
|
||||
{this.state.groupTopic && this.state.sharer ?
|
||||
<>
|
||||
<div className="hr" />
|
||||
<div className="panel-form-column">
|
||||
@@ -729,12 +744,10 @@ class InfoView extends React.Component {
|
||||
</label>
|
||||
</div>
|
||||
<div className="panel-form-row">
|
||||
{this.state.sharer ?
|
||||
<a href="#" className="flat-button" onClick={this.handleShowAddMembers}>
|
||||
<i className="material-icons">person_add</i> <FormattedMessage id="button_add_members"
|
||||
defaultMessage="Add members" description="Flat button [Add members] (to topic)" />
|
||||
</a>
|
||||
: null}
|
||||
</div>
|
||||
<FormattedMessage id="group_has_no_members" defaultMessage="No members"
|
||||
description="Shown in place of group members">{
|
||||
|
||||
+171
-62
@@ -15,22 +15,23 @@ import LoadSpinner from '../widgets/load-spinner.jsx';
|
||||
import LogoView from './logo-view.jsx';
|
||||
import SendMessage from '../widgets/send-message.jsx';
|
||||
|
||||
import { DEFAULT_P2P_ACCESS_MODE, KEYPRESS_DELAY, MESSAGES_PAGE, MAX_EXTERN_ATTACHMENT_SIZE,
|
||||
MAX_IMAGE_DIM, MAX_INBAND_ATTACHMENT_SIZE, READ_DELAY } from '../config.js';
|
||||
import { SUPPORTED_IMAGE_FORMATS, filePasted, fileToBase64, imageFileToBase64,
|
||||
imageFileScaledToBase64, makeImageUrl } from '../lib/blob-helpers.js';
|
||||
import { DEFAULT_P2P_ACCESS_MODE, IMAGE_PREVIEW_DIM, KEYPRESS_DELAY, MESSAGES_PAGE,
|
||||
MAX_EXTERN_ATTACHMENT_SIZE, MAX_IMAGE_DIM, MAX_INBAND_ATTACHMENT_SIZE, READ_DELAY } from '../config.js';
|
||||
import { SUPPORTED_IMAGE_FORMATS, blobToBase64, filePasted, fileToBase64,
|
||||
imageScaled, makeImageUrl } from '../lib/blob-helpers.js';
|
||||
import HashNavigation from '../lib/navigation.js';
|
||||
import { bytesToHumanSize, shortDateFormat } from '../lib/strformat.js';
|
||||
|
||||
// Run timer with this frequency (ms) for checking notification queue.
|
||||
const NOTIFICATION_EXEC_INTERVAL = 300;
|
||||
|
||||
const messages = defineMessages({
|
||||
online_now: {
|
||||
'online_now': {
|
||||
id: 'online_now',
|
||||
defaultMessage: 'online now',
|
||||
description: 'Indicator that the user or topic is currently online',
|
||||
},
|
||||
last_seen: {
|
||||
'last_seen': {
|
||||
id: 'last_seen_timestamp',
|
||||
defaultMessage: 'Last seen',
|
||||
description: 'Label for the timestamp of when the user or topic was last online'
|
||||
@@ -39,6 +40,11 @@ const messages = defineMessages({
|
||||
id: 'title_not_found',
|
||||
defaultMessage: 'Not found',
|
||||
description: 'Title shown when topic is not found'
|
||||
},
|
||||
'channel': {
|
||||
id: 'channel',
|
||||
defaultMessage: 'channel',
|
||||
description: 'Subtitle shown for channels in MessagesView instead of last seen'
|
||||
}
|
||||
});
|
||||
|
||||
@@ -85,6 +91,7 @@ class MessagesView extends React.Component {
|
||||
this.handleEnablePeer = this.handleEnablePeer.bind(this);
|
||||
this.handleAttachFile = this.handleAttachFile.bind(this);
|
||||
this.handleAttachImage = this.handleAttachImage.bind(this);
|
||||
this.handleCancelUpload = this.handleCancelUpload.bind(this);
|
||||
this.postReadNotification = this.postReadNotification.bind(this);
|
||||
this.clearNotificationQueue = this.clearNotificationQueue.bind(this);
|
||||
|
||||
@@ -152,9 +159,13 @@ class MessagesView extends React.Component {
|
||||
|
||||
// Don't request the tags. They are useless unless the user
|
||||
// is the owner and is editing the topic.
|
||||
let getQuery = topic.startMetaQuery().withLaterDesc().withLaterSub();
|
||||
let getQuery = topic.startMetaQuery().withLaterDesc();
|
||||
if (this.state.isSharer || (newTopic && !topic.isChannel())) {
|
||||
// Request subscriptions only if one of S,O,A is given or it's a new non-channel topic.
|
||||
getQuery = getQuery.withLaterSub();
|
||||
}
|
||||
if (this.state.isReader || newTopic) {
|
||||
// If reading is either permitted or we don't know because it's a new topic. Ask for messages.
|
||||
// Reading is either permitted or we don't know because it's a new topic. Ask for messages.
|
||||
getQuery = getQuery.withLaterData(MESSAGES_PAGE);
|
||||
if (this.state.isReader) {
|
||||
getQuery = getQuery.withLaterDel();
|
||||
@@ -165,6 +176,11 @@ class MessagesView extends React.Component {
|
||||
const setQuery = newTopic ? this.props.newTopicParams : undefined;
|
||||
topic.subscribe(getQuery.build(), setQuery)
|
||||
.then((ctrl) => {
|
||||
if (ctrl.code == 303) {
|
||||
// Redirect to another topic requested.
|
||||
HashNavigation.navigateTo(HashNavigation.setUrlTopic('', ctrl.params.topic));
|
||||
return;
|
||||
}
|
||||
if (this.state.topic != ctrl.topic) {
|
||||
this.setState({topic: ctrl.topic});
|
||||
}
|
||||
@@ -202,7 +218,8 @@ class MessagesView extends React.Component {
|
||||
typingIndicator: false,
|
||||
scrollPosition: 0,
|
||||
fetchingMessages: false,
|
||||
peerMessagingDisabled: false
|
||||
peerMessagingDisabled: false,
|
||||
channel: false
|
||||
};
|
||||
} else if (nextProps.topic != prevState.topic) {
|
||||
const topic = nextProps.tinode.getTopic(nextProps.topic);
|
||||
@@ -262,6 +279,9 @@ class MessagesView extends React.Component {
|
||||
peerMessagingDisabled: false
|
||||
});
|
||||
}
|
||||
Object.assign(nextState, {
|
||||
channel: topic.isChannel()
|
||||
});
|
||||
} else {
|
||||
// Invalid topic.
|
||||
Object.assign(nextState, {
|
||||
@@ -269,7 +289,8 @@ class MessagesView extends React.Component {
|
||||
onlineSubs: [],
|
||||
title: '',
|
||||
avatar: null,
|
||||
peerMessagingDisabled: false
|
||||
peerMessagingDisabled: false,
|
||||
channel: false
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -284,6 +305,9 @@ class MessagesView extends React.Component {
|
||||
if (!nextProps.acs.isReader('given') != prevState.readingBlocked) {
|
||||
nextState.readingBlocked = !prevState.readingBlocked;
|
||||
}
|
||||
if (nextProps.acs.isSharer() != prevState.isSharer) {
|
||||
nextState.isSharer = !prevState.isSharer;
|
||||
}
|
||||
} else {
|
||||
if (prevState.isWriter) {
|
||||
nextState.isWriter = false;
|
||||
@@ -294,6 +318,9 @@ class MessagesView extends React.Component {
|
||||
if (!prevState.readingBlocked) {
|
||||
prevState.readingBlocked = true;
|
||||
}
|
||||
if (prevState.isSharer) {
|
||||
nextState.isSharer = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (isUnconfirmed(nextProps.acs) == !prevState.unconformed) {
|
||||
@@ -533,6 +560,9 @@ class MessagesView extends React.Component {
|
||||
}
|
||||
|
||||
handleClosePreview() {
|
||||
if (this.state.imagePreview && this.state.imagePreview.url) {
|
||||
URL.revokeObjectURL(this.state.imagePreview.url);
|
||||
}
|
||||
this.setState({ imagePostview: null, imagePreview: null, docPreview: null });
|
||||
}
|
||||
|
||||
@@ -569,9 +599,11 @@ class MessagesView extends React.Component {
|
||||
handleShowContextMenuMessage(params, messageSpecificMenuItems) {
|
||||
params.topicName = this.state.topic;
|
||||
const menuItems = messageSpecificMenuItems || [];
|
||||
menuItems.push('message_delete');
|
||||
const topic = this.props.tinode.getTopic(params.topicName);
|
||||
if (topic) {
|
||||
if (!topic.isChannel()) {
|
||||
menuItems.push('message_delete');
|
||||
}
|
||||
const acs = topic.getAccessMode();
|
||||
if (acs && acs.isDeleter()) {
|
||||
menuItems.push('message_delete_hard');
|
||||
@@ -596,8 +628,15 @@ class MessagesView extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
// Send attachment 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) {
|
||||
if (file.size > MAX_INBAND_ATTACHMENT_SIZE) {
|
||||
// Server-provided limit reduced for base64 encoding and overhead.
|
||||
const maxInbandAttachmentSize = (this.props.tinode.getServerLimit('maxMessageSize',
|
||||
MAX_INBAND_ATTACHMENT_SIZE) * 0.75 - 1024) | 0;
|
||||
|
||||
if (file.size > maxInbandAttachmentSize) {
|
||||
// Too large to send inband - uploading out of band and sending as a link.
|
||||
const uploader = this.props.tinode.getLargeFileHelper();
|
||||
if (!uploader) {
|
||||
@@ -605,25 +644,33 @@ class MessagesView extends React.Component {
|
||||
return;
|
||||
}
|
||||
const uploadCompletionPromise = uploader.upload(file);
|
||||
const msg = Drafty.attachFile(null, file.type, null, file.name, file.size, uploadCompletionPromise);
|
||||
const msg = Drafty.attachFile(null, {
|
||||
mime: file.type,
|
||||
filename: file.name,
|
||||
size: file.size,
|
||||
urlPromise: uploadCompletionPromise
|
||||
});
|
||||
// Pass data and the uploader to the TinodeWeb.
|
||||
this.props.sendMessage(msg, uploadCompletionPromise, uploader);
|
||||
} else {
|
||||
// Small enough to send inband.
|
||||
fileToBase64(file,
|
||||
(mime, bits, fname) => {
|
||||
this.props.sendMessage(Drafty.attachFile(null, mime, bits, fname));
|
||||
this.props.sendMessage(Drafty.attachFile(null, {mime: mime, data: bits, filename: fname}));
|
||||
},
|
||||
this.props.onError
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// handleAttachFile method is called when [Attach file] button is clicked.
|
||||
handleAttachFile(file) {
|
||||
if (file.size > MAX_EXTERN_ATTACHMENT_SIZE) {
|
||||
const maxExternAttachmentSize = this.props.tinode.getServerLimit('maxFileUploadSize', MAX_EXTERN_ATTACHMENT_SIZE);
|
||||
|
||||
if (file.size > maxExternAttachmentSize) {
|
||||
// Too large.
|
||||
this.props.onError(this.props.intl.formatMessage(messages.file_attachment_too_large,
|
||||
{size: bytesToHumanSize(file.size), limit: bytesToHumanSize(MAX_EXTERN_ATTACHMENT_SIZE)}), 'err');
|
||||
this.props.onError(this.props.intl.formatMessage({id: 'file_attachment_too_large'},
|
||||
{size: bytesToHumanSize(file.size), limit: bytesToHumanSize(maxExternAttachmentSize)}), 'err');
|
||||
} else {
|
||||
this.setState({ docPreview: {
|
||||
file: file,
|
||||
@@ -634,48 +681,89 @@ class MessagesView extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
sendImageAttachment(caption, mime, bits, width, height, fname) {
|
||||
let msg = Drafty.insertImage(null, 0, mime, bits, width, height, fname);
|
||||
// sendImageAttachment sends the image bits inband as Drafty message.
|
||||
sendImageAttachment(caption, blob) {
|
||||
const mime = this.state.imagePreview.type;
|
||||
const width = this.state.imagePreview.width;
|
||||
const height = this.state.imagePreview.height;
|
||||
const fname = this.state.imagePreview.filename;
|
||||
|
||||
// Server-provided limit reduced for base64 encoding and overhead.
|
||||
const maxInbandAttachmentSize = (this.props.tinode.getServerLimit('maxMessageSize',
|
||||
MAX_INBAND_ATTACHMENT_SIZE) * 0.75 - 1024) | 0;
|
||||
|
||||
if (blob.size > maxInbandAttachmentSize) {
|
||||
// Too large to send inband - uploading out of band and sending as a link.
|
||||
const uploader = this.props.tinode.getLargeFileHelper();
|
||||
if (!uploader) {
|
||||
this.props.onError(this.props.intl.formatMessage(messages.cannot_initiate_upload));
|
||||
return;
|
||||
}
|
||||
const uploadCompletionPromise = uploader.upload(blob);
|
||||
|
||||
// Make small preview to show while uploading.
|
||||
imageScaled(blob, IMAGE_PREVIEW_DIM, IMAGE_PREVIEW_DIM, -1, false,
|
||||
(tinyBlob) => {
|
||||
// Convert tiny image into base64 for serialization and previewing.
|
||||
blobToBase64(tinyBlob, (blobMime, tinyBits64) => {
|
||||
let msg = Drafty.insertImage(null, 0, {
|
||||
mime: mime,
|
||||
_tempPreview: tinyBits64, // This preview will not be serialized.
|
||||
width: width,
|
||||
height: height,
|
||||
filename: fname,
|
||||
size: blob.size,
|
||||
urlPromise: uploadCompletionPromise
|
||||
});
|
||||
if (caption) {
|
||||
msg = Drafty.appendLineBreak(msg);
|
||||
msg = Drafty.append(msg, Drafty.init(caption));
|
||||
}
|
||||
// Pass data and the uploader to the TinodeWeb.
|
||||
this.props.sendMessage(msg, uploadCompletionPromise, uploader);
|
||||
}
|
||||
)},
|
||||
// Failure
|
||||
(err) => {
|
||||
this.props.onError(err, 'err');
|
||||
}
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Upload the image if it's too big to be send inband.
|
||||
blobToBase64(blob, (blobMime, bits64) => {
|
||||
let msg = Drafty.insertImage(null, 0, {
|
||||
mime: blobMime,
|
||||
preview: bits64, // Serializable preview
|
||||
width: width,
|
||||
height: height,
|
||||
filename: fname,
|
||||
size: blob.size
|
||||
});
|
||||
if (caption) {
|
||||
msg = Drafty.appendLineBreak(msg);
|
||||
msg = Drafty.append(msg, Drafty.init(caption));
|
||||
}
|
||||
this.props.sendMessage(msg);
|
||||
});
|
||||
}
|
||||
|
||||
// handleAttachImage method is called when [Attach image] button is clicked.
|
||||
handleAttachImage(file) {
|
||||
// Check if the uploaded file is indeed an image and if it isn't too large.
|
||||
if (file.size > MAX_INBAND_ATTACHMENT_SIZE || SUPPORTED_IMAGE_FORMATS.indexOf(file.type) < 0) {
|
||||
// Convert image for size or format.
|
||||
imageFileScaledToBase64(file, MAX_IMAGE_DIM, MAX_IMAGE_DIM, false,
|
||||
const maxExternAttachmentSize = this.props.tinode.getServerLimit('maxFileUploadSize', MAX_EXTERN_ATTACHMENT_SIZE);
|
||||
|
||||
// Get image dimensions and size, optionally scale it down.
|
||||
imageScaled(file, MAX_IMAGE_DIM, MAX_IMAGE_DIM, maxExternAttachmentSize, false,
|
||||
// Success
|
||||
(bits, mime, width, height, fname) => {
|
||||
(blob, mime, width, height, fname) => {
|
||||
this.setState({imagePreview: {
|
||||
url: URL.createObjectURL(file),
|
||||
bits: bits,
|
||||
url: URL.createObjectURL(blob),
|
||||
blob: blob,
|
||||
filename: fname,
|
||||
width: width,
|
||||
height: height,
|
||||
size: bits.length,
|
||||
type: mime
|
||||
}});
|
||||
},
|
||||
// Failure
|
||||
(err) => {
|
||||
this.props.onError(err, 'err');
|
||||
});
|
||||
} else {
|
||||
// Image can be uploaded as is. No conversion is needed.
|
||||
imageFileToBase64(file,
|
||||
// Success
|
||||
(bits, mime, width, height, fname) => {
|
||||
this.setState({imagePreview: {
|
||||
url: URL.createObjectURL(file),
|
||||
bits: bits,
|
||||
filename: fname,
|
||||
width: width,
|
||||
height: height,
|
||||
size: bits.length,
|
||||
size: blob.size,
|
||||
type: mime
|
||||
}});
|
||||
},
|
||||
@@ -685,6 +773,13 @@ class MessagesView extends React.Component {
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
handleCancelUpload(seq, uploader) {
|
||||
const found = this.state.messages.find(msg => msg.seq == seq);
|
||||
if (found) {
|
||||
found._cancelled = true;
|
||||
}
|
||||
uploader.cancel();
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -726,7 +821,8 @@ class MessagesView extends React.Component {
|
||||
);
|
||||
} else {
|
||||
const topic = this.props.tinode.getTopic(this.state.topic);
|
||||
const groupTopic = topic.getType() == 'grp';
|
||||
const isChannel = topic.isChannel();
|
||||
const groupTopic = topic.getType() == 'grp' && !isChannel;
|
||||
let messageNodes = [];
|
||||
let previousFrom = null;
|
||||
let chatBoxClass = null;
|
||||
@@ -735,32 +831,33 @@ class MessagesView extends React.Component {
|
||||
let nextFrom = null;
|
||||
|
||||
if (i + 1 < this.state.messages.length) {
|
||||
nextFrom = this.state.messages[i+1].from
|
||||
nextFrom = this.state.messages[i+1].from || 'chan';
|
||||
}
|
||||
|
||||
let sequence = 'single';
|
||||
if (msg.from == previousFrom) {
|
||||
if (msg.from == nextFrom) {
|
||||
let thisFrom = msg.from || 'chan';
|
||||
if (thisFrom == previousFrom) {
|
||||
if (thisFrom == nextFrom) {
|
||||
sequence = 'middle';
|
||||
} else {
|
||||
sequence = 'last';
|
||||
}
|
||||
} else if (msg.from == nextFrom) {
|
||||
} else if (thisFrom == nextFrom) {
|
||||
sequence = 'first';
|
||||
}
|
||||
previousFrom = msg.from;
|
||||
previousFrom = thisFrom;
|
||||
|
||||
const isReply = !(msg.from == this.props.myUserId);
|
||||
const isReply = !(thisFrom == this.props.myUserId);
|
||||
const deliveryStatus = topic.msgStatus(msg);
|
||||
|
||||
let userName, userAvatar, userFrom;
|
||||
if (groupTopic) {
|
||||
const user = topic.userDesc(msg.from);
|
||||
const user = topic.userDesc(thisFrom);
|
||||
if (user && user.public) {
|
||||
userName = user.public.fn;
|
||||
userAvatar = makeImageUrl(user.public.photo);
|
||||
}
|
||||
userFrom = msg.from;
|
||||
userFrom = thisFrom;
|
||||
chatBoxClass='chat-box group';
|
||||
} else {
|
||||
chatBoxClass='chat-box';
|
||||
@@ -769,23 +866,34 @@ class MessagesView extends React.Component {
|
||||
messageNodes.push(
|
||||
<ChatMessage
|
||||
tinode={this.props.tinode}
|
||||
content={msg.content} deleted={msg.hi}
|
||||
content={msg.content}
|
||||
deleted={msg.hi}
|
||||
mimeType={msg.head ? msg.head.mime : null}
|
||||
timestamp={msg.ts} response={isReply} seq={msg.seq}
|
||||
userFrom={userFrom} userName={userName} userAvatar={userAvatar}
|
||||
sequence={sequence} received={deliveryStatus} uploader={msg._uploader}
|
||||
timestamp={msg.ts}
|
||||
response={isReply}
|
||||
seq={msg.seq}
|
||||
userFrom={userFrom}
|
||||
userName={userName}
|
||||
userAvatar={userAvatar}
|
||||
sequence={sequence}
|
||||
received={deliveryStatus}
|
||||
uploader={msg._uploader}
|
||||
viewportWidth={this.props.viewportWidth}
|
||||
showContextMenu={this.handleShowContextMenuMessage}
|
||||
showContextMenu={this.state.channel? false : this.handleShowContextMenuMessage}
|
||||
onImagePreview={this.handleImagePostview}
|
||||
onFormResponse={this.handleFormResponse}
|
||||
onError={this.props.onError}
|
||||
onCancelUpload={this.handleCancelUpload}
|
||||
key={msg.seq} />
|
||||
);
|
||||
}
|
||||
|
||||
let lastSeen = null;
|
||||
if (isChannel) {
|
||||
lastSeen = formatMessage(messages.channel);
|
||||
} else {
|
||||
const cont = this.props.tinode.getMeTopic().getContact(this.state.topic);
|
||||
if (cont && Tinode.topicType(cont.topic) == 'p2p') {
|
||||
if (cont && Tinode.isP2PTopicName(cont.topic)) {
|
||||
if (cont.online) {
|
||||
lastSeen = formatMessage(messages.online_now);
|
||||
} else if (cont.seen) {
|
||||
@@ -794,6 +902,7 @@ class MessagesView extends React.Component {
|
||||
// TODO: also handle user agent in c.seen.ua
|
||||
}
|
||||
}
|
||||
}
|
||||
const avatar = this.state.avatar || true;
|
||||
const online = this.props.online ? 'online' + (this.state.typingIndicator ? ' typing' : '') : 'offline';
|
||||
|
||||
@@ -811,7 +920,7 @@ class MessagesView extends React.Component {
|
||||
avatar={avatar}
|
||||
topic={this.state.topic}
|
||||
title={this.state.title} />
|
||||
<span className={online} />
|
||||
{!isChannel ? <span className={online} /> : null}
|
||||
</div>
|
||||
<div id="topic-title-group">
|
||||
<div id="topic-title" className="panel-title">{
|
||||
|
||||
@@ -59,13 +59,13 @@ class NewTopicView extends React.Component {
|
||||
handleContactSelected(sel) {
|
||||
if (this.state.tabSelected === 'p2p') {
|
||||
HashNavigation.navigateTo(HashNavigation.removeUrlParam(window.location.hash, 'tab'));
|
||||
this.props.onCreateTopic(sel, undefined);
|
||||
this.props.onCreateTopic(sel);
|
||||
}
|
||||
}
|
||||
|
||||
handleNewGroupSubmit(name, dataUrl, priv, tags) {
|
||||
handleNewGroupSubmit(name, dataUrl, priv, tags, isChannel) {
|
||||
HashNavigation.navigateTo(HashNavigation.removeUrlParam(window.location.hash, 'tab'));
|
||||
this.props.onCreateTopic(undefined, vcard(name, dataUrl), priv, tags);
|
||||
this.props.onCreateTopic(undefined, vcard(name, dataUrl), priv, tags, isChannel);
|
||||
}
|
||||
|
||||
handleGroupByID(topicName) {
|
||||
@@ -100,7 +100,9 @@ class NewTopicView extends React.Component {
|
||||
</li>
|
||||
</ul>
|
||||
{this.state.tabSelected === 'grp' ?
|
||||
<NewTopicGroup onSubmit={this.handleNewGroupSubmit} /> :
|
||||
<NewTopicGroup
|
||||
tinode={this.props.tinode}
|
||||
onSubmit={this.handleNewGroupSubmit} /> :
|
||||
this.state.tabSelected === 'byid' ?
|
||||
<NewTopicById
|
||||
onSubmit={this.handleGroupByID}
|
||||
|
||||
@@ -218,6 +218,7 @@ class SidepanelView extends React.Component {
|
||||
|
||||
view === 'newtpk' ?
|
||||
<NewTopicView
|
||||
tinode={this.props.tinode}
|
||||
searchResults={this.props.searchResults}
|
||||
onInitFind={this.props.onInitFind}
|
||||
onSearchContacts={this.props.onSearchContacts}
|
||||
|
||||
+22
-15
@@ -108,7 +108,7 @@ class TinodeWeb extends React.Component {
|
||||
this.togglePushToken = this.togglePushToken.bind(this);
|
||||
this.requestPushToken = this.requestPushToken.bind(this);
|
||||
this.handleSidepanelCancel = this.handleSidepanelCancel.bind(this);
|
||||
this.handleNewTopicRequest = this.handleNewTopicRequest.bind(this);
|
||||
this.handleStartTopicRequest = this.handleStartTopicRequest.bind(this);
|
||||
this.handleNewTopicCreated = this.handleNewTopicCreated.bind(this);
|
||||
this.handleTopicUpdateRequest = this.handleTopicUpdateRequest.bind(this);
|
||||
this.handleChangePermissions = this.handleChangePermissions.bind(this);
|
||||
@@ -390,7 +390,7 @@ class TinodeWeb extends React.Component {
|
||||
} else {
|
||||
this.tinode.connect().catch((err) => {
|
||||
// Socket error
|
||||
this.setState({loginDisabled: false, autoLogin: false});
|
||||
this.setState({loginDisabled: false, autoLogin: false, loadSpinnerVisible: false});
|
||||
this.handleError(err.message, 'err');
|
||||
});
|
||||
}
|
||||
@@ -634,7 +634,7 @@ class TinodeWeb extends React.Component {
|
||||
// For chatList topics merge only p2p topics and convert them to the
|
||||
// same format as foundContacts.
|
||||
for (const c of chatList) {
|
||||
if (Tinode.topicType(c.topic) == 'p2p') {
|
||||
if (Tinode.isP2PTopicName(c.topic)) {
|
||||
merged[c.topic] = {
|
||||
user: c.topic,
|
||||
updated: c.updated,
|
||||
@@ -726,7 +726,7 @@ class TinodeWeb extends React.Component {
|
||||
}
|
||||
|
||||
// User clicked on a contact in the side panel or deleted a contact.
|
||||
handleTopicSelected(topicName, unused_index, online, acs) {
|
||||
handleTopicSelected(topicName) {
|
||||
// Clear newTopicParams after use.
|
||||
if (this.state.newTopicParams && this.state.newTopicParams._topicName != topicName) {
|
||||
this.setState({
|
||||
@@ -744,8 +744,8 @@ class TinodeWeb extends React.Component {
|
||||
// Different contact selected.
|
||||
if (this.state.topicSelected != topicName) {
|
||||
this.setState({
|
||||
topicSelectedOnline: online,
|
||||
topicSelectedAcs: acs
|
||||
topicSelectedOnline: this.tinode.isTopicOnline(topicName),
|
||||
topicSelectedAcs: this.tinode.getTopicAccessMode(topicName)
|
||||
});
|
||||
HashNavigation.navigateTo(HashNavigation.setUrlTopic('', topicName));
|
||||
}
|
||||
@@ -774,6 +774,9 @@ class TinodeWeb extends React.Component {
|
||||
|
||||
// User is sending a message, either plain text or a drafty object, possibly
|
||||
// with attachments.
|
||||
// - msg - Drafty message with content
|
||||
// - promise - Promise to be resolved when the upload is completed
|
||||
// - uploader - for tracking progress
|
||||
handleSendMessage(msg, promise, uploader) {
|
||||
const topic = this.tinode.getTopic(this.state.topicSelected);
|
||||
|
||||
@@ -813,7 +816,7 @@ class TinodeWeb extends React.Component {
|
||||
// Accept given permissions.
|
||||
const mode = topic.getAccessMode().getGiven();
|
||||
response = topic.setMeta({sub: {mode: mode}});
|
||||
if (topic.getType() == 'p2p') {
|
||||
if (topic.isP2P()) {
|
||||
// For P2P topics change 'given' permission of the peer too.
|
||||
// In p2p topics the other user has the same name as the topic.
|
||||
response = response.then((ctrl) => {
|
||||
@@ -1060,22 +1063,26 @@ class TinodeWeb extends React.Component {
|
||||
HashNavigation.navigateTo(HashNavigation.setUrlSidePanel(window.location.hash, hash));
|
||||
}
|
||||
|
||||
// Request to start a new topic. New P2P topic requires peer's name.
|
||||
handleNewTopicRequest(peerName, pub, priv, tags) {
|
||||
// Request to start a topic, new or selected from search results, or "by ID".
|
||||
handleStartTopicRequest(topicName, pub, priv, tags, isChannel) {
|
||||
// Check if topic is indeed new. If not, launch it.
|
||||
if (topicName && this.tinode.isTopicCached(topicName)) {
|
||||
this.handleTopicSelected(topicName);
|
||||
return;
|
||||
}
|
||||
|
||||
const topicName = peerName || this.tinode.newGroupTopicName();
|
||||
const params = {
|
||||
_topicName: topicName,
|
||||
};
|
||||
if (peerName) {
|
||||
const params = {};
|
||||
if (Tinode.isP2PTopicName(topicName)) {
|
||||
// Because we are initialing the subscription, set 'want' to all permissions.
|
||||
params.sub = {mode: DEFAULT_P2P_ACCESS_MODE};
|
||||
// Give the other user all permissions too.
|
||||
params.desc = {defacs: {auth: DEFAULT_P2P_ACCESS_MODE}};
|
||||
} else {
|
||||
topicName = topicName || this.tinode.newGroupTopicName(isChannel);
|
||||
params.desc = {public: pub, private: {comment: priv}};
|
||||
params.tags = tags;
|
||||
}
|
||||
params._topicName = topicName;
|
||||
this.setState({newTopicParams: params}, () => {this.handleTopicSelected(topicName)});
|
||||
}
|
||||
|
||||
@@ -1458,7 +1465,7 @@ class TinodeWeb extends React.Component {
|
||||
onCredDelete={this.handleCredDelete}
|
||||
onCredConfirm={this.handleCredConfirm}
|
||||
onTopicSelected={this.handleTopicSelected}
|
||||
onCreateTopic={this.handleNewTopicRequest}
|
||||
onCreateTopic={this.handleStartTopicRequest}
|
||||
onLogout={this.handleLogout}
|
||||
onDeleteAccount={this.handleDeleteAccount}
|
||||
onShowAlert={this.handleShowAlert}
|
||||
|
||||
+25
-12
@@ -19,11 +19,21 @@ export default class Attachment extends React.Component {
|
||||
}
|
||||
|
||||
downloadFile(url, filename, mimetype) {
|
||||
var downloader = this.props.tinode.getLargeFileHelper();
|
||||
if (!url) {
|
||||
this.props.onError("Invalid download URL '" + url + "'");
|
||||
return;
|
||||
}
|
||||
|
||||
const downloader = this.props.tinode.getLargeFileHelper();
|
||||
this.setState({downloader: downloader});
|
||||
downloader.download(url, filename, mimetype, (loaded) => {
|
||||
downloader.download(url, filename, mimetype,
|
||||
(loaded) => {
|
||||
this.setState({progress: loaded / this.props.size});
|
||||
}).then(() => {
|
||||
},
|
||||
(err) => {
|
||||
this.props.onError(err, 'err');
|
||||
}
|
||||
).then(() => {
|
||||
this.setState({downloader: null, progress: 0});
|
||||
}).catch((err) => {
|
||||
if (err) {
|
||||
@@ -34,7 +44,7 @@ export default class Attachment extends React.Component {
|
||||
}
|
||||
|
||||
handleCancel() {
|
||||
if (this.props.uploader) {
|
||||
if (this.props.uploading) {
|
||||
this.props.onCancelUpload();
|
||||
} else if (this.state.downloader) {
|
||||
this.state.downloader.cancel();
|
||||
@@ -54,7 +64,7 @@ export default class Attachment extends React.Component {
|
||||
// If the URL is relative use LargeFileHelper to attach authentication
|
||||
// credentials to the request.
|
||||
let url, helperFunc;
|
||||
if (!this.props.uploader && !this.state.downloader && isUrlRelative(this.props.downloadUrl)) {
|
||||
if (!this.props.uploading && !this.state.downloader && isUrlRelative(this.props.downloadUrl)) {
|
||||
// Relative URL. Use download helper.
|
||||
url = '#';
|
||||
helperFunc = (e) => {
|
||||
@@ -62,22 +72,25 @@ export default class Attachment extends React.Component {
|
||||
this.downloadFile(this.props.downloadUrl, this.props.filename, this.props.mimetype);
|
||||
};
|
||||
} else {
|
||||
url = sanitizeUrl(this.props.downloadUrl) || 'about:blank';
|
||||
url = sanitizeUrl(this.props.downloadUrl);
|
||||
helperFunc = null;
|
||||
}
|
||||
const downloadWidget = <><i className="material-icons">file_download</i> <FormattedMessage id="save_attachment"
|
||||
defaultMessage="save" description="Call to save an attachment" /></>;
|
||||
return (
|
||||
<div className="attachment">
|
||||
<div><i className="material-icons big gray">insert_drive_file</i></div>
|
||||
<div className="flex-column">
|
||||
<div>{filename} {size}</div>
|
||||
{this.props.uploader || this.state.downloader ?
|
||||
<FileProgress progress={this.props.uploader ? this.props.progress : this.state.progress}
|
||||
{this.props.uploading || this.state.downloader ?
|
||||
<FileProgress progress={this.props.uploading ? this.props.progress : this.state.progress}
|
||||
onCancel={this.handleCancel} />
|
||||
:
|
||||
<div><a href={url} download={this.props.filename} onClick={helperFunc} >
|
||||
<i className="material-icons">file_download</i> <FormattedMessage id="save_attachment"
|
||||
defaultMessage="save" description="Call to save an attachment" />
|
||||
</a></div>
|
||||
<div>{url ?
|
||||
<a href={url} download={this.props.filename} onClick={helperFunc}>{downloadWidget}</a>
|
||||
:
|
||||
<span className="light-gray">{downloadWidget}</span>
|
||||
}</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,8 +2,8 @@ import React from 'react';
|
||||
|
||||
import LetterTile from './letter-tile.jsx';
|
||||
|
||||
import { AVATAR_SIZE } from '../config.js';
|
||||
import { imageFileScaledToBase64, makeImageUrl } from '../lib/blob-helpers.js';
|
||||
import { AVATAR_SIZE, MAX_EXTERN_ATTACHMENT_SIZE } from '../config.js';
|
||||
import { imageScaled, blobToBase64, makeImageUrl } from '../lib/blob-helpers.js';
|
||||
|
||||
export default class AvatarUpload extends React.Component {
|
||||
constructor(props) {
|
||||
@@ -23,12 +23,15 @@ export default class AvatarUpload extends React.Component {
|
||||
}
|
||||
|
||||
handleFileUpload(e) {
|
||||
imageFileScaledToBase64(e.target.files[0], AVATAR_SIZE, AVATAR_SIZE, true,
|
||||
// Success
|
||||
(base64bits, mime) => {
|
||||
var du = makeImageUrl({data: base64bits, type: mime});
|
||||
imageScaled(e.target.files[0], AVATAR_SIZE, AVATAR_SIZE, MAX_EXTERN_ATTACHMENT_SIZE, true,
|
||||
// Image successfully scaled and converted.
|
||||
(mime, blob) => {
|
||||
// Convert blob to base64-encoded bits.
|
||||
blobToBase64(blob, (unused, base64bits) => {
|
||||
const du = makeImageUrl({data: base64bits, type: mime});
|
||||
this.setState({dataUrl: du});
|
||||
this.props.onImageChanged(du);
|
||||
});
|
||||
},
|
||||
// Failure
|
||||
(err) => {
|
||||
|
||||
@@ -6,6 +6,7 @@ import { Drafty } from 'tinode-sdk'
|
||||
import Attachment from './attachment.jsx';
|
||||
import LetterTile from './letter-tile.jsx';
|
||||
import ReceivedMarker from './received-marker.jsx'
|
||||
import UploadingImage from './uploading-image.jsx'
|
||||
import { sanitizeImageUrl, sanitizeUrl } from '../lib/utils.js';
|
||||
|
||||
export default class ChatMessage extends React.Component {
|
||||
@@ -68,7 +69,7 @@ export default class ChatMessage extends React.Component {
|
||||
}
|
||||
|
||||
handleCancelUpload() {
|
||||
this.props.uploader.cancel();
|
||||
this.props.onCancelUpload(this.props.seq, this.props.uploader);
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -91,7 +92,7 @@ export default class ChatMessage extends React.Component {
|
||||
attachments.push(<Attachment
|
||||
tinode={this.props.tinode}
|
||||
downloadUrl={Drafty.getDownloadUrl(att)}
|
||||
filename={att.name} uploader={Drafty.isUploading(att)}
|
||||
filename={att.name} uploading={Drafty.isProcessing(att)}
|
||||
mimetype={att.mime} size={Drafty.getEntitySize(att)}
|
||||
progress={this.state.progress}
|
||||
onCancelUpload={this.handleCancelUpload}
|
||||
@@ -136,7 +137,7 @@ export default class ChatMessage extends React.Component {
|
||||
received={this.props.received} />
|
||||
: null}
|
||||
</div>
|
||||
{this.props.deleted ?
|
||||
{this.props.deleted || !this.props.showContextMenu ?
|
||||
null :
|
||||
<span className="menuTrigger">
|
||||
<a href="#" onClick={this.handleContextClick}>
|
||||
@@ -167,24 +168,30 @@ import { fitImageSize } from '../lib/blob-helpers.js';
|
||||
function draftyFormatter(style, data, values, key) {
|
||||
let el = Drafty.tagName(style);
|
||||
if (el) {
|
||||
let attr = Drafty.attrValue(style, data) || {};
|
||||
const attr = Drafty.attrValue(style, data) || {};
|
||||
attr.key = key;
|
||||
switch (style) {
|
||||
case 'IM':
|
||||
// Additional processing for images
|
||||
if (data) {
|
||||
attr.className = 'inline-image';
|
||||
let dim = fitImageSize(data.width, data.height,
|
||||
Math.min(this.props.viewportWidth - REM_SIZE * 4, REM_SIZE * 36), REM_SIZE * 24, false);
|
||||
dim = dim || {dstWidth: BROKEN_IMAGE_SIZE, dstHeight: BROKEN_IMAGE_SIZE};
|
||||
const dim = fitImageSize(data.width, data.height,
|
||||
Math.min(this.props.viewportWidth - REM_SIZE * 4, REM_SIZE * 36), REM_SIZE * 24, false) ||
|
||||
{dstWidth: BROKEN_IMAGE_SIZE, dstHeight: BROKEN_IMAGE_SIZE};
|
||||
attr.style = { width: dim.dstWidth + 'px', height: dim.dstHeight + 'px' };
|
||||
attr.src = sanitizeImageUrl(attr.src);
|
||||
if (!Drafty.isProcessing(data)) {
|
||||
attr.src = this.props.tinode.authorizeURL(sanitizeImageUrl(attr.src));
|
||||
attr.alt = data.name;
|
||||
if (attr.src) {
|
||||
attr.onClick = this.handleImagePreview;
|
||||
attr.className += ' image-clickable';
|
||||
} else {
|
||||
attr.src = 'img/broken_image.png';
|
||||
}
|
||||
} else {
|
||||
// Use custom element instead of <img>.
|
||||
el = UploadingImage;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'BN':
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
import React from 'react';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
|
||||
import Tinode from 'tinode-sdk';
|
||||
|
||||
import Contact from './contact.jsx';
|
||||
import ContactAction from './contact-action.jsx';
|
||||
|
||||
@@ -55,6 +57,7 @@ class ContactList extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
const isChannel = Tinode.isChannelTopicName(key);
|
||||
const selected = showCheckmark ?
|
||||
(this.props.topicSelected.indexOf(key) > -1) :
|
||||
(this.props.topicSelected === key);
|
||||
@@ -82,7 +85,8 @@ class ContactList extends React.Component {
|
||||
badges={badges}
|
||||
showCheckmark={showCheckmark}
|
||||
selected={selected}
|
||||
showOnline={this.props.showOnline}
|
||||
showOnline={this.props.showOnline && !isChannel}
|
||||
isChannel={isChannel}
|
||||
onSelected={this.props.onTopicSelected}
|
||||
showContextMenu={this.props.showContextMenu}
|
||||
item={key}
|
||||
|
||||
@@ -18,7 +18,8 @@ export default class Contact extends React.Component {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (this.props.onSelected) {
|
||||
this.props.onSelected(this.props.item, this.props.index, this.props.now, this.props.acs);
|
||||
// Index this.props.index is used by GroupManager.
|
||||
this.props.onSelected(this.props.item, this.props.index);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,6 +68,7 @@ export default class Contact extends React.Component {
|
||||
</div>
|
||||
<div className="text-box">
|
||||
<div><span className="contact-title">{title}</span>
|
||||
{this.props.isChannel ? <img src="/img/channel.png" className="channel" alt="channel" /> : null}
|
||||
<UnreadBadge count={this.props.unread} /><ContactBadges badges={icon_badges} />
|
||||
</div>
|
||||
{this.props.comment ? <div className="contact-comment">{this.props.comment}</div> : null}
|
||||
|
||||
@@ -30,8 +30,7 @@ export default class ImagePreview extends React.PureComponent {
|
||||
|
||||
handleSendImage(caption) {
|
||||
this.props.onClose();
|
||||
this.props.onSendMessage(caption, this.props.content.type, this.props.content.bits,
|
||||
this.props.content.width, this.props.content.height, this.props.content.filename);
|
||||
this.props.onSendMessage(caption, this.props.content.blob);
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -47,7 +46,7 @@ export default class ImagePreview extends React.PureComponent {
|
||||
size.maxHeight = '100%';
|
||||
|
||||
let filename = this.props.content.filename;
|
||||
// Averate font aspect ratio is ~0.5; File name takes 1/3 of the viewport width.
|
||||
// Average font aspect ratio is ~0.5; File name takes 1/3 of the viewport width.
|
||||
const maxlength = Math.max(((this.state.width / REM_SIZE / 1.5) | 0) - 2, 12);
|
||||
if (filename.length > maxlength) {
|
||||
filename = filename.slice(0, maxlength/2 - 1) + '…' + filename.slice(1 - maxlength/2);
|
||||
@@ -68,7 +67,7 @@ export default class ImagePreview extends React.PureComponent {
|
||||
<a href="#" onClick={(e) => {e.preventDefault(); this.props.onClose();}}><i className="material-icons gray">close</i></a>
|
||||
</div>
|
||||
<div id="image-preview-container" ref={(node) => this.assignWidth(node)}>
|
||||
<img src={this.props.content.url} style={size} />
|
||||
<img src={this.props.content.url} style={size} className="image-preview" alt={this.props.content.filename} />
|
||||
</div>
|
||||
{this.props.onSendMessage ?
|
||||
<SendMessage
|
||||
|
||||
@@ -52,7 +52,7 @@ class NewTopicById extends React.PureComponent {
|
||||
description="Prompt for entering user or group ID">{
|
||||
(prompt) => <input type="text" placeholder={prompt}
|
||||
value={this.state.groupId} onChange={this.handleChange}
|
||||
onKeyPress={this.handleKeyPress} required />
|
||||
onKeyPress={this.handleKeyPress} required autoFocus />
|
||||
}</FormattedMessage>
|
||||
</div>
|
||||
<div className="dialog-buttons">
|
||||
|
||||
@@ -2,6 +2,7 @@ import React from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import AvatarUpload from './avatar-upload.jsx';
|
||||
import CheckBox from './checkbox.jsx';
|
||||
import TagManager from './tag-manager.jsx';
|
||||
|
||||
import { MAX_TITLE_LENGTH } from '../config.js';
|
||||
@@ -14,14 +15,15 @@ export default class NewTopicGroup extends React.PureComponent {
|
||||
fn: '', // full/formatted name
|
||||
private: '',
|
||||
imageDataUrl: null,
|
||||
tags: []
|
||||
tags: [],
|
||||
isChannel: false
|
||||
};
|
||||
|
||||
this.handleFnChange = this.handleFnChange.bind(this);
|
||||
this.handlePrivateChange = this.handlePrivateChange.bind(this);
|
||||
this.handleImageChanged = this.handleImageChanged.bind(this);
|
||||
this.handleTagsChanged = this.handleTagsChanged.bind(this);
|
||||
this.handleTagsChanged = this.handleTagsChanged.bind(this);
|
||||
this.handleChannelToggle = this.handleChannelToggle.bind(this);
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
}
|
||||
|
||||
@@ -41,13 +43,17 @@ export default class NewTopicGroup extends React.PureComponent {
|
||||
this.setState({tags: tags});
|
||||
}
|
||||
|
||||
handleChannelToggle() {
|
||||
this.setState({isChannel: !this.state.isChannel});
|
||||
}
|
||||
|
||||
handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const fn = this.state.fn.trim().substring(0, MAX_TITLE_LENGTH);
|
||||
const comment = this.state.private.trim().substring(0, MAX_TITLE_LENGTH);
|
||||
if (fn) {
|
||||
this.props.onSubmit(fn, this.state.imageDataUrl, comment, this.state.tags);
|
||||
this.props.onSubmit(fn, this.state.imageDataUrl, comment, this.state.tags, this.state.isChannel);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,11 +87,18 @@ export default class NewTopicGroup extends React.PureComponent {
|
||||
onError={this.props.onError}
|
||||
onImageChanged={this.handleImageChanged} />
|
||||
</div>
|
||||
<div className="panel-form-row">
|
||||
<CheckBox checked={this.state.isChannel} onChange={this.handleChannelToggle}/>
|
||||
<label onClick={this.handleChannelToggle}><FormattedMessage id="channel_prompt"
|
||||
defaultMessage="This is a channel"
|
||||
description="Checkbox label when creating a channel" /></label>
|
||||
</div>
|
||||
<FormattedMessage id="title_tag_manager">{
|
||||
(title) => <TagManager
|
||||
tags={this.state.tags}
|
||||
activated={true}
|
||||
onTagsChanged={this.handleTagsChanged}
|
||||
tinode={this.props.tinode}
|
||||
title={title} />
|
||||
}</FormattedMessage>
|
||||
<div className="dialog-buttons">
|
||||
|
||||
@@ -4,7 +4,7 @@ import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import ChipInput from './chip-input.jsx';
|
||||
|
||||
import { MAX_TAG_COUNT, MIN_TAG_LENGTH } from '../config.js';
|
||||
import { MAX_TAG_COUNT, MAX_TAG_LENGTH, MIN_TAG_LENGTH } from '../config.js';
|
||||
import { arrayEqual } from '../lib/utils.js';
|
||||
|
||||
export default class TagManager extends React.Component {
|
||||
@@ -48,7 +48,9 @@ export default class TagManager extends React.Component {
|
||||
}
|
||||
|
||||
handleAddTag(tag) {
|
||||
if (tag.length > 0 && this.state.tags.length < MAX_TAG_COUNT) {
|
||||
const maxTagCount = this.props.tinode.getServerLimit('maxTagCount', MAX_TAG_COUNT);
|
||||
|
||||
if (tag.length > 0 && this.state.tags.length < maxTagCount) {
|
||||
const tags = this.state.tags.slice(0);
|
||||
tags.push(tag);
|
||||
|
||||
@@ -84,10 +86,13 @@ export default class TagManager extends React.Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const minTagLength = this.props.tinode.getServerLimit('minTagLength', MIN_TAG_LENGTH);
|
||||
const maxTagLength = this.props.tinode.getServerLimit('maxTagLength', MAX_TAG_LENGTH);
|
||||
|
||||
let tags = [];
|
||||
if (this.state.activated) {
|
||||
this.state.tags.map((tag) => {
|
||||
tags.push({user: tag, invalid: (tag.length < MIN_TAG_LENGTH)});
|
||||
tags.push({user: tag, invalid: (tag.length < minTagLength || tag.length > maxTagLength)});
|
||||
});
|
||||
} else {
|
||||
this.state.tags.map((tag) => {
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
// Image view with progress bar and a cancel button.
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import FileProgress from './file-progress.jsx';
|
||||
|
||||
export default class UploadingImage extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="inline-image">
|
||||
{React.createElement('img', this.props)}
|
||||
<div className="rounded-container">
|
||||
<FileProgress progress={this.props.progress} onCancel={this.props.onCancelUpload} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
+417
-214
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+395
-137
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
Binary file not shown.
|
After Width: | Height: | Size: 61 KiB |
Reference in New Issue
Block a user