Compare commits

...
40 Commits
Author SHA1 Message Date
or-else 7d1011400e merge devel 2025-08-10 12:15:14 +03:00
or-else 3a4794bacb merge master 2025-08-10 10:33:20 +03:00
or-else 56790262f3 rename drafty form response content type to text/x-drafy-fr 2025-08-10 10:30:47 +03:00
or-else fa436b61be remove unused css style 2025-08-09 14:45:14 +03:00
or-else bb1a3f4cea some colors fixed in doc preview 2025-08-09 09:43:40 +03:00
or-else 36cafbe87b dependencies updated 2025-08-09 09:15:18 +03:00
or-else a84a5a677a bump version to 0.24.3 and rebuild 2025-08-07 16:13:47 +03:00
or-else 6bec95e084 automatically change mime-type of json files to application/octeat-stream 2025-08-07 16:12:51 +03:00
or-else 290571f305 transition to application/json+drafty as mime type for form responses 2025-08-07 15:39:43 +03:00
or-else 7ce9470bf3 more consistent jest time mocks 2025-07-30 18:12:55 +03:00
or-else 9eba65b711 italian translation 2025-07-30 17:20:08 +03:00
or-else 0400c804aa dependencies updated, version bumped, rebuilt 2025-07-25 11:44:06 +03:00
or-else 7ea1a24857 more unit tests added 2025-07-25 11:43:45 +03:00
or-else 80d3f5336b dependencies updated, rebuilt, version bumped to 24.1 2025-07-09 15:22:38 +03:00
or-else 6a208cf040 rebuilt 2025-07-09 10:54:31 +03:00
or-else 9cef61d447 color tweaks, support for text size adjustment 2025-07-09 10:54:18 +03:00
or-else 2cb9f69a9d option for adjusting text size, rearranged config components 2025-07-09 10:53:32 +03:00
or-else 276f2bed34 make some components lazy-loaded 2025-07-09 10:51:27 +03:00
or-else 2f05cd0651 translations updated for new features 2025-07-09 10:49:43 +03:00
or-else 1ba3671873 missing files added, minor color tweaks 2025-06-14 10:51:20 +03:00
or-else 6d02cc903c dark mode mostly done 2025-06-13 16:26:48 +03:00
or-else ff60b3902e bump version and rebuild 2025-06-08 16:54:42 +03:00
or-else e641dc0d32 automatically add dir="rtl" to <html> tag when the language is RTL 2025-06-06 10:41:10 +03:00
or-else 8535da995f disable tag editing for user profiles 2025-06-03 19:30:53 +03:00
or-else baad48f044 added missing arabic translations 2025-06-03 18:38:03 +03:00
or-else 72009a693b tweak alias query, update dependencies, change version to -rc3 2025-06-02 17:05:49 +03:00
or-else b3adf15f12 merge devel with arabic support 2025-05-28 17:31:59 +03:00
or-else e8b48dfd84 hook up arabic translation, rebuild 2025-05-28 13:05:16 +03:00
or-else 7c6fb38b02 rebuilt 2025-05-28 12:56:59 +03:00
or-else 28f332f56a cosmetic fixes, rebuilt 2025-05-28 12:56:17 +03:00
or-else 6fdc208ca7 crash due to incorrect reference to topic 2025-05-28 12:55:59 +03:00
Gene 2f74357aac Merge pull request #137 from lamoboos223/devel
added Arabic localization
2025-05-28 12:54:38 +03:00
Lama Alosaimi e692f80bcf removed (,) from service-worker.js 2025-05-24 13:37:34 +03:00
Lama Alosaimi a411b80fc2 added arabic localization 2025-05-24 13:35:46 +03:00
or-else 837013e97c alias cleanup and fixes 2025-05-09 19:19:02 +03:00
or-else 360b37ae13 version updated to 0.24.0-rc2 2025-05-06 11:36:40 +03:00
or-else c69229934e dependencies updated 2025-05-06 11:33:32 +03:00
or-else 8288ad4733 fix alias regex: allow only alphanum as the first char 2025-05-06 10:43:59 +03:00
or-else 7d627edfce make background dark when viewing images 2025-05-01 15:05:42 +03:00
or-else 04279395bc clean up alias validator timer 2025-04-23 21:09:47 +03:00
119 changed files with 11162 additions and 5514 deletions
+2
View File
@@ -41,10 +41,12 @@ The app is fully internationalized using [React-Intl](https://github.com/formatj
The app is currently available in the following languages:
* English - default
* Arabic
* Chinese simplified
* Chinese traditional
* French
* German
* Italian
* Korean
* Romanian
* Russian
+694 -505
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
File diff suppressed because one or more lines are too long
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

+1
View File
@@ -0,0 +1 @@
<svg height="24" viewBox="0 -960 960 960" width="24" fill="#5f6368" xmlns="http://www.w3.org/2000/svg"><path d="M197-706l-84-85 56-57 85 85zM791-112l-85-85 57-57 84 85zM0-440v-80h120v80zM840-440v-80h120v80zM169-113l-57-56 85-85 57 57zM763-706l-57-57 85-84 57 56zM440 0v-120h80V0zM440-840v-120h80v120zM743-380a766.09 766.09 0 01-39.5-11c-13-4-25.833-8.667-38.5-14 5.333-12 9.167-24.167 11.5-36.5 2.333-12.333 3.5-25.167 3.5-38.5 0-55.333-19.5-102.5-58.5-141.5S535.333-680 480-680c-13.333 0-26.167 1.167-38.5 3.5-12.333 2.333-24.5 6.167-36.5 11.5-5.333-12.667-9.833-25.333-13.5-38a1098.034 1098.034 0 01-10.5-39c16-6 32.333-10.5 49-13.5s33.667-4.5 51-4.5c78 0 144.167 27.167 198.5 81.5S761-558 761-480c0 17.333-1.5 34.333-4.5 51s-7.5 33-13.5 49z" fill="orange"/><path d="M453-452c34 34 72 63.167 114 87.5s87 43.167 135 56.5c-26.667 34-59.333 60.5-98 79.5S524.333-200 481-200c-78 0-144.167-27.167-198.5-81.5S201-402 201-480c0-43.333 9.5-84.333 28.5-123s45.5-71.333 79.5-98c13.333 48 32.167 93 56.5 135S419-486 453-452z" fill="#0c80d6"/></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

+2599 -2541
View File
File diff suppressed because it is too large Load Diff
+7 -5
View File
@@ -1,7 +1,7 @@
{
"name": "tinode-webapp",
"description": "Tinode messenger for the web",
"version": "0.24.0-rc1",
"version": "0.25.0",
"repository": {
"type": "git",
"url": "git+https://github.com/tinode/webapp.git"
@@ -46,10 +46,12 @@
"web"
],
"locales": [
"ar",
"de",
"en",
"es",
"fr",
"it",
"ko",
"ro",
"ru",
@@ -73,7 +75,7 @@
"react": "^18.3.0",
"react-dom": "^18.3.0",
"react-intl": "^7.0.0",
"tinode-sdk": "^0.24.0-rc1",
"tinode-sdk": "^0.24.4",
"webm-duration-fix": "^1.0.4"
},
"devDependencies": {
@@ -81,13 +83,13 @@
"@babel/preset-env": "^7.22.10",
"@babel/preset-react": "^7.22.5",
"@formatjs/cli": "^6.1.3",
"babel-loader": "^9.1.3",
"babel-loader": "^10.0.0",
"babel-plugin-formatjs": "^10.5.3",
"browserslist": "^4.21.10",
"copy-webpack-plugin": "^12.0.0",
"copy-webpack-plugin": "^13.0.0",
"cssnano": "^7.0.6",
"glob": "^11.0.0",
"jest": "^29.7.0",
"jest": "^30.0.0",
"postcss": "^8.4.27",
"postcss-cli": "^11.0.0",
"webpack": "^5.88.2",
+13 -4
View File
@@ -26,6 +26,10 @@ const i18n = {
'new_message': "Nuevo mensaje",
'new_chat': "Nueva conversación",
},
'it': {
'new_message': "Nuovo Messaggio",
'new_chat': "Nuova Chat",
},
'ko': {
'new_message': "새로운 메시지",
'new_chat': "새로운 채팅",
@@ -53,10 +57,14 @@ const i18n = {
'zh-TW': {
'new_message': "新訊息",
'new_chat': "新聊天",
},
'ar': {
'new_message': "رسالة جديدة",
'new_chat': "محادثة جديدة",
}
};
self.i18nMessage = function(id) {
self.i18nMessage = function (id) {
if (!id) {
return null;
}
@@ -82,7 +90,7 @@ fbMessaging.onBackgroundMessage(payload => {
return;
}
const titles = {'msg': 'new_message', 'sub': 'new_chat'};
const titles = { 'msg': 'new_message', 'sub': 'new_chat' };
const pushType = payload.data.what || 'msg';
const title = payload.data.title || self.i18nMessage(titles[pushType]);
if (title) {
@@ -119,7 +127,8 @@ self.addEventListener('notificationclick', event => {
event.waitUntil(self.clients.matchAll({
type: 'window',
includeUncontrolled: true})
includeUncontrolled: true
})
.then(windowClients => {
let anyClient = null;
for (let i = 0; i < windowClients.length; i++) {
@@ -162,7 +171,7 @@ self.addEventListener('fetch', event => {
const reqUrl = new URL(event.request.url);
// Using ignoreSearch=true to read cached images and docs despite different auth signatures.
const cachedResponse = await cache.match(event.request, {ignoreSearch: (self.location.origin == reqUrl.origin)});
const cachedResponse = await cache.match(event.request, { ignoreSearch: (self.location.origin == reqUrl.origin) });
if (cachedResponse) {
return cachedResponse;
}
+9
View File
@@ -148,3 +148,12 @@ export const SELF_AVATAR_URI = 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS
// Toast display duration, milliseconds.
export const TOAST_DURATION = 3_000;
// Initial/default color scheme.
export const DEFAULT_COLOR_SCHEME = 'light dark';
// Default text size in points.
export const DEFAULT_TEXT_SIZE = 10;
// MIME type for the legacy Drafty form-response data.
export const DRAFTY_FR_MIME_TYPE_LEGACY = 'application/json'; // Remove in 2026.
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1791
View File
File diff suppressed because it is too large Load Diff
+49
View File
@@ -1738,5 +1738,54 @@
"description": "Notification that text has been copied to clipboard",
"missing": false,
"obsolete": false
},
"color_schema_dark": {
"translation": "Dunkel",
"defaultMessage": "Dark",
"description": "Name of the color schema",
"missing": false,
"obsolete": false
},
"color_schema_light": {
"translation": "Hell",
"defaultMessage": "Light",
"description": "Name of the color schema",
"missing": false,
"obsolete": false
},
"color_schema_system": {
"translation": "Systemstandard",
"defaultMessage": "System default",
"description": "Name of the color schema",
"missing": false,
"obsolete": false
},
"label_color_schema": {
"translation": "Thema:",
"defaultMessage": "Theme:",
"description": "Label for selecting color scheme (dark, light) in Settings",
"missing": false,
"obsolete": false
},
"sidepanel_title_appearance": {
"translation": "Erscheinungsbild",
"defaultMessage": "Appearance",
"description": "Sidepanel title for AccAppearanceView.",
"missing": false,
"obsolete": false
},
"label_text_size": {
"translation": "Textgröße:",
"defaultMessage": "Text size:",
"description": "Label adjusting text size",
"missing": false,
"obsolete": false
},
"incognito_mode_description": {
"translation": "",
"defaultMessage": "Hide your online status and typing indicators.",
"description": "Description for incognito mode toggle",
"missing": true,
"obsolete": false
}
}
+49
View File
@@ -1738,5 +1738,54 @@
"description": "Notification that text has been copied to clipboard",
"missing": false,
"obsolete": false
},
"color_schema_dark": {
"translation": "Dark",
"defaultMessage": "Dark",
"description": "Name of the color schema",
"missing": false,
"obsolete": false
},
"color_schema_light": {
"translation": "Light",
"defaultMessage": "Light",
"description": "Name of the color schema",
"missing": false,
"obsolete": false
},
"color_schema_system": {
"translation": "System default",
"defaultMessage": "System default",
"description": "Name of the color schema",
"missing": false,
"obsolete": false
},
"label_color_schema": {
"translation": "Theme:",
"defaultMessage": "Theme:",
"description": "Label for selecting color scheme (dark, light) in Settings",
"missing": false,
"obsolete": false
},
"sidepanel_title_appearance": {
"translation": "Appearance",
"defaultMessage": "Appearance",
"description": "Sidepanel title for AccAppearanceView.",
"missing": false,
"obsolete": false
},
"label_text_size": {
"translation": "Text size:",
"defaultMessage": "Text size:",
"description": "Label adjusting text size",
"missing": false,
"obsolete": false
},
"incognito_mode_description": {
"translation": "Hide your online status and typing indicators.",
"defaultMessage": "Hide your online status and typing indicators.",
"description": "Description for incognito mode toggle",
"missing": false,
"obsolete": false
}
}
+49
View File
@@ -1738,5 +1738,54 @@
"description": "Notification that text has been copied to clipboard",
"missing": false,
"obsolete": false
},
"color_schema_dark": {
"translation": "Oscuro",
"defaultMessage": "Dark",
"description": "Name of the color schema",
"missing": false,
"obsolete": false
},
"color_schema_light": {
"translation": "Claro",
"defaultMessage": "Light",
"description": "Name of the color schema",
"missing": false,
"obsolete": false
},
"color_schema_system": {
"translation": "Predeterminado del sistema",
"defaultMessage": "System default",
"description": "Name of the color schema",
"missing": false,
"obsolete": false
},
"label_color_schema": {
"translation": "Tema:",
"defaultMessage": "Theme:",
"description": "Label for selecting color scheme (dark, light) in Settings",
"missing": false,
"obsolete": false
},
"sidepanel_title_appearance": {
"translation": "Apariencia",
"defaultMessage": "Appearance",
"description": "Sidepanel title for AccAppearanceView.",
"missing": false,
"obsolete": false
},
"label_text_size": {
"translation": "Tamaño del texto:",
"defaultMessage": "Text size:",
"description": "Label adjusting text size",
"missing": false,
"obsolete": false
},
"incognito_mode_description": {
"translation": "",
"defaultMessage": "Hide your online status and typing indicators.",
"description": "Description for incognito mode toggle",
"missing": true,
"obsolete": false
}
}
+28
View File
@@ -211,6 +211,18 @@
"defaultMessage": "Code does not match",
"description": "Error message when the credential validation code is incorrect."
},
"color_schema_dark": {
"defaultMessage": "Dark",
"description": "Name of the color schema"
},
"color_schema_light": {
"defaultMessage": "Light",
"description": "Name of the color schema"
},
"color_schema_system": {
"defaultMessage": "System default",
"description": "Name of the color schema"
},
"contacts_not_found": {
"defaultMessage": "You have no chats<br />¯∖_(ツ)_/¯",
"description": "HTML message shown in ContactList when no contacts are found"
@@ -374,6 +386,10 @@
"defaultMessage": "Image caption",
"description": "Prompt in SendMessage for attached image"
},
"incognito_mode_description": {
"defaultMessage": "Hide your online status and typing indicators.",
"description": "Description for incognito mode toggle"
},
"invalid_content": {
"defaultMessage": "invalid content",
"description": "Shown when the message is unreadable"
@@ -394,6 +410,10 @@
"defaultMessage": "Client:",
"description": "Label for a client version"
},
"label_color_schema": {
"defaultMessage": "Theme:",
"description": "Label for selecting color scheme (dark, light) in Settings"
},
"label_content_type": {
"defaultMessage": "Content type:",
"description": "Label for file content type (mime)"
@@ -480,6 +500,10 @@
"defaultMessage": "Size:",
"description": "Label for file size"
},
"label_text_size": {
"defaultMessage": "Text size:",
"description": "Label adjusting text size"
},
"label_topic_name": {
"defaultMessage": "Name",
"description": "Label for editing topic name"
@@ -868,6 +892,10 @@
"defaultMessage": "Account Settings",
"description": "Sidepanel title for AccountSettingsView."
},
"sidepanel_title_appearance": {
"defaultMessage": "Appearance",
"description": "Sidepanel title for AccAppearanceView."
},
"sidepanel_title_archive": {
"defaultMessage": "Archived Chats",
"description": "Sidepanel title for ContactsView-Archive."
+49
View File
@@ -1738,5 +1738,54 @@
"description": "Notification that text has been copied to clipboard",
"missing": false,
"obsolete": false
},
"color_schema_dark": {
"translation": "Sombre",
"defaultMessage": "Dark",
"description": "Name of the color schema",
"missing": false,
"obsolete": false
},
"color_schema_light": {
"translation": "Clair",
"defaultMessage": "Light",
"description": "Name of the color schema",
"missing": false,
"obsolete": false
},
"color_schema_system": {
"translation": "Thème du système",
"defaultMessage": "System default",
"description": "Name of the color schema",
"missing": false,
"obsolete": false
},
"label_color_schema": {
"translation": "Thème :",
"defaultMessage": "Theme:",
"description": "Label for selecting color scheme (dark, light) in Settings",
"missing": false,
"obsolete": false
},
"sidepanel_title_appearance": {
"translation": "Apparence",
"defaultMessage": "Appearance",
"description": "Sidepanel title for AccAppearanceView.",
"missing": false,
"obsolete": false
},
"label_text_size": {
"translation": "Taille du texte :",
"defaultMessage": "Text size:",
"description": "Label adjusting text size",
"missing": false,
"obsolete": false
},
"incognito_mode_description": {
"translation": "",
"defaultMessage": "Hide your online status and typing indicators.",
"description": "Description for incognito mode toggle",
"missing": true,
"obsolete": false
}
}
+1791
View File
File diff suppressed because it is too large Load Diff
+49
View File
@@ -1738,5 +1738,54 @@
"description": "Notification that text has been copied to clipboard",
"missing": false,
"obsolete": false
},
"color_schema_dark": {
"translation": "어둡게",
"defaultMessage": "Dark",
"description": "Name of the color schema",
"missing": false,
"obsolete": false
},
"color_schema_light": {
"translation": "밝게",
"defaultMessage": "Light",
"description": "Name of the color schema",
"missing": false,
"obsolete": false
},
"color_schema_system": {
"translation": "시스템 기본값",
"defaultMessage": "System default",
"description": "Name of the color schema",
"missing": false,
"obsolete": false
},
"label_color_schema": {
"translation": "테마:",
"defaultMessage": "Theme:",
"description": "Label for selecting color scheme (dark, light) in Settings",
"missing": false,
"obsolete": false
},
"sidepanel_title_appearance": {
"translation": "모양",
"defaultMessage": "Appearance",
"description": "Sidepanel title for AccAppearanceView.",
"missing": false,
"obsolete": false
},
"label_text_size": {
"translation": "텍스트 크기:",
"defaultMessage": "Text size:",
"description": "Label adjusting text size",
"missing": false,
"obsolete": false
},
"incognito_mode_description": {
"translation": "",
"defaultMessage": "Hide your online status and typing indicators.",
"description": "Description for incognito mode toggle",
"missing": true,
"obsolete": false
}
}
+49
View File
@@ -1738,5 +1738,54 @@
"description": "Notification that text has been copied to clipboard",
"missing": false,
"obsolete": false
},
"color_schema_dark": {
"translation": "Întunecat",
"defaultMessage": "Dark",
"description": "Name of the color schema",
"missing": false,
"obsolete": false
},
"color_schema_light": {
"translation": "Luminos",
"defaultMessage": "Light",
"description": "Name of the color schema",
"missing": false,
"obsolete": false
},
"color_schema_system": {
"translation": "Implicit sistem",
"defaultMessage": "System default",
"description": "Name of the color schema",
"missing": false,
"obsolete": false
},
"label_color_schema": {
"translation": "Temă:",
"defaultMessage": "Theme:",
"description": "Label for selecting color scheme (dark, light) in Settings",
"missing": false,
"obsolete": false
},
"sidepanel_title_appearance": {
"translation": "Aspect",
"defaultMessage": "Appearance",
"description": "Sidepanel title for AccAppearanceView.",
"missing": false,
"obsolete": false
},
"label_text_size": {
"translation": "Dimensiune text:",
"defaultMessage": "Text size:",
"description": "Label adjusting text size",
"missing": false,
"obsolete": false
},
"incognito_mode_description": {
"translation": "",
"defaultMessage": "Hide your online status and typing indicators.",
"description": "Description for incognito mode toggle",
"missing": true,
"obsolete": false
}
}
+49
View File
@@ -1738,5 +1738,54 @@
"description": "Notification that text has been copied to clipboard",
"missing": false,
"obsolete": false
},
"color_schema_dark": {
"translation": "Тёмная",
"defaultMessage": "Dark",
"description": "Name of the color schema",
"missing": false,
"obsolete": false
},
"color_schema_light": {
"translation": "Светлая",
"defaultMessage": "Light",
"description": "Name of the color schema",
"missing": false,
"obsolete": false
},
"color_schema_system": {
"translation": "Системная",
"defaultMessage": "System default",
"description": "Name of the color schema",
"missing": false,
"obsolete": false
},
"label_color_schema": {
"translation": "Тема:",
"defaultMessage": "Theme:",
"description": "Label for selecting color scheme (dark, light) in Settings",
"missing": false,
"obsolete": false
},
"sidepanel_title_appearance": {
"translation": "Оформление",
"defaultMessage": "Appearance",
"description": "Sidepanel title for AccAppearanceView.",
"missing": false,
"obsolete": false
},
"label_text_size": {
"translation": "Размер текста:",
"defaultMessage": "Text size:",
"description": "Label adjusting text size",
"missing": false,
"obsolete": false
},
"incognito_mode_description": {
"translation": "",
"defaultMessage": "Hide your online status and typing indicators.",
"description": "Description for incognito mode toggle",
"missing": true,
"obsolete": false
}
}
+49
View File
@@ -1738,5 +1738,54 @@
"description": "Notification that text has been copied to clipboard",
"missing": false,
"obsolete": false
},
"color_schema_dark": {
"translation": "มืด",
"defaultMessage": "Dark",
"description": "Name of the color schema",
"missing": false,
"obsolete": false
},
"color_schema_light": {
"translation": "สว่าง",
"defaultMessage": "Light",
"description": "Name of the color schema",
"missing": false,
"obsolete": false
},
"color_schema_system": {
"translation": "ค่าเริ่มต้นของระบบ",
"defaultMessage": "System default",
"description": "Name of the color schema",
"missing": false,
"obsolete": false
},
"label_color_schema": {
"translation": "ธีม:",
"defaultMessage": "Theme:",
"description": "Label for selecting color scheme (dark, light) in Settings",
"missing": false,
"obsolete": false
},
"sidepanel_title_appearance": {
"translation": "ลักษณะ",
"defaultMessage": "Appearance",
"description": "Sidepanel title for AccAppearanceView.",
"missing": false,
"obsolete": false
},
"label_text_size": {
"translation": "ขนาดตัวอักษร:",
"defaultMessage": "Text size:",
"description": "Label adjusting text size",
"missing": false,
"obsolete": false
},
"incognito_mode_description": {
"translation": "",
"defaultMessage": "Hide your online status and typing indicators.",
"description": "Description for incognito mode toggle",
"missing": true,
"obsolete": false
}
}
+49
View File
@@ -1738,5 +1738,54 @@
"description": "Notification that text has been copied to clipboard",
"missing": false,
"obsolete": false
},
"color_schema_dark": {
"translation": "Темна",
"defaultMessage": "Dark",
"description": "Name of the color schema",
"missing": false,
"obsolete": false
},
"color_schema_light": {
"translation": "Світла",
"defaultMessage": "Light",
"description": "Name of the color schema",
"missing": false,
"obsolete": false
},
"color_schema_system": {
"translation": "Системна",
"defaultMessage": "System default",
"description": "Name of the color schema",
"missing": false,
"obsolete": false
},
"label_color_schema": {
"translation": "Тема:",
"defaultMessage": "Theme:",
"description": "Label for selecting color scheme (dark, light) in Settings",
"missing": false,
"obsolete": false
},
"sidepanel_title_appearance": {
"translation": "Вигляд",
"defaultMessage": "Appearance",
"description": "Sidepanel title for AccAppearanceView.",
"missing": false,
"obsolete": false
},
"label_text_size": {
"translation": "Розмір тексту:",
"defaultMessage": "Text size:",
"description": "Label adjusting text size",
"missing": false,
"obsolete": false
},
"incognito_mode_description": {
"translation": "",
"defaultMessage": "Hide your online status and typing indicators.",
"description": "Description for incognito mode toggle",
"missing": true,
"obsolete": false
}
}
+49
View File
@@ -1738,5 +1738,54 @@
"description": "Notification that text has been copied to clipboard",
"missing": false,
"obsolete": false
},
"color_schema_dark": {
"translation": "深色",
"defaultMessage": "Dark",
"description": "Name of the color schema",
"missing": false,
"obsolete": false
},
"color_schema_light": {
"translation": "淺色",
"defaultMessage": "Light",
"description": "Name of the color schema",
"missing": false,
"obsolete": false
},
"color_schema_system": {
"translation": "系統預設",
"defaultMessage": "System default",
"description": "Name of the color schema",
"missing": false,
"obsolete": false
},
"label_color_schema": {
"translation": "主題:",
"defaultMessage": "Theme:",
"description": "Label for selecting color scheme (dark, light) in Settings",
"missing": false,
"obsolete": false
},
"sidepanel_title_appearance": {
"translation": "外觀",
"defaultMessage": "Appearance",
"description": "Sidepanel title for AccAppearanceView.",
"missing": false,
"obsolete": false
},
"label_text_size": {
"translation": "文字大小:",
"defaultMessage": "Text size:",
"description": "Label adjusting text size",
"missing": false,
"obsolete": false
},
"incognito_mode_description": {
"translation": "",
"defaultMessage": "Hide your online status and typing indicators.",
"description": "Description for incognito mode toggle",
"missing": true,
"obsolete": false
}
}
+49
View File
@@ -1738,5 +1738,54 @@
"description": "Notification that text has been copied to clipboard",
"missing": false,
"obsolete": false
},
"color_schema_dark": {
"translation": "深色",
"defaultMessage": "Dark",
"description": "Name of the color schema",
"missing": false,
"obsolete": false
},
"color_schema_light": {
"translation": "浅色",
"defaultMessage": "Light",
"description": "Name of the color schema",
"missing": false,
"obsolete": false
},
"color_schema_system": {
"translation": "系统默认",
"defaultMessage": "System default",
"description": "Name of the color schema",
"missing": false,
"obsolete": false
},
"label_color_schema": {
"translation": "主题:",
"defaultMessage": "Theme:",
"description": "Label for selecting color scheme (dark, light) in Settings",
"missing": false,
"obsolete": false
},
"sidepanel_title_appearance": {
"translation": "外观",
"defaultMessage": "Appearance",
"description": "Sidepanel title for AccAppearanceView.",
"missing": false,
"obsolete": false
},
"label_text_size": {
"translation": "文字大小:",
"defaultMessage": "Text size:",
"description": "Label adjusting text size",
"missing": false,
"obsolete": false
},
"incognito_mode_description": {
"translation": "",
"defaultMessage": "Hide your online status and typing indicators.",
"description": "Description for incognito mode toggle",
"missing": true,
"obsolete": false
}
}
+10 -2
View File
@@ -24,6 +24,7 @@ if (typeof FIREBASE_INIT != 'undefined' && FIREBASE_INIT && FIREBASE_INIT.measur
// Allow loading translation strings for just one language.
const messageLoader = {
'ar': _ => import('./i18n.min/ar.json'),
'de': _ => import('./i18n.min/de.json'),
'en': _ => import('./i18n.min/en.json'),
'es': _ => import('./i18n.min/es.json'),
@@ -37,6 +38,9 @@ const messageLoader = {
'zh-TW': _ => import('./i18n.min/zh-TW.json')
};
// List of languages that are written right-to-left.
const rtl = ['ar', 'fa', 'he', 'ur'];
// Detect human language to use in the UI:
// Check parameters from URL hash #?hl=ru, then browser, then use 'en' as a fallback.
const { params } = HashNavigation.parseUrlHash(window.location.hash);
@@ -54,9 +58,13 @@ const baseLanguage = normalized.split('-')[0].toLowerCase();
// Try the full locale first, then the locale without the region code, fallback to 'en'.
const htmlLang = messageLoader[normalized] ? language : messageLoader[baseLanguage] ? baseLanguage : 'en';
const html = document.getElementsByTagName('html')[0];
// Set lang attribute of the HTML element: <html lang="XX">
document.getElementsByTagName('html')[0].setAttribute('lang', htmlLang);
html.setAttribute('lang', htmlLang);
if (rtl.includes(baseLanguage)) {
// Set right-to-left dir attribute of the HTML element: <html dir="rtl">
html.setAttribute('dir', 'rtl');
}
// Render the app.
const root = createRoot(document.getElementById('mountPoint'));
messageLoader[htmlLang]().then(messages =>
+4 -4
View File
@@ -10,8 +10,8 @@ import InlineVideo from '../widgets/inline-video.jsx';
import LazyImage from '../widgets/lazy-image.jsx'
import UploadingImage from '../widgets/uploading-image.jsx'
import { BROKEN_IMAGE_SIZE, CLICKABLE_URL_SCHEMES, IMAGE_THUMBNAIL_DIM, NO_DIMENSIONS_VIDEO, REM_SIZE,
VIDEO_THUMBNAIL_WIDTH } from '../config.js';
import { BROKEN_IMAGE_SIZE, CLICKABLE_URL_SCHEMES, IMAGE_THUMBNAIL_DIM, NO_DIMENSIONS_VIDEO,
REM_SIZE, VIDEO_THUMBNAIL_WIDTH } from '../config.js';
import { base64ToBlob, blobToBase64, fitImageSize, imageScaled } from './blob-helpers.js';
import { idToColorClass, secondsToTime, shortenFileName } from './strformat.js';
import { cancelablePromise, sanitizeUrl, sanitizeUrlForMime } from './utils.js';
@@ -304,7 +304,7 @@ export function previewFormatter(style, data, values, key) {
break;
case 'EX':
if (data) {
if (data.mime == 'application/json') {
if (Drafty.isFormResponseType(data.mime)) {
// Ignore JSON attachments: they are form response payloads.
return null;
}
@@ -426,7 +426,7 @@ function quoteFormatter(style, data, values, key) {
case 'EX':
let fname;
if (data) {
if (data.mime == 'application/json') {
if (Drafty.isFormResponseType(data.mime)) {
// Ignore JSON attachments: they are form response payloads.
return null;
}
+154
View File
@@ -0,0 +1,154 @@
import {
shortDateFormat,
relativeDateFormat,
secondsToTime,
bytesToHumanSize,
shortenFileName,
idToColorClass,
flagEmoji
} from './strformat.js';
describe('shortDateFormat', () => {
jest
.useFakeTimers()
.setSystemTime(new Date('2025-07-25T12:30:00Z'));
test('should format for same day', () => {
const then = new Date('2025-07-25T10:15:00');
// Note: output depends on the test runner's locale and timezone.
// Assuming a locale where the format is HH:mm.
expect(shortDateFormat(then, 'en-US')).toMatch(/10:15/);
});
test('should format for same year, different day', () => {
const then = new Date('2025-06-20T10:15:00');
expect(shortDateFormat(then, 'en-US')).toMatch(/Jun 20, 10:15/);
});
test('should format for different year', () => {
const then = new Date('2024-07-25T10:15:00');
expect(shortDateFormat(then, 'en-US')).toBe('Jul 25, 2024');
});
});
describe('relativeDateFormat', () => {
// Mock current date to a fixed point for consistent testing.
jest
.useFakeTimers()
.setSystemTime(new Date('2025-07-25T12:30:00Z'));
test('should format for today', () => {
const then = new Date('2025-07-25T10:00:00Z');
expect(relativeDateFormat(then, 'en-US')).toBe('today');
});
test('should format for yesterday', () => {
const then = new Date('2025-07-24T10:00:00Z');
expect(relativeDateFormat(then, 'en-US')).toBe('yesterday');
});
test('should format for a past date', () => {
const then = new Date('2025-07-20T10:00:00Z');
expect(relativeDateFormat(then, 'en-US')).toBe('7/20/2025');
});
test('should format for tomorrow', () => {
const then = new Date('2025-07-26T10:00:00Z');
expect(relativeDateFormat(then, 'en-US')).toBe('tomorrow');
});
});
describe('secondsToTime', () => {
test('should format seconds only', () => {
expect(secondsToTime(45)).toBe('0:45');
});
test('should format minutes and seconds', () => {
expect(secondsToTime(156)).toBe('2:36');
});
test('should format hours, minutes, and seconds', () => {
expect(secondsToTime(3756)).toBe('1:02:36');
});
test('should format with fixed minutes', () => {
expect(secondsToTime(3726, true)).toBe('1:02:06');
expect(secondsToTime(126, true)).toBe('02:06');
});
test('should handle non-number input', () => {
expect(secondsToTime('abc')).toBe('');
});
});
describe('bytesToHumanSize', () => {
test('should format 0 bytes', () => {
expect(bytesToHumanSize(0)).toBe('0 Bytes');
});
test('should format bytes', () => {
expect(bytesToHumanSize(500)).toBe('500 Bytes');
});
test('should format kilobytes', () => {
expect(bytesToHumanSize(1500)).toBe('1.46 KB');
});
test('should format megabytes', () => {
expect(bytesToHumanSize(1500000)).toBe('1.43 MB');
});
test('should format gigabytes', () => {
expect(bytesToHumanSize(1500000000)).toBe('1.40 GB');
});
});
describe('shortenFileName', () => {
test('should not shorten if not needed', () => {
expect(shortenFileName('shortname.txt', 20)).toBe('shortname.txt');
});
test('should shorten a long file name', () => {
const longName = 'this_is_a_very_long_file_name_that_needs_shortening.txt';
expect(shortenFileName(longName, 20)).toBe('this_is_a…ening.txt');
expect(shortenFileName(longName, 20).length).toBeLessThanOrEqual(20);
});
test('should handle non-string input', () => {
expect(shortenFileName(null, 20)).toBeNull();
expect(shortenFileName(undefined, 20)).toBeUndefined();
});
});
describe('idToColorClass', () => {
test('should generate light background color class', () => {
expect(idToColorClass('usr123', true, false)).toMatch(/lt-bg-\d+/);
});
test('should generate dark background color class', () => {
expect(idToColorClass('usr123', false, false)).toMatch(/dk-bg-\d+/);
});
test('should generate light foreground color class', () => {
expect(idToColorClass('usr123', true, true)).toMatch(/lt-fg-\d+/);
});
test('should generate dark foreground color class', () => {
expect(idToColorClass('usr123', false, true)).toMatch(/dk-fg-\d+/);
});
test('should be deterministic', () => {
expect(idToColorClass('usr123', true, false)).toBe('lt-bg-2');
expect(idToColorClass('grpABC', false, true)).toBe('dk-fg-3');
});
});
describe('flagEmoji', () => {
test('should convert country code to flag', () => {
expect(flagEmoji('US')).toBe('🇺🇸');
});
test('should handle lowercase country code', () => {
expect(flagEmoji('jp')).toBe('🇯🇵');
});
});
+1 -1
View File
@@ -1,3 +1,3 @@
// This is a generated file. Don't edit.
export const PACKAGE_VERSION = "0.24.0-rc1";
export const PACKAGE_VERSION = "0.25.0";
+95
View File
@@ -0,0 +1,95 @@
// Edit account parameters.
import React from 'react';
import { FormattedMessage } from 'react-intl';
export default class AccAppearanceView extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
colorSchema: props.colorSchema || 'light dark',
textSize: (props.textSize * 10) || '100',
};
this.handleColorSchemaSelected = this.handleColorSchemaSelected.bind(this);
this.handleTextSizeChanged = this.handleTextSizeChanged.bind(this);
}
handleColorSchemaSelected(e) {
this.setState({colorSchema: e.currentTarget.value});
this.props.onChangeColorSchema(e.currentTarget.value);
}
handleTextSizeChanged(e) {
// In %%.
this.setState({textSize: e.currentTarget.value});
// Convert %% to pt, 100% = 10pt.
this.props.onTextSizeChanged((e.currentTarget.value / 10) | 0);
}
render() {
return (
<div id="settings-form" className="scrollable-panel">
<div className="panel-form-row">
<label className="small">
<FormattedMessage id="label_color_schema" defaultMessage="Theme:"
description="Label for selecting color scheme (dark, light) in Settings" />
</label>
</div>
<div className="panel-form-row">
<ul className="quoted">
<li key="system">
<input type="radio" id="system" name="color-scheme-select" value="light dark"
checked={this.state.colorSchema === 'light dark'}
onChange={this.handleColorSchemaSelected} />&nbsp;
<label htmlFor="system">
<FormattedMessage id="color_schema_system" defaultMessage="System default" description="Name of the color schema"/>&nbsp;
<img src="../img/routine.svg" style={{verticalAlign: 'top', width: '1.6rem', height: '1.6rem'}}/>
</label>
</li>
<li key="light">
<input type="radio" id="light" name="color-scheme-select" value="light"
checked={this.state.colorSchema === 'light'}
onChange={this.handleColorSchemaSelected} />&nbsp;
<label htmlFor="light">
<FormattedMessage id="color_schema_light" defaultMessage="Light" description="Name of the color schema"/>&nbsp;
<i className="material-icons orange large">light_mode</i>
</label>
</li>
<li key="dark">
<input type="radio" id="dark" name="color-scheme-select" value="dark"
checked={this.state.colorSchema === 'dark'}
onChange={this.handleColorSchemaSelected} />&nbsp;
<label htmlFor="dark">
<FormattedMessage id="color_schema_dark" defaultMessage="Dark" description="Name of the color schema"/>&nbsp;
<i className="material-icons blue large">dark_mode</i>
</label>
</li>
</ul>
</div>
<div className="hr" />
<div className="panel-form-row">
<label className="small">
<FormattedMessage id="label_text_size" defaultMessage="Text size:"
description="Label adjusting text size" />
</label>
</div>
<div className="panel-form-column">
<div className="panel-form-row">
<input type="range" id="text_size" name="text_size" min="80" max="120" step="10"
list="text_size_options" value={this.state.textSize} onChange={this.handleTextSizeChanged} />
</div>
<div className="panel-form-row">
<datalist id="text_size_options">
<option value="80" label="80%" />
<option value="90" label="90%" />
<option value="100" label="100%" />
<option value="110" label="110%" />
<option value="120" label="120%" />
</datalist>
</div>
</div>
</div>
);
}
};
+1 -11
View File
@@ -4,7 +4,7 @@ import { FormattedMessage } from 'react-intl';
import CheckBox from '../widgets/checkbox.jsx';
export default class EditAccountView extends React.PureComponent {
export default class AccNotificationsView extends React.PureComponent {
constructor(props) {
super(props);
@@ -16,8 +16,6 @@ export default class EditAccountView extends React.PureComponent {
this.props.onToggleMessageSounds(checked);
} else if (what == 'alert') {
this.props.onTogglePushNotifications(checked);
} else if (what == 'incognito') {
this.props.onToggleIncognitoMode(checked);
}
}
@@ -47,14 +45,6 @@ export default class EditAccountView extends React.PureComponent {
checked={this.props.desktopAlerts}
onChange={this.props.desktopAlertsEnabled ? this.handleCheckboxClick : null} />
</div>
<div className="panel-form-row">
<label htmlFor="incognito-mode">
<FormattedMessage id="label_incognito_mode" defaultMessage="Incognito mode:"
description="Label for incognito mode toggle" />
</label>
<CheckBox name="incognito" id="incognito-mode"
checked={this.props.incognitoMode} onChange={this.handleCheckboxClick} />
</div>
</div>
);
}
+21
View File
@@ -2,6 +2,7 @@
import React from 'react';
import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
import CheckBox from '../widgets/checkbox.jsx';
import InPlaceEdit from '../widgets/in-place-edit.jsx';
import PermissionsEditor from '../widgets/permissions-editor.jsx';
@@ -42,6 +43,7 @@ class AccSecurityView extends React.Component {
this.handleHidePermissionsEditor = this.handleHidePermissionsEditor.bind(this);
this.handlePermissionsChanged = this.handlePermissionsChanged.bind(this);
this.handleDeleteAccount = this.handleDeleteAccount.bind(this);
this.handleCheckboxClick = this.handleCheckboxClick.bind(this);
}
handlePasswordUpdate(pwd) {
@@ -70,6 +72,10 @@ class AccSecurityView extends React.Component {
this.setState(newState);
}
handleCheckboxClick(what, checked) {
this.props.onToggleIncognitoMode(checked);
}
handleDeleteAccount(e) {
e.preventDefault();
const {formatMessage} = this.props.intl;
@@ -109,6 +115,21 @@ class AccSecurityView extends React.Component {
</div>
</div>
<div className="hr" />
<div className="panel-form-row">
<label htmlFor="incognito-mode">
<FormattedMessage id="label_incognito_mode" defaultMessage="Incognito mode:"
description="Label for incognito mode toggle" />
</label>
<CheckBox name="incognito" id="incognito-mode"
checked={this.props.incognitoMode} onChange={this.handleCheckboxClick} />
</div>
<div className="panel-form-row">
<span className="small">
<FormattedMessage id="incognito_mode_description" defaultMessage="Hide your online status and typing indicators."
description="Description for incognito mode toggle" />
</span>
</div>
<div className="hr" />
<div className="panel-form-column">
<a href="#" className="danger flat-button" onClick={(e) => {e.preventDefault(); this.props.onLogout();}}>
<i className="material-icons">exit_to_app</i> &nbsp;<FormattedMessage id="button_logout"
+4
View File
@@ -148,6 +148,10 @@ export default class AccountSettingsView extends React.Component {
</div>
<div className="hr" />
<div className="panel-form-column">
<a href="#" className="flat-button" onClick={e => {e.preventDefault(); this.props.onNavigate('appearance');}}>
<i className="material-icons">tune</i>&nbsp;<FormattedMessage id="sidepanel_title_appearance"
defaultMessage="Appearance" description="Sidepanel title for AccAppearanceView." />
</a>
<a href="#" className="flat-button" onClick={e => {e.preventDefault(); this.props.onNavigate('notif');}}>
<i className="material-icons">notifications</i>&nbsp;<FormattedMessage id="sidepanel_title_acc_notifications"
defaultMessage="Notifications" description="Sidepanel title for AccNotificationsView." />
+10 -5
View File
@@ -22,8 +22,8 @@ import SendMessage from '../widgets/send-message.jsx';
import VideoPreview from '../widgets/video-preview.jsx';
import { DEFAULT_P2P_ACCESS_MODE, EDIT_PREVIEW_LENGTH, IMAGE_PREVIEW_DIM, IMMEDIATE_P2P_SUBSCRIPTION,
KEYPRESS_DELAY, MESSAGES_PAGE, MAX_EXTERN_ATTACHMENT_SIZE, MAX_IMAGE_DIM, MAX_INBAND_ATTACHMENT_SIZE,
READ_DELAY, QUOTED_REPLY_LENGTH, VIDEO_PREVIEW_DIM } from '../config.js';
DRAFTY_FR_MIME_TYPE_LEGACY, KEYPRESS_DELAY, MESSAGES_PAGE, MAX_EXTERN_ATTACHMENT_SIZE, MAX_IMAGE_DIM,
MAX_INBAND_ATTACHMENT_SIZE, READ_DELAY, QUOTED_REPLY_LENGTH, VIDEO_PREVIEW_DIM } from '../config.js';
import { CALL_STATE_OUTGOING_INITATED, CALL_STATE_IN_PROGRESS } from '../constants.js';
import { blobToBase64, fileToBase64, imageScaled, makeImageUrl } from '../lib/blob-helpers.js';
import HashNavigation from '../lib/navigation.js';
@@ -1022,6 +1022,11 @@ class MessagesView extends React.Component {
const maxInbandAttachmentSize = (this.props.tinode.getServerParam('maxMessageSize',
MAX_INBAND_ATTACHMENT_SIZE) * 0.75 - 1024) | 0;
// If the attachment is a JSON file, then use 'application/octet-stream' instead of 'application/json'.
// This is a temporary workaround for the collision with the 'application/json' MIME type of form responses.
// Remove this code in 2026 or so.
const jsonMimeConverter = (fileType) => fileType === DRAFTY_FR_MIME_TYPE_LEGACY ? 'application/octet-stream' : fileType;
if (file.size > maxInbandAttachmentSize) {
// Too large to send inband - uploading out of band and sending as a link.
const uploader = this.props.tinode.getLargeFileHelper();
@@ -1031,7 +1036,7 @@ class MessagesView extends React.Component {
}
const uploadCompletionPromise = uploader.upload(file);
const msg = Drafty.attachFile(null, {
mime: file.type,
mime: jsonMimeConverter(file.type),
filename: file.name,
size: file.size,
urlPromise: uploadCompletionPromise
@@ -1042,7 +1047,7 @@ class MessagesView extends React.Component {
// Small enough to send inband.
fileToBase64(file)
.then(b64 => this.sendMessage(Drafty.attachFile(null, {
mime: b64.mime,
mime: jsonMimeConverter(b64.mime),
data: b64.bits,
filename: b64.name,
size: file.size
@@ -1673,7 +1678,7 @@ class MessagesView extends React.Component {
onClick={this.goToLatestMessage}>
<i className="material-icons">arrow_downward</i>
</button>
<div id="messages-panel" ref={this.handleScrollReference}>
<div id="messages-panel" ref={this.handleScrollReference} color-scheme={this.props.colorSchema}>
<ul id="scroller" className={chatBoxClass}>
{messageNodes}
</ul>
+11 -9
View File
@@ -57,21 +57,23 @@ class NewTopicView extends React.Component {
}
handleSearchContacts(query) {
query = query.trim();
if (!query) {
query = Tinode.DEL_CHAR;
} else if (!/[\s,:]/.test(query) && query != Tinode.DEL_CHAR) {
// No colons, spaces or commas, not DEL char. Try as email or phone.
// If not email or phone, treat as alias.
query = query.trim() || Tinode.DEL_CHAR;
if (!/[\s,:]/.test(query) && query != Tinode.DEL_CHAR) {
// No colons, spaces or commas, not DEL char. Try as email, phone, or alias.
const email = asEmail(query);
if (email) {
query = `email:${email}`;
query = `${Tinode.TAG_EMAIL}${email}`;
} else {
const tel = asPhone(query);
if (tel) {
query = `tel:${tel}`;
query = `${Tinode.TAG_PHONE}${tel}`;
} else {
query = `alias:${query}`;
if (query[0] == '@') {
// Strip the leading '@' sign if present.
query = query.substring(1);
}
// 'alice' -> 'alias:alice,alice'
query = `${Tinode.TAG_ALIAS}${query},${query}`;
}
}
}
+21 -5
View File
@@ -9,8 +9,9 @@ import TopicCommonView from './topic-common-view.jsx';
const AccountSettingsView = React.lazy(_ => import('./account-settings-view.jsx'));
import ContactsView from './contacts-view.jsx';
const CreateAccountView = React.lazy(_ => import('./create-account-view.jsx'));
import AccAppearanceView from './acc-appearance-view.jsx';
import AccNotificationsView from './acc-notifications-view.jsx';
import AccSecurityView from './acc-security-view.jsx';
const AccSecurityView = React.lazy(_ => import('./acc-security-view.jsx'));
import AccSupportView from './acc-support-view.jsx';
import LoginView from './login-view.jsx';
import NewTopicView from './new-topic-view.jsx';
@@ -60,6 +61,11 @@ const messages = defineMessages({
description: 'Sidepanel title for AccNotificationsView.',
defaultMessage: 'Notifications'
},
appearance: {
id: 'sidepanel_title_appearance',
description: 'Sidepanel title for AccAppearanceView.',
defaultMessage: 'Appearance'
},
support: {
id: 'sidepanel_title_acc_support',
description: 'Sidepanel title for AccSupportView.',
@@ -200,25 +206,35 @@ class SidepanelView extends React.PureComponent {
onUpdateTagsRequest={this.props.onUpdateAccountTags}
onError={this.props.onError} /> :
view === 'appearance' ?
<AccAppearanceView
colorSchema={this.props.colorSchema}
textSize={this.props.textSize}
onChangeColorSchema={this.props.onChangeColorSchema}
onTextSizeChanged={this.props.onTextSizeChanged} /> :
view === 'notif' ?
<AccNotificationsView
messageSounds={this.props.messageSounds}
desktopAlerts={this.props.desktopAlerts}
desktopAlertsEnabled={this.props.desktopAlertsEnabled}
incognitoMode={this.props.incognitoMode}
onTogglePushNotifications={this.props.onTogglePushNotifications}
onToggleMessageSounds={this.props.onToggleMessageSounds}
onToggleIncognitoMode={this.props.onToggleIncognitoMode} /> :
onToggleMessageSounds={this.props.onToggleMessageSounds} /> :
view === 'security' ?
<Suspense fallback={<div className="panel-form-row"><FormattedMessage id="loading_note"
defaultMessage="Loading..." description="Message shown when component is loading"/></div>}>
<AccSecurityView
tinode={this.props.tinode}
incognitoMode={this.props.incognitoMode}
onUpdateAccountDesc={this.props.onUpdateAccountDesc}
onUpdatePassword={this.props.onUpdatePassword}
onLogout={this.props.onLogout}
onDeleteAccount={this.props.onDeleteAccount}
onShowAlert={this.props.onShowAlert}
onShowBlocked={this.props.onShowBlocked} /> :
onShowBlocked={this.props.onShowBlocked}
onToggleIncognitoMode={this.props.onToggleIncognitoMode} />
</Suspense> :
view === 'support' ?
<AccSupportView
+36 -5
View File
@@ -14,12 +14,12 @@ import ForwardDialog from '../widgets/forward-dialog.jsx';
import CallIncoming from '../widgets/call-incoming.jsx';
const PhoneCountrySelector = React.lazy(_ => import('../widgets/phone-country-selector.jsx'));
import InfoView from './info-view.jsx';
const InfoView = React.lazy(_ => import('./info-view.jsx'));
import MessagesView from './messages-view.jsx';
import SidepanelView from './sidepanel-view.jsx';
import { API_KEY, APP_NAME, DEFAULT_P2P_ACCESS_MODE, FORWARDED_PREVIEW_LENGTH, LOGGING_ENABLED,
MEDIA_BREAKPOINT, WAKE_UP_TICK, WAKE_UP_TIMEOUT } from '../config.js';
import { API_KEY, APP_NAME, DEFAULT_COLOR_SCHEME, DEFAULT_P2P_ACCESS_MODE, DEFAULT_TEXT_SIZE, FORWARDED_PREVIEW_LENGTH,
LOGGING_ENABLED, MEDIA_BREAKPOINT, WAKE_UP_TICK, WAKE_UP_TIMEOUT } from '../config.js';
import { CALL_STATE_NONE, CALL_STATE_OUTGOING_INITATED,
CALL_STATE_INCOMING_RECEIVED, CALL_STATE_IN_PROGRESS,
CALL_HEAD_STARTED } from '../constants.js';
@@ -134,6 +134,8 @@ class TinodeWeb extends React.Component {
this.handleUpdatePasswordRequest = this.handleUpdatePasswordRequest.bind(this);
this.handleUpdateAccountTagsRequest = this.handleUpdateAccountTagsRequest.bind(this);
this.handleToggleIncognitoMode = this.handleToggleIncognitoMode.bind(this);
this.handleChangeColorSchema = this.handleChangeColorSchema.bind(this);
this.handleChangeTextSize = this.handleChangeTextSize.bind(this);
this.handleSettings = this.handleSettings.bind(this);
this.handleGlobalSettings = this.handleGlobalSettings.bind(this);
this.handleShowArchive = this.handleShowArchive.bind(this);
@@ -213,6 +215,8 @@ class TinodeWeb extends React.Component {
// "On" is the default, so saving the "off" state.
messageSounds: !settings.messageSoundsOff,
incognitoMode: false,
colorSchema: settings.colorSchema || DEFAULT_COLOR_SCHEME, // default: system default
textSize: settings.textSize || DEFAULT_TEXT_SIZE, // default size: 10pt
// Persistent request to enable alerts.
desktopAlerts: persist && !!settings.desktopAlerts,
// Enable / disable checkbox.
@@ -293,6 +297,9 @@ class TinodeWeb extends React.Component {
window.addEventListener('offline', this.handleOnlineOff);
window.addEventListener('hashchange', this.handleHashRoute);
document.documentElement.style.colorScheme = this.state.colorSchema;
document.documentElement.style.setProperty('--message-text-size', `${this.state.textSize}pt`);
// Process background notifications from the service worker.
if (typeof BroadcastChannel == 'function') {
const serviceWorkerChannel = new BroadcastChannel('tinode-sw');
@@ -517,7 +524,7 @@ class TinodeWeb extends React.Component {
if (hash.path && hash.path.length > 0) {
// Left-side panel selector.
if (['register','settings','edit','notif','security','support','general','crop',
if (['register','settings','edit','notif','appearance','security','support','general','crop',
'cred','reset','newtpk','archive','blocked','contacts',''].includes(hash.path[0])) {
newState.sidePanelSelected = hash.path[0];
} else {
@@ -1204,6 +1211,18 @@ class TinodeWeb extends React.Component {
});
}
handleChangeColorSchema(scheme) {
this.setState({colorSchema: scheme});
LocalStorageUtil.updateObject('settings', {colorSchema: scheme});
document.documentElement.style.colorScheme = scheme;
}
handleChangeTextSize(size) {
this.setState({textSize: size | 0});
LocalStorageUtil.updateObject('settings', {textSize: size});
document.documentElement.style.setProperty('--message-text-size', `${size}pt`);
}
handleUpdateAccountTagsRequest(_, tags) {
this.tinode.getMeTopic().setMeta({tags: tags})
.catch(err => this.handleError(err.message, 'err'));
@@ -1316,7 +1335,7 @@ class TinodeWeb extends React.Component {
handleSidepanelCancel() {
const parsed = HashNavigation.parseUrlHash(window.location.hash);
let path = '';
if (['security','support','general','notif'].includes(parsed.path[0])) {
if (['security','support','general','notif','appearance'].includes(parsed.path[0])) {
path = 'edit';
} else if ('crop' == parsed.path[0]) {
path = 'general';
@@ -1470,6 +1489,10 @@ class TinodeWeb extends React.Component {
firebaseDelToken(this.fcm);
}
// Reset color scheme.
document.documentElement.style.colorScheme = DEFAULT_COLOR_SCHEME;
document.documentElement.style.setProperty('--message-text-size', `${DEFAULT_TEXT_SIZE}pt`);
clearInterval(this.reconnectCountdown);
let cleared;
@@ -2059,6 +2082,8 @@ class TinodeWeb extends React.Component {
secureConnection={this.state.secureConnection}
serverVersion={this.state.serverVersion}
reqCredMethod={this.state.reqCredMethod}
textSize={this.state.textSize}
colorSchema={this.state.colorSchema}
onGlobalSettings={this.handleGlobalSettings}
onSignUp={this.handleNewAccount}
@@ -2073,6 +2098,8 @@ class TinodeWeb extends React.Component {
onTogglePushNotifications={this.toggleFCMToken}
onToggleMessageSounds={this.handleToggleMessageSounds}
onToggleIncognitoMode={this.handleToggleIncognitoMode}
onChangeColorSchema={this.handleChangeColorSchema}
onTextSizeChanged={this.handleChangeTextSize}
onCredAdd={this.handleCredAdd}
onCredDelete={this.handleCredDelete}
onCredConfirm={this.handleCredConfirm}
@@ -2114,6 +2141,7 @@ class TinodeWeb extends React.Component {
serverVersion={this.state.serverVersion}
serverAddress={this.state.serverAddress}
applicationVisible={this.state.applicationVisible}
colorSchema={this.state.colorSchema}
forwardMessage={this.state.forwardMessage}
onCancelForwardMessage={this.handleHideForwardDialog}
@@ -2148,6 +2176,8 @@ class TinodeWeb extends React.Component {
: null}
{this.state.infoPanel ?
<Suspense fallback={<div><FormattedMessage id="loading_note" defaultMessage="Loading..."
description="Message shown when component is loading"/></div>}>
<InfoView
tinode={this.tinode}
connected={this.state.connected}
@@ -2180,6 +2210,7 @@ class TinodeWeb extends React.Component {
showContextMenu={this.handleShowContextMenu}
/>
</Suspense>
:
null
}
+2 -2
View File
@@ -162,8 +162,8 @@ class BaseChatMessage extends React.PureComponent {
const attachments = [];
if (this.props.mimeType == Drafty.getContentType() && Drafty.isValid(content)) {
Drafty.attachments(content, (att, i) => {
if (att.mime == 'application/json') {
// Don't show json objects as attachments.
if (Drafty.isFormResponseType(att.mime)) {
// Don't show json drafty form response objects as attachments.
// They are not meant for users.
return;
}
+1 -1
View File
@@ -51,7 +51,7 @@ export default class DocPreview 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">
<div className="flex-column narrow">
<div className="doc-card">
<i className="material-icons gray">{iconFromMime(this.props.content.type)}</i>
<div><b><FormattedMessage id="label_file_name" defaultMessage="File name:"
description="Label for a file name" /></b> {shortenFileName(this.props.content.name, 24) || '-'}</div>
+16 -3
View File
@@ -15,6 +15,7 @@ export default class TagManager extends React.Component {
this.state = {
tags: this.props.tags || [],
alias: Tinode.tagByPrefix(this.props.tags, Tinode.TAG_ALIAS) || '',
tagInput: '',
activated: this.props.activated
};
@@ -29,7 +30,10 @@ export default class TagManager extends React.Component {
static getDerivedStateFromProps(nextProps, prevState) {
const tags = nextProps.tags || [];
if (!arrayEqual(tags, prevState.tags) && !prevState.activated) {
return {tags: tags};
return {
tags: tags || [],
alias: Tinode.tagByPrefix(tags, Tinode.TAG_ALIAS) || '',
};
}
return null;
}
@@ -76,8 +80,12 @@ export default class TagManager extends React.Component {
}
handleSubmit() {
// Add unprocessed input to tags, submit the list.
this.props.onSubmit(this.handleAddTag(this.state.tagInput.trim()));
// Add unprocessed input to tags.
let tags = this.handleAddTag(this.state.tagInput.trim());
// Add back the alias (and overwrite one possibly added by hand).
tags = Tinode.setUniqueTag(tags, this.state.alias);
// Submit the updated list.
this.props.onSubmit(tags);
this.setState({activated: false, tags: this.props.tags || []});
}
@@ -95,11 +103,16 @@ export default class TagManager extends React.Component {
let tags = [];
if (this.state.activated) {
this.state.tags.forEach(tag => {
if (tag != this.state.alias) {
tags.push({user: tag, invalid: (tag.length < minTagLength || tag.length > maxTagLength)});
}
});
} else {
this.state.tags.forEach(tag => {
if (tag != this.state.alias) {
tags.push(<span className="badge" key={tags.length}>{tag}</span>);
}
});
if (tags.length == 0) {
tags = (
+29 -22
View File
@@ -38,7 +38,6 @@ const messages = defineMessages({
}
});
const ALIAS_REGEX = /^[a-z0-9_\-]{4,24}$/;
const ALIAS_AVAILABILITY_CHECK_DELAY = 1000; // milliseconds
class TopicDescEdit extends React.Component {
@@ -79,6 +78,7 @@ class TopicDescEdit extends React.Component {
this.handleDescriptionUpdate = this.handleDescriptionUpdate.bind(this);
this.handleTagsUpdated = this.handleTagsUpdated.bind(this);
this.validateAlias = this.validateAlias.bind(this);
this.cancelValidator = this.cancelValidator.bind(this);
}
componentDidMount() {
@@ -90,6 +90,7 @@ class TopicDescEdit extends React.Component {
componentWillUnmount() {
const topic = this.props.tinode.getTopic(this.props.topic);
topic.onTagsUpdated = this.previousOnTags;
this.cancelValidator();
}
tnNewTags(tags) {
@@ -177,28 +178,24 @@ class TopicDescEdit extends React.Component {
}
handleAliasUpdate(alias) {
alias = alias.trim() || '';
const tags = this.state.tags.filter(t => !t.startsWith('alias:'));
if (alias) {
tags.push('alias:' + alias.toLowerCase());
}
alias = (alias || '').trim();
const tags = alias ?
Tinode.setUniqueTag(this.state.tags, Tinode.TAG_ALIAS + alias.toLowerCase()) :
Tinode.clearTagPrefix(this.state.tags, Tinode.TAG_ALIAS);
this.handleTagsUpdated(tags);
}
validateAlias(alias) {
// Reset pending delayed check.
if (this.aliasCheckTimer) {
this.aliasCheckPromise.reject(null);
clearTimeout(this.aliasCheckTimer);
this.aliasCheckTimer = null;
}
this.cancelValidator();
alias = alias.trim();
if (!alias) {
this.setState({aliasError: ''});
return true;
}
const valid = ALIAS_REGEX.test(alias);
const valid = Tinode.isValidTagValue(alias);
if (!valid) {
this.setState({aliasError: this.props.intl.formatMessage(messages.alias_invalid)});
return false;
@@ -206,6 +203,12 @@ class TopicDescEdit extends React.Component {
this.setState({aliasError: ''});
// Setup a delayed check for alias availability.
const fnd = this.props.tinode.getFndTopic();
if (!fnd) {
// Unable to check alias availability.
this.setState({aliasError: ''});
return true;
}
this.aliasCheckPromise = {};
const validationPromise = new Promise((resolve, reject) => {
this.aliasCheckPromise.resolve = resolve;
@@ -213,15 +216,10 @@ class TopicDescEdit extends React.Component {
});
this.aliasCheckTimer = setTimeout(_ => {
this.aliasCheckTimer = null;
const fnd = this.props.tinode.getFndTopic();
fnd.subscribe()
.then(_ => fnd.setMeta({desc: {public: `alias:${alias}`}}))
.then(_ => fnd.getMeta(fnd.startMetaQuery().withTags().build()))
.then(meta => {
const taken = Array.isArray(meta.tags);
this.setState({aliasError: taken ?
this.props.intl.formatMessage(messages.alias_already_taken) : ''});
this.aliasCheckPromise.resolve(!taken)
fnd.checkTagUniqueness(`${Tinode.TAG_ALIAS}${alias}`, this.props.topic)
.then(ok => {
this.aliasCheckPromise.resolve(ok)
this.setState({aliasError: ok ? '' : this.props.intl.formatMessage(messages.alias_already_taken)});
})
.catch(err => {
this.aliasCheckPromise.reject(err);
@@ -231,6 +229,15 @@ class TopicDescEdit extends React.Component {
return validationPromise;
}
cancelValidator() {
// Reset pending delayed check.
if (this.aliasCheckTimer) {
clearTimeout(this.aliasCheckTimer);
this.aliasCheckTimer = null;
this.aliasCheckPromise.reject(null);
}
}
handleTagsUpdated(tags) {
// Check if tags have actually changed.
if (arrayEqual(this.state.tags.slice(0), tags.slice(0))) {
@@ -373,7 +380,7 @@ class TopicDescEdit extends React.Component {
: null
}
</div>
{editable ?
{editable && !this.state.isMe ?
<>
<div className="hr" />
<FormattedMessage id="title_tag_manager" defaultMessage="Tags (search & discovery)"
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1 +1 @@
{"version":3,"file":"316.prod.js","mappings":"qkNASA,MAAMA,UAA6BC,IAAAA,cACjCC,WAAAA,CAAYC,GACVC,MAAMD,GAENE,KAAKC,UAAY,GACjB,MAAM,kBAAEC,GAAsBJ,EAAMK,KACpCC,EAAeC,SAAQC,IACPA,EAAGC,KAAKC,MAAM,KACtBH,SAAQI,IACZT,KAAKC,UAAUS,KAAK,CAClBH,KAAME,EAAKE,OACXC,KAAMN,EAAGM,KACTC,MAAMC,EAAAA,EAAAA,IAAUR,EAAGM,MACnBG,KAAMb,EAAkBI,EAAGM,KAAM,CAACI,KAAM,YACxC,GACF,IAGJhB,KAAKC,UAAUgB,MAAK,CAACC,EAAGC,IAAMD,EAAEH,KAAKK,cAAcD,EAAEJ,OACvD,CAEAM,iBAAAA,GACMrB,KAAKsB,aACPtB,KAAKsB,YAAYC,eAAe,CAACC,MAAO,SAAUC,OAAQ,WAE9D,CAEAC,MAAAA,GACE,MAAMzB,EAAY,GACZ0B,EAAW3B,KAAKF,MAAM6B,UAAY,KAYxC,OAXA3B,KAAKC,UAAUI,SAAQ,CAACuB,EAAGC,KACzB,MAAMC,EAASF,EAAEhB,MAAQe,EAAW,YAAc,GAClD1B,EAAUS,KAAKd,IAAAA,cAAA,MAAImC,UAAWD,EAAOE,IAAKH,EACxCI,IAAKA,IAAYL,EAAEhB,MAAQe,IAAW3B,KAAKsB,YAAcW,EAAG,EAC5DC,QAASC,GAAKnC,KAAKF,MAAMsC,SAASR,EAAEhB,KAAMgB,EAAErB,OAC5CX,IAAAA,cAAA,QAAMmC,UAAU,gBAAgBH,EAAEf,MAClCjB,IAAAA,cAAA,QAAMmC,UAAU,WAAU,IAAOH,EAAEb,MACnCnB,IAAAA,cAAA,QAAMmC,UAAU,aAAY,KAAQH,EAAErB,OAClC,IAINX,IAAAA,cAAA,OAAKmC,UAAU,mBAAmBD,MAAO,CAACO,OAAQ,UAChDzC,IAAAA,cAAA,MAAImC,UAAU,0BACX9B,GAIT,EAGF,WAAeqC,EAAAA,EAAAA,YAAW3C,E","sources":["webpack://tinode-webapp/./src/widgets/phone-country-selector.jsx"],"sourcesContent":["// Selector for country dialing code.\n\nimport React from 'react';\nimport { injectIntl } from 'react-intl';\n\nimport * as dcodes from '../dcodes.json';\n\nimport { flagEmoji } from '../lib/strformat';\n\nclass PhoneCountrySelector extends React.PureComponent {\n constructor(props) {\n super(props);\n\n this.countries = [];\n const { formatDisplayName } = props.intl;\n dcodes.default.forEach(dc => {\n const parts = dc.dial.split(',');\n parts.forEach(part => {\n this.countries.push({\n dial: part.trim(),\n code: dc.code,\n flag: flagEmoji(dc.code),\n name: formatDisplayName(dc.code, {type: 'region'})\n });\n });\n });\n\n this.countries.sort((a, b) => a.name.localeCompare(b.name));\n }\n\n componentDidMount() {\n if (this.selectedRef) {\n this.selectedRef.scrollIntoView({block: 'center', inline: 'nearest'});\n }\n }\n\n render() {\n const countries = [];\n const selected = this.props.selected || 'US';\n this.countries.forEach((c, idx) => {\n const style = (c.code == selected ? 'selected ' : '');\n countries.push(<li className={style} key={idx}\n ref={ref => {if (c.code == selected) {this.selectedRef = ref}}}\n onClick={_ => this.props.onSubmit(c.code, c.dial)}>\n <span className=\"country-flag\">{c.flag}</span>\n <span className=\"country\">&nbsp;{c.name}</span>\n <span className=\"dial-code\">&nbsp;+{c.dial}</span>\n </li>);\n });\n\n return (\n <div className=\"scrollable-panel\" style={{height: '30rem'}}>\n <ul className=\"phone-country-selector\">\n {countries}\n </ul>\n </div>\n );\n }\n}\n\nexport default injectIntl(PhoneCountrySelector);\n"],"names":["PhoneCountrySelector","React","constructor","props","super","this","countries","formatDisplayName","intl","dcodes","forEach","dc","dial","split","part","push","trim","code","flag","flagEmoji","name","type","sort","a","b","localeCompare","componentDidMount","selectedRef","scrollIntoView","block","inline","render","selected","c","idx","style","className","key","ref","onClick","_","onSubmit","height","injectIntl"],"sourceRoot":""}
{"version":3,"file":"316.prod.js","mappings":"qkNASA,MAAMA,UAA6BC,IAAAA,cACjCC,WAAAA,CAAYC,GACVC,MAAMD,GAENE,KAAKC,UAAY,GACjB,MAAM,kBAAEC,GAAsBJ,EAAMK,KACpCC,EAAeC,QAAQC,IACPA,EAAGC,KAAKC,MAAM,KACtBH,QAAQI,IACZT,KAAKC,UAAUS,KAAK,CAClBH,KAAME,EAAKE,OACXC,KAAMN,EAAGM,KACTC,MAAMC,EAAAA,EAAAA,IAAUR,EAAGM,MACnBG,KAAMb,EAAkBI,EAAGM,KAAM,CAACI,KAAM,iBAK9ChB,KAAKC,UAAUgB,KAAK,CAACC,EAAGC,IAAMD,EAAEH,KAAKK,cAAcD,EAAEJ,MACvD,CAEAM,iBAAAA,GACMrB,KAAKsB,aACPtB,KAAKsB,YAAYC,eAAe,CAACC,MAAO,SAAUC,OAAQ,WAE9D,CAEAC,MAAAA,GACE,MAAMzB,EAAY,GACZ0B,EAAW3B,KAAKF,MAAM6B,UAAY,KAYxC,OAXA3B,KAAKC,UAAUI,QAAQ,CAACuB,EAAGC,KACzB,MAAMC,EAASF,EAAEhB,MAAQe,EAAW,YAAc,GAClD1B,EAAUS,KAAKd,IAAAA,cAAA,MAAImC,UAAWD,EAAOE,IAAKH,EACxCI,IAAKA,IAAYL,EAAEhB,MAAQe,IAAW3B,KAAKsB,YAAcW,IACzDC,QAASC,GAAKnC,KAAKF,MAAMsC,SAASR,EAAEhB,KAAMgB,EAAErB,OAC5CX,IAAAA,cAAA,QAAMmC,UAAU,gBAAgBH,EAAEf,MAClCjB,IAAAA,cAAA,QAAMmC,UAAU,WAAU,IAAOH,EAAEb,MACnCnB,IAAAA,cAAA,QAAMmC,UAAU,aAAY,KAAQH,EAAErB,UAKxCX,IAAAA,cAAA,OAAKmC,UAAU,mBAAmBD,MAAO,CAACO,OAAQ,UAChDzC,IAAAA,cAAA,MAAImC,UAAU,0BACX9B,GAIT,EAGF,WAAeqC,EAAAA,EAAAA,YAAW3C,E","sources":["webpack://tinode-webapp/./src/widgets/phone-country-selector.jsx"],"sourcesContent":["// Selector for country dialing code.\n\nimport React from 'react';\nimport { injectIntl } from 'react-intl';\n\nimport * as dcodes from '../dcodes.json';\n\nimport { flagEmoji } from '../lib/strformat';\n\nclass PhoneCountrySelector extends React.PureComponent {\n constructor(props) {\n super(props);\n\n this.countries = [];\n const { formatDisplayName } = props.intl;\n dcodes.default.forEach(dc => {\n const parts = dc.dial.split(',');\n parts.forEach(part => {\n this.countries.push({\n dial: part.trim(),\n code: dc.code,\n flag: flagEmoji(dc.code),\n name: formatDisplayName(dc.code, {type: 'region'})\n });\n });\n });\n\n this.countries.sort((a, b) => a.name.localeCompare(b.name));\n }\n\n componentDidMount() {\n if (this.selectedRef) {\n this.selectedRef.scrollIntoView({block: 'center', inline: 'nearest'});\n }\n }\n\n render() {\n const countries = [];\n const selected = this.props.selected || 'US';\n this.countries.forEach((c, idx) => {\n const style = (c.code == selected ? 'selected ' : '');\n countries.push(<li className={style} key={idx}\n ref={ref => {if (c.code == selected) {this.selectedRef = ref}}}\n onClick={_ => this.props.onSubmit(c.code, c.dial)}>\n <span className=\"country-flag\">{c.flag}</span>\n <span className=\"country\">&nbsp;{c.name}</span>\n <span className=\"dial-code\">&nbsp;+{c.dial}</span>\n </li>);\n });\n\n return (\n <div className=\"scrollable-panel\" style={{height: '30rem'}}>\n <ul className=\"phone-country-selector\">\n {countries}\n </ul>\n </div>\n );\n }\n}\n\nexport default injectIntl(PhoneCountrySelector);\n"],"names":["PhoneCountrySelector","React","constructor","props","super","this","countries","formatDisplayName","intl","dcodes","forEach","dc","dial","split","part","push","trim","code","flag","flagEmoji","name","type","sort","a","b","localeCompare","componentDidMount","selectedRef","scrollIntoView","block","inline","render","selected","c","idx","style","className","key","ref","onClick","_","onSubmit","height","injectIntl"],"sourceRoot":""}
+2
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+2
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+634 -2037
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+510
View File
@@ -0,0 +1,510 @@
"use strict";
(self["webpackChunktinode_webapp"] = self["webpackChunktinode_webapp"] || []).push([["src_views_acc-security-view_jsx"],{
/***/ "./src/views/acc-security-view.jsx":
/*!*****************************************!*\
!*** ./src/views/acc-security-view.jsx ***!
\*****************************************/
/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
__webpack_require__.r(__webpack_exports__);
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! react */ "react");
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_0__);
/* harmony import */ var react_intl__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! react-intl */ "react-intl");
/* harmony import */ var react_intl__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(react_intl__WEBPACK_IMPORTED_MODULE_1__);
/* harmony import */ var _widgets_checkbox_jsx__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ../widgets/checkbox.jsx */ "./src/widgets/checkbox.jsx");
/* harmony import */ var _widgets_in_place_edit_jsx__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ../widgets/in-place-edit.jsx */ "./src/widgets/in-place-edit.jsx");
/* harmony import */ var _widgets_permissions_editor_jsx__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ../widgets/permissions-editor.jsx */ "./src/widgets/permissions-editor.jsx");
const messages = (0,react_intl__WEBPACK_IMPORTED_MODULE_1__.defineMessages)({
delete_account: {
id: "delete_account",
defaultMessage: [{
"type": 0,
"value": "Delete account"
}]
},
delete_account_warning: {
id: "delete_account_warning",
defaultMessage: [{
"type": 0,
"value": "Are you sure you want to delete your account? It cannot be undone."
}]
}
});
class AccSecurityView extends (react__WEBPACK_IMPORTED_MODULE_0___default().Component) {
constructor(props) {
super(props);
const me = this.props.tinode.getMeTopic();
let blockedCount = 0;
me.contacts(c => {
if (c.acs && !c.acs.isJoiner()) {
blockedCount++;
}
});
const defacs = me.getDefaultAccess();
this.state = {
auth: defacs ? defacs.auth : null,
anon: defacs ? defacs.anon : null,
showPermissionEditorFor: undefined,
blockedCount: blockedCount
};
this.handlePasswordUpdate = this.handlePasswordUpdate.bind(this);
this.handleLaunchPermissionsEditor = this.handleLaunchPermissionsEditor.bind(this);
this.handleHidePermissionsEditor = this.handleHidePermissionsEditor.bind(this);
this.handlePermissionsChanged = this.handlePermissionsChanged.bind(this);
this.handleDeleteAccount = this.handleDeleteAccount.bind(this);
this.handleCheckboxClick = this.handleCheckboxClick.bind(this);
}
handlePasswordUpdate(pwd) {
this.setState({
password: pwd
});
this.props.onUpdatePassword(pwd);
}
handleLaunchPermissionsEditor(which) {
this.setState({
showPermissionEditorFor: which,
editedPermissions: this.state[which]
});
}
handleHidePermissionsEditor() {
this.setState({
showPermissionEditorFor: undefined
});
}
handlePermissionsChanged(perm) {
let defacs = {};
defacs[this.state.showPermissionEditorFor] = perm;
this.props.onUpdateAccountDesc('me', undefined, undefined, defacs);
let newState = {
showPermissionEditorFor: undefined
};
newState[this.state.showPermissionEditorFor] = perm;
this.setState(newState);
}
handleCheckboxClick(what, checked) {
this.props.onToggleIncognitoMode(checked);
}
handleDeleteAccount(e) {
e.preventDefault();
const {
formatMessage
} = this.props.intl;
this.props.onShowAlert(formatMessage(messages.delete_account), formatMessage(messages.delete_account_warning), _ => this.props.onDeleteAccount(), null, true, null);
}
render() {
return react__WEBPACK_IMPORTED_MODULE_0___default().createElement((react__WEBPACK_IMPORTED_MODULE_0___default().Fragment), null, this.state.showPermissionEditorFor ? react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_widgets_permissions_editor_jsx__WEBPACK_IMPORTED_MODULE_4__["default"], {
mode: this.state.editedPermissions,
skip: "O",
onSubmit: this.handlePermissionsChanged,
onCancel: this.handleHidePermissionsEditor
}) : react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {
className: "scrollable-panel"
}, react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {
className: "panel-form-column"
}, react__WEBPACK_IMPORTED_MODULE_0___default().createElement("label", {
className: "small"
}, react__WEBPACK_IMPORTED_MODULE_0___default().createElement(react_intl__WEBPACK_IMPORTED_MODULE_1__.FormattedMessage, {
id: "label_password",
defaultMessage: [{
"type": 0,
"value": "Password"
}]
})), react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", null, react__WEBPACK_IMPORTED_MODULE_0___default().createElement(react_intl__WEBPACK_IMPORTED_MODULE_1__.FormattedMessage, {
id: "password_unchanged_prompt",
defaultMessage: [{
"type": 0,
"value": "Unchanged"
}]
}, password_unchanged => react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_widgets_in_place_edit_jsx__WEBPACK_IMPORTED_MODULE_3__["default"], {
placeholder: password_unchanged,
type: "password",
onFinished: this.handlePasswordUpdate
})))), react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {
className: "hr"
}), react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {
className: "panel-form-row"
}, react__WEBPACK_IMPORTED_MODULE_0___default().createElement("label", {
htmlFor: "incognito-mode"
}, react__WEBPACK_IMPORTED_MODULE_0___default().createElement(react_intl__WEBPACK_IMPORTED_MODULE_1__.FormattedMessage, {
id: "label_incognito_mode",
defaultMessage: [{
"type": 0,
"value": "Incognito mode:"
}]
})), react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_widgets_checkbox_jsx__WEBPACK_IMPORTED_MODULE_2__["default"], {
name: "incognito",
id: "incognito-mode",
checked: this.props.incognitoMode,
onChange: this.handleCheckboxClick
})), react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {
className: "panel-form-row"
}, react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", {
className: "small"
}, react__WEBPACK_IMPORTED_MODULE_0___default().createElement(react_intl__WEBPACK_IMPORTED_MODULE_1__.FormattedMessage, {
id: "incognito_mode_description",
defaultMessage: [{
"type": 0,
"value": "Hide your online status and typing indicators."
}]
}))), react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {
className: "hr"
}), react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {
className: "panel-form-column"
}, react__WEBPACK_IMPORTED_MODULE_0___default().createElement("a", {
href: "#",
className: "danger flat-button",
onClick: e => {
e.preventDefault();
this.props.onLogout();
}
}, react__WEBPACK_IMPORTED_MODULE_0___default().createElement("i", {
className: "material-icons"
}, "exit_to_app"), " \xA0", react__WEBPACK_IMPORTED_MODULE_0___default().createElement(react_intl__WEBPACK_IMPORTED_MODULE_1__.FormattedMessage, {
id: "button_logout",
defaultMessage: [{
"type": 0,
"value": "Logout"
}]
})), react__WEBPACK_IMPORTED_MODULE_0___default().createElement("a", {
href: "#",
className: "danger flat-button",
onClick: e => {
this.handleDeleteAccount(e);
}
}, react__WEBPACK_IMPORTED_MODULE_0___default().createElement("i", {
className: "material-icons"
}, "delete"), " \xA0", react__WEBPACK_IMPORTED_MODULE_0___default().createElement(react_intl__WEBPACK_IMPORTED_MODULE_1__.FormattedMessage, {
id: "button_delete_account",
defaultMessage: [{
"type": 0,
"value": "Delete account"
}]
}))), react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {
className: "hr"
}), react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {
className: "panel-form-column"
}, react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", null, react__WEBPACK_IMPORTED_MODULE_0___default().createElement("label", {
className: "small"
}, react__WEBPACK_IMPORTED_MODULE_0___default().createElement(react_intl__WEBPACK_IMPORTED_MODULE_1__.FormattedMessage, {
id: "label_default_access_mode",
defaultMessage: [{
"type": 0,
"value": "Default access mode:"
}]
}))), react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {
className: "quoted"
}, react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", null, "Auth: ", react__WEBPACK_IMPORTED_MODULE_0___default().createElement("tt", {
className: "clickable",
onClick: this.handleLaunchPermissionsEditor.bind(this, 'auth')
}, this.state.auth)), react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", null, "Anon: ", react__WEBPACK_IMPORTED_MODULE_0___default().createElement("tt", {
className: "clickable",
onClick: this.handleLaunchPermissionsEditor.bind(this, 'anon')
}, this.state.anon)))), this.state.blockedCount > 0 ? react__WEBPACK_IMPORTED_MODULE_0___default().createElement((react__WEBPACK_IMPORTED_MODULE_0___default().Fragment), null, react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {
className: "hr"
}), react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {
className: "panel-form-row"
}, react__WEBPACK_IMPORTED_MODULE_0___default().createElement("i", {
className: "material-icons"
}, "block"), "\xA0", react__WEBPACK_IMPORTED_MODULE_0___default().createElement("a", {
href: "#",
className: "gray",
onClick: e => {
e.preventDefault();
this.props.onShowBlocked();
}
}, react__WEBPACK_IMPORTED_MODULE_0___default().createElement(react_intl__WEBPACK_IMPORTED_MODULE_1__.FormattedMessage, {
id: "blocked_contacts_link",
defaultMessage: [{
"type": 0,
"value": "Blocked contacts ("
}, {
"type": 1,
"value": "count"
}, {
"type": 0,
"value": ")"
}],
values: {
count: this.state.blockedCount
}
})))) : null));
}
}
;
/* harmony default export */ __webpack_exports__["default"] = ((0,react_intl__WEBPACK_IMPORTED_MODULE_1__.injectIntl)(AccSecurityView));
/***/ }),
/***/ "./src/widgets/permissions-editor.jsx":
/*!********************************************!*\
!*** ./src/widgets/permissions-editor.jsx ***!
\********************************************/
/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
__webpack_require__.r(__webpack_exports__);
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! react */ "react");
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_0__);
/* harmony import */ var react_intl__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! react-intl */ "react-intl");
/* harmony import */ var react_intl__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(react_intl__WEBPACK_IMPORTED_MODULE_1__);
/* harmony import */ var _checkbox_jsx__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./checkbox.jsx */ "./src/widgets/checkbox.jsx");
/* harmony import */ var _contact_jsx__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./contact.jsx */ "./src/widgets/contact.jsx");
/* harmony import */ var _lib_blob_helpers_js__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ../lib/blob-helpers.js */ "./src/lib/blob-helpers.js");
const messages = (0,react_intl__WEBPACK_IMPORTED_MODULE_1__.defineMessages)({
joiner: {
id: "permission_join",
defaultMessage: [{
"type": 0,
"value": "Join ("
}, {
"type": 1,
"value": "val"
}, {
"type": 0,
"value": ")"
}]
},
reader: {
id: "permission_read",
defaultMessage: [{
"type": 0,
"value": "Read ("
}, {
"type": 1,
"value": "val"
}, {
"type": 0,
"value": ")"
}]
},
writer: {
id: "permission_write",
defaultMessage: [{
"type": 0,
"value": "Write ("
}, {
"type": 1,
"value": "val"
}, {
"type": 0,
"value": ")"
}]
},
preser: {
id: "permission_pres",
defaultMessage: [{
"type": 0,
"value": "Get notified ("
}, {
"type": 1,
"value": "val"
}, {
"type": 0,
"value": ")"
}]
},
approver: {
id: "permission_admin",
defaultMessage: [{
"type": 0,
"value": "Approve ("
}, {
"type": 1,
"value": "val"
}, {
"type": 0,
"value": ")"
}]
},
sharer: {
id: "permission_share",
defaultMessage: [{
"type": 0,
"value": "Share ("
}, {
"type": 1,
"value": "val"
}, {
"type": 0,
"value": ")"
}]
},
deleter: {
id: "permission_delete",
defaultMessage: [{
"type": 0,
"value": "Delete ("
}, {
"type": 1,
"value": "val"
}, {
"type": 0,
"value": ")"
}]
},
owner: {
id: "permission_owner",
defaultMessage: [{
"type": 0,
"value": "Owner ("
}, {
"type": 1,
"value": "val"
}, {
"type": 0,
"value": ")"
}]
}
});
class PermissionsEditor extends (react__WEBPACK_IMPORTED_MODULE_0___default().Component) {
constructor(props) {
super(props);
this.state = {
mode: (props.mode || '').replace('N', '')
};
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
this.handleCancel = this.handleCancel.bind(this);
}
handleChange(val) {
let mode = this.state.mode;
const idx = mode.indexOf(val);
if (idx == -1) {
mode += val;
} else {
mode = mode.replace(val, '');
}
this.setState({
mode: mode
});
}
handleSubmit() {
const mode = (this.state.mode || 'N').split('').sort().join('');
const before = (this.props.mode || 'N').split('').sort().join('');
if (mode !== before) {
this.props.onSubmit(mode);
} else {
this.props.onCancel();
}
}
handleCancel() {
this.props.onCancel();
}
render() {
const {
formatMessage
} = this.props.intl;
const all = 'JRWPASDO';
const names = {
'J': formatMessage(messages.joiner, {
val: 'J'
}),
'R': formatMessage(messages.reader, {
val: 'R'
}),
'W': formatMessage(messages.writer, {
val: 'W'
}),
'P': formatMessage(messages.preser, {
val: 'P'
}),
'A': formatMessage(messages.approver, {
val: 'A'
}),
'S': formatMessage(messages.sharer, {
val: 'S'
}),
'D': formatMessage(messages.deleter, {
val: 'D'
}),
'O': formatMessage(messages.owner, {
val: 'O'
})
};
let skip = this.props.skip || '';
let mode = this.state.mode;
let compare = (this.props.compare || '').replace('N', '');
let items = [];
for (let i = 0; i < all.length; i++) {
let c = all.charAt(i);
if (skip.indexOf(c) >= 0 && mode.indexOf(c) < 0) {
continue;
}
items.push(react__WEBPACK_IMPORTED_MODULE_0___default().createElement("tr", {
key: c
}, react__WEBPACK_IMPORTED_MODULE_0___default().createElement("td", null, names[c]), react__WEBPACK_IMPORTED_MODULE_0___default().createElement("td", {
className: "checkbox"
}, skip.indexOf(c) < 0 ? react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_checkbox_jsx__WEBPACK_IMPORTED_MODULE_2__["default"], {
name: c,
checked: mode.indexOf(c) >= 0,
onChange: this.handleChange
}) : react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_checkbox_jsx__WEBPACK_IMPORTED_MODULE_2__["default"], {
name: c,
checked: mode.indexOf(c) >= 0
})), this.props.compare ? react__WEBPACK_IMPORTED_MODULE_0___default().createElement("td", {
className: "checkbox"
}, react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_checkbox_jsx__WEBPACK_IMPORTED_MODULE_2__["default"], {
name: c,
checked: compare.indexOf(c) >= 0
})) : null));
}
return react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {
className: "panel-form-column"
}, this.props.userTitle ? react__WEBPACK_IMPORTED_MODULE_0___default().createElement("ul", {
className: "contact-box small"
}, react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_contact_jsx__WEBPACK_IMPORTED_MODULE_3__["default"], {
tinode: this.props.tinode,
item: this.props.item,
title: this.props.userTitle,
small: true,
avatar: (0,_lib_blob_helpers_js__WEBPACK_IMPORTED_MODULE_4__.makeImageUrl)(this.props.userAvatar ? this.props.userAvatar : null)
})) : null, react__WEBPACK_IMPORTED_MODULE_0___default().createElement("label", {
className: "small"
}, react__WEBPACK_IMPORTED_MODULE_0___default().createElement(react_intl__WEBPACK_IMPORTED_MODULE_1__.FormattedMessage, {
id: "title_permissions",
defaultMessage: [{
"type": 0,
"value": "Permissions"
}]
})), react__WEBPACK_IMPORTED_MODULE_0___default().createElement("table", {
className: "permission-editor"
}, this.props.compare ? react__WEBPACK_IMPORTED_MODULE_0___default().createElement("thead", null, react__WEBPACK_IMPORTED_MODULE_0___default().createElement("tr", null, react__WEBPACK_IMPORTED_MODULE_0___default().createElement("th", null), react__WEBPACK_IMPORTED_MODULE_0___default().createElement("th", null, this.props.modeTitle), react__WEBPACK_IMPORTED_MODULE_0___default().createElement("th", null, this.props.compareTitle))) : null, react__WEBPACK_IMPORTED_MODULE_0___default().createElement("tbody", null, items)), react__WEBPACK_IMPORTED_MODULE_0___default().createElement("br", null), react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {
className: "dialog-buttons"
}, react__WEBPACK_IMPORTED_MODULE_0___default().createElement("button", {
className: "outline",
onClick: this.handleCancel
}, react__WEBPACK_IMPORTED_MODULE_0___default().createElement(react_intl__WEBPACK_IMPORTED_MODULE_1__.FormattedMessage, {
id: "button_cancel",
defaultMessage: [{
"type": 0,
"value": "Cancel"
}]
})), react__WEBPACK_IMPORTED_MODULE_0___default().createElement("button", {
className: "primary",
onClick: this.handleSubmit
}, react__WEBPACK_IMPORTED_MODULE_0___default().createElement(react_intl__WEBPACK_IMPORTED_MODULE_1__.FormattedMessage, {
id: "button_ok",
defaultMessage: [{
"type": 0,
"value": "OK"
}]
}))));
}
}
;
/* harmony default export */ __webpack_exports__["default"] = ((0,react_intl__WEBPACK_IMPORTED_MODULE_1__.injectIntl)(PermissionsEditor));
/***/ })
}]);
//# sourceMappingURL=src_views_acc-security-view_jsx.dev.js.map
File diff suppressed because one or more lines are too long

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