Compare commits

...
31 Commits
Author SHA1 Message Date
or-else ba94788fbe fix service worker and notifications init for FF and Safari 2023-01-09 11:18:21 -08:00
or-else 45fbff1b4a rebuilt with sdk fix: incorrect reference when replying to edited message 2023-01-05 17:20:20 -08:00
or-else f99f5a223f assign file size for in-band attachments 2023-01-05 13:22:08 -08:00
or-else c0e4cd4965 rebuilt with SDK fix: ensure edits cannot change message sender 2023-01-05 12:33:42 -08:00
or-else 1d3a9040cb rebuilt with the sdk fix for previews of forwarded messages 2023-01-04 21:00:12 -08:00
or-else cab80e4d62 translations for the new error message 2023-01-04 18:04:15 -08:00
or-else f7c5322e36 clear "reply/editing/forwarding state when switching topics 2023-01-04 17:59:45 -08:00
or-else cb64e1f247 fix handling of inline videos 2023-01-04 17:52:28 -08:00
or-else afee6b5dd9 clear edited message text when switching topics 2023-01-04 13:58:19 -08:00
or-else 9f01eab324 disable editing quoted messages 2023-01-04 13:05:03 -08:00
or-else 1bc6f87844 use "in progress" instead of 0:00 for calls in progress 2023-01-03 17:34:53 -08:00
or-else 0ea112b145 Merge branch 'devel' into next 2023-01-02 17:45:07 -08:00
or-else 768edd744d fallback to fixed size when video has no size info 2023-01-02 13:18:26 -08:00
or-else 1aadf4e7bd rebuilt with correct sdk version 2022-12-31 16:01:55 -08:00
or-else 2d92e475b5 dependencies updated and rebuilt 2022-12-31 13:55:38 -08:00
or-else a28c3eb5cd minor vidual correction 2022-12-31 13:48:57 -08:00
or-else 97208e1448 bum version to 0.20.4 2022-12-30 12:26:46 -08:00
or-else 9fbc601870 fix preview of received videos 2022-12-30 10:20:13 -08:00
or-else 4489646144 fix for a couple of minor problems 2022-12-20 18:34:20 -08:00
or-else 261b2dbdd7 minor color tweak 2022-12-16 09:28:13 -08:00
or-else 224323f443 merge devel 2022-12-16 09:15:07 -08:00
or-else 6d031f033e another small tweak to badges 2022-12-16 09:13:27 -08:00
or-else c38e1832dc tweak topic type badges 2022-12-16 09:11:05 -08:00
or-else 13036da36b more cleanup of video attachments 2022-12-14 21:05:40 -08:00
or-else f5c628fbb5 new translations 2022-12-14 19:39:10 -08:00
or-else 5673d82b33 replies to video recordings work 2022-12-14 19:24:59 -08:00
or-else 8c3287941a video works although may have bugs 2022-12-14 16:50:00 -08:00
or-else 830d15f7ec rebuilt 2022-12-14 14:27:56 -08:00
or-else 4e42d96d66 fix video message previews 2022-12-14 14:27:33 -08:00
or-else ed77f48189 move dnd to MessagesView, otherwise problems with layout 2022-12-14 14:26:47 -08:00
or-else ed3107405f make drag n drop prettier 2022-12-14 10:54:39 -08:00
69 changed files with 2311 additions and 1999 deletions
+3 -1
View File
@@ -58,7 +58,9 @@ More translations are welcome. See [instructions](https://github.com/tinode/chat
* End-to-End encryption.
* Emoji support is weak.
* Mentions, hashtags.
* Previews not generated for videos, links or docs.
* Previews not generated for links and docs.
* Location and contact sharing.
* Recording video messages.
## Other
+20 -26
View File
@@ -1011,7 +1011,8 @@ i.search {
}
.contact-box .contact-comment .material-icons {
margin-bottom: .2rem;
margin-bottom: 0.2rem;
color: #777;
}
/* Avatar and online-offline indicator */
@@ -1080,15 +1081,15 @@ i.search {
}
img.channel {
width: 1rem;
height: 1rem;
width: 1.25rem;
height: 1.25rem;
vertical-align: middle;
margin-left: 0.3rem;
}
.large img.channel {
width: 1.35rem;
height: 1.35rem;
width: 1.45rem;
height: 1.45rem;
margin-left: 0.5rem;
margin-bottom: 0.15rem;
}
@@ -1990,7 +1991,6 @@ video {
/* Inline and preview image styles */
.inline-image, .image-preview {
display: flex;
position: relative;
overflow: hidden;
border-radius: 0.1rem;
@@ -2075,6 +2075,7 @@ img.image-preview::before {
height: 4rem;
background-color: rgba(0, 0, 0, 0.4);
border-radius: 50%;
pointer-events: none;
cursor: pointer;
}
@@ -2223,8 +2224,8 @@ ul.tabbar li.active > a {
.material-icons.as-badge {
padding-left: 0.2rem;
font-size: 1.25rem;
color: #bbb;
font-size: 1.15rem;
color: #aaa;
}
/* Badge with a label, like [you], [muted], etc */
@@ -2976,31 +2977,24 @@ ul.tabbar li.active > a {
}
/*
Drag-and-drop container.
Drag-and-drop area.
*/
.drag-and-drop {
.drag-n-drop {
display: flex;
flex-direction: column;
border: dashed grey 0.15rem;
border-radius: 1rem;
background-color: rgba(255,255,255,.95);
position: absolute;
width: 100%;
height: 100%;
}
.drag-and-drop .banner {
border: dashed grey 1px;
background-color: rgba(255,255,255,.8);
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
z-index: 9999;
/* Center text & font. */
display: flex;
top: 1.5rem;
bottom: 1.5rem;
left: 1.5rem;
right: 1.5rem;
z-index: 3;
justify-content: center;
align-items: center;
font-size: 2rem;
color: grey;
outline: 0.5rem solid rgba(255,255,255,.95);
}
/*
+1 -1
View File
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

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 620 B

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

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