Compare commits

...
46 Commits
Author SHA1 Message Date
Gene 1efc9a90cb Merge pull request #53 from kimtaeyoung-98/master
Korean translation of en.json
2020-06-01 20:11:33 +03:00
kimtaeyoung-98 de04006afc en.json문서의 한국어 번역
Removed spaces.
2020-06-02 01:37:13 +09:00
kimtaeyoung-98 d484bff407 Korean translation of en.json
Korean translation of en.json
2020-06-01 19:04:08 +09:00
or-else 7cd5f786b2 rebuilt with sdk fix: always close the socket 2020-05-24 11:04:48 +03:00
or-else 65c94c9f12 a few more i18n fixes 2020-05-10 14:26:36 +03:00
or-else c3e5b1cabc fix i18n formatting issues 2020-05-10 14:02:52 +03:00
or-else ad97c5638f rebuilt 2020-05-10 12:54:20 +03:00
or-else c9d05642e2 fix issue with scrolling on image preview 2020-05-10 12:53:50 +03:00
or-else 3410128d6f rebuilt 2020-05-09 09:25:47 +03:00
or-else b7a8d4d09d mute in addition to blocking 2020-05-08 11:06:53 +03:00
or-else 919047cfde define and use max tag count 2020-05-01 17:19:18 +03:00
or-else fbaf98c117 use standard inband limit 2020-04-29 15:17:13 +03:00
or-else 5d96331ef5 bump version and rebuild 2020-04-26 11:20:03 +03:00
or-else 587d457712 bump version and rebuild 2020-04-22 11:50:56 +03:00
or-else 979b283928 fix for hidden group-manager control panel 2020-04-21 21:16:54 +03:00
or-else 7c70dea398 bump version and rebuild 2020-04-20 12:01:26 +03:00
or-else 3f04d0e23b more localizations 2020-04-18 12:26:45 +03:00
or-else d953f6ac3e bump version and rebuild 2020-04-17 14:36:44 +03:00
or-else e5e78755ae blocked chats display 2020-04-17 14:30:26 +03:00
or-else 52c1016b45 fix disappearing [you] badge in topic members 2020-04-15 15:55:40 +03:00
or-else c0a3ba6a92 make topic and user deletions "hard" 2020-04-15 09:45:49 +03:00
or-else 6be7b87173 small refactoring of enabled permissions in topic 2020-04-15 09:45:15 +03:00
or-else 35a63ff5ab fix alert dialog 2020-04-13 19:00:38 +03:00
or-else 375750a6ff bump version and rebuild 2020-04-13 16:13:34 +03:00
or-else ea8ac3c293 refactoring of account settings into multiple screens 2020-04-13 16:01:24 +03:00
or-else 5507ccd728 refactoring account settings into multiple screens 2020-04-13 10:35:33 +03:00
or-else b96c8c2516 bump version and rebuild 2020-04-03 14:22:36 +03:00
or-else e04240870d rebuilt 2020-04-02 10:15:54 +03:00
or-else 506cd00b27 chrome bug was fixed 2020-04-02 08:18:38 +03:00
or-else cdd40facec support for incognito mode 2020-04-01 21:13:32 +03:00
or-else 6222610836 rebuilt with different background 2020-03-26 14:27:24 +03:00
or-else 16b5bdf37d Merge branch 'devel' of https://github.com/tinode/webapp into devel 2020-03-20 15:08:23 +03:00
or-else 6e39fe9bef use different background image for messages panel 2020-03-20 15:08:11 +03:00
Gene 78c2f1aaa1 Merge pull request #46 from mzch/master
Fix 2 bugs in Linux environment
2020-03-14 18:25:37 +03:00
Koichi MATSUMOTO 947b166935 move postcss-cli to devDependencies
move postcss-cli to devDependencies
2020-03-14 22:20:15 +09:00
Koichi MATSUMOTO 337afcf075 fix lack of postcss-cli
fix lack of postcss-cli
2020-03-14 20:08:04 +09:00
Koichi MATSUMOTO 77e67bb714 fix typo
fix typo
2020-03-14 20:02:40 +09:00
or-else dbcac419de replaced UglifyJsPlugin with TerserPlugin 2020-03-14 09:39:52 +03:00
or-else 475e5eb3e7 dependencies updated 2020-03-14 09:09:27 +03:00
or-else 12f05e8570 bump version and rebuild 2020-03-08 16:28:45 +03:00
or-else 527a01bebb critical bug: login stalls after refresh 2020-02-27 19:54:32 +03:00
or-else ffe2ab7737 bump version and rebuild 2020-02-25 09:30:06 +03:00
or-else 2fd8346dc5 check Drafty for validity 2020-02-23 21:44:08 +03:00
or-else fd4cb58f0c more prominent "messaging disabled" note 2020-02-23 20:46:22 +03:00
or-else d2647d5f0c fix: incorrect redirect on failed sign up 2020-02-23 13:26:24 +03:00
or-else e27c106c33 pass himan UI language to tinode-sdk 2020-02-23 10:57:04 +03:00
45 changed files with 11010 additions and 11008 deletions
+1
View File
@@ -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
View File
@@ -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 {
+1 -1
View File
File diff suppressed because one or more lines are too long
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

+2 -2
View File
@@ -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
View File
@@ -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>
+2599 -1801
View File
File diff suppressed because it is too large Load Diff
+18 -17
View File
@@ -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"
}
}
+38 -12
View File
@@ -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)
@@ -86,7 +105,14 @@ self.addEventListener('fetch', event => {
})
.catch((err) => {
// Something went wrong.
console.log("Service worker Fetch: ", err);
console.log("Service worker Fetch:", err);
})
);
});
// 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
View File
@@ -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
View File
@@ -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",
+173
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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";
+186
View File
@@ -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>
);
}
};
+61
View File
@@ -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>
);
}
};
+155
View File
@@ -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> &nbsp;<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> &nbsp;<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>&nbsp;
<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);
+49
View File
@@ -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> &nbsp;<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> &nbsp;<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> &nbsp;<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>
);
}
};
+14 -2
View File
@@ -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
View File
@@ -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>&nbsp;
<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> &nbsp;<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>&nbsp;
<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> &nbsp;<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>&nbsp;<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> &nbsp;<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>&nbsp;<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> &nbsp;<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>&nbsp;<FormattedMessage id="sidepanel_title_acc_support" />
</a>
</div>
</div>
</div>
}</React.Fragment>
);
}
};
+3 -1
View File
@@ -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;
+2 -2
View File
@@ -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" />
+5 -2
View File
@@ -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} />
);
+70 -11
View File
@@ -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
View File
@@ -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}
+3 -2
View File
@@ -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);}}>
+12
View File
@@ -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>
);
}
}
+5 -4
View File
@@ -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 (
+2 -2
View File
@@ -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()) {
+3 -3
View File
@@ -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')
},
};
}
+3 -3
View File
@@ -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});
}
+3 -1
View File
@@ -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>
);
}
}
+10 -12
View File
@@ -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>
);
}
+5 -3
View File
@@ -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>
);
}
+2 -2
View File
@@ -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);
+2924 -4562
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+260 -118
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+17 -9
View File
@@ -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
})
]
},