diff --git a/callwidget.cpp b/callwidget.cpp index 21a6a3061a2826797270ac7eab90070344b8ba85..1edc850cee05fdb400171c2f1574eeeea1d1626e 100644 --- a/callwidget.cpp +++ b/callwidget.cpp @@ -1283,7 +1283,7 @@ CallWidget::CreateCopyPasteContextMenu() menu_->addAction(&action1); connect(&action1, SIGNAL(triggered()), this, SLOT(Copy())); } - if (mimeData->hasText()) { + if (mimeData->hasImage() || mimeData->hasUrls() || mimeData->hasText()) { menu_->addAction(&action2); connect(&action2, SIGNAL(triggered()), this, SLOT(Paste())); } @@ -1294,12 +1294,36 @@ void CallWidget::Paste() { const QMimeData* mimeData = clipboard_->mimeData(); - if (mimeData->hasHtml()) { - ui->messageView->setMessagesContent(mimeData->text()); - } else if (mimeData->hasText()) { - ui->messageView->setMessagesContent(mimeData->text()); + + if (mimeData->hasImage()) { + + //save temp data into base64 format + QPixmap pixmap = qvariant_cast<QPixmap>(mimeData->imageData()); + QByteArray ba; + QBuffer bu(&ba); + bu.open(QIODevice::WriteOnly); + pixmap.save(&bu, "PNG"); + auto str = QString::fromStdString(ba.toBase64().toStdString()); + + ui->messageView->setMessagesImageContent(str,0); + } + else if (mimeData->hasUrls()) { + + QList<QUrl> urlList = mimeData->urls(); + // extract the local paths of the files + for (int i = 0; i < urlList.size(); ++i) { + // Trim file:/// from url + QString filePath = urlList.at(i).toString().remove(0, 8); + QString fileType = QFileInfo(filePath).suffix(); + if ( fileType == "png" || fileType == "jpg" || fileType == "jpeg" || + fileType == "gif" || fileType == "bmp") { + ui->messageView->setMessagesImageContent(filePath,1); + } else { + ui->messageView->setMessagesFileContent(filePath); + } + } } else { - ui->messageView->setMessagesContent(tr("Cannot display data")); + ui->messageView->setMessagesContent(mimeData->text()); } } diff --git a/messagewebview.cpp b/messagewebview.cpp index 6fbdcd64769f0dcf6fa1c0fd663d3bc1d6966125..27fc7c57c207bbe51dc7c110f3a40b9fbdc947b2 100644 --- a/messagewebview.cpp +++ b/messagewebview.cpp @@ -21,10 +21,12 @@ #include "messagewebview.h" +#include <QCryptographicHash> #include <QDebug> #include <QDesktopServices> #include <QFileDialog> #include <QMenu> +#include <QMessagebox> #include <QMimeData> #include <QMouseEvent> #include <QScrollBar> @@ -183,9 +185,35 @@ bool MessageWebView::eventFilter(QObject *watched, QEvent *event) } } -void MessageWebView::setMessagesContent(QString text) +void MessageWebView::setMessagesContent(const QString& text) { - page()->runJavaScript(QStringLiteral("document.getElementById('message').value = '%1'").arg(text)); + page()->runJavaScript(QStringLiteral("document.getElementById('message').value += '%1';").arg(text)); +} + +void +MessageWebView::setMessagesImageContent(const QString &path, const short& type) +{ + if (type == 0) { + QString param = QString("addImage_base64('%1')").arg(path); + page()->runJavaScript(param); + } else if (type == 1) { + QString param = QString("addImage_path('%1')").arg(path); + page()->runJavaScript(param); + } +} + +void +MessageWebView::setMessagesFileContent(const QString &path) +{ + qint64 fileSize = QFileInfo(path).size(); + QString fileName = QFileInfo(path).fileName(); + //if file name is too large, trim it + if (fileName.length() > 15) { + fileName = fileName.remove(12, fileName.length() - 12) + "..."; + } + QString param = QString("addFile_path('%1','%2','%3')") + .arg(path, fileName, Utils::humanFileSize(fileSize)); + page()->runJavaScript(param); } void MessageWebView::copySelectedText(QClipboard* clipboard) @@ -465,26 +493,89 @@ PrivateBridging::sendMessage(const QString& arg) LRCInstance::getCurrentConversationModel()->sendMessage(convUid, arg.toStdString()); } catch (...) { qDebug() << "JS bridging - exception during sendMessage:" << arg; + return -1; + } + return 0; +} + +Q_INVOKABLE int +PrivateBridging::sendImage(const QString& arg) +{ + if (arg.startsWith("data:image/png;base64,")) { + //img tag contains base64 data, trim "data:image/png;base64," from data + QByteArray data = QByteArray::fromStdString(arg.toStdString().substr(22)); + auto img_name_hash = QString::fromStdString(QCryptographicHash::hash(data, QCryptographicHash::Sha1).toHex().toStdString()); + QString fileName = "\\img_" + img_name_hash + ".png"; + + QPixmap image_to_save; + if (!image_to_save.loadFromData(QByteArray::fromBase64(data))) { + qDebug().noquote() << "JS bridging - errors during loadFromData" << "\n"; + return -1; + } + + QString path = QString(Utils::WinGetEnv("TEMP")) + fileName; + if (!image_to_save.save(path,"PNG")) { + qDebug().noquote() << "JS bridging - errors during QPixmap save" << "\n"; + return -1; + } + + try { + auto convUid = LRCInstance::getSelectedConvUid(); + LRCInstance::getCurrentConversationModel()->sendFile(convUid, path.toStdString(), fileName.toStdString()); + } catch (...) { + qDebug().noquote() << "JS bridging - exception during sendFile - base64 img" << "\n"; + return -1; + } + + } else { + //img tag contains file paths + QFileInfo fi(arg); + QString fileName = fi.fileName(); + try { + auto convUid = LRCInstance::getSelectedConvUid(); + LRCInstance::getCurrentConversationModel()->sendFile(convUid, arg.toStdString(), fileName.toStdString()); + } catch (...) { + qDebug().noquote() << "JS bridging - exception during sendFile - image from path" << "\n"; + return -1; + } } return 0; } Q_INVOKABLE int -PrivateBridging::sendFile() +PrivateBridging::sendFile(const QString&path) { qDebug() << "JS bridging - MessageWebView::sendFile"; - QString filePath = QFileDialog::getOpenFileName((QWidget*)this->parent(), tr("Choose File"), "", tr("Files") + " (*)"); - QFileInfo fi(filePath); + QFileInfo fi(path); QString fileName = fi.fileName(); try { auto convUid = LRCInstance::getSelectedConvUid(); - LRCInstance::getCurrentConversationModel()->sendFile(convUid, filePath.toStdString(), fileName.toStdString()); + LRCInstance::getCurrentConversationModel()->sendFile(convUid, path.toStdString(), fileName.toStdString()); } catch (...) { qDebug() << "JS bridging - exception during sendFile"; } return 0; } +Q_INVOKABLE int +PrivateBridging::selectFile() +{ + QString filePath = QFileDialog::getOpenFileName((QWidget*)this->parent(), tr("Choose File"), "", tr("Files") + " (*)"); + if (filePath.length() == 0) + return 0; + QString fileType = QFileInfo(filePath).suffix(); + + if (auto messageView = qobject_cast<MessageWebView*>(this->parent())) { + if (fileType == "png" || fileType == "jpg" || fileType == "jepg" || fileType == "gif" || fileType == "bmp") { + messageView->setMessagesImageContent(filePath,1); + return 0; + } + messageView->setMessagesFileContent(filePath); + return 0; + } + return -1; +} + Q_INVOKABLE int PrivateBridging::acceptInvitation() { diff --git a/messagewebview.h b/messagewebview.h index 262b911ca66eaa93106ba0ac6866cc2c04027916..919f0e2fb7949c4a955005f44a5c49f6784c6865 100644 --- a/messagewebview.h +++ b/messagewebview.h @@ -39,7 +39,9 @@ public: Q_INVOKABLE int acceptFile(const QString& arg); Q_INVOKABLE int refuseFile(const QString& arg); Q_INVOKABLE int sendMessage(const QString& arg); - Q_INVOKABLE int sendFile(); + Q_INVOKABLE int sendImage(const QString& arg); + Q_INVOKABLE int sendFile(const QString& arg); + Q_INVOKABLE int selectFile(); Q_INVOKABLE int log(const QString& arg); Q_INVOKABLE int acceptInvitation(); Q_INVOKABLE int refuseInvitation(); @@ -78,8 +80,10 @@ public: const std::string& contactUri = "", const std::string& contactId = ""); void setMessagesVisibility(bool visible); - void setMessagesContent(QString text); + void setMessagesContent(const QString& text); void copySelectedText(QClipboard* clipboard); + void setMessagesImageContent(const QString& path, const short &type); + void setMessagesFileContent(const QString& path); bool textSelected(); void runJsText(); @@ -92,7 +96,6 @@ protected: void resizeEvent(QResizeEvent *event); bool eventFilter(QObject *watched, QEvent *event); - signals: void conversationRemoved(); void messagesCleared(); diff --git a/utils.cpp b/utils.cpp index 2db4234d1ac3a7ea55e89167c880486620a2501a..dd1929b6a1b2429d2af28318dbead417dbef4933 100644 --- a/utils.cpp +++ b/utils.cpp @@ -779,3 +779,23 @@ Utils::swapQListWidgetItems(QListWidget* list, bool down) down ? list->insertItem(currIndex, temp) : list->insertItem(otherIndex, current); down ? list->insertItem(otherIndex, current) : list->insertItem(currIndex, temp); } + +QString +Utils::humanFileSize(qint64 fileSize) +{ + float fileSizeF = static_cast<float>(fileSize); + float thresh = 1024; + + if(abs(fileSizeF) < thresh) { + return QString::number(fileSizeF) + " B"; + } + QString units[] = { "kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB" }; + int unit_position = -1; + do { + fileSizeF /= thresh; + ++unit_position; + } while (abs(fileSizeF) >= thresh && unit_position < units->size() - 1); + //Round up to two decimal + fileSizeF = roundf(fileSizeF * 100) / 100; + return QString::number(fileSizeF) + " " + units[unit_position]; +} diff --git a/utils.h b/utils.h index e66aef8504d7da1094995650f253febd17f21698..eb5512716028f2e3c19e7ac798488e3976ff3b21 100644 --- a/utils.h +++ b/utils.h @@ -113,6 +113,9 @@ lrc::api::conversation::Info getConversationFromUid(const std::string & convUid, // misc helpers void swapQListWidgetItems(QListWidget* list, bool down = true); +// Byte to human readable size +QString humanFileSize(qint64 fileSize); + template <typename Func1, typename Func2> void oneShotConnect(const typename QtPrivate::FunctionPointer<Func1>::Object* sender, Func1 signal, Func2 slot) @@ -190,4 +193,4 @@ indexInVector(const std::vector<T>& vec, const T& item) } return std::distance(vec.begin(), it); } -} \ No newline at end of file +} diff --git a/web/chatview.css b/web/chatview.css index 0052dd9c94f07670525118ea4fca0bdbf956f160..c02d1b2663bb771a6e57172b508a2eedab89ca3a 100644 --- a/web/chatview.css +++ b/web/chatview.css @@ -333,6 +333,143 @@ a:hover { pointer-events: none; } +#file_image_send_container { + visibility: hidden; + position: fixed; + bottom: var(--messagebar-size); + z-index: 1; + display: flex; + justify-content: flex-start; + left: 0; + right: 0; + height: 8em; + /*border-top-left-radius: 25px; + border-top-right-radius: 25px;*/ + border: 2px solid lightgray; + padding: 20px; + border-bottom: none; + background-color: #cfdbdd; + overflow-x: overlay; +} + + #file_image_send_container::-webkit-scrollbar{ + height: 10px; + } + + #file_image_send_container::after { + /*Create the margins with pseudo-elements*/ + /*to solve overflow:scroll and The Right Padding Problem*/ + content: ' '; + min-width: 20px; + } + +.img_wrapper { + position: relative; + max-width: 65px; + min-width: 65px; + max-height: 80px; + border: 3px solid rgba(255,255,255,0); + padding: 30px; + border-radius: 20px; + background-color: #cfdbdd; + display: flex; + justify-content: center; + align-items: center; + margin: 5px; +} + +/* Make the image responsive */ + .img_wrapper img { + height: 118px; + min-width: 131px; + object-fit: cover; + border-radius: 20px; + } + +/* Style the button and place it at the top right corner of the image */ + .img_wrapper .btn { + position: absolute; + color: #fff; + border: 1px solid #AEAEAE; + border-radius: 40%; + background: rgba(96,95,97,0.5); + font-size: 10px; + font-weight: bold; + top: -5%; + right: -5%; + } + + .img_wrapper .btn:hover { + background-color: lightgray; + } + + .img_wrapper .btn:focus { + outline: none; + color: black; + } + +.file_wrapper { + position: relative; + max-width: 65px; + min-width: 65px; + max-height: 80px; + border: 3px solid rgba(255,255,255,0); + padding: 30px; + border-radius: 20px; + background-color: white; + display: flex; + justify-content: flex-start; + align-items: center; + margin: 5px; + font-family: sans-serif; +} + /* Style the button and place it at the top right corner of the image */ + .file_wrapper .btn { + position: absolute; + color: #fff; + border: 1px solid #AEAEAE; + border-radius: 40%; + background: rgba(96,95,97,0.5); + font-size: 10px; + font-weight: bold; + top: -5%; + right: -5%; + } + + .file_wrapper .btn:hover { + background-color: lightgray; + } + + .file_wrapper .btn:focus { + outline: none; + color: black; + } + + .file_wrapper .svg-icon { + position: absolute; + max-width: 2em; + max-height: 25px; + margin-right: 2em; + top: 8%; + left: 1px; + } + + .file_wrapper .svg-icon path, + .file_wrapper .svg-icon polygon, + .file_wrapper .svg-icon rect { + fill: #000000; + } + + .file_wrapper .svg-icon circle { + stroke: #4691f6; + stroke-width: 1; + } + .file_wrapper .fileinfo { + position: absolute; + top: 30%; + left: 7%; + } + #back_to_bottom_button { visibility: hidden; margin: auto; diff --git a/web/chatview.html b/web/chatview.html index 0333c61d998a13249ac6871314ae58620ff73552..5263398357381fce28ffbd8a02123b46dabf19d9 100644 --- a/web/chatview.html +++ b/web/chatview.html @@ -43,8 +43,9 @@ <div id="back_to_bottom_button_container"> <div id="back_to_bottom_button" onclick="back_to_bottom()">Jump to latest ▼</div> </div> + <div id="file_image_send_container"></div> <div id="sendMessage"> - <div class="nav-button action-button" onclick="sendFile()" title="Send File"> + <div class="nav-button action-button" onclick="selectFile()" title="Send File"> <svg class="svgicon" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"> <path d="M16.5 6v11.5c0 2.21-1.79 4-4 4s-4-1.79-4-4V5c0-1.38 1.12-2.5 2.5-2.5s2.5 1.12 2.5 2.5v10.5c0 .55-.45 1-1 1s-1-.45-1-1V6H10v9.5c0 1.38 1.12 2.5 2.5 2.5s2.5-1.12 2.5-2.5V5c0-2.21-1.79-4-4-4S7 2.79 7 5v12.5c0 3.04 2.46 5.5 5.5 5.5s5.5-2.46 5.5-5.5V6h-1.5z" /> <path d="M0 0h24v24H0z" fill="none" /> diff --git a/web/chatview.js b/web/chatview.js index 0ccd0cd49d95e935cf8d842952fa9fe566ee3879..f923e214829d041d1c10af4d552ba9459217a621 100644 --- a/web/chatview.js +++ b/web/chatview.js @@ -32,6 +32,8 @@ const inviteImage = document.getElementById("invite_image") const invitationText = document.getElementById("text") var messages = document.getElementById("messages") var backToBottomBtn = document.getElementById("back_to_bottom_button") +var sendContainer = document.getElementById("file_image_send_container") + /* States: allows us to avoid re-doing something if it isn't meaningful */ var displayLinksEnabled = true @@ -273,11 +275,31 @@ function formatDate(date) { */ function sendMessage() { + //send image in sendContainer + var data_to_send = sendContainer.innerHTML; + var imgSrcExtract = new RegExp('<img src="(.*?)">', 'g'); + var fileSrcExtract = new RegExp('<div class="file_wrapper" data-path="(.*?)">', 'g'); + + var img_src; + while ((img_src = imgSrcExtract.exec(data_to_send)) !== null ) { + window.jsbridge.sendImage(img_src[1]); + } + + var file_src; + while ((file_src = fileSrcExtract.exec(data_to_send)) !== null) { + window.jsbridge.sendFile(file_src[1]); + } + + sendContainer.innerHTML = ""; + sendContainer.style.visibility = "hidden"; + reduce_send_container(); + var message = messageBarInput.value if (message.length > 0) { messageBarInput.value = "" window.jsbridge.sendMessage(message) } + } /* exported acceptInvitation */ @@ -293,12 +315,6 @@ function blockConversation() { window.jsbridge.blockConversation() } -/* exported sendFile */ -function sendFile() -{ - window.jsbridge.sendFile(); -} - /** * Convert text to HTML. */ @@ -1633,3 +1649,109 @@ function isTextSelected() { return true; return false; } + +/* exported selectFile - sselect files from Qt */ +function selectFile() { + + window.jsbridge.selectFile(); + //js or qt +} + +/** + * add file (local file) to message area + */ +function addFile_path(path, name, size) { + var html = '<div class="file_wrapper" data-path=' + path + '>' + + '<svg class="svg-icon" viewBox="0 0 20 20">' + + '<path fill = "none" d = "M17.222,5.041l-4.443-4.414c-0.152-0.151-0.356-0.235-0.571-0.235h-8.86c-0.444,0-0.807,0.361-0.807,0.808v17.602c0,0.448,0.363,0.808,0.807,0.808h13.303c0.448,0,0.808-0.36,0.808-0.808V5.615C17.459,5.399,17.373,5.192,17.222,5.041zM15.843,17.993H4.157V2.007h7.72l3.966,3.942V17.993z" ></path>' + + '<path fill="none" d="M5.112,7.3c0,0.446,0.363,0.808,0.808,0.808h8.077c0.445,0,0.808-0.361,0.808-0.808c0-0.447-0.363-0.808-0.808-0.808H5.92C5.475,6.492,5.112,6.853,5.112,7.3z"></path>' + + '<path fill="none" d="M5.92,5.331h4.342c0.445,0,0.808-0.361,0.808-0.808c0-0.446-0.363-0.808-0.808-0.808H5.92c-0.444,0-0.808,0.361-0.808,0.808C5.112,4.97,5.475,5.331,5.92,5.331z"></path>' + + '<path fill="none" d="M13.997,9.218H5.92c-0.444,0-0.808,0.361-0.808,0.808c0,0.446,0.363,0.808,0.808,0.808h8.077c0.445,0,0.808-0.361,0.808-0.808C14.805,9.58,14.442,9.218,13.997,9.218z"></path>' + + '<path fill="none" d="M13.997,11.944H5.92c-0.444,0-0.808,0.361-0.808,0.808c0,0.446,0.363,0.808,0.808,0.808h8.077c0.445,0,0.808-0.361,0.808-0.808C14.805,12.306,14.442,11.944,13.997,11.944z"></path>' + + '<path fill="none" d="M13.997,14.67H5.92c-0.444,0-0.808,0.361-0.808,0.808c0,0.447,0.363,0.808,0.808,0.808h8.077c0.445,0,0.808-0.361,0.808-0.808C14.805,15.032,14.442,14.67,13.997,14.67z"></path>' + + '</svg >' + + '<div class="fileinfo">' + + '<p>' + name + '</p>' + + '<p>' + size + '</p>' + + '</div >' + + '<button class="btn" onclick="remove(this)">X</button>' + + '</div >'; + // At first, visiblity can empty + if (sendContainer.style.visibility.length == 0 || sendContainer.style.visibility == "hidden") { + grow_send_container(); + sendContainer.style.visibility = "visible"; + } + //add html here since display is set to flex, image will change accordingly + sendContainer.innerHTML += html; +} + +/** + * add image (base64 array) to message area + */ +function addImage_base64(base64) { + + var html = '<div class="img_wrapper">' + + '<img src="data:image/png;base64,' + base64 + '"/>' + + '<button class="btn" onclick="remove(this)">X</button>' + + '</div >'; + // At first, visiblity can empty + if (sendContainer.style.visibility.length == 0 || sendContainer.style.visibility == "hidden") { + grow_send_container(); + sendContainer.style.visibility = "visible"; + } + //add html here since display is set to flex, image will change accordingly + sendContainer.innerHTML += html; +} + +/** + * add image (image path) to message area + */ +function addImage_path(path) { + + + var html = '<div class="img_wrapper">' + + '<img src="' + path + '"/>' + + '<button class="btn" onclick="remove(this)">X</button>' + + '</div >'; + // At first, visiblity can empty + if (sendContainer.style.visibility.length == 0 || sendContainer.style.visibility == "hidden") { + grow_send_container(); + sendContainer.style.visibility = "visible"; + } + //add html here since display is set to flex, image will change accordingly + sendContainer.innerHTML += html; +} + +/** + * This function adjusts the body paddings so that that the file_image_send_container doesn't + * overlap messages when it grows. + */ +/* exported grow_send_container */ +function grow_send_container() { + exec_keeping_scroll_position(function () { + var msgbar_size = window.getComputedStyle(document.body).getPropertyValue("--messagebar-size"); + document.body.style.paddingBottom = (parseInt(msgbar_size) + 158).toString() + "px"; + //6em + }, []) +} + +/** + * This function adjusts the body paddings so that that the file_image_send_container will hide + * and recover padding bottom + */ +/* exported grow_send_container */ +function reduce_send_container() { + exec_keeping_scroll_position(function () { + document.body.style.paddingBottom = (parseInt(document.body.style.paddingBottom) - 158).toString() + "px"; + //6em + }, []) +} + +// Remove current cancel button division and hide the sendContainer +function remove(e) { + e.parentNode.parentNode.removeChild(e.parentNode); + if (sendContainer.innerHTML.length == 0) { + reduce_send_container(); + sendContainer.style.visibility = "hidden"; + } +}