Compare commits
46
Commits
e7840b05a6
...
1efc9a90cb
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1efc9a90cb | ||
|
|
de04006afc | ||
|
|
d484bff407 | ||
|
|
7cd5f786b2 | ||
|
|
65c94c9f12 | ||
|
|
c3e5b1cabc | ||
|
|
ad97c5638f | ||
|
|
c9d05642e2 | ||
|
|
3410128d6f | ||
|
|
b7a8d4d09d | ||
|
|
919047cfde | ||
|
|
fbaf98c117 | ||
|
|
5d96331ef5 | ||
|
|
587d457712 | ||
|
|
979b283928 | ||
|
|
7c70dea398 | ||
|
|
3f04d0e23b | ||
|
|
d953f6ac3e | ||
|
|
e5e78755ae | ||
|
|
52c1016b45 | ||
|
|
c0a3ba6a92 | ||
|
|
6be7b87173 | ||
|
|
35a63ff5ab | ||
|
|
375750a6ff | ||
|
|
ea8ac3c293 | ||
|
|
5507ccd728 | ||
|
|
b96c8c2516 | ||
|
|
e04240870d | ||
|
|
506cd00b27 | ||
|
|
cdd40facec | ||
|
|
6222610836 | ||
|
|
16b5bdf37d | ||
|
|
6e39fe9bef | ||
|
|
78c2f1aaa1 | ||
|
|
947b166935 | ||
|
|
337afcf075 | ||
|
|
77e67bb714 | ||
|
|
dbcac419de | ||
|
|
475e5eb3e7 | ||
|
|
12f05e8570 | ||
|
|
527a01bebb | ||
|
|
ffe2ab7737 | ||
|
|
2fd8346dc5 | ||
|
|
fd4cb58f0c | ||
|
|
d2647d5f0c | ||
|
|
e27c106c33 |
@@ -5,3 +5,4 @@ index.prod.js.map binary
|
||||
base.min.css binary
|
||||
tinode.dev.js binary
|
||||
tinode.prod.js binary
|
||||
package-lock.json binary
|
||||
|
||||
+26
-1
@@ -37,6 +37,11 @@ a.red:focus {
|
||||
color: #F44336;
|
||||
}
|
||||
|
||||
a.gray:hover,
|
||||
a.gray:focus {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
a.flat-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -381,10 +386,19 @@ div.alert .content {
|
||||
color: #bbb;
|
||||
}
|
||||
|
||||
.large {
|
||||
font-size: 120%;
|
||||
}
|
||||
|
||||
.small {
|
||||
font-size: 90%;
|
||||
}
|
||||
|
||||
.float-right {
|
||||
margin-left: auto;
|
||||
float: right;
|
||||
}
|
||||
|
||||
.hr {
|
||||
display: block;
|
||||
width: 100%;
|
||||
@@ -985,6 +999,10 @@ i.search {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.avatar-upload.read-only {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.avatar-upload .clear-avatar {
|
||||
position: absolute;
|
||||
font-size: 1rem;
|
||||
@@ -1148,7 +1166,7 @@ i.search {
|
||||
overflow-y: scroll;
|
||||
background-color: #eceff1;
|
||||
height: 100%;
|
||||
background-image: url("../img/ignasi_pattern_s.png");
|
||||
background-image: url("../img/bg_messages.png");
|
||||
}
|
||||
|
||||
#messages-container {
|
||||
@@ -1210,6 +1228,12 @@ i.search {
|
||||
height: 4rem;
|
||||
}
|
||||
|
||||
#send-message-panel #writing-disabled {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
#sendMessage {
|
||||
overflow: hidden;
|
||||
overflow-wrap: break-word;
|
||||
@@ -1540,6 +1564,7 @@ i.search {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
#group-manager-buttons {
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Executable
BIN
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 29 KiB |
+2
-2
@@ -34,8 +34,8 @@
|
||||
<!-- React-Intl for translations -->
|
||||
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/react-intl@3/dist/react-intl.js"></script>
|
||||
<!-- Firebase for push notifications -->
|
||||
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/firebase@7.6.1/firebase-app.js"></script>
|
||||
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/firebase@7.6.1/firebase-messaging.js"></script>
|
||||
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/firebase@7.14.0/firebase-app.js"></script>
|
||||
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/firebase@7.14.0/firebase-messaging.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
+5
-5
@@ -22,11 +22,11 @@
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,400i,500,700,700i" />
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons" />
|
||||
<link rel="stylesheet" href="css/base.min.css" />
|
||||
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/react@16.12.0/umd/react.production.min.js"></script>
|
||||
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/react-dom@16.12.0/umd/react-dom.production.min.js"></script>
|
||||
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/react-intl@3.11.0/dist/react-intl.min.js"></script>
|
||||
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/firebase@7.6.1/firebase-app.js"></script>
|
||||
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/firebase@7.6.1/firebase-messaging.js"></script>
|
||||
<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.0/firebase-app.js"></script>
|
||||
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/firebase@7.14.0/firebase-messaging.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<noscript><div id="noscript-message"><div id="noscript-title">Javascript support is required.</div>
|
||||
|
||||
Generated
+2599
-1801
File diff suppressed because it is too large
Load Diff
+18
-17
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "tinode-webapp",
|
||||
"description": "Tinode messenger for the web",
|
||||
"version": "0.16.4-rc2",
|
||||
"version": "0.16.5",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/tinode/example-react-js.git"
|
||||
@@ -31,7 +31,7 @@
|
||||
"audio/msg.mp3",
|
||||
"css/base.css",
|
||||
"css/base.min.css",
|
||||
"img/ignasi_pattern_s.png",
|
||||
"img/bg_messages.png",
|
||||
"img/logo.svg",
|
||||
"img/logo32x32.png",
|
||||
"img/logo32x32a.png",
|
||||
@@ -68,24 +68,25 @@
|
||||
"author": "Tinode Authors <info@tinode.co>",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"firebase": "^7.6.1",
|
||||
"react": "^16.12.0",
|
||||
"react-dom": "^16.12.0",
|
||||
"react-intl": "^3.11.0",
|
||||
"tinode-sdk": "^0.16.4-rc2"
|
||||
"firebase": "^7.14.0",
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.1",
|
||||
"react-intl": "^3.12.1",
|
||||
"tinode-sdk": "^0.16.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.7.7",
|
||||
"@babel/preset-env": "^7.7.7",
|
||||
"@babel/preset-react": "^7.7.4",
|
||||
"babel-loader": "^8.0.6",
|
||||
"babel-plugin-react-intl": "^5.1.16",
|
||||
"browserslist": "^4.8.3",
|
||||
"@babel/core": "^7.9.0",
|
||||
"@babel/preset-env": "^7.9.5",
|
||||
"@babel/preset-react": "^7.9.4",
|
||||
"babel-loader": "^8.1.0",
|
||||
"babel-plugin-react-intl": "^5.1.18",
|
||||
"browserslist": "^4.11.1",
|
||||
"copy-webpack-plugin": "^5.1.1",
|
||||
"cssnano": "^4.1.10",
|
||||
"postcss": "^7.0.26",
|
||||
"uglifyjs-webpack-plugin": "^2.2.0",
|
||||
"webpack": "^4.41.5",
|
||||
"webpack-cli": "^3.3.10"
|
||||
"postcss": "^7.0.27",
|
||||
"postcss-cli": "^7.1.0",
|
||||
"terser-webpack-plugin": "^2.3.5",
|
||||
"webpack": "^4.42.1",
|
||||
"webpack-cli": "^3.3.11"
|
||||
}
|
||||
}
|
||||
|
||||
+37
-11
@@ -1,19 +1,42 @@
|
||||
// Must be located at the root.
|
||||
importScripts('https://cdn.jsdelivr.net/npm/firebase@7.6.1/firebase-app.js');
|
||||
importScripts('https://cdn.jsdelivr.net/npm/firebase@7.6.1/firebase-messaging.js');
|
||||
importScripts('https://cdn.jsdelivr.net/npm/firebase@7.14.0/firebase-app.js');
|
||||
importScripts('https://cdn.jsdelivr.net/npm/firebase@7.14.0/firebase-messaging.js');
|
||||
importScripts('firebase-init.js');
|
||||
|
||||
firebase.initializeApp(FIREBASE_INIT);
|
||||
|
||||
// This method shows the notifications.
|
||||
firebase.messaging().setBackgroundMessageHandler(function(payload) {
|
||||
if (payload.data.silent === 'true') {
|
||||
// Basic internationalization.
|
||||
const i18n = {
|
||||
'en': {
|
||||
'new_message': "New message",
|
||||
'new_chat': "New chat",
|
||||
},
|
||||
'ru': {
|
||||
'new_message': "Новое сообщение",
|
||||
'new_chat': "Новый чат",
|
||||
},
|
||||
'zh': {
|
||||
'new_message': "新讯息",
|
||||
'new_chat': "新聊天",
|
||||
}
|
||||
};
|
||||
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.
|
||||
return lang[id] || i18n['en'][id] || id;
|
||||
}
|
||||
|
||||
// This method shows the push notifications.
|
||||
firebase.messaging().setBackgroundMessageHandler((payload) => {
|
||||
if (payload.data.silent == 'true') {
|
||||
return;
|
||||
}
|
||||
|
||||
const pushType = payload.data.what || 'msg';
|
||||
const title = payload.data.title || (pushType == 'msg' ? "New message" : "New chat");
|
||||
const title = payload.data.title || self.i18nMessage(pushType == 'msg' ? 'new_message' : 'new_chat');
|
||||
const options = {
|
||||
body: payload.data.content || "",
|
||||
body: payload.data.content || '',
|
||||
icon: '/img/logo96.png',
|
||||
badge: '/img/badge96.png',
|
||||
tag: payload.data.topic || undefined,
|
||||
@@ -73,10 +96,6 @@ self.addEventListener('notificationclick', event => {
|
||||
|
||||
// This is needed for 'Add to Home Screen'.
|
||||
self.addEventListener('fetch', event => {
|
||||
// Workaround for https://bugs.chromium.org/p/chromium/issues/detail?id=823392
|
||||
if (event.request.cache == 'only-if-cached' && event.request.mode != 'same-origin') {
|
||||
return;
|
||||
}
|
||||
event.respondWith(
|
||||
// Try to find response in cache.
|
||||
caches.match(event.request)
|
||||
@@ -90,3 +109,10 @@ self.addEventListener('fetch', event => {
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
// This code get the human language from the webapp for localization of strings.
|
||||
self.addEventListener('message', event => {
|
||||
const data = JSON.parse(event.data);
|
||||
self.locale = data.locale || '';
|
||||
self.baseLocale = self.locale.toLowerCase().split(/[-_]/)[0];
|
||||
});
|
||||
|
||||
+4
-2
@@ -24,6 +24,8 @@ export const READ_DELAY = 1000;
|
||||
|
||||
// The shortest allowed tag length. Matches the value on the server.
|
||||
export const MIN_TAG_LENGTH = 4;
|
||||
// The maximum number of tags allowed. Matches the value on the server.
|
||||
export const MAX_TAG_COUNT = 16;
|
||||
|
||||
// Access mode for P2P subscriptions initiated by the current user.
|
||||
export const DEFAULT_P2P_ACCESS_MODE = 'JRWPS';
|
||||
@@ -54,8 +56,8 @@ export const MESSAGES_PAGE = 24;
|
||||
// This is size of an object *before* base64 encoding is applied.
|
||||
// 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 - overhead);
|
||||
export const MAX_INBAND_ATTACHMENT_SIZE = 195840;
|
||||
// MAX_INBAND_ATTACHMENT_SIZE = base64DecodedLen(max_message_size) - 1024;
|
||||
export const MAX_INBAND_ATTACHMENT_SIZE = 195584;
|
||||
|
||||
// Absolute maximum attachment size to be used with the server = 8MB. Increase to
|
||||
// something like 100MB in production.
|
||||
|
||||
+34
-17
@@ -1,25 +1,33 @@
|
||||
{
|
||||
"validate_credential_action": "confirm",
|
||||
"title_tag_manager": "Tags (user discovery)",
|
||||
"label_user_contacts": "Contacts:",
|
||||
"button_add_another": "Add another",
|
||||
"label_message_sound": "Message sound:",
|
||||
"label_push_notifications": "Notification alerts:",
|
||||
"label_push_notifications_disabled": "Notification alerts (requires HTTPS):",
|
||||
"label_incognito_mode": "Incognito mode:",
|
||||
"delete_account": "Delete account",
|
||||
"delete_account_arning": "Are you sure you want to delete your account? It cannot be undone.",
|
||||
"label_password": "Password",
|
||||
"password_unchanged_prompt": "Unchanged",
|
||||
"button_logout": "Logout",
|
||||
"button_delete_account": "Delete account",
|
||||
"label_default_access_mode": "Default access mode:",
|
||||
"blocked_contacts_link": "Blocked contacts ({count})",
|
||||
"link_contact_us": "Contact Us",
|
||||
"link_terms_of_service": "Terms of Service",
|
||||
"link_privacy_policy": "Privacy Policy",
|
||||
"label_sdk": "SDK:",
|
||||
"label_server_address": "Server address:",
|
||||
"archived_contacts": "Archived contacts ({count})",
|
||||
"contacts_not_found": "You have no chats<br />¯∖_(ツ)_/¯",
|
||||
"full_name_prompt": "Full name, e.g. John Doe",
|
||||
"email_prompt": "Email, e.g. jdoe@example.com",
|
||||
"button_sign_up": "Sign up",
|
||||
"validate_credential_action": "confirm",
|
||||
"label_your_name": "Your name",
|
||||
"label_password": "Password",
|
||||
"password_unchanged_prompt": "Unchanged",
|
||||
"label_message_sound": "Message sound:",
|
||||
"label_push_notifications": "Notification alerts:",
|
||||
"label_push_notifications_disabled": "Notification alerts (requires HTTPS):",
|
||||
"title_tag_manager": "Tags (user discovery)",
|
||||
"label_user_contacts": "Contacts:",
|
||||
"label_user_id": "Address:",
|
||||
"label_default_access_mode": "Default access mode:",
|
||||
"button_logout": "Logout",
|
||||
"label_legal": "Legal",
|
||||
"link_contact_us": "Contact Us",
|
||||
"link_terms_of_service": "Terms of Service",
|
||||
"link_privacy_policy": "Privacy Policy",
|
||||
"label_user_id": "ID:",
|
||||
"button_edit": "Edit",
|
||||
"requested_permissions": "Requested",
|
||||
"granted_permissions": "Granted",
|
||||
"menu_item_edit_permissions": "Edit permissions",
|
||||
@@ -77,14 +85,23 @@
|
||||
"sidepanel_title_login": "Sign In",
|
||||
"sidepanel_title_register": "Create Account",
|
||||
"sidepanel_title_settings": "Settings",
|
||||
"sidepanel_title_edit_account": "Edit Account",
|
||||
"sidepanel_title_account_settings": "Account Settings",
|
||||
"sidepanel_title_acc_general": "General",
|
||||
"sidepanel_title_acc_security": "Security",
|
||||
"sidepanel_title_acc_notifications": "Notifications",
|
||||
"sidepanel_title_acc_support": "Support",
|
||||
"sidepanel_title_newtpk": "Start New Chat",
|
||||
"sidepanel_title_cred": "Confirm Credentials",
|
||||
"sidepanel_title_reset": "Reset Password",
|
||||
"sidepanel_title_archive": "Archived Chats",
|
||||
"sidepanel_title_blocked": "Blocked Chats",
|
||||
"update_available": "Update available. <a href=\"\">Reload</a>.",
|
||||
"reconnect_countdown": "Disconnected. Reconnecting in {seconds}…",
|
||||
"reconnect_now": "Try now",
|
||||
"push_init_failed": "Failed to initialize push notifications",
|
||||
"invalid_security_token": "Invalid security token",
|
||||
"no_connection": "No connection",
|
||||
"code_doesnot_match": "Code does not match",
|
||||
"phone_dative": "phone",
|
||||
"email_dative": "email",
|
||||
"enter_confirmation_code_prompt": "Enter confirmation code sent to you by {method}:",
|
||||
@@ -114,7 +131,7 @@
|
||||
"action_cancel": "cancel",
|
||||
"upload_finishing": "finishing...",
|
||||
"no_contacts": "You have no contacts :-(",
|
||||
"contacts_not_found_short": "No contacts match '{query}'",
|
||||
"contacts_not_found_short": "No contacts match ''{query}''",
|
||||
"title_group_members": "Group Members",
|
||||
"title_all_contacts": "All Contacts",
|
||||
"button_ok": "OK",
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
{
|
||||
"validate_credential_action": "확인",
|
||||
"title_tag_manager": "태그(사용자검색)",
|
||||
"label_user_contacts": "연락처: ",
|
||||
"button_add_another": "다른항목추가",
|
||||
"label_message_sound": "메시지소리: ",
|
||||
"label_push_notifications": "통지경보: ",
|
||||
"label_push_notifications_disabled": "통지경보(HTTPS필요): ",
|
||||
"label_incognito_mode": "시크릿모드: ",
|
||||
"delete_account": "계정삭제",
|
||||
"delete_account_arning": "계정을삭제하시겠습니까?실행취소할수없습니다.",
|
||||
"label_password": "비밀번호",
|
||||
"password_unchanged_prompt": "변경되지않음",
|
||||
"button_logout": "로그아웃",
|
||||
"button_delete_account": "계정삭제",
|
||||
"label_default_access_mode": "기본액세스모드: ",
|
||||
"blocked_contacts_link": "차단된연락처({수})",
|
||||
"link_contact_us": "문의처",
|
||||
"link_terms_of_service": "서비스약관",
|
||||
"link_privacy_policy": "개인정보보호정책",
|
||||
"label_sdk": "SDK: ",
|
||||
"label_server_address": "서버주소: ",
|
||||
"archived_contacts": "보관된연락처({수})",
|
||||
"contacts_not_found": "채팅이없습니다<br/>¯∖_(ツ)_/¯",
|
||||
"full_name_prompt": "전체이름(예: JohnDoe)"
|
||||
"email_prompt": "이메일(예: jdoe@example.com)"
|
||||
"button_sign_up": "가입",
|
||||
"label_your_name": "이름",
|
||||
"label_user_id": "ID: ",
|
||||
"button_edit": "편집",
|
||||
"requested_permissions": "요청",
|
||||
"grant_permissions": "승인",
|
||||
"menu_item_edit_permissions": "편집권한",
|
||||
"label_other_user": "기타",
|
||||
"action_clear_messages": "메시지지우기",
|
||||
"clear_messages_warning": "모든메시지를지우시겠습니까?실행취소할수없습니다.",
|
||||
"action_delete_messages": "모든메시지지우기",
|
||||
"delete_messages_warning": "모든사람의모든메시지를삭제하시겠습니까?실행취소할수없습니다.",
|
||||
"action_leave_chat": "대화나누기",
|
||||
"leave_chat_warning": "이대화를나가시겠습니까?",
|
||||
"action_block_contact": "연락차단",
|
||||
"block_contact_warning": "이연락처를차단하시겠습니까?",
|
||||
"action_report_chat": "대화기록",
|
||||
"report_chat_warning": "이대화를차단하고기록하시겠습니까?",
|
||||
"title_info": "정보",
|
||||
"label_topic_name": "이름",
|
||||
"label_private": "개인코멘트",
|
||||
"private_editing_placeholder": "나만볼수있습니다",
|
||||
"label_muting_topic": "음소거: ",
|
||||
"action_more": "더보기",
|
||||
"label_your_permissions": "권한: ",
|
||||
"label_permissions": "권한: ",
|
||||
"label_you": "당신: ",
|
||||
"label_default_access": "기본액세스모드: ",
|
||||
"label_group_members": "그룹회원: ",
|
||||
"button_add_members": "회원추가",
|
||||
"group_has_no_members": "회원없음",
|
||||
"login_prompt": "로그인",
|
||||
"password_prompt": "비밀번호",
|
||||
"stay_logged_in": "로그인상태유지",
|
||||
"forgot_password_link": "비밀번호를잊으셨습니까?",
|
||||
"button_sign_in": "로그인",
|
||||
"label_client": "클라이언트: ",
|
||||
"label_server": "서버: ",
|
||||
"online_now": "현재접속중",
|
||||
"last_seen_timestamp": "마지막으로본",
|
||||
"title_not_found": "찾을수없음",
|
||||
"unnamed_topic": "이름없는",
|
||||
"message_not_readable": "메시지에접근할수없음",
|
||||
"peers_messaging_disabled": "동료의메시지가비활성화되었습니다.",
|
||||
"enable_peers_messaging": "활성화",
|
||||
"search_for_contacts": "검색을사용하여연락처찾기",
|
||||
"search_no_results": "검색결과가없습니다",
|
||||
"tabtitle_find_user": "찾기",
|
||||
"tabtitle_new_group": "새그룹",
|
||||
"tabtitle_group_by_id": "id로",
|
||||
"new_password_placeholder": "새비밀번호입력",
|
||||
"label_reset_password": "비밀번호재설정이메일보내기: ",
|
||||
"credential_email_prompt": "등록이메일",
|
||||
"button_reset": "재설정",
|
||||
"button_send_request": "요청보내기",
|
||||
"label_server_to_use": "사용할서버: ",
|
||||
"label_wire_transport": "와이어수송: ",
|
||||
"button_update": "업데이트",
|
||||
"sidepanel_title_login": "로그인",
|
||||
"sidepanel_title_register": "계정만들기",
|
||||
"sidepanel_title_settings": "설정",
|
||||
"sidepanel_title_account_settings": "계정설정",
|
||||
"sidepanel_title_acc_general": "일반",
|
||||
"sidepanel_title_acc_security": "보안",
|
||||
"sidepanel_title_acc_notifications": "알림",
|
||||
"sidepanel_title_acc_support": "지원",
|
||||
"sidepanel_title_newtpk": "새채팅시작",
|
||||
"sidepanel_title_cred": "자격증명확인",
|
||||
"sidepanel_title_reset": "비밀번호재설정",
|
||||
"sidepanel_title_archive": "보관된채팅",
|
||||
"sidepanel_title_blocked": "차단된채팅",
|
||||
"update_available": "업데이트가능.<ahref=\"\">재로드</a>.",
|
||||
"reconnect_countdown": "연결이끊어졌습니다.다시연결하는중{초}…",
|
||||
"reconnect_now": "지금시도",
|
||||
"push_init_failed": "푸시알림을초기화하지못했습니다",
|
||||
"invalid_security_token": "유효하지않은보안토큰",
|
||||
"no_connection": "연결없음",
|
||||
"code_doesnot_match": "코드가일치하지않습니다",
|
||||
"phone_dative": "전화",
|
||||
"email_dative": "이메일",
|
||||
"enter_confirmation_code_prompt": "{방법}으로보낸확인코드를입력하십시오: ",
|
||||
"numeric_confirmation_code_prompt": "숫자만",
|
||||
"button_confirm": "확인",
|
||||
"save_attachment": "저장",
|
||||
"delete_content": "컨텐츠삭제",
|
||||
"invalid_content": "잘못된내용",
|
||||
"user_not_found": "찾을수없음",
|
||||
"badge_you": "당신",
|
||||
"badge_owner": "소유자",
|
||||
"menu_item_info": "정보",
|
||||
"menu_item_clear_messages": "메시지지우기",
|
||||
"menu_item_clear_messages_for_all": "모두지우기",
|
||||
"menu_item_delete": "삭제",
|
||||
"menu_item_delete_for_all": "모두삭제",
|
||||
"menu_item_send_retry": "다시시도",
|
||||
"menu_item_mute": "음소거",
|
||||
"menu_item_unmute": "음소거해제",
|
||||
"menu_item_delete_topic": "삭제",
|
||||
"topic_delete_warning": "이대화를삭제하시겠습니까?",
|
||||
"menu_item_unblock": "차단해제",
|
||||
"menu_item_block": "차단",
|
||||
"topic_block_warning": "이대화를차단하시겠습니까?",
|
||||
"menu_item_member_delete": "제거",
|
||||
"menu_item_archive_topic": "보관",
|
||||
"action_cancel": "취소",
|
||||
"upload_finishing": "마무리...",
|
||||
"no_contacts": "연락처가없습니다: -(",
|
||||
"contacts_not_found_short": "'{문의}'와일치하는연락처가없습니다."
|
||||
"title_group_members": "그룹구성원",
|
||||
"title_all_contacts": "모든연락처",
|
||||
"button_ok": "OK",
|
||||
"button_cancel": "취소",
|
||||
"more_online_members": "+{넘침}더보기",
|
||||
"download_action": "다운로드",
|
||||
"label_file_name": "파일이름: ",
|
||||
"label_content_type": "컨텐츠유형: ",
|
||||
"label_size": "크기: ",
|
||||
"chat_invitation": "새로운대화를시작하도록초대되었습니다.무엇을하시겠습니까?",
|
||||
"chat_invitation_accept": "수락",
|
||||
"chat_invitation_ignore": "무시",
|
||||
"chat_invitation_block": "차단",
|
||||
"error_invalid_id": "잘못된ID",
|
||||
"group_user_id_prompt": "그룹또는사용자ID",
|
||||
"button_subscribe": "구독",
|
||||
"topic_name_editing_placeholder": "그룹의자유형이름",
|
||||
"button_create": "작성",
|
||||
"permission_join": "참여({val})",
|
||||
"permission_read": "읽기({val})",
|
||||
"permission_write": "쓰기({val})",
|
||||
"permission_pres": "알림받기({val})",
|
||||
"permission_admin": "승인({val})",
|
||||
"permission_share": "공유({val})",
|
||||
"permission_delete": "삭제({val})",
|
||||
"permission_owner": "소유자({val})",
|
||||
"title_permissions": "권한",
|
||||
"message_sending": "보내기...",
|
||||
"message_sending_failed": "실패",
|
||||
"search_placeholder": "이메일: alice@example.com,전화: 17025550003...와같은목록",
|
||||
"messaging_disabled_prompt": "메시지비활성화",
|
||||
"new_message_prompt": "새메시지",
|
||||
"image_caption_prompt": "이미지설명",
|
||||
"file_attachment_too_large": "파일크기{크기}이(가){제한}제한을초과합니다.",
|
||||
"cannot_initiate_file_upload": "파일업로드를시작할수없습니다.",
|
||||
"tags_not_found": "태그가정의되지않았습니다.일부를추가하십시오.",
|
||||
"tags_editor_no_tags": "일부태그추가",
|
||||
"title_manage_tags": "관리"
|
||||
}
|
||||
+22
-5
@@ -6,7 +6,7 @@
|
||||
"label_your_name": "Ваше имя",
|
||||
"label_password": "Пароль",
|
||||
"password_unchanged_prompt": "Не изменен",
|
||||
"label_user_id": "Адрес:",
|
||||
"label_user_id": "ID:",
|
||||
"label_default_access_mode": "Доступ по умолчанию:",
|
||||
"label_message_sound": "Звук нового сообщения:",
|
||||
"label_push_notifications": "Уведомления:",
|
||||
@@ -34,7 +34,7 @@
|
||||
"sidepanel_title_login": "Авторизация",
|
||||
"sidepanel_title_register": "Зарегистрироваться",
|
||||
"sidepanel_title_settings": "Настройки",
|
||||
"sidepanel_title_edit_account": "Редактировать аккаунт",
|
||||
"sidepanel_title_account_settings": "Настройки аккаунта",
|
||||
"sidepanel_title_newtpk": "Новый чат",
|
||||
"sidepanel_title_cred": "Подтвердить",
|
||||
"sidepanel_title_reset": "Сменить пароль",
|
||||
@@ -109,7 +109,7 @@
|
||||
"action_cancel": "отменить",
|
||||
"upload_finishing": "завершение...",
|
||||
"no_contacts": "Ничего нет :-(",
|
||||
"contacts_not_found_short": "Нет контактов для запроса '{query}'",
|
||||
"contacts_not_found_short": "Нет контактов для запроса ''{query}''",
|
||||
"title_all_contacts": "Все контакты",
|
||||
"error_invalid_id": "Неверный ID",
|
||||
"group_user_id_prompt": "ID чата или пользователя",
|
||||
@@ -135,7 +135,6 @@
|
||||
"more_online_members": "+еще {overflow}",
|
||||
"label_user_contacts": "Конакты:",
|
||||
"validate_credential_action": "подтвердить",
|
||||
"label_legal": "Прочее",
|
||||
"link_contact_us": "Связаться с нами",
|
||||
"link_terms_of_service": "Условия сервиса",
|
||||
"link_privacy_policy": "Политика конфиденциальности",
|
||||
@@ -152,5 +151,23 @@
|
||||
"topic_block_warning": "Вы действительно хотите заблокировать этот чат?",
|
||||
"search_no_results": "Ничего не найдено",
|
||||
"deleted_content": "удалено",
|
||||
"image_caption_prompt": "Подпись к фото"
|
||||
"image_caption_prompt": "Подпись к фото",
|
||||
"label_incognito_mode": "Режим инкогнито:",
|
||||
"delete_account": "Удалить аккаунт",
|
||||
"delete_account_arning": "Вы уверены, что ходите удалить свой аккаунт? Его невозможно будет восстановить.",
|
||||
"button_delete_account": "Удалить аккаунт",
|
||||
"sidepanel_title_acc_security": "Безопасность",
|
||||
"sidepanel_title_acc_notifications": "Уведомления",
|
||||
"sidepanel_title_acc_support": "Поддержка",
|
||||
"label_sdk": "SDK:",
|
||||
"label_server_address": "Адрес сервера:",
|
||||
"button_edit": "Редактировать",
|
||||
"button_add_another": "Добавить",
|
||||
"sidepanel_title_acc_general": "Общие настройки",
|
||||
"blocked_contacts_link": "Заблокированные ({count})",
|
||||
"sidepanel_title_blocked": "Заблокированные чаты",
|
||||
"push_init_failed": "Ошибка инициализации пуш уведомлений",
|
||||
"invalid_security_token": "Токен некорректен",
|
||||
"no_connection": "Нет связи",
|
||||
"code_doesnot_match": "Код не совпадает"
|
||||
}
|
||||
|
||||
+22
-5
@@ -61,7 +61,7 @@
|
||||
"sidepanel_title_login": "登录",
|
||||
"sidepanel_title_register": "创建账户",
|
||||
"sidepanel_title_settings": "设置",
|
||||
"sidepanel_title_edit_account": "编辑账户",
|
||||
"sidepanel_title_account_settings": "帐号设定",
|
||||
"sidepanel_title_newtpk": "开始新会话",
|
||||
"sidepanel_title_cred": "确认凭据",
|
||||
"sidepanel_title_reset": "重置密码",
|
||||
@@ -93,8 +93,8 @@
|
||||
"menu_item_archive_topic": "归档",
|
||||
"action_cancel": "取消",
|
||||
"upload_finishing": "正在结束...",
|
||||
"no_contacts": "你尚无联系人 :-(",
|
||||
"contacts_not_found_short": "无联系人匹配 '{query}'",
|
||||
"no_contacts": "你尚无联系人 (._.)",
|
||||
"contacts_not_found_short": "无联系人匹配''{query}''",
|
||||
"title_group_members": "群组成员",
|
||||
"title_all_contacts": "全部联系人",
|
||||
"button_ok": "好",
|
||||
@@ -134,7 +134,6 @@
|
||||
"more_online_members": "还有{overflow}个",
|
||||
"label_user_contacts": "往来:",
|
||||
"validate_credential_action": "确认",
|
||||
"label_legal": "法律东西",
|
||||
"link_contact_us": "联系我们",
|
||||
"link_terms_of_service": "条款和条件",
|
||||
"link_privacy_policy": "隐私政策",
|
||||
@@ -152,5 +151,23 @@
|
||||
"search_no_results": "搜索返回任何结果",
|
||||
"deleted_content": "内容已删除",
|
||||
"image_caption_prompt": "图片标题",
|
||||
"menu_item_send_retry": "重试"
|
||||
"menu_item_send_retry": "重试",
|
||||
"label_incognito_mode": "无痕模式:",
|
||||
"delete_account": "删除帐户",
|
||||
"delete_account_arning": "您确定要删除您的帐户吗?无法撤消。",
|
||||
"button_delete_account": "删除帐户",
|
||||
"sidepanel_title_acc_security": "安全",
|
||||
"sidepanel_title_acc_notifications": "通知",
|
||||
"sidepanel_title_acc_support": "支持",
|
||||
"label_sdk": "开发包:",
|
||||
"label_server_address": "服务器地址:",
|
||||
"button_edit": "编辑",
|
||||
"button_add_another": "加上另一个",
|
||||
"sidepanel_title_acc_general": "常用设定",
|
||||
"blocked_contacts_link": "封锁的联络人 ({count})",
|
||||
"sidepanel_title_blocked": "被阻止的聊天",
|
||||
"push_init_failed": "初始化推送通知失败",
|
||||
"invalid_security_token": "无效的安全令牌",
|
||||
"no_connection": "无连接",
|
||||
"code_doesnot_match": "代码不匹配"
|
||||
}
|
||||
|
||||
+78
-27
@@ -1,26 +1,34 @@
|
||||
{
|
||||
"en": {
|
||||
"validate_credential_action": "confirm",
|
||||
"title_tag_manager": "Tags (user discovery)",
|
||||
"label_user_contacts": "Contacts:",
|
||||
"button_add_another": "Add another",
|
||||
"label_message_sound": "Message sound:",
|
||||
"label_push_notifications": "Notification alerts:",
|
||||
"label_push_notifications_disabled": "Notification alerts (requires HTTPS):",
|
||||
"label_incognito_mode": "Incognito mode:",
|
||||
"delete_account": "Delete account",
|
||||
"delete_account_arning": "Are you sure you want to delete your account? It cannot be undone.",
|
||||
"label_password": "Password",
|
||||
"password_unchanged_prompt": "Unchanged",
|
||||
"button_logout": "Logout",
|
||||
"button_delete_account": "Delete account",
|
||||
"label_default_access_mode": "Default access mode:",
|
||||
"blocked_contacts_link": "Blocked contacts ({count})",
|
||||
"link_contact_us": "Contact Us",
|
||||
"link_terms_of_service": "Terms of Service",
|
||||
"link_privacy_policy": "Privacy Policy",
|
||||
"label_sdk": "SDK:",
|
||||
"label_server_address": "Server address:",
|
||||
"archived_contacts": "Archived contacts ({count})",
|
||||
"contacts_not_found": "You have no chats<br />¯∖_(ツ)_/¯",
|
||||
"full_name_prompt": "Full name, e.g. John Doe",
|
||||
"email_prompt": "Email, e.g. jdoe@example.com",
|
||||
"button_sign_up": "Sign up",
|
||||
"validate_credential_action": "confirm",
|
||||
"label_your_name": "Your name",
|
||||
"label_password": "Password",
|
||||
"password_unchanged_prompt": "Unchanged",
|
||||
"label_message_sound": "Message sound:",
|
||||
"label_push_notifications": "Notification alerts:",
|
||||
"label_push_notifications_disabled": "Notification alerts (requires HTTPS):",
|
||||
"title_tag_manager": "Tags (user discovery)",
|
||||
"label_user_contacts": "Contacts:",
|
||||
"label_user_id": "Address:",
|
||||
"label_default_access_mode": "Default access mode:",
|
||||
"button_logout": "Logout",
|
||||
"label_legal": "Legal",
|
||||
"link_contact_us": "Contact Us",
|
||||
"link_terms_of_service": "Terms of Service",
|
||||
"link_privacy_policy": "Privacy Policy",
|
||||
"label_user_id": "ID:",
|
||||
"button_edit": "Edit",
|
||||
"requested_permissions": "Requested",
|
||||
"granted_permissions": "Granted",
|
||||
"menu_item_edit_permissions": "Edit permissions",
|
||||
@@ -78,14 +86,23 @@
|
||||
"sidepanel_title_login": "Sign In",
|
||||
"sidepanel_title_register": "Create Account",
|
||||
"sidepanel_title_settings": "Settings",
|
||||
"sidepanel_title_edit_account": "Edit Account",
|
||||
"sidepanel_title_account_settings": "Account Settings",
|
||||
"sidepanel_title_acc_general": "General",
|
||||
"sidepanel_title_acc_security": "Security",
|
||||
"sidepanel_title_acc_notifications": "Notifications",
|
||||
"sidepanel_title_acc_support": "Support",
|
||||
"sidepanel_title_newtpk": "Start New Chat",
|
||||
"sidepanel_title_cred": "Confirm Credentials",
|
||||
"sidepanel_title_reset": "Reset Password",
|
||||
"sidepanel_title_archive": "Archived Chats",
|
||||
"sidepanel_title_blocked": "Blocked Chats",
|
||||
"update_available": "Update available. <a href=\"\">Reload</a>.",
|
||||
"reconnect_countdown": "Disconnected. Reconnecting in {seconds}…",
|
||||
"reconnect_now": "Try now",
|
||||
"push_init_failed": "Failed to initialize push notifications",
|
||||
"invalid_security_token": "Invalid security token",
|
||||
"no_connection": "No connection",
|
||||
"code_doesnot_match": "Code does not match",
|
||||
"phone_dative": "phone",
|
||||
"email_dative": "email",
|
||||
"enter_confirmation_code_prompt": "Enter confirmation code sent to you by {method}:",
|
||||
@@ -115,7 +132,7 @@
|
||||
"action_cancel": "cancel",
|
||||
"upload_finishing": "finishing...",
|
||||
"no_contacts": "You have no contacts :-(",
|
||||
"contacts_not_found_short": "No contacts match '{query}'",
|
||||
"contacts_not_found_short": "No contacts match ''{query}''",
|
||||
"title_group_members": "Group Members",
|
||||
"title_all_contacts": "All Contacts",
|
||||
"button_ok": "OK",
|
||||
@@ -163,7 +180,7 @@
|
||||
"label_your_name": "Ваше имя",
|
||||
"label_password": "Пароль",
|
||||
"password_unchanged_prompt": "Не изменен",
|
||||
"label_user_id": "Адрес:",
|
||||
"label_user_id": "ID:",
|
||||
"label_default_access_mode": "Доступ по умолчанию:",
|
||||
"label_message_sound": "Звук нового сообщения:",
|
||||
"label_push_notifications": "Уведомления:",
|
||||
@@ -191,7 +208,7 @@
|
||||
"sidepanel_title_login": "Авторизация",
|
||||
"sidepanel_title_register": "Зарегистрироваться",
|
||||
"sidepanel_title_settings": "Настройки",
|
||||
"sidepanel_title_edit_account": "Редактировать аккаунт",
|
||||
"sidepanel_title_account_settings": "Настройки аккаунта",
|
||||
"sidepanel_title_newtpk": "Новый чат",
|
||||
"sidepanel_title_cred": "Подтвердить",
|
||||
"sidepanel_title_reset": "Сменить пароль",
|
||||
@@ -266,7 +283,7 @@
|
||||
"action_cancel": "отменить",
|
||||
"upload_finishing": "завершение...",
|
||||
"no_contacts": "Ничего нет :-(",
|
||||
"contacts_not_found_short": "Нет контактов для запроса '{query}'",
|
||||
"contacts_not_found_short": "Нет контактов для запроса ''{query}''",
|
||||
"title_all_contacts": "Все контакты",
|
||||
"error_invalid_id": "Неверный ID",
|
||||
"group_user_id_prompt": "ID чата или пользователя",
|
||||
@@ -292,7 +309,6 @@
|
||||
"more_online_members": "+еще {overflow}",
|
||||
"label_user_contacts": "Конакты:",
|
||||
"validate_credential_action": "подтвердить",
|
||||
"label_legal": "Прочее",
|
||||
"link_contact_us": "Связаться с нами",
|
||||
"link_terms_of_service": "Условия сервиса",
|
||||
"link_privacy_policy": "Политика конфиденциальности",
|
||||
@@ -309,7 +325,25 @@
|
||||
"topic_block_warning": "Вы действительно хотите заблокировать этот чат?",
|
||||
"search_no_results": "Ничего не найдено",
|
||||
"deleted_content": "удалено",
|
||||
"image_caption_prompt": "Подпись к фото"
|
||||
"image_caption_prompt": "Подпись к фото",
|
||||
"label_incognito_mode": "Режим инкогнито:",
|
||||
"delete_account": "Удалить аккаунт",
|
||||
"delete_account_arning": "Вы уверены, что ходите удалить свой аккаунт? Его невозможно будет восстановить.",
|
||||
"button_delete_account": "Удалить аккаунт",
|
||||
"sidepanel_title_acc_security": "Безопасность",
|
||||
"sidepanel_title_acc_notifications": "Уведомления",
|
||||
"sidepanel_title_acc_support": "Поддержка",
|
||||
"label_sdk": "SDK:",
|
||||
"label_server_address": "Адрес сервера:",
|
||||
"button_edit": "Редактировать",
|
||||
"button_add_another": "Добавить",
|
||||
"sidepanel_title_acc_general": "Общие настройки",
|
||||
"blocked_contacts_link": "Заблокированные ({count})",
|
||||
"sidepanel_title_blocked": "Заблокированные чаты",
|
||||
"push_init_failed": "Ошибка инициализации пуш уведомлений",
|
||||
"invalid_security_token": "Токен некорректен",
|
||||
"no_connection": "Нет связи",
|
||||
"code_doesnot_match": "Код не совпадает"
|
||||
},
|
||||
"zh": {
|
||||
"archived_contacts": "已归档联系人 ({count})",
|
||||
@@ -374,7 +408,7 @@
|
||||
"sidepanel_title_login": "登录",
|
||||
"sidepanel_title_register": "创建账户",
|
||||
"sidepanel_title_settings": "设置",
|
||||
"sidepanel_title_edit_account": "编辑账户",
|
||||
"sidepanel_title_account_settings": "帐号设定",
|
||||
"sidepanel_title_newtpk": "开始新会话",
|
||||
"sidepanel_title_cred": "确认凭据",
|
||||
"sidepanel_title_reset": "重置密码",
|
||||
@@ -406,8 +440,8 @@
|
||||
"menu_item_archive_topic": "归档",
|
||||
"action_cancel": "取消",
|
||||
"upload_finishing": "正在结束...",
|
||||
"no_contacts": "你尚无联系人 :-(",
|
||||
"contacts_not_found_short": "无联系人匹配 '{query}'",
|
||||
"no_contacts": "你尚无联系人 (._.)",
|
||||
"contacts_not_found_short": "无联系人匹配''{query}''",
|
||||
"title_group_members": "群组成员",
|
||||
"title_all_contacts": "全部联系人",
|
||||
"button_ok": "好",
|
||||
@@ -447,7 +481,6 @@
|
||||
"more_online_members": "还有{overflow}个",
|
||||
"label_user_contacts": "往来:",
|
||||
"validate_credential_action": "确认",
|
||||
"label_legal": "法律东西",
|
||||
"link_contact_us": "联系我们",
|
||||
"link_terms_of_service": "条款和条件",
|
||||
"link_privacy_policy": "隐私政策",
|
||||
@@ -465,6 +498,24 @@
|
||||
"search_no_results": "搜索返回任何结果",
|
||||
"deleted_content": "内容已删除",
|
||||
"image_caption_prompt": "图片标题",
|
||||
"menu_item_send_retry": "重试"
|
||||
"menu_item_send_retry": "重试",
|
||||
"label_incognito_mode": "无痕模式:",
|
||||
"delete_account": "删除帐户",
|
||||
"delete_account_arning": "您确定要删除您的帐户吗?无法撤消。",
|
||||
"button_delete_account": "删除帐户",
|
||||
"sidepanel_title_acc_security": "安全",
|
||||
"sidepanel_title_acc_notifications": "通知",
|
||||
"sidepanel_title_acc_support": "支持",
|
||||
"label_sdk": "开发包:",
|
||||
"label_server_address": "服务器地址:",
|
||||
"button_edit": "编辑",
|
||||
"button_add_another": "加上另一个",
|
||||
"sidepanel_title_acc_general": "常用设定",
|
||||
"blocked_contacts_link": "封锁的联络人 ({count})",
|
||||
"sidepanel_title_blocked": "被阻止的聊天",
|
||||
"push_init_failed": "初始化推送通知失败",
|
||||
"invalid_security_token": "无效的安全令牌",
|
||||
"no_connection": "无连接",
|
||||
"code_doesnot_match": "代码不匹配"
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
// This is a generated file. Don't edit.
|
||||
|
||||
export const PACKAGE_VERSION = "0.16.4-rc2";
|
||||
export const PACKAGE_VERSION = "0.16.5";
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
// Edit account parameters.
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import Tinode from 'tinode-sdk';
|
||||
|
||||
import AvatarUpload from '../widgets/avatar-upload.jsx';
|
||||
import InPlaceEdit from '../widgets/in-place-edit.jsx';
|
||||
import TagManager from '../widgets/tag-manager.jsx';
|
||||
|
||||
import { MAX_TITLE_LENGTH } from '../config.js';
|
||||
import { makeImageUrl } from '../lib/blob-helpers.js';
|
||||
import { arrayEqual, asEmail, asPhone, vcard } from '../lib/utils.js';
|
||||
|
||||
export default class AccGeneralView extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
const me = this.props.tinode.getMeTopic();
|
||||
this.state = {
|
||||
fullName: me.public ? me.public.fn : undefined,
|
||||
avatar: makeImageUrl(me.public ? me.public.photo : null),
|
||||
tags: me.tags(),
|
||||
credentials: me.getCredentials() || [],
|
||||
addCredActive: false,
|
||||
addCredInvalid: false,
|
||||
newCred: '',
|
||||
previousOnTags: me.onTagsUpdated
|
||||
};
|
||||
|
||||
this.tnNewTags = this.tnNewTags.bind(this);
|
||||
this.tnCredsUpdated = this.tnCredsUpdated.bind(this);
|
||||
this.handleFullNameUpdate = this.handleFullNameUpdate.bind(this);
|
||||
this.handleImageChanged = this.handleImageChanged.bind(this);
|
||||
this.handleCredChange = this.handleCredChange.bind(this);
|
||||
this.handleCredKeyDown = this.handleCredKeyDown.bind(this);
|
||||
this.handleCredEntered = this.handleCredEntered.bind(this);
|
||||
this.handleTagsUpdated = this.handleTagsUpdated.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const me = this.props.tinode.getMeTopic();
|
||||
me.onCredsUpdated = this.tnCredsUpdated;
|
||||
me.onTagsUpdated = this.tnNewTags;
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
const me = this.props.tinode.getMeTopic();
|
||||
me.onTagsUpdated = this.state.previousOnTags;
|
||||
me.onCredsUpdated = undefined;
|
||||
}
|
||||
|
||||
tnNewTags(tags) {
|
||||
this.setState({tags: tags});
|
||||
}
|
||||
|
||||
tnCredsUpdated(creds) {
|
||||
this.setState({credentials: creds || []});
|
||||
}
|
||||
|
||||
handleFullNameUpdate(fn) {
|
||||
fn = fn.trim().substring(0, MAX_TITLE_LENGTH);
|
||||
if (fn) {
|
||||
this.setState({fullName: fn});
|
||||
this.props.onUpdateAccount(undefined, vcard(fn, null));
|
||||
}
|
||||
}
|
||||
|
||||
handleImageChanged(img) {
|
||||
this.setState({avatar: img});
|
||||
this.props.onUpdateAccount(undefined, vcard(null, img || Tinode.DEL_CHAR));
|
||||
}
|
||||
|
||||
handleCredChange(e) {
|
||||
this.setState({newCred: e.target.value, addCredInvalid: false});
|
||||
}
|
||||
|
||||
handleCredKeyDown(e) {
|
||||
if (e.keyCode === 27) {
|
||||
// Escape pressed
|
||||
this.setState({newCred: '', addCredActive: false});
|
||||
} else if (e.keyCode === 13) {
|
||||
// Enter pressed
|
||||
this.handleCredEntered(e);
|
||||
}
|
||||
}
|
||||
|
||||
handleCredEntered(e) {
|
||||
let value = this.state.newCred.trim();
|
||||
if (!value) {
|
||||
this.setState({addCredActive: false, addCredInvalid: false});
|
||||
return;
|
||||
}
|
||||
|
||||
let val = asPhone(value);
|
||||
let method;
|
||||
if (val) {
|
||||
method = 'tel';
|
||||
} else {
|
||||
val = asEmail(value);
|
||||
if (val) {
|
||||
method = 'email';
|
||||
}
|
||||
}
|
||||
if (method) {
|
||||
this.props.onCredAdd(method, val);
|
||||
this.setState({addCredActive: false, newCred: ''});
|
||||
} else {
|
||||
this.setState({addCredInvalid: true});
|
||||
}
|
||||
}
|
||||
|
||||
handleTagsUpdated(tags) {
|
||||
// Check if tags have actually changed.
|
||||
if (arrayEqual(this.state.tags.slice(0), tags.slice(0))) {
|
||||
return;
|
||||
}
|
||||
this.props.onUpdateTags(tags);
|
||||
}
|
||||
|
||||
render() {
|
||||
const credentials = [];
|
||||
this.state.credentials.map((cred) => {
|
||||
credentials.push(<div key={cred.meth + ":" + cred.val + ":" + cred.done}>{cred.meth}: <tt>{cred.val}</tt>
|
||||
<span > {!cred.done ?
|
||||
<a href="#" onClick={(e) => {e.preventDefault(); this.props.onCredConfirm(cred.meth, cred.val);}}>
|
||||
<FormattedMessage id="validate_credential_action" defaultMessage="confirm"
|
||||
description="Validate credentail call to action" />
|
||||
</a>
|
||||
: null} <a href="#" onClick={(e) => {e.preventDefault(); this.props.onCredDelete(cred.meth, cred.val);}}><i
|
||||
className="material-icons gray">delete_outline</i></a></span></div>);
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="scrollable-panel">
|
||||
<div className="panel-form-row">
|
||||
<div className="panel-form-column">
|
||||
<label className="small"><FormattedMessage id="label_your_name" /></label>
|
||||
<div><FormattedMessage id="full_name_prompt">{
|
||||
(full_name_placeholder) => <InPlaceEdit
|
||||
placeholder={full_name_placeholder}
|
||||
value={this.state.fullName}
|
||||
onFinished={this.handleFullNameUpdate} />
|
||||
}</FormattedMessage></div>
|
||||
</div>
|
||||
<AvatarUpload
|
||||
avatar={this.state.avatar}
|
||||
uid={this.props.myUserId}
|
||||
title={this.state.fullName}
|
||||
onImageChanged={this.handleImageChanged}
|
||||
onError={this.props.onError} />
|
||||
</div>
|
||||
<div className="hr" />
|
||||
<FormattedMessage id="title_tag_manager" defaultMessage="Tags (user discovery)"
|
||||
description="Section title for TagManager">{
|
||||
(title_tag_manager) => <TagManager
|
||||
title={title_tag_manager}
|
||||
activated={false}
|
||||
tags={this.state.tags}
|
||||
onSubmit={this.handleTagsUpdated} />
|
||||
}</FormattedMessage>
|
||||
<div className="hr" />
|
||||
<div className="panel-form-column">
|
||||
<label className="small">
|
||||
<FormattedMessage id="label_user_contacts" defaultMessage="Contacts:"
|
||||
description="Label for user contacts" />
|
||||
</label>
|
||||
<div className="quoted">
|
||||
{credentials}
|
||||
{this.state.addCredActive ?
|
||||
<input type="text" value={this.state.value}
|
||||
className={this.state.addCredInvalid ? 'invalid' : null}
|
||||
placeholder="Phone number or email" required="required" autoFocus
|
||||
onChange={this.handleCredChange} onKeyDown={this.handleCredKeyDown} onBlur={this.handleCredEntered} />
|
||||
: null}
|
||||
<div>
|
||||
<a href="#" onClick={(e) => {e.preventDefault(); this.setState({addCredActive: true});}}>
|
||||
<i className="material-icons">add</i>
|
||||
<FormattedMessage id="button_add_another" defaultMessage="Add another" description="Call to action [+ add another]" />
|
||||
</a></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,61 @@
|
||||
// Edit account parameters.
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import CheckBox from '../widgets/checkbox.jsx';
|
||||
|
||||
export default class EditAccountView extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.handleCheckboxClick = this.handleCheckboxClick.bind(this);
|
||||
}
|
||||
|
||||
handleCheckboxClick(what, checked) {
|
||||
if (what == 'sound') {
|
||||
this.props.onToggleMessageSounds(checked);
|
||||
} else if (what == 'alert') {
|
||||
this.props.onTogglePushNotifications(checked);
|
||||
} else if (what == 'incognito') {
|
||||
this.props.onToggleIncognitoMode(checked);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="scrollable-panel">
|
||||
<div className="panel-form-row">
|
||||
<label htmlFor="message-sound">
|
||||
<FormattedMessage id="label_message_sound" defaultMessage="Message sound:"
|
||||
description="Label for message sounds toggle" />
|
||||
</label>
|
||||
<CheckBox name="sound" id="message-sound"
|
||||
checked={this.props.messageSounds} onChange={this.handleCheckboxClick} />
|
||||
</div>
|
||||
<div className="panel-form-row">
|
||||
<label htmlFor="desktop-alerts">
|
||||
{this.props.desktopAlertsEnabled ?
|
||||
<FormattedMessage id="label_push_notifications" defaultMessage="Notification alerts:"
|
||||
description="Label for push notifications switch" />
|
||||
:
|
||||
<FormattedMessage id="label_push_notifications_disabled"
|
||||
defaultMessage="Notification alerts (requires HTTPS):"
|
||||
description="Label for push notifications switch" />
|
||||
}
|
||||
</label>
|
||||
<CheckBox name="alert" id="desktop-alerts"
|
||||
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>
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,155 @@
|
||||
// Edit account parameters.
|
||||
import React from 'react';
|
||||
import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
|
||||
|
||||
import InPlaceEdit from '../widgets/in-place-edit.jsx';
|
||||
import PermissionsEditor from '../widgets/permissions-editor.jsx';
|
||||
|
||||
const messages = defineMessages({
|
||||
delete_account: {
|
||||
id: 'delete_account',
|
||||
defaultMessage: 'Delete account',
|
||||
description: 'Title for delete account warning'
|
||||
},
|
||||
delete_account_warning: {
|
||||
id: 'delete_account_arning',
|
||||
defaultMessage: 'Are you sure you want to delete your account? It cannot be undone.',
|
||||
description: 'Warning message when deleting an account'
|
||||
}
|
||||
});
|
||||
|
||||
class AccSecurityView extends React.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);
|
||||
}
|
||||
|
||||
handlePasswordUpdate(pwd) {
|
||||
this.setState({password: pwd});
|
||||
this.props.onUpdateAccount(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.onUpdateAccount(undefined, undefined, defacs);
|
||||
|
||||
let newState = {showPermissionEditorFor: undefined};
|
||||
newState[this.state.showPermissionEditorFor] = perm;
|
||||
this.setState(newState);
|
||||
}
|
||||
|
||||
handleDeleteAccount(e) {
|
||||
e.preventDefault();
|
||||
const {formatMessage} = this.props.intl;
|
||||
this.props.onShowAlert(
|
||||
formatMessage(messages.delete_account), // title
|
||||
formatMessage(messages.delete_account_warning), // content
|
||||
(() => { this.props.onDeleteAccount(); }), // onConfirm
|
||||
null, // use default text "OK"
|
||||
true, // Show Reject button
|
||||
null // use default text "Cancel"
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<React.Fragment>{this.state.showPermissionEditorFor ?
|
||||
<PermissionsEditor
|
||||
mode={this.state.editedPermissions}
|
||||
skip="O"
|
||||
onSubmit={this.handlePermissionsChanged}
|
||||
onCancel={this.handleHidePermissionsEditor} />
|
||||
:
|
||||
<div className="scrollable-panel">
|
||||
<div className="panel-form-column">
|
||||
<label className="small">
|
||||
<FormattedMessage id="label_password" defaultMessage="Password"
|
||||
description="Label for password editing" />
|
||||
</label>
|
||||
<div>
|
||||
<FormattedMessage id="password_unchanged_prompt" defaultMessage="Unchanged"
|
||||
description="Message in editor while password is unchanged">{
|
||||
(password_unchanged) => <InPlaceEdit
|
||||
placeholder={password_unchanged}
|
||||
type="password"
|
||||
onFinished={this.handlePasswordUpdate} />
|
||||
}</FormattedMessage>
|
||||
</div>
|
||||
</div>
|
||||
<div className="hr" />
|
||||
<div className="panel-form-column">
|
||||
<a href="#" className="red flat-button" onClick={(e) => {e.preventDefault(); this.props.onLogout();}}>
|
||||
<i className="material-icons">exit_to_app</i> <FormattedMessage id="button_logout"
|
||||
defaultMessage="Logout" description="Button [Logout]" />
|
||||
</a>
|
||||
<a href="#" className="red flat-button" onClick={(e) => {this.handleDeleteAccount(e);}}>
|
||||
<i className="material-icons">delete</i> <FormattedMessage id="button_delete_account"
|
||||
defaultMessage="Delete account" description="Button [Delete account]" />
|
||||
</a>
|
||||
</div>
|
||||
<div className="hr" />
|
||||
<div className="panel-form-column">
|
||||
<div>
|
||||
<label className="small">
|
||||
<FormattedMessage id="label_default_access_mode" defaultMessage="Default access mode:"
|
||||
description="Label for default access mode" />
|
||||
</label>
|
||||
</div>
|
||||
<div className="quoted">
|
||||
<div>Auth: <tt className="clickable"
|
||||
onClick={this.handleLaunchPermissionsEditor.bind(this, 'auth')}>{this.state.auth}</tt></div>
|
||||
<div>Anon: <tt className="clickable"
|
||||
onClick={this.handleLaunchPermissionsEditor.bind(this, 'anon')}>{this.state.anon}</tt></div>
|
||||
</div>
|
||||
</div>
|
||||
{this.state.blockedCount > 0 ?
|
||||
<>
|
||||
<div className="hr" />
|
||||
<div className="panel-form-row">
|
||||
<i className="material-icons">block</i>
|
||||
<a href="#" className="gray" onClick={(e) => {e.preventDefault(); this.props.onShowBlocked();}}>
|
||||
<FormattedMessage id="blocked_contacts_link" defaultMessage="Blocked contacts ({count})"
|
||||
values={{ count: this.state.blockedCount }} description="Blocked contacts link" />
|
||||
</a>
|
||||
</div>
|
||||
</>
|
||||
: null }
|
||||
</div>
|
||||
}</React.Fragment>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default injectIntl(AccSecurityView);
|
||||
@@ -0,0 +1,49 @@
|
||||
// Edit account parameters.
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import Tinode from 'tinode-sdk';
|
||||
|
||||
import { APP_NAME, LINK_CONTACT_US, LINK_PRIVACY_POLICY, LINK_TERMS_OF_SERVICE } from '../config.js';
|
||||
|
||||
export default class AccSupportView extends React.PureComponent {
|
||||
render() {
|
||||
return (
|
||||
<div className="scrollable-panel">
|
||||
<div className="panel-form-column">
|
||||
<a href={LINK_CONTACT_US} className="flat-button" target="_blank">
|
||||
<i className="material-icons">email</i> <FormattedMessage id="link_contact_us"
|
||||
defaultMessage="Contact Us" description="Ancor text for contacting us by email" />
|
||||
</a>
|
||||
<a href={LINK_TERMS_OF_SERVICE} className="flat-button" target="_blank">
|
||||
<i className="material-icons">description</i> <FormattedMessage id="link_terms_of_service"
|
||||
defaultMessage="Terms of Service" description="Ancor text for terms of service link" />
|
||||
</a>
|
||||
<a href={LINK_PRIVACY_POLICY} className="flat-button" target="_blank">
|
||||
<i className="material-icons">policy</i> <FormattedMessage id="link_privacy_policy"
|
||||
defaultMessage="Privacy Policy" description="Ancor text for privacy policy link" />
|
||||
</a>
|
||||
</div>
|
||||
<div className="hr" />
|
||||
<div className="panel-form-column">
|
||||
<div className="panel-form-row">
|
||||
<label className="small"><FormattedMessage id="label_client" /></label>
|
||||
{APP_NAME}
|
||||
</div>
|
||||
<div className="panel-form-row">
|
||||
<label className="small"><FormattedMessage id="label_sdk" defaultMessage="SDK:" /></label>
|
||||
{Tinode.getLibrary()}
|
||||
</div>
|
||||
<div className="panel-form-row">
|
||||
<label className="small"><FormattedMessage id="label_server" /></label>
|
||||
{this.props.serverVersion}
|
||||
</div>
|
||||
<div className="panel-form-row">
|
||||
<label className="small"><FormattedMessage id="label_server_address" defaultMessage="Server address:" /></label>
|
||||
{this.props.serverAddress}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -28,6 +28,15 @@ export default class ContactsView extends React.Component {
|
||||
let unreadThreads = 0;
|
||||
let archivedCount = 0;
|
||||
props.chatList.map((c) => {
|
||||
const blocked = c.acs && !c.acs.isJoiner();
|
||||
// Show only blocked contacts only when props.blocked == true.
|
||||
if (blocked && props.blocked) {
|
||||
contacts.push(c);
|
||||
}
|
||||
if (blocked || props.blocked) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (c.private && c.private.arch) {
|
||||
if (props.archive) {
|
||||
contacts.push(c);
|
||||
@@ -39,7 +48,8 @@ export default class ContactsView extends React.Component {
|
||||
unreadThreads += c.unread > 0 ? 1 : 0;
|
||||
}
|
||||
});
|
||||
contacts.sort(function(a, b) {
|
||||
|
||||
contacts.sort((a, b) => {
|
||||
return (b.touched || 0) - (a.touched || 0);
|
||||
});
|
||||
|
||||
@@ -58,7 +68,9 @@ export default class ContactsView extends React.Component {
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
if (prevProps.chatList != this.props.chatList || prevProps.archive != this.props.archive) {
|
||||
if (prevProps.chatList != this.props.chatList ||
|
||||
prevProps.archive != this.props.archive ||
|
||||
prevProps.blocked != this.props.blocked) {
|
||||
const newState = ContactsView.deriveStateFromProps(this.props);
|
||||
this.setState(newState);
|
||||
if (newState.unreadThreads != prevState.unreadThreads) {
|
||||
|
||||
+19
-288
@@ -2,331 +2,62 @@
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import Tinode from 'tinode-sdk';
|
||||
|
||||
import AvatarUpload from '../widgets/avatar-upload.jsx';
|
||||
import CheckBox from '../widgets/checkbox.jsx';
|
||||
import InPlaceEdit from '../widgets/in-place-edit.jsx';
|
||||
import PermissionsEditor from '../widgets/permissions-editor.jsx';
|
||||
import TagManager from '../widgets/tag-manager.jsx';
|
||||
|
||||
import { LINK_CONTACT_US, LINK_PRIVACY_POLICY, LINK_TERMS_OF_SERVICE, MAX_TITLE_LENGTH } from '../config.js';
|
||||
import { makeImageUrl } from '../lib/blob-helpers.js';
|
||||
import { arrayEqual, asEmail, asPhone, vcard } from '../lib/utils.js';
|
||||
|
||||
export default class EditAccountView extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
const me = this.props.tinode.getMeTopic();
|
||||
const defacs = me.getDefaultAccess();
|
||||
this.state = {
|
||||
fullName: me.public ? me.public.fn : undefined,
|
||||
avatar: makeImageUrl(me.public ? me.public.photo : null),
|
||||
auth: defacs ? defacs.auth : null,
|
||||
anon: defacs ? defacs.anon : null,
|
||||
tags: me.tags(),
|
||||
credentials: me.getCredentials() || [],
|
||||
addCredActive: false,
|
||||
addCredInvalid: false,
|
||||
newCred: '',
|
||||
showPermissionEditorFor: undefined,
|
||||
previousOnTags: me.onTagsUpdated
|
||||
avatar: makeImageUrl(me.public ? me.public.photo : null)
|
||||
};
|
||||
|
||||
this.tnNewTags = this.tnNewTags.bind(this);
|
||||
this.tnCredsUpdated = this.tnCredsUpdated.bind(this);
|
||||
this.handleFullNameUpdate = this.handleFullNameUpdate.bind(this);
|
||||
this.handlePasswordUpdate = this.handlePasswordUpdate.bind(this);
|
||||
this.handleImageChanged = this.handleImageChanged.bind(this);
|
||||
this.handleCheckboxClick = this.handleCheckboxClick.bind(this);
|
||||
this.handleCredChange = this.handleCredChange.bind(this);
|
||||
this.handleCredKeyDown = this.handleCredKeyDown.bind(this);
|
||||
this.handleCredEntered = this.handleCredEntered.bind(this);
|
||||
this.handleLaunchPermissionsEditor = this.handleLaunchPermissionsEditor.bind(this);
|
||||
this.handleHidePermissionsEditor = this.handleHidePermissionsEditor.bind(this);
|
||||
this.handlePermissionsChanged = this.handlePermissionsChanged.bind(this);
|
||||
this.handleTagsUpdated = this.handleTagsUpdated.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const me = this.props.tinode.getMeTopic();
|
||||
me.onCredsUpdated = this.tnCredsUpdated;
|
||||
me.onTagsUpdated = this.tnNewTags;
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
const me = this.props.tinode.getMeTopic();
|
||||
me.onTagsUpdated = this.state.previousOnTags;
|
||||
me.onCredsUpdated = undefined;
|
||||
}
|
||||
|
||||
tnNewTags(tags) {
|
||||
this.setState({tags: tags});
|
||||
}
|
||||
|
||||
tnCredsUpdated(creds) {
|
||||
this.setState({credentials: creds || []});
|
||||
}
|
||||
|
||||
handleFullNameUpdate(fn) {
|
||||
fn = fn.trim().substring(0, MAX_TITLE_LENGTH);
|
||||
if (fn) {
|
||||
this.setState({fullName: fn});
|
||||
this.props.onUpdateAccount(undefined, vcard(fn, null));
|
||||
}
|
||||
}
|
||||
|
||||
handlePasswordUpdate(pwd) {
|
||||
this.setState({password: pwd});
|
||||
this.props.onUpdateAccount(pwd);
|
||||
}
|
||||
|
||||
handleImageChanged(img) {
|
||||
this.setState({avatar: img});
|
||||
this.props.onUpdateAccount(undefined, vcard(null, img || Tinode.DEL_CHAR));
|
||||
}
|
||||
|
||||
handleCheckboxClick(what, checked) {
|
||||
if (what == 'sound') {
|
||||
this.props.onToggleMessageSounds(checked);
|
||||
} else if (what == 'alert') {
|
||||
this.props.onTogglePushNotifications(checked);
|
||||
}
|
||||
}
|
||||
|
||||
handleCredChange(e) {
|
||||
this.setState({newCred: e.target.value, addCredInvalid: false});
|
||||
}
|
||||
|
||||
handleCredKeyDown(e) {
|
||||
if (e.keyCode === 27) {
|
||||
// Escape pressed
|
||||
this.setState({newCred: '', addCredActive: false});
|
||||
} else if (e.keyCode === 13) {
|
||||
// Enter pressed
|
||||
this.handleCredEntered(e);
|
||||
}
|
||||
}
|
||||
|
||||
handleCredEntered(e) {
|
||||
let value = this.state.newCred.trim();
|
||||
if (!value) {
|
||||
this.setState({addCredActive: false, addCredInvalid: false});
|
||||
return;
|
||||
}
|
||||
|
||||
let val = asPhone(value);
|
||||
let method;
|
||||
if (val) {
|
||||
method = 'tel';
|
||||
} else {
|
||||
val = asEmail(value);
|
||||
if (val) {
|
||||
method = 'email';
|
||||
}
|
||||
}
|
||||
if (method) {
|
||||
this.props.onCredAdd(method, val);
|
||||
this.setState({addCredActive: false, newCred: ''});
|
||||
} else {
|
||||
this.setState({addCredInvalid: true});
|
||||
}
|
||||
}
|
||||
|
||||
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.onUpdateAccount(undefined, undefined, defacs);
|
||||
|
||||
let newState = {showPermissionEditorFor: undefined};
|
||||
newState[this.state.showPermissionEditorFor] = perm;
|
||||
this.setState(newState);
|
||||
}
|
||||
|
||||
handleTagsUpdated(tags) {
|
||||
// Check if tags have actually changed.
|
||||
if (arrayEqual(this.state.tags.slice(0), tags.slice(0))) {
|
||||
return;
|
||||
}
|
||||
this.props.onUpdateTags(tags);
|
||||
}
|
||||
|
||||
render() {
|
||||
let credentials = [];
|
||||
this.state.credentials.map((cred) => {
|
||||
credentials.push(<div key={cred.meth + ":" + cred.val + ":" + cred.done}>{cred.meth}: <tt>{cred.val}</tt>
|
||||
<span > {!cred.done ?
|
||||
<a href="#" onClick={(e) => {e.preventDefault(); this.props.onCredConfirm(cred.meth, cred.val);}}>
|
||||
<FormattedMessage id="validate_credential_action" defaultMessage="confirm"
|
||||
description="Validate credentail call to action" />
|
||||
</a>
|
||||
: null} <a href="#" onClick={(e) => {e.preventDefault(); this.props.onCredDelete(cred.meth, cred.val);}}><i
|
||||
className="material-icons gray">delete_outline</i></a></span></div>);
|
||||
});
|
||||
|
||||
return (
|
||||
<React.Fragment>{this.state.showPermissionEditorFor ?
|
||||
<PermissionsEditor
|
||||
mode={this.state.editedPermissions}
|
||||
skip="O"
|
||||
onSubmit={this.handlePermissionsChanged}
|
||||
onCancel={this.handleHidePermissionsEditor} />
|
||||
:
|
||||
<div id="edit-account" className="scrollable-panel">
|
||||
<div className="scrollable-panel">
|
||||
<div className="panel-form-row">
|
||||
<div className="panel-form-column">
|
||||
<div><label className="small">
|
||||
<label className="small">
|
||||
<FormattedMessage id="label_your_name" defaultMessage="Your name"
|
||||
description="Label for full name editing" />
|
||||
</label></div>
|
||||
<div><FormattedMessage id="full_name_prompt">{
|
||||
(full_name_placeholder) => <InPlaceEdit
|
||||
placeholder={full_name_placeholder}
|
||||
value={this.state.fullName}
|
||||
onFinished={this.handleFullNameUpdate} />
|
||||
}</FormattedMessage></div>
|
||||
<div><label className="small">
|
||||
<FormattedMessage id="label_password" defaultMessage="Password"
|
||||
description="Label for password editing" />
|
||||
</label></div>
|
||||
</label>
|
||||
<div className="large">{this.state.fullName}</div>
|
||||
<div>
|
||||
<FormattedMessage id="password_unchanged_prompt" defaultMessage="Unchanged"
|
||||
description="Message in editor while password is unchanged">{
|
||||
(password_unchanged) => <InPlaceEdit
|
||||
placeholder={password_unchanged}
|
||||
type="password"
|
||||
onFinished={this.handlePasswordUpdate} />
|
||||
}</FormattedMessage>
|
||||
<label className="small"><FormattedMessage id="label_user_id" defaultMessage="ID:"
|
||||
description="Label for user address (ID)" /></label>
|
||||
<tt>{this.props.myUserId}</tt>
|
||||
</div>
|
||||
</div>
|
||||
<AvatarUpload
|
||||
avatar={this.state.avatar}
|
||||
readOnly={!this.state.owner}
|
||||
uid={this.props.myUserId}
|
||||
title={this.state.fullName}
|
||||
onImageChanged={this.handleImageChanged}
|
||||
onError={this.props.onError} />
|
||||
</div>
|
||||
<div className="hr" />
|
||||
<div className="panel-form-row">
|
||||
<label htmlFor="message-sound">
|
||||
<FormattedMessage id="label_message_sound" defaultMessage="Message sound:"
|
||||
description="Label for message sounds toggle" />
|
||||
</label>
|
||||
<CheckBox name="sound" id="message-sound"
|
||||
checked={this.props.messageSounds}
|
||||
onChange={this.handleCheckboxClick} />
|
||||
title={this.state.fullName} />
|
||||
</div>
|
||||
<div className="panel-form-row">
|
||||
<label htmlFor="desktop-alerts">
|
||||
{this.props.desktopAlertsEnabled ?
|
||||
<FormattedMessage id="label_push_notifications" defaultMessage="Notification alerts:"
|
||||
description="Label for push notifications switch" />
|
||||
:
|
||||
<FormattedMessage id="label_push_notifications_disabled"
|
||||
defaultMessage="Notification alerts (requires HTTPS):"
|
||||
description="Label for push notifications switch" />
|
||||
}
|
||||
</label>
|
||||
<CheckBox name="alert" id="desktop-alerts"
|
||||
checked={this.props.desktopAlerts}
|
||||
onChange={this.props.desktopAlertsEnabled ? this.handleCheckboxClick : null} />
|
||||
</div>
|
||||
<div className="hr" />
|
||||
<FormattedMessage id="title_tag_manager" defaultMessage="Tags (user discovery)"
|
||||
description="Section title for TagManager">{
|
||||
(title_tag_manager) => <TagManager
|
||||
title={title_tag_manager}
|
||||
activated={false}
|
||||
tags={this.state.tags}
|
||||
onSubmit={this.handleTagsUpdated} />
|
||||
}</FormattedMessage>
|
||||
<div className="hr" />
|
||||
<div className="panel-form-column">
|
||||
<div>
|
||||
<label className="small">
|
||||
<FormattedMessage id="label_user_contacts" defaultMessage="Contacts:"
|
||||
description="Label for user contacts" />
|
||||
</label>
|
||||
</div>
|
||||
<div className="quoted">
|
||||
{credentials}
|
||||
{this.state.addCredActive ?
|
||||
<input type="text" value={this.state.value}
|
||||
className={this.state.addCredInvalid ? "invalid" : null}
|
||||
placeholder="Phone number or email" required="required" autoFocus
|
||||
onChange={this.handleCredChange} onKeyDown={this.handleCredKeyDown} onBlur={this.handleCredEntered} />
|
||||
: null}
|
||||
<div><a href="#" onClick={(e) => {e.preventDefault(); this.setState({addCredActive: true});}}>+ Add another</a></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="hr" />
|
||||
<div className="panel-form-column">
|
||||
<div className="panel-form-row">
|
||||
<label className="small"><FormattedMessage id="label_user_id" defaultMessage="Address:"
|
||||
description="Label for user address (ID)" /></label>
|
||||
<tt>{this.props.myUserId}</tt>
|
||||
</div>
|
||||
<div>
|
||||
<label className="small">
|
||||
<FormattedMessage id="label_default_access_mode" defaultMessage="Default access mode:"
|
||||
description="Label for default access mode" />
|
||||
</label>
|
||||
</div>
|
||||
<div className="quoted">
|
||||
<div>Auth: <tt className="clickable"
|
||||
onClick={this.handleLaunchPermissionsEditor.bind(this, 'auth')}>{this.state.auth}</tt></div>
|
||||
<div>Anon: <tt className="clickable"
|
||||
onClick={this.handleLaunchPermissionsEditor.bind(this, 'anon')}>{this.state.anon}</tt></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="hr" />
|
||||
<div className="panel-form-column">
|
||||
<a href="#" className="red flat-button" onClick={(e) => {e.preventDefault(); this.props.onLogout();}}>
|
||||
<i className="material-icons">exit_to_app</i> <FormattedMessage id="button_logout"
|
||||
defaultMessage="Logout" description="Button [Logout]" />
|
||||
<a href="#" className="flat-button" onClick={(e) => {e.preventDefault(); this.props.onBasicNavigate('general');}}>
|
||||
<i className="material-icons">edit</i>
|
||||
<FormattedMessage id="button_edit" defaultMessage="Edit" description="Call to action [Edit]" />
|
||||
</a>
|
||||
</div>
|
||||
<div className="hr" />
|
||||
<div className="panel-form-column">
|
||||
<div>
|
||||
<label className="small">
|
||||
<FormattedMessage id="label_legal" defaultMessage="Legal"
|
||||
description="Section label" />
|
||||
</label>
|
||||
</div>
|
||||
<div className="quoted">
|
||||
<a href={LINK_CONTACT_US} className="flat-button" target="_blank">
|
||||
<i className="material-icons">email</i> <FormattedMessage id="link_contact_us"
|
||||
defaultMessage="Contact Us" description="Ancor text for contacting us by email" />
|
||||
<a href="#" className="flat-button" onClick={(e) => {e.preventDefault(); this.props.onBasicNavigate('notif');}}>
|
||||
<i className="material-icons">notifications</i> <FormattedMessage id="sidepanel_title_acc_notifications" />
|
||||
</a>
|
||||
</div>
|
||||
<div className="quoted">
|
||||
<a href={LINK_TERMS_OF_SERVICE} className="flat-button" target="_blank">
|
||||
<i className="material-icons">description</i> <FormattedMessage id="link_terms_of_service"
|
||||
defaultMessage="Terms of Service" description="Ancor text for terms of service link" />
|
||||
<a href="#" className="flat-button" onClick={(e) => {e.preventDefault(); this.props.onBasicNavigate('security');}}>
|
||||
<i className="material-icons">security</i> <FormattedMessage id="sidepanel_title_acc_security" />
|
||||
</a>
|
||||
</div>
|
||||
<div className="quoted">
|
||||
<a href={LINK_PRIVACY_POLICY} className="flat-button" target="_blank">
|
||||
<i className="material-icons">policy</i> <FormattedMessage id="link_privacy_policy"
|
||||
defaultMessage="Privacy Policy" description="Ancor text for privacy policy link" />
|
||||
<a href="#" className="flat-button" onClick={(e) => {e.preventDefault(); this.props.onBasicNavigate('support');}}>
|
||||
<i className="material-icons">contact_support</i> <FormattedMessage id="sidepanel_title_acc_support" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}</React.Fragment>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -335,7 +335,9 @@ class InfoView extends React.Component {
|
||||
case 'want':
|
||||
toEdit = this.state.modeWant;
|
||||
toCompare = this.state.modeGiven;
|
||||
toSkip = this.state.groupTopic ? 'O' : 'ASDO';
|
||||
toSkip = this.state.owner ? 'O' : // Don't allow owner to unset 'O' permission.
|
||||
// Allow accepting any of 'ASDO' permissions but don't allow asking for them.
|
||||
Tinode.AccessMode.encode(Tinode.AccessMode.diff('ASDO', this.state.modeGiven));
|
||||
titleEdit = formatMessage(messages.requested);
|
||||
titleCompare = formatMessage(messages.granted);
|
||||
break;
|
||||
|
||||
@@ -8,9 +8,9 @@ import { APP_NAME } from '../config.js';
|
||||
|
||||
export default class LogoView extends React.PureComponent {
|
||||
render() {
|
||||
var version = APP_NAME + ' (' + Tinode.getLibrary() + ')';
|
||||
const version = APP_NAME + ' (' + Tinode.getLibrary() + ')';
|
||||
return (
|
||||
<div id="dummy-view" className={this.props.hideSelf ? 'nodisplay' : null}>
|
||||
<div id="dummy-view">
|
||||
<div>
|
||||
<a href="https://github.com/tinode/chat/">
|
||||
<img id="logo" alt="logo" src="img/logo.svg" />
|
||||
|
||||
@@ -9,7 +9,7 @@ import DocPreview from '../widgets/doc-preview.jsx';
|
||||
import ErrorPanel from '../widgets/error-panel.jsx';
|
||||
import GroupSubs from '../widgets/group-subs.jsx';
|
||||
import ImagePreview from '../widgets/image-preview.jsx';
|
||||
import Invitation from '../widgets/Invitation.jsx';
|
||||
import Invitation from '../widgets/invitation.jsx';
|
||||
import LetterTile from '../widgets/letter-tile.jsx';
|
||||
import LoadSpinner from '../widgets/load-spinner.jsx';
|
||||
import LogoView from './logo-view.jsx';
|
||||
@@ -113,8 +113,10 @@ class MessagesView extends React.Component {
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
if (this.messagesScroller) {
|
||||
if (prevState.topic != this.state.topic || prevState.messages.length != this.state.messages.length) {
|
||||
// New message
|
||||
this.messagesScroller.scrollTop = this.messagesScroller.scrollHeight - this.state.scrollPosition;
|
||||
} else if (prevProps.viewportHeight > this.props.viewportHeight) {
|
||||
// Componet changed height.
|
||||
this.messagesScroller.scrollTop += prevProps.viewportHeight - this.props.viewportHeight;
|
||||
}
|
||||
}
|
||||
@@ -332,6 +334,7 @@ class MessagesView extends React.Component {
|
||||
if (node) {
|
||||
node.addEventListener('scroll', this.handleScrollEvent);
|
||||
this.messagesScroller = node;
|
||||
this.messagesScroller.scrollTop = this.messagesScroller.scrollHeight - this.state.scrollPosition;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -692,7 +695,7 @@ class MessagesView extends React.Component {
|
||||
component = null;
|
||||
} else if (!this.state.topic) {
|
||||
component = (
|
||||
<LogoView hideSelf={this.props.hideSelf}
|
||||
<LogoView
|
||||
serverVersion={this.props.serverVersion}
|
||||
serverAddress={this.props.serverAddress} />
|
||||
);
|
||||
|
||||
@@ -8,6 +8,10 @@ import SideNavbar from '../widgets/side-navbar.jsx';
|
||||
import ContactsView from './contacts-view.jsx';
|
||||
import CreateAccountView from './create-account-view.jsx';
|
||||
import EditAccountView from './edit-account-view.jsx';
|
||||
import AccGeneralView from './acc-general-view.jsx';
|
||||
import AccNotificationsView from './acc-notifications-view.jsx';
|
||||
import AccSecurityView from './acc-security-view.jsx';
|
||||
import AccSupportView from './acc-support-view.jsx';
|
||||
import LoginView from './login-view.jsx';
|
||||
import NewTopicView from './new-topic-view.jsx';
|
||||
import PasswordResetView from './password-reset-view.jsx';
|
||||
@@ -32,9 +36,29 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Settings'
|
||||
},
|
||||
'edit': {
|
||||
id: 'sidepanel_title_edit_account',
|
||||
id: 'sidepanel_title_account_settings',
|
||||
description: 'Sidepanel title for EditAccountView.',
|
||||
defaultMessage: 'Edit Account'
|
||||
defaultMessage: 'Account Settings'
|
||||
},
|
||||
'general': {
|
||||
id: 'sidepanel_title_acc_general',
|
||||
description: 'Sidepanel title for AccGeneralView.',
|
||||
defaultMessage: 'General'
|
||||
},
|
||||
'security': {
|
||||
id: 'sidepanel_title_acc_security',
|
||||
description: 'Sidepanel title for AccSecurityView.',
|
||||
defaultMessage: 'Security'
|
||||
},
|
||||
'notif': {
|
||||
id: 'sidepanel_title_acc_notifications',
|
||||
description: 'Sidepanel title for AccNotificationsView.',
|
||||
defaultMessage: 'Notifications'
|
||||
},
|
||||
'support': {
|
||||
id: 'sidepanel_title_acc_support',
|
||||
description: 'Sidepanel title for AccSupportView.',
|
||||
defaultMessage: 'Support'
|
||||
},
|
||||
'newtpk': {
|
||||
id: 'sidepanel_title_newtpk',
|
||||
@@ -55,6 +79,11 @@ const messages = defineMessages({
|
||||
id: 'sidepanel_title_archive',
|
||||
description: 'Sidepanel title for ContactsView-Archive.',
|
||||
defaultMessage: 'Archived Chats'
|
||||
},
|
||||
'blocked': {
|
||||
id: 'sidepanel_title_blocked',
|
||||
description: 'Sidepanel title for ContactsView-Blocked.',
|
||||
defaultMessage: 'Blocked Chats'
|
||||
}
|
||||
});
|
||||
|
||||
@@ -63,12 +92,17 @@ class SidepanelView extends React.Component {
|
||||
super(props);
|
||||
|
||||
this.handleLoginRequested = this.handleLoginRequested.bind(this);
|
||||
this.handleNewTopic = this.handleNewTopic.bind(this);
|
||||
}
|
||||
|
||||
handleLoginRequested(login, password) {
|
||||
this.props.onLoginRequest(login, password);
|
||||
}
|
||||
|
||||
handleNewTopic() {
|
||||
this.props.onBasicNavigate('newtpk');
|
||||
}
|
||||
|
||||
render() {
|
||||
const {formatMessage} = this.props.intl;
|
||||
const view = this.props.state || (this.props.myUserId ? 'contacts' : 'login');
|
||||
@@ -96,7 +130,7 @@ class SidepanelView extends React.Component {
|
||||
myUserId={this.props.myUserId}
|
||||
onSignUp={this.props.onSignUp}
|
||||
onSettings={this.props.onSettings}
|
||||
onNewTopic={this.props.onNewTopic}
|
||||
onNewTopic={this.handleNewTopic}
|
||||
onCancel={onCancel} />
|
||||
|
||||
<ErrorPanel
|
||||
@@ -131,27 +165,52 @@ class SidepanelView extends React.Component {
|
||||
<EditAccountView
|
||||
tinode={this.props.tinode}
|
||||
myUserId={this.props.myUserId}
|
||||
messageSounds={this.props.messageSounds}
|
||||
desktopAlerts={this.props.desktopAlerts}
|
||||
desktopAlertsEnabled={this.props.desktopAlertsEnabled}
|
||||
onBasicNavigate={this.props.onBasicNavigate} /> :
|
||||
|
||||
view === 'general' ?
|
||||
<AccGeneralView
|
||||
tinode={this.props.tinode}
|
||||
myUserId={this.props.myUserId}
|
||||
onUpdateAccount={this.props.onUpdateAccount}
|
||||
onUpdateTags={this.props.onUpdateAccountTags}
|
||||
onTogglePushNotifications={this.props.onTogglePushNotifications}
|
||||
onToggleMessageSounds={this.props.onToggleMessageSounds}
|
||||
onCredAdd={this.props.onCredAdd}
|
||||
onCredDelete={this.props.onCredDelete}
|
||||
onCredConfirm={this.props.onCredConfirm}
|
||||
onLogout={this.props.onLogout}
|
||||
onCancel={this.props.onCancel}
|
||||
onBasicNavigate={this.props.onBasicNavigate}
|
||||
onError={this.props.onError} /> :
|
||||
|
||||
(view === 'contacts' || view == 'archive') ?
|
||||
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} /> :
|
||||
|
||||
view === 'security' ?
|
||||
<AccSecurityView
|
||||
tinode={this.props.tinode}
|
||||
onUpdateAccount={this.props.onUpdateAccount}
|
||||
onLogout={this.props.onLogout}
|
||||
onDeleteAccount={this.props.onDeleteAccount}
|
||||
onShowAlert={this.props.onShowAlert}
|
||||
onShowBlocked={this.props.onShowBlocked} /> :
|
||||
|
||||
view === 'support' ?
|
||||
<AccSupportView
|
||||
serverAddress={this.props.serverAddress}
|
||||
serverVersion={this.props.serverVersion} /> :
|
||||
|
||||
(view === 'contacts' || view == 'archive' || view == 'blocked') ?
|
||||
<ContactsView
|
||||
tinode={this.props.tinode}
|
||||
myUserId={this.props.myUserId}
|
||||
connected={this.props.connected}
|
||||
topicSelected={this.props.topicSelected}
|
||||
archive={view == 'archive'}
|
||||
blocked={view == 'blocked'}
|
||||
chatList={this.props.chatList}
|
||||
showContextMenu={this.props.showContextMenu}
|
||||
onTopicSelected={this.props.onTopicSelected}
|
||||
|
||||
+129
-41
@@ -42,6 +42,26 @@ const messages = defineMessages({
|
||||
id: 'reconnect_now',
|
||||
defaultMessage: 'Try now',
|
||||
description: 'Prompt for reconnecting now'
|
||||
},
|
||||
push_init_failed: {
|
||||
id: 'push_init_failed',
|
||||
defaultMessage: 'Failed to initialize push notifications',
|
||||
description: 'Error message when push notifications have failed to initialize.'
|
||||
},
|
||||
invalid_security_token: {
|
||||
id: 'invalid_security_token',
|
||||
defaultMessage: 'Invalid security token',
|
||||
description: 'Error message when resetting password.'
|
||||
},
|
||||
no_connection: {
|
||||
id: 'no_connection',
|
||||
defaultMessage: 'No connection',
|
||||
description: 'Warning that the user is offline.'
|
||||
},
|
||||
code_doesnot_match: {
|
||||
id: 'code_doesnot_match',
|
||||
defaultMessage: 'Code does not match',
|
||||
description: 'Error message when the credential validation code is incorrect.'
|
||||
}
|
||||
});
|
||||
|
||||
@@ -80,9 +100,11 @@ class TinodeWeb extends React.Component {
|
||||
this.handleNewAccountRequest = this.handleNewAccountRequest.bind(this);
|
||||
this.handleUpdateAccountRequest = this.handleUpdateAccountRequest.bind(this);
|
||||
this.handleUpdateAccountTagsRequest = this.handleUpdateAccountTagsRequest.bind(this);
|
||||
this.handleToggleIncognitoMode = this.handleToggleIncognitoMode.bind(this);
|
||||
this.handleSettings = this.handleSettings.bind(this);
|
||||
this.handleGlobalSettings = this.handleGlobalSettings.bind(this);
|
||||
this.handleShowArchive = this.handleShowArchive.bind(this);
|
||||
this.handleShowBlocked = this.handleShowBlocked.bind(this);
|
||||
this.handleToggleMessageSounds = this.handleToggleMessageSounds.bind(this);
|
||||
this.handleCredAdd = this.handleCredAdd.bind(this);
|
||||
this.handleCredDelete = this.handleCredDelete.bind(this);
|
||||
@@ -91,13 +113,13 @@ class TinodeWeb extends React.Component {
|
||||
this.togglePushToken = this.togglePushToken.bind(this);
|
||||
this.requestPushToken = this.requestPushToken.bind(this);
|
||||
this.handleSidepanelCancel = this.handleSidepanelCancel.bind(this);
|
||||
this.handleNewTopic = this.handleNewTopic.bind(this);
|
||||
this.handleNewTopicRequest = this.handleNewTopicRequest.bind(this);
|
||||
this.handleNewTopicCreated = this.handleNewTopicCreated.bind(this);
|
||||
this.handleTopicUpdateRequest = this.handleTopicUpdateRequest.bind(this);
|
||||
this.handleChangePermissions = this.handleChangePermissions.bind(this);
|
||||
this.handleTagsUpdated = this.handleTagsUpdated.bind(this);
|
||||
this.handleLogout = this.handleLogout.bind(this);
|
||||
this.handleDeleteAccount = this.handleDeleteAccount.bind(this);
|
||||
this.handleDeleteMessagesRequest = this.handleDeleteMessagesRequest.bind(this);
|
||||
this.handleLeaveUnsubRequest = this.handleLeaveUnsubRequest.bind(this);
|
||||
this.handleBlockTopicRequest = this.handleBlockTopicRequest.bind(this);
|
||||
@@ -122,11 +144,14 @@ class TinodeWeb extends React.Component {
|
||||
connected: false,
|
||||
// Connected and subscribed to 'me'
|
||||
ready: false,
|
||||
// Try to re-login on new connection.
|
||||
autoLogin: false,
|
||||
transport: settings.transport || null,
|
||||
serverAddress: settings.serverAddress || detectServerAddress(),
|
||||
serverVersion: "no connection",
|
||||
// "On" is the default, so saving the "off" state.
|
||||
messageSounds: !settings.messageSoundsOff,
|
||||
incognitoMode: false,
|
||||
desktopAlerts: settings.desktopAlerts,
|
||||
desktopAlertsEnabled: (isSecureConnection() || isLocalHost()) &&
|
||||
(typeof firebase != 'undefined') && (typeof navigator != 'undefined') &&
|
||||
@@ -192,7 +217,8 @@ class TinodeWeb extends React.Component {
|
||||
viewportHeight: document.documentElement.clientHeight
|
||||
});
|
||||
|
||||
this.tinode = TinodeWeb.tnSetup(this.state.serverAddress, this.state.transport);
|
||||
const {formatMessage, locale} = this.props.intl;
|
||||
this.tinode = TinodeWeb.tnSetup(this.state.serverAddress, this.state.transport, locale);
|
||||
this.tinode.onConnect = this.handleConnected;
|
||||
this.tinode.onDisconnect = this.handleDisconnect;
|
||||
this.tinode.onAutoreconnectIteration = this.handleAutoreconnectIteration;
|
||||
@@ -205,6 +231,7 @@ class TinodeWeb extends React.Component {
|
||||
navigator.serviceWorker.register('/service-worker.js').then((reg) => {
|
||||
this.checkForAppUpdate(reg);
|
||||
this.fbPush.useServiceWorker(reg);
|
||||
reg.active.postMessage(JSON.stringify({locale: locale}));
|
||||
this.initDesktopAlerts();
|
||||
if (this.state.desktopAlerts) {
|
||||
if (!this.state.firebaseToken) {
|
||||
@@ -218,7 +245,7 @@ class TinodeWeb extends React.Component {
|
||||
console.log("Failed to register service worker:", err);
|
||||
});
|
||||
} catch (err) {
|
||||
this.handleError("Failed to initialize push notifications", 'err');
|
||||
this.handleError(formatMessage({id: 'push_init_failed'}), 'err');
|
||||
console.log("Failed to initialize push notifications", err);
|
||||
this.setState({desktopAlertsEnabled: false});
|
||||
}
|
||||
@@ -229,6 +256,8 @@ class TinodeWeb extends React.Component {
|
||||
|
||||
const parsedNav = HashNavigation.parseUrlHash(window.location.hash);
|
||||
if (token) {
|
||||
this.setState({autoLogin: true});
|
||||
|
||||
// When reading from storage, date is returned as string.
|
||||
token.expires = new Date(token.expires);
|
||||
this.tinode.setAuthToken(token);
|
||||
@@ -257,14 +286,15 @@ class TinodeWeb extends React.Component {
|
||||
}
|
||||
|
||||
// Setup transport (usually websocket) and server address. This will terminate connection with the server.
|
||||
static tnSetup(serverAddress, transport) {
|
||||
let tinode = new Tinode(APP_NAME, serverAddress, API_KEY, transport, isSecureConnection());
|
||||
static tnSetup(serverAddress, transport, locale) {
|
||||
const tinode = new Tinode(APP_NAME, serverAddress, API_KEY, transport, isSecureConnection());
|
||||
tinode.setHumanLanguage(locale);
|
||||
tinode.enableLogging(LOGGING_ENABLED, true);
|
||||
return tinode;
|
||||
}
|
||||
|
||||
handleResize() {
|
||||
var mobile = document.documentElement.clientWidth <= MEDIA_BREAKPOINT;
|
||||
const mobile = document.documentElement.clientWidth <= MEDIA_BREAKPOINT;
|
||||
this.setState({
|
||||
viewportWidth: document.documentElement.clientWidth,
|
||||
viewportHeight: document.documentElement.clientHeight
|
||||
@@ -289,10 +319,11 @@ class TinodeWeb extends React.Component {
|
||||
|
||||
// Handle for hashchange event: display appropriate panels.
|
||||
handleHashRoute() {
|
||||
var hash = HashNavigation.parseUrlHash(window.location.hash);
|
||||
const hash = HashNavigation.parseUrlHash(window.location.hash);
|
||||
if (hash.path && hash.path.length > 0) {
|
||||
// Left-side panel selector.
|
||||
if (['register','settings','edit','cred','reset','newtpk','archive','contacts',''].includes(hash.path[0])) {
|
||||
if (['register','settings','edit','notif','security','support','general',
|
||||
'cred','reset','newtpk','archive','blocked','contacts',''].includes(hash.path[0])) {
|
||||
this.setState({sidePanelSelected: hash.path[0]});
|
||||
} else {
|
||||
console.log("Unknown sidepanel view", hash.path[0]);
|
||||
@@ -325,11 +356,10 @@ class TinodeWeb extends React.Component {
|
||||
}
|
||||
|
||||
handleOnline(online) {
|
||||
var newState = {liveConnection: online};
|
||||
if (online) {
|
||||
this.handleError('', null);
|
||||
this.handleError();
|
||||
} else {
|
||||
this.handleError("No connection", 'warn');
|
||||
this.handleError(this.props.intl.formatMessage({id: 'no_connection'}), 'warn');
|
||||
}
|
||||
this.setState({liveConnection: online});
|
||||
}
|
||||
@@ -348,7 +378,8 @@ class TinodeWeb extends React.Component {
|
||||
loginDisabled: true,
|
||||
login: login,
|
||||
password: password,
|
||||
loadSpinnerVisible: true
|
||||
loadSpinnerVisible: true,
|
||||
autoLogin: true
|
||||
});
|
||||
this.handleError('', null);
|
||||
|
||||
@@ -357,7 +388,7 @@ class TinodeWeb extends React.Component {
|
||||
} else {
|
||||
this.tinode.connect().catch((err) => {
|
||||
// Socket error
|
||||
this.setState({loginDisabled: false});
|
||||
this.setState({loginDisabled: false, autoLogin: false});
|
||||
this.handleError(err.message, 'err');
|
||||
});
|
||||
}
|
||||
@@ -365,12 +396,15 @@ class TinodeWeb extends React.Component {
|
||||
|
||||
// Connection succeeded.
|
||||
handleConnected() {
|
||||
var params = this.tinode.getServerInfo();
|
||||
const params = this.tinode.getServerInfo();
|
||||
this.setState({
|
||||
serverVersion: params.ver + ' ' + (params.build ? params.build : 'none') + '; '
|
||||
serverVersion: params.ver + ' ' + (params.build ? params.build : 'none')
|
||||
});
|
||||
|
||||
if (this.state.autoLogin) {
|
||||
this.doLogin(this.state.login, this.state.password, {meth: this.state.credMethod, resp: this.state.credCode});
|
||||
}
|
||||
}
|
||||
|
||||
// Called for each auto-reconnect iteration.
|
||||
handleAutoreconnectIteration(sec, prom) {
|
||||
@@ -435,7 +469,7 @@ class TinodeWeb extends React.Component {
|
||||
cred = Tinode.credential(cred);
|
||||
// Try to login with login/password. If they are not available, try token. If no token, ask for login/password.
|
||||
let promise = null;
|
||||
let token = this.tinode.getAuthToken();
|
||||
const token = this.tinode.getAuthToken();
|
||||
if (login && password) {
|
||||
this.setState({password: null});
|
||||
promise = this.tinode.loginBasic(login, password, cred);
|
||||
@@ -448,7 +482,7 @@ class TinodeWeb extends React.Component {
|
||||
if (ctrl.code >= 300 && ctrl.text === 'validate credentials') {
|
||||
this.setState({loadSpinnerVisible: false});
|
||||
if (cred) {
|
||||
this.handleError("Code does not match", 'warn');
|
||||
this.handleError(this.props.intl.formatMessage({id: 'code_doesnot_match'}), 'warn');
|
||||
}
|
||||
this.handleCredentialsRequest(ctrl.params);
|
||||
} else {
|
||||
@@ -460,7 +494,8 @@ class TinodeWeb extends React.Component {
|
||||
loginDisabled: false,
|
||||
credMethod: undefined,
|
||||
credCode: undefined,
|
||||
loadSpinnerVisible: false
|
||||
loadSpinnerVisible: false,
|
||||
autoLogin: false
|
||||
});
|
||||
this.handleError(err.message, 'err');
|
||||
localStorage.removeItem('auth-token');
|
||||
@@ -475,21 +510,21 @@ class TinodeWeb extends React.Component {
|
||||
}
|
||||
|
||||
handleCredentialsRequest(params) {
|
||||
var parsed = HashNavigation.parseUrlHash(window.location.hash);
|
||||
const parsed = HashNavigation.parseUrlHash(window.location.hash);
|
||||
parsed.path[0] = 'cred';
|
||||
parsed.params['method'] = params.cred[0];
|
||||
HashNavigation.navigateTo(HashNavigation.composeUrlHash(parsed.path, parsed.params));
|
||||
}
|
||||
|
||||
handleLoginSuccessful() {
|
||||
this.handleError('', null);
|
||||
this.handleError();
|
||||
|
||||
// Refresh authentication token.
|
||||
if (LocalStorageUtil.getObject('keep-logged-in')) {
|
||||
LocalStorageUtil.setObject('auth-token', this.tinode.getAuthToken());
|
||||
}
|
||||
// Logged in fine, subscribe to 'me' attaching callbacks from the contacts view.
|
||||
var me = this.tinode.getMeTopic();
|
||||
const me = this.tinode.getMeTopic();
|
||||
me.onMetaDesc = this.tnMeMetaDesc;
|
||||
me.onContactUpdate = this.tnMeContactUpdate;
|
||||
me.onSubsUpdated = this.tnMeSubsUpdated;
|
||||
@@ -497,7 +532,8 @@ class TinodeWeb extends React.Component {
|
||||
connected: true,
|
||||
credMethod: undefined,
|
||||
credCode: undefined,
|
||||
myUserId: this.tinode.getCurrentUserID()
|
||||
myUserId: this.tinode.getCurrentUserID(),
|
||||
autoLogin: true,
|
||||
});
|
||||
// Subscribe, fetch topic desc, the list of subscriptions. Messages are not fetched.
|
||||
me.subscribe(
|
||||
@@ -519,12 +555,19 @@ class TinodeWeb extends React.Component {
|
||||
}
|
||||
|
||||
tnMeMetaDesc(desc) {
|
||||
if (desc && desc.public) {
|
||||
if (desc) {
|
||||
if (desc.public) {
|
||||
this.setState({
|
||||
sidePanelTitle: desc.public.fn,
|
||||
sidePanelAvatar: makeImageUrl(desc.public.photo)
|
||||
});
|
||||
}
|
||||
if (desc.acs) {
|
||||
this.setState({
|
||||
incognitoMode: !desc.acs.isPresencer()
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Reactions to updates to the contact list.
|
||||
@@ -584,7 +627,7 @@ class TinodeWeb extends React.Component {
|
||||
// Merge search results and contact list to create a single flat
|
||||
// list of kown contacts for GroupManager to use.
|
||||
static prepareSearchableContacts(chatList, foundContacts) {
|
||||
let merged = {};
|
||||
const merged = {};
|
||||
|
||||
// For chatList topics merge only p2p topics and convert them to the
|
||||
// same format as foundContacts.
|
||||
@@ -778,7 +821,7 @@ class TinodeWeb extends React.Component {
|
||||
break;
|
||||
case 'delete':
|
||||
// Ignore invitation by deleting it.
|
||||
response = topic.delTopic();
|
||||
response = topic.delTopic(true);
|
||||
break;
|
||||
case 'block':
|
||||
// Ban the topic making futher invites impossible.
|
||||
@@ -801,11 +844,16 @@ class TinodeWeb extends React.Component {
|
||||
|
||||
// User chose a Sign Up menu item.
|
||||
handleNewAccount() {
|
||||
this.handleError();
|
||||
|
||||
HashNavigation.navigateTo(HashNavigation.setUrlSidePanel(window.location.hash, 'register'));
|
||||
}
|
||||
|
||||
// Actual registration of a new account.
|
||||
handleNewAccountRequest(login_, password_, public_, cred_, tags_) {
|
||||
// Clear old error, if any.
|
||||
this.handleError();
|
||||
|
||||
this.tinode.connect(this.state.serverAddress)
|
||||
.then(() => {
|
||||
return this.tinode.createAccountBasic(login_, password_,
|
||||
@@ -822,6 +870,8 @@ class TinodeWeb extends React.Component {
|
||||
}
|
||||
|
||||
handleUpdateAccountRequest(password, pub, defacs) {
|
||||
this.handleError();
|
||||
|
||||
if (pub || defacs) {
|
||||
const params = {};
|
||||
if (pub) {
|
||||
@@ -841,6 +891,14 @@ class TinodeWeb extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
handleToggleIncognitoMode(on) {
|
||||
const me = this.tinode.getMeTopic();
|
||||
const am = me.getAccessMode().updateWant(on ? '-P' : '+P').getWant();
|
||||
me.setMeta({sub: {mode: am}}).catch((err) => {
|
||||
this.handleError(err.message, 'err');
|
||||
});
|
||||
}
|
||||
|
||||
handleUpdateAccountTagsRequest(tags) {
|
||||
this.tinode.getMeTopic().setMeta({tags: tags})
|
||||
.catch((err) => {
|
||||
@@ -850,19 +908,21 @@ class TinodeWeb extends React.Component {
|
||||
|
||||
// User chose Settings menu item.
|
||||
handleSettings() {
|
||||
this.handleError();
|
||||
|
||||
HashNavigation.navigateTo(HashNavigation.setUrlSidePanel(window.location.hash,
|
||||
this.state.myUserId ? 'edit' : 'settings'));
|
||||
}
|
||||
|
||||
// User updated global parameters.
|
||||
handleGlobalSettings(settings) {
|
||||
let serverAddress = settings.serverAddress || this.state.serverAddress;
|
||||
let transport = settings.transport || this.state.transport;
|
||||
const serverAddress = settings.serverAddress || this.state.serverAddress;
|
||||
const transport = settings.transport || this.state.transport;
|
||||
if (this.tinode) {
|
||||
this.tinode.onDisconnect = undefined;
|
||||
this.tinode.disconnect();
|
||||
}
|
||||
this.tinode = TinodeWeb.tnSetup(serverAddress, transport);
|
||||
this.tinode = TinodeWeb.tnSetup(serverAddress, transport, this.props.intl.locale);
|
||||
this.tinode.onConnect = this.handleConnected;
|
||||
this.tinode.onDisconnect = this.handleDisconnect;
|
||||
|
||||
@@ -884,6 +944,12 @@ class TinodeWeb extends React.Component {
|
||||
this.state.myUserId ? 'archive' : ''));
|
||||
}
|
||||
|
||||
// User viewes 'Blocked chats'.
|
||||
handleShowBlocked() {
|
||||
HashNavigation.navigateTo(HashNavigation.setUrlSidePanel(window.location.hash,
|
||||
this.state.myUserId ? 'blocked' : ''));
|
||||
}
|
||||
|
||||
// Initialize desktop alerts = push notifications.
|
||||
initDesktopAlerts() {
|
||||
// Google could not be bothered to mention that
|
||||
@@ -969,7 +1035,15 @@ class TinodeWeb extends React.Component {
|
||||
// User clicked Cancel button in Setting or Sign Up panel.
|
||||
handleSidepanelCancel() {
|
||||
const parsed = HashNavigation.parseUrlHash(window.location.hash);
|
||||
parsed.path[0] = this.state.myUserId ? 'contacts' : '';
|
||||
let path = '';
|
||||
if (['security','support','general','notif'].includes(parsed.path[0])) {
|
||||
path = 'edit';
|
||||
} else if ('blocked' == parsed.path[0]) {
|
||||
path = 'security';
|
||||
} else if (this.state.myUserId) {
|
||||
path = 'contacts';
|
||||
}
|
||||
parsed.path[0] = path;
|
||||
if (parsed.params) {
|
||||
delete parsed.params.code;
|
||||
delete parsed.params.method;
|
||||
@@ -979,9 +1053,9 @@ class TinodeWeb extends React.Component {
|
||||
this.setState({errorText: '', errorLevel: null});
|
||||
}
|
||||
|
||||
// User clicked a (+) menu item.
|
||||
handleNewTopic() {
|
||||
HashNavigation.navigateTo(HashNavigation.setUrlSidePanel(window.location.hash, 'newtpk'));
|
||||
// Basic nagigator by hash value. No need to bind to this.
|
||||
basicNavigator(hash) {
|
||||
HashNavigation.navigateTo(HashNavigation.setUrlSidePanel(window.location.hash, hash));
|
||||
}
|
||||
|
||||
// Request to start a new topic. New P2P topic requires peer's name.
|
||||
@@ -1077,12 +1151,18 @@ class TinodeWeb extends React.Component {
|
||||
this.tinode.disconnect();
|
||||
}
|
||||
this.setState(this.getBlankState());
|
||||
this.tinode = TinodeWeb.tnSetup(this.state.serverAddress, this.state.transport);
|
||||
this.tinode = TinodeWeb.tnSetup(this.state.serverAddress, this.state.transport, this.props.intl.locale);
|
||||
this.tinode.onConnect = this.handleConnected;
|
||||
this.tinode.onDisconnect = this.handleDisconnect;
|
||||
HashNavigation.navigateTo('');
|
||||
}
|
||||
|
||||
handleDeleteAccount() {
|
||||
this.tinode.delCurrentUser(true).then((ctrl) => {
|
||||
this.handleLogout();
|
||||
});
|
||||
}
|
||||
|
||||
handleDeleteMessagesRequest(topicName) {
|
||||
const topic = this.tinode.getTopic(topicName);
|
||||
if (!topic) {
|
||||
@@ -1156,7 +1236,6 @@ class TinodeWeb extends React.Component {
|
||||
|
||||
defaultTopicContextMenu(topicName) {
|
||||
const topic = this.tinode.getTopic(topicName);
|
||||
const {formatMessage} = this.props.intl;
|
||||
|
||||
let muted = false, blocked = false, self_blocked = false, subscribed = false, deleter = false, archived = false;
|
||||
if (topic) {
|
||||
@@ -1173,7 +1252,10 @@ class TinodeWeb extends React.Component {
|
||||
}
|
||||
|
||||
return [
|
||||
subscribed ? {title: formatMessage({id: 'menu_item_info'}), handler: this.handleShowInfoView} : null,
|
||||
subscribed ? {
|
||||
title: this.props.intl.formatMessage({id: 'menu_item_info'}),
|
||||
handler: this.handleShowInfoView
|
||||
} : null,
|
||||
subscribed ? 'messages_clear' : null,
|
||||
subscribed && deleter ? 'messages_clear_hard' : null,
|
||||
muted ? (blocked ? null : 'topic_unmute') : 'topic_mute',
|
||||
@@ -1202,16 +1284,16 @@ class TinodeWeb extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
handleShowAlert(title, content, onConfirm, confirm, onReject, reject) {
|
||||
handleShowAlert(title, content, onConfirm, confirmText, onReject, rejectText) {
|
||||
this.setState({
|
||||
alertVisible: true,
|
||||
alertParams: {
|
||||
title: title,
|
||||
content: content,
|
||||
onConfirm: onConfirm,
|
||||
confirm: confirm,
|
||||
confirm: confirmText,
|
||||
onReject: onReject,
|
||||
reject: reject
|
||||
reject: rejectText
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1284,7 +1366,7 @@ class TinodeWeb extends React.Component {
|
||||
handleResetPassword(scheme, newPassword, token) {
|
||||
token = base64ReEncode(token);
|
||||
if (!token) {
|
||||
this.handleError("Invalid security token", 'err');
|
||||
this.handleError(this.props.intl.formatMessage({id: 'invalid_security_token'}), 'err');
|
||||
} else {
|
||||
this.tinode.connect()
|
||||
.then(() => {
|
||||
@@ -1355,30 +1437,36 @@ class TinodeWeb extends React.Component {
|
||||
messageSounds={this.state.messageSounds}
|
||||
desktopAlerts={this.state.desktopAlerts}
|
||||
desktopAlertsEnabled={this.state.desktopAlertsEnabled}
|
||||
incognitoMode={this.state.incognitoMode}
|
||||
serverAddress={this.state.serverAddress}
|
||||
onGlobalSettings={this.handleGlobalSettings}
|
||||
serverVersion={this.state.serverVersion}
|
||||
|
||||
onGlobalSettings={this.handleGlobalSettings}
|
||||
onSignUp={this.handleNewAccount}
|
||||
onSettings={this.handleSettings}
|
||||
onBasicNavigate={this.basicNavigator}
|
||||
onLoginRequest={this.handleLoginRequest}
|
||||
onCreateAccount={this.handleNewAccountRequest}
|
||||
onUpdateAccount={this.handleUpdateAccountRequest}
|
||||
onUpdateAccountTags={this.handleUpdateAccountTagsRequest}
|
||||
onTogglePushNotifications={this.togglePushToken}
|
||||
onToggleMessageSounds={this.handleToggleMessageSounds}
|
||||
onToggleIncognitoMode={this.handleToggleIncognitoMode}
|
||||
onCredAdd={this.handleCredAdd}
|
||||
onCredDelete={this.handleCredDelete}
|
||||
onCredConfirm={this.handleCredConfirm}
|
||||
onTopicSelected={this.handleTopicSelected}
|
||||
onCreateTopic={this.handleNewTopicRequest}
|
||||
onNewTopic={this.handleNewTopic}
|
||||
onLogout={this.handleLogout}
|
||||
onDeleteAccount={this.handleDeleteAccount}
|
||||
onShowAlert={this.handleShowAlert}
|
||||
onCancel={this.handleSidepanelCancel}
|
||||
onError={this.handleError}
|
||||
onValidateCredentials={this.handleValidateCredentialsRequest}
|
||||
onPasswordResetRequest={this.handlePasswordResetRequest}
|
||||
onResetPassword={this.handleResetPassword}
|
||||
onShowArchive={this.handleShowArchive}
|
||||
onShowBlocked={this.handleShowBlocked}
|
||||
|
||||
onInitFind={this.tnInitFind}
|
||||
searchResults={this.state.searchResults}
|
||||
|
||||
@@ -41,9 +41,10 @@ export default class AvatarUpload extends React.Component {
|
||||
render() {
|
||||
// Randomize id value in case more than one AvatarUpload is shown
|
||||
// at the same time.
|
||||
var randId = "file-input-avatar-" + (Math.random() + '').substr(2);
|
||||
const randId = 'file-input-avatar-' + (Math.random() + '').substr(2);
|
||||
const className = 'avatar-upload' + (this.props.readOnly ? ' read-only' : '');
|
||||
return (
|
||||
<div className="avatar-upload">
|
||||
<div className={className}>
|
||||
{this.props.readOnly || !this.state.dataUrl ?
|
||||
null :
|
||||
<a href="#" className="clear-avatar" onClick={(e) => {e.preventDefault(); this.props.onImageChanged(null);}}>
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
// The <- button to be displayed in title bars.
|
||||
import React from 'react';
|
||||
|
||||
export default class ButtonBack extends React.PureComponent {
|
||||
render() {
|
||||
return (
|
||||
<a href="#" onClick={(e) => {e.preventDefault(); this.props.onBack();}}>
|
||||
<i className="material-icons">arrow_back</i>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -81,7 +81,7 @@ export default class ChatMessage extends React.Component {
|
||||
|
||||
let content = this.props.content;
|
||||
const attachments = [];
|
||||
if (this.props.mimeType == Drafty.getContentType()) {
|
||||
if (this.props.mimeType == Drafty.getContentType() && Drafty.isValid(content)) {
|
||||
Drafty.attachments(content, function(att, i) {
|
||||
if (att.mime == 'application/json') {
|
||||
// Don't show json objects as attachments.
|
||||
@@ -106,10 +106,11 @@ export default class ChatMessage extends React.Component {
|
||||
defaultMessage="content deleted" description="Shown when messages are deleted" />
|
||||
</i></>
|
||||
} else if (typeof content != 'string') {
|
||||
content = <><i className="material-icons gray">error_outline</i> <i className="gray">
|
||||
content = <>
|
||||
<i className="material-icons gray">error_outline</i> <i className="gray">
|
||||
<FormattedMessage id="invalid_content"
|
||||
defaultMessage="invalid content" description="Shown when message is unreadable" />
|
||||
</i></>
|
||||
defaultMessage="invalid content" description="Shown when message is unreadable" /></i>
|
||||
</>
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -39,7 +39,7 @@ class ContactList extends React.Component {
|
||||
/>);
|
||||
} else {
|
||||
// Normal contact
|
||||
const key = c.topic ? c.topic : c.user;
|
||||
const key = this.props.showMode ? c.user : (c.topic || c.user);
|
||||
// If filter function is provided, filter out the items
|
||||
// which don't satisfy the condition.
|
||||
if (this.props.filterFunc && this.props.filter) {
|
||||
@@ -60,7 +60,7 @@ class ContactList extends React.Component {
|
||||
(this.props.topicSelected === key);
|
||||
const badges = [];
|
||||
if (this.props.showMode) {
|
||||
if (key === this.props.myUserId) {
|
||||
if (key == this.props.myUserId) {
|
||||
badges.push({name: formatMessage(messages.badge_you), color: 'green'});
|
||||
}
|
||||
if (c.acs && c.acs.isOwner()) {
|
||||
|
||||
@@ -198,7 +198,7 @@ class ContextMenu extends React.Component {
|
||||
console.log("Topic not found: ", params.topicName);
|
||||
return;
|
||||
}
|
||||
return topic.delTopic().catch((err) => {
|
||||
return topic.delTopic(true).catch((err) => {
|
||||
if (errorHandler) {
|
||||
errorHandler(err.message, 'err');
|
||||
}
|
||||
@@ -261,12 +261,12 @@ class ContextMenu extends React.Component {
|
||||
'member_block': {
|
||||
id: 'member_block',
|
||||
title: formatMessage(messages.block),
|
||||
handler: this.topicPermissionSetter.bind(this, '-J')
|
||||
handler: this.topicPermissionSetter.bind(this, '-JP')
|
||||
},
|
||||
'member_unblock': {
|
||||
id: 'member_unblock',
|
||||
title: formatMessage(messages.unblock),
|
||||
handler: this.topicPermissionSetter.bind(this, '+J')
|
||||
handler: this.topicPermissionSetter.bind(this, '+JP')
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -11,9 +11,9 @@ const messages = defineMessages({
|
||||
defaultMessage: 'You have no contacts :-(',
|
||||
description: 'Shown in ContactsView when the user has no contacts'
|
||||
},
|
||||
contacts_not_found: {
|
||||
contacts_not_found_short: {
|
||||
id: 'contacts_not_found_short',
|
||||
defaultMessage: "No contacts match '{query}'",
|
||||
defaultMessage: "No contacts match ''{query}''",
|
||||
description: 'Shown in ContactsView when search returned no results'
|
||||
}
|
||||
});
|
||||
@@ -112,7 +112,7 @@ class GroupManager extends React.Component {
|
||||
const {formatMessage} = this.props.intl;
|
||||
const msg = !val ?
|
||||
formatMessage(messages.no_contacts) :
|
||||
formatMessage(messages.contacts_not_found, {query: val});
|
||||
formatMessage(messages.contacts_not_found_short, {query: val});
|
||||
|
||||
this.setState({contactFilter: val, noContactsMessage: msg});
|
||||
}
|
||||
|
||||
@@ -4,7 +4,9 @@ import React from 'react';
|
||||
export default class MenuCancel extends React.PureComponent {
|
||||
render() {
|
||||
return (
|
||||
<a href="#" onClick={(e) => {e.preventDefault(); this.props.onCancel();}}><i className="material-icons">close</i></a>
|
||||
<a href="#" onClick={(e) => {e.preventDefault(); this.props.onCancel();}}>
|
||||
<i className="material-icons">close</i>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,13 +150,9 @@ class SendMessage extends React.PureComponent {
|
||||
formatMessage(messages.type_new_message));
|
||||
return (
|
||||
<div id="send-message-panel">
|
||||
{this.props.onAttachFile ?
|
||||
(this.props.disabled ?
|
||||
{!this.props.disabled ?
|
||||
<>
|
||||
<i className="material-icons disabled">photo</i>
|
||||
<i className="material-icons disabled">attach_file</i>
|
||||
</>
|
||||
:
|
||||
{this.props.onAttachFile ?
|
||||
<>
|
||||
<a href="#" onClick={(e) => {e.preventDefault(); this.attachImage.click();}} title="Add image">
|
||||
<i className="material-icons secondary">photo</i>
|
||||
@@ -164,25 +160,27 @@ class SendMessage extends React.PureComponent {
|
||||
<a href="#" onClick={(e) => {e.preventDefault(); this.attachFile.click();}} title="Attach file">
|
||||
<i className="material-icons secondary">attach_file</i>
|
||||
</a>
|
||||
</>)
|
||||
</>
|
||||
:
|
||||
null}
|
||||
{this.props.noInput ?
|
||||
<div className="hr thin" /> :
|
||||
<textarea id="sendMessage" placeholder={prompt}
|
||||
disabled={this.props.disabled} value={this.state.message}
|
||||
onChange={this.handleMessageTyping} onKeyPress={this.handleKeyPress}
|
||||
value={this.state.message} onChange={this.handleMessageTyping}
|
||||
onKeyPress={this.handleKeyPress}
|
||||
ref={(ref) => {this.messageEditArea = ref;}}
|
||||
autoFocus />}
|
||||
{this.props.disabled ?
|
||||
<i className="material-icons disabled">send</i> :
|
||||
<a href="#" onClick={this.handleSend} title="Send">
|
||||
<i className="material-icons">send</i>
|
||||
</a>}
|
||||
</a>
|
||||
<input type="file" ref={(ref) => {this.attachFile = ref;}}
|
||||
onChange={this.handleAttachFile} style={{display: 'none'}} />
|
||||
<input type="file" ref={(ref) => {this.attachImage = ref;}} accept="image/*"
|
||||
onChange={this.handleAttachImage} style={{display: 'none'}} />
|
||||
</>
|
||||
:
|
||||
<div id="writing-disabled">{prompt}</div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
|
||||
import LetterTile from './letter-tile.jsx';
|
||||
import MenuCancel from './menu-cancel.jsx';
|
||||
import ButtonBack from './button-back.jsx';
|
||||
import MenuContacts from './menu-contacts.jsx';
|
||||
import MenuStart from './menu-start.jsx';
|
||||
|
||||
@@ -9,20 +9,22 @@ export default class SideNavbar extends React.PureComponent {
|
||||
render() {
|
||||
return (
|
||||
<div id="side-caption-panel" className="caption-panel">
|
||||
{this.props.onCancel ? <ButtonBack onBack={this.props.onCancel} /> : null}
|
||||
{this.props.avatar ?
|
||||
<div id="self-avatar" className="avatar-box">
|
||||
<LetterTile
|
||||
avatar={this.props.avatar}
|
||||
topic={this.props.myUserId}
|
||||
title={this.props.title} />
|
||||
</div>
|
||||
:
|
||||
null}
|
||||
<div id="sidepanel-title" className="panel-title">{this.props.title}</div>
|
||||
{this.props.state === 'login' ?
|
||||
<MenuStart onSignUp={this.props.onSignUp} onSettings={this.props.onSettings} /> :
|
||||
this.props.state === 'contacts' ?
|
||||
<MenuContacts onNewTopic={this.props.onNewTopic} onSettings={this.props.onSettings} /> :
|
||||
null}
|
||||
{this.props.onCancel ?
|
||||
<MenuCancel onCancel={this.props.onCancel} /> : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import ChipInput from './chip-input.jsx';
|
||||
|
||||
import { MIN_TAG_LENGTH } from '../config.js';
|
||||
import { MAX_TAG_COUNT, MIN_TAG_LENGTH } from '../config.js';
|
||||
import { arrayEqual } from '../lib/utils.js';
|
||||
|
||||
export default class TagManager extends React.Component {
|
||||
@@ -48,7 +48,7 @@ export default class TagManager extends React.Component {
|
||||
}
|
||||
|
||||
handleAddTag(tag) {
|
||||
if (tag.length > 0) {
|
||||
if (tag.length > 0 && this.state.tags.length < MAX_TAG_COUNT) {
|
||||
const tags = this.state.tags.slice(0);
|
||||
tags.push(tag);
|
||||
|
||||
|
||||
+2921
-4559
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
+260
-118
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+17
-9
@@ -1,4 +1,4 @@
|
||||
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
|
||||
const TerserPlugin = require('terser-webpack-plugin');
|
||||
const CopyPlugin = require('copy-webpack-plugin');
|
||||
const path = require('path');
|
||||
|
||||
@@ -27,15 +27,23 @@ module.exports = (env, argv) => {
|
||||
},
|
||||
optimization: {
|
||||
minimizer: [
|
||||
// we specify a custom UglifyJsPlugin here to get source maps in production.
|
||||
new UglifyJsPlugin({
|
||||
cache: true,
|
||||
parallel: true,
|
||||
uglifyOptions: {
|
||||
compress: true,
|
||||
mangle: true
|
||||
new TerserPlugin({
|
||||
sourceMap: true,
|
||||
terserOptions: {
|
||||
ecma: undefined,
|
||||
warnings: false,
|
||||
parse: {},
|
||||
compress: {},
|
||||
mangle: true, // Note `mangle.properties` is `false` by default.
|
||||
module: false,
|
||||
output: null,
|
||||
toplevel: false,
|
||||
nameCache: null,
|
||||
ie8: false,
|
||||
keep_classnames: undefined,
|
||||
keep_fnames: false,
|
||||
safari10: false,
|
||||
},
|
||||
sourceMap: true
|
||||
})
|
||||
]
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user