mirror of
https://git.jami.net/savoirfairelinux/jami-client-qt.git
synced 2025-09-10 12:03:18 +02:00
chat: preprocess text msgs w/md4c+tidy-html5
Introduces MessageParser to encapsulate text treatment for raw text messages. The async parsing sequence is as follows: - Markdown -> HTML (md4c) - link coloration (tidy-html5) - notify UI - request link preview info from PreviewEngine for the first link - Preview engine uses QtNetwork instead of QtWebengine - Linkification is handled by MessageParser instead of linkify.js QtWebengine is no longer required for message parsing. Gitlab: #1033 Gitlab: #855 Change-Id: Ief9b91aa291caf284f08230acaf57976f80fa05b
This commit is contained in:
parent
07527be378
commit
8db188c513
37 changed files with 979 additions and 4312 deletions
8
.gitmodules
vendored
8
.gitmodules
vendored
|
@ -19,3 +19,11 @@
|
|||
path = extras/packaging/update/sparkle/Sparkle
|
||||
url = https://github.com/sparkle-project/Sparkle.git
|
||||
ignore = dirty
|
||||
[submodule "3rdparty/md4c"]
|
||||
path = 3rdparty/md4c
|
||||
url = https://github.com/mity/md4c.git
|
||||
ignore = dirty
|
||||
[submodule "3rdparty/tidy-html5"]
|
||||
path = 3rdparty/tidy-html5
|
||||
url = https://github.com/htacg/tidy-html5.git
|
||||
ignore = dirty
|
||||
|
|
1
3rdparty/md4c
vendored
Submodule
1
3rdparty/md4c
vendored
Submodule
|
@ -0,0 +1 @@
|
|||
Subproject commit e9ff661ff818ee94a4a231958d9b6768dc6882c9
|
1
3rdparty/tidy-html5
vendored
Submodule
1
3rdparty/tidy-html5
vendored
Submodule
|
@ -0,0 +1 @@
|
|||
Subproject commit d08ddc2860aa95ba8e301343a30837f157977cba
|
|
@ -29,6 +29,18 @@ else()
|
|||
project(jami)
|
||||
endif()
|
||||
|
||||
option(WITH_DAEMON_SUBMODULE "Build with daemon submodule" OFF)
|
||||
option(ENABLE_TESTS "Build with tests" OFF)
|
||||
option(WITH_WEBENGINE "Build with WebEngine" ON)
|
||||
if(WITH_WEBENGINE)
|
||||
add_definitions(-DWITH_WEBENGINE)
|
||||
endif()
|
||||
|
||||
# init some variables for includes, libs, etc.
|
||||
set(CLIENT_INCLUDE_DIRS, "")
|
||||
set(CLIENT_LINK_DIRS, "")
|
||||
set(CLIENT_LIBS, "")
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
set(CMAKE_CXX_FLAGS_DEBUG "-Og -ggdb")
|
||||
|
@ -41,12 +53,9 @@ set(CMAKE_INCLUDE_CURRENT_DIR ON)
|
|||
# Main project directories:
|
||||
|
||||
# jami-daemon
|
||||
if(NOT DEFINED WITH_DAEMON_SUBMODULE)
|
||||
set(WITH_DAEMON_SUBMODULE false)
|
||||
# daemon
|
||||
if(NOT WITH_DAEMON_SUBMODULE)
|
||||
set(DAEMON_DIR ${PROJECT_SOURCE_DIR}/../daemon)
|
||||
else()
|
||||
# daemon
|
||||
set(DAEMON_DIR ${PROJECT_SOURCE_DIR}/daemon)
|
||||
endif()
|
||||
# src
|
||||
|
@ -93,7 +102,6 @@ add_subdirectory(${LIBCLIENT_SRC_DIR})
|
|||
set(QT_MODULES
|
||||
Quick
|
||||
Network
|
||||
NetworkAuth
|
||||
Svg
|
||||
Gui
|
||||
Qml
|
||||
|
@ -106,10 +114,6 @@ set(QT_MODULES
|
|||
Widgets
|
||||
Positioning)
|
||||
|
||||
if(NOT DEFINED WITH_WEBENGINE)
|
||||
set(WITH_WEBENGINE true)
|
||||
endif()
|
||||
|
||||
if(WITH_WEBENGINE)
|
||||
list(APPEND QT_MODULES
|
||||
WebEngineCore
|
||||
|
@ -232,7 +236,9 @@ set(COMMON_SOURCES
|
|||
${APP_SRC_DIR}/callparticipantsmodel.cpp
|
||||
${APP_SRC_DIR}/tipsmodel.cpp
|
||||
${APP_SRC_DIR}/positioning.cpp
|
||||
${APP_SRC_DIR}/currentcall.cpp)
|
||||
${APP_SRC_DIR}/currentcall.cpp
|
||||
${APP_SRC_DIR}/messageparser.cpp
|
||||
${APP_SRC_DIR}/previewengine.cpp)
|
||||
|
||||
set(COMMON_HEADERS
|
||||
${APP_SRC_DIR}/avatarimageprovider.h
|
||||
|
@ -293,16 +299,9 @@ set(COMMON_HEADERS
|
|||
${APP_SRC_DIR}/callparticipantsmodel.h
|
||||
${APP_SRC_DIR}/tipsmodel.h
|
||||
${APP_SRC_DIR}/positioning.h
|
||||
${APP_SRC_DIR}/currentcall.h)
|
||||
|
||||
if(WITH_WEBENGINE)
|
||||
list(APPEND COMMON_SOURCES
|
||||
${APP_SRC_DIR}/previewengine.cpp)
|
||||
add_definitions(-DWITH_WEBENGINE)
|
||||
else()
|
||||
list(APPEND COMMON_SOURCES
|
||||
${APP_SRC_DIR}/nowebengine/previewengine.cpp)
|
||||
endif()
|
||||
${APP_SRC_DIR}/currentcall.h
|
||||
${APP_SRC_DIR}/messageparser.h
|
||||
${APP_SRC_DIR}/htmlparser.h)
|
||||
|
||||
# For libavutil/avframe.
|
||||
set(LIBJAMI_CONTRIB_DIR "${DAEMON_DIR}/contrib")
|
||||
|
@ -494,10 +493,26 @@ if(ENABLE_LIBWRAP)
|
|||
${LIBCLIENT_SRC_DIR}/qtwrapper/instancemanager_wrap.h)
|
||||
endif()
|
||||
|
||||
# SFPM
|
||||
set(BUILD_SFPM_PIC ON CACHE BOOL "enable -fPIC for SFPM" FORCE)
|
||||
add_subdirectory(3rdparty/SortFilterProxyModel)
|
||||
set(SFPM_OBJECTS $<TARGET_OBJECTS:SortFilterProxyModel>)
|
||||
|
||||
# md4c
|
||||
set(BUILD_SHARED_LIBS OFF CACHE BOOL "Don't build shared md4c library" FORCE)
|
||||
add_subdirectory(3rdparty/md4c EXCLUDE_FROM_ALL)
|
||||
list(APPEND CLIENT_LINK_DIRS ${MD4C_BINARY_DIR}/src)
|
||||
list(APPEND CLIENT_INCLUDE_DIRS ${MD4C_SOURCE_DIR}/src)
|
||||
list(APPEND CLIENT_LIBS md4c-html)
|
||||
|
||||
# tidy-html5
|
||||
set(BUILD_SHARED_LIB OFF CACHE BOOL "Don't build shared tidy library" FORCE)
|
||||
set(SUPPORT_CONSOLE_APP OFF CACHE BOOL "Don't build tidy console app" FORCE)
|
||||
add_subdirectory(3rdparty/tidy-html5 EXCLUDE_FROM_ALL)
|
||||
list(APPEND CLIENT_LINK_DIRS ${tidy_BINARY_DIR}/Release)
|
||||
list(APPEND CLIENT_INCLUDE_DIRS ${tidy_SOURCE_DIR}/include)
|
||||
list(APPEND CLIENT_LIBS tidy-static)
|
||||
|
||||
# common executable sources
|
||||
qt_add_executable(
|
||||
${PROJECT_NAME}
|
||||
|
@ -520,9 +535,7 @@ if(MSVC)
|
|||
PROPERTIES
|
||||
WIN32_EXECUTABLE TRUE)
|
||||
|
||||
target_link_libraries(
|
||||
${PROJECT_NAME}
|
||||
PRIVATE
|
||||
list(APPEND CLIENT_LIBS
|
||||
${JAMID_LIB}
|
||||
${GNUTLS_LIB}
|
||||
${LIBCLIENT_NAME}
|
||||
|
@ -559,9 +572,7 @@ if(MSVC)
|
|||
# executable name
|
||||
set_target_properties(${PROJECT_NAME} PROPERTIES OUTPUT_NAME "Jami")
|
||||
elseif (NOT APPLE)
|
||||
target_link_libraries(
|
||||
${PROJECT_NAME}
|
||||
PRIVATE
|
||||
list(APPEND CLIENT_LIBS
|
||||
${QT_LIBS}
|
||||
${LIBCLIENT_NAME}
|
||||
${qrencode}
|
||||
|
@ -693,13 +704,14 @@ else()
|
|||
set(libs ${libs} ${SPARKLE_FRAMEWORK})
|
||||
endif(ENABLE_SPARKLE)
|
||||
target_sources(${PROJECT_NAME} PRIVATE ${resources})
|
||||
target_link_libraries(${PROJECT_NAME} PRIVATE ${libs})
|
||||
FILE(GLOB CONTRIB ${LIBJAMI_CONTRIB_DIR}/apple-darwin/lib/*.a)
|
||||
list(APPEND CLIENT_LIBS ${libs})
|
||||
|
||||
file(GLOB CONTRIB ${LIBJAMI_CONTRIB_DIR}/apple-darwin/lib/*.a)
|
||||
list(APPEND CLIENT_LIBS ${CONTRIB})
|
||||
|
||||
target_link_libraries(${PROJECT_NAME} PRIVATE ${CONTRIB})
|
||||
find_package(Iconv REQUIRED)
|
||||
target_link_libraries(${PROJECT_NAME} PRIVATE Iconv::Iconv)
|
||||
target_link_libraries(${PROJECT_NAME} PRIVATE
|
||||
list(APPEND CLIENT_LIBS Iconv::Iconv)
|
||||
list(APPEND CLIENT_LIBS
|
||||
"-framework AVFoundation"
|
||||
"-framework CoreAudio -framework CoreMedia -framework CoreVideo"
|
||||
"-framework VideoToolbox -framework AudioUnit"
|
||||
|
@ -761,6 +773,10 @@ else()
|
|||
endif()
|
||||
endif()
|
||||
|
||||
target_include_directories(${PROJECT_NAME} PRIVATE ${CLIENT_INCLUDE_DIRS})
|
||||
target_link_directories(${PROJECT_NAME} PRIVATE ${CLIENT_LINK_DIRS})
|
||||
target_link_libraries(${PROJECT_NAME} PRIVATE ${CLIENT_LIBS})
|
||||
|
||||
qt_import_qml_plugins(${PROJECT_NAME})
|
||||
qt_finalize_executable(${PROJECT_NAME})
|
||||
|
||||
|
|
2
extras/packaging/gnu-linux/Jenkinsfile
vendored
2
extras/packaging/gnu-linux/Jenkinsfile
vendored
|
@ -34,6 +34,8 @@
|
|||
// Configuration globals.
|
||||
def SUBMODULES = ['daemon',
|
||||
'3rdparty/SortFilterProxyModel',
|
||||
'3rdparty/md4c',
|
||||
'3rdparty/tidy-html5',
|
||||
'3rdparty/qrencode-win32',
|
||||
'extras/packaging/update/sparkle/Sparkle']
|
||||
def TARGETS = [:]
|
||||
|
|
|
@ -1,100 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2021 SoapBox Innovations Inc.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*/
|
||||
var linkifyStr = (function (linkifyjs) {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
Convert strings of text into linkable HTML text
|
||||
*/
|
||||
|
||||
function escapeText(text) {
|
||||
return text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||
}
|
||||
|
||||
function escapeAttr(href) {
|
||||
return href.replace(/"/g, '"');
|
||||
}
|
||||
|
||||
function attributesToString(attributes) {
|
||||
var result = [];
|
||||
|
||||
for (var attr in attributes) {
|
||||
var val = attributes[attr] + '';
|
||||
result.push(attr + "=\"" + escapeAttr(val) + "\"");
|
||||
}
|
||||
|
||||
return result.join(' ');
|
||||
}
|
||||
|
||||
function defaultRender(_ref) {
|
||||
var tagName = _ref.tagName,
|
||||
attributes = _ref.attributes,
|
||||
content = _ref.content;
|
||||
return "<" + tagName + " " + attributesToString(attributes) + ">" + escapeText(content) + "</" + tagName + ">";
|
||||
}
|
||||
/**
|
||||
* Convert a plan text string to an HTML string with links. Expects that the
|
||||
* given strings does not contain any HTML entities. Use the linkify-html
|
||||
* interface if you need to parse HTML entities.
|
||||
*
|
||||
* @param {string} str string to linkify
|
||||
* @param {import('linkifyjs').Opts} [opts] overridable options
|
||||
* @returns {string}
|
||||
*/
|
||||
|
||||
|
||||
function linkifyStr(str, opts) {
|
||||
if (opts === void 0) {
|
||||
opts = {};
|
||||
}
|
||||
|
||||
opts = new linkifyjs.Options(opts, defaultRender);
|
||||
var tokens = linkifyjs.tokenize(str);
|
||||
var result = [];
|
||||
|
||||
for (var i = 0; i < tokens.length; i++) {
|
||||
var token = tokens[i];
|
||||
|
||||
if (token.t === 'nl' && opts.get('nl2br')) {
|
||||
result.push('<br>\n');
|
||||
} else if (!token.isLink || !opts.check(token)) {
|
||||
result.push(escapeText(token.toString()));
|
||||
} else {
|
||||
result.push(opts.render(token));
|
||||
}
|
||||
}
|
||||
|
||||
return result.join('');
|
||||
}
|
||||
|
||||
if (!String.prototype.linkify) {
|
||||
Object.defineProperty(String.prototype, 'linkify', {
|
||||
writable: false,
|
||||
value: function linkify(options) {
|
||||
return linkifyStr(this, options);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return linkifyStr;
|
||||
|
||||
})(linkify);
|
File diff suppressed because one or more lines are too long
|
@ -1,126 +0,0 @@
|
|||
/* MIT License
|
||||
|
||||
Copyright (c) 2019 Andrej Gajdos
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.*/
|
||||
|
||||
/**
|
||||
* Retrieves the title of a webpage which is used to fill out the preview of a hyperlink
|
||||
* @param doc the DOM of the url that is being previewed
|
||||
* @returns the title of the given webpage
|
||||
*/
|
||||
|
||||
function getTitle(doc){
|
||||
const og_title = doc.querySelector('meta[property="og:title"]')
|
||||
if (og_title !== null && og_title.content.length > 0) {
|
||||
return og_title.content
|
||||
}
|
||||
const twitter_title = doc.querySelector('meta[name="twitter:title"]')
|
||||
if (twitter_title !== null && twitter_title.content.length > 0) {
|
||||
return twitter_title.content
|
||||
}
|
||||
const doc_title = doc.title
|
||||
if (doc_title !== null && doc_title.length > 0) {
|
||||
return doc_title
|
||||
}
|
||||
if (doc.querySelector("h1") !== null){
|
||||
const header_1 = doc.querySelector("h1").innerHTML
|
||||
if (header_1 !== null && header_1.length > 0) {
|
||||
return header_1
|
||||
}
|
||||
}
|
||||
if (doc.querySelector("h2") !== null){
|
||||
const header_2 = doc.querySelector("h2").innerHTML
|
||||
if (header_2 !== null && header_2.length > 0) {
|
||||
return header_2
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtains a description of the webpage for the hyperlink preview
|
||||
* @param doc the DOM of the url that is being previewed
|
||||
* @returns a description of the webpage
|
||||
*/
|
||||
function getDescription(doc){
|
||||
const og_description = doc.querySelector('meta[property="og:description"]')
|
||||
if (og_description !== null && og_description.content.length > 0) {
|
||||
return og_description.content
|
||||
}
|
||||
const twitter_description = doc.querySelector('meta[name="twitter:description"]')
|
||||
if (twitter_description !== null && twitter_description.content.length > 0) {
|
||||
return twitter_description.content
|
||||
}
|
||||
const meta_description = doc.querySelector('meta[name="description"]')
|
||||
if (meta_description !== null && meta_description.content.length > 0) {
|
||||
return meta_description.content
|
||||
}
|
||||
var all_paragraphs = doc.querySelectorAll("p")
|
||||
let first_visible_paragraph = null
|
||||
for (var i = 0; i < all_paragraphs.length; i++) {
|
||||
if (all_paragraphs[i].offsetParent !== null &&
|
||||
!all_paragraphs[i].childElementCount !== 0) {
|
||||
first_visible_paragraph = all_paragraphs[i].textContent
|
||||
break
|
||||
}
|
||||
}
|
||||
return first_visible_paragraph
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the image that represents a webpage.
|
||||
* @param doc the DOM of the url that is being previewed
|
||||
* @returns the image representing the url or null if no such image was found
|
||||
*/
|
||||
function getImage(doc) {
|
||||
const og_image = doc.querySelector('meta[property="og:image"]')
|
||||
if (og_image !== null && og_image.content.length > 0){
|
||||
return og_image.content
|
||||
}
|
||||
const image_rel_link = doc.querySelector('link[rel="image_src"]')
|
||||
if (image_rel_link !== null && image_rel_link.href.length > 0){
|
||||
return image_rel_link.href
|
||||
}
|
||||
const twitter_img = doc.querySelector('meta[name="twitter:image"]')
|
||||
if (twitter_img !== null && twitter_img.content.length > 0) {
|
||||
return twitter_img.content
|
||||
}
|
||||
|
||||
let imgs = Array.from(doc.getElementsByTagName("img"))
|
||||
if (imgs.length > 0) {
|
||||
imgs = imgs.filter(img => {
|
||||
let add_image = true
|
||||
if (img.naturalWidth > img.naturalHeight) {
|
||||
if (img.naturalWidth / img.naturalHeight > 3) {
|
||||
add_image = false
|
||||
}
|
||||
} else {
|
||||
if (img.naturalHeight / img.naturalWidth > 3) {
|
||||
add_image = false
|
||||
}
|
||||
}
|
||||
if (img.naturalHeight <= 50 || img.naturalWidth <= 50) {
|
||||
add_image = false
|
||||
}
|
||||
return add_image
|
||||
})
|
||||
}
|
||||
return null
|
||||
}
|
|
@ -1,93 +0,0 @@
|
|||
_ = new QWebChannel(qt.webChannelTransport, function (channel) {
|
||||
window.jsbridge = channel.objects.jsbridge
|
||||
})
|
||||
|
||||
function log(msg) {
|
||||
window.jsbridge.log(msg)
|
||||
}
|
||||
|
||||
function getPreviewInfo(messageId, url) {
|
||||
var title = null
|
||||
var description = null
|
||||
var image = null
|
||||
var u = new URL(url)
|
||||
if (u.protocol === '') {
|
||||
url = "https://".concat(url)
|
||||
}
|
||||
var domain = (new URL(url))
|
||||
fetch(url, {
|
||||
mode: 'no-cors',
|
||||
headers: {'Set-Cookie': 'SameSite=None; Secure'}
|
||||
}).then(function (response) {
|
||||
const contentType = response.headers.get('content-type');
|
||||
if (!contentType || !contentType.includes('text/html')) {
|
||||
return
|
||||
}
|
||||
return response.body
|
||||
}).then(body => {
|
||||
const reader = body.getReader();
|
||||
|
||||
return new ReadableStream({
|
||||
start(controller) {
|
||||
return pump();
|
||||
|
||||
function pump() {
|
||||
return reader.read().then(({ done, value }) => {
|
||||
// When no more data needs to be consumed, close the stream
|
||||
if (done) {
|
||||
controller.close();
|
||||
return;
|
||||
}
|
||||
if(value.byteLength > 2*1024*1024) {
|
||||
controller.close();
|
||||
return;
|
||||
}
|
||||
|
||||
// Enqueue the next data chunk into our target stream
|
||||
controller.enqueue(value);
|
||||
return pump();
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
}, e => Promise.reject(e))
|
||||
.then(stream => new Response(stream))
|
||||
.then(response => response.text())
|
||||
.then(function (html) {
|
||||
// create DOM from html string
|
||||
var parser = new DOMParser()
|
||||
var doc = parser.parseFromString(html, "text/html")
|
||||
if (!url.includes("twitter.com")){
|
||||
title = getTitle(doc)
|
||||
} else {
|
||||
title = "Twitter. It's what's happening."
|
||||
}
|
||||
image = getImage(doc, url)
|
||||
description = getDescription(doc)
|
||||
domain = (domain.hostname).replace("www.", "")
|
||||
}).catch(function (err) {
|
||||
log("Error occured while fetching document: " + err)
|
||||
}).finally(() => {
|
||||
window.jsbridge.emitInfoReady(messageId, {
|
||||
'title': title,
|
||||
'image': image,
|
||||
'description': description,
|
||||
'url': url,
|
||||
'domain': domain,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function parseMessage(messageId, message, showPreview, color='#0645AD') {
|
||||
var links = linkify.find(message)
|
||||
if (links.length === 0) {
|
||||
return
|
||||
}
|
||||
if (showPreview)
|
||||
getPreviewInfo(messageId, links[0].href)
|
||||
window.jsbridge.emitLinkified(messageId, linkifyStr(message, {
|
||||
attributes: {
|
||||
style: "color:" + color + ";"
|
||||
}
|
||||
}))
|
||||
}
|
|
@ -329,15 +329,15 @@ ApplicationWindow {
|
|||
function onUpdateErrorOccurred(error) {
|
||||
presentUpdateInfoDialog((function () {
|
||||
switch (error) {
|
||||
case NetWorkManager.ACCESS_DENIED:
|
||||
case NetworkManager.ACCESS_DENIED:
|
||||
return JamiStrings.genericError;
|
||||
case NetWorkManager.DISCONNECTED:
|
||||
case NetworkManager.DISCONNECTED:
|
||||
return JamiStrings.networkDisconnected;
|
||||
case NetWorkManager.NETWORK_ERROR:
|
||||
case NetworkManager.NETWORK_ERROR:
|
||||
return JamiStrings.updateNetworkError;
|
||||
case NetWorkManager.SSL_ERROR:
|
||||
case NetworkManager.SSL_ERROR:
|
||||
return JamiStrings.updateSSLError;
|
||||
case NetWorkManager.CANCELED:
|
||||
case NetworkManager.CANCELED:
|
||||
return JamiStrings.updateDownloadCanceled;
|
||||
default:
|
||||
return {};
|
||||
|
|
|
@ -51,10 +51,11 @@ SBSMessageBase {
|
|||
padding: isEmojiOnly ? 0 : JamiTheme.preferredMarginSize
|
||||
anchors.right: isOutgoing ? parent.right : undefined
|
||||
text: {
|
||||
if (LinkifiedBody !== "" && Linkified.length === 0) {
|
||||
MessagesAdapter.parseMessageUrls(Id, Body, UtilsAdapter.getAppValue(Settings.DisplayHyperlinkPreviews), root.colorUrl);
|
||||
if (Body !== "" && ParsedBody.length === 0) {
|
||||
MessagesAdapter.parseMessage(Id, Body, UtilsAdapter.getAppValue(Settings.DisplayHyperlinkPreviews), root.colorUrl, CurrentConversation.color);
|
||||
return ""
|
||||
}
|
||||
return (LinkifiedBody !== "") ? LinkifiedBody : "*(" + JamiStrings.deletedMessage + ")*";
|
||||
return (ParsedBody !== "") ? ParsedBody : "*(" + JamiStrings.deletedMessage + ")*";
|
||||
}
|
||||
horizontalAlignment: Text.AlignLeft
|
||||
|
||||
|
@ -76,7 +77,8 @@ SBSMessageBase {
|
|||
font.pixelSize: isEmojiOnly ? JamiTheme.chatviewEmojiSize : JamiTheme.emojiBubbleSize
|
||||
font.hintingPreference: Font.PreferNoHinting
|
||||
renderType: Text.NativeRendering
|
||||
textFormat: Text.MarkdownText
|
||||
textFormat: Text.RichText
|
||||
clip: true
|
||||
onLinkHovered: root.hoveredLink = hoveredLink
|
||||
onLinkActivated: Qt.openUrlExternally(new URL(hoveredLink))
|
||||
readOnly: true
|
||||
|
@ -227,7 +229,7 @@ SBSMessageBase {
|
|||
renderType: Text.NativeRendering
|
||||
textFormat: TextEdit.RichText
|
||||
color: UtilsAdapter.luma(bubble.color) ? JamiTheme.chatviewTextColorLight : JamiTheme.chatviewTextColorDark
|
||||
visible: LinkPreviewInfo.title !== null
|
||||
visible: LinkPreviewInfo.title.length > 0
|
||||
text: LinkPreviewInfo.title
|
||||
}
|
||||
Label {
|
||||
|
@ -237,7 +239,7 @@ SBSMessageBase {
|
|||
wrapMode: Label.WrapAtWordBoundaryOrAnywhere
|
||||
renderType: Text.NativeRendering
|
||||
textFormat: TextEdit.RichText
|
||||
visible: LinkPreviewInfo.description !== null
|
||||
visible: LinkPreviewInfo.description.length > 0
|
||||
font.underline: root.hoveredLink
|
||||
text: LinkPreviewInfo.description
|
||||
color: root.colorUrl
|
||||
|
@ -263,10 +265,5 @@ SBSMessageBase {
|
|||
duration: 100
|
||||
}
|
||||
}
|
||||
Component.onCompleted: {
|
||||
if (Linkified.length === 0) {
|
||||
MessagesAdapter.parseMessageUrls(Id, Body, UtilsAdapter.getAppValue(Settings.DisplayHyperlinkPreviews), root.colorUrl);
|
||||
}
|
||||
opacity = 1;
|
||||
}
|
||||
Component.onCompleted: opacity = 1;
|
||||
}
|
||||
|
|
|
@ -183,7 +183,7 @@ primaryConnectionChanged(NMClient* nm, GParamSpec*, ConnectivityMonitor* cm)
|
|||
{
|
||||
auto connection = nm_client_get_primary_connection(nm);
|
||||
logConnectionInfo(connection);
|
||||
cm->connectivityChanged();
|
||||
Q_EMIT cm->connectivityChanged();
|
||||
}
|
||||
|
||||
static void
|
||||
|
|
|
@ -20,7 +20,6 @@
|
|||
|
||||
#include <QObject>
|
||||
|
||||
#ifdef Q_OS_WIN
|
||||
class ConnectivityMonitor final : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
@ -34,6 +33,7 @@ Q_SIGNALS:
|
|||
void connectivityChanged();
|
||||
|
||||
private:
|
||||
#ifdef Q_OS_WIN
|
||||
void destroy();
|
||||
|
||||
struct INetworkListManager* pNetworkListManager_;
|
||||
|
@ -41,21 +41,5 @@ private:
|
|||
struct IConnectionPoint* pConnectPoint_;
|
||||
class NetworkEventHandler* netEventHandler_;
|
||||
unsigned long cookie_;
|
||||
};
|
||||
|
||||
#else
|
||||
// TODO: platform implementations should be in the daemon.
|
||||
|
||||
class ConnectivityMonitor final : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit ConnectivityMonitor(QObject* parent = nullptr);
|
||||
~ConnectivityMonitor();
|
||||
|
||||
bool isOnline();
|
||||
|
||||
Q_SIGNALS:
|
||||
void connectivityChanged();
|
||||
};
|
||||
#endif // Q_OS_WIN
|
||||
};
|
||||
|
|
107
src/app/htmlparser.h
Normal file
107
src/app/htmlparser.h
Normal file
|
@ -0,0 +1,107 @@
|
|||
/*
|
||||
* Copyright (C) 2023 Savoir-faire Linux Inc.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QVariantMap>
|
||||
|
||||
#include "tidy.h"
|
||||
#include "tidybuffio.h"
|
||||
|
||||
// This class is used to parse HTML strings. It uses the libtidy library to parse
|
||||
// the HTML and traverse the DOM tree. It can be used to extract a list of tags
|
||||
// and their values from an HTML string.
|
||||
// Currently, it is used to extract the <a> and <pre> tags from a message body,
|
||||
// and in the future it can be used in conjunction with QtNetwork to generate link
|
||||
// previews without having to use QtWebEngine.
|
||||
class HtmlParser : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
HtmlParser(QObject* parent = nullptr)
|
||||
: QObject(parent)
|
||||
{
|
||||
doc_ = tidyCreate();
|
||||
tidyOptSetBool(doc_, TidyQuiet, yes);
|
||||
tidyOptSetBool(doc_, TidyShowWarnings, no);
|
||||
}
|
||||
|
||||
~HtmlParser()
|
||||
{
|
||||
tidyRelease(doc_);
|
||||
}
|
||||
|
||||
bool parseHtmlString(const QString& html)
|
||||
{
|
||||
return tidyParseString(doc_, html.toLocal8Bit().data()) >= 0;
|
||||
}
|
||||
|
||||
using TagInfoList = QMap<TidyTagId, QList<QString>>;
|
||||
|
||||
// A function that traverses the DOM tree and fills a QVariantMap with a list
|
||||
// of the tags and their values. The result is structured as follows:
|
||||
// {tagId1: ["tagValue1", "tagValue2", ...],
|
||||
// tagId: ["tagValue1", "tagValue2", ...],
|
||||
// ... }
|
||||
TagInfoList getTags(QList<TidyTagId> tags, int maxDepth = -1)
|
||||
{
|
||||
TagInfoList result;
|
||||
traverseNode(
|
||||
tidyGetRoot(doc_),
|
||||
tags,
|
||||
[&result](const QString& value, TidyTagId tag) { result[tag].append(value); },
|
||||
maxDepth);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
QString getFirstTagValue(TidyTagId tag, int maxDepth = -1)
|
||||
{
|
||||
QString result;
|
||||
traverseNode(
|
||||
tidyGetRoot(doc_),
|
||||
{tag},
|
||||
[&result](const QString& value, TidyTagId) { result = value; },
|
||||
maxDepth);
|
||||
return result;
|
||||
}
|
||||
|
||||
private:
|
||||
void traverseNode(TidyNode node,
|
||||
QList<TidyTagId> tags,
|
||||
const std::function<void(const QString&, TidyTagId)>& cb,
|
||||
int depth = -1)
|
||||
{
|
||||
TidyBuffer nodeValue = {};
|
||||
for (auto tag : tags) {
|
||||
if (tidyNodeGetId(node) == tag && tidyNodeGetText(doc_, node, &nodeValue) == yes && cb) {
|
||||
cb(QString::fromLocal8Bit(nodeValue.bp), tag);
|
||||
if (depth != -1 && --depth == 0) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Traverse the children of the current node.
|
||||
for (TidyNode child = tidyGetChild(node); child; child = tidyGetNext(child)) {
|
||||
traverseNode(child, tags, cb, depth);
|
||||
}
|
||||
}
|
||||
|
||||
TidyDoc doc_;
|
||||
};
|
|
@ -122,7 +122,7 @@ MainApplication::init()
|
|||
connectivityMonitor_.reset(new ConnectivityMonitor(this));
|
||||
settingsManager_.reset(new AppSettingsManager(this));
|
||||
systemTray_.reset(new SystemTray(settingsManager_.get(), this));
|
||||
previewEngine_.reset(new PreviewEngine(this));
|
||||
previewEngine_.reset(new PreviewEngine(connectivityMonitor_.get(), this));
|
||||
|
||||
QObject::connect(settingsManager_.get(),
|
||||
&AppSettingsManager::retranslate,
|
||||
|
|
|
@ -31,7 +31,7 @@ Rectangle {
|
|||
property var body: {
|
||||
if (MessagesAdapter.editId === "")
|
||||
return "";
|
||||
return MessagesAdapter.dataForInteraction(MessagesAdapter.editId, MessageList.LinkifiedBody);
|
||||
return MessagesAdapter.dataForInteraction(MessagesAdapter.editId, MessageList.ParsedBody);
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
|
|
184
src/app/messageparser.cpp
Normal file
184
src/app/messageparser.cpp
Normal file
|
@ -0,0 +1,184 @@
|
|||
/*
|
||||
* Copyright (C) 2023 Savoir-faire Linux Inc.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "messageparser.h"
|
||||
|
||||
#include "previewengine.h"
|
||||
#include "htmlparser.h"
|
||||
|
||||
#include <QRegularExpression>
|
||||
#include <QtConcurrent>
|
||||
|
||||
#include "md4c-html.h"
|
||||
|
||||
MessageParser::MessageParser(PreviewEngine* previewEngine, QObject* parent)
|
||||
: QObject(parent)
|
||||
, previewEngine_(previewEngine)
|
||||
, htmlParser_(new HtmlParser(this))
|
||||
, threadPool_(new QThreadPool(this))
|
||||
{
|
||||
threadPool_->setMaxThreadCount(1);
|
||||
connect(previewEngine_, &PreviewEngine::infoReady, this, &MessageParser::linkInfoReady);
|
||||
}
|
||||
|
||||
void
|
||||
MessageParser::parseMessage(const QString& messageId,
|
||||
const QString& msg,
|
||||
bool previewLinks,
|
||||
const QColor& linkColor,
|
||||
const QColor& backgroundColor)
|
||||
{
|
||||
// Run everything here on a separate thread.
|
||||
threadPool_->start(
|
||||
[this, messageId, md = msg, previewLinks, linkColor, backgroundColor]() mutable -> void {
|
||||
preprocessMarkdown(md);
|
||||
auto html = markdownToHtml(md.toUtf8().constData());
|
||||
|
||||
// Now that we have the HTML, we can parse it to get a list of tags and their values.
|
||||
// We are only interested in the <a> and <pre> tags.
|
||||
htmlParser_->parseHtmlString(html);
|
||||
auto tagsMap = htmlParser_->getTags({TidyTag_A, TidyTag_DEL, TidyTag_PRE});
|
||||
|
||||
static QString styleTag("<style>%1</style>");
|
||||
QString style;
|
||||
|
||||
// Check for any <pre> tags. If there are any, we need to:
|
||||
// 1. add some CSS to color them.
|
||||
// 2. add some CSS to make them wrap.
|
||||
if (tagsMap.contains(TidyTag_PRE)) {
|
||||
auto textColor = (0.2126 * backgroundColor.red() + 0.7152 * backgroundColor.green()
|
||||
+ 0.0722 * backgroundColor.blue())
|
||||
< 153
|
||||
? QColor(255, 255, 255)
|
||||
: QColor(0, 0, 0);
|
||||
style += QString("pre,code{background-color:%1;"
|
||||
"color:%2;white-space:pre-wrap;}")
|
||||
.arg(backgroundColor.name(), textColor.name());
|
||||
}
|
||||
|
||||
// md4c makes DEL tags instead of S tags for ~~strikethrough-text~~,
|
||||
// so we need to style the DEL tag content.
|
||||
if (tagsMap.contains(TidyTag_DEL)) {
|
||||
style += QString("del{text-decoration:line-through;}");
|
||||
}
|
||||
|
||||
// Check for any <a> tags. If there are any, we need to:
|
||||
// 1. add some CSS to color them.
|
||||
// 2. parse them to get a preview IF the user has enabled link previews.
|
||||
if (tagsMap.contains(TidyTag_A)) {
|
||||
style += QString("a{color:%1;}").arg(linkColor.name());
|
||||
|
||||
// Update the UI before we start parsing the link.
|
||||
html.prepend(QString(styleTag).arg(style));
|
||||
Q_EMIT messageParsed(messageId, html);
|
||||
|
||||
// If the user has enabled link previews, then we need to generate the link preview.
|
||||
if (previewLinks) {
|
||||
// Get the first link in the message.
|
||||
auto anchorTag = tagsMap[TidyTag_A].first();
|
||||
static QRegularExpression hrefRegex("href=\"(.*?)\"");
|
||||
auto match = hrefRegex.match(anchorTag);
|
||||
if (match.hasMatch()) {
|
||||
Q_EMIT previewEngine_->parseLink(messageId, match.captured(1));
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// If the message didn't contain any links, then we can just update the UI.
|
||||
html.prepend(QString(styleTag).arg(style));
|
||||
Q_EMIT messageParsed(messageId, html);
|
||||
});
|
||||
}
|
||||
|
||||
void
|
||||
MessageParser::preprocessMarkdown(QString& markdown)
|
||||
{
|
||||
// Match all instances of the linefeed character.
|
||||
static QRegularExpression newlineRegex("\n");
|
||||
static const QString newline = " \n";
|
||||
|
||||
// Replace all instances of the linefeed character with 2 spaces + a linefeed character
|
||||
// in order to force a line break in the HTML.
|
||||
// Note: we should only do this for non-code fenced blocks.
|
||||
static QRegularExpression codeFenceRe("`{1,3}([\\s\\S]*?)`{1,3}");
|
||||
auto match = codeFenceRe.globalMatch(markdown);
|
||||
|
||||
// If there are no code blocks, then we can just replace all linefeeds with 2 spaces
|
||||
// + a linefeed, and we're done.
|
||||
if (!match.hasNext()) {
|
||||
markdown.replace(newlineRegex, newline);
|
||||
return;
|
||||
}
|
||||
|
||||
// Save each block of text and code. The text blocks will be
|
||||
// processed for line breaks and the code blocks will be left
|
||||
// as is.
|
||||
enum BlockType { Text, Code };
|
||||
QVector<QPair<BlockType, QString>> codeBlocks;
|
||||
|
||||
int start = 0;
|
||||
while (match.hasNext()) {
|
||||
auto m = match.next();
|
||||
auto nonCodelength = m.capturedStart() - start;
|
||||
if (nonCodelength) {
|
||||
codeBlocks.push_back({Text, markdown.mid(start, nonCodelength)});
|
||||
}
|
||||
codeBlocks.push_back({Code, m.captured(0)});
|
||||
start = m.capturedStart() + m.capturedLength();
|
||||
}
|
||||
// There may be some text after the last code block.
|
||||
if (start < markdown.size()) {
|
||||
codeBlocks.push_back({Text, markdown.mid(start)});
|
||||
}
|
||||
|
||||
// Now we can process the text blocks.
|
||||
markdown.clear();
|
||||
for (auto& block : codeBlocks) {
|
||||
if (block.first == Text) {
|
||||
// Replace all newlines with two spaces and a newline.
|
||||
block.second.replace(newlineRegex, newline);
|
||||
}
|
||||
markdown += block.second;
|
||||
}
|
||||
}
|
||||
|
||||
// A callback function that will be called by the md4c library (`md_html`) to output the HTML.
|
||||
static void
|
||||
htmlChunkCb(const MD_CHAR* data, MD_SIZE data_size, void* userData)
|
||||
{
|
||||
QByteArray* array = static_cast<QByteArray*>(userData);
|
||||
if (data_size > 0) {
|
||||
array->append(data, int(data_size));
|
||||
}
|
||||
};
|
||||
|
||||
QString
|
||||
MessageParser::markdownToHtml(const char* markdown)
|
||||
{
|
||||
static auto md_flags = MD_FLAG_PERMISSIVEAUTOLINKS | MD_FLAG_NOINDENTEDCODEBLOCKS
|
||||
| MD_FLAG_TASKLISTS | MD_FLAG_STRIKETHROUGH | MD_FLAG_UNDERLINE;
|
||||
size_t data_len = strlen(markdown);
|
||||
if (data_len <= 0) {
|
||||
return QString();
|
||||
} else {
|
||||
QByteArray array;
|
||||
int result = md_html(markdown, MD_SIZE(data_len), &htmlChunkCb, &array, md_flags, 0);
|
||||
return result == 0 ? QString::fromUtf8(array) : QString();
|
||||
}
|
||||
}
|
77
src/app/messageparser.h
Normal file
77
src/app/messageparser.h
Normal file
|
@ -0,0 +1,77 @@
|
|||
/*
|
||||
* Copyright (C) 2023 Savoir-faire Linux Inc.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QColor>
|
||||
#include <QThreadPool>
|
||||
|
||||
class PreviewEngine;
|
||||
class HtmlParser;
|
||||
|
||||
// This class is used to parse messages and encapsulate the logic that
|
||||
// prepares a message for display in the UI. The basic steps are:
|
||||
// 1. preprocess the markdown message (e.g. handle line breaks)
|
||||
// 2. transform markdown syntax into HTML
|
||||
// 3. generate previews for the first link in the message (if any)
|
||||
// 4. add the appropriate CSS classes to the HTML (e.g. for links, code blocks, etc.)
|
||||
//
|
||||
// Step 3. is done asynchronously, so the message is displayed as soon as possible
|
||||
// and the preview is added later.
|
||||
class MessageParser final : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_DISABLE_COPY(MessageParser)
|
||||
public:
|
||||
// Create a new MessageParser instance. We take an instance of PreviewEngine.
|
||||
explicit MessageParser(PreviewEngine* previewEngine, QObject* parent = nullptr);
|
||||
~MessageParser() = default;
|
||||
|
||||
// Parse the message. This will emit the messageParsed signal when the
|
||||
// message is ready to be displayed.
|
||||
void parseMessage(const QString& messageId,
|
||||
const QString& msg,
|
||||
bool previewLinks,
|
||||
const QColor& linkColor,
|
||||
const QColor& backgroundColor);
|
||||
|
||||
// Emitted when the message is ready to be displayed.
|
||||
Q_SIGNAL void messageParsed(const QString& msgId, const QString& msg);
|
||||
|
||||
// Emitted when the message preview is ready to be displayed.
|
||||
Q_SIGNAL void linkInfoReady(const QString& msgId, const QVariantMap& info);
|
||||
|
||||
private:
|
||||
// Preprocess the markdown message (e.g. handle line breaks).
|
||||
void preprocessMarkdown(QString& markdown);
|
||||
|
||||
// Transform markdown syntax into HTML.
|
||||
QString markdownToHtml(const char* markdown);
|
||||
|
||||
// Generate a preview for the given link, then emit the messageParsed signal.
|
||||
void generatePreview(const QString& msgId, const QString& link);
|
||||
|
||||
// The PreviewEngine instance used to generate previews.
|
||||
PreviewEngine* previewEngine_;
|
||||
|
||||
// An instance of HtmlParser used to parse HTML.
|
||||
HtmlParser* htmlParser_;
|
||||
|
||||
// Used to queue parse operations.
|
||||
QThreadPool* threadPool_;
|
||||
};
|
|
@ -25,6 +25,7 @@
|
|||
|
||||
#include "appsettingsmanager.h"
|
||||
#include "qtutils.h"
|
||||
#include "messageparser.h"
|
||||
|
||||
#include <api/datatransfermodel.h>
|
||||
|
||||
|
@ -48,7 +49,7 @@ MessagesAdapter::MessagesAdapter(AppSettingsManager* settingsManager,
|
|||
QObject* parent)
|
||||
: QmlAdapterBase(instance, parent)
|
||||
, settingsManager_(settingsManager)
|
||||
, previewEngine_(previewEngine)
|
||||
, messageParser_(new MessageParser(previewEngine, this))
|
||||
, filteredMsgListModel_(new FilteredMsgListModel(this))
|
||||
, mediaInteractions_(std::make_unique<MessageListModel>())
|
||||
, timestampTimer_(new QTimer(this))
|
||||
|
@ -71,8 +72,8 @@ MessagesAdapter::MessagesAdapter(AppSettingsManager* settingsManager,
|
|||
set_mediaMessageListModel(QVariant::fromValue(mediaInteractions_.get()));
|
||||
});
|
||||
|
||||
connect(previewEngine_, &PreviewEngine::infoReady, this, &MessagesAdapter::onPreviewInfoReady);
|
||||
connect(previewEngine_, &PreviewEngine::linkified, this, &MessagesAdapter::onMessageLinkified);
|
||||
connect(messageParser_, &MessageParser::messageParsed, this, &MessagesAdapter::onMessageParsed);
|
||||
connect(messageParser_, &MessageParser::linkInfoReady, this, &MessagesAdapter::onLinkInfoReady);
|
||||
|
||||
connect(timestampTimer_, &QTimer::timeout, this, &MessagesAdapter::timestampUpdated);
|
||||
timestampTimer_->start(timestampUpdateIntervalMs_);
|
||||
|
@ -445,6 +446,24 @@ MessagesAdapter::onNewInteraction(const QString& convUid,
|
|||
}
|
||||
}
|
||||
|
||||
void
|
||||
MessagesAdapter::onMessageParsed(const QString& messageId, const QString& parsed)
|
||||
{
|
||||
const QString& convId = lrcInstance_->get_selectedConvUid();
|
||||
const QString& accId = lrcInstance_->get_currentAccountId();
|
||||
auto& conversation = lrcInstance_->getConversationFromConvUid(convId, accId);
|
||||
conversation.interactions->setParsedMessage(messageId, parsed);
|
||||
}
|
||||
|
||||
void
|
||||
MessagesAdapter::onLinkInfoReady(const QString& messageId, const QVariantMap& info)
|
||||
{
|
||||
const QString& convId = lrcInstance_->get_selectedConvUid();
|
||||
const QString& accId = lrcInstance_->get_currentAccountId();
|
||||
auto& conversation = lrcInstance_->getConversationFromConvUid(convId, accId);
|
||||
conversation.interactions->addHyperlinkInfo(messageId, info);
|
||||
}
|
||||
|
||||
void
|
||||
MessagesAdapter::acceptInvitation(const QString& convId)
|
||||
{
|
||||
|
@ -540,15 +559,6 @@ MessagesAdapter::removeContact(const QString& convUid, bool banContact)
|
|||
accInfo.contactModel->removeContact(contactUri, banContact);
|
||||
}
|
||||
|
||||
void
|
||||
MessagesAdapter::onPreviewInfoReady(QString messageId, QVariantMap info)
|
||||
{
|
||||
const QString& convId = lrcInstance_->get_selectedConvUid();
|
||||
const QString& accId = lrcInstance_->get_currentAccountId();
|
||||
auto& conversation = lrcInstance_->getConversationFromConvUid(convId, accId);
|
||||
conversation.interactions->addHyperlinkInfo(messageId, info);
|
||||
}
|
||||
|
||||
void
|
||||
MessagesAdapter::onConversationMessagesLoaded(uint32_t loadingRequestId, const QString& convId)
|
||||
{
|
||||
|
@ -558,21 +568,13 @@ MessagesAdapter::onConversationMessagesLoaded(uint32_t loadingRequestId, const Q
|
|||
}
|
||||
|
||||
void
|
||||
MessagesAdapter::parseMessageUrls(const QString& messageId,
|
||||
const QString& msg,
|
||||
bool showPreview,
|
||||
QColor color)
|
||||
MessagesAdapter::parseMessage(const QString& msgId,
|
||||
const QString& msg,
|
||||
bool showPreview,
|
||||
const QColor& linkColor,
|
||||
const QColor& backgroundColor)
|
||||
{
|
||||
previewEngine_->parseMessage(messageId, msg, showPreview, color);
|
||||
}
|
||||
|
||||
void
|
||||
MessagesAdapter::onMessageLinkified(const QString& messageId, const QString& linkified)
|
||||
{
|
||||
const QString& convId = lrcInstance_->get_selectedConvUid();
|
||||
const QString& accId = lrcInstance_->get_currentAccountId();
|
||||
auto& conversation = lrcInstance_->getConversationFromConvUid(convId, accId);
|
||||
conversation.interactions->linkifyMessage(messageId, linkified);
|
||||
messageParser_->parseMessage(msgId, msg, showPreview, linkColor, backgroundColor);
|
||||
}
|
||||
|
||||
void
|
||||
|
|
|
@ -28,6 +28,9 @@
|
|||
|
||||
#include <QSortFilterProxyModel>
|
||||
|
||||
class AppSettingsManager;
|
||||
class MessageParser;
|
||||
|
||||
class FilteredMsgListModel final : public QSortFilterProxyModel
|
||||
{
|
||||
Q_OBJECT
|
||||
|
@ -50,8 +53,6 @@ public:
|
|||
};
|
||||
};
|
||||
|
||||
class AppSettingsManager;
|
||||
|
||||
class MessagesAdapter final : public QmlAdapterBase
|
||||
{
|
||||
Q_OBJECT
|
||||
|
@ -74,7 +75,6 @@ Q_SIGNALS:
|
|||
void newMessageBarPlaceholderText(QString& placeholderText);
|
||||
void newFilePasted(QString filePath);
|
||||
void newTextPasted();
|
||||
void previewInformationToQML(QString messageId, QStringList previewInformation);
|
||||
void moreMessagesLoaded(qint32 loadingRequestId);
|
||||
void timestampUpdated();
|
||||
void fileCopied(const QString& dest);
|
||||
|
@ -123,10 +123,11 @@ protected:
|
|||
Q_INVOKABLE QString getFormattedDay(const quint64 timestamp);
|
||||
Q_INVOKABLE QString getFormattedTime(const quint64 timestamp);
|
||||
Q_INVOKABLE QString getBestFormattedDate(const quint64 timestamp);
|
||||
Q_INVOKABLE void parseMessageUrls(const QString& messageId,
|
||||
const QString& msg,
|
||||
bool showPreview,
|
||||
QColor color = "#0645AD");
|
||||
Q_INVOKABLE void parseMessage(const QString& msgId,
|
||||
const QString& msg,
|
||||
bool previewLinks,
|
||||
const QColor& linkColor = QColor(0x06, 0x45, 0xad),
|
||||
const QColor& backgroundColor = QColor(0x0, 0x0, 0x0));
|
||||
Q_INVOKABLE void onPaste();
|
||||
Q_INVOKABLE int getIndexOfMessage(const QString& messageId) const;
|
||||
Q_INVOKABLE QString getStatusString(int status);
|
||||
|
@ -147,9 +148,9 @@ private Q_SLOTS:
|
|||
void onNewInteraction(const QString& convUid,
|
||||
const QString& interactionId,
|
||||
const interaction::Info& interaction);
|
||||
void onPreviewInfoReady(QString messageIndex, QVariantMap urlInMessage);
|
||||
void onMessageParsed(const QString& messageId, const QString& parsed);
|
||||
void onLinkInfoReady(const QString& messageIndex, const QVariantMap& info);
|
||||
void onConversationMessagesLoaded(uint32_t requestId, const QString& convId);
|
||||
void onMessageLinkified(const QString& messageId, const QString& linkified);
|
||||
void onComposingStatusChanged(const QString& convId,
|
||||
const QString& contactUri,
|
||||
bool isComposing);
|
||||
|
@ -160,7 +161,8 @@ private:
|
|||
QList<QString> conversationTypersUrlToName(const QSet<QString>& typersSet);
|
||||
|
||||
AppSettingsManager* settingsManager_;
|
||||
PreviewEngine* previewEngine_;
|
||||
MessageParser* messageParser_;
|
||||
|
||||
FilteredMsgListModel* filteredMsgListModel_;
|
||||
|
||||
static constexpr const int loadChunkSize_ {20};
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
/*!
|
||||
/*
|
||||
* Copyright (C) 2019-2023 Savoir-faire Linux Inc.
|
||||
* Author: Mingrui Zhang <mingrui.zhang@savoirfairelinux.com>
|
||||
* Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
@ -20,23 +18,29 @@
|
|||
#include "networkmanager.h"
|
||||
|
||||
#include "connectivitymonitor.h"
|
||||
#include "utils.h"
|
||||
|
||||
#include <QMetaEnum>
|
||||
#include <QtNetwork>
|
||||
|
||||
NetWorkManager::NetWorkManager(ConnectivityMonitor* cm, QObject* parent)
|
||||
NetworkManager::NetworkManager(ConnectivityMonitor* cm, QObject* parent)
|
||||
: QObject(parent)
|
||||
, manager_(new QNetworkAccessManager(this))
|
||||
, reply_(nullptr)
|
||||
, connectivityMonitor_(cm)
|
||||
, lastConnectionState_(cm->isOnline())
|
||||
{
|
||||
Q_EMIT statusChanged(GetStatus::IDLE);
|
||||
|
||||
connect(connectivityMonitor_, &ConnectivityMonitor::connectivityChanged, [this] {
|
||||
cancelRequest();
|
||||
|
||||
#if QT_CONFIG(ssl)
|
||||
connect(manager_,
|
||||
&QNetworkAccessManager::sslErrors,
|
||||
this,
|
||||
[this](QNetworkReply* reply, const QList<QSslError>& errors) {
|
||||
Q_UNUSED(reply);
|
||||
Q_FOREACH (const QSslError& error, errors) {
|
||||
qWarning() << Q_FUNC_INFO << error.errorString();
|
||||
Q_EMIT errorOccured(GetError::SSL_ERROR, error.errorString());
|
||||
}
|
||||
});
|
||||
#endif
|
||||
connect(connectivityMonitor_, &ConnectivityMonitor::connectivityChanged, this, [this] {
|
||||
auto connected = connectivityMonitor_->isOnline();
|
||||
if (connected && !lastConnectionState_) {
|
||||
manager_->deleteLater();
|
||||
|
@ -48,126 +52,16 @@ NetWorkManager::NetWorkManager(ConnectivityMonitor* cm, QObject* parent)
|
|||
}
|
||||
|
||||
void
|
||||
NetWorkManager::get(const QUrl& url, const DoneCallBack& doneCb, const QString& path)
|
||||
NetworkManager::sendGetRequest(const QUrl& url,
|
||||
std::function<void(const QByteArray&)> onDoneCallback)
|
||||
{
|
||||
if (!connectivityMonitor_->isOnline()) {
|
||||
Q_EMIT errorOccured(GetError::DISCONNECTED);
|
||||
return;
|
||||
}
|
||||
|
||||
if (reply_ && reply_->isRunning()) {
|
||||
qWarning() << Q_FUNC_INFO << "currently downloading";
|
||||
return;
|
||||
} else if (url.isEmpty()) {
|
||||
qWarning() << Q_FUNC_INFO << "missing url";
|
||||
return;
|
||||
}
|
||||
|
||||
if (!path.isEmpty()) {
|
||||
QFileInfo fileInfo(url.path());
|
||||
QString fileName = fileInfo.fileName();
|
||||
|
||||
file_.reset(new QFile(path + "/" + fileName));
|
||||
if (!file_->open(QIODevice::WriteOnly)) {
|
||||
Q_EMIT errorOccured(GetError::ACCESS_DENIED);
|
||||
file_.reset(nullptr);
|
||||
return;
|
||||
auto reply = manager_->get(QNetworkRequest(url));
|
||||
QObject::connect(reply, &QNetworkReply::finished, this, [reply, onDoneCallback]() {
|
||||
if (reply->error() == QNetworkReply::NoError) {
|
||||
onDoneCallback(reply->readAll());
|
||||
} else {
|
||||
onDoneCallback(reply->errorString().toUtf8());
|
||||
}
|
||||
}
|
||||
|
||||
QNetworkRequest request(url);
|
||||
reply_ = manager_->get(request);
|
||||
|
||||
Q_EMIT statusChanged(GetStatus::STARTED);
|
||||
|
||||
connect(reply_,
|
||||
QOverload<QNetworkReply::NetworkError>::of(&QNetworkReply::errorOccurred),
|
||||
[this, doneCb, path](QNetworkReply::NetworkError error) {
|
||||
reply_->disconnect();
|
||||
reset(true);
|
||||
qWarning() << Q_FUNC_INFO << "NetworkError: "
|
||||
<< QMetaEnum::fromType<QNetworkReply::NetworkError>().valueToKey(error);
|
||||
Q_EMIT errorOccured(GetError::NETWORK_ERROR);
|
||||
});
|
||||
|
||||
connect(reply_, &QNetworkReply::finished, [this, doneCb, path]() {
|
||||
reply_->disconnect();
|
||||
QString response = {};
|
||||
if (path.isEmpty())
|
||||
response = QString(reply_->readAll());
|
||||
reset(!path.isEmpty());
|
||||
Q_EMIT statusChanged(GetStatus::FINISHED);
|
||||
if (doneCb)
|
||||
doneCb(response);
|
||||
reply->deleteLater();
|
||||
});
|
||||
|
||||
connect(reply_,
|
||||
&QNetworkReply::downloadProgress,
|
||||
this,
|
||||
&NetWorkManager::downloadProgressChanged);
|
||||
|
||||
connect(reply_, &QNetworkReply::readyRead, this, &NetWorkManager::onHttpReadyRead);
|
||||
|
||||
#if QT_CONFIG(ssl)
|
||||
connect(reply_,
|
||||
SIGNAL(sslErrors(const QList<QSslError>&)),
|
||||
this,
|
||||
SLOT(onSslErrors(QList<QSslError>)),
|
||||
Qt::UniqueConnection);
|
||||
#endif
|
||||
}
|
||||
|
||||
void
|
||||
NetWorkManager::reset(bool flush)
|
||||
{
|
||||
reply_->deleteLater();
|
||||
reply_ = nullptr;
|
||||
if (file_ && flush) {
|
||||
file_->flush();
|
||||
file_->close();
|
||||
file_.reset(nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
NetWorkManager::onSslErrors(const QList<QSslError>& sslErrors)
|
||||
{
|
||||
#if QT_CONFIG(ssl)
|
||||
reply_->disconnect();
|
||||
reset(true);
|
||||
|
||||
QString errorsString;
|
||||
for (const QSslError& error : sslErrors) {
|
||||
if (errorsString.length() > 0) {
|
||||
errorsString += "\n";
|
||||
}
|
||||
errorsString += error.errorString();
|
||||
}
|
||||
Q_EMIT errorOccured(GetError::SSL_ERROR, errorsString);
|
||||
return;
|
||||
#else
|
||||
Q_UNUSED(sslErrors);
|
||||
#endif
|
||||
}
|
||||
|
||||
void
|
||||
NetWorkManager::onHttpReadyRead()
|
||||
{
|
||||
/*
|
||||
* This slot gets called every time the QNetworkReply has new data.
|
||||
* We read all of its new data and write it into the file.
|
||||
* That way we use less RAM than when reading it at the finished()
|
||||
* signal of the QNetworkReply
|
||||
*/
|
||||
if (file_)
|
||||
file_->write(reply_->readAll());
|
||||
}
|
||||
|
||||
void
|
||||
NetWorkManager::cancelRequest()
|
||||
{
|
||||
if (reply_) {
|
||||
reply_->abort();
|
||||
Q_EMIT errorOccured(GetError::CANCELED);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
/*!
|
||||
/*
|
||||
* Copyright (C) 2019-2023 Savoir-faire Linux Inc.
|
||||
* Author: Mingrui Zhang <mingrui.zhang@savoirfairelinux.com>
|
||||
* Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
@ -27,50 +25,26 @@
|
|||
class QNetworkAccessManager;
|
||||
class ConnectivityMonitor;
|
||||
|
||||
class NetWorkManager : public QObject
|
||||
class NetworkManager : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit NetWorkManager(ConnectivityMonitor* cm, QObject* parent = nullptr);
|
||||
virtual ~NetWorkManager() = default;
|
||||
|
||||
enum GetStatus { IDLE, STARTED, FINISHED };
|
||||
explicit NetworkManager(ConnectivityMonitor* cm, QObject* parent = nullptr);
|
||||
virtual ~NetworkManager() = default;
|
||||
|
||||
enum GetError { DISCONNECTED, NETWORK_ERROR, ACCESS_DENIED, SSL_ERROR, CANCELED };
|
||||
Q_ENUM(GetError)
|
||||
|
||||
using DoneCallBack = std::function<void(const QString&)>;
|
||||
|
||||
/*!
|
||||
* using qt get request to store the reply in file
|
||||
* @param url - network address
|
||||
* @param doneCb - done callback
|
||||
* @param path - optional file saving path, if empty
|
||||
* a string will be passed as the second paramter of doneCb
|
||||
*/
|
||||
void get(const QUrl& url, const DoneCallBack& doneCb = {}, const QString& path = {});
|
||||
|
||||
/*!
|
||||
* manually abort the current request
|
||||
*/
|
||||
Q_INVOKABLE void cancelRequest();
|
||||
void sendGetRequest(const QUrl& url, std::function<void(const QByteArray&)> onDoneCallback);
|
||||
|
||||
Q_SIGNALS:
|
||||
void statusChanged(GetStatus error);
|
||||
void downloadProgressChanged(qint64 bytesRead, qint64 totalBytes);
|
||||
void errorOccured(GetError error, const QString& msg = {});
|
||||
|
||||
private Q_SLOTS:
|
||||
void onSslErrors(const QList<QSslError>& sslErrors);
|
||||
void onHttpReadyRead();
|
||||
protected:
|
||||
QNetworkAccessManager* manager_;
|
||||
|
||||
private:
|
||||
void reset(bool flush = true);
|
||||
|
||||
QNetworkAccessManager* manager_;
|
||||
QNetworkReply* reply_;
|
||||
QScopedPointer<QFile> file_;
|
||||
ConnectivityMonitor* connectivityMonitor_;
|
||||
bool lastConnectionState_;
|
||||
};
|
||||
Q_DECLARE_METATYPE(NetWorkManager*)
|
||||
Q_DECLARE_METATYPE(NetworkManager*)
|
||||
|
|
|
@ -1,52 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2022-2023 Savoir-faire Linux Inc.
|
||||
* Author: Kateryna Kostiuk <kateryna.kostiuk@savoirfairelinux.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "previewengine.h"
|
||||
|
||||
struct PreviewEngine::Impl : public QObject
|
||||
{
|
||||
Impl(PreviewEngine&)
|
||||
: QObject(nullptr)
|
||||
{}
|
||||
};
|
||||
|
||||
PreviewEngine::PreviewEngine(QObject* parent)
|
||||
: QObject(parent)
|
||||
, pimpl_(std::make_unique<Impl>(*this))
|
||||
{}
|
||||
|
||||
PreviewEngine::~PreviewEngine() {}
|
||||
|
||||
void
|
||||
PreviewEngine::parseMessage(const QString&, const QString&, bool, QColor)
|
||||
{}
|
||||
|
||||
void
|
||||
PreviewEngine::log(const QString&)
|
||||
{}
|
||||
|
||||
void
|
||||
PreviewEngine::emitInfoReady(const QString&, const QVariantMap&)
|
||||
{}
|
||||
|
||||
void
|
||||
PreviewEngine::emitLinkified(const QString&, const QString&)
|
||||
{}
|
||||
|
||||
#include "moc_previewengine.cpp"
|
||||
#include "previewengine.moc"
|
|
@ -84,7 +84,7 @@ UpdateManager::UpdateManager(const QString& url,
|
|||
ConnectivityMonitor* cm,
|
||||
LRCInstance* instance,
|
||||
QObject* parent)
|
||||
: NetWorkManager(cm, parent)
|
||||
: NetworkManager(cm, parent)
|
||||
, pimpl_(std::make_unique<Impl>())
|
||||
{}
|
||||
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
/*
|
||||
* Copyright (C) 2021-2023 Savoir-faire Linux Inc.
|
||||
* Author: Trevor Tabah <trevor.tabah@savoirfairelinux.com>
|
||||
* Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
@ -19,101 +17,103 @@
|
|||
|
||||
#include "previewengine.h"
|
||||
|
||||
#include "utils.h"
|
||||
#include <QRegularExpression>
|
||||
|
||||
#include <QWebEngineScript>
|
||||
#include <QWebEngineProfile>
|
||||
#include <QWebEngineSettings>
|
||||
|
||||
#include <QtWebChannel>
|
||||
#include <QWebEnginePage>
|
||||
|
||||
struct PreviewEngine::Impl : public QWebEnginePage
|
||||
static QString
|
||||
getInnerHtml(const QString& tag)
|
||||
{
|
||||
public:
|
||||
PreviewEngine& parent_;
|
||||
QWebChannel* channel_;
|
||||
|
||||
Impl(PreviewEngine& parent)
|
||||
: QWebEnginePage((QObject*) nullptr)
|
||||
, parent_(parent)
|
||||
{
|
||||
QWebEngineProfile* profile = QWebEngineProfile::defaultProfile();
|
||||
|
||||
QDir dataDir(QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation));
|
||||
dataDir.cdUp();
|
||||
auto cachePath = dataDir.absolutePath() + "/jami";
|
||||
profile->setCachePath(cachePath);
|
||||
profile->setPersistentStoragePath(cachePath);
|
||||
profile->setPersistentCookiesPolicy(QWebEngineProfile::NoPersistentCookies);
|
||||
profile->setHttpCacheType(QWebEngineProfile::NoCache);
|
||||
|
||||
settings()->setAttribute(QWebEngineSettings::JavascriptEnabled, true);
|
||||
settings()->setAttribute(QWebEngineSettings::ScrollAnimatorEnabled, false);
|
||||
settings()->setAttribute(QWebEngineSettings::ErrorPageEnabled, false);
|
||||
settings()->setAttribute(QWebEngineSettings::PluginsEnabled, false);
|
||||
settings()->setAttribute(QWebEngineSettings::ScreenCaptureEnabled, false);
|
||||
settings()->setAttribute(QWebEngineSettings::LinksIncludedInFocusChain, false);
|
||||
settings()->setAttribute(QWebEngineSettings::LocalStorageEnabled, false);
|
||||
settings()->setAttribute(QWebEngineSettings::AllowRunningInsecureContent, true);
|
||||
settings()->setAttribute(QWebEngineSettings::LocalContentCanAccessRemoteUrls, true);
|
||||
settings()->setAttribute(QWebEngineSettings::XSSAuditingEnabled, false);
|
||||
settings()->setAttribute(QWebEngineSettings::LocalContentCanAccessFileUrls, true);
|
||||
|
||||
channel_ = new QWebChannel(this);
|
||||
channel_->registerObject(QStringLiteral("jsbridge"), &parent_);
|
||||
|
||||
setWebChannel(channel_);
|
||||
runJavaScript(Utils::QByteArrayFromFile(":webengine/linkify.js"),
|
||||
QWebEngineScript::MainWorld);
|
||||
runJavaScript(Utils::QByteArrayFromFile(":webengine/linkify-string.js"),
|
||||
QWebEngineScript::MainWorld);
|
||||
runJavaScript(Utils::QByteArrayFromFile(":webengine/qwebchannel.js"),
|
||||
QWebEngineScript::MainWorld);
|
||||
runJavaScript(Utils::QByteArrayFromFile(":webengine/previewInfo.js"),
|
||||
QWebEngineScript::MainWorld);
|
||||
runJavaScript(Utils::QByteArrayFromFile(":webengine/previewInterop.js"),
|
||||
QWebEngineScript::MainWorld);
|
||||
}
|
||||
|
||||
void parseMessage(const QString& messageId, const QString& msg, bool showPreview, QColor color)
|
||||
{
|
||||
QString colorStr = "'" + color.name() + "'";
|
||||
runJavaScript(QString("parseMessage(`%1`, `%2`, %3, %4)")
|
||||
.arg(messageId, msg, showPreview ? "true" : "false", colorStr));
|
||||
}
|
||||
static const QRegularExpression re(">([^<]+)<");
|
||||
const auto match = re.match(tag);
|
||||
return match.hasMatch() ? match.captured(1) : QString {};
|
||||
};
|
||||
|
||||
PreviewEngine::PreviewEngine(QObject* parent)
|
||||
: QObject(parent)
|
||||
, pimpl_(std::make_unique<Impl>(*this))
|
||||
{}
|
||||
const QRegularExpression PreviewEngine::newlineRe("\\n");
|
||||
|
||||
PreviewEngine::~PreviewEngine() {}
|
||||
|
||||
void
|
||||
PreviewEngine::parseMessage(const QString& messageId,
|
||||
const QString& msg,
|
||||
bool showPreview,
|
||||
QColor color)
|
||||
PreviewEngine::PreviewEngine(ConnectivityMonitor* cm, QObject* parent)
|
||||
: NetworkManager(cm, parent)
|
||||
, htmlParser_(new HtmlParser(this))
|
||||
{
|
||||
pimpl_->parseMessage(messageId, msg, showPreview, color);
|
||||
// Connect on a queued connection to avoid blocking caller thread.
|
||||
connect(this, &PreviewEngine::parseLink, this, &PreviewEngine::onParseLink, Qt::QueuedConnection);
|
||||
}
|
||||
|
||||
QString
|
||||
PreviewEngine::getTagContent(QList<QString>& tags, const QString& value)
|
||||
{
|
||||
Q_FOREACH (auto tag, tags) {
|
||||
const QRegularExpression re("(property|name)=\"(og:|twitter:|)" + value
|
||||
+ "\".*?content=\"([^\"]+)\"");
|
||||
|
||||
const auto match = re.match(tag.remove(newlineRe));
|
||||
if (match.hasMatch()) {
|
||||
return match.captured(3);
|
||||
}
|
||||
}
|
||||
return QString {};
|
||||
}
|
||||
|
||||
QString
|
||||
PreviewEngine::getTitle(HtmlParser::TagInfoList& metaTags)
|
||||
{
|
||||
// Try with opengraph/twitter props
|
||||
QString title = getTagContent(metaTags[TidyTag_META], "title");
|
||||
if (title.isEmpty()) { // Try with title tag
|
||||
title = getInnerHtml(htmlParser_->getFirstTagValue(TidyTag_TITLE));
|
||||
}
|
||||
if (title.isEmpty()) { // Try with h1 tag
|
||||
title = getInnerHtml(htmlParser_->getFirstTagValue(TidyTag_H1));
|
||||
}
|
||||
if (title.isEmpty()) { // Try with h2 tag
|
||||
title = getInnerHtml(htmlParser_->getFirstTagValue(TidyTag_H2));
|
||||
}
|
||||
return title;
|
||||
}
|
||||
|
||||
QString
|
||||
PreviewEngine::getDescription(HtmlParser::TagInfoList& metaTags)
|
||||
{
|
||||
// Try with og/twitter props
|
||||
QString d = getTagContent(metaTags[TidyTag_META], "description");
|
||||
if (d.isEmpty()) { // Try with first paragraph
|
||||
d = getInnerHtml(htmlParser_->getFirstTagValue(TidyTag_P));
|
||||
}
|
||||
return d;
|
||||
}
|
||||
|
||||
QString
|
||||
PreviewEngine::getImage(HtmlParser::TagInfoList& metaTags)
|
||||
{
|
||||
static const QRegularExpression newlineRe("\\n");
|
||||
// Try with og/twitter props
|
||||
QString image = getTagContent(metaTags[TidyTag_META], "image");
|
||||
if (image.isEmpty()) { // Try with href of link tag (rel="image_src")
|
||||
auto tags = htmlParser_->getTags({TidyTag_LINK});
|
||||
Q_FOREACH (auto tag, tags[TidyTag_LINK]) {
|
||||
static const QRegularExpression re("rel=\"image_src\".*?href=\"([^\"]+)\"");
|
||||
const auto match = re.match(tag.remove(newlineRe));
|
||||
if (match.hasMatch()) {
|
||||
return match.captured(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
return image;
|
||||
}
|
||||
|
||||
void
|
||||
PreviewEngine::log(const QString& str)
|
||||
PreviewEngine::onParseLink(const QString& messageId, const QString& link)
|
||||
{
|
||||
qDebug() << str;
|
||||
}
|
||||
|
||||
void
|
||||
PreviewEngine::emitInfoReady(const QString& messageId, const QVariantMap& info)
|
||||
{
|
||||
Q_EMIT infoReady(messageId, info);
|
||||
}
|
||||
|
||||
void
|
||||
PreviewEngine::emitLinkified(const QString& messageId, const QString& linkifiedStr)
|
||||
{
|
||||
Q_EMIT linkified(messageId, linkifiedStr);
|
||||
sendGetRequest(QUrl(link), [this, messageId, link](const QByteArray& html) {
|
||||
htmlParser_->parseHtmlString(html);
|
||||
auto metaTags = htmlParser_->getTags({TidyTag_META});
|
||||
QString domain = QUrl(link).host();
|
||||
if (domain.isEmpty()) {
|
||||
domain = link;
|
||||
}
|
||||
Q_EMIT infoReady(messageId,
|
||||
{{"title", getTitle(metaTags)},
|
||||
{"description", getDescription(metaTags)},
|
||||
{"image", getImage(metaTags)},
|
||||
{"url", link},
|
||||
{"domain", domain}});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
/*
|
||||
* Copyright (C) 2021-2023 Savoir-faire Linux Inc.
|
||||
* Author: Trevor Tabah <trevor.tabah@savoirfairelinux.com>
|
||||
* Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
@ -19,31 +17,32 @@
|
|||
|
||||
#pragma once
|
||||
|
||||
#include <QColor>
|
||||
#include <QObject>
|
||||
#include "networkmanager.h"
|
||||
|
||||
class PreviewEngine : public QObject
|
||||
#include "htmlparser.h"
|
||||
|
||||
class PreviewEngine final : public NetworkManager
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_DISABLE_COPY(PreviewEngine)
|
||||
public:
|
||||
PreviewEngine(QObject* parent = nullptr);
|
||||
~PreviewEngine();
|
||||
|
||||
void parseMessage(const QString& messageId,
|
||||
const QString& msg,
|
||||
bool showPreview,
|
||||
QColor color = "#0645AD");
|
||||
|
||||
Q_INVOKABLE void log(const QString& str);
|
||||
Q_INVOKABLE void emitInfoReady(const QString& messageId, const QVariantMap& info);
|
||||
Q_INVOKABLE void emitLinkified(const QString& messageId, const QString& linkifiedStr);
|
||||
PreviewEngine(ConnectivityMonitor* cm, QObject* parent = nullptr);
|
||||
~PreviewEngine() = default;
|
||||
|
||||
Q_SIGNALS:
|
||||
void parseLink(const QString& messageId, const QString& link);
|
||||
void infoReady(const QString& messageId, const QVariantMap& info);
|
||||
void linkified(const QString& messageId, const QString& linkifiedStr);
|
||||
|
||||
private:
|
||||
struct Impl;
|
||||
std::unique_ptr<Impl> pimpl_;
|
||||
Q_SLOT void onParseLink(const QString& messageId, const QString& link);
|
||||
|
||||
// An instance of HtmlParser used to parse HTML.
|
||||
HtmlParser* htmlParser_;
|
||||
|
||||
QString getTagContent(QList<QString>& tags, const QString& value);
|
||||
QString getTitle(HtmlParser::TagInfoList& metaTags);
|
||||
QString getDescription(HtmlParser::TagInfoList& metaTags);
|
||||
QString getImage(HtmlParser::TagInfoList& metaTags);
|
||||
|
||||
static const QRegularExpression newlineRe;
|
||||
};
|
||||
|
|
|
@ -226,7 +226,7 @@ registerTypes(QQmlEngine* engine,
|
|||
|
||||
// Enums
|
||||
QML_REGISTERUNCREATABLE(NS_ENUMS, Settings)
|
||||
QML_REGISTERUNCREATABLE(NS_ENUMS, NetWorkManager)
|
||||
QML_REGISTERUNCREATABLE(NS_ENUMS, NetworkManager)
|
||||
QML_REGISTERUNCREATABLE(NS_ENUMS, WizardViewStepModel)
|
||||
QML_REGISTERUNCREATABLE(NS_ENUMS, DeviceItemListModel)
|
||||
QML_REGISTERUNCREATABLE(NS_ENUMS, VideoInputDeviceModel)
|
||||
|
|
|
@ -36,7 +36,7 @@ SimpleMessageDialog {
|
|||
Connections {
|
||||
target: UpdateManager
|
||||
|
||||
function onUpdateDownloadProgressChanged(bytesRead, totalBytes) {
|
||||
function onDownloadProgressChanged(bytesRead, totalBytes) {
|
||||
downloadDialog.setDownloadProgress(bytesRead, totalBytes);
|
||||
}
|
||||
|
||||
|
@ -98,10 +98,10 @@ SimpleMessageDialog {
|
|||
buttonTitles: [JamiStrings.optionCancel]
|
||||
buttonStyles: [SimpleMessageDialog.ButtonStyle.TintedBlue]
|
||||
buttonCallBacks: [function () {
|
||||
UpdateManager.cancelUpdate();
|
||||
UpdateManager.cancelDownload();
|
||||
}]
|
||||
onVisibleChanged: {
|
||||
if (!visible)
|
||||
UpdateManager.cancelUpdate();
|
||||
UpdateManager.cancelDownload();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -39,7 +39,7 @@ static constexpr char betaMsiSubUrl[] = "/beta/jami.beta.x64.msi";
|
|||
|
||||
struct UpdateManager::Impl : public QObject
|
||||
{
|
||||
Impl(const QString& url, ConnectivityMonitor* cm, LRCInstance* instance, UpdateManager& parent)
|
||||
Impl(const QString& url, LRCInstance* instance, UpdateManager& parent)
|
||||
: QObject(nullptr)
|
||||
, parent_(parent)
|
||||
, lrcInstance_(instance)
|
||||
|
@ -60,14 +60,14 @@ struct UpdateManager::Impl : public QObject
|
|||
// Fail without UI if this is a programmatic check.
|
||||
if (!quiet)
|
||||
connect(&parent_,
|
||||
&NetWorkManager::errorOccured,
|
||||
&NetworkManager::errorOccured,
|
||||
&parent_,
|
||||
&UpdateManager::updateErrorOccurred);
|
||||
|
||||
cleanUpdateFiles();
|
||||
QUrl versionUrl {isBeta ? QUrl::fromUserInput(baseUrlString_ + betaVersionSubUrl)
|
||||
: QUrl::fromUserInput(baseUrlString_ + versionSubUrl)};
|
||||
parent_.get(versionUrl, [this, quiet](const QString& latestVersionString) {
|
||||
parent_.sendGetRequest(versionUrl, [this, quiet](const QByteArray& latestVersionString) {
|
||||
if (latestVersionString.isEmpty()) {
|
||||
qWarning() << "Error checking version";
|
||||
if (!quiet)
|
||||
|
@ -92,16 +92,12 @@ struct UpdateManager::Impl : public QObject
|
|||
{
|
||||
parent_.disconnect();
|
||||
connect(&parent_,
|
||||
&NetWorkManager::errorOccured,
|
||||
&NetworkManager::errorOccured,
|
||||
&parent_,
|
||||
&UpdateManager::updateErrorOccurred);
|
||||
connect(&parent_, &NetWorkManager::statusChanged, this, [this](GetStatus status) {
|
||||
connect(&parent_, &UpdateManager::statusChanged, this, [this](GetStatus status) {
|
||||
switch (status) {
|
||||
case GetStatus::STARTED:
|
||||
connect(&parent_,
|
||||
&NetWorkManager::downloadProgressChanged,
|
||||
&parent_,
|
||||
&UpdateManager::updateDownloadProgressChanged);
|
||||
Q_EMIT parent_.updateDownloadStarted();
|
||||
break;
|
||||
case GetStatus::FINISHED:
|
||||
|
@ -115,9 +111,11 @@ struct UpdateManager::Impl : public QObject
|
|||
QUrl downloadUrl {(beta || isBeta) ? QUrl::fromUserInput(baseUrlString_ + betaMsiSubUrl)
|
||||
: QUrl::fromUserInput(baseUrlString_ + msiSubUrl)};
|
||||
|
||||
parent_.get(
|
||||
parent_.downloadFile(
|
||||
downloadUrl,
|
||||
[this, downloadUrl](const QString&) {
|
||||
[this, downloadUrl](bool success, const QString& errorMessage) {
|
||||
Q_UNUSED(success)
|
||||
Q_UNUSED(errorMessage)
|
||||
lrcInstance_->finish();
|
||||
Q_EMIT lrcInstance_->quitEngineRequested();
|
||||
auto args = QString(" /passive /norestart WIXNONUILAUNCH=1");
|
||||
|
@ -132,7 +130,7 @@ struct UpdateManager::Impl : public QObject
|
|||
|
||||
void cancelUpdate()
|
||||
{
|
||||
parent_.cancelRequest();
|
||||
parent_.cancelDownload();
|
||||
};
|
||||
|
||||
void setAutoUpdateCheck(bool state)
|
||||
|
@ -175,11 +173,14 @@ UpdateManager::UpdateManager(const QString& url,
|
|||
ConnectivityMonitor* cm,
|
||||
LRCInstance* instance,
|
||||
QObject* parent)
|
||||
: NetWorkManager(cm, parent)
|
||||
, pimpl_(std::make_unique<Impl>(url, cm, instance, *this))
|
||||
: NetworkManager(cm, parent)
|
||||
, pimpl_(std::make_unique<Impl>(url, instance, *this))
|
||||
{}
|
||||
|
||||
UpdateManager::~UpdateManager() {}
|
||||
UpdateManager::~UpdateManager()
|
||||
{
|
||||
cancelDownload();
|
||||
}
|
||||
|
||||
void
|
||||
UpdateManager::checkForUpdates(bool quiet)
|
||||
|
@ -225,3 +226,112 @@ UpdateManager::isAutoUpdaterEnabled()
|
|||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
void
|
||||
UpdateManager::cancelDownload()
|
||||
{
|
||||
if (downloadReply_) {
|
||||
Q_EMIT errorOccured(GetError::CANCELED);
|
||||
downloadReply_->abort();
|
||||
resetDownload();
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
UpdateManager::downloadFile(const QUrl& url,
|
||||
std::function<void(bool, const QString&)> onDoneCallback,
|
||||
const QString& filePath)
|
||||
{
|
||||
// If there is already a download in progress, return.
|
||||
if (downloadReply_ && downloadReply_->isRunning()) {
|
||||
qWarning() << Q_FUNC_INFO << "Download already in progress";
|
||||
return;
|
||||
}
|
||||
|
||||
// Clean up any previous download.
|
||||
resetDownload();
|
||||
|
||||
// If the url is invalid, return.
|
||||
if (!url.isValid()) {
|
||||
Q_EMIT errorOccured(GetError::NETWORK_ERROR, "Invalid url");
|
||||
return;
|
||||
}
|
||||
|
||||
// If the file path is empty, return.
|
||||
if (filePath.isEmpty()) {
|
||||
Q_EMIT errorOccured(GetError::NETWORK_ERROR, "Invalid file path");
|
||||
return;
|
||||
}
|
||||
|
||||
// Create the file. Return if it cannot be created.
|
||||
QFileInfo fileInfo(url.path());
|
||||
QString fileName = fileInfo.fileName();
|
||||
file_.reset(new QFile(filePath + "/" + fileName));
|
||||
if (!file_->open(QIODevice::WriteOnly)) {
|
||||
Q_EMIT errorOccured(GetError::ACCESS_DENIED);
|
||||
file_.reset();
|
||||
qWarning() << Q_FUNC_INFO << "Could not open file for writing";
|
||||
return;
|
||||
}
|
||||
|
||||
// Start the download.
|
||||
QNetworkRequest request(url);
|
||||
downloadReply_ = manager_->get(request);
|
||||
|
||||
connect(downloadReply_, &QNetworkReply::readyRead, this, [=]() {
|
||||
if (file_ && file_->isOpen()) {
|
||||
file_->write(downloadReply_->readAll());
|
||||
}
|
||||
});
|
||||
|
||||
connect(downloadReply_,
|
||||
&QNetworkReply::downloadProgress,
|
||||
this,
|
||||
[=](qint64 bytesReceived, qint64 bytesTotal) {
|
||||
Q_EMIT downloadProgressChanged(bytesReceived, bytesTotal);
|
||||
});
|
||||
|
||||
connect(downloadReply_,
|
||||
QOverload<QNetworkReply::NetworkError>::of(&QNetworkReply::errorOccurred),
|
||||
this,
|
||||
[this](QNetworkReply::NetworkError error) {
|
||||
downloadReply_->disconnect();
|
||||
resetDownload();
|
||||
qWarning() << Q_FUNC_INFO
|
||||
<< QMetaEnum::fromType<QNetworkReply::NetworkError>().valueToKey(error);
|
||||
Q_EMIT errorOccured(GetError::NETWORK_ERROR);
|
||||
});
|
||||
|
||||
connect(downloadReply_, &QNetworkReply::finished, this, [this, onDoneCallback]() {
|
||||
bool success = false;
|
||||
QString errorMessage;
|
||||
if (downloadReply_->error() == QNetworkReply::NoError) {
|
||||
resetDownload();
|
||||
success = true;
|
||||
} else {
|
||||
errorMessage = downloadReply_->errorString();
|
||||
resetDownload();
|
||||
}
|
||||
onDoneCallback(success, errorMessage);
|
||||
Q_EMIT statusChanged(GetStatus::FINISHED);
|
||||
});
|
||||
|
||||
Q_EMIT statusChanged(GetStatus::STARTED);
|
||||
}
|
||||
|
||||
void
|
||||
UpdateManager::resetDownload()
|
||||
{
|
||||
if (downloadReply_) {
|
||||
downloadReply_->deleteLater();
|
||||
downloadReply_ = nullptr;
|
||||
}
|
||||
if (file_) {
|
||||
if (file_->isOpen()) {
|
||||
file_->flush();
|
||||
file_->close();
|
||||
}
|
||||
file_->deleteLater();
|
||||
file_.reset();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,7 +25,7 @@
|
|||
class LRCInstance;
|
||||
class ConnectivityMonitor;
|
||||
|
||||
class UpdateManager final : public NetWorkManager
|
||||
class UpdateManager final : public NetworkManager
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_DISABLE_COPY(UpdateManager)
|
||||
|
@ -36,6 +36,9 @@ public:
|
|||
QObject* parent = nullptr);
|
||||
~UpdateManager();
|
||||
|
||||
enum GetStatus { STARTED, FINISHED };
|
||||
Q_ENUM(GetStatus)
|
||||
|
||||
Q_INVOKABLE void checkForUpdates(bool quiet = false);
|
||||
Q_INVOKABLE void applyUpdates(bool beta = false);
|
||||
Q_INVOKABLE void cancelUpdate();
|
||||
|
@ -43,15 +46,28 @@ public:
|
|||
Q_INVOKABLE bool isCurrentVersionBeta();
|
||||
Q_INVOKABLE bool isUpdaterEnabled();
|
||||
Q_INVOKABLE bool isAutoUpdaterEnabled();
|
||||
Q_INVOKABLE void cancelDownload();
|
||||
|
||||
void downloadFile(const QUrl& url,
|
||||
std::function<void(bool, const QString&)> onDoneCallback,
|
||||
const QString& filePath);
|
||||
|
||||
Q_SIGNALS:
|
||||
void statusChanged(GetStatus status);
|
||||
void downloadProgressChanged(qint64 bytesRead, qint64 totalBytes);
|
||||
|
||||
void updateCheckReplyReceived(bool ok, bool found = false);
|
||||
void updateErrorOccurred(const NetWorkManager::GetError& error);
|
||||
void updateErrorOccurred(const NetworkManager::GetError& error);
|
||||
void updateDownloadStarted();
|
||||
void updateDownloadProgressChanged(qint64 bytesRead, qint64 totalBytes);
|
||||
void updateDownloadFinished();
|
||||
void appCloseRequested();
|
||||
|
||||
private:
|
||||
void resetDownload();
|
||||
QNetworkReply* downloadReply_ {nullptr};
|
||||
QScopedPointer<QFile> file_;
|
||||
|
||||
private:
|
||||
struct Impl;
|
||||
std::unique_ptr<Impl> pimpl_;
|
||||
|
|
|
@ -297,7 +297,7 @@ public:
|
|||
* @var isRead
|
||||
* @var commit
|
||||
* @var linkPreviewInfo
|
||||
* @var linkified
|
||||
* @var parsedBody
|
||||
*/
|
||||
struct Info
|
||||
{
|
||||
|
@ -312,7 +312,7 @@ struct Info
|
|||
bool isRead = false;
|
||||
MapStringString commit;
|
||||
QVariantMap linkPreviewInfo = {};
|
||||
QString linkified;
|
||||
QString parsedBody = {};
|
||||
QVariantMap reactions;
|
||||
QString react_to;
|
||||
QVector<Body> previousBodies;
|
||||
|
|
|
@ -454,14 +454,8 @@ MessageListModel::dataForItem(item_t item, int, int role) const
|
|||
return QVariant(item.second.isRead);
|
||||
case Role::LinkPreviewInfo:
|
||||
return QVariant(item.second.linkPreviewInfo);
|
||||
case Role::Linkified:
|
||||
return QVariant(item.second.linkified);
|
||||
case Role::LinkifiedBody: {
|
||||
if (!item.second.linkified.isEmpty()) {
|
||||
return QVariant(item.second.linkified);
|
||||
}
|
||||
return QVariant(item.second.body);
|
||||
}
|
||||
case Role::ParsedBody:
|
||||
return QVariant(item.second.parsedBody);
|
||||
case Role::ActionUri:
|
||||
return QVariant(item.second.commit["uri"]);
|
||||
case Role::ConfId:
|
||||
|
@ -484,9 +478,9 @@ MessageListModel::dataForItem(item_t item, int, int role) const
|
|||
case Role::ReplyToBody: {
|
||||
if (repliedMsg == -1)
|
||||
return QVariant("");
|
||||
auto linkified = data(repliedMsg, Role::Linkified).toString();
|
||||
if (!linkified.isEmpty())
|
||||
return QVariant(linkified);
|
||||
auto parsed = data(repliedMsg, Role::ParsedBody).toString();
|
||||
if (!parsed.isEmpty())
|
||||
return QVariant(parsed);
|
||||
return QVariant(data(repliedMsg, Role::Body).toString());
|
||||
}
|
||||
case Role::TotalSize:
|
||||
|
@ -551,15 +545,15 @@ MessageListModel::addHyperlinkInfo(const QString& messageId, const QVariantMap&
|
|||
}
|
||||
|
||||
void
|
||||
MessageListModel::linkifyMessage(const QString& messageId, const QString& linkified)
|
||||
MessageListModel::setParsedMessage(const QString& messageId, const QString& parsed)
|
||||
{
|
||||
int index = getIndexOfMessage(messageId);
|
||||
if (index == -1) {
|
||||
return;
|
||||
}
|
||||
QModelIndex modelIndex = QAbstractListModel::index(index, 0);
|
||||
interactions_[index].second.linkified = linkified;
|
||||
Q_EMIT dataChanged(modelIndex, modelIndex, {Role::Linkified, Role::LinkifiedBody});
|
||||
interactions_[index].second.parsedBody = parsed;
|
||||
Q_EMIT dataChanged(modelIndex, modelIndex, {Role::ParsedBody});
|
||||
}
|
||||
|
||||
void
|
||||
|
@ -712,12 +706,11 @@ MessageListModel::editMessage(const QString& msgId, interaction::Info& info)
|
|||
}
|
||||
}
|
||||
info.body = it->rbegin()->body;
|
||||
info.linkified.clear();
|
||||
info.parsedBody.clear();
|
||||
editedBodies_.erase(it);
|
||||
emitDataChanged(msgId,
|
||||
{MessageList::Role::Body,
|
||||
MessageList::Role::Linkified,
|
||||
MessageList::Role::LinkifiedBody,
|
||||
MessageList::Role::ParsedBody,
|
||||
MessageList::Role::PreviousBodies,
|
||||
MessageList::Role::IsEmojiOnly});
|
||||
|
||||
|
|
|
@ -45,8 +45,7 @@ struct Info;
|
|||
X(ConfId) \
|
||||
X(DeviceId) \
|
||||
X(LinkPreviewInfo) \
|
||||
X(Linkified) \
|
||||
X(LinkifiedBody) \
|
||||
X(ParsedBody) \
|
||||
X(PreviousBodies) \
|
||||
X(Reactions) \
|
||||
X(ReplyTo) \
|
||||
|
@ -124,7 +123,7 @@ public:
|
|||
bool contains(const QString& msgId);
|
||||
int getIndexOfMessage(const QString& messageId) const;
|
||||
void addHyperlinkInfo(const QString& messageId, const QVariantMap& info);
|
||||
void linkifyMessage(const QString& messageId, const QString& linkified);
|
||||
void setParsedMessage(const QString& messageId, const QString& parsed);
|
||||
|
||||
void setRead(const QString& peer, const QString& messageId);
|
||||
QString getRead(const QString& peer);
|
||||
|
|
|
@ -38,12 +38,18 @@ set(TEST_QML_RESOURCES
|
|||
${CMAKE_SOURCE_DIR}/src/app/resources.qrc)
|
||||
|
||||
# Common jami files
|
||||
add_library(test_common_obj OBJECT ${COMMON_SOURCES} ${COMMON_HEADERS})
|
||||
add_library(test_common_obj OBJECT
|
||||
${COMMON_SOURCES}
|
||||
${COMMON_HEADERS})
|
||||
|
||||
target_include_directories(test_common_obj PRIVATE
|
||||
${CLIENT_INCLUDE_DIRS}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}
|
||||
${CMAKE_SOURCE_DIR}/src)
|
||||
target_link_directories(test_common_obj PRIVATE ${CLIENT_LINK_DIRS})
|
||||
target_link_libraries(test_common_obj ${QML_TEST_LIBS})
|
||||
target_compile_definitions(test_common_obj PRIVATE ENABLE_TESTS="ON")
|
||||
|
||||
include_directories(${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_SOURCE_DIR}/src)
|
||||
|
||||
set(COMMON_TESTS_SOURCES
|
||||
${QML_RESOURCES}
|
||||
${QML_RESOURCES_QML}
|
||||
|
@ -77,28 +83,18 @@ set(UNIT_TESTS_SOURCE_FILES
|
|||
${CMAKE_SOURCE_DIR}/tests/unittests/main_unittest.cpp
|
||||
${CMAKE_SOURCE_DIR}/tests/unittests/account_unittest.cpp
|
||||
${CMAKE_SOURCE_DIR}/tests/unittests/contact_unittest.cpp
|
||||
${CMAKE_SOURCE_DIR}/tests/unittests/messageparser_unittest.cpp
|
||||
${CMAKE_SOURCE_DIR}/tests/unittests/globaltestenvironment.h
|
||||
${COMMON_TESTS_SOURCES})
|
||||
|
||||
set(ALL_TESTS_LIBS
|
||||
${QML_TEST_LIBS}
|
||||
gtest
|
||||
${ringclient}
|
||||
${qrencode}
|
||||
${X11}
|
||||
${LIBNM_LIBRARIES}
|
||||
${LIBNOTIFY_LIBRARIES}
|
||||
${LIBGDKPIXBUF_LIBRARIES}
|
||||
${WINDOWS_LIBS})
|
||||
${CLIENT_LIBS})
|
||||
|
||||
set(ALL_TESTS_INCLUDES
|
||||
${TESTS_INCLUDES}
|
||||
${LRC}/include/libringclient
|
||||
${LRC}/include
|
||||
${LIBNM_INCLUDE_DIRS}
|
||||
${LIBNOTIFY_INCLUDE_DIRS}
|
||||
${LIBGDKPIXBUF_INCLUDE_DIRS}
|
||||
${WINDOWS_INCLUDES})
|
||||
${CLIENT_INCLUDE_DIRS})
|
||||
|
||||
function(setup_test TEST_NAME TEST_SOURCES TEST_INPUT)
|
||||
string(TOLOWER ${TEST_NAME} TEST_BINARY_NAME)
|
||||
|
@ -120,4 +116,4 @@ setup_test(Qml_Tests
|
|||
|
||||
# Unit tests
|
||||
setup_test(Unit_Tests
|
||||
"${UNIT_TESTS_SOURCE_FILES}" "")
|
||||
"${UNIT_TESTS_SOURCE_FILES}" "")
|
||||
|
|
|
@ -59,7 +59,7 @@ public Q_SLOTS:
|
|||
connectivityMonitor_.reset(new ConnectivityMonitor(this));
|
||||
settingsManager_.reset(new AppSettingsManager(this));
|
||||
systemTray_.reset(new SystemTray(settingsManager_.get(), this));
|
||||
previewEngine_.reset(new PreviewEngine(this));
|
||||
previewEngine_.reset(new PreviewEngine(connectivityMonitor_.get(), this));
|
||||
|
||||
QFontDatabase::addApplicationFont(":/images/FontAwesome.otf");
|
||||
|
||||
|
|
|
@ -21,7 +21,8 @@
|
|||
#include "appsettingsmanager.h"
|
||||
#include "connectivitymonitor.h"
|
||||
#include "systemtray.h"
|
||||
|
||||
#include "previewengine.h"
|
||||
#include "messageparser.h"
|
||||
#include "accountadapter.h"
|
||||
|
||||
#include <QTest>
|
||||
|
@ -55,6 +56,9 @@ public:
|
|||
systemTray.get(),
|
||||
lrcInstance.data(),
|
||||
nullptr));
|
||||
|
||||
previewEngine.reset(new PreviewEngine(connectivityMonitor.get(), nullptr));
|
||||
messageParser.reset(new MessageParser(previewEngine.data(), nullptr));
|
||||
}
|
||||
|
||||
void TearDown()
|
||||
|
@ -75,6 +79,8 @@ public:
|
|||
QScopedPointer<ConnectivityMonitor> connectivityMonitor;
|
||||
QScopedPointer<AppSettingsManager> settingsManager;
|
||||
QScopedPointer<SystemTray> systemTray;
|
||||
QScopedPointer<PreviewEngine> previewEngine;
|
||||
QScopedPointer<MessageParser> messageParser;
|
||||
};
|
||||
|
||||
extern TestEnvironment globalEnv;
|
||||
|
|
171
tests/unittests/messageparser_unittest.cpp
Normal file
171
tests/unittests/messageparser_unittest.cpp
Normal file
|
@ -0,0 +1,171 @@
|
|||
/*
|
||||
* Copyright (C) 2023 Savoir-faire Linux Inc.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "globaltestenvironment.h"
|
||||
|
||||
class MessageParserFixture : public ::testing::Test
|
||||
{
|
||||
public:
|
||||
// Prepare unit test context. Called at
|
||||
// prior each unit test execution
|
||||
void SetUp() override {}
|
||||
|
||||
// Close unit test context. Called
|
||||
// after each unit test ending
|
||||
void TearDown() override {}
|
||||
};
|
||||
|
||||
/*!
|
||||
* WHEN We parse a markdown text body with no link.
|
||||
* THEN The HTML body should be returned correctly without the link.
|
||||
*/
|
||||
TEST_F(MessageParserFixture, TextIsParsedCorrectly)
|
||||
{
|
||||
auto linkColor = QColor::fromRgb(0, 0, 255);
|
||||
auto backgroundColor = QColor::fromRgb(0, 0, 255);
|
||||
|
||||
QSignalSpy messageParsedSpy(globalEnv.messageParser.data(), &MessageParser::messageParsed);
|
||||
QSignalSpy linkInfoReadySpy(globalEnv.messageParser.data(), &MessageParser::linkInfoReady);
|
||||
|
||||
globalEnv.messageParser->parseMessage("msgId_01",
|
||||
"This is a **bold** text",
|
||||
true,
|
||||
linkColor,
|
||||
backgroundColor);
|
||||
|
||||
// Wait for the messageParsed signal which should be emitted once.
|
||||
messageParsedSpy.wait();
|
||||
EXPECT_EQ(messageParsedSpy.count(), 1);
|
||||
|
||||
QList<QVariant> messageParserArguments = messageParsedSpy.takeFirst();
|
||||
EXPECT_TRUE(messageParserArguments.at(0).typeId() == qMetaTypeId<QString>());
|
||||
EXPECT_EQ(messageParserArguments.at(0).toString(), "msgId_01");
|
||||
EXPECT_TRUE(messageParserArguments.at(1).typeId() == qMetaTypeId<QString>());
|
||||
EXPECT_EQ(messageParserArguments.at(1).toString(),
|
||||
"<style></style><p>This is a <strong>bold</strong> text</p>\n");
|
||||
|
||||
// No link info should be returned.
|
||||
linkInfoReadySpy.wait();
|
||||
EXPECT_EQ(linkInfoReadySpy.count(), 0);
|
||||
}
|
||||
|
||||
/*!
|
||||
* WHEN We parse a text body with a link.
|
||||
* THEN The HTML body should be returned correctly including the link.
|
||||
*/
|
||||
TEST_F(MessageParserFixture, ALinkIsParsedCorrectly)
|
||||
{
|
||||
auto linkColor = QColor::fromRgb(0, 0, 255);
|
||||
auto backgroundColor = QColor::fromRgb(0, 0, 255);
|
||||
|
||||
QSignalSpy messageParsedSpy(globalEnv.messageParser.data(), &MessageParser::messageParsed);
|
||||
QSignalSpy linkInfoReadySpy(globalEnv.messageParser.data(), &MessageParser::linkInfoReady);
|
||||
|
||||
// Parse a message with a link.
|
||||
globalEnv.messageParser->parseMessage("msgId_02",
|
||||
"https://www.google.com",
|
||||
true,
|
||||
linkColor,
|
||||
backgroundColor);
|
||||
|
||||
// Wait for the messageParsed signal which should be emitted once.
|
||||
messageParsedSpy.wait();
|
||||
EXPECT_EQ(messageParsedSpy.count(), 1);
|
||||
|
||||
QList<QVariant> messageParserArguments = messageParsedSpy.takeFirst();
|
||||
EXPECT_TRUE(messageParserArguments.at(0).typeId() == qMetaTypeId<QString>());
|
||||
EXPECT_EQ(messageParserArguments.at(0).toString(), "msgId_02");
|
||||
EXPECT_TRUE(messageParserArguments.at(1).typeId() == qMetaTypeId<QString>());
|
||||
EXPECT_EQ(messageParserArguments.at(1).toString(),
|
||||
"<style>a{color:#0000ff;}</style><p><a "
|
||||
"href=\"https://www.google.com\">https://www.google.com</a></p>\n");
|
||||
|
||||
// Wait for the linkInfoReady signal which should be emitted once.
|
||||
linkInfoReadySpy.wait();
|
||||
EXPECT_EQ(linkInfoReadySpy.count(), 1);
|
||||
|
||||
QList<QVariant> linkInfoReadyArguments = linkInfoReadySpy.takeFirst();
|
||||
EXPECT_TRUE(linkInfoReadyArguments.at(0).typeId() == qMetaTypeId<QString>());
|
||||
EXPECT_EQ(linkInfoReadyArguments.at(0).toString(), "msgId_02");
|
||||
EXPECT_TRUE(linkInfoReadyArguments.at(1).typeId() == qMetaTypeId<QVariantMap>());
|
||||
QVariantMap linkInfo = linkInfoReadyArguments.at(1).toMap();
|
||||
EXPECT_EQ(linkInfo["url"].toString(), "https://www.google.com");
|
||||
// The rest of the link info is not tested here.
|
||||
}
|
||||
|
||||
/*!
|
||||
* WHEN We parse a text body with end of line characters.
|
||||
* THEN The HTML body should be returned correctly with the end of line characters.
|
||||
*/
|
||||
TEST_F(MessageParserFixture, EndOfLineCharactersAreParsedCorrectly)
|
||||
{
|
||||
auto linkColor = QColor::fromRgb(0, 0, 255);
|
||||
auto backgroundColor = QColor::fromRgb(0, 0, 255);
|
||||
|
||||
QSignalSpy messageParsedSpy(globalEnv.messageParser.data(), &MessageParser::messageParsed);
|
||||
QSignalSpy linkInfoReadySpy(globalEnv.messageParser.data(), &MessageParser::linkInfoReady);
|
||||
|
||||
// Parse a message with a link.
|
||||
globalEnv.messageParser->parseMessage("msgId_03",
|
||||
"Text with\n2 lines",
|
||||
true,
|
||||
linkColor,
|
||||
backgroundColor);
|
||||
|
||||
// Wait for the messageParsed signal which should be emitted once.
|
||||
messageParsedSpy.wait();
|
||||
EXPECT_EQ(messageParsedSpy.count(), 1);
|
||||
|
||||
QList<QVariant> messageParserArguments = messageParsedSpy.takeFirst();
|
||||
EXPECT_TRUE(messageParserArguments.at(0).typeId() == qMetaTypeId<QString>());
|
||||
EXPECT_EQ(messageParserArguments.at(0).toString(), "msgId_03");
|
||||
EXPECT_TRUE(messageParserArguments.at(1).typeId() == qMetaTypeId<QString>());
|
||||
EXPECT_EQ(messageParserArguments.at(1).toString(),
|
||||
"<style></style><p>Text with<br>\n2 lines</p>\n");
|
||||
}
|
||||
|
||||
/*!
|
||||
* WHEN We parse a text body with some fenced code.
|
||||
* THEN The HTML body should be returned correctly with the code wrapped in a <pre> tag.
|
||||
*/
|
||||
TEST_F(MessageParserFixture, FencedCodeIsParsedCorrectly)
|
||||
{
|
||||
auto linkColor = QColor::fromRgb(0, 0, 255);
|
||||
auto backgroundColor = QColor::fromRgb(0, 0, 255);
|
||||
|
||||
QSignalSpy messageParsedSpy(globalEnv.messageParser.data(), &MessageParser::messageParsed);
|
||||
QSignalSpy linkInfoReadySpy(globalEnv.messageParser.data(), &MessageParser::linkInfoReady);
|
||||
|
||||
// Parse a message with a link.
|
||||
globalEnv.messageParser->parseMessage("msgId_04",
|
||||
"Text with \n```\ncode\n```",
|
||||
true,
|
||||
linkColor,
|
||||
backgroundColor);
|
||||
|
||||
// Wait for the messageParsed signal which should be emitted once.
|
||||
messageParsedSpy.wait();
|
||||
EXPECT_EQ(messageParsedSpy.count(), 1);
|
||||
|
||||
QList<QVariant> messageParserArguments = messageParsedSpy.takeFirst();
|
||||
EXPECT_TRUE(messageParserArguments.at(0).typeId() == qMetaTypeId<QString>());
|
||||
EXPECT_EQ(messageParserArguments.at(0).toString(), "msgId_04");
|
||||
EXPECT_TRUE(messageParserArguments.at(1).typeId() == qMetaTypeId<QString>());
|
||||
EXPECT_EQ(messageParserArguments.at(1).toString(),
|
||||
"<style>pre,code{background-color:#0000ff;color:#ffffff;white-space:pre-wrap;"
|
||||
"}</style><p>Text with</p>\n<pre><code>code\n</code></pre>\n");
|
||||
}
|
Loading…
Add table
Reference in a new issue