Compare commits
41
Commits
79970cf558
...
53d748a7bd
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
53d748a7bd | ||
|
|
6cb2b77f69 | ||
|
|
d133287e7e | ||
|
|
3ef14a6fe6 | ||
|
|
f84096488a | ||
|
|
884a822408 | ||
|
|
fe000cec33 | ||
|
|
3202dcb1b0 | ||
|
|
eb20ebff6e | ||
|
|
a0e8aa5c54 | ||
|
|
00e2642aa9 | ||
|
|
0396a617c8 | ||
|
|
9a55e4b72e | ||
|
|
60a88bc3d5 | ||
|
|
e73c41f3c6 | ||
|
|
4b561de04c | ||
|
|
35ec5b98cf | ||
|
|
4bd0052d9f | ||
|
|
545b0c79cb | ||
|
|
c66bf89883 | ||
|
|
60fa6d7806 | ||
|
|
194306ac4f | ||
|
|
f29305b0af | ||
|
|
1ac74dbe36 | ||
|
|
c76953294c | ||
|
|
1458184fc2 | ||
|
|
14b63f4627 | ||
|
|
9492455bc3 | ||
|
|
32ad99128e | ||
|
|
64f1fd4380 | ||
|
|
6386fc7a46 | ||
|
|
29425b48fc | ||
|
|
a9ba11a0ff | ||
|
|
d13b24263e | ||
|
|
0f13f2edb9 | ||
|
|
a042db27e8 | ||
|
|
ff619da2b7 | ||
|
|
93e6d388b6 | ||
|
|
173d153b82 | ||
|
|
0ddb3a4f75 | ||
|
|
9193b9be19 |
@@ -11,10 +11,6 @@ Although the app is generally usable, keep in mind that this is work in progress
|
||||
|
||||
Try possibly newer or older version live at https://api.tinode.co/. For passwords for demo accounts and other instructions [see here](https://github.com/tinode/chat/#demo).
|
||||
|
||||
Demo avatar photos are from https://www.pexels.com/ under [CC0 license](https://www.pexels.com/photo-license/).
|
||||
|
||||
Background pattern from http://subtlepatterns.com/, commercial and non-commercial use allowed with attribution.
|
||||
|
||||
## Getting support
|
||||
|
||||
* Read [client-side](http://tinode.github.io/js-api/) and [server-side](https://github.com/tinode/chat/blob/master/docs/API.md) API documentation.
|
||||
@@ -25,7 +21,7 @@ Background pattern from http://subtlepatterns.com/, commercial and non-commercia
|
||||
|
||||
The app is fully internationalized using [React-Intl](https://github.com/yahoo/react-intl). The UI language is selected automatically from the language specified by the browser. A specific language can be forced by adding `hl=XX` parameter to the URL, i.e. https://api.tinode.co/#?hl=ru.
|
||||
|
||||
As of the time of this writing there are only two translations. More translations are welcome. Send a pull request with a json file with translated strings. Take a look at [English](/src/i18n/en.json) and [Russian](/src/i18n/ru.json) translations for guidance.
|
||||
As of the time of this writing the following translations exist: English, Russian, Simplified Chinese. More translations are welcome. Send a pull request with a json file with translated strings. Take a look at [English](/src/i18n/en.json), [Russian](/src/i18n/ru.json), or [Simplified Chinese](/src/i18n/zh-CN.json) translations for guidance.
|
||||
|
||||
|
||||
## Missing functionality
|
||||
@@ -34,6 +30,13 @@ As of the time of this writing there are only two translations. More translation
|
||||
* No end-to-end encryption.
|
||||
* Emoji support is weak.
|
||||
|
||||
## Other
|
||||
|
||||
Demo avatars and some other graphics are from https://www.pexels.com/ under [CC0 license](https://www.pexels.com/photo-license/).
|
||||
|
||||
Background patterns from http://subtlepatterns.com/, commercial and non-commercial use allowed with attribution.
|
||||
|
||||
|
||||
## Responsive design
|
||||
|
||||
### Desktop screenshot
|
||||
|
||||
+68
-7
@@ -199,12 +199,22 @@ input[type="search"]:focus {
|
||||
box-shadow: 0 1px 0 rgba(33, 150, 243, 0.25);
|
||||
}
|
||||
|
||||
textarea.invalid,
|
||||
input[type="text"].invalid,
|
||||
input[type="email"].invalid,
|
||||
input[type="password"].invalid,
|
||||
input[type="search"].invalid {
|
||||
border-bottom: 1px solid #e57373;
|
||||
box-shadow: 0 1px 0 rgba(229, 115, 115, 0.25);
|
||||
}
|
||||
|
||||
input[type="text"]::placeholder,
|
||||
input[type="email"]::placeholder,
|
||||
input[type="password"]::placeholder,
|
||||
input[type="search"]::placeholder,
|
||||
textarea::placeholder {
|
||||
color: #bbb;
|
||||
font-weight: lighter;
|
||||
}
|
||||
|
||||
input[type="text"].with-visibility,
|
||||
@@ -302,6 +312,10 @@ li.right:hover .menuTrigger {
|
||||
color: #D32F2F; /* red 700 */
|
||||
}
|
||||
|
||||
.amber {
|
||||
color: #FFC107; /* amber 500 */
|
||||
}
|
||||
|
||||
.gray {
|
||||
color: #757575;
|
||||
}
|
||||
@@ -317,7 +331,7 @@ li.right:hover .menuTrigger {
|
||||
.hr {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 0.6rem;
|
||||
height: 0.45rem;
|
||||
min-height: 0.3rem;
|
||||
background-color: #f0f0f0;
|
||||
box-shadow: inset 0 0.25rem 0.25rem -0.25rem rgba(0,0,0,0.15),
|
||||
@@ -515,6 +529,11 @@ table.permission-editor td.checkbox {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.clean-clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
|
||||
/* END of top-level layout */
|
||||
|
||||
/* Sidepanel styles */
|
||||
@@ -722,6 +741,9 @@ i.search {
|
||||
border-collapse: collapse;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
.contact-box > li:last-child {
|
||||
border-bottom: none; /* remove border from the last contact */
|
||||
}
|
||||
.contact-box > li:hover {
|
||||
background-color: #eceff1; /* blue-gray 50 highlight */
|
||||
}
|
||||
@@ -1040,6 +1062,17 @@ i.search {
|
||||
margin-right: 0.2rem;
|
||||
}
|
||||
|
||||
#topic-users .avatar-box > .material-icons {
|
||||
font-size: 1.4rem;
|
||||
line-height: 1.8rem;
|
||||
}
|
||||
|
||||
/* Overflow span "+18 more" */
|
||||
#topic-users > span {
|
||||
line-height: 2rem;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
/* Scrollable panel where the messages are shown */
|
||||
#messages-panel {
|
||||
position: relative;
|
||||
@@ -1068,18 +1101,35 @@ i.search {
|
||||
|
||||
#write-only-note {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background-color: white;
|
||||
color: #666;
|
||||
height: auto;
|
||||
width: auto;
|
||||
line-height: 2rem;
|
||||
text-align: center;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
padding: 0.25rem 1.5rem;
|
||||
box-shadow: 0.1rem 0.1rem 0.1rem rgba(0,0,0,0.2);
|
||||
padding: 0.75rem 1.25rem 0.75rem 1.25rem;
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: 0 1px 0.5px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
#peer-messaging-disabled-note {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
bottom: 4.3rem;
|
||||
transform: translate(-50%);
|
||||
background-color: white;
|
||||
color: #666;
|
||||
width: fit-content;
|
||||
line-height: 2rem;
|
||||
text-align: center;
|
||||
padding: 0.75rem 1.25rem 0.75rem 1.25rem;
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: 0 1px 0.5px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
/* Footer-panel with the input for the new message */
|
||||
#send-message-panel {
|
||||
display: flex;
|
||||
@@ -1227,7 +1277,7 @@ i.search {
|
||||
display: flex;
|
||||
position: relative;
|
||||
max-width: 40rem;
|
||||
min-width: 12rem;
|
||||
min-width: 9rem;
|
||||
height: auto;
|
||||
padding: 0.75rem 0.5rem 0.75rem 1.25rem;
|
||||
margin: 0;
|
||||
@@ -1499,6 +1549,12 @@ ul.tabbar li.active > a:after {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.material-icons.as-badge {
|
||||
padding-left: 0.2rem;
|
||||
font-size: 1.25rem;
|
||||
color: #bbb;
|
||||
}
|
||||
|
||||
/* Badge with a label, like [you], [muted], etc */
|
||||
.badge {
|
||||
font-family: 'Roboto Mono', Courier, monospace, sans-serif;
|
||||
@@ -1750,6 +1806,11 @@ ul.tabbar li.active > a:after {
|
||||
font-size: 90%;
|
||||
}
|
||||
|
||||
.chip.invalid {
|
||||
color: #c66;
|
||||
background-color: #fee;
|
||||
}
|
||||
|
||||
.chip > a {
|
||||
display: inline-block;
|
||||
color: #ccc;
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
+3
-3
@@ -18,7 +18,7 @@
|
||||
<link rel="apple-touch-icon" sizes="192x192" href="img/logo192.png" type="image/png" />
|
||||
<link rel="canonical" href="https://api.tinode.co/" />
|
||||
<!-- Google's Roboto font -->
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,400italic,500,700" />
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,400i,500,700,700i" />
|
||||
<!-- Google's material design icons -->
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons" />
|
||||
<!-- Styles for local elements -->
|
||||
@@ -30,8 +30,8 @@
|
||||
<script crossorigin="anonymous" src="https://unpkg.com/prop-types@15/prop-types.js"></script>
|
||||
<script crossorigin="anonymous" src="https://unpkg.com/react-intl@2/dist/react-intl.js"></script>
|
||||
<!-- Firebase scripts -->
|
||||
<script crossorigin="anonymous" src="https://unpkg.com/firebase@5.8.1/firebase-app.js"></script>
|
||||
<script crossorigin="anonymous" src="https://unpkg.com/firebase@5.8.1/firebase-messaging.js"></script>
|
||||
<script crossorigin="anonymous" src="https://unpkg.com/firebase@5.8.6/firebase-app.js"></script>
|
||||
<script crossorigin="anonymous" src="https://unpkg.com/firebase@5.8.6/firebase-messaging.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
+6
-6
@@ -13,15 +13,15 @@
|
||||
<link rel="shortcut icon" id="shortcut-icon" href="img/logo32x32.png" type="image/png" />
|
||||
<link rel="apple-touch-icon" sizes="192x192" href="img/logo192.png" type="image/png" />
|
||||
<link rel="canonical" href="https://api.tinode.co/" />
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,400italic,500,700" />
|
||||
<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://unpkg.com/react@16.8.1/umd/react.production.min.js"></script>
|
||||
<script crossorigin="anonymous" src="https://unpkg.com/react-dom@16.8.1/umd/react-dom.production.min.js"></script>
|
||||
<script crossorigin="anonymous" src="https://unpkg.com/prop-types@15.6.2/prop-types.min.js"></script>
|
||||
<script crossorigin="anonymous" src="https://unpkg.com/react@16.8.4/umd/react.production.min.js"></script>
|
||||
<script crossorigin="anonymous" src="https://unpkg.com/react-dom@16.8.4/umd/react-dom.production.min.js"></script>
|
||||
<script crossorigin="anonymous" src="https://unpkg.com/prop-types@15.7.2/prop-types.min.js"></script>
|
||||
<script crossorigin="anonymous" src="https://unpkg.com/react-intl@2.8.0/dist/react-intl.min.js"></script>
|
||||
<script crossorigin="anonymous" src="https://unpkg.com/firebase@5.8.1/firebase-app.js"></script>
|
||||
<script crossorigin="anonymous" src="https://unpkg.com/firebase@5.8.1/firebase-messaging.js"></script>
|
||||
<script crossorigin="anonymous" src="https://unpkg.com/firebase@5.8.6/firebase-app.js"></script>
|
||||
<script crossorigin="anonymous" src="https://unpkg.com/firebase@5.8.6/firebase-messaging.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<noscript><div id="noscript-alertbox"><div id="noscript-title">Javascript support is required.</div>
|
||||
|
||||
Generated
+454
-1404
File diff suppressed because it is too large
Load Diff
+11
-11
@@ -1,18 +1,18 @@
|
||||
{
|
||||
"name": "tinode-webapp",
|
||||
"description": "Tinode messenger for the web",
|
||||
"version": "0.15.13-rc3",
|
||||
"version": "0.15.15-rc2",
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.2.2",
|
||||
"@babel/preset-env": "^7.3.1",
|
||||
"@babel/core": "^7.3.4",
|
||||
"@babel/preset-env": "^7.3.4",
|
||||
"@babel/preset-react": "^7.0.0",
|
||||
"babel-loader": "^8.0.5",
|
||||
"babel-plugin-react-intl": "^3.0.1",
|
||||
"browserslist": "^4.4.1",
|
||||
"cssnano": "^4.1.8",
|
||||
"postcss": "^7.0.6",
|
||||
"uglifyjs-webpack-plugin": "^2.1.1",
|
||||
"webpack": "^4.29.3",
|
||||
"cssnano": "^4.1.10",
|
||||
"postcss": "^7.0.17",
|
||||
"uglifyjs-webpack-plugin": "^2.1.2",
|
||||
"webpack": "^4.29.6",
|
||||
"webpack-cli": "^3.2.3"
|
||||
},
|
||||
"repository": {
|
||||
@@ -77,10 +77,10 @@
|
||||
"author": "Tinode Authors <info@tinode.co>",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"firebase": "^5.8.1",
|
||||
"react": "^16.8.1",
|
||||
"react-dom": "^16.8.1",
|
||||
"firebase": "^5.8.6",
|
||||
"react": "^16.8.4",
|
||||
"react-dom": "^16.8.4",
|
||||
"react-intl": "^2.8.0",
|
||||
"tinode-sdk": "^0.15.12"
|
||||
"tinode-sdk": "^0.15.14"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,12 @@ export const READ_DELAY = 1000;
|
||||
// The shortest allowed tag length. Matches the value on the server.
|
||||
export const MIN_TAG_LENGTH = 4;
|
||||
|
||||
// Access mode for subscriptions initiated by the current user.
|
||||
export const DEFAULT_ACCESS_MODE = 'JRWPS';
|
||||
|
||||
// Access mode for no access.
|
||||
export const NO_ACCESS_MODE = 'N';
|
||||
|
||||
// Mediaquery breakpoint between desktop and mobile, in px. Should match the value
|
||||
// in @media (max-size: 640px) in base.css
|
||||
export const MEDIA_BREAKPOINT = 640;
|
||||
@@ -53,3 +59,7 @@ export const MAX_EXTERN_ATTACHMENT_SIZE = 1 << 23;
|
||||
// Maximum allowed linear dimension of an inline image in pixels. You may want
|
||||
// to adjust it to 1600 or 2400 for production.
|
||||
export const MAX_IMAGE_DIM = 768;
|
||||
|
||||
// Maximum number of online users to be shown in a topic title bar. Others will be
|
||||
// hidden under "+X more"
|
||||
export const MAX_ONLINE_IN_TOPIC = 4;
|
||||
|
||||
+7
-3
@@ -7,12 +7,13 @@
|
||||
"label_your_name": "Your name",
|
||||
"label_password": "Password",
|
||||
"password_unchanged_prompt": "Unchanged",
|
||||
"label_user_id": "Address:",
|
||||
"label_default_access_mode": "Default access mode:",
|
||||
"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",
|
||||
"requested_permissions": "Requested",
|
||||
"granted_permissions": "Granted",
|
||||
@@ -45,6 +46,8 @@
|
||||
"title_not_found": "Not found",
|
||||
"unnamed_topic": "Unnamed",
|
||||
"messages_not_readable": "no access to messages",
|
||||
"peers_messaging_disabled": "Peer's messaging is disabled.",
|
||||
"enable_peers_messaging": "Enable",
|
||||
"tabtitle_find_user": "find",
|
||||
"tabtitle_new_group": "new group",
|
||||
"tabtitle_group_by_id": "by id",
|
||||
@@ -66,7 +69,7 @@
|
||||
"sidepanel_title_reset": "Reset Password",
|
||||
"sidepanel_title_archive": "Archived Chats",
|
||||
"update_available": "Update available. <a href=\"\">Reload</a>.",
|
||||
"reconnect_countdown": "Disconnected. Reconnecting in {seconds}s…",
|
||||
"reconnect_countdown": "Disconnected. Reconnecting in {seconds}…",
|
||||
"reconnect_now": "Try now",
|
||||
"phone_dative": "phone",
|
||||
"email_dative": "email",
|
||||
@@ -98,6 +101,7 @@
|
||||
"title_all_contacts": "All Contacts",
|
||||
"button_ok": "OK",
|
||||
"button_cancel": "Cancel",
|
||||
"more_online_members": "+{overflow} more",
|
||||
"download_action": "download",
|
||||
"label_file_name": "File name:",
|
||||
"label_content_type": "Content type:",
|
||||
|
||||
+7
-3
@@ -43,7 +43,7 @@
|
||||
"title_manage_tags": "Редактор тагов",
|
||||
"message_sending": "в пути...",
|
||||
"message_sending_failed": "ошибка",
|
||||
"search_placeholder": "Список, напр. email:alice@example.com, tel:17025550003...",
|
||||
"search_placeholder": "Список, напр. email:alice@example.com, tel:+17025550003...",
|
||||
"messaging_disabled_prompt": "Отправка недоступна",
|
||||
"new_message_prompt": "Новое сообщение",
|
||||
"file_attachment_too_large": "Размер файла {size} превышает {limit} лимит.",
|
||||
@@ -120,7 +120,7 @@
|
||||
"badge_owner": "влад.",
|
||||
"update_available": "Есть новая версия приложения. <a href=\"\">Обновить</a>.",
|
||||
"user_not_found": "Не найден",
|
||||
"reconnect_countdown": "Нет связи. Подключение через {seconds}с…",
|
||||
"reconnect_countdown": "Нет связи. Подключение через {seconds}…",
|
||||
"reconnect_now": "Подключить сейчас.",
|
||||
"save_attachment": "сохранить",
|
||||
"menu_item_archive_topic": "В архив",
|
||||
@@ -129,5 +129,9 @@
|
||||
"chat_invitation": "Вас пригласили начать новый чат. Как вы хотите поступить?",
|
||||
"chat_invitation_accept": "Принять",
|
||||
"chat_invitation_ignore": "Игнорировать",
|
||||
"chat_invitation_block": "Заблокировать"
|
||||
"chat_invitation_block": "Заблокировать",
|
||||
"peers_messaging_disabled": "Чат заблокирован у корреспондента.",
|
||||
"enable_peers_messaging": "Разблокировать.",
|
||||
"more_online_members": "+еще {overflow}",
|
||||
"label_user_contacts": "Конакты"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
{
|
||||
"archived_contacts": "已归档联系人 ({count})",
|
||||
"contacts_not_found": "你尚无会话<br />¯∖_(ツ)_/¯",
|
||||
"full_name_prompt": "全名,例如张伟",
|
||||
"email_prompt": "电子邮件,例如 zhang@example.com",
|
||||
"button_sign_up": "注册",
|
||||
"label_your_name": "你的姓名",
|
||||
"label_password": "密码",
|
||||
"password_unchanged_prompt": "未改变",
|
||||
"label_user_id": "地址:",
|
||||
"label_default_access_mode": "蓦然访问模式:",
|
||||
"label_message_sound": "消息提示音:",
|
||||
"label_push_notifications": "通知提醒:",
|
||||
"label_push_notifications_disabled": "通知提醒(需要 HTTPS):",
|
||||
"title_tag_manager": "标签(用户发现)",
|
||||
"button_logout": "登出",
|
||||
"requested_permissions": "已请求",
|
||||
"granted_permissions": "已授予",
|
||||
"menu_item_edit_permissions": "编辑权限",
|
||||
"label_other_user": "其他",
|
||||
"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": "添加成员",
|
||||
"button_leave": "离开",
|
||||
"group_has_no_members": "无成员",
|
||||
"action_leave_chat": "离开",
|
||||
"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": "未命名",
|
||||
"messages_not_readable": "无消息访问权限",
|
||||
"peers_messaging_disabled": "成员间消息已禁用。",
|
||||
"enable_peers_messaging": "启用",
|
||||
"tabtitle_find_user": "搜索",
|
||||
"tabtitle_new_group": "新群组",
|
||||
"tabtitle_group_by_id": "通过 id",
|
||||
"search_for_contacts": "使用搜索寻找联系人",
|
||||
"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_edit_account": "编辑账户",
|
||||
"sidepanel_title_newtpk": "开始新会话",
|
||||
"sidepanel_title_cred": "确认凭据",
|
||||
"sidepanel_title_reset": "重置密码",
|
||||
"sidepanel_title_archive": "已存档会话",
|
||||
"update_available": "更新可用。<a href=\"\">重新载入</a>.",
|
||||
"reconnect_countdown": "连接已断开。{seconds} 秒后重新连接…",
|
||||
"reconnect_now": "立即尝试",
|
||||
"phone_dative": "电话",
|
||||
"email_dative": "电子邮件",
|
||||
"enter_confirmation_code_prompt": "输入通过{method}发送的验证码:",
|
||||
"numeric_confirmation_code_prompt": "仅数字",
|
||||
"button_confirm": "确认",
|
||||
"save_attachment": "保存",
|
||||
"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_mute": "静音",
|
||||
"menu_item_unmute": "取消静音",
|
||||
"menu_item_delete_topic": "删除",
|
||||
"menu_item_unblock": "取消屏蔽",
|
||||
"menu_item_block": "屏蔽",
|
||||
"menu_item_member_delete": "移除",
|
||||
"menu_item_archive_topic": "归档",
|
||||
"action_cancel": "取消",
|
||||
"upload_finishing": "正在结束...",
|
||||
"no_contacts": "你尚无联系人 :-(",
|
||||
"contacts_not_found_short": "无联系人匹配 '{query}'",
|
||||
"title_group_members": "群组成员",
|
||||
"title_all_contacts": "全部联系人",
|
||||
"button_ok": "好",
|
||||
"button_cancel": "取消",
|
||||
"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": "列表如 email:alice@example.com, tel:+17025550003...",
|
||||
"messaging_disabled_prompt": "消息已禁用",
|
||||
"new_message_prompt": "新消息",
|
||||
"file_attachment_too_large": "文件大小 {size} 超过 {limit} 限制。",
|
||||
"cannot_initiate_file_upload": "无法初始化文件上传。",
|
||||
"tags_not_found": "尚未定义标签。添加一些。",
|
||||
"tags_editor_no_tags": "添加一些标签",
|
||||
"title_manage_tags": "管理标签",
|
||||
"more_online_members": "还有{overflow}个",
|
||||
"label_user_contacts": "往来"
|
||||
}
|
||||
@@ -14,6 +14,14 @@ export function shortDateFormat(then, locale) {
|
||||
{hour12: false, year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit'});
|
||||
}
|
||||
|
||||
// Convert seconds to minutes:seconds, i.e. 156 sec -> 2:36.
|
||||
export function secondsToTime(seconds) {
|
||||
const min = Math.floor(seconds / 60);
|
||||
let sec = seconds % 60;
|
||||
sec = sec < 10 ? `0${sec}` : sec;
|
||||
return `${min}:${sec}`;
|
||||
}
|
||||
|
||||
// Convert a number of bytes to human-readable format.
|
||||
export function bytesToHumanSize(bytes) {
|
||||
if (!bytes || bytes == 0) {
|
||||
|
||||
@@ -61,3 +61,22 @@ export function arrayEqual(a, b) {
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Checks (loosely) if the given string is a phone. If so, returns the phone number in a format
|
||||
// as close to E.164 as possible.
|
||||
export function asPhone(val) {
|
||||
val = val.trim();
|
||||
if (/^\s*(?:\+?(\d{1,3}))?[- (.]*(\d{3})[- ).]*(\d{3})[- .]*(\d{2})[- .]*(\d{2})?\s*$/.test(val)) {
|
||||
return val.replace(/[- ().]*/, '');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Checks (loosely) if the given string is an email. If so returns the email.
|
||||
export function asEmail(val) {
|
||||
val = val.trim();
|
||||
if (/^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$/.test(val)) {
|
||||
return val;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
+150
-6
@@ -8,12 +8,13 @@
|
||||
"label_your_name": "Your name",
|
||||
"label_password": "Password",
|
||||
"password_unchanged_prompt": "Unchanged",
|
||||
"label_user_id": "Address:",
|
||||
"label_default_access_mode": "Default access mode:",
|
||||
"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",
|
||||
"requested_permissions": "Requested",
|
||||
"granted_permissions": "Granted",
|
||||
@@ -46,6 +47,8 @@
|
||||
"title_not_found": "Not found",
|
||||
"unnamed_topic": "Unnamed",
|
||||
"messages_not_readable": "no access to messages",
|
||||
"peers_messaging_disabled": "Peer's messaging is disabled.",
|
||||
"enable_peers_messaging": "Enable",
|
||||
"tabtitle_find_user": "find",
|
||||
"tabtitle_new_group": "new group",
|
||||
"tabtitle_group_by_id": "by id",
|
||||
@@ -67,7 +70,7 @@
|
||||
"sidepanel_title_reset": "Reset Password",
|
||||
"sidepanel_title_archive": "Archived Chats",
|
||||
"update_available": "Update available. <a href=\"\">Reload</a>.",
|
||||
"reconnect_countdown": "Disconnected. Reconnecting in {seconds}s…",
|
||||
"reconnect_countdown": "Disconnected. Reconnecting in {seconds}…",
|
||||
"reconnect_now": "Try now",
|
||||
"phone_dative": "phone",
|
||||
"email_dative": "email",
|
||||
@@ -99,6 +102,7 @@
|
||||
"title_all_contacts": "All Contacts",
|
||||
"button_ok": "OK",
|
||||
"button_cancel": "Cancel",
|
||||
"more_online_members": "+{overflow} more",
|
||||
"download_action": "download",
|
||||
"label_file_name": "File name:",
|
||||
"label_content_type": "Content type:",
|
||||
@@ -177,7 +181,7 @@
|
||||
"title_manage_tags": "Редактор тагов",
|
||||
"message_sending": "в пути...",
|
||||
"message_sending_failed": "ошибка",
|
||||
"search_placeholder": "Список, напр. email:alice@example.com, tel:17025550003...",
|
||||
"search_placeholder": "Список, напр. email:alice@example.com, tel:+17025550003...",
|
||||
"messaging_disabled_prompt": "Отправка недоступна",
|
||||
"new_message_prompt": "Новое сообщение",
|
||||
"file_attachment_too_large": "Размер файла {size} превышает {limit} лимит.",
|
||||
@@ -254,7 +258,7 @@
|
||||
"badge_owner": "влад.",
|
||||
"update_available": "Есть новая версия приложения. <a href=\"\">Обновить</a>.",
|
||||
"user_not_found": "Не найден",
|
||||
"reconnect_countdown": "Нет связи. Подключение через {seconds}с…",
|
||||
"reconnect_countdown": "Нет связи. Подключение через {seconds}…",
|
||||
"reconnect_now": "Подключить сейчас.",
|
||||
"save_attachment": "сохранить",
|
||||
"menu_item_archive_topic": "В архив",
|
||||
@@ -263,6 +267,146 @@
|
||||
"chat_invitation": "Вас пригласили начать новый чат. Как вы хотите поступить?",
|
||||
"chat_invitation_accept": "Принять",
|
||||
"chat_invitation_ignore": "Игнорировать",
|
||||
"chat_invitation_block": "Заблокировать"
|
||||
"chat_invitation_block": "Заблокировать",
|
||||
"peers_messaging_disabled": "Чат заблокирован у корреспондента.",
|
||||
"enable_peers_messaging": "Разблокировать.",
|
||||
"more_online_members": "+еще {overflow}",
|
||||
"label_user_contacts": "Конакты"
|
||||
},
|
||||
"zh-CN": {
|
||||
"archived_contacts": "已归档联系人 ({count})",
|
||||
"contacts_not_found": "你尚无会话<br />¯∖_(ツ)_/¯",
|
||||
"full_name_prompt": "全名,例如张伟",
|
||||
"email_prompt": "电子邮件,例如 zhang@example.com",
|
||||
"button_sign_up": "注册",
|
||||
"label_your_name": "你的姓名",
|
||||
"label_password": "密码",
|
||||
"password_unchanged_prompt": "未改变",
|
||||
"label_user_id": "地址:",
|
||||
"label_default_access_mode": "蓦然访问模式:",
|
||||
"label_message_sound": "消息提示音:",
|
||||
"label_push_notifications": "通知提醒:",
|
||||
"label_push_notifications_disabled": "通知提醒(需要 HTTPS):",
|
||||
"title_tag_manager": "标签(用户发现)",
|
||||
"button_logout": "登出",
|
||||
"requested_permissions": "已请求",
|
||||
"granted_permissions": "已授予",
|
||||
"menu_item_edit_permissions": "编辑权限",
|
||||
"label_other_user": "其他",
|
||||
"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": "添加成员",
|
||||
"button_leave": "离开",
|
||||
"group_has_no_members": "无成员",
|
||||
"action_leave_chat": "离开",
|
||||
"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": "未命名",
|
||||
"messages_not_readable": "无消息访问权限",
|
||||
"peers_messaging_disabled": "成员间消息已禁用。",
|
||||
"enable_peers_messaging": "启用",
|
||||
"tabtitle_find_user": "搜索",
|
||||
"tabtitle_new_group": "新群组",
|
||||
"tabtitle_group_by_id": "通过 id",
|
||||
"search_for_contacts": "使用搜索寻找联系人",
|
||||
"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_edit_account": "编辑账户",
|
||||
"sidepanel_title_newtpk": "开始新会话",
|
||||
"sidepanel_title_cred": "确认凭据",
|
||||
"sidepanel_title_reset": "重置密码",
|
||||
"sidepanel_title_archive": "已存档会话",
|
||||
"update_available": "更新可用。<a href=\"\">重新载入</a>.",
|
||||
"reconnect_countdown": "连接已断开。{seconds} 秒后重新连接…",
|
||||
"reconnect_now": "立即尝试",
|
||||
"phone_dative": "电话",
|
||||
"email_dative": "电子邮件",
|
||||
"enter_confirmation_code_prompt": "输入通过{method}发送的验证码:",
|
||||
"numeric_confirmation_code_prompt": "仅数字",
|
||||
"button_confirm": "确认",
|
||||
"save_attachment": "保存",
|
||||
"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_mute": "静音",
|
||||
"menu_item_unmute": "取消静音",
|
||||
"menu_item_delete_topic": "删除",
|
||||
"menu_item_unblock": "取消屏蔽",
|
||||
"menu_item_block": "屏蔽",
|
||||
"menu_item_member_delete": "移除",
|
||||
"menu_item_archive_topic": "归档",
|
||||
"action_cancel": "取消",
|
||||
"upload_finishing": "正在结束...",
|
||||
"no_contacts": "你尚无联系人 :-(",
|
||||
"contacts_not_found_short": "无联系人匹配 '{query}'",
|
||||
"title_group_members": "群组成员",
|
||||
"title_all_contacts": "全部联系人",
|
||||
"button_ok": "好",
|
||||
"button_cancel": "取消",
|
||||
"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": "列表如 email:alice@example.com, tel:+17025550003...",
|
||||
"messaging_disabled_prompt": "消息已禁用",
|
||||
"new_message_prompt": "新消息",
|
||||
"file_attachment_too_large": "文件大小 {size} 超过 {limit} 限制。",
|
||||
"cannot_initiate_file_upload": "无法初始化文件上传。",
|
||||
"tags_not_found": "尚未定义标签。添加一些。",
|
||||
"tags_editor_no_tags": "添加一些标签",
|
||||
"title_manage_tags": "管理标签",
|
||||
"more_online_members": "还有{overflow}个"
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
// This is a generated file. Don't edit.
|
||||
|
||||
export const PACKAGE_VERSION = "0.15.13-rc3";
|
||||
export const PACKAGE_VERSION = "0.15.15-rc2";
|
||||
|
||||
+123
-38
@@ -1,3 +1,4 @@
|
||||
|
||||
// Edit account parameters.
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
@@ -9,30 +10,38 @@ import PermissionsEditor from '../widgets/permissions-editor.jsx';
|
||||
import TagManager from '../widgets/tag-manager.jsx';
|
||||
|
||||
import { makeImageUrl } from '../lib/blob-helpers.js';
|
||||
import { vcard } from '../lib/utils.js';
|
||||
import { arrayEqual, asEmail, asPhone, vcard } from '../lib/utils.js';
|
||||
|
||||
export default class EditAccountView extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
let me = this.props.tinode.getMeTopic();
|
||||
let defacs = me.getDefaultAccess();
|
||||
let fnd = this.props.tinode.getFndTopic();
|
||||
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: fnd.tags(),
|
||||
tags: me.tags(),
|
||||
credentials: me.getCredentials() || [],
|
||||
addCredActive: false,
|
||||
addCredInvalid: false,
|
||||
newCred: '',
|
||||
showPermissionEditorFor: undefined,
|
||||
previousOnTags: fnd.onTagsUpdated
|
||||
previousOnTags: me.onTagsUpdated
|
||||
};
|
||||
|
||||
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.handleStartAddCred = this.handleStartAddCred.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);
|
||||
@@ -40,24 +49,26 @@ export default class EditAccountView extends React.Component {
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
let fnd = this.props.tinode.getFndTopic();
|
||||
fnd.onTagsUpdated = this.tnNewTags;
|
||||
if (!fnd.isSubscribed()) {
|
||||
fnd.subscribe(fnd.startMetaQuery().withLaterDesc().withTags().build()).catch((err) => {
|
||||
this.props.onError(err.message, 'err');
|
||||
});
|
||||
}
|
||||
const me = this.props.tinode.getMeTopic();
|
||||
me.onCredsUpdated = this.tnCredsUpdated;
|
||||
me.onTagsUpdated = this.tnNewTags;
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
var fnd = this.props.tinode.getFndTopic();
|
||||
fnd.onTagsUpdated = this.state.previousOnTags;
|
||||
const me = this.props.tinode.getMeTopic();
|
||||
me.onTagsUpdated = this.state.previousOnTags;
|
||||
me.onCredsUpdated = undefined;
|
||||
}
|
||||
|
||||
tnNewTags(tags) {
|
||||
this.setState({tags: tags});
|
||||
}
|
||||
|
||||
tnCredsUpdated(creds) {
|
||||
console.log("tnCredsUpdated", creds, new Error());
|
||||
this.setState({credentials: creds || []});
|
||||
}
|
||||
|
||||
handleFullNameUpdate(fn) {
|
||||
this.setState({fullName: fn});
|
||||
this.props.onUpdateAccount(undefined, vcard(fn, null));
|
||||
@@ -81,6 +92,49 @@ export default class EditAccountView extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
handleStartAddCred() {
|
||||
this.setState({addCredActive: true});
|
||||
}
|
||||
|
||||
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,
|
||||
@@ -111,13 +165,25 @@ export default class EditAccountView extends React.Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
var tags = [];
|
||||
this.state.tags.map(function(tag) {
|
||||
/*
|
||||
let tags = [];
|
||||
this.state.tags.map((tag) => {
|
||||
tags.push(<span className="badge" key={tags.length}>{tag}</span>);
|
||||
});
|
||||
if (tags.length == 0) {
|
||||
tags = <i><FormattedMessage id="tags_not_found" /></i>;
|
||||
}
|
||||
*/
|
||||
let credentials = [];
|
||||
this.state.credentials.map((cred) => {
|
||||
credentials.push(<div key={cred.meth + ":" + cred.val}>{cred.meth}: <tt>{cred.val}</tt>
|
||||
{!cred.done ?
|
||||
<a href="javascript:;"
|
||||
onClick={this.props.onCredConfirm.bind(this, cred.meth, cred.val)}>confirm</a>
|
||||
: null} <a href="javascript:;" onClick={this.props.onCredDelete.bind(this, cred.meth, cred.val)}><i
|
||||
className="material-icons gray">delete_outline</i></a></div>);
|
||||
});
|
||||
|
||||
return (
|
||||
<React.Fragment>{this.state.showPermissionEditorFor ?
|
||||
<PermissionsEditor
|
||||
@@ -161,26 +227,6 @@ export default class EditAccountView extends React.Component {
|
||||
onError={this.props.onError} />
|
||||
</div>
|
||||
<div className="hr" />
|
||||
<div className="panel-form-column">
|
||||
<div className="panel-form-row">
|
||||
<label><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-row">
|
||||
<label htmlFor="message-sound">
|
||||
<FormattedMessage id="label_message_sound" defaultMessage="Message sound:"
|
||||
@@ -198,7 +244,7 @@ export default class EditAccountView extends React.Component {
|
||||
:
|
||||
<FormattedMessage id="label_push_notifications_disabled"
|
||||
defaultMessage="Notification alerts (requires HTTPS):"
|
||||
description="Label for push notifications switch (disabled)" />
|
||||
description="Label for push notifications switch" />
|
||||
}
|
||||
</label>
|
||||
<CheckBox name="alert" id="desktop-alerts"
|
||||
@@ -215,6 +261,45 @@ export default class EditAccountView extends React.Component {
|
||||
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="javascript:;" onClick={this.handleStartAddCred}>+ 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="javascript:;" className="red flat-button" onClick={this.props.onLogout}>
|
||||
<i className="material-icons">exit_to_app</i> <FormattedMessage id="button_logout"
|
||||
|
||||
@@ -15,8 +15,10 @@ import MoreButton from '../widgets/more-button.jsx';
|
||||
import PermissionsEditor from '../widgets/permissions-editor.jsx';
|
||||
import TagManager from '../widgets/tag-manager.jsx';
|
||||
|
||||
import { NO_ACCESS_MODE } from '../config.js';
|
||||
|
||||
import { makeImageUrl } from '../lib/blob-helpers.js';
|
||||
import { vcard } from '../lib/utils.js';
|
||||
import { arrayEqual, vcard } from '../lib/utils.js';
|
||||
|
||||
const messages = defineMessages({
|
||||
requested: {
|
||||
@@ -145,6 +147,9 @@ class InfoView extends React.Component {
|
||||
if (user2) {
|
||||
newState.modeGiven2 = user2.acs.getGiven();
|
||||
newState.modeWant2 = user2.acs.getWant();
|
||||
} else {
|
||||
newState.modeGiven2 = NO_ACCESS_MODE;
|
||||
newState.modeWant2 = NO_ACCESS_MODE;
|
||||
}
|
||||
} else {
|
||||
topic.subscribers((sub) => {
|
||||
@@ -196,7 +201,7 @@ class InfoView extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
onSubsUpdated() {
|
||||
onSubsUpdated(subs) {
|
||||
const topic = this.props.tinode.getTopic(this.props.topic);
|
||||
if (!topic) {
|
||||
return;
|
||||
@@ -204,7 +209,7 @@ class InfoView extends React.Component {
|
||||
this.resetSubs(topic, this.props);
|
||||
|
||||
if (this.previousSubsUpdated && this.previousSubsUpdated != this.onSubsUpdated) {
|
||||
this.previousSubsUpdated();
|
||||
this.previousSubsUpdated(subs);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+121
-57
@@ -14,7 +14,7 @@ import LoadSpinner from '../widgets/load-spinner.jsx';
|
||||
import LogoView from './logo-view.jsx';
|
||||
import SendMessage from '../widgets/send-message.jsx';
|
||||
|
||||
import { KEYPRESS_DELAY, MESSAGES_PAGE } from '../config.js';
|
||||
import { DEFAULT_ACCESS_MODE, KEYPRESS_DELAY, MESSAGES_PAGE } from '../config.js';
|
||||
import { makeImageUrl } from '../lib/blob-helpers.js';
|
||||
import { shortDateFormat } from '../lib/strformat.js';
|
||||
|
||||
@@ -38,8 +38,19 @@ const messages = defineMessages({
|
||||
|
||||
// Checks if the access permissions are granted but not yet accepted.
|
||||
function isUnconfirmed(acs) {
|
||||
const ex = acs ? acs.getExcessive() || '' : '';
|
||||
return (ex.includes('R') || ex.includes('W'));
|
||||
if (acs) {
|
||||
const ex = acs.getExcessive() || '';
|
||||
return acs.isJoiner('given') && (ex.includes('R') || ex.includes('W'));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function isPeerRestricted(acs) {
|
||||
if (acs) {
|
||||
const ms = acs.getMissing() || '';
|
||||
return acs.isJoiner('want') && (ms.includes('R') || ms.includes('W'));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
class MessagesView extends React.Component {
|
||||
@@ -64,6 +75,7 @@ class MessagesView extends React.Component {
|
||||
this.handleShowContextMenuMessage = this.handleShowContextMenuMessage.bind(this);
|
||||
this.handleBackNavigation = this.handleBackNavigation.bind(this);
|
||||
this.handleNewChatAcceptance = this.handleNewChatAcceptance.bind(this);
|
||||
this.handleEnablePeer = this.handleEnablePeer.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
@@ -108,19 +120,24 @@ class MessagesView extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
if (topic && !topic.isSubscribed() &&
|
||||
((!prevProps.connected && this.props.connected) || (this.state.topic != prevState.topic))) {
|
||||
if (topic && !topic.isSubscribed() && this.props.ready &&
|
||||
((this.state.topic != prevState.topic) || !prevProps.ready)) {
|
||||
// Is this a new topic?
|
||||
const newTopic = (this.props.newTopicParams && this.props.newTopicParams._topicName == this.props.topic);
|
||||
|
||||
// Don't request the tags. They are useless unless the user
|
||||
// is the owner and is editing the topic.
|
||||
const getQuery = topic.startMetaQuery()
|
||||
.withLaterDesc()
|
||||
.withLaterSub()
|
||||
.withLaterData(MESSAGES_PAGE)
|
||||
.withLaterDel();
|
||||
const setQuery = Tinode.isNewGroupTopicName(this.props.topic) ?
|
||||
this.props.newGroupTopicParams : undefined;
|
||||
// Show "loading" spinner.
|
||||
this.setState({ fetchingMessages: true });
|
||||
let getQuery = topic.startMetaQuery().withLaterDesc().withLaterSub();
|
||||
if (this.state.isReader || newTopic) {
|
||||
// If reading is either permitted or we don't know because it's a new topic. Ask for messages.
|
||||
getQuery = getQuery.withLaterData(MESSAGES_PAGE);
|
||||
if (this.state.isReader) {
|
||||
getQuery = getQuery.withLaterDel();
|
||||
}
|
||||
// And show "loading" spinner.
|
||||
this.setState({ fetchingMessages: true });
|
||||
}
|
||||
const setQuery = newTopic ? this.props.newTopicParams : undefined;
|
||||
topic.subscribe(getQuery.build(), setQuery)
|
||||
.then((ctrl) => {
|
||||
if (this.state.topic != ctrl.topic) {
|
||||
@@ -135,7 +152,7 @@ class MessagesView extends React.Component {
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log('Failed subscription to', this.state.topic);
|
||||
console.log("Failed subscription to", this.state.topic);
|
||||
this.props.onError(err.message, 'err');
|
||||
const blankState = MessagesView.getDerivedStateFromProps({}, {});
|
||||
blankState.title = this.props.intl.formatMessage(messages.not_found);
|
||||
@@ -157,7 +174,8 @@ class MessagesView extends React.Component {
|
||||
imagePreview: null,
|
||||
typingIndicator: false,
|
||||
scrollPosition: 0,
|
||||
fetchingMessages: false
|
||||
fetchingMessages: false,
|
||||
peerMessagingDisabled: false
|
||||
};
|
||||
} else if (nextProps.topic != prevState.topic) {
|
||||
const topic = nextProps.tinode.getTopic(nextProps.topic);
|
||||
@@ -188,44 +206,69 @@ class MessagesView extends React.Component {
|
||||
}
|
||||
});
|
||||
|
||||
nextState = Object.assign(nextState, {
|
||||
Object.assign(nextState, {
|
||||
messages: msgs,
|
||||
onlineSubs: subs
|
||||
});
|
||||
|
||||
if (topic.public) {
|
||||
nextState = Object.assign(nextState, {
|
||||
Object.assign(nextState, {
|
||||
title: topic.public.fn,
|
||||
avatar: makeImageUrl(topic.public.photo)
|
||||
});
|
||||
} else {
|
||||
nextState = Object.assign(nextState, {
|
||||
Object.assign(nextState, {
|
||||
title: '',
|
||||
avatar: null
|
||||
});
|
||||
}
|
||||
|
||||
const peer = topic.p2pPeerDesc();
|
||||
if (peer) {
|
||||
Object.assign(nextState, {
|
||||
peerMessagingDisabled: isPeerRestricted(peer.acs)
|
||||
});
|
||||
} else if (prevState.peerMessagingDisabled) {
|
||||
Object.assign(nextState, {
|
||||
peerMessagingDisabled: false
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Invalid topic.
|
||||
nextState = Object.assign(nextState, {
|
||||
Object.assign(nextState, {
|
||||
messages: [],
|
||||
onlineSubs: [],
|
||||
title: '',
|
||||
avatar: null
|
||||
avatar: null,
|
||||
peerMessagingDisabled: false
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let val = nextProps.acs ? !nextProps.acs.isWriter() : true;
|
||||
if (val != prevState.readOnly) {
|
||||
nextState.readOnly = val;
|
||||
if (nextProps.acs) {
|
||||
if (nextProps.acs.isWriter() != prevState.isWriter) {
|
||||
nextState.isWriter = !prevState.isWriter;
|
||||
}
|
||||
if (nextProps.acs.isReader() != prevState.isReader) {
|
||||
nextState.isReader = !prevState.isReader;
|
||||
}
|
||||
if (!nextProps.acs.isReader('given') != prevState.readingBlocked) {
|
||||
nextState.readingBlocked = !prevState.readingBlocked;
|
||||
}
|
||||
} else {
|
||||
if (prevState.isWriter) {
|
||||
nextState.isWriter = false;
|
||||
}
|
||||
if (prevState.isReader) {
|
||||
nextState.isReader = false;
|
||||
}
|
||||
if (!prevState.readingBlocked) {
|
||||
prevState.readingBlocked = true;
|
||||
}
|
||||
}
|
||||
val = nextProps.acs ? !nextProps.acs.isReader() : true;
|
||||
if (val != prevState.writeOnly) {
|
||||
nextState.writeOnly = val;
|
||||
}
|
||||
val = isUnconfirmed(nextProps.acs);
|
||||
if (val != prevState.unconformed) {
|
||||
nextState.unconfirmed = val;
|
||||
|
||||
if (isUnconfirmed(nextProps.acs) == !prevState.unconformed) {
|
||||
nextState.unconfirmed = !prevState.unconformed;
|
||||
}
|
||||
|
||||
// Clear subscribers online when there is no connection.
|
||||
@@ -270,9 +313,9 @@ class MessagesView extends React.Component {
|
||||
this.setState({scrollPosition: event.target.scrollHeight - event.target.scrollTop});
|
||||
if (event.target.scrollTop <= 0) {
|
||||
this.setState((prevState, props) => {
|
||||
let newState = {};
|
||||
const newState = {};
|
||||
if (!prevState.fetchingMessages) {
|
||||
let topic = this.props.tinode.getTopic(this.state.topic);
|
||||
const topic = this.props.tinode.getTopic(this.state.topic);
|
||||
if (topic && topic.isSubscribed() && topic.msgHasMoreMessages()) {
|
||||
newState.fetchingMessages = true;
|
||||
topic.getMessagesPage(MESSAGES_PAGE).catch((err) => {
|
||||
@@ -301,32 +344,42 @@ class MessagesView extends React.Component {
|
||||
|
||||
if (desc.acs) {
|
||||
this.setState({
|
||||
readOnly: !desc.acs.isWriter(),
|
||||
writeOnly: !desc.acs.isReader(),
|
||||
isWriter: desc.acs.isWriter(),
|
||||
isReader: desc.acs.isReader(),
|
||||
readingBlocked: !desc.acs.isReader('given'),
|
||||
unconfirmed: isUnconfirmed(desc.acs),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
handleSubsUpdated() {
|
||||
console.log("handleSubsUpdated");
|
||||
|
||||
if (this.state.topic) {
|
||||
var subs = [];
|
||||
var topic = this.props.tinode.getTopic(this.state.topic);
|
||||
const subs = [];
|
||||
const topic = this.props.tinode.getTopic(this.state.topic);
|
||||
topic.subscribers((sub) => {
|
||||
if (sub.online && sub.user != this.props.myUserId) {
|
||||
subs.push(sub);
|
||||
}
|
||||
});
|
||||
this.setState({onlineSubs: subs});
|
||||
const newState = {onlineSubs: subs};
|
||||
const peer = topic.p2pPeerDesc();
|
||||
if (peer) {
|
||||
Object.assign(newState, {
|
||||
peerMessagingDisabled: isPeerRestricted(peer.acs)
|
||||
});
|
||||
} else if (this.state.peerMessagingDisabled) {
|
||||
Object.assign(newState, {
|
||||
peerMessagingDisabled: false
|
||||
});
|
||||
}
|
||||
this.setState(newState);
|
||||
}
|
||||
}
|
||||
|
||||
handleNewMessage(msg) {
|
||||
// Regenerate messages list
|
||||
var topic = this.props.tinode.getTopic(this.state.topic);
|
||||
var newState = {messages: []};
|
||||
const topic = this.props.tinode.getTopic(this.state.topic);
|
||||
const newState = {messages: []};
|
||||
topic.messages(function(m) {
|
||||
if (!m.deleted) {
|
||||
newState.messages = newState.messages.concat(m);
|
||||
@@ -341,9 +394,10 @@ class MessagesView extends React.Component {
|
||||
newState.scrollPosition = 0;
|
||||
}
|
||||
|
||||
// Aknowledge all messages, including own messages.
|
||||
let status = topic.msgStatus(msg);
|
||||
if (status >= Tinode.MESSAGE_STATUS_SENT) {
|
||||
// Aknowledge messages except own messages. They are
|
||||
// automatically assumed to be read and recived.
|
||||
const status = topic.msgStatus(msg);
|
||||
if (status >= Tinode.MESSAGE_STATUS_SENT && msg.from != this.props.myUserId) {
|
||||
this.props.readTimerHandler(() => {
|
||||
topic.noteRead(msg.seq);
|
||||
});
|
||||
@@ -394,7 +448,7 @@ class MessagesView extends React.Component {
|
||||
this.props.sendMessage(Drafty.attachJSON(Drafty.parse(text), data));
|
||||
} else if (action == 'url') {
|
||||
const url = new URL(data.ref);
|
||||
let params = url.searchParams;
|
||||
const params = url.searchParams;
|
||||
for (let key in data.resp) {
|
||||
if (data.resp.hasOwnProperty(key)) {
|
||||
params.set(key, data.resp[key]);
|
||||
@@ -440,6 +494,10 @@ class MessagesView extends React.Component {
|
||||
this.props.onNewChat(this.state.topic, action);
|
||||
}
|
||||
|
||||
handleEnablePeer() {
|
||||
this.props.onChangePermissions(this.state.topic, DEFAULT_ACCESS_MODE, this.state.topic);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {formatMessage} = this.props.intl;
|
||||
|
||||
@@ -504,7 +562,7 @@ class MessagesView extends React.Component {
|
||||
}
|
||||
|
||||
let lastSeen = null;
|
||||
let cont = this.props.tinode.getMeTopic().getContact(this.state.topic);
|
||||
const cont = this.props.tinode.getMeTopic().getContact(this.state.topic);
|
||||
if (cont && Tinode.topicType(cont.topic) == 'p2p') {
|
||||
if (cont.online) {
|
||||
lastSeen = formatMessage(messages.online_now);
|
||||
@@ -514,8 +572,8 @@ class MessagesView extends React.Component {
|
||||
// TODO: also handle user agent in c.seen.ua
|
||||
}
|
||||
}
|
||||
let avatar = this.state.avatar || true;
|
||||
let online = this.props.online ? 'online' + (this.state.typingIndicator ? ' typing' : '') : 'offline';
|
||||
const avatar = this.state.avatar || true;
|
||||
const online = this.props.online ? 'online' + (this.state.typingIndicator ? ' typing' : '') : 'offline';
|
||||
|
||||
component = (
|
||||
<div id="topic-view" className={this.props.hideSelf ? 'nodisplay' : null}>
|
||||
@@ -564,29 +622,35 @@ class MessagesView extends React.Component {
|
||||
<ul id="scroller" className={chatBoxClass}>
|
||||
{messageNodes}
|
||||
</ul>
|
||||
<div>Typing notifications here.</div>
|
||||
</div>
|
||||
{this.state.writeOnly ?
|
||||
{!this.state.isReader ?
|
||||
<div id="write-only-background">
|
||||
{this.state.readingBlocked ?
|
||||
<div id="write-only-note">
|
||||
<FormattedMessage id="messages_not_readable" defaultMessage="no access to messages"
|
||||
description="Message shown in topic without the read access" />
|
||||
</div>
|
||||
: null }
|
||||
</div>
|
||||
:
|
||||
null
|
||||
}
|
||||
: null }
|
||||
</div>
|
||||
{this.state.peerMessagingDisabled && !this.state.unconfirmed ?
|
||||
<div id="peer-messaging-disabled-note">
|
||||
<i className="material-icons secondary">block</i> <FormattedMessage
|
||||
id="peers_messaging_disabled" defaultMessage="Peer's messaging is disabled."
|
||||
description="Shown when the p2p peer's messaging is disabled" /> <a href="javascript:;"
|
||||
onClick={this.handleEnablePeer}><FormattedMessage id="enable_peers_messaging"
|
||||
defaultMessage="Enable" description="Call to action to enable peer's messaging" /></a>.
|
||||
</div> : null}
|
||||
{this.state.unconfirmed ?
|
||||
<Invitation onAction={this.handleNewChatAcceptance} />
|
||||
:
|
||||
<SendMessage
|
||||
tinode={this.props.tinode}
|
||||
topic={this.props.topic}
|
||||
disabled={this.state.readOnly}
|
||||
disabled={!this.state.isWriter}
|
||||
sendMessage={this.props.sendMessage}
|
||||
onError={this.props.onError} />
|
||||
}
|
||||
onError={this.props.onError} />}
|
||||
{this.state.imagePreview ?
|
||||
<ImagePreview content={this.state.imagePreview}
|
||||
onClose={this.handleCloseImagePreview} /> : null}
|
||||
|
||||
@@ -135,6 +135,9 @@ class SidepanelView extends React.Component {
|
||||
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}
|
||||
onError={this.props.onError} /> :
|
||||
|
||||
+84
-24
@@ -14,11 +14,12 @@ import InfoView from './info-view.jsx';
|
||||
import MessagesView from './messages-view.jsx';
|
||||
import SidepanelView from './sidepanel-view.jsx';
|
||||
|
||||
import { API_KEY, APP_NAME, MEDIA_BREAKPOINT, READ_DELAY, RECEIVED_DELAY } from '../config.js';
|
||||
import { makeImageUrl } from '../lib/blob-helpers.js';
|
||||
import { API_KEY, APP_NAME, DEFAULT_ACCESS_MODE, MEDIA_BREAKPOINT, READ_DELAY, RECEIVED_DELAY } from '../config.js';
|
||||
import { base64ReEncode, makeImageUrl } from '../lib/blob-helpers.js';
|
||||
import { detectServerAddress, isLocalHost, isSecureConnection } from '../lib/host-name.js';
|
||||
import LocalStorageUtil from '../lib/local-storage.js';
|
||||
import HashNavigation from '../lib/navigation.js';
|
||||
import { secondsToTime } from '../lib/strformat.js'
|
||||
import { updateFavicon } from '../lib/utils.js';
|
||||
|
||||
// Sound to play on message received.
|
||||
@@ -32,7 +33,7 @@ const messages = defineMessages({
|
||||
},
|
||||
reconnect_countdown: {
|
||||
id: 'reconnect_countdown',
|
||||
defaultMessage: 'Disconnected. Reconnecting in {seconds}s…',
|
||||
defaultMessage: 'Disconnected. Reconnecting in {seconds}…',
|
||||
description: 'Message shown when an app update is available.'
|
||||
},
|
||||
reconnect_now: {
|
||||
@@ -83,6 +84,9 @@ class TinodeWeb extends React.Component {
|
||||
this.handleGlobalSettings = this.handleGlobalSettings.bind(this);
|
||||
this.handleShowArchive = this.handleShowArchive.bind(this);
|
||||
this.handleToggleMessageSounds = this.handleToggleMessageSounds.bind(this);
|
||||
this.handleCredAdd = this.handleCredAdd.bind(this);
|
||||
this.handleCredDelete = this.handleCredDelete.bind(this);
|
||||
this.handleCredConfirm = this.handleCredConfirm.bind(this);
|
||||
this.initDesktopAlerts = this.initDesktopAlerts.bind(this);
|
||||
this.togglePushToken = this.togglePushToken.bind(this);
|
||||
this.requestPushToken = this.requestPushToken.bind(this);
|
||||
@@ -113,6 +117,8 @@ class TinodeWeb extends React.Component {
|
||||
|
||||
return {
|
||||
connected: false,
|
||||
// Connected and subscribed to 'me'
|
||||
ready: false,
|
||||
transport: settings.transport || null,
|
||||
serverAddress: settings.serverAddress || detectServerAddress(),
|
||||
serverVersion: "no connection",
|
||||
@@ -141,7 +147,7 @@ class TinodeWeb extends React.Component {
|
||||
topicSelected: '',
|
||||
topicSelectedOnline: false,
|
||||
topicSelectedAcs: null,
|
||||
newGroupTopicParams: null,
|
||||
newTopicParams: null,
|
||||
loginDisabled: false,
|
||||
displayMobile: (window.innerWidth <= MEDIA_BREAKPOINT),
|
||||
showInfoPanel: false,
|
||||
@@ -393,8 +399,9 @@ class TinodeWeb extends React.Component {
|
||||
let count = sec / 1000;
|
||||
count = count | count;
|
||||
this.reconnectCountdown = setInterval(() => {
|
||||
const timeLeft = (count > 99) ? secondsToTime(count) : count;
|
||||
this.handleError(
|
||||
formatHTMLMessage(messages.reconnect_countdown, {seconds: count}),
|
||||
formatHTMLMessage(messages.reconnect_countdown, {seconds: timeLeft}),
|
||||
'warn',
|
||||
() => {
|
||||
clearInterval(this.reconnectCountdown);
|
||||
@@ -410,6 +417,7 @@ class TinodeWeb extends React.Component {
|
||||
handleDisconnect(err) {
|
||||
this.setState({
|
||||
connected: false,
|
||||
ready: false,
|
||||
topicSelectedOnline: false,
|
||||
dialogSelected: null,
|
||||
errorText: err && err.message ? err.message : "Disconnected",
|
||||
@@ -488,11 +496,14 @@ class TinodeWeb extends React.Component {
|
||||
credCode: undefined,
|
||||
myUserId: this.tinode.getCurrentUserID()
|
||||
});
|
||||
console.log("handleLoginSuccessful");
|
||||
// Subscribe, fetch topic desc, the list of subscriptions. Messages are not fetched.
|
||||
me.subscribe(
|
||||
me.startMetaQuery().
|
||||
withLaterSub().
|
||||
withDesc().
|
||||
withTags().
|
||||
withCreds().
|
||||
build()
|
||||
).catch((err) => {
|
||||
localStorage.removeItem('auth-token');
|
||||
@@ -591,9 +602,14 @@ class TinodeWeb extends React.Component {
|
||||
}
|
||||
|
||||
resetContactList() {
|
||||
let newState = {
|
||||
const newState = {
|
||||
chatList: []
|
||||
};
|
||||
|
||||
if (!this.state.ready) {
|
||||
newState.ready = true;
|
||||
}
|
||||
|
||||
this.tinode.getMeTopic().contacts((c) => {
|
||||
newState.chatList.push(c);
|
||||
if (this.state.topicSelected == c.topic) {
|
||||
@@ -609,7 +625,7 @@ class TinodeWeb extends React.Component {
|
||||
// Sending "received" notifications
|
||||
tnData(data) {
|
||||
const topic = this.tinode.getTopic(data.topic);
|
||||
if (topic.msgStatus(data) >= Tinode.MESSAGE_STATUS_SENT) {
|
||||
if (topic.msgStatus(data) >= Tinode.MESSAGE_STATUS_SENT && data.from != this.state.myUserId) {
|
||||
clearTimeout(this.receivedTimer);
|
||||
this.receivedTimer = setTimeout(() => {
|
||||
this.receivedTimer = undefined;
|
||||
@@ -625,7 +641,7 @@ class TinodeWeb extends React.Component {
|
||||
if (fnd.isSubscribed()) {
|
||||
this.tnFndSubsUpdated();
|
||||
} else {
|
||||
fnd.subscribe(fnd.startMetaQuery().withSub().withTags().build()).catch((err) => {
|
||||
fnd.subscribe(fnd.startMetaQuery().withSub().build()).catch((err) => {
|
||||
this.handleError(err.message, 'err');
|
||||
});
|
||||
}
|
||||
@@ -657,6 +673,13 @@ class TinodeWeb extends React.Component {
|
||||
|
||||
// User clicked on a contact in the side panel or deleted a contact.
|
||||
handleTopicSelected(topicName, unused_index, online, acs) {
|
||||
// Clear newTopicParams after use.
|
||||
if (this.state.newTopicParams && this.state.newTopicParams._topicName != topicName) {
|
||||
this.setState({
|
||||
newTopicParams: null
|
||||
});
|
||||
}
|
||||
|
||||
if (topicName) {
|
||||
this.setState({
|
||||
errorText: '',
|
||||
@@ -682,9 +705,8 @@ class TinodeWeb extends React.Component {
|
||||
topicSelectedAcs: null,
|
||||
showInfoPanel: false
|
||||
});
|
||||
if (this.state.topicSelected) {
|
||||
HashNavigation.navigateTo(HashNavigation.setUrlTopic('', null));
|
||||
}
|
||||
|
||||
HashNavigation.navigateTo(HashNavigation.setUrlTopic('', null));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -738,7 +760,7 @@ class TinodeWeb extends React.Component {
|
||||
const mode = topic.getAccessMode().getGiven();
|
||||
response = topic.setMeta({sub: {mode: mode}});
|
||||
if (topic.getType() == 'p2p') {
|
||||
// For P2P topics chage 'given' permission of the peer too.
|
||||
// For P2P topics change 'given' permission of the peer too.
|
||||
// In p2p topics the other user has the same name as the topic.
|
||||
response = response.then((ctrl) => {
|
||||
topic.setMeta({sub: {user: topicName, mode: mode}});
|
||||
@@ -751,7 +773,11 @@ class TinodeWeb extends React.Component {
|
||||
break;
|
||||
case 'block':
|
||||
// Ban the topic making futher invites impossible.
|
||||
response = topic.setMeta({sub: {mode: 'N'}});
|
||||
// Just self-ban.
|
||||
const am = topic.getAccessMode().updateWant('-JP').getWant();
|
||||
response = topic.setMeta({sub: {mode: am}}).then((ctrl) => {
|
||||
return this.handleTopicSelected(null);
|
||||
});
|
||||
break;
|
||||
default:
|
||||
console.log("Unknown invitation action", '"' + action + '""');
|
||||
@@ -807,7 +833,7 @@ class TinodeWeb extends React.Component {
|
||||
}
|
||||
|
||||
handleUpdateAccountTagsRequest(tags) {
|
||||
this.tinode.getFndTopic().setMeta({tags: tags})
|
||||
this.tinode.getMeTopic().setMeta({tags: tags})
|
||||
.catch((err) => {
|
||||
this.handleError(err.message, 'err');
|
||||
});
|
||||
@@ -913,6 +939,24 @@ class TinodeWeb extends React.Component {
|
||||
});
|
||||
}
|
||||
|
||||
handleCredAdd(method, value) {
|
||||
// TODO: implement
|
||||
console.log("add credential", method, value);
|
||||
}
|
||||
|
||||
handleCredDelete(method, value) {
|
||||
console.log("delete credential", method, value);
|
||||
const me = this.tinode.getMeTopic();
|
||||
me.delCredential(method, value).catch((err) => {
|
||||
this.handleError(err.message, 'err');
|
||||
});
|
||||
}
|
||||
|
||||
handleCredConfirm(method, response) {
|
||||
// TODO: implement
|
||||
console.log("confirm credential", method, response);
|
||||
}
|
||||
|
||||
// User clicked Cancel button in Setting or Sign Up panel.
|
||||
handleSidepanelCancel() {
|
||||
const parsed = HashNavigation.parseUrlHash(window.location.hash);
|
||||
@@ -934,14 +978,19 @@ class TinodeWeb extends React.Component {
|
||||
// Request to start a new topic. New P2P topic requires peer's name.
|
||||
handleNewTopicRequest(peerName, pub, priv, tags) {
|
||||
const topicName = peerName || this.tinode.newGroupTopicName();
|
||||
if (!peerName) {
|
||||
this.setState(
|
||||
{newGroupTopicParams: {desc: {public: pub, private: {comment: priv}}, tags: tags}},
|
||||
() => {this.handleTopicSelected(topicName)}
|
||||
);
|
||||
const params = {
|
||||
_topicName: topicName,
|
||||
};
|
||||
if (peerName) {
|
||||
// Because we are initialing the subscription, set 'want' to all permissions.
|
||||
params.sub = {mode: DEFAULT_ACCESS_MODE};
|
||||
// Give the other user all permissions too.
|
||||
params.desc = {defacs: {auth: DEFAULT_ACCESS_MODE}};
|
||||
} else {
|
||||
this.handleTopicSelected(topicName);
|
||||
params.desc = {public: pub, private: {comment: priv}};
|
||||
params.tags = tags;
|
||||
}
|
||||
this.setState({newTopicParams: params}, () => {this.handleTopicSelected(topicName)});
|
||||
}
|
||||
|
||||
// New topic was creted, here is the new topic name.
|
||||
@@ -1056,7 +1105,7 @@ class TinodeWeb extends React.Component {
|
||||
const topic = this.tinode.getTopic(topicName);
|
||||
const {formatMessage} = this.props.intl;
|
||||
|
||||
let muted = false, blocked = false, subscribed = false, deleter = false, archived = false;
|
||||
let muted = false, blocked = false, self_blocked = false, subscribed = false, deleter = false, archived = false;
|
||||
if (topic) {
|
||||
subscribed = topic.isSubscribed();
|
||||
archived = topic.isArchived();
|
||||
@@ -1065,6 +1114,7 @@ class TinodeWeb extends React.Component {
|
||||
if (acs) {
|
||||
muted = acs.isMuted();
|
||||
blocked = !acs.isJoiner();
|
||||
self_blocked = !acs.isJoiner('want');
|
||||
deleter = acs.isDeleter();
|
||||
}
|
||||
}
|
||||
@@ -1073,8 +1123,8 @@ class TinodeWeb extends React.Component {
|
||||
subscribed ? {title: formatMessage({id: 'menu_item_info'}), handler: this.handleShowInfoView} : null,
|
||||
subscribed ? 'messages_clear' : null,
|
||||
subscribed && deleter ? 'messages_clear_hard' : null,
|
||||
muted ? 'topic_unmute' : 'topic_mute',
|
||||
blocked ? 'topic_unblock' : 'topic_block',
|
||||
muted ? (blocked ? null : 'topic_unmute') : 'topic_mute',
|
||||
self_blocked ? 'topic_unblock' : 'topic_block',
|
||||
!archived ? 'topic_archive' : null,
|
||||
'topic_delete'
|
||||
];
|
||||
@@ -1181,6 +1231,11 @@ class TinodeWeb extends React.Component {
|
||||
items={this.state.contextMenuItems}
|
||||
hide={this.handleHideContextMenu}
|
||||
onAction={this.handleContextMenuAction}
|
||||
onTopicRemoved={(topicName) => {
|
||||
if (topicName == this.state.topicSelected) {
|
||||
this.handleTopicSelected(null);
|
||||
}
|
||||
}}
|
||||
onError={this.handleError} />
|
||||
:
|
||||
null
|
||||
@@ -1223,6 +1278,9 @@ class TinodeWeb extends React.Component {
|
||||
onUpdateAccountTags={this.handleUpdateAccountTagsRequest}
|
||||
onTogglePushNotifications={this.togglePushToken}
|
||||
onToggleMessageSounds={this.handleToggleMessageSounds}
|
||||
onCredAdd={this.handleCredAdd}
|
||||
onCredDelete={this.handleCredDelete}
|
||||
onCredConfirm={this.handleCredConfirm}
|
||||
onTopicSelected={this.handleTopicSelected}
|
||||
onCreateTopic={this.handleNewTopicRequest}
|
||||
onNewTopic={this.handleNewTopic}
|
||||
@@ -1243,6 +1301,7 @@ class TinodeWeb extends React.Component {
|
||||
<MessagesView
|
||||
tinode={this.tinode}
|
||||
connected={this.state.connected}
|
||||
ready={this.state.ready}
|
||||
online={this.state.topicSelectedOnline}
|
||||
acs={this.state.topicSelectedAcs}
|
||||
displayMobile={this.state.displayMobile}
|
||||
@@ -1260,7 +1319,7 @@ class TinodeWeb extends React.Component {
|
||||
errorAction={this.state.errorAction}
|
||||
errorActionText={this.state.errorActionText}
|
||||
|
||||
newGroupTopicParams={this.state.newGroupTopicParams}
|
||||
newTopicParams={this.state.newTopicParams}
|
||||
|
||||
onHideMessagesView={this.handleHideMessagesView}
|
||||
onData={this.tnData}
|
||||
@@ -1268,6 +1327,7 @@ class TinodeWeb extends React.Component {
|
||||
onNewTopicCreated={this.handleNewTopicCreated}
|
||||
readTimerHandler={this.handleReadTimer}
|
||||
showContextMenu={this.handleShowContextMenu}
|
||||
onChangePermissions={this.handleChangePermissions}
|
||||
onNewChat={this.handleNewChatInvitation}
|
||||
sendMessage={this.handleSendMessage} />
|
||||
|
||||
|
||||
@@ -118,6 +118,7 @@ export default class ChipInput extends React.Component {
|
||||
noAvatar={this.props.avatarDisabled}
|
||||
topic={item.user}
|
||||
required={item.user === this.props.required}
|
||||
invalid={item.invalid}
|
||||
index={count}
|
||||
key={item.user} />
|
||||
);
|
||||
|
||||
@@ -15,8 +15,9 @@ export default class Chip extends React.PureComponent {
|
||||
|
||||
render() {
|
||||
const title = this.props.title || this.props.topic;
|
||||
const className = this.props.invalid ? 'chip invalid' : 'chip';
|
||||
return (
|
||||
<div className="chip">
|
||||
<div className={className}>
|
||||
{this.props.noAvatar ?
|
||||
<span className="spacer" /> :
|
||||
<div className="avatar-box">
|
||||
|
||||
@@ -1,17 +1,23 @@
|
||||
import React from 'react';
|
||||
|
||||
/* Contact's labels: [you], [muted], [blocked], etc */
|
||||
// FIXME: this class is unused.
|
||||
const icon_mapping = {'muted': 'notifications_off', 'banned': 'block'};
|
||||
|
||||
/* Contact's labels: [you], or icons [muted], [blocked] */
|
||||
export default class ContactBadges extends React.PureComponent {
|
||||
render() {
|
||||
let badges = null;
|
||||
if (this.props.badges && this.props.badges.length > 0) {
|
||||
badges = [];
|
||||
this.props.badges.map(function(b) {
|
||||
let style = 'badge' + (b.color ? ' ' + b.color : '');
|
||||
badges.push(<span className={style} key={b.key || b.name}>{b.name}</span>);
|
||||
if (b.icon) {
|
||||
badges.push(<i className="material-icons as-badge" key={b.key || b.icon}>{icon_mapping[b.icon]}</i>);
|
||||
} else {
|
||||
const style = 'badge' + (b.color ? ' ' + b.color : '');
|
||||
badges.push(<span className={style} key={b.key || b.name}>{b.name}</span>);
|
||||
}
|
||||
});
|
||||
return <React.Fragment>{badges}</React.Fragment>;
|
||||
}
|
||||
return <span>{badges}</span>;
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
+13
-4
@@ -39,8 +39,17 @@ export default class Contact extends React.Component {
|
||||
const online = this.props.now ? 'online' : 'offline';
|
||||
const avatar = this.props.avatar ? this.props.avatar : true;
|
||||
const badges = this.props.badges ? this.props.badges.slice() : [];
|
||||
if (this.props.showMode && this.props.acs) {
|
||||
badges.push({name: this.props.acs.getMode(), key: 'mode'});
|
||||
const icon_badges = [];
|
||||
if (this.props.acs) {
|
||||
if (this.props.showMode) {
|
||||
badges.push({name: this.props.acs.getMode(), key: 'mode'});
|
||||
}
|
||||
if (this.props.acs.isMuted()) {
|
||||
icon_badges.push({icon: 'muted'});
|
||||
}
|
||||
if (!this.props.acs.isJoiner()) {
|
||||
icon_badges.push({icon: 'banned'});
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -58,10 +67,10 @@ export default class Contact extends React.Component {
|
||||
</div>
|
||||
<div className="text-box">
|
||||
<div><span className="contact-title">{title}</span>
|
||||
{this.props.unread > 0 ? <UnreadBadge count={this.props.unread} /> : null}
|
||||
<UnreadBadge count={this.props.unread} /><ContactBadges badges={icon_badges} />
|
||||
</div>
|
||||
{this.props.comment ? <div className="contact-comment">{this.props.comment}</div> : null}
|
||||
<ContactBadges badges={badges} />
|
||||
<span><ContactBadges badges={badges} /></span>
|
||||
</div>
|
||||
{this.props.showContextMenu ?
|
||||
<span className="menuTrigger">
|
||||
|
||||
@@ -4,6 +4,7 @@ import ReactDOM from 'react-dom';
|
||||
import { injectIntl, defineMessages } from 'react-intl';
|
||||
|
||||
import { REM_SIZE } from '../config.js';
|
||||
import HashNavigation from '../lib/navigation.js';
|
||||
|
||||
const messages = defineMessages({
|
||||
info: {
|
||||
@@ -126,12 +127,17 @@ class ContextMenu extends React.Component {
|
||||
'topic_unblock': {
|
||||
id: 'topic_unblock',
|
||||
title: formatMessage(messages.unblock),
|
||||
handler: this.topicPermissionSetter.bind(this, '+J')
|
||||
handler: this.topicPermissionSetter.bind(this, '+JP')
|
||||
},
|
||||
'topic_block': {
|
||||
id: 'topic_block',
|
||||
title: formatMessage(messages.block),
|
||||
handler: this.topicPermissionSetter.bind(this, '-J')
|
||||
handler: (params, errorHandler) => {
|
||||
return this.topicPermissionSetter('-JP', params, errorHandler).then((ctrl) => {
|
||||
this.props.onTopicRemoved(params.topicName);
|
||||
return ctrl;
|
||||
});
|
||||
}
|
||||
},
|
||||
'topic_delete': {
|
||||
id: 'topic_delete',
|
||||
@@ -302,11 +308,13 @@ class ContextMenu extends React.Component {
|
||||
am = topic.getAccessMode().updateWant(mode).getWant();
|
||||
}
|
||||
|
||||
return topic.setMeta({sub: {user: params.user, mode: am}}).catch((err) => {
|
||||
if (errorHandler) {
|
||||
let result = topic.setMeta({sub: {user: params.user, mode: am}});
|
||||
if (errorHandler) {
|
||||
result = result.catch((err) => {
|
||||
errorHandler(err.message, 'err');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
/* GroupSubs: a list of group subscribers currently online */
|
||||
|
||||
// GroupSubs: a list of group subscribers currently online.
|
||||
// Show in the topic title bar
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import { MAX_ONLINE_IN_TOPIC } from '../config.js'
|
||||
|
||||
import LetterTile from './letter-tile.jsx';
|
||||
import { makeImageUrl } from '../lib/blob-helpers.js';
|
||||
@@ -12,7 +15,10 @@ export default class GroupSubs extends React.Component {
|
||||
|
||||
render() {
|
||||
const usersOnline = [];
|
||||
(this.props.subscribers || []).map((sub) => {
|
||||
const totalCount = (this.props.subscribers || []).length;
|
||||
const countToShow = Math.min(MAX_ONLINE_IN_TOPIC, totalCount);
|
||||
|
||||
(this.props.subscribers || []).some((sub) => {
|
||||
usersOnline.push(
|
||||
<div className="avatar-box" key={sub.user}>
|
||||
<LetterTile
|
||||
@@ -21,9 +27,17 @@ export default class GroupSubs extends React.Component {
|
||||
title={sub.public ? sub.public.fn : null} />
|
||||
</div>
|
||||
);
|
||||
return usersOnline.length == countToShow;
|
||||
});
|
||||
|
||||
return (
|
||||
<div id="topic-users">{usersOnline}</div>
|
||||
<div id="topic-users">{usersOnline} {totalCount > countToShow ?
|
||||
<span>
|
||||
<FormattedMessage id="more_online_members" defaultMessage="+{overflow} more"
|
||||
description="Shown in MessagesView title bar when the number of online subscribers exceeds MAX_ONLINE_IN_TOPIC"
|
||||
values={{ overflow: (totalCount - countToShow) }} />
|
||||
</span> : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -8,12 +8,15 @@ export default class LetterTile extends React.PureComponent {
|
||||
render() {
|
||||
let avatar;
|
||||
if (this.props.avatar === true) {
|
||||
const isGroup = (Tinode.topicType(this.props.topic) == 'grp');
|
||||
if (this.props.topic && this.props.title && this.props.title.trim()) {
|
||||
let letter = this.props.title.trim().charAt(0);
|
||||
let color = "lettertile dark-color" + (Math.abs(stringHash(this.props.topic)) % 16);
|
||||
const letter = this.props.title.trim().charAt(0);
|
||||
const color = 'lettertile '
|
||||
+ (isGroup ? 'color' : 'dark-color')
|
||||
+ (Math.abs(stringHash(this.props.topic)) % 16);
|
||||
avatar = (<div className={color}><div>{letter}</div></div>)
|
||||
} else {
|
||||
avatar = (Tinode.topicType(this.props.topic) == 'grp') ?
|
||||
avatar = isGroup ?
|
||||
<i className="material-icons">group</i> :
|
||||
<i className="material-icons">person</i>;
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ export default class MoreButton extends React.PureComponent {
|
||||
}
|
||||
|
||||
handleToggle() {
|
||||
let open = !this.state.open;
|
||||
const open = !this.state.open;
|
||||
this.setState({open: open});
|
||||
if (this.props.onToggle) {
|
||||
this.props.onToggle(open);
|
||||
@@ -21,7 +21,7 @@ export default class MoreButton extends React.PureComponent {
|
||||
}
|
||||
|
||||
render() {
|
||||
return (<label className="small" onClick={this.handleToggle}>{this.props.title}...
|
||||
return (<label className="small clean-clickable" onClick={this.handleToggle}>{this.props.title}...
|
||||
{this.state.open ? <i className="material-icons">expand_more</i> :
|
||||
<i className="material-icons">chevron_right</i>}
|
||||
</label>);
|
||||
|
||||
@@ -35,7 +35,7 @@ class ReceivedMarker extends React.PureComponent {
|
||||
if (this.props.received <= Tinode.MESSAGE_STATUS_SENDING) {
|
||||
marker = (<i className="material-icons small">access_time</i>); // watch face
|
||||
} else if (this.props.received == Tinode.MESSAGE_STATUS_FAILED) {
|
||||
marker = (<i className="material-icons small red">error_outline</i>); // icon (!)
|
||||
marker = (<i className="material-icons small amber">warning</i>); // yellow icon /!\
|
||||
} else if (this.props.received == Tinode.MESSAGE_STATUS_SENT) {
|
||||
marker = (<i className="material-icons small">done</i>); // checkmark
|
||||
} else if (this.props.received == Tinode.MESSAGE_STATUS_RECEIVED) {
|
||||
|
||||
+25
-26
@@ -38,32 +38,36 @@ export default class TagManager extends React.Component {
|
||||
|
||||
handleTagInput(text) {
|
||||
this.setState({tagInput: text});
|
||||
// Check if user entered ',', ' ' or ';'
|
||||
if (text.length > 0) {
|
||||
var last = text[text.length-1];
|
||||
if (last == ',' || last == ' ' || last == ';') {
|
||||
var tag = text.substr(0, text.length-1).trim();
|
||||
if (tag.length >= MIN_TAG_LENGTH) {
|
||||
this.handleAddTag(tag);
|
||||
const last = text[text.length-1];
|
||||
if (text[0] == '"') {
|
||||
// This is a quoted string.
|
||||
if (text.length > 1 && last == '"') {
|
||||
this.handleAddTag(text.substring(1, text.length-1));
|
||||
}
|
||||
} else if (last == ',' || last == ' ' || last == ';' || last == '"') {
|
||||
// User entered ',', ' ' or ';'
|
||||
this.handleAddTag(text.substring(0, text.length-1).trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleAddTag(tag) {
|
||||
tag = tag.trim();
|
||||
if (tag.length > 0) {
|
||||
let tags = this.state.tags.slice(0);
|
||||
const tags = this.state.tags.slice(0);
|
||||
tags.push(tag);
|
||||
|
||||
this.setState({tags: tags, tagInput: ''});
|
||||
if (this.props.onTagsChanged) {
|
||||
this.props.onTagsChanged(tags);
|
||||
}
|
||||
return tags;
|
||||
}
|
||||
return this.state.tags;
|
||||
}
|
||||
|
||||
handleRemoveTag(tag, index) {
|
||||
var tags = this.state.tags.slice(0);
|
||||
const tags = this.state.tags.slice(0);
|
||||
tags.splice(index, 1);
|
||||
this.setState({tags: tags});
|
||||
if (this.props.onTagsChanged) {
|
||||
@@ -72,16 +76,9 @@ export default class TagManager extends React.Component {
|
||||
}
|
||||
|
||||
handleSubmit() {
|
||||
let tags = this.state.tags.slice(0);
|
||||
let inp = this.state.tagInput.trim();
|
||||
if (inp.length > 0) {
|
||||
tags.push(inp);
|
||||
if (this.props.onTagsChanged) {
|
||||
this.props.onTagsChanged(tags);
|
||||
}
|
||||
}
|
||||
this.props.onSubmit(tags);
|
||||
this.setState({activated: false, tagInput: '', tags: this.props.tags});
|
||||
// Add unprocessed input to tags, submit the list.
|
||||
this.props.onSubmit(this.handleAddTag(this.state.tagInput.trim()));
|
||||
this.setState({activated: false, tags: this.props.tags});
|
||||
}
|
||||
|
||||
handleCancel() {
|
||||
@@ -92,19 +89,21 @@ export default class TagManager extends React.Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
var tags = [];
|
||||
let tags = [];
|
||||
if (this.state.activated) {
|
||||
this.state.tags.map(function(tag) {
|
||||
tags.push({user: tag});
|
||||
this.state.tags.map((tag) => {
|
||||
tags.push({user: tag, invalid: (tag.length < MIN_TAG_LENGTH)});
|
||||
});
|
||||
} else {
|
||||
this.state.tags.map(function(tag) {
|
||||
this.state.tags.map((tag) => {
|
||||
tags.push(<span className="badge" key={tags.length}>{tag}</span>);
|
||||
});
|
||||
if (tags.length == 0) {
|
||||
tags = (<i>
|
||||
<FormattedMessage id="tags_not_found" defaultMessage="No tags defined. Add some." description="" />
|
||||
</i>);
|
||||
tags = (
|
||||
<i>
|
||||
<FormattedMessage id="tags_not_found" defaultMessage="No tags defined. Add some." description="" />
|
||||
</i>
|
||||
);
|
||||
}
|
||||
}
|
||||
return (
|
||||
|
||||
+799
-468
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user