Compare commits
31
Commits
03e87697cf
...
ba94788fbe
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ba94788fbe | ||
|
|
45fbff1b4a | ||
|
|
f99f5a223f | ||
|
|
c0e4cd4965 | ||
|
|
1d3a9040cb | ||
|
|
cab80e4d62 | ||
|
|
f7c5322e36 | ||
|
|
cb64e1f247 | ||
|
|
afee6b5dd9 | ||
|
|
9f01eab324 | ||
|
|
1bc6f87844 | ||
|
|
0ea112b145 | ||
|
|
768edd744d | ||
|
|
1aadf4e7bd | ||
|
|
2d92e475b5 | ||
|
|
a28c3eb5cd | ||
|
|
97208e1448 | ||
|
|
9fbc601870 | ||
|
|
4489646144 | ||
|
|
261b2dbdd7 | ||
|
|
224323f443 | ||
|
|
6d031f033e | ||
|
|
c38e1832dc | ||
|
|
13036da36b | ||
|
|
f5c628fbb5 | ||
|
|
5673d82b33 | ||
|
|
8c3287941a | ||
|
|
830d15f7ec | ||
|
|
4e42d96d66 | ||
|
|
ed77f48189 | ||
|
|
ed3107405f |
@@ -58,7 +58,9 @@ More translations are welcome. See [instructions](https://github.com/tinode/chat
|
||||
* End-to-End encryption.
|
||||
* Emoji support is weak.
|
||||
* Mentions, hashtags.
|
||||
* Previews not generated for videos, links or docs.
|
||||
* Previews not generated for links and docs.
|
||||
* Location and contact sharing.
|
||||
* Recording video messages.
|
||||
|
||||
## Other
|
||||
|
||||
|
||||
+20
-26
@@ -1011,7 +1011,8 @@ i.search {
|
||||
}
|
||||
|
||||
.contact-box .contact-comment .material-icons {
|
||||
margin-bottom: .2rem;
|
||||
margin-bottom: 0.2rem;
|
||||
color: #777;
|
||||
}
|
||||
|
||||
/* Avatar and online-offline indicator */
|
||||
@@ -1080,15 +1081,15 @@ i.search {
|
||||
}
|
||||
|
||||
img.channel {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
vertical-align: middle;
|
||||
margin-left: 0.3rem;
|
||||
}
|
||||
|
||||
.large img.channel {
|
||||
width: 1.35rem;
|
||||
height: 1.35rem;
|
||||
width: 1.45rem;
|
||||
height: 1.45rem;
|
||||
margin-left: 0.5rem;
|
||||
margin-bottom: 0.15rem;
|
||||
}
|
||||
@@ -1990,7 +1991,6 @@ video {
|
||||
|
||||
/* Inline and preview image styles */
|
||||
.inline-image, .image-preview {
|
||||
display: flex;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border-radius: 0.1rem;
|
||||
@@ -2075,6 +2075,7 @@ img.image-preview::before {
|
||||
height: 4rem;
|
||||
background-color: rgba(0, 0, 0, 0.4);
|
||||
border-radius: 50%;
|
||||
pointer-events: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@@ -2223,8 +2224,8 @@ ul.tabbar li.active > a {
|
||||
|
||||
.material-icons.as-badge {
|
||||
padding-left: 0.2rem;
|
||||
font-size: 1.25rem;
|
||||
color: #bbb;
|
||||
font-size: 1.15rem;
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
/* Badge with a label, like [you], [muted], etc */
|
||||
@@ -2976,31 +2977,24 @@ ul.tabbar li.active > a {
|
||||
}
|
||||
|
||||
/*
|
||||
Drag-and-drop container.
|
||||
Drag-and-drop area.
|
||||
*/
|
||||
.drag-and-drop {
|
||||
.drag-n-drop {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border: dashed grey 0.15rem;
|
||||
border-radius: 1rem;
|
||||
background-color: rgba(255,255,255,.95);
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.drag-and-drop .banner {
|
||||
border: dashed grey 1px;
|
||||
background-color: rgba(255,255,255,.8);
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 9999;
|
||||
/* Center text & font. */
|
||||
display: flex;
|
||||
top: 1.5rem;
|
||||
bottom: 1.5rem;
|
||||
left: 1.5rem;
|
||||
right: 1.5rem;
|
||||
z-index: 3;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: 2rem;
|
||||
color: grey;
|
||||
outline: 0.5rem solid rgba(255,255,255,.95);
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 3.1 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 620 B |
Binary file not shown.
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.4 KiB |
Generated
+946
-1231
File diff suppressed because it is too large
Load Diff
+26
-26
@@ -111,35 +111,35 @@ self.addEventListener('notificationclick', event => {
|
||||
|
||||
event.waitUntil(self.clients.matchAll({
|
||||
type: 'window',
|
||||
includeUncontrolled: true
|
||||
}).then(windowClients => {
|
||||
let anyClient = null;
|
||||
for (let i = 0; i < windowClients.length; i++) {
|
||||
const url = new URL(windowClients[i].url);
|
||||
if (url.hash.includes(data.topic)) {
|
||||
// Found the Tinode tab with the right topic open.
|
||||
return windowClients[i].focus();
|
||||
} else {
|
||||
// This will be the least recently used tab.
|
||||
anyClient = windowClients[i];
|
||||
includeUncontrolled: true})
|
||||
.then(windowClients => {
|
||||
let anyClient = null;
|
||||
for (let i = 0; i < windowClients.length; i++) {
|
||||
const url = new URL(windowClients[i].url);
|
||||
if (url.hash.includes(data.topic)) {
|
||||
// Found the Tinode tab with the right topic open.
|
||||
return windowClients[i].focus();
|
||||
} else {
|
||||
// This will be the least recently used tab.
|
||||
anyClient = windowClients[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Found tab with Tinode on a different topic,
|
||||
// navigate to the right topic.
|
||||
if (anyClient) {
|
||||
const url = new URL(anyClient.url);
|
||||
// Found tab with Tinode on a different topic,
|
||||
// navigate to the right topic.
|
||||
if (anyClient) {
|
||||
const url = new URL(anyClient.url);
|
||||
url.hash = urlHash;
|
||||
return anyClient.focus().then(thisClient => {
|
||||
return thisClient.navigate(url);
|
||||
});
|
||||
}
|
||||
|
||||
// Did not find a Tinode browser tab. Open one.
|
||||
const url = new URL(self.location.origin);
|
||||
url.hash = urlHash;
|
||||
return anyClient.focus().then(thisClient => {
|
||||
return thisClient.navigate(url);
|
||||
});
|
||||
}
|
||||
|
||||
// Did not find a Tinode browser tab. Open one.
|
||||
const url = new URL(self.location.origin);
|
||||
url.hash = urlHash;
|
||||
return clients.openWindow(url);
|
||||
}));
|
||||
return clients.openWindow(url);
|
||||
}));
|
||||
});
|
||||
|
||||
// This is needed for 'Add to Home Screen'.
|
||||
|
||||
@@ -53,6 +53,9 @@ export const MAX_AVATAR_BYTES = 4096;
|
||||
// Size of the broken_image shown in MessagesView
|
||||
export const BROKEN_IMAGE_SIZE = 32;
|
||||
|
||||
// Size of video preview when dimensions are missing.
|
||||
export const NO_DIMENSIONS_VIDEO = 128;
|
||||
|
||||
// Number of chat messages to fetch in one call.
|
||||
export const MESSAGES_PAGE = 24;
|
||||
|
||||
@@ -81,6 +84,8 @@ export const VIDEO_PREVIEW_DIM = 96;
|
||||
|
||||
// Linear dimensions of image thumbnail: shrink image under this size for thumbnails in reply quote previews.
|
||||
export const IMAGE_THUMBNAIL_DIM = 36;
|
||||
// Width of video thumbnail: allow it to be wider, but not taller. Use IMAGE_THUMBNAIL_DIM for height.
|
||||
export const VIDEO_THUMBNAIL_WIDTH = 48;
|
||||
|
||||
// Maximum number of online users to be shown in a topic title bar. Others will be
|
||||
// hidden under "+X more"
|
||||
|
||||
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
+26
-5
@@ -317,21 +317,21 @@
|
||||
"drafty_attachment": {
|
||||
"translation": "Dateianhang",
|
||||
"defaultMessage": "Attachment",
|
||||
"description": "Comment for attachment in drafty",
|
||||
"description": "Comment for attachment in Drafty",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"drafty_form": {
|
||||
"translation": "Formular:",
|
||||
"defaultMessage": "Form:",
|
||||
"description": "Comment for form in drafty",
|
||||
"description": "Comment for form in Drafty",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"drafty_image": {
|
||||
"translation": "Bild",
|
||||
"defaultMessage": "Picture",
|
||||
"description": "Comment for embedded images in drafty",
|
||||
"description": "Comment for embedded images in Drafty",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
@@ -1523,10 +1523,31 @@
|
||||
"obsolete": false
|
||||
},
|
||||
"drag_file": {
|
||||
"translation": "",
|
||||
"translation": "Datei hierher ziehen",
|
||||
"defaultMessage": "Drag file here",
|
||||
"description": "Prompt on the file drag-n-drop overlay banner",
|
||||
"missing": true,
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"drafty_video": {
|
||||
"translation": "Videoaufnahme",
|
||||
"defaultMessage": "Video recording",
|
||||
"description": "Comment for videos embedded in Drafty",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"call_in_progress": {
|
||||
"translation": "in Arbeit",
|
||||
"defaultMessage": "in progress",
|
||||
"description": "Label for call in progress",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"unrecognized_video_format": {
|
||||
"translation": "Das Format dieses Videos wird nicht erkannt",
|
||||
"defaultMessage": "Format of this video is not recognized",
|
||||
"description": "Error message when uploaded video is invalid",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
}
|
||||
}
|
||||
|
||||
+24
-3
@@ -317,21 +317,21 @@
|
||||
"drafty_attachment": {
|
||||
"translation": "Attachment",
|
||||
"defaultMessage": "Attachment",
|
||||
"description": "Comment for attachment in drafty",
|
||||
"description": "Comment for attachment in Drafty",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"drafty_form": {
|
||||
"translation": "Form:",
|
||||
"defaultMessage": "Form:",
|
||||
"description": "Comment for form in drafty",
|
||||
"description": "Comment for form in Drafty",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"drafty_image": {
|
||||
"translation": "Picture",
|
||||
"defaultMessage": "Picture",
|
||||
"description": "Comment for embedded images in drafty",
|
||||
"description": "Comment for embedded images in Drafty",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
@@ -1528,5 +1528,26 @@
|
||||
"description": "Prompt on the file drag-n-drop overlay banner",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"drafty_video": {
|
||||
"translation": "Video recording",
|
||||
"defaultMessage": "Video recording",
|
||||
"description": "Comment for videos embedded in Drafty",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"call_in_progress": {
|
||||
"translation": "in progress",
|
||||
"defaultMessage": "in progress",
|
||||
"description": "Label for call in progress",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"unrecognized_video_format": {
|
||||
"translation": "Format of this video is not recognized",
|
||||
"defaultMessage": "Format of this video is not recognized",
|
||||
"description": "Error message when uploaded video is invalid",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
}
|
||||
}
|
||||
|
||||
+26
-5
@@ -317,21 +317,21 @@
|
||||
"drafty_attachment": {
|
||||
"translation": "Archivo",
|
||||
"defaultMessage": "Attachment",
|
||||
"description": "Comment for attachment in drafty",
|
||||
"description": "Comment for attachment in Drafty",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"drafty_form": {
|
||||
"translation": "Formulario:",
|
||||
"defaultMessage": "Form:",
|
||||
"description": "Comment for form in drafty",
|
||||
"description": "Comment for form in Drafty",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"drafty_image": {
|
||||
"translation": "Imagen",
|
||||
"defaultMessage": "Picture",
|
||||
"description": "Comment for embedded images in drafty",
|
||||
"description": "Comment for embedded images in Drafty",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
@@ -1523,10 +1523,31 @@
|
||||
"obsolete": false
|
||||
},
|
||||
"drag_file": {
|
||||
"translation": "",
|
||||
"translation": "Arrastra el archivo aquí",
|
||||
"defaultMessage": "Drag file here",
|
||||
"description": "Prompt on the file drag-n-drop overlay banner",
|
||||
"missing": true,
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"drafty_video": {
|
||||
"translation": "Grabación de vídeo",
|
||||
"defaultMessage": "Video recording",
|
||||
"description": "Comment for videos embedded in Drafty",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"call_in_progress": {
|
||||
"translation": "en progreso",
|
||||
"defaultMessage": "in progress",
|
||||
"description": "Label for call in progress",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"unrecognized_video_format": {
|
||||
"translation": "No se reconoce el formato de este video",
|
||||
"defaultMessage": "Format of this video is not recognized",
|
||||
"description": "Error message when uploaded video is invalid",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,6 +135,10 @@
|
||||
"defaultMessage": "disconnected",
|
||||
"description": "Label for disconnected call"
|
||||
},
|
||||
"call_in_progress": {
|
||||
"defaultMessage": "in progress",
|
||||
"description": "Label for call in progress"
|
||||
},
|
||||
"call_missed": {
|
||||
"defaultMessage": "missed",
|
||||
"description": "Label for missed incoming call"
|
||||
@@ -225,20 +229,24 @@
|
||||
},
|
||||
"drafty_attachment": {
|
||||
"defaultMessage": "Attachment",
|
||||
"description": "Comment for attachment in drafty"
|
||||
"description": "Comment for attachment in Drafty"
|
||||
},
|
||||
"drafty_form": {
|
||||
"defaultMessage": "Form:",
|
||||
"description": "Comment for form in drafty"
|
||||
"description": "Comment for form in Drafty"
|
||||
},
|
||||
"drafty_image": {
|
||||
"defaultMessage": "Picture",
|
||||
"description": "Comment for embedded images in drafty"
|
||||
"description": "Comment for embedded images in Drafty"
|
||||
},
|
||||
"drafty_unknown": {
|
||||
"defaultMessage": "Unsupported",
|
||||
"description": "Unsupported entity in drafty"
|
||||
},
|
||||
"drafty_video": {
|
||||
"defaultMessage": "Video recording",
|
||||
"description": "Comment for videos embedded in Drafty"
|
||||
},
|
||||
"drag_file": {
|
||||
"defaultMessage": "Drag file here",
|
||||
"description": "Prompt on the file drag-n-drop overlay banner"
|
||||
@@ -856,6 +864,10 @@
|
||||
"defaultMessage": "Unnamed",
|
||||
"description": "Title shown when the topic has no name"
|
||||
},
|
||||
"unrecognized_video_format": {
|
||||
"defaultMessage": "Format of this video is not recognized",
|
||||
"description": "Error message when uploaded video is invalid"
|
||||
},
|
||||
"update_available": {
|
||||
"defaultMessage": "Update available.",
|
||||
"description": "Message shown when an app update is available."
|
||||
|
||||
+26
-5
@@ -317,21 +317,21 @@
|
||||
"drafty_attachment": {
|
||||
"translation": "Pièce(s) jointe(s)",
|
||||
"defaultMessage": "Attachment",
|
||||
"description": "Comment for attachment in drafty",
|
||||
"description": "Comment for attachment in Drafty",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"drafty_form": {
|
||||
"translation": "Formulaire:",
|
||||
"defaultMessage": "Form:",
|
||||
"description": "Comment for form in drafty",
|
||||
"description": "Comment for form in Drafty",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"drafty_image": {
|
||||
"translation": "Image",
|
||||
"defaultMessage": "Picture",
|
||||
"description": "Comment for embedded images in drafty",
|
||||
"description": "Comment for embedded images in Drafty",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
@@ -1523,10 +1523,31 @@
|
||||
"obsolete": false
|
||||
},
|
||||
"drag_file": {
|
||||
"translation": "",
|
||||
"translation": "Faites glisser le fichier ici",
|
||||
"defaultMessage": "Drag file here",
|
||||
"description": "Prompt on the file drag-n-drop overlay banner",
|
||||
"missing": true,
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"drafty_video": {
|
||||
"translation": "Enregistrement video",
|
||||
"defaultMessage": "Video recording",
|
||||
"description": "Comment for videos embedded in Drafty",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"call_in_progress": {
|
||||
"translation": "en cours",
|
||||
"defaultMessage": "in progress",
|
||||
"description": "Label for call in progress",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"unrecognized_video_format": {
|
||||
"translation": "Le format de cette vidéo n'est pas reconnu",
|
||||
"defaultMessage": "Format of this video is not recognized",
|
||||
"description": "Error message when uploaded video is invalid",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
}
|
||||
}
|
||||
|
||||
+26
-5
@@ -317,21 +317,21 @@
|
||||
"drafty_attachment": {
|
||||
"translation": "부착",
|
||||
"defaultMessage": "Attachment",
|
||||
"description": "Comment for attachment in drafty",
|
||||
"description": "Comment for attachment in Drafty",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"drafty_form": {
|
||||
"translation": "형태:",
|
||||
"defaultMessage": "Form:",
|
||||
"description": "Comment for form in drafty",
|
||||
"description": "Comment for form in Drafty",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"drafty_image": {
|
||||
"translation": "이미지",
|
||||
"defaultMessage": "Picture",
|
||||
"description": "Comment for embedded images in drafty",
|
||||
"description": "Comment for embedded images in Drafty",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
@@ -1523,10 +1523,31 @@
|
||||
"obsolete": false
|
||||
},
|
||||
"drag_file": {
|
||||
"translation": "",
|
||||
"translation": "여기로 파일 드래그",
|
||||
"defaultMessage": "Drag file here",
|
||||
"description": "Prompt on the file drag-n-drop overlay banner",
|
||||
"missing": true,
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"drafty_video": {
|
||||
"translation": "녹화",
|
||||
"defaultMessage": "Video recording",
|
||||
"description": "Comment for videos embedded in Drafty",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"call_in_progress": {
|
||||
"translation": "통화 중",
|
||||
"defaultMessage": "in progress",
|
||||
"description": "Label for call in progress",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"unrecognized_video_format": {
|
||||
"translation": "이 동영상의 형식을 인식할 수 없습니다.",
|
||||
"defaultMessage": "Format of this video is not recognized",
|
||||
"description": "Error message when uploaded video is invalid",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
}
|
||||
}
|
||||
|
||||
+26
-5
@@ -317,21 +317,21 @@
|
||||
"drafty_attachment": {
|
||||
"translation": "Atasament",
|
||||
"defaultMessage": "Attachment",
|
||||
"description": "Comment for attachment in drafty",
|
||||
"description": "Comment for attachment in Drafty",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"drafty_form": {
|
||||
"translation": "Formă:",
|
||||
"defaultMessage": "Form:",
|
||||
"description": "Comment for form in drafty",
|
||||
"description": "Comment for form in Drafty",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"drafty_image": {
|
||||
"translation": "Imagine",
|
||||
"defaultMessage": "Picture",
|
||||
"description": "Comment for embedded images in drafty",
|
||||
"description": "Comment for embedded images in Drafty",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
@@ -1523,10 +1523,31 @@
|
||||
"obsolete": false
|
||||
},
|
||||
"drag_file": {
|
||||
"translation": "",
|
||||
"translation": "Trageți fișierul aici",
|
||||
"defaultMessage": "Drag file here",
|
||||
"description": "Prompt on the file drag-n-drop overlay banner",
|
||||
"missing": true,
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"drafty_video": {
|
||||
"translation": "Înregistrare video",
|
||||
"defaultMessage": "Video recording",
|
||||
"description": "Comment for videos embedded in Drafty",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"call_in_progress": {
|
||||
"translation": "în curs",
|
||||
"defaultMessage": "in progress",
|
||||
"description": "Label for call in progress",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"unrecognized_video_format": {
|
||||
"translation": "Formatul acestui videoclip nu este recunoscut",
|
||||
"defaultMessage": "Format of this video is not recognized",
|
||||
"description": "Error message when uploaded video is invalid",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
}
|
||||
}
|
||||
|
||||
+27
-6
@@ -317,21 +317,21 @@
|
||||
"drafty_attachment": {
|
||||
"translation": "Аттачмент",
|
||||
"defaultMessage": "Attachment",
|
||||
"description": "Comment for attachment in drafty",
|
||||
"description": "Comment for attachment in Drafty",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"drafty_form": {
|
||||
"translation": "Форма:",
|
||||
"defaultMessage": "Form:",
|
||||
"description": "Comment for form in drafty",
|
||||
"description": "Comment for form in Drafty",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"drafty_image": {
|
||||
"translation": "Картинка",
|
||||
"defaultMessage": "Picture",
|
||||
"description": "Comment for embedded images in drafty",
|
||||
"description": "Comment for embedded images in Drafty",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
@@ -1118,7 +1118,7 @@
|
||||
"obsolete": false
|
||||
},
|
||||
"tags_not_found": {
|
||||
"translation": "Тегов нет. Добавьте",
|
||||
"translation": "Тегов нет. Добавьте.",
|
||||
"defaultMessage": "No tags defined. Add some.",
|
||||
"description": "",
|
||||
"missing": false,
|
||||
@@ -1523,10 +1523,31 @@
|
||||
"obsolete": false
|
||||
},
|
||||
"drag_file": {
|
||||
"translation": "",
|
||||
"translation": "Перетащите файл сюда",
|
||||
"defaultMessage": "Drag file here",
|
||||
"description": "Prompt on the file drag-n-drop overlay banner",
|
||||
"missing": true,
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"drafty_video": {
|
||||
"translation": "Видеозапись",
|
||||
"defaultMessage": "Video recording",
|
||||
"description": "Comment for videos embedded in Drafty",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"call_in_progress": {
|
||||
"translation": "в процессе",
|
||||
"defaultMessage": "in progress",
|
||||
"description": "Label for call in progress",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"unrecognized_video_format": {
|
||||
"translation": "Неизвестный формат видео файла",
|
||||
"defaultMessage": "Format of this video is not recognized",
|
||||
"description": "Error message when uploaded video is invalid",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
}
|
||||
}
|
||||
|
||||
+26
-5
@@ -317,21 +317,21 @@
|
||||
"drafty_attachment": {
|
||||
"translation": "附加檔案",
|
||||
"defaultMessage": "Attachment",
|
||||
"description": "Comment for attachment in drafty",
|
||||
"description": "Comment for attachment in Drafty",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"drafty_form": {
|
||||
"translation": "表單:",
|
||||
"defaultMessage": "Form:",
|
||||
"description": "Comment for form in drafty",
|
||||
"description": "Comment for form in Drafty",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"drafty_image": {
|
||||
"translation": "圖片",
|
||||
"defaultMessage": "Picture",
|
||||
"description": "Comment for embedded images in drafty",
|
||||
"description": "Comment for embedded images in Drafty",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
@@ -1523,10 +1523,31 @@
|
||||
"obsolete": false
|
||||
},
|
||||
"drag_file": {
|
||||
"translation": "",
|
||||
"translation": "將文件拖到這裡",
|
||||
"defaultMessage": "Drag file here",
|
||||
"description": "Prompt on the file drag-n-drop overlay banner",
|
||||
"missing": true,
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"drafty_video": {
|
||||
"translation": "視頻錄製",
|
||||
"defaultMessage": "Video recording",
|
||||
"description": "Comment for videos embedded in Drafty",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"call_in_progress": {
|
||||
"translation": "通話中",
|
||||
"defaultMessage": "in progress",
|
||||
"description": "Label for call in progress",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"unrecognized_video_format": {
|
||||
"translation": "無法識別此視頻的格式",
|
||||
"defaultMessage": "Format of this video is not recognized",
|
||||
"description": "Error message when uploaded video is invalid",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
}
|
||||
}
|
||||
|
||||
+26
-5
@@ -317,21 +317,21 @@
|
||||
"drafty_attachment": {
|
||||
"translation": "附件",
|
||||
"defaultMessage": "Attachment",
|
||||
"description": "Comment for attachment in drafty",
|
||||
"description": "Comment for attachment in Drafty",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"drafty_form": {
|
||||
"translation": "形式:",
|
||||
"defaultMessage": "Form:",
|
||||
"description": "Comment for form in drafty",
|
||||
"description": "Comment for form in Drafty",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"drafty_image": {
|
||||
"translation": "图像",
|
||||
"defaultMessage": "Picture",
|
||||
"description": "Comment for embedded images in drafty",
|
||||
"description": "Comment for embedded images in Drafty",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
@@ -1523,10 +1523,31 @@
|
||||
"obsolete": false
|
||||
},
|
||||
"drag_file": {
|
||||
"translation": "",
|
||||
"translation": "将文件拖到这里",
|
||||
"defaultMessage": "Drag file here",
|
||||
"description": "Prompt on the file drag-n-drop overlay banner",
|
||||
"missing": true,
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"drafty_video": {
|
||||
"translation": "视频录制",
|
||||
"defaultMessage": "Video recording",
|
||||
"description": "Comment for videos embedded in Drafty",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"call_in_progress": {
|
||||
"translation": "通话中",
|
||||
"defaultMessage": "in progress",
|
||||
"description": "Label for call in progress",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"unrecognized_video_format": {
|
||||
"translation": "无法识别此视频的格式",
|
||||
"defaultMessage": "Format of this video is not recognized",
|
||||
"description": "Error message when uploaded video is invalid",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
}
|
||||
}
|
||||
|
||||
+84
-22
@@ -10,7 +10,7 @@ import InlineVideo from '../widgets/inline-video.jsx';
|
||||
import LazyImage from '../widgets/lazy-image.jsx'
|
||||
import UploadingImage from '../widgets/uploading-image.jsx'
|
||||
|
||||
import { IMAGE_THUMBNAIL_DIM, BROKEN_IMAGE_SIZE, REM_SIZE } from '../config.js';
|
||||
import { BROKEN_IMAGE_SIZE, IMAGE_THUMBNAIL_DIM, NO_DIMENSIONS_VIDEO, REM_SIZE, VIDEO_THUMBNAIL_WIDTH } from '../config.js';
|
||||
import { base64ToBlob, blobToBase64, fitImageSize, imageScaled } from './blob-helpers.js';
|
||||
import { idToColorClass, secondsToTime, shortenFileName } from './strformat.js';
|
||||
import { cancelablePromise, sanitizeUrlForMime } from './utils.js';
|
||||
@@ -19,17 +19,22 @@ const messages = defineMessages({
|
||||
drafty_form: {
|
||||
id: 'drafty_form',
|
||||
defaultMessage: 'Form: ',
|
||||
description: 'Comment for form in drafty'
|
||||
description: 'Comment for form in Drafty'
|
||||
},
|
||||
drafty_attachment: {
|
||||
id: 'drafty_attachment',
|
||||
defaultMessage: 'Attachment',
|
||||
description: 'Comment for attachment in drafty'
|
||||
description: 'Comment for attachment in Drafty'
|
||||
},
|
||||
drafty_image: {
|
||||
id: 'drafty_image',
|
||||
defaultMessage: 'Picture',
|
||||
description: 'Comment for embedded images in drafty'
|
||||
description: 'Comment for embedded images in Drafty'
|
||||
},
|
||||
drafty_video: {
|
||||
id: 'drafty_video',
|
||||
defaultMessage: 'Video recording',
|
||||
description: 'Comment for videos embedded in Drafty'
|
||||
},
|
||||
drafty_unknown: {
|
||||
id: 'drafty_unknown',
|
||||
@@ -213,12 +218,11 @@ function handleVideoData(el, data, attr) {
|
||||
};
|
||||
return el;
|
||||
}
|
||||
|
||||
attr.className = 'inline-image';
|
||||
const dim = fitImageSize(data.width, data.height,
|
||||
this.viewportWidth > 0 ? Math.min(this.viewportWidth - REM_SIZE * 6.5, REM_SIZE * 34.5) :
|
||||
REM_SIZE * 34.5, REM_SIZE * 24, false) ||
|
||||
{dstWidth: BROKEN_IMAGE_SIZE, dstHeight: BROKEN_IMAGE_SIZE};
|
||||
{dstWidth: NO_DIMENSIONS_VIDEO, dstHeight: NO_DIMENSIONS_VIDEO};
|
||||
attr.style = {
|
||||
width: dim.dstWidth + 'px',
|
||||
height: dim.dstHeight + 'px',
|
||||
@@ -227,6 +231,7 @@ function handleVideoData(el, data, attr) {
|
||||
minHeight: dim.dstHeight + 'px'
|
||||
};
|
||||
if (!Drafty.isProcessing(data)) {
|
||||
attr.src = this.authorizeURL(sanitizeUrlForMime(attr.src, 'image'));
|
||||
attr.alt = data.name;
|
||||
if (data.ref || data.val) {
|
||||
attr.onClick = this.onVideoPreview;
|
||||
@@ -319,6 +324,11 @@ export function previewFormatter(style, data, values, key) {
|
||||
el = null;
|
||||
values = null;
|
||||
break;
|
||||
case 'VD':
|
||||
// Replace image with '[icon] Video'.
|
||||
el = React.Fragment;
|
||||
values = [<i key="im" className="material-icons">play_circle_outline</i>, ' ', this.formatMessage(messages.drafty_video)];
|
||||
break;
|
||||
default:
|
||||
if (!el) {
|
||||
// Unknown element.
|
||||
@@ -333,7 +343,7 @@ export function previewFormatter(style, data, values, key) {
|
||||
return React.createElement(el, attr, values);
|
||||
};
|
||||
|
||||
// Converts Drafty object into a quoted reply. 'this' is set by the caller.
|
||||
// Converts Drafty object into a quoted reply; 'this' is set by the caller.
|
||||
function inlineImageAttr(attr, data) {
|
||||
attr.style = {
|
||||
width: IMAGE_THUMBNAIL_DIM + 'px',
|
||||
@@ -345,11 +355,33 @@ function inlineImageAttr(attr, data) {
|
||||
attr.alt = this.formatMessage(messages.drafty_image);
|
||||
if (!data) {
|
||||
attr.src = 'img/broken_image.png';
|
||||
} else {
|
||||
attr.src = attr.src || 'img/broken_image.png';
|
||||
}
|
||||
attr.title = attr.alt;
|
||||
return attr;
|
||||
}
|
||||
|
||||
// Converts Drafty object into a quoted reply; 'this' is set by the caller.
|
||||
function inlineVideoAttr(attr, data) {
|
||||
const dim = fitImageSize(data.width, data.height, VIDEO_THUMBNAIL_WIDTH, IMAGE_THUMBNAIL_DIM);
|
||||
attr.style = {
|
||||
width: dim.width + 'px',
|
||||
height: dim.height + 'px',
|
||||
maxWidth: VIDEO_THUMBNAIL_WIDTH + 'px',
|
||||
maxHeight: IMAGE_THUMBNAIL_DIM + 'px',
|
||||
}
|
||||
attr.className = 'inline-image';
|
||||
attr.alt = this.formatMessage(messages.drafty_video);
|
||||
attr.title = attr.alt;
|
||||
if (!data) {
|
||||
attr.src = 'img/broken_video.png';
|
||||
} else {
|
||||
attr.src = attr.src || 'img/broken_video.png';
|
||||
}
|
||||
return attr;
|
||||
}
|
||||
|
||||
// Displays a portion of Drafty within 'QQ' quotes. 'this' is set by the caller.
|
||||
// 'this' must contain:
|
||||
// formatMessage: this.props.intl.formatMessage
|
||||
@@ -357,7 +389,7 @@ function inlineImageAttr(attr, data) {
|
||||
// authorizeURL: this.props.tinode.authorizeURL
|
||||
// onQuoteClick: this.handleQuoteClick (optional)
|
||||
function quoteFormatter(style, data, values, key) {
|
||||
if (['BR', 'EX', 'IM', 'MN'].includes(style)) {
|
||||
if (['BR', 'EX', 'IM', 'MN', 'VD'].includes(style)) {
|
||||
let el = Drafty.tagName(style);
|
||||
let attr = Drafty.attrValue(style, data) || {};
|
||||
attr.key = key;
|
||||
@@ -372,6 +404,13 @@ function quoteFormatter(style, data, values, key) {
|
||||
// Fragment attributes.
|
||||
attr = {key: key};
|
||||
break;
|
||||
case 'VD':
|
||||
attr = inlineVideoAttr.call(this, attr, data);
|
||||
values = [React.createElement('img', attr, null), ' ', attr.alt];
|
||||
el = React.Fragment;
|
||||
// Fragment attributes.
|
||||
attr = {key: key};
|
||||
break;
|
||||
case 'MN':
|
||||
el = 'span';
|
||||
attr.className = 'mention'
|
||||
@@ -402,17 +441,27 @@ function quoteFormatter(style, data, values, key) {
|
||||
}
|
||||
|
||||
// Create image thumbnail suitable for inclusion in a quote.
|
||||
function quoteImage(data) {
|
||||
function quoteImageOrVideo(data, isVideo) {
|
||||
let promise;
|
||||
let bits, ref, mime;
|
||||
if (isVideo) {
|
||||
bits = data.preview;
|
||||
mime = data.premime || 'image/jpeg';
|
||||
ref = data.preref;
|
||||
} else {
|
||||
bits = data.val;
|
||||
mime = data.mime;
|
||||
ref = data.ref;
|
||||
}
|
||||
// Get the blob from the image data.
|
||||
if (data.val) {
|
||||
const blob = base64ToBlob(data.val, data.mime);
|
||||
if (bits) {
|
||||
const blob = base64ToBlob(bits, mime);
|
||||
if (!blob) {
|
||||
throw new Error("Invalid image");
|
||||
}
|
||||
promise = Promise.resolve(blob);
|
||||
} else if (data.ref) {
|
||||
promise = fetch(this.authorizeURL(sanitizeUrlForMime(data.ref, 'image')))
|
||||
} else if (ref) {
|
||||
promise = fetch(this.authorizeURL(sanitizeUrlForMime(ref, 'image')))
|
||||
.then(evt => {
|
||||
if (evt.ok) {
|
||||
return evt.blob();
|
||||
@@ -427,23 +476,34 @@ function quoteImage(data) {
|
||||
// Scale the blob.
|
||||
return promise
|
||||
.then(blob => {
|
||||
// Cut the square from the center of the image and shrink it.
|
||||
return imageScaled(blob, IMAGE_THUMBNAIL_DIM, IMAGE_THUMBNAIL_DIM, -1, true)
|
||||
// If it's an image, cut the square from the center of the image and shrink it.
|
||||
// If it's a video, allow it to be rectantular.
|
||||
return imageScaled(blob, isVideo ? VIDEO_THUMBNAIL_WIDTH : IMAGE_THUMBNAIL_DIM, IMAGE_THUMBNAIL_DIM, -1, !isVideo)
|
||||
}).then(scaled => {
|
||||
data.mime = scaled.mime;
|
||||
if (isVideo) {
|
||||
data.premime = scaled.mime;
|
||||
} else {
|
||||
data.mime = scaled.mime;
|
||||
}
|
||||
data.size = scaled.blob.size;
|
||||
data.width = scaled.width;
|
||||
data.height = scaled.height;
|
||||
delete data.ref;
|
||||
delete data.preref;
|
||||
// Keeping the original file name, if provided: ex.data.name;
|
||||
|
||||
data.src = URL.createObjectURL(scaled.blob);
|
||||
return blobToBase64(scaled.blob);
|
||||
}).then(b64 => {
|
||||
data.val = b64.bits;
|
||||
if (isVideo) {
|
||||
data.preview = b64.bits;
|
||||
} else {
|
||||
data.val = b64.bits;
|
||||
}
|
||||
return data;
|
||||
}).catch(err => {
|
||||
delete data.val;
|
||||
delete data.preview;
|
||||
delete data.src;
|
||||
data.width = IMAGE_THUMBNAIL_DIM;
|
||||
data.height = IMAGE_THUMBNAIL_DIM;
|
||||
@@ -454,16 +514,18 @@ function quoteImage(data) {
|
||||
|
||||
// Create a preview of a reply.
|
||||
export function replyFormatter(style, data, values, key, stack) {
|
||||
if (style == 'IM') {
|
||||
const attr = inlineImageAttr.call(this, {key: key}, data);
|
||||
if (style == 'IM' || style == 'VD') {
|
||||
const isImage = style == 'IM';
|
||||
const attr = isImage ? inlineImageAttr.call(this, {key: key}, data) :
|
||||
inlineVideoAttr.call(this, {key: key}, data);
|
||||
|
||||
let loadedPromise;
|
||||
try {
|
||||
loadedPromise = cancelablePromise(quoteImage.call(this, data));
|
||||
loadedPromise = cancelablePromise(quoteImageOrVideo.call(this, data, style == 'VD'));
|
||||
} catch (error) {
|
||||
console.error("Failed to quote image", error);
|
||||
console.warn("Failed to quote image:", error.message);
|
||||
loadedPromise = cancelablePromise(error);
|
||||
}
|
||||
|
||||
attr.whenDone = loadedPromise;
|
||||
values = [React.createElement(LazyImage, attr, null), ' ', attr.alt];
|
||||
return React.createElement(React.Fragment, {key: key}, values);
|
||||
|
||||
+128
-17
@@ -9,7 +9,6 @@ import CallPanel from '../widgets/call-panel.jsx';
|
||||
import ChatMessage from '../widgets/chat-message.jsx';
|
||||
import ContactBadges from '../widgets/contact-badges.jsx';
|
||||
import DocPreview from '../widgets/doc-preview.jsx';
|
||||
import DragAndDrop from '../widgets/drag-and-drop.jsx'
|
||||
import ErrorPanel from '../widgets/error-panel.jsx';
|
||||
import GroupSubs from '../widgets/group-subs.jsx';
|
||||
import ImagePreview from '../widgets/image-preview.jsx';
|
||||
@@ -117,13 +116,14 @@ class MessagesView extends React.Component {
|
||||
this.sendKeyPress = this.sendKeyPress.bind(this);
|
||||
this.subscribe = this.subscribe.bind(this);
|
||||
this.handleScrollReference = this.handleScrollReference.bind(this);
|
||||
this.mountDnDEvents = this.mountDnDEvents.bind(this);
|
||||
this.handleScrollEvent = this.handleScrollEvent.bind(this);
|
||||
this.handleDescChange = this.handleDescChange.bind(this);
|
||||
this.handleSubsUpdated = this.handleSubsUpdated.bind(this);
|
||||
this.handleMessageUpdate = this.handleMessageUpdate.bind(this);
|
||||
this.handleAllMessagesReceived = this.handleAllMessagesReceived.bind(this);
|
||||
this.handleInfoReceipt = this.handleInfoReceipt.bind(this);
|
||||
this.handleImagePostview = this.handleImagePostview.bind(this);
|
||||
this.handleExpandMedia = this.handleExpandMedia.bind(this);
|
||||
this.handleClosePreview = this.handleClosePreview.bind(this);
|
||||
this.handleFormResponse = this.handleFormResponse.bind(this);
|
||||
this.handleContextClick = this.handleContextClick.bind(this);
|
||||
@@ -144,9 +144,22 @@ class MessagesView extends React.Component {
|
||||
this.handleQuoteClick = this.handleQuoteClick.bind(this);
|
||||
this.handleCallHangup = this.handleCallHangup.bind(this);
|
||||
|
||||
this.isDragEnabled = this.isDragEnabled.bind(this);
|
||||
this.handleDragIn = this.handleDragIn.bind(this);
|
||||
this.handleDragOut = this.handleDragOut.bind(this);
|
||||
this.handleDrag = this.handleDrag.bind(this);
|
||||
this.handleDrop = this.handleDrop.bind(this);
|
||||
|
||||
this.chatMessageRefs = {};
|
||||
this.getOrCreateMessageRef = this.getOrCreateMessageRef.bind(this);
|
||||
|
||||
// Keeps track of the drag event.
|
||||
// Need a counter b/c the browser's 'drag' events may fire multiple times
|
||||
// when the user takes the mouse pointer over the container:
|
||||
// for the component itself and for all nested/child elements.
|
||||
this.dragCounter = 0;
|
||||
this.dndRef = null;
|
||||
|
||||
this.readNotificationQueue = [];
|
||||
this.readNotificationTimer = null;
|
||||
|
||||
@@ -166,6 +179,14 @@ class MessagesView extends React.Component {
|
||||
if (this.messagesScroller) {
|
||||
this.messagesScroller.addEventListener('scroll', this.handleScrollEvent);
|
||||
}
|
||||
|
||||
// Drag and drop events
|
||||
if (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() {
|
||||
@@ -175,6 +196,14 @@ class MessagesView extends React.Component {
|
||||
|
||||
// Flush all notifications.
|
||||
this.clearNotificationQueue();
|
||||
|
||||
// Drag and drop events
|
||||
if (this.dndRef) {
|
||||
this.dndRef.removeEventListener('dragenter', this.handleDragIn);
|
||||
this.dndRef.removeEventListener('dragleave', this.handleDragOut);
|
||||
this.dndRef.removeEventListener('dragover', this.handleDrag);
|
||||
this.dndRef.removeEventListener('drop', this.handleDrop);
|
||||
}
|
||||
}
|
||||
|
||||
// Scroll last message into view on component update e.g. on message received
|
||||
@@ -245,6 +274,8 @@ class MessagesView extends React.Component {
|
||||
docPreview: null,
|
||||
imagePreview: null,
|
||||
imagePostview: null,
|
||||
videoPreview: null,
|
||||
videoPostview: null,
|
||||
rtcPanel: null,
|
||||
typingIndicator: false,
|
||||
scrollPosition: 0,
|
||||
@@ -253,22 +284,29 @@ class MessagesView extends React.Component {
|
||||
channel: false,
|
||||
reply: null,
|
||||
contentToEdit: null,
|
||||
showGoToLastButton: false
|
||||
showGoToLastButton: false,
|
||||
dragging: false
|
||||
};
|
||||
} else if (nextProps.topic != prevState.topic) {
|
||||
const topic = nextProps.tinode.getTopic(nextProps.topic);
|
||||
|
||||
nextState = {
|
||||
topic: nextProps.topic,
|
||||
deleted: topic._deleted,
|
||||
docPreview: null,
|
||||
imagePreview: null,
|
||||
imagePostview: null,
|
||||
videoPreview: null,
|
||||
videoPostview: null,
|
||||
rtcPanel: null,
|
||||
typingIndicator: false,
|
||||
scrollPosition: 0,
|
||||
fetchingMessages: false,
|
||||
showGoToLastButton: false,
|
||||
deleted: topic._deleted
|
||||
forwardMessage: null,
|
||||
reply: null,
|
||||
contentToEdit: null,
|
||||
dragging: false
|
||||
};
|
||||
|
||||
if (nextProps.forwardMessage) {
|
||||
@@ -505,6 +543,17 @@ class MessagesView extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
/* Mound draf and drop events */
|
||||
mountDnDEvents(dnd) {
|
||||
if (dnd) {
|
||||
dnd.addEventListener('dragenter', this.handleDragIn);
|
||||
dnd.addEventListener('dragleave', this.handleDragOut);
|
||||
dnd.addEventListener('dragover', this.handleDrag);
|
||||
dnd.addEventListener('drop', this.handleDrop);
|
||||
this.dndRef = dnd;
|
||||
}
|
||||
}
|
||||
|
||||
goToLatestMessage() {
|
||||
this.setState({scrollPosition: 0});
|
||||
if (this.messagesScroller) {
|
||||
@@ -681,8 +730,16 @@ class MessagesView extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
handleImagePostview(content) {
|
||||
this.setState({ imagePostview: content });
|
||||
handleExpandMedia(content) {
|
||||
if (!content) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (content.video) {
|
||||
this.setState({ videoPostview: content });
|
||||
} else {
|
||||
this.setState({ imagePostview: content });
|
||||
}
|
||||
}
|
||||
|
||||
handleClosePreview() {
|
||||
@@ -692,7 +749,7 @@ class MessagesView extends React.Component {
|
||||
if (this.state.videoPreview && this.state.videoPreview.url) {
|
||||
URL.revokeObjectURL(this.state.videoPreview.url);
|
||||
}
|
||||
this.setState({ imagePostview: null, imagePreview: null, docPreview: null, videoPreview: null });
|
||||
this.setState({ imagePostview: null, imagePreview: null, docPreview: null, videoPreview: null, videoPostview: null});
|
||||
}
|
||||
|
||||
handleFormResponse(action, text, data) {
|
||||
@@ -834,7 +891,12 @@ class MessagesView extends React.Component {
|
||||
} else {
|
||||
// Small enough to send inband.
|
||||
fileToBase64(file)
|
||||
.then(b64 => this.sendMessage(Drafty.attachFile(null, {mime: b64.mime, data: b64.bits, filename: b64.name})))
|
||||
.then(b64 => this.sendMessage(Drafty.attachFile(null, {
|
||||
mime: b64.mime,
|
||||
data: b64.bits,
|
||||
filename: b64.name,
|
||||
size: file.size
|
||||
})))
|
||||
.catch(err => this.props.onError(err.message, 'err'));
|
||||
}
|
||||
}
|
||||
@@ -872,7 +934,7 @@ class MessagesView extends React.Component {
|
||||
const mime = this.state.imagePreview.mime;
|
||||
const width = this.state.imagePreview.width;
|
||||
const height = this.state.imagePreview.height;
|
||||
const fname = this.state.imagePreview.name;
|
||||
const fname = this.state.imagePreview.filename;
|
||||
|
||||
// Server-provided limit reduced for base64 encoding and overhead.
|
||||
const maxInbandAttachmentSize = (this.props.tinode.getServerParam('maxMessageSize',
|
||||
@@ -958,7 +1020,7 @@ class MessagesView extends React.Component {
|
||||
|
||||
if (uploads.length == 0) {
|
||||
// Both video and preview are small enough to send inband.
|
||||
Promise.all(blobToBase64(videoBlob), blobToBase64(previewBlob))
|
||||
Promise.all([blobToBase64(videoBlob), blobToBase64(previewBlob)])
|
||||
.then(b64s => {
|
||||
const [v64, i64] = b64s;
|
||||
let msg = Drafty.insertVideo(null, 0, {
|
||||
@@ -1027,7 +1089,7 @@ class MessagesView extends React.Component {
|
||||
this.setState({videoPreview: {
|
||||
url: URL.createObjectURL(file),
|
||||
blob: file,
|
||||
name: file.name,
|
||||
filename: file.name,
|
||||
size: file.size,
|
||||
mime: file.type
|
||||
}});
|
||||
@@ -1040,7 +1102,7 @@ class MessagesView extends React.Component {
|
||||
this.setState({imagePreview: {
|
||||
url: URL.createObjectURL(scaled.blob),
|
||||
blob: scaled.blob,
|
||||
name: scaled.name,
|
||||
filename: scaled.name,
|
||||
width: scaled.width,
|
||||
height: scaled.height,
|
||||
size: scaled.blob.size,
|
||||
@@ -1188,6 +1250,44 @@ class MessagesView extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
isDragEnabled() {
|
||||
return this.state.isWriter && !this.state.unconfirmed && !this.props.forwardMessage && !this.state.peerMessagingDisabled;
|
||||
}
|
||||
|
||||
handleDrag(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
handleDragIn(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.dragCounter++;
|
||||
if (e.dataTransfer.items && e.dataTransfer.items.length > 0) {
|
||||
this.setState({dragging: true});
|
||||
}
|
||||
}
|
||||
|
||||
handleDragOut(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.dragCounter--;
|
||||
if (this.dragCounter <= 0) {
|
||||
this.setState({dragging: false});
|
||||
}
|
||||
}
|
||||
|
||||
handleDrop(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.setState({dragging: false});
|
||||
if (this.isDragEnabled() && e.dataTransfer.files && e.dataTransfer.files.length > 0) {
|
||||
this.handleFileDrop(e.dataTransfer.files);
|
||||
e.dataTransfer.clearData();
|
||||
this.dragCounter = 0;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {formatMessage} = this.props.intl;
|
||||
|
||||
@@ -1220,6 +1320,7 @@ class MessagesView extends React.Component {
|
||||
content={this.state.videoPreview}
|
||||
tinode={this.props.tinode}
|
||||
reply={this.state.reply}
|
||||
onError={this.props.onError}
|
||||
onCancelReply={this.handleCancelReply}
|
||||
onClose={this.handleClosePreview}
|
||||
onSendMessage={this.sendVideoAttachment} />
|
||||
@@ -1231,6 +1332,15 @@ class MessagesView extends React.Component {
|
||||
content={this.state.imagePostview}
|
||||
onClose={this.handleClosePreview} />
|
||||
);
|
||||
} else if (this.state.videoPostview) {
|
||||
// Play received video.
|
||||
component2 = (
|
||||
<VideoPreview
|
||||
content={this.state.videoPostview}
|
||||
tinode={this.props.tinode}
|
||||
onError={this.props.onError}
|
||||
onClose={this.handleClosePreview} />
|
||||
);
|
||||
} else if (this.state.docPreview) {
|
||||
// Preview attachment before sending.
|
||||
component2 = (
|
||||
@@ -1354,7 +1464,7 @@ class MessagesView extends React.Component {
|
||||
userIsWriter={this.state.isWriter}
|
||||
viewportWidth={this.props.viewportWidth} // Used by `formatter`.
|
||||
showContextMenu={this.handleShowMessageContextMenu}
|
||||
onImagePreview={this.handleImagePostview}
|
||||
onExpandMedia={this.handleExpandMedia}
|
||||
onFormResponse={this.handleFormResponse}
|
||||
onCancelUpload={this.handleCancelUpload}
|
||||
pickReply={this.handlePickReply}
|
||||
@@ -1486,13 +1596,14 @@ class MessagesView extends React.Component {
|
||||
onClearError={this.props.onError} />
|
||||
: null}
|
||||
<LoadSpinner show={this.state.fetchingMessages} />
|
||||
{!this.state.unconfirmed && !this.props.forwardMessage ?
|
||||
<DragAndDrop actionPrompt={this.props.intl.formatMessage(messages.drag_file)} onDrop={this.handleFileDrop} >{messagesComponent}</DragAndDrop>
|
||||
: messagesComponent}
|
||||
{messagesComponent}
|
||||
{this.state.dragging && this.isDragEnabled() ?
|
||||
<div className="drag-n-drop">{formatMessage(messages.drag_file)}</div>
|
||||
: null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
component = <div id="topic-view">{component2}</div>
|
||||
component = <div id="topic-view" ref={this.mountDnDEvents}>{component2}</div>
|
||||
}
|
||||
return component;
|
||||
}
|
||||
|
||||
+34
-24
@@ -299,11 +299,7 @@ class TinodeWeb extends React.Component {
|
||||
}).then(_ => {
|
||||
// Initialize desktop alerts.
|
||||
if (this.state.desktopAlertsEnabled) {
|
||||
this.initFCMessaging().then(_ => {
|
||||
if (this.state.desktopAlerts) {
|
||||
this.tinode.setDeviceToken(this.state.firebaseToken);
|
||||
}
|
||||
}).catch(_ => {
|
||||
this.initFCMessaging().catch(_ => {
|
||||
// do nothing: handled earlier.
|
||||
// catch needed to pervent unnecessary logging of error.
|
||||
});
|
||||
@@ -370,20 +366,27 @@ class TinodeWeb extends React.Component {
|
||||
const onError = (msg, err) => {
|
||||
console.error(msg, err);
|
||||
this.handleError(formatMessage(messages.push_init_failed), 'err');
|
||||
this.setState({desktopAlertsEnabled: false, firebaseToken: null});
|
||||
this.setState({firebaseToken: null});
|
||||
LocalStorageUtil.updateObject('settings', {desktopAlerts: false});
|
||||
}
|
||||
|
||||
try {
|
||||
this.fcm = firebaseGetMessaging(firebaseInitApp(FIREBASE_INIT, APP_NAME));
|
||||
return navigator.serviceWorker.register('/service-worker.js').then((reg) => {
|
||||
this.checkForAppUpdate(reg);
|
||||
reg.active.postMessage(JSON.stringify({locale: locale, version: PACKAGE_VERSION}));
|
||||
return reg;
|
||||
}).then((reg) => {
|
||||
return navigator.serviceWorker.getRegistration('/service-worker.js').then(reg => {
|
||||
if (reg) {
|
||||
return reg
|
||||
}
|
||||
return navigator.serviceWorker.register('/service-worker.js').then(reg => {
|
||||
this.checkForAppUpdate(reg);
|
||||
return reg;
|
||||
});
|
||||
}).then(reg => {
|
||||
// Pass locale and version config to the service worker.
|
||||
console.log("SW active:", reg.active, "installing:", reg.installing);
|
||||
(reg.active || reg.installing).postMessage(JSON.stringify({locale: locale, version: PACKAGE_VERSION}));
|
||||
// Request token.
|
||||
return TinodeWeb.requestFCMToken(this.fcm, reg);
|
||||
}).then((token) => {
|
||||
}).then(token => {
|
||||
const persist = LocalStorageUtil.getObject('keep-logged-in');
|
||||
if (token != this.state.firebaseToken) {
|
||||
this.tinode.setDeviceToken(token);
|
||||
@@ -416,17 +419,17 @@ class TinodeWeb extends React.Component {
|
||||
return firebaseGetToken(fcm, {
|
||||
serviceWorkerRegistration: sw,
|
||||
vapidKey: FIREBASE_INIT.messagingVapidKey
|
||||
}).then((token) => {
|
||||
}).then(token => {
|
||||
if (token) {
|
||||
return token;
|
||||
} else {
|
||||
// Try to request permissions.
|
||||
return Notification.requestPermission().then((permission) => {
|
||||
return Notification.requestPermission().then(permission => {
|
||||
if (permission === 'granted') {
|
||||
return firebaseGetToken(fcm, {
|
||||
serviceWorkerRegistration: reg,
|
||||
vapidKey: FIREBASE_INIT.messagingVapidKey
|
||||
}).then((token) => {
|
||||
}).then(token => {
|
||||
if (token) {
|
||||
return token;
|
||||
} else {
|
||||
@@ -576,6 +579,12 @@ class TinodeWeb extends React.Component {
|
||||
this.handleError(err.message, 'err');
|
||||
});
|
||||
}
|
||||
|
||||
if (this.state.desktopAlertsEnabled && !this.state.firebaseToken) {
|
||||
// Firefox and Safari: "The Notification permission may only be requested from inside a
|
||||
// short running user-generated event handler".
|
||||
this.initFCMessaging();
|
||||
}
|
||||
}
|
||||
|
||||
// Enable or disable saving the password and IndexedDB.
|
||||
@@ -1920,15 +1929,16 @@ class TinodeWeb extends React.Component {
|
||||
:
|
||||
null
|
||||
}
|
||||
<Alert
|
||||
visible={this.state.alertVisible}
|
||||
title={this.state.alertParams.title}
|
||||
content={this.state.alertParams.content}
|
||||
onReject={this.state.alertParams.onReject ? (_ => this.setState({alertVisible: false})) : null}
|
||||
reject={this.state.alertParams.reject}
|
||||
onConfirm={_ => {this.setState({alertVisible: false}); this.state.alertParams.onConfirm();}}
|
||||
confirm={this.state.alertParams.confirm}
|
||||
/>
|
||||
{this.state.alertVisible ?
|
||||
<Alert
|
||||
visible={this.state.alertVisible}
|
||||
title={this.state.alertParams.title}
|
||||
content={this.state.alertParams.content}
|
||||
onReject={this.state.alertParams.onReject ? (_ => this.setState({alertVisible: false})) : null}
|
||||
reject={this.state.alertParams.reject}
|
||||
onConfirm={_ => {this.setState({alertVisible: false}); this.state.alertParams.onConfirm();}}
|
||||
confirm={this.state.alertParams.confirm}
|
||||
/> : null}
|
||||
<SidepanelView
|
||||
tinode={this.tinode}
|
||||
connected={this.state.connected}
|
||||
|
||||
+15
-18
@@ -3,26 +3,23 @@ import { FormattedMessage } from 'react-intl';
|
||||
|
||||
export default class Alert extends React.PureComponent {
|
||||
render() {
|
||||
return this.props.visible ?
|
||||
<div className="alert-container">
|
||||
<div className="alert">
|
||||
<div className="title">{this.props.title}</div>
|
||||
<div className="content">{this.props.content}</div>
|
||||
<div className="dialog-buttons">
|
||||
{this.props.onReject ?
|
||||
<button className="outline" onClick={this.props.onReject}>
|
||||
{this.props.reject || <FormattedMessage id="button_cancel" defaultMessage="Cancel" description="Button [Cancel]" />}
|
||||
</button>
|
||||
:
|
||||
null
|
||||
}
|
||||
<button className="primary" onClick={this.props.onConfirm}>
|
||||
{this.props.confirm || <FormattedMessage id="button_ok" defaultMessage="OK" description="Button [OK]" />}
|
||||
<div className="alert-container">
|
||||
<div className="alert">
|
||||
<div className="title">{this.props.title}</div>
|
||||
<div className="content">{this.props.content}</div>
|
||||
<div className="dialog-buttons">
|
||||
{this.props.onReject ?
|
||||
<button className="outline" onClick={this.props.onReject}>
|
||||
{this.props.reject || <FormattedMessage id="button_cancel" defaultMessage="Cancel" description="Button [Cancel]" />}
|
||||
</button>
|
||||
</div>
|
||||
:
|
||||
null
|
||||
}
|
||||
<button className="primary" onClick={this.props.onConfirm}>
|
||||
{this.props.confirm || <FormattedMessage id="button_ok" defaultMessage="OK" description="Button [OK]" />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
:
|
||||
null;
|
||||
</div>
|
||||
}
|
||||
};
|
||||
|
||||
@@ -140,7 +140,7 @@ class CallPanel extends React.PureComponent {
|
||||
|
||||
start() {
|
||||
if (this.state.localStream) {
|
||||
this.props.onError(this.props.intl.formatMessage(messages.already_in_call));
|
||||
this.props.onError(this.props.intl.formatMessage(messages.already_in_call), 'info');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -28,6 +28,8 @@ export default class CallStatus extends React.PureComponent {
|
||||
duration = <FormattedMessage id="call_disconnected" defaultMessage="disconnected" description="Label for disconnected call" />;
|
||||
break;
|
||||
}
|
||||
} else if (this.props.callState == 'started' && !this.props.duration) {
|
||||
duration = <FormattedMessage id="call_in_progress" defaultMessage="in progress" description="Label for call in progress" />
|
||||
} else {
|
||||
duration = <span>{secondsToTime(this.props.duration / 1000)}</span>;
|
||||
}
|
||||
|
||||
@@ -23,7 +23,8 @@ class BaseChatMessage extends React.PureComponent {
|
||||
props.uploader.onProgress = this.handleProgress.bind(this);
|
||||
}
|
||||
|
||||
this.handleImagePreview = this.handleImagePreview.bind(this);
|
||||
this.handleExpandImage = this.handleExpandImage.bind(this);
|
||||
this.handlePlayVideo = this.handlePlayVideo.bind(this);
|
||||
this.handleFormButtonClick = this.handleFormButtonClick.bind(this);
|
||||
this.handleContextClick = this.handleContextClick.bind(this);
|
||||
this.handleCancelUpload = this.handleCancelUpload.bind(this);
|
||||
@@ -34,18 +35,18 @@ class BaseChatMessage extends React.PureComponent {
|
||||
viewportWidth: props.viewportWidth,
|
||||
authorizeURL: props.tinode.authorizeURL.bind(props.tinode),
|
||||
|
||||
onImagePreview: this.handleImagePreview,
|
||||
onVideoPreview: this.handleImagePreview,
|
||||
onImagePreview: this.handleExpandImage,
|
||||
onVideoPreview: this.handlePlayVideo,
|
||||
onFormButtonClick: this.handleFormButtonClick,
|
||||
onQuoteClick: this.handleQuoteClick
|
||||
};
|
||||
}
|
||||
|
||||
handleImagePreview(e) {
|
||||
handleExpandImage(e) {
|
||||
e.preventDefault();
|
||||
this.props.onImagePreview({
|
||||
this.props.onExpandMedia({
|
||||
url: e.target.src,
|
||||
filename: e.target.title,
|
||||
filename: e.target.dataset.name,
|
||||
width: e.target.dataset.width,
|
||||
height: e.target.dataset.height,
|
||||
size: e.target.dataset.size,
|
||||
@@ -53,6 +54,21 @@ class BaseChatMessage extends React.PureComponent {
|
||||
});
|
||||
}
|
||||
|
||||
handlePlayVideo(e) {
|
||||
e.preventDefault();
|
||||
this.props.onExpandMedia({
|
||||
video: true,
|
||||
url: e.target.dataset.src,
|
||||
preview: e.target.src,
|
||||
filename: e.target.dataset.name,
|
||||
width: e.target.dataset.width,
|
||||
height: e.target.dataset.height,
|
||||
duration: e.target.dataset.duration,
|
||||
size: e.target.dataset.size,
|
||||
type: e.target.dataset.mime
|
||||
});
|
||||
}
|
||||
|
||||
handleFormButtonClick(e) {
|
||||
e.preventDefault();
|
||||
const data = {
|
||||
@@ -82,9 +98,15 @@ class BaseChatMessage extends React.PureComponent {
|
||||
if (!this.props.response) {
|
||||
let immutable = false;
|
||||
Drafty.entities(this.props.content, (_0, _1, tp) => {
|
||||
immutable = ['AU', 'EX', 'FM', 'IM', 'QQ', 'VC', 'VD'].includes(tp);
|
||||
immutable = ['AU', 'EX', 'FM', 'IM', 'VC', 'VD'].includes(tp);
|
||||
return immutable;
|
||||
});
|
||||
if (!immutable) {
|
||||
Drafty.styles(this.props.content, tp => {
|
||||
immutable = ['QQ'].includes(tp);
|
||||
return immutable;
|
||||
});
|
||||
}
|
||||
if (!immutable) {
|
||||
menuItems.push('menu_item_edit');
|
||||
}
|
||||
|
||||
@@ -60,6 +60,7 @@ class ContactList extends React.Component {
|
||||
}
|
||||
|
||||
const isChannel = Tinode.isChannelTopicName(key);
|
||||
const isGroup = !isChannel && Tinode.isGroupTopicName(key);
|
||||
const selected = showCheckmark ?
|
||||
(this.props.topicSelected.indexOf(key) > -1) :
|
||||
(this.props.topicSelected === key);
|
||||
@@ -112,6 +113,7 @@ class ContactList extends React.Component {
|
||||
selected={selected}
|
||||
showOnline={this.props.showOnline && !isChannel}
|
||||
isChannel={isChannel}
|
||||
isGroup={isGroup}
|
||||
showContextMenu={this.props.showContextMenu}
|
||||
isVerified={c.trusted && c.trusted.verified}
|
||||
isStaff={c.trusted && c.trusted.staff}
|
||||
|
||||
@@ -105,6 +105,7 @@ class Contact extends React.Component {
|
||||
</div>
|
||||
<div className="text-box">
|
||||
<div><span className={titleClass}>{title}</span>
|
||||
{this.props.isGroup ? <i className="material-icons as-badge">group</i> : null}
|
||||
{this.props.isChannel ? <img src="/img/channel.png" className="channel" alt="channel" /> : null}
|
||||
<ContactBadges badges={icon_badges} />
|
||||
{!this.props.deleted ? <UnreadBadge count={this.props.unread} /> : null}
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
// Drag & drop container: displays a banner on drag and calls a handler on drop.
|
||||
import React from 'react';
|
||||
|
||||
export default class DragAndDrop extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
// A file is being dragged over the container.
|
||||
dragging: false
|
||||
};
|
||||
this.dropRef = React.createRef();
|
||||
// Keeps track of the drag event.
|
||||
// Need a counter b/c the browser's 'drag' events may fire multiple times
|
||||
// when the user takes the mouse pointer over the container:
|
||||
// for the component itself and for all nested/child elements.
|
||||
this.dragCounter = 0;
|
||||
|
||||
this.handleDrag = this.handleDrag.bind(this);
|
||||
this.handleDragIn = this.handleDragIn.bind(this);
|
||||
this.handleDragOut = this.handleDragOut.bind(this);
|
||||
this.handleDrop = this.handleDrop.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
let div = this.dropRef.current;
|
||||
div.addEventListener('dragenter', this.handleDragIn);
|
||||
div.addEventListener('dragleave', this.handleDragOut);
|
||||
div.addEventListener('dragover', this.handleDrag);
|
||||
div.addEventListener('drop', this.handleDrop);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
let div = this.dropRef.current;
|
||||
div.removeEventListener('dragenter', this.handleDragIn);
|
||||
div.removeEventListener('dragleave', this.handleDragOut);
|
||||
div.removeEventListener('dragover', this.handleDrag);
|
||||
div.removeEventListener('drop', this.handleDrop);
|
||||
}
|
||||
|
||||
handleDrag(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
handleDragIn(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.dragCounter++;
|
||||
if (e.dataTransfer.items && e.dataTransfer.items.length > 0) {
|
||||
this.setState({dragging: true});
|
||||
}
|
||||
}
|
||||
|
||||
handleDragOut(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.dragCounter--;
|
||||
if (this.dragCounter == 0) {
|
||||
this.setState({dragging: false});
|
||||
}
|
||||
}
|
||||
|
||||
handleDrop(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.setState({dragging: false});
|
||||
if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
|
||||
this.props.onDrop(e.dataTransfer.files);
|
||||
e.dataTransfer.clearData();
|
||||
this.dragCounter = 0;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className='drag-and-drop' ref={this.dropRef}>
|
||||
{this.state.dragging ?
|
||||
<div className='banner'>{this.props.actionPrompt}</div>
|
||||
: null}
|
||||
{this.props.children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -47,25 +47,25 @@ export default class ImagePreview extends React.PureComponent {
|
||||
|
||||
// Average font aspect ratio is ~0.5; File name takes 1/3 of the viewport width.
|
||||
const maxlength = Math.max(((this.state.width / REM_SIZE / 1.5) | 0) - 2, 12);
|
||||
const fname = shortenFileName(this.props.content.name, maxlength) || '-';
|
||||
const fname = shortenFileName(this.props.content.filename, maxlength) || '-';
|
||||
|
||||
const width = this.props.content.width || '-';
|
||||
const height = this.props.content.height || '-';
|
||||
return (
|
||||
<div id="image-preview">
|
||||
<div id="image-preview-caption-panel">
|
||||
{!this.props.onSendMessage ?
|
||||
<a href={this.props.content.url} download={true}>
|
||||
{this.props.onSendMessage ?
|
||||
<span>{fname}</span>
|
||||
:
|
||||
<a href={this.props.content.url} download={this.props.content.filename}>
|
||||
<i className="material-icons">file_download</i> <FormattedMessage
|
||||
id="download_action" defaultMessage="download" description="Call to action [download]" />
|
||||
</a>
|
||||
:
|
||||
<span>{fname}</span>
|
||||
}
|
||||
<a href="#" onClick={(e) => {e.preventDefault(); this.props.onClose();}}><i className="material-icons gray">close</i></a>
|
||||
</div>
|
||||
<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.name} />
|
||||
<img src={this.props.content.url} style={size} className="image-preview" alt={this.props.content.filename} />
|
||||
</div>
|
||||
{this.props.onSendMessage ?
|
||||
<SendMessage
|
||||
@@ -81,7 +81,7 @@ export default class ImagePreview extends React.PureComponent {
|
||||
<div>
|
||||
<div><b><FormattedMessage id="label_file_name" defaultMessage="File name:"
|
||||
description="Label for a file name" /></b></div>
|
||||
<div><span title={this.props.content.name}>{fname}</span></div>
|
||||
<div><span title={this.props.content.filename}>{fname}</span></div>
|
||||
</div>
|
||||
<div>
|
||||
<div><b><FormattedMessage id="label_content_type" defaultMessage="Content type:"
|
||||
|
||||
@@ -8,18 +8,30 @@ import { secondsToTime } from '../lib/strformat.js'
|
||||
export default class InlineVideo extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.videoRef = React.createRef();
|
||||
|
||||
this.handleClick = this.handleClick.bind(this);
|
||||
}
|
||||
|
||||
handleClick(e) {
|
||||
if (this.props.onClick) {
|
||||
// the click target has to be the image element.
|
||||
this.props.onClick(e);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const duration = secondsToTime(this.props['data-duration'] / 1000);
|
||||
const className = 'inline-video' + (this.props.onClick ? ' image-clickable' : '');
|
||||
return (
|
||||
<div className="inline-video">
|
||||
<div className={className}>
|
||||
{React.createElement('img', this.props)}
|
||||
{this.props.onClick ?
|
||||
<div className="play-control" onClick={this.props.onClick}>
|
||||
<div className="play-control">
|
||||
<i className="material-icons white bigger">play_arrow</i>
|
||||
</div>
|
||||
: null}
|
||||
: null}
|
||||
{duration ? <div className="duration">{duration}</div> : null}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -6,7 +6,7 @@ export default class LazyImage extends React.PureComponent {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
src: 'img/placeholder.png',
|
||||
src: this.props.isvideo ? 'img/blankvid.png' : 'img/blankimg.png',
|
||||
style: Object.assign({padding: '4px'}, this.props.style),
|
||||
className: this.props.className,
|
||||
alt: this.props.alt,
|
||||
@@ -19,7 +19,7 @@ export default class LazyImage extends React.PureComponent {
|
||||
this.props.whenDone
|
||||
.promise
|
||||
.then(data => this.setState({src: data.src, style: {...this.state.style, padding: 0}}))
|
||||
.catch(_ => this.setState({src: 'img/broken_image.png'}));
|
||||
.catch(_ => this.setState({src: this.props.isvideo ? 'img/broken_video.png' : 'img/broken_image.png'}));
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
@@ -28,11 +28,11 @@ export default class LazyImage extends React.PureComponent {
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (prevProps.whenDone != this.props.whenDone) {
|
||||
this.setState({src: 'img/placeholder.png', style: {...this.state.style, padding: '4px'}});
|
||||
this.setState({src: this.props.isvideo ? 'img/blankvid.png' : 'img/blankimg.png', style: {...this.state.style, padding: '4px'}});
|
||||
this.props.whenDone
|
||||
.promise
|
||||
.then(data => this.setState({src: data.src, style: {...this.state.style, padding: 0}}))
|
||||
.catch(_ => this.setState({src: 'img/broken_image.png'}));
|
||||
.catch(_ => this.setState({src: this.props.isvideo ? 'img/broken_video.png' : 'img/broken_image.png'}));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -162,7 +162,6 @@ class SendMessage extends React.PureComponent {
|
||||
|
||||
handleDropAttach(files) {
|
||||
if (files && files.length > 0) {
|
||||
console.log('Dropping ', files);
|
||||
this.props.onAttachFile(files[0]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,18 @@
|
||||
import React from 'react';
|
||||
import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
|
||||
import SendMessage from './send-message.jsx';
|
||||
|
||||
export default class VideoPreview extends React.PureComponent {
|
||||
import { bytesToHumanSize } from '../lib/strformat.js';
|
||||
|
||||
const messages = defineMessages({
|
||||
unrecognized_video_format: {
|
||||
id: 'unrecognized_video_format',
|
||||
defaultMessage: 'Format of this video is not recognized',
|
||||
description: 'Error message when uploaded video is invalid',
|
||||
}
|
||||
});
|
||||
|
||||
class VideoPreview extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
@@ -17,7 +28,12 @@ export default class VideoPreview extends React.PureComponent {
|
||||
height: this.videoRef.current.videoHeight,
|
||||
duration: (this.videoRef.current.duration * 1000) | 0,
|
||||
mime: this.props.content.mime,
|
||||
name: this.props.content.name
|
||||
name: this.props.content.filename
|
||||
}
|
||||
|
||||
if (params.width == 0 || params.height == 0) {
|
||||
this.props.onError(this.props.intl.formatMessage(messages.unrecognized_video_format), 'err');
|
||||
return;
|
||||
}
|
||||
|
||||
// Capture screen from a video.
|
||||
@@ -37,15 +53,29 @@ export default class VideoPreview extends React.PureComponent {
|
||||
return null;
|
||||
}
|
||||
|
||||
const width = this.props.content.width || '-';
|
||||
const height = this.props.content.height || '-';
|
||||
|
||||
const controlist = this.props.onSendMessage ? 'nodownload' : '';
|
||||
const autoPlay = !this.props.onSendMessage;
|
||||
|
||||
return (
|
||||
<div id="image-preview">
|
||||
<div id="image-preview-caption-panel">
|
||||
<span>{this.props.content.name}</span>
|
||||
<span>{this.props.content.filename}</span>
|
||||
<a href="#" onClick={e => {e.preventDefault(); this.props.onClose();}}><i className="material-icons gray">close</i></a>
|
||||
</div>
|
||||
<div id="image-preview-container">
|
||||
<video controls ref={this.videoRef} src={this.props.content.url} className="image-preview" alt={this.props.content.name} />
|
||||
<video
|
||||
className="image-preview"
|
||||
controls controlsList={controlist}
|
||||
disablePictureInPicture ref={this.videoRef}
|
||||
autoPlay={autoPlay}
|
||||
src={this.props.tinode.authorizeURL(this.props.content.url)}
|
||||
poster={this.props.content.preview}
|
||||
alt={this.props.content.filename} />
|
||||
</div>
|
||||
{this.props.onSendMessage ?
|
||||
<SendMessage
|
||||
messagePrompt="add_image_caption"
|
||||
acceptBlank={true}
|
||||
@@ -54,7 +84,27 @@ export default class VideoPreview extends React.PureComponent {
|
||||
onCancelReply={this.props.onCancelReply}
|
||||
onSendMessage={this.handleSendVideo}
|
||||
onError={this.props.onError} />
|
||||
:
|
||||
<div id="image-preview-footer">
|
||||
<div>
|
||||
<div><b><FormattedMessage id="label_file_name" defaultMessage="File name:"
|
||||
description="Label for a file name" /></b></div>
|
||||
<div><span title={this.props.content.filename}>{this.props.content.filename}</span></div>
|
||||
</div>
|
||||
<div>
|
||||
<div><b><FormattedMessage id="label_content_type" defaultMessage="Content type:"
|
||||
description="Label for file content type (mime)" /></b></div>
|
||||
<div>{this.props.content.type}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div><b><FormattedMessage id="label_size" defaultMessage="Size:"
|
||||
description="Label for file size" /></b></div>
|
||||
<div>{width} × {height} px; {bytesToHumanSize(this.props.content.size)}</div>
|
||||
</div>
|
||||
</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default injectIntl(VideoPreview);
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+599
-433
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
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
+54
-9
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user