Page MenuHomePhorge

No OneTemporary

Size
73 KB
Referenced Files
None
Subscribers
None
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 34f0889..1ffd6c9 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -1,113 +1,115 @@
cmake_minimum_required(VERSION 3.16)
project(kazv)
include(FeatureSummary)
set(kazv_VERSION_MAJOR 0)
set(kazv_VERSION_MINOR 7)
set(kazv_VERSION_PATCH 0)
set(kazv_VERSION_STRING ${kazv_VERSION_MAJOR}.${kazv_VERSION_MINOR}.${kazv_VERSION_PATCH})
set(CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake" ${CMAKE_MODULE_PATH})
set(CMAKE_CXX_STANDARD 20)
if(NOT kazv_KF_QT_MAJOR_VERSION)
set(kazv_KF_QT_MAJOR_VERSION 6)
endif()
message(STATUS "KF/Qt major version: ${kazv_KF_QT_MAJOR_VERSION}")
if(NOT kazv_KF_QT_MAJOR_VERSION EQUAL 6)
message(FATAL_ERROR "KF/Qt major version must be 6")
endif()
set(QT_MAJOR_VERSION ${kazv_KF_QT_MAJOR_VERSION})
set(KF_MAJOR_VERSION ${kazv_KF_QT_MAJOR_VERSION})
set(QT_MIN_VERSION 6.9.0)
set(KF_MIN_VERSION 6.0.0)
option(kazv_LINK_BREEZE_ICONS "Link to Breeze icons library" OFF)
option(kazv_BUILD_TESTS "Build tests" ON)
option(kazv_ENABLE_APPIUM_TESTS "Run appium tests" "")
set(kazv_LINK_BREEZE_ICONS_MACRO 0)
if(kazv_LINK_BREEZE_ICONS)
set(kazv_LINK_BREEZE_ICONS_MACRO 1)
endif()
find_package(ECM ${KF_MIN_VERSION} REQUIRED NO_MODULE)
set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${ECM_MODULE_PATH})
set(KDE_SKIP_NULLPTR_WARNINGS_SETTINGS ON)
include(KDEInstallDirs)
include(KDECMakeSettings)
include(KDECompilerSettings NO_POLICY_SCOPE)
include(ECMSetupVersion)
include(ECMGenerateHeaders)
include(ECMPoQmTools)
include(ECMAddTests)
include(ECMQtDeclareLoggingCategory)
include(ECMDeprecationSettings)
include(ECMQmlModule)
if(ANDROID)
find_package(Perl 5.12 REQUIRED)
include(ECMAddAndroidApk)
endif()
ecm_set_disabled_deprecation_versions(
QT 5.15.14
KF 5.116.0
KCOREADDONS 5.116.0
)
find_package(Qt${QT_MAJOR_VERSION} ${QT_MIN_VERSION} REQUIRED COMPONENTS
Core Gui Qml QuickControls2 Svg Concurrent Widgets
Multimedia Test Network QuickTest HttpServer Sql
)
qt6_policy(SET QTP0001 NEW)
find_package(QCoro6 REQUIRED COMPONENTS Core)
qcoro_enable_coroutines()
set(kazv_KF_EXTRA_MODULES)
if(kazv_LINK_BREEZE_ICONS)
list(APPEND kazv_KF_EXTRA_MODULES BreezeIcons)
endif()
find_package(KF${KF_MAJOR_VERSION} ${KF_MIN_VERSION} REQUIRED COMPONENTS Kirigami Config Notifications CoreAddons ${kazv_KF_EXTRA_MODULES})
find_package(KF6KirigamiAddons)
find_package(Threads REQUIRED)
find_package(nlohmann_json REQUIRED)
find_package(libkazv REQUIRED)
find_package(cmark)
set(CMARK_TARGET_NAME cmark::cmark)
if(NOT cmark_FOUND)
# CMark 0.29.0 on Ubuntu 20.04 has cmark.cmake instead of cmark-config.cmake
# But that is not usable, so we fall back to pkgconfig
find_package(PkgConfig REQUIRED)
pkg_search_module(cmark libcmark IMPORTED_TARGET GLOBAL)
set(CMARK_TARGET_NAME cmark)
endif()
if(ANDROID)
find_package(OpenSSL REQUIRED)
endif()
+find_package(lexbor REQUIRED)
+
if(${kazv_ENABLE_APPIUM_TESTS} STREQUAL "")
find_package(SeleniumWebDriverATSPI)
if(SeleniumWebDriverATSPI_FOUND)
message(STATUS "Enabling appium tests because selenium-webdriver-at-spi is found. To turn it off, explicitly set kazv_ENABLE_APPIUM_TESTS to OFF")
set(kazv_ENABLE_APPIUM_TESTS ON)
endif()
elseif(kazv_ENABLE_APPIUM_TESTS)
find_package(SeleniumWebDriverATSPI REQUIRED)
endif()
kde_enable_exceptions()
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -foperator-names -DQT_NO_EMIT")
add_subdirectory(src)
install(PROGRAMS moe.kazv.mxc.kazv.desktop DESTINATION ${KDE_INSTALL_APPDIR})
install(FILES moe.kazv.mxc.kazv.appdata.xml DESTINATION ${KDE_INSTALL_METAINFODIR})
install(FILES kazv.notifyrc DESTINATION ${KDE_INSTALL_KNOTIFYRCDIR})
install(FILES icons/kazvlogo-256.png DESTINATION ${KDE_INSTALL_ICONDIR}/hicolor/256x256/apps RENAME moe.kazv.mxc.kazv.png)
feature_summary(WHAT ALL INCLUDE_QUIET_PACKAGES FATAL_ON_MISSING_REQUIRED_PACKAGES)
diff --git a/README.md b/README.md
index 1380e77..f9bc846 100644
--- a/README.md
+++ b/README.md
@@ -1,134 +1,135 @@
# Description
kazv is a convergent matrix client and instant messaging app.
# License
Copyright (C) 2020-2023 the Kazv Project <https://lily-is.land/kazv>
kazv is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
kazv 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 Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with kazv. If not, see <https://www.gnu.org/licenses/>.
# Compiled executables
| Operating system | Architecture | Format | Download links |
|------------------|--------------|----------|----------------------------------------------------|
| GNU/Linux | amd64 | AppImage | [Debug][gl-amd64-ai-d] [Production][gl-amd64-ai-p] |
[gl-amd64-ai-d]: https://b.lily-is.land/plugins/lilybuild_artifacts/repos/6/latest/servant/artifacts/archive?job=build:GNU/Linux-amd64-debug:kf6
[gl-amd64-ai-p]: https://b.lily-is.land/plugins/lilybuild_artifacts/repos/6/latest/servant/artifacts/archive?job=build:GNU/Linux-amd64-prod:kf6
# Development
- Matrix room: `#kazv:tusooa.xyz`
- Official repository: [on Forgejo (main)](https://r.lily-is.land/the-kazv-project/kazv), [on Phorge (mirror)](https://iron.lily-is.land/diffusion/K/)
- Code review: [on Phorge (primary)](https://iron.lily-is.land/differential/query/fPoUK_z6_KRG/), [on Forgejo (secondary)](https://r.lily-is.land/the-kazv-project/kazv/pulls)
- For more: Read [Contributing to kazv](https://kazv.chat/en/contributing/)
# Installation
## GNU/ or musl/Linux
### flatpak
If you have flatpak, you can install kazv via flatpak. Because flatpak currently cannot be built in an unprivileged container, we are unfortunately unable to provide a built image as a part of CI/CD. See [this flatpak bug][flatpak-docker-bug] for more details. It seems that upstream is very uninterested in resolving this security issue, as this bug report has been open for more than five years with zero (0) comment from Collaborators of this repository.
[flatpak-docker-bug]: https://github.com/flatpak/flatpak/issues/3027
However, we will release *manually-built* flatpak packages from version 0.6.0 onwards. You can also build the flatpak via the instructions specified in the README file in the `packaging/GNU-Linux/flatpak` directory.
### Gentoo
If you are using Gentoo, you can use [tusooa-overlay][tusooa-overlay]
to install kazv.
[tusooa-overlay]: https://gitlab.com/tusooa/tusooa-overlay
### Nix/NixOS
Stable kazv is available as a [part of nixpkgs][nixpkgs].
Development version is available in [XYenon's nur-packages repository][xyenon-repo].
[nixpkgs]: https://github.com/NixOS/nixpkgs/blob/master/pkgs/by-name/ka/kazv/package.nix
[xyenon-repo]: https://github.com/XYenon/nur-packages
## Windows
You can build kazv using [Craft][craft]. You will need to use [our Craft blueprints][craft-blueprints-project-kazv]. Follow the instructions in that repository to build and run kazv. We also upload the binaries to the [release page][kazv-release].
[craft]: https://community.kde.org/Craft
[craft-blueprints-project-kazv]: https://r.lily-is.land/the-kazv-project/craft-blueprints-project-kazv
[kazv-release]: https://r.lily-is.land/the-kazv-project/kazv/releases
## Dependencies
- [ECM (extra-cmake-modules)](https://api.kde.org/ecm/)
- [Qt6](https://doc.qt.io): Core Gui Qml QuickControls2 Svg Concurrent Multimedia Network Widgets HttpServer (ImageFormats is a recommended runtime dependency, without which formats like webp cannot be displayed) (Test and QuickTest if you want to build tests)
- [KF6 (KDE Frameworks 6)](https://api.kde.org/): Kirigami KConfig Notifications CoreAddons KirigamiAddons (BreezeIcons is recommended)
- [nlohmann_json](https://github.com/nlohmann/json)
- [libkazv](https://r.lily-is.land/the-kazv-project/libkazv)
- [cmark](https://github.com/commonmark/cmark)
+- [lexbor](https://github.com/lexbor/lexbor)
## Build process
Normal build:
```
mkdir build && cd build
cmake ..
make install
```
Updating external JavaScript libraries (advanced, usually not needed):
You need to first have Node.js available.
```
cd src/js/Intl.js
npm install
cd ..
npm install
./transform.bash
```
Follow the instruction in [emoji.lisp](./tools/emoji.lisp) to update the emoji list (usually not needed)
## Appium tests
Some of the tests use appium. Please refer to the [article on KDE develop site](https://develop.kde.org/docs/apps/tests/appium/) for guides on how to setup appium and selenium-webdriver-at-spi. Once you have them installed, you can pass `-Dkazv_ENABLE_APPIUM_TESTS=ON` to enable them. The tests will run together with other tests when you invoke `ctest` or `make test`.
# Contributing
## Bugs
Report bugs to [our repository on Lily Islands](https://lily-is.land/kazv/kazv) (chances are it is the page you are currently viewing), or email to [project-bugs@kazv.moe](mailto:project-bugs@kazv.moe).
Report security vulnerabilities using a confidential GitLab issue on Lily Islands, or email to [project-security@kazv.moe](mailto:project-security@kazv.moe).
## Write code
To contribute code to the Kazv Project, open a merge request on Lily Islands, or email your patch to [project-patch@kazv.moe](mailto:project-patch@kazv.moe).
## Translations
kazv uses [fluent](https://projectfluent.org) for translations. To add a new language, add it into `src/l10n/translations.qrc`, `src/l10n/config.json`, and populate translations in the files under `src/l10n/<language code>`.
Before a change can be landed, the UI has to be translated into all of the **core languages**. One who submits a patch only needs to do the translations for at **least one** core language. The rest can be translated by others.
The list of **core languages** is defined in `src/js/l10n-config.json`. If you want to add other languages into the list and you have enough commitment to keep the translations up-to-date (refer to the rule before), you can submit a patch to do so.
We have a CI job to validate all the translation files, and ensure translations in all of the core languages are up-to-date.
## Donate
You can now donate to the Kazv Project on Liberapay: <https://liberapay.com/theKazvProject/>.
diff --git a/packaging/GNU-Linux/flatpak/moe.kazv.mxc.kazv.yaml b/packaging/GNU-Linux/flatpak/moe.kazv.mxc.kazv.yaml
index e2abe92..a41de03 100644
--- a/packaging/GNU-Linux/flatpak/moe.kazv.mxc.kazv.yaml
+++ b/packaging/GNU-Linux/flatpak/moe.kazv.mxc.kazv.yaml
@@ -1,117 +1,124 @@
id: moe.kazv.mxc.kazv
runtime: org.kde.Platform
runtime-version: '6.10'
sdk: org.kde.Sdk
sdk-extensions:
- org.freedesktop.Sdk.Extension.rust-stable
build-options:
append-path: /usr/lib/sdk/rust-stable/bin
command: kazv
finish-args:
- "--share=ipc"
- "--socket=fallback-x11"
- "--socket=wayland"
- "--share=network"
- "--filesystem=xdg-documents"
modules:
- name: boost
buildsystem: simple
build-commands:
- "./bootstrap.sh --prefix=/app --with-libraries=system,regex,serialization"
- "./b2 -j $FLATPAK_BUILDER_N_JOBS install"
sources:
- type: "archive"
url: https://archives.boost.io/release/1.78.0/source/boost_1_78_0.tar.bz2
sha256: "8681f175d4bdb26c52222665793eef08490d7758529330f98d3b29dd0735bccc"
- name: cryptopp
buildsystem: simple
build-commands:
- make
- make PREFIX=/ DESTDIR="${FLATPAK_DEST}" install
sources:
- type: archive
url: "https://github.com/weidai11/cryptopp/archive/refs/tags/CRYPTOPP_8_9_0.tar.gz"
sha256: ab5174b9b5c6236588e15a1aa1aaecb6658cdbe09501c7981ac8db276a24d9ab
- buildsystem: simple
name: vodozemac
build-commands:
- make -C cpp src/lib.rs
- CARGO_HOME=/run/build/vodozemac/cargo cargo --offline fetch --manifest-path Cargo.toml --verbose
- cd cpp && CARGO_HOME=/run/build/vodozemac/cargo make && CARGO_HOME=/run/build/vodozemac/cargo make PREFIX=/ DESTDIR="${FLATPAK_DEST}" install
sources:
- cargo-sources.json
- type: git
url: https://lily-is.land/kazv/vodozemac-bindings.git
tag: servant
- buildsystem: cmake-ninja
config-opts:
- '-DJSON_MultipleHeaders=ON'
- '-DJSON_BuildTests=OFF'
name: nlohmann-json
sources:
- type: archive
url: https://github.com/nlohmann/json/archive/v3.12.0.tar.gz
sha256: 4b92eb0c06d10683f7447ce9406cb97cd4b453be18d7279320f7b2f025c10187
- buildsystem: cmake-ninja
name: immer
config-opts:
- '-Dimmer_BUILD_TESTS=OFF'
- '-Dimmer_BUILD_EXAMPLES=OFF'
- '-Dimmer_BUILD_DOCS=OFF'
- '-Dimmer_BUILD_EXTRAS=OFF'
sources:
- type: git
url: https://github.com/arximboldi/immer.git
- buildsystem: cmake-ninja
name: zug
config-opts:
- '-Dzug_BUILD_TESTS=OFF'
- '-Dzug_BUILD_EXAMPLES=OFF'
- '-Dzug_BUILD_DOCS=OFF'
sources:
- type: git
url: https://github.com/arximboldi/zug.git
- buildsystem: cmake-ninja
name: lager
config-opts:
- '-Dlager_BUILD_TESTS=OFF'
- '-Dlager_BUILD_EXAMPLES=OFF'
- '-Dlager_BUILD_DOCS=OFF'
sources:
- type: git
url: https://github.com/arximboldi/lager.git
- buildsystem: cmake-ninja
name: libkazv
config-opts:
- '-Dlibkazv_BUILD_KAZVJOB=OFF'
- '-Dlibkazv_BUILD_TESTS=OFF'
- '-Dlibkazv_BUILD_EXAMPLES=OFF'
sources:
- type: git
url: https://lily-is.land/kazv/libkazv.git
tag: servant
- buildsystem: cmake-ninja
name: cmark
builddir: true
sources:
- type: archive
url: https://github.com/commonmark/cmark/archive/refs/tags/0.31.1.tar.gz
sha256: 3da93db5469c30588cfeb283d9d62edfc6ded9eb0edc10a4f5bbfb7d722ea802
- buildsystem: cmake-ninja
name: kirigami-addons
builddir: true
sources:
- type: git
url: https://invent.kde.org/libraries/kirigami-addons.git
tag: v1.4.0
+ - buildsystem: cmake-ninja
+ name: lexbor
+ builddir: true
+ sources:
+ - type: archive
+ url: https://github.com/lexbor/lexbor/archive/refs/tags/v3.0.0.tar.gz
+ sha256: eafaa79ef9871f0bbb1978eda8677d184f7ecdcaa203d7cd25b3f86e32c014c2
- buildsystem: cmake-ninja
name: kazv
sources:
- type: dir
path: ../../..
skip:
- packaging
- .git
- build
- node_modules
- TAGS
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index c773d82..1924a31 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -1,367 +1,370 @@
function(kazv_add_qml_files)
set(options "")
set(oneValueArgs BASE_DIRECTORY)
set(multiValueArgs SOURCES)
cmake_parse_arguments(PARSE_ARGV 0 kazv_add_qml_files "${options}" "${oneValueArgs}" "${multiValueArgs}")
set(target "${kazv_add_qml_files_UNPARSED_ARGUMENTS}")
set(allSources)
foreach(source ${kazv_add_qml_files_SOURCES})
message(STATUS "Adding source: ${kazv_add_qml_files_BASE_DIRECTORY} / ${source}")
set_source_files_properties(
"${kazv_add_qml_files_BASE_DIRECTORY}/${source}"
PROPERTIES
QT_RESOURCE_ALIAS "${source}"
)
list(APPEND allSources "${kazv_add_qml_files_BASE_DIRECTORY}/${source}")
endforeach()
qt_target_qml_sources(
"${target}"
QML_FILES ${allSources}
)
endfunction()
if(WIN32)
set(KAZV_IS_WINDOWS 1)
else()
set(KAZV_IS_WINDOWS 0)
endif()
if(ANDROID)
set(KAZV_IS_ANDROID 1)
else()
set(KAZV_IS_ANDROID 0)
endif()
configure_file(kazv-version.cpp.in kazv-version.cpp)
configure_file(kazv-platform.hpp.in kazv-platform.hpp)
configure_file(kazv-defs.hpp.in kazv-defs.hpp)
+add_subdirectory(html-sanitizer)
+
set(kazvqmlmodule_SRCS
qt-job-handler.cpp
qt-job.cpp
${CMAKE_CURRENT_BINARY_DIR}/kazv-version.cpp
constants.cpp
matrix-sdk.cpp
matrix-session.cpp
matrix-session-controller.cpp
sso-login-process.cpp
kazv-session-lock-guard.cpp
matrix-room.cpp
matrix-room-list.cpp
matrix-room-timeline.cpp
matrix-room-pinned-events-timeline.cpp
matrix-room-member.cpp
matrix-room-member-list-model.cpp
matrix-room-state.cpp
matrix-event-reader.cpp
matrix-event-reader-list-model.cpp
matrix-event-reaction-list-model.cpp
matrix-event-reaction.cpp
matrix-event.cpp
matrix-event-list.cpp
matrix-link.cpp
meta-types.cpp
l10n-provider.cpp
qt-rand-adapter.cpp
helper.cpp
matrix-promise.cpp
kazv-util.cpp
matrix-sticker-pack.cpp
matrix-sticker.cpp
matrix-sticker-pack-list.cpp
matrix-sticker-pack-source.cpp
matrix-user-given-attrs-map.cpp
matrix-verification.cpp
matrix-verification-list.cpp
kazv-abstract-list-model.cpp
qt-promise-handler.cpp
device-mgmt/matrix-device.cpp
device-mgmt/matrix-device-list.cpp
kazv-config.cpp
kazv-path-config.cpp
kazv-io-manager.cpp
kazv-io-job.cpp
kazv-file.cpp
upload-job-model.cpp
kazv-markdown.cpp
kazv-clipboard.cpp
db-store.cpp
)
ecm_qt_declare_logging_category(kazvqmlmodule_SRCS
HEADER kazv-log.hpp
IDENTIFIER kazvLog
CATEGORY_NAME moe.kazv.mxc.kazv
)
qt_add_qml_module(kazvqmlmodule
STATIC
URI moe.kazv.mxc.kazv
VERSION 0.0
SOURCES ${kazvqmlmodule_SRCS}
)
kconfig_target_kcfg_file(kazvqmlmodule
FILE kazv-settings.kcfg
CLASS_NAME KazvSettings
MUTATORS
GENERATE_MOC
)
kconfig_target_kcfg_file(kazvqmlmodule
FILE kazv-sessions-settings.kcfg
CLASS_NAME KazvSessionsSettings
MUTATORS
GENERATE_MOC
)
target_link_libraries(kazvqmlmodule PUBLIC
libkazv::kazvall
Qt${QT_MAJOR_VERSION}::Core
Qt${QT_MAJOR_VERSION}::Network
Threads::Threads
Qt${QT_MAJOR_VERSION}::Gui
Qt${QT_MAJOR_VERSION}::Qml
Qt${QT_MAJOR_VERSION}::Quick
Qt${QT_MAJOR_VERSION}::QuickControls2
Qt${QT_MAJOR_VERSION}::Svg
Qt${QT_MAJOR_VERSION}::Concurrent
Qt${QT_MAJOR_VERSION}::Widgets
Qt${QT_MAJOR_VERSION}::Sql
Qt${QT_MAJOR_VERSION}::HttpServer
KF${KF_MAJOR_VERSION}::ConfigCore
KF${KF_MAJOR_VERSION}::ConfigGui
KF${KF_MAJOR_VERSION}::Notifications
KF${KF_MAJOR_VERSION}::CoreAddons
QCoro::Core
${CMARK_TARGET_NAME}
)
if(ANDROID)
target_link_libraries(kazvqmlmodule PUBLIC
KF${KF_MAJOR_VERSION}::Kirigami
OpenSSL::SSL
)
endif()
+target_link_libraries(kazvqmlmodule PRIVATE kazvsanitizer)
target_include_directories(kazvqmlmodule PUBLIC ${CMAKE_CURRENT_BINARY_DIR})
target_include_directories(kazvqmlmodule PUBLIC ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/device-mgmt)
qt_add_qml_module(kazvshortcutsqmlmodule
STATIC
URI moe.kazv.mxc.kazvshortcuts
VERSION 0.0
SOURCES
shortcuts/shortcut-util.cpp
)
target_link_libraries(kazvshortcutsqmlmodule PUBLIC
libkazv::kazvall
Qt${QT_MAJOR_VERSION}::Core
Qt${QT_MAJOR_VERSION}::Gui
Qt${QT_MAJOR_VERSION}::Qml
)
target_include_directories(kazvshortcutsqmlmodule
PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/shortcuts
)
add_library(kazvprivlib INTERFACE)
target_link_libraries(kazvprivlib
INTERFACE
kazvqmlmodule
kazvqmlmoduleplugin
kazvshortcutsqmlmodule
kazvshortcutsqmlmoduleplugin
)
qt_add_qml_module(kazvqmlqmlmodule
STATIC
URI moe.kazv.mxc.kazvqml
VERSION 0.0
)
kazv_add_qml_files(kazvqmlqmlmodule
BASE_DIRECTORY contents/ui
SOURCES
Main.qml
About.qml
PageManager.qml
LoginPage.qml
LoginFlows.qml
MainPage.qml
TabView.qml
Tab.qml
ClosableScrollablePage.qml
ClosableScrollableOverlay.qml
SelfDestroyableOverlaySheet.qml
FocusRingBackground.qml
RoomListView.qml
RoomListViewItemDelegate.qml
RoomPage.qml
RoomPinnedEventsPage.qml
RoomTimelineView.qml
SendMessageBox.qml
EventView.qml
EventViewWrapper.qml
EventViewCompact.qml
EventSourceView.qml
EventHistoryView.qml
EventReactions.qml
Bubble.qml
MediaFileMenu.qml
KazvIOMenu.qml
ConfirmUploadPopup.qml
StickerPicker.qml
AddStickerPopup.qml
StickerPackNameProvider.qml
ConfirmationOverlay.qml
AcceptRejectNoopOverlay.qml
VerificationsPage.qml
SasCodes.qml
event-types/Simple.qml
event-types/Text.qml
event-types/Emote.qml
event-types/Notice.qml
event-types/State.qml
event-types/TextTemplate.qml
event-types/Image.qml
event-types/File.qml
event-types/Video.qml
event-types/Audio.qml
event-types/MediaBubble.qml
event-types/Redacted.qml
event-types/Reaction.qml
event-types/Fallback.qml
TypingIndicator.qml
EventReadIndicator.qml
SelectableText.qml
ReactToEventPopup.qml
FileHandler.qml
media/PlayButton.qml
ActionSettingsPage.qml
CreateRoomPage.qml
JoinRoomPage.qml
RoomMemberListView.qml
RoomMemberListViewItemDelegate.qml
UserNameProvider.qml
RoomNameProvider.qml
Completion.qml
AsyncHandler.qml
UploadFileHelper.qml
SettingsPage.qml
settings/ProfileSettings.qml
settings/CacheSettings.qml
settings/SecuritySettings.qml
room-settings/RoomSettingsPage.qml
room-settings/RoomTagHandler.qml
room-settings/RoomMemberListPage.qml
room-settings/RoomInvitePage.qml
room-settings/RoomStickerPacksPage.qml
room-settings/RoomStickerPackItemDelegate.qml
room-settings/RoomStatePage.qml
device-mgmt/Device.qml
device-mgmt/DeviceList.qml
UserPage.qml
KazvAction.qml
shortcuts/ActionCollection.qml
shortcuts/ActionItem.qml
shortcuts/ActionSettings.qml
shortcuts/ShortcutInput.qml
actions/GlobalDrawerActions.qml
actions/RoomPageActions.qml
actions/BubbleActions.qml
actions/RoomListActions.qml
actions/DialogActions.qml
actions/SettingsActions.qml
actions/AllActions.qml
MessageNotification.qml
Notifier.qml
NotificationAction.qml
AvatarAdapter.qml
ImageAdapter.qml
FileDialogAdapter.qml
FolderDialogAdapter.qml
ScrollablePageAdapter.qml
emoji/Activities.qml
emoji/Flags.qml
emoji/Objects.qml
emoji/SmileysAndEmotion.qml
emoji/TravelAndPlaces.qml
emoji/AnimalsAndNature.qml
emoji/FoodAndDrink.qml
emoji/PeopleAndBody.qml
emoji/Symbols.qml
)
kazv_add_qml_files(kazvqmlqmlmodule
BASE_DIRECTORY js
SOURCES
l10n.js
global-this.js
matrix-helpers.js
sas-emoji-defs.js
)
kazv_add_qml_files(kazvqmlqmlmodule
BASE_DIRECTORY js/transformed-libs
SOURCES
fluent-bundle.js
fluent-sequence.js
fluent-langneg.js
bundled-deps.js
)
target_link_libraries(kazvqmlqmlmodule PUBLIC kazvprivlib)
set(kazv_SRCS
main.cpp
l10n/translations.qrc
)
add_executable(kazv ${kazv_SRCS})
target_link_libraries(kazv
PRIVATE
kazvqmlqmlmodule
kazvqmlqmlmoduleplugin
kazvprivlib
)
if(kazv_LINK_BREEZE_ICONS)
target_link_libraries(kazv PRIVATE KF${KF_MAJOR_VERSION}::BreezeIcons)
endif()
if(ANDROID)
message(STATUS "Collecting icons for apk deployment")
execute_process(
COMMAND
${PERL_EXECUTABLE} ${CMAKE_SOURCE_DIR}/tools/scan-icons
RESULT_VARIABLE KAZV_SCAN_ICONS_RESULT
OUTPUT_VARIABLE KAZV_FOUND_ICONS
COMMAND_ERROR_IS_FATAL ANY
)
kirigami_package_breeze_icons(ICONS ${KAZV_FOUND_ICONS})
endif()
set(kazv_INSTALL_ARGS ${KF_INSTALL_TARGETS_DEFAULT_ARGS})
if(NOT kazv_INSTALL_ARGS)
set(kazv_INSTALL_ARGS ${KF${KF_MAJOR_VERSION}_INSTALL_TARGETS_DEFAULT_ARGS})
endif()
install(TARGETS kazv ${kazv_INSTALL_ARGS})
if(ANDROID)
ecm_add_android_apk(kazv ANDROID_DIR ${CMAKE_CURRENT_SOURCE_DIR}/android)
endif()
if(kazv_BUILD_TESTS)
add_subdirectory(tests)
endif()
diff --git a/src/contents/ui/event-types/TextTemplate.qml b/src/contents/ui/event-types/TextTemplate.qml
index 7709d2c..8ca0329 100644
--- a/src/contents/ui/event-types/TextTemplate.qml
+++ b/src/contents/ui/event-types/TextTemplate.qml
@@ -1,204 +1,199 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2020-2023 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import QtQuick 2.2
import QtQuick.Layouts 1.15
import QtQuick.Controls 2.15
import org.kde.kirigami 2.13 as Kirigami
import moe.kazv.mxc.kazv 0.0 as MK
import '.' as Types
import '..' as Kazv
import '../matrix-helpers.js' as Helpers
Types.Simple {
id: upper
property var text
default property var children
property alias textFormat: label.textFormat
property alias innerBubble: bubble
property var linkToActivate
property var selectedUserId
property var ensureMemberEvent: Kazv.AsyncHandler {
trigger: () => room.ensureStateEvent('m.room.member', upper.selectedUserId)
onResolved: (success, data) => {
if (success) {
activateUserPage(room.member(upper.selectedUserId), room);
} else {
// TODO: This opens the matrix.to url directly.
// In a future version this should take you to a window that
// gives you the option to create a DM with that user.
openLink(upper.linkToActivate);
}
}
}
Kazv.Bubble {
id: bubble
Layout.fillWidth: true
RowLayout {
Layout.fillWidth: true
property var label: Kazv.SelectableText {
objectName: 'textEventContent'
id: label
Layout.fillWidth: true
wrapMode: Text.Wrap
text: upper.text
onLinkActivated: (link) => {
const userId = MK.KazvUtil.matrixLinkUserId(link);
if (userId) {
upper.selectedUserId = userId;
upper.linkToActivate = link;
ensureMemberEvent.call();
} else {
openLink(link);
}
}
onHoveredLinkChanged: (link) => {
if (link) {
// first give it text, then make it visible
label.ToolTip.text = link;
label.ToolTip.visible = true;
} else {
// first make it invisible, then remove the text
label.ToolTip.visible = false;
label.ToolTip.text = '';
}
}
ToolTip.delay: Kirigami.Units.toolTipDelay
ToolTip.timeout: Helpers.toolTipTimeout
}
data: [
...(Array.isArray(upper.children) ? upper.children :
upper.children ? [upper.children] : []),
label
]
}
}
property var isHtmlFormatted: event.content.format === 'org.matrix.custom.html' && event.content.formatted_body
property var openRoomAliasLink: Kazv.AsyncHandler {
property var link
property var joinRoom
trigger: () => matrixSession.getRoomIdByAlias(link.identifiers[0])
onResolved: {
if (success) {
if (sdkVars.roomList.contains(data.roomId)) {
switchToRoomRequested(data.roomId);
} else {
joinRoom(link.identifiers[0], link.routingServers);
}
} else {
showPassiveNotification(l10n.get('get-room-id-by-alias-failed-prompt', { errorCode: data.errorCode, errorMsg: data.error }));
}
}
}
function getMaybeFormattedText() {
if (isHtmlFormatted) {
const stylesheet = `
<style>
del {
text-decoration: line-through;
}
a[href^="https://matrix.to/#/@"] {
background-color: ${Kirigami.Theme.positiveBackgroundColor};
}
</style>
`;
- const formattedBody = event.content.formatted_body;
- if (event.replyingToEventId && formattedBody.startsWith('<mx-reply>')) {
- const index = formattedBody.indexOf('</mx-reply>');
- return stylesheet + formattedBody.slice(index + '</mx-reply>'.length);
- } else {
- return stylesheet + formattedBody;
- }
+ const formattedBody = MK.KazvUtil.sanitizeHtml(event.content.formatted_body);
+ return stylesheet + formattedBody;
} else {
return event.content.body;
}
}
// https://github.com/matrix-org/matrix-spec-proposals/blob/main/proposals/2312-matrix-uri.md#operations-on-matrix-uris
function openLink(link) {
const matrixLink = MK.KazvUtil.matrixLink(link);
// Not a valid matrix uri
if (!matrixLink.isValid) {
Qt.openUrlExternally(link);
return;
}
const action = matrixLink.action;
const servers = matrixLink.routingServers;
const joinRoom = (roomId, servers) => {
pushJoinRoomPage();
pageStack.currentItem.presetPage(roomId, servers);
};
// Handle link to a user
if (matrixLink.isUser) {
const userId = matrixLink.identifiers[0];
if (action !== 'chat') {
mentionUserRequested(userId);
return;
}
// If the room with the user already exists, switch to the room.
// Otherwise try to create the room.
for (const roomId of matrixSession.directRoomIds(userId)) {
const invitedOrJoined = (roomId) => {
const membership = sdkVars.roomList.room(roomId).membership;
return (membership == MK.MatrixRoom.Invite) || (membership == MK.MatrixRoom.Join);
};
if (sdkVars.roomList.contains(roomId) && invitedOrJoined(roomId)) {
switchToRoomRequested(roomId);
return;
}
}
pushCreateRoomPage();
pageStack.currentItem.presetPage(userId);
return;
}
// Handle link to a event
if (matrixLink.isEvent) {
const roomId = matrixLink.identifiers[0];
const eventId = matrixLink.identifiers[1];
if (sdkVars.roomList.contains(roomId)) {
switchToRoomRequested(roomId);
pageStack.currentItem.delayGoToEvent(eventId, 500);
} else {
joinRoom(roomId, servers);
}
return;
}
// Handle link to a room
if (matrixLink.isRoom) {
// If user has joined the room, open it.
// Otherwise try to join it.
if (matrixLink.isRoomAlias) {
openRoomAliasLink.link = matrixLink;
openRoomAliasLink.joinRoom = joinRoom;
openRoomAliasLink.call();
return;
}
const roomId = matrixLink.identifiers[0];
if (sdkVars.roomList.contains(roomId)) {
switchToRoomRequested(roomId);
} else {
joinRoom(roomId, servers);
}
return;
}
}
}
diff --git a/src/html-sanitizer/CMakeLists.txt b/src/html-sanitizer/CMakeLists.txt
new file mode 100644
index 0000000..bdb8358
--- /dev/null
+++ b/src/html-sanitizer/CMakeLists.txt
@@ -0,0 +1,6 @@
+set(kazvsanitizer_SRCS
+ sanitize.cpp
+)
+add_library(kazvsanitizer STATIC ${kazvsanitizer_SRCS})
+target_link_libraries(kazvsanitizer PRIVATE lexbor::lexbor)
+target_include_directories(kazvsanitizer INTERFACE ${CMAKE_CURRENT_SOURCE_DIR})
diff --git a/src/html-sanitizer/sanitize.cpp b/src/html-sanitizer/sanitize.cpp
new file mode 100644
index 0000000..4dd9cac
--- /dev/null
+++ b/src/html-sanitizer/sanitize.cpp
@@ -0,0 +1,289 @@
+/*
+ * This file is part of kazv.
+ * SPDX-FileCopyrightText: 2026 tusooa <tusooa@kazv.moe>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+// #include <kazv-defs.hpp> intentionally omitted
+#include <lexbor/html/html.h>
+#include <lexbor/dom/dom.h>
+#include <memory>
+#include <functional>
+#include <variant>
+#include <set>
+#include <iterator>
+#include <cassert>
+#include <string>
+#include <string_view>
+
+using namespace std::string_literals;
+using namespace std::string_view_literals;
+
+namespace
+{
+ struct DocDeleter
+ {
+ void operator()(lxb_html_document_t *doc)
+ {
+ lxb_html_document_destroy(doc);
+ }
+ };
+
+ using DocUP = std::unique_ptr<lxb_html_document_t, DocDeleter>;
+
+ // Remove current node and promote all of its children to the parent
+ struct Promote {};
+ // Delete the subtree of the current node
+ struct DeleteAll {};
+ // Do not do anything with the current node. All changes have already been made
+ // by the visitor. Childrens will still be visited.
+ struct Nothing {};
+
+ using VisitResult = std::variant<
+ Promote, DeleteAll, Nothing
+ >;
+ using Visitor = std::function<VisitResult(lxb_dom_node_t *)>;
+
+ struct LexborAttributeIterator {
+ using difference_type = std::ptrdiff_t;
+ using value_type = lxb_dom_attr_t *;
+
+ value_type m_val;
+
+ value_type operator*() const
+ {
+ return m_val;
+ }
+
+ LexborAttributeIterator& operator++()
+ {
+ m_val = lxb_dom_element_next_attribute(m_val);
+ return *this;
+ }
+
+ void operator++(int)
+ {
+ ++*this;
+ }
+ };
+}
+
+// Matrix spec client-server api
+static const std::set<lxb_tag_id_t> allowedTags{
+ LXB_TAG__TEXT,
+ LXB_TAG_DEL,
+ LXB_TAG_H1,
+ LXB_TAG_H2,
+ LXB_TAG_H3,
+ LXB_TAG_H4,
+ LXB_TAG_H5,
+ LXB_TAG_H6,
+ LXB_TAG_BLOCKQUOTE,
+ LXB_TAG_P,
+ LXB_TAG_A,
+ LXB_TAG_UL,
+ LXB_TAG_OL,
+ LXB_TAG_SUP,
+ LXB_TAG_SUB,
+ LXB_TAG_LI,
+ LXB_TAG_B,
+ LXB_TAG_I,
+ LXB_TAG_U,
+ LXB_TAG_STRONG,
+ LXB_TAG_EM,
+ LXB_TAG_S,
+ LXB_TAG_CODE,
+ LXB_TAG_HR,
+ LXB_TAG_BR,
+ LXB_TAG_DIV,
+ LXB_TAG_TABLE,
+ LXB_TAG_THEAD,
+ LXB_TAG_TBODY,
+ LXB_TAG_TR,
+ LXB_TAG_TH,
+ LXB_TAG_TD,
+ LXB_TAG_CAPTION,
+ LXB_TAG_PRE,
+ LXB_TAG_SPAN,
+ LXB_TAG_IMG,
+ LXB_TAG_DETAILS,
+ LXB_TAG_SUMMARY,
+};
+
+template<class Pred>
+static void filterAttrs(lxb_dom_element_t *element, Pred pred)
+{
+ auto attr = lxb_dom_element_first_attribute(element);
+
+ while (attr) {
+ auto next = lxb_dom_element_next_attribute(attr);
+ std::size_t nameLen;
+ const lxb_char_t *name = lxb_dom_attr_local_name(attr, &nameLen);
+ auto nameView = std::string_view(reinterpret_cast<const char *>(name), nameLen);
+
+ std::size_t valueLen;
+ const lxb_char_t *value = lxb_dom_attr_value(attr, &valueLen);
+ auto valueView = std::string_view(reinterpret_cast<const char *>(value), valueLen);
+
+ if (!pred(nameView, valueView)) {
+ lxb_dom_element_remove_attribute(element, name, nameLen);
+ }
+
+ attr = next;
+ }
+}
+
+static VisitResult sanitizeVisitor(lxb_dom_node_t *node)
+{
+ auto tagId = lxb_dom_node_tag_id(node);
+ if (!(
+ lxb_dom_node_type(node) == LXB_DOM_NODE_TYPE_ELEMENT
+ || lxb_dom_node_type(node) == LXB_DOM_NODE_TYPE_TEXT
+ )) {
+ return DeleteAll{};
+ }
+ // validate tag names
+ if (!allowedTags.contains(tagId) || node->ns != LXB_NS_HTML) {
+ if (tagId >= LXB_TAG__LAST_ENTRY) {
+ std::size_t tagNameSize{0};
+ auto tagName = lxb_tag_name_by_id_noi(tagId, &tagNameSize);
+ auto tagNameStr = std::string_view(reinterpret_cast<const char *>(tagName), tagNameSize);
+ if (tagNameStr == "mx-reply") {
+ return DeleteAll{};
+ }
+ }
+ return Promote{};
+ }
+
+ // validate attributes
+ if (lxb_dom_node_type(node) == LXB_DOM_NODE_TYPE_ELEMENT) {
+ auto elem = lxb_dom_interface_element(node);
+ switch (tagId) {
+ case LXB_TAG_A:
+ filterAttrs(elem, [](const auto &name, const auto &value) {
+ return name == "target"
+ || (name == "href" && (
+ value.starts_with("https:")
+ || value.starts_with("http:")
+ || value.starts_with("ftp:")
+ || value.starts_with("mailto:")
+ || value.starts_with("magnet:")
+ // isn't in spec as of 1.19, but we should support it
+ // because we want to support matrix: uri handling
+ || value.starts_with("matrix:")
+ ));
+ });
+ break;
+
+ case LXB_TAG_SPAN:
+ filterAttrs(elem, [](const auto &name, [[maybe_unused]] const auto &value) {
+ return name == "data-mx-bg-color"
+ || name == "data-mx-color"
+ || name == "data-mx-spoiler"
+ || name == "data-mx-maths";
+ });
+ break;
+
+ case LXB_TAG_IMG:
+ filterAttrs(elem, [](const auto &name, const auto &value) {
+ return name == "width"
+ || name == "height"
+ || name == "alt"
+ || name == "title"
+ || (name == "src" && value.starts_with("mxc://"));
+ });
+ break;
+
+ case LXB_TAG_OL:
+ filterAttrs(elem, [](const auto &name, [[maybe_unused]] const auto &value) {
+ return name == "start";
+ });
+ break;
+
+ case LXB_TAG_CODE:
+ filterAttrs(elem, [](const auto &name, const auto &value) {
+ return name == "class" && value.starts_with("language-");
+ });
+ break;
+
+ case LXB_TAG_DIV:
+ filterAttrs(elem, [](const auto &name, [[maybe_unused]] const auto &value) {
+ return name == "data-mx-maths";
+ });
+ break;
+
+ default:
+ filterAttrs(elem, []([[maybe_unused]] const auto &name, [[maybe_unused]] const auto &value) {
+ return false;
+ });
+ break;
+ }
+ }
+ return Nothing{};
+}
+
+static lxb_dom_node_t *addAllChildren(lxb_dom_node_t *root, lxb_dom_node_t *insertBefore, lxb_dom_node_t *source)
+{
+ auto first = lxb_dom_node_first_child(source);
+ auto cur = first;
+ while (cur) {
+ auto next = lxb_dom_node_next(cur);
+ if (!insertBefore) {
+ auto r = lxb_dom_node_append_child(root, cur);
+ assert(r == LXB_DOM_EXCEPTION_OK);
+ } else {
+ auto r = lxb_dom_node_insert_before_spec(root, cur, insertBefore);
+ assert(r == LXB_DOM_EXCEPTION_OK);
+ }
+ cur = next;
+ }
+ return first;
+}
+
+static void sanitizeTraverse(lxb_dom_node_t *root, Visitor visit)
+{
+ auto cur = lxb_dom_node_first_child(root);
+ while (cur) {
+ auto res = std::invoke(visit, cur);
+ auto origNext = lxb_dom_node_next(cur);
+ auto next = origNext;
+ if (std::holds_alternative<Promote>(res)) {
+ auto r = lxb_dom_node_remove_child(root, cur);
+ assert(r == LXB_DOM_EXCEPTION_OK);
+ auto firstChild = addAllChildren(root, origNext, cur);
+ lxb_dom_node_destroy_deep(cur);
+ if (firstChild) {
+ next = firstChild;
+ }
+ } else if (std::holds_alternative<DeleteAll>(res)) {
+ auto r = lxb_dom_node_remove_child(root, cur);
+ assert(r == LXB_DOM_EXCEPTION_OK);
+ lxb_dom_node_destroy_deep(cur);
+ }
+ sanitizeTraverse(cur, visit);
+ cur = next;
+ }
+}
+
+std::string sanitizeHtml(const std::string &text)
+{
+ auto doc = DocUP(lxb_html_document_create());
+ if (!doc) {
+ return "";
+ }
+ lxb_dom_document_opt_set(lxb_dom_interface_document(doc.get()), LXB_DOM_DOCUMENT_OPT_WO_EVENTS);
+ if (lxb_html_document_parse(doc.get(), reinterpret_cast<const lxb_char_t *>(""), 0) != LXB_STATUS_OK) {
+ return "";
+ }
+ lxb_dom_element_t *context = lxb_dom_interface_element(doc->body);
+ lxb_dom_node_t *fragRoot = lxb_html_document_parse_fragment(
+ doc.get(), context, reinterpret_cast<const lxb_char_t *>(text.data()), text.size());
+
+ sanitizeTraverse(fragRoot, sanitizeVisitor);
+ lexbor_str_t str = {0};
+ auto status = lxb_html_serialize_deep_str(fragRoot, &str);
+ if (status != LXB_STATUS_OK) {
+ return "";
+ }
+ return std::string(reinterpret_cast<const char *>(str.data), str.length);
+}
diff --git a/src/html-sanitizer/sanitize.hpp b/src/html-sanitizer/sanitize.hpp
new file mode 100644
index 0000000..7847600
--- /dev/null
+++ b/src/html-sanitizer/sanitize.hpp
@@ -0,0 +1,11 @@
+/*
+ * This file is part of kazv.
+ * SPDX-FileCopyrightText: 2026 tusooa <tusooa@kazv.moe>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+#pragma once
+// #include <kazv-defs.hpp> intentionally omitted
+#include <string>
+
+std::string sanitizeHtml(const std::string &text);
diff --git a/src/kazv-util.cpp b/src/kazv-util.cpp
index 26e2dc8..6ac00f9 100644
--- a/src/kazv-util.cpp
+++ b/src/kazv-util.cpp
@@ -1,44 +1,50 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2023 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#include <kazv-defs.hpp>
#include <QUrl>
#include <QMimeDatabase>
#include "matrix-link.hpp"
#include "kazv-util.hpp"
+#include "html-sanitizer/sanitize.hpp"
KazvUtil::KazvUtil(QObject *parent)
: QObject(parent)
, m_kfQtMajorVersion(KAZV_KF_QT_MAJOR_VERSION)
{}
KazvUtil::~KazvUtil() = default;
QString KazvUtil::escapeHtml(const QString &orig) const
{
return orig.toHtmlEscaped();
}
QString KazvUtil::mimeTypeForUrl(const QUrl &url) const
{
return QMimeDatabase().mimeTypeForUrl(url).name();
}
QString KazvUtil::matrixLinkUserId(const QString &url) const
{
MatrixLink link = QUrl(url);
if (link.isUser()) {
return link.identifiers()[0];
}
return QStringLiteral("");
}
MatrixLink *KazvUtil::matrixLink(const QString &url) const
{
return new MatrixLink(QUrl(url));
}
+
+QString KazvUtil::sanitizeHtml(const QString &raw) const
+{
+ return QString::fromStdString(::sanitizeHtml(raw.toStdString()));
+}
diff --git a/src/kazv-util.hpp b/src/kazv-util.hpp
index 4df9bf8..7a9b9b9 100644
--- a/src/kazv-util.hpp
+++ b/src/kazv-util.hpp
@@ -1,39 +1,41 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2023 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#pragma once
#include <kazv-defs.hpp>
#include "matrix-link.hpp"
#include <QObject>
#include <QString>
#include <QQmlEngine>
class KazvUtil : public QObject
{
Q_OBJECT
QML_ELEMENT
QML_SINGLETON
Q_PROPERTY(int kfQtMajorVersion CONSTANT MEMBER m_kfQtMajorVersion)
public:
KazvUtil(QObject *parent = 0);
~KazvUtil() override;
public Q_SLOTS:
QString escapeHtml(const QString &orig) const;
QString mimeTypeForUrl(const QUrl &url) const;
/// @return The user id for a given matrix link, or the empty string
/// if @c url is not a matrix link pointing to a user
QString matrixLinkUserId(const QString &url) const;
MatrixLink *matrixLink(const QString &url) const;
+ QString sanitizeHtml(const QString &raw) const;
+
private:
int m_kfQtMajorVersion;
};
diff --git a/src/tests/CMakeLists.txt b/src/tests/CMakeLists.txt
index ba70b0c..3e1ced8 100644
--- a/src/tests/CMakeLists.txt
+++ b/src/tests/CMakeLists.txt
@@ -1,157 +1,166 @@
set(KAZV_TEST_TEMP_DIR ${CMAKE_CURRENT_BINARY_DIR}/test-temp)
configure_file(test-temp.hpp.in test-temp.hpp)
set(kazvtestlib_SRCS
test-model.cpp
)
add_library(kazvtestlib STATIC ${kazvtestlib_SRCS})
target_include_directories(kazvtestlib PUBLIC ${CMAKE_CURRENT_BINARY_DIR})
target_link_libraries(kazvtestlib PUBLIC kazvprivlib libkazv::kazvtestfixtures)
add_executable(matrix-sdk-session-loader matrix-sdk-session-loader.cpp matrix-sdk-sessions-test.cpp)
target_compile_definitions(matrix-sdk-session-loader PRIVATE MATRIX_SDK_SESSIONS_TEST_NO_MAIN)
target_link_libraries(matrix-sdk-session-loader Qt${QT_MAJOR_VERSION}::Test kazvtestlib)
ecm_add_tests(
qt-job-handler-test.cpp
qt-promise-handler-test.cpp
qt-json-test.cpp
device-mgmt-test.cpp
matrix-session-test.cpp
matrix-session-controller-test.cpp
matrix-sdk-sessions-test.cpp
matrix-promise-test.cpp
matrix-room-timeline-test.cpp
matrix-room-pinned-events-timeline-test.cpp
matrix-room-member-list-model-test.cpp
matrix-room-list-test.cpp
matrix-room-state-test.cpp
kazv-markdown-test.cpp
matrix-sticker-pack-test.cpp
matrix-link-test.cpp
matrix-user-given-attrs-map-test.cpp
kazv-abstract-list-model-test.cpp
matrix-event-list-test.cpp
matrix-event-test.cpp
kazv-file-test.cpp
db-store-test.cpp
sso-login-process-test.cpp
kazv-config-test.cpp
LINK_LIBRARIES Qt${QT_MAJOR_VERSION}::Test kazvtestlib
)
ecm_add_test(
kazv-io-job-test.cpp
LINK_LIBRARIES Qt${QT_MAJOR_VERSION}::Test Qt${QT_MAJOR_VERSION}::HttpServer kazvtestlib
)
ecm_add_test(
kazv-io-manager-test.cpp
LINK_LIBRARIES Qt${QT_MAJOR_VERSION}::Test Qt${QT_MAJOR_VERSION}::HttpServer kazvtestlib
)
+ecm_add_test(
+ kazv-sanitizer-test.cpp
+ LINK_LIBRARIES Qt${QT_MAJOR_VERSION}::Test Qt${QT_MAJOR_VERSION}::HttpServer kazvsanitizer
+)
+
add_executable(quicktest quick-test.cpp)
target_link_libraries(quicktest PRIVATE Qt${QT_MAJOR_VERSION}::QuickTest kazvtestlib)
if(KAZV_BUILD_BENCHMARKS)
ecm_add_test(
matrix-room-timeline-benchmark-test.cpp
LINK_LIBRARIES Qt${QT_MAJOR_VERSION}::Test kazvtestlib
)
+
+ ecm_add_test(kazv-sanitizer-benchmark-test.cpp
+ LINK_LIBRARIES Qt${QT_MAJOR_VERSION}::Test kazvsanitizer
+ )
endif()
set(quicktest_QMLS
tst_About.qml
tst_AddStickerPopup.qml
tst_AvatarImageAdapters.qml
tst_ConfirmUploadPopup.qml
tst_CreateRoomPage.qml
tst_DeviceList.qml
tst_DevicePopup.qml
tst_Device.qml
tst_EventHistoryView.qml
tst_EventReactions.qml
tst_EventReadIndicator.qml
tst_EventSourceView.qml
tst_EventView.qml
tst_EventViewRedacted.qml
tst_EventViewVideo.qml
tst_FileHandler.qml
tst_JoinRoomPage.qml
tst_KazvIOMenu.qml
tst_kazvshortcutsActionItem.qml
tst_LoginPage.qml
tst_MatrixHelpers.qml
tst_Notifications.qml
tst_PageManager.qml
tst_ReactToEventPopup.qml
tst_RoomInvitePage.qml
tst_RoomListViewItemDelegate.qml
tst_RoomMemberListPage.qml
tst_RoomNameProvider.qml
tst_RoomPage.qml
tst_RoomPinnedEventsPage.qml
tst_RoomSettingsPage.qml
tst_RoomStatePage.qml
tst_RoomStickerPackItemDelegate.qml
tst_RoomStickerPacksPage.qml
tst_RoomTimelineView.qml
tst_SendMessageBoxDrafts.qml
tst_SendMessageBox.qml
tst_StickerPackNameProvider.qml
tst_StickerPicker.qml
tst_TypingIndicator.qml
tst_UserNameProvider.qml
tst_UserPage.qml
)
foreach(testfilename ${quicktest_QMLS})
message(STATUS "Adding quick test ${testfilename}")
string(REPLACE .qml "" testname ${testfilename})
add_test(NAME ${testname} COMMAND quicktest -input ${CMAKE_CURRENT_SOURCE_DIR}/quick-tests/${testfilename})
set_property(TEST ${testname}
PROPERTY LABELS "quicktest")
endforeach()
if(kazv_ENABLE_APPIUM_TESTS)
qt_add_qml_module(kazvappiumcomponentqmlmodule
STATIC
URI moe.kazv.mxc.kazvappiumcomponent
VERSION 0.0
)
kazv_add_qml_files(kazvappiumcomponentqmlmodule
BASE_DIRECTORY run-appium-component-qml
SOURCES
Main.qml
)
add_executable(run-appium-component run-appium-component.cpp)
target_link_libraries(run-appium-component
PRIVATE
kazvappiumcomponentqmlmodule
kazvappiumcomponentqmlmoduleplugin
kazvtestlib
Qt::Quick
)
set(appium_tests_SOURCES
TestStickerPicker.py
TestEventViewUnavailable.py
TestRoomTimelineView.py
)
foreach(testfilename ${appium_tests_SOURCES})
message(STATUS "Adding appium test ${testfilename}")
string(REPLACE .py "" testname ${testfilename})
add_test(NAME ${testname}
COMMAND
${SELENIUM_WEBDRIVER_AT_SPI_RUN_PATH}
${CMAKE_CURRENT_SOURCE_DIR}/appium-tests/${testfilename}
)
set_property(TEST ${testname}
PROPERTY ENVIRONMENT "KAZV_APPIUM_TEST_BIN=$<TARGET_FILE:run-appium-component>;TEST_WITH_VIDEO_RECORDER=0")
set_property(TEST ${testname}
PROPERTY LABELS "appiumtest")
endforeach()
endif()
diff --git a/src/tests/kazv-sanitizer-benchmark-test.cpp b/src/tests/kazv-sanitizer-benchmark-test.cpp
new file mode 100644
index 0000000..12fae6a
--- /dev/null
+++ b/src/tests/kazv-sanitizer-benchmark-test.cpp
@@ -0,0 +1,44 @@
+/*
+ * This file is part of kazv.
+ * SPDX-FileCopyrightText: 2026 tusooa <nannanko@kazv.moe>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+// #include <kazv-defs.hpp> intentionally omitted
+#include "sanitize.hpp"
+#include <QtTest>
+
+static const std::string input = R"xx(
+Lorem ipsum <p>dolor</p> sit amet, consectetur adipiscing elit. Curabitur augue risus, commodo quis dolor a, ultricies semper turpis. In aliquet finibus nunc at lobortis. Cras porta rutrum <div>elit ac <img alt="dapibus">. Suspendisse <p xxx="yyy">efficitur</p> sit amet enim eget consectetur. Vivamus luctus nisi volutpat lobortis dignissim. Praesent <script lang="text/javascript">nec velit quis sapien fringilla consectetur fringilla et leo. Donec lorem felis, volutpat ut</script> neque at, consequat elementum eros</div>. Praesent pretium <html>enim</html> urna, suscipit venenatis urna bibendum sit amet. Nam tincidunt nibh at purus <strong><strong><strong>egestas</strong> pretium</strong>. Sed</strong> euismod tellus ac ex interdum, id facilisis purus egestas. Cras ut dui ligula. Nunc dapibus auctor rhoncus.
+)xx";
+
+class KazvSanitizerBenchmarkTest : public QObject
+{
+ Q_OBJECT
+
+private Q_SLOTS:
+ void testSanitize();
+ void testSanitizeQString();
+};
+
+void KazvSanitizerBenchmarkTest::testSanitize()
+{
+ QBENCHMARK {
+ sanitizeHtml(input);
+ }
+}
+
+void KazvSanitizerBenchmarkTest::testSanitizeQString()
+{
+ auto inputQStr = QString::fromStdString(input);
+ // Because lexbor only reads and writes UTF-8 but in QML we only have QString
+ // we want to benchmark the performance of the double conversion.
+ // See also: https://github.com/lexbor/lexbor/issues/271
+ QBENCHMARK {
+ QString::fromStdString(sanitizeHtml(inputQStr.toStdString()));
+ }
+}
+
+QTEST_MAIN(KazvSanitizerBenchmarkTest)
+
+#include "kazv-sanitizer-benchmark-test.moc"
diff --git a/src/tests/kazv-sanitizer-test.cpp b/src/tests/kazv-sanitizer-test.cpp
new file mode 100644
index 0000000..c51c75e
--- /dev/null
+++ b/src/tests/kazv-sanitizer-test.cpp
@@ -0,0 +1,41 @@
+/*
+ * This file is part of kazv.
+ * SPDX-FileCopyrightText: 2026 tusooa <nannanko@kazv.moe>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+// #include <kazv-defs.hpp> intentionally omitted
+#include "sanitize.hpp"
+#include <QtTest>
+
+class KazvSanitizerTest : public QObject
+{
+ Q_OBJECT
+
+private Q_SLOTS:
+ void testSanitize();
+};
+
+void KazvSanitizerTest::testSanitize()
+{
+ QCOMPARE(sanitizeHtml("abc"), "abc");
+ QCOMPARE(sanitizeHtml("abc <!-- uuu -->"), "abc ");
+ QCOMPARE(sanitizeHtml("<xxx>yyy</xxx>"), "yyy");
+ QCOMPARE(sanitizeHtml("<xxx>yyy<zzz> uuu</zzz><www /></xxx>"), "yyy uuu");
+ QCOMPARE(sanitizeHtml("<xxx>yyy<zzz> uuu</zzz><a href='https://u'>x</a></xxx>"), "yyy uuu<a href=\"https://u\">x</a>");
+ QCOMPARE(sanitizeHtml("<xxx>yyy<zzz> uuu</zzz><a href='u'>x</a></xxx>"), "yyy uuu<a>x</a>");
+ QCOMPARE(sanitizeHtml("<p id='uwu'>yyy<zzz> uuu</zzz><a href='https://u'>x</a></p>"), "<p>yyy uuu<a href=\"https://u\">x</a></p>");
+ QCOMPARE(sanitizeHtml("<p id='uwu'>yyy<zzz> uuu</zzz><a href='https://u'>x</a></p>"), "<p>yyy uuu<a href=\"https://u\">x</a></p>");
+ QCOMPARE(sanitizeHtml("<svg:p>yyy</svg:p>"), "yyy");
+ QCOMPARE(sanitizeHtml("<img alt='uwu' bad='xxx' bad2='yyy' src='mxc://aa/bb' alt='0w0'>yyy</img>"), "<img alt=\"uwu\" src=\"mxc://aa/bb\">yyy");
+ QCOMPARE(sanitizeHtml("<!DOCTYPE html>000"), "000");
+ QCOMPARE(sanitizeHtml("<p>000 <p>111</p></p>"), "<p>000 </p><p>111</p><p></p>");
+ QCOMPARE(sanitizeHtml("u<![CDATA[ 0 0 0 ]]>1"), "u1");
+ QCOMPARE(sanitizeHtml("<a href=\"ftp:\">000</a>"), "<a href=\"ftp:\">000</a>");
+ QCOMPARE(sanitizeHtml("<mx-reply>xxx</mx-reply>yyy"), "yyy");
+ QCOMPARE(sanitizeHtml("<x>心情简单</x>yyy"), "心情简单yyy");
+}
+
+QTEST_MAIN(KazvSanitizerTest)
+
+#include "kazv-sanitizer-test.moc"
diff --git a/src/tests/quick-tests/tst_EventView.qml b/src/tests/quick-tests/tst_EventView.qml
index 7b17175..f1aa5a8 100644
--- a/src/tests/quick-tests/tst_EventView.qml
+++ b/src/tests/quick-tests/tst_EventView.qml
@@ -1,719 +1,749 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2023 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import QtQuick 2.3
import QtQuick.Layouts 1.15
import QtTest 1.0
import moe.kazv.mxc.kazv 0.0 as MK
import org.kde.kirigami 2.13 as Kirigami
import '../../contents/ui' as Kazv
import 'test-helpers.js' as Helpers
import 'test-helpers' as QmlHelpers
QmlHelpers.TestItem {
id: item
property var activateUserPage: mockHelper.noop()
property var paginateBackRequested: mockHelper.noop()
property var viewEventHistoryRequested: mockHelper.noop()
property var pinEventRequested: mockHelper.noop()
property var unpinEventRequested: mockHelper.noop()
property var deleteEventRequested: mockHelper.noop()
property var timeline: ({
gaps: {},
})
property var pinnedEvents: ({
count: 2,
eventIds: {
'$123': true,
'$456': true,
},
})
property var room: ({
resendMessage: mockHelper.promise(),
removeLocalEcho: mockHelper.promise(),
ensureStateEvent: mockHelper.promise(),
messageById: (_id) => item.textEvent,
member: (_id) => ({}),
})
property var localEcho: ({
eventId: '',
sender: '',
type: 'm.room.message',
stateKey: '',
content: {
msgtype: 'm.text',
body: 'some body',
},
encrypted: false,
isState: false,
unsignedData: {},
isLocalEcho: true,
isSending: true,
isFailed: false,
isAvailable: true,
txnId: 'some-txn-id',
})
property var failedLocalEcho: Object.assign({}, item.localEcho, { isSending: false, isFailed: true, })
property var textEvent: ({
eventId: '',
sender: '@foo:tusooa.xyz',
type: 'm.room.message',
stateKey: '',
content: {
msgtype: 'm.text',
body: 'some body',
},
isAvailable: true,
formattedTime: '4:06 P.M.',
})
property var htmlEvent: ({
eventId: '',
sender: '@foo:tusooa.xyz',
type: 'm.room.message',
stateKey: '',
content: {
msgtype: 'm.text',
body: '**some body**',
format: 'org.matrix.custom.html',
formatted_body: '<strong>some body</strong>',
},
isAvailable: true,
formattedTime: '4:06 P.M.',
})
+ property var htmlEventDangerous: ({
+ eventId: '',
+ sender: '@foo:tusooa.xyz',
+ type: 'm.room.message',
+ stateKey: '',
+ content: {
+ msgtype: 'm.text',
+ body: '**some body**',
+ format: 'org.matrix.custom.html',
+ formatted_body: '<img src="https://example.com"><strong>some body</strong>',
+ },
+ isAvailable: true,
+ formattedTime: '4:06 P.M.',
+ })
+
property var replyHtmlEvent: ({
eventId: '',
sender: '@foo:tusooa.xyz',
type: 'm.room.message',
stateKey: '',
replyingToEventId: '$xxx',
content: {
msgtype: 'm.text',
body: '> some quote\n**some body**',
format: 'org.matrix.custom.html',
formatted_body: '<mx-reply><blockquote>some quote</blockquote></mx-reply><strong>some body</strong>',
},
isAvailable: true,
formattedTime: '4:06 P.M.',
})
property var replyNoQuoteHtmlEvent: ({
eventId: '',
sender: '@foo:tusooa.xyz',
type: 'm.room.message',
stateKey: '',
replyingToEventId: '$xxx',
content: {
msgtype: 'm.text',
body: '**some body**',
format: 'org.matrix.custom.html',
formatted_body: '<strong>some body</strong>',
},
isAvailable: true,
formattedTime: '4:06 P.M.',
})
property var redactedEvent: ({
eventId: '',
sender: '@foo:tusooa.xyz',
type: 'm.room.message',
stateKey: '',
content: {},
redacted: true,
isAvailable: true,
formattedTime: '4:06 P.M.',
})
property var imageEvent: ({
eventId: '',
sender: '@foo:tusooa.xyz',
type: 'm.room.message',
stateKey: '',
content: {
msgtype: 'm.image',
body: 'some body',
info: {
h: Kirigami.Units.gridUnit * 10,
w: Kirigami.Units.gridUnit * 10,
}
},
isAvailable: true,
formattedTime: '4:06 P.M.',
})
property var stickerEvent: ({
eventId: '',
sender: '@foo:tusooa.xyz',
type: 'm.sticker',
stateKey: '',
content: {
body: 'some body',
},
isAvailable: true,
formattedTime: '4:06 P.M.',
})
property var eventBySomeoneElse: ({
eventId: '',
sender: '@bar:tusooa.xyz',
type: 'm.room.message',
stateKey: '',
content: {
msgtype: 'm.image',
body: 'some body',
},
isAvailable: true,
formattedTime: '4:06 P.M.',
})
property var eventTextBySomeoneElse: ({
eventId: '',
sender: '@bar:tusooa.xyz',
type: 'm.room.message',
stateKey: '',
content: {
msgtype: 'm.text',
body: 'some body',
},
isAvailable: true,
formattedTime: '4:06 P.M.',
})
property var ignoredEvent: ({
eventId: '',
sender: '@foo:tusooa.xyz',
type: 'm.room.message',
relationType: 'm.replace',
stateKey: '',
content: {
msgtype: 'm.image',
body: 'some body',
},
isAvailable: true,
formattedTime: '4:06 P.M.',
})
property var unknownTypeEvent: ({
eventId: '',
sender: '@foo:tusooa.xyz',
type: 'moe.kazv.mxc.unknown.event',
stateKey: '',
content: {
msgtype: 'm.image',
body: 'some body',
},
isAvailable: true,
})
property var unknownMsgtypeEvent: ({
eventId: '',
sender: '@foo:tusooa.xyz',
type: 'm.room.message',
stateKey: '',
content: {
msgtype: 'moe.kazv.mxc.unknown.msgtype',
body: 'some body',
},
isAvailable: true,
})
property var cannotDecryptEvent: ({
eventId: '',
sender: '@foo:tusooa.xyz',
type: 'm.room.message',
stateKey: '',
content: {
msgtype: 'moe.kazv.mxc.cannot.decrypt',
body: 'some body',
},
isAvailable: true,
})
property var editedEvent: ({
eventId: '!some-event-id',
sender: '@foo:tusooa.xyz',
type: 'm.room.message',
stateKey: '',
content: {
msgtype: 'm.text',
body: 'some body',
},
isAvailable: true,
formattedTime: '4:06 P.M.',
isEdited: true,
})
property var pinnedEvent: ({
eventId: '$123',
sender: '@foo:tusooa.xyz',
type: 'm.room.message',
content: {
msgtype: 'm.text',
body: 'some body',
},
isAvailable: true,
formattedTime: '4:06 P.M.',
})
property var notPinnedEvent: ({
eventId: '$789',
sender: '@foo:tusooa.xyz',
type: 'm.room.message',
content: {
msgtype: 'm.text',
body: 'some body',
},
isAvailable: true,
formattedTime: '4:06 P.M.',
})
property var sender: ({
membership: 'join',
userId: '@foo:tusooa.xyz',
name: 'foo',
avatarMxcUri: '',
})
property var senderOther: ({
membership: 'join',
userId: '@bar:tusooa.xyz',
name: 'bar',
avatarMxcUri: '',
})
property var eventViewComp: Component {
Kazv.EventView {
Layout.fillWidth: true
event: QtObject {
property var eventId: '$some-event-id'
function readers() { return 0; }
}
sender: item.sender
isGapped: true
}
}
property var eventComp: Component {
QtObject {
property var eventId: '$some-event-id'
function readers() { return 0; }
}
}
ColumnLayout {
anchors.fill: parent
Kazv.EventView {
Layout.fillWidth: true
id: eventView
event: item.localEcho
sender: item.sender
}
Kazv.EventView {
Layout.fillWidth: true
id: eventViewFailed
event: item.failedLocalEcho
sender: item.sender
}
Kazv.EventView {
Layout.fillWidth: true
id: eventViewText
event: item.textEvent
sender: item.sender
}
Kazv.EventView {
Layout.fillWidth: true
id: eventViewHtml
event: item.htmlEvent
sender: item.sender
}
+ Kazv.EventView {
+ Layout.fillWidth: true
+ id: eventViewHtmlDangerous
+ event: item.htmlEventDangerous
+ sender: item.sender
+ }
+
Kazv.EventView {
Layout.fillWidth: true
id: eventViewHtmlReply
event: item.replyHtmlEvent
sender: item.sender
}
Kazv.EventView {
Layout.fillWidth: true
id: eventViewHtmlReplyNoQuote
event: item.replyNoQuoteHtmlEvent
sender: item.sender
}
Kazv.EventView {
Layout.fillWidth: true
id: eventViewRedacted
event: item.redactedEvent
sender: item.sender
}
Kazv.EventView {
id: eventViewUnknownType
event: item.unknownTypeEvent
sender: item.sender
}
Kazv.EventView {
id: eventViewUnknownMsgtype
event: item.unknownMsgtypeEvent
sender: item.sender
}
Kazv.EventView {
id: eventViewCannotDecrypt
event: item.cannotDecryptEvent
sender: item.sender
}
Kazv.EventView {
id: eventViewCollapseSameSender
event: item.imageEvent
sender: item.sender
prevEvent: item.redactedEvent
}
Kazv.EventView {
id: eventViewCollapseLocalEcho
event: item.localEcho
sender: item.sender
prevEvent: item.redactedEvent
}
Kazv.EventView {
id: eventViewDontCollapseDifferentSender
event: item.imageEvent
sender: item.sender
prevEvent: item.eventBySomeoneElse
}
Kazv.EventView {
id: eventViewDontCollapseLocalEchoDifferentSender
event: item.localEcho
sender: item.sender
prevEvent: item.eventBySomeoneElse
}
Kazv.EventView {
id: eventViewDontCollapseIgnoredEvent
event: item.imageEvent
sender: item.sender
prevEvent: item.ignoredEvent
}
Kazv.EventView {
id: eventViewTextBySomeoneElse
event: item.eventTextBySomeoneElse
sender: item.senderOther
}
Kazv.EventView {
id: eventViewImage
event: item.imageEvent
sender: item.sender
}
Kazv.EventView {
id: eventViewImageCompact
compactMode: true
event: item.imageEvent
sender: item.sender
}
Kazv.EventView {
id: eventViewSticker
event: item.stickerEvent
sender: item.sender
}
Kazv.EventView {
id: eventViewStickerCompact
compactMode: true
event: item.stickerEvent
sender: item.sender
}
Kazv.EventView {
Layout.fillWidth: true
id: eventViewEdited
event: item.editedEvent
sender: item.sender
}
Kazv.EventView {
Layout.fillWidth: true
id: eventViewPinned
event: item.pinnedEvent
sender: item.sender
}
Kazv.EventView {
Layout.fillWidth: true
id: eventViewNotPinned
event: item.notPinnedEvent
sender: item.sender
}
}
TestCase {
id: eventViewTest
name: 'EventViewTest'
when: windowShown
function init() {
item.matrixSession.userId = '@foo:tusooa.xyz';
eventView.event = item.localEcho;
mockHelper.clearAll();
}
function test_localEcho() {
const indicator = findChild(eventView, 'localEchoIndicator');
verify(indicator);
verify(indicator.icon.name === 'state-sync');
verify(indicator.Accessible.name === l10n.get('event-sending'));
const resendButton = findChild(eventView, 'localEchoIndicator');
verify(resendButton);
verify(!resendButton.enabled);
verify(!findChild(eventView, 'timeIndicator').visible);
}
function test_localEchoFailedRemove() {
const indicator = findChild(eventViewFailed, 'localEchoIndicator');
verify(indicator);
verify(indicator.visible);
verify(indicator.icon.name === 'state-warning');
verify(indicator.Accessible.name === l10n.get('event-resend'));
const menuComp = findChild(eventViewFailed, 'bubble').menuComp;
const menu = menuComp.createObject(menuComp.parent);
const deleteAction = findChild(menu, 'deleteMenuItem');
verify(deleteAction.enabled);
deleteAction.trigger();
compare(deleteEventRequested.calledTimes(), 1);
}
function test_sentMessage() {
const indicator = findChild(eventViewText, 'localEchoIndicator');
verify(indicator);
verify(!indicator.visible);
verify(findChild(eventViewText, 'timeIndicator').visible);
}
function test_htmlMessage() {
const text = findChild(eventViewHtml, 'eventViewMainItem');
verify(text.textFormat === TextEdit.RichText);
verify(text.text.includes('some body'));
}
+ function test_htmlMessageDangerous() {
+ const text = findChild(eventViewHtmlDangerous, 'eventViewMainItem');
+ verify(text.textFormat === TextEdit.RichText);
+ verify(text.text.includes('<img>'));
+ verify(!text.text.includes('<img src="https:'));
+ verify(text.text.includes('some body'));
+ }
+
function test_htmlMessageReply() {
const text = findChild(eventViewHtmlReply, 'eventViewMainItem');
verify(text.textFormat === TextEdit.RichText);
verify(!text.text.includes('<blockquote>'));
}
function test_htmlMessageReplyNoQuote() {
const text = findChild(eventViewHtmlReplyNoQuote, 'eventViewMainItem');
verify(text.textFormat === TextEdit.RichText);
verify(!text.text.includes('<blockquote>'));
verify(text.text.includes('some body'));
}
function test_menuText() {
const menuComp = findChild(eventViewText, 'bubble').menuComp;
const menu = menuComp.createObject(menuComp.parent);
verify(menu);
tryVerify(() => findChild(menu, 'replyToMenuItem').enabled);
tryVerify(() => findChild(menu, 'editMenuItem').enabled);
tryVerify(() => findChild(menu, 'deleteMenuItem').enabled);
}
function test_menuRedacted() {
const menuComp = findChild(eventViewRedacted, 'bubble').menuComp;
const menu = menuComp.createObject(menuComp.parent);
verify(menu);
tryVerify(() => !findChild(menu, 'replyToMenuItem').enabled);
tryVerify(() => !findChild(menu, 'deleteMenuItem').enabled);
}
function test_fallback() {
verify(findChild(eventViewUnknownType, 'fallbackIcon'));
verify(findChild(eventViewUnknownType, 'textEventContent').text ===
l10n.get('event-fallback-text', { type: unknownTypeEvent.type }));
verify(findChild(eventViewUnknownMsgtype, 'fallbackIcon'));
verify(findChild(eventViewUnknownMsgtype, 'textEventContent').text ===
l10n.get('event-msgtype-fallback-text', { msgtype: unknownMsgtypeEvent.content.msgtype }));
verify(findChild(eventViewCannotDecrypt, 'fallbackIcon'));
verify(findChild(eventViewCannotDecrypt, 'textEventContent').text ===
l10n.get('event-cannot-decrypt-text'));
}
function test_collapseSender() {
verify(!findChild(eventViewCollapseSameSender, 'senderAvatar').visible);
verify(findChild(eventViewCollapseSameSender, 'senderCollapsedPlaceholder').visible);
verify(!findChild(eventViewCollapseLocalEcho, 'senderAvatar').visible);
verify(findChild(eventViewCollapseLocalEcho, 'senderCollapsedPlaceholder').visible);
verify(findChild(eventViewDontCollapseDifferentSender, 'senderAvatar').visible);
verify(!findChild(eventViewDontCollapseDifferentSender, 'senderCollapsedPlaceholder').visible);
verify(findChild(eventViewDontCollapseLocalEchoDifferentSender, 'senderAvatar').visible);
verify(!findChild(eventViewDontCollapseLocalEchoDifferentSender, 'senderCollapsedPlaceholder').visible);
verify(findChild(eventViewDontCollapseIgnoredEvent, 'senderAvatar').visible);
verify(!findChild(eventViewDontCollapseIgnoredEvent, 'senderCollapsedPlaceholder').visible);
}
function test_eventByOther() {
const menuComp = findChild(eventViewTextBySomeoneElse, 'bubble').menuComp;
const menu = menuComp.createObject(menuComp.parent);
verify(menu);
tryVerify(() => !findChild(menu, 'editMenuItem').enabled);
}
function test_menuSelfDestruct() {
const menuComp = findChild(eventViewTextBySomeoneElse, 'bubble').menuComp;
const menu = menuComp.createObject(menuComp.parent);
verify(menu);
menu.popup(item);
menu.dismiss();
const menu2 = menuComp.createObject(menuComp.parent);
menu2.popup(item);
menu2.dismiss();
}
function test_userLink() {
const text = findChild(eventViewText, 'textEventContent');
text.linkActivated('https://matrix.to/#/@mew:example.com');
tryVerify(() => item.room.ensureStateEvent.calledTimes());
verify(Helpers.deepEqual(
item.room.ensureStateEvent.lastArgs(),
['m.room.member', '@mew:example.com']));
item.room.ensureStateEvent.lastRetVal().resolve(true, {});
tryVerify(() => item.activateUserPage.calledTimes());
}
function test_senderNameOverrided() {
const name = findChild(eventViewText, 'userNicknameText');
compare(name.text, l10n.get(
'user-name-overrided',
{ overridedName: 'something', globalName: 'foo' }));
}
function test_senderNameNotOverrided() {
const name = findChild(eventViewTextBySomeoneElse, 'userNicknameText');
compare(name.text, 'bar');
}
function test_paginateBack() {
const obj = item.eventViewComp.createObject(item);
tryVerify(() => item.paginateBackRequested.calledTimes());
verify(item.paginateBackRequested.lastArgs()[0] === '$some-event-id');
item.paginateBackRequested.clear();
obj.event.eventId = '$some-other-event-id';
tryVerify(() => item.paginateBackRequested.calledTimes() === 1);
verify(item.paginateBackRequested.lastArgs()[0] === '$some-other-event-id');
item.paginateBackRequested.clear();
const newEvent = eventComp.createObject(item);
obj.event = newEvent;
tryVerify(() => item.paginateBackRequested.calledTimes() === 1);
verify(item.paginateBackRequested.lastArgs()[0] === '$some-event-id');
item.paginateBackRequested.clear();
obj.isGapped = false;
verify(item.paginateBackRequested.calledTimes() === 0);
}
function test_avatarClick() {
const avatar = findChild(eventViewText, 'senderAvatar');
mouseClick(avatar);
tryVerify(() => item.activateUserPage.calledTimes() === 1);
// verify that the user id is passed
verify(item.activateUserPage.lastArgs()[2] === '@foo:tusooa.xyz');
}
function test_imageEvent() {
verify(!findChild(eventViewImage, 'summaryLabel').visible);
verify(findChild(eventViewImage, 'bodyLabel').visible);
verify(findChild(eventViewImageCompact, 'summaryLabel').visible);
verify(!findChild(eventViewImageCompact, 'bodyLabel').visible);
verify(!findChild(eventViewSticker, 'summaryLabel').visible);
verify(findChild(eventViewSticker, 'bodyLabel').visible);
verify(findChild(eventViewStickerCompact, 'summaryLabel').visible);
verify(!findChild(eventViewStickerCompact, 'bodyLabel').visible);
const image = findChild(eventViewImageCompact, 'mainImage');
verify(image.height <= Kirigami.Units.gridUnit * 5);
}
function test_history() {
{
const menuComp = findChild(eventViewText, 'bubble').menuComp;
const menu = menuComp.createObject(menuComp.parent);
const action = findChild(menu, 'viewHistoryMenuItem');
verify(!action.enabled);
}
{
const menuComp = findChild(eventViewEdited, 'bubble').menuComp;
const menu = menuComp.createObject(menuComp.parent);
const action = findChild(menu, 'viewHistoryMenuItem');
verify(action.enabled);
action.trigger();
compare(viewEventHistoryRequested.calledTimes(), 1);
compare(viewEventHistoryRequested.lastArgs()[0], '!some-event-id');
}
}
function test_pinned() {
{
const menuComp = findChild(eventViewPinned, 'bubble').menuComp;
const menu = menuComp.createObject(menuComp.parent);
const action = findChild(menu, 'pinUnpinMenuItem');
verify(action.enabled);
action.trigger();
compare(unpinEventRequested.calledTimes(), 1);
compare(unpinEventRequested.lastArgs()[0], '$123');
compare(pinEventRequested.calledTimes(), 0);
}
{
const menuComp = findChild(eventViewNotPinned, 'bubble').menuComp;
const menu = menuComp.createObject(menuComp.parent);
const action = findChild(menu, 'pinUnpinMenuItem');
verify(action.enabled);
action.trigger();
compare(pinEventRequested.calledTimes(), 1);
compare(pinEventRequested.lastArgs()[0], '$789');
compare(pinEventRequested.calledTimes(), 1);
}
{
const menuComp = findChild(eventView, 'bubble').menuComp;
const menu = menuComp.createObject(menuComp.parent);
const action = findChild(menu, 'pinUnpinMenuItem');
verify(!action.enabled);
}
}
}
}

File Metadata

Mime Type
text/x-diff
Expires
Fri, Oct 9, 3:15 AM (1 d, 4 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1784414
Default Alt Text
(73 KB)

Event Timeline