Compare commits

...
54 Commits
Author SHA1 Message Date
or-else ebe47b345d pix editing ofpinned messages 2023-04-05 10:14:07 -07:00
or-else 454dc204c8 minor visual tweaks 2023-03-25 11:13:04 -07:00
or-else d0c88fbd0d finishing touches, all seems to work 2023-03-24 15:15:40 -07:00
or-else e68ebc8336 works as expected on desktop, mobile needs work 2023-03-24 13:56:24 -07:00
or-else cca97d410e widget for pinned messages 2023-03-23 18:22:30 -07:00
or-else eafd87fb17 make bg change on click more subtle 2023-03-23 18:16:32 -07:00
or-else 71ee3fbec4 widget for showing pinned messages (wip) 2023-03-23 18:09:11 -07:00
or-else 11fe81fdf1 adding pinned messages (wip) 2023-03-22 17:50:33 -07:00
or-else 9640c604b9 Merge branch 'devel' into next 2023-03-22 11:00:37 -07:00
or-else 6273b3287f rebuilt with correct version of the sdk 2023-03-22 09:22:39 -07:00
or-else 9c5e3a3eca dependencies updated 2023-03-22 09:19:31 -07:00
or-else 9098c1f6a6 merge devel 2023-03-20 17:21:25 -07:00
or-else a2439be540 Merge branch 'devel' into next 2023-03-20 16:12:41 -07:00
or-else a2fb3fe1a0 rebuilt 2023-03-20 16:12:36 -07:00
or-else 560e3369e4 rebuilt 2023-03-20 16:11:43 -07:00
or-else 491175cecc dependencies updated 2023-03-20 16:10:36 -07:00
or-else 6d96d327f1 fix backward scrolling on fetching older messages 2023-03-20 11:35:01 -07:00
or-else 6ee2851943 rebuilt 2023-03-19 15:14:24 -07:00
or-else 192a3d4ec2 don't crash if FIREBASE_INIT is not defined 2023-03-19 15:14:00 -07:00
or-else c6f7911c82 dependencies updated, version to 0.22.0, rebuilt 2023-03-15 10:43:29 -07:00
or-else 9a1f0c734a dependencies updated 2023-03-15 08:41:22 -07:00
or-else 52f879490e fix for tinode/chat/issues/838 2023-03-14 18:14:33 -07:00
or-else 8df3c34cf4 moved hiding components to the top level 2023-03-14 15:54:22 -07:00
or-else 2fb6a9bb0a escape key closes image, video, or document preview 2023-03-14 14:23:16 -07:00
or-else 5ae58fb2a6 allow message input textarea height to grow with more text 2023-03-14 13:53:21 -07:00
or-else 43798ea1d9 minor tweaks 2023-03-14 09:44:54 -07:00
or-else 323042bd96 rebuild 2023-03-03 15:56:09 -08:00
or-else 172b9eb26d removed duplicate styling 2023-03-03 15:52:53 -08:00
or-else 1b90c613f7 fix drag and drop in firefox 2023-03-02 16:19:13 -08:00
or-else c6b4460389 merge devel and bump version to 0.22.0-rc1 2023-03-02 13:32:42 -08:00
or-else 497318b722 updated dependencies and bumped version 2023-03-01 16:49:14 -08:00
or-else 423f53880d Merge branch 'devel' 2023-03-01 14:34:32 -08:00
or-else a93ede25e9 make date bubble lighter 2023-02-24 18:50:09 -08:00
or-else b4d3e0274f fix color of the date bubble 2023-02-24 16:23:22 -08:00
or-else 4f116ab3e6 replace box-shadow with filter: drop-shadow to remove artifacts 2023-02-24 15:39:08 -08:00
or-else 75b2880b63 bump version 2023-02-20 16:18:28 -08:00
or-else 1149980a47 rebuilt 2023-02-20 16:05:36 -08:00
aforge 87b48cd67d Fix strutil.secondsToTime(). 2023-02-16 13:26:55 -08:00
aforge 4be09a034e Calls: create data channel only on the caller's side.
Removes redundancy and a memory leak (and thus an error upon closing).
2023-02-16 13:25:15 -08:00
aforge 3c0b57333c Calls: create data channel only on the caller's side.
Removes redundancy and a memory leak (and thus an error upon closing).
2023-02-15 17:55:29 -08:00
Gene 0c9b23eee9 Merge pull request #118 from sun2008mt/master
Update zh.json
2023-02-09 08:33:58 -08:00
Marc SUN d11f94095f Update zh.json 2023-02-09 18:14:08 +08:00
or-else 133ed3cc53 merge devel 2023-02-08 13:40:09 -08:00
or-else 7bc50fac36 handing of "busy" webrtc status 2023-02-08 13:30:50 -08:00
aforge fbdd017d38 Fix strutil.secondsToTime(). 2023-02-06 22:35:48 -08:00
or-else 8fdb4a8a4c bump version to 0.21.1 and rebuild 2023-02-06 10:22:00 -08:00
or-else e1b1ae503f fix issues with video uploading and display 2023-02-06 09:58:56 -08:00
or-else 7b44913724 fix display of broken videos 2023-02-06 09:56:50 -08:00
or-else 7e3b2fc254 switch tinode-sdk from browserify to webpack 2023-02-06 09:54:12 -08:00
aforge fc6a8fbafc Do not crash in MessagesView.handleMessageUpdate if topic gets unset. 2023-02-06 09:51:53 -08:00
or-else 111b9d8641 rebuilt with the sdk fix 2023-02-05 17:49:31 -08:00
or-else eeea27ca82 fix issues with video uploading and display 2023-02-05 17:08:41 -08:00
or-else fc2ff79329 fix display of broken videos 2023-02-05 13:46:08 -08:00
or-else f755314c3b rebuilt 2023-02-05 12:43:17 -08:00
109 changed files with 5986 additions and 9874 deletions
+115 -13
View File
@@ -197,7 +197,7 @@ textarea {
height: 2.5rem; height: 2.5rem;
border-bottom: 1px solid #ccc; border-bottom: 1px solid #ccc;
box-shadow: 0 1px 0 rgba(0, 0, 0, 0.1); box-shadow: 0 1px 0 rgba(0, 0, 0, 0.1);
transition: all 0.1s ease-in-out; transition: all 0.1s ease-in-out, height 0s;
} }
input[type="text"], input[type="text"],
@@ -273,11 +273,6 @@ textarea.inplace-edit {
background-color: rgba(0,0,0,0.3); background-color: rgba(0,0,0,0.3);
} }
/* Firefox only */
* {
scrollbar-width: thin;
scrollbar-color: silver;
}
/* END of Scrollbar styling. */ /* END of Scrollbar styling. */
/* BEGIN Popup/dropdown menu */ /* BEGIN Popup/dropdown menu */
@@ -315,9 +310,8 @@ ul.menu > li.separator:hover {
} }
/* The down arrow head - trigger for context menu */ /* The down arrow head - trigger for context menu */
li .menuTrigger { .menuTrigger {
position: absolute; position: absolute;
top: 0.25rem;
right: 0.25rem; right: 0.25rem;
width: 1.4rem; width: 1.4rem;
height: 1.4rem; height: 1.4rem;
@@ -327,6 +321,13 @@ li .menuTrigger {
opacity: 0; opacity: 0;
background-color: inherit; background-color: inherit;
} }
.menuTrigger a {
color: #757575;
}
li .menuTrigger {
top: 0.25rem;
}
li:hover .menuTrigger { li:hover .menuTrigger {
opacity: 0.9; opacity: 0.9;
} }
@@ -1392,6 +1393,7 @@ img.channel {
#topic-users { #topic-users {
margin-left: auto; margin-left: auto;
margin-right: 1rem; margin-right: 1rem;
width: min-content;
} }
#topic-users .avatar-box { #topic-users .avatar-box {
@@ -1513,6 +1515,13 @@ img.channel {
color: #2196f3!important; color: #2196f3!important;
} }
/* The textarea where the message is entered. */
#send-message-input {
overflow: hidden auto;
min-height: 2.5rem;
max-height: 6rem;
}
/* New chat invitation panel */ /* New chat invitation panel */
.accept-invite-panel { .accept-invite-panel {
@@ -1608,6 +1617,7 @@ img.channel {
line-height: 1.5rem; line-height: 1.5rem;
background-color: #eceff1; background-color: #eceff1;
box-shadow: 0px -2px 3px -1px rgba(0,0,0,0.1); /* Faint shadow above panel */ box-shadow: 0px -2px 3px -1px rgba(0,0,0,0.1); /* Faint shadow above panel */
z-index: 1;
} }
#image-preview-container .material-icons { #image-preview-container .material-icons {
@@ -1856,9 +1866,9 @@ video {
text-align: left; text-align: left;
color: #212121; color: #212121;
border-radius: 0.5rem; border-radius: 0.5rem;
box-shadow: 0 1px 0.5px rgba(0, 0, 0, 0.12);
transition: ease-out background-color 0.5s; transition: ease-out background-color 0.5s;
transition-delay: 0.3s; transition-delay: 0.3s;
filter: drop-shadow(0.5px 0.5px 0.7px rgb(0,0,0,0.3));
} }
.chat-box li.single { .chat-box li.single {
@@ -1926,7 +1936,7 @@ video {
.meta .bubble.date { .meta .bubble.date {
font-size: smaller; font-size: smaller;
padding: 0.5rem 0.5rem 0.5rem 0.5rem; padding: 0.5rem 0.5rem 0.5rem 0.5rem;
background-color: rgb(244 244 244 / 66%); background-color: rgb(255 255 255 / 80%);
text-transform: uppercase; text-transform: uppercase;
} }
@@ -1940,7 +1950,7 @@ video {
bottom: 0; bottom: 0;
background-color: #c5e1a5; background-color: #c5e1a5;
clip-path: polygon(100% 0, 0 110%, 100% 110%); clip-path: polygon(100% 0, 0 110%, 100% 110%);
box-shadow: 0 1px 0.5px rgb(0 0 0 / 12%); -webkit-clip-path: polygon(100% 0, 0 110%, 100% 110%);
border-bottom-left-radius: 0.3rem; border-bottom-left-radius: 0.3rem;
transition: ease-out background-color 0.5s; transition: ease-out background-color 0.5s;
transition-delay: 0.3s; transition-delay: 0.3s;
@@ -1960,7 +1970,7 @@ video {
bottom: 0; bottom: 0;
background-color: #fafafa; background-color: #fafafa;
clip-path: polygon(0 0, 100% 110%, 0 110%); clip-path: polygon(0 0, 100% 110%, 0 110%);
box-shadow: 0 1px 0.5px rgb(0 0 0 / 12%); -webkit-clip-path: polygon(0 0, 100% 110%, 0 110%);
border-bottom-right-radius: 0.3rem; border-bottom-right-radius: 0.3rem;
transition: ease-out background-color 0.5s; transition: ease-out background-color 0.5s;
transition-delay: 0.3s; transition-delay: 0.3s;
@@ -2317,6 +2327,7 @@ ul.tabbar li.active > a {
width: 100%; width: 100%;
padding-bottom: 100%; padding-bottom: 100%;
border-radius: 50%; border-radius: 50%;
user-select: none;
} }
/* Div with letter tile content */ /* Div with letter tile content */
@@ -3016,6 +3027,84 @@ ul.tabbar li.active > a {
color: #666C; color: #666C;
} }
/* Pinned messages */
#pinned-wrapper {
position: relative;
display: flex;
align-items: center;
height: 100%;
margin-right: 1rem;
}
.pinned {
position: relative;
display: flex;
align-items: center;
padding: 0.15rem 0.5rem 0.25rem 0.5rem;
background-color: #fafafa;
background-position: center;
line-height: 1.5em;
font-weight: lighter;
border-radius: 0 0.25rem 0.25rem 0;
color: #666;
height: 100%;
width: 100%;
min-width: 24rem;
cursor: pointer;
transition: background 0.4s;
overflow-x: hidden;
text-overflow: ellipsis;
}
.pinned:hover {
background-color: #fafdff;
}
.pinned:active {
background-color: #d8e8ff;
transition: background 0s;
}
.pinned-scroll {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
border-radius: 0.25rem 0 0 0.25rem;
background-color: #eeee;
justify-content: space-evenly;
height: 100%;
padding: 0.1rem;
margin-left: 0.35rem;
}
.pinned-scroll .dot {
width: 0.3rem;
height: 0.3rem;
border-radius: 50%;
background-color: #999;
}
.pinned-scroll .adot {
width: 0.45rem;
height: 0.45rem;
border-radius: 50%;
background-color: #039be5;
}
#pinned-wrapper .menuTrigger.upper {
top: 0.25rem;
}
#pinned-wrapper .menuTrigger.lower {
bottom: 0.25rem;
}
#pinned-wrapper:hover .menuTrigger {
opacity: 0.9;
}
#pinned-wrapper:hover .menuTrigger a {
color: #757575;
}
/* /*
Drag-and-drop area. Drag-and-drop area.
*/ */
@@ -3103,6 +3192,10 @@ ul.phone-country-selector span.country {
#mountPoint { #mountPoint {
padding: 0; padding: 0;
} }
.pinned {
min-width: 12rem;
}
} }
@media (max-width: 640px) { @media (max-width: 640px) {
@@ -3128,7 +3221,7 @@ ul.phone-country-selector span.country {
} }
/* Must make it a bit visible on mobile because there is no :hover */ /* Must make it a bit visible on mobile because there is no :hover */
li .menuTrigger { .menuTrigger {
opacity: 0.3; opacity: 0.3;
} }
@@ -3136,4 +3229,13 @@ ul.phone-country-selector span.country {
/* Just an indicator that the screen is in mobile mode */ /* Just an indicator that the screen is in mobile mode */
transition-property: -tinode-mobile; transition-property: -tinode-mobile;
} }
#pinned-wrapper {
height: 3.5rem;
margin: 0.25rem;
}
.pinned {
min-width: unset;
}
} }
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -25,7 +25,7 @@
<link rel="stylesheet" href="css/base.min.css" /> <link rel="stylesheet" href="css/base.min.css" />
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/react@18.2.0/umd/react.production.min.js"></script> <script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/react@18.2.0/umd/react.production.min.js"></script>
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/react-dom@18.2.0/umd/react-dom.production.min.js"></script> <script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/react-dom@18.2.0/umd/react-dom.production.min.js"></script>
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/react-intl@6.2.1/react-intl.iife.min.js"></script> <script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/react-intl@6.3.2/react-intl.iife.min.js"></script>
</head> </head>
<body> <body>
<noscript><div id="noscript-message"><div id="noscript-title">Javascript support is required.</div> <noscript><div id="noscript-message"><div id="noscript-title">Javascript support is required.</div>
+3997 -5361
View File
File diff suppressed because it is too large Load Diff
+19 -18
View File
@@ -1,10 +1,10 @@
{ {
"name": "tinode-webapp", "name": "tinode-webapp",
"description": "Tinode messenger for the web", "description": "Tinode messenger for the web",
"version": "0.21.0", "version": "0.23.0-rc1",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://github.com/tinode/example-react-js.git" "url": "https://github.com/tinode/webapp.git"
}, },
"scripts": { "scripts": {
"start": "echo 'Read installation instructions'", "start": "echo 'Read installation instructions'",
@@ -58,35 +58,36 @@
], ],
"main": "umd/index.prod.js", "main": "umd/index.prod.js",
"bugs": { "bugs": {
"url": "https://github.com/tinode/example-react-js/issues" "url": "https://github.com/tinode/webapp/issues"
}, },
"homepage": "https://github.com/tinode/chat", "homepage": "https://github.com/tinode/chat",
"email": "info@tinode.co", "email": "info@tinode.co",
"author": "Tinode Authors <info@tinode.co>", "author": "Tinode Authors <info@tinode.co>",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"firebase": "^9.14.0", "firebase": "^9.17.2",
"libphonenumber-js": "^1.10.18", "libphonenumber-js": "^1.10.24",
"npm-check-updates": "^16.7.12",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-intl": "^6.2.1", "react-intl": "^6.3.0",
"tinode-sdk": "^0.21.0", "tinode-sdk": "^0.23.0-rc1",
"webm-duration-fix": "^1.0.4" "webm-duration-fix": "^1.0.4"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.20.2", "@babel/core": "^7.21.3",
"@babel/preset-env": "^7.20.2", "@babel/preset-env": "^7.20.2",
"@babel/preset-react": "^7.18.6", "@babel/preset-react": "^7.18.6",
"@formatjs/cli": "^5.1.3", "@formatjs/cli": "^6.0.4",
"babel-loader": "^9.1.0", "babel-loader": "^9.1.2",
"babel-plugin-formatjs": "^10.3.31", "babel-plugin-formatjs": "^10.4.0",
"browserslist": "^4.21.4", "browserslist": "^4.21.5",
"copy-webpack-plugin": "^11.0.0", "copy-webpack-plugin": "^11.0.0",
"cssnano": "^5.1.14", "cssnano": "^5.1.15",
"glob": "^8.0.3", "glob": "^9.3.0",
"postcss": "^8.4.19", "postcss": "^8.4.21",
"postcss-cli": "^10.0.0", "postcss-cli": "^10.1.0",
"webpack": "^5.75.0", "webpack": "^5.76.1",
"webpack-cli": "^5.0.0" "webpack-cli": "^5.0.1"
} }
} }
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
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
+22 -1
View File
@@ -1634,11 +1634,32 @@
"missing": true, "missing": true,
"obsolete": false "obsolete": false
}, },
"change_phonel": { "change_phone": {
"translation": "", "translation": "",
"defaultMessage": "Change phone number", "defaultMessage": "Change phone number",
"description": "Prompt to change current phone number", "description": "Prompt to change current phone number",
"missing": true, "missing": true,
"obsolete": false "obsolete": false
},
"call_busy": {
"translation": "besetzt",
"defaultMessage": "busy",
"description": "Label for busy call line",
"missing": false,
"obsolete": false
},
"pin_message": {
"translation": "",
"defaultMessage": "Pin",
"description": "Menu item [Pin] for pinning message to the top.",
"missing": true,
"obsolete": false
},
"unpin_message": {
"translation": "",
"defaultMessage": "Unpin",
"description": "Menu item [Unpin] for un-pinning the message.",
"missing": true,
"obsolete": false
} }
} }
+22 -1
View File
@@ -1634,11 +1634,32 @@
"missing": false, "missing": false,
"obsolete": false "obsolete": false
}, },
"change_phonel": { "change_phone": {
"translation": "Change phone number", "translation": "Change phone number",
"defaultMessage": "Change phone number", "defaultMessage": "Change phone number",
"description": "Prompt to change current phone number", "description": "Prompt to change current phone number",
"missing": false, "missing": false,
"obsolete": false "obsolete": false
},
"call_busy": {
"translation": "busy",
"defaultMessage": "busy",
"description": "Label for busy call line",
"missing": false,
"obsolete": false
},
"pin_message": {
"translation": "Pin",
"defaultMessage": "Pin",
"description": "Menu item [Pin] for pinning message to the top.",
"missing": false,
"obsolete": false
},
"unpin_message": {
"translation": "Unpin",
"defaultMessage": "Unpin",
"description": "Menu item [Unpin] for un-pinning the message.",
"missing": false,
"obsolete": false
} }
} }
+22 -1
View File
@@ -1634,11 +1634,32 @@
"missing": true, "missing": true,
"obsolete": false "obsolete": false
}, },
"change_phonel": { "change_phone": {
"translation": "", "translation": "",
"defaultMessage": "Change phone number", "defaultMessage": "Change phone number",
"description": "Prompt to change current phone number", "description": "Prompt to change current phone number",
"missing": true, "missing": true,
"obsolete": false "obsolete": false
},
"call_busy": {
"translation": "ocupado",
"defaultMessage": "busy",
"description": "Label for busy call line",
"missing": false,
"obsolete": false
},
"pin_message": {
"translation": "",
"defaultMessage": "Pin",
"description": "Menu item [Pin] for pinning message to the top.",
"missing": true,
"obsolete": false
},
"unpin_message": {
"translation": "",
"defaultMessage": "Unpin",
"description": "Menu item [Unpin] for un-pinning the message.",
"missing": true,
"obsolete": false
} }
} }
+13 -1
View File
@@ -119,6 +119,10 @@
"defaultMessage": "Update", "defaultMessage": "Update",
"description": "Button [Update]" "description": "Button [Update]"
}, },
"call_busy": {
"defaultMessage": "busy",
"description": "Label for busy call line"
},
"call_cancelled": { "call_cancelled": {
"defaultMessage": "cancelled", "defaultMessage": "cancelled",
"description": "Label for cancelled outgoing call" "description": "Label for cancelled outgoing call"
@@ -159,7 +163,7 @@
"defaultMessage": "Change email", "defaultMessage": "Change email",
"description": "Prompt to change current email" "description": "Prompt to change current email"
}, },
"change_phonel": { "change_phone": {
"defaultMessage": "Change phone number", "defaultMessage": "Change phone number",
"description": "Prompt to change current phone number" "description": "Prompt to change current phone number"
}, },
@@ -752,6 +756,10 @@
"defaultMessage": "phone", "defaultMessage": "phone",
"description": "Dative case of 'phone', i.e. 'phone' in 'by phone'" "description": "Dative case of 'phone', i.e. 'phone' in 'by phone'"
}, },
"pin_message": {
"defaultMessage": "Pin",
"description": "Menu item [Pin] for pinning message to the top."
},
"private_editing_placeholder": { "private_editing_placeholder": {
"defaultMessage": "Visible to you only", "defaultMessage": "Visible to you only",
"description": "Placeholder for editing 'private'" "description": "Placeholder for editing 'private'"
@@ -908,6 +916,10 @@
"defaultMessage": "Unnamed", "defaultMessage": "Unnamed",
"description": "Title shown when the topic has no name" "description": "Title shown when the topic has no name"
}, },
"unpin_message": {
"defaultMessage": "Unpin",
"description": "Menu item [Unpin] for un-pinning the message."
},
"unrecognized_video_format": { "unrecognized_video_format": {
"defaultMessage": "Format of this video is not recognized", "defaultMessage": "Format of this video is not recognized",
"description": "Error message when uploaded video is invalid" "description": "Error message when uploaded video is invalid"
+22 -1
View File
@@ -1634,11 +1634,32 @@
"missing": true, "missing": true,
"obsolete": false "obsolete": false
}, },
"change_phonel": { "change_phone": {
"translation": "", "translation": "",
"defaultMessage": "Change phone number", "defaultMessage": "Change phone number",
"description": "Prompt to change current phone number", "description": "Prompt to change current phone number",
"missing": true, "missing": true,
"obsolete": false "obsolete": false
},
"call_busy": {
"translation": "occupé",
"defaultMessage": "busy",
"description": "Label for busy call line",
"missing": false,
"obsolete": false
},
"pin_message": {
"translation": "",
"defaultMessage": "Pin",
"description": "Menu item [Pin] for pinning message to the top.",
"missing": true,
"obsolete": false
},
"unpin_message": {
"translation": "",
"defaultMessage": "Unpin",
"description": "Menu item [Unpin] for un-pinning the message.",
"missing": true,
"obsolete": false
} }
} }
+22 -1
View File
@@ -1634,11 +1634,32 @@
"missing": true, "missing": true,
"obsolete": false "obsolete": false
}, },
"change_phonel": { "change_phone": {
"translation": "", "translation": "",
"defaultMessage": "Change phone number", "defaultMessage": "Change phone number",
"description": "Prompt to change current phone number", "description": "Prompt to change current phone number",
"missing": true, "missing": true,
"obsolete": false "obsolete": false
},
"call_busy": {
"translation": "바쁘다",
"defaultMessage": "busy",
"description": "Label for busy call line",
"missing": false,
"obsolete": false
},
"pin_message": {
"translation": "",
"defaultMessage": "Pin",
"description": "Menu item [Pin] for pinning message to the top.",
"missing": true,
"obsolete": false
},
"unpin_message": {
"translation": "",
"defaultMessage": "Unpin",
"description": "Menu item [Unpin] for un-pinning the message.",
"missing": true,
"obsolete": false
} }
} }
+22 -1
View File
@@ -1634,11 +1634,32 @@
"missing": true, "missing": true,
"obsolete": false "obsolete": false
}, },
"change_phonel": { "change_phone": {
"translation": "", "translation": "",
"defaultMessage": "Change phone number", "defaultMessage": "Change phone number",
"description": "Prompt to change current phone number", "description": "Prompt to change current phone number",
"missing": true, "missing": true,
"obsolete": false "obsolete": false
},
"call_busy": {
"translation": "ocupat",
"defaultMessage": "busy",
"description": "Label for busy call line",
"missing": false,
"obsolete": false
},
"pin_message": {
"translation": "",
"defaultMessage": "Pin",
"description": "Menu item [Pin] for pinning message to the top.",
"missing": true,
"obsolete": false
},
"unpin_message": {
"translation": "",
"defaultMessage": "Unpin",
"description": "Menu item [Unpin] for un-pinning the message.",
"missing": true,
"obsolete": false
} }
} }
+22 -1
View File
@@ -1634,11 +1634,32 @@
"missing": true, "missing": true,
"obsolete": false "obsolete": false
}, },
"change_phonel": { "change_phone": {
"translation": "", "translation": "",
"defaultMessage": "Change phone number", "defaultMessage": "Change phone number",
"description": "Prompt to change current phone number", "description": "Prompt to change current phone number",
"missing": true, "missing": true,
"obsolete": false "obsolete": false
},
"call_busy": {
"translation": "занято",
"defaultMessage": "busy",
"description": "Label for busy call line",
"missing": false,
"obsolete": false
},
"pin_message": {
"translation": "",
"defaultMessage": "Pin",
"description": "Menu item [Pin] for pinning message to the top.",
"missing": true,
"obsolete": false
},
"unpin_message": {
"translation": "",
"defaultMessage": "Unpin",
"description": "Menu item [Unpin] for un-pinning the message.",
"missing": true,
"obsolete": false
} }
} }
+22 -1
View File
@@ -1634,11 +1634,32 @@
"missing": true, "missing": true,
"obsolete": false "obsolete": false
}, },
"change_phonel": { "change_phone": {
"translation": "", "translation": "",
"defaultMessage": "Change phone number", "defaultMessage": "Change phone number",
"description": "Prompt to change current phone number", "description": "Prompt to change current phone number",
"missing": true, "missing": true,
"obsolete": false "obsolete": false
},
"call_busy": {
"translation": "忙碌的",
"defaultMessage": "busy",
"description": "Label for busy call line",
"missing": false,
"obsolete": false
},
"pin_message": {
"translation": "",
"defaultMessage": "Pin",
"description": "Menu item [Pin] for pinning message to the top.",
"missing": true,
"obsolete": false
},
"unpin_message": {
"translation": "",
"defaultMessage": "Unpin",
"description": "Menu item [Unpin] for un-pinning the message.",
"missing": true,
"obsolete": false
} }
} }
+23 -2
View File
@@ -448,7 +448,7 @@
"obsolete": false "obsolete": false
}, },
"label_default_access_mode": { "label_default_access_mode": {
"translation": "蓦然访问模式:", "translation": "默认访问模式:",
"defaultMessage": "Default access mode:", "defaultMessage": "Default access mode:",
"description": "Label for default access mode", "description": "Label for default access mode",
"missing": false, "missing": false,
@@ -1634,11 +1634,32 @@
"missing": true, "missing": true,
"obsolete": false "obsolete": false
}, },
"change_phonel": { "change_phone": {
"translation": "", "translation": "",
"defaultMessage": "Change phone number", "defaultMessage": "Change phone number",
"description": "Prompt to change current phone number", "description": "Prompt to change current phone number",
"missing": true, "missing": true,
"obsolete": false "obsolete": false
},
"call_busy": {
"translation": "忙碌的",
"defaultMessage": "busy",
"description": "Label for busy call line",
"missing": false,
"obsolete": false
},
"pin_message": {
"translation": "",
"defaultMessage": "Pin",
"description": "Menu item [Pin] for pinning message to the top.",
"missing": true,
"obsolete": false
},
"unpin_message": {
"translation": "",
"defaultMessage": "Unpin",
"description": "Menu item [Unpin] for un-pinning the message.",
"missing": true,
"obsolete": false
} }
} }
+1 -1
View File
@@ -10,7 +10,7 @@ import TinodeWeb from './views/tinode-web.jsx';
import HashNavigation from './lib/navigation.js'; import HashNavigation from './lib/navigation.js';
// Insert google analytics script and tag if configured. // Insert google analytics script and tag if configured.
if (FIREBASE_INIT && FIREBASE_INIT.measurementId) { if (typeof FIREBASE_INIT != 'undefined' && FIREBASE_INIT && FIREBASE_INIT.measurementId) {
const head = document.getElementsByTagName('head')[0]; const head = document.getElementsByTagName('head')[0];
let script = document.createElement('script'); let script = document.createElement('script');
script.src = 'https://www.googletagmanager.com/gtag/js?id=' + FIREBASE_INIT.measurementId; script.src = 'https://www.googletagmanager.com/gtag/js?id=' + FIREBASE_INIT.measurementId;
+1 -1
View File
@@ -227,7 +227,7 @@ export function filePasted(event, onImageSuccess, onAttachmentSuccess, onError)
// Get mime type from data URL header. // Get mime type from data URL header.
export function getMimeType(header) { export function getMimeType(header) {
var mime = /^data:(image\/[-+a-z0-9.]+);base64/.exec(header); const mime = /^data:(image\/[-+a-z0-9.]+);base64/.exec(header);
return (mime && mime.length > 1) ? mime[1] : null; return (mime && mime.length > 1) ? mime[1] : null;
} }
-2
View File
@@ -236,8 +236,6 @@ function handleVideoData(el, data, attr) {
if (data.ref || data.val) { if (data.ref || data.val) {
attr.onClick = this.onVideoPreview; attr.onClick = this.onVideoPreview;
attr.loading = 'lazy'; attr.loading = 'lazy';
} else {
attr.src = null;
} }
el = InlineVideo; el = InlineVideo;
} else { } else {
+1 -1
View File
@@ -39,7 +39,7 @@ export function secondsToTime(seconds, fixedMin) {
} }
let min = (Math.floor(seconds / 60) | 0) % 60; let min = (Math.floor(seconds / 60) | 0) % 60;
let hours = Math.floor(seconds / 36000) | 0; let hours = Math.floor(seconds / 3600) | 0;
if (fixedMin || hours > 0) { if (fixedMin || hours > 0) {
min = min < 10 ? `0${min}` : min; min = min < 10 ? `0${min}` : min;
+1
View File
@@ -172,6 +172,7 @@ export function deliveryMarker(received) {
case Tinode.MESSAGE_STATUS_SENDING: case Tinode.MESSAGE_STATUS_SENDING:
return { name: 'access_time' }; // watch face return { name: 'access_time' }; // watch face
case Tinode.MESSAGE_STATUS_FAILED: case Tinode.MESSAGE_STATUS_FAILED:
case Tinode.MESSAGE_STATUS_FATAL:
return { name: 'warning', color: 'danger-color' }; // yellow icon /!\ return { name: 'warning', color: 'danger-color' }; // yellow icon /!\
case Tinode.MESSAGE_STATUS_SENT: case Tinode.MESSAGE_STATUS_SENT:
return { name: 'done' }; // checkmark return { name: 'done' }; // checkmark
+1 -1
View File
@@ -1,3 +1,3 @@
// This is a generated file. Don't edit. // This is a generated file. Don't edit.
export const PACKAGE_VERSION = "0.21.0"; export const PACKAGE_VERSION = "0.23.0-rc1";
+1 -1
View File
@@ -27,7 +27,7 @@ export default class ContactsView extends React.Component {
const contacts = []; const contacts = [];
let unreadThreads = 0; let unreadThreads = 0;
let archivedCount = 0; let archivedCount = 0;
props.chatList.map((c) => { props.chatList.forEach(c => {
const blocked = c.acs && !c.acs.isJoiner(); const blocked = c.acs && !c.acs.isJoiner();
// Show only blocked contacts only when props.blocked == true. // Show only blocked contacts only when props.blocked == true.
if (blocked && props.blocked) { if (blocked && props.blocked) {
+114 -41
View File
@@ -17,6 +17,7 @@ import LetterTile from '../widgets/letter-tile.jsx';
import LoadSpinner from '../widgets/load-spinner.jsx'; import LoadSpinner from '../widgets/load-spinner.jsx';
import LogoView from './logo-view.jsx'; import LogoView from './logo-view.jsx';
import MetaMessage from '../widgets/meta-message.jsx'; import MetaMessage from '../widgets/meta-message.jsx';
import PinnedMessages from '../widgets/pinned-messages.jsx';
import SendMessage from '../widgets/send-message.jsx'; import SendMessage from '../widgets/send-message.jsx';
import VideoPreview from '../widgets/video-preview.jsx'; import VideoPreview from '../widgets/video-preview.jsx';
@@ -32,6 +33,8 @@ import { bytesToHumanSize, relativeDateFormat, shortDateFormat } from '../lib/st
const NOTIFICATION_EXEC_INTERVAL = 300; const NOTIFICATION_EXEC_INTERVAL = 300;
// Scroll distance before [go to latest message] button is shown. // Scroll distance before [go to latest message] button is shown.
const SHOW_GO_TO_LAST_DIST = 100; const SHOW_GO_TO_LAST_DIST = 100;
// Sctoll distance from the top when fetching the page of earlier messages is triggered.
const FETCH_PAGE_TRIGGER = 40;
const messages = defineMessages({ const messages = defineMessages({
online_now: { online_now: {
@@ -121,6 +124,7 @@ class MessagesView extends React.Component {
this.handleDescChange = this.handleDescChange.bind(this); this.handleDescChange = this.handleDescChange.bind(this);
this.handleSubsUpdated = this.handleSubsUpdated.bind(this); this.handleSubsUpdated = this.handleSubsUpdated.bind(this);
this.handleMessageUpdate = this.handleMessageUpdate.bind(this); this.handleMessageUpdate = this.handleMessageUpdate.bind(this);
this.handleAuxUpdate = this.handleAuxUpdate.bind(this);
this.handleAllMessagesReceived = this.handleAllMessagesReceived.bind(this); this.handleAllMessagesReceived = this.handleAllMessagesReceived.bind(this);
this.handleInfoReceipt = this.handleInfoReceipt.bind(this); this.handleInfoReceipt = this.handleInfoReceipt.bind(this);
this.handleExpandMedia = this.handleExpandMedia.bind(this); this.handleExpandMedia = this.handleExpandMedia.bind(this);
@@ -142,9 +146,11 @@ class MessagesView extends React.Component {
this.handleEditMessage = this.handleEditMessage.bind(this); this.handleEditMessage = this.handleEditMessage.bind(this);
this.handleCancelReply = this.handleCancelReply.bind(this); this.handleCancelReply = this.handleCancelReply.bind(this);
this.handleQuoteClick = this.handleQuoteClick.bind(this); this.handleQuoteClick = this.handleQuoteClick.bind(this);
this.handleUnpinMessage = this.handleUnpinMessage.bind(this);
this.handleCallHangup = this.handleCallHangup.bind(this); this.handleCallHangup = this.handleCallHangup.bind(this);
this.isDragEnabled = this.isDragEnabled.bind(this); this.isDragEnabled = this.isDragEnabled.bind(this);
this.handleDragStart = this.handleDragStart.bind(this);
this.handleDragIn = this.handleDragIn.bind(this); this.handleDragIn = this.handleDragIn.bind(this);
this.handleDragOut = this.handleDragOut.bind(this); this.handleDragOut = this.handleDragOut.bind(this);
this.handleDrag = this.handleDrag.bind(this); this.handleDrag = this.handleDrag.bind(this);
@@ -181,12 +187,7 @@ class MessagesView extends React.Component {
} }
// Drag and drop events // Drag and drop events
if (this.dndRef) { this.mountDnDEvents(this.dndRef);
this.dndRef.addEventListener('dragenter', this.handleDragIn);
this.dndRef.addEventListener('dragleave', this.handleDragOut);
this.dndRef.addEventListener('dragover', this.handleDrag);
this.dndRef.addEventListener('drop', this.handleDrop);
}
} }
componentWillUnmount() { componentWillUnmount() {
@@ -199,6 +200,7 @@ class MessagesView extends React.Component {
// Drag and drop events // Drag and drop events
if (this.dndRef) { if (this.dndRef) {
this.dndRef.removeEventListener('dragstart', this.handleDragStart);
this.dndRef.removeEventListener('dragenter', this.handleDragIn); this.dndRef.removeEventListener('dragenter', this.handleDragIn);
this.dndRef.removeEventListener('dragleave', this.handleDragOut); this.dndRef.removeEventListener('dragleave', this.handleDragOut);
this.dndRef.removeEventListener('dragover', this.handleDrag); this.dndRef.removeEventListener('dragover', this.handleDrag);
@@ -210,12 +212,12 @@ class MessagesView extends React.Component {
// or vertical shrinking. // or vertical shrinking.
componentDidUpdate(prevProps, prevState) { componentDidUpdate(prevProps, prevState) {
if (this.messagesScroller && if (this.messagesScroller &&
(prevState.topic != this.state.topic || prevState.messageCount != this.state.messageCount)) { (prevState.topic != this.state.topic || prevState.maxSeqId != this.state.maxSeqId ||
prevState.minSeqId != this.state.minSeqId)) {
// New message. // New message.
if (this.state.scrollPosition < SHOW_GO_TO_LAST_DIST) { if (this.state.scrollPosition < SHOW_GO_TO_LAST_DIST) {
this.messagesScroller.scrollTop = this.messagesScroller.scrollHeight - this.messagesScroller.scrollTop = this.messagesScroller.scrollHeight -
this.state.scrollPosition - this.state.scrollPosition - this.messagesScroller.offsetHeight;
this.messagesScroller.offsetHeight;
} }
} }
@@ -235,6 +237,7 @@ class MessagesView extends React.Component {
topic.onMetaDesc = this.handleDescChange; topic.onMetaDesc = this.handleDescChange;
topic.onSubsUpdated = this.handleSubsUpdated; topic.onSubsUpdated = this.handleSubsUpdated;
topic.onPres = this.handleSubsUpdated; topic.onPres = this.handleSubsUpdated;
topic.onAuxUpdated = this.handleAuxUpdate;
} }
} }
@@ -261,7 +264,8 @@ class MessagesView extends React.Component {
if (!nextProps.topic) { if (!nextProps.topic) {
// Default state: no topic. // Default state: no topic.
nextState = { nextState = {
messageCount: 0, minSeqId: -1,
maxSeqId: -1,
latestClearId: -1, latestClearId: -1,
onlineSubs: [], onlineSubs: [],
topic: null, topic: null,
@@ -285,7 +289,9 @@ class MessagesView extends React.Component {
reply: null, reply: null,
contentToEdit: null, contentToEdit: null,
showGoToLastButton: false, showGoToLastButton: false,
dragging: false dragging: false,
pins: [],
selectedPin: 0
}; };
} else if (nextProps.topic != prevState.topic) { } else if (nextProps.topic != prevState.topic) {
const topic = nextProps.tinode.getTopic(nextProps.topic); const topic = nextProps.tinode.getTopic(nextProps.topic);
@@ -304,7 +310,8 @@ class MessagesView extends React.Component {
fetchingMessages: false, fetchingMessages: false,
showGoToLastButton: false, showGoToLastButton: false,
contentToEdit: null, contentToEdit: null,
dragging: false dragging: false,
selectedPin: 0
}; };
if (nextProps.forwardMessage) { if (nextProps.forwardMessage) {
@@ -356,9 +363,11 @@ class MessagesView extends React.Component {
}); });
} }
Object.assign(nextState, { Object.assign(nextState, {
messageCount: topic.messageCount(), minSeqId: topic.minMsgSeq(),
maxSeqId: topic.maxMsgSeq(),
latestClearId: topic.maxClearId(), latestClearId: topic.maxClearId(),
channel: topic.isChannelType() channel: topic.isChannelType(),
pins: (topic.aux('pins') || []).slice()
}); });
if (nextProps.callTopic == topic.name && shouldPresentCallPanel(nextProps.callState)) { if (nextProps.callTopic == topic.name && shouldPresentCallPanel(nextProps.callState)) {
@@ -367,13 +376,15 @@ class MessagesView extends React.Component {
} else { } else {
// Invalid topic. // Invalid topic.
Object.assign(nextState, { Object.assign(nextState, {
messageCount: 0, minSeqId: -1,
maxSeqId: -1,
latestClearId: -1, latestClearId: -1,
onlineSubs: [], onlineSubs: [],
title: '', title: '',
avatar: null, avatar: null,
peerMessagingDisabled: false, peerMessagingDisabled: false,
channel: false channel: false,
pins: []
}); });
} }
} else { } else {
@@ -391,6 +402,9 @@ class MessagesView extends React.Component {
if (nextProps.acs.isReader() != prevState.isReader) { if (nextProps.acs.isReader() != prevState.isReader) {
nextState.isReader = !prevState.isReader; nextState.isReader = !prevState.isReader;
} }
if (nextProps.acs.isAdmin() != prevState.isAdmin) {
nextState.isAdmin = !prevState.isAdmin;
}
if (!nextProps.acs.isReader('given') != prevState.readingBlocked) { if (!nextProps.acs.isReader('given') != prevState.readingBlocked) {
nextState.readingBlocked = !prevState.readingBlocked; nextState.readingBlocked = !prevState.readingBlocked;
} }
@@ -404,6 +418,9 @@ class MessagesView extends React.Component {
if (prevState.isReader) { if (prevState.isReader) {
nextState.isReader = false; nextState.isReader = false;
} }
if (prevState.isAdmin) {
nextState.isAdmin = false;
}
if (!prevState.readingBlocked) { if (!prevState.readingBlocked) {
prevState.readingBlocked = true; prevState.readingBlocked = true;
} }
@@ -434,7 +451,7 @@ class MessagesView extends React.Component {
// Don't request the tags. They are useless unless the user // Don't request the tags. They are useless unless the user
// is the owner and is editing the topic. // is the owner and is editing the topic.
let getQuery = topic.startMetaQuery().withLaterDesc().withLaterSub(); let getQuery = topic.startMetaQuery().withLaterDesc().withLaterSub().withAux();
if (this.state.isReader || newTopic) { if (this.state.isReader || newTopic) {
// Reading is either permitted or we don't know because it's a new topic. Ask for messages. // Reading is either permitted or we don't know because it's a new topic. Ask for messages.
getQuery = getQuery.withLaterData(MESSAGES_PAGE); getQuery = getQuery.withLaterData(MESSAGES_PAGE);
@@ -457,24 +474,24 @@ class MessagesView extends React.Component {
this.setState({topic: ctrl.topic}); this.setState({topic: ctrl.topic});
} }
this.props.onNewTopicCreated(this.props.topic, ctrl.topic); this.props.onNewTopicCreated(this.props.topic, ctrl.topic);
// If there are unsent messages (except video call messages), // If there are unsent messages (except hard-failed and video call messages),
// try sending them now. Unsent video call messages will be dropped. // try sending them now. Hard-failed and unsent video call messages will be dropped.
let calls = []; let discard = [];
topic.queuedMessages((pub) => { topic.queuedMessages(pub => {
if (pub._sending) { if (pub._sending) {
return; return;
} }
if (pub.head && pub.head.webrtc) { if (pub._fatal || (pub.head && pub.head.webrtc)) {
// Filter out unsent video call messages. // Filter out unsent failed & video call messages.
calls.push(pub.seq); discard.push(pub.seq);
return; return;
} }
if (topic.isSubscribed()) { if (topic.isSubscribed()) {
this.retrySend(pub); this.retrySend(pub);
} }
}); });
if (calls.length > 0) { if (discard.length > 0) {
topic.delMessagesList(calls, true); topic.delMessagesList(discard, true);
} }
}) })
.catch(err => { .catch(err => {
@@ -505,6 +522,7 @@ class MessagesView extends React.Component {
oldTopic.onMetaDesc = undefined; oldTopic.onMetaDesc = undefined;
oldTopic.onSubsUpdated = undefined; oldTopic.onSubsUpdated = undefined;
oldTopic.onPres = undefined; oldTopic.onPres = undefined;
oldTopic.onAuxUpdated = undefined;
}); });
} }
} }
@@ -514,7 +532,8 @@ class MessagesView extends React.Component {
if (node) { if (node) {
node.addEventListener('scroll', this.handleScrollEvent); node.addEventListener('scroll', this.handleScrollEvent);
this.messagesScroller = node; this.messagesScroller = node;
this.messagesScroller.scrollTop = this.messagesScroller.scrollHeight - this.state.scrollPosition; this.messagesScroller.scrollTop = this.messagesScroller.scrollHeight -
this.state.scrollPosition - this.messagesScroller.offsetHeight;
} }
} }
@@ -531,7 +550,7 @@ class MessagesView extends React.Component {
return; return;
} }
if (event.target.scrollTop <= 0) { if (event.target.scrollTop <= FETCH_PAGE_TRIGGER) {
const topic = this.props.tinode.getTopic(this.state.topic); const topic = this.props.tinode.getTopic(this.state.topic);
if (topic && topic.isSubscribed() && topic.msgHasMoreMessages()) { if (topic && topic.isSubscribed() && topic.msgHasMoreMessages()) {
this.setState({fetchingMessages: true}, _ => { this.setState({fetchingMessages: true}, _ => {
@@ -543,9 +562,10 @@ class MessagesView extends React.Component {
} }
} }
/* Mound draf and drop events */ /* Mount drag and drop events */
mountDnDEvents(dnd) { mountDnDEvents(dnd) {
if (dnd) { if (dnd) {
dnd.addEventListener('dragstart', this.handleDragStart);
dnd.addEventListener('dragenter', this.handleDragIn); dnd.addEventListener('dragenter', this.handleDragIn);
dnd.addEventListener('dragleave', this.handleDragOut); dnd.addEventListener('dragleave', this.handleDragOut);
dnd.addEventListener('dragover', this.handleDrag); dnd.addEventListener('dragover', this.handleDrag);
@@ -557,7 +577,7 @@ class MessagesView extends React.Component {
goToLatestMessage() { goToLatestMessage() {
this.setState({scrollPosition: 0}); this.setState({scrollPosition: 0});
if (this.messagesScroller) { if (this.messagesScroller) {
this.messagesScroller.scrollTop = this.messagesScroller.scrollHeight; this.messagesScroller.scrollTop = this.messagesScroller.scrollHeight - this.messagesScroller.offsetHeight;
} }
} }
@@ -578,6 +598,7 @@ class MessagesView extends React.Component {
this.setState({ this.setState({
isWriter: desc.acs.isWriter(), isWriter: desc.acs.isWriter(),
isReader: desc.acs.isReader(), isReader: desc.acs.isReader(),
isAdmin: desc.acs.isAdmin(),
readingBlocked: !desc.acs.isReader('given'), readingBlocked: !desc.acs.isReader('given'),
unconfirmed: isUnconfirmed(desc.acs), unconfirmed: isUnconfirmed(desc.acs),
}); });
@@ -684,7 +705,7 @@ class MessagesView extends React.Component {
} }
clearTimeout(this.keyPressTimer) clearTimeout(this.keyPressTimer)
this.setState({messageCount: topic.messageCount(), typingIndicator: false}, _ => { this.setState({maxSeqId: topic.maxMsgSeq(), minSeqId: topic.minMsgSeq(), typingIndicator: false}, _ => {
// Scroll to the bottom if the message is added to the end of the message // Scroll to the bottom if the message is added to the end of the message
// list if already at the bottom, otherwise show [go to latest] button. // list if already at the bottom, otherwise show [go to latest] button.
// Implemented as a callback to be sure the scroll height has been updated. // Implemented as a callback to be sure the scroll height has been updated.
@@ -694,6 +715,11 @@ class MessagesView extends React.Component {
} else { } else {
this.goToLatestMessage(); this.goToLatestMessage();
} }
} else {
if (this.messagesScroller) {
this.messagesScroller.scrollTop = this.messagesScroller.scrollHeight - this.state.scrollPosition -
this.messagesScroller.offsetHeight;
}
} }
}); });
@@ -713,6 +739,18 @@ class MessagesView extends React.Component {
} }
} }
handleAuxUpdate(aux) {
const pins = (aux['pins'] || []).slice();
let selectedPin = this.state.selectedPin;
if (pins.length > this.state.pins.length) {
// New pins added, show the latest.
selectedPin = 0;
} else if (selectedPin >= pins.length) {
selectedPin = Math.max(0, pins.length - 1);
}
this.setState({pins: pins, selectedPin: selectedPin});
}
handleInfoReceipt(info) { handleInfoReceipt(info) {
switch (info.what) { switch (info.what) {
case 'kp': { case 'kp': {
@@ -739,9 +777,9 @@ class MessagesView extends React.Component {
} }
if (content.video) { if (content.video) {
this.setState({ videoPostview: content }); this.setState({videoPostview: content});
} else { } else {
this.setState({ imagePostview: content }); this.setState({imagePostview: content});
} }
} }
@@ -766,7 +804,7 @@ class MessagesView extends React.Component {
params.set(key, data.resp[key]); params.set(key, data.resp[key]);
} }
} }
['name', 'seq'].map((key) => { ['name', 'seq'].forEach(key => {
if (data[key]) { if (data[key]) {
params.set(key, data[key]); params.set(key, data[key]);
} }
@@ -1057,8 +1095,8 @@ class MessagesView extends React.Component {
// Small preview to show while uploading. // Small preview to show while uploading.
b64conv[2] = imageScaled(previewBlob, VIDEO_PREVIEW_DIM, VIDEO_PREVIEW_DIM, -1, false) b64conv[2] = imageScaled(previewBlob, VIDEO_PREVIEW_DIM, VIDEO_PREVIEW_DIM, -1, false)
.then(scaled => blobToBase64(scaled.blob)); .then(scaled => blobToBase64(scaled.blob));
Promise.all(b64conv)
// Convert tiny image into base64 for serialization and previewing. // Convert tiny image into base64 for serialization and previewing.
Promise.all(b64conv)
.then(b64s => { .then(b64s => {
const [video, img, preview] = b64s; const [video, img, preview] = b64s;
let msg = Drafty.insertVideo(null, 0, { let msg = Drafty.insertVideo(null, 0, {
@@ -1074,6 +1112,7 @@ class MessagesView extends React.Component {
size: videoBlob.size, size: videoBlob.size,
urlPromise: uploadCompletionPromise urlPromise: uploadCompletionPromise
}); });
if (caption) { if (caption) {
msg = Drafty.appendLineBreak(msg); msg = Drafty.appendLineBreak(msg);
msg = Drafty.append(msg, Drafty.parse(caption)); msg = Drafty.append(msg, Drafty.parse(caption));
@@ -1253,13 +1292,19 @@ class MessagesView extends React.Component {
} }
} }
handleUnpinMessage(seq) {
const topic = this.props.tinode.getTopic(this.state.topic);
topic.pinMessage(seq, false);
}
isDragEnabled() { isDragEnabled() {
return this.state.isWriter && !this.state.unconfirmed && !this.props.forwardMessage && !this.state.peerMessagingDisabled; return this.state.isWriter && !this.state.unconfirmed && !this.props.forwardMessage && !this.state.peerMessagingDisabled;
} }
handleDrag(e) { handleDragStart(e) {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
e.dataTransfer.clearData();
} }
handleDragIn(e) { handleDragIn(e) {
@@ -1280,13 +1325,17 @@ class MessagesView extends React.Component {
} }
} }
handleDrag(e) {
e.preventDefault();
e.stopPropagation();
}
handleDrop(e) { handleDrop(e) {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
this.setState({dragging: false}); this.setState({dragging: false});
if (this.isDragEnabled() && e.dataTransfer.files && e.dataTransfer.files.length > 0) { if (this.isDragEnabled() && e.dataTransfer.files && e.dataTransfer.files.length > 0) {
this.handleFileDrop(e.dataTransfer.files); this.handleFileDrop(e.dataTransfer.files);
e.dataTransfer.clearData();
this.dragCounter = 0; this.dragCounter = 0;
} }
} }
@@ -1295,9 +1344,7 @@ class MessagesView extends React.Component {
const {formatMessage} = this.props.intl; const {formatMessage} = this.props.intl;
let component; let component;
if (this.props.hideSelf) { if (!this.state.topic) {
component = null;
} else if (!this.state.topic) {
component = ( component = (
<LogoView <LogoView
serverVersion={this.props.serverVersion} serverVersion={this.props.serverVersion}
@@ -1390,6 +1437,9 @@ class MessagesView extends React.Component {
} }
} }
const pinnedMessages = [];
this.state.pins.forEach(seq => pinnedMessages.push(topic.latestMsgVersion(seq)));
const messageNodes = []; const messageNodes = [];
let previousFrom = null; let previousFrom = null;
let prevDate = null; let prevDate = null;
@@ -1466,6 +1516,8 @@ class MessagesView extends React.Component {
received={deliveryStatus} received={deliveryStatus}
uploader={msg._uploader} uploader={msg._uploader}
userIsWriter={this.state.isWriter} userIsWriter={this.state.isWriter}
userIsAdmin={this.state.isAdmin}
pinned={this.state.pins.includes(msg.seq)}
viewportWidth={this.props.viewportWidth} // Used by `formatter`. viewportWidth={this.props.viewportWidth} // Used by `formatter`.
showContextMenu={this.handleShowMessageContextMenu} showContextMenu={this.handleShowMessageContextMenu}
onExpandMedia={this.handleExpandMedia} onExpandMedia={this.handleExpandMedia}
@@ -1581,11 +1633,22 @@ class MessagesView extends React.Component {
}<ContactBadges badges={icon_badges} /></div> }<ContactBadges badges={icon_badges} /></div>
<div id="topic-last-seen">{lastSeen}</div> <div id="topic-last-seen">{lastSeen}</div>
</div> </div>
<div style={{marginLeft: 'auto'}}/>
{!this.props.displayMobile ?
<PinnedMessages
tinode={this.props.tinode}
messages={pinnedMessages}
selected={this.state.selectedPin}
isAdmin={this.state.isAdmin}
setSelected={index => this.setState({selectedPin: index})}
onSelected={this.handleQuoteClick}
onCancel={this.handleUnpinMessage} />
: null}
{groupTopic ? {groupTopic ?
<GroupSubs <GroupSubs
tinode={this.props.tinode} tinode={this.props.tinode}
subscribers={this.state.onlineSubs} /> : subscribers={this.state.onlineSubs} /> :
<div id="topic-users" /> null
} }
<div> <div>
<a href="#" onClick={this.handleContextClick}> <a href="#" onClick={this.handleContextClick}>
@@ -1594,10 +1657,20 @@ class MessagesView extends React.Component {
</div> </div>
</div> </div>
{this.props.displayMobile ? {this.props.displayMobile ?
<>
<PinnedMessages
tinode={this.props.tinode}
messages={pinnedMessages}
selected={this.state.selectedPin}
isAdmin={this.state.isAdmin}
setSelected={index => this.setState({selectedPin: index})}
onSelected={this.handleQuoteClick}
onCancel={this.handleUnpinMessage} />
<ErrorPanel <ErrorPanel
level={this.props.errorLevel} level={this.props.errorLevel}
text={this.props.errorText} text={this.props.errorText}
onClearError={this.props.onError} /> onClearError={this.props.onError} />
</>
: null} : null}
<LoadSpinner show={this.state.fetchingMessages} /> <LoadSpinner show={this.state.fetchingMessages} />
{messagesComponent} {messagesComponent}
+1 -1
View File
@@ -46,7 +46,7 @@ export default class SettingsView extends React.PureComponent {
render() { render() {
const names = {def: "default", ws: "websocket", lp: "long polling"}; const names = {def: "default", ws: "websocket", lp: "long polling"};
const transportOptions = []; const transportOptions = [];
['def', 'ws', 'lp'].map((item) => { ['def', 'ws', 'lp'].forEach(item => {
const id = 'transport-' + item; const id = 'transport-' + item;
const name = names[item]; const name = names[item];
transportOptions.push( transportOptions.push(
+1 -1
View File
@@ -124,7 +124,7 @@ class SidepanelView extends React.PureComponent {
} }
return ( return (
<div id="sidepanel" className={this.props.hideSelf ? 'nodisplay' : null}> <div id="sidepanel">
<SideNavbar <SideNavbar
state={view} state={view}
title={title} title={title}
+20 -11
View File
@@ -277,8 +277,10 @@ class TinodeWeb extends React.Component {
componentDidMount() { componentDidMount() {
window.addEventListener('resize', this.handleResize); window.addEventListener('resize', this.handleResize);
window.addEventListener('online', (e) => { this.handleOnline(true); }); this.handleOnlineOn = _ => { this.handleOnline(true); }
window.addEventListener('offline', (e) => { this.handleOnline(false); }); window.addEventListener('online', this.handleOnlineOn);
this.handleOnlineOff = _ => { this.handleOnline(false); }
window.addEventListener('offline', this.handleOnlineOff);
window.addEventListener('hashchange', this.handleHashRoute); window.addEventListener('hashchange', this.handleHashRoute);
// Process background notifications from the service worker. // Process background notifications from the service worker.
@@ -315,9 +317,6 @@ class TinodeWeb extends React.Component {
}); });
} }
// Parse and save the hash navigation params.
const parsedNav = HashNavigation.parseUrlHash(window.location.hash);
// Read contacts from cache. // Read contacts from cache.
this.resetContactList(); this.resetContactList();
@@ -337,6 +336,8 @@ class TinodeWeb extends React.Component {
this.readTimer = null; this.readTimer = null;
this.readTimerCallback = null; this.readTimerCallback = null;
// Parse the hash navigation params.
const parsedNav = HashNavigation.parseUrlHash(window.location.hash);
// Maybe navigate to home screen. // Maybe navigate to home screen.
if (!['cred', 'reset', 'register'].includes(parsedNav.path[0])) { if (!['cred', 'reset', 'register'].includes(parsedNav.path[0])) {
// Save possible topic name. // Save possible topic name.
@@ -354,6 +355,8 @@ class TinodeWeb extends React.Component {
componentWillUnmount() { componentWillUnmount() {
window.removeEventListener('resize', this.handleResize); window.removeEventListener('resize', this.handleResize);
window.removeEventListener('hashchange', this.handleHashRoute); window.removeEventListener('hashchange', this.handleHashRoute);
window.removeEventListener('online', this.handleOnlineOn);
window.removeEventListener('offline', this.handleOnlineOff);
document.removeEventListener('visibilitychange', this.handleVisibilityEvent); document.removeEventListener('visibilitychange', this.handleVisibilityEvent);
} }
@@ -503,8 +506,12 @@ class TinodeWeb extends React.Component {
let topicName = hash.path[1] || null; let topicName = hash.path[1] || null;
if (topicName != this.state.topicSelected) { if (topicName != this.state.topicSelected) {
if (!Tinode.topicType(topicName)) { if (!Tinode.topicType(topicName)) {
// Clear invalid topic name. // Clear invalid topic name and hide messages view on mobile.
topicName = null; topicName = null;
newState.mobilePanel = 'sidepanel';
} else {
// Topic valid: show messages view on mobile.
newState.mobilePanel = 'topic-view';
} }
Object.assign(newState, { Object.assign(newState, {
topicSelected: topicName, topicSelected: topicName,
@@ -1679,14 +1686,14 @@ class TinodeWeb extends React.Component {
} }
if (added && added.length > 0) { if (added && added.length > 0) {
added.map((uid) => { added.forEach(uid => {
topic.invite(uid, null) topic.invite(uid, null)
.catch(err => this.handleError(err.message, 'err')); .catch(err => this.handleError(err.message, 'err'));
}); });
} }
if (removed && removed.length > 0) { if (removed && removed.length > 0) {
removed.map((uid) => { removed.forEach(uid => {
topic.delSubscription(uid) topic.delSubscription(uid)
.catch(err => this.handleError(err.message, 'err')); .catch(err => this.handleError(err.message, 'err'));
}); });
@@ -1981,11 +1988,12 @@ class TinodeWeb extends React.Component {
(_ => {this.setState({alertVisible: false}); this.state.alertParams.onConfirm();}) : null} (_ => {this.setState({alertVisible: false}); this.state.alertParams.onConfirm();}) : null}
confirm={this.state.alertParams.confirm} confirm={this.state.alertParams.confirm}
/> : null} /> : null}
{!this.state.displayMobile || this.state.mobilePanel == 'sidepanel' ?
<SidepanelView <SidepanelView
tinode={this.tinode} tinode={this.tinode}
connected={this.state.connected} connected={this.state.connected}
displayMobile={this.state.displayMobile} displayMobile={this.state.displayMobile}
hideSelf={this.state.displayMobile && this.state.mobilePanel !== 'sidepanel'}
state={this.state.sidePanelSelected} state={this.state.sidePanelSelected}
title={this.state.sidePanelTitle} title={this.state.sidePanelTitle}
avatar={this.state.sidePanelAvatar} avatar={this.state.sidePanelAvatar}
@@ -2052,7 +2060,9 @@ class TinodeWeb extends React.Component {
onSearchContacts={this.handleSearchContacts} onSearchContacts={this.handleSearchContacts}
showContextMenu={this.handleShowContextMenu} /> showContextMenu={this.handleShowContextMenu} />
: null}
{!this.state.displayMobile || (this.state.mobilePanel == 'topic-view' && !this.state.infoPanel) ?
<MessagesView <MessagesView
tinode={this.tinode} tinode={this.tinode}
connected={this.state.connected} connected={this.state.connected}
@@ -2062,8 +2072,6 @@ class TinodeWeb extends React.Component {
displayMobile={this.state.displayMobile} displayMobile={this.state.displayMobile}
viewportWidth={this.state.viewportWidth} viewportWidth={this.state.viewportWidth}
viewportHeight={this.state.viewportHeight} viewportHeight={this.state.viewportHeight}
hideSelf={this.state.displayMobile &&
(this.state.mobilePanel !== 'topic-view' || this.state.infoPanel)}
topic={this.state.topicSelected} topic={this.state.topicSelected}
myUserId={this.state.myUserId} myUserId={this.state.myUserId}
// User public.fn. // User public.fn.
@@ -2101,6 +2109,7 @@ class TinodeWeb extends React.Component {
onNewChat={this.handleNewChatInvitation} onNewChat={this.handleNewChatInvitation}
sendMessage={this.handleSendMessage} sendMessage={this.handleSendMessage}
onVideoCallClosed={this.handleCallClose} /> onVideoCallClosed={this.handleCallClose} />
: null}
{this.state.infoPanel ? {this.state.infoPanel ?
<InfoView <InfoView
+1 -1
View File
@@ -29,7 +29,7 @@ class BadgeList extends React.PureComponent {
let badges = null; let badges = null;
if (this.props.trustedBadges && this.props.trustedBadges.length > 0) { if (this.props.trustedBadges && this.props.trustedBadges.length > 0) {
badges = []; badges = [];
this.props.trustedBadges.map((b) => { this.props.trustedBadges.forEach(b => {
const comment = this.props.short ? null : formatMessage(messages['badge_' + b]); const comment = this.props.short ? null : formatMessage(messages['badge_' + b]);
const style = 'material-icons ' + b + '-color'; const style = 'material-icons ' + b + '-color';
badges.push(<div className="trusted-badge" key={b}><i className={style}>{icon_mapping[b] || b}</i> {comment}</div>); badges.push(<div className="trusted-badge" key={b}><i className={style}>{icon_mapping[b] || b}</i> {comment}</div>);
+4 -1
View File
@@ -7,7 +7,7 @@ import { secondsToTime } from '../lib/strformat.js';
export default class CallMessage extends React.PureComponent { export default class CallMessage extends React.PureComponent {
render() { render() {
const isCallDropped = ['declined', 'disconnected', 'missed'].includes(this.props.callState); const isCallDropped = ['busy', 'declined', 'disconnected', 'missed'].includes(this.props.callState);
const successClass = 'material-icons medium ' + (isCallDropped ? 'red' : 'green'); const successClass = 'material-icons medium ' + (isCallDropped ? 'red' : 'green');
const callIcon = this.props.incoming ? const callIcon = this.props.incoming ?
(isCallDropped ? 'call_missed' : 'call_received') : (isCallDropped ? 'call_missed' : 'call_received') :
@@ -15,6 +15,9 @@ export default class CallMessage extends React.PureComponent {
let duration; let duration;
if (isCallDropped) { if (isCallDropped) {
switch (this.props.callState) { switch (this.props.callState) {
case 'busy':
duration = <FormattedMessage id="call_busy" defaultMessage="busy" description="Label for busy call line" />;
break;
case 'declined': case 'declined':
duration = <FormattedMessage id="call_declined" defaultMessage="declined" description="Label for declined call" />; duration = <FormattedMessage id="call_declined" defaultMessage="declined" description="Label for declined call" />;
break; break;
+16 -10
View File
@@ -121,7 +121,8 @@ class CallPanel extends React.PureComponent {
handleVideoCallAccepted(info) { handleVideoCallAccepted(info) {
RING_SOUND.pause(); RING_SOUND.pause();
const pc = this.createPeerConnection(); // Create peer connection with a data channel.
const pc = this.createPeerConnection(true);
const stream = this.state.localStream; const stream = this.state.localStream;
stream.getTracks().forEach(track => { stream.getTracks().forEach(track => {
pc.addTrack(track, stream); pc.addTrack(track, stream);
@@ -230,7 +231,7 @@ class CallPanel extends React.PureComponent {
this.state.pc.onicecandidateerror = null; this.state.pc.onicecandidateerror = null;
this.state.pc.ondatachannel = null; this.state.pc.ondatachannel = null;
if (this.state.dataChannel) { if (this.state.dataChannel && (this.state.dataChannel.readyState == 'open' || this.state.dataChannel.readyState == 'connecting')) {
this.state.dataChannel.close(); this.state.dataChannel.close();
} }
this.state.pc.close(); this.state.pc.close();
@@ -284,11 +285,12 @@ class CallPanel extends React.PureComponent {
} }
} }
handleDataChannelClose() { handleDataChannelClose(event) {
/* no action */ console.log('close data channel:', event);
} }
handleDataChannelEvent(event) { handleDataChannelEvent(event) {
console.log('data channel event:', event);
const channel = event.channel; const channel = event.channel;
channel.onerror = this.handleDataChannelError; channel.onerror = this.handleDataChannelError;
channel.onmessage = this.handleDataChannelMessage; channel.onmessage = this.handleDataChannelMessage;
@@ -297,7 +299,7 @@ class CallPanel extends React.PureComponent {
this.setState({dataChannel: channel}); this.setState({dataChannel: channel});
} }
createPeerConnection() { createPeerConnection(withDataChannel) {
const iceServers = this.props.tinode.getServerParam('iceServers', null); const iceServers = this.props.tinode.getServerParam('iceServers', null);
const pc = iceServers ? new RTCPeerConnection({iceServers: iceServers}) : new RTCPeerConnection(); const pc = iceServers ? new RTCPeerConnection({iceServers: iceServers}) : new RTCPeerConnection();
@@ -310,15 +312,17 @@ class CallPanel extends React.PureComponent {
pc.ontrack = this.handleTrackEvent; pc.ontrack = this.handleTrackEvent;
pc.ondatachannel = this.handleDataChannelEvent; pc.ondatachannel = this.handleDataChannelEvent;
// creating data channel events let stateUpdate = {pc: pc, waitingForPeer: false};
if (withDataChannel) {
// Create data channel for exchanging events.
const channel = pc.createDataChannel("events", {ordered: true}); const channel = pc.createDataChannel("events", {ordered: true});
channel.onerror = this.handleDataChannelError; channel.onerror = this.handleDataChannelError;
channel.onmessage = this.handleDataChannelMessage; channel.onmessage = this.handleDataChannelMessage;
channel.onopen = this.handleDataChannelOpen; channel.onopen = this.handleDataChannelOpen;
channel.onclose = this.handleDataChannelClose; channel.onclose = this.handleDataChannelClose;
stateUpdate.dataChannel = channel;
this.setState({pc: pc, dataChannel: channel, waitingForPeer: false}); }
this.setState(stateUpdate);
return pc; return pc;
} }
@@ -441,7 +445,9 @@ class CallPanel extends React.PureComponent {
handleVideoOfferMsg(info) { handleVideoOfferMsg(info) {
let localStream = null; let localStream = null;
const pc = this.state.pc ? this.state.pc : this.createPeerConnection(); // If needed, create peer connection.
// Not creating data channel since it should be created by the peer.
const pc = this.state.pc ? this.state.pc : this.createPeerConnection(false);
const desc = new RTCSessionDescription(info.payload); const desc = new RTCSessionDescription(info.payload);
pc.setRemoteDescription(desc).then(_ => { pc.setRemoteDescription(desc).then(_ => {
+4 -1
View File
@@ -9,13 +9,16 @@ import { secondsToTime } from '../lib/strformat.js';
// success: true | false. // success: true | false.
export default class CallStatus extends React.PureComponent { export default class CallStatus extends React.PureComponent {
render() { render() {
const isCallDropped = ['declined', 'disconnected', 'missed'].includes(this.props.callState); const isCallDropped = ['busy', 'declined', 'disconnected', 'missed'].includes(this.props.callState);
const icon2 = this.props.incoming ? const icon2 = this.props.incoming ?
(isCallDropped ? 'call_missed' : 'call_received') : (isCallDropped ? 'call_missed' : 'call_received') :
(isCallDropped ? 'call_missed_outgoing' : 'call_made'); (isCallDropped ? 'call_missed_outgoing' : 'call_made');
let duration; let duration;
if (isCallDropped) { if (isCallDropped) {
switch (this.props.callState) { switch (this.props.callState) {
case 'busy':
duration = <FormattedMessage id="call_busy" defaultMessage="busy" description="Label for busy call line" />;
break;
case 'declined': case 'declined':
duration = <FormattedMessage id="call_declined" defaultMessage="declined" description="Label for declined call" />; duration = <FormattedMessage id="call_declined" defaultMessage="declined" description="Label for declined call" />;
break; break;
+6 -1
View File
@@ -93,7 +93,8 @@ class BaseChatMessage extends React.PureComponent {
if (this.props.received == Tinode.MESSAGE_STATUS_FAILED) { if (this.props.received == Tinode.MESSAGE_STATUS_FAILED) {
menuItems.push('menu_item_send_retry'); menuItems.push('menu_item_send_retry');
} }
if (this.props.userIsWriter && this.props.received > Tinode.MESSAGE_STATUS_FAILED) { if (this.props.received > Tinode.MESSAGE_STATUS_FATAL) {
if (this.props.userIsWriter) {
menuItems.push('menu_item_reply'); menuItems.push('menu_item_reply');
if (!this.props.response) { if (!this.props.response) {
let immutable = false; let immutable = false;
@@ -112,6 +113,10 @@ class BaseChatMessage extends React.PureComponent {
} }
} }
} }
if (this.props.userIsAdmin) {
menuItems.push(this.props.pinned ? 'menu_item_unpin' : 'menu_item_pin');
}
}
menuItems.push('menu_item_forward'); menuItems.push('menu_item_forward');
this.props.showContextMenu({ this.props.showContextMenu({
+3 -3
View File
@@ -44,7 +44,7 @@ export default class ChipInput extends React.Component {
static indexChips(chips) { static indexChips(chips) {
const index = {}; const index = {};
let count = 0; let count = 0;
chips.map((item) => { chips.forEach(item => {
index[item.user] = count; index[item.user] = count;
count ++; count ++;
}); });
@@ -55,7 +55,7 @@ export default class ChipInput extends React.Component {
static sortChips(chips, keep) { static sortChips(chips, keep) {
const required = []; const required = [];
const normal = []; const normal = [];
chips.map((item) => { chips.forEach(item => {
if (keep && keep.includes(item.user)) { if (keep && keep.includes(item.user)) {
required.push(item); required.push(item);
} else { } else {
@@ -115,7 +115,7 @@ export default class ChipInput extends React.Component {
const chips = []; const chips = [];
let count = 0; let count = 0;
const staticMembers = this.props.staticMembers || []; const staticMembers = this.props.staticMembers || [];
this.state.sortedChips.map((item) => { this.state.sortedChips.forEach(item => {
chips.push( chips.push(
<Chip <Chip
tinode={this.props.tinode} tinode={this.props.tinode}
+1 -1
View File
@@ -8,7 +8,7 @@ export default class ContactBadges extends React.PureComponent {
let badges = null; let badges = null;
if (this.props.badges && this.props.badges.length > 0) { if (this.props.badges && this.props.badges.length > 0) {
badges = []; badges = [];
this.props.badges.map((b) => { this.props.badges.forEach(b => {
const color = (b.color ? ' ' + b.color : ''); const color = (b.color ? ' ' + b.color : '');
if (b.icon) { if (b.icon) {
badges.push(<i className={'material-icons as-badge' + color} key={b.key || b.icon}>{icon_mapping[b.icon] || b.icon}</i>); badges.push(<i className={'material-icons as-badge' + color} key={b.key || b.icon}>{icon_mapping[b.icon] || b.icon}</i>);
+1 -1
View File
@@ -32,7 +32,7 @@ class ContactList extends React.Component {
const contactNodes = []; const contactNodes = [];
let contactsCount = 0; let contactsCount = 0;
if (this.props.contacts && this.props.contacts.length > 0) { if (this.props.contacts && this.props.contacts.length > 0) {
this.props.contacts.map((c) => { this.props.contacts.forEach(c => {
if (c.action) { if (c.action) {
// Action item // Action item
contactNodes.push( contactNodes.push(
+35 -1
View File
@@ -116,6 +116,16 @@ const messages = defineMessages({
defaultMessage: 'Are you sure you want to clear all messages? It cannot be undone.', defaultMessage: 'Are you sure you want to clear all messages? It cannot be undone.',
description: 'Alert dialog warning when deleting all messages.' description: 'Alert dialog warning when deleting all messages.'
}, },
pin_message: {
id: 'pin_message',
defaultMessage: 'Pin',
description: 'Menu item [Pin] for pinning message to the top.'
},
unpin_message: {
id: 'unpin_message',
defaultMessage: 'Unpin',
description: 'Menu item [Unpin] for un-pinning the message.'
},
}); });
class ContextMenu extends React.Component { class ContextMenu extends React.Component {
@@ -205,6 +215,20 @@ class ContextMenu extends React.Component {
return this.editMessage(params, errorHandler); return this.editMessage(params, errorHandler);
} }
}, },
'menu_item_pin': {
id: 'menu_item_pin',
title: formatMessage(messages.pin_message),
handler: (params, errorHandler) => {
return this.pinMessage(true, params, errorHandler);
}
},
'menu_item_unpin': {
id: 'menu_item_unpin',
title: formatMessage(messages.unpin_message),
handler: (params, errorHandler) => {
return this.pinMessage(false, params, errorHandler);
}
},
'topic_unmute': { 'topic_unmute': {
id: 'topic_unmute', id: 'topic_unmute',
title: formatMessage(messages.unmute), title: formatMessage(messages.unmute),
@@ -428,6 +452,16 @@ class ContextMenu extends React.Component {
}); });
} }
// Pin or unpin the message.
pinMessage(pin, params, errorHandler) {
const topic = this.props.tinode.getTopic(params.topicName);
if (!topic) {
return;
}
topic.pinMessage(params.seq, pin)
.catch(err => errorHandler ? errorHandler(err.message, 'err') : null);
}
// Function is used by context menu to set permissions. // Function is used by context menu to set permissions.
topicPermissionSetter(mode, params, errorHandler) { topicPermissionSetter(mode, params, errorHandler) {
const topic = this.props.tinode.getTopic(params.topicName); const topic = this.props.tinode.getTopic(params.topicName);
@@ -454,7 +488,7 @@ class ContextMenu extends React.Component {
render() { render() {
const menu = []; const menu = [];
let count = 0; let count = 0;
this.props.items.map((item) => { this.props.items.forEach(item => {
if (typeof item == 'string') { if (typeof item == 'string') {
item = this.MenuItems[item]; item = this.MenuItems[item];
} }
+1 -1
View File
@@ -84,7 +84,7 @@ class CredentialEdit extends React.PureComponent {
const changePhone = ( const changePhone = (
<><div className="panel-form-row"> <><div className="panel-form-row">
<label className="large"><FormattedMessage id="change_phonel" defaultMessage="Change phone number" <label className="large"><FormattedMessage id="change_phone" defaultMessage="Change phone number"
description="Prompt to change current phone number"/></label> description="Prompt to change current phone number"/></label>
</div> </div>
<div className="group"> <div className="group">
+16
View File
@@ -17,6 +17,22 @@ export default class DocPreview extends React.PureComponent {
super(props); super(props);
this.handleSendDoc = this.handleSendDoc.bind(this); this.handleSendDoc = this.handleSendDoc.bind(this);
this.handleKeyDown = this.handleKeyDown.bind(this);
}
componentDidMount() {
document.addEventListener('keydown', this.handleKeyDown);
}
componentWillUnmount() {
document.removeEventListener('keydown', this.handleKeyDown);
}
handleKeyDown(e) {
e.preventDefault();
if (e.key === 'Escape') {
this.props.onClose();
}
} }
handleSendDoc(caption) { handleSendDoc(caption) {
+11 -13
View File
@@ -41,7 +41,7 @@ class GroupManager extends React.Component {
static indexMembers(members) { static indexMembers(members) {
let index = {}; let index = {};
members.map((m) => { members.forEach(m => {
index[m.user] = {delta: 0, present: true}; // Delta: 0 unchanged, +1 added, -1 removed index[m.user] = {delta: 0, present: true}; // Delta: 0 unchanged, +1 added, -1 removed
}); });
return index; return index;
@@ -49,7 +49,7 @@ class GroupManager extends React.Component {
static staticMembers(members, keepInitial, requiredMember) { static staticMembers(members, keepInitial, requiredMember) {
let stat = []; let stat = [];
members.map((m) => { members.forEach(m => {
if (keepInitial || m.user == requiredMember) { if (keepInitial || m.user == requiredMember) {
stat.push(m.user); stat.push(m.user);
} }
@@ -59,7 +59,7 @@ class GroupManager extends React.Component {
static selectedContacts(members) { static selectedContacts(members) {
let sel = []; let sel = [];
members.map((m) => { members.forEach(m => {
sel.push(m.user); sel.push(m.user);
}); });
return sel; return sel;
@@ -130,20 +130,19 @@ class GroupManager extends React.Component {
} }
handleSubmit() { handleSubmit() {
var instance = this; const members = [];
var members = []; const added = [];
var added = []; const removed = [];
var removed = [];
var keys = Object.keys(this.state.index); const keys = Object.keys(this.state.index);
keys.map(function(k) { keys.forEach(k => {
if (instance.state.index[k].present) { if (this.state.index[k].present) {
members.push(k); members.push(k);
} }
if (instance.state.index[k].delta > 0) { if (this.state.index[k].delta > 0) {
added.push(k); added.push(k);
} else if (instance.state.index[k].delta < 0) { } else if (this.state.index[k].delta < 0) {
removed.push(k); removed.push(k);
} }
}); });
@@ -155,7 +154,6 @@ class GroupManager extends React.Component {
} }
render() { render() {
const {formatMessage} = this.props.intl;
return ( return (
<div id="group-manager"> <div id="group-manager">
<div className="panel-form-row"> <div className="panel-form-row">
+3 -1
View File
@@ -30,7 +30,9 @@ export default class GroupSubs extends React.Component {
); );
return usersOnline.length == countToShow; return usersOnline.length == countToShow;
}); });
if (usersOnline.length == 0) {
return null;
}
return ( return (
<div id="topic-users">{usersOnline} {totalCount > countToShow ? <div id="topic-users">{usersOnline} {totalCount > countToShow ?
<span> <span>
+1 -1
View File
@@ -36,7 +36,7 @@ export default class HostSelector extends React.PureComponent {
} }
render() { render() {
var hostOptions = []; const hostOptions = [];
for (let key in KNOWN_HOSTS) { for (let key in KNOWN_HOSTS) {
let item = KNOWN_HOSTS[key]; let item = KNOWN_HOSTS[key];
hostOptions.push( hostOptions.push(
+21 -1
View File
@@ -16,6 +16,26 @@ export default class ImagePreview extends React.PureComponent {
}; };
this.handleSendImage = this.handleSendImage.bind(this); this.handleSendImage = this.handleSendImage.bind(this);
this.handleKeyDown = this.handleKeyDown.bind(this);
}
componentDidMount() {
document.addEventListener('keydown', this.handleKeyDown);
}
componentWillUnmount() {
document.removeEventListener('keydown', this.handleKeyDown);
}
handleKeyDown(e) {
if (this.props.onSendMessage) {
return;
}
e.preventDefault();
if (e.key === 'Escape') {
this.props.onClose();
}
} }
assignWidth(node) { assignWidth(node) {
@@ -64,7 +84,7 @@ export default class ImagePreview extends React.PureComponent {
} }
<a href="#" onClick={(e) => {e.preventDefault(); this.props.onClose();}}><i className="material-icons gray">close</i></a> <a href="#" onClick={(e) => {e.preventDefault(); this.props.onClose();}}><i className="material-icons gray">close</i></a>
</div> </div>
<div id="image-preview-container" ref={(node) => this.assignWidth(node)}> <div id="image-preview-container" ref={node => this.assignWidth(node)}>
<img src={this.props.content.url} style={size} className="image-preview" alt={this.props.content.filename} /> <img src={this.props.content.url} style={size} className="image-preview" alt={this.props.content.filename} />
</div> </div>
{this.props.onSendMessage ? {this.props.onSendMessage ?
+3 -2
View File
@@ -27,11 +27,12 @@ export default class InlineVideo extends React.PureComponent {
return ( return (
<div className={className}> <div className={className}>
{React.createElement('img', this.props)} {React.createElement('img', this.props)}
{this.props.onClick ?
<div className="play-control"> <div className="play-control">
{this.props.onClick ?
<i className="material-icons white bigger">play_arrow</i> <i className="material-icons white bigger">play_arrow</i>
:
<img src="img/broken_video.png" style={{filter: 'invert(100%)'}} width="36" height="36" />}
</div> </div>
: null}
{duration ? <div className="duration">{duration}</div> : null} {duration ? <div className="duration">{duration}</div> : null}
</div> </div>
); );
+94
View File
@@ -0,0 +1,94 @@
// Send message form.
import React from 'react';
import { injectIntl } from 'react-intl';
import { Drafty } from 'tinode-sdk';
import { previewFormatter } from '../lib/formatters.js';
class PinnedMessages extends React.PureComponent {
constructor(props) {
super(props);
this.getSelectedIndex = this.getSelectedIndex.bind(this);
this.handleCancel = this.handleCancel.bind(this);
this.handleSelected = this.handleSelected.bind(this);
this.handleMoveNext = this.handleMoveNext.bind(this);
this.handleMovePrev = this.handleMovePrev.bind(this);
}
getSelectedIndex() {
const list = (this.props.messages || []);
return list.length - this.props.selected - 1;
}
handleCancel(e) {
e.preventDefault();
this.props.onCancel(this.props.messages[this.getSelectedIndex()].seq);
}
handleSelected(e) {
e.preventDefault();
this.props.onSelected(this.props.messages[this.getSelectedIndex()].seq);
}
handleMoveNext(e) {
e.preventDefault();
e.stopPropagation();
const idx = Math.max(this.props.selected - 1, 0);
if (idx != this.props.selected) {
this.props.setSelected(idx);
}
}
handleMovePrev(e) {
e.preventDefault();
e.stopPropagation();
const idx = Math.min(this.props.selected + 1, (this.props.messages || []).length - 1);
if (idx != this.props.selected) {
this.props.setSelected(idx);
}
}
render() {
const selected = this.getSelectedIndex();
let shown = (this.props.messages || [])[selected];
shown = shown ? Drafty.format(shown.content, previewFormatter, {
formatMessage: this.props.intl.formatMessage.bind(this.props.intl),
authorizeURL: this.props.tinode.authorizeURL.bind(this.props.tinode)
}) : null;
const dots = [];
this.props.messages.forEach(_ => {
const cn = dots.length == selected ? 'adot' : 'dot';
dots.push(<div key={dots.length} className={cn} />);
});
return shown ?
(<div id="pinned-wrapper">
{this.props.isAdmin ?
<div className="cancel">
<a href="#" onClick={this.handleCancel}><i className="material-icons gray">close</i></a>
</div> :
<div><i className="material-icons gray">push_pin</i></div>
}
<div className="pinned-scroll">{dots}</div>
<div className="pinned" onClick={this.handleSelected}>{shown}</div>
{selected > 0 ?
<span className="menuTrigger upper">
<a href="#" onClick={this.handleMovePrev}>
<i className="material-icons">expand_less</i>
</a>
</span>
: null}
{this.props.selected > 0 ?
<span className="menuTrigger lower">
<a href="#" onClick={this.handleMoveNext}>
<i className="material-icons">expand_more</i>
</a>
</span>
: null}
</div>) : null;
}
}
export default injectIntl(PinnedMessages);
+2 -1
View File
@@ -30,7 +30,8 @@ class ReceivedMarker extends React.PureComponent {
let timestamp; let timestamp;
if (this.props.received <= Tinode.MESSAGE_STATUS_SENDING) { if (this.props.received <= Tinode.MESSAGE_STATUS_SENDING) {
timestamp = formatMessage(messages.message_sending); timestamp = formatMessage(messages.message_sending);
} else if (this.props.received == Tinode.MESSAGE_STATUS_FAILED) { } else if (this.props.received == Tinode.MESSAGE_STATUS_FAILED ||
this.props.received == Tinode.MESSAGE_STATUS_FATAL) {
timestamp = formatMessage(messages.message_sending_failed); timestamp = formatMessage(messages.message_sending_failed);
} else { } else {
timestamp = this.props.timestamp.toLocaleTimeString(this.props.intl.locale, { timeStyle: 'short' }); timestamp = this.props.timestamp.toLocaleTimeString(this.props.intl.locale, { timeStyle: 'short' });
+5 -1
View File
@@ -111,6 +111,10 @@ class SendMessage extends React.PureComponent {
// Set focus on desktop, but not on mobile: focus causes soft keyboard to pop up. // Set focus on desktop, but not on mobile: focus causes soft keyboard to pop up.
this.messageEditArea.focus(); this.messageEditArea.focus();
} }
// Adjust height of the message area for the amount of text.
this.messageEditArea.style.height = '0px';
this.messageEditArea.style.height = this.messageEditArea.scrollHeight + 'px';
} }
if (prevProps.topicName != this.props.topicName) { if (prevProps.topicName != this.props.topicName) {
@@ -276,7 +280,7 @@ class SendMessage extends React.PureComponent {
</Suspense>) : </Suspense>) :
<textarea id="send-message-input" placeholder={prompt} <textarea id="send-message-input" placeholder={prompt}
value={this.state.message} onChange={this.handleMessageTyping} value={this.state.message} onChange={this.handleMessageTyping}
onKeyPress={this.handleKeyPress} onKeyDown={this.handleKeyPress}
ref={ref => {this.messageEditArea = ref;}} />)} ref={ref => {this.messageEditArea = ref;}} />)}
{this.state.message || !audioEnabled ? {this.state.message || !audioEnabled ?
<a href="#" onClick={this.handleSend} title={formatMessage(messages.icon_title_send)}> <a href="#" onClick={this.handleSend} title={formatMessage(messages.icon_title_send)}>
+1 -1
View File
@@ -12,7 +12,7 @@ export default class SideNavbar extends React.PureComponent {
render() { render() {
const icon_badges = []; const icon_badges = [];
if (this.props.trustedBadges) { if (this.props.trustedBadges) {
this.props.trustedBadges.map((b) => { this.props.trustedBadges.forEach(b => {
icon_badges.push({icon: b, color: 'badge-inv'}); icon_badges.push({icon: b, color: 'badge-inv'});
}); });
} }
+2 -2
View File
@@ -92,11 +92,11 @@ export default class TagManager extends React.Component {
let tags = []; let tags = [];
if (this.state.activated) { if (this.state.activated) {
this.state.tags.map((tag) => { this.state.tags.forEach(tag => {
tags.push({user: tag, invalid: (tag.length < minTagLength || tag.length > maxTagLength)}); tags.push({user: tag, invalid: (tag.length < minTagLength || tag.length > maxTagLength)});
}); });
} else { } else {
this.state.tags.map((tag) => { this.state.tags.forEach(tag => {
tags.push(<span className="badge" key={tags.length}>{tag}</span>); tags.push(<span className="badge" key={tags.length}>{tag}</span>);
}); });
if (tags.length == 0) { if (tags.length == 0) {
+20
View File
@@ -19,6 +19,26 @@ class VideoPreview extends React.PureComponent {
this.videoRef = React.createRef(); this.videoRef = React.createRef();
this.handleSendVideo = this.handleSendVideo.bind(this); this.handleSendVideo = this.handleSendVideo.bind(this);
this.handleKeyDown = this.handleKeyDown.bind(this);
}
componentDidMount() {
document.addEventListener('keydown', this.handleKeyDown);
}
componentWillUnmount() {
document.removeEventListener('keydown', this.handleKeyDown);
}
handleKeyDown(e) {
if (this.props.onSendMessage) {
return;
}
e.preventDefault();
if (e.key === 'Escape') {
this.props.onClose();
}
} }
handleSendVideo(caption) { handleSendVideo(caption) {
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1 +1 @@
{"version":3,"file":"290.prod.js","mappings":"2MASA,MAAMA,UAA6BC,IAAAA,cACjCC,YAAYC,GACVC,MAAMD,GAENE,KAAKC,UAAY,GACjB,MAAM,kBAAEC,GAAsBJ,EAAMK,KACpCC,EAAAA,SAAuBC,IACPA,EAAGC,KAAKC,MAAM,KACtBC,SAAQC,IACZT,KAAKC,UAAUS,KAAK,CAClBJ,KAAMG,EAAKE,OACXC,KAAMP,EAAGO,KACTC,MAAMC,EAAAA,EAAAA,IAAUT,EAAGO,MACnBG,KAAMb,EAAkBG,EAAGO,KAAM,CAACI,KAAM,YACxC,GACF,IAGJhB,KAAKC,UAAUgB,MAAK,CAACC,EAAGC,IAAMD,EAAEH,KAAKK,cAAcD,EAAEJ,OACvD,CAEAM,oBACMrB,KAAKsB,aACPtB,KAAKsB,YAAYC,eAAe,CAACC,MAAO,SAAUC,OAAQ,WAE9D,CAEAC,SACE,MAAMzB,EAAY,GACZ0B,EAAW3B,KAAKF,MAAM6B,UAAY,KAYxC,OAXA3B,KAAKC,UAAUO,SAAQ,CAACoB,EAAGC,KACzB,MAAMC,EAASF,EAAEhB,MAAQe,EAAW,YAAc,GAClD1B,EAAUS,KAAK,wBAAIqB,UAAWD,EAAOE,IAAKH,EACxCI,IAAKA,IAAYL,EAAEhB,MAAQe,IAAW3B,KAAKsB,YAAcW,EAAG,EAC5DC,QAASC,GAAKnC,KAAKF,MAAMsC,SAASR,EAAEhB,KAAMgB,EAAEtB,OAC5C,0BAAMyB,UAAU,gBAAgBH,EAAEf,MAClC,0BAAMkB,UAAU,WAAU,IAAOH,EAAEb,MACnC,0BAAMgB,UAAU,aAAY,KAAQH,EAAEtB,OAClC,IAIN,yBAAKyB,UAAU,mBAAmBD,MAAO,CAACO,OAAQ,UAChD,wBAAIN,UAAU,0BACX9B,GAIT,EAGF,SAAeqC,EAAAA,EAAAA,YAAW3C,E","sources":["webpack://tinode-webapp/./src/widgets/phone-country-selector.jsx"],"sourcesContent":["// Selector for country dialing code.\n\nimport React from 'react';\nimport { injectIntl } from 'react-intl';\n\nimport * as dcodes from '../dcodes.json';\n\nimport { flagEmoji } from '../lib/strformat';\n\nclass PhoneCountrySelector extends React.PureComponent {\n constructor(props) {\n super(props);\n\n this.countries = [];\n const { formatDisplayName } = props.intl;\n dcodes.default.forEach(dc => {\n const parts = dc.dial.split(',');\n parts.forEach(part => {\n this.countries.push({\n dial: part.trim(),\n code: dc.code,\n flag: flagEmoji(dc.code),\n name: formatDisplayName(dc.code, {type: 'region'})\n });\n });\n });\n\n this.countries.sort((a, b) => a.name.localeCompare(b.name));\n }\n\n componentDidMount() {\n if (this.selectedRef) {\n this.selectedRef.scrollIntoView({block: 'center', inline: 'nearest'});\n }\n }\n\n render() {\n const countries = [];\n const selected = this.props.selected || 'US';\n this.countries.forEach((c, idx) => {\n const style = (c.code == selected ? 'selected ' : '');\n countries.push(<li className={style} key={idx}\n ref={ref => {if (c.code == selected) {this.selectedRef = ref}}}\n onClick={_ => this.props.onSubmit(c.code, c.dial)}>\n <span className=\"country-flag\">{c.flag}</span>\n <span className=\"country\">&nbsp;{c.name}</span>\n <span className=\"dial-code\">&nbsp;+{c.dial}</span>\n </li>);\n });\n\n return (\n <div className=\"scrollable-panel\" style={{height: '30rem'}}>\n <ul className=\"phone-country-selector\">\n {countries}\n </ul>\n </div>\n );\n }\n}\n\nexport default injectIntl(PhoneCountrySelector);\n"],"names":["PhoneCountrySelector","React","constructor","props","super","this","countries","formatDisplayName","intl","dcodes","dc","dial","split","forEach","part","push","trim","code","flag","flagEmoji","name","type","sort","a","b","localeCompare","componentDidMount","selectedRef","scrollIntoView","block","inline","render","selected","c","idx","style","className","key","ref","onClick","_","onSubmit","height","injectIntl"],"sourceRoot":""} {"version":3,"file":"290.prod.js","mappings":"2MASA,MAAMA,UAA6BC,IAAAA,cACjCC,YAAYC,GACVC,MAAMD,GAENE,KAAKC,UAAY,GACjB,MAAM,kBAAEC,GAAsBJ,EAAMK,KACpCC,EAAAA,SAAuBC,IACPA,EAAGC,KAAKC,MAAM,KACtBC,SAAQC,IACZT,KAAKC,UAAUS,KAAK,CAClBJ,KAAMG,EAAKE,OACXC,KAAMP,EAAGO,KACTC,MAAMC,EAAAA,EAAAA,IAAUT,EAAGO,MACnBG,KAAMb,EAAkBG,EAAGO,KAAM,CAACI,KAAM,YACxC,GACF,IAGJhB,KAAKC,UAAUgB,MAAK,CAACC,EAAGC,IAAMD,EAAEH,KAAKK,cAAcD,EAAEJ,OACvD,CAEAM,oBACMrB,KAAKsB,aACPtB,KAAKsB,YAAYC,eAAe,CAACC,MAAO,SAAUC,OAAQ,WAE9D,CAEAC,SACE,MAAMzB,EAAY,GACZ0B,EAAW3B,KAAKF,MAAM6B,UAAY,KAYxC,OAXA3B,KAAKC,UAAUO,SAAQ,CAACoB,EAAGC,KACzB,MAAMC,EAASF,EAAEhB,MAAQe,EAAW,YAAc,GAClD1B,EAAUS,KAAKd,IAAAA,cAAA,MAAImC,UAAWD,EAAOE,IAAKH,EACxCI,IAAKA,IAAYL,EAAEhB,MAAQe,IAAW3B,KAAKsB,YAAcW,EAAG,EAC5DC,QAASC,GAAKnC,KAAKF,MAAMsC,SAASR,EAAEhB,KAAMgB,EAAEtB,OAC5CV,IAAAA,cAAA,QAAMmC,UAAU,gBAAgBH,EAAEf,MAClCjB,IAAAA,cAAA,QAAMmC,UAAU,WAAU,IAAOH,EAAEb,MACnCnB,IAAAA,cAAA,QAAMmC,UAAU,aAAY,KAAQH,EAAEtB,OAClC,IAINV,IAAAA,cAAA,OAAKmC,UAAU,mBAAmBD,MAAO,CAACO,OAAQ,UAChDzC,IAAAA,cAAA,MAAImC,UAAU,0BACX9B,GAIT,EAGF,SAAeqC,EAAAA,EAAAA,YAAW3C,E","sources":["webpack://tinode-webapp/./src/widgets/phone-country-selector.jsx"],"sourcesContent":["// Selector for country dialing code.\n\nimport React from 'react';\nimport { injectIntl } from 'react-intl';\n\nimport * as dcodes from '../dcodes.json';\n\nimport { flagEmoji } from '../lib/strformat';\n\nclass PhoneCountrySelector extends React.PureComponent {\n constructor(props) {\n super(props);\n\n this.countries = [];\n const { formatDisplayName } = props.intl;\n dcodes.default.forEach(dc => {\n const parts = dc.dial.split(',');\n parts.forEach(part => {\n this.countries.push({\n dial: part.trim(),\n code: dc.code,\n flag: flagEmoji(dc.code),\n name: formatDisplayName(dc.code, {type: 'region'})\n });\n });\n });\n\n this.countries.sort((a, b) => a.name.localeCompare(b.name));\n }\n\n componentDidMount() {\n if (this.selectedRef) {\n this.selectedRef.scrollIntoView({block: 'center', inline: 'nearest'});\n }\n }\n\n render() {\n const countries = [];\n const selected = this.props.selected || 'US';\n this.countries.forEach((c, idx) => {\n const style = (c.code == selected ? 'selected ' : '');\n countries.push(<li className={style} key={idx}\n ref={ref => {if (c.code == selected) {this.selectedRef = ref}}}\n onClick={_ => this.props.onSubmit(c.code, c.dial)}>\n <span className=\"country-flag\">{c.flag}</span>\n <span className=\"country\">&nbsp;{c.name}</span>\n <span className=\"dial-code\">&nbsp;+{c.dial}</span>\n </li>);\n });\n\n return (\n <div className=\"scrollable-panel\" style={{height: '30rem'}}>\n <ul className=\"phone-country-selector\">\n {countries}\n </ul>\n </div>\n );\n }\n}\n\nexport default injectIntl(PhoneCountrySelector);\n"],"names":["PhoneCountrySelector","React","constructor","props","super","this","countries","formatDisplayName","intl","dcodes","dc","dial","split","forEach","part","push","trim","code","flag","flagEmoji","name","type","sort","a","b","localeCompare","componentDidMount","selectedRef","scrollIntoView","block","inline","render","selected","c","idx","style","className","key","ref","onClick","_","onSubmit","height","injectIntl"],"sourceRoot":""}
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+2
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
-2
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+460 -150
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -351,7 +351,7 @@ class CredentialEdit extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureC
}, react__WEBPACK_IMPORTED_MODULE_0___default().createElement("label", { }, react__WEBPACK_IMPORTED_MODULE_0___default().createElement("label", {
className: "large" className: "large"
}, react__WEBPACK_IMPORTED_MODULE_0___default().createElement(react_intl__WEBPACK_IMPORTED_MODULE_1__.FormattedMessage, { }, react__WEBPACK_IMPORTED_MODULE_0___default().createElement(react_intl__WEBPACK_IMPORTED_MODULE_1__.FormattedMessage, {
id: "change_phonel", id: "change_phone",
defaultMessage: [{ defaultMessage: [{
"type": 0, "type": 0,
"value": "Change phone number" "value": "Change phone number"
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
{"version":3,"file":"src_widgets_phone-country-selector_jsx.dev.js","mappings":";;;;;;;;;;;;;;;;;;;AAE0B;AACc;AAEC;AAEI;AAE7C,MAAMI,oBAAoB,SAASJ,4DAAmB,CAAC;EACrDM,WAAW,CAACC,KAAK,EAAE;IACjB,KAAK,CAACA,KAAK,CAAC;IAEZ,IAAI,CAACC,SAAS,GAAG,EAAE;IACnB,MAAM;MAAEC;IAAkB,CAAC,GAAGF,KAAK,CAACG,IAAI;IACxCR,iDAAsB,CAACW,EAAE,IAAI;MAC3B,MAAMC,KAAK,GAAGD,EAAE,CAACE,IAAI,CAACC,KAAK,CAAC,GAAG,CAAC;MAChCF,KAAK,CAACF,OAAO,CAACK,IAAI,IAAI;QACpB,IAAI,CAACT,SAAS,CAACU,IAAI,CAAC;UAClBH,IAAI,EAAEE,IAAI,CAACE,IAAI,EAAE;UACjBC,IAAI,EAAEP,EAAE,CAACO,IAAI;UACbC,IAAI,EAAElB,yDAAS,CAACU,EAAE,CAACO,IAAI,CAAC;UACxBE,IAAI,EAAEb,iBAAiB,CAACI,EAAE,CAACO,IAAI,EAAE;YAACG,IAAI,EAAE;UAAQ,CAAC;QACnD,CAAC,CAAC;MACJ,CAAC,CAAC;IACJ,CAAC,CAAC;IAEF,IAAI,CAACf,SAAS,CAACgB,IAAI,CAAC,CAACC,CAAC,EAAEC,CAAC,KAAKD,CAAC,CAACH,IAAI,CAACK,aAAa,CAACD,CAAC,CAACJ,IAAI,CAAC,CAAC;EAC7D;EAEAM,iBAAiB,GAAG;IAClB,IAAI,IAAI,CAACC,WAAW,EAAE;MACpB,IAAI,CAACA,WAAW,CAACC,cAAc,CAAC;QAACC,KAAK,EAAE,QAAQ;QAAEC,MAAM,EAAE;MAAS,CAAC,CAAC;IACvE;EACF;EAEAC,MAAM,GAAG;IACP,MAAMzB,SAAS,GAAG,EAAE;IACpB,MAAM0B,QAAQ,GAAG,IAAI,CAAC3B,KAAK,CAAC2B,QAAQ,IAAI,IAAI;IAC5C,IAAI,CAAC1B,SAAS,CAACI,OAAO,CAAC,CAACuB,CAAC,EAAEC,GAAG,KAAK;MACjC,MAAMC,KAAK,GAAIF,CAAC,CAACf,IAAI,IAAIc,QAAQ,GAAG,WAAW,GAAG,EAAG;MACrD1B,SAAS,CAACU,IAAI,CAAC;QAAI,SAAS,EAAEmB,KAAM;QAAC,GAAG,EAAED,GAAI;QAC5C,GAAG,EAAEE,GAAG,IAAI;UAAC,IAAIH,CAAC,CAACf,IAAI,IAAIc,QAAQ,EAAE;YAAC,IAAI,CAACL,WAAW,GAAGS,GAAG;UAAA;QAAC,CAAE;QAC/D,OAAO,EAAEC,CAAC,IAAI,IAAI,CAAChC,KAAK,CAACiC,QAAQ,CAACL,CAAC,CAACf,IAAI,EAAEe,CAAC,CAACpB,IAAI;MAAE,GAClD;QAAM,SAAS,EAAC;MAAc,GAAEoB,CAAC,CAACd,IAAI,CAAQ,EAC9C;QAAM,SAAS,EAAC;MAAS,GAAC,MAAM,EAACc,CAAC,CAACb,IAAI,CAAQ,EAC/C;QAAM,SAAS,EAAC;MAAW,GAAC,OAAO,EAACa,CAAC,CAACpB,IAAI,CAAQ,CAC/C,CAAC;IACR,CAAC,CAAC;IAEF,OACE;MAAK,SAAS,EAAC,kBAAkB;MAAC,KAAK,EAAE;QAAC0B,MAAM,EAAE;MAAO;IAAE,GACzD;MAAI,SAAS,EAAC;IAAwB,GACnCjC,SAAS,CACP,CACD;EAEV;AACF;AAEA,iEAAeP,sDAAU,CAACG,oBAAoB,CAAC","sources":["webpack://tinode-webapp/./src/widgets/phone-country-selector.jsx"],"sourcesContent":["// Selector for country dialing code.\n\nimport React from 'react';\nimport { injectIntl } from 'react-intl';\n\nimport * as dcodes from '../dcodes.json';\n\nimport { flagEmoji } from '../lib/strformat';\n\nclass PhoneCountrySelector extends React.PureComponent {\n constructor(props) {\n super(props);\n\n this.countries = [];\n const { formatDisplayName } = props.intl;\n dcodes.default.forEach(dc => {\n const parts = dc.dial.split(',');\n parts.forEach(part => {\n this.countries.push({\n dial: part.trim(),\n code: dc.code,\n flag: flagEmoji(dc.code),\n name: formatDisplayName(dc.code, {type: 'region'})\n });\n });\n });\n\n this.countries.sort((a, b) => a.name.localeCompare(b.name));\n }\n\n componentDidMount() {\n if (this.selectedRef) {\n this.selectedRef.scrollIntoView({block: 'center', inline: 'nearest'});\n }\n }\n\n render() {\n const countries = [];\n const selected = this.props.selected || 'US';\n this.countries.forEach((c, idx) => {\n const style = (c.code == selected ? 'selected ' : '');\n countries.push(<li className={style} key={idx}\n ref={ref => {if (c.code == selected) {this.selectedRef = ref}}}\n onClick={_ => this.props.onSubmit(c.code, c.dial)}>\n <span className=\"country-flag\">{c.flag}</span>\n <span className=\"country\">&nbsp;{c.name}</span>\n <span className=\"dial-code\">&nbsp;+{c.dial}</span>\n </li>);\n });\n\n return (\n <div className=\"scrollable-panel\" style={{height: '30rem'}}>\n <ul className=\"phone-country-selector\">\n {countries}\n </ul>\n </div>\n );\n }\n}\n\nexport default injectIntl(PhoneCountrySelector);\n"],"names":["React","injectIntl","dcodes","flagEmoji","PhoneCountrySelector","PureComponent","constructor","props","countries","formatDisplayName","intl","default","forEach","dc","parts","dial","split","part","push","trim","code","flag","name","type","sort","a","b","localeCompare","componentDidMount","selectedRef","scrollIntoView","block","inline","render","selected","c","idx","style","ref","_","onSubmit","height"],"sourceRoot":""} {"version":3,"file":"src_widgets_phone-country-selector_jsx.dev.js","mappings":";;;;;;;;;;;;;;;;;;;AAE0B;AACc;AAEC;AAEI;AAE7C,MAAMI,oBAAoB,SAASJ,4DAAmB,CAAC;EACrDM,WAAWA,CAACC,KAAK,EAAE;IACjB,KAAK,CAACA,KAAK,CAAC;IAEZ,IAAI,CAACC,SAAS,GAAG,EAAE;IACnB,MAAM;MAAEC;IAAkB,CAAC,GAAGF,KAAK,CAACG,IAAI;IACxCR,iDAAsB,CAACW,EAAE,IAAI;MAC3B,MAAMC,KAAK,GAAGD,EAAE,CAACE,IAAI,CAACC,KAAK,CAAC,GAAG,CAAC;MAChCF,KAAK,CAACF,OAAO,CAACK,IAAI,IAAI;QACpB,IAAI,CAACT,SAAS,CAACU,IAAI,CAAC;UAClBH,IAAI,EAAEE,IAAI,CAACE,IAAI,EAAE;UACjBC,IAAI,EAAEP,EAAE,CAACO,IAAI;UACbC,IAAI,EAAElB,yDAAS,CAACU,EAAE,CAACO,IAAI,CAAC;UACxBE,IAAI,EAAEb,iBAAiB,CAACI,EAAE,CAACO,IAAI,EAAE;YAACG,IAAI,EAAE;UAAQ,CAAC;QACnD,CAAC,CAAC;MACJ,CAAC,CAAC;IACJ,CAAC,CAAC;IAEF,IAAI,CAACf,SAAS,CAACgB,IAAI,CAAC,CAACC,CAAC,EAAEC,CAAC,KAAKD,CAAC,CAACH,IAAI,CAACK,aAAa,CAACD,CAAC,CAACJ,IAAI,CAAC,CAAC;EAC7D;EAEAM,iBAAiBA,CAAA,EAAG;IAClB,IAAI,IAAI,CAACC,WAAW,EAAE;MACpB,IAAI,CAACA,WAAW,CAACC,cAAc,CAAC;QAACC,KAAK,EAAE,QAAQ;QAAEC,MAAM,EAAE;MAAS,CAAC,CAAC;IACvE;EACF;EAEAC,MAAMA,CAAA,EAAG;IACP,MAAMzB,SAAS,GAAG,EAAE;IACpB,MAAM0B,QAAQ,GAAG,IAAI,CAAC3B,KAAK,CAAC2B,QAAQ,IAAI,IAAI;IAC5C,IAAI,CAAC1B,SAAS,CAACI,OAAO,CAAC,CAACuB,CAAC,EAAEC,GAAG,KAAK;MACjC,MAAMC,KAAK,GAAIF,CAAC,CAACf,IAAI,IAAIc,QAAQ,GAAG,WAAW,GAAG,EAAG;MACrD1B,SAAS,CAACU,IAAI,CAAClB,0DAAA;QAAIuC,SAAS,EAAEF,KAAM;QAACG,GAAG,EAAEJ,GAAI;QAC5CK,GAAG,EAAEA,GAAG,IAAI;UAAC,IAAIN,CAAC,CAACf,IAAI,IAAIc,QAAQ,EAAE;YAAC,IAAI,CAACL,WAAW,GAAGY,GAAG;UAAA;QAAC,CAAE;QAC/DC,OAAO,EAAEC,CAAC,IAAI,IAAI,CAACpC,KAAK,CAACqC,QAAQ,CAACT,CAAC,CAACf,IAAI,EAAEe,CAAC,CAACpB,IAAI;MAAE,GAClDf,0DAAA;QAAMuC,SAAS,EAAC;MAAc,GAAEJ,CAAC,CAACd,IAAI,CAAQ,EAC9CrB,0DAAA;QAAMuC,SAAS,EAAC;MAAS,GAAC,MAAM,EAACJ,CAAC,CAACb,IAAI,CAAQ,EAC/CtB,0DAAA;QAAMuC,SAAS,EAAC;MAAW,GAAC,OAAO,EAACJ,CAAC,CAACpB,IAAI,CAAQ,CAC/C,CAAC;IACR,CAAC,CAAC;IAEF,OACEf,0DAAA;MAAKuC,SAAS,EAAC,kBAAkB;MAACF,KAAK,EAAE;QAACQ,MAAM,EAAE;MAAO;IAAE,GACzD7C,0DAAA;MAAIuC,SAAS,EAAC;IAAwB,GACnC/B,SAAS,CACP,CACD;EAEV;AACF;AAEA,iEAAeP,sDAAU,CAACG,oBAAoB,CAAC","sources":["webpack://tinode-webapp/./src/widgets/phone-country-selector.jsx"],"sourcesContent":["// Selector for country dialing code.\n\nimport React from 'react';\nimport { injectIntl } from 'react-intl';\n\nimport * as dcodes from '../dcodes.json';\n\nimport { flagEmoji } from '../lib/strformat';\n\nclass PhoneCountrySelector extends React.PureComponent {\n constructor(props) {\n super(props);\n\n this.countries = [];\n const { formatDisplayName } = props.intl;\n dcodes.default.forEach(dc => {\n const parts = dc.dial.split(',');\n parts.forEach(part => {\n this.countries.push({\n dial: part.trim(),\n code: dc.code,\n flag: flagEmoji(dc.code),\n name: formatDisplayName(dc.code, {type: 'region'})\n });\n });\n });\n\n this.countries.sort((a, b) => a.name.localeCompare(b.name));\n }\n\n componentDidMount() {\n if (this.selectedRef) {\n this.selectedRef.scrollIntoView({block: 'center', inline: 'nearest'});\n }\n }\n\n render() {\n const countries = [];\n const selected = this.props.selected || 'US';\n this.countries.forEach((c, idx) => {\n const style = (c.code == selected ? 'selected ' : '');\n countries.push(<li className={style} key={idx}\n ref={ref => {if (c.code == selected) {this.selectedRef = ref}}}\n onClick={_ => this.props.onSubmit(c.code, c.dial)}>\n <span className=\"country-flag\">{c.flag}</span>\n <span className=\"country\">&nbsp;{c.name}</span>\n <span className=\"dial-code\">&nbsp;+{c.dial}</span>\n </li>);\n });\n\n return (\n <div className=\"scrollable-panel\" style={{height: '30rem'}}>\n <ul className=\"phone-country-selector\">\n {countries}\n </ul>\n </div>\n );\n }\n}\n\nexport default injectIntl(PhoneCountrySelector);\n"],"names":["React","injectIntl","dcodes","flagEmoji","PhoneCountrySelector","PureComponent","constructor","props","countries","formatDisplayName","intl","default","forEach","dc","parts","dial","split","part","push","trim","code","flag","name","type","sort","a","b","localeCompare","componentDidMount","selectedRef","scrollIntoView","block","inline","render","selected","c","idx","style","createElement","className","key","ref","onClick","_","onSubmit","height"],"sourceRoot":""}

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