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.
|
* End-to-End encryption.
|
||||||
* Emoji support is weak.
|
* Emoji support is weak.
|
||||||
* Mentions, hashtags.
|
* 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
|
## Other
|
||||||
|
|
||||||
|
|||||||
+20
-26
@@ -1011,7 +1011,8 @@ i.search {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.contact-box .contact-comment .material-icons {
|
.contact-box .contact-comment .material-icons {
|
||||||
margin-bottom: .2rem;
|
margin-bottom: 0.2rem;
|
||||||
|
color: #777;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Avatar and online-offline indicator */
|
/* Avatar and online-offline indicator */
|
||||||
@@ -1080,15 +1081,15 @@ i.search {
|
|||||||
}
|
}
|
||||||
|
|
||||||
img.channel {
|
img.channel {
|
||||||
width: 1rem;
|
width: 1.25rem;
|
||||||
height: 1rem;
|
height: 1.25rem;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
margin-left: 0.3rem;
|
margin-left: 0.3rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.large img.channel {
|
.large img.channel {
|
||||||
width: 1.35rem;
|
width: 1.45rem;
|
||||||
height: 1.35rem;
|
height: 1.45rem;
|
||||||
margin-left: 0.5rem;
|
margin-left: 0.5rem;
|
||||||
margin-bottom: 0.15rem;
|
margin-bottom: 0.15rem;
|
||||||
}
|
}
|
||||||
@@ -1990,7 +1991,6 @@ video {
|
|||||||
|
|
||||||
/* Inline and preview image styles */
|
/* Inline and preview image styles */
|
||||||
.inline-image, .image-preview {
|
.inline-image, .image-preview {
|
||||||
display: flex;
|
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border-radius: 0.1rem;
|
border-radius: 0.1rem;
|
||||||
@@ -2075,6 +2075,7 @@ img.image-preview::before {
|
|||||||
height: 4rem;
|
height: 4rem;
|
||||||
background-color: rgba(0, 0, 0, 0.4);
|
background-color: rgba(0, 0, 0, 0.4);
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
|
pointer-events: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2223,8 +2224,8 @@ ul.tabbar li.active > a {
|
|||||||
|
|
||||||
.material-icons.as-badge {
|
.material-icons.as-badge {
|
||||||
padding-left: 0.2rem;
|
padding-left: 0.2rem;
|
||||||
font-size: 1.25rem;
|
font-size: 1.15rem;
|
||||||
color: #bbb;
|
color: #aaa;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Badge with a label, like [you], [muted], etc */
|
/* 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;
|
display: flex;
|
||||||
flex-direction: column;
|
border: dashed grey 0.15rem;
|
||||||
|
border-radius: 1rem;
|
||||||
|
background-color: rgba(255,255,255,.95);
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 100%;
|
top: 1.5rem;
|
||||||
height: 100%;
|
bottom: 1.5rem;
|
||||||
}
|
left: 1.5rem;
|
||||||
|
right: 1.5rem;
|
||||||
.drag-and-drop .banner {
|
z-index: 3;
|
||||||
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;
|
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
font-size: 2rem;
|
font-size: 2rem;
|
||||||
color: grey;
|
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({
|
event.waitUntil(self.clients.matchAll({
|
||||||
type: 'window',
|
type: 'window',
|
||||||
includeUncontrolled: true
|
includeUncontrolled: true})
|
||||||
}).then(windowClients => {
|
.then(windowClients => {
|
||||||
let anyClient = null;
|
let anyClient = null;
|
||||||
for (let i = 0; i < windowClients.length; i++) {
|
for (let i = 0; i < windowClients.length; i++) {
|
||||||
const url = new URL(windowClients[i].url);
|
const url = new URL(windowClients[i].url);
|
||||||
if (url.hash.includes(data.topic)) {
|
if (url.hash.includes(data.topic)) {
|
||||||
// Found the Tinode tab with the right topic open.
|
// Found the Tinode tab with the right topic open.
|
||||||
return windowClients[i].focus();
|
return windowClients[i].focus();
|
||||||
} else {
|
} else {
|
||||||
// This will be the least recently used tab.
|
// This will be the least recently used tab.
|
||||||
anyClient = windowClients[i];
|
anyClient = windowClients[i];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Found tab with Tinode on a different topic,
|
// Found tab with Tinode on a different topic,
|
||||||
// navigate to the right topic.
|
// navigate to the right topic.
|
||||||
if (anyClient) {
|
if (anyClient) {
|
||||||
const url = new URL(anyClient.url);
|
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;
|
url.hash = urlHash;
|
||||||
return anyClient.focus().then(thisClient => {
|
return clients.openWindow(url);
|
||||||
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);
|
|
||||||
}));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// This is needed for 'Add to Home Screen'.
|
// 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
|
// Size of the broken_image shown in MessagesView
|
||||||
export const BROKEN_IMAGE_SIZE = 32;
|
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.
|
// Number of chat messages to fetch in one call.
|
||||||
export const MESSAGES_PAGE = 24;
|
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.
|
// Linear dimensions of image thumbnail: shrink image under this size for thumbnails in reply quote previews.
|
||||||
export const IMAGE_THUMBNAIL_DIM = 36;
|
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
|
// Maximum number of online users to be shown in a topic title bar. Others will be
|
||||||
// hidden under "+X more"
|
// 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": {
|
"drafty_attachment": {
|
||||||
"translation": "Dateianhang",
|
"translation": "Dateianhang",
|
||||||
"defaultMessage": "Attachment",
|
"defaultMessage": "Attachment",
|
||||||
"description": "Comment for attachment in drafty",
|
"description": "Comment for attachment in Drafty",
|
||||||
"missing": false,
|
"missing": false,
|
||||||
"obsolete": false
|
"obsolete": false
|
||||||
},
|
},
|
||||||
"drafty_form": {
|
"drafty_form": {
|
||||||
"translation": "Formular:",
|
"translation": "Formular:",
|
||||||
"defaultMessage": "Form:",
|
"defaultMessage": "Form:",
|
||||||
"description": "Comment for form in drafty",
|
"description": "Comment for form in Drafty",
|
||||||
"missing": false,
|
"missing": false,
|
||||||
"obsolete": false
|
"obsolete": false
|
||||||
},
|
},
|
||||||
"drafty_image": {
|
"drafty_image": {
|
||||||
"translation": "Bild",
|
"translation": "Bild",
|
||||||
"defaultMessage": "Picture",
|
"defaultMessage": "Picture",
|
||||||
"description": "Comment for embedded images in drafty",
|
"description": "Comment for embedded images in Drafty",
|
||||||
"missing": false,
|
"missing": false,
|
||||||
"obsolete": false
|
"obsolete": false
|
||||||
},
|
},
|
||||||
@@ -1523,10 +1523,31 @@
|
|||||||
"obsolete": false
|
"obsolete": false
|
||||||
},
|
},
|
||||||
"drag_file": {
|
"drag_file": {
|
||||||
"translation": "",
|
"translation": "Datei hierher ziehen",
|
||||||
"defaultMessage": "Drag file here",
|
"defaultMessage": "Drag file here",
|
||||||
"description": "Prompt on the file drag-n-drop overlay banner",
|
"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
|
"obsolete": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+24
-3
@@ -317,21 +317,21 @@
|
|||||||
"drafty_attachment": {
|
"drafty_attachment": {
|
||||||
"translation": "Attachment",
|
"translation": "Attachment",
|
||||||
"defaultMessage": "Attachment",
|
"defaultMessage": "Attachment",
|
||||||
"description": "Comment for attachment in drafty",
|
"description": "Comment for attachment in Drafty",
|
||||||
"missing": false,
|
"missing": false,
|
||||||
"obsolete": false
|
"obsolete": false
|
||||||
},
|
},
|
||||||
"drafty_form": {
|
"drafty_form": {
|
||||||
"translation": "Form:",
|
"translation": "Form:",
|
||||||
"defaultMessage": "Form:",
|
"defaultMessage": "Form:",
|
||||||
"description": "Comment for form in drafty",
|
"description": "Comment for form in Drafty",
|
||||||
"missing": false,
|
"missing": false,
|
||||||
"obsolete": false
|
"obsolete": false
|
||||||
},
|
},
|
||||||
"drafty_image": {
|
"drafty_image": {
|
||||||
"translation": "Picture",
|
"translation": "Picture",
|
||||||
"defaultMessage": "Picture",
|
"defaultMessage": "Picture",
|
||||||
"description": "Comment for embedded images in drafty",
|
"description": "Comment for embedded images in Drafty",
|
||||||
"missing": false,
|
"missing": false,
|
||||||
"obsolete": false
|
"obsolete": false
|
||||||
},
|
},
|
||||||
@@ -1528,5 +1528,26 @@
|
|||||||
"description": "Prompt on the file drag-n-drop overlay banner",
|
"description": "Prompt on the file drag-n-drop overlay banner",
|
||||||
"missing": false,
|
"missing": false,
|
||||||
"obsolete": 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": {
|
"drafty_attachment": {
|
||||||
"translation": "Archivo",
|
"translation": "Archivo",
|
||||||
"defaultMessage": "Attachment",
|
"defaultMessage": "Attachment",
|
||||||
"description": "Comment for attachment in drafty",
|
"description": "Comment for attachment in Drafty",
|
||||||
"missing": false,
|
"missing": false,
|
||||||
"obsolete": false
|
"obsolete": false
|
||||||
},
|
},
|
||||||
"drafty_form": {
|
"drafty_form": {
|
||||||
"translation": "Formulario:",
|
"translation": "Formulario:",
|
||||||
"defaultMessage": "Form:",
|
"defaultMessage": "Form:",
|
||||||
"description": "Comment for form in drafty",
|
"description": "Comment for form in Drafty",
|
||||||
"missing": false,
|
"missing": false,
|
||||||
"obsolete": false
|
"obsolete": false
|
||||||
},
|
},
|
||||||
"drafty_image": {
|
"drafty_image": {
|
||||||
"translation": "Imagen",
|
"translation": "Imagen",
|
||||||
"defaultMessage": "Picture",
|
"defaultMessage": "Picture",
|
||||||
"description": "Comment for embedded images in drafty",
|
"description": "Comment for embedded images in Drafty",
|
||||||
"missing": false,
|
"missing": false,
|
||||||
"obsolete": false
|
"obsolete": false
|
||||||
},
|
},
|
||||||
@@ -1523,10 +1523,31 @@
|
|||||||
"obsolete": false
|
"obsolete": false
|
||||||
},
|
},
|
||||||
"drag_file": {
|
"drag_file": {
|
||||||
"translation": "",
|
"translation": "Arrastra el archivo aquí",
|
||||||
"defaultMessage": "Drag file here",
|
"defaultMessage": "Drag file here",
|
||||||
"description": "Prompt on the file drag-n-drop overlay banner",
|
"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
|
"obsolete": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -135,6 +135,10 @@
|
|||||||
"defaultMessage": "disconnected",
|
"defaultMessage": "disconnected",
|
||||||
"description": "Label for disconnected call"
|
"description": "Label for disconnected call"
|
||||||
},
|
},
|
||||||
|
"call_in_progress": {
|
||||||
|
"defaultMessage": "in progress",
|
||||||
|
"description": "Label for call in progress"
|
||||||
|
},
|
||||||
"call_missed": {
|
"call_missed": {
|
||||||
"defaultMessage": "missed",
|
"defaultMessage": "missed",
|
||||||
"description": "Label for missed incoming call"
|
"description": "Label for missed incoming call"
|
||||||
@@ -225,20 +229,24 @@
|
|||||||
},
|
},
|
||||||
"drafty_attachment": {
|
"drafty_attachment": {
|
||||||
"defaultMessage": "Attachment",
|
"defaultMessage": "Attachment",
|
||||||
"description": "Comment for attachment in drafty"
|
"description": "Comment for attachment in Drafty"
|
||||||
},
|
},
|
||||||
"drafty_form": {
|
"drafty_form": {
|
||||||
"defaultMessage": "Form:",
|
"defaultMessage": "Form:",
|
||||||
"description": "Comment for form in drafty"
|
"description": "Comment for form in Drafty"
|
||||||
},
|
},
|
||||||
"drafty_image": {
|
"drafty_image": {
|
||||||
"defaultMessage": "Picture",
|
"defaultMessage": "Picture",
|
||||||
"description": "Comment for embedded images in drafty"
|
"description": "Comment for embedded images in Drafty"
|
||||||
},
|
},
|
||||||
"drafty_unknown": {
|
"drafty_unknown": {
|
||||||
"defaultMessage": "Unsupported",
|
"defaultMessage": "Unsupported",
|
||||||
"description": "Unsupported entity in drafty"
|
"description": "Unsupported entity in drafty"
|
||||||
},
|
},
|
||||||
|
"drafty_video": {
|
||||||
|
"defaultMessage": "Video recording",
|
||||||
|
"description": "Comment for videos embedded in Drafty"
|
||||||
|
},
|
||||||
"drag_file": {
|
"drag_file": {
|
||||||
"defaultMessage": "Drag file here",
|
"defaultMessage": "Drag file here",
|
||||||
"description": "Prompt on the file drag-n-drop overlay banner"
|
"description": "Prompt on the file drag-n-drop overlay banner"
|
||||||
@@ -856,6 +864,10 @@
|
|||||||
"defaultMessage": "Unnamed",
|
"defaultMessage": "Unnamed",
|
||||||
"description": "Title shown when the topic has no name"
|
"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": {
|
"update_available": {
|
||||||
"defaultMessage": "Update available.",
|
"defaultMessage": "Update available.",
|
||||||
"description": "Message shown when an app update is available."
|
"description": "Message shown when an app update is available."
|
||||||
|
|||||||
+26
-5
@@ -317,21 +317,21 @@
|
|||||||
"drafty_attachment": {
|
"drafty_attachment": {
|
||||||
"translation": "Pièce(s) jointe(s)",
|
"translation": "Pièce(s) jointe(s)",
|
||||||
"defaultMessage": "Attachment",
|
"defaultMessage": "Attachment",
|
||||||
"description": "Comment for attachment in drafty",
|
"description": "Comment for attachment in Drafty",
|
||||||
"missing": false,
|
"missing": false,
|
||||||
"obsolete": false
|
"obsolete": false
|
||||||
},
|
},
|
||||||
"drafty_form": {
|
"drafty_form": {
|
||||||
"translation": "Formulaire:",
|
"translation": "Formulaire:",
|
||||||
"defaultMessage": "Form:",
|
"defaultMessage": "Form:",
|
||||||
"description": "Comment for form in drafty",
|
"description": "Comment for form in Drafty",
|
||||||
"missing": false,
|
"missing": false,
|
||||||
"obsolete": false
|
"obsolete": false
|
||||||
},
|
},
|
||||||
"drafty_image": {
|
"drafty_image": {
|
||||||
"translation": "Image",
|
"translation": "Image",
|
||||||
"defaultMessage": "Picture",
|
"defaultMessage": "Picture",
|
||||||
"description": "Comment for embedded images in drafty",
|
"description": "Comment for embedded images in Drafty",
|
||||||
"missing": false,
|
"missing": false,
|
||||||
"obsolete": false
|
"obsolete": false
|
||||||
},
|
},
|
||||||
@@ -1523,10 +1523,31 @@
|
|||||||
"obsolete": false
|
"obsolete": false
|
||||||
},
|
},
|
||||||
"drag_file": {
|
"drag_file": {
|
||||||
"translation": "",
|
"translation": "Faites glisser le fichier ici",
|
||||||
"defaultMessage": "Drag file here",
|
"defaultMessage": "Drag file here",
|
||||||
"description": "Prompt on the file drag-n-drop overlay banner",
|
"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
|
"obsolete": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+26
-5
@@ -317,21 +317,21 @@
|
|||||||
"drafty_attachment": {
|
"drafty_attachment": {
|
||||||
"translation": "부착",
|
"translation": "부착",
|
||||||
"defaultMessage": "Attachment",
|
"defaultMessage": "Attachment",
|
||||||
"description": "Comment for attachment in drafty",
|
"description": "Comment for attachment in Drafty",
|
||||||
"missing": false,
|
"missing": false,
|
||||||
"obsolete": false
|
"obsolete": false
|
||||||
},
|
},
|
||||||
"drafty_form": {
|
"drafty_form": {
|
||||||
"translation": "형태:",
|
"translation": "형태:",
|
||||||
"defaultMessage": "Form:",
|
"defaultMessage": "Form:",
|
||||||
"description": "Comment for form in drafty",
|
"description": "Comment for form in Drafty",
|
||||||
"missing": false,
|
"missing": false,
|
||||||
"obsolete": false
|
"obsolete": false
|
||||||
},
|
},
|
||||||
"drafty_image": {
|
"drafty_image": {
|
||||||
"translation": "이미지",
|
"translation": "이미지",
|
||||||
"defaultMessage": "Picture",
|
"defaultMessage": "Picture",
|
||||||
"description": "Comment for embedded images in drafty",
|
"description": "Comment for embedded images in Drafty",
|
||||||
"missing": false,
|
"missing": false,
|
||||||
"obsolete": false
|
"obsolete": false
|
||||||
},
|
},
|
||||||
@@ -1523,10 +1523,31 @@
|
|||||||
"obsolete": false
|
"obsolete": false
|
||||||
},
|
},
|
||||||
"drag_file": {
|
"drag_file": {
|
||||||
"translation": "",
|
"translation": "여기로 파일 드래그",
|
||||||
"defaultMessage": "Drag file here",
|
"defaultMessage": "Drag file here",
|
||||||
"description": "Prompt on the file drag-n-drop overlay banner",
|
"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
|
"obsolete": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+26
-5
@@ -317,21 +317,21 @@
|
|||||||
"drafty_attachment": {
|
"drafty_attachment": {
|
||||||
"translation": "Atasament",
|
"translation": "Atasament",
|
||||||
"defaultMessage": "Attachment",
|
"defaultMessage": "Attachment",
|
||||||
"description": "Comment for attachment in drafty",
|
"description": "Comment for attachment in Drafty",
|
||||||
"missing": false,
|
"missing": false,
|
||||||
"obsolete": false
|
"obsolete": false
|
||||||
},
|
},
|
||||||
"drafty_form": {
|
"drafty_form": {
|
||||||
"translation": "Formă:",
|
"translation": "Formă:",
|
||||||
"defaultMessage": "Form:",
|
"defaultMessage": "Form:",
|
||||||
"description": "Comment for form in drafty",
|
"description": "Comment for form in Drafty",
|
||||||
"missing": false,
|
"missing": false,
|
||||||
"obsolete": false
|
"obsolete": false
|
||||||
},
|
},
|
||||||
"drafty_image": {
|
"drafty_image": {
|
||||||
"translation": "Imagine",
|
"translation": "Imagine",
|
||||||
"defaultMessage": "Picture",
|
"defaultMessage": "Picture",
|
||||||
"description": "Comment for embedded images in drafty",
|
"description": "Comment for embedded images in Drafty",
|
||||||
"missing": false,
|
"missing": false,
|
||||||
"obsolete": false
|
"obsolete": false
|
||||||
},
|
},
|
||||||
@@ -1523,10 +1523,31 @@
|
|||||||
"obsolete": false
|
"obsolete": false
|
||||||
},
|
},
|
||||||
"drag_file": {
|
"drag_file": {
|
||||||
"translation": "",
|
"translation": "Trageți fișierul aici",
|
||||||
"defaultMessage": "Drag file here",
|
"defaultMessage": "Drag file here",
|
||||||
"description": "Prompt on the file drag-n-drop overlay banner",
|
"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
|
"obsolete": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+27
-6
@@ -317,21 +317,21 @@
|
|||||||
"drafty_attachment": {
|
"drafty_attachment": {
|
||||||
"translation": "Аттачмент",
|
"translation": "Аттачмент",
|
||||||
"defaultMessage": "Attachment",
|
"defaultMessage": "Attachment",
|
||||||
"description": "Comment for attachment in drafty",
|
"description": "Comment for attachment in Drafty",
|
||||||
"missing": false,
|
"missing": false,
|
||||||
"obsolete": false
|
"obsolete": false
|
||||||
},
|
},
|
||||||
"drafty_form": {
|
"drafty_form": {
|
||||||
"translation": "Форма:",
|
"translation": "Форма:",
|
||||||
"defaultMessage": "Form:",
|
"defaultMessage": "Form:",
|
||||||
"description": "Comment for form in drafty",
|
"description": "Comment for form in Drafty",
|
||||||
"missing": false,
|
"missing": false,
|
||||||
"obsolete": false
|
"obsolete": false
|
||||||
},
|
},
|
||||||
"drafty_image": {
|
"drafty_image": {
|
||||||
"translation": "Картинка",
|
"translation": "Картинка",
|
||||||
"defaultMessage": "Picture",
|
"defaultMessage": "Picture",
|
||||||
"description": "Comment for embedded images in drafty",
|
"description": "Comment for embedded images in Drafty",
|
||||||
"missing": false,
|
"missing": false,
|
||||||
"obsolete": false
|
"obsolete": false
|
||||||
},
|
},
|
||||||
@@ -1118,7 +1118,7 @@
|
|||||||
"obsolete": false
|
"obsolete": false
|
||||||
},
|
},
|
||||||
"tags_not_found": {
|
"tags_not_found": {
|
||||||
"translation": "Тегов нет. Добавьте",
|
"translation": "Тегов нет. Добавьте.",
|
||||||
"defaultMessage": "No tags defined. Add some.",
|
"defaultMessage": "No tags defined. Add some.",
|
||||||
"description": "",
|
"description": "",
|
||||||
"missing": false,
|
"missing": false,
|
||||||
@@ -1523,10 +1523,31 @@
|
|||||||
"obsolete": false
|
"obsolete": false
|
||||||
},
|
},
|
||||||
"drag_file": {
|
"drag_file": {
|
||||||
"translation": "",
|
"translation": "Перетащите файл сюда",
|
||||||
"defaultMessage": "Drag file here",
|
"defaultMessage": "Drag file here",
|
||||||
"description": "Prompt on the file drag-n-drop overlay banner",
|
"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
|
"obsolete": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+26
-5
@@ -317,21 +317,21 @@
|
|||||||
"drafty_attachment": {
|
"drafty_attachment": {
|
||||||
"translation": "附加檔案",
|
"translation": "附加檔案",
|
||||||
"defaultMessage": "Attachment",
|
"defaultMessage": "Attachment",
|
||||||
"description": "Comment for attachment in drafty",
|
"description": "Comment for attachment in Drafty",
|
||||||
"missing": false,
|
"missing": false,
|
||||||
"obsolete": false
|
"obsolete": false
|
||||||
},
|
},
|
||||||
"drafty_form": {
|
"drafty_form": {
|
||||||
"translation": "表單:",
|
"translation": "表單:",
|
||||||
"defaultMessage": "Form:",
|
"defaultMessage": "Form:",
|
||||||
"description": "Comment for form in drafty",
|
"description": "Comment for form in Drafty",
|
||||||
"missing": false,
|
"missing": false,
|
||||||
"obsolete": false
|
"obsolete": false
|
||||||
},
|
},
|
||||||
"drafty_image": {
|
"drafty_image": {
|
||||||
"translation": "圖片",
|
"translation": "圖片",
|
||||||
"defaultMessage": "Picture",
|
"defaultMessage": "Picture",
|
||||||
"description": "Comment for embedded images in drafty",
|
"description": "Comment for embedded images in Drafty",
|
||||||
"missing": false,
|
"missing": false,
|
||||||
"obsolete": false
|
"obsolete": false
|
||||||
},
|
},
|
||||||
@@ -1523,10 +1523,31 @@
|
|||||||
"obsolete": false
|
"obsolete": false
|
||||||
},
|
},
|
||||||
"drag_file": {
|
"drag_file": {
|
||||||
"translation": "",
|
"translation": "將文件拖到這裡",
|
||||||
"defaultMessage": "Drag file here",
|
"defaultMessage": "Drag file here",
|
||||||
"description": "Prompt on the file drag-n-drop overlay banner",
|
"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
|
"obsolete": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+26
-5
@@ -317,21 +317,21 @@
|
|||||||
"drafty_attachment": {
|
"drafty_attachment": {
|
||||||
"translation": "附件",
|
"translation": "附件",
|
||||||
"defaultMessage": "Attachment",
|
"defaultMessage": "Attachment",
|
||||||
"description": "Comment for attachment in drafty",
|
"description": "Comment for attachment in Drafty",
|
||||||
"missing": false,
|
"missing": false,
|
||||||
"obsolete": false
|
"obsolete": false
|
||||||
},
|
},
|
||||||
"drafty_form": {
|
"drafty_form": {
|
||||||
"translation": "形式:",
|
"translation": "形式:",
|
||||||
"defaultMessage": "Form:",
|
"defaultMessage": "Form:",
|
||||||
"description": "Comment for form in drafty",
|
"description": "Comment for form in Drafty",
|
||||||
"missing": false,
|
"missing": false,
|
||||||
"obsolete": false
|
"obsolete": false
|
||||||
},
|
},
|
||||||
"drafty_image": {
|
"drafty_image": {
|
||||||
"translation": "图像",
|
"translation": "图像",
|
||||||
"defaultMessage": "Picture",
|
"defaultMessage": "Picture",
|
||||||
"description": "Comment for embedded images in drafty",
|
"description": "Comment for embedded images in Drafty",
|
||||||
"missing": false,
|
"missing": false,
|
||||||
"obsolete": false
|
"obsolete": false
|
||||||
},
|
},
|
||||||
@@ -1523,10 +1523,31 @@
|
|||||||
"obsolete": false
|
"obsolete": false
|
||||||
},
|
},
|
||||||
"drag_file": {
|
"drag_file": {
|
||||||
"translation": "",
|
"translation": "将文件拖到这里",
|
||||||
"defaultMessage": "Drag file here",
|
"defaultMessage": "Drag file here",
|
||||||
"description": "Prompt on the file drag-n-drop overlay banner",
|
"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
|
"obsolete": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+84
-22
@@ -10,7 +10,7 @@ import InlineVideo from '../widgets/inline-video.jsx';
|
|||||||
import LazyImage from '../widgets/lazy-image.jsx'
|
import LazyImage from '../widgets/lazy-image.jsx'
|
||||||
import UploadingImage from '../widgets/uploading-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 { base64ToBlob, blobToBase64, fitImageSize, imageScaled } from './blob-helpers.js';
|
||||||
import { idToColorClass, secondsToTime, shortenFileName } from './strformat.js';
|
import { idToColorClass, secondsToTime, shortenFileName } from './strformat.js';
|
||||||
import { cancelablePromise, sanitizeUrlForMime } from './utils.js';
|
import { cancelablePromise, sanitizeUrlForMime } from './utils.js';
|
||||||
@@ -19,17 +19,22 @@ const messages = defineMessages({
|
|||||||
drafty_form: {
|
drafty_form: {
|
||||||
id: 'drafty_form',
|
id: 'drafty_form',
|
||||||
defaultMessage: 'Form: ',
|
defaultMessage: 'Form: ',
|
||||||
description: 'Comment for form in drafty'
|
description: 'Comment for form in Drafty'
|
||||||
},
|
},
|
||||||
drafty_attachment: {
|
drafty_attachment: {
|
||||||
id: 'drafty_attachment',
|
id: 'drafty_attachment',
|
||||||
defaultMessage: 'Attachment',
|
defaultMessage: 'Attachment',
|
||||||
description: 'Comment for attachment in drafty'
|
description: 'Comment for attachment in Drafty'
|
||||||
},
|
},
|
||||||
drafty_image: {
|
drafty_image: {
|
||||||
id: 'drafty_image',
|
id: 'drafty_image',
|
||||||
defaultMessage: 'Picture',
|
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: {
|
drafty_unknown: {
|
||||||
id: 'drafty_unknown',
|
id: 'drafty_unknown',
|
||||||
@@ -213,12 +218,11 @@ function handleVideoData(el, data, attr) {
|
|||||||
};
|
};
|
||||||
return el;
|
return el;
|
||||||
}
|
}
|
||||||
|
|
||||||
attr.className = 'inline-image';
|
attr.className = 'inline-image';
|
||||||
const dim = fitImageSize(data.width, data.height,
|
const dim = fitImageSize(data.width, data.height,
|
||||||
this.viewportWidth > 0 ? Math.min(this.viewportWidth - REM_SIZE * 6.5, REM_SIZE * 34.5) :
|
this.viewportWidth > 0 ? Math.min(this.viewportWidth - REM_SIZE * 6.5, REM_SIZE * 34.5) :
|
||||||
REM_SIZE * 34.5, REM_SIZE * 24, false) ||
|
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 = {
|
attr.style = {
|
||||||
width: dim.dstWidth + 'px',
|
width: dim.dstWidth + 'px',
|
||||||
height: dim.dstHeight + 'px',
|
height: dim.dstHeight + 'px',
|
||||||
@@ -227,6 +231,7 @@ function handleVideoData(el, data, attr) {
|
|||||||
minHeight: dim.dstHeight + 'px'
|
minHeight: dim.dstHeight + 'px'
|
||||||
};
|
};
|
||||||
if (!Drafty.isProcessing(data)) {
|
if (!Drafty.isProcessing(data)) {
|
||||||
|
attr.src = this.authorizeURL(sanitizeUrlForMime(attr.src, 'image'));
|
||||||
attr.alt = data.name;
|
attr.alt = data.name;
|
||||||
if (data.ref || data.val) {
|
if (data.ref || data.val) {
|
||||||
attr.onClick = this.onVideoPreview;
|
attr.onClick = this.onVideoPreview;
|
||||||
@@ -319,6 +324,11 @@ export function previewFormatter(style, data, values, key) {
|
|||||||
el = null;
|
el = null;
|
||||||
values = null;
|
values = null;
|
||||||
break;
|
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:
|
default:
|
||||||
if (!el) {
|
if (!el) {
|
||||||
// Unknown element.
|
// Unknown element.
|
||||||
@@ -333,7 +343,7 @@ export function previewFormatter(style, data, values, key) {
|
|||||||
return React.createElement(el, attr, values);
|
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) {
|
function inlineImageAttr(attr, data) {
|
||||||
attr.style = {
|
attr.style = {
|
||||||
width: IMAGE_THUMBNAIL_DIM + 'px',
|
width: IMAGE_THUMBNAIL_DIM + 'px',
|
||||||
@@ -345,11 +355,33 @@ function inlineImageAttr(attr, data) {
|
|||||||
attr.alt = this.formatMessage(messages.drafty_image);
|
attr.alt = this.formatMessage(messages.drafty_image);
|
||||||
if (!data) {
|
if (!data) {
|
||||||
attr.src = 'img/broken_image.png';
|
attr.src = 'img/broken_image.png';
|
||||||
|
} else {
|
||||||
|
attr.src = attr.src || 'img/broken_image.png';
|
||||||
}
|
}
|
||||||
attr.title = attr.alt;
|
attr.title = attr.alt;
|
||||||
return attr;
|
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.
|
// Displays a portion of Drafty within 'QQ' quotes. 'this' is set by the caller.
|
||||||
// 'this' must contain:
|
// 'this' must contain:
|
||||||
// formatMessage: this.props.intl.formatMessage
|
// formatMessage: this.props.intl.formatMessage
|
||||||
@@ -357,7 +389,7 @@ function inlineImageAttr(attr, data) {
|
|||||||
// authorizeURL: this.props.tinode.authorizeURL
|
// authorizeURL: this.props.tinode.authorizeURL
|
||||||
// onQuoteClick: this.handleQuoteClick (optional)
|
// onQuoteClick: this.handleQuoteClick (optional)
|
||||||
function quoteFormatter(style, data, values, key) {
|
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 el = Drafty.tagName(style);
|
||||||
let attr = Drafty.attrValue(style, data) || {};
|
let attr = Drafty.attrValue(style, data) || {};
|
||||||
attr.key = key;
|
attr.key = key;
|
||||||
@@ -372,6 +404,13 @@ function quoteFormatter(style, data, values, key) {
|
|||||||
// Fragment attributes.
|
// Fragment attributes.
|
||||||
attr = {key: key};
|
attr = {key: key};
|
||||||
break;
|
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':
|
case 'MN':
|
||||||
el = 'span';
|
el = 'span';
|
||||||
attr.className = 'mention'
|
attr.className = 'mention'
|
||||||
@@ -402,17 +441,27 @@ function quoteFormatter(style, data, values, key) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create image thumbnail suitable for inclusion in a quote.
|
// Create image thumbnail suitable for inclusion in a quote.
|
||||||
function quoteImage(data) {
|
function quoteImageOrVideo(data, isVideo) {
|
||||||
let promise;
|
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.
|
// Get the blob from the image data.
|
||||||
if (data.val) {
|
if (bits) {
|
||||||
const blob = base64ToBlob(data.val, data.mime);
|
const blob = base64ToBlob(bits, mime);
|
||||||
if (!blob) {
|
if (!blob) {
|
||||||
throw new Error("Invalid image");
|
throw new Error("Invalid image");
|
||||||
}
|
}
|
||||||
promise = Promise.resolve(blob);
|
promise = Promise.resolve(blob);
|
||||||
} else if (data.ref) {
|
} else if (ref) {
|
||||||
promise = fetch(this.authorizeURL(sanitizeUrlForMime(data.ref, 'image')))
|
promise = fetch(this.authorizeURL(sanitizeUrlForMime(ref, 'image')))
|
||||||
.then(evt => {
|
.then(evt => {
|
||||||
if (evt.ok) {
|
if (evt.ok) {
|
||||||
return evt.blob();
|
return evt.blob();
|
||||||
@@ -427,23 +476,34 @@ function quoteImage(data) {
|
|||||||
// Scale the blob.
|
// Scale the blob.
|
||||||
return promise
|
return promise
|
||||||
.then(blob => {
|
.then(blob => {
|
||||||
// Cut the square from the center of the image and shrink it.
|
// If it's an image, 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 a video, allow it to be rectantular.
|
||||||
|
return imageScaled(blob, isVideo ? VIDEO_THUMBNAIL_WIDTH : IMAGE_THUMBNAIL_DIM, IMAGE_THUMBNAIL_DIM, -1, !isVideo)
|
||||||
}).then(scaled => {
|
}).then(scaled => {
|
||||||
data.mime = scaled.mime;
|
if (isVideo) {
|
||||||
|
data.premime = scaled.mime;
|
||||||
|
} else {
|
||||||
|
data.mime = scaled.mime;
|
||||||
|
}
|
||||||
data.size = scaled.blob.size;
|
data.size = scaled.blob.size;
|
||||||
data.width = scaled.width;
|
data.width = scaled.width;
|
||||||
data.height = scaled.height;
|
data.height = scaled.height;
|
||||||
delete data.ref;
|
delete data.ref;
|
||||||
|
delete data.preref;
|
||||||
// Keeping the original file name, if provided: ex.data.name;
|
// Keeping the original file name, if provided: ex.data.name;
|
||||||
|
|
||||||
data.src = URL.createObjectURL(scaled.blob);
|
data.src = URL.createObjectURL(scaled.blob);
|
||||||
return blobToBase64(scaled.blob);
|
return blobToBase64(scaled.blob);
|
||||||
}).then(b64 => {
|
}).then(b64 => {
|
||||||
data.val = b64.bits;
|
if (isVideo) {
|
||||||
|
data.preview = b64.bits;
|
||||||
|
} else {
|
||||||
|
data.val = b64.bits;
|
||||||
|
}
|
||||||
return data;
|
return data;
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
delete data.val;
|
delete data.val;
|
||||||
|
delete data.preview;
|
||||||
delete data.src;
|
delete data.src;
|
||||||
data.width = IMAGE_THUMBNAIL_DIM;
|
data.width = IMAGE_THUMBNAIL_DIM;
|
||||||
data.height = IMAGE_THUMBNAIL_DIM;
|
data.height = IMAGE_THUMBNAIL_DIM;
|
||||||
@@ -454,16 +514,18 @@ function quoteImage(data) {
|
|||||||
|
|
||||||
// Create a preview of a reply.
|
// Create a preview of a reply.
|
||||||
export function replyFormatter(style, data, values, key, stack) {
|
export function replyFormatter(style, data, values, key, stack) {
|
||||||
if (style == 'IM') {
|
if (style == 'IM' || style == 'VD') {
|
||||||
const attr = inlineImageAttr.call(this, {key: key}, data);
|
const isImage = style == 'IM';
|
||||||
|
const attr = isImage ? inlineImageAttr.call(this, {key: key}, data) :
|
||||||
|
inlineVideoAttr.call(this, {key: key}, data);
|
||||||
|
|
||||||
let loadedPromise;
|
let loadedPromise;
|
||||||
try {
|
try {
|
||||||
loadedPromise = cancelablePromise(quoteImage.call(this, data));
|
loadedPromise = cancelablePromise(quoteImageOrVideo.call(this, data, style == 'VD'));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to quote image", error);
|
console.warn("Failed to quote image:", error.message);
|
||||||
loadedPromise = cancelablePromise(error);
|
loadedPromise = cancelablePromise(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
attr.whenDone = loadedPromise;
|
attr.whenDone = loadedPromise;
|
||||||
values = [React.createElement(LazyImage, attr, null), ' ', attr.alt];
|
values = [React.createElement(LazyImage, attr, null), ' ', attr.alt];
|
||||||
return React.createElement(React.Fragment, {key: key}, values);
|
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 ChatMessage from '../widgets/chat-message.jsx';
|
||||||
import ContactBadges from '../widgets/contact-badges.jsx';
|
import ContactBadges from '../widgets/contact-badges.jsx';
|
||||||
import DocPreview from '../widgets/doc-preview.jsx';
|
import DocPreview from '../widgets/doc-preview.jsx';
|
||||||
import DragAndDrop from '../widgets/drag-and-drop.jsx'
|
|
||||||
import ErrorPanel from '../widgets/error-panel.jsx';
|
import ErrorPanel from '../widgets/error-panel.jsx';
|
||||||
import GroupSubs from '../widgets/group-subs.jsx';
|
import GroupSubs from '../widgets/group-subs.jsx';
|
||||||
import ImagePreview from '../widgets/image-preview.jsx';
|
import ImagePreview from '../widgets/image-preview.jsx';
|
||||||
@@ -117,13 +116,14 @@ class MessagesView extends React.Component {
|
|||||||
this.sendKeyPress = this.sendKeyPress.bind(this);
|
this.sendKeyPress = this.sendKeyPress.bind(this);
|
||||||
this.subscribe = this.subscribe.bind(this);
|
this.subscribe = this.subscribe.bind(this);
|
||||||
this.handleScrollReference = this.handleScrollReference.bind(this);
|
this.handleScrollReference = this.handleScrollReference.bind(this);
|
||||||
|
this.mountDnDEvents = this.mountDnDEvents.bind(this);
|
||||||
this.handleScrollEvent = this.handleScrollEvent.bind(this);
|
this.handleScrollEvent = this.handleScrollEvent.bind(this);
|
||||||
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.handleAllMessagesReceived = this.handleAllMessagesReceived.bind(this);
|
this.handleAllMessagesReceived = this.handleAllMessagesReceived.bind(this);
|
||||||
this.handleInfoReceipt = this.handleInfoReceipt.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.handleClosePreview = this.handleClosePreview.bind(this);
|
||||||
this.handleFormResponse = this.handleFormResponse.bind(this);
|
this.handleFormResponse = this.handleFormResponse.bind(this);
|
||||||
this.handleContextClick = this.handleContextClick.bind(this);
|
this.handleContextClick = this.handleContextClick.bind(this);
|
||||||
@@ -144,9 +144,22 @@ class MessagesView extends React.Component {
|
|||||||
this.handleQuoteClick = this.handleQuoteClick.bind(this);
|
this.handleQuoteClick = this.handleQuoteClick.bind(this);
|
||||||
this.handleCallHangup = this.handleCallHangup.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.chatMessageRefs = {};
|
||||||
this.getOrCreateMessageRef = this.getOrCreateMessageRef.bind(this);
|
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.readNotificationQueue = [];
|
||||||
this.readNotificationTimer = null;
|
this.readNotificationTimer = null;
|
||||||
|
|
||||||
@@ -166,6 +179,14 @@ class MessagesView extends React.Component {
|
|||||||
if (this.messagesScroller) {
|
if (this.messagesScroller) {
|
||||||
this.messagesScroller.addEventListener('scroll', this.handleScrollEvent);
|
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() {
|
componentWillUnmount() {
|
||||||
@@ -175,6 +196,14 @@ class MessagesView extends React.Component {
|
|||||||
|
|
||||||
// Flush all notifications.
|
// Flush all notifications.
|
||||||
this.clearNotificationQueue();
|
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
|
// Scroll last message into view on component update e.g. on message received
|
||||||
@@ -245,6 +274,8 @@ class MessagesView extends React.Component {
|
|||||||
docPreview: null,
|
docPreview: null,
|
||||||
imagePreview: null,
|
imagePreview: null,
|
||||||
imagePostview: null,
|
imagePostview: null,
|
||||||
|
videoPreview: null,
|
||||||
|
videoPostview: null,
|
||||||
rtcPanel: null,
|
rtcPanel: null,
|
||||||
typingIndicator: false,
|
typingIndicator: false,
|
||||||
scrollPosition: 0,
|
scrollPosition: 0,
|
||||||
@@ -253,22 +284,29 @@ class MessagesView extends React.Component {
|
|||||||
channel: false,
|
channel: false,
|
||||||
reply: null,
|
reply: null,
|
||||||
contentToEdit: null,
|
contentToEdit: null,
|
||||||
showGoToLastButton: false
|
showGoToLastButton: false,
|
||||||
|
dragging: false
|
||||||
};
|
};
|
||||||
} 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);
|
||||||
|
|
||||||
nextState = {
|
nextState = {
|
||||||
topic: nextProps.topic,
|
topic: nextProps.topic,
|
||||||
|
deleted: topic._deleted,
|
||||||
docPreview: null,
|
docPreview: null,
|
||||||
imagePreview: null,
|
imagePreview: null,
|
||||||
imagePostview: null,
|
imagePostview: null,
|
||||||
|
videoPreview: null,
|
||||||
|
videoPostview: null,
|
||||||
rtcPanel: null,
|
rtcPanel: null,
|
||||||
typingIndicator: false,
|
typingIndicator: false,
|
||||||
scrollPosition: 0,
|
scrollPosition: 0,
|
||||||
fetchingMessages: false,
|
fetchingMessages: false,
|
||||||
showGoToLastButton: false,
|
showGoToLastButton: false,
|
||||||
deleted: topic._deleted
|
forwardMessage: null,
|
||||||
|
reply: null,
|
||||||
|
contentToEdit: null,
|
||||||
|
dragging: false
|
||||||
};
|
};
|
||||||
|
|
||||||
if (nextProps.forwardMessage) {
|
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() {
|
goToLatestMessage() {
|
||||||
this.setState({scrollPosition: 0});
|
this.setState({scrollPosition: 0});
|
||||||
if (this.messagesScroller) {
|
if (this.messagesScroller) {
|
||||||
@@ -681,8 +730,16 @@ class MessagesView extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleImagePostview(content) {
|
handleExpandMedia(content) {
|
||||||
this.setState({ imagePostview: content });
|
if (!content) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (content.video) {
|
||||||
|
this.setState({ videoPostview: content });
|
||||||
|
} else {
|
||||||
|
this.setState({ imagePostview: content });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleClosePreview() {
|
handleClosePreview() {
|
||||||
@@ -692,7 +749,7 @@ class MessagesView extends React.Component {
|
|||||||
if (this.state.videoPreview && this.state.videoPreview.url) {
|
if (this.state.videoPreview && this.state.videoPreview.url) {
|
||||||
URL.revokeObjectURL(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) {
|
handleFormResponse(action, text, data) {
|
||||||
@@ -834,7 +891,12 @@ class MessagesView extends React.Component {
|
|||||||
} else {
|
} else {
|
||||||
// Small enough to send inband.
|
// Small enough to send inband.
|
||||||
fileToBase64(file)
|
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'));
|
.catch(err => this.props.onError(err.message, 'err'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -872,7 +934,7 @@ class MessagesView extends React.Component {
|
|||||||
const mime = this.state.imagePreview.mime;
|
const mime = this.state.imagePreview.mime;
|
||||||
const width = this.state.imagePreview.width;
|
const width = this.state.imagePreview.width;
|
||||||
const height = this.state.imagePreview.height;
|
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.
|
// Server-provided limit reduced for base64 encoding and overhead.
|
||||||
const maxInbandAttachmentSize = (this.props.tinode.getServerParam('maxMessageSize',
|
const maxInbandAttachmentSize = (this.props.tinode.getServerParam('maxMessageSize',
|
||||||
@@ -958,7 +1020,7 @@ class MessagesView extends React.Component {
|
|||||||
|
|
||||||
if (uploads.length == 0) {
|
if (uploads.length == 0) {
|
||||||
// Both video and preview are small enough to send inband.
|
// Both video and preview are small enough to send inband.
|
||||||
Promise.all(blobToBase64(videoBlob), blobToBase64(previewBlob))
|
Promise.all([blobToBase64(videoBlob), blobToBase64(previewBlob)])
|
||||||
.then(b64s => {
|
.then(b64s => {
|
||||||
const [v64, i64] = b64s;
|
const [v64, i64] = b64s;
|
||||||
let msg = Drafty.insertVideo(null, 0, {
|
let msg = Drafty.insertVideo(null, 0, {
|
||||||
@@ -1027,7 +1089,7 @@ class MessagesView extends React.Component {
|
|||||||
this.setState({videoPreview: {
|
this.setState({videoPreview: {
|
||||||
url: URL.createObjectURL(file),
|
url: URL.createObjectURL(file),
|
||||||
blob: file,
|
blob: file,
|
||||||
name: file.name,
|
filename: file.name,
|
||||||
size: file.size,
|
size: file.size,
|
||||||
mime: file.type
|
mime: file.type
|
||||||
}});
|
}});
|
||||||
@@ -1040,7 +1102,7 @@ class MessagesView extends React.Component {
|
|||||||
this.setState({imagePreview: {
|
this.setState({imagePreview: {
|
||||||
url: URL.createObjectURL(scaled.blob),
|
url: URL.createObjectURL(scaled.blob),
|
||||||
blob: scaled.blob,
|
blob: scaled.blob,
|
||||||
name: scaled.name,
|
filename: scaled.name,
|
||||||
width: scaled.width,
|
width: scaled.width,
|
||||||
height: scaled.height,
|
height: scaled.height,
|
||||||
size: scaled.blob.size,
|
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() {
|
render() {
|
||||||
const {formatMessage} = this.props.intl;
|
const {formatMessage} = this.props.intl;
|
||||||
|
|
||||||
@@ -1220,6 +1320,7 @@ class MessagesView extends React.Component {
|
|||||||
content={this.state.videoPreview}
|
content={this.state.videoPreview}
|
||||||
tinode={this.props.tinode}
|
tinode={this.props.tinode}
|
||||||
reply={this.state.reply}
|
reply={this.state.reply}
|
||||||
|
onError={this.props.onError}
|
||||||
onCancelReply={this.handleCancelReply}
|
onCancelReply={this.handleCancelReply}
|
||||||
onClose={this.handleClosePreview}
|
onClose={this.handleClosePreview}
|
||||||
onSendMessage={this.sendVideoAttachment} />
|
onSendMessage={this.sendVideoAttachment} />
|
||||||
@@ -1231,6 +1332,15 @@ class MessagesView extends React.Component {
|
|||||||
content={this.state.imagePostview}
|
content={this.state.imagePostview}
|
||||||
onClose={this.handleClosePreview} />
|
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) {
|
} else if (this.state.docPreview) {
|
||||||
// Preview attachment before sending.
|
// Preview attachment before sending.
|
||||||
component2 = (
|
component2 = (
|
||||||
@@ -1354,7 +1464,7 @@ class MessagesView extends React.Component {
|
|||||||
userIsWriter={this.state.isWriter}
|
userIsWriter={this.state.isWriter}
|
||||||
viewportWidth={this.props.viewportWidth} // Used by `formatter`.
|
viewportWidth={this.props.viewportWidth} // Used by `formatter`.
|
||||||
showContextMenu={this.handleShowMessageContextMenu}
|
showContextMenu={this.handleShowMessageContextMenu}
|
||||||
onImagePreview={this.handleImagePostview}
|
onExpandMedia={this.handleExpandMedia}
|
||||||
onFormResponse={this.handleFormResponse}
|
onFormResponse={this.handleFormResponse}
|
||||||
onCancelUpload={this.handleCancelUpload}
|
onCancelUpload={this.handleCancelUpload}
|
||||||
pickReply={this.handlePickReply}
|
pickReply={this.handlePickReply}
|
||||||
@@ -1486,13 +1596,14 @@ class MessagesView extends React.Component {
|
|||||||
onClearError={this.props.onError} />
|
onClearError={this.props.onError} />
|
||||||
: null}
|
: null}
|
||||||
<LoadSpinner show={this.state.fetchingMessages} />
|
<LoadSpinner show={this.state.fetchingMessages} />
|
||||||
{!this.state.unconfirmed && !this.props.forwardMessage ?
|
{messagesComponent}
|
||||||
<DragAndDrop actionPrompt={this.props.intl.formatMessage(messages.drag_file)} onDrop={this.handleFileDrop} >{messagesComponent}</DragAndDrop>
|
{this.state.dragging && this.isDragEnabled() ?
|
||||||
: messagesComponent}
|
<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;
|
return component;
|
||||||
}
|
}
|
||||||
|
|||||||
+34
-24
@@ -299,11 +299,7 @@ class TinodeWeb extends React.Component {
|
|||||||
}).then(_ => {
|
}).then(_ => {
|
||||||
// Initialize desktop alerts.
|
// Initialize desktop alerts.
|
||||||
if (this.state.desktopAlertsEnabled) {
|
if (this.state.desktopAlertsEnabled) {
|
||||||
this.initFCMessaging().then(_ => {
|
this.initFCMessaging().catch(_ => {
|
||||||
if (this.state.desktopAlerts) {
|
|
||||||
this.tinode.setDeviceToken(this.state.firebaseToken);
|
|
||||||
}
|
|
||||||
}).catch(_ => {
|
|
||||||
// do nothing: handled earlier.
|
// do nothing: handled earlier.
|
||||||
// catch needed to pervent unnecessary logging of error.
|
// catch needed to pervent unnecessary logging of error.
|
||||||
});
|
});
|
||||||
@@ -370,20 +366,27 @@ class TinodeWeb extends React.Component {
|
|||||||
const onError = (msg, err) => {
|
const onError = (msg, err) => {
|
||||||
console.error(msg, err);
|
console.error(msg, err);
|
||||||
this.handleError(formatMessage(messages.push_init_failed), 'err');
|
this.handleError(formatMessage(messages.push_init_failed), 'err');
|
||||||
this.setState({desktopAlertsEnabled: false, firebaseToken: null});
|
this.setState({firebaseToken: null});
|
||||||
LocalStorageUtil.updateObject('settings', {desktopAlerts: false});
|
LocalStorageUtil.updateObject('settings', {desktopAlerts: false});
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.fcm = firebaseGetMessaging(firebaseInitApp(FIREBASE_INIT, APP_NAME));
|
this.fcm = firebaseGetMessaging(firebaseInitApp(FIREBASE_INIT, APP_NAME));
|
||||||
return navigator.serviceWorker.register('/service-worker.js').then((reg) => {
|
return navigator.serviceWorker.getRegistration('/service-worker.js').then(reg => {
|
||||||
this.checkForAppUpdate(reg);
|
if (reg) {
|
||||||
reg.active.postMessage(JSON.stringify({locale: locale, version: PACKAGE_VERSION}));
|
return reg
|
||||||
return reg;
|
}
|
||||||
}).then((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.
|
// Request token.
|
||||||
return TinodeWeb.requestFCMToken(this.fcm, reg);
|
return TinodeWeb.requestFCMToken(this.fcm, reg);
|
||||||
}).then((token) => {
|
}).then(token => {
|
||||||
const persist = LocalStorageUtil.getObject('keep-logged-in');
|
const persist = LocalStorageUtil.getObject('keep-logged-in');
|
||||||
if (token != this.state.firebaseToken) {
|
if (token != this.state.firebaseToken) {
|
||||||
this.tinode.setDeviceToken(token);
|
this.tinode.setDeviceToken(token);
|
||||||
@@ -416,17 +419,17 @@ class TinodeWeb extends React.Component {
|
|||||||
return firebaseGetToken(fcm, {
|
return firebaseGetToken(fcm, {
|
||||||
serviceWorkerRegistration: sw,
|
serviceWorkerRegistration: sw,
|
||||||
vapidKey: FIREBASE_INIT.messagingVapidKey
|
vapidKey: FIREBASE_INIT.messagingVapidKey
|
||||||
}).then((token) => {
|
}).then(token => {
|
||||||
if (token) {
|
if (token) {
|
||||||
return token;
|
return token;
|
||||||
} else {
|
} else {
|
||||||
// Try to request permissions.
|
// Try to request permissions.
|
||||||
return Notification.requestPermission().then((permission) => {
|
return Notification.requestPermission().then(permission => {
|
||||||
if (permission === 'granted') {
|
if (permission === 'granted') {
|
||||||
return firebaseGetToken(fcm, {
|
return firebaseGetToken(fcm, {
|
||||||
serviceWorkerRegistration: reg,
|
serviceWorkerRegistration: reg,
|
||||||
vapidKey: FIREBASE_INIT.messagingVapidKey
|
vapidKey: FIREBASE_INIT.messagingVapidKey
|
||||||
}).then((token) => {
|
}).then(token => {
|
||||||
if (token) {
|
if (token) {
|
||||||
return token;
|
return token;
|
||||||
} else {
|
} else {
|
||||||
@@ -576,6 +579,12 @@ class TinodeWeb extends React.Component {
|
|||||||
this.handleError(err.message, 'err');
|
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.
|
// Enable or disable saving the password and IndexedDB.
|
||||||
@@ -1920,15 +1929,16 @@ class TinodeWeb extends React.Component {
|
|||||||
:
|
:
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
<Alert
|
{this.state.alertVisible ?
|
||||||
visible={this.state.alertVisible}
|
<Alert
|
||||||
title={this.state.alertParams.title}
|
visible={this.state.alertVisible}
|
||||||
content={this.state.alertParams.content}
|
title={this.state.alertParams.title}
|
||||||
onReject={this.state.alertParams.onReject ? (_ => this.setState({alertVisible: false})) : null}
|
content={this.state.alertParams.content}
|
||||||
reject={this.state.alertParams.reject}
|
onReject={this.state.alertParams.onReject ? (_ => this.setState({alertVisible: false})) : null}
|
||||||
onConfirm={_ => {this.setState({alertVisible: false}); this.state.alertParams.onConfirm();}}
|
reject={this.state.alertParams.reject}
|
||||||
confirm={this.state.alertParams.confirm}
|
onConfirm={_ => {this.setState({alertVisible: false}); this.state.alertParams.onConfirm();}}
|
||||||
/>
|
confirm={this.state.alertParams.confirm}
|
||||||
|
/> : null}
|
||||||
<SidepanelView
|
<SidepanelView
|
||||||
tinode={this.tinode}
|
tinode={this.tinode}
|
||||||
connected={this.state.connected}
|
connected={this.state.connected}
|
||||||
|
|||||||
+15
-18
@@ -3,26 +3,23 @@ import { FormattedMessage } from 'react-intl';
|
|||||||
|
|
||||||
export default class Alert extends React.PureComponent {
|
export default class Alert extends React.PureComponent {
|
||||||
render() {
|
render() {
|
||||||
return this.props.visible ?
|
<div className="alert-container">
|
||||||
<div className="alert-container">
|
<div className="alert">
|
||||||
<div className="alert">
|
<div className="title">{this.props.title}</div>
|
||||||
<div className="title">{this.props.title}</div>
|
<div className="content">{this.props.content}</div>
|
||||||
<div className="content">{this.props.content}</div>
|
<div className="dialog-buttons">
|
||||||
<div className="dialog-buttons">
|
{this.props.onReject ?
|
||||||
{this.props.onReject ?
|
<button className="outline" onClick={this.props.onReject}>
|
||||||
<button className="outline" onClick={this.props.onReject}>
|
{this.props.reject || <FormattedMessage id="button_cancel" defaultMessage="Cancel" description="Button [Cancel]" />}
|
||||||
{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]" />}
|
|
||||||
</button>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
:
|
</div>
|
||||||
null;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -140,7 +140,7 @@ class CallPanel extends React.PureComponent {
|
|||||||
|
|
||||||
start() {
|
start() {
|
||||||
if (this.state.localStream) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ export default class CallStatus extends React.PureComponent {
|
|||||||
duration = <FormattedMessage id="call_disconnected" defaultMessage="disconnected" description="Label for disconnected call" />;
|
duration = <FormattedMessage id="call_disconnected" defaultMessage="disconnected" description="Label for disconnected call" />;
|
||||||
break;
|
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 {
|
} else {
|
||||||
duration = <span>{secondsToTime(this.props.duration / 1000)}</span>;
|
duration = <span>{secondsToTime(this.props.duration / 1000)}</span>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,8 @@ class BaseChatMessage extends React.PureComponent {
|
|||||||
props.uploader.onProgress = this.handleProgress.bind(this);
|
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.handleFormButtonClick = this.handleFormButtonClick.bind(this);
|
||||||
this.handleContextClick = this.handleContextClick.bind(this);
|
this.handleContextClick = this.handleContextClick.bind(this);
|
||||||
this.handleCancelUpload = this.handleCancelUpload.bind(this);
|
this.handleCancelUpload = this.handleCancelUpload.bind(this);
|
||||||
@@ -34,18 +35,18 @@ class BaseChatMessage extends React.PureComponent {
|
|||||||
viewportWidth: props.viewportWidth,
|
viewportWidth: props.viewportWidth,
|
||||||
authorizeURL: props.tinode.authorizeURL.bind(props.tinode),
|
authorizeURL: props.tinode.authorizeURL.bind(props.tinode),
|
||||||
|
|
||||||
onImagePreview: this.handleImagePreview,
|
onImagePreview: this.handleExpandImage,
|
||||||
onVideoPreview: this.handleImagePreview,
|
onVideoPreview: this.handlePlayVideo,
|
||||||
onFormButtonClick: this.handleFormButtonClick,
|
onFormButtonClick: this.handleFormButtonClick,
|
||||||
onQuoteClick: this.handleQuoteClick
|
onQuoteClick: this.handleQuoteClick
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
handleImagePreview(e) {
|
handleExpandImage(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.props.onImagePreview({
|
this.props.onExpandMedia({
|
||||||
url: e.target.src,
|
url: e.target.src,
|
||||||
filename: e.target.title,
|
filename: e.target.dataset.name,
|
||||||
width: e.target.dataset.width,
|
width: e.target.dataset.width,
|
||||||
height: e.target.dataset.height,
|
height: e.target.dataset.height,
|
||||||
size: e.target.dataset.size,
|
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) {
|
handleFormButtonClick(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const data = {
|
const data = {
|
||||||
@@ -82,9 +98,15 @@ class BaseChatMessage extends React.PureComponent {
|
|||||||
if (!this.props.response) {
|
if (!this.props.response) {
|
||||||
let immutable = false;
|
let immutable = false;
|
||||||
Drafty.entities(this.props.content, (_0, _1, tp) => {
|
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;
|
return immutable;
|
||||||
});
|
});
|
||||||
|
if (!immutable) {
|
||||||
|
Drafty.styles(this.props.content, tp => {
|
||||||
|
immutable = ['QQ'].includes(tp);
|
||||||
|
return immutable;
|
||||||
|
});
|
||||||
|
}
|
||||||
if (!immutable) {
|
if (!immutable) {
|
||||||
menuItems.push('menu_item_edit');
|
menuItems.push('menu_item_edit');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ class ContactList extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const isChannel = Tinode.isChannelTopicName(key);
|
const isChannel = Tinode.isChannelTopicName(key);
|
||||||
|
const isGroup = !isChannel && Tinode.isGroupTopicName(key);
|
||||||
const selected = showCheckmark ?
|
const selected = showCheckmark ?
|
||||||
(this.props.topicSelected.indexOf(key) > -1) :
|
(this.props.topicSelected.indexOf(key) > -1) :
|
||||||
(this.props.topicSelected === key);
|
(this.props.topicSelected === key);
|
||||||
@@ -112,6 +113,7 @@ class ContactList extends React.Component {
|
|||||||
selected={selected}
|
selected={selected}
|
||||||
showOnline={this.props.showOnline && !isChannel}
|
showOnline={this.props.showOnline && !isChannel}
|
||||||
isChannel={isChannel}
|
isChannel={isChannel}
|
||||||
|
isGroup={isGroup}
|
||||||
showContextMenu={this.props.showContextMenu}
|
showContextMenu={this.props.showContextMenu}
|
||||||
isVerified={c.trusted && c.trusted.verified}
|
isVerified={c.trusted && c.trusted.verified}
|
||||||
isStaff={c.trusted && c.trusted.staff}
|
isStaff={c.trusted && c.trusted.staff}
|
||||||
|
|||||||
@@ -105,6 +105,7 @@ class Contact extends React.Component {
|
|||||||
</div>
|
</div>
|
||||||
<div className="text-box">
|
<div className="text-box">
|
||||||
<div><span className={titleClass}>{title}</span>
|
<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}
|
{this.props.isChannel ? <img src="/img/channel.png" className="channel" alt="channel" /> : null}
|
||||||
<ContactBadges badges={icon_badges} />
|
<ContactBadges badges={icon_badges} />
|
||||||
{!this.props.deleted ? <UnreadBadge count={this.props.unread} /> : null}
|
{!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.
|
// 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 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 width = this.props.content.width || '-';
|
||||||
const height = this.props.content.height || '-';
|
const height = this.props.content.height || '-';
|
||||||
return (
|
return (
|
||||||
<div id="image-preview">
|
<div id="image-preview">
|
||||||
<div id="image-preview-caption-panel">
|
<div id="image-preview-caption-panel">
|
||||||
{!this.props.onSendMessage ?
|
{this.props.onSendMessage ?
|
||||||
<a href={this.props.content.url} download={true}>
|
<span>{fname}</span>
|
||||||
|
:
|
||||||
|
<a href={this.props.content.url} download={this.props.content.filename}>
|
||||||
<i className="material-icons">file_download</i> <FormattedMessage
|
<i className="material-icons">file_download</i> <FormattedMessage
|
||||||
id="download_action" defaultMessage="download" description="Call to action [download]" />
|
id="download_action" defaultMessage="download" description="Call to action [download]" />
|
||||||
</a>
|
</a>
|
||||||
:
|
|
||||||
<span>{fname}</span>
|
|
||||||
}
|
}
|
||||||
<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.name} />
|
<img src={this.props.content.url} style={size} className="image-preview" alt={this.props.content.filename} />
|
||||||
</div>
|
</div>
|
||||||
{this.props.onSendMessage ?
|
{this.props.onSendMessage ?
|
||||||
<SendMessage
|
<SendMessage
|
||||||
@@ -81,7 +81,7 @@ export default class ImagePreview extends React.PureComponent {
|
|||||||
<div>
|
<div>
|
||||||
<div><b><FormattedMessage id="label_file_name" defaultMessage="File name:"
|
<div><b><FormattedMessage id="label_file_name" defaultMessage="File name:"
|
||||||
description="Label for a file name" /></b></div>
|
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>
|
<div>
|
||||||
<div><b><FormattedMessage id="label_content_type" defaultMessage="Content type:"
|
<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 {
|
export default class InlineVideo extends React.PureComponent {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(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() {
|
render() {
|
||||||
const duration = secondsToTime(this.props['data-duration'] / 1000);
|
const duration = secondsToTime(this.props['data-duration'] / 1000);
|
||||||
|
const className = 'inline-video' + (this.props.onClick ? ' image-clickable' : '');
|
||||||
return (
|
return (
|
||||||
<div className="inline-video">
|
<div className={className}>
|
||||||
{React.createElement('img', this.props)}
|
{React.createElement('img', this.props)}
|
||||||
{this.props.onClick ?
|
{this.props.onClick ?
|
||||||
<div className="play-control" onClick={this.props.onClick}>
|
<div className="play-control">
|
||||||
<i className="material-icons white bigger">play_arrow</i>
|
<i className="material-icons white bigger">play_arrow</i>
|
||||||
</div>
|
</div>
|
||||||
: null}
|
: null}
|
||||||
{duration ? <div className="duration">{duration}</div> : null}
|
{duration ? <div className="duration">{duration}</div> : null}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ export default class LazyImage extends React.PureComponent {
|
|||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
src: 'img/placeholder.png',
|
src: this.props.isvideo ? 'img/blankvid.png' : 'img/blankimg.png',
|
||||||
style: Object.assign({padding: '4px'}, this.props.style),
|
style: Object.assign({padding: '4px'}, this.props.style),
|
||||||
className: this.props.className,
|
className: this.props.className,
|
||||||
alt: this.props.alt,
|
alt: this.props.alt,
|
||||||
@@ -19,7 +19,7 @@ export default class LazyImage extends React.PureComponent {
|
|||||||
this.props.whenDone
|
this.props.whenDone
|
||||||
.promise
|
.promise
|
||||||
.then(data => this.setState({src: data.src, style: {...this.state.style, padding: 0}}))
|
.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() {
|
componentWillUnmount() {
|
||||||
@@ -28,11 +28,11 @@ export default class LazyImage extends React.PureComponent {
|
|||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
componentDidUpdate(prevProps) {
|
||||||
if (prevProps.whenDone != this.props.whenDone) {
|
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
|
this.props.whenDone
|
||||||
.promise
|
.promise
|
||||||
.then(data => this.setState({src: data.src, style: {...this.state.style, padding: 0}}))
|
.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) {
|
handleDropAttach(files) {
|
||||||
if (files && files.length > 0) {
|
if (files && files.length > 0) {
|
||||||
console.log('Dropping ', files);
|
|
||||||
this.props.onAttachFile(files[0]);
|
this.props.onAttachFile(files[0]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,18 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
|
||||||
import SendMessage from './send-message.jsx';
|
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) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
@@ -17,7 +28,12 @@ export default class VideoPreview extends React.PureComponent {
|
|||||||
height: this.videoRef.current.videoHeight,
|
height: this.videoRef.current.videoHeight,
|
||||||
duration: (this.videoRef.current.duration * 1000) | 0,
|
duration: (this.videoRef.current.duration * 1000) | 0,
|
||||||
mime: this.props.content.mime,
|
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.
|
// Capture screen from a video.
|
||||||
@@ -37,15 +53,29 @@ export default class VideoPreview extends React.PureComponent {
|
|||||||
return null;
|
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 (
|
return (
|
||||||
<div id="image-preview">
|
<div id="image-preview">
|
||||||
<div id="image-preview-caption-panel">
|
<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>
|
<a href="#" onClick={e => {e.preventDefault(); this.props.onClose();}}><i className="material-icons gray">close</i></a>
|
||||||
</div>
|
</div>
|
||||||
<div id="image-preview-container">
|
<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>
|
</div>
|
||||||
|
{this.props.onSendMessage ?
|
||||||
<SendMessage
|
<SendMessage
|
||||||
messagePrompt="add_image_caption"
|
messagePrompt="add_image_caption"
|
||||||
acceptBlank={true}
|
acceptBlank={true}
|
||||||
@@ -54,7 +84,27 @@ export default class VideoPreview extends React.PureComponent {
|
|||||||
onCancelReply={this.props.onCancelReply}
|
onCancelReply={this.props.onCancelReply}
|
||||||
onSendMessage={this.handleSendVideo}
|
onSendMessage={this.handleSendVideo}
|
||||||
onError={this.props.onError} />
|
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>
|
</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