diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000000..bf31858672 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,3 @@ +github: [Ri0n, Vitozz, tehnick] +Liberapay: Psi +custom: https://yoomoney.ru/to/410011685497701 diff --git a/.github/workflows/ubuntu.yml b/.github/workflows/ubuntu.yml index 0c3ba4d5ae..e2410333ac 100644 --- a/.github/workflows/ubuntu.yml +++ b/.github/workflows/ubuntu.yml @@ -19,8 +19,8 @@ jobs: chattype: ['BASIC', 'WEBENGINE'] include: - compiler: 'clang' - cc: 'clang' - cxx: 'clang++' + cc: 'clang-17' + cxx: 'clang++-17' - compiler: 'gcc' cc: 'gcc' cxx: 'g++' @@ -32,15 +32,28 @@ jobs: - name: Updating apt package metadata run: | sudo apt-get update - - name: Installing compiler ${{ matrix.compiler }} + + - name: Installing compiler GCC + if: ${{ matrix.compiler == 'gcc' }} run: | sudo apt-get -y install ${{ matrix.compiler }} + + - name: Installing compiler CLang + if: ${{ matrix.compiler == 'clang' }} + run: | + wget https://apt.llvm.org/llvm.sh + chmod +x ./llvm.sh + sudo ./llvm.sh 17 + + - name: Remove conflicting slotted packages + run: | + sudo apt-get remove libunwind-* - name: Installing build system run: | sudo apt-get -y install cmake make - name: Installing development packages run: | - sudo apt-get -y install \ + sudo apt-get -y install --no-install-recommends \ gstreamer1.0-dev \ libhttp-parser-dev \ libhunspell-dev \ diff --git a/3rdparty/qite b/3rdparty/qite index 94a6223efa..e9d33c84a0 160000 --- a/3rdparty/qite +++ b/3rdparty/qite @@ -1 +1 @@ -Subproject commit 94a6223efa4be82d2b1804321604ee08801c0538 +Subproject commit e9d33c84a0b346185b27efb44189f18acaa53906 diff --git a/CHANGELOG b/CHANGELOG index 15cf2ab460..919d67f8f1 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -14,6 +14,7 @@ New in 2.0 (UNRELEASED) - Fixed build for macOS using cmake + different macOS specific improvements - Different MS Windows specific improvements - Added official support of Haiku operating system +- Added DOAP file - PsiMedia (Psi Multimedia) library was rewritten and now is loaded as usual Psi plugin instead of loading library directly - Changed plugins API (plugins from old releases of Psi will not work): diff --git a/CMakeLists.txt b/CMakeLists.txt index f8ab33b40f..9a542c1b10 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -24,8 +24,8 @@ list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake/modules" ) include(psi-cmake-functions) include(policyRules) -# Define LINUX on Linux like as WIN32 on Windows and APPLE on macOS -if(UNIX AND NOT (APPLE OR HAIKU)) +# Define LINUX on Linux like and BSD as WIN32 on Windows and APPLE on macOS +if(UNIX AND (NOT (APPLE OR HAIKU) AND NOT CYGWIN)) set(LINUX ON) endif() @@ -38,7 +38,7 @@ set(SBM_LIST ) foreach(submodule ${SBM_LIST}) if(NOT EXISTS "${submodule}") - message(FATAL_ERROR "Psi submodules not found.\nPlease run:\n====\ncd ${PROJECT_SOURCE_DIR}\ngit submodule init\ngit submodule update\n====\nbefore run cmake again") + message(FATAL_ERROR "Psi ${submodule} submodule not found.\nPlease run:\n====\ncd ${PROJECT_SOURCE_DIR}\ngit submodule update --init --recursive\n====\nbefore run cmake again") endif() endforeach() @@ -67,7 +67,6 @@ option( USE_ENCHANT "Build psi with enchant spellcheck" OFF ) option( USE_ASPELL "Build psi with aspell spellcheck" OFF ) option( USE_CCACHE "Use ccache utility if found" ON ) option( USE_CRASH "Enable builtin sigsegv handling" OFF ) -option( USE_DBUS "Enable DBUS support" ON ) option( USE_KEYCHAIN "Enable QtKeychain support" ON ) option( ONLY_BINARY "Build and install only binary file" OFF ) option( ONLY_PLUGINS "Build psi plugins only" OFF ) @@ -79,8 +78,12 @@ option( DEV_MODE "Enable prepare-bin-libs target for MS Windows only. Set PSI_DA # Iris options option( BUNDLED_IRIS "Build iris library bundled" ON ) option( BUNDLED_IRIS_ALL "Build bundled iris library with bundled QCA and bundled USRSCTP" OFF) -option( IRIS_BUNDLED_QCA "Adds: DTLS, Blake2b and other useful for XMPP crypto-stuff" ${DEFAULT_BUNDLED_QCA} ) +# note Blake2b is needed only with Qt5. Qt6 has its own implementation +option( IRIS_BUNDLED_QCA "Adds: DTLS, Blake2b (needed with Qt5) and other useful for XMPP crypto-stuff" ${DEFAULT_BUNDLED_QCA} ) option( IRIS_BUNDLED_USRSCTP "Compile compatible usrsctp lib when system one is not available or uncompatible (required for p2p file transfer)" ${DEFAULT_BUNDLED_USRSCTP} ) +option( BUNDLED_KEYCHAIN "Build QtKeychain library bundled" OFF ) +option( USE_TASKBARNOTIFIER "Use taskbar notifications for incoming events" ON ) + if (UNIX AND "${CMAKE_BUILD_TYPE}" STREQUAL "Debug") option( ENABLE_ASAN "Enable compilation with address sanitizer" OFF ) endif() @@ -91,6 +94,7 @@ option( USE_MXE "Use MXE (cross-compilation build environment for MS Windows)" $ # Other systems if(LINUX) + option( USE_DBUS "Enable DBUS support" ON ) option( USE_X11 "Enable X11 features support" ON ) option( USE_XSS "Enable Xscreensaver support" ON ) option( LIMIT_X11_USAGE "Disable usage of X11 features which may crash program" OFF ) @@ -100,10 +104,6 @@ elseif(APPLE) option( USE_MAC_DOC "Use macOS dock" OFF ) endif() -if(WIN32 AND ("${CMAKE_BUILD_TYPE}" STREQUAL "Debug" OR ("${CMAKE_BUILD_TYPE}" STREQUAL "RelWithDebInfo"))) - option( NO_DEBUG_OPTIMIZATION "Disable optimization for debug builds" OFF ) -endif() - if( USE_HUNSPELL AND (USE_ENCHANT AND USE_ASPELL) ) message(FATAL_ERROR "Flags USE_HUNSPELL, USE_ASPELL and USE_ENCHANT cannot be enabled at the same time.\nPlease enable only one of them") elseif( USE_HUNSPELL AND USE_ASPELL ) @@ -125,17 +125,10 @@ set(IS_WEBENGINE OFF CACHE INTERNAL "Use webengine. Internal variable") string(TOLOWER "${CHAT_TYPE}" LCHAT_TYPE) if("${LCHAT_TYPE}" STREQUAL "webkit") - add_definitions( - -DWEBKIT - ) set(IS_WEBKIT ON) message(STATUS "Chatlog type - QtWebKit") elseif("${LCHAT_TYPE}" STREQUAL "webengine") set(IS_WEBENGINE ON) - add_definitions( - -DWEBKIT - -DWEBENGINE=1 - ) message(STATUS "Chatlog type - QtWebEngine") else() set(IS_WEBKIT OFF) @@ -146,14 +139,14 @@ endif() message(STATUS "System name - ${CMAKE_SYSTEM_NAME}") if("${CMAKE_BUILD_TYPE}" STREQUAL "Debug" OR ("${CMAKE_BUILD_TYPE}" STREQUAL "RelWithDebInfo")) - set(ISDEBUG ON) + set(ISDEBUG ON CACHE INTERNAL "Debug on RelWithDebInfo build type enabled") option(PLUGIN_INSTALL_PATH_DEBUG "Add -DPLUGIN_INSTALL_PATH_DEBUG definition" OFF) option(CHATVIEW_CORRECTION_DEBUG "Add -DCORRECTION_DEBUG definition" OFF) if(PLUGIN_INSTALL_PATH_DEBUG) add_definitions(-DPLUGIN_INSTALL_PATH_DEBUG) endif() - if(CHATVIEW_CORRECTION_DEBUG) - add_definitions(-DCORRECTION_DEBUG) + if(WIN32) + option( NO_DEBUG_OPTIMIZATION "Disable optimization for debug builds" OFF ) endif() endif() @@ -176,35 +169,23 @@ elseif(NOT ENABLE_PLUGINS AND BUILD_PSIMEDIA) message(FATAL_ERROR "BUILD_PSIMEDIA flag not works without ENABLE_PLUGINS flag.\nPlease enable ENABLE_PLUGINS flag or disable BUILD_PSIMEDIA flag") endif() -if(USE_CRASH) - add_definitions(-DUSE_CRASH) -endif() - -if(USE_MXE) +if(WIN32 OR USE_MXE) set(BUNDLED_IRIS_ALL ON) endif() # For GNU/Linux and *BSD systems: -if(UNIX AND NOT (APPLE OR HAIKU)) +if(UNIX AND (NOT (APPLE OR HAIKU) AND NOT CYGWIN)) if(USE_X11) - add_definitions( -DHAVE_X11 ) message(STATUS "X11 features support - ENABLED") if(LIMIT_X11_USAGE) - add_definitions( -DLIMIT_X11_USAGE ) message(STATUS "Unsafe X11 features support - DISABLED") set(USE_XSS OFF) message(STATUS "Xscreensaver support - DISABLED") endif() if(USE_XSS) - add_definitions( -DHAVE_XSS ) message(STATUS "Xscreensaver support - ENABLED") endif() endif() - add_definitions( - -DHAVE_FREEDESKTOP - -DAPP_PREFIX=${CMAKE_INSTALL_PREFIX} - -DAPP_BIN_NAME=${PROJECT_NAME} - ) if(USE_DBUS) message(STATUS "DBus support - ENABLED") endif() @@ -226,8 +207,8 @@ if(PROJECT_OS_NETBSD) endif() # Qt dependencies make building very slow -# Track only .h files -include_regular_expression("^.*\\.h$") +# Track only .h and .hpp files +include_regular_expression("^.*\\.h$|^.*\\.hpp$") # Put executable in build root dir set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/psi" ) @@ -236,7 +217,7 @@ set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/psi" ) if(APPLE) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-overloaded-virtual") elseif(WIN32) - include("${CMAKE_CURRENT_SOURCE_DIR}/win32/win32_definitions.cmake") + include(win32_definitions) endif() if(ENABLE_ASAN) @@ -286,6 +267,14 @@ if(USE_CCACHE) find_program(CCACHE_PATH ccache DOC "Path to ccache") if(CCACHE_PATH) message(STATUS "Found ccache at ${CCACHE_PATH}") + if(MSVC AND (CMAKE_VERSION VERSION_GREATER "3.13.0")) + set(CMAKE_MSVC_DEBUG_INFORMATION_FORMAT "$<$:Embedded>") + set(CMAKE_VS_GLOBALS + "TrackFileAccess=false" + "UseMultiToolTask=true" + "DebugInformationFormat=OldStyle" + ) + endif() set_property(GLOBAL PROPERTY RULE_LAUNCH_COMPILE ${CCACHE_PATH}) set_property(GLOBAL PROPERTY RULE_LAUNCH_LINK ${CCACHE_PATH}) endif() diff --git a/INSTALL.md b/INSTALL.md index e61bc9b743..18782ec5a4 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -28,7 +28,7 @@ See "Packages and installers" section in [README.md](README.md) or [README.html] * libhttp-parser (optional, for plugins only) * libotr (optional, for plugins only) * libtidy (optional, for plugins only) -* libsignal-protocol-c (optional, for plugins only) +* libomemo-c (optional, for plugins only) ### Common @@ -74,7 +74,7 @@ Plugin build dependencies: sudo apt install -qq \ libhttp-parser-dev \ libotr5-dev \ - libsignal-protocol-c-dev \ + libomemo-c-dev \ libtidy-dev ``` @@ -108,7 +108,7 @@ Plugin build dependencies: ```shell sudo zypper in libotr-devel \ - libsignal-protocol-c-devel \ + libomemo-c-devel \ libtidy-devel ``` diff --git a/README.html b/README.html index 1c55455d9f..12028968a1 100644 --- a/README.html +++ b/README.html @@ -9,7 +9,7 @@

License

Description

-

Psi is an XMPP client designed for experienced users. It is highly portable and runs on GNU/Linux, MS Windows, macOS, FreeBSD and Haiku.

+

Psi is an XMPP client designed for experienced users. It is highly portable and runs on GNU/Linux, MS Windows, macOS, FreeBSD, NetBSD and Haiku.

User interface of program is very flexible in customization. For example, there are "multi windows" and "all in one" modes, support of different iconsets and themes. Psi supports file sharing and audio/video calls. Security is also a major consideration, and Psi provides it for both client-to-server (TLS) and client-to-client (OpenPGP, OTR, OMEMO) via appropriate plugins.

@@ -32,7 +32,7 @@

Installation

For build from sources see INSTALL file.

-

GNU/Linux and FreeBSD users may install packages from official and unofficial repositories, ports, etc.

+

GNU/Linux and *BSD users may install packages from official and unofficial repositories, ports, etc.

macOS users may install and update official builds using Homebrew cask:

@@ -79,11 +79,11 @@

Developers

Lead developers

Other contributors

@@ -94,7 +94,7 @@

How you can help

Bug reports

-

If you found a bug please report about it in our Bug Tracker. If you have doubts contact with us in XMPP Conference <psi-dev@conference.jabber.ru> (preferable) or in a Mailing List.

+

If you found a bug please report about it in our Bug Tracker. If you have doubts contact with us in XMPP Conference psi-dev@conference.jabber.ru (preferable) or in a Mailing List.

Beta testing

@@ -102,7 +102,7 @@

Beta testing

Comments and wishes

-

We like constructive comments and wishes to functions of program. You may contact with us in XMPP Conference <psi-dev@conference.jabber.ru> for discussing of your ideas. Some of them will be drawn up as feature requests in our Bug Tracker.

+

We like constructive comments and wishes to functions of program. You may contact with us in XMPP Conference psi-dev@conference.jabber.ru for discussing of your ideas. Some of them will be drawn up as feature requests in our Bug Tracker.

Translations

diff --git a/README.md b/README.md index 81edf11251..6b37b27d29 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ This program is licensed under the GNU General Public License. See the [COPYING] ## Description -Psi is an XMPP client designed for experienced users. It is highly portable and runs on GNU/Linux, MS Windows, macOS, FreeBSD and Haiku. +Psi is an XMPP client designed for experienced users. It is highly portable and runs on GNU/Linux, MS Windows, macOS, FreeBSD, NetBSD and Haiku. User interface of program is very flexible in customization. For example, there are "multi windows" and "all in one" modes, support of different iconsets and themes. Psi supports file sharing and audio/video calls. Security is also a major consideration, and Psi provides it for both client-to-server (TLS) and client-to-client (OpenPGP, OTR, OMEMO) via appropriate plugins. @@ -30,7 +30,7 @@ See [CHANGELOG](https://github.com/psi-im/psi/blob/master/CHANGELOG) file. For build from sources see [INSTALL](https://github.com/psi-im/psi/blob/master/INSTALL.md) file. -GNU/Linux and FreeBSD users may install [packages](https://github.com/psi-im/psi#packages-and-installers) from official and unofficial repositories, ports, etc. +GNU/Linux and *BSD users may install [packages](https://github.com/psi-im/psi#packages-and-installers) from official and unofficial repositories, ports, etc. macOS users may install and update official builds using [Homebrew](https://brew.sh/) cask: @@ -86,7 +86,7 @@ There are a lot of people who were involved into Psi and Psi+ development. Some ### Bug reports -If you found a bug please report about it in our [Bug Tracker](https://github.com/psi-im/psi/issues). If you have doubts contact with us in [XMPP Conference](https://chatlogs.jabber.ru/psi-dev@conference.jabber.ru) <psi-dev@conference.jabber.ru> (preferable) or in a [Mailing List](https://groups.google.com/forum/#!forum/psi-users). +If you found a bug please report about it in our [Bug Tracker](https://github.com/psi-im/psi/issues). If you have doubts contact with us in XMPP Conference [psi-dev@conference.jabber.ru](xmpp:psi-dev@conference.jabber.ru?join) (preferable) or in a [Mailing List](https://groups.google.com/forum/#!forum/psi-users). ### Beta testing @@ -94,7 +94,7 @@ As we (intentionally) do not have nor beta versions of Psi, nor daily build buil ### Comments and wishes -We like constructive comments and wishes to functions of program. You may contact with us in [XMPP Conference](https://chatlogs.jabber.ru/psi-dev@conference.jabber.ru) <psi-dev@conference.jabber.ru> for discussing of your ideas. Some of them will be drawn up as feature requests in our [Bug Tracker](https://github.com/psi-im/psi/issues). +We like constructive comments and wishes to functions of program. You may contact with us in XMPP Conference [psi-dev@conference.jabber.ru](xmpp:psi-dev@conference.jabber.ru?join) for discussing of your ideas. Some of them will be drawn up as feature requests in our [Bug Tracker](https://github.com/psi-im/psi/issues). ### Translations diff --git a/Readme-cmake-ru.txt b/Readme-cmake-ru.md similarity index 94% rename from Readme-cmake-ru.txt rename to Readme-cmake-ru.md index dedbd76ef4..4f06d7cccd 100644 --- a/Readme-cmake-ru.txt +++ b/Readme-cmake-ru.md @@ -77,6 +77,11 @@ JINGLE_SCTP) (по-умолчанию -OFF) +> -DBUNDLED_KEYCHAIN=ON + + компилировать библиотеку QtKeychain из официального репозитория github + Полезно для сборки под macOS (по-умолчанию -OFF) + > -DUSE_ASPELL=OFF использовать механизм проверки орфографии Aspell (по-умолчанию -OFF) @@ -188,6 +193,13 @@ отключает поддержку функций X11 которые могур приводить к падению программы (по-умолчанию OFF) +> -DUSE_TASKBARNOTIFIER=ON + + Показывает количество входящих событий на значке программы. + Для систем Linux используется служба DBus com.canonical.Unity, если она доступна. + Windows использует механизм оверлея значков. + Или просто меняет иконку программы в других случаях. (по-умолчанию ON) + ## Работа с плагинами: ### Следующие флаги работают только если включены флаги ENABLE_PLUGINS или ONLY_PLUGINS diff --git a/Readme-cmake.txt b/Readme-cmake.md similarity index 95% rename from Readme-cmake.txt rename to Readme-cmake.md index 371173b3d5..9678c6fecf 100644 --- a/Readme-cmake.txt +++ b/Readme-cmake.md @@ -80,6 +80,10 @@ or uncompatible (required for p2p file transfer. Available only if JINGLE_SCTP flag set to ON) +> -DBUNDLED_KEYCHAIN=ON + + to build QtKeychain library bundled (useful for macOS) (default OFF) + > -DUSE_ASPELL=OFF to use Aspell spellchecker (default OFF) @@ -182,6 +186,13 @@ or Disable usage of X11 features which may crash program (default OFF) +> -DUSE_TASKBARNOTIFIER=ON + + Shows the incoming events count on the program icon. + For Linux systems, it uses the DBus service com.canonical.Unity if available. + Windows it uses an icon overlay mechanism. + Or simply changes the program icon for other cases. (default ON) + ## Work with plugins: ### Next flags are working only if ENABLE_PLUGINS or ONLY_PLUGINS are enabled diff --git a/Readme-dev-cmake-en.txt b/Readme-dev-cmake-en.txt index 5f5b1086c4..96a1c9f244 100644 --- a/Readme-dev-cmake-en.txt +++ b/Readme-dev-cmake-en.txt @@ -7,7 +7,6 @@ This file contains the CMake files description determines chatlog type Basic/Webkit/Webengine; contains general compile definitions; enables ccache, mxe; - contains useful copy function; adds iris, 3rdparty, src, plugins subdirectories; in case of only plugins build - enables plugins; if plugins/generic/psimedia directory exists and BUILD_PSIMEDIA flag is @@ -22,19 +21,16 @@ This file contains the CMake files description ./cmake/modules - directory contains modules for searching libraries, for determining a program version, for work with sources and generation of additional targets and files. ./cmake/modules/COPYING-CMAKE-SCRIPTS - license file (to solve problems with mainaining) -./cmake/modules/FindLibGpgError.cmake - finds libgpg-error library -./cmake/modules/FindQJDns.cmake - finds qjdns library +./cmake/modules/FindLibGpgError.cmake - finds libgpg-error library [moved to the OTR plugin sources] ./cmake/modules/FindEnchant.cmake - finds enchant library -./cmake/modules/FindLibOtr.cmake - finds otr library -./cmake/modules/FindQJSON.cmake - finds qjson library (disabled as outdated) +./cmake/modules/FindLibOtr.cmake - finds otr library [moved to the OTR plugin sources] ./cmake/modules/FindHunspell.cmake - finds hunspell library -./cmake/modules/FindLibTidy.cmake - finds tidy or html-tidy library +./cmake/modules/FindLibTidy.cmake - finds tidy or html-tidy library [moved to the OTR plugin sources] ./cmake/modules/FindSparkle.cmake - finds sparkle (not yet tested) ./cmake/modules/FindMINIZIP.cmake - finds minizip library ./cmake/modules/FindXCB.cmake - finds xcb library -./cmake/modules/FindLibGcrypt.cmake - finds libgctypt or libgcrypt2 library -./cmake/modules/FindQca.cmake - finds qca-qt5 library -./cmake/modules/FindZLIB.cmake - finds zlib library +./cmake/modules/FindLibGcrypt.cmake - finds libgctypt or libgcrypt2 library [moved to the OTR plugin sources] +./cmake/modules/FindQca.cmake - finds qca-qt library ./cmake/modules/FindPsiPluginsApi.cmake - module to search files useful at build plugins ./cmake/modules/get-version.cmake - determines clien version using git utility or by parsing a ../version file @@ -44,6 +40,14 @@ This file contains the CMake files description with make windeploy command ./cmake/modules/generate_desktopfile.cmake - generates .desktop file ./cmake/modules/fix-codestyle.cmake - module for codestyle fix using clang-format +./cmake/modules/win32_definitions.cmake - provides the next features: + determines PSI_SDK and GSTREAMER_SDK pathes + determines the MinGW or MSVC compilation flags + contains the main definitions for OS Windows +./cmake/modules/psi-cmake-functions.cmake - contains useful functions that can be run anywhere in the project +./cmake/modules/qtkeychain-bundled - module to compile qtkeychain library together with psi as static library +./cmake/modules/policyRules.cmake - contains default policy rules definitions + ./iris/CMakeLists.txt - builds iris library: contains options for the iris library (duplicated in a main script) @@ -158,11 +162,6 @@ This file contains the CMake files description whit "*TYPE*" type and "*PLUGIN*" name. The structure of these files are mostly the same -./win32/win32_definitions.cmake - provides the next features: - determines PSI_SDK and GSTREAMER_SDK pathes - determines the MinGW or MSVC compilation flags - contains the main definitions for OS Windows - ./win32/psi_win.rc.in - template-file to generate psi_win.rc file /*** @@ -188,7 +187,7 @@ In this case the variables.cmake generates at Psi build and contains: ***/ /*** -If there is a plguin debug necessity in OS Linux you can do next: +If there is a plugin debug necessity in OS Linux you can do next: - download psi-plus-snapshots repository - open CMakeLists.txt from root directory using qtcreator - choose profile and build type diff --git a/TODO b/TODO index 32fdd89459..bee7d3d08a 100644 --- a/TODO +++ b/TODO @@ -74,7 +74,7 @@ Extra if they say yes, then disable auto-open for profiles save presence changes to history? win32: docking (all optional) - grapple to edge of screen, like ICQ for windows + grapple to edge of screen. right-click in chat/eventdlg should have options to paste your current URL or IP address KDE-enhanced mode "previous" button in the eventdlg? @@ -116,7 +116,6 @@ Extra save order on the server support empty groups that get removed on signoff Have a way of marking some people as 'important' contacts, so they will always trigger sound - psuedo-chat support like Mirabilis ICQ / Licq (ie, split window, but still used like normal messages) friendlier infodlg. get rid of those lame tabs cvlist sorting options sort by group, online/offline split diff --git a/admin/emoji.py b/admin/emoji.py index da2f33c53e..e379195c09 100755 --- a/admin/emoji.py +++ b/admin/emoji.py @@ -23,9 +23,16 @@ data = [] ranges = [] -template_main="""// This is a generated file. See emoji.py for details +template_main = """// This is a generated file. See emoji.py for details // clang-format off -static std::vector db = {{{groups} + +#include "emojiregistry.h" + +#include +{groups_declarations}; + +static std::array db {{ +{groups_list} }}; static std::map ranges = {{ @@ -35,25 +42,28 @@ // clang-format on """ -template_group=""" - {{ - QT_TR_NOOP("{name}"), - {{{subgroups} - }} - }}""" +template_groups_list_item = """ std::move({var_name})""" + +template_group = """ +EmojiRegistry::Group {var_name} {{ + QT_TR_NOOP("{name}"), + {{{subgroups} + }} +}}""" template_subgroup = """ - {{ - "{name}", - {{{emojis} - }} - }}""" + {{ + "{name}", + {{{emojis} + }} + }}""" template_emoji = """ - {{ - "{code}", - "{desc}" - }}""" + {{ + "{code}", + "{desc}" + }}""" + def reset(): global group, subgroup @@ -61,14 +71,20 @@ def reset(): subgroup = dict(name="", emojis=[]) +def sanitize_name(name): + return re.sub(r"[^a-zA-Z0-9_]", "_", name) + + def parse(f): for line in f: if line.startswith("# group:"): reset() group["name"] = line.split(":")[1].strip() + group["var_name"] = "emoji_" + sanitize_name(group["name"]) data.append(group) elif line.startswith("# subgroup:"): subgroup = dict(name=line.split(":")[1].strip(), emojis=[]) + subgroup["var_name"] = sanitize_name(subgroup["name"]) group["subgroups"].append(subgroup) elif not line.strip() or line.startswith("#"): continue @@ -91,7 +107,7 @@ def cleanup_empty(): def generate_ranges(): - emojis=[] + emojis = [] for g in data: for sg in g["subgroups"]: emojis += sg["emojis"] @@ -108,16 +124,18 @@ def generate_ranges(): def generate_cpp_db(): - print(template_main.format(groups=",".join([ - template_group.format(name=group["name"], subgroups=",".join([ - template_subgroup.format(name=sub["name"], emojis=",".join([ - template_emoji.format(code=emoji[0], desc=emoji[1]) - for emoji in sub["emojis"] + print(template_main.format( + groups_list=",\n".join([template_groups_list_item.format(var_name=group["var_name"]) for group in data]), + groups_declarations=";\n".join([ + template_group.format(name=group["name"], var_name=group["var_name"],subgroups=",".join([ + template_subgroup.format(name=sub["name"], emojis=",".join([ + template_emoji.format(code=emoji[0], desc=emoji[1]) + for emoji in sub["emojis"] + ])) + for sub in group["subgroups"] ])) - for sub in group["subgroups"] - ])) - for group in data - ]), ranges=",\n ".join([f"{{{r[0]}, {r[1]}}}" for r in ranges]))) + for group in data + ]), ranges=",\n ".join([f"{{{r[0]}, {r[1]}}}" for r in ranges]))) # https://unicode.org/Public/emoji/15.0/emoji-test.txt diff --git a/client_icons.txt b/client_icons.txt index 6f99867936..6fb75929aa 100644 --- a/client_icons.txt +++ b/client_icons.txt @@ -4,11 +4,13 @@ asterisk asterisk atalk atalk,android#atalk beem beem bitlbee bitlbee +blabber blabber bombus bombus bot akarixb,aspro,blacksmith,capsula,dictbot,fatal,fin.jabber.ru,freq,gluxi,historian,imformer,j-cool,j-tmb,jabbrik,jabga,jabrvista,jame,jabrss,justa,lytgeygen,magnet2.py,neutrina,osiris,pako#bot,qabber,sleekbot,snapi,sofserver,storm,sulci,talisman,ultimate,utah,witcher,yamaneko candy candy centerim centerim chatsecure chatsecure,gibberbot +cheogram cheogram coccinella coccinella conv6ations conv6ations conversations conversations @@ -47,6 +49,7 @@ meegim meegim miranda miranda miranda-ng miranda#ng monal monal +monocles monocles movim movim,moxl.movim mozilla mozilla,thunderbird,instantbird omnipresence omnipresence @@ -58,7 +61,6 @@ poezio poezio,poez.io profanity profanity,www#profanity psi psi,psi-im psiplus psi+,psi-plus,psi-dev -pyicq-t pyicq,icq#transport qip qip,2010.qip.ru qutim qutim qxmpp-api qxmpp @@ -69,6 +71,7 @@ sawim sawim sleekxmpp-api sleekxmpp slixmpp-api slixmpp smack-api smack,igniterealtime.org#smack +snikket snikket spark spark,igniterealtime.org spectrum spectrum,binarytransport stanza-api stanza.io,stanzajs diff --git a/cmake/modules/generate_desktopfile.cmake b/cmake/modules/generate_desktopfile.cmake index 692c3dff7e..be52297386 100644 --- a/cmake/modules/generate_desktopfile.cmake +++ b/cmake/modules/generate_desktopfile.cmake @@ -3,14 +3,10 @@ cmake_minimum_required( VERSION 3.10.0 ) set(DESKTOP_FILE "${PROJECT_SOURCE_DIR}/linux/psi.desktop") set(DESKTOP_FILE_SEC_PART "${PROJECT_SOURCE_DIR}/linux/psi-extra-action1.desktop") file(READ ${DESKTOP_FILE} DESK_FILE_CONTENTS) -file(READ ${DESKTOP_FILE_SEC_PART} PART2_CONTENTS) -set(OUT_DESK_FILE "${CMAKE_BINARY_DIR}/${VERBOSED_NAME}.desktop") -file(WRITE ${OUT_DESK_FILE} - "${DESK_FILE_CONTENTS} -${PART2_CONTENTS}" - ) -unset(DESK_FILE_CONTENTS) -unset(PART2_CONTENTS) +file(READ ${DESKTOP_FILE_SEC_PART} ACTION_CONTENTS) +set(OUT_DESK_FILE "${CMAKE_CURRENT_BINARY_DIR}/${VERBOSED_NAME}.desktop") +string(APPEND DESK_FILE_CONTENTS ${ACTION_CONTENTS}) +unset(ACTION_CONTENTS) set(EXEC_REGEXP "Exec=psi ") set(NAME_REGEXP "Name=Psi") @@ -22,9 +18,7 @@ else() set(WMCLASS_NAME "Psi") endif() -set(TMP_DESK_FILE "${CMAKE_CURRENT_BINARY_DIR}/${VERBOSED_NAME}.desktop.in") -file(WRITE ${TMP_DESK_FILE} "") -file(READ ${OUT_DESK_FILE} DESK_FILE_CONTENTS) +file(WRITE ${OUT_DESK_FILE} "") #hack for desktop file generaion string(REGEX REPLACE "${EXEC_REGEXP}" "Exec=${VERBOSED_NAME} " FIX1 "${DESK_FILE_CONTENTS}") string(REGEX REPLACE "${ICON_REGEXP}" "Icon=${VERBOSED_NAME}" FIX2 "${FIX1}") @@ -37,15 +31,14 @@ else() endif() string(REGEX REPLACE "${WMCLASS_REGEXP}" "StartupWMClass=${WMCLASS_NAME}" FIX4 "${FIX3}") if(FIX4) - file(APPEND ${TMP_DESK_FILE} "${FIX4}") + file(APPEND ${OUT_DESK_FILE} "${FIX4}") elseif(FIX3) - file(APPEND ${TMP_DESK_FILE} "${FIX3}") + file(APPEND ${OUT_DESK_FILE} "${FIX3}") elseif(FIX2) - file(APPEND ${TMP_DESK_FILE} "${FIX2}") + file(APPEND ${OUT_DESK_FILE} "${FIX2}") else() - file(APPEND ${TMP_DESK_FILE} "${FIX1}") + file(APPEND ${OUT_DESK_FILE} "${FIX1}") endif() -configure_file(${TMP_DESK_FILE} ${OUT_DESK_FILE} COPYONLY) unset(DESK_FILE_CONTENTS) message(STATUS "${OUT_DESK_FILE} file generated") diff --git a/cmake/modules/qtkeychain-bundled.cmake b/cmake/modules/qtkeychain-bundled.cmake new file mode 100644 index 0000000000..e5b687fdc0 --- /dev/null +++ b/cmake/modules/qtkeychain-bundled.cmake @@ -0,0 +1,66 @@ +cmake_minimum_required(VERSION 3.10.0) + +set(QtkeychainRepo "https://github.com/frankosterfeld/qtkeychain.git") + +include(GNUInstallDirs) +set(libname qt${QT_DEFAULT_MAJOR_VERSION}keychain) +message(STATUS "Qt${QT_DEFAULT_MAJOR_VERSION}Keychain: using bundled") +set(Qtkeychain_PREFIX ${CMAKE_CURRENT_BINARY_DIR}/${libname}) +set(Qtkeychain_BUILD_DIR ${Qtkeychain_PREFIX}/build) +set(Qtkeychain_INSTALL_DIR ${Qtkeychain_PREFIX}/install) +set(Qtkeychain_INCLUDE_DIR ${Qtkeychain_INSTALL_DIR}/include) +set(Qtkeychain_LIBRARY_NAME +${CMAKE_STATIC_LIBRARY_PREFIX}${libname}${CMAKE_STATIC_LIBRARY_SUFFIX} +) +set(Qtkeychain_LIBRARY ${Qtkeychain_INSTALL_DIR}/${CMAKE_INSTALL_LIBDIR}/${Qtkeychain_LIBRARY_NAME}) + +if(APPLE) + set(COREFOUNDATION_LIBRARY "-framework CoreFoundation") + set(COREFOUNDATION_LIBRARY_SECURITY "-framework Security") + list(APPEND Qtkeychain_LIBRARY ${COREFOUNDATION_LIBRARY} ${COREFOUNDATION_LIBRARY_SECURITY}) +endif() + +if(${QT_DEFAULT_MAJOR_VERSION} VERSION_GREATER "5") + set(BUILD_WITH_QT6 ON) +else() + set(BUILD_WITH_QT6 OFF) +endif() + +include(FindGit) +find_package(Git REQUIRED) + +include(FindPkgConfig) + +include(ExternalProject) +#set CMake options and transfer the environment to an external project +set(Qtkeychain_BUILD_OPTIONS + -DBUILD_SHARED_LIBS=OFF + -DCMAKE_POSITION_INDEPENDENT_CODE=ON + -DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE} + -DCMAKE_INSTALL_PREFIX=${Qtkeychain_INSTALL_DIR} + -DCMAKE_PREFIX_PATH=${CMAKE_PREFIX_PATH} + -DBUILD_TEST_APPLICATION=OFF + -DBUILD_WITH_QT6=${BUILD_WITH_QT6} + -DCMAKE_CXX_FLAGS=${CMAKE_CXX_FLAGS} + -DCMAKE_TOOLCHAIN_FILE=${CMAKE_TOOLCHAIN_FILE} + -DCMAKE_MAKE_PROGRAM=${CMAKE_MAKE_PROGRAM} + -DCMAKE_INSTALL_LIBDIR=${CMAKE_INSTALL_LIBDIR} + -DLIBSECRET_SUPPORT=OFF + -DOSX_FRAMEWORK=OFF +) + +ExternalProject_Add(QtkeychainProject + PREFIX ${Qtkeychain_PREFIX} + BINARY_DIR ${Qtkeychain_BUILD_DIR} + GIT_REPOSITORY "${QtkeychainRepo}" + GIT_TAG main + CMAKE_ARGS ${Qtkeychain_BUILD_OPTIONS} + BUILD_BYPRODUCTS ${Qtkeychain_LIBRARY} + UPDATE_COMMAND "" +) + +set(QTKEYCHAIN_LIBRARIES ${Qtkeychain_LIBRARY}) +set(QTKEYCHAIN_INCLUDE_DIRS ${Qtkeychain_INCLUDE_DIR}) + +message(STATUS "LIB=${Qtkeychain_LIBRARY}") +message(STATUS "HEADER=${Qtkeychain_INCLUDE_DIR}") diff --git a/cmake/modules/win32-prepare-deps.cmake b/cmake/modules/win32-prepare-deps.cmake index a6e1595f3d..58b1ec8be6 100644 --- a/cmake/modules/win32-prepare-deps.cmake +++ b/cmake/modules/win32-prepare-deps.cmake @@ -70,6 +70,7 @@ if(WIN32) list(APPEND PATHES ${CMAKE_PREFIX_PATH}/bin ${CMAKE_PREFIX_PATH}/lib + ${CMAKE_PREFIX_PATH}/lib/ossl-modules ) endif() if(EXISTS "${SDK_PATH}") @@ -359,6 +360,7 @@ if(WIN32) if(USE_MXE) set(PSIMEDIA_DEPS libffi-6.dll + libffi-8.dll libfontconfig-1.dll libgio-2.0-0.dll libgmodule-2.0-0.dll @@ -444,7 +446,7 @@ if(WIN32) libhunspell.dll libotr-5.dll libotr.dll - libsignal-protocol-c.dll + libomemo-c.dll libssl-1_1-x64.dll libssl-3-x64.dll libssl-1_1.dll @@ -456,6 +458,7 @@ if(WIN32) libzlib.dll libzstd.dll legacy.dll + protobuf-c${D}.dll ssleay32.dll zlib1.dll ) @@ -492,6 +495,7 @@ if(WIN32) libmng-2.dll libpcre-1.dll libpcre16-0.dll + libpcre2-8-0.dll libpcre2-16-0.dll libpng16-16.dll libsharpyuv-0.dll diff --git a/win32/win32_definitions.cmake b/cmake/modules/win32_definitions.cmake similarity index 94% rename from win32/win32_definitions.cmake rename to cmake/modules/win32_definitions.cmake index 65d329aa7a..f2cc4975a1 100644 --- a/win32/win32_definitions.cmake +++ b/cmake/modules/win32_definitions.cmake @@ -51,7 +51,6 @@ if(WIN32) set(LIBGPGERROR_ROOT "${SDK_PATH}/" CACHE STRING "Path to libgpg-error library") set(LIBOTR_ROOT "${SDK_PATH}/" CACHE STRING "Path to libotr library") set(LIBTIDY_ROOT "${SDK_PATH}/" CACHE STRING "Path to libtidy library") - set(SIGNAL_PROTOCOL_C_ROOT "${SDK_PATH}/" CACHE STRING "Path to libsignal-protocol-c library") endif() set(ZLIB_DIR "${SDK_PATH}/" CACHE STRING "Path to zlib") set(OPENSSL_ROOT_DIR "${SDK_PATH}/" CACHE STRING "Path to openssl library") @@ -67,8 +66,11 @@ if(WIN32) endif() else() if(USE_MXE) - if(USE_KEYCHAIN AND (EXISTS "${CMAKE_PREFIX_PATH}/lib/cmake/Qt${QT_DEFAULT_MAJOR_VERSION}Keychain")) - set(Qt${QT_DEFAULT_MAJOR_VERSION}Keychain_DIR "${CMAKE_PREFIX_PATH}/lib/cmake/Qt${QT_DEFAULT_MAJOR_VERSION}Keychain" CACHE STRING "Path to Qt${QT_DEFAULT_MAJOR_VERSION}Keychain cmake files") + get_filename_component(_MXE_TOOLCHAIN_PATH "${CMAKE_TOOLCHAIN_FILE}" DIRECTORY) + get_filename_component(_MXE_ROOT_PATH "${_MXE_TOOLCHAIN_PATH}/../.." ABSOLUTE) + set(_KEYCHAIN_PATH "${_MXE_ROOT_PATH}/lib/cmake/Qt${QT_DEFAULT_MAJOR_VERSION}Keychain") + if(USE_KEYCHAIN AND (EXISTS ${_KEYCHAIN_PATH})) + set(Qt${QT_DEFAULT_MAJOR_VERSION}Keychain_DIR ${_KEYCHAIN_PATH} CACHE STRING "Path to Qt${QT_DEFAULT_MAJOR_VERSION}Keychain cmake files") endif() else() message(WARNING "Psi SDK not found at ${SDK_PATH}. Please set SDK_PATH variable or add Psi dependencies path to PATH system environmet variable") diff --git a/iconsets.qrc.in b/iconsets.qrc.in index f36208bd1e..0207d5fd43 100644 --- a/iconsets.qrc.in +++ b/iconsets.qrc.in @@ -379,6 +379,7 @@ iconsets/clients/default/adium.png iconsets/clients/default/bitlbee.png iconsets/clients/default/bot.png + iconsets/clients/default/cheogram.png iconsets/clients/default/conv6ations.png iconsets/clients/default/conversations.png iconsets/clients/default/dino.png @@ -391,6 +392,7 @@ iconsets/clients/default/mcabber.png iconsets/clients/default/miranda-ng.png iconsets/clients/default/miranda.png + iconsets/clients/default/monocles.png iconsets/clients/default/movim.png iconsets/clients/default/pidgin.png iconsets/clients/default/pix-art.png diff --git a/iconsets/clients/default/cheogram.png b/iconsets/clients/default/cheogram.png new file mode 100644 index 0000000000..58ab7c5e86 Binary files /dev/null and b/iconsets/clients/default/cheogram.png differ diff --git a/iconsets/clients/default/icondef.xml b/iconsets/clients/default/icondef.xml index 09449e99c4..6bea7119ad 100644 --- a/iconsets/clients/default/icondef.xml +++ b/iconsets/clients/default/icondef.xml @@ -26,6 +26,11 @@ bot.png + + clients/cheogram + cheogram.png + + clients/conv6ations conv6ations.png @@ -81,6 +86,11 @@ miranda-ng.png + + clients/monocles + monocles.png + + clients/movim movim.png diff --git a/iconsets/clients/default/monocles.png b/iconsets/clients/default/monocles.png new file mode 100644 index 0000000000..99c71e6726 Binary files /dev/null and b/iconsets/clients/default/monocles.png differ diff --git a/iris b/iris index 6d76ccde9d..35cd90c51f 160000 --- a/iris +++ b/iris @@ -1 +1 @@ -Subproject commit 6d76ccde9dfd6752b79c2d3b9966351c91821415 +Subproject commit 35cd90c51f7325c7e95ec1b1fe75469a3c22f0bd diff --git a/linux/build-in-ubuntu.sh b/linux/build-in-ubuntu.sh index dfa9e96739..329b92a5d1 100755 --- a/linux/build-in-ubuntu.sh +++ b/linux/build-in-ubuntu.sh @@ -34,10 +34,10 @@ BUILD_OPTIONS="-DCMAKE_INSTALL_PREFIX=/usr \ -DENABLE_PLUGINS=${ENABLE_PLUGINS} \ -DUSE_HUNSPELL=ON \ -DUSE_KEYCHAIN=ON \ - -DUSE_SPARKLE=OFF \ - -DBUNDLED_QCA=OFF \ - -DBUNDLED_USRSCTP=ON \ - -DBUILD_DEV_PLUGINS=OFF \ + -DBUNDLED_KEYCHAIN=OFF \ + -DBUNDLED_OMEMO_C_ALL=ON \ + -DIRIS_BUNDLED_QCA=OFF \ + -DIRIS_BUNDLED_USRSCTP=ON \ -DVERBOSE_PROGRAM_NAME=ON \ " diff --git a/linux/com.psi_plus.Psi_plus.json b/linux/com.psi_plus.Psi_plus.json index 409c37e3a7..d923c910b7 100644 --- a/linux/com.psi_plus.Psi_plus.json +++ b/linux/com.psi_plus.Psi_plus.json @@ -94,13 +94,13 @@ ] }, { - "name": "libsignal-protocol-c", + "name": "libomemo-c", "buildsystem": "cmake", "config-opts": ["-DCMAKE_BUILD_TYPE=Release", "-DBUILD_TESTING=0", "-DCMAKE_POSITION_INDEPENDENT_CODE=ON", "-DCMAKE_INSTALL_PREFIX=/app"], "sources": [ { "type": "git", - "url": "https://github.com/signalapp/libsignal-protocol-c.git" + "url": "https://github.com/dino/libomemo-c.git" } ] }, diff --git a/linux/org.psi_im.Psi.json b/linux/org.psi_im.Psi.json index 42c38c094c..9c15f3c4ce 100644 --- a/linux/org.psi_im.Psi.json +++ b/linux/org.psi_im.Psi.json @@ -94,13 +94,13 @@ ] }, { - "name": "libsignal-protocol-c", + "name": "libomemo-c", "buildsystem": "cmake", "config-opts": ["-DCMAKE_BUILD_TYPE=Release", "-DBUILD_TESTING=0", "-DCMAKE_POSITION_INDEPENDENT_CODE=ON", "-DCMAKE_INSTALL_PREFIX=/app"], "sources": [ { "type": "git", - "url": "https://github.com/signalapp/libsignal-protocol-c.git" + "url": "https://github.com/dino/libomemo-c.git" } ] }, diff --git a/linux/psi-extra-action1.desktop b/linux/psi-extra-action1.desktop index b76b07f9f7..239a88931e 100644 --- a/linux/psi-extra-action1.desktop +++ b/linux/psi-extra-action1.desktop @@ -3,3 +3,8 @@ Exec=psi --choose-profile Name=Start with another profile Icon=psi + +[Desktop Action QuitApplication] +Exec=psi --quit +Name=Quit the application +Icon=application-exit diff --git a/linux/psi.desktop b/linux/psi.desktop index e21afe6ac4..6957078f9d 100644 --- a/linux/psi.desktop +++ b/linux/psi.desktop @@ -13,7 +13,7 @@ X-KDE-StartupNotify=true StartupWMClass=Psi Categories=Network;InstantMessaging;Qt; Keywords=XMPP;Jabber;Chat;InstantMessaging; -Actions=SelectProfile; +Actions=SelectProfile;QuitApplication; # Translations GenericName[be]=Кліент XMPP @@ -66,5 +66,5 @@ Comment[sv]=Kommunicera över XMPPnätverket Comment[uk]=Програма для спілкування в мережі XMPP Comment[ur_PK]=جیبر نیٹ ورک پر مواصلت کریں Comment[vi]=Liên lạc qua mạng XMPP nhé -Comment[zh_CN]=通过XMPP网络进行通信 +Comment[zh_CN]=通过 XMPP 网络进行交流 Comment[zh_TW]=通過XMPP網絡進行通信 diff --git a/mac/build-using-homebrew.sh b/mac/build-using-homebrew.sh index cc6f0faac2..67629eaf4e 100755 --- a/mac/build-using-homebrew.sh +++ b/mac/build-using-homebrew.sh @@ -3,7 +3,7 @@ # Authors: Boris Pek # License: Public Domain # Created: 2018-10-07 -# Updated: 2022-04-04 +# Updated: 2024-05-17 # Version: N/A # # Description: script for building of app bundles for macOS @@ -26,7 +26,7 @@ # brew install hunspell minizip qt@5 qtkeychain # # Build dependencies for Psi plugins: -# brew install tidy-html5 libotr libsignal-protocol-c +# brew install tidy-html5 libotr libomemo-c # # Additional tools: # brew install gnupg pinentry-mac wget htop @@ -59,11 +59,13 @@ BUILD_OPTIONS="-DCMAKE_BUILD_TYPE=Release \ -DBUILD_DEV_PLUGINS=${ENABLE_DEV_PLUGINS} \ -DBUILD_PSIMEDIA=${ENABLE_PSIMEDIA} \ -DENABLE_PLUGINS=${ENABLE_PLUGINS} \ + -DUSE_SPARKLE=OFF \ -DUSE_HUNSPELL=ON \ -DUSE_KEYCHAIN=ON \ - -DUSE_SPARKLE=OFF \ - -DBUNDLED_QCA=ON \ - -DBUNDLED_USRSCTP=ON \ + -DBUNDLED_KEYCHAIN=ON \ + -DBUNDLED_OMEMO_C_ALL=ON \ + -DIRIS_BUNDLED_QCA=ON \ + -DIRIS_BUNDLED_USRSCTP=ON \ -DVERBOSE_PROGRAM_NAME=ON" mkdir -p "${MAIN_DIR}/builddir" diff --git a/options/default.xml b/options/default.xml index 568516344f..970d3d6352 100644 --- a/options/default.xml +++ b/options/default.xml @@ -75,7 +75,6 @@ true - false true true @@ -102,7 +101,7 @@ - psi/classic + psi/new_classic true @@ -333,7 +332,7 @@ QLineEdit#le_status_text { 500 150 false - psi/classic + psi/new_classic 960 @@ -497,7 +496,7 @@ QLineEdit#le_status_text { true false - 0 + 100 @@ -645,7 +644,7 @@ QLineEdit#le_status_text { Shift+Del - Ctrl+C + Ctrl+M Ctrl+R F2 diff --git a/plugins/include/optionaccessinghost.h b/plugins/include/optionaccessinghost.h index 4962894d03..d264c9d886 100644 --- a/plugins/include/optionaccessinghost.h +++ b/plugins/include/optionaccessinghost.h @@ -32,8 +32,8 @@ class OptionAccessingHost { public: virtual ~OptionAccessingHost() { } - virtual void setPluginOption(const QString &option, const QVariant &value) = 0; - virtual QVariant getPluginOption(const QString &option, const QVariant &defValue = QVariant::Invalid) = 0; + virtual void setPluginOption(const QString &option, const QVariant &value) = 0; + virtual QVariant getPluginOption(const QString &option, const QVariant &defValue = {}) = 0; virtual void setGlobalOption(const QString &option, const QVariant &value) = 0; virtual QVariant getGlobalOption(const QString &option) = 0; diff --git a/psi.doap b/psi.doap index 948ddc3947..a397556539 100644 --- a/psi.doap +++ b/psi.doap @@ -4,7 +4,7 @@ xmlns:xmpp='https://linkmauve.fr/ns/xmpp-doap#' xmlns:schema='https://schema.org/'> - psi + Psi 2001-07-07 @@ -13,64 +13,816 @@ Psi is an XMPP client designed for experienced users. It is highly portable and runs on GNU/Linux, MS Windows, macOS, FreeBSD and Haiku. - + - + + + en + C++ Linux macOS - BSD + FreeBSD + NetBSD Windows Haiku + + + + + + + + - + + complete + + + + + + + + complete + + + + + + + + complete + + + + + + + + complete + Will be dropped. + + + + + + + + partial + Only legacy FT. + + + + + + + + complete + + + + + + + + complete + + + + + + + + complete + Via plugin. + + + + + + + + complete + + + + + + + + complete + + + + + + + + complete + + + + + + + + complete + + + + + + + + complete + + + + + + + + complete + + + + + + + + complete + Via plugin. + + + + + + + + complete + + + + + + + + complete + + + + + + + complete - 1.1.0 - 2.0 - + complete 1.0 2.0 - + - + complete - 0.19.1 - 2.0 - + + + + + + + + complete + + + + + + + + partial + jabber:x:oob only. + + + + + + + + complete + + + + + + + + complete + + + + + + + + complete + + + + + + + + partial + Will be dropped. + + + + + + + + complete + + + + + + + + complete + + + + + + + + partial + 1.1.4 + Loading avatars from URLs is not supported at the moment. + + + + + + + + complete + + + + + + + + complete + + + + + + + + complete + + + + + + + + complete + + + + + + + + complete + + + + + + + + complete + + + + + + + + complete + + + + + + + + complete + + + + + + + + complete + + + + + + + + complete + + + + + + + + complete + + + + + + + + complete + + + + + + + + complete + + + + + + + + complete + + + + + + + + complete + + + + + + + + complete + + + + + + + + complete + 1.1.1 + + + + + + + + complete + + + + + + + + complete + + + + + + + + complete + + + + + + + + complete + + + + + + + + complete + Via psimedia library. + + + + + + + + complete + + + + + + + + partial + Only for RTP calls in psimedia. + + + + + + + + complete + + + + + + + + complete + + + + + + + + complete + + + + + + + + complete + Via plugin. + + + + + + + + complete + + + + + + + + complete + 1.0.0 + Currently only for file transfer over WebRTC data channels. + + + + + + + + complete + + + + + + + + complete + Via plugin. + + + + + + + + complete + + + + + + + + complete + + + + + + + + complete + 0.19.1 + 2.0 + + + + + + + + complete + + + + + + + + complete + + + + + + + + complete + + + + + + + + complete + + + + + + + + complete + + + + + + + + complete + + + + + + + + complete + 0.12.0 + + + + + + + + complete + + + + + + + + complete + 1.2.1 + + + + + + + + partial + 1.0.0 + Just DTLS part but not SRTP. + + + + + + + + complete + + + + + + + + complete + 0.3.1 + + + + + + + + complete + 0.7.0 + + + + + + + + complete + 1.1.0 + 2.0 + Both :0 and legacy namespace. + + + + + + + + complete + Via plugin. + + + + + + + + complete + 1.1.0 + + + + + + + + complete + Currently used only for file transfer/sharing. + + + + + + + + complete + + + + + + + + partial + Incomplete: low level part is ready, information dialogs for user are necessary. + + + + + + + + partial + 2.0 + Via plugin. It is not yet possible to send an encrypted file. + + + + + + + + complete + + + + + + + + complete + 1.0.0 + + + + + + + + complete + All of them. + + + + + + + + partial + Without security checks for now. + 0.4.1 + + + + + + + + partial + No allowed reaction handling / everything is allowed + 0.2.1 + + + + + + + + complete + 0.1.2 + 2.0 + + + + + + + + complete + 0.1.0 - 0.1.5 + 1.5 2020-09-06 diff --git a/qa/integration/psi-data/profiles/default/options.xml b/qa/integration/psi-data/profiles/default/options.xml index d37f7ee2e9..ebc5b68f9c 100644 --- a/qa/integration/psi-data/profiles/default/options.xml +++ b/qa/integration/psi-data/profiles/default/options.xml @@ -454,7 +454,7 @@ Ctrl+M Ctrl+R - Ctrl+C + Del Ctrl+L diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 208ebf9ed3..e41c94927d 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,4 +1,4 @@ -set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD 20) list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}" @@ -7,20 +7,20 @@ set(CMAKE_AUTOMOC ON) if(LINUX) set(LIB_SUFFIX "" CACHE STRING "Define suffix of directory name (32/64)") - if(NOT DEV_MODE) - set(PSI_LIBDIR "${CMAKE_INSTALL_PREFIX}/lib${LIB_SUFFIX}/${PROJECT_NAME}" CACHE STRING "Path to Psi/Psi+ libraries directory") - set(PSI_DATADIR "${CMAKE_INSTALL_PREFIX}/share/${PROJECT_NAME}" CACHE STRING "Path to Psi/Psi+ data directory") - else() + if(DEV_MODE) set(PSI_LIBDIR "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}") set(PSI_DATADIR "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}") + else() + set(PSI_LIBDIR "${CMAKE_INSTALL_PREFIX}/lib${LIB_SUFFIX}/${PROJECT_NAME}" CACHE STRING "Path to Psi/Psi+ libraries directory") + set(PSI_DATADIR "${CMAKE_INSTALL_PREFIX}/share/${PROJECT_NAME}" CACHE STRING "Path to Psi/Psi+ data directory") endif() endif() if(APPLE AND NOT PSI_LIBDIR) - if(NOT DEV_MODE) - set(PSI_LIBDIR "/Applications/${CLIENT_NAME}.app/Contents/Resources/plugins") - else() + if(DEV_MODE) set(PSI_LIBDIR "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/plugins") + else() + set(PSI_LIBDIR "/Applications/${CLIENT_NAME}.app/Contents/Resources/plugins") endif() endif() @@ -81,7 +81,11 @@ else() find_package(Qt${QT_DEFAULT_MAJOR_VERSION} 6.6 REQUIRED COMPONENTS ${REQUIRED_QT_COMPONENTS}) endif() if(USE_KEYCHAIN) - find_package(Qt${QT_DEFAULT_MAJOR_VERSION} REQUIRED COMPONENTS Keychain) + if(NOT BUNDLED_KEYCHAIN) + find_package(Qt${QT_DEFAULT_MAJOR_VERSION} REQUIRED COMPONENTS Keychain) + else() + include(qtkeychain-bundled) + endif() set(KEYCHAIN_LIBS ${QTKEYCHAIN_LIBRARIES}) include_directories(${QTKEYCHAIN_INCLUDE_DIRS}) add_definitions(-DHAVE_KEYCHAIN) @@ -399,7 +403,7 @@ if(WIN32 AND (${QT_DEFAULT_MAJOR_VERSION} GREATER_EQUAL 6)) _qt_internal_generate_win32_rc_file(${PROJECT_NAME}) endif() -include(${PROJECT_SOURCE_DIR}/cmake/modules/fix-codestyle.cmake) +include(fix-codestyle) #Add webkit/webengine suffix to output binary name if(VERBOSE_PROGRAM_NAME) @@ -433,6 +437,10 @@ if(UNIX OR IS_WEBENGINE) endif() add_dependencies(${PROJECT_NAME} build_ui_files) +if(BUNDLED_KEYCHAIN) + add_dependencies(${PROJECT_NAME} QtkeychainProject) +endif() + #Add win32 additional dependencies if(WIN32 AND MSVC) list(APPEND EXTRA_LIBS @@ -477,6 +485,46 @@ if(APPLE) target_link_libraries(${PROJECT_NAME} CocoaUtilities) endif() +#Privare Psi project defenitions +if(LINUX) + if(NOT VERBOSED_NAME) + set(VERBOSED_NAME ${PROJECT_NAME}) + endif() + target_compile_definitions(${PROJECT_NAME} PRIVATE + HAVE_FREEDESKTOP + APP_PREFIX=${CMAKE_INSTALL_PREFIX} + APP_BIN_NAME=${VERBOSED_NAME} + ) + #Set compile definions for options static library + target_compile_definitions(options PRIVATE + HAVE_FREEDESKTOP + APP_BIN_NAME=${VERBOSED_NAME} + ) +endif() + +#TaskbarNotifier definition +if(USE_TASKBARNOTIFIER) + target_compile_definitions(${PROJECT_NAME} PRIVATE USE_TASKBARNOTIFIER) +endif() +if(USE_CRASH) + target_compile_definitions(${PROJECT_NAME} PRIVATE USE_CRASH) +endif() +if(LINUX AND USE_X11) + target_compile_definitions(${PROJECT_NAME} PRIVATE HAVE_X11) + if(LIMIT_X11_USAGE) + target_compile_definitions(${PROJECT_NAME} PRIVATE LIMIT_X11_USAGE) + endif() +endif() +if(IS_WEBKIT OR IS_WEBENGINE) + target_compile_definitions(${PROJECT_NAME} PRIVATE WEBKIT) + if(IS_WEBENGINE) + target_compile_definitions(${PROJECT_NAME} PRIVATE WEBENGINE) + endif() +endif() +if(ISDEBUG AND CHATVIEW_CORRECTION_DEBUG) + target_compile_definitions(${PROJECT_NAME} PRIVATE CORRECTION_DEBUG) +endif() + #Pre-install section set(OTHER_FILES ${PROJECT_SOURCE_DIR}/certs @@ -533,7 +581,7 @@ if(LINUX) set(VERBOSED_NAME ${PROJECT_NAME}) endif() #Generate .desktop file - include(${PROJECT_SOURCE_DIR}/cmake/modules/generate_desktopfile.cmake) + include(generate_desktopfile) install(FILES ${OUT_DESK_FILE} DESTINATION ${APPS_INSTALL_DIR}) if(PSI_PLUS) set(PSI_LOGO_PREFIX ${PROJECT_SOURCE_DIR}/iconsets/system/default/psiplus) @@ -605,7 +653,7 @@ elseif(WIN32) copy("${PROJECT_SOURCE_DIR}/myspell/" "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/myspell/" prepare-bin) endif() if(ENABLE_PORTABLE OR DEV_MODE) - include(${PROJECT_SOURCE_DIR}/cmake/modules/win32-prepare-deps.cmake) + include(win32-prepare-deps) endif() elseif(APPLE) include("${PROJECT_SOURCE_DIR}/mac/macos_definitions.cmake") diff --git a/src/accountadddlg.cpp b/src/accountadddlg.cpp index 14d84face5..16f4d8d299 100644 --- a/src/accountadddlg.cpp +++ b/src/accountadddlg.cpp @@ -105,8 +105,8 @@ void AccountAddDlg::add() delete w; - newAccount = psi->createAccount(le_name->text(), jid, pass, opt_host, host, port, false, ssl, proxy, - tlsOverrideDomain, tlsOverrideCert); + newAccount = psi->createAccount(le_name->text(), jid, pass, opt_host, host, port, ssl, proxy, tlsOverrideDomain, + tlsOverrideCert); } else { newAccount = psi->createAccount(le_name->text()); } diff --git a/src/accountlabel.cpp b/src/accountlabel.cpp index e63df9985e..409c8a57c5 100644 --- a/src/accountlabel.cpp +++ b/src/accountlabel.cpp @@ -23,7 +23,7 @@ AccountLabel::AccountLabel(QWidget *parent) : QLabel(parent), showJid_(true) { - setFrameStyle(QFrame::Panel | QFrame::Sunken); + // setFrameStyle(QFrame::Panel | QFrame::Sunken); } AccountLabel::~AccountLabel() { } diff --git a/src/accountmanagedlg.cpp b/src/accountmanagedlg.cpp index 9e08e71378..345c56e389 100644 --- a/src/accountmanagedlg.cpp +++ b/src/accountmanagedlg.cpp @@ -191,9 +191,8 @@ void AccountRemoveDlg::remove() QString pass = le_pass->text(); Jid j(Jid(d->acc.jid).withResource(d->acc.resource)); - client->connectToServer(j, d->acc.legacy_ssl_probe, d->acc.ssl == UserAccount::SSL_Legacy, - d->acc.ssl == UserAccount::SSL_Yes, d->acc.opt_host ? d->acc.host : QString(), d->acc.port, - d->acc.proxyID, &pass); + client->connectToServer(j, d->acc.ssl == UserAccount::Direct_TLS, d->acc.ssl == UserAccount::TLS_Yes, + d->acc.opt_host ? d->acc.host : QString(), d->acc.port, d->acc.proxyID, &pass); } void AccountRemoveDlg::client_handshaken() diff --git a/src/accountmodifydlg.cpp b/src/accountmodifydlg.cpp index 85bfa1b5fe..996c05bf36 100644 --- a/src/accountmodifydlg.cpp +++ b/src/accountmodifydlg.cpp @@ -96,10 +96,10 @@ void AccountModifyDlg::init() le_name->setText(acc.name); le_jid->setText(JIDUtil::accountToString(acc.jid, false)); - cb_ssl->addItem(tr("Always"), UserAccount::SSL_Yes); - cb_ssl->addItem(tr("When available"), UserAccount::SSL_Auto); - cb_ssl->addItem(tr("Never"), UserAccount::SSL_No); - cb_ssl->addItem(tr("Legacy SSL"), UserAccount::SSL_Legacy); + cb_ssl->addItem(tr("Always"), UserAccount::TLS_Yes); + cb_ssl->addItem(tr("When available"), UserAccount::TLS_Auto); + cb_ssl->addItem(tr("Never"), UserAccount::TLS_No); + cb_ssl->addItem(tr("Direct TLS"), UserAccount::Direct_TLS); cb_ssl->setCurrentIndex(cb_ssl->findData(acc.ssl)); connect(cb_ssl, SIGNAL(activated(int)), SLOT(sslActivated(int))); @@ -378,20 +378,20 @@ void AccountModifyDlg::setPassword(const QString &pw) void AccountModifyDlg::sslActivated(int i) { - if ((cb_ssl->itemData(i) == UserAccount::SSL_Yes || cb_ssl->itemData(i) == UserAccount::SSL_Legacy) + if ((cb_ssl->itemData(i) == UserAccount::TLS_Yes || cb_ssl->itemData(i) == UserAccount::Direct_TLS) && !checkSSL()) { - cb_ssl->setCurrentIndex(cb_ssl->findData(UserAccount::SSL_Auto)); - } else if (cb_ssl->itemData(i) == UserAccount::SSL_Legacy && !ck_host->isChecked()) { + cb_ssl->setCurrentIndex(cb_ssl->findData(UserAccount::TLS_Auto)); + } else if (cb_ssl->itemData(i) == UserAccount::Direct_TLS && !ck_host->isChecked()) { QMessageBox::critical(this, tr("Error"), - tr("Legacy SSL is only available in combination with manual host/port.")); - cb_ssl->setCurrentIndex(cb_ssl->findData(UserAccount::SSL_Auto)); + tr("Direct TLS is only available in combination with manual host/port.")); + cb_ssl->setCurrentIndex(cb_ssl->findData(UserAccount::TLS_Auto)); } } bool AccountModifyDlg::checkSSL() { if (!QCA::isSupported("tls")) { - QMessageBox::critical(this, tr("SSL error"), tr("Cannot enable SSL/TLS. Plugin not found.")); + QMessageBox::critical(this, tr("TLS error"), tr("Cannot enable TLS. Plugin not found.")); return false; } return true; @@ -403,8 +403,8 @@ void AccountModifyDlg::hostToggled(bool on) lb_host->setEnabled(on); le_port->setEnabled(on); lb_port->setEnabled(on); - if (!on && cb_ssl->currentIndex() == cb_ssl->findData(UserAccount::SSL_Legacy)) { - cb_ssl->setCurrentIndex(cb_ssl->findData(UserAccount::SSL_Auto)); + if (!on && cb_ssl->currentIndex() == cb_ssl->findData(UserAccount::Direct_TLS)) { + cb_ssl->setCurrentIndex(cb_ssl->findData(UserAccount::TLS_Auto)); } } @@ -507,7 +507,7 @@ void AccountModifyDlg::save() acc.opt_sm = ck_enableSM->isChecked(); acc.ibbOnly = ck_ibbOnly->isChecked(); acc.dtProxy = le_dtProxy->text(); - acc.stunHost = cb_stunHost->currentIndex() ? cb_stunHost->currentText().trimmed() : ""; + acc.stunHost = cb_stunHost->currentText().trimmed(); acc.stunHosts.clear(); // first item is no host for (int i = 1; i < cb_stunHost->count(); i++) { diff --git a/src/accountregdlg.cpp b/src/accountregdlg.cpp index a4a981aa83..5958782d18 100644 --- a/src/accountregdlg.cpp +++ b/src/accountregdlg.cpp @@ -44,7 +44,7 @@ AccountRegDlg::AccountRegDlg(PsiCon *psi, QWidget *parent) : QDialog(parent), ps // step // Initialize settings - ssl_ = UserAccount::SSL_Auto; + ssl_ = UserAccount::TLS_Auto; port_ = 5222; // Server select button @@ -62,9 +62,9 @@ AccountRegDlg::AccountRegDlg(PsiCon *psi, QWidget *parent) : QDialog(parent), ps connect(ui_.ck_host, SIGNAL(toggled(bool)), SLOT(hostToggled(bool))); // SSL - ui_.cb_ssl->addItem(tr("Always"), UserAccount::SSL_Yes); - ui_.cb_ssl->addItem(tr("When available"), UserAccount::SSL_Auto); - ui_.cb_ssl->addItem(tr("Legacy SSL"), UserAccount::SSL_Legacy); + ui_.cb_ssl->addItem(tr("Always"), UserAccount::TLS_Yes); + ui_.cb_ssl->addItem(tr("When available"), UserAccount::TLS_Auto); + ui_.cb_ssl->addItem(tr("Direct TLS"), UserAccount::Direct_TLS); ui_.cb_ssl->setCurrentIndex(ui_.cb_ssl->findData(ssl_)); connect(ui_.cb_ssl, SIGNAL(activated(int)), SLOT(sslActivated(int))); @@ -114,13 +114,13 @@ void AccountRegDlg::done(int r) void AccountRegDlg::sslActivated(int i) { - if ((ui_.cb_ssl->itemData(i) == UserAccount::SSL_Yes || ui_.cb_ssl->itemData(i) == UserAccount::SSL_Legacy) + if ((ui_.cb_ssl->itemData(i) == UserAccount::TLS_Yes || ui_.cb_ssl->itemData(i) == UserAccount::Direct_TLS) && !checkSSL()) { - ui_.cb_ssl->setCurrentIndex(ui_.cb_ssl->findData(UserAccount::SSL_Auto)); - } else if (ui_.cb_ssl->itemData(i) == UserAccount::SSL_Legacy && !ui_.ck_host->isChecked()) { + ui_.cb_ssl->setCurrentIndex(ui_.cb_ssl->findData(UserAccount::TLS_Auto)); + } else if (ui_.cb_ssl->itemData(i) == UserAccount::Direct_TLS && !ui_.ck_host->isChecked()) { QMessageBox::critical(this, tr("Error"), - tr("Legacy SSL is only available in combination with manual host/port.")); - ui_.cb_ssl->setCurrentIndex(ui_.cb_ssl->findData(UserAccount::SSL_Auto)); + tr("Direct TLS is only available in combination with manual host/port.")); + ui_.cb_ssl->setCurrentIndex(ui_.cb_ssl->findData(UserAccount::TLS_Auto)); } } @@ -139,8 +139,8 @@ void AccountRegDlg::hostToggled(bool on) ui_.le_port->setEnabled(on); ui_.lb_host->setEnabled(on); ui_.lb_port->setEnabled(on); - if (!on && ui_.cb_ssl->currentIndex() == ui_.cb_ssl->findData(UserAccount::SSL_Legacy)) { - ui_.cb_ssl->setCurrentIndex(ui_.cb_ssl->findData(UserAccount::SSL_Auto)); + if (!on && ui_.cb_ssl->currentIndex() == ui_.cb_ssl->findData(UserAccount::Direct_TLS)) { + ui_.cb_ssl->setCurrentIndex(ui_.cb_ssl->findData(UserAccount::TLS_Auto)); } } @@ -196,7 +196,7 @@ void AccountRegDlg::next() // Connect to the server ui_.busy->start(); block(); - client_->connectToServer(server_, false, ssl_ == UserAccount::SSL_Legacy, ssl_ == UserAccount::SSL_Yes, + client_->connectToServer(server_, ssl_ == UserAccount::Direct_TLS, ssl_ == UserAccount::TLS_Yes, opt_host_ ? host_ : QString(), port_, proxy_); } else if (ui_.sw_register->currentWidget() == ui_.page_fields) { // Initialize the form diff --git a/src/activeprofiles.cpp b/src/activeprofiles.cpp index f1a57ea5f9..9997f527fa 100644 --- a/src/activeprofiles.cpp +++ b/src/activeprofiles.cpp @@ -114,3 +114,14 @@ ActiveProfiles *ActiveProfiles::instance() * \fn void ActiveProfiles::raiseRequested() * \brief Signal emitted when other Psi instance requested to raise main window. */ + +/** + * \fn bool ActiveProfiles::quit(const QString &profile) const + * \brief Closes the active Psi application instance running \a profile. + * If \a profile is empty, other running instance is selected. + */ + +/** + * \fn void ActiveProfiles::quitRequested() + * \brief Signal emitted when other Psi instance requested to quit application. + */ diff --git a/src/activeprofiles.h b/src/activeprofiles.h index 65d8243713..a512b4e3d7 100644 --- a/src/activeprofiles.h +++ b/src/activeprofiles.h @@ -40,6 +40,7 @@ class ActiveProfiles : public QObject { bool setStatus(const QString &profile, const QString &status, const QString &message) const; bool openUri(const QString &profile, const QString &uri) const; bool raise(const QString &profile, bool withUI) const; + bool quit(const QString &profile) const; ~ActiveProfiles(); @@ -48,6 +49,7 @@ class ActiveProfiles : public QObject { void setStatusRequested(const QString &status, const QString &message); void openUriRequested(const QString &uri); void raiseRequested(); + void quitRequested(); protected: static ActiveProfiles *instance_; diff --git a/src/activeprofiles_dbus.cpp b/src/activeprofiles_dbus.cpp index 688366cbe6..d95ad5a255 100644 --- a/src/activeprofiles_dbus.cpp +++ b/src/activeprofiles_dbus.cpp @@ -213,3 +213,9 @@ bool ActiveProfiles::raise(const QString &profile, bool withUI) const } return rmsg.type() == QDBusMessage::ReplyMessage; } + +bool ActiveProfiles::quit(const QString &profile) const +{ + QDBusInterface(d->dbusName(profile), "/Main", PSIDBUSMAINIF).call(QDBus::NoBlock, "quit"); + return true; +} diff --git a/src/activeprofiles_stub.cpp b/src/activeprofiles_stub.cpp index 2cf561b7e5..4aecba4a2f 100644 --- a/src/activeprofiles_stub.cpp +++ b/src/activeprofiles_stub.cpp @@ -72,3 +72,9 @@ bool ActiveProfiles::raise(const QString &profile, bool withUI) const Q_UNUSED(withUI); return true; } + +bool ActiveProfiles::quit(const QString &profile) const +{ + Q_UNUSED(profile); + return true; +} diff --git a/src/activeprofiles_win.cpp b/src/activeprofiles_win.cpp index 55114f0a8e..b3690db912 100644 --- a/src/activeprofiles_win.cpp +++ b/src/activeprofiles_win.cpp @@ -198,15 +198,20 @@ bool ActiveProfiles::Private::nativeEvent(const QByteArray &eventType, void *mes } if (list.count() > 1) { - if (list[0] == "openUri") { + if (list[0] == QStringLiteral("openUri")) { emit ap->openUriRequested(list.value(1)); *result = TRUE; - } else if (list[0] == "setStatus") { + } else if (list[0] == QStringLiteral("setStatus")) { emit ap->setStatusRequested(list.value(1), list.value(2)); *result = TRUE; - } else if (list[0] == "recvNextEvent") { + } + } else if (list.count() == 1) { + if (list[0] == QStringLiteral("recvNextEvent")) { emit ap->recvNextEventRequested(); *result = TRUE; + } else if (list[0] == QStringLiteral("quit")) { + emit ap->quitRequested(); + *result = TRUE; } } } @@ -324,21 +329,24 @@ bool ActiveProfiles::raise(const QString &profile, bool withUI) const bool ActiveProfiles::openUri(const QString &profile, const QString &uri) const { - QStringList list; - list << "openUri" << uri; + QStringList list { QStringLiteral("openUri"), uri }; return d->sendStringList(profile.isEmpty() ? d->pickProfile() : profile, list); } bool ActiveProfiles::recvNextEvent(const QString &profile) const { - QStringList list; - list << "recvNextEvent"; + QStringList list { QStringLiteral("recvNextEvent") }; return d->sendStringList(profile.isEmpty() ? d->pickProfile() : profile, list); } bool ActiveProfiles::setStatus(const QString &profile, const QString &status, const QString &message) const { - QStringList list; - list << "setStatus" << status << message; + QStringList list { QStringLiteral("setStatus"), status, message }; + return d->sendStringList(profile.isEmpty() ? d->pickProfile() : profile, list); +} + +bool ActiveProfiles::quit(const QString &profile) const +{ + QStringList list { QStringLiteral("quit") }; return d->sendStringList(profile.isEmpty() ? d->pickProfile() : profile, list); } diff --git a/src/adduser.ui b/src/adduser.ui index 8e55bc3930..954c11cf3c 100644 --- a/src/adduser.ui +++ b/src/adduser.ui @@ -298,13 +298,6 @@ p, li { white-space: pre-wrap; } - - - - &Close - - - @@ -321,6 +314,13 @@ p, li { white-space: pre-wrap; } + + + + &Close + + + diff --git a/src/adduserdlg.cpp b/src/adduserdlg.cpp index ac41028869..ebb694df65 100644 --- a/src/adduserdlg.cpp +++ b/src/adduserdlg.cpp @@ -24,7 +24,7 @@ #include "infodlg.h" #include "iris/xmpp_client.h" #include "iris/xmpp_tasks.h" -#include "iris/xmpp_vcard.h" +#include "iris/xmpp_vcard4.h" #include "psiaccount.h" #include "psiiconset.h" #include "tasklist.h" @@ -293,9 +293,9 @@ void AddUserDlg::errorGateway(const QString &str, const QString &err) void AddUserDlg::getVCardActivated() { - const VCard vcard = VCardFactory::instance()->vcard(jid()); + const auto vcard = VCardFactory::instance()->vcard(jid()); - InfoDlg *w = new InfoDlg(InfoWidget::Contact, jid(), vcard, d->pa, nullptr, false); + InfoDlg *w = new InfoDlg(InfoWidget::Contact, jid(), vcard, d->pa, nullptr); w->show(); // automatically retrieve info if it doesn't exist @@ -305,40 +305,41 @@ void AddUserDlg::getVCardActivated() void AddUserDlg::resolveNickActivated() { - JT_VCard *jt = VCardFactory::instance()->getVCard( - jid(), d->pa->client()->rootTask(), this, - [this]() { - JT_VCard *jt = static_cast(sender()); - - if (jt->success()) { - QString nickname; - const XMPP::VCard vcard = jt->vcard(); - if (!vcard.nickName().isEmpty()) { - nickname = vcard.nickName(); - } else if (!vcard.fullName().isEmpty()) { - nickname = vcard.fullName(); - } else { - nickname = vcard.givenName(); - if (nickname.isEmpty()) { - nickname = vcard.middleName(); - } else if (!vcard.middleName().isEmpty()) { - nickname += " " + vcard.middleName(); - } - if (nickname.isEmpty()) { - nickname = vcard.familyName(); - } else if (!vcard.familyName().isEmpty()) { - nickname += " " + vcard.familyName(); - } - } + auto *jt = VCardFactory::instance()->getVCard(d->pa, jid()); + connect(jt, &VCardRequest::finished, this, [this, jt]() { + if (!jt->success()) { + return; + } + auto vcard = jt->vcard(); + if (vcard) { + QString nickname = vcard.nickName().preferred().data.value(0); + if (nickname.isEmpty()) { + nickname = vcard.fullName().preferred(); + } + if (nickname.isEmpty()) { + auto names = vcard.names(); + nickname = names.data.given.value(0); + auto middle = names.data.additional.value(0); + auto surname = names.data.surname.value(0); if (nickname.isEmpty()) { - nickname = jt->jid().bare(); + nickname = middle; + } else if (!middle.isEmpty()) { + nickname += " " + middle; + } + if (nickname.isEmpty()) { + nickname = surname; + } else if (!surname.isEmpty()) { + nickname += " " + surname; } - le_nick->setText(nickname); } - }, - false); - d->tasks->append(jt); + + if (nickname.isEmpty()) { + nickname = jt->jid().bare(); + } + le_nick->setText(nickname); + } + }); } /** diff --git a/src/alertmanager.cpp b/src/alertmanager.cpp index 5fe917454b..da98d7178f 100644 --- a/src/alertmanager.cpp +++ b/src/alertmanager.cpp @@ -12,7 +12,7 @@ * 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 . + * along with this program. If not, see . * */ diff --git a/src/alertmanager.h b/src/alertmanager.h index 58ca6e9d5c..d3e0cf32aa 100644 --- a/src/alertmanager.h +++ b/src/alertmanager.h @@ -12,7 +12,7 @@ * 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 . + * along with this program. If not, see . * */ diff --git a/src/applicationinfo.cpp b/src/applicationinfo.cpp index 16903d06ea..f60794aee9 100644 --- a/src/applicationinfo.cpp +++ b/src/applicationinfo.cpp @@ -1,13 +1,10 @@ #include "applicationinfo.h" -#include "activeprofiles.h" #ifdef HAVE_CONFIG #include "config.h" #endif #include "profiles.h" -#include "psiapplication.h" #include "systeminfo.h" -#include "translationmanager.h" #include #include @@ -86,7 +83,8 @@ QStringList ApplicationInfo::getCertificateStoreDirs() #if defined(Q_OS_LINUX) && defined(SHARE_SUFF) additionalPath, #endif - ApplicationInfo::resourcesDir() + "/certs", ApplicationInfo::homeDir(ApplicationInfo::DataLocation) + "/certs" + ApplicationInfo::resourcesDir() + "/certs", + ApplicationInfo::homeDir(ApplicationInfo::DataLocation) + "/certs" }; return dirs; } @@ -103,7 +101,10 @@ QStringList ApplicationInfo::dataDirs() #if defined(Q_OS_LINUX) && defined(SHARE_SUFF) additionalPath, #endif - ":", ".", homeDir(DataLocation), resourcesDir() + ":", + ".", + homeDir(DataLocation), + resourcesDir() }; return dirs; } @@ -120,7 +121,8 @@ QStringList ApplicationInfo::pluginDirs() #if defined(Q_OS_LINUX) && defined(SHARE_SUFF) additionalPath, #endif - ApplicationInfo::resourcesDir() + "/plugins", homeDir(ApplicationInfo::DataLocation) + "/plugins", + ApplicationInfo::resourcesDir() + "/plugins", + homeDir(ApplicationInfo::DataLocation) + "/plugins", libDir() + "/plugins" }; return dirs; @@ -146,9 +148,9 @@ QString ApplicationInfo::resourcesDir() // System routine locates resource files. We "know" that Psi.icns is // in the Resources directory. QString resourcePath; - CFBundleRef mainBundle = CFBundleGetMainBundle(); + CFBundleRef mainBundle = CFBundleGetMainBundle(); #ifdef PSI_PLUS - const char *appIconName = "application-plus.icns"; + const char *appIconName = "application-plus.icns"; #else const char *appIconName = "application.icns"; #endif @@ -347,8 +349,12 @@ QString ApplicationInfo::desktopFileBaseName() { return QLatin1String(xstr(APP_B QString ApplicationInfo::desktopFile() { QString dFile; - auto _desktopFile = QString(xstr(APP_PREFIX) "/share/applications/") + desktopFileBaseName(); - QFile f(_desktopFile); +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) + auto _desktopFile = QString(xstr(APP_PREFIX) "/share/applications/") + desktopFileBaseName(); +#else + auto _desktopFile = QString(xstr(APP_PREFIX) "/share/applications/") + desktopFileBaseName() + ".desktop"; +#endif + QFile f(_desktopFile); if (f.open(QIODevice::ReadOnly)) { dFile = QString::fromUtf8(f.readAll()); } diff --git a/src/avatars.cpp b/src/avatars.cpp index 5ef1674d61..3a6931fe51 100644 --- a/src/avatars.cpp +++ b/src/avatars.cpp @@ -34,12 +34,13 @@ #include "iris/xmpp_pubsubitem.h" #include "iris/xmpp_resource.h" #include "iris/xmpp_tasks.h" -#include "iris/xmpp_vcard.h" +#include "iris/xmpp_vcard4.h" #include "iris/xmpp_xmlcommon.h" #include "pepmanager.h" #include "pixmaputil.h" #include "psiaccount.h" #include "vcardfactory.h" +#include "xmpp/xmpp-im/xmpp_serverinfomanager.h" #include #include @@ -58,9 +59,13 @@ #define MAX_AVATAR_SIZE 96 // #define MAX_AVATAR_DISPLAY_SIZE 64 +// #define AVATAR_EDBUG 1 + //------------------------------------------------------------------------------ -static QByteArray scaleAvatar(const QByteArray &b) +namespace { + +QByteArray scaleAvatar(const QByteArray &b) { // int maxSize = (LEGOPTS.avatarsSize > MAX_AVATAR_SIZE ? MAX_AVATAR_SIZE : LEGOPTS.avatarsSize); int maxSize = AvatarFactory::maxAvatarSize(); @@ -80,6 +85,37 @@ static QByteArray scaleAvatar(const QByteArray &b) } } +VCardFactory::Flags flags2AvatarFlags(AvatarFactory::Flags flags) +{ + VCardFactory::Flags ret; + if (flags & AvatarFactory::MucRoom) { + ret |= VCardFactory::MucRoom; + } + if (flags & AvatarFactory::MucUser) { + ret |= VCardFactory::MucUser; + } + if (flags & AvatarFactory::Cache) { + ret |= VCardFactory::Cache; + } + return ret; +} + +inline QPixmap ensureSquareAvatar(const QPixmap &original) +{ + if (original.isNull() || original.width() == original.height()) + return original; + + int size = qMax(original.width(), original.height()); + QPixmap square = PixmapUtil::createTransparentPixmap(size, size); + + QPainter p(&square); + p.drawPixmap((size - original.width()) / 2, (size - original.height()) / 2, original); + + return square; +} + +} + //------------------------------------------------------------------------------ //------------------------------------------------------------------------------ @@ -101,7 +137,12 @@ class AvatarCache : public FileCache { public: enum IconType { NoneType, AvatarType, VCardType, AvatarFromVCardType, CustomType }; - enum OpResult { NoData, NotChanged, Changed, UserUpdateRequired }; + enum OpResult { + NoData, /* data for gived hash/jid is not in the cache, but it's required */ + NotChanged, /* data with same hash is already in the cache. or there is nothing to remove */ + Changed, /* icon changed in the cache but user update is not needed. another is active */ + UserUpdateRequired /* active icon has changed in the cache and we need to inform the user */ + }; // design of the structure is matter of priority, not possible ways of avatars receiving. struct JidIcons { @@ -120,6 +161,250 @@ class AvatarCache : public FileCache { return _instance; } + void rememberHash(const Jid &j, const QByteArray &hash) { jid2hash[j] = hash; } + void removeHash(const Jid &j) { jid2hash.remove(j); } + + void itemPublished(PsiAccount *pa, const Jid &jid, const QString &n, const PubSubItem &item) + { + QString jidFull = jid.full(); // it's always bare + AvatarCache::OpResult result = AvatarCache::Changed; + + if (n == PEP_AVATAR_DATA_NS) { + if (item.payload().tagName() == PEP_AVATAR_DATA_TN) { + auto hash = QByteArray::fromHex(item.id().toLatin1()); + if (hash.size() < 20) + return; // doesn't look like sha1 hash. just ignore it + // try append user first. since data may be unexpected and we want to save some cpu cycles. +#ifdef AVATAR_EDBUG + qDebug() << "append user " << jidFull << AvatarCache::AvatarType; +#endif + result = appendUser(hash, AvatarCache::AvatarType, jidFull); + if (result == AvatarCache::NoData) { + QByteArray ba = QByteArray::fromBase64(item.payload().text().toLatin1()); + if (!ba.isEmpty()) { + result = setIcon(AvatarCache::AvatarType, jidFull, ba, hash); + } + } + } else { + qWarning("avatars.cpp: Unexpected item payload"); + } + } else if (n == PEP_AVATAR_METADATA_NS && item.payload().tagName() == QLatin1String(PEP_AVATAR_METADATA_TN)) { + auto info = item.payload().firstChildElement(QLatin1String("info")); + if (info.isNull()) { +#ifdef AVATAR_EDBUG + qDebug() << "removeIcon AvatarType " << jidFull << AvatarCache::AvatarType; +#endif + result = AvatarCache::instance()->removeIcon(AvatarCache::AvatarType, jidFull); + auto it = jid2hash.find(jid); + if (it != jid2hash.end()) { + auto vcard_hash = it.value(); + jid2hash.erase(it); + VCardFactory::instance()->ensureVCardPhotoUpdated(pa, jid, {}, vcard_hash); + } + } else { + auto id = item.id().toLatin1(); + if (id == "current") { + return; // should we handle singletons? probably previous versions of the xep + } + QByteArray hash = QByteArray::fromHex(id); + if (hash.size() < 20) { +#ifdef AVATAR_EDBUG + qDebug() << "not sha1"; +#endif + return; // doesn't look like sha1 hash. just ignore it + } + + auto supportedMime = QImageReader::supportedMimeTypes(); + for (; !info.isNull(); info = info.nextSiblingElement(QLatin1String("info"))) { + if (!supportedMime.contains(info.attribute(QLatin1String("type")).toLower().toLatin1())) { + continue; + } + if (!info.attribute(QLatin1String("url")).isEmpty()) { + continue; // web avatars are not currently supported. TODO but their support is highly expected + } + if (info.attribute(QLatin1String("id")) != item.id()) { + continue; // that's something totally unexpected + } +#ifdef AVATAR_EDBUG + qDebug() << "appendUser AvatarType " << jidFull; +#endif + // found in-band png (by xep84 hash is for png) avatar. So we can make request + result = appendUser(hash, AvatarCache::AvatarType, jidFull); + if (result == AvatarCache::NoData) { + pa->pepManager()->get(jid, PEP_AVATAR_DATA_NS, item.id()); + return; + } + break; + } + } + } + + if (result == UserUpdateRequired) { +#ifdef AVATAR_EDBUG + qDebug() << "remove from iconset and emit avatarChanged on itemPublished" << jidFull; +#endif + iconset_->removeIcon(QString(QLatin1String("avatars/%1")).arg(jidFull)); + emit avatarChanged(jidFull); + } + } + + OpResult ensureVCardUpdated(const Jid &jid, const QByteArray &hash, AvatarFactory::Flags flags) + { + QString fullJid = (flags & AvatarFactory::MucUser) ? jid.full() : jid.bare(); + OpResult result; + + if (hash.isEmpty()) { // photo removal + result = AvatarCache::instance()->removeIcon(AvatarCache::VCardType, fullJid); + } else { + result = appendUser(hash, AvatarCache::VCardType, fullJid); + } + if (result == AvatarCache::UserUpdateRequired) { +#ifdef AVATAR_EDBUG + qDebug() << "remove from iconset and emit avatarChanged. ensureVCardUpdated hash=" << hash.toHex() + << fullJid; +#endif + iconset_->removeIcon(QString(QLatin1String("avatars/%1")).arg(fullJid)); + emit avatarChanged(fullJid); + } + + return result; + } + + void importManualAvatar(const Jid &j, const QString &fileName) + { + QFile f(fileName); + if (!(f.open(QIODevice::ReadOnly) + && AvatarCache::instance()->setIcon(AvatarCache::CustomType, j.bare(), f.readAll()))) { + qWarning("Failed to set manual avatar"); + } +#ifdef AVATAR_EDBUG + qDebug() << "remove from iconset and emit avatarChanged. importManualAvatar" << j.bare(); +#endif + iconset_->removeIcon(QString(QLatin1String("avatars/%1")).arg(j.bare())); + emit avatarChanged(j); + } + + void removeManualAvatar(const Jid &j) + { + if (AvatarCache::instance()->removeIcon(AvatarCache::CustomType, j.bare()) == AvatarCache::UserUpdateRequired) { +#ifdef AVATAR_EDBUG + qDebug() << "remove from iconset and emit avatarChanged. removeManualAvatar" << j.bare(); +#endif + iconset_->removeIcon(QString(QLatin1String("avatars/%1")).arg(j.bare())); + emit avatarChanged(j); + } + } + + QPixmap getAvatar(const Jid &_jid) + { + QString bareJid = _jid.bare(); + QString iconName = QString("avatars/%1").arg(bareJid); + auto iconp = iconset_->icon(iconName); + if (iconp) { +#ifdef AVATAR_EDBUG + qDebug() << "return icons" << iconName << "from iconset for jid" << bareJid; +#endif + return iconp->pixmap(); + } + + auto icons = this->icons(bareJid); + QImage img; + if (icons.customAvatar) { + img = QImage::fromData(icons.customAvatar->data()); + if (img.isNull()) { + removeIcon(AvatarCache::CustomType, bareJid); + } + } + if (img.isNull() && icons.avatar) { + img = QImage::fromData(icons.avatar->data()); + if (img.isNull()) { + removeIcon(AvatarCache::AvatarType, bareJid); + } + } + + if (img.isNull()) { +#ifdef AVATAR_EDBUG + qDebug() << "return from vcardfactory for jid " << _jid.full(); +#endif + auto vcard = VCardFactory::instance()->vcard(_jid); + if (vcard.isNull() || QByteArray(vcard.photo()).isNull()) { + return QPixmap(); + } + QByteArray data = vcard.photo(); + if (setIcon(AvatarCache::VCardType, bareJid, data) != AvatarCache::NoData) { + auto item = this->icons(bareJid).avatar; + if (!item) + qWarning("Avatars cache is damaged"); + else + img = QImage::fromData(item->data()); // from scaled avatar + } + } + + if (img.isNull()) { + return QPixmap(); + } + + QPixmap pm = QPixmap::fromImage(std::move(img)); + pm = ensureSquareAvatar(pm); + + // Update iconset + PsiIcon icon; + icon.setImpix(pm); +#ifdef AVATAR_EDBUG + qDebug() << "setting icon to iconset " << iconName << "=" << pm; +#endif + iconset_->setIcon(iconName, icon); + + return pm; + } + + QPixmap getMucAvatar(const Jid &_jid) + { + QString fullJid = _jid.full(); + + QString iconName = QString("avatars/%1").arg(fullJid); + auto iconp = iconset_->icon(iconName); + if (iconp) { + return iconp->pixmap(); + } + + auto icons = AvatarCache::instance()->icons(fullJid); + QByteArray data; + if (!icons.avatar) { + auto vcard = VCardFactory::instance()->mucVcard(_jid); + if (vcard.isNull() || QByteArray(vcard.photo()).isNull()) { + return QPixmap(); + } + data = vcard.photo(); + AvatarCache::instance()->setIcon(AvatarCache::VCardType, _jid.full(), data); + icons = AvatarCache::instance()->icons(fullJid); // should return scaled copy + } + + if (icons.avatar) { + data = icons.avatar->data(); + } + + // for mucs icons.avatar is always made of vcard and anything else is not supported. at least for now. + QImage img(QImage::fromData(data)); + + if (img.isNull()) { + return QPixmap(); + } + + QPixmap pm = QPixmap::fromImage(std::move(img)); + pm = ensureSquareAvatar(pm); + + // Update iconset + PsiIcon icon; + icon.setImpix(pm); +#ifdef AVATAR_EDBUG + qDebug() << "setting icon " << QString("avatars/%1").arg(fullJid) << "=" << pm; +#endif + iconset_->setIcon(QString("avatars/%1").arg(fullJid), icon); // FIXME do we ever release it? + + return pm; + } + JidIcons icons(const QString &jid) { auto it = jidToIcons.constFind(jid); @@ -227,7 +512,7 @@ class AvatarCache : public FileCache { } if (!canDel(*it, iconType)) { - return NoData; // or NotChanged.. no suitable icon to delete + return NotChanged; // no suitable icon to delete } FileCacheItem *oldActiveIcon = activeAvatarIcon(*it); @@ -259,44 +544,18 @@ class AvatarCache : public FileCache { return oldActiveIcon == newActiveIcon ? Changed : UserUpdateRequired; } - OpResult appendUser(const QByteArray &hash, IconType iconType, const QString &jid) - { - auto item = get(XMPP::Hash(XMPP::Hash::Sha1, hash)); - if (!item) { - return NoData; - } - - auto &icons = jidToIcons[jid]; - if (!canAdd(icons, iconType)) { // for new icons-item we definitely can - return NotChanged; - } - - FileCacheItem *oldActiveIcon = activeAvatarIcon(icons); - FileCacheItem *prevIcon = setIconItem(icons, item, iconType); - if (prevIcon == item) { - return NotChanged; // not changed - } - appendUser(item, iconType, jid); - if (prevIcon) { - removeUser(prevIcon, iconType, jid); - } - if (icons.avatarFromVCard && iconType == VCardType) { - icons.avatar = nullptr; // we have to regenerate it from new vcard - } - - FileCacheItem *newActiveIcon - = ensureHasAvatar(icons, jid); // for example we have added avatar which was shared with smb. - return oldActiveIcon == newActiveIcon ? Changed : UserUpdateRequired; - } +signals: + void avatarChanged(const Jid &); protected: // all the active items are protected (undeletabe) during session. so it's fine to not update user of changes. - // from other side we call removeItem of parent class and even if called this function, by design it's only if no - // users. + // from other side we call removeItem of parent class and even if called this function, by design it's only if + // no users. void removeItem(FileCacheItem *item, bool needSync) { QStringList jids = item->metadata().value(QLatin1String("jids")).toStringList(); - // weh have user of this icon. And this users can use other icons as well. so we have to properly update them + // weh have user of this icon. And this users can use other icons as well. so we have to properly update + // them for (const QString &j : jids) { IconType itype = extractIconType(j); QString jid = extractIconJid(j); @@ -315,7 +574,48 @@ class AvatarCache : public FileCache { } private: - AvatarCache() : FileCache(AvatarFactory::getCacheDir()) { updateJids(); } + AvatarCache() : FileCache(AvatarFactory::getCacheDir()) + { + // Register iconset + iconset_ = new Iconset(); + iconset_->addToFactory(); // the factory will own the iconset + updateJids(); + + connect(VCardFactory::instance(), &VCardFactory::vcardChanged, this, + [this](const Jid &j, VCardFactory::Flags flags) { + QByteArray ba; + QString fullJid; + auto vcard = VCardFactory::instance()->vcard(j, flags); + if (flags & VCardFactory::MucUser) { + fullJid = j.full(); + } else { + fullJid = j.bare(); + } + if (!vcard) { + return; // wtf?? + } + ba = vcard.photo(); + OpResult result; + if (ba.isEmpty()) { +#ifdef AVATAR_EDBUG + qDebug() << "removeIcon VCardType from cache for " << fullJid; +#endif + result = AvatarCache::instance()->removeIcon(AvatarCache::VCardType, fullJid); + } else { +#ifdef AVATAR_EDBUG + qDebug() << "setIcon VCardType to cache for " << fullJid; +#endif + result = AvatarCache::instance()->setIcon(AvatarCache::VCardType, fullJid, ba); + } + if (result == UserUpdateRequired) { +#ifdef AVATAR_EDBUG + qDebug() << "removing icon from iconset:" << QString(QLatin1String("avatars/%1")).arg(fullJid); +#endif + iconset_->removeIcon(QString(QLatin1String("avatars/%1")).arg(fullJid)); + emit avatarChanged(j); + } + }); + } bool areIconsEmpty(const JidIcons &icons) const { return !icons.avatar && !icons.vcard && !icons.customAvatar; } @@ -415,13 +715,43 @@ class AvatarCache : public FileCache { return ret; } - void appendUser(FileCacheItem *item, IconType iconType, const QString &jid) + OpResult appendUser(const QByteArray &hash, IconType iconType, const QString &jid) { - QVariantMap md = item->metadata(); - QStringList jids = md.value(QLatin1String("jids")).toStringList(); - jids.append(typedJid(iconType, jid)); - md.insert(QLatin1String("jids"), jids); - item->setMetadata(md); + auto item = get(XMPP::Hash(XMPP::Hash::Sha1, hash)); + if (!item) { + return NoData; + } + + auto &icons = jidToIcons[jid]; + if (!canAdd(icons, iconType)) { // for new icons-item we definitely can + return NotChanged; + } + + FileCacheItem *oldActiveIcon = activeAvatarIcon(icons); + FileCacheItem *prevIcon = setIconItem(icons, item, iconType); + if (prevIcon == item) { + return NotChanged; // not changed + } + { + QVariantMap md = item->metadata(); + QStringList jids = md.value(QLatin1String("jids")).toStringList(); + jids.append(typedJid(iconType, jid)); + md.insert(QLatin1String("jids"), jids); + item->setMetadata(md); + lazySync(); + } + + if (prevIcon) { + removeUser(prevIcon, iconType, jid); + } + if (icons.avatarFromVCard && iconType == VCardType) { + icons.avatar = nullptr; // we have to regenerate it from new vcard + } + + FileCacheItem *newActiveIcon + = ensureHasAvatar(icons, jid); // for example we have added avatar which was shared with smb. + + return oldActiveIcon == newActiveIcon ? Changed : UserUpdateRequired; } void removeUser(FileCacheItem *item, IconType iconType, const QString &jid) @@ -439,6 +769,7 @@ class AvatarCache : public FileCache { } else { md.insert(QLatin1String("jids"), jids); item->setMetadata(md); + lazySync(); } } @@ -520,11 +851,14 @@ class AvatarCache : public FileCache { if (jidsChanged) { md.insert(QLatin1String("jids"), jids); it.value()->setMetadata(md); + lazySync(); } } } QHash jidToIcons; + QHash jid2hash; + Iconset *iconset_; static AvatarCache *_instance; }; @@ -541,55 +875,27 @@ class AvatarFactory::Private { QString selfAvatarHash_; PsiAccount *pa_; - Iconset iconset_; - - QQueue> vcardReqQueue_; - QTimer vcardReqTimer_; }; AvatarFactory::AvatarFactory(PsiAccount *pa) : d(new Private) { d->pa_ = pa; - // Register iconset - d->iconset_.addToFactory(); - - d->vcardReqTimer_.setSingleShot(false); - d->vcardReqTimer_.setInterval(VcardReqInterval); - QObject::connect(&d->vcardReqTimer_, &QTimer::timeout, this, [this]() { - Jid j; - QByteArray hash; - bool isMuc; - std::tie(j, hash, isMuc) = d->vcardReqQueue_.dequeue(); - if (d->vcardReqQueue_.isEmpty()) { - d->vcardReqTimer_.stop(); - } - if (!d->pa_->isConnected()) - return; - VCardFactory::instance()->getVCard( - j, d->pa_->client()->rootTask(), this, - [this, hash]() { - auto task = dynamic_cast(sender()); - if (task->success() && !task->vcard().isNull()) { - QByteArray ba = task->vcard().photo(); - if (!ba.isNull()) { - QString fullJid = task->jid().full(); // jids for regular contacts are already without resource - if (AvatarCache::instance()->setIcon(AvatarCache::VCardType, fullJid, ba, hash) - == AvatarCache::UserUpdateRequired) { - d->iconset_.removeIcon(QString(QLatin1String("avatars/%1")).arg(task->jid().full())); - emit avatarChanged(task->jid()); - } - } - } - }, - !isMuc, isMuc, false); - }); // Connect signals - connect(VCardFactory::instance(), &VCardFactory::vcardPhotoAvailable, this, &AvatarFactory::vcardUpdated); - connect(d->pa_->client(), &XMPP::Client::resourceAvailable, this, &AvatarFactory::resourceAvailable); + connect(AvatarCache::instance(), &AvatarCache::avatarChanged, this, &AvatarFactory::avatarChanged); + + connect(d->pa_->client(), &XMPP::Client::resourceAvailable, this, [this](const Jid &jid, const Resource &r) { + if (jid.compare(d->pa_->jid(), false)) { + return; // to avoid endless recursion with vcard update + } + statusUpdate(jid.withResource(QString()), r.status()); + }); // PEP - connect(d->pa_->pepManager(), &PEPManager::itemPublished, this, &AvatarFactory::itemPublished); + connect(d->pa_->pepManager(), &PEPManager::itemPublished, this, + [this](const Jid &jid, const QString &n, const PubSubItem &item) { + AvatarCache::instance()->itemPublished(d->pa_, jid, n, item); + }); connect(d->pa_->pepManager(), &PEPManager::publish_success, this, &AvatarFactory::publish_success); } @@ -597,73 +903,8 @@ AvatarFactory::~AvatarFactory() { delete d; } PsiAccount *AvatarFactory::account() const { return d->pa_; } -inline static QPixmap ensureSquareAvatar(const QPixmap &original) -{ - if (original.isNull() || original.width() == original.height()) - return original; - - int size = qMax(original.width(), original.height()); - QPixmap square = PixmapUtil::createTransparentPixmap(size, size); - - QPainter p(&square); - p.drawPixmap((size - original.width()) / 2, (size - original.height()) / 2, original); - - return square; -} - -QPixmap AvatarFactory::getAvatar(const Jid &_jid) -{ - QString bareJid = _jid.bare(); - QString iconName = QString("avatars/%1").arg(bareJid); - auto iconp = d->iconset_.icon(iconName); - if (iconp) { - return iconp->pixmap(); - } - - auto icons = AvatarCache::instance()->icons(bareJid); - QImage img; - if (icons.customAvatar) { - img = QImage::fromData(icons.customAvatar->data()); - if (img.isNull()) { - AvatarCache::instance()->removeIcon(AvatarCache::CustomType, bareJid); - } - } - if (img.isNull() && icons.avatar) { - img = QImage::fromData(icons.avatar->data()); - if (img.isNull()) { - AvatarCache::instance()->removeIcon(AvatarCache::AvatarType, bareJid); - } - } - - if (img.isNull()) { - auto vcard = VCardFactory::instance()->vcard(_jid); - if (vcard.isNull() || vcard.photo().isNull()) { - return QPixmap(); - } - QByteArray data = vcard.photo(); - if (AvatarCache::instance()->setIcon(AvatarCache::VCardType, bareJid, data) != AvatarCache::NoData) { - auto item = AvatarCache::instance()->icons(bareJid).avatar; - if (!item) - qWarning("Avatars cache is damaged"); - else - img = QImage::fromData(item->data()); // from scaled avatar - } - } - - if (img.isNull()) { - return QPixmap(); - } - - QPixmap pm = QPixmap::fromImage(std::move(img)); - pm = ensureSquareAvatar(pm); - - // Update iconset - PsiIcon icon; - icon.setImpix(pm); - d->iconset_.setIcon(iconName, icon); - - return pm; -} +QPixmap AvatarFactory::getAvatar(const Jid &_jid) { return AvatarCache::instance()->getAvatar(_jid); } +QPixmap AvatarFactory::getMucAvatar(const Jid &_jid) { return AvatarCache::instance()->getMucAvatar(_jid); } #if 0 QPixmap AvatarFactory::getAvatarByHash(const QString &hash) @@ -697,14 +938,9 @@ AvatarFactory::UserHashes AvatarFactory::userHashes(const Jid &jid) const auto icons = AvatarCache::instance()->icons(jid.full()); if (!icons.vcard) { // hm try to get from vcard factory then // we don't call this method often. so it's fine to query vcard factory every time. - bool isMuc = !jid.resource().isEmpty(); - VCard vcard; - if (isMuc) { - vcard = VCardFactory::instance()->mucVcard(jid); - } else { - vcard = VCardFactory::instance()->vcard(jid); - } - if (!vcard.isNull() && !vcard.photo().isNull()) { + auto flags = jid.resource().isEmpty() ? VCardFactory::Flags {} : VCardFactory::MucUser; + auto vcard = VCardFactory::instance()->vcard(jid, flags); + if (!vcard.isNull() && !QByteArray(vcard.photo()).isNull()) { if (AvatarCache::instance()->setIcon(AvatarCache::VCardType, jid.full(), vcard.photo()) != AvatarCache::NoData) { icons = AvatarCache::instance()->icons(jid.full()); @@ -720,133 +956,76 @@ AvatarFactory::UserHashes AvatarFactory::userHashes(const Jid &jid) const return ret; } -QPixmap AvatarFactory::getMucAvatar(const Jid &_jid) -{ - QString fullJid = _jid.full(); - - QString iconName = QString("avatars/%1").arg(fullJid); - auto iconp = d->iconset_.icon(iconName); - if (iconp) { - return iconp->pixmap(); - } - - auto icons = AvatarCache::instance()->icons(fullJid); - QByteArray data; - if (!icons.avatar) { - auto vcard = VCardFactory::instance()->mucVcard(_jid); - if (vcard.isNull() || vcard.photo().isNull()) { - return QPixmap(); - } - data = vcard.photo(); - AvatarCache::instance()->setIcon(AvatarCache::VCardType, _jid.full(), data); - icons = AvatarCache::instance()->icons(fullJid); // should return scaled copy - } - - if (icons.avatar) { - data = icons.avatar->data(); - } - - // for mucs icons.avatar is always made of vcard and anything else is not supported. at least for now. - QImage img(QImage::fromData(data)); - - if (img.isNull()) { - return QPixmap(); - } - - QPixmap pm = QPixmap::fromImage(std::move(img)); - pm = ensureSquareAvatar(pm); - - // Update iconset - PsiIcon icon; - icon.setImpix(pm); - d->iconset_.setIcon(QString("avatars/%1").arg(fullJid), icon); // FIXME do we ever release it? - - return pm; -} - void AvatarFactory::setSelfAvatar(const QString &fileName) { if (!fileName.isEmpty()) { - QFile avatar_file(fileName); - if (!avatar_file.open(QIODevice::ReadOnly)) - return; - - QByteArray avatar_data = scaleAvatar(avatar_file.readAll()); - QImage avatar_image = QImage::fromData(avatar_data); - if (!avatar_image.isNull()) { - // Publish data - QDomDocument *doc = account()->client()->doc(); - QString hash = QString::fromLatin1(QCryptographicHash::hash(avatar_data, QCryptographicHash::Sha1).toHex()); - QDomElement el = doc->createElementNS(PEP_AVATAR_DATA_NS, PEP_AVATAR_DATA_TN); - el.appendChild(doc->createTextNode(QString::fromLatin1(avatar_data.toBase64()))); - d->selfAvatarData_ = avatar_data; - d->selfAvatarHash_ = hash; - account()->pepManager()->publish(PEP_AVATAR_DATA_NS, PubSubItem(hash, el)); - } + setSelfAvatar(QImage(fileName)); } else { - account()->pepManager()->disable(PEP_AVATAR_METADATA_TN, PEP_AVATAR_METADATA_NS, "current"); + account()->pepManager()->disable(PEP_AVATAR_METADATA_TN, PEP_AVATAR_METADATA_NS, {}); } } -void AvatarFactory::importManualAvatar(const Jid &j, const QString &fileName) +void AvatarFactory::setSelfAvatar(const QImage &image) { - QFile f(fileName); - if (!(f.open(QIODevice::ReadOnly) - && AvatarCache::instance()->setIcon(AvatarCache::CustomType, j.bare(), f.readAll()))) { - qWarning("Failed to set manual avatar"); + if (image.isNull()) { + account()->pepManager()->disable(PEP_AVATAR_METADATA_TN, PEP_AVATAR_METADATA_NS, {}); + } else { + // Publish data + + QByteArray data; + QBuffer buffer(&data); + buffer.open(QIODevice::WriteOnly); + auto maxSz = maxAvatarSize(); + image.scaled(image.size().boundedTo(QSize(maxSz, maxSz)), Qt::KeepAspectRatio, Qt::SmoothTransformation) + .save(&buffer, "PNG", 0); + auto hash = QCryptographicHash::hash(data, QCryptographicHash::Sha1); + QString id = QString::fromLatin1(hash.toHex()); + + QDomDocument *doc = account()->client()->doc(); + QDomElement el = doc->createElementNS(PEP_AVATAR_DATA_NS, PEP_AVATAR_DATA_TN); + el.appendChild(doc->createTextNode(QString::fromLatin1(data.toBase64()))); + d->selfAvatarData_ = data; + d->selfAvatarHash_ = id; + account()->pepManager()->publish(PEP_AVATAR_DATA_NS, PubSubItem(id, el), PEPManager::Access::Open); } - d->iconset_.removeIcon(QString(QLatin1String("avatars/%1")).arg(j.bare())); - emit avatarChanged(j); } -void AvatarFactory::removeManualAvatar(const Jid &j) +void AvatarFactory::importManualAvatar(const Jid &j, const QString &fileName) { - if (AvatarCache::instance()->removeIcon(AvatarCache::CustomType, j.bare()) == AvatarCache::UserUpdateRequired) { - d->iconset_.removeIcon(QString(QLatin1String("avatars/%1")).arg(j.bare())); - emit avatarChanged(j); - } + AvatarCache::instance()->importManualAvatar(j, fileName); } +void AvatarFactory::removeManualAvatar(const Jid &j) { AvatarCache::instance()->removeManualAvatar(j); } + bool AvatarFactory::hasManualAvatar(const Jid &j) { return bool(AvatarCache::instance()->icons(j.bare()).customAvatar); } -void AvatarFactory::resourceAvailable(const Jid &jid, const Resource &r) +void AvatarFactory::statusUpdate(const Jid &jid, const XMPP::Status &status, Flags flags) { - statusUpdate(jid.withResource(QString()), r.status()); + if (status.type() == Status::Offline) { + AvatarCache::instance()->removeHash((flags & MucUser) ? jid : jid.withResource({})); + } else { + if (status.photoHash()) { // even if it's empty. user adretises something. + auto hash = *status.photoHash(); + ensureVCardUpdated(jid, hash, flags); + } + } } -void AvatarFactory::newMucItem(const Jid &fullJid, const Status &s) { statusUpdate(fullJid, s); } - -void AvatarFactory::statusUpdate(const Jid &jid, const XMPP::Status &status) +void AvatarFactory::ensureVCardUpdated(const Jid &jid, const QByteArray &hash, Flags flags) { - // MAJOR ASSUMPTION: jid has resource - it's muc. Do we ever need resources for anything else? - bool isMuc = !jid.resource().isEmpty(); - - if (status.hasPhotoHash()) { // even if it's empty. user adretises something. - auto hash = status.photoHash(); - QString fullJid - = jid.full(); // it's not muc. so just bare jids. probably something like XEP-0316 may break this rule + auto xj = jid; + if (!(flags & MucUser)) { + xj = jid.withResource({}); + } + auto result = AvatarCache::instance()->ensureVCardUpdated(xj, hash, flags); - if (hash.isEmpty()) { // photo removal - if (AvatarCache::instance()->removeIcon(AvatarCache::VCardType, fullJid) - == AvatarCache::UserUpdateRequired) { - d->iconset_.removeIcon(QString(QLatin1String("avatars/%1")).arg(fullJid)); - emit avatarChanged(jid); - } - } else { - auto result = AvatarCache::instance()->appendUser(hash, AvatarCache::VCardType, fullJid); - if (result == AvatarCache::UserUpdateRequired) { - d->iconset_.removeIcon(QString(QLatin1String("avatars/%1")).arg(fullJid)); - emit avatarChanged(jid); - } else if (result == AvatarCache::NoData) { - d->vcardReqQueue_.enqueue(std::tuple { jid, hash, isMuc }); - if (!d->vcardReqTimer_.isActive()) { - d->vcardReqTimer_.start(); - } - } - } + if (result == AvatarCache::NoData) { + VCardFactory::instance()->ensureVCardPhotoUpdated(d->pa_, xj, flags2AvatarFlags(flags), hash); + } else { + AvatarCache::instance()->rememberHash(xj, hash); } } @@ -898,98 +1077,6 @@ QPixmap AvatarFactory::roundedAvatar(const QPixmap &pix, int rad, int avSize) return avatar_icon; } -void AvatarFactory::vcardUpdated(const Jid &j, bool isMuc) -{ - QByteArray ba; - QString fullJid; - XMPP::VCard vcard; - if (isMuc) { - vcard = VCardFactory::instance()->mucVcard(j); - fullJid = j.full(); - } else { - vcard = VCardFactory::instance()->vcard(j); - fullJid = j.bare(); - } - if (!vcard) { - return; // wtf?? - } - ba = vcard.photo(); - if (!ba.isEmpty()) { - if (AvatarCache::instance()->setIcon(AvatarCache::VCardType, fullJid, ba) == AvatarCache::UserUpdateRequired) { - d->iconset_.removeIcon(QString(QLatin1String("avatars/%1")).arg(fullJid)); - emit avatarChanged(j); - } - } -} - -void AvatarFactory::itemPublished(const Jid &jid, const QString &n, const PubSubItem &item) -{ - AvatarCache *cache = AvatarCache::instance(); - QString jidFull = jid.full(); // it's always bare - AvatarCache::OpResult result = AvatarCache::Changed; - - if (n == PEP_AVATAR_DATA_NS) { - if (item.payload().tagName() == PEP_AVATAR_DATA_TN) { - auto hash = QByteArray::fromHex(item.id().toLatin1()); - if (hash.size() < 20) - return; // doesn't look like sha1 hash. just ignore it - // try append user first. since data may be unexpected and we want to save some cpu cycles. - result = cache->appendUser(hash, AvatarCache::AvatarType, jidFull); - if (result == AvatarCache::NoData) { - QByteArray ba = QByteArray::fromBase64(item.payload().text().toLatin1()); - if (!ba.isEmpty()) { - result = cache->setIcon(AvatarCache::AvatarType, jidFull, ba, hash); - } - } - } else { - qWarning("avatars.cpp: Unexpected item payload"); - } - } else if (n == PEP_AVATAR_METADATA_NS) { - auto id = item.id().toLatin1(); - bool isCurrent = id == "current"; - QByteArray hash; - if (!isCurrent) { - hash = QByteArray::fromHex(id); - if (hash.size() < 20) - return; // doesn't look like sha1 hash. just ignore it - } - - if (item.payload().tagName() == QLatin1String(PEP_AVATAR_METADATA_TN) - && item.payload().firstChildElement().isNull()) { - // user wants to stop publishing avatar - // previously we used "stop" element. now specs are changed - result = AvatarCache::instance()->removeIcon(AvatarCache::AvatarType, jidFull); - } else { - auto mimes = QImageReader::supportedMimeTypes(); - - for (QDomElement e = item.payload().firstChildElement(QLatin1String("info")); !e.isNull(); - e = e.nextSiblingElement(QLatin1String("info"))) { - if (!mimes.contains(e.attribute(QLatin1String("type")).toLower().toLatin1())) { - continue; // unsupported mime - } - if (!e.attribute(QLatin1String("url")).isEmpty()) { - continue; // web avatars are not currently supported. TODO but their support is highly expected - } - if (e.attribute(QLatin1String("id")) != item.id()) { - continue; // that's something totally unexpected - } - // found in-band png (by xep84 hash is for png) avatar. So we can make request - result = cache->appendUser(hash, AvatarCache::AvatarType, jidFull); - if (result == AvatarCache::NoData) { - d->pa_->pepManager()->get(jid, PEP_AVATAR_DATA_NS, item.id()); - return; - } - break; - } - } - } - - if (result == AvatarCache::UserUpdateRequired) { - d->iconset_.removeIcon(QString(QLatin1String("avatars/%1")).arg(jidFull)); - emit avatarChanged(jid); - } -} - void AvatarFactory::publish_success(const QString &n, const PubSubItem &item) { if (n == PEP_AVATAR_DATA_NS && item.id() == d->selfAvatarHash_) { @@ -999,17 +1086,22 @@ void AvatarFactory::publish_success(const QString &n, const PubSubItem &item) QDomElement meta_el = doc->createElementNS(PEP_AVATAR_METADATA_NS, PEP_AVATAR_METADATA_TN); QDomElement info_el = doc->createElement("info"); info_el.setAttribute("id", d->selfAvatarHash_); -#if QT_VERSION >= QT_VERSION_CHECK(5, 11, 0) info_el.setAttribute("bytes", avatar_image.sizeInBytes()); -#else - info_el.setAttribute("bytes", avatar_image.byteCount()); -#endif info_el.setAttribute("height", avatar_image.height()); info_el.setAttribute("width", avatar_image.width()); info_el.setAttribute("type", image2type(d->selfAvatarData_)); meta_el.appendChild(info_el); - account()->pepManager()->publish(PEP_AVATAR_METADATA_NS, PubSubItem(d->selfAvatarHash_, meta_el)); + account()->pepManager()->publish(PEP_AVATAR_METADATA_NS, PubSubItem(d->selfAvatarHash_, meta_el), + PEPManager::Access::Open); + if (account()->client()->serverInfoManager()->accountFeatures().hasAvatarConversion()) { + VCardFactory::instance()->setPhoto(account()->jid(), d->selfAvatarData_, VCardFactory::Silent); + } d->selfAvatarData_.clear(); // we don't need it anymore + } else if (n == PEP_AVATAR_METADATA_NS) { + bool removed = item.payload().firstChildElement("metadata").firstChildElement("info").isNull(); + if (account()->client()->serverInfoManager()->accountFeatures().hasAvatarConversion() && removed) { + VCardFactory::instance()->deletePhoto(account()->jid(), VCardFactory::Silent); + } } } diff --git a/src/avatars.h b/src/avatars.h index 17c01c419a..87710f6043 100644 --- a/src/avatars.h +++ b/src/avatars.h @@ -41,6 +41,8 @@ class VCardMucAvatar; class VCardStaticAvatar; namespace XMPP { + +class DiscoItem; class Jid; class Resource; class PubSubItem; @@ -53,8 +55,6 @@ using namespace XMPP; class AvatarFactory : public QObject { Q_OBJECT - static const int VcardReqInterval = 500; // query vcard avatars once per half second - public: struct UserHashes { QByteArray avatar; // current active avatar @@ -66,6 +66,9 @@ class AvatarFactory : public QObject { QString metaType; }; + enum Flag { MucRoom = 0x1, MucUser = 0x2, Cache = 0x4 }; + Q_DECLARE_FLAGS(Flags, Flag); + AvatarFactory(PsiAccount *pa); ~AvatarFactory(); @@ -75,33 +78,31 @@ class AvatarFactory : public QObject { UserHashes userHashes(const Jid &jid) const; PsiAccount *account() const; void setSelfAvatar(const QString &fileName); + void setSelfAvatar(const QImage &image); void importManualAvatar(const Jid &j, const QString &fileName); void removeManualAvatar(const Jid &j); bool hasManualAvatar(const Jid &j); - void newMucItem(const Jid &fullJid, const Status &s); QPixmap getMucAvatar(const Jid &jid); static QString getCacheDir(); static int maxAvatarSize(); static QPixmap roundedAvatar(const QPixmap &pix, int rad, int avatarSize); - void statusUpdate(const Jid &jid, const XMPP::Status &status); + void statusUpdate(const Jid &jid, const XMPP::Status &status, Flags flags = {}); + void ensureVCardUpdated(const Jid &jid, const QByteArray &hash, Flags flags = {}); signals: void avatarChanged(const XMPP::Jid &); protected slots: - void itemPublished(const XMPP::Jid &, const QString &, const PubSubItem &); void publish_success(const QString &, const XMPP::PubSubItem &); - void resourceAvailable(const XMPP::Jid &, const XMPP::Resource &); - -private slots: - void vcardUpdated(const XMPP::Jid &, bool isMuc); private: class Private; Private *d; }; +Q_DECLARE_OPERATORS_FOR_FLAGS(AvatarFactory::Flags) + #endif // AVATARS_H diff --git a/src/bookmarkmanage.ui b/src/bookmarkmanage.ui index 2ffb25bd86..4f437cb04b 100644 --- a/src/bookmarkmanage.ui +++ b/src/bookmarkmanage.ui @@ -7,7 +7,7 @@ 0 0 539 - 257 + 289 @@ -19,14 +19,14 @@ - QAbstractItemView::InternalMove + QAbstractItemView::DragDropMode::InternalMove - QDialogButtonBox::NoButton + QDialogButtonBox::StandardButton::NoButton @@ -34,101 +34,114 @@ - - + + - Host: + Room: - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter - - + + - - + + - Room: + Password: - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter - - - - - - - Nickname: + + + + Qt::Orientation::Vertical - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + 66 + 31 + - + - - + + - - + + + + + - Password: + Auto-join: - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter - + - QLineEdit::Password + QLineEdit::EchoMode::Password - - - - Qt::Vertical + + + + Host: - - - 66 - 31 - + + Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter - + + + + - + - Auto-join: + Nickname: - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter - - + + + + Name: + + + Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter + + + + + - Qt::Horizontal + Qt::Orientation::Horizontal - QDialogButtonBox::Cancel|QDialogButtonBox::Ok + QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Save @@ -157,7 +170,7 @@ - Qt::Horizontal + Qt::Orientation::Horizontal diff --git a/src/bookmarkmanagedlg.cpp b/src/bookmarkmanagedlg.cpp index 705fc39136..fdb8081c9a 100644 --- a/src/bookmarkmanagedlg.cpp +++ b/src/bookmarkmanagedlg.cpp @@ -66,6 +66,7 @@ BookmarkManageDlg::BookmarkManageDlg(PsiAccount *account) : QDialog(), account_( connect(ui_.listView->selectionModel(), SIGNAL(selectionChanged(const QItemSelection &, const QItemSelection &)), SLOT(selectionChanged(const QItemSelection &, const QItemSelection &))); + connect(ui_.mucName, SIGNAL(textEdited(const QString &)), SLOT(updateCurrentItem())); connect(ui_.host, SIGNAL(textEdited(const QString &)), SLOT(updateCurrentItem())); connect(ui_.room, SIGNAL(textEdited(const QString &)), SLOT(updateCurrentItem())); connect(ui_.nickname, SIGNAL(textEdited(const QString &)), SLOT(updateCurrentItem())); @@ -101,6 +102,7 @@ void BookmarkManageDlg::loadBookmarks() for (const ConferenceBookmark &c : account_->bookmarkManager()->conferences()) { QStandardItem *item = new QStandardItem(c.name()); + item->setData(QVariant(c.name()), MucNameRole); item->setData(QVariant(c.jid().full()), JidRole); item->setData(QVariant(c.autoJoin()), AutoJoinRole); item->setData(QVariant(c.nick()), NickRole); @@ -111,7 +113,7 @@ void BookmarkManageDlg::loadBookmarks() ConferenceBookmark BookmarkManageDlg::bookmarkFor(const QModelIndex &index) const { - return ConferenceBookmark(index.data(Qt::DisplayRole).toString(), index.data(JidRole).toString(), + return ConferenceBookmark(index.data(MucNameRole).toString(), index.data(JidRole).toString(), ConferenceBookmark::JoinType(index.data(AutoJoinRole).toInt()), index.data(NickRole).toString(), index.data(PasswordRole).toString()); } @@ -168,13 +170,19 @@ void BookmarkManageDlg::selectionChanged(const QItemSelection &selected, const Q } XMPP::Jid jid = XMPP::Jid(current.data(JidRole).toString()); + ui_.mucName->setText(current.data(MucNameRole).toString()); + ui_.mucName->setCursorPosition(0); ui_.host->setText(jid.domain()); + ui_.host->setCursorPosition(0); ui_.room->setText(jid.node()); + ui_.room->setCursorPosition(0); ui_.nickname->setText(current.data(NickRole).toString()); + ui_.nickname->setCursorPosition(0); ui_.password->setText(current.data(PasswordRole).toString()); + ui_.password->setCursorPosition(0); ui_.autoJoin->setCurrentIndex(current.data(AutoJoinRole).toInt()); QList editors; - editors << ui_.host << ui_.room << ui_.nickname << ui_.password << ui_.autoJoin; + editors << ui_.mucName << ui_.host << ui_.room << ui_.nickname << ui_.password << ui_.autoJoin; for (QWidget *w : std::as_const(editors)) { w->setEnabled(current.isValid()); } @@ -191,6 +199,8 @@ void BookmarkManageDlg::updateCurrentItem() QStandardItem *item = model_->item(currentIndex().row()); if (item) { + item->setData(ui_.mucName->text().isEmpty() ? jid().full() : ui_.mucName->text(), Qt::DisplayRole); + item->setData(ui_.mucName->text(), MucNameRole); item->setData(QVariant(jid().full()), JidRole); item->setData(QVariant(ui_.autoJoin->currentIndex()), AutoJoinRole); item->setData(QVariant(ui_.nickname->text()), NickRole); @@ -233,6 +243,7 @@ void BookmarkManageDlg::importBookmarks() ConferenceBookmark c(elem); QStandardItem *item = new QStandardItem(c.name()); + item->setData(QVariant(c.name()), MucNameRole); item->setData(QVariant(c.jid().full()), JidRole); item->setData(QVariant(c.autoJoin()), AutoJoinRole); item->setData(QVariant(c.nick()), NickRole); diff --git a/src/bookmarkmanagedlg.h b/src/bookmarkmanagedlg.h index 15d64ef93b..1d4b3cf406 100644 --- a/src/bookmarkmanagedlg.h +++ b/src/bookmarkmanagedlg.h @@ -45,10 +45,11 @@ public slots: private: enum Role { // DisplayRole / EditRole - JidRole = Qt::UserRole + 0, - AutoJoinRole = Qt::UserRole + 1, - NickRole = Qt::UserRole + 2, - PasswordRole = Qt::UserRole + 3 + MucNameRole = Qt::UserRole + 0, + JidRole = Qt::UserRole + 2, + AutoJoinRole = Qt::UserRole + 3, + NickRole = Qt::UserRole + 4, + PasswordRole = Qt::UserRole + 5 }; void loadBookmarks(); diff --git a/src/bookmarkmanager.cpp b/src/bookmarkmanager.cpp index 0d7a0f33e1..aca621a313 100644 --- a/src/bookmarkmanager.cpp +++ b/src/bookmarkmanager.cpp @@ -23,7 +23,6 @@ #include "iris/xmpp_task.h" #include "iris/xmpp_xmlcommon.h" #include "psiaccount.h" -#include "psioptions.h" // ----------------------------------------------------------------------------- @@ -241,12 +240,11 @@ void BookmarkManager::getBookmarks_finished() } } + setIsAvailable(true); if (urlsWereChanged) emit urlsChanged(urls_); if (conferencesWereChanged) emit conferencesChanged(conferences_); - - setIsAvailable(true); } else { setIsAvailable(false); } diff --git a/src/captchadlg.cpp b/src/captchadlg.cpp index 5611b6b302..45660d023b 100644 --- a/src/captchadlg.cpp +++ b/src/captchadlg.cpp @@ -15,13 +15,12 @@ CaptchaDlg::CaptchaDlg(QWidget *parent, const CaptchaChallenge &challenge, PsiAc setAttribute(Qt::WA_DeleteOnClose); ui->setupUi(this); - QVBoxLayout *l = new QVBoxLayout; + QVBoxLayout *l = new QVBoxLayout(this); dataWidget = new XDataWidget(pa->psi(), this, pa->client(), challenge.arbiter()); dataWidget->setForm(challenge.form()); l->addWidget(dataWidget); l->addStretch(); l->addWidget(ui->buttonBox); - setLayout(l); } CaptchaDlg::~CaptchaDlg() { delete ui; } diff --git a/src/chatdlg.cpp b/src/chatdlg.cpp index 35d1f8d229..7420e001d4 100644 --- a/src/chatdlg.cpp +++ b/src/chatdlg.cpp @@ -141,12 +141,15 @@ void ChatDlg::init() chatView()->setDialog(this); bool isPrivate = account()->groupchats().contains(jid().bare()); chatView()->setSessionData(false, isPrivate, jid(), jid().full()); // FIXME fix nick updating + chatView()->setLocalNickname(account()->nick()); #ifdef WEBKIT chatView()->setAccount(account()); #else chatView()->setMediaOpener(account()->fileSharingDeviceOpener()); #endif chatView()->init(); + connect(chatView(), &ChatView::outgoingReactions, this, &ChatDlg::sendOutgoingReactions); + connect(chatView(), &ChatView::outgoingMessageRetraction, this, &ChatDlg::sendMessageRetraction); // seems its useless hack // connect(chatView(), SIGNAL(selectionChanged()), SLOT(logSelectionChanged())); // @@ -167,7 +170,7 @@ void ChatDlg::init() connect(account(), SIGNAL(pgpKeyChanged()), SLOT(updatePgp())); connect(account(), SIGNAL(encryptedMessageSent(int, bool, int, const QString &)), SLOT(encryptedMessageSent(int, bool, int, const QString &))); - account()->dialogRegister(this, jid()); + account()->dialogRegister(this, isPrivate ? jid() : jid().withResource({})); chatView()->setFocusPolicy(Qt::NoFocus); chatEdit()->setFocus(); @@ -373,7 +376,8 @@ void ChatDlg::setJid(const Jid &j) account()->dialogUnregister(this); TabbableWidget::setJid(j); updateRealJid(); - account()->dialogRegister(this, jid()); + bool isPrivate = account()->groupchats().contains(jid().bare()); + account()->dialogRegister(this, isPrivate ? jid() : jid().withResource({})); updateContact(jid(), false); } } @@ -677,7 +681,7 @@ void ChatDlg::doSend() } Message m(jid()); - m.setType("chat"); + m.setType(Message::Type::Chat); m.setTimeStamp(QDateTime::currentDateTime()); if (isPgpEncryptionEnabled()) { m.setWasEncrypted(true); @@ -697,7 +701,7 @@ void ChatDlg::doSend() QString id = account()->client()->genUniqueId(); m.setId(id); // we need id early for message manipulations in chatview if (chatEdit()->isCorrection()) { - m.setReplaceId(chatEdit()->lastMessageId()); + m.setReplaceId(chatEdit()->correctionId()); } chatEdit()->setLastMessageId(id); chatEdit()->resetCorrection(); @@ -758,6 +762,22 @@ void ChatDlg::doneSend() resetComposing(); } +void ChatDlg::sendOutgoingReactions(const QString &messageId, const QSet &reactions) +{ + Message m(jid()); + m.setType(Message::Type::Chat); + m.setReactions({ messageId, reactions }); + emit aSend(m); +} + +void ChatDlg::sendMessageRetraction(const QString &messageId) +{ + Message m(jid()); + m.setType(Message::Type::Chat); + m.setRetraction(messageId); + emit aSend(m); +} + void ChatDlg::encryptedMessageSent(int x, bool b, int e, const QString &dtext) { Q_UNUSED(e); @@ -792,6 +812,14 @@ void ChatDlg::incomingMessage(const Message &m) if (dm.messageReceipt() == ReceiptReceived) { chatView()->markReceived(dm.messageReceiptId()); } + if (!m.reactions().targetId.isEmpty()) { + auto mv = MessageView::reactionsMessage({}, m.reactions().targetId, m.reactions().reactions); + chatView()->dispatchMessage(mv); + } + if (!m.retraction().isEmpty()) { + auto mv = MessageView::retractionMessage(m.retraction()); + chatView()->dispatchMessage(mv); + } } else { // Normal message // Check if user requests event messages @@ -1042,7 +1070,7 @@ void ChatDlg::setChatState(ChatState state) || (state == XMPP::StateComposing && lastChatState_ == XMPP::StateInactive)) { // First go to the paused state Message tm(jid()); - m.setType("chat"); + m.setType(Message::Type::Chat); m.setChatState(XMPP::StatePaused); if (account()->isAvailable()) { account()->dj_sendMessage(m, false); @@ -1053,7 +1081,7 @@ void ChatDlg::setChatState(ChatState state) // Send event message if (m.containsEvents() || m.chatState() != XMPP::StateNone) { - m.setType("chat"); + m.setType(Message::Type::Chat); if (account()->isAvailable()) { account()->dj_sendMessage(m, false); } diff --git a/src/chatdlg.h b/src/chatdlg.h index 4e2025ec04..ff946009de 100644 --- a/src/chatdlg.h +++ b/src/chatdlg.h @@ -146,6 +146,8 @@ private slots: void initComposing(); void setComposing(); void getHistory(); + void sendOutgoingReactions(const QString &messageId, const QSet &reactions); + void sendMessageRetraction(const QString &messageId); protected slots: void checkComposing(); diff --git a/src/chatdlg.ui b/src/chatdlg.ui index 99ebe99f40..3e1cee12fd 100644 --- a/src/chatdlg.ui +++ b/src/chatdlg.ui @@ -6,7 +6,7 @@ 0 0 - 455 + 463 344 @@ -29,7 +29,7 @@ - Qt::Vertical + Qt::Orientation::Vertical @@ -39,10 +39,10 @@ - QFrame::NoFrame + QFrame::Shape::NoFrame - QFrame::Plain + QFrame::Shadow::Plain @@ -93,32 +93,13 @@ true - QComboBox::AdjustToMinimumContentsLengthWithIcon + QComboBox::SizeAdjustPolicy::AdjustToMinimumContentsLengthWithIcon 10 - - - - - 16 - 16 - - - - - 16 - 16 - - - - - - - @@ -143,16 +124,16 @@ Message length - QFrame::Panel + QFrame::Shape::Panel - QFrame::Sunken + QFrame::Shadow::Sunken 0 - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter @@ -216,7 +197,7 @@ - QToolButton::InstantPopup + QToolButton::ToolButtonPopupMode::InstantPopup @@ -235,10 +216,10 @@ - QToolButton::InstantPopup + QToolButton::ToolButtonPopupMode::InstantPopup - Qt::NoArrow + Qt::ArrowType::NoArrow @@ -253,7 +234,7 @@ - Qt::NoFocus + Qt::FocusPolicy::NoFocus @@ -267,10 +248,10 @@ - QFrame::NoFrame + QFrame::Shape::NoFrame - QFrame::Plain + QFrame::Shadow::Plain @@ -294,7 +275,7 @@ - Qt::Horizontal + Qt::Orientation::Horizontal @@ -309,7 +290,6 @@ - 75 true @@ -348,7 +328,7 @@ - Qt::CustomContextMenu + Qt::ContextMenuPolicy::CustomContextMenu Send @@ -361,7 +341,7 @@ Avatar - Qt::AlignCenter + Qt::AlignmentFlag::AlignCenter diff --git a/src/chatview_te.cpp b/src/chatview_te.cpp index 6f0d268b01..26278d51af 100644 --- a/src/chatview_te.cpp +++ b/src/chatview_te.cpp @@ -95,6 +95,10 @@ void ChatView::setSessionData(bool isMuc, bool isMucPrivate, const XMPP::Jid &ji name_ = name; } +void ChatView::setLocalNickname(const QString &nickname) { + localNickname_ = nickname; +} + void ChatView::clear() { PsiTextView::clear(); @@ -106,7 +110,7 @@ void ChatView::contextMenuEvent(QContextMenuEvent *e) const QUrl anc = QUrl::fromEncoded(anchorAt(e->pos()).toLatin1()); if (anc.scheme() == "addnick") { - emit showNM(anc.path().mid(1)); + emit showNickMenu(anc.path().mid(1)); e->accept(); } else { QMenu *menu = createStandardContextMenu(e->pos()); @@ -125,37 +129,47 @@ QMenu *ChatView::createStandardContextMenu(const QPoint &position) void ChatView::addLogIconsResources() { - struct { + struct CVIcon { const char *name; const char *icon; - } icons[] = { { "log_icon_receive", "psi/notification_chat_receive" }, - { "log_icon_send", "psi/notification_chat_send" }, - { "log_icon_receive_encrypted", "psi/notification_chat_receive_encrypted" }, - { "log_icon_send_encrypted", "psi/notification_chat_send_encrypted" }, - { "log_icon_time", "psi/notification_chat_time" }, - { "log_icon_info", "psi/notification_chat_info" }, - { "log_icon_delivered", "psi/notification_chat_delivery_ok" }, - { "log_icon_delivered_encrypted", "psi/notification_chat_delivery_ok_encrypted" }, - { "log_icon_corrected", "psi/action_templates_edit" }, - { "log_icon_history", "psi/history" } }; + }; useMessageIcons_ = PsiOptions::instance()->getOption("options.ui.chat.use-message-icons").toBool(); int scaledSize = int(fontInfo().pixelSize() * EqTextIconK + .5); bool scale = PsiOptions::instance()->getOption("options.ui.chat.scaled-message-icons").toBool(); + auto fs = QFontInfo(font()).pixelSize(); - auto fs = QFontInfo(font()).pixelSize(); - for (auto &i : icons) { - auto res = QUrl(QLatin1String("icon:") + i.name); + auto addResource = [&](const CVIcon &i) { + auto res = QUrl(QStringLiteral("icon:") + QLatin1String(i.name)); + auto icon = IconsetFactory::iconPixmap(i.icon, scaledSize); + if (icon.height() > HugeIconTextViewK * fs || scale) { + icon = icon.scaledToHeight(scaledSize, Qt::SmoothTransformation); + } + document()->addResource(QTextDocument::ImageResource, res, icon); + }; + + CVIcon optional_icons[] = { { "log_icon_receive", "psi/notification_chat_receive" }, + { "log_icon_send", "psi/notification_chat_send" }, + { "log_icon_receive_encrypted", "psi/notification_chat_receive_encrypted" }, + { "log_icon_send_encrypted", "psi/notification_chat_send_encrypted" }, + { "log_icon_time", "psi/notification_chat_time" }, + { "log_icon_info", "psi/notification_chat_info" }, + { "log_icon_delivered", "psi/notification_chat_delivery_ok" }, + { "log_icon_delivered_encrypted", "psi/notification_chat_delivery_ok_encrypted" }, + { "log_icon_history", "psi/history" } }; + CVIcon noneopt_icons[] { { "log_icon_corrected", "psi/action_templates_edit" } }; + + for (auto &i : optional_icons) { if (useMessageIcons_) { - auto icon = IconsetFactory::iconPixmap(i.icon, scaledSize); - if (icon.height() > HugeIconTextViewK * fs || scale) { - icon = icon.scaledToHeight(scaledSize, Qt::SmoothTransformation); - } - document()->addResource(QTextDocument::ImageResource, res, icon); + addResource(i); } else { - document()->addResource(QTextDocument::ImageResource, res, QVariant()); + document()->addResource(QTextDocument::ImageResource, QUrl(QLatin1String("icon:") + i.name), QVariant()); } } + + for (auto &i : noneopt_icons) { + addResource(i); + } } void ChatView::markReceived(QString id) @@ -365,11 +379,6 @@ void ChatView::dispatchMessage(const MessageView &mv) } } -QString ChatView::replaceMarker(const MessageView &mv) const -{ - return " "; -} - void ChatView::renderMucMessage(const MessageView &mv, QTextCursor &insertCursor) { const QString timestr = formatTimeStamp(mv.dateTime()); @@ -406,7 +415,7 @@ void ChatView::renderMucMessage(const MessageView &mv, QTextCursor &insertCursor QString nick = QString("" + TextUtil::escape(mv.nick()) + ""; - QString inner = alerttagso + mv.formattedText() + replaceMarker(mv) + alerttagsc; + QString inner = alerttagso + mv.formattedText() + alerttagsc; if (mv.isEmote()) { insertText(icon + QString("").arg(nickcolor) + QString("[%1]").arg(timestr) @@ -464,7 +473,7 @@ void ChatView::renderMessage(const MessageView &mv, QTextCursor &insertCursor) } QString str; - QString inner = mv.formattedText() + replaceMarker(mv); + QString inner = mv.formattedText(); if (mv.isEmote()) { str = icon + QString("").arg(color) + QString("[%1]").arg(timestr) + QString(" *%1 ").arg(TextUtil::escape(mv.nick())) + inner + ""; diff --git a/src/chatview_te.h b/src/chatview_te.h index 09a4b99ee9..f92bded260 100644 --- a/src/chatview_te.h +++ b/src/chatview_te.h @@ -57,6 +57,7 @@ class ChatView : public PsiTextView, public ChatViewCommon { void init(); void setDialog(QWidget *dialog); void setSessionData(bool isMuc, bool isMucPrivate, const XMPP::Jid &jid, const QString name); + void setLocalNickname(const QString &nickname); void insertText(const QString &text, QTextCursor &insertCursor); void appendText(const QString &text); @@ -83,13 +84,12 @@ public slots: QString formatTimeStamp(const QDateTime &time); QString colorString(bool local, bool spooled) const; - QString replaceMarker(const MessageView &mv) const; - void renderMucMessage(const MessageView &, QTextCursor &insertCursor); - void renderMessage(const MessageView &, QTextCursor &insertCursor); - void renderSysMessage(const MessageView &); - void renderSubject(const MessageView &); - void renderMucSubject(const MessageView &); - void renderUrls(const MessageView &); + void renderMucMessage(const MessageView &, QTextCursor &insertCursor); + void renderMessage(const MessageView &, QTextCursor &insertCursor); + void renderSysMessage(const MessageView &); + void renderSubject(const MessageView &); + void renderMucSubject(const MessageView &); + void renderUrls(const MessageView &); protected slots: void autoCopy(); @@ -98,9 +98,15 @@ private slots: void slotScroll(); signals: - void showNM(const QString &); + void showNickMenu(const QString &); void quote(const QString &text); void nickInsertClick(const QString &nick); + void outgoingReactions(const QString &messageId, const QSet &reactions); + void outgoingMessageRetraction(const QString &messageId); + void editMessageRequested(const QString &messageId, const QString &text); + void forwardMessageRequested(const QString &messageId, const QString &nick, const QString &text); + void openInfoRequested(const QString &nickname); + void openChatRequested(const QString &nickname); private: bool isMuc_; @@ -110,6 +116,7 @@ private slots: int oldTrackBarPosition; XMPP::Jid jid_; QString name_; + QString localNickname_; QPointer dialog_; QAction *actQuote_; }; diff --git a/src/chatview_webkit.cpp b/src/chatview_webkit.cpp index 1198f01eb1..6a7f5c6bff 100644 --- a/src/chatview_webkit.cpp +++ b/src/chatview_webkit.cpp @@ -71,7 +71,9 @@ class ChatViewThemeSessionBridge; class ChatViewPrivate { public: - ChatViewPrivate() = default; + ChatViewPrivate(ChatView *q) : q(q) { } + + ChatView *q; Theme theme; @@ -90,6 +92,7 @@ class ChatViewPrivate { AvatarFactory::UserHashes remoteIcons; AvatarFactory::UserHashes localIcons; ChatViewThemeProvider *themeProvider = nullptr; + QString localNickName; static QString closeIconTags(const QString &richText) { @@ -136,6 +139,35 @@ class ChatViewPrivate { ret.append(QStringView { msg }.mid(post)); return ret; } + + void sendJsObject(const QVariantMap &map) + { + jsBuffer_.append(map); + checkJsBuffer(); + } + + void checkJsBuffer(); + + void sendReactionsToUI(const QString &nick, const QString &messageId, const QSet &reactions) + { + QVariantMap m; + m["type"] = QLatin1String("reactions"); + m["sender"] = nick; + m["messageid"] = messageId; + auto rl = q->updateReactions(nick, messageId, reactions); + auto vl = QVariantList(); + for (auto &r : std::as_const(rl)) { + auto vmr = QVariantMap(); + if (!r.base.isEmpty()) { + vmr[QLatin1String("base")] = r.base; + } + vmr[QLatin1String("text")] = r.code; + vmr[QLatin1String("nicks")] = r.nicks; + vl << vmr; + } + m[QLatin1String("reactions")] = vl; + sendJsObject(m); + } }; //---------------------------------------------------------------------------- @@ -171,8 +203,13 @@ class ChatViewJSObject : public ChatViewThemeSession { std::function callback) { if (url.path().startsWith("/psibob/")) { - QString cid = url.path().mid(sizeof("/psibob/") - 1); - _view->d->account_->loadBob(_view->d->jid_, cid, this, callback); + QString cid = url.path().mid(sizeof("/psibob/") - 1); + auto sender = QUrlQuery(url).queryItemValue("sender", QUrl::FullyDecoded); + auto j = _view->d->jid_; + if (!sender.isEmpty()) { + j = j.withResource(sender); + } + _view->d->account_->loadBob(j, cid, this, callback); return true; } // qDebug("Unhandled url: %s", qPrintable(url.toString())); @@ -293,6 +330,55 @@ class ChatViewJSObject : public ChatViewThemeSession { connect(reply, SIGNAL(finished()), SLOT(onUrlHeadersReady())); } + Q_INVOKABLE void react(const QString &messageId, const QString &reaction) + { + if (messageId.isEmpty()) { + qWarning("empty message id on sending reaction. broken theme?"); + return; + } + _view->outgoingReaction(messageId, reaction); + } + + Q_INVOKABLE void deleteMessage(const QString &messageId) + { + emit _view->outgoingMessageRetraction(messageId); + if (!_view->d->isMuc_) { + // send back immediately coz it's not it's not iq and not MUC where server sends it back + QVariantMap vm; + vm["type"] = QLatin1String("msgretract"); + vm["targetid"] = messageId; + emit newMessage(vm); + } + } + + Q_INVOKABLE void replyMessage(const QString &messageId, const QString "edHtml) + { + auto plainText = TextUtil::rich2plain(quotedHtml); + emit _view->quote(plainText); + } + + Q_INVOKABLE void copyMessage(const QString &messageId, const QString &html) + { + auto plainText = TextUtil::rich2plain(html); + QApplication::clipboard()->setText(plainText); + } + + Q_INVOKABLE void showInfo(const QString &nick) { emit _view->openInfoRequested(nick); } + + Q_INVOKABLE void openChat(const QString &nick) { emit _view->openChatRequested(nick); } + + Q_INVOKABLE void kick(const QString &nick) { qDebug() << "kick" << nick; } + + Q_INVOKABLE void editMessage(const QString &messageId, const QString &messageHtml) + { + emit _view->editMessageRequested(messageId, TextUtil::rich2plain(messageHtml)); + } + + Q_INVOKABLE void forwardMessage(const QString &messageId, const QString &nick, const QString &messageHtml) + { + emit _view->forwardMessageRequested(messageId, nick, TextUtil::rich2plain(messageHtml)); + } + private slots: void onUrlHeadersReady() { @@ -336,6 +422,17 @@ private slots: //---------------------------------------------------------------------------- #ifdef WEBENGINE +class ExtBrowserPage : public QWebEnginePage { +protected: + using QWebEnginePage::QWebEnginePage; + bool acceptNavigationRequest(const QUrl &url, NavigationType type, bool isMainFrame) + { + DesktopUtil::openUrl(url); + deleteLater(); + return false; + } +}; + class ChatViewPage : public QWebEnginePage { protected: using QWebEnginePage::QWebEnginePage; @@ -349,8 +446,22 @@ class ChatViewPage : public QWebEnginePage { } return true; } + + QWebEnginePage *createWindow(WebWindowType type) { return new ExtBrowserPage(); } }; #else + +class ExtBrowserPage : public QWebPage { +protected: + using QWebPage::QWebPage; + bool acceptNavigationRequest(const QUrl &url, NavigationType type, bool isMainFrame) + { + DesktopUtil::openUrl(url); + deleteLater(); + return false; + } +}; + class ChatViewPage : public QWebPage { Q_OBJECT @@ -381,6 +492,8 @@ class ChatViewPage : public QWebPage { } return true; } + + QWebPage *createWindow(WebWindowType type) { return new ExtBrowserPage(); } }; #endif @@ -388,7 +501,7 @@ class ChatViewPage : public QWebPage { //---------------------------------------------------------------------------- // ChatView //---------------------------------------------------------------------------- -ChatView::ChatView(QWidget *parent) : QFrame(parent), d(new ChatViewPrivate) +ChatView::ChatView(QWidget *parent) : QFrame(parent), d(new ChatViewPrivate(this)) { d->jsObject = new ChatViewJSObject(this); /* It's a session bridge between html and c++ part */ d->webView = new WebView(this); @@ -422,12 +535,12 @@ ChatView::ChatView(QWidget *parent) : QFrame(parent), d(new ChatViewPrivate) QVariantMap m; m["type"] = "receivehooks"; m["hooks"] = PluginManager::instance()->messageViewJSFilters(); - sendJsObject(m); + d->sendJsObject(m); connect(PluginManager::instance(), &PluginManager::jsFiltersUpdated, this, [this]() { QVariantMap m; m["type"] = "receivehooks"; m["hooks"] = PluginManager::instance()->messageViewJSFilters(); - sendJsObject(m); + d->sendJsObject(m); }); #endif } @@ -477,7 +590,7 @@ void ChatView::markReceived(QString id) m["type"] = "receipt"; m["id"] = id; m["encrypted"] = d->isEncryptionEnabled_; - sendJsObject(m); + d->sendJsObject(m); } QSize ChatView::sizeHint() const { return minimumSizeHint(); } @@ -504,6 +617,8 @@ void ChatView::setAccount(PsiAccount *acc) connect(d->themeProvider, SIGNAL(themeChanged()), SLOT(init())); } +void ChatView::setLocalNickname(const QString &nickname) { d->localNickName = nickname; } + void ChatView::contextMenuEvent(QContextMenuEvent *e) { QUrl linkUrl; @@ -518,7 +633,7 @@ void ChatView::contextMenuEvent(QContextMenuEvent *e) linkUrl = d->webView->page()->mainFrame()->hitTestContent(e->pos()).linkUrl(); #endif if (linkUrl.scheme() == "addnick") { - emit showNM(linkUrl.path().mid(1)); + emit showNickMenu(linkUrl.path().mid(1)); e->accept(); } } @@ -531,7 +646,7 @@ void ChatView::changeEvent(QEvent *event) || event->type() == QEvent::FontChange) { QVariantMap m; m["type"] = "settings"; - sendJsObject(m); + d->sendJsObject(m); } QFrame::changeEvent(event); } @@ -547,26 +662,11 @@ void ChatView::psiOptionChanged(const QString &option) } } -void ChatView::sendJsObject(const QVariantMap &map) -{ - d->jsBuffer_.append(map); - checkJsBuffer(); -} - -void ChatView::checkJsBuffer() -{ - if (d->sessionReady_) { - while (!d->jsBuffer_.isEmpty()) { - emit d->jsObject->newMessage(d->jsBuffer_.takeFirst()); - } - } -} - void ChatView::sessionInited() { qDebug("Session is initialized"); d->sessionReady_ = true; - checkJsBuffer(); + d->checkJsBuffer(); } bool ChatView::handleCopyEvent(QObject *object, QEvent *event, ChatEdit *chatEdit) @@ -586,6 +686,95 @@ bool ChatView::handleCopyEvent(QObject *object, QEvent *event, ChatEdit *chatEdi // input point of all messages void ChatView::dispatchMessage(const MessageView &mv) { + static QHash types; + if (types.isEmpty()) { + types.insert(MessageView::Message, "message"); + types.insert(MessageView::System, "system"); + types.insert(MessageView::Status, "status"); + types.insert(MessageView::Subject, "subject"); + types.insert(MessageView::Urls, "urls"); + types.insert(MessageView::MUCJoin, "join"); + types.insert(MessageView::MUCPart, "part"); + types.insert(MessageView::FileTransferRequest, "ftreq"); + types.insert(MessageView::FileTransferFinished, "ftfin"); + types.insert(MessageView::NickChange, "newnick"); + types.insert(MessageView::Reactions, "reactions"); + types.insert(MessageView::MessageRetraction, "msgretract"); + } + QVariantMap m; + switch (mv.type()) { + case MessageView::Message: + m["message"] = d->prepareShares(ChatViewPrivate::closeIconTags(mv.formattedText())); + m["emote"] = mv.isEmote(); + m["local"] = mv.isLocal(); + m["sender"] = mv.nick(); + m["userid"] = mv.userId(); + m["spooled"] = mv.isSpooled(); + m["id"] = mv.messageId(); + if (d->isMuc_) { // maybe w/o conditions ? + m["alert"] = mv.isAlert(); + } else { + m["awaitingReceipt"] = mv.isAwaitingReceipt(); + } + if (mv.references().count()) { + QVariantMap rvm; + for (auto const &r : mv.references()) { + auto md = r->metaData(); + md.insert("type", r->mimeType()); + rvm.insert(r->sums()[0].toString(), md); + } + m["references"] = rvm; + } + break; + case MessageView::NickChange: + m["sender"] = mv.nick(); + m["newnick"] = mv.userText(); + m["message"] = ChatViewPrivate::closeIconTags(mv.text()); + break; + case MessageView::MUCJoin: { + Jid j = d->jid_.withResource(mv.nick()); + m["avatar"] = ChatViewJSObject::avatarUrl(d->account_->avatarFactory()->userHashes(j).avatar); + m["nickcolor"] = getMucNickColor(mv.nick(), mv.isLocal()); + } + case MessageView::MUCPart: + m["nopartjoin"] = mv.isJoinLeaveHidden(); + PSI_FALLSTHROUGH; // falls through + case MessageView::Status: + m["sender"] = mv.nick(); + m["status"] = mv.status(); + m["priority"] = mv.statusPriority(); + m["message"] = ChatViewPrivate::closeIconTags(mv.text()); + m["usertext"] = ChatViewPrivate::closeIconTags(mv.formattedUserText()); + m["nostatus"] = mv.isStatusChangeHidden(); // looks strange? but chatview can use status for something anyway + break; + case MessageView::System: + case MessageView::Subject: + m["message"] = ChatViewPrivate::closeIconTags(mv.formattedText()); + m["usertext"] = ChatViewPrivate::closeIconTags(mv.formattedUserText()); + break; + case MessageView::Urls: { + QVariantMap vmUrls; + for (auto it = mv.urls().constBegin(); it != mv.urls().constEnd(); ++it) { + vmUrls.insert(it.key(), it.value()); + } + m["urls"] = vmUrls; + break; + } + case MessageView::Reactions: { + auto n = d->isMuc_ ? mv.nick() : QString::fromLatin1(mv.isLocal() ? "l" : "r"); + d->sendReactionsToUI(n, mv.reactionsId(), mv.reactions()); + return; + } + case MessageView::MessageRetraction: + m["targetid"] = mv.retractionId(); + break; + case MessageView::FileTransferRequest: + case MessageView::FileTransferFinished: + break; + } + + m["time"] = mv.dateTime(); + m["type"] = types.value(mv.type()); QString replaceId = mv.replaceId(); if (replaceId.isEmpty() && (mv.type() == MessageView::Message || mv.type() == MessageView::Subject) && updateLastMsgTime(mv.dateTime())) { @@ -593,33 +782,18 @@ void ChatView::dispatchMessage(const MessageView &mv) m["date"] = mv.dateTime(); m["type"] = "message"; m["mtype"] = "lastDate"; - sendJsObject(m); - } - QVariantMap vm = mv.toVariantMap(d->isMuc_, true); - if (mv.type() == MessageView::MUCJoin) { - Jid j = d->jid_.withResource(mv.nick()); - vm["avatar"] = ChatViewJSObject::avatarUrl(d->account_->avatarFactory()->userHashes(j).avatar); - vm["nickcolor"] = getMucNickColor(mv.nick(), mv.isLocal()); - } - auto it = vm.find(QLatin1String("usertext")); - if (it != vm.end()) { - *it = ChatViewPrivate::closeIconTags(it.value().toString()); - } - it = vm.find(QLatin1String("message")); - if (it != vm.end()) { - *it = d->prepareShares(it.value().toString()); - *it = ChatViewPrivate::closeIconTags(it.value().toString()); + d->sendJsObject(m); } - vm["encrypted"] = d->isEncryptionEnabled_; + m["encrypted"] = d->isEncryptionEnabled_; if (!replaceId.isEmpty()) { - vm["type"] = "replace"; - vm["replaceId"] = replaceId; - } else { - vm["mtype"] = vm["type"]; - vm["type"] = "message"; + m["type"] = "replace"; + m["replaceId"] = replaceId; + } else if (mv.type() != MessageView::MessageRetraction) { + m["mtype"] = m["type"]; + m["type"] = "message"; } - sendJsObject(vm); + d->sendJsObject(m); } void ChatView::sendJsCode(const QString &js) @@ -627,7 +801,7 @@ void ChatView::sendJsCode(const QString &js) QVariantMap m; m["type"] = "js"; m["js"] = js; - sendJsObject(m); + d->sendJsObject(m); } void ChatView::scrollUp() { emit d->jsObject->scrollRequested(-50); } @@ -666,7 +840,7 @@ void ChatView::updateAvatar(const Jid &jid, UserType utype) m["type"] = "avatar"; m["sender"] = jid.resource(); m["avatar"] = ChatViewJSObject::avatarUrl(d->account_->avatarFactory()->userHashes(jid).avatar); - sendJsObject(m); + d->sendJsObject(m); } } @@ -674,14 +848,14 @@ void ChatView::clear() { QVariantMap m; m["type"] = "clear"; - sendJsObject(m); + d->sendJsObject(m); } void ChatView::doTrackBar() { QVariantMap m; m["type"] = "trackbar"; - sendJsObject(m); + d->sendJsObject(m); } WebView *ChatView::textWidget() { return d->webView; } @@ -690,4 +864,24 @@ QWidget *ChatView::realTextWidget() { return d->webView; } QObject *ChatView::jsBridge() { return d->jsObject; } +void ChatView::outgoingReaction(const QString &messageId, const QString &reaction) +{ + auto n = d->isMuc_ ? d->localNickName : QString::fromLatin1("l"); + auto reactions = onReactionSwitched(n, messageId, reaction); + emit outgoingReactions(messageId, reactions); + if (!d->isMuc_) { + // with private message we are not going to get it back. so back immediately + d->sendReactionsToUI(n, messageId, reactions); + } +} + +void ChatViewPrivate::checkJsBuffer() +{ + if (sessionReady_) { + while (!jsBuffer_.isEmpty()) { + emit jsObject->newMessage(jsBuffer_.takeFirst()); + } + } +} + #include "chatview_webkit.moc" diff --git a/src/chatview_webkit.h b/src/chatview_webkit.h index 00830c2410..ba7d86d1c8 100644 --- a/src/chatview_webkit.h +++ b/src/chatview_webkit.h @@ -53,11 +53,11 @@ class ChatView : public QFrame, public ChatViewCommon { void setDialog(QWidget *dialog); void setSessionData(bool isMuc, bool isMucPrivate, const XMPP::Jid &jid, const QString name); void setAccount(PsiAccount *acc); + void setLocalNickname(const QString &nickname); void contextMenuEvent(QContextMenuEvent *event); bool handleCopyEvent(QObject *object, QEvent *event, ChatEdit *chatEdit); - void sendJsObject(const QVariantMap &); void dispatchMessage(const MessageView &m); void sendJsCode(const QString &js); @@ -80,6 +80,9 @@ public slots: void changeEvent(QEvent *event); // void keyPressEvent(QKeyEvent *); +private: + void outgoingReaction(const QString &messageId, const QString &reaction); + protected slots: void psiOptionChanged(const QString &); // void autoCopy(); @@ -88,13 +91,18 @@ public slots: void init(); private slots: - void checkJsBuffer(); void sessionInited(); signals: - void showNM(const QString &); + void showNickMenu(const QString &); void nickInsertClick(const QString &nick); void quote(const QString &text); + void outgoingReactions(const QString &messageId, const QSet &reactions); + void outgoingMessageRetraction(const QString &messageId); + void editMessageRequested(const QString &messageId, const QString &text); + void forwardMessageRequested(const QString &messageId, const QString &nick, const QString &text); + void openInfoRequested(const QString &nickname); + void openChatRequested(const QString &nickname); private: friend class ChatViewPrivate; diff --git a/src/chatviewcommon.cpp b/src/chatviewcommon.cpp index d2cc5e167b..e59ed2a389 100644 --- a/src/chatviewcommon.cpp +++ b/src/chatviewcommon.cpp @@ -23,6 +23,7 @@ #include #include +#include #include #include @@ -37,7 +38,9 @@ void ChatViewCommon::setLooks(QWidget *w) bool ChatViewCommon::updateLastMsgTime(QDateTime t) { bool doInsert = t.date() != _lastMsgTime.date(); - _lastMsgTime = t; + if (!_lastMsgTime.isValid() || t > _lastMsgTime) { + _lastMsgTime = t; + } return doInsert; } @@ -81,9 +84,15 @@ QString ChatViewCommon::getMucNickColor(const QString &nick, bool isSelf) return nickColors[it.value() % (nickColors.size() - 1)]; } while (false); - return QLatin1String("#000000"); // FIXME it's bad for fallback color + return qApp->palette().color(QPalette::Inactive, QPalette::WindowText).name(); } +void ChatViewCommon::addUser(const QString &nickname) { } + +void ChatViewCommon::removeUser(const QString &nickname) { } + +void ChatViewCommon::renameUser(const QString &oldNickname, const QString &newNickname) { } + QList &ChatViewCommon::generatePalette() { static QColor bg; @@ -119,3 +128,72 @@ bool ChatViewCommon::compatibleColors(const QColor &c1, const QColor &c2) return !((dC < 80. && dV > 100) || (dC < 110. && dV <= 100 && dV > 10) || (dC < 125. && dV <= 10)); } + +QList +ChatViewCommon::updateReactions(const QString &senderNickname, const QString &messageId, const QSet &reactions) +{ + auto msgIt = _reactions.find(messageId); + QSet toAdd = reactions; + QSet toRemove; + + QHash>::Iterator userIt; + if (msgIt != _reactions.end()) { + auto &sotredReactions = msgIt.value(); + userIt = sotredReactions.perUser.find(senderNickname); + if (userIt != sotredReactions.perUser.end()) { + toAdd = reactions - userIt.value(); + toRemove = userIt.value() - reactions; + } else { + userIt = sotredReactions.perUser.insert(senderNickname, {}); + } + } else { + msgIt = _reactions.insert(messageId, {}); + userIt = msgIt.value().perUser.insert(senderNickname, {}); + } + *userIt = reactions; + for (auto const &v : toAdd) { + msgIt.value().total[v].append(senderNickname); + } + for (auto const &v : toRemove) { + auto it = msgIt.value().total.find(v); + it->removeOne(senderNickname); + if (it->isEmpty()) { + msgIt.value().total.erase(it); + } + } + auto &total = msgIt.value().total; + + QList ret; + for (auto it = total.begin(); it != total.end(); ++it) { + static const auto skinRemove = QRegularExpression("([\\x{1F3FB}-\\x{1F3FF}]|[\\x{1F9B0}-\\x{1F9B2}])"); + QString orig = it.key(); + auto sanitized = orig.remove(skinRemove); + ret << ReactionsItem { sanitized != orig ? sanitized : QString {}, orig, it.value() }; + } + if (total.isEmpty()) { + _reactions.erase(msgIt); + } else if (userIt->isEmpty()) { + msgIt.value().perUser.erase(userIt); + } + return ret; +} + +QSet ChatViewCommon::onReactionSwitched(const QString &senderNickname, const QString &messageId, + const QString &reaction) +{ + auto msgIt = _reactions.find(messageId); + if (msgIt == _reactions.end()) { + return { { reaction } }; + } + auto userIt = msgIt->perUser.find(senderNickname); + if (userIt == msgIt->perUser.end()) { + return { { reaction } }; + } + auto ret = *userIt; + if (ret.contains(reaction)) { + ret.remove(reaction); + } else { + ret.insert(reaction); + } + return ret; +} diff --git a/src/chatviewcommon.h b/src/chatviewcommon.h index 8c211d7f27..1ee7c802d5 100644 --- a/src/chatviewcommon.h +++ b/src/chatviewcommon.h @@ -38,14 +38,40 @@ class ChatViewCommon { QString getMucNickColor(const QString &, bool); QList getPalette(); +protected: + // a cache of reactions per message + struct Reactions { + QMap total; // unicode reaction => nicknames + QHash> perUser; // nickname => unicode reactions + }; + + struct ReactionsItem { + QString base; + QString code; + QStringList nicks; + }; + + void addUser(const QString &nickname); + void removeUser(const QString &nickname); + void renameUser(const QString &oldNickname, const QString &newNickname); + + // takes incoming reactions and returns reactions to be send to UI + QList updateReactions(const QString &senderNickname, const QString &messageId, + const QSet &reactions); + + // to be called from UI stuff. return list of reactions to send over network + QSet onReactionSwitched(const QString &senderNickname, const QString &messageId, const QString &reaction); + protected: QDateTime _lastMsgTime; private: - QList &generatePalette(); - bool compatibleColors(const QColor &, const QColor &); - int _nickNumber; - QMap _nicks; + QList &generatePalette(); + bool compatibleColors(const QColor &, const QColor &); + + int _nickNumber; + QMap _nicks; + QHash _reactions; // messageId -> reactions }; #endif // CHATVIEWBASE_H diff --git a/src/chatviewtheme.cpp b/src/chatviewtheme.cpp index 08551236c2..e95bbb0bc5 100644 --- a/src/chatviewtheme.cpp +++ b/src/chatviewtheme.cpp @@ -25,6 +25,7 @@ #include "coloropt.h" #include "common.h" #include "jsutil.h" +#include "networkaccessmanager.h" #include "psicon.h" #include "psioptions.h" #include "theme_p.h" @@ -69,7 +70,7 @@ ChatViewThemePrivate::ChatViewThemePrivate(ChatViewThemeProvider *provider) : Th ChatViewThemePrivate::~ChatViewThemePrivate() { - qDebug("ChatViewThemePrivate::~ChatViewThemePrivate"); + // qDebug("ChatViewThemePrivate::~ChatViewThemePrivate"); delete wv; } @@ -89,8 +90,9 @@ bool ChatViewThemePrivate::exists() * @param adapterPath path to directry with adapter * @return true on success */ -bool ChatViewThemePrivate::load(std::function loadCallback) +bool ChatViewThemePrivate::load(const QString &style, std::function loadCallback) { + this->style = style; if (!exists()) { return false; } @@ -133,11 +135,11 @@ bool ChatViewThemePrivate::load(std::function loadCallback) "new QWebChannel(qt.webChannelTransport, function (channel) {\n" "window.srvLoader = channel.objects.srvLoader;\n" "window.srvUtil = channel.objects.srvUtil;\n" - "initPsiTheme().adapter.loadTheme();\n" + "initPsiTheme().adapter.loadTheme(\"%2\");\n" "});\n" "});\n" "") - .arg(themeType), + .arg(themeType, style), jsLoader->serverUrl()); return true; #else @@ -154,11 +156,13 @@ bool ChatViewThemePrivate::load(std::function loadCallback) evaluateFromFile(sp, wv->page()->mainFrame()); } - QString resStr = wv->page() - ->mainFrame() - ->evaluateJavaScript("try { initPsiTheme().adapter.loadTheme(); \"ok\"; } " - "catch(e) { \"Error:\" + e + \"\\n\" + window.psiim.util.props(e); }") - .toString(); + QString resStr + = wv->page() + ->mainFrame() + ->evaluateJavaScript(QString("try { initPsiTheme().adapter.loadTheme(\"%1\"); \"ok\"; } " + "catch(e) { \"Error:\" + e + \"\\n\" + window.psiim.util.props(e); }") + .arg(style)) + .toString(); if (resStr == "ok") { return true; @@ -446,6 +450,25 @@ void ChatViewJSLoader::setMetaData(const QVariantMap &map) if (!v.isEmpty()) { theme->homeUrl = v; } + + vl = map["features"].toStringList(); + if (vl.count()) { + theme->features = vl; + } + + vl = map["stylesList"].toStringList(); + if (vl.count()) { + theme->stylesList = vl; + if (theme->style.isEmpty()) { + auto const currentId = theme->provider->current().id(); + auto currentStyle = currentId.isEmpty() ? QString {} : theme->provider->current().style(); + if (vl.contains(currentStyle)) { + theme->style = currentStyle; + } else { + theme->style = vl[0]; + } + } + } } void ChatViewJSLoader::finishThemeLoading() @@ -529,6 +552,15 @@ QVariantMap ChatViewJSLoader::checkFilesExist(const QStringList &files, const QS return ret; } +QStringList ChatViewJSLoader::listFiles() +{ + QScopedPointer loader(Theme(theme).resourceLoader()); + if (loader) { + return loader->listAll(); + } + return {}; +} + QString ChatViewJSLoader::getFileContents(const QString &name) const { return QString(Theme(theme).loadData(name)); } QString ChatViewJSLoader::getFileContentsFromAdapterDir(const QString &name) const diff --git a/src/chatviewtheme_p.h b/src/chatviewtheme_p.h index 6f70ac20f9..d4ac2aefd4 100644 --- a/src/chatviewtheme_p.h +++ b/src/chatviewtheme_p.h @@ -95,6 +95,7 @@ public slots: QString getFileContents(const QString &name) const; QString getFileContentsFromAdapterDir(const QString &name) const; void setTransparent(); + QStringList listFiles(); }; // JS Bridge object emedded by theme. Has any logic unrelted to contact itself @@ -160,7 +161,7 @@ class ChatViewThemePrivate : public ThemePrivate { ~ChatViewThemePrivate(); bool exists(); - bool load(std::function loadCallback); + bool load(const QString &style, std::function loadCallback); bool hasPreview() const; QWidget *previewWidget(); diff --git a/src/chatviewthemeprovider.cpp b/src/chatviewthemeprovider.cpp index 85e789cd99..fed9352340 100644 --- a/src/chatviewthemeprovider.cpp +++ b/src/chatviewthemeprovider.cpp @@ -60,7 +60,8 @@ const QStringList ChatViewThemeProvider::themeIds() const QString typeName = tDirInfo.fileName(); foreach (QFileInfo themeInfo, QDir(tDirInfo.absoluteFilePath()).entryInfoList(QDir::AllDirs | QDir::NoDotAndDotDot) - + QDir(tDirInfo.absoluteFilePath()).entryInfoList(QStringList("*.theme"), QDir::Files)) { + + QDir(tDirInfo.absoluteFilePath()) + .entryInfoList(QStringList { { "*.theme", "*.zip" } }, QDir::Files)) { ret << (QString("%1/%2").arg(typeName, themeInfo.fileName())); // qDebug("found theme: %s", qPrintable(QString("%1/%2").arg(typeName).arg(themeInfo.fileName()))); } @@ -85,55 +86,73 @@ Theme ChatViewThemeProvider::theme(const QString &id) } /** - * @brief Load theme from settings or classic on failure. - * Signal themeChanged when necessary + * @brief Load theme from settings or New Classic on failure. + * Signals themeChanged if different theme was loaded before. * * @return false on failure to load any theme */ -bool ChatViewThemeProvider::loadCurrent() +PsiThemeProvider::LoadRestult ChatViewThemeProvider::loadCurrent() { - QString loadedId = curTheme.id(); - QString themeName = PsiOptions::instance()->getOption(optionString()).toString(); - if (!loadedId.isEmpty() && loadedId == themeName) { - return true; // already loaded. nothing todo + QString loadedId = curTheme.id(); + QString loadedStyle = loadedId.isEmpty() ? loadedId : curTheme.style(); + QString themeName = PsiOptions::instance()->getOption(optionString()).toString(); + QString style = PsiOptions::instance()->getOption(optionString() + QLatin1String("-style")).toString(); + if (!loadedId.isEmpty() && loadedId == themeName && loadedStyle == style) { + return LoadSuccess; // already loaded. nothing todo } - Theme t(theme(themeName)); - if (!t.exists()) { - if (themeName != QLatin1String("psi/classic")) { + auto newClassic = QStringLiteral("psi/new_classic"); + curLoadingTheme = theme(themeName); // may trigger destructor of prev curLoadingTheme + if (!curLoadingTheme.exists()) { + if (themeName != newClassic) { qDebug("Invalid theme id: %s", qPrintable(themeName)); - qDebug("fallback to classic chatview theme"); - PsiOptions::instance()->setOption(optionString(), QLatin1String("psi/classic")); - return loadCurrent(); + qDebug("fallback to new_classic chatview theme"); + PsiOptions::instance()->setOption(optionString(), newClassic); + style.clear(); + curLoadingTheme = theme(newClassic); + } else { + qDebug("New Classic theme failed to load. No fallback.."); + return LoadFailure; } - qDebug("Classic theme failed to load. No fallback.."); - return false; } - bool startedLoading = t.load([this, t, loadedId](bool success) { - if (!success && t.id() != QLatin1String("psi/classic")) { - qDebug("Failed to load theme \"%s\"", qPrintable(t.id())); - qDebug("fallback to classic chatview theme"); - PsiOptions::instance()->setOption(optionString(), QLatin1String("psi/classic")); - loadCurrent(); - } else if (success) { + bool startedLoading = curLoadingTheme.load(style, [this, loadedId, loadedStyle, newClassic](bool success) { + auto t = curLoadingTheme; + curLoadingTheme = {}; + if (success) { curTheme = t; - if (t.id() != loadedId) { + if (t.id() != loadedId || t.style() != loadedStyle) { emit themeChanged(); } - } // else it was already classic + return; + } + if (t.id() != newClassic) { + qDebug("Failed to load theme \"%s\"", qPrintable(t.id())); + qDebug("fallback to classic chatview theme"); + PsiOptions::instance()->setOption(optionString(), newClassic); + loadCurrent(); + } else { + // nowhere to fallback + emit currentLoadFailed(); + } }); - return startedLoading; // does not really matter. may fail later on loading + if (startedLoading) { + return LoadInProgress; // does not really matter. may fail later on loading + } + return LoadFailure; } void ChatViewThemeProvider::unloadCurrent() { curTheme = Theme(); } +void ChatViewThemeProvider::cancelCurrentLoading() { curLoadingTheme = {}; /* should call destructor */ } + Theme ChatViewThemeProvider::current() const { return curTheme; } -void ChatViewThemeProvider::setCurrentTheme(const QString &id) +void ChatViewThemeProvider::setCurrentTheme(const QString &id, const QString &style) { PsiOptions::instance()->setOption(optionString(), id); - if (!curTheme.isValid() || curTheme.id() != id) { + PsiOptions::instance()->setOption(optionString() + QLatin1String("-style"), style); + if (!curTheme.isValid() || curTheme.id() != id || curTheme.style() != style) { loadCurrent(); } } diff --git a/src/chatviewthemeprovider.h b/src/chatviewthemeprovider.h index 1911a5ff5f..5bf08327cc 100644 --- a/src/chatviewthemeprovider.h +++ b/src/chatviewthemeprovider.h @@ -33,32 +33,31 @@ class ChatViewThemeProvider : public PsiThemeProvider { public: ChatViewThemeProvider(PsiCon *); - const char *type() const { return "chatview"; } - const QStringList themeIds() const; - Theme theme(const QString &id); + const char *type() const override { return "chatview"; } + const QStringList themeIds() const override; + Theme theme(const QString &id) override; - bool loadCurrent(); - void unloadCurrent(); - Theme current() const; // currently loaded theme + LoadRestult loadCurrent() override; + void unloadCurrent() override; + void cancelCurrentLoading() override; + Theme current() const override; // currently loaded theme - void setCurrentTheme(const QString &); - virtual int screenshotWidth() const { return 512; } // hack + void setCurrentTheme(const QString &, const QString &style = {}) override; + virtual int screenshotWidth() const override { return 512; } // hack #ifdef WEBENGINE QWebEngineUrlRequestInterceptor *requestInterceptor(); #endif - QString optionsName() const { return tr("Chat Message Style"); } - QString optionsDescription() const { return tr("Configure your chat theme here"); } + QString optionsName() const override { return tr("Chat Message Style"); } + QString optionsDescription() const override { return tr("Configure your chat theme here"); } protected: virtual const char *optionString() const { return "options.ui.chat.theme"; } -signals: - void themeChanged(); - private: Theme curTheme; + Theme curLoadingTheme; // load-in-progress theme to replace cutTheme in success }; class GroupChatViewThemeProvider : public ChatViewThemeProvider { diff --git a/src/coloropt.cpp b/src/coloropt.cpp index 12f0789457..57cb1f0e76 100644 --- a/src/coloropt.cpp +++ b/src/coloropt.cpp @@ -32,31 +32,35 @@ ColorOpt::ColorOpt() : QObject(nullptr) const char *opt; QPalette::ColorRole role; } SourceType; - SourceType source[] = { { "contactlist.status.online", QPalette::Text }, - { "contactlist.status.offline", QPalette::Text }, - { "contactlist.status.away", QPalette::Text }, - { "contactlist.status.do-not-disturb", QPalette::Text }, - { "contactlist.profile.header-foreground", QPalette::Text }, - { "contactlist.profile.header-background", QPalette::Dark }, - { "contactlist.grouping.header-foreground", QPalette::Text }, - { "contactlist.grouping.header-background", QPalette::Base }, - { "contactlist.background", QPalette::Base }, - { "contactlist.status-change-animation1", QPalette::Text }, - { "contactlist.status-change-animation2", QPalette::Base }, - { "contactlist.status-messages", QPalette::Text }, - { "tooltip.background", QPalette::ToolTipBase }, - { "tooltip.text", QPalette::ToolTipText }, - { "messages.received", QPalette::Text }, - { "messages.sent", QPalette::Text }, - { "messages.informational", QPalette::Text }, - { "messages.usertext", QPalette::Text }, - { "messages.highlighting", QPalette::Text }, - { "messages.link", QPalette::Link }, - { "messages.link-visited", QPalette::Link }, - { "passive-popup.border", QPalette::Window } }; - for (unsigned int i = 0; i < sizeof(source) / sizeof(SourceType); i++) { - QString opt = QString("options.ui.look.colors.%1").arg(source[i].opt); - colors.insert(opt, ColorData(PsiOptions::instance()->getOption(opt).value(), source[i].role)); + auto source = std::to_array({ { "contactlist.status.online", QPalette::Text }, + { "contactlist.status.offline", QPalette::Text }, + { "contactlist.status.away", QPalette::Text }, + { "contactlist.status.do-not-disturb", QPalette::Text }, + { "contactlist.profile.header-foreground", QPalette::Text }, + { "contactlist.profile.header-background", QPalette::Dark }, + { "contactlist.grouping.header-foreground", QPalette::Text }, + { "contactlist.grouping.header-background", QPalette::Base }, + { "contactlist.background", QPalette::Base }, + { "contactlist.status-change-animation1", QPalette::Text }, + { "contactlist.status-change-animation2", QPalette::Base }, + { "contactlist.status-messages", QPalette::Text }, + { "muc.role-moderator", QPalette::Text }, + { "muc.role-participant", QPalette::Text }, + { "muc.role-visitor", QPalette::Text }, + { "muc.role-norole", QPalette::Text }, + { "tooltip.background", QPalette::ToolTipBase }, + { "tooltip.text", QPalette::ToolTipText }, + { "messages.received", QPalette::Text }, + { "messages.sent", QPalette::Text }, + { "messages.informational", QPalette::Text }, + { "messages.usertext", QPalette::Text }, + { "messages.highlighting", QPalette::Text }, + { "messages.link", QPalette::Link }, + { "messages.link-visited", QPalette::Link }, + { "passive-popup.border", QPalette::Window } }); + for (const auto &item : source) { + QString opt = QString("options.ui.look.colors.%1").arg(item.opt); + colors.insert(opt, ColorData(PsiOptions::instance()->getOption(opt).value(), item.role)); } } diff --git a/src/common.cpp b/src/common.cpp index 622d6695a9..e6680f357d 100644 --- a/src/common.cpp +++ b/src/common.cpp @@ -24,11 +24,8 @@ #endif #include "activity.h" #include "applicationinfo.h" -#include "profiles.h" -#include "psievent.h" #include "psiiconset.h" #include "psioptions.h" -#include "rtparse.h" #include "tabdlg.h" #ifdef HAVE_X11 #include "x11windowsystem.h" @@ -45,12 +42,8 @@ #include #include #include -#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) -#include -#else -#include -#endif #include +#include #include #include @@ -93,7 +86,7 @@ QString clipStatus(const QString &str, int width, int height) // only take the first "width" chars QString line; bool hasNewline = false; - for (int n = 0; at < len; ++n, ++at) { + for (; at < len; ++at) { if (str.at(at) == '\n') { hasNewline = true; break; @@ -260,54 +253,61 @@ bool fileCopy(const QString &src, const QString &dest) return true; } -/** Detect default player helper on unix like systems - */ -QString soundDetectPlayer() +static QString existingFullResourcePath(const QString &str) { - // prefer ALSA on linux - if (QFile("/proc/asound").exists()) { - return "aplay -q"; + QString fullPath = str; + if (QDir::isRelativePath(str)) { + fullPath = ApplicationInfo::resourcesDir() + '/' + str; } - // fallback to "play" - return "play"; + + if (!QFile::exists(fullPath)) { + return {}; + } + return fullPath; } void soundPlay(const QString &s) { - if (s.isEmpty()) + if (s.isEmpty() || s.startsWith('-')) return; - QString str = s; - if (str == "!beep") { + if (s == QLatin1String("!beep")) { QApplication::beep(); return; } - if (QDir::isRelativePath(str)) { - str = ApplicationInfo::resourcesDir() + '/' + str; + QString fullPath; +#if !(defined(Q_OS_WIN) || defined(Q_OS_MAC)) + QString player = PsiOptions::instance() + ->getOption("options.ui.notifications.sounds.unix-sound-player") + .toString() + .simplified(); + if (!player.isEmpty()) { + auto fullPath = existingFullResourcePath(s); + if (fullPath.isEmpty()) { + return; + } + QStringList args = player.split(' '); + args += fullPath; + QString prog = args.takeFirst(); + if (QProcess::startDetached(prog, args)) { + return; + } else { + qWarning("failed to play with %s. Falling back to sound effect", qUtf8Printable(player)); + } } +#endif - if (!QFile::exists(str)) { - return; + static QHash effects; + auto effect = effects.value(s); + if (!effect) { + if (fullPath.isEmpty() && (fullPath = existingFullResourcePath(s)).isEmpty()) { + return; + } + effect = effects[s] = new QSoundEffect(qApp); + effect->setSource(QUrl::fromLocalFile(fullPath)); } - -#if defined(Q_OS_WIN) || defined(Q_OS_MAC) -#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) - QSound::play(str); -#else - QSoundEffect effect; - effect.setSource(QUrl::fromLocalFile(str)); - effect.play(); -#endif -#else - QString player = PsiOptions::instance()->getOption("options.ui.notifications.sounds.unix-sound-player").toString(); - if (player == "") - player = soundDetectPlayer(); - QStringList args = player.split(' '); - args += str; - QString prog = args.takeFirst(); - QProcess::startDetached(prog, args); -#endif + effect->play(); } bool lastPriorityNotEmpty() @@ -469,7 +469,7 @@ void clearMenu(QMenu *m) bool isKde() { return qgetenv("XDG_SESSION_DESKTOP") == "KDE" || qgetenv("DESKTOP_SESSION").endsWith("plasma") - || qgetenv("DESKTOP_SESSION").endsWith("plasma5"); + || qgetenv("DESKTOP_SESSION").endsWith("plasma5") || qgetenv("DESKTOP_SESSION").endsWith("plasmawayland"); } void bringToFront(QWidget *widget, bool) @@ -481,6 +481,12 @@ void bringToFront(QWidget *widget, bool) X11WindowSystem::instance()->bringToFront(w); #endif + // dirty hack to bring window to front in wayland desktop session + if (qApp->platformName() == "wayland" && qApp->applicationState() & Qt::ApplicationInactive) { + w->setWindowFlags(w->windowFlags() | Qt::WindowStaysOnTopHint); + w->setWindowFlags(w->windowFlags() & ~Qt::WindowStaysOnTopHint); + } + if (w->isMaximized()) { w->showMaximized(); } else { @@ -489,6 +495,7 @@ void bringToFront(QWidget *widget, bool) // if(grabFocus) // w->setActiveWindow(); + w->raise(); w->activateWindow(); diff --git a/src/common.h b/src/common.h index 8a8f08c367..03364bf90c 100644 --- a/src/common.h +++ b/src/common.h @@ -165,7 +165,7 @@ TabbableWidget *findActiveTab(); #include "x11windowsystem.h" #define X11WM_CLASS(x) \ { \ - X11WindowSystem::instance()->x11wmClass(winId(), (x)); \ + X11WindowSystem::instance()->x11wmClass(this, (x)); \ }; #else #define X11WM_CLASS(x) /* dummy */ diff --git a/src/contactlistaccountmenu.cpp b/src/contactlistaccountmenu.cpp index e79368c040..8993c70085 100644 --- a/src/contactlistaccountmenu.cpp +++ b/src/contactlistaccountmenu.cpp @@ -33,6 +33,7 @@ #include "psioptions.h" #include +#include class ContactListAccountMenu::Private : public QObject { Q_OBJECT @@ -209,10 +210,19 @@ private slots: if (account->bookmarkManager()->isAvailable()) { bookmarksManageAction_->setEnabled(true); bookmarksMenu_->addSeparator(); - for (const ConferenceBookmark &c : account->bookmarkManager()->conferences()) { + using SortableItem = std::pair; + auto const &conferences = account->bookmarkManager()->conferences(); + + std::vector sorted; + sorted.reserve(conferences.size()); + for (auto const &c : conferences) { + sorted.emplace_back(c.name().isEmpty() ? c.jid().bare() : c.name().toLower(), &c); + } + std::ranges::sort(sorted); + for (const auto &cp : sorted) { + auto c = *std::get<1>(cp); QAction *joinAction = new QAction(c.name(), this); - joinAction->setProperty("bookmark", bookmarksJoinActions_.count()); - connect(joinAction, SIGNAL(triggered()), SLOT(bookmarksJoin())); + connect(joinAction, &QAction::triggered, account, [this, c]() { account->actionJoin(c, true); }); bookmarksMenu_->addAction(joinAction); bookmarksJoinActions_ << joinAction; } @@ -307,16 +317,6 @@ private slots: w->show(); } - void bookmarksJoin() - { - if (!account) - return; - - QAction *joinAction = static_cast(sender()); - ConferenceBookmark c = account->bookmarkManager()->conferences()[joinAction->property("bookmark").toInt()]; - account->actionJoin(c, true); - } - void addContact() { if (!account) diff --git a/src/contactlistgroupmenu.cpp b/src/contactlistgroupmenu.cpp index a1ad8082e6..3603874fa9 100644 --- a/src/contactlistgroupmenu.cpp +++ b/src/contactlistgroupmenu.cpp @@ -146,7 +146,7 @@ void ContactListGroupMenu::Private::mucLeave() Jid j(gc); GCMainDlg *gcDlg = pa->findDialog(j.bare()); if (gcDlg) - gcDlg->close(); + gcDlg->leave(); } } diff --git a/src/contactlistmodel.cpp b/src/contactlistmodel.cpp index 7b2ca909d2..e37946a911 100644 --- a/src/contactlistmodel.cpp +++ b/src/contactlistmodel.cpp @@ -119,11 +119,11 @@ void ContactListModel::Private::realAddContact(PsiContact *contact) monitoredContacts.insert(contact, q->toModelIndex(item)); } - connect(contact, SIGNAL(destroyed(PsiContact *)), SLOT(removeContact(PsiContact *))); - connect(contact, SIGNAL(groupsChanged()), SLOT(contactGroupsChanged())); - connect(contact, SIGNAL(updated()), SLOT(contactUpdated())); - connect(contact, SIGNAL(alert()), SLOT(contactUpdated())); - connect(contact, SIGNAL(anim()), SLOT(contactUpdated())); + connect(contact, &PsiContact::destroyed, this, &Private::removeContact); + connect(contact, &PsiContact::groupsChanged, this, &Private::contactGroupsChanged); + connect(contact, &PsiContact::updated, this, &Private::contactUpdated); + connect(contact, &PsiContact::alert, this, &Private::contactUpdated); + connect(contact, &PsiContact::anim, this, &Private::contactUpdated); } void ContactListModel::Private::addContacts(const QList &contacts) diff --git a/src/contactlistview.cpp b/src/contactlistview.cpp index 36fe70238f..d375032da5 100644 --- a/src/contactlistview.cpp +++ b/src/contactlistview.cpp @@ -27,6 +27,7 @@ #include "psioptions.h" #include "psitooltip.h" +#include #include #include #include @@ -36,6 +37,7 @@ #include #include #include +#include ContactListView::ContactListView(QWidget *parent) : HoverableTreeView(parent), contextMenuActive_(false) { @@ -118,6 +120,12 @@ void ContactListView::updateContextMenu() if (item) { contextMenu_ = createContextMenuFor(item); addContextMenuActions(); + + if (qApp->platformName() == QLatin1String("wayland")) { + // see https://bugs.kde.org/show_bug.cgi?id=453532 + contextMenu_->winId(); + contextMenu_->windowHandle()->setTransientParent(window()->windowHandle()); + } } } } @@ -318,7 +326,12 @@ void ContactListView::activate(const QModelIndex &index) { itemActivated(index); void ContactListView::itemActivated(const QModelIndex &index) { - model()->setData(index, QVariant(true), ContactListModel::ActivateRole); + if (activateAction == Activate) { + model()->setData(index, QVariant(true), ContactListModel::ActivateRole); + } else { + emit contactSelected( + model()->data(index, ContactListModel::ContactListItemRole).value()->contact()); + } } static QAbstractItemModel *realModel(QAbstractItemModel *model) diff --git a/src/contactlistview.h b/src/contactlistview.h index 1a285e5108..dc445d6b6e 100644 --- a/src/contactlistview.h +++ b/src/contactlistview.h @@ -29,11 +29,14 @@ class ContactListItemMenu; class ContactListModel; class QLineEdit; class QWidget; +class PsiContact; class ContactListView : public HoverableTreeView { Q_OBJECT public: + enum ActivateAction { Activate, SignalSelected }; + ContactListView(QWidget *parent = nullptr); ContactListModel *realModel() const; @@ -47,6 +50,7 @@ class ContactListView : public HoverableTreeView { void activate(const QModelIndex &index); void toggleExpandedState(const QModelIndex &index); void ensureVisible(const QModelIndex &index); + void setActivateAction(ActivateAction action) { activateAction = action; } // reimplemented void setModel(QAbstractItemModel *model) override; @@ -58,6 +62,7 @@ public slots: void realExpanded(const QModelIndex &); void realCollapsed(const QModelIndex &); void modelItemsUpdated(); + void contactSelected(PsiContact *); protected: // reimplemented @@ -98,6 +103,7 @@ protected slots: private: QPointer contextMenu_; bool contextMenuActive_; + ActivateAction activateAction = Activate; }; #endif // CONTACTLISTVIEW_H diff --git a/src/contactlistviewdelegate.cpp b/src/contactlistviewdelegate.cpp index efb39da360..50e2e75202 100644 --- a/src/contactlistviewdelegate.cpp +++ b/src/contactlistviewdelegate.cpp @@ -75,9 +75,13 @@ static const QString animation1ColorPath(QStringLiteral("options.ui.look.colors. static const QString animation2ColorPath(QStringLiteral("options.ui.look.colors.contactlist.status-change-animation2")); static const QString statusMessageColorPath(QStringLiteral("options.ui.look.colors.contactlist.status-messages")); static const QString - headerBackgroungColorPath(QStringLiteral("options.ui.look.colors.contactlist.grouping.header-background")); + accountHeaderBackgroundColorPath(QStringLiteral("options.ui.look.colors.contactlist.profile.header-background")); static const QString - headerForegroungColorPath(QStringLiteral("options.ui.look.colors.contactlist.grouping.header-foreground")); + accountHeaderForegroundColorPath(QStringLiteral("options.ui.look.colors.contactlist.profile.header-foreground")); +static const QString + groupHeaderBackgroundColorPath(QStringLiteral("options.ui.look.colors.contactlist.grouping.header-background")); +static const QString + groupHeaderForegroundColorPath(QStringLiteral("options.ui.look.colors.contactlist.grouping.header-foreground")); static QRect relativeRect(const QStyleOption &option, const QSize &size, const QRect &prevRect, int padding = 0) { @@ -113,13 +117,7 @@ static QRect relativeRect(const QStyleOption &option, const QSize &size, const Q ContactListViewDelegate::Private::Private(ContactListViewDelegate *parent, ContactListView *contactList) : QObject(), q(parent), contactList(contactList), horizontalMargin_(5), verticalMargin_(3), statusIconSize_(0), avatarRadius_(0), alertTimer_(new QTimer(this)), animTimer(new QTimer(this)), fontMetrics_(QFont()), - statusFontMetrics_(QFont()), statusSingle_(false), showStatusMessages_(false), slimGroup_(false), - outlinedGroup_(false), showClientIcons_(false), showMoodIcons_(false), showActivityIcons_(false), - showGeolocIcons_(false), showTuneIcons_(false), showAvatars_(false), useDefaultAvatar_(false), avatarAtLeft_(false), - showStatusIcons_(false), statusIconsOverAvatars_(false), enableGroups_(false), allClients_(false), animPhase(false), - _awayColor(QColor()), _dndColor(QColor()), _offlineColor(QColor()), _onlineColor(QColor()), - _animation1Color(QColor()), _animation2Color(QColor()), _statusMessageColor(QColor()), - _headerBackgroundColor(QColor()), _headerForegroundColor(QColor()) + statusFontMetrics_(QFont()) { alertTimer_->setInterval(ALERT_INTERVAL); alertTimer_->setSingleShot(false); @@ -311,13 +309,23 @@ void ContactListViewDelegate::Private::colorOptionChanged(const QString &option) if (showStatusMessages_ && statusSingle_) updateViewPort = true; } - if (bulkUpdate || (!updated && option == headerBackgroungColorPath)) { - _headerBackgroundColor = ColorOpt::instance()->color(headerBackgroungColorPath); - updated = true; - updateViewPort = true; + if (bulkUpdate || (!updated && option == accountHeaderBackgroundColorPath)) { + _accountHeaderBackgroundColor = ColorOpt::instance()->color(accountHeaderBackgroundColorPath); + updated = true; + updateViewPort = true; + } + if (bulkUpdate || (!updated && option == accountHeaderForegroundColorPath)) { + _accountHeaderForegroundColor = ColorOpt::instance()->color(accountHeaderForegroundColorPath); + updated = true; + updateViewPort = true; + } + if (bulkUpdate || (!updated && option == groupHeaderBackgroundColorPath)) { + _groupHeaderBackgroundColor = ColorOpt::instance()->color(groupHeaderBackgroundColorPath); + updated = true; + updateViewPort = true; } - if (bulkUpdate || (!updated && option == headerForegroungColorPath)) { - _headerForegroundColor = ColorOpt::instance()->color(headerForegroungColorPath); + if (bulkUpdate || (!updated && option == groupHeaderForegroundColorPath)) { + _groupHeaderForegroundColor = ColorOpt::instance()->color(groupHeaderForegroundColorPath); // updated = true; updateViewPort = true; } @@ -943,15 +951,15 @@ void ContactListViewDelegate::Private::drawGroup(QPainter *painter, const QModel o.fontMetrics = fontMetrics_; QPalette palette = o.palette; if (!slimGroup_) - palette.setColor(QPalette::Base, _headerBackgroundColor); - palette.setColor(QPalette::Text, _headerForegroundColor); + palette.setColor(QPalette::Base, _groupHeaderBackgroundColor); + palette.setColor(QPalette::Text, _groupHeaderForegroundColor); o.palette = palette; drawBackground(painter, o, index); QRect r = opt.rect; if (!slimGroup_ && outlinedGroup_) { - painter->setPen(QPen(_headerForegroundColor)); + painter->setPen(QPen(_groupHeaderForegroundColor)); QRect gr(r); int s = painter->pen().width(); gr.adjust(0, 0, -s, -s); @@ -979,7 +987,7 @@ void ContactListViewDelegate::Private::drawGroup(QPainter *painter, const QModel #else int x = r.left() + fontMetrics_.width(text) + 8; #endif - painter->setPen(QPen(_headerBackgroundColor, 2)); + painter->setPen(QPen(_groupHeaderBackgroundColor, 2)); painter->drawLine(x, h, r.right(), h); } } @@ -990,14 +998,14 @@ void ContactListViewDelegate::Private::drawAccount(QPainter *painter, const QMod o.font = font_; o.fontMetrics = fontMetrics_; QPalette palette = o.palette; - palette.setColor(QPalette::Base, _headerBackgroundColor); - palette.setColor(QPalette::Text, _headerForegroundColor); + palette.setColor(QPalette::Base, _accountHeaderBackgroundColor); + palette.setColor(QPalette::Text, _accountHeaderForegroundColor); o.palette = palette; drawBackground(painter, o, index); if (outlinedGroup_) { - painter->setPen(QPen(_headerForegroundColor)); + painter->setPen(QPen(_accountHeaderForegroundColor)); QRect r(opt.rect); int s = painter->pen().width(); r.adjust(0, 0, -s, -s); diff --git a/src/contactlistviewdelegate_p.h b/src/contactlistviewdelegate_p.h index d45031cf1d..c88ac8edeb 100644 --- a/src/contactlistviewdelegate_p.h +++ b/src/contactlistviewdelegate_p.h @@ -77,16 +77,32 @@ public slots: int statusIconSize_; int avatarRadius_; - QTimer *alertTimer_; - QTimer *animTimer; - QFont font_, statusFont_; - QFontMetrics fontMetrics_, statusFontMetrics_; - bool statusSingle_; - bool showStatusMessages_, slimGroup_, outlinedGroup_, showClientIcons_, showMoodIcons_, showActivityIcons_, - showGeolocIcons_, showTuneIcons_; - bool showAvatars_, useDefaultAvatar_, avatarAtLeft_, showStatusIcons_, statusIconsOverAvatars_; - bool enableGroups_, allClients_; - bool animPhase; + QTimer *alertTimer_ = nullptr; + QTimer *animTimer = nullptr; + QFont font_; + QFont statusFont_; + + QFontMetrics fontMetrics_; + QFontMetrics statusFontMetrics_; + + bool statusSingle_ = false; + bool showStatusMessages_ = false; + bool slimGroup_ = false; + bool outlinedGroup_ = false; + bool showClientIcons_ = false; + bool showMoodIcons_ = false; + bool showActivityIcons_ = false; + bool showGeolocIcons_ = false; + bool showTuneIcons_ = false; + bool showAvatars_ = false; + bool useDefaultAvatar_ = false; + bool avatarAtLeft_ = false; + bool showStatusIcons_ = false; + bool statusIconsOverAvatars_ = false; + bool enableGroups_ = false; + bool allClients_ = false; + bool animPhase = false; + mutable QSet alertingIndexes; mutable QSet animIndexes; @@ -98,8 +114,10 @@ public slots: QColor _animation1Color; QColor _animation2Color; QColor _statusMessageColor; - QColor _headerBackgroundColor; - QColor _headerForegroundColor; + QColor _accountHeaderBackgroundColor; + QColor _accountHeaderForegroundColor; + QColor _groupHeaderBackgroundColor; + QColor _groupHeaderForegroundColor; // Geometry QRect contactBoundingRect_; diff --git a/src/contactmanager/contactmanagerdlg.cpp b/src/contactmanager/contactmanagerdlg.cpp index 461e79b687..e1b7c542c6 100644 --- a/src/contactmanager/contactmanagerdlg.cpp +++ b/src/contactmanager/contactmanagerdlg.cpp @@ -43,8 +43,12 @@ ContactManagerDlg::ContactManagerDlg(PsiAccount *pa) : QDialog(nullptr, Qt::Wind setWindowIcon(IconsetFactory::icon("psi/action_contacts_manager").icon()); um = new ContactManagerModel(this, pa_); - um->reloadUsers(); - ui_.usersView->setModel(um); + connect(um, &ContactManagerModel::modelReset, this, [this]() { ui_.usersView->viewport()->update(); }); + + auto proxy = new QSortFilterProxyModel(this); + proxy->setSourceModel(um); + + ui_.usersView->setModel(proxy); ui_.usersView->init(); ui_.cbAction->addItem(IconsetFactory::icon("psi/sendMessage").icon(), tr("Message"), 1); @@ -65,8 +69,6 @@ ContactManagerDlg::ContactManagerDlg(PsiAccount *pa) : QDialog(nullptr, Qt::Wind connect(ui_.btnExecute, SIGNAL(clicked()), this, SLOT(executeCurrent())); connect(ui_.btnSelect, SIGNAL(clicked()), this, SLOT(doSelect())); - connect(pa_->client(), SIGNAL(rosterRequestFinished(bool, int, QString)), this, - SLOT(client_rosterUpdated(bool, int, QString))); connect(ui_.usersView, &ContactManagerView::doubleClicked, this, [this](const QModelIndex &index) { ContactManagerView *v = ui_.usersView; bool itemCS = v->model()->data(v->model()->index(index.row(), 0), Qt::CheckStateRole) == Qt::Checked; @@ -100,17 +102,17 @@ void ContactManagerDlg::doSelect() void ContactManagerDlg::executeCurrent() { - int action = ui_.cbAction->itemData(ui_.cbAction->currentIndex()).toInt(); - QList users = um->checkedUsers(); - if (!users.count() && action != 9) { + int action = ui_.cbAction->itemData(ui_.cbAction->currentIndex()).toInt(); + auto users = um->checkedUsers(); + if (!users.size() && action != 9) { return; } switch (action) { case 1: // message { QList list; - for (UserListItem *u : users) { - list.append(u->jid().full()); + for (auto const &item : users) { + list.append(item.jid().full()); } pa_->actionSendMessage(list); } break; @@ -122,41 +124,36 @@ void ContactManagerDlg::executeCurrent() != QMessageBox::Yes) { return; } - um->startBatch(); - for (UserListItem *u : users) { - if (u->isTransport() && !Jid(pa_->client()->host()).compare(u->jid())) { - JT_UnRegister *ju = new JT_UnRegister(pa_->client()->rootTask()); - ju->unreg(u->jid()); - ju->go(true); - } - JT_Roster *r = new JT_Roster(pa_->client()->rootTask()); - r->remove(u->jid()); - r->go(true); - } - um->clear(); - pa_->client()->rosterRequest(); + um->removeUsers(users); } break; case 3: // Auth request - for (UserListItem *u : users) { - pa_->dj_authReq(u->jid()); + for (auto const &item : users) { + pa_->dj_authReq(item.jid()); } break; case 4: // Auth grant - for (UserListItem *u : users) { - pa_->dj_auth(u->jid()); + for (auto const &item : users) { + pa_->dj_auth(item.jid()); } break; case 5: // change domain - changeDomain(users); - break; + { + QString domain = ui_.edtActionParam->text(); + if (domain.size()) { + um->changeDomain(users, domain); + } else { + QMessageBox::warning(this, tr("Invalid"), tr("Please fill parameter field with new domain name")); + } + } break; case 6: // resolve nicks - for (UserListItem *u : std::as_const(users)) { - pa_->resolveContactName(u->jid()); + for (auto const &item : users) { + pa_->resolveContactName(item.jid()); } break; - case 7: - changeGroup(users); - break; + case 7: { + QStringList groups(ui_.cmbActionParam->currentText()); + um->changeGroups(users, groups); + } break; case 8: // export exportRoster(users); break; @@ -185,49 +182,7 @@ void ContactManagerDlg::showParamField(int index) } } -void ContactManagerDlg::changeDomain(QList &users) -{ - QString domain = ui_.edtActionParam->text(); - if (domain.size()) { - um->startBatch(); - um->clear(); - for (UserListItem *u : users) { - JT_Roster *r = new JT_Roster(pa_->client()->rootTask()); - if (!u->jid().node().isEmpty()) { - r->set(u->jid().withDomain(domain), u->name(), u->groups()); - r->remove(u->jid()); - } - r->go(true); - } - pa_->client()->rosterRequest(); - } else { - QMessageBox::warning(this, tr("Invalid"), tr("Please fill parameter field with new domain name")); - } -} - -void ContactManagerDlg::changeGroup(QList &users) -{ - QStringList groups(ui_.cmbActionParam->currentText()); - - for (UserListItem *u : users) { - JT_Roster *r = new JT_Roster(pa_->client()->rootTask()); - r->set(u->jid(), u->name(), groups); - r->go(true); - } -} - -void ContactManagerDlg::client_rosterUpdated(bool success, int statusCode, QString statusString) -{ - if (success) { - um->reloadUsers(); - } - ui_.usersView->viewport()->update(); - Q_UNUSED(statusCode); - Q_UNUSED(statusString); - um->stopBatch(); -} - -void ContactManagerDlg::exportRoster(QList &users) +void ContactManagerDlg::exportRoster(const ContactManagerModel::UserList &users) { QString fileName = QFileDialog::getSaveFileName(this, tr("Roster file"), QDir::homePath()); if (!fileName.isEmpty()) { @@ -235,13 +190,13 @@ void ContactManagerDlg::exportRoster(QList &users) QString nick; QDomElement root = doc.createElement("roster"); doc.appendChild(root); - for (UserListItem *u : users) { + for (auto u : std::as_const(users)) { QDomElement contact = root.appendChild(doc.createElement("contact")).toElement(); - contact.setAttribute("jid", u->jid().bare()); - for (const QString &group : u->groups()) { + contact.setAttribute("jid", u.jid().bare()); + for (const QString &group : u.groups()) { contact.appendChild(doc.createElement("group")).appendChild(doc.createTextNode(group)); } - nick = u->name(); + nick = u.name(); if (!nick.isEmpty()) { contact.appendChild(doc.createElement("nick")).appendChild(doc.createTextNode(nick)); } @@ -305,14 +260,11 @@ void ContactManagerDlg::importRoster() QMessageBox::Cancel | QMessageBox::Yes); confirmDlg.setDetailedText(labelContent.join("\n")); if (confirmDlg.exec() == QMessageBox::Yes) { - um->startBatch(); - um->clear(); for (const QString &jid : jids) { JT_Roster *r = new JT_Roster(pa_->client()->rootTask()); r->set(Jid(jid), nicks[jid], groups[jid]); r->go(true); } - pa_->client()->rosterRequest(); // через ж.., но пускай пока так. } file.close(); } diff --git a/src/contactmanager/contactmanagerdlg.h b/src/contactmanager/contactmanagerdlg.h index 2d72fc2ba2..f5efbb7546 100644 --- a/src/contactmanager/contactmanagerdlg.h +++ b/src/contactmanager/contactmanagerdlg.h @@ -42,9 +42,7 @@ class ContactManagerDlg : public QDialog { void changeEvent(QEvent *e) override; private: - void changeDomain(QList &users); - void changeGroup(QList &users); - void exportRoster(QList &users); + void exportRoster(const ContactManagerModel::UserList &users); void importRoster(); Ui::ContactManagerDlg ui_; @@ -55,7 +53,6 @@ private slots: void doSelect(); void executeCurrent(); void showParamField(int index); - void client_rosterUpdated(bool, int, QString); }; #endif // CONTACTMANAGERDLG_H diff --git a/src/contactmanager/contactmanagermodel.cpp b/src/contactmanager/contactmanagermodel.cpp index 848f117e25..1c73b37969 100644 --- a/src/contactmanager/contactmanagermodel.cpp +++ b/src/contactmanager/contactmanagermodel.cpp @@ -19,108 +19,227 @@ #include "contactmanagermodel.h" -#include "QDebug" #include "iris/xmpp_client.h" #include "iris/xmpp_tasks.h" #include "psiaccount.h" #include "userlist.h" -ContactManagerModel::ContactManagerModel(QObject *parent, PsiAccount *pa) : QAbstractTableModel(parent), pa_(pa) -{ - columnNames << "" << tr("Nick") << tr("Group") << tr("Node") << tr("Domain") << tr("Subscription"); - roles << CheckRole << NickRole << GroupRole << NodeRole << DomainRole << SubscriptionRole; - connect(pa_, SIGNAL(updateContact(UserListItem)), this, SLOT(view_contactUpdated(UserListItem))); - connect(pa_->client(), SIGNAL(rosterItemUpdated(const RosterItem &)), this, - SLOT(client_rosterItemUpdated(const RosterItem &))); -} +#include "iris/xmpp_liveroster.h" -void ContactManagerModel::reloadUsers() -{ - beginResetModel(); - clear(); - const UserList *ul = pa_->userList(); - for (UserListItem *u : *ul) { - if (u->inList()) { - addContact(u); +#include +#include + +class CMModelItem::Private : public QSharedData { +public: + Private(const RosterItem &item) : + jid(item.jid()), name(item.name()), groups(item.groups()), subscription(item.subscription()) + { + } + + Jid jid; + QString name; + QStringList groups; + Subscription subscription; +}; + +CMModelItem::CMModelItem(const RosterItem &item) : d(new Private(item)) { } + +CMModelItem::CMModelItem(const CMModelItem &other) : d(other.d) { } + +CMModelItem::~CMModelItem() = default; + +const Jid &CMModelItem::jid() const { return d->jid; } +const QString &CMModelItem::name() const { return d->name; } +const QStringList &CMModelItem::groups() const { return d->groups; } +const Subscription &CMModelItem::subscription() const { return d->subscription; } + +class ContactManagerModel::Private { + ContactManagerModel *q; + +public: + PsiAccount *pa; + UserList userList; + QStringList columnNames; + QList roles; + QSet checks; // TODO move it too CMModelItem. it's pointless to have it after refactoring + + Private(ContactManagerModel *q, PsiAccount *pa) : q(q), pa(pa) + { + columnNames << "" << tr("Nick") << tr("Group") << tr("Node") << tr("Domain") << tr("Subscription"); + roles << CheckRole << NickRole << GroupRole << NodeRole << DomainRole << SubscriptionRole; + + std::ranges::for_each(pa->client()->roster(), [this](const RosterItem &item) { userList.push_front(item); }); + + connect(pa->client(), &XMPP::Client::rosterItemAdded, q, + [this](const RosterItem &item) { contactAdded(item); }); + connect(pa->client(), &XMPP::Client::rosterItemRemoved, q, + [this](const RosterItem &item) { contactRemoved(item); }); + connect(pa->client(), &XMPP::Client::rosterItemUpdated, q, + [this](const RosterItem &item) { contactUpdated(item); }); + } + + QString userFieldString(const CMModelItem &u, ContactManagerModel::Role columnRole) const + { + QString data; + switch (columnRole) { + case NodeRole: // node + data = u.jid().node(); + break; + case DomainRole: // domain + data = u.jid().domain(); + break; + case NickRole: // nick + data = u.name(); + break; + case GroupRole: // group + if (u.groups().isEmpty()) { + data = ""; + } else { + data = u.groups().first(); + } + break; + case SubscriptionRole: // subscription + data = u.subscription().toString(); + break; + default: + break; + } + return data; + } + void removeUsers(const UserList &users) + { + for (const auto &item : users) { + if (item.jid().node().isEmpty() && !Jid(pa->client()->host()).compare(item.jid())) { + JT_UnRegister *ju = new JT_UnRegister(pa->client()->rootTask()); + ju->unreg(item.jid()); + ju->go(true); + } + JT_Roster *r = new JT_Roster(pa->client()->rootTask()); + r->remove(item.jid()); + r->go(true); + } + } + + void changeDomain(const UserList &users, const QString &domain) + { + auto items = findUsers(users); + for (auto [idx, item] : items) { + if (item.jid().node().isEmpty()) { + continue; + } + JT_Roster *r = new JT_Roster(pa->client()->rootTask()); + r->set(item.jid().withDomain(domain), item.name(), item.groups()); + r->go(true); + r = new JT_Roster(pa->client()->rootTask()); + r->remove(item.jid()); + r->go(true); + } + } + + void changeGroups(const UserList &users, const QStringList &groups) + { + auto items = findUsers(users); + for (auto [idx, item] : items) { + JT_Roster *r = new JT_Roster(pa->client()->rootTask()); + r->set(item.jid(), item.name(), groups); + r->go(true); } } - endResetModel(); -} -void ContactManagerModel::clear() +private: + QList> findUsers(const UserList &users) + { + QList> ret; + for (auto const &item : users) { + auto it = std::ranges::find_if(userList, [&item](const auto &u) { return u.jid() == item.jid(); }); + if (it != userList.end()) { + ret.push_back({ int(std::distance(userList.begin(), it)), *it }); + } + } + return ret; + } + + inline std::optional findRow(const CMModelItem &item) + { + auto it = std::ranges::find_if(userList, [&item](auto const &i) { return item.jid() == i.jid(); }); + if (it != userList.end()) { + return std::distance(userList.begin(), it); + } + return {}; + } + + void contactAdded(const RosterItem &ri) + { + q->beginInsertRows({}, int(userList.size()), int(userList.size())); + userList.push_back(ri); + q->endInsertRows(); + } + + void contactRemoved(const RosterItem &ri) + { + auto row = findRow(ri); + if (row) { + q->beginRemoveRows(QModelIndex(), *row, *row); + userList.erase(userList.begin() + *row); + q->endRemoveRows(); + } + } + + void contactUpdated(const RosterItem &ri) + { + auto row = findRow(ri); + if (row) { + emit q->dataChanged(q->index(*row, 1), q->index(*row, columnNames.count() - 1)); + } + } +}; + +ContactManagerModel::ContactManagerModel(QObject *parent, PsiAccount *pa) : + QAbstractTableModel(parent), d(new Private(this, pa)) { - _userList.clear(); - checks.clear(); } +ContactManagerModel::~ContactManagerModel() { qDebug("ContactManagerModel destroyed"); } + int ContactManagerModel::rowCount(const QModelIndex &parent) const { if (parent.isValid()) return 0; - return _userList.count(); + return int(d->userList.size()); } int ContactManagerModel::columnCount(const QModelIndex &parent) const { Q_UNUSED(parent); - return columnNames.count(); + return d->columnNames.count(); } QVariant ContactManagerModel::data(const QModelIndex &index, int role) const { - Role columnRole = roles[index.column()]; - UserListItem *u = _userList.at(index.row()); - if (u) { + Role columnRole = d->roles[index.column()]; + if (index.row() < d->userList.size()) { + auto u = d->userList.at(index.row()); switch (role) { case Qt::DisplayRole: - return userFieldString(u, columnRole); + return d->userFieldString(u, columnRole); case Qt::TextAlignmentRole: if (columnRole == CheckRole || columnRole == NodeRole) return int(Qt::AlignRight | Qt::AlignVCenter); break; case Qt::CheckStateRole: if (columnRole == CheckRole) { - return checks.contains(u->jid().full()) ? 2 : 0; + return d->checks.contains(u.jid().full()) ? 2 : 0; } } } return QVariant(); } -QString ContactManagerModel::userFieldString(UserListItem *u, ContactManagerModel::Role columnRole) const -{ - QString data; - switch (columnRole) { - case NodeRole: // node - data = u->jid().node(); - break; - case DomainRole: // domain - data = u->jid().domain(); - break; - case NickRole: // nick - data = u->name(); - break; - case GroupRole: // group - if (u->groups().isEmpty()) { - data = ""; - } else { - data = u->groups().first(); - } - break; - case SubscriptionRole: // subscription - data = u->subscription().toString(); - break; - default: - break; - } - return data; -} - QVariant ContactManagerModel::headerData(int section, Qt::Orientation orientation, int role) const { if (role == Qt::DisplayRole) { if (orientation == Qt::Horizontal) { - return columnNames[section]; + return d->columnNames[section]; } else { return section + 1; } @@ -130,17 +249,15 @@ QVariant ContactManagerModel::headerData(int section, Qt::Orientation orientatio QStringList ContactManagerModel::manageableFields() { - QStringList ret = columnNames; + QStringList ret = d->columnNames; ret.removeFirst(); return ret; } -void ContactManagerModel::addContact(UserListItem *u) { _userList.append(u); } - Qt::ItemFlags ContactManagerModel::flags(const QModelIndex &index) const { Qt::ItemFlags flags = Qt::ItemIsSelectable | Qt::ItemIsEnabled; - Role columnRole = roles[index.column()]; + Role columnRole = d->roles[index.column()]; if (columnRole == CheckRole) { flags |= (Qt::ItemIsUserCheckable); } @@ -151,25 +268,25 @@ bool ContactManagerModel::setData(const QModelIndex &index, const QVariant &valu { Q_UNUSED(role); if (index.isValid()) { - Role columnRole = roles[index.column()]; + Role columnRole = d->roles[index.column()]; if (columnRole == CheckRole) { - QString jid = _userList.at(index.row())->jid().full(); + QString jid = d->userList.at(index.row()).jid().full(); if (value.toInt() == 3) { // iversion - if (checks.contains(jid)) { - checks.remove(jid); + if (d->checks.contains(jid)) { + d->checks.remove(jid); } else { - checks.insert(jid); + d->checks.insert(jid); } emit dataChanged(index, index); } else { - if (checks.contains(jid)) { + if (d->checks.contains(jid)) { if (!value.toBool()) { - checks.remove(jid); + d->checks.remove(jid); emit dataChanged(index, index); } } else { if (value.toBool()) { - checks.insert(jid); + d->checks.insert(jid); emit dataChanged(index, index); } } @@ -179,61 +296,12 @@ bool ContactManagerModel::setData(const QModelIndex &index, const QVariant &valu return false; } -ContactManagerModel::Role ContactManagerModel::sortRole; -Qt::SortOrder ContactManagerModel::sortOrder = Qt::AscendingOrder; - -void ContactManagerModel::sort(int column, Qt::SortOrder order = Qt::AscendingOrder) -{ - if (column < 0) - return; - Role columnRole = roles[column]; - ContactManagerModel::sortRole = columnRole; - ContactManagerModel::sortOrder = order; - if (columnRole != CheckRole) { - emit layoutAboutToBeChanged(); - std::sort(_userList.begin(), _userList.end(), ContactManagerModel::sortLessThan); - emit layoutChanged(); - } -} - -bool ContactManagerModel::sortLessThan(UserListItem *u1, UserListItem *u2) +ContactManagerModel::UserList ContactManagerModel::checkedUsers() { - QString g1, g2; - bool result = false; - switch (ContactManagerModel::sortRole) { - case NodeRole: // node - result = u1->jid().node() < u2->jid().node(); - break; - case DomainRole: // domain - result = u1->jid().domain() < u2->jid().domain(); - break; - case NickRole: // nick - result = u1->name() < u2->name(); - break; - case GroupRole: // group - if (!u1->groups().isEmpty()) { - g1 = u1->groups().first(); - } - if (!u2->groups().isEmpty()) { - g2 = u2->groups().first(); - } - result = g1 < g2; - break; - case SubscriptionRole: // subscription - result = u1->subscription().toString() < u2->subscription().toString(); - break; - default: - break; - } - return ContactManagerModel::sortOrder == Qt::AscendingOrder ? result : !result; -} - -QList ContactManagerModel::checkedUsers() -{ - QList users; - for (UserListItem *u : std::as_const(_userList)) { - if (checks.contains(u->jid().full())) { - users.append(u); + UserList users; + for (auto u : d->userList) { + if (d->checks.contains(u.jid().full())) { + users.push_back(u); } } return users; @@ -242,38 +310,32 @@ QList ContactManagerModel::checkedUsers() void ContactManagerModel::invertByMatch(int columnIndex, int matchType, const QString &str) { emit layoutAboutToBeChanged(); - Role columnRole = roles[columnIndex]; + Role columnRole = d->roles[columnIndex]; QString data; QRegularExpression reg; if (matchType == ContactManagerModel::RegexpMatch) { reg = QRegularExpression(str); } - for (UserListItem *u : std::as_const(_userList)) { - data = userFieldString(u, columnRole); + for (auto u : d->userList) { + data = d->userFieldString(u, columnRole); if ((matchType == ContactManagerModel::SimpleMatch && str == data) || (matchType == ContactManagerModel::RegexpMatch && reg.match(data).hasMatch())) { - QString jid = u->jid().full(); - if (checks.contains(jid)) { - checks.remove(jid); + QString jid = u.jid().full(); + if (d->checks.contains(jid)) { + d->checks.remove(jid); } else { - checks.insert(jid); + d->checks.insert(jid); } } } emit layoutChanged(); } -void ContactManagerModel::view_contactUpdated(const UserListItem &u) { contactUpdated(u.jid()); } +void ContactManagerModel::removeUsers(const UserList &users) { d->removeUsers(users); } -void ContactManagerModel::client_rosterItemUpdated(const RosterItem &item) { contactUpdated(item.jid()); } +void ContactManagerModel::changeDomain(const UserList &users, const QString &domain) { d->changeDomain(users, domain); } -void ContactManagerModel::contactUpdated(const Jid &jid) +void ContactManagerModel::changeGroups(const UserList &users, const QStringList &groups) { - int i = 0; - for (UserListItem *lu : std::as_const(_userList)) { - if (lu->jid() == jid) { - emit dataChanged(index(i, 1), index(i, columnNames.count() - 1)); - } - i++; - } + d->changeGroups(users, groups); } diff --git a/src/contactmanager/contactmanagermodel.h b/src/contactmanager/contactmanagermodel.h index 6d90401e21..2f2514e02c 100644 --- a/src/contactmanager/contactmanagermodel.h +++ b/src/contactmanager/contactmanagermodel.h @@ -21,18 +21,38 @@ #define CONTACTMANAGERMODEL_H #include -#include +#include #include +#include +#include + class PsiAccount; class UserListItem; namespace XMPP { class Jid; class RosterItem; +class Subscription; } using namespace XMPP; +class CMModelItem { + class Private; + QExplicitlySharedDataPointer d; + +public: + CMModelItem(const RosterItem &item); + ~CMModelItem(); + + CMModelItem(const CMModelItem &other); + + const Jid &jid() const; + const QString &name() const; + const QStringList &groups() const; + const Subscription &subscription() const; +}; + class ContactManagerModel : public QAbstractTableModel { Q_OBJECT public: @@ -48,41 +68,29 @@ class ContactManagerModel : public QAbstractTableModel { const static int SimpleMatch = 1; const static int RegexpMatch = 2; + using UserList = std::deque; + ContactManagerModel(QObject *parent, PsiAccount *pa); + ~ContactManagerModel(); + int rowCount(const QModelIndex &parent = QModelIndex()) const override; int columnCount(const QModelIndex &parent = QModelIndex()) const override; QVariant data(const QModelIndex &index, int role) const override; QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; Qt::ItemFlags flags(const QModelIndex &index) const override; bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override; - void sort(int column, Qt::SortOrder order) override; - static bool sortLessThan(UserListItem *u1, UserListItem *u2); - static Role sortRole; - static Qt::SortOrder sortOrder; - QStringList manageableFields(); - void reloadUsers(); - void clear(); - void addContact(UserListItem *u); - QList checkedUsers(); - void invertByMatch(int columnIndex, int matchType, const QString &str); + QStringList manageableFields(); + UserList checkedUsers(); + void invertByMatch(int columnIndex, int matchType, const QString &str); - void startBatch() { emit layoutAboutToBeChanged(); } - void stopBatch() { emit layoutChanged(); } + void removeUsers(const UserList &users); + void changeDomain(const UserList &users, const QString &domain); + void changeGroups(const UserList &users, const QStringList &groups); private: - PsiAccount *pa_; - QList _userList; - QStringList columnNames; - QList roles; - QSet checks; - - QString userFieldString(UserListItem *u, ContactManagerModel::Role columnRole) const; - void contactUpdated(const Jid &); - -private slots: - void view_contactUpdated(const UserListItem &); - void client_rosterItemUpdated(const RosterItem &); + class Private; + std::unique_ptr d; }; #endif // CONTACTMANAGERMODEL_H diff --git a/src/dbus.cpp b/src/dbus.cpp index 2cdb5f368d..cb818a8089 100644 --- a/src/dbus.cpp +++ b/src/dbus.cpp @@ -28,6 +28,7 @@ public Q_SLOTS: void sleep(); void wake(); void recvNextEvent(); + void quit(); /*Q_SIGNALS: void psi_pong(); */ @@ -39,15 +40,15 @@ PsiConAdapter::PsiConAdapter(PsiCon *psicon_) : QDBusAbstractAdaptor(psicon_) { PsiConAdapter::~PsiConAdapter() { } -void PsiConAdapter::openURI(QString uri) { emit ActiveProfiles::instance() -> openUriRequested(uri); } +void PsiConAdapter::openURI(QString uri) { emit ActiveProfiles::instance()->openUriRequested(uri); } void PsiConAdapter::setStatus(QString status, QString message) { - emit ActiveProfiles::instance() -> setStatusRequested(status, message); + emit ActiveProfiles::instance()->setStatusRequested(status, message); } // FIXME libguniqueapp uses activate -void PsiConAdapter::raise() { emit ActiveProfiles::instance() -> raiseRequested(); } +void PsiConAdapter::raise() { emit ActiveProfiles::instance()->raiseRequested(); } void PsiConAdapter::sleep() { psicon->doSleep(); } @@ -61,4 +62,6 @@ void addPsiConAdapter(PsiCon *psicon) QDBusConnection::sessionBus().registerObject("/Main", psicon); } +void PsiConAdapter::quit() { emit ActiveProfiles::instance()->quitRequested(); } + #include "dbus.moc" diff --git a/src/discodlg.cpp b/src/discodlg.cpp index 4aa5f0c8ed..3e1d3d5c82 100644 --- a/src/discodlg.cpp +++ b/src/discodlg.cpp @@ -66,8 +66,6 @@ PsiIcon category2icon(PsiAccount *acc, const Jid &jid, const QString &category, if (type == "facebook") trans = "facebook"; - else if (type == "icq") - trans = "icq"; else if (type == "skype") trans = "skype"; else if (type == "vkontakte") @@ -873,11 +871,15 @@ class DiscoDlg::Private : public QObject { int itemsPerPage; + QString addressLastText; + QString nodeLastText; + public: // functions Private(DiscoDlg *parent, PsiAccount *pa); ~Private(); public slots: + void doDiscoUserInput(); void doDisco(QString host = QString(), QString node = QString(), bool doHistory = true); void actionStop(); @@ -1023,6 +1025,12 @@ DiscoDlg::Private::~Private() delete data.tasks; } +void DiscoDlg::Private::doDiscoUserInput() { + addressLastText = dlg->cb_address->currentText(); + nodeLastText = dlg->cb_node->currentText().trimmed(); + doDisco(addressLastText, nodeLastText, true); +} + void DiscoDlg::Private::doDisco(QString _host, QString _node, bool doHistory) { PsiAccount *pa = data.pa; @@ -1032,16 +1040,11 @@ void DiscoDlg::Private::doDisco(QString _host, QString _node, bool doHistory) // Strip whitespace Jid j; QString host = _host; - if (host.isEmpty()) - host = dlg->cb_address->currentText(); j = host.trimmed(); if (!j.isValid()) return; QString n = _node.trimmed(); - if (n.isEmpty()) - n = dlg->cb_node->currentText().trimmed(); - // check, whether we need to update history if ((jid.full() != j.full()) || (node != n)) { if (doHistory) { @@ -1078,8 +1081,10 @@ void DiscoDlg::Private::doDisco(QString _host, QString _node, bool doHistory) void DiscoDlg::Private::updateComboBoxes(Jid j, QString n) { - dlg->cb_address->blockSignals(true); - dlg->cb_node->blockSignals(true); + dlg->cb_address->disconnect(this); + dlg->cb_node->disconnect(this); + dlg->cb_address->lineEdit()->disconnect(this); + dlg->cb_node->lineEdit()->disconnect(this); data.pa->psi()->recentBrowseAdd(j.full()); dlg->cb_address->clear(); @@ -1089,8 +1094,7 @@ void DiscoDlg::Private::updateComboBoxes(Jid j, QString n) dlg->cb_node->clear(); dlg->cb_node->addItems(data.pa->psi()->recentNodeList()); - dlg->cb_address->blockSignals(false); - dlg->cb_node->blockSignals(false); + dlg->connectAddressNodeSignals(); } void DiscoDlg::Private::actionStop() { data.tasks->clear(); } @@ -1383,7 +1387,7 @@ DiscoDlg::DiscoDlg(PsiAccount *pa, const Jid &jid, const QString &node) : QDialo setWindowIcon(PsiIconset::instance()->transportStatus("transport", STATUS_ONLINE).icon()); X11WM_CLASS("disco"); - connect(pb_browse, SIGNAL(clicked()), d, SLOT(doDisco())); + connect(pb_browse, SIGNAL(clicked()), d, SLOT(doDiscoUserInput())); pb_browse->setDefault(false); pb_browse->setAutoDefault(false); @@ -1393,21 +1397,14 @@ DiscoDlg::DiscoDlg(PsiAccount *pa, const Jid &jid, const QString &node) : QDialo cb_address->addItems(pa->psi()->recentBrowseList()); // FIXME cb_address->setFocus(); - connect(cb_address->lineEdit(), &QLineEdit::editingFinished, d, - [this]() { d->doDisco(cb_address->currentText(), cb_node->currentText()); }); - connect(cb_address, qOverload(&QComboBox::currentIndexChanged), d, - [this](int) { d->doDisco(cb_address->currentText(), cb_node->currentText()); }); cb_address->setEditText(d->jid.full()); cb_node->addItems(pa->psi()->recentNodeList()); - connect(cb_node->lineEdit(), &QLineEdit::editingFinished, d, - [this]() { d->doDisco(cb_address->currentText(), cb_node->currentText()); }); - connect(cb_node, qOverload(&QComboBox::currentIndexChanged), d, - [this](int) { d->doDisco(cb_address->currentText(), cb_node->currentText()); }); cb_node->setCurrentIndex(cb_node->findText(node)); + //connectAddressNodeSignals(); if (pa->loggedIn()) - doDisco(); + d->doDiscoUserInput(); } DiscoDlg::~DiscoDlg() @@ -1422,6 +1419,34 @@ DiscoDlg::~DiscoDlg() bool(ck_autoInfo->isChecked())); } +void DiscoDlg::connectAddressNodeSignals() { + connect(cb_address->lineEdit(), &QLineEdit::editingFinished, d, + [this]() { + if (cb_address->currentText() != d->addressLastText) { + d->doDiscoUserInput(); + } + }); + connect(cb_address, qOverload(&QComboBox::currentIndexChanged), d, + [this](int) { + if (cb_address->currentText() != d->addressLastText) { + d->doDiscoUserInput(); + } + }); + + connect(cb_node->lineEdit(), &QLineEdit::editingFinished, d, + [this]() { + if (cb_node->currentText() != d->nodeLastText) { + d->doDiscoUserInput(); + } + }); + connect(cb_node, qOverload(&QComboBox::currentIndexChanged), d, + [this](int) { + if (cb_node->currentText() != d->nodeLastText) { + d->doDiscoUserInput(); + } + }); +} + void DiscoDlg::doDisco(QString host, QString node) { d->doDisco(host, node); } int DiscoDlg::itemsPerPage() const diff --git a/src/discodlg.h b/src/discodlg.h index 6e59ecdf7f..819873cd06 100644 --- a/src/discodlg.h +++ b/src/discodlg.h @@ -43,11 +43,11 @@ class DiscoDlg : public QDialog, public Ui::Disco { signals: void featureActivated(QString feature, Jid jid, QString node); -public: - class Private; - friend class Private; +private: + void connectAddressNodeSignals(); private: + class Private; Private *d; }; diff --git a/src/edbflatfile.cpp b/src/edbflatfile.cpp index deaa1ed032..8902c2dc5f 100644 --- a/src/edbflatfile.cpp +++ b/src/edbflatfile.cpp @@ -597,13 +597,13 @@ PsiEvent::Ptr EDBFlatFile::File::lineToEvent(const QString &line) Message m; m.setTimeStamp(QDateTime::fromString(strData.at(Time), Qt::ISODate)); if (type == 1) - m.setType("chat"); + m.setType(Message::Type::Chat); else if (type == 4) - m.setType("error"); + m.setType(Message::Type::Error); else if (type == 5) - m.setType("headline"); + m.setType(Message::Type::Headline); else - m.setType(""); + m.setType(Message::Type::Normal); bool originLocal = strData.at(Origin) == "to"; m.setFrom(j); @@ -665,11 +665,11 @@ QString EDBFlatFile::File::eventToLine(const PsiEvent::Ptr &e) sTime = m.timeStamp().toString(Qt::ISODate); int n = 0; - if (m.type() == "chat") + if (m.type() == Message::Type::Chat) n = 1; - else if (m.type() == "error") + else if (m.type() == Message::Type::Error) n = 4; - else if (m.type() == "headline") + else if (m.type() == Message::Type::Headline) n = 5; sType.setNum(n); sOrigin = e->originLocal() ? "to" : "from"; diff --git a/src/edbsqlite.cpp b/src/edbsqlite.cpp index e8ad1d3d56..093119eb39 100644 --- a/src/edbsqlite.cpp +++ b/src/edbsqlite.cpp @@ -414,11 +414,11 @@ bool EDBSqLite::appendEvent(const QString &accId, const XMPP::Jid &jid, const Ps MessageEvent::Ptr me = e.staticCast(); const Message &m = me->message().displayMessage(); dTime = m.timeStamp(); - if (m.type() == "chat") + if (m.type() == Message::Type::Chat) nType = 1; - else if (m.type() == "error") + else if (m.type() == Message::Type::Error) nType = 4; - else if (m.type() == "headline") + else if (m.type() == Message::Type::Headline) nType = 5; } else if (e->type() == PsiEvent::Auth) { @@ -498,13 +498,13 @@ PsiEvent::Ptr EDBSqLite::getEvent(const QSqlRecord &record) Message m; m.setTimeStamp(record.value("date").toDateTime()); if (type == 1) - m.setType("chat"); + m.setType(Message::Type::Chat); else if (type == 4) - m.setType("error"); + m.setType(Message::Type::Error); else if (type == 5) - m.setType("headline"); + m.setType(Message::Type::Headline); else - m.setType(""); + m.setType(Message::Type::Normal); m.setFrom(Jid(record.value("jid").toString())); QVariant text = record.value("m_text"); if (!text.isNull()) { diff --git a/src/eventdlg.cpp b/src/eventdlg.cpp index e6766c8748..30b7f0ae34 100644 --- a/src/eventdlg.cpp +++ b/src/eventdlg.cpp @@ -786,12 +786,14 @@ void EventDlg::init() d->w_http_id->hide(); // data form - d->xdata = new XDataWidget(d->psi, this, d->pa->client(), d->jid); - d->xdata_form = new QWidget(this); + d->xdata = new XDataWidget(d->psi, this, d->pa->client(), d->jid); + d->xdata_form = new QWidget(this); + d->xdata_form->setFocusProxy(d->xdata); QVBoxLayout *vb_xdata = new QVBoxLayout(d->xdata_form); d->xdata_instruction = new QLabel(d->xdata_form); vb_xdata->addWidget(d->xdata_instruction); vb_xdata->addWidget(d->xdata); + vb1->addStretch(); vb1->addWidget(d->xdata_form); d->xdata_form->hide(); @@ -1313,9 +1315,9 @@ void EventDlg::doSend() Message m; if (d->cb_type->currentIndex() == 0) - m.setType(""); + m.setType(Message::Type::Normal); else - m.setType("chat"); + m.setType(Message::Type::Chat); m.setBody(d->mle->getPlainText()); m.setSubject(d->le_subj->text()); @@ -1730,6 +1732,12 @@ void EventDlg::updateEvent(const PsiEvent::Ptr &e) // show data form d->xdata->setForm(form, false); + auto xdfLFW = d->xdata->lastFocusabelWidget(); + if (xdfLFW) { + setTabOrder(d->le_subj, d->xdata_form); + setTabOrder(xdfLFW, d->pb_form_submit); // FIXME doesn't work? why? + } + d->xdata_form->setFocus(); d->xdata_form->show(); // set instructions diff --git a/src/filecache.cpp b/src/filecache.cpp index 9bd1bbd715..902446cb71 100644 --- a/src/filecache.cpp +++ b/src/filecache.cpp @@ -307,8 +307,14 @@ bool ctimeLessThan(FileCacheItem *a, FileCacheItem *b) { return a->created() < b void FileCache::sync() { sync(false); } +void FileCache::lazySync() { _syncTimer->start(); } + void FileCache::sync(bool finishSession) { + if (_syncTimer->isActive()) { + _syncTimer->stop(); + } + QList loadedItems; QList onDiskItems; qint64 sumMemorySize = 0; @@ -333,8 +339,9 @@ void FileCache::sync(bool finishSession) sumFileSize += item->size(); onDiskItems.append(item); } - } else if (!item->isRegistered()) { // just put to registry item without data and stop reviewing it - toRegistry(item); // save item to registry if not yet + } + if (!item->isRegistered()) { // just put to registry item without data and stop reviewing it + toRegistry(item); // save item to registry if not yet } } @@ -351,9 +358,9 @@ void FileCache::sync(bool finishSession) } item->unload(); // will flush data to disk if necesary sumMemorySize -= item->size(); - if (!item->isRegistered()) { - toRegistry(item); // save item to registry if not yet - } + // if (!item->isRegistered()) { + // toRegistry(item); // save item to registry if not yet + // } } } diff --git a/src/filecache.h b/src/filecache.h index f9d2014c33..f22c5d8b43 100644 --- a/src/filecache.h +++ b/src/filecache.h @@ -187,6 +187,7 @@ class FileCache : public QObject { public slots: void sync(); + void lazySync(); private: void toRegistry(FileCacheItem *); diff --git a/src/filesharedlg.cpp b/src/filesharedlg.cpp index 65ba4030dc..d68bf17862 100644 --- a/src/filesharedlg.cpp +++ b/src/filesharedlg.cpp @@ -19,7 +19,6 @@ #include "filesharedlg.h" -#include "filecache.h" #include "filesharingmanager.h" #include "fileutil.h" #include "iris/httpfileupload.h" @@ -104,7 +103,9 @@ void FileShareDlg::publish() auto publisher = item->property("publisher").value(); if (publisher->isPublished()) { item->setState(MultiFileTransferModel::Done); - item->setCurrentSize(item->fullSize()); + if (item->fullSize()) { + item->setCurrentSize(*item->fullSize()); + } readyPublishers.append(publisher); return; } @@ -114,7 +115,9 @@ void FileShareDlg::publish() connect(publisher, &FileSharingItem::publishFinished, this, [this, publisher, item]() { if (publisher->uris().count()) { item->setState(MultiFileTransferModel::Done); - item->setCurrentSize(item->fullSize()); + if (item->fullSize()) { + item->setCurrentSize(*item->fullSize()); + } } else { item->setState(MultiFileTransferModel::Failed); hasFailures = true; @@ -152,7 +155,8 @@ void FileShareDlg::finish() if (uri.isValid()) { text = uri.toString(QUrl::FullyEncoded); } else { - text = QLatin1String("SIMS(") + i->mimeType() + ", " + QString::number(i->fileSize()) + "B, " + text = QLatin1String("SIMS(") + i->mimeType() + ", " + + (i->fileSize() ? (QString::number(*i->fileSize()) + "B, ") : "stream ") + tr("requires compliant client") + ")"; } QString refText = QString(" %1").arg(text); @@ -182,7 +186,7 @@ void FileShareDlg::shareFiles(PsiAccount *acc, const XMPP::Jid &myJid, const Cal void FileShareDlg::shareFiles(PsiAccount *acc, const Jid &myJid, const QMimeData *data, const Callback &callback, QWidget *parent) { - auto items = acc->psi()->fileSharingManager()->fromMimeData(data, acc); + auto items = acc->psi()->fileSharingManager()->createFromMimeData(data, acc); if (items.isEmpty()) return; auto dlg = new FileShareDlg(acc, myJid, items, callback, parent); diff --git a/src/filesharingdownloader.cpp b/src/filesharingdownloader.cpp index 7835c124ae..1f37c31b99 100644 --- a/src/filesharingdownloader.cpp +++ b/src/filesharingdownloader.cpp @@ -43,11 +43,12 @@ class AbstractFileShareDownloader : public QObject { Q_OBJECT protected: - QString _lastError; - qint64 rangeStart = 0; - qint64 rangeSize = 0; // 0 - all the remaining - PsiAccount *acc; - QString sourceUri; + QString _lastError; + std::optional _totalSize; // from response + std::optional _requestRange; + std::optional _responseRange; + PsiAccount *acc; + QString sourceUri; void downloadError(const QString &err) { @@ -55,7 +56,7 @@ class AbstractFileShareDownloader : public QObject { _lastError = err; qDebug("Jingle failed: %s", qPrintable(err)); } - QTimer::singleShot(0, this, &AbstractFileShareDownloader::failed); + emit failed(); } Jid selectOnlineJid(const QList &jids) const @@ -81,23 +82,18 @@ class AbstractFileShareDownloader : public QObject { { } - virtual void start() = 0; - virtual qint64 bytesAvailable() const = 0; - virtual qint64 read(char *data, qint64 maxSize) = 0; - virtual void abort(bool isFailure = false, const QString &reason = QString()) = 0; - virtual bool isConnected() const = 0; - virtual bool hasFileSize() const = 0; - virtual qint64 fileSize() const = 0; - virtual void close() { } + virtual void start() = 0; + virtual qint64 bytesAvailable() const = 0; + virtual qint64 read(char *data, qint64 maxSize) = 0; + virtual void abort(bool isFailure = false, const QString &reason = QString()) = 0; + virtual bool isConnected() const = 0; + virtual std::optional fileSize() const = 0; + virtual void close() { } inline const QString &lastError() const { return _lastError; } - void setRange(qint64 offset, qint64 length) - { - rangeStart = offset; - rangeSize = length; - } - inline bool isRanged() const { return rangeSize || rangeStart; } - std::tuple range() const { return std::tuple(rangeStart, rangeSize); } + void setRequestRange(const FileShareDownloader::Range &range) { _requestRange = range; } + const std::optional &requestRange() const { return _requestRange; } + const std::optional &responseRange() const { return _responseRange; } signals: void metaDataChanged(); @@ -158,8 +154,8 @@ class JingleFileShareDownloader : public AbstractFileShareDownloader { downloadError(QString::fromLatin1("Jingle file transfer is disabled")); return; } - if (isRanged()) - file.setRange(XMPP::Jingle::FileTransfer::Range(rangeStart, rangeSize)); + if (_requestRange) + file.setRange(XMPP::Jingle::FileTransfer::Range(_requestRange->start, _requestRange->size)); app->setFile(file); app->setStreamingMode(true); session->addContent(app); @@ -171,9 +167,11 @@ class JingleFileShareDownloader : public AbstractFileShareDownloader { }); connect(app, &Jingle::FileTransfer::Application::connectionReady, this, [this]() { - auto r = app->acceptFile().range(); - rangeStart = qint64(r.offset); - rangeSize = qint64(r.length); + qDebug("FSP connectionReady"); + auto r = app->acceptFile().range(); + if (r.isValid()) { + _responseRange = FileShareDownloader::Range { r.offset, r.length }; + } connection = app->connection(); connect(connection.data(), &XMPP::Jingle::Connection::readyRead, this, &JingleFileShareDownloader::readyRead); @@ -191,7 +189,7 @@ class JingleFileShareDownloader : public AbstractFileShareDownloader { emit disconnected(); return; } - downloadError(tr("Jingle download failed")); + downloadError(tr("Jingle download failed: %s").arg(r.text())); }); session->initiate(); @@ -229,8 +227,10 @@ class JingleFileShareDownloader : public AbstractFileShareDownloader { bool isConnected() const { return connection && app && app->state() == XMPP::Jingle::State::Active; } - bool hasFileSize() const { return app && app->acceptFile().hasSize(); } - qint64 fileSize() const { return app ? qint64(app->acceptFile().size()) : 0; } + std::optional fileSize() const + { + return app ? app->acceptFile().size() : std::optional {}; + } }; class NAMFileShareDownloader : public AbstractFileShareDownloader { @@ -250,10 +250,12 @@ class NAMFileShareDownloader : public AbstractFileShareDownloader { void start() { QNetworkRequest req = QNetworkRequest(QUrl(sourceUri)); - if (isRanged()) { - QString range = QString("bytes=%1-%2") - .arg(QString::number(rangeStart), - rangeSize ? QString::number(rangeStart + rangeSize - 1) : QString()); + if (_requestRange) { + QString range + = QString("bytes=%1-%2") + .arg(QString::number(_requestRange->start), + _requestRange->size ? QString::number(_requestRange->start + _requestRange->size - 1) + : QString()); req.setRawHeader("Range", range.toLatin1()); } #if QT_VERSION >= QT_VERSION_CHECK(5, 9, 0) @@ -265,18 +267,45 @@ class NAMFileShareDownloader : public AbstractFileShareDownloader { connect(reply, &QNetworkReply::metaDataChanged, this, [this]() { int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); if (status == 206) { // partial content - QByteArray ba = reply->rawHeader("Content-Range"); - bool parsed; - std::tie(parsed, rangeStart, rangeSize) = Http::parseContentRangeHeader(ba); - if (ba.size() && !parsed) { + QByteArray ba = reply->rawHeader("Content-Range"); + auto range = Http::parseContentRangeHeader(ba); + if (!ba.size() || !range) { namFailed(QLatin1String("Invalid HTTP response range")); return; } - } else if (status != 200 && status != 203) { - rangeStart = 0; - rangeSize = 0; // make it not-ranged - namFailed(tr("Unexpected HTTP status") + QString(": %1").arg(status)); - return; + auto const &[start, size, totalSize] = *range; + + if (_requestRange) { // we reqeusted some range + if (start != _requestRange->start) { + namFailed(QLatin1String("Unexpected response range start. Expected ") + + QString::number(_requestRange->start) + QLatin1String(" but got ") + + QString::number(start)); + return; + } + } + _responseRange = FileShareDownloader::Range { start, size }; + _totalSize = totalSize; + + } else { + _responseRange = {}; // reset range since not a partial content + if (status != 200 && status != 203) { + namFailed(tr("Unexpected HTTP status") + QString(": %1").arg(status)); + return; + } + } + + if (!_totalSize) { + auto clHeader = reply->header(QNetworkRequest::ContentLengthHeader); + if (clHeader.isValid()) { + bool ok; + auto size = clHeader.toULongLong(&ok); + if (ok) { + _totalSize = size; + } else { + namFailed(QLatin1String("Failed to parse Cotent-Length: ") + clHeader.toString()); + return; + } + } } emit metaDataChanged(); @@ -290,7 +319,8 @@ class NAMFileShareDownloader : public AbstractFileShareDownloader { #endif [=](QNetworkReply::NetworkError code) { qDebug("reply errored %d", code); }); connect(reply, &QNetworkReply::finished, this, [this]() { - qDebug("reply is finished. error code=%d. bytes available=%lld", reply->error(), reply->bytesAvailable()); + qDebug("FSD reply is finished. error code=%d. bytes available=%lld", reply->error(), + reply->bytesAvailable()); if (reply->error() == QNetworkReply::NoError) emit disconnected(); else @@ -315,8 +345,7 @@ class NAMFileShareDownloader : public AbstractFileShareDownloader { bool isConnected() const { return reply && reply->isRunning(); } - bool hasFileSize() const { return reply && reply->header(QNetworkRequest::ContentLengthHeader).isValid(); } - qint64 fileSize() const { return reply ? reply->header(QNetworkRequest::ContentLengthHeader).toLongLong() : 0; } + std::optional fileSize() const { return _totalSize; } }; class BOBFileShareDownloader : public AbstractFileShareDownloader { @@ -350,15 +379,11 @@ class BOBFileShareDownloader : public AbstractFileShareDownloader { downloadError(tr("Download using \"Bits Of Binary\" failed")); return; } - receivedData = data; - if (isRanged()) { // there is not such a thing like ranged bob - rangeStart = 0; - rangeSize = 0; // make it not-ranged - } + receivedData = data; + _responseRange = {}; // make it not-ranged. impossble for bob anyway + _totalSize = data.size(); emit metaDataChanged(); emit readyRead(); - connected = false; - emit disconnected(); }); } @@ -366,15 +391,29 @@ class BOBFileShareDownloader : public AbstractFileShareDownloader { qint64 read(char *data, qint64 maxSize) { + bool hasData = !receivedData.isEmpty(); + auto const maybeFinal = [&]() { + if (hasData && receivedData.size() == 0 && connected) { + QMetaObject::invokeMethod( + this, + [this]() { + connected = false; + emit disconnected(); + }, + Qt::QueuedConnection); + } + }; + if (maxSize >= receivedData.size()) { - qint64 ret = receivedData.size(); - memcpy(data, receivedData.data(), size_t(ret)); + maxSize = receivedData.size(); + memcpy(data, receivedData.data(), size_t(maxSize)); receivedData.clear(); - return ret; + } else { + memcpy(data, receivedData.data(), size_t(maxSize)); + receivedData = receivedData.mid(int(maxSize)); } - memcpy(data, receivedData.data(), size_t(maxSize)); - receivedData = receivedData.mid(int(maxSize)); + maybeFinal(); return maxSize; } @@ -387,31 +426,30 @@ class BOBFileShareDownloader : public AbstractFileShareDownloader { bool isConnected() const { return connected; } - bool hasFileSize() const { return !receivedData.isNull(); } - qint64 fileSize() const { return receivedData.isNull() ? 0 : receivedData.size(); } + std::optional fileSize() const { return _totalSize; } }; class FileShareDownloader::Private : public QObject { Q_OBJECT public: - FileShareDownloader *q = nullptr; - PsiAccount *acc = nullptr; - QList sums; - Jingle::FileTransfer::File file; - QList jids; - QStringList uris; // sorted from low priority to high. - std::unique_ptr tmpFile; - QString dstFileName; - QString lastError; - qint64 rangeStart = 0; - quint64 rangeSize = 0; // 0 - all the remaining - qint64 bytesLeft = -1; - AbstractFileShareDownloader *downloader = nullptr; - bool metaReady = false; - bool finished = false; - bool success = false; - bool selfDelete = false; - FileSharingItem::SourceType currentType = FileSharingItem::SourceType::None; + FileShareDownloader *q = nullptr; + PsiAccount *acc = nullptr; + QList sums; + Jingle::FileTransfer::File file; + QList jids; + QStringList uris; // sorted from low priority to high. + std::unique_ptr tmpFile; + QString dstFileName; + QString lastError; + std::optional requestRange; + std::optional responseRange; + std::optional bytesLeft; + AbstractFileShareDownloader *downloader = nullptr; + bool metaReady = false; + bool finished = false; + bool success = false; + bool selfDelete = false; + FileSharingItem::SourceType currentType = FileSharingItem::SourceType::None; void finishWithError(const QString &errStr) { @@ -434,7 +472,7 @@ class FileShareDownloader::Private : public QObject { void checkCacheReady() { - if (!bytesLeft) { + if (bytesLeft.has_value() && !*bytesLeft) { if (tmpFile) { tmpFile->close(); tmpFile.reset(); @@ -443,7 +481,7 @@ class FileShareDownloader::Private : public QObject { finished = true; if (selfDelete) { - qDebug("all data downloaded"); + qDebug("FSD all data downloaded"); downloader->close(); q->deleteLater(); } @@ -479,8 +517,9 @@ class FileShareDownloader::Private : public QObject { finishWithError("Unhandled downloader"); return; } - - downloader->setRange(rangeStart, rangeSize); + if (requestRange) { + downloader->setRequestRange(*requestRange); + } connect(downloader, &AbstractFileShareDownloader::failed, q, [this]() { success = false; @@ -492,13 +531,13 @@ class FileShareDownloader::Private : public QObject { }); connect(downloader, &AbstractFileShareDownloader::metaDataChanged, q, [this]() { - metaReady = true; + metaReady = true; + responseRange = downloader->responseRange(); - std::tie(rangeStart, rangeSize) = downloader->range(); - if (rangeSize) { - bytesLeft = rangeSize; - } else if (!rangeStart && downloader->hasFileSize()) { // definitely not ranged and full size is known - bytesLeft = downloader->fileSize(); + if (downloader->responseRange()) { + bytesLeft = downloader->responseRange()->size; + } else if (downloader->fileSize()) { // definitely not ranged and full size is known + bytesLeft = *downloader->fileSize(); // then we are going to cache it as well. TODO: review caching when size is unknown auto partDir = QDir(acc->psi()->fileSharingManager()->cacheDir() + "/partial"); @@ -541,7 +580,7 @@ FileShareDownloader::FileShareDownloader(PsiAccount *acc, const QListsuccess; } @@ -570,18 +609,11 @@ void FileShareDownloader::abort() } } -void FileShareDownloader::setRange(qint64 start, qint64 size) -{ - d->rangeStart = start; - d->rangeSize = size; -} +void FileShareDownloader::setRequestRange(const std::optional &range) { d->requestRange = range; } -bool FileShareDownloader::isRanged() const { return d->rangeStart > 0 || d->rangeSize > 0; } +const std::optional &FileShareDownloader::requestRange() const { return d->requestRange; } -std::tuple FileShareDownloader::range() const -{ - return std::tuple(d->rangeStart, d->rangeSize); -} +const std::optional &FileShareDownloader::responseRange() const { return d->responseRange; } QString FileShareDownloader::takeFile() const { @@ -608,8 +640,8 @@ qint64 FileShareDownloader::readData(char *data, qint64 maxSize) return 0; } - if (d->bytesLeft != -1) { - d->bytesLeft -= bytesRead; + if (d->bytesLeft.has_value()) { + *d->bytesLeft -= bytesRead; } d->checkCacheReady(); diff --git a/src/filesharingdownloader.h b/src/filesharingdownloader.h index d2decf02a1..4fccea7c59 100644 --- a/src/filesharingdownloader.h +++ b/src/filesharingdownloader.h @@ -20,7 +20,9 @@ #pragma once #include + #include +#include class PsiAccount; @@ -39,6 +41,11 @@ namespace Jingle { class FileShareDownloader : public QIODevice { Q_OBJECT public: + struct Range { + quint64 start; + quint64 size; // 0 - all the remaining + }; + FileShareDownloader(PsiAccount *acc, const QList &sums, const XMPP::Jingle::FileTransfer::File &file, const QList &jids, const QStringList &uris, QObject *manager); ~FileShareDownloader(); @@ -47,9 +54,9 @@ class FileShareDownloader : public QIODevice { bool isConnected() const; bool open(QIODevice::OpenMode mode = QIODevice::ReadOnly) override; void abort(); - void setRange(qint64 start, qint64 size); - bool isRanged() const; - std::tuple range() const; + void setRequestRange(const std::optional &range); + const std::optional &requestRange() const; + const std::optional &responseRange() const; QString takeFile() const; const XMPP::Jingle::FileTransfer::File &jingleFile() const; diff --git a/src/filesharinghttpproxy.cpp b/src/filesharinghttpproxy.cpp deleted file mode 100644 index 2d8aa0abbc..0000000000 --- a/src/filesharinghttpproxy.cpp +++ /dev/null @@ -1,309 +0,0 @@ -/* - * filesharinghttpproxy.cpp - http proxy for shared files downloads - * Copyright (C) 2019 Sergey Ilinykh - * - * 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 2 - * 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 . - * - */ - -#include "filesharinghttpproxy.h" -#include "filecache.h" -#include "filesharingitem.h" -#include "filesharingmanager.h" -#include "psiaccount.h" -#include "psicon.h" -#include "qhttpserverconnection.hpp" -#include "qhttpserverrequest.hpp" -#include "qhttpserverresponse.hpp" -#include "webserver.h" - -#include -#include -#include - -#define HTTP_CHUNK (512 * 1024 * 1024) - -FileSharingHttpProxy::FileSharingHttpProxy(PsiAccount *acc, const QString &sourceIdHex, - qhttp::server::QHttpRequest *req, qhttp::server::QHttpResponse *res) : - QObject(res), item(acc->psi()->fileSharingManager()->item(XMPP::Hash::from(sourceIdHex))), acc(acc), request(req), - response(res) -{ - auto baseUrl = acc->psi()->webServer()->serverUrl().toString(); - qDebug("FSP %s %s%s range: %s", qPrintable(req->methodString()), qPrintable(baseUrl), - qPrintable(req->url().toString()), qPrintable(req->headers().value("range"))); - - if (!item) { - res->setStatusCode(qhttp::ESTATUS_NOT_FOUND); - emit req->end(); - return; - } - - auto status = qhttp::TStatusCode(parseHttpRangeRequest()); - if (status != qhttp::ESTATUS_OK) { - res->setStatusCode(status); - qWarning("http range parse failed: %d", status); - emit req->end(); - return; // handled with error - } - - if (isRanged && item->isSizeKnown()) { - if (requestedStart == 0 && requestedSize == item->fileSize()) - isRanged = false; - else if (quint64(requestedStart) + requestedSize > item->fileSize()) - requestedSize = item->fileSize() - requestedStart; // don't request more than declared in share - } - - auto cache = item->cache(); - if (cache) { - proxyCache(); - return; // handled with success - } - - downloader = item->download(isRanged, requestedStart, requestedSize); - Q_ASSERT(downloader); - downloader->setParent(this); - - connect(downloader, &FileShareDownloader::metaDataChanged, this, &FileSharingHttpProxy::onMetadataChanged); - connect(downloader, &FileShareDownloader::failed, this, [this]() { - if (!headersSent) { - response->setStatusCode(qhttp::ESTATUS_BAD_GATEWAY); // something finnished with errors quite early - response->end(); - } - }); - - downloader->open(); -} - -FileSharingHttpProxy::~FileSharingHttpProxy() { qDebug("FSP deleted"); } - -// returns -int FileSharingHttpProxy::parseHttpRangeRequest() -{ - QByteArray rangesBa = request->headers().value("range"); - if (!rangesBa.size()) - return qhttp::ESTATUS_OK; - - if ((item->isSizeKnown() && !item->fileSize()) || !rangesBa.startsWith("bytes=")) { - return qhttp::ESTATUS_REQUESTED_RANGE_NOT_SATISFIABLE; - } - - if (rangesBa.indexOf(',') != -1) { - return qhttp::ESTATUS_NOT_IMPLEMENTED; - } - - auto ba = QByteArray::fromRawData(rangesBa.data() + sizeof("bytes"), rangesBa.size() - int(sizeof("bytes"))); - - auto rangeBounds = ba.trimmed().split('-'); - if (rangeBounds.size() != 2) { - return qhttp::ESTATUS_BAD_REQUEST; - } - - bool ok; - quint64 start; - quint64 end; - - if (rangeBounds[0].isEmpty()) { // bytes from the end are requested. Jingle-ft doesn't support this - return qhttp::ESTATUS_NOT_IMPLEMENTED; - } - - start = rangeBounds[0].toULongLong(&ok); - if (!ok) { - return qhttp::ESTATUS_BAD_REQUEST; - } - if (rangeBounds[1].size()) { // if we have end - end = rangeBounds[1].toLongLong(&ok); // then parse it - if (!ok || start > end) { // if something not parsed or range is invalid - return qhttp::ESTATUS_BAD_REQUEST; - } - - if (!item->isSizeKnown() || start < item->fileSize()) { - isRanged = true; - requestedStart = start; - requestedSize = end - start + 1; - } - } else { // no end. all the remaining - if (!item->isSizeKnown() || start < item->fileSize()) { - isRanged = true; - requestedStart = start; - requestedSize = 0; - } - } - - if (item->isSizeKnown() && !isRanged) { // isRanged is not set. So it doesn't fit - response->addHeader("Content-Range", QByteArray("bytes */") + QByteArray::number(item->fileSize())); - return qhttp::ESTATUS_REQUESTED_RANGE_NOT_SATISFIABLE; - } - - return qhttp::ESTATUS_OK; -} - -void FileSharingHttpProxy::setupHeaders(qint64 fileSize, QString contentType, QDateTime lastModified, bool isRanged, - qint64 rangeStart, qint64 rangeSize) -{ - if (lastModified.isValid()) - response->addHeader("Last-Modified", lastModified.toString(Qt::RFC2822Date).toLatin1()); - if (contentType.size()) - response->addHeader("Content-Type", contentType.toLatin1()); - - bool keepAlive = true; - response->addHeader("Accept-Ranges", "bytes"); - if (isRanged) { - response->setStatusCode(qhttp::ESTATUS_PARTIAL_CONTENT); - auto range = QString(QLatin1String("bytes %1-%2/%3")) - .arg(rangeStart) - .arg(rangeStart + rangeSize - 1) - .arg(fileSize == -1 ? QString('*') : QString::number(fileSize)); - response->addHeader("Content-Range", range.toLatin1()); - response->addHeader("Content-Length", QByteArray::number(rangeSize)); - } else { - response->setStatusCode(qhttp::ESTATUS_OK); - if (fileSize == -1) - keepAlive = false; - else - response->addHeader("Content-Length", QByteArray::number(fileSize)); - } - if (keepAlive) { - response->addHeader("Connection", "keep-alive"); - } -} - -void FileSharingHttpProxy::proxyCache() -{ - auto status = qhttp::TStatusCode(parseHttpRangeRequest()); - if (status != qhttp::ESTATUS_OK) { - response->setStatusCode(status); - qWarning("http range parse failed: %d", status); - emit request->end(); - return; // handled with error - } - QFile *file = new QFile(item->fileName(), response); - QFileInfo fi(*file); - if (!file->open(QIODevice::ReadOnly)) { - response->setStatusCode(qhttp::ESTATUS_NOT_FOUND); - qWarning("FSP failed to open cached file: %s", qPrintable(file->errorString())); - emit request->end(); - return; // handled with error - } - qint64 size = fi.size(); - if (isRanged) { - if (requestedSize) - size = (requestedStart + requestedSize) > fi.size() ? fi.size() - requestedStart : requestedSize; - else // remaining part - size = fi.size() - requestedStart; - file->seek(requestedStart); - } - // TODO If-Modified-Since - setupHeaders(fi.size(), item->mimeType(), fi.lastModified(), isRanged, requestedStart, size); - connect(response, &qhttp::server::QHttpResponse::allBytesWritten, file, [this, file, size]() { - qint64 toWrite = requestedStart + size - file->pos(); - if (!toWrite) { - return; - } - if (toWrite > HTTP_CHUNK) - response->write(file->read(HTTP_CHUNK)); - else - response->end(file->read(toWrite)); - }); - - if (size < HTTP_CHUNK) { - response->end(file->read(size)); - } -} - -void FileSharingHttpProxy::onMetadataChanged() -{ - qint64 start; - quint64 size; - std::tie(start, size) = downloader->range(); - auto const file = downloader->jingleFile(); - - if (downloader->isRanged()) - qDebug("FSP metaDataChanged: rangeStart=%lld rangeSize=%lld", start, size); - else if (downloader->jingleFile().hasSize()) - qDebug("FSP metaDataChanged: size=%" PRIu64, downloader->jingleFile().size()); - else - qDebug("FSP metaDataChanged: unknown size or range"); - - // check range satisfaction - if (isRanged && downloader->isRanged() && !file.hasSize() && !size) { // size unknown for ranged response. - qWarning("Unknown size for ranged response"); - downloader->disconnect(this); - downloader->deleteLater(); - response->setStatusCode(qhttp::ESTATUS_BAD_GATEWAY); - response->end(); - return; - } - - if (isRanged && !downloader->isRanged()) { - qWarning("FSP: remote doesn't support ranged. transfer everything"); - isRanged = false; - start = 0; - size = file.size(); - } - - if (isRanged && !size) { - size = file.size() - start; - } - bytesLeft = isRanged ? size : (file.hasSize() ? file.size() : -1); - - setupHeaders(file.hasSize() ? qint64(file.size()) : -1, file.mediaType(), file.date(), downloader->isRanged(), - start, size); - headersSent = true; - - connect(downloader, &FileShareDownloader::readyRead, this, &FileSharingHttpProxy::transfer); - connect(downloader, &FileShareDownloader::disconnected, this, &FileSharingHttpProxy::transfer); - connect(response, &qhttp::server::QHttpResponse::allBytesWritten, this, &FileSharingHttpProxy::transfer); -} - -void FileSharingHttpProxy::transfer() -{ - qint64 bytesAvail = downloader ? downloader->bytesAvailable() : 0; - bool isConnected = downloader ? downloader->isConnected() : false; - if (response->connection()->tcpSocket()->bytesToWrite() >= HTTP_CHUNK) { - qDebug("FSP available=%lld wait till previous chunk is wrtten", bytesAvail); - return; - } - - if (isConnected) { - if (bytesAvail) { - qint64 toTransfer = bytesAvail > HTTP_CHUNK ? HTTP_CHUNK : bytesAvail; - auto data = downloader->read(toTransfer); - if (bytesLeft != -1) { - if (data.size() < bytesLeft) - response->write(data); - else { - // if (data.size() > bytesLeft) - // data.resize(bytesLeft); - response->end(data); - } - bytesLeft -= data.size(); - } else { - response->write(data); - } - qDebug("FSP transferred %lld bytes of %lld bytes", qint64(data.size()), toTransfer); - } else { - qDebug("FSP we have to wait for readyRead or disconnected"); - } - return; - } - - // so we are not connected - if (bytesAvail) { - response->end(downloader->read(bytesAvail)); - qDebug("FSP transferred final %lld bytes", bytesAvail); - } else { - response->end(); - qDebug("FSP ended with no additional data"); - } -} diff --git a/src/filesharinghttpproxy.h b/src/filesharinghttpproxy.h deleted file mode 100644 index 2c6dea1b27..0000000000 --- a/src/filesharinghttpproxy.h +++ /dev/null @@ -1,69 +0,0 @@ -/* - * filesharinghttpproxy.h - http proxy for shared files downloads - * Copyright (C) 2019 Sergey Ilinykh - * - * 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 2 - * 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 . - * - */ - -#ifndef FILESHARINGHTTPPROXY_H -#define FILESHARINGHTTPPROXY_H - -#include -#include - -class FileCacheItem; -class FileSharingItem; -class FileShareDownloader; -class PsiAccount; - -namespace qhttp { namespace server { - class QHttpRequest; - class QHttpResponse; -}} - -class FileSharingHttpProxy : public QObject { - Q_OBJECT -public: - explicit FileSharingHttpProxy(PsiAccount *acc, const QString &sourceIdHex, qhttp::server::QHttpRequest *request, - qhttp::server::QHttpResponse *response); - ~FileSharingHttpProxy(); - -signals: - -public slots: -private slots: - void onMetadataChanged(); - void transfer(); - -private: - int parseHttpRangeRequest(); - void setupHeaders(qint64 fileSize, QString contentType, QDateTime lastModified, bool isRanged, qint64 rangeStart, - qint64 rangeSize); - void proxyCache(); - -private: - FileSharingItem *item = nullptr; - PsiAccount *acc; - qhttp::server::QHttpRequest *request; - qhttp::server::QHttpResponse *response; - QPointer downloader; - qint64 requestedStart = 0; - qint64 requestedSize = 0; // if == 0 then all the remaining - qint64 bytesLeft = -1; // -1 - unknown - bool isRanged = false; - bool headersSent = false; -}; - -#endif // FILESHARINGHTTPPROXY_H diff --git a/src/filesharingitem.cpp b/src/filesharingitem.cpp index a646dcc1bc..ab038bab5c 100644 --- a/src/filesharingitem.cpp +++ b/src/filesharingitem.cpp @@ -70,11 +70,7 @@ FileSharingItem::FileSharingItem(const MediaSharing &ms, const Jid &from, PsiAcc _uris = ms.sources; } - if (ms.file.hasSize()) { - _flags |= SizeKnown; - _fileSize = ms.file.size(); - } - + _fileSize = ms.file.size(); _jids << from; QByteArray ampl = ms.file.amplitudes(); @@ -86,7 +82,7 @@ FileSharingItem::FileSharingItem(const MediaSharing &ms, const Jid &from, PsiAcc } FileSharingItem::FileSharingItem(const QImage &image, PsiAccount *acc, FileSharingManager *manager) : - QObject(manager), _acc(acc), _manager(manager), _fileType(FileType::TempFile), _flags(SizeKnown) + QObject(manager), _acc(acc), _manager(manager), _fileType(FileType::TempFile) { QByteArray ba; QBuffer buffer(&ba); @@ -107,8 +103,7 @@ FileSharingItem::FileSharingItem(const QImage &image, PsiAccount *acc, FileShari } FileSharingItem::FileSharingItem(const QString &fileName, PsiAccount *acc, FileSharingManager *manager) : - QObject(manager), _acc(acc), _manager(manager), _fileType(FileType::LocalLink), _flags(SizeKnown), - _fileName(fileName) + QObject(manager), _acc(acc), _manager(manager), _fileType(FileType::LocalLink), _fileName(fileName) { QFile file(fileName); if (!file.open(QIODevice::ReadOnly)) @@ -129,7 +124,7 @@ FileSharingItem::FileSharingItem(const QString &fileName, PsiAccount *acc, FileS FileSharingItem::FileSharingItem(const QString &mime, const QByteArray &data, const QVariantMap &metaData, PsiAccount *acc, FileSharingManager *manager) : - QObject(manager), _acc(acc), _manager(manager), _fileType(FileType::TempFile), _flags(SizeKnown), + QObject(manager), _acc(acc), _manager(manager), _fileType(FileType::TempFile), _modifyTime(QDateTime::currentDateTimeUtc()), _metaData(metaData) { _sums.append(Hash::from(Hash::Sha1, data)); @@ -167,7 +162,7 @@ bool FileSharingItem::initFromCache(FileCacheItem *cache) if (!cache) return false; - _flags = SizeKnown; + _flags = {}; auto md = cache->metadata(); _mimeType = md.value(QString::fromLatin1("type")).toString(); QString link = md.value(QString::fromLatin1("link")).toString(); @@ -375,37 +370,40 @@ void FileSharingItem::publish(const XMPP::Jid &myJid) } } -FileShareDownloader *FileSharingItem::download(bool isRanged, qint64 start, quint64 size) +FileShareDownloader *FileSharingItem::download(std::optional range) { - if (isRanged && (_flags & SizeKnown) && start == 0 && size == _fileSize) - isRanged = false; + // if (range && range->start == 0 && _fileSize && range->size == *_fileSize) + // range = {}; XMPP::Jingle::FileTransfer::File file; file.setDate(_modifyTime); file.setMediaType(_mimeType); file.setName(_fileName); - if (_flags & SizeKnown) - file.setSize(_fileSize); + if (_fileSize) + file.setSize(*_fileSize); for (auto const &h : std::as_const(_sums)) { file.addHash(h); } - FileShareDownloader *downloader = new FileShareDownloader(_acc, _sums, file, _jids, _uris, this); - if (isRanged) { - downloader->setRange(start, size); - return downloader; + auto downloaderRange = std::optional {}; + if (range) { + downloaderRange = FileShareDownloader::Range { range->start, range->size }; } - if (_downloader) { - qWarning("double download for the same file: %s", qPrintable(_fileName)); - return downloader; // seems like we are downloading this file twice, but what we can do? - } + // if (_downloader && _downloader->requestedRange() == downloaderRange) { + // qWarning("double download for the same file: %s", qPrintable(_fileName)); + // return _downloader; + // } - _downloader = downloader; - connect(downloader, &FileShareDownloader::cacheReady, this, [this]() { - QString dlFileName = _downloader->takeFile(); - _downloader->disconnect(this); - _downloader = nullptr; + auto downloader = new FileShareDownloader(_acc, _sums, file, _jids, _uris, this); + if (downloaderRange) { + downloader->setRequestRange(downloaderRange); + return downloader; + } + connect(downloader, &FileShareDownloader::cacheReady, this, [this, downloader]() { + QString dlFileName = downloader->takeFile(); + downloader->disconnect(this); + // downloader = nullptr; if (_modifyTime.isValid()) FileUtil::setModificationTime(dlFileName, _modifyTime); @@ -435,13 +433,13 @@ FileShareDownloader *FileSharingItem::download(bool isRanged, qint64 start, quin emit downloadFinished(); }); - connect(downloader, &FileShareDownloader::failed, this, [this]() { - _downloader->disconnect(this); - _downloader = nullptr; + connect(downloader, &FileShareDownloader::failed, this, [downloader, this]() { + downloader->disconnect(this); + // downloader = nullptr; emit downloadFinished(); }); - connect(downloader, &FileShareDownloader::destroyed, this, [this]() { _downloader = nullptr; }); + // connect(downloader, &FileShareDownloader::destroyed, this, [this]() { _downloader = nullptr; }); return downloader; } diff --git a/src/filesharingitem.h b/src/filesharingitem.h index a1401eca53..9d56781611 100644 --- a/src/filesharingitem.h +++ b/src/filesharingitem.h @@ -59,12 +59,16 @@ class FileSharingItem : public QObject { HttpFinished = 0x1, JingleFinished = 0x2, PublishNotified = 0x4, - SizeKnown = 0x8, }; Q_DECLARE_FLAGS(Flags, Flag) using HashSums = QList; + struct Range { + quint64 start; + quint64 size; + }; + FileSharingItem(FileCacheItem *cache, PsiAccount *acc, FileSharingManager *manager); FileSharingItem(const XMPP::MediaSharing &ms, const XMPP::Jid &from, PsiAccount *acc, FileSharingManager *manager); FileSharingItem(const QImage &image, PsiAccount *acc, FileSharingManager *manager); @@ -73,16 +77,15 @@ class FileSharingItem : public QObject { FileSharingManager *manager); ~FileSharingItem(); - QIcon thumbnail(const QSize &size) const; - QImage preview(const QSize &maxSize) const; - QString displayName() const; - QString fileName() const; - inline const QString &mimeType() const { return _mimeType; } - inline const HashSums &sums() const { return _sums; } - inline QVariantMap metaData() const { return _metaData; } - inline quint64 fileSize() const { return _fileSize; } - inline bool isSizeKnown() const { return bool(_flags & SizeKnown); } - inline const QStringList &uris() const { return _uris; } + QIcon thumbnail(const QSize &size) const; + QImage preview(const QSize &maxSize) const; + QString displayName() const; + QString fileName() const; + inline const QString &mimeType() const { return _mimeType; } + inline const HashSums &sums() const { return _sums; } + inline QVariantMap metaData() const { return _metaData; } + inline std::optional fileSize() const { return _fileSize; } + inline const QStringList &uris() const { return _uris; } // reborn flag updates ttl for the item FileCacheItem *cache(bool reborn = false) const; @@ -103,7 +106,7 @@ class FileSharingItem : public QObject { * * It's responsibility of the caller to delete downloader when it's done, or one can call setSelfDelete(true) */ - FileShareDownloader *download(bool isRanged = false, qint64 start = 0, quint64 size = 0); + FileShareDownloader *download(std::optional range = {}); // accept public internet uri and returns it's type static SourceType sourceType(const QString &uri); @@ -121,21 +124,21 @@ class FileSharingItem : public QObject { void logChanged(); private: - PsiAccount *_acc = nullptr; - FileSharingManager *_manager = nullptr; - FileShareDownloader *_downloader = nullptr; - FileType _fileType; - Flags _flags; - quint64 _fileSize = 0; - QDateTime _modifyTime; // utc - QStringList _uris; - QString _fileName; // file name with path - HashSums _sums; - QString _mimeType; - QString _description; - QVariantMap _metaData; - QStringList _log; - QList _jids; + PsiAccount *_acc = nullptr; + FileSharingManager *_manager = nullptr; + // FileShareDownloader *_downloader = nullptr; + FileType _fileType; + Flags _flags; + std::optional _fileSize; + QDateTime _modifyTime; // utc + QStringList _uris; + QString _fileName; // file name with path + HashSums _sums; + QString _mimeType; + QString _description; + QVariantMap _metaData; + QStringList _log; + QList _jids; }; Q_DECLARE_OPERATORS_FOR_FLAGS(FileSharingItem::Flags) diff --git a/src/filesharingmanager.cpp b/src/filesharingmanager.cpp index d55f40b19e..0179842709 100644 --- a/src/filesharingmanager.cpp +++ b/src/filesharingmanager.cpp @@ -34,7 +34,6 @@ #include "iris/xmpp_reference.h" #include "iris/xmpp_vcard.h" #ifdef HAVE_WEBSERVER -#include "filesharinghttpproxy.h" #include "webserver.h" #endif #include "messageview.h" @@ -117,10 +116,9 @@ FileCacheItem *FileSharingManager::moveToCache(const QList &sums, co FileSharingItem *FileSharingManager::item(const Hash &id) { return d->items.value(id); } -QList FileSharingManager::fromMimeData(const QMimeData *data, PsiAccount *acc) +QList FileSharingManager::createFromMimeData(const QMimeData *data, PsiAccount *acc) { - QList ret; - QStringList files; + QStringList files; QString voiceMsgMime; QString voiceAmplitudesMime(QLatin1String("application/x-psi-amplitudes")); @@ -152,9 +150,10 @@ QList FileSharingManager::fromMimeData(const QMimeData *data, } } if (files.isEmpty() && img.isNull() && !hasVoice) { - return ret; + return {}; } + QList ret; if (files.isEmpty()) { // so we have an image FileSharingItem *item = nullptr; if (hasVoice) { @@ -218,9 +217,10 @@ void FileSharingManager::fillMessageView(MessageView &mv, const Message &m, PsiA MediaSharing ms = r.mediaSharing(); // qDebug() << "BEGIN:" << r.begin() << "END:" << r.end(); // only audio and image supported for now - if (!ms.isValid() || !ms.file.hasComputedHashes() || !ms.file.hasSize() + if (!ms.isValid() || !ms.file.hasComputedHashes() || !ms.file.size().has_value() || !(ms.file.mediaType().startsWith(QLatin1String("audio")) - || ms.file.mediaType().startsWith(QLatin1String("image")))) { + || ms.file.mediaType().startsWith(QLatin1String("image")) + || ms.file.mediaType().startsWith(QLatin1String("video")))) { continue; } auto item = new FileSharingItem(ms, m.from(), acc, this); @@ -273,11 +273,17 @@ bool FileSharingManager::jingleAutoAcceptIncomingDownloadRequest(Jingle::Session } for (auto const &p : toAccept) { - auto ft = p.first; - auto item = p.second; + auto ft = p.first; // jingle-ft app + auto item = p.second; // FileCacheItem + + auto acceptFile = ft->file(); + acceptFile.setHashes(item->sums()); + ft->setAcceptFile(acceptFile); + + // TODO hashes for ranges connect(ft, &Jingle::FileTransfer::Application::deviceRequested, this, - [ft, item, this](quint64 offset, quint64 /*size*/) { + [ft, item, this](quint64 offset, std::optional /*size*/) { auto vm = item->metadata(); QString fileName = vm.value(QString::fromLatin1("link")).toString(); if (fileName.isEmpty()) { @@ -297,16 +303,6 @@ bool FileSharingManager::jingleAutoAcceptIncomingDownloadRequest(Jingle::Session return true; } -#ifdef HAVE_WEBSERVER -// returns true if request handled. false if we need to find another hander -bool FileSharingManager::downloadHttpRequest(PsiAccount *acc, const QString &sourceIdHex, - qhttp::server::QHttpRequest *req, qhttp::server::QHttpResponse *res) -{ - new FileSharingHttpProxy(acc, sourceIdHex, req, res); - return true; -} -#endif - XMPP::Hash FileSharingDeviceOpener::urlToSourceId(const QUrl &url) { if (url.scheme() != QLatin1String("share")) diff --git a/src/filesharingmanager.h b/src/filesharingmanager.h index 35e5c3ca54..0099e75230 100644 --- a/src/filesharingmanager.h +++ b/src/filesharingmanager.h @@ -79,7 +79,7 @@ class FileSharingManager : public QObject { FileSharingItem *item(const XMPP::Hash &id); // FileSharingItem* fromReference(const XMPP::Reference &ref, PsiAccount *acc); - QList fromMimeData(const QMimeData *data, PsiAccount *acc); + QList createFromMimeData(const QMimeData *data, PsiAccount *acc); QList fromFilesList(const QStringList &fileList, PsiAccount *acc); // registers source for file and returns share id for future access to the source @@ -88,10 +88,6 @@ class FileSharingManager : public QObject { // returns false if unable to accept automatically bool jingleAutoAcceptIncomingDownloadRequest(XMPP::Jingle::Session *session); -#ifdef HAVE_WEBSERVER - bool downloadHttpRequest(PsiAccount *acc, const QString &sourceIdHex, qhttp::server::QHttpRequest *req, - qhttp::server::QHttpResponse *res); -#endif signals: public slots: diff --git a/src/filesharingnamproxy.cpp b/src/filesharingnamproxy.cpp deleted file mode 100644 index 208f2d4f88..0000000000 --- a/src/filesharingnamproxy.cpp +++ /dev/null @@ -1,268 +0,0 @@ -/* - * filesharingnamproxy.cpp - proxy network access reply for shared files - * Copyright (C) 2019 Sergey Ilinykh - * - * 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 2 - * 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 . - * - */ - -#include "filesharingnamproxy.h" -#include "filecache.h" -#include "filesharingitem.h" -#include "filesharingmanager.h" -#include "httputil.h" -#include "psiaccount.h" -#include "psicon.h" - -#include -#include - -FileSharingNAMReply::FileSharingNAMReply(PsiAccount *acc, const QString &sourceIdHex, const QNetworkRequest &req) : - item(acc->psi()->fileSharingManager()->item(XMPP::Hash::from(sourceIdHex))), acc(acc) -{ - setRequest(req); - setOperation(QNetworkAccessManager::GetOperation); // TODO rewrite when we have others - QTimer::singleShot(0, this, &FileSharingNAMReply::init); -} - -FileSharingNAMReply::~FileSharingNAMReply() { qDebug("FS-NAM destroy reply"); } - -void FileSharingNAMReply::init() -{ - qDebug() << "New FS-NAM" << request().url().toString(); - const auto headers = request().rawHeaderList(); - for (auto const &h : headers) - qDebug(" %s: %s", h.data(), request().rawHeader(h).data()); - - auto rangesBa = request().rawHeader(QByteArray::fromRawData("Range", 5)); - - if (rangesBa.size()) { - Http::ParseResult result; - qint64 rangeStart, rangeSize; - std::tie(result, rangeStart, rangeSize) - = Http::parseRangeHeader(rangesBa, item->isSizeKnown() ? item->fileSize() : -1); - switch (result) { - case Http::Parsed: - isRanged = rangeStart != 0 || rangeSize != 0; - requestedStart = rangeStart; - requestedSize = rangeSize; - break; - case Http::Unparsed: - finishWithError(QNetworkReply::ProtocolFailure, 400, "Bad request"); - return; - case Http::NotImplementedRangeType: - finishWithError(QNetworkReply::UnknownContentError, 416, "Expected bytes range"); - return; - case Http::NotImplementedTailLoad: - finishWithError(QNetworkReply::UnknownContentError, 501, "tail load is not supported"); - return; - case Http::NotImplementedMultirange: - finishWithError(QNetworkReply::UnknownContentError, 501, "multi range is not supported"); - return; - case Http::OutOfRange: - setRawHeader("Content-Range", QByteArray("bytes */") + QByteArray::number(item->fileSize())); - finishWithError(QNetworkReply::UnknownContentError, 416, "Requested range not satisfiable"); - return; - } - } - - if (isRanged && item->isSizeKnown()) { - if (requestedStart == 0 && requestedSize == item->fileSize()) - isRanged = false; - else if (requestedStart + requestedSize > item->fileSize()) - requestedSize = item->fileSize() - requestedStart; // don't request more than declared in share - } - - auto cache = item->cache(); - if (cache) { - cachedFile = new QFile(item->fileName(), this); - QFileInfo fi(*cachedFile); - - cachedFile->open(QIODevice::ReadOnly); - qint64 size = fi.size(); - if (isRanged) { - if (requestedSize) - size = (requestedStart + requestedSize) > quint64(fi.size()) ? fi.size() - requestedStart - : requestedSize; - else // remaining part - size = fi.size() - requestedStart; - cachedFile->seek(requestedStart); - } - // TODO If-Modified-Since - setupHeaders(fi.size(), item->mimeType(), fi.lastModified(), isRanged, requestedStart, size); - if (cachedFile->size()) - emit readyRead(); - return; // handled with success - } - - downloader = item->download(isRanged, requestedStart, requestedSize); - Q_ASSERT(downloader); - downloader->setParent(this); - - connect(downloader, &FileShareDownloader::metaDataChanged, this, &FileSharingNAMReply::onMetadataChanged); - connect(downloader, &FileShareDownloader::readyRead, this, &FileSharingNAMReply::readyRead); - connect(downloader, &FileShareDownloader::failed, this, [this]() { - if (!headersSent) { - finishWithError(QNetworkReply::ServiceUnavailableError, 502, "Upstream failed"); - } - }); - - downloader->open(); -} - -void FileSharingNAMReply::finishWithError(QNetworkReply::NetworkError networkError, int httpCode, - const char *httpReason) -{ - qDebug() << "FS-NAM failed" << networkError << httpCode << httpReason; - QString reason = QString::fromLatin1(httpReason); - if (httpCode) { - setAttribute(QNetworkRequest::HttpStatusCodeAttribute, httpCode); - if (httpReason) { - setAttribute(QNetworkRequest::HttpReasonPhraseAttribute, reason); - } - emit metaDataChanged(); - } - if (httpCode) { - setError(networkError, reason); -#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) - emit errorOccurred(networkError); -#else - emit error(networkError); -#endif - } - if (downloader) { - downloader->disconnect(this); - downloader->deleteLater(); - } - setFinished(true); - emit finished(); -} - -void FileSharingNAMReply::setupHeaders(qint64 fileSize, QString contentType, QDateTime lastModified, bool isRanged, - qint64 rangeStart, qint64 rangeSize) -{ - if (lastModified.isValid()) - setRawHeader("Last-Modified", lastModified.toString(Qt::RFC2822Date).toLatin1()); - if (contentType.size()) - setRawHeader("Content-Type", contentType.toLatin1()); - - bool keepAlive = true; - setRawHeader("Accept-Ranges", "bytes"); - if (isRanged) { - setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 206); // partial content - auto range = QString(QLatin1String("bytes %1-%2/%3")) - .arg(rangeStart) - .arg(rangeStart + rangeSize - 1) - .arg(fileSize == -1 ? QString('*') : QString::number(fileSize)); - setRawHeader("Content-Range", range.toLatin1()); - setHeader(QNetworkRequest::ContentLengthHeader, rangeSize); - } else { - setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 200); - if (fileSize == -1) - keepAlive = false; - else - setHeader(QNetworkRequest::ContentLengthHeader, fileSize); - } - if (keepAlive) { - setRawHeader("Connection", "keep-alive"); - } - - qDebug("FS-NAM set open mode to readonly"); - setOpenMode(QIODevice::ReadOnly); - - qDebug() << "FS-NAM headers sent"; - auto const &headers = rawHeaderList(); - for (auto const &h : headers) { - qDebug(" %s: %s", h.constData(), rawHeader(h).constData()); - } - - emit metaDataChanged(); -} - -void FileSharingNAMReply::onMetadataChanged() -{ - qint64 start; - qint64 size; - std::tie(start, size) = downloader->range(); - auto const file = downloader->jingleFile(); - - if (downloader->isRanged()) - qDebug("FSP metaDataChanged: rangeStart=%lld rangeSize=%lld", start, size); - else if (downloader->jingleFile().hasSize()) - qDebug("FSP metaDataChanged: size=%" PRIu64, downloader->jingleFile().size()); - else - qDebug("FSP metaDataChanged: unknown size or range"); - - // check range satisfaction - if (isRanged && downloader->isRanged() && !file.hasSize() && !size) { // size unknown for ranged response. - qWarning("Unknown size for ranged response"); - finishWithError(QNetworkReply::ServiceUnavailableError, 502, "Upstream failed"); - return; - } - - if (isRanged && !downloader->isRanged()) { - qWarning("FSP: remote doesn't support ranged. transfer everything"); - isRanged = false; - start = 0; - size = file.size(); - } - - if (isRanged && !size) { - size = file.size() - start; - } - bytesLeft = isRanged ? size : (file.hasSize() ? file.size() : -1); - - setupHeaders(file.hasSize() ? qint64(file.size()) : -1, file.mediaType(), file.date(), downloader->isRanged(), - start, size); - headersSent = true; - if (downloader->bytesAvailable()) - emit readyRead(); -} - -void FileSharingNAMReply::abort() -{ - if (downloader) - downloader->abort(); - setError(QNetworkReply::OperationCanceledError, "aborted"); - setFinished(true); - emit finished(); -} - -qint64 FileSharingNAMReply::bytesAvailable() const -{ - if (cachedFile) - return (cachedFile->isOpen() ? cachedFile->size() - cachedFile->pos() : 0) + QNetworkReply::bytesAvailable(); - return (downloader ? downloader->bytesAvailable() : 0) + QNetworkReply::bytesAvailable(); -} - -qint64 FileSharingNAMReply::readData(char *buf, qint64 maxlen) -{ - // qDebug() << "reading" << maxlen << "bytes"; - if (cachedFile) { - if (!cachedFile->isOpen()) - return 0; - auto len = cachedFile->read(buf, maxlen); - if (!bytesAvailable()) - QTimer::singleShot(0, this, [this]() { - setFinished(true); - emit finished(); - }); - return len; - } - - if (!downloader->isOpen()) - return 0; - - return downloader->read(buf, maxlen); -} diff --git a/src/filesharingnamproxy.h b/src/filesharingnamproxy.h deleted file mode 100644 index 24f082c316..0000000000 --- a/src/filesharingnamproxy.h +++ /dev/null @@ -1,63 +0,0 @@ -/* - * filesharingnamproxy.h - proxy network access reply for shared files - * Copyright (C) 2019 Sergey Ilinykh - * - * 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 2 - * 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 . - * - */ - -#ifndef FILESHARINGNAMPROXY_H -#define FILESHARINGNAMPROXY_H - -#include -#include - -class FileSharingItem; -class FileShareDownloader; -class PsiAccount; -class QFile; - -class FileSharingNAMReply : public QNetworkReply { - Q_OBJECT - - FileSharingItem *item = nullptr; - PsiAccount *acc; - QPointer downloader; - qint64 requestedStart = 0; - quint64 requestedSize = 0; // if == 0 then all the remaining - qint64 bytesLeft = -1; // -1 - unknown - bool isRanged = false; - bool headersSent = false; - QFile *cachedFile = nullptr; - -public: - FileSharingNAMReply(PsiAccount *acc, const QString &sourceIdHex, const QNetworkRequest &req); - ~FileSharingNAMReply(); - - // reimplemented - void abort() override; - qint64 readData(char *buffer, qint64 maxlen) override; - qint64 bytesAvailable() const override; - -private slots: - void onMetadataChanged(); - void init(); - -private: - void setupHeaders(qint64 fileSize, QString contentType, QDateTime lastModified, bool isRanged, qint64 rangeStart, - qint64 rangeSize); - void finishWithError(QNetworkReply::NetworkError error, int httpCode = 0, const char *httpStatus = nullptr); -}; - -#endif // FILESHARINGNAMPROXY_H diff --git a/src/filesharingproxy.cpp b/src/filesharingproxy.cpp new file mode 100644 index 0000000000..3c82938a09 --- /dev/null +++ b/src/filesharingproxy.cpp @@ -0,0 +1,597 @@ +/* + * filesharingproxy.cpp - proxy network access reply for shared files + * Copyright (C) 2024 Sergey Ilinykh + * + * 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 2 + * 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 . + * + */ + +#include "filesharingproxy.h" + +#define QHTTP_MEMORY_LOG 1 + +#include "filesharingdownloader.h" +#include "filesharingitem.h" +#include "filesharingmanager.h" +#include "httputil.h" +#include "psiaccount.h" +#include "psicon.h" +#ifdef HAVE_WEBSERVER +#include "qhttpfwd.hpp" +#include "qhttpserverresponse.hpp" +#include "webserver.h" +#endif + +#include +#include +#include + +#include + +#define HTTP_CHUNK (512 * 1024) + +template class ControlBase : public QObject { + +public: + enum class StatusCode { + Ok, + PartialContent, // 206 + BadRequest, // 400 + NotFound, // 404 + RangeNotSatisfied, // 416 + NotImplemented, // 501 + BadGateway, // 502 + ServiceUnavailable, // 503 + }; + + PsiAccount *acc; + FileSharingItem *item = nullptr; + QPointer downloader; + std::optional requestedRange; + std::optional bytesLeft; // if not set - unknown + qint64 totalTranferred = 0; + bool headersSent = false; + bool dataHungry = true; + bool finished = false; + + ControlBase(PsiAccount *acc, const QString &sourceIdHex, QObject *parent) : + QObject(parent), acc(acc), item(acc->psi()->fileSharingManager()->item(XMPP::Hash::from(sourceIdHex))) + { + } + + ~ControlBase() { qDebug("FSP destroyed. Total transferred bytes: %lld", totalTranferred); } + + void process() + { + auto self = static_cast(this); + if (!item) { + _finishWithMetadataError(StatusCode::NotFound); + return; + } + + QByteArray rangeHeaderValue = self->requestHeader("range"); + if (rangeHeaderValue.size()) { + auto status = parseHttpRangeRequest(rangeHeaderValue); + if (status != StatusCode::Ok) { + qWarning("http range parse failed: %d", int(status)); + _finishWithMetadataError(status); + return; // handled with error + } + } + + auto cache = item->cache(); + if (cache) { + proxyCache(); + return; // handled with success + } + + downloader = item->download(requestedRange); + Q_ASSERT(downloader); + downloader->setParent(this); + + connect(downloader, &FileShareDownloader::metaDataChanged, this, &ControlBase::onMetadataChanged); + connect(downloader, &FileShareDownloader::failed, this, [this]() { + if (!headersSent) { + _finishWithMetadataError(StatusCode::BadGateway); + } + }); + + downloader->open(); + } + + // returns + StatusCode parseHttpRangeRequest(const QByteArray &rangeValue) + { + auto const [parseResult, start, size] = Http::parseRangeHeader(rangeValue, item->fileSize()); + + switch (parseResult) { + case Http::Parsed: + requestedRange = FileSharingItem::Range { start, size }; + return StatusCode::Ok; + case Http::Unparsed: + return StatusCode::BadRequest; + case Http::NotImplementedRangeType: + case Http::NotImplementedTailLoad: + case Http::NotImplementedMultirange: + return StatusCode::NotImplemented; + case Http::OutOfRange: + static_cast(this)->setResponseHeader( + "Content-Range", QByteArray("bytes */") + QByteArray::number(quint64(*item->fileSize()))); + return StatusCode::RangeNotSatisfied; + } + return StatusCode::NotImplemented; + } + + void proxyCache() + { + // auto status = qhttp::TStatusCode(parseHttpRangeRequest()); + // if (status != qhttp::ESTATUS_OK) { + // response->setStatusCode(status); + // qWarning("http range parse failed: %d", status); + // emit request->end(); + // return; // handled with error + // } + auto self = static_cast(this); + QFile *file = new QFile(item->fileName(), this); + QFileInfo fi(*file); + if (!file->open(QIODevice::ReadOnly)) { + qWarning("FSP failed to open cached file: %s", qPrintable(file->errorString())); + _finishWithMetadataError(StatusCode::NotFound); + return; // handled with error + } + auto size = quint64(fi.size()); + auto actualRange = requestedRange; + if (requestedRange) { + if (requestedRange->start >= size) { + self->setResponseHeader("Content-Range", QByteArray("bytes */") + QByteArray::number(size)); + self->setResponseHeader("Content-Length", "0"); + _finishWithMetadataError(StatusCode::RangeNotSatisfied); + return; + } + if (requestedRange->size) + actualRange->size = qint64(requestedRange->start + requestedRange->size) > fi.size() + ? quint64(fi.size()) - requestedRange->start + : requestedRange->size; + else // remaining part + actualRange->size = quint64(fi.size()) - requestedRange->start; + file->seek(requestedRange->start); + } + // TODO If-Modified-Since + setupHeaders(fi.size(), item->mimeType(), fi.lastModified(), actualRange); + self->connectReadyWrite(file, [this, file, size]() { + qint64 toWrite = (requestedRange ? requestedRange->start : 0) + size - file->pos(); + if (!toWrite) { + return; + } + if (toWrite > HTTP_CHUNK) + _write(file->read(HTTP_CHUNK)); + else { + _write(file->read(toWrite)); + _finish(); + } + }); + + if (size < HTTP_CHUNK) { + _write(file->read(size)); + _finish(); + } + } + + void onMetadataChanged() + { + auto const &responseRange = downloader->responseRange(); + auto const file = downloader->jingleFile(); + + if (responseRange) + qDebug("FSP metaDataChanged: rangeStart=%lld rangeSize=%lld", responseRange->start, responseRange->size); + else if (file.size()) + qDebug("FSP metaDataChanged: size=%lu", *file.size()); + else + qDebug("FSP metaDataChanged: unknown size or range"); + + // check range satisfaction + if (requestedRange && responseRange && !file.size().has_value() + && !requestedRange->size) { // size unknown for ranged response. + qWarning("Unknown size for ranged response"); + _finishWithMetadataError(StatusCode::NotImplemented); + return; + } + + auto reqsponseRange = requestedRange; + if (requestedRange && !responseRange) { + qWarning("FSP: remote doesn't support ranged. transfer everything"); + reqsponseRange = {}; + } + + if (reqsponseRange && !reqsponseRange->size) { + reqsponseRange->size = *file.size() - reqsponseRange->start; + } + if (reqsponseRange || file.size()) { + bytesLeft = reqsponseRange ? reqsponseRange->size : file.size(); + } + + setupHeaders(file.size(), file.mediaType(), file.date(), reqsponseRange); + headersSent = true; + + connect(downloader, &FileShareDownloader::readyRead, this, [this]() { + if (dataHungry) { + transfer(); + } + }); + connect(downloader, &FileShareDownloader::disconnected, this, &ControlBase::transfer); + static_cast(this)->connectReadyWrite(this, [this]() { + dataHungry = true; + transfer(); + }); + } + + void setupHeaders(std::optional fileSize, QString contentType, QDateTime lastModified, + const std::optional &range) + { + auto self = static_cast(this); + if (contentType == QLatin1String("audio/x-vorbis+ogg")) { + contentType = QLatin1String("audio/ogg"); + } + if (lastModified.isValid()) + self->setResponseHeader("Last-Modified", lastModified.toString(Qt::RFC2822Date).toLatin1()); + if (contentType.size()) + self->setResponseHeader("Content-Type", contentType.toLatin1()); + + bool keepAlive = true; + self->setResponseHeader("Accept-Ranges", "bytes"); + if (range) { + self->setResponseStatusCode(StatusCode::PartialContent); + auto rangeStr = QString(QLatin1String("bytes %1-%2/%3")) + .arg(range->start) + .arg(range->start + range->size - 1) + .arg(fileSize ? QString::number(*fileSize) : QString('*')); + self->setResponseHeader("Content-Range", rangeStr.toLatin1()); + self->setResponseHeader("Content-Length", QByteArray::number(range->size)); + } else { + self->setResponseStatusCode(StatusCode::Ok); + if (!fileSize) + keepAlive = false; + else + self->setResponseHeader("Content-Length", QByteArray::number(*fileSize)); + } + if (keepAlive) { + self->setResponseHeader("Connection", "keep-alive"); + } + } + + void transfer() + { + if (finished) { + return; + } + qint64 bytesAvail = downloader ? downloader->bytesAvailable() : 0; + bool isConnected = downloader ? downloader->isConnected() : false; + + if (isConnected) { + if (bytesAvail) { + qint64 toTransfer = bytesAvail > HTTP_CHUNK ? HTTP_CHUNK : bytesAvail; + auto data = downloader->read(toTransfer); + // qDebug("read %lld bytes from available %lld", qint64(data.size()), bytesAvail); + if (bytesLeft.has_value()) { + if (data.size() < bytesLeft) + _write(data); + else { + // if (data.size() > bytesLeft) + // data.resize(bytesLeft); + _write(data); + _finish(); + } + *bytesLeft -= data.size(); + } else { + _write(data); + } + // qDebug("FSP transferred %lld bytes of %lld bytes", qint64(data.size()), toTransfer); + } else { + // qDebug("FSP we have to wait for readyRead or disconnected"); + } + return; + } + + // so we are not connected + if (bytesAvail) { + _write(downloader->read(bytesAvail)); + _finish(); + qDebug("FSP transferred final %lld bytes", bytesAvail); + } else { + _finish(); + qDebug("FSP ended with no additional data"); + } + } + + void ensureUpstreamStopped() + { + if (downloader) { + downloader->disconnect(this); + downloader->deleteLater(); + } + } + + void _write(const QByteArray &data) + { + // qDebug("FSP: write %lld bytes", qint64(data.size())); + static_cast(this)->write(data); + totalTranferred += data.size(); + dataHungry = false; + } + + void _finish() + { + if (finished) { + return; + } + finished = true; + static_cast(this)->finish(); + } + + void _finishWithMetadataError(StatusCode code, QByteArray httpReason = {}) + { + if (finished) { + return; + } + qDebug() << "FS finishWithMetadataError " << int(code) << httpReason; + auto self = static_cast(this); + ensureUpstreamStopped(); + if (downloader) { // TODO we need download manager instead + downloader->disconnect(this); + downloader->deleteLater(); + } + finished = true; + self->finishWithMetadataError(code, httpReason); + } +}; + +namespace { +class FileSharingNAMReply : public QNetworkReply { + Q_OBJECT + + QByteArray buffer; + bool metadataSignalled = false; + bool finished_ = false; + +public: + FileSharingNAMReply(const QNetworkRequest &request) + { + setRequest(request); + setOpenMode(QIODevice::ReadOnly); + } + + qint64 bytesAvailable() const { return buffer.size() + QNetworkReply::bytesAvailable(); } + + inline void setRawHeader(const char *headerName, const QByteArray &value) + { + qDebug("FSP reply set header %s=%s", headerName, value.data()); + QNetworkReply::setRawHeader(headerName, value); + } + + inline void setAttribute(QNetworkRequest::Attribute attr, QVariant value) + { + QNetworkReply::setAttribute(attr, std::move(value)); + } + + void appendData(const QByteArray &data) + { + // qDebug("FSP append %lld bytes for reading", qint64(data.size())); + if (!metadataSignalled) { + qDebug("FSP reply signalling metadataChanged"); + QTimer::singleShot(0, this, SIGNAL(metaDataChanged())); + metadataSignalled = true; + } + buffer += data; + QTimer::singleShot(0, this, SIGNAL(readyRead())); + } + + void finishWithError(QNetworkReply::NetworkError networkError, int httpCode, const QByteArray &reason) + { + if (finished_) { + return; + } + qDebug("FSP reply finishWithError"); + setAttribute(QNetworkRequest::HttpStatusCodeAttribute, httpCode); + if (!reason.isEmpty()) { + setAttribute(QNetworkRequest::HttpReasonPhraseAttribute, reason); + } + if (!metadataSignalled) { + emit metaDataChanged(); + metadataSignalled = true; + } + setError(networkError, reason); +#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) + emit errorOccurred(networkError); +#else + emit error(networkError); +#endif + finish(); + } + + void finish() + { + if (finished_) { + return; + } + finished_ = true; + qDebug("FSP reply finished"); + setFinished(true); + QTimer::singleShot(0, this, [this]() { + setFinished(true); + emit finished(); + }); + // emit finished(); + // we should not delete it here, because it may still have data buffered + // but we need to stop downloader if it's still running + } + +protected: + qint64 readData(char *buf, qint64 maxlen) + { + auto sz = std::min(maxlen, qint64(buffer.size())); + // qDebug("FSP read %lld bytes", sz); + if (sz) { + std::memcpy(buf, buffer.data(), sz); + buffer.remove(0, sz); + } + return sz; + } + + // QNetworkReply interface +public slots: + void abort() + { + qDebug("FSP reply aborted"); + setError(QNetworkReply::OperationCanceledError, "aborted"); + finish(); + } +}; + +class NAMProxy : public ControlBase { + Q_OBJECT + + QNetworkRequest request; + +public: + FileSharingNAMReply *reply; + + NAMProxy(PsiAccount *acc, const QString &sourceIdHex, const QNetworkRequest &req, FileSharingNAMReply *reply) : + ControlBase(acc, sourceIdHex, reply), request(req), reply(reply) + { + qDebug("FSP GET %s range: %s", qUtf8Printable(req.url().toString()), qPrintable(request.rawHeader("range"))); + connect(reply, &QNetworkReply::finished, this, &ControlBase::ensureUpstreamStopped); + } + + static std::pair mapStatusCode(ControlBase::StatusCode code) + { + switch (code) { + case StatusCode::Ok: + return { QNetworkReply::NoError, 200 }; + case StatusCode::PartialContent: + return { QNetworkReply::NoError, 206 }; + case StatusCode::BadRequest: + return { QNetworkReply::ProtocolFailure, 400 }; + case StatusCode::NotFound: + return { QNetworkReply::ContentNotFoundError, 404 }; + case StatusCode::RangeNotSatisfied: + return { QNetworkReply::UnknownContentError, 416 }; + case StatusCode::NotImplemented: + return { QNetworkReply::OperationNotImplementedError, 501 }; + case StatusCode::BadGateway: + return { QNetworkReply::ServiceUnavailableError, 502 }; + case StatusCode::ServiceUnavailable: + return { QNetworkReply::ServiceUnavailableError, 503 }; + } + return { QNetworkReply::OperationNotImplementedError, 501 }; + } + + void finishWithMetadataError(ControlBase::StatusCode code, QByteArray httpReason = {}) + { + auto const [replyError, httpCode] = mapStatusCode(code); + reply->finishWithError(replyError, httpCode, httpReason); + } + void finish() { reply->finish(); } + inline QByteArray requestHeader(const char *headerName) { return request.rawHeader(headerName); } + inline void setResponseHeader(const char *headerName, QByteArray value) { reply->setRawHeader(headerName, value); } + inline void setResponseStatusCode(StatusCode code) + { + reply->setAttribute(QNetworkRequest::HttpStatusCodeAttribute, std::get<1>(mapStatusCode(code))); + } + void connectReadyWrite(QObject *ctx, std::function &&callback) + { + connect(reply, &QNetworkReply::bytesWritten, ctx, [this, cb = std::move(callback)](qint64 bytes) { + Q_UNUSED(bytes); + if (reply->bytesToWrite() < HTTP_CHUNK) { + cb(); + } + }); + } + void write(const QByteArray &data) { return reply->appendData(data); } +}; +#ifdef HAVE_WEBSERVER +class HTTPProxy : public ControlBase { + Q_OBJECT + + qhttp::server::QHttpRequest *request; + qhttp::server::QHttpResponse *response; + +public: + HTTPProxy(PsiAccount *acc, const QString &sourceIdHex, qhttp::server::QHttpRequest *request, + qhttp::server::QHttpResponse *response) : + ControlBase(acc, sourceIdHex, response), request(request), response(response) + { + auto baseUrl = acc->psi()->webServer()->serverUrl().toString(); + qDebug("FSP %s %s%s range: %s", qPrintable(request->methodString()), qPrintable(baseUrl), + qPrintable(request->url().toString()), qPrintable(request->headers().value("range"))); + } + + static qhttp::TStatusCode mapStatusCode(ControlBase::StatusCode code) + { + switch (code) { + case StatusCode::Ok: + return qhttp::ESTATUS_OK; + case StatusCode::PartialContent: + return qhttp::ESTATUS_PARTIAL_CONTENT; + case StatusCode::BadRequest: + return qhttp::ESTATUS_BAD_REQUEST; + case StatusCode::NotFound: + return qhttp::ESTATUS_NOT_FOUND; + case StatusCode::RangeNotSatisfied: + return qhttp::ESTATUS_REQUESTED_RANGE_NOT_SATISFIABLE; + case StatusCode::NotImplemented: + return qhttp::ESTATUS_NOT_IMPLEMENTED; + case StatusCode::BadGateway: + return qhttp::ESTATUS_BAD_GATEWAY; + case StatusCode::ServiceUnavailable: + return qhttp::ESTATUS_SERVICE_UNAVAILABLE; + } + return qhttp::ESTATUS_NOT_IMPLEMENTED; + } + + void finishWithMetadataError(ControlBase::StatusCode code, QByteArray httpReason = {}) + { + Q_UNUSED(httpReason); + response->setStatusCode(mapStatusCode(code)); + response->end(); + } + void finish() { response->end(); } + inline QByteArray requestHeader(const char *headerName) { return request->headers().value(headerName); } + inline void setResponseHeader(const char *headerName, QByteArray value) { response->addHeader(headerName, value); } + inline void setResponseStatusCode(StatusCode code) { response->setStatusCode(mapStatusCode(code)); } + void connectReadyWrite(QObject *ctx, std::function &&callback) + { + connect(response, &qhttp::server::QHttpResponse::allBytesWritten, ctx, std::move(callback)); + } + void write(const QByteArray &data) { response->write(data); } +}; +#endif +} + +namespace FileSharingProxy { +#ifdef HAVE_WEBSERVER +void proxify(PsiAccount *acc, const QString &sourceIdHex, qhttp::server::QHttpRequest *request, + qhttp::server::QHttpResponse *response) +{ + (new HTTPProxy(acc, sourceIdHex, request, response))->process(); +} +#endif +QNetworkReply *proxify(PsiAccount *acc, const QString &sourceIdHex, const QNetworkRequest &req) +{ + FileSharingNAMReply *reply = new FileSharingNAMReply(req); + (new NAMProxy(acc, sourceIdHex, req, reply))->process(); + return reply; +} +} + +#include "filesharingproxy.moc" diff --git a/src/filesharingproxy.h b/src/filesharingproxy.h new file mode 100644 index 0000000000..2c589be206 --- /dev/null +++ b/src/filesharingproxy.h @@ -0,0 +1,43 @@ +/* + * filesharingproxy.h - proxy network access reply for shared files + * Copyright (C) 2024 Sergey Ilinykh + * + * 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 2 + * 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 . + * + */ + +#ifndef FILESHARINGPROXY_H +#define FILESHARINGPROXY_H + +class QString; +class QNetworkRequest; +class QNetworkReply; + +class PsiAccount; + +namespace qhttp { namespace server { + class QHttpRequest; + class QHttpResponse; +}} + +namespace FileSharingProxy { +#ifdef HAVE_WEBSERVER +void proxify(PsiAccount *acc, const QString &sourceIdHex, qhttp::server::QHttpRequest *request, + qhttp::server::QHttpResponse *response); +#endif +QNetworkReply *proxify(PsiAccount *acc, const QString &sourceIdHex, const QNetworkRequest &req); + +}; + +#endif // FILESHARINGPROXY_H diff --git a/src/gcuserview.cpp b/src/gcuserview.cpp index f607268226..5da9260301 100644 --- a/src/gcuserview.cpp +++ b/src/gcuserview.cpp @@ -139,7 +139,7 @@ class GCUserViewDelegate : public QItemDelegate { QPalette palette = o.palette; MUCItem::Role r = index.data(GCUserModel::StatusRole).value().mucItem().role(); QRect rect = o.rect; - int gap = qMax(int(fontHeight_ / 6), 1); + int gap = qMax(int(fontHeight_ / 3), 1); if (nickColoring_) { if (r == MUCItem::Moderator) diff --git a/src/geolocationdlg.cpp b/src/geolocationdlg.cpp index ba72fce145..25def4eed0 100644 --- a/src/geolocationdlg.cpp +++ b/src/geolocationdlg.cpp @@ -35,7 +35,7 @@ GeoLocationDlg::GeoLocationDlg(QList list) : QDialog(nullptr), pa_ if (pa_.isEmpty()) close(); ui_.setupUi(this); - setWindowIcon(IconsetFactory::icon("system/geolocation").icon()); + setWindowIcon(IconsetFactory::icon("pep/geolocation").icon()); setModal(false); connect(ui_.pb_cancel, SIGNAL(clicked()), SLOT(close())); diff --git a/src/groupchatdlg.cpp b/src/groupchatdlg.cpp index c5539983bc..06c717bc47 100644 --- a/src/groupchatdlg.cpp +++ b/src/groupchatdlg.cpp @@ -50,6 +50,7 @@ #include "iris/xmpp_caps.h" #include "iris/xmpp_message.h" #include "iris/xmpp_tasks.h" +#include "iris/xmpp_vcard4.h" #include "popupmanager.h" #include "psiaccount.h" #include "psiactionlist.h" @@ -101,7 +102,9 @@ #include #include #include -#include +#include +#include + #ifdef Q_OS_WIN #include #endif @@ -157,9 +160,9 @@ class StatusPingTask : public Task { } else { XMPP::Stanza::Error err; err.fromXml(tag, client()->stream().baseNS()); - if (err.condition == XMPP::Stanza::Error::ItemNotFound) { + if (err.condition == XMPP::Stanza::Error::ErrorCond::ItemNotFound) { emit result(NoSuch, id()); - } else if (err.condition == XMPP::Stanza::Error::NotAcceptable) { + } else if (err.condition == XMPP::Stanza::Error::ErrorCond::NotAcceptable) { emit result(NotOccupant, id()); } else { emit result(OtherErr, id()); @@ -220,6 +223,7 @@ class GCMainDlg::Private : public QObject, public MCmdProviderIface { QString mucName, discoMucName, discoMucDescription, vcardMucName; QString password; QMap subjectMap; + QString lastTopic; bool nonAnonymous; // got status code 100 ? ActionList *actions; IconAction *act_bookmark, *act_pastesend; @@ -243,6 +247,7 @@ class GCMainDlg::Private : public QObject, public MCmdProviderIface { bool alert; bool gcSelfPresenceSupported = false; bool gcSelfAvatarRequested = false; // when self presence is not supported + bool forceQuit = false; // ignore hide-when-closing option QStringList hist; int histAt; @@ -356,6 +361,7 @@ public slots: if (!nick.isEmpty()) { prev_self = self; self = nick; + dlg->ui_.log->setLocalNickname(self); dlg->account()->groupChatChangeNick(dlg->jid().domain(), dlg->jid().node(), self, dlg->account()->status()); } @@ -399,6 +405,7 @@ join {,} [pass{,} // FIXME nick can't be empty.... prev_self = self; self = nick; + dlg->ui_.log->setLocalNickname(self); dlg->account()->groupChatChangeNick(dlg->jid().domain(), dlg->jid().node(), self, dlg->account()->status()); newstate = nullptr; @@ -462,7 +469,7 @@ join {,} [pass{,} newstate = oldstate; return true; } else if (cmd == "leave") { - dlg->close(); + dlg->leave(); } else if (!cmd.isEmpty()) { return false; } @@ -517,7 +524,7 @@ join {,} [pass{,} void doFileShare(const QList &refs, const QString &desc) { Message m(dlg->jid()); - m.setType("groupchat"); + m.setType(Message::Type::Groupchat); m.setReferences(refs); m.setBody(desc); emit dlg->aSend(m); @@ -670,6 +677,32 @@ void GCMainDlg::onNickInsertClick(const QString &nick) ui_.mle->chatEdit()->setFocus(); } +void GCMainDlg::outgoingReactions(const QString &messageId, const QSet &reactions) +{ + Message m(jid()); + m.setType(Message::Type::Groupchat); + m.setReactions({ messageId, reactions }); + QString id = account()->client()->genUniqueId(); + m.setId(id); // we need id early for message manipulations in chatview + m.setTimeStamp(QDateTime::currentDateTime()); + // d->mle()->appendMessageHistory(m.body()); + + emit aSend(m); +} + +void GCMainDlg::sendMessageRetraction(const QString &messageId) +{ + Message m(jid()); + m.setType(Message::Type::Groupchat); + m.setRetraction(messageId); + // QString id = account()->client()->genUniqueId(); + // m.setId(id); // we need id early for message manipulations in chatview + // m.setTimeStamp(QDateTime::currentDateTime()); + // d->mle()->appendMessageHistory(m.body()); + + emit aSend(m); +} + void GCMainDlg::doContactContextMenu(const QString &nick) { auto itm = d->usersModel->findEntry(nick); @@ -860,15 +893,18 @@ GCMainDlg::GCMainDlg(PsiAccount *pa, const Jid &j, TabManager *tabManager) : Tab d->tabmode = PsiOptions::instance()->getOption("options.ui.tabs.use-tabs").toBool(); ui_.log->setSessionData(true, false, jid(), jid().full()); // FIXME change conference name + ui_.log->setLocalNickname(d->self); #ifdef WEBKIT ui_.log->setAccount(account()); #else ui_.log->setMediaOpener(account()->fileSharingDeviceOpener()); #endif - connect(ui_.log, SIGNAL(showNM(QString)), this, SLOT(doContactContextMenu(QString))); + connect(ui_.log, SIGNAL(showNickMenu(QString)), this, SLOT(doContactContextMenu(QString))); connect(URLObject::getInstance(), SIGNAL(openURL(QString)), SLOT(openURL(QString))); connect(ui_.log, SIGNAL(nickInsertClick(QString)), SLOT(onNickInsertClick(QString))); + connect(ui_.log, &ChatView::outgoingReactions, this, &GCMainDlg::outgoingReactions); + connect(ui_.log, &ChatView::outgoingMessageRetraction, this, &GCMainDlg::sendMessageRetraction); PsiToolTip::install(ui_.le_topic); @@ -899,6 +935,10 @@ GCMainDlg::GCMainDlg(PsiAccount *pa, const Jid &j, TabManager *tabManager) : Tab hb3a->addWidget(d->typeahead); ui_.vboxLayout1->addLayout(hb3a); + ui_.lb_ident->setAccount(account()); + ui_.lb_ident->setShowJid(false); + connect(account()->psi(), &PsiCon::accountCountChanged, this, &GCMainDlg::updateIdentityVisibility); + ActionList *actList = account()->psi()->actionList()->actionLists(PsiActionList::Actions_Groupchat).at(0); for (const QString &name : actList->actions()) { auto action = actList->copyAction(name, this); @@ -966,7 +1006,8 @@ GCMainDlg::GCMainDlg(PsiAccount *pa, const Jid &j, TabManager *tabManager) : Tab ui_.le_topic->addAction(d->act_bookmark); d->act_copy_muc_jid = new QAction(tr("Copy Groupchat JID"), this); - connect(d->act_copy_muc_jid, SIGNAL(triggered()), SLOT(copyMucJid())); + connect(d->act_copy_muc_jid, &QAction::triggered, this, + [this]() { QApplication::clipboard()->setText(jid().bare()); }); ui_.le_topic->addAction(d->act_copy_muc_jid); BookmarkManager *bm = account()->bookmarkManager(); @@ -1004,7 +1045,7 @@ GCMainDlg::GCMainDlg(PsiAccount *pa, const Jid &j, TabManager *tabManager) : Tab connect(ui_.pb_send, SIGNAL(customContextMenuRequested(const QPoint)), SLOT(sendButtonMenu())); d->act_close = new QAction(this); addAction(d->act_close); - connect(d->act_close, SIGNAL(triggered()), SLOT(close())); + connect(d->act_close, &QAction::triggered, this, &GCMainDlg::leave); d->act_hide = new QAction(this); addAction(d->act_hide); connect(d->act_hide, SIGNAL(triggered()), SLOT(hideTab())); @@ -1059,11 +1100,7 @@ GCMainDlg::GCMainDlg(PsiAccount *pa, const Jid &j, TabManager *tabManager) : Tab updateMucName(); updateGCVCard(); - JT_DiscoInfo *disco = new JT_DiscoInfo( - account()->client()->rootTask()); // FIXME in fact xep says we should do this before entering. - connect(disco, SIGNAL(finished()), SLOT(discoInfoFinished())); // but we need this just for name for now. - disco->get(jid()); // From other side we could provide the name outside. - disco->go(true); + updateConfiguration(); setLooks(); setToolbuttons(); @@ -1072,6 +1109,15 @@ GCMainDlg::GCMainDlg(PsiAccount *pa, const Jid &j, TabManager *tabManager) : Tab setConnecting(); connect(ui_.log, &ChatView::quote, ui_.mle->chatEdit(), &ChatEdit::insertAsQuote); + connect(ui_.log, &ChatView::editMessageRequested, ui_.mle->chatEdit(), &ChatEdit::startCorrection); + connect(ui_.log, &ChatView::forwardMessageRequested, account(), + [this](const QString &messageId, const QString &nick, const QString &text) { + account()->psi()->invokeForwardMessage(jid().withResource(nick), text); + }); + connect(ui_.log, &ChatView::openInfoRequested, account(), + [this](const QString &nick) { account()->invokeGCInfo(jid().withResource(nick)); }); + connect(ui_.log, &ChatView::openChatRequested, account(), + [this](const QString &nick) { account()->invokeGCChat(jid().withResource(nick)); }); connect(pa->avatarFactory(), &AvatarFactory::avatarChanged, this, &GCMainDlg::avatarUpdated); #ifdef PSI_PLUGINS @@ -1189,6 +1235,12 @@ void GCMainDlg::scrollDown() { ui_.log->scrollDown(); } void GCMainDlg::closeEvent(QCloseEvent *e) { + if (!d->forceQuit && PsiOptions::instance()->getOption("options.ui.muc.hide-when-closing").toBool() + && !isTabbed()) { + hide(); + e->ignore(); + return; + } e->accept(); if (d->state != Private::Connected) account()->groupChatLeave(d->dlg->jid().domain(), d->dlg->jid().node()); @@ -1311,6 +1363,15 @@ void GCMainDlg::updateMucName() } } +void GCMainDlg::updateConfiguration() +{ + JT_DiscoInfo *disco = new JT_DiscoInfo( + account()->client()->rootTask()); // FIXME in fact xep says we should do this before entering. + connect(disco, SIGNAL(finished()), SLOT(discoInfoFinished())); // but we need this just for name for now. + disco->get(jid()); // From other side we could provide the name outside. + disco->go(true); +} + void GCMainDlg::discoInfoFinished() { JT_DiscoInfo *t = static_cast(sender()); @@ -1323,6 +1384,13 @@ void GCMainDlg::discoInfoFinished() if (d->mucNameSource >= Private::TitleDisco) { updateMucName(); } + if (!d->gcSelfPresenceSupported) { + if (t->item().features().hasVCard()) { + auto avatarHash = x.getField("muc#roominfo_avatarhash").value().value(0); + account()->avatarFactory()->ensureVCardUpdated(jid(), QByteArray::fromHex(avatarHash.toLatin1()), + AvatarFactory::MucRoom); + } + } } // this one is called as result of avatarChanged event from avatar factory or @@ -1347,12 +1415,12 @@ void GCMainDlg::setMucSelfAvatar() void GCMainDlg::updateGCVCard() { - const VCard vcard = VCardFactory::instance()->vcard(jid()); - QPixmap avatar; + const auto vcard = VCardFactory::instance()->vcard(jid()); + QPixmap avatar; if (vcard) { - d->vcardMucName = vcard.nickName(); + d->vcardMucName = vcard.nickName().preferred().data.value(0); if (d->vcardMucName.isEmpty()) { - d->vcardMucName = vcard.fullName(); + d->vcardMucName = vcard.fullName().preferred(); } if (d->mucNameSource >= Private::TitleVCard) { updateMucName(); @@ -1410,6 +1478,7 @@ void GCMainDlg::mle_returnPressed() if (!nick.isEmpty() && newJid.isValid()) { d->prev_self = d->self; d->self = newJid.resource(); + ui_.log->setLocalNickname(d->self); account()->groupChatChangeNick(jid().domain(), jid().node(), d->self, account()->status()); } ui_.mle->chatEdit()->setText(""); @@ -1424,12 +1493,12 @@ void GCMainDlg::mle_returnPressed() return; Message m(jid()); - m.setType("groupchat"); + m.setType(Message::Type::Groupchat); m.setBody(str); QString id = account()->client()->genUniqueId(); m.setId(id); // we need id early for message manipulations in chatview if (ui_.mle->chatEdit()->isCorrection()) { - m.setReplaceId(ui_.mle->chatEdit()->lastMessageId()); + m.setReplaceId(ui_.mle->chatEdit()->correctionId()); } ui_.mle->chatEdit()->setLastMessageId(id); ui_.mle->chatEdit()->resetCorrection(); @@ -1477,14 +1546,14 @@ void GCMainDlg::openTopic() d->topicDlg->setAttribute(Qt::WA_DeleteOnClose); d->topicDlg->show(); QObject::connect(d->topicDlg, &GroupchatTopicDlg::accepted, this, - [=]() { sendNewTopic(d->topicDlg->subjectMap()); }); + [this]() { sendNewTopic(d->topicDlg->subjectMap()); }); } } void GCMainDlg::sendNewTopic(const QMap &topics) { Message m(jid()); - m.setType("groupchat"); + m.setType(Message::Type::Groupchat); for (auto it = topics.constBegin(); it != topics.constEnd(); ++it) { m.setSubject(it.value(), LanguageManager::toString(it.key())); } @@ -1506,8 +1575,8 @@ void GCMainDlg::doShowInfo() { QVBoxLayout *layout = new QVBoxLayout; - const VCard vcard = VCardFactory::instance()->vcard(jid()); - auto info = new InfoWidget(InfoWidget::Contact, jid(), vcard, account()); + const auto vcard = VCardFactory::instance()->vcard(jid()); + auto info = new InfoWidget(InfoWidget::MucView, jid(), vcard, account()); layout->addWidget(info); ui.tab_vcard->setLayout(layout); // connect(vcard_, SIGNAL(busy()), ui_.busy, SLOT(start())); @@ -1553,26 +1622,32 @@ void GCMainDlg::doBookmark() bm->setBookmarks(confs); return; } - ConferenceBookmark &b = confs[confInd]; - QDialog *dlg = new QDialog(this); - QVBoxLayout *layout = new QVBoxLayout; - QHBoxLayout *blayout = new QHBoxLayout; - QFormLayout *formLayout = new QFormLayout; - QLineEdit *txtName = new QLineEdit; - QLineEdit *txtNick = new QLineEdit; + ConferenceBookmark &b = confs[confInd]; + + QMenu *menu = new QMenu(this); + + menu->setAttribute(Qt::WA_DeleteOnClose); + // menu->winId(); + // menu->windowHandle()->setTransientParent(window()->windowHandle()); + auto wa = new QWidgetAction(menu); + auto dlg = new QWidget(menu); + wa->setDefaultWidget(dlg); + + QVBoxLayout *layout = new QVBoxLayout(dlg); + QHBoxLayout *blayout = new QHBoxLayout; + QFormLayout *formLayout = new QFormLayout; + QLineEdit *txtName = new QLineEdit; + QLineEdit *txtNick = new QLineEdit; // QCheckBox *chkAJoin = new QCheckBox; QComboBox *cbAutoJoin = new QComboBox; cbAutoJoin->addItems(ConferenceBookmark::joinTypeNames()); QPushButton *saveBtn = new QPushButton(dlg->style()->standardIcon(QStyle::SP_DialogSaveButton), tr("Save"), dlg); QPushButton *deleteBtn = new QPushButton(dlg->style()->standardIcon(QStyle::SP_DialogDiscardButton), tr("Delete"), dlg); - QPushButton *cancelBtn - = new QPushButton(dlg->style()->standardIcon(QStyle::SP_DialogCancelButton), tr("Cancel"), dlg); blayout->insertStretch(0); blayout->addWidget(saveBtn); blayout->addWidget(deleteBtn); - blayout->addWidget(cancelBtn); txtName->setText(b.name()); txtNick->setText(b.nick()); cbAutoJoin->setCurrentIndex(b.autoJoin()); @@ -1582,37 +1657,38 @@ void GCMainDlg::doBookmark() formLayout->addRow(tr("&Auto join:"), cbAutoJoin); layout->addLayout(formLayout); layout->addLayout(blayout); - dlg->setWindowIcon(IconsetFactory::icon("psi/bookmark_remove").icon()); - dlg->setLayout(layout); dlg->setMinimumWidth(300); - dlg->connect(saveBtn, SIGNAL(clicked()), dlg, SLOT(accept())); - dlg->connect(deleteBtn, SIGNAL(clicked()), dlg, SLOT(reject())); - dlg->connect(cancelBtn, SIGNAL(clicked()), dlg, SLOT(reject())); - connect(deleteBtn, SIGNAL(clicked()), this, SLOT(doRemoveBookmark())); - - dlg->setWindowTitle(tr("Bookmark conference")); - dlg->adjustSize(); - dlg->move(ui_.le_topic->mapToGlobal(QPoint(ui_.le_topic->width() - dlg->width(), ui_.le_topic->height()))); - if (dlg->exec() == QDialog::Accepted) { + dlg->connect(saveBtn, &QPushButton::clicked, this, [menu, txtName, txtNick, cbAutoJoin, this](bool) { ConferenceBookmark conf(txtName->text(), jid(), ConferenceBookmark::JoinType(cbAutoJoin->currentIndex()), txtNick->text(), d->password); - confs[confInd] = conf; - bm->setBookmarks(confs); - if (getDisplayName() != txtName->text()) - account()->actionRename(jid(), txtName->text()); - } - delete dlg; -} + auto bm = account()->bookmarkManager(); + if (bm->isAvailable()) { + int confInd = bm->indexOfConference(jid()); + QList confs = bm->conferences(); -void GCMainDlg::copyMucJid() { QApplication::clipboard()->setText(jid().bare()); } + if (confInd == -1) { + confs.append(conf); + } else { + confs[confInd] = conf; + } + bm->setBookmarks(confs); -void GCMainDlg::doRemoveBookmark() -{ - BookmarkManager *bm = account()->bookmarkManager(); - if (bm->isAvailable()) { - bm->removeConference(jid()); - } + if (getDisplayName() != txtName->text()) + account()->actionRename(jid(), txtName->text()); + } + menu->close(); + }); + dlg->connect(deleteBtn, &QPushButton::clicked, this, [menu, this]() { + BookmarkManager *bm = account()->bookmarkManager(); + if (bm->isAvailable()) { + bm->removeConference(jid()); + } + menu->close(); + }); + + menu->addAction(wa); + menu->popup(ui_.le_topic->mapToGlobal(QPoint(ui_.le_topic->width() - dlg->width(), ui_.le_topic->height()))); } void GCMainDlg::configureRoom() @@ -1662,6 +1738,7 @@ void GCMainDlg::setJid(const Jid &j) { TabbableWidget::setJid(j); d->self = d->prev_self = j.resource(); + ui_.log->setLocalNickname(d->self); } void GCMainDlg::goConn() @@ -1723,6 +1800,12 @@ void GCMainDlg::pa_updatedActivity() PsiAccount *GCMainDlg::account() const { return TabbableWidget::account(); } +void GCMainDlg::leave() +{ + d->forceQuit = true; + close(); +} + void GCMainDlg::error(int, const QString &str) { d->actions->action("gchat_set_topic")->setEnabled(false); @@ -1772,7 +1855,7 @@ void GCMainDlg::mucKickMsgHelper(const QString &nick, const Status &s, const QSt void GCMainDlg::gcSelfPresence(const Status &s) { d->gcSelfPresenceSupported = true; - account()->avatarFactory()->statusUpdate(jid().withResource(QString()), s); + account()->avatarFactory()->statusUpdate(jid(), s, AvatarFactory::MucRoom); } void GCMainDlg::presence(const QString &nick, const Status &s) @@ -1782,6 +1865,7 @@ void GCMainDlg::presence(const QString &nick, const Status &s) if (s.errorCode() == 409) { message = tr("Please choose a different nickname"); d->self = d->prev_self; + ui_.log->setLocalNickname(d->self); } else { message = tr("An error occurred (errorcode: %1)").arg(s.errorCode()); } @@ -1795,13 +1879,6 @@ void GCMainDlg::presence(const QString &nick, const Status &s) bool isSelf = (nick == d->self); if (isSelf) { - if (!d->gcSelfPresenceSupported && !d->gcSelfAvatarRequested) { - d->gcSelfAvatarRequested = true; - VCardFactory::instance()->getVCard( - jid(), account()->client()->rootTask(), this, [this]() { GCMainDlg::updateGCVCard(); }, true, false, - true); - } - if (s.isAvailable()) setStatusTabIcon(s.type()); UserListItem *u = account()->find(d->dlg->jid().bare()); @@ -2023,7 +2100,7 @@ void GCMainDlg::presence(const QString &nick, const Status &s) } if (!nick.isEmpty()) - account()->avatarFactory()->newMucItem(jidForNick(nick), s); + account()->avatarFactory()->statusUpdate(jidForNick(nick), s, AvatarFactory::MucUser); } XMPP::Jid GCMainDlg::jidForNick(const QString &nick) const { return Jid(jid()).withResource(nick); } @@ -2062,12 +2139,7 @@ void GCMainDlg::message(const Message &_m, const PsiEvent::Ptr &e) d->nonAnonymous = false; } if (dm.getMUCStatuses().contains(104)) { - // new MUC vcard available - if (!d->gcSelfPresenceSupported) { - // we had to handle avatar hash from presence already - VCardFactory::instance()->getVCard( - jid(), account()->client()->rootTask(), this, [this]() { GCMainDlg::updateGCVCard(); }, true); - } + updateConfiguration(); } PsiOptions *options = PsiOptions::instance(); @@ -2092,6 +2164,10 @@ void GCMainDlg::message(const Message &_m, const PsiEvent::Ptr &e) } if (!topic.isNull()) { + if (d->lastTopic == topic) { + return; // ignore same topic + } + d->lastTopic = topic; QString subjectTooltip = TextUtil::plain2rich(topic); subjectTooltip = TextUtil::linkify(subjectTooltip); if (options->getOption("options.ui.emoticons.use-emoticons").toBool()) { @@ -2121,6 +2197,18 @@ void GCMainDlg::message(const Message &_m, const PsiEvent::Ptr &e) return; } + if (!dm.reactions().targetId.isEmpty()) { + auto mv = MessageView::reactionsMessage(from, dm.reactions().targetId, dm.reactions().reactions); + ui_.log->dispatchMessage(mv); + return; + } + + if (!dm.retraction().isEmpty()) { + auto mv = MessageView::retractionMessage(dm.retraction()); + ui_.log->dispatchMessage(mv); + return; + } + if (dm.body().isEmpty()) return; @@ -2202,7 +2290,7 @@ void GCMainDlg::appendSysMsg(const QString &str, bool alert) void GCMainDlg::dispatchMessage(const MessageView &mv) { - if (d->trackBar && !mv.isLocal() && !mv.isSpooled()) + if (d->trackBar && !mv.isLocal() && !mv.isSpooled() && mv.reactionsId().isEmpty()) d->doTrackBar(); ui_.log->dispatchMessage(mv); @@ -2352,6 +2440,17 @@ void GCMainDlg::setLooks() : Qt::ScrollBarAsNeeded); ui_.lv_users->setLooks(); setMucSelfAvatar(); + updateIdentityVisibility(); +} + +void GCMainDlg::updateIdentityVisibility() +{ + if (!PsiOptions::instance()->getOption("options.ui.chat.use-small-chats").toBool()) { + bool visible = account()->psi()->contactList()->enabledAccounts().count() > 1; + ui_.lb_ident->setVisible(visible); + } else { + ui_.lb_ident->setVisible(false); + } } void GCMainDlg::setToolbuttons() diff --git a/src/groupchatdlg.h b/src/groupchatdlg.h index a6b1b69f81..29a7e934a1 100644 --- a/src/groupchatdlg.h +++ b/src/groupchatdlg.h @@ -51,6 +51,8 @@ class GCMainDlg : public TabbableWidget { PsiAccount *account() const; + void leave(); + void error(int, const QString &); void gcSelfPresence(const Status &s); void presence(const QString &, const Status &); @@ -119,8 +121,6 @@ private slots: void doShowInfo(); void doClear(); void doClearButton(); - void copyMucJid(); - void doRemoveBookmark(); void buildMenu(); void logSelectionChanged(); void setConnecting(); @@ -145,6 +145,8 @@ private slots: void avatarUpdated(const Jid &jid); void doContactContextMenu(const QString &nick); + void outgoingReactions(const QString &messageId, const QSet &reactions); + void sendMessageRetraction(const QString &messageId); public: class Private; friend class Private; @@ -169,6 +171,8 @@ private slots: inline XMPP::Jid jidForNick(const QString &nick) const; void setMucSelfAvatar(); + void updateIdentityVisibility(); + void updateConfiguration(); }; #endif // GROUPCHATDLG_H diff --git a/src/groupchatdlg.ui b/src/groupchatdlg.ui index 4bc08bd914..cfe6aaee0c 100644 --- a/src/groupchatdlg.ui +++ b/src/groupchatdlg.ui @@ -32,7 +32,7 @@ - Qt::Vertical + Qt::Orientation::Vertical @@ -42,10 +42,10 @@ - QFrame::NoFrame + QFrame::Shape::NoFrame - QFrame::Plain + QFrame::Shadow::Plain @@ -109,13 +109,20 @@ - QToolButton::InstantPopup + QToolButton::ToolButtonPopupMode::InstantPopup true + + + + AccountLabel + + + @@ -133,16 +140,16 @@ - QToolButton::InstantPopup + QToolButton::ToolButtonPopupMode::InstantPopup - Qt::ToolButtonTextOnly + Qt::ToolButtonStyle::ToolButtonTextOnly true - Qt::NoArrow + Qt::ArrowType::NoArrow @@ -158,7 +165,7 @@ - Qt::Horizontal + Qt::Orientation::Horizontal @@ -168,7 +175,7 @@ - Qt::NoFocus + Qt::FocusPolicy::NoFocus @@ -179,7 +186,7 @@ - Qt::ScrollBarAlwaysOff + Qt::ScrollBarPolicy::ScrollBarAlwaysOff @@ -194,10 +201,10 @@ - QFrame::NoFrame + QFrame::Shape::NoFrame - QFrame::Plain + QFrame::Shadow::Plain @@ -224,7 +231,7 @@ - Qt::Horizontal + Qt::Orientation::Horizontal @@ -237,7 +244,6 @@ - 75 true @@ -274,7 +280,7 @@ - Qt::CustomContextMenu + Qt::ContextMenuPolicy::CustomContextMenu Send @@ -297,11 +303,6 @@ - - PixmapRatioLabel - QLabel -
pixmapratiolabel.h
-
ChatView QFrame @@ -319,6 +320,16 @@ QTextEdit
chateditproxy.h
+ + AccountLabel + QLabel +
accountlabel.h
+
+ + PixmapRatioLabel + QLabel +
pixmapratiolabel.h
+
ActionLineEdit QLineEdit diff --git a/src/groupchattopicdlg.cpp b/src/groupchattopicdlg.cpp index 270b48b1a4..820ac6aac5 100644 --- a/src/groupchattopicdlg.cpp +++ b/src/groupchattopicdlg.cpp @@ -15,18 +15,22 @@ GroupchatTopicDlg::GroupchatTopicDlg(GCMainDlg *parent) : m_ui->setupUi(this); QKeySequence sendKey = ShortcutManager::instance()->shortcut("chat.send"); if (sendKey == QKeySequence(Qt::Key_Enter) || sendKey == QKeySequence(Qt::Key_Return)) { - sendKey = QKeySequence(Qt::CTRL | Qt::Key_Return); +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) + sendKey = QKeySequence(quint32(Qt::CTRL) | quint32(Qt::Key_Return)); +#else + sendKey = QKeySequence(QKeyCombination(Qt::CTRL, Qt::Key_Return)); +#endif } m_ui->buttonBox->button(QDialogButtonBox::Ok)->setShortcut(sendKey); auto cw = new QToolButton(); cw->setIcon(IconsetFactory::icon("psi/add").icon()); m_ui->twLang->setCornerWidget(cw); - QObject::connect(m_ui->twLang, &QTabWidget::tabCloseRequested, this, [=](int index) { + QObject::connect(m_ui->twLang, &QTabWidget::tabCloseRequested, this, [this](int index) { m_ui->twLang->widget(index)->deleteLater(); m_ui->twLang->removeTab(index); }); - QObject::connect(cw, &QToolButton::clicked, this, [=](bool checked) { + QObject::connect(cw, &QToolButton::clicked, this, [this, cw](bool checked) { Q_UNUSED(checked); if (!addLangDlg) { addLangDlg = new QDialog(this); @@ -54,7 +58,7 @@ GroupchatTopicDlg::GroupchatTopicDlg(GCMainDlg *parent) : addLangDlg->adjustSize(); addLangDlg->move(cw->mapToGlobal(QPoint(cw->width() - addLangDlg->width(), cw->height()))); addLangDlg->show(); - QObject::connect(addLangDlg, &QDialog::accepted, this, [=]() { + QObject::connect(addLangDlg, &QDialog::accepted, this, [this]() { LanguageManager::LangId id; id.language = quint16(m_addLangUi->cmbLang->currentData().toInt()); id.script = quint8(m_addLangUi->cmbScript->currentData().toInt()); @@ -76,7 +80,7 @@ GroupchatTopicDlg::GroupchatTopicDlg(GCMainDlg *parent) : QObject::connect(m_addLangUi->cmbLang, static_cast(&QComboBox::currentIndexChanged), this, - [=](int index) { + [this](int index) { Q_UNUSED(index) populateCountryAndScript(); }); diff --git a/src/httpauthmanager.cpp b/src/httpauthmanager.cpp index e0a1811188..72f667a0cc 100644 --- a/src/httpauthmanager.cpp +++ b/src/httpauthmanager.cpp @@ -75,7 +75,7 @@ void HttpAuthListener::reply(bool confirm, const PsiHttpAuthRequest &req) if (s.kind() == XMPP::Stanza::Message) { XMPP::Message m(s.from()); if (!confirm) { - m.setType("error"); + m.setType(XMPP::Message::Type::Error); m.setError(XMPP::HttpAuthRequest::denyError); } m.setHttpAuthRequest(req); diff --git a/src/httputil.cpp b/src/httputil.cpp index db54d88e0f..69139abbde 100644 --- a/src/httputil.cpp +++ b/src/httputil.cpp @@ -23,80 +23,78 @@ namespace Http { -std::tuple parseRangeHeader(const QByteArray &rangesBa, qint64 fileSize) +std::tuple parseRangeHeader(const QByteArray &rangesBa, std::optional fileSize) { - bool isRanged = false; - qint64 requestedStart = 0; - qint64 requestedSize = 0; - if (!rangesBa.startsWith("bytes=")) { - return std::tuple(NotImplementedRangeType, 0, 0); + return { NotImplementedRangeType, 0, 0 }; } if (rangesBa.indexOf(',') != -1) { - return std::tuple(NotImplementedMultirange, 0, 0); + return { NotImplementedMultirange, 0, 0 }; } auto ba = QByteArray::fromRawData(rangesBa.data() + sizeof("bytes"), rangesBa.size() - int(sizeof("bytes"))); auto l = ba.trimmed().split('-'); if (l.size() != 2) { - return std::tuple(Unparsed, 0, 0); + return { Unparsed, 0, 0 }; } - bool ok; - qint64 start; - qint64 end; + bool ok; + quint64 start; + std::optional end; if (!l[0].size()) { // bytes from the end are requested. Jingle-ft doesn't support this - return std::tuple(NotImplementedTailLoad, 0, 0); + return { NotImplementedTailLoad, 0, 0 }; } - start = l[0].toLongLong(&ok); + start = l[0].toULongLong(&ok); if (!ok) { - return std::tuple(Unparsed, 0, 0); + return { Unparsed, 0, 0 }; } - if (l[1].size()) { // if we have end - end = l[1].toLongLong(&ok); // then parse it - if (!ok || start > end) { // if something not parsed or range is invalid - return std::tuple(Unparsed, 0, 0); - } - - if (fileSize == -1 || start < fileSize) { - isRanged = true; - requestedStart = start; - requestedSize = end - start + 1; - } - } else { // no end. all the remaining - if (fileSize == -1 || start < fileSize) { - isRanged = true; - requestedStart = start; - requestedSize = 0; + if (l[1].size()) { // if we have end + end = l[1].toULongLong(&ok); // then parse it + if (!ok || start > *end) { // if something not parsed or range is invalid + return { Unparsed, 0, 0 }; } } - if (fileSize >= 0 && !isRanged) { // isRanged is not set. So it doesn't fit - return std::tuple(OutOfRange, 0, 0); + if (fileSize && start >= *fileSize) { + return { OutOfRange, 0, 0 }; } - - return std::tuple(Parsed, requestedStart, requestedSize); + return { Parsed, start, end ? (*end - start + 1) : 0 }; } -std::tuple parseContentRangeHeader(const QByteArray &value) +std::optional>> parseContentRangeHeader(const QByteArray &value) { auto arr = value.split(' '); - if (arr.size() != 2 || arr[0] != "bytes" || (arr = arr[1].split('-')).size() != 2) - return std::tuple(false, 0, 0); - qint64 start, size; - bool ok; - start = arr[0].toLongLong(&ok); - if (ok) { - arr = arr[1].split('/'); - size = arr[0].toLongLong(&ok) - start + 1; + if (arr.size() != 2 || arr[0] != "bytes" || (arr = arr[1].split('/')).size() != 2) + return {}; + + quint64 start, end; + std::optional totalSize; + bool ok; + + if (arr[1] != "*") { + totalSize = arr[1].toULongLong(&ok); + if (!ok) { + return {}; + } + } + + arr = arr[0].split('-'); + if (arr.size() != 2) { + start = arr[0].toULongLong(&ok); + if (ok) { + end = arr[0].toULongLong(&ok); + if (ok && start <= end) { + if (!totalSize || (start < *totalSize && end < *totalSize)) { + return std::make_tuple(start, end - start + 1, totalSize); + } + } + } } - if (!ok || size <= 0) - return std::tuple(false, 0, 0); - return std::tuple(true, start, size); + return {}; } } diff --git a/src/httputil.h b/src/httputil.h index dfb71ca8f5..66b740e582 100644 --- a/src/httputil.h +++ b/src/httputil.h @@ -18,6 +18,8 @@ */ #include + +#include #include namespace Http { @@ -34,12 +36,14 @@ enum ParseResult { /** * @brief parseRangeHeader parses http bytes uni-"Range" header * @param value - heder value - * @param fileSize - if destination file size is know it will be checked. set -1 when not known + * @param fileSize - if destination file size is known it will be checked. set -1 when not known * @return (result, start, size) * * While it's allowed by standard this parser will return error is start is not set in the header */ -std::tuple parseRangeHeader(const QByteArray &value, qint64 fileSize = -1); -std::tuple parseContentRangeHeader(const QByteArray &value); +std::tuple parseRangeHeader(const QByteArray &value, + std::optional fileSize = -1); + +std::optional>> parseContentRangeHeader(const QByteArray &value); } diff --git a/src/infodlg.cpp b/src/infodlg.cpp index 47505490b3..d90844efd2 100644 --- a/src/infodlg.cpp +++ b/src/infodlg.cpp @@ -19,19 +19,20 @@ #include "infodlg.h" +#include "avatars.h" #include "busywidget.h" #include "common.h" #include "desktoputil.h" #include "discodlg.h" #include "fileutil.h" #include "iconset.h" -#include "iconwidget.h" #include "iris/xmpp_client.h" #include "iris/xmpp_serverinfomanager.h" #include "iris/xmpp_tasks.h" #include "iris/xmpp_vcard.h" #include "lastactivitytask.h" #include "msgmle.h" +#include "pepmanager.h" #include "psiaccount.h" #include "psioptions.h" #include "psirichtext.h" @@ -39,6 +40,8 @@ #include "userlist.h" #include "vcardfactory.h" #include "vcardphotodlg.h" +#include "xmpp/xmpp-im/xmpp_caps.h" +#include "xmpp/xmpp-im/xmpp_vcard4.h" #include #include @@ -144,13 +147,12 @@ class InfoWidget::Private { int type = 0; int actionType = 0; Jid jid; - VCard vcard; - PsiAccount *pa = nullptr; - bool busy = false; - bool te_edited = false; - bool cacheVCard = false; - JT_VCard *jt = nullptr; + VCard4::VCard vcard; + PsiAccount *pa = nullptr; + bool busy = false; + bool te_edited = false; QByteArray photo; + QString photoMime; QDate bday; QString dateTextFormat; QList infoRequested; @@ -200,20 +202,18 @@ class InfoWidget::Private { } }; -InfoWidget::InfoWidget(int type, const Jid &j, const VCard &vcard, PsiAccount *pa, QWidget *parent, bool cacheVCard) : +InfoWidget::InfoWidget(int type, const Jid &j, const VCard4::VCard &vcard, PsiAccount *pa, QWidget *parent) : QWidget(parent) { m_ui.setupUi(this); d = new Private; d->type = type; d->jid = j; - d->vcard = vcard ? vcard : VCard::makeEmpty(); + d->vcard = vcard; d->pa = pa; d->busy = false; d->te_edited = false; - d->jt = nullptr; - d->pa->dialogRegister(this, j); - d->cacheVCard = cacheVCard; + d->pa->dialogRegister(window(), j); d->dateTextFormat = "d MMM yyyy"; setWindowTitle(d->jid.full()); @@ -304,6 +304,23 @@ InfoWidget::InfoWidget(int type, const Jid &j, const VCard &vcard, PsiAccount *p ur.setStatus(pa->gcContactStatus(j)); d->userListItem->userResourceList().append(ur); d->userListItem->setAvatarFactory(pa->avatarFactory()); + connect(d->pa->client(), &XMPP::Client::groupChatPresence, this, [this](const Jid &j, const Status &s) { + if (d->jid.compare(j, true)) { + UserResource ur; + ur.setName(j.resource()); + ur.setStatus(s); + UserResourceList url; + url.append(ur); + d->userListItem->userResourceList() = url; + updateStatus(); + requestResourceInfo(j); + if (s.photoHash()) { + // for vcard we need rather immediate update, regardless if pubsub avatar is set. + // so don't use AvatarFactory and use VCardFactory directly. who knows what's else changed + VCardFactory::instance()->ensureVCardPhotoUpdated(d->pa, j, VCardFactory::MucUser, *s.photoHash()); + } + } + }); } else { d->userListItem = nullptr; } @@ -314,8 +331,20 @@ InfoWidget::InfoWidget(int type, const Jid &j, const VCard &vcard, PsiAccount *p connect(d->pa->client(), SIGNAL(resourceUnavailable(const Jid &, const Resource &)), SLOT(contactUnavailable(const Jid &, const Resource &))); connect(d->pa, SIGNAL(updateContact(const Jid &)), SLOT(contactUpdated(const Jid &))); + connect(VCardFactory::instance(), &VCardFactory::vcardChanged, this, + [this](const Jid &j, VCardFactory::Flags flags) { + if (d->jid.compare(j, flags & VCardFactory::MucUser)) { + auto vcard = VCardFactory::instance()->vcard(j, flags); + if (vcard) { + d->vcard = vcard; + setData(d->vcard); + } + updateNick(); + } + }); m_ui.te_status->setReadOnly(true); m_ui.te_status->setAcceptRichText(true); + m_ui.te_status->setKeepAtBottom(false); PsiRichText::install(m_ui.te_status->document()); updateStatus(); const auto &items = d->findRelevant(j); @@ -334,7 +363,7 @@ InfoWidget::InfoWidget(int type, const Jid &j, const VCard &vcard, PsiAccount *p InfoWidget::~InfoWidget() { - d->pa->dialogUnregister(this); + d->pa->dialogUnregister(window()); delete d->userListItem; d->userListItem = nullptr; delete d; @@ -359,156 +388,89 @@ bool InfoWidget::aboutToClose() : tr("You have not published your account information changes.\nAre you sure you want to discard " "them?"), QMessageBox::Discard | QMessageBox::No); - if (n != 0) { + if (n != QMessageBox::Discard) { return false; } } - - // cancel active transaction (refresh only) - if (d->busy && d->actionType == 0) { - delete d->jt; - d->jt = nullptr; - } - return true; } -void InfoWidget::jt_finished() +void InfoWidget::setData(const VCard4::VCard &i) { - d->jt = nullptr; - JT_VCard *jtVCard = static_cast(sender()); - - d->busy = false; - emit released(); - fieldsEnable(true); - - if (jtVCard->success()) { - if (d->actionType == 0) { - d->vcard = jtVCard->vcard(); - setData(d->vcard); - } else if (d->actionType == 1) { - d->vcard = jtVCard->vcard(); - if (d->cacheVCard) - VCardFactory::instance()->setVCard(d->jid, d->vcard); - setData(d->vcard); - } - - if (d->jid.compare(d->pa->jid(), false)) { - if (!d->vcard.nickName().isEmpty()) - d->pa->setNick(d->vcard.nickName()); - else - d->pa->setNick(d->pa->jid().node()); - } - - if (d->actionType == 1) - QMessageBox::information(this, tr("Success"), - d->type == MucAdm ? tr("Your conference information has been published.") - : tr("Your account information has been published.")); - } else { - if (d->actionType == 0) { - if (d->type == Self) - QMessageBox::critical( - this, tr("Error"), - tr("Unable to retrieve your account information. Perhaps you haven't entered any yet.")); - else if (d->type == MucAdm) - QMessageBox::critical(this, tr("Error"), - tr("Unable to retrieve information about this conference.\nReason: %1") - .arg(jtVCard->statusString())); - else - QMessageBox::critical( - this, tr("Error"), - tr("Unable to retrieve information about this contact.\nReason: %1").arg(jtVCard->statusString())); + auto names = i.names(); + d->le_givenname->setText(names.data.given.value(0)); + d->le_middlename->setText(names.data.additional.value(0)); + d->le_familyname->setText(names.data.surname.value(0)); + m_ui.le_nickname->setText(i.nickName().preferred().data.value(0)); + d->bday = i.bday(); + m_ui.le_bday->setText(d->bday.toString(d->dateTextFormat)); + + const QString fullName = i.fullName().value(0).data; + if (d->type != MucAdm && fullName.isEmpty()) { + auto fn = QString("%1 %2 %3") + .arg(names.data.given.value(0), names.data.additional.value(0), names.data.surname.value(0)) + .simplified(); + if (d->type == Self) { + m_ui.le_fullname->setPlaceholderText(fn); } else { - QMessageBox::critical( - this, tr("Error"), - tr("Unable to publish your account information.\nReason: %1").arg(jtVCard->statusString())); + m_ui.le_fullname->setText(fn); } - } -} - -void InfoWidget::setData(const VCard &i) -{ - d->le_givenname->setText(i.givenName()); - d->le_middlename->setText(i.middleName()); - d->le_familyname->setText(i.familyName()); - m_ui.le_nickname->setText(i.nickName()); - d->bday = QDate::fromString(i.bdayStr(), Qt::ISODate); - if (d->bday.isValid()) { - m_ui.le_bday->setText(d->bday.toString(d->dateTextFormat)); - } else { - m_ui.le_bday->setText(i.bdayStr()); - } - const QString fullName = i.fullName(); - if (d->type != Self && d->type != MucAdm && fullName.isEmpty()) { - m_ui.le_fullname->setText(QString("%1 %2 %3").arg(i.givenName(), i.middleName(), i.familyName())); } else { m_ui.le_fullname->setText(fullName); } - m_ui.le_fullname->setToolTip(QString("") + tr("First Name:") + " " + TextUtil::escape(d->vcard.givenName()) - + "
" + "" + tr("Middle Name:") + " " - + TextUtil::escape(d->vcard.middleName()) + "
" + "" + tr("Last Name:") + " " - + TextUtil::escape(d->vcard.familyName())); + m_ui.le_fullname->setToolTip(QString("") + tr("First Name:") + " " + + TextUtil::escape(names.data.given.value(0)) + "
" + "" + tr("Middle Name:") + + " " + TextUtil::escape(names.data.additional.value(0)) + "
" + "" + + tr("Last Name:") + " " + TextUtil::escape(names.data.surname.value(0))); // E-Mail handling - VCard::Email email; - VCard::Email internetEmail; - for (auto const &e : i.emailList()) { - if (e.pref) { - email = e; - } - if (e.internet) { - internetEmail = e; - } - } - if (email.userid.isEmpty() && !i.emailList().isEmpty()) - email = internetEmail.userid.isEmpty() ? i.emailList()[0] : internetEmail; - m_ui.le_email->setText(email.userid); + auto email = i.emails().preferred(); + m_ui.le_email->setText(email); AddressTypeDlg::AddrTypes addTypes; - addTypes |= (email.pref ? AddressTypeDlg::Pref : AddressTypeDlg::None); - addTypes |= (email.home ? AddressTypeDlg::Home : AddressTypeDlg::None); - addTypes |= (email.work ? AddressTypeDlg::Work : AddressTypeDlg::None); - addTypes |= (email.internet ? AddressTypeDlg::Internet : AddressTypeDlg::None); - addTypes |= (email.x400 ? AddressTypeDlg::X400 : AddressTypeDlg::None); + addTypes |= (email.parameters.pref ? AddressTypeDlg::Pref : AddressTypeDlg::None); + addTypes |= (email.parameters.type.contains(QLatin1String("home")) ? AddressTypeDlg::Home : AddressTypeDlg::None); + addTypes |= (email.parameters.type.contains(QLatin1String("work")) ? AddressTypeDlg::Work : AddressTypeDlg::None); d->emailsDlg->setTypes(addTypes); - m_ui.le_homepage->setText(i.url()); - d->homepageAction->setVisible(!i.url().isEmpty()); - - QString phone; - if (!i.phoneList().isEmpty()) - phone = i.phoneList()[0].number; - m_ui.le_phone->setText(phone); - - VCard::Address addr; - if (!i.addressList().isEmpty()) - addr = i.addressList()[0]; - m_ui.le_street->setText(addr.street); - m_ui.le_ext->setText(addr.extaddr); - m_ui.le_city->setText(addr.locality); - m_ui.le_state->setText(addr.region); - m_ui.le_pcode->setText(addr.pcode); - m_ui.le_country->setText(addr.country); + QUrl homepage = i.urls(); + m_ui.le_homepage->setText(homepage.toString()); + d->homepageAction->setVisible(!m_ui.le_homepage->text().isEmpty()); + m_ui.le_phone->setText(i.phones()); - m_ui.le_orgName->setText(i.org().name); + auto addr = i.addresses().preferred().data; + m_ui.le_street->setText(addr.street.value(0)); + m_ui.le_ext->setText(addr.extaddr.value(0)); + m_ui.le_city->setText(addr.locality.value(0)); + m_ui.le_state->setText(addr.region.value(0)); + m_ui.le_pcode->setText(addr.code.value(0)); + m_ui.le_country->setText(addr.country.value(0)); - QString unit; - if (!i.org().unit.isEmpty()) - unit = i.org().unit[0]; - m_ui.le_orgUnit->setText(unit); + m_ui.le_orgName->setText(i.org()); m_ui.le_title->setText(i.title()); m_ui.le_role->setText(i.role()); - m_ui.te_desc->setPlainText(i.desc().isEmpty() ? i.note() : i.desc()); + m_ui.te_desc->setPlainText(i.note()); - if (!i.photo().isEmpty()) { - // printf("There is a picture...\n"); + auto pix = d->type == MucContact ? d->pa->avatarFactory()->getMucAvatar(d->jid) + : d->pa->avatarFactory()->getAvatar(d->jid); + if (pix.isNull()) { d->photo = i.photo(); - if (!updatePhoto()) { + if (d->photo.isEmpty()) { clearPhoto(); + } else { + // FIXME ideally we should come here after avatar factory is updated + // the this code would be unnecessary + updatePhoto(); } } else { - clearPhoto(); + // yes we could use pixmap directly. let's keep it for future code optimization + QByteArray ba; + QBuffer b(&ba); + b.open(QIODevice::WriteOnly); + pix.toImage().save(&b, "PNG"); + d->photo = ba; + updatePhoto(); } setEdited(false); @@ -528,8 +490,11 @@ bool InfoWidget::updatePhoto() { const QImage img = QImage::fromData(d->photo); if (img.isNull()) { + d->photo = {}; return false; } + QMimeDatabase db; + d->photoMime = db.mimeTypeForData(d->photo).name(); int max_width = m_ui.tb_photo->width() - 10; // FIXME: Ugly magic number int max_height = m_ui.tb_photo->height() - 10; // FIXME: Ugly magic number @@ -665,6 +630,23 @@ void InfoWidget::setReadOnly(bool x) m_ui.te_desc->setReadOnly(x); } +void InfoWidget::release() +{ + d->busy = false; + emit released(); + fieldsEnable(true); +} + +void InfoWidget::updateNick() +{ + if (d->jid.compare(d->pa->jid(), false)) { + if (!d->vcard.nickName().preferred().data.isEmpty()) + d->pa->setNick(d->vcard.nickName().preferred()); + else + d->pa->setNick(d->pa->jid().node()); + } +} + void InfoWidget::doRefresh() { if (!d->pa->checkConnected(this)) @@ -674,11 +656,40 @@ void InfoWidget::doRefresh() fieldsEnable(false); - d->actionType = 0; emit busy(); - d->jt = VCardFactory::instance()->getVCard( - d->jid, d->pa->client()->rootTask(), this, [this]() { jt_finished(); }, d->cacheVCard, d->type == MucContact); + VCardFactory::Flags flags; + if (d->type == MucContact) { + flags |= VCardFactory::MucUser; + } + if (d->type == MucAdm || d->type == MucView) { + flags |= VCardFactory::MucRoom; + } + auto request = VCardFactory::instance()->getVCard(d->pa, d->jid, flags); + QObject::connect(request, &VCardRequest::finished, this, [this, request]() { + release(); + if (request->success()) { + auto vcard = request->vcard(); + if (vcard) { + d->vcard = vcard; + setData(d->vcard); + } + updateNick(); + } else { + if (d->type == Self) + QMessageBox::critical( + this, tr("Error"), + tr("Unable to retrieve your account information. Perhaps you haven't entered any yet.")); + else if (d->type == MucAdm || d->type == MucView) + QMessageBox::critical(this, tr("Error"), + tr("Unable to retrieve information about this conference.\nReason: %1") + .arg(request->errorString())); + else + QMessageBox::critical( + this, tr("Error"), + tr("Unable to retrieve information about this contact.\nReason: %1").arg(request->errorString())); + } + }); } void InfoWidget::publish() @@ -693,17 +704,46 @@ void InfoWidget::publish() return; } - VCard submit_vcard = makeVCard(); + d->vcard = makeVCard(); fieldsEnable(false); - d->actionType = 1; emit busy(); + VCard4::VCard v = d->vcard; + VCardFactory::Flags flags; + Jid target; if (d->type == MucAdm) { - VCardFactory::instance()->setTargetVCard(d->pa, submit_vcard, d->jid, this, SLOT(jt_finished())); - } else { - VCardFactory::instance()->setVCard(d->pa, submit_vcard, this, SLOT(jt_finished())); + flags |= VCardFactory::MucRoom; + target = d->jid; + } + auto task = VCardFactory::instance()->setVCard(d->pa, v, target, flags); + connect(task, &JT_VCard::finished, this, [this, task]() { + release(); + if (task->success()) { + updateNick(); + QMessageBox::information(this, tr("Success"), + d->type == MucAdm ? tr("Your conference information has been published.") + : tr("Your account information has been published.")); + } else { + QMessageBox::critical( + this, tr("Error"), + tr("Unable to publish your account information.\nReason: %1").arg(task->statusString())); + } + }); + + bool hasAvaConv = account()->client()->serverInfoManager()->accountFeatures().hasAvatarConversion(); + if (!hasAvaConv) { + v.detach(); + if (d->photo.size()) { + v.setPhoto(VCard4::UriValue { d->photo, d->photoMime }); + } + VCardFactory::instance()->setVCard(d->pa, v, target, flags | VCardFactory::ForceVCardTemp); + } + + if (d->type == Self) { + // publish or retract avatar depending on d->photo contents + d->pa->avatarFactory()->setSelfAvatar(QImage::fromData(d->photo)); } } @@ -739,86 +779,162 @@ void InfoWidget::doClearBirthDate() d->bdayPopup->hide(); } -VCard InfoWidget::makeVCard() +VCard4::VCard InfoWidget::makeVCard() { - VCard v = VCard::makeEmpty(); - - v.setFullName(m_ui.le_fullname->text()); - v.setGivenName(d->le_givenname->text()); - v.setMiddleName(d->le_middlename->text()); - v.setFamilyName(d->le_familyname->text()); - v.setNickName(m_ui.le_nickname->text()); - v.setBdayStr(d->bday.isValid() ? d->bday.toString(Qt::ISODate) : m_ui.le_bday->text()); - - if (!m_ui.le_email->text().isEmpty()) { - VCard::Email email; - auto types = d->emailsDlg->types(); + VCard4::VCard v; + + using namespace VCard4; + + // Full Name + QString fullName = m_ui.le_fullname->text(); + if (!fullName.isEmpty()) { + v.setFullName({ { PString { Parameters(), fullName } } }); + } + + // Given Name + PNames names; + QString givenName = d->le_givenname->text(); + QString middleName = d->le_middlename->text(); + QString familyName = d->le_familyname->text(); + + if (!givenName.isEmpty()) { + names.data.given.append(givenName); + } + if (!middleName.isEmpty()) { + names.data.additional.append(middleName); + } + if (!familyName.isEmpty()) { + names.data.surname.append(familyName); + } + if (!names.data.isEmpty()) { + v.setNames(names); + } + + // Nickname + QString nickName = m_ui.le_nickname->text(); + if (!nickName.isEmpty()) { + v.setNickName({ nickName }); + } + + // Birthday + if (d->bday.isValid()) { + v.setBday({ Parameters(), d->bday }); + } else { + QString bdayStr = m_ui.le_bday->text(); + if (!bdayStr.isEmpty()) { + v.setBday({ Parameters(), bdayStr }); + } + } + + // Email + QString emailText = m_ui.le_email->text(); + if (!emailText.isEmpty()) { + PStrings emails; + Parameters emailParams; + auto types = d->emailsDlg->types(); if (types & AddressTypeDlg::Pref) { - email.pref = true; + emailParams.pref = 1; types ^= AddressTypeDlg::Pref; } if (!types) { - email.internet = true; + emailParams.type << "internet"; } else { - email.internet = bool(types & AddressTypeDlg::Internet); - email.home = bool(types & AddressTypeDlg::Home); - email.work = bool(types & AddressTypeDlg::Work); - email.x400 = bool(types & AddressTypeDlg::X400); + if (types & AddressTypeDlg::Internet) + emailParams.type << "internet"; + if (types & AddressTypeDlg::Home) + emailParams.type << "home"; + if (types & AddressTypeDlg::Work) + emailParams.type << "work"; + if (types & AddressTypeDlg::X400) + emailParams.type << "x400"; } - email.userid = m_ui.le_email->text(); - - VCard::EmailList list; - list << email; - v.setEmailList(list); + emails.append({ emailParams, emailText }); + v.setEmails(emails); } - v.setUrl(m_ui.le_homepage->text()); - - if (!m_ui.le_phone->text().isEmpty()) { - VCard::Phone p; - p.home = true; - p.voice = true; - p.number = m_ui.le_phone->text(); - - VCard::PhoneList list; - list << p; - v.setPhoneList(list); + // URL + QString homepage = m_ui.le_homepage->text(); + if (!homepage.isEmpty()) { + QUrl url(homepage); + if (url.isValid()) { + v.setUrls(url); + } } - if (!d->photo.isEmpty()) { - // printf("Adding a pixmap to the vCard...\n"); - v.setPhoto(d->photo); + // Phone + QString phoneText = m_ui.le_phone->text(); + if (!phoneText.isEmpty()) { + PUrisOrTexts phones; + Parameters phoneParams; + phoneParams.type << "home" + << "voice"; + phones.append({ phoneParams, phoneText }); + v.setPhones(phones); } - if (!m_ui.le_street->text().isEmpty() || !m_ui.le_ext->text().isEmpty() || !m_ui.le_city->text().isEmpty() - || !m_ui.le_state->text().isEmpty() || !m_ui.le_pcode->text().isEmpty() || !m_ui.le_country->text().isEmpty()) { - VCard::Address addr; - addr.home = true; - addr.street = m_ui.le_street->text(); - addr.extaddr = m_ui.le_ext->text(); - addr.locality = m_ui.le_city->text(); - addr.region = m_ui.le_state->text(); - addr.pcode = m_ui.le_pcode->text(); - addr.country = m_ui.le_country->text(); - - VCard::AddressList list; - list << addr; - v.setAddressList(list); + // Photo + if (d->type != Self && !d->photo.isEmpty()) { + v.setPhoto(UriValue(d->photo, d->photoMime)); } - VCard::Org org; + // Address + QString street = m_ui.le_street->text(); + QString ext = m_ui.le_ext->text(); + QString city = m_ui.le_city->text(); + QString state = m_ui.le_state->text(); + QString pcode = m_ui.le_pcode->text(); + QString country = m_ui.le_country->text(); + if (!street.isEmpty() || !ext.isEmpty() || !city.isEmpty() || !state.isEmpty() || !pcode.isEmpty() + || !country.isEmpty()) { + PAddresses addresses; + Parameters addressParams; + addressParams.type << "home"; + VCard4::Address address; + if (!street.isEmpty()) + address.street.append(street); + if (!ext.isEmpty()) + address.extaddr.append(ext); + if (!city.isEmpty()) + address.locality.append(city); + if (!state.isEmpty()) + address.region.append(state); + if (!pcode.isEmpty()) + address.code.append(pcode); + if (!country.isEmpty()) + address.country.append(country); + addresses.append({ addressParams, address }); + v.setAddresses(addresses); + } - org.name = m_ui.le_orgName->text(); + // Organization + QString orgName = m_ui.le_orgName->text(); + QString orgUnit = m_ui.le_orgUnit->text(); + if (!orgName.isEmpty()) { + PStringLists org; + org.append({ Parameters(), { orgName } }); + if (!orgUnit.isEmpty()) { + org.append({ Parameters(), { orgUnit } }); + } + v.setOrg(org); + } - if (!m_ui.le_orgUnit->text().isEmpty()) { - org.unit << m_ui.le_orgUnit->text(); + // Title + QString title = m_ui.le_title->text(); + if (!title.isEmpty()) { + v.setTitle({ { PString { Parameters(), title } } }); } - v.setOrg(org); + // Role + QString role = m_ui.le_role->text(); + if (!role.isEmpty()) { + v.setRole({ { PString { Parameters(), role } } }); + } - v.setTitle(m_ui.le_title->text()); - v.setRole(m_ui.le_role->text()); - v.setDesc(m_ui.te_desc->toPlainText()); + // Description (note) + QString desc = m_ui.te_desc->toPlainText(); + if (!desc.isEmpty()) { + v.setNote({ { PString { Parameters(), desc } } }); + } return v; } @@ -900,6 +1016,10 @@ void InfoWidget::updateStatus() } else { m_ui.te_status->clear(); } + + auto cursor = m_ui.te_status->textCursor(); + cursor.setPosition(0); + m_ui.te_status->setTextCursor(cursor); } /** @@ -1047,14 +1167,14 @@ void InfoWidget::goHomepage() // -------------------------------------------- // InfoDlg // -------------------------------------------- -InfoDlg::InfoDlg(int type, const Jid &j, const VCard &vc, PsiAccount *pa, QWidget *parent, bool cacheVCard) +InfoDlg::InfoDlg(int type, const Jid &j, const VCard4::VCard &vc, PsiAccount *pa, QWidget *parent) : QDialog(parent) { setAttribute(Qt::WA_DeleteOnClose); setWindowFlags(Qt::Dialog | Qt::WindowTitleHint | Qt::WindowMinMaxButtonsHint | Qt::WindowCloseButtonHint | Qt::CustomizeWindowHint); setModal(false); m_ui.setupUi(this); - m_iw = new InfoWidget(type, j, vc, pa, parent, cacheVCard); + m_iw = new InfoWidget(type, j, vc, pa, this); m_ui.loContents->addWidget(m_iw); if (type == InfoWidget::Self) { @@ -1069,13 +1189,13 @@ InfoDlg::InfoDlg(int type, const Jid &j, const VCard &vc, PsiAccount *pa, QWidge else adjustSize(); - connect(m_ui.pb_refresh, SIGNAL(clicked()), m_iw, SLOT(doRefresh())); - connect(m_ui.pb_refresh, SIGNAL(clicked()), m_iw, SLOT(updateStatus())); - connect(m_ui.pb_submit, SIGNAL(clicked()), m_iw, SLOT(publish())); - connect(m_ui.pb_close, SIGNAL(clicked()), this, SLOT(close())); - connect(m_ui.pb_disco, SIGNAL(clicked()), this, SLOT(doDisco())); - connect(m_iw, SIGNAL(busy()), SLOT(doBusy())); - connect(m_iw, SIGNAL(released()), SLOT(release())); + connect(m_ui.pb_refresh, &IconButton::clicked, m_iw, &InfoWidget::doRefresh); + connect(m_ui.pb_refresh, &IconButton::clicked, m_iw, &InfoWidget::updateStatus); + connect(m_ui.pb_submit, &IconButton::clicked, m_iw, &InfoWidget::publish); + connect(m_ui.pb_close, &IconButton::clicked, this, &InfoDlg::close); + connect(m_ui.pb_disco, &IconButton::clicked, this, &InfoDlg::doDisco); + connect(m_iw, &InfoWidget::busy, this, &InfoDlg::doBusy); + connect(m_iw, &InfoWidget::released, this, &InfoDlg::release); } void InfoDlg::closeEvent(QCloseEvent *e) diff --git a/src/infodlg.h b/src/infodlg.h index 377238c5dc..7702796e87 100644 --- a/src/infodlg.h +++ b/src/infodlg.h @@ -29,15 +29,17 @@ namespace XMPP { class Jid; class VCard; class Resource; +namespace VCard4 { + class VCard; +} } using namespace XMPP; class InfoWidget : public QWidget { Q_OBJECT public: - enum { Self, Contact, MucContact, MucAdm }; - InfoWidget(int type, const XMPP::Jid &, const XMPP::VCard &, PsiAccount *, QWidget *parent = nullptr, - bool cacheVCard = true); + enum { Self, Contact, MucContact, MucAdm, MucView }; + InfoWidget(int type, const XMPP::Jid &, const XMPP::VCard4::VCard &, PsiAccount *, QWidget *parent = nullptr); ~InfoWidget(); bool aboutToClose(); /* call this when you are going to close parent dialog */ PsiAccount *account() const; @@ -62,7 +64,6 @@ private slots: void clientVersionFinished(); void entityTimeFinished(); void requestLastActivityFinished(); - void jt_finished(); void doShowCal(); void doUpdateFromCalendar(const QDate &); void doClearBirthDate(); @@ -76,19 +77,19 @@ private slots: class Private; Private *d; Ui::Info m_ui; - // QPushButton* pb_refresh_; - // QPushButton* pb_close_; - // QPushButton* pb_submit_; - - void setData(const XMPP::VCard &); - XMPP::VCard makeVCard(); - void fieldsEnable(bool); - void setReadOnly(bool); - bool edited(); - void setEdited(bool); - void setPreviewPhoto(const QString &str); - void requestResourceInfo(const XMPP::Jid &j); - void requestLastActivity(); + + XMPP::VCard4::VCard makeVCard(); + + void setData(const XMPP::VCard4::VCard &); + void fieldsEnable(bool); + void setReadOnly(bool); + bool edited(); + void setEdited(bool); + void setPreviewPhoto(const QString &str); + void requestResourceInfo(const XMPP::Jid &j); + void requestLastActivity(); + void release(); + void updateNick(); signals: void busy(); @@ -98,8 +99,7 @@ private slots: class InfoDlg : public QDialog { Q_OBJECT public: - InfoDlg(int type, const XMPP::Jid &, const XMPP::VCard &, PsiAccount *, QWidget *parent = nullptr, - bool cacheVCard = true); + InfoDlg(int type, const XMPP::Jid &, const XMPP::VCard4::VCard &, PsiAccount *, QWidget *parent = nullptr); inline InfoWidget *infoWidget() const { return m_iw; } protected: diff --git a/src/main.cpp b/src/main.cpp index ed5c90fb92..071a283dca 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -132,6 +132,10 @@ bool PsiMain::useActiveInstance() ActiveProfiles::instance()->raise(cmdline.value("profile"), true); } + if (cmdline.contains("quit")) { + ActiveProfiles::instance()->quit(cmdline.value("profile")); + } + return true; } else return cmdline.contains("remote"); @@ -282,11 +286,11 @@ void PsiMain::sessionStart() connect(pcon, SIGNAL(quit(int)), SLOT(sessionQuit(int))); if (cmdline.contains("uri")) { - emit ActiveProfiles::instance() -> openUriRequested(cmdline.value("uri")); + emit ActiveProfiles::instance()->openUriRequested(cmdline.value("uri")); cmdline.remove("uri"); } if (cmdline.contains("status") || cmdline.contains("status-message")) { - emit ActiveProfiles::instance() -> setStatusRequested(cmdline.value("status"), cmdline.value("status-message")); + emit ActiveProfiles::instance()->setStatusRequested(cmdline.value("status"), cmdline.value("status-message")); cmdline.remove("status"); cmdline.remove("status-message"); } @@ -508,16 +512,18 @@ PSI_EXPORT_FUNC int main(int argc, char *argv[]) QCoreApplication::addLibraryPath(appPath); #endif -#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0) && QT_VERSION <= QT_VERSION_CHECK(6, 8, 0) && defined(WEBENGINE) - // let's hope https://bugreports.qt.io/browse/QTBUG-119221 is going to be fixed before 6.8.0 - // Qt::WA_NativeWindow is already added to webview.cpp - qputenv("QT_WIDGETS_RHI", "1"); +#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0) && defined(WEBENGINE) #ifdef Q_OS_WIN - qputenv("QT_WIDGETS_RHI_BACKEND", "d3d11"); -#elif defined(Q_OS_MAC) - qputenv("QT_WIDGETS_RHI_BACKEND", "metal"); + // https://bugreports.qt.io/browse/QTBUG-119221 + // See also Qt::WA_NativeWindow in WebView::WebView. + // Note, it can be highly unstable on some systems. If you enable this, then also remove AA_UseSoftwareOpenGL + // below. + qputenv("QT_WIDGETS_RHI", "1"); + qputenv("QT_WIDGETS_RHI_BACKEND", "d3d11"); // macos: metal, linux: opengl #else - qputenv("QT_WIDGETS_RHI_BACKEND", "opengl"); + if (cmdline.contains("swrender")) { + QCoreApplication::setAttribute(Qt::AA_UseSoftwareOpenGL); + } #endif #endif diff --git a/src/mainwin.cpp b/src/mainwin.cpp index 2566696e1d..493d90966f 100644 --- a/src/mainwin.cpp +++ b/src/mainwin.cpp @@ -40,7 +40,6 @@ #include "psicontactlist.h" #include "psievent.h" #include "psiiconset.h" -#include "psimedia/psimedia.h" #include "psioptions.h" #include "psirosterwidget.h" #include "psitoolbar.h" @@ -51,6 +50,9 @@ #include "statusdlg.h" #include "tabdlg.h" #include "tabmanager.h" +#ifdef USE_TASKBARNOTIFIER +#include "taskbarnotifier.h" +#endif #include "textutil.h" #include @@ -71,9 +73,6 @@ #include #include #ifdef Q_OS_WIN -#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) -#include "widgets/thumbnailtoolbar.h" -#endif #include #endif #ifdef HAVE_X11 @@ -126,10 +125,14 @@ class MainWin::Private { TabDlg *mainTabs; QString statusTip; PsiToolBar *viewToolBar; - int tabsSize; - int rosterSize; - bool isLeftRoster; - bool allInOne; +#ifdef USE_TASKBARNOTIFIER + TaskBarNotifier *taskBarNotifier; +#endif + + int tabsSize; + int rosterSize; + bool isLeftRoster; + bool allInOne; PopupAction *optionsButton, *statusButton; IconActionGroup *statusGroup, *viewGroups; @@ -159,9 +162,6 @@ class MainWin::Private { #ifdef Q_OS_WIN DWORD deactivationTickCount; -#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) - QPointer thumbnailToolBar_; -#endif #endif void registerActions(); @@ -181,9 +181,13 @@ MainWin::Private::Private(PsiCon *_psi, MainWin *_mainWin) : #ifdef Q_OS_MAC dockMenu(nullptr), #endif - vb_roster(nullptr), splitter(nullptr), mainTabs(nullptr), viewToolBar(nullptr), tabsSize(0), rosterSize(0), - isLeftRoster(false), psi(_psi), mainWin(_mainWin), rosterAvatar(nullptr), searchText(nullptr), searchPb(nullptr), - searchWidget(nullptr), hideTimer(nullptr), nextAnim(nullptr), nextAmount(0), lastStatus(0), rosterWidget_(nullptr) + vb_roster(nullptr), splitter(nullptr), mainTabs(nullptr), viewToolBar(nullptr), +#ifdef USE_TASKBARNOTIFIER + taskBarNotifier(nullptr), +#endif + tabsSize(0), rosterSize(0), isLeftRoster(false), psi(_psi), mainWin(_mainWin), rosterAvatar(nullptr), + searchText(nullptr), searchPb(nullptr), searchWidget(nullptr), hideTimer(nullptr), nextAnim(nullptr), nextAmount(0), + lastStatus(0), rosterWidget_(nullptr) { statusGroup = static_cast(getAction("status_group")); @@ -305,7 +309,7 @@ MainWin::MainWin(bool _onTop, bool _asTool, PsiCon *psi) : setAttribute(Qt::WA_AlwaysShowToolTips); d = new Private(psi, this); - setWindowIcon(PsiIconset::instance()->status(STATUS_OFFLINE).icon()); + setWindowIcon(PsiIconset::instance()->system().icon("psi/logo_128")->icon()); d->onTop = _onTop; d->asTool = _asTool; @@ -534,10 +538,6 @@ MainWin::MainWin(bool _onTop, bool _asTool, PsiCon *psi) : }); d->eventNotifier->hide(); -#if defined(Q_OS_WIN) && QT_VERSION < QT_VERSION_CHECK(6, 0, 0) - updateWinTaskbar(_asTool); -#endif - connect(qApp, SIGNAL(dockActivated()), SLOT(dockActivated())); qApp->installEventFilter(this); @@ -549,6 +549,13 @@ MainWin::MainWin(bool _onTop, bool _asTool, PsiCon *psi) : optionChanged("options.ui.contactlist.css"); reinitAutoHide(); +#ifdef USE_TASKBARNOTIFIER + d->taskBarNotifier = new TaskBarNotifier(this); +#ifdef Q_OS_WIN + d->taskBarNotifier->enableFlashWindow(d->allInOne + && PsiOptions::instance()->getOption("options.ui.flash-windows").toBool()); +#endif +#endif } MainWin::~MainWin() @@ -557,6 +564,12 @@ MainWin::~MainWin() delete d->tray; d->tray = nullptr; } +#ifdef USE_TASKBARNOTIFIER + if (d->taskBarNotifier) { + delete d->taskBarNotifier; + d->taskBarNotifier = nullptr; + } +#endif saveToolbarsState(); @@ -580,135 +593,116 @@ void MainWin::optionChanged(const QString &option) if (option == toolbarsStateOptionPath) { loadToolbarsState(); } else if (option == "options.ui.contactlist.css") { - const QString css = PsiOptions::instance()->getOption("options.ui.contactlist.css").toString(); + const QString css = PsiOptions::instance()->getOption(option).toString(); if (!css.isEmpty()) { setStyleSheet(css); } } +#if defined(USE_TASKBARNOTIFIER) && defined(Q_OS_WIN) + else if (d->allInOne && option == "options.ui.flash-windows") { + d->taskBarNotifier->enableFlashWindow(PsiOptions::instance()->getOption(option).toBool()); + } +#endif } void MainWin::registerAction(IconAction *action) { - const char *activated = SIGNAL(triggered()); - const char *toggled = SIGNAL(toggled(bool)); - const char *setChecked = SLOT(setChecked(bool)); - PsiContactList *contactList = psiCon()->contactList(); - struct MenuAction { - const char *name; - const char *signal; - QObject *receiver; - const char *slot; + auto cd = [action](const QString &actionName, auto signal, auto dst, auto slot) { + if (actionName != action->objectName()) { + return false; + } + QObject::connect(action, signal, dst, slot, Qt::UniqueConnection); + return true; }; - std::vector actionlist = { - { "choose_status", activated, this, SLOT(actChooseStatusActivated()) }, - { "reconnect_all", activated, this, SLOT(actReconnectActivated()) }, - - { "active_contacts", activated, this, SLOT(actActiveContacts()) }, - { "show_offline", toggled, contactList, SLOT(setShowOffline(bool)) }, - // { "show_away", toggled, contactList, SLOT( setShowAway(bool) ) }, - { "show_hidden", toggled, contactList, SLOT(setShowHidden(bool)) }, - { "show_agents", toggled, contactList, SLOT(setShowAgents(bool)) }, - { "show_self", toggled, contactList, SLOT(setShowSelf(bool)) }, - { "show_statusmsg", toggled, d->rosterWidget_, SLOT(setShowStatusMsg(bool)) }, - { "enable_groups", toggled, this, SLOT(actEnableGroupsActivated(bool)) }, - - { "button_options", activated, this, SIGNAL(doOptions()) }, - - { "menu_disco", SIGNAL(activated(PsiAccount *, int)), this, SLOT(activatedAccOption(PsiAccount *, int)) }, - { "menu_add_contact", SIGNAL(activated(PsiAccount *, int)), this, SLOT(activatedAccOption(PsiAccount *, int)) }, - { "menu_xml_console", SIGNAL(activated(PsiAccount *, int)), this, SLOT(activatedAccOption(PsiAccount *, int)) }, - - { "menu_new_message", activated, this, SIGNAL(blankMessage()) }, -#ifdef GROUPCHAT - { "menu_join_groupchat", activated, this, SIGNAL(doGroupChat()) }, -#endif - { "menu_options", activated, this, SIGNAL(doOptions()) }, - { "menu_file_transfer", activated, this, SIGNAL(doFileTransDlg()) }, - { "menu_toolbars", activated, this, SIGNAL(doToolbars()) }, - { "menu_accounts", activated, this, SIGNAL(doAccounts()) }, - { "menu_change_profile", activated, this, SIGNAL(changeProfile()) }, - { "menu_quit", activated, this, SLOT(try2tryCloseProgram()) }, - { "menu_play_sounds", toggled, this, SLOT(actPlaySoundsActivated(bool)) }, -#ifdef USE_PEP - { "publish_tune", toggled, this, SLOT(actPublishTuneActivated(bool)) }, - { "set_mood", activated, this, SLOT(actSetMoodActivated()) }, - { "set_activity", activated, this, SLOT(actSetActivityActivated()) }, - { "set_geoloc", activated, this, SLOT(actSetGeolocActivated()) }, -#endif - { "help_readme", activated, this, SLOT(actReadmeActivated()) }, - { "help_online_wiki", activated, this, SLOT(actOnlineWikiActivated()) }, - { "help_online_home", activated, this, SLOT(actOnlineHomeActivated()) }, - { "help_online_forum", activated, this, SLOT(actOnlineForumActivated()) }, - { "help_psi_muc", activated, this, SLOT(actJoinPsiMUCActivated()) }, - { "help_report_bug", activated, this, SLOT(actBugReportActivated()) }, - { "help_about", activated, this, SLOT(actAboutActivated()) }, - { "help_about_qt", activated, this, SLOT(actAboutQtActivated()) }, - { "help_diag_qcaplugin", activated, this, SLOT(actDiagQCAPluginActivated()) }, - { "help_diag_qcakeystore", activated, this, SLOT(actDiagQCAKeyStoreActivated()) }, - { nullptr, nullptr, nullptr, nullptr } + auto mcd = [action](const QString &actionName, auto signal, auto dst, auto slot) { + if (actionName != action->objectName()) { + return; + } + QObject::connect(qobject_cast(action), signal, dst, slot, Qt::UniqueConnection); }; - int i; - QString aName; - for (i = 0; !(aName = QLatin1String(actionlist[i].name)).isEmpty(); i++) { - if (aName == action->objectName()) { -#ifdef USE_PEP - // Check before connecting, otherwise we get a loop - if (aName == "publish_tune") { - action->setChecked( - PsiOptions::instance()->getOption("options.extended-presence.tune.publish").toBool()); - d->rosterAvatar->setTuneAction(action); - } -#endif + // clang-format off + cd(QStringLiteral("choose_status"), &IconAction::triggered, this, &MainWin::actChooseStatusActivated); + cd(QStringLiteral("reconnect_all"), &IconAction::triggered, this, &MainWin::actReconnectActivated); - disconnect(action, actionlist[i].signal, actionlist[i].receiver, actionlist[i].slot); // for safety - connect(action, actionlist[i].signal, actionlist[i].receiver, actionlist[i].slot); - - // special cases - if (aName == "menu_play_sounds") { - action->setChecked( - PsiOptions::instance()->getOption("options.ui.notifications.sounds.enable").toBool()); - } else if (aName == "enable_groups") { - action->setChecked(PsiOptions::instance()->getOption("options.ui.contactlist.enable-groups").toBool()); - } - // else if ( aName == "foobar" ) - // ; - } + cd(QStringLiteral("active_contacts"), &IconAction::triggered, this, &MainWin::actActiveContacts); + cd(QStringLiteral("show_offline"), &IconAction::toggled, contactList, &PsiContactList::setShowOffline); + // cd( "show_away"), toggled, contactList, &PsiContactList:: setShowAway); + cd(QStringLiteral("show_hidden"), &IconAction::toggled, contactList, &PsiContactList::setShowHidden); + cd(QStringLiteral("show_agents"), &IconAction::toggled, contactList, &PsiContactList::setShowAgents); + cd(QStringLiteral("show_self"), &IconAction::toggled, contactList, &PsiContactList::setShowSelf); + cd(QStringLiteral("show_statusmsg"), &IconAction::toggled, d->rosterWidget_, &PsiRosterWidget::setShowStatusMsg); + if (cd(QStringLiteral("enable_groups"), &IconAction::toggled, this, &MainWin::actEnableGroupsActivated)) { + action->setChecked(PsiOptions::instance()->getOption("options.ui.contactlist.enable-groups").toBool()); } + cd(QStringLiteral("button_options"), &IconAction::triggered, this, &MainWin::doOptions); - struct { - const char *name; - QObject *sender; - const char *signal; - const char *slot; - bool checked; - } reverseactionlist[] - = { // { "show_away", contactList, SIGNAL(showAwayChanged(bool)), setChecked, contactList->showAway()}, - { "show_hidden", contactList, SIGNAL(showHiddenChanged(bool)), setChecked, contactList->showHidden() }, - { "show_offline", contactList, SIGNAL(showOfflineChanged(bool)), setChecked, contactList->showOffline() }, - { "show_self", contactList, SIGNAL(showSelfChanged(bool)), setChecked, contactList->showSelf() }, - { "show_agents", contactList, SIGNAL(showAgentsChanged(bool)), setChecked, contactList->showAgents() }, - { "show_statusmsg", nullptr, nullptr, nullptr, false }, - { "", nullptr, nullptr, nullptr, false } - }; - - for (i = 0; !(aName = QString(reverseactionlist[i].name)).isEmpty(); i++) { - if (aName == action->objectName()) { - if (reverseactionlist[i].sender) { - disconnect(reverseactionlist[i].sender, reverseactionlist[i].signal, action, - reverseactionlist[i].slot); // for safety - connect(reverseactionlist[i].sender, reverseactionlist[i].signal, action, reverseactionlist[i].slot); - } + mcd(QStringLiteral("menu_disco"), &MAction::activated, this, &MainWin::activatedAccOption); + mcd(QStringLiteral("menu_add_contact"), &MAction::activated, this, &MainWin::activatedAccOption); + mcd(QStringLiteral("menu_xml_console"), &MAction::activated, this, &MainWin::activatedAccOption); - if (aName == "show_statusmsg") { - action->setChecked(PsiOptions::instance()->getOption(showStatusMessagesOptionPath).toBool()); - } else - action->setChecked(reverseactionlist[i].checked); + cd(QStringLiteral("menu_new_message"), &IconAction::triggered, this, &MainWin::blankMessage); +#ifdef GROUPCHAT + cd(QStringLiteral("menu_join_groupchat"), &IconAction::triggered, this, &MainWin::doGroupChat); +#endif + cd(QStringLiteral("menu_options"), &IconAction::triggered, this, &MainWin::doOptions); + cd(QStringLiteral("menu_file_transfer"), &IconAction::triggered, this, &MainWin::doFileTransDlg); + cd(QStringLiteral("menu_toolbars"), &IconAction::triggered, this, &MainWin::doToolbars); + cd(QStringLiteral("menu_accounts"), &IconAction::triggered, this, &MainWin::doAccounts); + cd(QStringLiteral("menu_change_profile"), &IconAction::triggered, this, &MainWin::changeProfile); + cd(QStringLiteral("menu_quit"), &IconAction::triggered, this, &MainWin::try2tryCloseProgram); + if (cd(QStringLiteral("menu_play_sounds"), &IconAction::toggled, this, &MainWin::actPlaySoundsActivated)) { + bool state = PsiOptions::instance()->getOption("options.ui.notifications.sounds.enable").toBool(); + action->setChecked(state); + auto playSoundsToggle = [action](bool state){ + action->setToolTip(state?tr("Disable Sounds"):tr("Enable Sounds")); + }; + connect(action, &IconAction::toggled, this, playSoundsToggle); + playSoundsToggle(state); + } +#ifdef USE_PEP + if (cd(QStringLiteral("publish_tune"), &IconAction::toggled, this, &MainWin::actPublishTuneActivated)) { + action->setChecked(PsiOptions::instance()->getOption("options.extended-presence.tune.publish").toBool()); + d->rosterAvatar->setTuneAction(action); + } + cd(QStringLiteral("set_mood"), &IconAction::triggered, this, &MainWin::actSetMoodActivated); + cd(QStringLiteral("set_activity"), &IconAction::triggered, this, &MainWin::actSetActivityActivated); + cd(QStringLiteral("set_geoloc"), &IconAction::triggered, this, &MainWin::actSetGeolocActivated); +#endif + + cd(QStringLiteral("help_readme"), &IconAction::triggered, this, &MainWin::actReadmeActivated); + cd(QStringLiteral("help_online_wiki"), &IconAction::triggered, this, &MainWin::actOnlineWikiActivated); + cd(QStringLiteral("help_online_home"), &IconAction::triggered, this, &MainWin::actOnlineHomeActivated); + cd(QStringLiteral("help_online_forum"), &IconAction::triggered, this, &MainWin::actOnlineForumActivated); + cd(QStringLiteral("help_psi_muc"), &IconAction::triggered, this, &MainWin::actJoinPsiMUCActivated); + cd(QStringLiteral("help_report_bug"), &IconAction::triggered, this, &MainWin::actBugReportActivated); + cd(QStringLiteral("help_about"), &IconAction::triggered, this, &MainWin::actAboutActivated); + cd(QStringLiteral("help_about_qt"), &IconAction::triggered, this, &MainWin::actAboutQtActivated); + cd(QStringLiteral("help_diag_qcaplugin"), &IconAction::triggered, this, &MainWin::actDiagQCAPluginActivated); + cd(QStringLiteral("help_diag_qcakeystore"), &IconAction::triggered, this, &MainWin::actDiagQCAKeyStoreActivated); + // clang-format on + + auto connectReverse = [action](const QString &actionName, auto src, auto signal, auto slot, bool checked) { + if (actionName != action->objectName()) { + return; } + if (src) { + QObject::connect(src, signal, action, slot, Qt::UniqueConnection); + } + action->setChecked(checked); + }; + // clang-format off + connectReverse(QStringLiteral("show_hidden"), contactList, &PsiContactList::showHiddenChanged, &IconAction::setChecked, contactList->showHidden()); + connectReverse(QStringLiteral("show_offline"), contactList, &PsiContactList::showOfflineChanged, &IconAction::setChecked, contactList->showOffline()); + connectReverse(QStringLiteral("show_self"), contactList, &PsiContactList::showSelfChanged, &IconAction::setChecked, contactList->showSelf()); + connectReverse(QStringLiteral("show_agents"), contactList, &PsiContactList::showAgentsChanged, &IconAction::setChecked, contactList->showAgents()); + if (action->objectName() == QStringLiteral("show_statusmsg")) { + action->setChecked(PsiOptions::instance()->getOption(showStatusMessagesOptionPath).toBool()); } + // clang-format on } void MainWin::reinitAutoHide() @@ -766,9 +760,6 @@ void MainWin::setWindowOpts(bool _onTop, bool _asTool) setWindowFlags(flags); show(); -#if defined(Q_OS_WIN) && QT_VERSION < QT_VERSION_CHECK(6, 0, 0) - updateWinTaskbar(_asTool); -#endif } void MainWin::setUseDock(bool use) @@ -858,7 +849,7 @@ void MainWin::buildToolbars() PsiOptions *options = PsiOptions::instance(); bool allInOne = options->getOption("options.ui.tabs.grouping").toString().contains('A'); - if (allInOne) { + if (allInOne && d->viewToolBar) { d->viewToolBar->initialize(); } const auto &bases = options->getChildOptionNames("options.ui.contactlist.toolbars", true, true); @@ -921,8 +912,17 @@ void MainWin::buildOptionsMenu() helpMenu->setIcon(IconsetFactory::icon("psi/help").icon()); QStringList actions; - actions << "help_readme" << "separator" << "help_online_wiki" << "help_online_home" << "help_online_forum" - << "help_psi_muc" << "help_report_bug" << "diagnostics" << "separator" << "help_about" << "help_about_qt"; + actions << "help_readme" + << "separator" + << "help_online_wiki" + << "help_online_home" + << "help_online_forum" + << "help_psi_muc" + << "help_report_bug" + << "diagnostics" + << "separator" + << "help_about" + << "help_about_qt"; d->updateMenu(actions, helpMenu); @@ -959,7 +959,9 @@ void MainWin::buildMainMenu() void MainWin::buildToolsMenu() { QStringList actions; - actions << "menu_file_transfer" << "separator" << "menu_xml_console"; + actions << "menu_file_transfer" + << "separator" + << "menu_xml_console"; d->updateMenu(actions, d->toolsMenu); } @@ -976,7 +978,8 @@ void MainWin::buildGeneralMenu(QMenu *menu) #ifdef GROUPCHAT << "menu_join_groupchat" #endif - << "menu_options" << "menu_file_transfer"; + << "menu_options" + << "menu_file_transfer"; if (PsiOptions::instance()->getOption("options.ui.menu.main.change-profile").toBool()) { actions << "menu_change_profile"; } @@ -1346,7 +1349,7 @@ void MainWin::decorateButton(int status) #endif d->statusMenu->statusChanged(makeStatus(STATUS_OFFLINE, "")); - setWindowIcon(PsiIconset::instance()->status(STATUS_OFFLINE).icon()); + // setWindowIcon(PsiIconset::instance()->status(STATUS_OFFLINE).icon()); } else { d->statusButton->setText(status2txt(status)); d->statusButton->setIcon(PsiIconset::instance()->statusPtr(status)); @@ -1356,7 +1359,7 @@ void MainWin::decorateButton(int status) d->statusMenuMB->statusChanged(makeStatus(status, d->psi->currentStatusMessage())); #endif d->statusMenu->statusChanged(makeStatus(status, d->psi->currentStatusMessage())); - setWindowIcon(PsiIconset::instance()->status(status).icon()); + // setWindowIcon(PsiIconset::instance()->status(status).icon()); } updateTray(); @@ -1387,6 +1390,11 @@ void MainWin::closeEvent(QCloseEvent *e) trayHide(); e->ignore(); return; + } else if (!quitOnClose) { // Minimize window to taskbar if there is no trayicon and quit-on-close option is + // disabled + setWindowState(windowState() | Qt::WindowMinimized); + e->ignore(); + return; } if (!askQuit()) { @@ -1489,30 +1497,6 @@ bool MainWin::nativeEvent(const QByteArray &eventType, MSG *msg, long *result) } return false; } - -#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) -void MainWin::updateWinTaskbar(bool enabled) -{ - if (!enabled) { - if (!d->thumbnailToolBar_) { - d->thumbnailToolBar_ = new PsiThumbnailToolBar(this, windowHandle()); - connect(d->thumbnailToolBar_, &PsiThumbnailToolBar::openOptions, this, &MainWin::doOptions); - connect(d->thumbnailToolBar_, &PsiThumbnailToolBar::setOnline, this, - [this]() { d->getAction("status_online")->trigger(); }); - connect(d->thumbnailToolBar_, &PsiThumbnailToolBar::setOffline, this, - [this]() { d->getAction("status_offline")->trigger(); }); - connect(d->thumbnailToolBar_, &PsiThumbnailToolBar::runActiveEvent, this, &MainWin::doRecvNextEvent); - connect(d->psi->contactList(), &PsiContactList::queueChanged, this, [this]() { - d->thumbnailToolBar_->updateToolBar(d->nextAmount > 0); - if (!isActiveWindow() && d->allInOne) - qApp->alert(this, 0); - }); - } - } else { - delete d->thumbnailToolBar_; - } -} -#endif #endif void MainWin::updateCaption() @@ -1669,12 +1653,18 @@ void MainWin::updateReadNext(PsiIcon *anim, int amount) d->eventNotifier->hide(); d->eventNotifier->setText(""); d->eventNotifier->setPsiIcon(""); +#ifdef USE_TASKBARNOTIFIER + if (!d->asTool && d->taskBarNotifier->isActive()) + d->taskBarNotifier->removeIconCountCaption(); +#endif } else { d->eventNotifier->setPsiIcon(anim); d->eventNotifier->setText(QString("") + numEventsString(d->nextAmount) + ""); d->eventNotifier->show(); - // make sure it shows - // qApp->processEvents(); +#ifdef USE_TASKBARNOTIFIER + if (!d->asTool) + d->taskBarNotifier->setIconCountCaption(d->nextAmount); +#endif } updateTray(); diff --git a/src/mainwin.h b/src/mainwin.h index e5a104ff66..fe61532de0 100644 --- a/src/mainwin.h +++ b/src/mainwin.h @@ -189,10 +189,6 @@ public slots: void buildStatusMenu(GlobalStatusMenu *statusMenu); -#if defined(Q_OS_WIN) && QT_VERSION < QT_VERSION_CHECK(6, 0, 0) - void updateWinTaskbar(bool enabled); -#endif - private: class Private; Private *d; diff --git a/src/mainwin_p.cpp b/src/mainwin_p.cpp index 7dcc47b02e..7632328272 100644 --- a/src/mainwin_p.cpp +++ b/src/mainwin_p.cpp @@ -470,12 +470,7 @@ IconAction *SpacerAction::copy() const { return new SpacerAction(nullptr); } // SeparatorAction //---------------------------------------------------------------------------- -SeparatorAction::SeparatorAction(QObject *parent, const char *name) : - IconAction(tr(""), tr(""), 0, parent, name) -{ - setSeparator(true); - setToolTip(tr("Separator")); -} +SeparatorAction::SeparatorAction(QObject *parent, const char *name) : IconAction(parent, name) { setSeparator(true); } SeparatorAction::~SeparatorAction() { } diff --git a/src/mcmdcompletion.cpp b/src/mcmdcompletion.cpp index 8c640f51df..822259cf77 100644 --- a/src/mcmdcompletion.cpp +++ b/src/mcmdcompletion.cpp @@ -12,7 +12,7 @@ * 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 . + * along with this program. If not, see . * */ diff --git a/src/mcmdcompletion.h b/src/mcmdcompletion.h index 4274c2fbb2..b14e63e785 100644 --- a/src/mcmdcompletion.h +++ b/src/mcmdcompletion.h @@ -12,7 +12,7 @@ * 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 . + * along with this program. If not, see . * */ diff --git a/src/mcmdmanager.cpp b/src/mcmdmanager.cpp index 913a9fe55c..534d6a35cc 100644 --- a/src/mcmdmanager.cpp +++ b/src/mcmdmanager.cpp @@ -12,7 +12,7 @@ * 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 . + * along with this program. If not, see . * */ diff --git a/src/mcmdmanager.h b/src/mcmdmanager.h index 7165a1253b..2fe6bb347d 100644 --- a/src/mcmdmanager.h +++ b/src/mcmdmanager.h @@ -12,7 +12,7 @@ * 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 . + * along with this program. If not, see . * */ diff --git a/src/mcmdsimplesite.cpp b/src/mcmdsimplesite.cpp index 0b601afa89..4365d9ad4c 100644 --- a/src/mcmdsimplesite.cpp +++ b/src/mcmdsimplesite.cpp @@ -12,7 +12,7 @@ * 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 . + * along with this program. If not, see . * */ diff --git a/src/mcmdsimplesite.h b/src/mcmdsimplesite.h index 49523e6fec..77194b0e4a 100644 --- a/src/mcmdsimplesite.h +++ b/src/mcmdsimplesite.h @@ -12,7 +12,7 @@ * 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 . + * along with this program. If not, see . * */ diff --git a/src/messageview.cpp b/src/messageview.cpp index 3c5ac8613a..0efbf262bb 100644 --- a/src/messageview.cpp +++ b/src/messageview.cpp @@ -89,6 +89,23 @@ MessageView MessageView::nickChangeMessage(const QString &nick, const QString &n return mv; } +MessageView MessageView::reactionsMessage(const QString &nick, const QString &targetMessageId, + const QSet &reactions) +{ + MessageView mv(Reactions); + mv.setNick(nick); + mv.setReactionsId(targetMessageId); + mv.setReactions(reactions); + return mv; +} + +MessageView MessageView::retractionMessage(const QString &targetMessageId) +{ + MessageView mv(MessageRetraction); + mv.setRetractionId(targetMessageId); + return mv; +} + MessageView MessageView::statusMessage(const QString &nick, int status, const QString &statusText, int priority) { QString message = QObject::tr("%1 is now %2").arg(nick, status2txt(status)); @@ -160,82 +177,3 @@ QString MessageView::formattedUserText() const } bool MessageView::hasStatus() const { return _type == Status || _type == MUCJoin; } - -QVariantMap MessageView::toVariantMap(bool isMuc, bool formatted) const -{ - static QHash types; - if (types.isEmpty()) { - types.insert(Message, "message"); - types.insert(System, "system"); - types.insert(Status, "status"); - types.insert(Subject, "subject"); - types.insert(Urls, "urls"); - types.insert(MUCJoin, "join"); - types.insert(MUCPart, "part"); - types.insert(FileTransferRequest, "ftreq"); - types.insert(FileTransferFinished, "ftfin"); - types.insert(NickChange, "newnick"); - } - QVariantMap m; - m["time"] = _dateTime; - m["type"] = types.value(_type); - switch (_type) { - case Message: - m["message"] = formatted ? formattedText() : _text; - m["emote"] = isEmote(); - m["local"] = isLocal(); - m["sender"] = _nick; - m["userid"] = _userId; - m["spooled"] = isSpooled(); - m["id"] = _messageId; - if (isMuc) { // maybe w/o conditions ? - m["alert"] = isAlert(); - } else { - m["awaitingReceipt"] = isAwaitingReceipt(); - } - if (_references.count()) { - QVariantMap rvm; - for (auto const &r : _references) { - auto md = r->metaData(); - md.insert("type", r->mimeType()); - rvm.insert(r->sums()[0].toString(), md); - } - m["references"] = rvm; - } - break; - case NickChange: - m["sender"] = _nick; - m["newnick"] = _userText; - m["message"] = _text; - break; - case MUCJoin: - case MUCPart: - m["nopartjoin"] = isJoinLeaveHidden(); - PSI_FALLSTHROUGH; // falls through - case Status: - m["sender"] = _nick; - m["status"] = _status; - m["priority"] = _statusPriority; - m["message"] = _text; - m["usertext"] = formatted ? formattedUserText() : _userText; - m["nostatus"] = isStatusChangeHidden(); // looks strange? but chatview can use status for something anyway - break; - case System: - case Subject: - m["message"] = formatted ? formattedText() : _text; - m["usertext"] = formatted ? formattedUserText() : _userText; - break; - case Urls: { - QVariantMap vmUrls; - for (auto it = _urls.constBegin(); it != _urls.constEnd(); ++it) { - vmUrls.insert(it.key(), it.value()); - } - m["urls"] = vmUrls; - break; - } - case FileTransferRequest: - case FileTransferFinished: - break; - } - return m; -} diff --git a/src/messageview.h b/src/messageview.h index 5c6369bdf5..a071e858f4 100644 --- a/src/messageview.h +++ b/src/messageview.h @@ -38,7 +38,9 @@ class MessageView { MUCPart, NickChange, FileTransferRequest, - FileTransferFinished + FileTransferFinished, + Reactions, + MessageRetraction }; enum Flag { @@ -71,6 +73,9 @@ class MessageView { static MessageView mucPartMessage(const QString &nick, const QString &message = QString(), const QString &statusText = QString()); static MessageView nickChangeMessage(const QString &nick, const QString &newNick); + static MessageView reactionsMessage(const QString &nick, const QString &targetMessageId, + const QSet &reactions); + static MessageView retractionMessage(const QString &targetMessageId); inline Type type() const { return _type; } inline const QString &text() const { return _text; } @@ -117,10 +122,16 @@ class MessageView { inline QMap urls() const { return _urls; } inline void setReplaceId(const QString &id) { _replaceId = id; } inline const QString &replaceId() const { return _replaceId; } + inline void setQuoteId(const QString &id) { _quoteId = id; } + inline const QString "eId() const { return _quoteId; } + inline void setRetractionId(const QString &id) { _retractionId = id; } + inline const QString &retractionId() const { return _retractionId; } inline void addReference(FileSharingItem *fsi) { _references.append(fsi); } inline const QList &references() const { return _references; } - - QVariantMap toVariantMap(bool isMuc, bool formatted = false) const; + inline void setReactionsId(const QString &id) { _reactionsId = id; } + inline const QString &reactionsId() const { return _reactionsId; } + inline void setReactions(const QSet &r) { _reactions = r; } + inline const QSet &reactions() const { return _reactions; } private: Type _type; @@ -135,7 +146,11 @@ class MessageView { QDateTime _dateTime; QMap _urls; QString _replaceId; + QString _quoteId; + QString _retractionId; + QString _reactionsId; QList _references; + QSet _reactions; }; Q_DECLARE_OPERATORS_FOR_FLAGS(MessageView::Flags) diff --git a/src/miniclient.cpp b/src/miniclient.cpp index 293c13a033..27f5a4c3f8 100644 --- a/src/miniclient.cpp +++ b/src/miniclient.cpp @@ -68,8 +68,8 @@ void MiniClient::reset() conn = nullptr; } -void MiniClient::connectToServer(const Jid &jid, bool legacy_ssl_probe, bool legacy_ssl, bool forcessl, - const QString &_host, int _port, QString proxy, QString *_pass) +void MiniClient::connectToServer(const Jid &jid, bool direct_tls, bool forcessl, const QString &_host, int _port, + QString proxy, QString *_pass) { j = jid; @@ -123,9 +123,7 @@ void MiniClient::connectToServer(const Jid &jid, bool legacy_ssl_probe, bool leg conn->setProxy(p); if (useHost) { conn->setOptHostPort(host, quint16(port)); - conn->setOptSSL(legacy_ssl); - } else { - conn->setOptProbe(legacy_ssl_probe); + conn->setOptSSL(direct_tls); } stream = new ClientStream(conn, tlsHandler); diff --git a/src/miniclient.h b/src/miniclient.h index 64df7dd4bd..afbbb8157a 100644 --- a/src/miniclient.h +++ b/src/miniclient.h @@ -47,7 +47,7 @@ class MiniClient : public QObject { ~MiniClient(); void reset(); - void connectToServer(const XMPP::Jid &j, bool legacy_ssl_probe, bool legacy_ssl, bool force_ssl, + void connectToServer(const XMPP::Jid &j, bool direct_tls, bool force_ssl, const QString &host, int port, QString proxy, QString *pass = nullptr); void close(); XMPP::Client *client(); diff --git a/src/minicmd.h b/src/minicmd.h index c97bd23874..a33588a399 100644 --- a/src/minicmd.h +++ b/src/minicmd.h @@ -12,7 +12,7 @@ * 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 . + * along with this program. If not, see . * */ diff --git a/src/msgmle.cpp b/src/msgmle.cpp index 2ad8e2e17f..9b6db687e2 100644 --- a/src/msgmle.cpp +++ b/src/msgmle.cpp @@ -48,7 +48,7 @@ #include #include -static const int TIMEOUT = 30000; // 30 secs maximum time interval +static const int TIMEOUT = 120000; // 2 min maximum time interval static const int SECOND = 1000; static const int maxOverlayTime = TIMEOUT / SECOND; static const QLatin1String capOption("options.ui.chat.auto-capitalize"); @@ -104,11 +104,11 @@ public slots: } else if (charsAdded > 1) { // Insert a piece of text return; } else { - QRegularExpression capitalizeAfter("(?:(?:\\s*\\b\\w{2,}\\b[.!?]+\\s+)|(?:^\\s*))+(\\w)", - QRegularExpression::UseUnicodePropertiesOption - | QRegularExpression::MultilineOption); - QRegularExpressionMatch match; - int index = te_->toPlainText().lastIndexOf(capitalizeAfter, -1, &match); + static QRegularExpression capitalizeAfter("(?:(?:\\s*\\b\\w{2,}\\b\\s*[.!?]+\\s+)|(?:^\\s*))+(\\w)", + QRegularExpression::UseUnicodePropertiesOption + | QRegularExpression::MultilineOption); + QRegularExpressionMatch match; + int index = te_->toPlainText().lastIndexOf(capitalizeAfter, -1, &match); if (index != -1 && pos == match.capturedStart(1)) { capitalizeNext_ = true; } @@ -280,7 +280,7 @@ bool ChatEdit::event(QEvent *event) if (event->type() == QEvent::ShortcutOverride) { return false; } - if (event->type() == QEvent::PaletteChange && recButton_ && !correction) { + if (event->type() == QEvent::PaletteChange && recButton_ && _correctionId.isEmpty()) { setRecButtonIcon(); } return QTextEdit::event(event); @@ -433,7 +433,7 @@ void ChatEdit::optionsChanged(const QString &option) void ChatEdit::showHistoryMessageNext() { - correction = false; + _correctionId.clear(); if (!typedMsgsHistory.isEmpty()) { if (typedMsgsIndex + 1 < typedMsgsHistory.size()) { ++typedMsgsIndex; @@ -463,14 +463,14 @@ void ChatEdit::pasteAsQuote() void ChatEdit::showHistoryMessagePrev() { - if (!typedMsgsHistory.isEmpty() && (typedMsgsIndex > 0 || correction)) { + if (!typedMsgsHistory.isEmpty() && (typedMsgsIndex > 0 || !_correctionId.isEmpty())) { // Save current typed text if (typedMsgsIndex == typedMsgsHistory.size()) { - currentText = toPlainText(); - correction = true; + currentText = toPlainText(); + _correctionId = lastId; } - if (typedMsgsIndex == typedMsgsHistory.size() - 1 && correction) { - correction = false; + if (typedMsgsIndex == typedMsgsHistory.size() - 1 && !_correctionId.isEmpty()) { + _correctionId.clear(); ++typedMsgsIndex; } --typedMsgsIndex; @@ -480,7 +480,7 @@ void ChatEdit::showHistoryMessagePrev() void ChatEdit::showHistoryMessageFirst() { - correction = false; + _correctionId.clear(); if (!typedMsgsHistory.isEmpty()) { if (currentText.isEmpty()) { typedMsgsIndex = typedMsgsHistory.size() - 1; @@ -496,7 +496,7 @@ void ChatEdit::showHistoryMessageFirst() void ChatEdit::showHistoryMessageLast() { - correction = false; + _correctionId.clear(); if (!typedMsgsHistory.isEmpty()) { typedMsgsIndex = 0; showMessageHistory(); @@ -553,7 +553,6 @@ bool ChatEdit::canInsertFromMimeData(const QMimeData *source) const void ChatEdit::updateBackground() { - setProperty("correction", correction); style()->unpolish(this); style()->polish(this); update(); @@ -634,11 +633,12 @@ void ChatEdit::insertAsQuote(const QString &text) QString prevLine = toPlainText().left(pos - 1); prevLine = prevLine.mid(prevLine.lastIndexOf("\n") + 1); - QString quote = QString::fromUtf8(u8"» ") + text; - quote.replace("\n", QString::fromUtf8(u8"\n» ")); + auto sym = QChar::fromLatin1(0x3E); // closing double quote + QString quote = sym + ' ' + text; + quote.replace("\n", QStringLiteral("\n%1 ").arg(sym)); // Check for previous quote and merge if true - if (!prevLine.isEmpty() && !prevLine.startsWith(QString::fromUtf8(u8"»"))) { + if (!prevLine.isEmpty() && !prevLine.startsWith(sym)) { quote.prepend("\n"); } quote.append("\n"); @@ -646,6 +646,12 @@ void ChatEdit::insertAsQuote(const QString &text) setFocus(Qt::OtherFocusReason); } +void ChatEdit::startCorrection(const QString messageId, const QString &text) +{ + _correctionId = messageId; + setEditText(text); +} + void ChatEdit::addSoundRecButton() { if (!recButton_) { diff --git a/src/msgmle.h b/src/msgmle.h index adfd19d9cd..9abe573afd 100644 --- a/src/msgmle.h +++ b/src/msgmle.h @@ -60,12 +60,13 @@ class ChatEdit : public QTextEdit { static bool checkSpellingGloballyEnabled(); void setCheckSpelling(bool); XMPP::HTMLElement toHTMLElement(); - bool isCorrection() { return correction; } + bool isCorrection() { return !_correctionId.isEmpty(); } void setLastMessageId(const QString &id) { lastId = id; } + const QString &correctionId() const { return _correctionId; } const QString &lastMessageId() { return lastId; } void resetCorrection() { - correction = false; + _correctionId.clear(); updateBackground(); } CapitalLettersController *capitalizer(); @@ -80,6 +81,7 @@ public slots: void doHTMLTextMenu(); void setCssString(const QString &css); void insertAsQuote(const QString &text); + void startCorrection(const QString messageId, const QString &text); protected slots: void applySuggestion(); @@ -114,24 +116,25 @@ protected slots: void setRecButtonIcon(); private: - QWidget *dialog_ = nullptr; - bool check_spelling_ = false; - SpellHighlighter *spellhighlighter_ = nullptr; - QPoint last_click_; - int previous_position_ = 0; - QStringList typedMsgsHistory; - int typedMsgsIndex = 0; - QAction *act_showMessagePrev = nullptr; - QAction *act_showMessageNext = nullptr; - QAction *act_showMessageFirst = nullptr; - QAction *act_showMessageLast = nullptr; - QAction *act_changeCase = nullptr; - QAction *actPasteAsQuote_ = nullptr; - QString currentText; - HTMLTextController *controller_ = nullptr; - CapitalLettersController *capitalizer_ = nullptr; - bool correction = false; + QWidget *dialog_ = nullptr; + bool check_spelling_ = false; + SpellHighlighter *spellhighlighter_ = nullptr; + QPoint last_click_; + int previous_position_ = 0; + QStringList typedMsgsHistory; + int typedMsgsIndex = 0; + QAction *act_showMessagePrev = nullptr; + QAction *act_showMessageNext = nullptr; + QAction *act_showMessageFirst = nullptr; + QAction *act_showMessageLast = nullptr; + QAction *act_changeCase = nullptr; + QAction *actPasteAsQuote_ = nullptr; + QString currentText; + HTMLTextController *controller_ = nullptr; + CapitalLettersController *capitalizer_ = nullptr; + // bool correction = false; QString lastId; + QString _correctionId; QPointer layout_; QPointer recButton_; QPointer overlay_; diff --git a/src/mucconfigdlg.cpp b/src/mucconfigdlg.cpp index fb5126e0bf..4bc71131ec 100644 --- a/src/mucconfigdlg.cpp +++ b/src/mucconfigdlg.cpp @@ -20,7 +20,7 @@ #include "mucconfigdlg.h" #include "infodlg.h" -#include "iris/xmpp_vcard.h" +#include "iris/xmpp_vcard4.h" #include "mucaffiliationsmodel.h" #include "mucaffiliationsproxymodel.h" #include "mucmanager.h" @@ -158,8 +158,8 @@ void MUCConfigDlg::refreshVcard() if (!ui_.tab_vcard->layout()) { QVBoxLayout *layout = new QVBoxLayout; - const VCard vcard = VCardFactory::instance()->vcard(manager_->room()); - vcard_ = new InfoWidget(InfoWidget::MucAdm, manager_->room(), vcard, manager_->account()); + const auto vcard = VCardFactory::instance()->vcard(manager_->room()); + vcard_ = new InfoWidget(InfoWidget::MucAdm, manager_->room(), vcard, manager_->account()); layout->addWidget(vcard_); ui_.tab_vcard->setLayout(layout); connect(vcard_, SIGNAL(busy()), ui_.busy, SLOT(start())); diff --git a/src/mucjoindlg.cpp b/src/mucjoindlg.cpp index 28c1a73499..4afafaa0e5 100644 --- a/src/mucjoindlg.cpp +++ b/src/mucjoindlg.cpp @@ -190,6 +190,9 @@ void MUCJoinDlg::updateFavorites() { QListWidgetItem *recentLwi = nullptr, *lwi; + if (!ui_.le_room->text().isEmpty()) { + ui_.lwFavorites->blockSignals(true); + } ui_.lwFavorites->clear(); QHash bmMap; // jid to item @@ -247,6 +250,7 @@ void MUCJoinDlg::updateFavorites() if (recentLwi && ui_.le_room->text().isEmpty()) { ui_.lwFavorites->setCurrentItem(recentLwi); } + ui_.lwFavorites->blockSignals(false); } void MUCJoinDlg::pa_disconnected() diff --git a/src/multifiletransferdelegate.cpp b/src/multifiletransferdelegate.cpp index 57ed47598c..8f2bc4f8e7 100644 --- a/src/multifiletransferdelegate.cpp +++ b/src/multifiletransferdelegate.cpp @@ -127,9 +127,11 @@ void MultiFileTransferDelegate::paint(QPainter *painter, const QStyleOptionViewI painter->restore(); // generate and draw status line - quint64 fullSize = index.data(MultiFileTransferModel::FullSizeRole).toULongLong(); - quint64 curSize = index.data(MultiFileTransferModel::CurrentSizeRole).toULongLong(); - int timeRemaining = index.data(MultiFileTransferModel::TimeRemainingRole).toInt(); + + bool fullSizeDefined = index.data(MultiFileTransferModel::FullSizeRole).toBool(); + quint64 fullSize = fullSizeDefined ? index.data(MultiFileTransferModel::FullSizeRole).toULongLong() : 0; + quint64 curSize = index.data(MultiFileTransferModel::CurrentSizeRole).toULongLong(); + int timeRemaining = index.data(MultiFileTransferModel::TimeRemainingRole).toInt(); // ----------------------------- // Transfer current status line @@ -138,9 +140,16 @@ void MultiFileTransferDelegate::paint(QPainter *painter, const QStyleOptionViewI s.reserve(128); { qlonglong div; - QString unit = TextUtil::sizeUnit(qlonglong(fullSize), &div); + QString unit; - s = TextUtil::roundedNumber(qint64(curSize), div) + '/' + TextUtil::roundedNumber(qint64(fullSize), div) + unit; + if (fullSizeDefined) { + unit = TextUtil::sizeUnit(qlonglong(fullSize), &div); + s = TextUtil::roundedNumber(qint64(curSize), div) + '/' + TextUtil::roundedNumber(qint64(fullSize), div) + + unit; + } else { + unit = TextUtil::sizeUnit(qlonglong(curSize), &div); + s = TextUtil::roundedNumber(qint64(curSize), div) + "/" + tr("not defined"); + } QString space(" "); switch (state) { diff --git a/src/multifiletransferdlg.cpp b/src/multifiletransferdlg.cpp index 1b9988c11d..a50a1e45ed 100644 --- a/src/multifiletransferdlg.cpp +++ b/src/multifiletransferdlg.cpp @@ -254,7 +254,7 @@ void MultiFileTransferDlg::initIncoming(XMPP::Jingle::Session *session) auto app = static_cast(c); auto file = app->file(); auto item = d->model->addTransfer(MultiFileTransferModel::Incoming, file.name(), - quint64(file.size())); // FIXME size is optional. ranges? + file.size()); // FIXME ranges? setupCommonSignals(app, item); auto thumb = file.thumbnail(); @@ -297,7 +297,7 @@ void MultiFileTransferDlg::initIncoming(XMPP::Jingle::Session *session) if (item) item->setFileName(fn); connect(app, &Jingle::FileTransfer::Application::deviceRequested, this, - [fn, app](quint64 offset, quint64 size) { + [fn, app](quint64 offset, std::optional size) { auto f = new QFile(fn, app); f->open(QIODevice::WriteOnly); f->seek(qint64(offset)); @@ -319,7 +319,7 @@ void MultiFileTransferDlg::initIncoming(XMPP::Jingle::Session *session) if (item) item->setFileName(fn); connect(app, &Jingle::FileTransfer::Application::deviceRequested, this, - [fn, app](quint64 offset, quint64 size) { + [fn, app](quint64 offset, std::optional size) { auto f = new QFile(fn, app); f->open(QIODevice::WriteOnly); f->seek(qint64(offset)); @@ -360,13 +360,14 @@ void MultiFileTransferDlg::addTransferContent(MultiFileTransferItem *item) return; } - connect(app, &Jingle::FileTransfer::Application::deviceRequested, item, [app, item](quint64 offset, quint64 size) { - auto f = new QFile(item->filePath(), app); - f->open(QIODevice::ReadOnly); - f->seek(qint64(offset)); - app->setDevice(f); - Q_UNUSED(size); - }); + connect(app, &Jingle::FileTransfer::Application::deviceRequested, item, + [app, item](quint64 offset, std::optional size) { + auto f = new QFile(item->filePath(), app); + f->open(QIODevice::ReadOnly); + f->seek(qint64(offset)); + app->setDevice(f); + Q_UNUSED(size); + }); setupCommonSignals(app, item); // take thumbnail diff --git a/src/multifiletransferitem.cpp b/src/multifiletransferitem.cpp index 9f65f01226..c5c7400f37 100644 --- a/src/multifiletransferitem.cpp +++ b/src/multifiletransferitem.cpp @@ -24,6 +24,8 @@ #include #include +using std::optional; + struct MultiFileTransferItem::Private { QString displayName; // usually base filename QString mediaType; @@ -31,7 +33,7 @@ struct MultiFileTransferItem::Private { QString info; QString errorString; // last error QString fileName; - quint64 fullSize = 0; + std::optional fullSize = 0; quint64 currentSize = 0; // currently transferred quint64 lastSize = 0; quint64 offset = 0; // initial offset if only part of file is transferred @@ -45,7 +47,8 @@ struct MultiFileTransferItem::Private { }; MultiFileTransferItem::MultiFileTransferItem(MultiFileTransferModel::Direction direction, const QString &displayName, - quint64 fullSize, QObject *parent) : QObject(parent), d(new Private) + std::optional fullSize, QObject *parent) : + QObject(parent), d(new Private) { d->direction = direction; d->displayName = displayName; @@ -56,7 +59,7 @@ MultiFileTransferItem::~MultiFileTransferItem() { emit aboutToBeDeleted(); } const QString &MultiFileTransferItem::displayName() const { return d->displayName; } -quint64 MultiFileTransferItem::fullSize() const { return d->fullSize; } +optional MultiFileTransferItem::fullSize() const { return d->fullSize; } quint64 MultiFileTransferItem::currentSize() const { return d->currentSize; } @@ -85,7 +88,9 @@ QString MultiFileTransferItem::toolTipText() const text += (QLatin1String("

") + d->description); } text += (QLatin1String("

") - + tr("Transferred: %1/%2 bytes").arg(QString::number(d->currentSize), QString::number(d->fullSize))); + + tr("Transferred: %1/%2 bytes") + .arg(QString::number(d->currentSize), + d->fullSize ? QString::number(*d->fullSize) : tr("not defined"))); if (!d->info.isEmpty()) { text += (QLatin1String("

") + d->info); } @@ -169,7 +174,7 @@ void MultiFileTransferItem::updateStats() } d->speed = quint32(sum / qulonglong(d->lastSpeeds.size())); - d->timeRemaining = quint32((d->fullSize - d->currentSize) / speedf); + d->timeRemaining = d->fullSize ? quint32((*d->fullSize - d->currentSize) / speedf) : 0; d->lastSize = d->currentSize; d->lastTimer.start(); } diff --git a/src/multifiletransferitem.h b/src/multifiletransferitem.h index 10fe6b2da8..70a840ee53 100644 --- a/src/multifiletransferitem.h +++ b/src/multifiletransferitem.h @@ -26,12 +26,12 @@ class MultiFileTransferItem : public QObject { Q_OBJECT public: - MultiFileTransferItem(MultiFileTransferModel::Direction direction, const QString &displayName, quint64 fullSize, - QObject *parent); + MultiFileTransferItem(MultiFileTransferModel::Direction direction, const QString &displayName, + std::optional fullSize, QObject *parent); ~MultiFileTransferItem(); const QString &displayName() const; - quint64 fullSize() const; + std::optional fullSize() const; quint64 currentSize() const; quint64 offset() const; // initial offset QIcon icon() const; diff --git a/src/multifiletransfermodel.cpp b/src/multifiletransfermodel.cpp index bc33017c7a..950566e3c8 100644 --- a/src/multifiletransfermodel.cpp +++ b/src/multifiletransfermodel.cpp @@ -87,8 +87,10 @@ QVariant MultiFileTransferModel::data(const QModelIndex &index, int role) const } // try our roles switch (role) { + case FullSizeDefinedRole: + return item->fullSize().has_value(); case FullSizeRole: - return item->fullSize(); + return *item->fullSize(); case CurrentSizeRole: return item->currentSize(); case SpeedRole: @@ -163,7 +165,7 @@ void MultiFileTransferModel::clear() } MultiFileTransferItem *MultiFileTransferModel::addTransfer(Direction direction, const QString &displayName, - quint64 fullSize) + std::optional fullSize) { beginInsertRows(QModelIndex(), transfers.size(), transfers.size()); auto t = new MultiFileTransferItem(direction, displayName, fullSize, this); diff --git a/src/multifiletransfermodel.h b/src/multifiletransfermodel.h index b65529b5b6..9be3c5adf5 100644 --- a/src/multifiletransfermodel.h +++ b/src/multifiletransfermodel.h @@ -23,7 +23,9 @@ #include #include #include + #include +#include class MultiFileTransferItem; @@ -35,7 +37,8 @@ class MultiFileTransferModel : public QAbstractListModel { enum State { AddTemplate, Pending, Active, Failed, Done }; enum { - FullSizeRole = Qt::UserRole, + FullSizeDefinedRole = Qt::UserRole, + FullSizeRole, CurrentSizeRole, SpeedRole, DescriptionRole, @@ -62,7 +65,8 @@ class MultiFileTransferModel : public QAbstractListModel { QHash roleNames() const override; void clear(); - MultiFileTransferItem *addTransfer(Direction direction, const QString &displayName, quint64 fullSize); + MultiFileTransferItem *addTransfer(Direction direction, const QString &displayName, + std::optional fullSize); void forEachTransfer(const std::function cb) const; void setAddEnabled(bool enabled = true); bool isAddEnabled() const; diff --git a/src/networkaccessmanager.cpp b/src/networkaccessmanager.cpp index c523fc9155..e2bb4330a8 100644 --- a/src/networkaccessmanager.cpp +++ b/src/networkaccessmanager.cpp @@ -30,7 +30,7 @@ NetworkAccessManager::NetworkAccessManager(QObject *parent) : QNetworkAccessMana QNetworkReply *NetworkAccessManager::createRequest(Operation op, const QNetworkRequest &req, QIODevice *outgoingData = nullptr) { - if (req.url().host() != QLatin1String("psi")) { + if (req.url().host() != QLatin1String("psi") || op != QNetworkAccessManager::GetOperation) { return QNetworkAccessManager::createRequest(op, req, outgoingData); } diff --git a/src/options/opt_advanced.cpp b/src/options/opt_advanced.cpp index acb68d9850..00f572bbdb 100644 --- a/src/options/opt_advanced.cpp +++ b/src/options/opt_advanced.cpp @@ -70,7 +70,7 @@ QWidget *OptionsTabAdvanced::widget() d->ck_scrollTo->setToolTip( tr("Makes Psi scroll the main window automatically so you can see new incoming events.")); d->ck_ignoreHeadline->setToolTip(tr("Makes Psi ignore all incoming \"headline\" events," - " like system-wide news on MSN, announcements, etc.")); + " like announcements, etc.")); connect(d->ck_messageevents, SIGNAL(toggled(bool)), d->ck_inactiveevents, SLOT(setEnabled(bool))); connect(d->ck_messageevents, SIGNAL(toggled(bool)), d->ck_sendComposingEvents, SLOT(setEnabled(bool))); diff --git a/src/options/opt_advanced.ui b/src/options/opt_advanced.ui index 93b64017b7..4ea57e6f94 100644 --- a/src/options/opt_advanced.ui +++ b/src/options/opt_advanced.ui @@ -133,7 +133,7 @@ - Ignore "Headline" events (e.g. MSN alerts) + Ignore "Headline" events (e.g. announcements) diff --git a/src/options/opt_appearance.cpp b/src/options/opt_appearance.cpp index 03c5649e8a..fe11e11df3 100644 --- a/src/options/opt_appearance.cpp +++ b/src/options/opt_appearance.cpp @@ -218,39 +218,50 @@ QWidget *OptionsTabAppearanceGeneral::widget() QString option; QString descr; }; - ColorWidgetData cwData[] - = { { d->ck_cOnline, d->pb_cOnline, "contactlist.status.online", s.arg(tr("online")) }, - { d->ck_cOffline, d->pb_cOffline, "contactlist.status.offline", s.arg(tr("offline")) }, - { d->ck_cAway, d->pb_cAway, "contactlist.status.away", s.arg(tr("away")) }, - { d->ck_cDND, d->pb_cDND, "contactlist.status.do-not-disturb", s.arg(tr("do not disturb")) }, - { d->ck_cStatus, d->pb_cStatus, "contactlist.status-messages", s.arg(tr("Status message")) }, - { d->ck_cProfileFore, d->pb_cProfileFore, "contactlist.profile.header-foreground", "" }, - { d->ck_cProfileBack, d->pb_cProfileBack, "contactlist.profile.header-background", "" }, - { d->ck_cGroupFore, d->pb_cGroupFore, "contactlist.grouping.header-foreground", "" }, - { d->ck_cGroupBack, d->pb_cGroupBack, "contactlist.grouping.header-background", "" }, - { d->ck_cListBack, d->pb_cListBack, "contactlist.background", "" }, - { d->ck_cAnimFront, d->pb_cAnimFront, "contactlist.status-change-animation1", "" }, - { d->ck_cAnimBack, d->pb_cAnimBack, "contactlist.status-change-animation2", "" }, - { d->ck_cMessageSent, d->pb_cMessageSent, "messages.sent", "" }, - { d->ck_cMessageReceived, d->pb_cMessageReceived, "messages.received", "" }, - { d->ck_cSysMsg, d->pb_cSysMsg, "messages.informational", "" }, - { d->ck_cUserText, d->pb_cUserText, "messages.usertext", "" }, - { d->ck_highlight, d->pb_highlight, "messages.highlighting", "" }, - { d->ck_cLink, d->pb_cLink, "messages.link", "" }, - { d->ck_cLinkVisited, d->pb_cLinkVisited, "messages.link-visited", "" }, - { d->ck_cToolTipText, d->pb_cToolTipText, "tooltip.text", "" }, - { d->ck_cToolTipBack, d->pb_cToolTipBack, "tooltip.background", "" } }; + std::array cwData(std::to_array( + { { d->ck_cProfileFore, d->pb_cProfileFore, "contactlist.profile.header-foreground", "" }, + { d->ck_cProfileBack, d->pb_cProfileBack, "contactlist.profile.header-background", "" }, + { d->ck_cGroupFore, d->pb_cGroupFore, "contactlist.grouping.header-foreground", "" }, + { d->ck_cGroupBack, d->pb_cGroupBack, "contactlist.grouping.header-background", "" }, + { d->ck_cAnimFront, d->pb_cAnimFront, "contactlist.status-change-animation1", "" }, + { d->ck_cAnimBack, d->pb_cAnimBack, "contactlist.status-change-animation2", "" }, + { d->ck_cMessageSent, d->pb_cMessageSent, "messages.sent", "" }, + { d->ck_cMessageReceived, d->pb_cMessageReceived, "messages.received", "" }, + { d->ck_cSysMsg, d->pb_cSysMsg, "messages.informational", "" }, + { d->ck_cUserText, d->pb_cUserText, "messages.usertext", "" }, + { d->ck_cOnline, d->pb_cOnline, "contactlist.status.online", s.arg(tr("online")) }, + { d->ck_cAway, d->pb_cAway, "contactlist.status.away", s.arg(tr("away")) }, + { d->ck_cDND, d->pb_cDND, "contactlist.status.do-not-disturb", s.arg(tr("do not disturb")) }, + { d->ck_cOffline, d->pb_cOffline, "contactlist.status.offline", s.arg(tr("offline")) }, + { d->ck_cListBack, d->pb_cListBack, "contactlist.background", "" }, + + { d->ck_cMucModerator, d->pb_cMucModerator, "muc.role-moderator", "" }, + { d->ck_cMucParticipant, d->pb_cMucParticipant, "muc.role-participant", "" }, + { d->ck_cMucVisitor, d->pb_cMucVisitor, "muc.role-visitor", "" }, + { d->ck_cMucNoRole, d->pb_cMucNoRole, "muc.role-norole", "" }, + + { d->ck_cStatus, d->pb_cStatus, "contactlist.status-messages", s.arg(tr("Status message")) }, + { d->ck_highlight, d->pb_highlight, "messages.highlighting", "" }, + { d->ck_cLink, d->pb_cLink, "messages.link", "" }, + { d->ck_cLinkVisited, d->pb_cLinkVisited, "messages.link-visited", "" }, + { d->ck_cToolTipText, d->pb_cToolTipText, "tooltip.text", "" }, + { d->ck_cToolTipBack, d->pb_cToolTipBack, "tooltip.background", "" } })); bg_color = new QButtonGroup(this); - for (unsigned int i = 0; i < sizeof(cwData) / sizeof(ColorWidgetData); i++) { - bg_color->addButton(cwData[i].button); - if (!cwData[i].descr.isEmpty()) { - cwData[i].cbox->setToolTip(cwData[i].descr); + for (auto const &d : cwData) { + bg_color->addButton(d.button); + if (!d.descr.isEmpty()) { + d.cbox->setToolTip(d.descr); } - connect(cwData[i].cbox, SIGNAL(stateChanged(int)), SLOT(colorCheckBoxClicked(int))); - colorWidgetsMap[cwData[i].cbox] = QPair(cwData[i].button, cwData[i].option); +#if QT_VERSION < QT_VERSION_CHECK(6, 7, 0) + connect(d.cbox, &QCheckBox::stateChanged, this, &OptionsTabAppearanceGeneral::colorCheckBoxClicked); +#else + connect(d.cbox, &QCheckBox::checkStateChanged, this, &OptionsTabAppearanceGeneral::colorCheckBoxClicked); +#endif + colorWidgetsMap[d.cbox] = QPair(d.button, d.option); } - connect(bg_color, SIGNAL(buttonClicked(QAbstractButton *)), SLOT(chooseColor(QAbstractButton *))); + connect(bg_color, qOverload(&QButtonGroup::buttonClicked), this, + &OptionsTabAppearanceGeneral::chooseColor); if (PsiOptions::instance()->getOption("options.ui.contactlist.status-messages.single-line").toBool()) { d->ck_cStatus->hide(); @@ -349,7 +360,11 @@ void OptionsTabAppearanceGeneral::chooseColor(QAbstractButton *button) } } +#if QT_VERSION < QT_VERSION_CHECK(6, 7, 0) void OptionsTabAppearanceGeneral::colorCheckBoxClicked(int state) +#else +void OptionsTabAppearanceGeneral::colorCheckBoxClicked(Qt::CheckState state) +#endif { QPair data = colorWidgetsMap[static_cast(sender())]; if (state) { diff --git a/src/options/opt_appearance.h b/src/options/opt_appearance.h index ee0daae666..5f1930d3a2 100644 --- a/src/options/opt_appearance.h +++ b/src/options/opt_appearance.h @@ -71,7 +71,11 @@ class OptionsTabAppearanceGeneral : public OptionsTab { private slots: void setData(PsiCon *, QWidget *); void chooseColor(QAbstractButton *button); +#if QT_VERSION < QT_VERSION_CHECK(6,7,0) void colorCheckBoxClicked(int); +#else + void colorCheckBoxClicked(Qt::CheckState); +#endif void chooseFont(QAbstractButton *button); private: diff --git a/src/options/opt_appearance.ui b/src/options/opt_appearance.ui index 46accf6bae..5a137e3446 100644 --- a/src/options/opt_appearance.ui +++ b/src/options/opt_appearance.ui @@ -50,14 +50,24 @@ 0 - -414 + -252 397 - 768 + 762 - - + + + + + 20 + 20 + + + + + + 0 @@ -75,43 +85,62 @@ - - + + + + Specifies the background color for an account name in the main window. + - Status messages: + Account heading background: - - - - Away contacts: + + + + + 20 + 20 + - - + + + + Specifies the color for sent messages in chat and history windows. + - Offline contacts: + Sent message foreground: - - + + + + + 20 + 20 + + + + + + - Tooltip: + Group chat participants: - - + + - Visited Link: + Link: - - + + 0 @@ -129,25 +158,27 @@ - - - - Online contacts: + + + + + 0 + 0 + - - - - 20 20 + + + - - + + 0 @@ -165,8 +196,8 @@ - - + + 0 @@ -184,15 +215,25 @@ - - + + + + Specifies the color for informational messages in chat windows, like status changes and offline messages. + - Highlight: + Informational messages in chats: - - + + + + Online contacts: + + + + + 0 @@ -210,13 +251,27 @@ - - - - Specifies the background color for a group name in the main window. + + + + Away contacts: + + + + + + + 20 + 20 + + + + + + - Group heading background: + Offline contacts: @@ -230,22 +285,18 @@ - - - - Account heading foreground: + + + + Specifies the background animation color for nicks. - - - - - DND contacts: + Nick animation background: - - + + 0 @@ -263,8 +314,8 @@ - - + + 0 @@ -282,25 +333,27 @@ - - - - Specifies the color for sent messages in chat and history windows. + + + + + 0 + 0 + - - Sent message foreground: + + + 20 + 20 + - - - - - Group heading foreground: + - - + + 0 @@ -318,6 +371,16 @@ + + + + Specifies the color for received messages in chat and history windows. + + + Received message foreground: + + + @@ -350,8 +413,8 @@ - - + + 0 @@ -369,8 +432,8 @@ - - + + 0 @@ -388,8 +451,35 @@ - - + + + + DND contacts: + + + + + + + Specifies the background color for a group name in the main window. + + + Group heading background: + + + + + + + Specifies the foreground animation color for nicks. + + + Nick animation foreground: + + + + + 0 @@ -407,48 +497,57 @@ - - - - Specifies the foreground animation color for nicks. - + + - Nick animation foreground: + Highlight: - - - - Specifies the color for informational messages in chat windows, like status changes and offline messages. + + + + Tooltip background: + + + + - Informational messages in chats: + Status messages: - - - - Specifies the background color for an account name in the main window. + + + + Tooltip: + + + + - Account heading background: + Group chat moderators: - - - - Specifies the color for received messages in chat and history windows. + + + + Group chat visitors: + + + + - Received message foreground: + Visited Link: - - + + 0 @@ -466,18 +565,8 @@ - - - - Specifies the background animation color for nicks. - - - Nick animation background: - - - - - + + 0 @@ -495,18 +584,39 @@ - - - - - 20 - 20 - + + + + Account heading foreground: - - + + + + Group heading foreground: + + + + + + + Specifies the color for additional text of system messages. MUC topic for example. + + + Additional message text: + + + + + + + Group chat contacts without role: + + + + + 0 @@ -524,8 +634,8 @@ - - + + 0 @@ -543,48 +653,42 @@ - - - - Link: - - - - - - - Specifies the color for additional text of system messages. MUC topic for example. - - - Additional message text: - - - - + + + + 0 + 0 + + 20 20 - - - - - Tooltip background: + - - + + + + + 0 + 0 + + 20 20 + + + diff --git a/src/options/opt_application.cpp b/src/options/opt_application.cpp index cc8ad01b52..c9fc9fdaa1 100644 --- a/src/options/opt_application.cpp +++ b/src/options/opt_application.cpp @@ -101,7 +101,11 @@ QWidget *OptionsTabApplication::widget() d->label->setText(tr("(TCP: %1, UDP: %1-%2)").arg(port).arg(port + 3)); } }); +#if QT_VERSION < QT_VERSION_CHECK(6, 7, 0) connect(d->ck_docklet, &QCheckBox::stateChanged, this, &OptionsTabApplication::doEnableQuitOnClose); +#else + connect(d->ck_docklet, &QCheckBox::checkStateChanged, this, &OptionsTabApplication::doEnableQuitOnClose); +#endif connect(d->ck_auto_load, &QCheckBox::toggled, this, [this]() { autostartOptChanged_ = true; }); return w; @@ -203,7 +207,11 @@ void OptionsTabApplication::restoreOptions() #endif d->ck_dockHideMW->setChecked(PsiOptions::instance()->getOption("options.contactlist.hide-on-start").toBool()); d->ck_dockToolMW->setChecked(PsiOptions::instance()->getOption("options.contactlist.use-toolwindow").toBool()); +#if QT_VERSION < QT_VERSION_CHECK(6, 7, 0) doEnableQuitOnClose(d->ck_docklet->isChecked() ? 1 : 0); +#else + doEnableQuitOnClose(d->ck_docklet->checkState()); +#endif // data transfer d->le_dtPort->setText( @@ -249,16 +257,25 @@ void OptionsTabApplication::restoreOptions() #endif } +#if QT_VERSION < QT_VERSION_CHECK(6, 7, 0) void OptionsTabApplication::doEnableQuitOnClose(int state) +#else +void OptionsTabApplication::doEnableQuitOnClose(Qt::CheckState state) +#endif { if (!w) return; +#if QT_VERSION < QT_VERSION_CHECK(6, 7, 0) + const auto enabled = state > 0; +#else + const auto enabled = state != Qt::Unchecked; +#endif OptApplicationUI *d = static_cast(w); - d->ck_quitOnClose->setEnabled(state > 0); - d->ck_dockToolMW->setEnabled(state > 0); + // d->ck_quitOnClose->setEnabled(enabled); + d->ck_dockToolMW->setEnabled(enabled); #ifdef Q_OS_WIN - d->ck_dockDCstyle->setEnabled(state > 0); + d->ck_dockDCstyle->setEnabled(enabled); #endif - d->ck_dockHideMW->setEnabled(state > 0); + d->ck_dockHideMW->setEnabled(enabled); } diff --git a/src/options/opt_application.h b/src/options/opt_application.h index 5698858112..787e0d7dd3 100644 --- a/src/options/opt_application.h +++ b/src/options/opt_application.h @@ -25,7 +25,11 @@ class OptionsTabApplication : public OptionsTab { bool autostartOptChanged_ = false; private slots: +#if QT_VERSION < QT_VERSION_CHECK(6,7,0) void doEnableQuitOnClose(int); +#else + void doEnableQuitOnClose(Qt::CheckState); +#endif }; #endif // OPT_APPLICATION_H diff --git a/src/options/opt_application.ui b/src/options/opt_application.ui index 29c4a5ab2c..8116d3db86 100644 --- a/src/options/opt_application.ui +++ b/src/options/opt_application.ui @@ -6,8 +6,8 @@ 0 0 - 359 - 416 + 336 + 447 @@ -55,7 +55,7 @@ - Use "double-click" style (like ICQ) + Use "double-click" style @@ -142,17 +142,14 @@ - + Qt::Horizontal - - QSizePolicy::Expanding - - 20 - 20 + 0 + 0 @@ -209,14 +206,14 @@ - + Qt::Horizontal - 40 - 20 + 0 + 0 @@ -224,14 +221,14 @@
- + Qt::Vertical - 20 - 40 + 0 + 0 diff --git a/src/options/opt_chat.cpp b/src/options/opt_chat.cpp index 973a6e39e0..21de3f9cd6 100644 --- a/src/options/opt_chat.cpp +++ b/src/options/opt_chat.cpp @@ -89,8 +89,13 @@ void OptionsTabChat::applyOptions() if (d->ck_chatSoftReturn->isChecked()) { vl << QVariant::fromValue(QKeySequence(Qt::Key_Enter)) << QVariant::fromValue(QKeySequence(Qt::Key_Return)); } else { - vl << QVariant::fromValue(QKeySequence(Qt::Key_Enter | Qt::CTRL)) - << QVariant::fromValue(QKeySequence(Qt::CTRL | Qt::Key_Return)); +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) + vl << QVariant::fromValue(QKeySequence(quint32(Qt::Key_Enter) | quint32(Qt::CTRL))) + << QVariant::fromValue(QKeySequence(quint32(Qt::CTRL) | quint32(Qt::Key_Return))); +#else + vl << QVariant::fromValue(QKeySequence(QKeyCombination(Qt::CTRL, Qt::Key_Enter))) + << QVariant::fromValue(QKeySequence(QKeyCombination(Qt::CTRL, Qt::Key_Return))); +#endif } o->setOption("options.shortcuts.chat.send", vl); } diff --git a/src/options/opt_sound.cpp b/src/options/opt_sound.cpp index 399e87d176..27898c9745 100644 --- a/src/options/opt_sound.cpp +++ b/src/options/opt_sound.cpp @@ -42,72 +42,106 @@ QWidget *OptionsTabSound::widget() w = new OptSoundUI(); OptSoundUI *d = static_cast(w); - sounds_ << d->le_oeMessage << d->le_oeChat1 << d->le_oeChat2 << d->le_oeGroupChat << d->le_oeHeadline - << d->le_oeSystem << d->le_oeOnline << d->le_oeOffline << d->le_oeSend << d->le_oeIncomingFT - << d->le_oeFTComplete; - - bg_se = new QButtonGroup; - bg_se->addButton(d->tb_seMessage); - modify_buttons_[d->tb_seMessage] = d->le_oeMessage; - bg_se->addButton(d->tb_seChat1); - modify_buttons_[d->tb_seChat1] = d->le_oeChat1; - bg_se->addButton(d->tb_seChat2); - modify_buttons_[d->tb_seChat2] = d->le_oeChat2; - bg_se->addButton(d->tb_seGroupChat); - modify_buttons_[d->tb_seGroupChat] = d->le_oeGroupChat; - bg_se->addButton(d->tb_seHeadline); - modify_buttons_[d->tb_seHeadline] = d->le_oeHeadline; - bg_se->addButton(d->tb_seSystem); - modify_buttons_[d->tb_seSystem] = d->le_oeSystem; - bg_se->addButton(d->tb_seOnline); - modify_buttons_[d->tb_seOnline] = d->le_oeOnline; - bg_se->addButton(d->tb_seOffline); - modify_buttons_[d->tb_seOffline] = d->le_oeOffline; - bg_se->addButton(d->tb_seSend); - modify_buttons_[d->tb_seSend] = d->le_oeSend; - bg_se->addButton(d->tb_seIncomingFT); - modify_buttons_[d->tb_seIncomingFT] = d->le_oeIncomingFT; - bg_se->addButton(d->tb_seFTComplete); - modify_buttons_[d->tb_seFTComplete] = d->le_oeFTComplete; - connect(bg_se, SIGNAL(buttonClicked(QAbstractButton *)), SLOT(chooseSoundEvent(QAbstractButton *))); - + sounds_ << UiSoundItem { d->ck_oeMessage, + d->le_oeMessage, + d->tb_seMessage, + d->tb_seMessagePlay, + QStringLiteral("incoming-message"), + QStringLiteral("sound/chat2.wav") }; + + sounds_ << UiSoundItem { d->ck_oeChat1, + d->le_oeChat1, + d->tb_seChat1, + d->tb_seChat1Play, + QStringLiteral("new-chat"), + QStringLiteral("sound/chat1.wav") }; + + sounds_ << UiSoundItem { d->ck_oeChat2, + d->le_oeChat2, + d->tb_seChat2, + d->tb_seChat2Play, + QStringLiteral("chat-message"), + QStringLiteral("sound/chat2.wav") }; + + sounds_ << UiSoundItem { d->ck_oeGroupChat, + d->le_oeGroupChat, + d->tb_seGroupChat, + d->tb_seGroupChatPlay, + QStringLiteral("groupchat-message"), + QStringLiteral("sound/chat2.wav") }; + + sounds_ << UiSoundItem { d->ck_oeHeadline, + d->le_oeHeadline, + d->tb_seHeadline, + d->tb_seHeadlinePlay, + QStringLiteral("incoming-headline"), + QStringLiteral("sound/chat2.wav") }; + + sounds_ << UiSoundItem { d->ck_oeSystem, + d->le_oeSystem, + d->tb_seSystem, + d->tb_seSystemPlay, + QStringLiteral("system-message"), + QStringLiteral("sound/chat2.wav") }; + + sounds_ << UiSoundItem { d->ck_oeOnline, + d->le_oeOnline, + d->tb_seOnline, + d->tb_seOnlinePlay, + QStringLiteral("contact-online"), + QStringLiteral("sound/online.wav") }; + + sounds_ << UiSoundItem { d->ck_oeOffline, + d->le_oeOffline, + d->tb_seOffline, + d->tb_seOfflinePlay, + QStringLiteral("contact-offline"), + QStringLiteral("sound/offline.wav") }; + + sounds_ << UiSoundItem { d->ck_oeSend, + d->le_oeSend, + d->tb_seSend, + d->tb_seSendPlay, + QStringLiteral("outgoing-chat"), + QStringLiteral("sound/send.wav") }; + + sounds_ << UiSoundItem { d->ck_oeIncomingFT, + d->le_oeIncomingFT, + d->tb_seIncomingFT, + d->tb_seIncomingFTPlay, + QStringLiteral("incoming-file-transfer"), + QStringLiteral("sound/ft_incoming.wav") }; + + sounds_ << UiSoundItem { d->ck_oeFTComplete, + d->le_oeFTComplete, + d->tb_seFTComplete, + d->tb_seFTCompletePlay, + QStringLiteral("completed-file-transfer"), + QStringLiteral("sound/ft_complete.wav") }; + + bg_se = new QButtonGroup; bg_sePlay = new QButtonGroup; - bg_sePlay->addButton(d->tb_seMessagePlay); - play_buttons_[d->tb_seMessagePlay] = d->le_oeMessage; - bg_sePlay->addButton(d->tb_seChat1Play); - play_buttons_[d->tb_seChat1Play] = d->le_oeChat1; - bg_sePlay->addButton(d->tb_seChat2Play); - play_buttons_[d->tb_seChat2Play] = d->le_oeChat2; - bg_sePlay->addButton(d->tb_seGroupChatPlay); - play_buttons_[d->tb_seGroupChatPlay] = d->le_oeGroupChat; - bg_sePlay->addButton(d->tb_seHeadlinePlay); - play_buttons_[d->tb_seHeadlinePlay] = d->le_oeHeadline; - bg_sePlay->addButton(d->tb_seSystemPlay); - play_buttons_[d->tb_seSystemPlay] = d->le_oeSystem; - bg_sePlay->addButton(d->tb_seOnlinePlay); - play_buttons_[d->tb_seOnlinePlay] = d->le_oeOnline; - bg_sePlay->addButton(d->tb_seOfflinePlay); - play_buttons_[d->tb_seOfflinePlay] = d->le_oeOffline; - bg_sePlay->addButton(d->tb_seSendPlay); - play_buttons_[d->tb_seSendPlay] = d->le_oeSend; - bg_sePlay->addButton(d->tb_seIncomingFTPlay); - play_buttons_[d->tb_seIncomingFTPlay] = d->le_oeIncomingFT; - bg_sePlay->addButton(d->tb_seFTCompletePlay); - play_buttons_[d->tb_seFTCompletePlay] = d->le_oeFTComplete; - connect(bg_sePlay, SIGNAL(buttonClicked(QAbstractButton *)), SLOT(previewSoundEvent(QAbstractButton *))); - - connect(d->pb_soundReset, SIGNAL(clicked()), SLOT(soundReset())); - - // set up proper tool button icons - int n; - for (n = 0; n < 11; n++) { - IconToolButton *tb = static_cast(bg_se->buttons().at(n)); + + for (auto const &sound : std::as_const(sounds_)) { + bg_se->addButton(sound.selectButton); + modify_buttons_[sound.selectButton] = sound.file; + bg_sePlay->addButton(sound.playButton); + play_buttons_[sound.playButton] = sound.file; + + // set up proper tool button icons + IconToolButton *tb = static_cast(sound.selectButton); tb->setPsiIcon(IconsetFactory::iconPtr("psi/browse")); - tb = static_cast(bg_sePlay->buttons().at(n)); + tb = static_cast(sound.playButton); tb->setPsiIcon(IconsetFactory::iconPtr("psi/play")); + + // TODO: add ToolTip for earch widget } - // TODO: add ToolTip for earch widget + connect(bg_se, qOverload(&QButtonGroup::buttonClicked), this, + &OptionsTabSound::chooseSoundEvent); + connect(bg_sePlay, qOverload(&QButtonGroup::buttonClicked), this, + &OptionsTabSound::previewSoundEvent); + connect(d->pb_soundReset, &QPushButton::clicked, this, &OptionsTabSound::soundReset); d->le_player->setToolTip(tr("If your system supports multiple sound players, you may" " choose your preferred sound player application here.")); @@ -138,19 +172,14 @@ void OptionsTabSound::applyOptions() PsiOptions::instance()->setOption("options.ui.notifications.sounds.notify-every-muc-message", d->ck_gcSound->isChecked()); - PsiOptions::instance()->setOption("options.ui.notifications.sounds.incoming-message", d->le_oeMessage->text()); - PsiOptions::instance()->setOption("options.ui.notifications.sounds.new-chat", d->le_oeChat1->text()); - PsiOptions::instance()->setOption("options.ui.notifications.sounds.chat-message", d->le_oeChat2->text()); - PsiOptions::instance()->setOption("options.ui.notifications.sounds.groupchat-message", d->le_oeGroupChat->text()); - PsiOptions::instance()->setOption("options.ui.notifications.sounds.system-message", d->le_oeSystem->text()); - PsiOptions::instance()->setOption("options.ui.notifications.sounds.incoming-headline", d->le_oeHeadline->text()); - PsiOptions::instance()->setOption("options.ui.notifications.sounds.contact-online", d->le_oeOnline->text()); - PsiOptions::instance()->setOption("options.ui.notifications.sounds.contact-offline", d->le_oeOffline->text()); - PsiOptions::instance()->setOption("options.ui.notifications.sounds.outgoing-chat", d->le_oeSend->text()); - PsiOptions::instance()->setOption("options.ui.notifications.sounds.incoming-file-transfer", - d->le_oeIncomingFT->text()); - PsiOptions::instance()->setOption("options.ui.notifications.sounds.completed-file-transfer", - d->le_oeFTComplete->text()); + for (auto const &ui : std::as_const(sounds_)) { + auto opt = QLatin1String("options.ui.notifications.sounds.") + ui.option; + auto value = ui.file->text(); + if (!value.isEmpty() && !ui.enabled->isChecked()) { + value = "-" + value; + } + PsiOptions::instance()->setOption(opt, value); + } } void OptionsTabSound::restoreOptions() @@ -174,27 +203,17 @@ void OptionsTabSound::restoreOptions() d->ck_gcSound->setChecked( PsiOptions::instance()->getOption("options.ui.notifications.sounds.notify-every-muc-message").toBool()); - d->le_oeMessage->setText( - PsiOptions::instance()->getOption("options.ui.notifications.sounds.incoming-message").toString()); - d->le_oeChat1->setText(PsiOptions::instance()->getOption("options.ui.notifications.sounds.new-chat").toString()); - d->le_oeChat2->setText( - PsiOptions::instance()->getOption("options.ui.notifications.sounds.chat-message").toString()); - d->le_oeGroupChat->setText( - PsiOptions::instance()->getOption("options.ui.notifications.sounds.groupchat-message").toString()); - d->le_oeSystem->setText( - PsiOptions::instance()->getOption("options.ui.notifications.sounds.system-message").toString()); - d->le_oeHeadline->setText( - PsiOptions::instance()->getOption("options.ui.notifications.sounds.incoming-headline").toString()); - d->le_oeOnline->setText( - PsiOptions::instance()->getOption("options.ui.notifications.sounds.contact-online").toString()); - d->le_oeOffline->setText( - PsiOptions::instance()->getOption("options.ui.notifications.sounds.contact-offline").toString()); - d->le_oeSend->setText( - PsiOptions::instance()->getOption("options.ui.notifications.sounds.outgoing-chat").toString()); - d->le_oeIncomingFT->setText( - PsiOptions::instance()->getOption("options.ui.notifications.sounds.incoming-file-transfer").toString()); - d->le_oeFTComplete->setText( - PsiOptions::instance()->getOption("options.ui.notifications.sounds.completed-file-transfer").toString()); + for (auto const &ui : std::as_const(sounds_)) { + auto opt = QLatin1String("options.ui.notifications.sounds.") + ui.option; + auto value = PsiOptions::instance()->getOption(opt).toString(); + if (value.startsWith("-")) { + ui.enabled->setChecked(false); + ui.file->setText(value.mid(1)); + } else { + ui.enabled->setChecked(true); + ui.file->setText(value); + } + } } void OptionsTabSound::setData(PsiCon *, QWidget *p) { parentWidget = p; } @@ -214,18 +233,9 @@ void OptionsTabSound::previewSoundEvent(QAbstractButton *b) { soundPlay(play_but void OptionsTabSound::soundReset() { - OptSoundUI *d = static_cast(w); - - d->le_oeMessage->setText("sound/chat2.wav"); - d->le_oeChat1->setText("sound/chat1.wav"); - d->le_oeChat2->setText("sound/chat2.wav"); - d->le_oeGroupChat->setText("sound/chat2.wav"); - d->le_oeSystem->setText("sound/chat2.wav"); - d->le_oeHeadline->setText("sound/chat2.wav"); - d->le_oeOnline->setText("sound/online.wav"); - d->le_oeOffline->setText("sound/offline.wav"); - d->le_oeSend->setText("sound/send.wav"); - d->le_oeIncomingFT->setText("sound/ft_incoming.wav"); - d->le_oeFTComplete->setText("sound/ft_complete.wav"); + for (auto const &ui : std::as_const(sounds_)) { + ui.file->setText(ui.defaultFile); + ui.enabled->setChecked(true); + } emit dataChanged(); } diff --git a/src/options/opt_sound.h b/src/options/opt_sound.h index 63a9dceca2..7b131815cf 100644 --- a/src/options/opt_sound.h +++ b/src/options/opt_sound.h @@ -9,6 +9,7 @@ class QAbstractButton; class QButtonGroup; class QLineEdit; class QWidget; +class QCheckBox; class OptionsTabSound : public OptionsTab { Q_OBJECT @@ -28,8 +29,19 @@ private slots: void setData(PsiCon *, QWidget *); private: - QWidget *w = nullptr, *parentWidget = nullptr; - QList sounds_; + struct UiSoundItem { + QCheckBox *enabled; + QLineEdit *file; + QAbstractButton *selectButton; + QAbstractButton *playButton; + QString option; + QString defaultFile; + }; + + QList sounds_; + + QWidget *w = nullptr, *parentWidget = nullptr; + // QList sounds_; QMap modify_buttons_; QMap play_buttons_; QButtonGroup *bg_se = nullptr, *bg_sePlay = nullptr; diff --git a/src/options/opt_sound.ui b/src/options/opt_sound.ui index 2fbcbbe2ed..13fb0b642b 100644 --- a/src/options/opt_sound.ui +++ b/src/options/opt_sound.ui @@ -75,13 +75,10 @@ - QFrame::HLine - - - QFrame::Sunken + QFrame::VLine - Qt::Horizontal + Qt::Vertical @@ -102,50 +99,57 @@ 6 - - - - + + + + Enter a filename or !beep for a system beep - - + + Enter a filename or !beep for a system beep + + + + Enter a filename or !beep for a system beep + + + + + + + + + + - + Headline: - - + + Enter a filename or !beep for a system beep - - - - Receive online status: - - - - - + + Enter a filename or !beep for a system beep - - + + @@ -158,87 +162,80 @@ - - - - Enter a filename or !beep for a system beep + + + + - - - - Receive message: + + + + - - + + - - - - + + + + Enter a filename or !beep for a system beep - - + + - - + + - - - - Enter a filename or !beep for a system beep + + + + Receive next chat: - - + + - - + + - Send message: + Receive online status: - - + + - - - - Enter a filename or !beep for a system beep - - - - - + + - System message: + Receive first chat: @@ -249,162 +246,162 @@ - - + + - - + + - - - - + + + + Enter a filename or !beep for a system beep - - - - + + + + Incoming file transfer: - - - - + + + + Enter a filename or !beep for a system beep - - + + - - + + + + File transfer complete: + + + + + - - - - Enter a filename or !beep for a system beep + + + + - - + + - Incoming file transfer: + Receive message: - - + + - File transfer complete: + Receive offline status: - - + + Enter a filename or !beep for a system beep - - - - Enter a filename or !beep for a system beep + + + + - - + + - - + + + + + + + + + Enter a filename or !beep for a system beep - - - - Receive next chat: + + + + Enter a filename or !beep for a system beep - - + + - - + + - Receive first chat: + Receive MUC message - - + + - - + + - Receive offline status: + Send message: - - + + - Receive MUC message - - - - - - - Enter a filename or !beep for a system beep - - - - - - - + System message: - - + + @@ -431,11 +428,8 @@ - - Qt::Horizontal - - QSizePolicy::Expanding + QSizePolicy::Fixed @@ -455,16 +449,10 @@ - + Qt::Vertical - - - 20 - 0 - - @@ -474,7 +462,7 @@ IconToolButton QWidget -
icontoolbutton.h
+
icontoolbutton.h
diff --git a/src/options/opt_theme.cpp b/src/options/opt_theme.cpp index 11c604b9b7..ffa7b2a4e2 100644 --- a/src/options/opt_theme.cpp +++ b/src/options/opt_theme.cpp @@ -84,6 +84,7 @@ QWidget *OptionsTabAppearanceTheme::widget() SLOT(themeSelected(QModelIndex, QModelIndex))); connect(themesModel, SIGNAL(rowsInserted(QModelIndex, int, int)), SLOT(modelRowsInserted(QModelIndex, int, int))); + connect(d->cmb_style, qOverload(&QComboBox::currentIndexChanged), this, [this](int) { emit dataChanged(); }); QTimer::singleShot(0, unsortedModel, &PsiThemeModel::load); @@ -92,7 +93,7 @@ QWidget *OptionsTabAppearanceTheme::widget() void OptionsTabAppearanceTheme::themeSelected(const QModelIndex ¤t, const QModelIndex &previous) { - Q_UNUSED(current); + updateStyles(current); if (!previous.isValid()) { return; // Psi won't start if it's impossible to load any theme. So we always have previous. } @@ -108,6 +109,7 @@ void OptionsTabAppearanceTheme::modelRowsInserted(const QModelIndex &parent, int const QModelIndex index = themesModel->index(i, 0); if (themesModel->data(index, PsiThemeModel::IsCurrent).toBool()) { d->themeView->setCurrentIndex(index); + updateStyles(index); } #if 0 const QString id = themesModel->data(index, PsiThemeModel::IdRole).toString(); @@ -187,13 +189,26 @@ QString OptionsTabAppearanceTheme::getThemeId(const QString &objName) const return (index > 0 ? objName.right(objName.length() - index - 1) : QString()); } +void OptionsTabAppearanceTheme::updateStyles(const QModelIndex &index) +{ + auto styles = themesModel->data(index, PsiThemeModel::StylesListRole).toStringList(); + OptAppearanceThemeUI *d = static_cast(w); + d->cmb_style->blockSignals(true); + d->cmb_style->clear(); + for (auto const &s : styles) { + d->cmb_style->addItem(s); + } + d->cmb_style->setCurrentText(themesModel->data(index, PsiThemeModel::CurrentStyleRole).toString()); + d->cmb_style->blockSignals(false); +} + void OptionsTabAppearanceTheme::applyOptions() { if (!w) return; OptAppearanceThemeUI *d = static_cast(w); - themesModel->setData(d->themeView->currentIndex(), true, PsiThemeModel::IsCurrent); + themesModel->setData(d->themeView->currentIndex(), d->cmb_style->currentText(), PsiThemeModel::IsCurrent); } void OptionsTabAppearanceTheme::restoreOptions() diff --git a/src/options/opt_theme.h b/src/options/opt_theme.h index 8485f84281..15b68ea501 100644 --- a/src/options/opt_theme.h +++ b/src/options/opt_theme.h @@ -59,6 +59,7 @@ private slots: private: QString getThemeId(const QString &objName) const; + void updateStyles(const QModelIndex &index); private: QWidget *w = nullptr; diff --git a/src/options/opt_theme.ui b/src/options/opt_theme.ui index 75f3750caf..b07cd3303c 100644 --- a/src/options/opt_theme.ui +++ b/src/options/opt_theme.ui @@ -19,7 +19,7 @@ - QAbstractItemView::NoEditTriggers + QAbstractItemView::EditTrigger::NoEditTriggers false @@ -42,10 +42,20 @@ + + + + Style: + + + + + + - Qt::Horizontal + Qt::Orientation::Horizontal @@ -67,7 +77,7 @@ <a href="thememanager://showmore/">More themes</a> - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter
diff --git a/src/options/opt_tree.cpp b/src/options/opt_tree.cpp index e01ed09ba5..5364f9036c 100644 --- a/src/options/opt_tree.cpp +++ b/src/options/opt_tree.cpp @@ -31,6 +31,7 @@ QWidget *OptionsTabTree::widget() layout->addWidget(lb); PsiOptionsEditor *poe = new PsiOptionsEditor(w); + poe->layout()->setContentsMargins(0, 0, 0, 0); layout->addWidget(poe); poe->show(); diff --git a/src/options/optionsdlgbase.cpp b/src/options/optionsdlgbase.cpp index 9156ba26d9..9c5dccdbae 100644 --- a/src/options/optionsdlgbase.cpp +++ b/src/options/optionsdlgbase.cpp @@ -281,7 +281,11 @@ void OptionsDlgBase::Private::createChangedMap() // Do NOT call addWidgetChangedSignal() for them. // Instead, connect the widget's signal to your tab own dataChaged() signal changedMap.insert("QButton", SIGNAL(stateChanged(int))); +#if QT_VERSION < QT_VERSION_CHECK(6,7,0) changedMap.insert("QCheckBox", SIGNAL(stateChanged(int))); +#else + changedMap.insert("QCheckBox", SIGNAL(checkStateChanged(Qt::CheckState))); +#endif // qt4 port: there are no stateChangedSignals anymore // changedMap.insert("QPushButton", SIGNAL(stateChanged(int))); // changedMap.insert("QRadioButton", SIGNAL(stateChanged(int))); diff --git a/src/pepmanager.cpp b/src/pepmanager.cpp index bffefe2c69..5dc949abc8 100644 --- a/src/pepmanager.cpp +++ b/src/pepmanager.cpp @@ -36,69 +36,6 @@ using namespace XMPP; // ----------------------------------------------------------------------------- -class PEPGetTask : public Task { -public: - PEPGetTask(Task *parent, const QString &jid, const QString &node, const QString &itemID) : - Task(parent), jid_(jid), node_(node) - { - iq_ = createIQ(doc(), "get", jid_, id()); - - QDomElement pubsub = doc()->createElementNS("http://jabber.org/protocol/pubsub", "pubsub"); - iq_.appendChild(pubsub); - - QDomElement items = doc()->createElement("items"); - items.setAttribute("node", node); - pubsub.appendChild(items); - - QDomElement item = doc()->createElement("item"); - item.setAttribute("id", itemID); - items.appendChild(item); - } - - void onGo() { send(iq_); } - - bool take(const QDomElement &x) - { - if (!iqVerify(x, jid_, id())) - return false; - - if (x.attribute("type") == "result") { - // FIXME Check namespace... - QDomElement e = x.firstChildElement("pubsub"); - if (!e.isNull()) { - QDomElement i = e.firstChildElement("items"); - if (!i.isNull()) { - QString iname = "item"; - for (QDomElement e1 = i.firstChildElement(iname); !e1.isNull(); e1 = e1.nextSiblingElement(iname)) { - for (QDomElement e2 = e1.firstChildElement(); !e2.isNull(); e2 = e2.nextSiblingElement()) { - items_ += PubSubItem(e1.attribute("id"), e2); - } - } - } - } - setSuccess(); - return true; - } else { - setError(x); - return true; - } - } - - const QList &items() const { return items_; } - - const QString &jid() const { return jid_; } - - const QString &node() const { return node_; } - -private: - QDomElement iq_; - QString jid_; - QString node_; - QList items_; -}; - -// ----------------------------------------------------------------------------- - /* class PEPUnsubscribeTask : public Task { @@ -227,8 +164,8 @@ class PEPCreateNodeTask : public Task class PEPPublishTask : public Task { public: - PEPPublishTask(Task *parent, const QString &node, const PubSubItem &it, PEPManager::Access access) : - Task(parent), node_(node), item_(it) + PEPPublishTask(Task *parent, const QString &node, const PubSubItem &it, std::optional access, + bool persisteItems = false) : Task(parent), node_(node), item_(it) { iq_ = createIQ(doc(), "set", "", id()); @@ -240,33 +177,40 @@ class PEPPublishTask : public Task { pubsub.appendChild(publish); QDomElement item = doc()->createElement("item"); - item.setAttribute("id", it.id()); + if (!it.id().isEmpty()) { + item.setAttribute("id", it.id()); + } publish.appendChild(item); - if (access != PEPManager::DefaultAccess) { - QDomElement conf = doc()->createElement("configure"); - QDomElement conf_x = doc()->createElementNS("jabber:x:data", "x"); - - // Form type - QDomElement conf_x_field_type = doc()->createElement("field"); - conf_x_field_type.setAttribute("var", "FORM_TYPE"); - conf_x_field_type.setAttribute("type", "hidden"); - QDomElement conf_x_field_type_value = doc()->createElement("value"); - conf_x_field_type_value.appendChild(doc()->createTextNode("http://jabber.org/protocol/pubsub#node_config")); - conf_x_field_type.appendChild(conf_x_field_type_value); - conf_x.appendChild(conf_x_field_type); - - // Access model - QDomElement access_model = doc()->createElement("field"); - access_model.setAttribute("var", "pubsub#access_model"); - QDomElement access_model_value = doc()->createElement("value"); - access_model.appendChild(access_model_value); - if (access == PEPManager::PublicAccess) { - access_model_value.appendChild(doc()->createTextNode("open")); - } else if (access == PEPManager::PresenceAccess) { - access_model_value.appendChild(doc()->createTextNode("presence")); + if (access || persisteItems) { + QDomElement conf = doc()->createElement("publish-options"); + XData form(XData::Data_Submit); + form.setRegistrarType(QLatin1String("http://jabber.org/protocol/pubsub#publish-options")); + XMPP::XData::FieldList fields; + if (access) { + XMPP::XData::Field f; + f.setVar(QLatin1String("pubsub#access_model")); + if (*access == PEPManager::Access::Open) { + f.setValue({ QLatin1String("open") }); + } else if (access == PEPManager::Access::Presence) { + f.setValue({ QLatin1String("presence") }); + } else if (access == PEPManager::Access::Roster) { + f.setValue({ QLatin1String("roster") }); + } else if (access == PEPManager::Access::Authorize) { + f.setValue({ QLatin1String("authorize") }); + } else if (access == PEPManager::Access::Whitelist) { + f.setValue({ QLatin1String("whitelist") }); + } + fields << f; + } + if (persisteItems) { + XMPP::XData::Field f; + f.setVar(QLatin1String("pubsub#persist_items")); + f.setValue({ QLatin1String("true") }); + fields << f; } - conf_x.appendChild(access_model); + form.setFields(fields); + auto conf_x = form.toXml(doc(), true); conf.appendChild(conf_x); pubsub.appendChild(conf); @@ -525,16 +469,17 @@ void PEPManager::unsubscribeFinished() saveSubscriptions(); }*/ -void PEPManager::publish(const QString &node, const PubSubItem &it, Access access) +Task *PEPManager::publish(const QString &node, const PubSubItem &it, std::optional access, bool persisteItems) { // if (!canPublish(node)) // return; if (!serverInfo_->hasPEP()) - return; + return nullptr; - PEPPublishTask *tp = new PEPPublishTask(client_->rootTask(), node, it, access); + PEPPublishTask *tp = new PEPPublishTask(client_->rootTask(), node, it, access, persisteItems); connect(tp, SIGNAL(finished()), SLOT(publishFinished())); tp->go(true); + return tp; } void PEPManager::retract(const QString &node, const QString &id) @@ -567,16 +512,17 @@ void PEPManager::publishFinished() } } -void PEPManager::get(const Jid &jid, const QString &node, const QString &id) +PEPGetTask *PEPManager::get(const Jid &jid, const QString &node, const QString &id) { PEPGetTask *g = new PEPGetTask(client_->rootTask(), jid.bare(), node, id); connect(g, SIGNAL(finished()), SLOT(getFinished())); g->go(true); + return g; } void PEPManager::messageReceived(const Message &m) { - if (m.type() != "error") { + if (m.type() != Message::Type::Error) { const auto &psrItems = m.pubsubRetractions(); for (const PubSubRetraction &i : psrItems) { emit itemRetracted(m.from(), m.pubsubNode(), i); @@ -611,8 +557,8 @@ void PEPManager::getFinished() emit itemPublished(task->jid(), task->node(), task->items().first()); } } else { - qWarning() << QString("[%3] PEP Get failed: '%1' (%2)") - .arg(task->statusString(), QString::number(task->statusCode()), client_->jid().full()); + qWarning() << QString("PEP Get (jid=%1 node=%2) failed: '%3' (%4)") + .arg(task->jid(), task->node(), task->statusString(), QString::number(task->statusCode())); } } @@ -663,3 +609,57 @@ void PEPManager::getSubscriptionsTaskFinished() emit getSubscriptions_error(task->jid(),task->statusCode(), task->statusString()); } }*/ + +PEPGetTask::PEPGetTask(Task *parent, const QString &jid, const QString &node, const QString &itemID) : + Task(parent), jid_(jid), node_(node) +{ + iq_ = createIQ(doc(), "get", jid_, id()); + + QDomElement pubsub = doc()->createElementNS("http://jabber.org/protocol/pubsub", "pubsub"); + iq_.appendChild(pubsub); + + QDomElement items = doc()->createElement("items"); + items.setAttribute("node", node); + pubsub.appendChild(items); + + if (!itemID.isEmpty()) { + QDomElement item = doc()->createElement("item"); + item.setAttribute("id", itemID); + items.appendChild(item); + } +} + +void PEPGetTask::onGo() { send(iq_); } + +bool PEPGetTask::take(const QDomElement &x) +{ + if (!iqVerify(x, jid_, id())) + return false; + + if (x.attribute("type") == "result") { + // FIXME Check namespace... + QDomElement e = x.firstChildElement("pubsub"); + if (!e.isNull()) { + QDomElement i = e.firstChildElement("items"); + if (!i.isNull()) { + QString iname = "item"; + for (QDomElement e1 = i.firstChildElement(iname); !e1.isNull(); e1 = e1.nextSiblingElement(iname)) { + for (QDomElement e2 = e1.firstChildElement(); !e2.isNull(); e2 = e2.nextSiblingElement()) { + items_ += PubSubItem(e1.attribute("id"), e2); + } + } + } + } + setSuccess(); + return true; + } else { + setError(x); + return true; + } +} + +const QList &PEPGetTask::items() const { return items_; } + +const QString &PEPGetTask::jid() const { return jid_; } + +const QString &PEPGetTask::node() const { return node_; } diff --git a/src/pepmanager.h b/src/pepmanager.h index cc4a088658..c734f13575 100644 --- a/src/pepmanager.h +++ b/src/pepmanager.h @@ -20,7 +20,9 @@ #ifndef PEPMANAGER_H #define PEPMANAGER_H -#include +#include "iris/xmpp_task.h" + +#include class PubSubSubscription; class QString; @@ -35,11 +37,32 @@ class ServerInfoManager; } using namespace XMPP; +class PEPGetTask : public Task { +public: + PEPGetTask(Task *parent, const QString &jid, const QString &node, const QString &itemID); + + void onGo(); + + bool take(const QDomElement &x); + + const QList &items() const; + + const QString &jid() const; + + const QString &node() const; + +private: + QDomElement iq_; + QString jid_; + QString node_; + QList items_; +}; + class PEPManager : public QObject { Q_OBJECT public: - enum Access { DefaultAccess, PresenceAccess, PublicAccess }; + enum class Access { Open, Presence, Roster, Authorize, Whitelist }; PEPManager(XMPP::Client *client, ServerInfoManager *serverInfo); @@ -50,10 +73,11 @@ class PEPManager : public QObject { // void subscribe(const QString&, const QString&); // void unsubscribe(const QString&, const QString&); - void publish(const QString &node, const PubSubItem &, Access = DefaultAccess); - void retract(const QString &node, const QString &id); - void disable(const QString &tagName, const QString &node, const QString &id); - void get(const Jid &jid, const QString &node, const QString &id); + XMPP::Task *publish(const QString &node, const PubSubItem &, std::optional = {}, + bool persisteItems = false); + void retract(const QString &node, const QString &id); + void disable(const QString &tagName, const QString &node, const QString &id); + PEPGetTask *get(const Jid &jid, const QString &node, const QString &id); // void getSubscriptions(const Jid& jid); diff --git a/src/pluginhost.cpp b/src/pluginhost.cpp index b4dab193b5..e81ae759c7 100644 --- a/src/pluginhost.cpp +++ b/src/pluginhost.cpp @@ -471,7 +471,7 @@ bool PluginHost::enable() PsiAccountController *pac = qobject_cast(plugin_); if (pac) { #ifndef PLUGINS_NO_DEBUG - qDebug("connectint psiaccount controller"); + qDebug("connecting psiaccount controller"); #endif pac->setPsiAccountControllingHost(this); } diff --git a/src/pluginhost.h b/src/pluginhost.h index a0d911d9ad..a46a6343d9 100644 --- a/src/pluginhost.h +++ b/src/pluginhost.h @@ -130,7 +130,7 @@ class PluginHost : public QObject, // OptionAccessingHost void setPluginOption(const QString &option, const QVariant &value) override; - QVariant getPluginOption(const QString &option, const QVariant &defValue = QVariant::Invalid) override; + QVariant getPluginOption(const QString &option, const QVariant &defValue = {}) override; void setGlobalOption(const QString &option, const QVariant &value) override; QVariant getGlobalOption(const QString &option) override; void optionChanged(const QString &option); diff --git a/src/profiledlg.cpp b/src/profiledlg.cpp index 1563a5fade..3f4a32f8a3 100644 --- a/src/profiledlg.cpp +++ b/src/profiledlg.cpp @@ -268,7 +268,7 @@ void ProfileManageDlg::slotProfileDelete() tr("Are you sure you want to delete the \"%1\" profile? " "This will delete all of the profile's message history as well as associated settings!") .arg(name)); - auto rejectButton = msgBox.addButton(tr("No, I changed my mind"), QMessageBox::RejectRole); + msgBox.addButton(tr("No, I changed my mind"), QMessageBox::RejectRole); auto acceptButton = msgBox.addButton(tr("Delete it!"), QMessageBox::AcceptRole); msgBox.exec(); diff --git a/src/profilenew.ui b/src/profilenew.ui index 3e8516e97e..8ba3173fb2 100644 --- a/src/profilenew.ui +++ b/src/profilenew.ui @@ -52,7 +52,7 @@ Keep your<i> Profile Name</i> simple. It should be a single word comprised of only letters or numbers.<br> <br> -The<i> Default Action</i> is what happens when you double click a contact in your list. The choices are<b> Message</b> (ICQ style) and<b> Chat</b> (AIM style). You can change this later from the Options menu.<br> +The<i> Default Action</i> is what happens when you double click a contact in your list. The choices are<b> Message</b> and<b> Chat</b>. You can change this later from the Options menu.<br> <br> Check the <i>Enable Emoticons</i> checkbox if you'd like text such as <b>:-)</b> to be turned into graphics like <icon name="psi/smile">. @@ -153,28 +153,16 @@ Check the <i>Enable Emoticons</i> checkbox if you'd like text such a
- + - Qt::Vertical - - - QSizePolicy::Expanding - - - - 20 - 131 - + Qt::Horizontal - QFrame::HLine - - - QFrame::Sunken + QFrame::NoFrame @@ -193,18 +181,9 @@ Check the <i>Enable Emoticons</i> checkbox if you'd like text such a 0
- + - Qt::Horizontal - - - QSizePolicy::Expanding - - - - 95 - 20 - + Qt::Vertical diff --git a/src/profiles.h b/src/profiles.h index 8039cbd883..8b62be48c5 100644 --- a/src/profiles.h +++ b/src/profiles.h @@ -60,11 +60,10 @@ class UserAccount { opt_reconn, opt_ignoreSSLWarnings, opt_useProxyForUpload, opt_compress, opt_sm; bool req_mutual_auth; - bool legacy_ssl_probe; bool opt_automatic_resource, priority_dep_on_status, ignore_global_actions; XMPP::ClientStream::AllowPlainType allow_plain; int security_level; - enum SSLFlag { SSL_No = 0, SSL_Yes = 1, SSL_Auto = 2, SSL_Legacy = 3 } ssl; + enum SSLFlag { TLS_No = 0, TLS_Yes = 1, TLS_Auto = 2, Direct_TLS = 3 } ssl; QString proxyID; diff --git a/src/psi_profiles.cpp b/src/psi_profiles.cpp index d4908e75b2..c80f108f6b 100644 --- a/src/psi_profiles.cpp +++ b/src/psi_profiles.cpp @@ -65,9 +65,8 @@ void UserAccount::reset() customAuth = false; storeSaltedHashedPassword = false; req_mutual_auth = false; - legacy_ssl_probe = false; security_level = QCA::SL_None; - ssl = SSL_Auto; + ssl = TLS_Auto; jid = ""; pass = ""; scramSaltedHashPassword = ""; @@ -137,7 +136,6 @@ void UserAccount::fromOptions(OptionsTree *o, QString base) opt_useProxyForUpload = o->getOption(base + ".use-proxy-for-upload", true).toBool(); opt_compress = o->getOption(base + ".compress").toBool(); req_mutual_auth = o->getOption(base + ".require-mutual-auth").toBool(); - legacy_ssl_probe = o->getOption(base + ".legacy-ssl-probe").toBool(); opt_automatic_resource = o->getOption(base + ".automatic-resource").toBool(); priority_dep_on_status = o->getOption(base + ".priority-depends-on-status", false).toBool(); ignore_global_actions = o->getOption(base + ".ignore-global-actions").toBool(); @@ -179,15 +177,15 @@ void UserAccount::fromOptions(OptionsTree *o, QString base) tmp = o->getOption(base + ".ssl").toString(); if (tmp == "no") { - ssl = SSL_No; + ssl = TLS_No; } else if (tmp == "yes") { - ssl = SSL_Yes; + ssl = TLS_Yes; } else if (tmp == "auto") { - ssl = SSL_Auto; + ssl = TLS_Auto; } else if (tmp == "legacy") { - ssl = SSL_Legacy; + ssl = Direct_TLS; } else { - ssl = SSL_Yes; + ssl = TLS_Yes; } host = o->getOption(base + ".host").toString(); @@ -296,7 +294,6 @@ void UserAccount::toOptions(OptionsTree *o, QString base) o->setOption(base + ".use-proxy-for-upload", opt_useProxyForUpload); o->setOption(base + ".compress", opt_compress); o->setOption(base + ".require-mutual-auth", req_mutual_auth); - o->setOption(base + ".legacy-ssl-probe", legacy_ssl_probe); o->setOption(base + ".automatic-resource", opt_automatic_resource); o->setOption(base + ".priority-depends-on-status", priority_dep_on_status); o->setOption(base + ".ignore-global-actions", ignore_global_actions); @@ -331,16 +328,16 @@ void UserAccount::toOptions(OptionsTree *o, QString base) o->setOption(base + ".use-host", opt_host); o->setOption(base + ".security-level", security_level); switch (ssl) { - case SSL_No: + case TLS_No: o->setOption(base + ".ssl", "no"); break; - case SSL_Yes: + case TLS_Yes: o->setOption(base + ".ssl", "yes"); break; - case SSL_Auto: + case TLS_Auto: o->setOption(base + ".ssl", "auto"); break; - case SSL_Legacy: + case Direct_TLS: o->setOption(base + ".ssl", "legacy"); break; default: diff --git a/src/psiaccount.cpp b/src/psiaccount.cpp index c0e432420b..80650f14d7 100644 --- a/src/psiaccount.cpp +++ b/src/psiaccount.cpp @@ -55,6 +55,8 @@ #include "fileutil.h" #include "geolocationdlg.h" #include "iris/bsocket.h" +#include "psithememanager.h" +#include "xmpp/xmpp-im/xmpp_vcard4.h" #ifdef GOOGLE_FT #include "googleftmanager.h" #endif @@ -159,6 +161,8 @@ #include #endif +#include + /*#ifdef Q_OS_WIN # include typedef int socklen_t; @@ -242,12 +246,6 @@ BlockTransportPopup::BlockTransportPopup(QObject *parent, const Jid &_j) : QObje j = _j; userCounter = 0; - // Hack for ICQ SMS - if (j.domain().left(3) == "icq") { - new BlockTransportPopup(parent, "sms." + j.domain()); // sms.icq.host.com - new BlockTransportPopup(parent, "sms" + j.domain().right(j.domain().length() - 3)); // sms.host.com - } - QTimer::singleShot(15000, this, SLOT(timeout())); } @@ -423,8 +421,8 @@ class PsiAccount::Private : public Alertable { RCLeaveMucServer *rcLeaveMucServer = nullptr; // Avatars - AvatarFactory *avatarFactory = nullptr; - QByteArray photoHash; + AvatarFactory *avatarFactory = nullptr; + std::optional photoHash; // Voice Call VoiceCaller *voiceCaller = nullptr; @@ -613,7 +611,7 @@ private slots: } else { PsiContact *contact = findContact(u.jid()); if (!contact) { - contact = addContact(u); + addContact(u); } else { contact->update(u); } @@ -729,11 +727,11 @@ public slots: acc.proxyID = ""; } - void vcardChanged(const Jid &j) + void vcardChanged(const Jid &j, VCardFactory::Flags) { // our own vcard? if (j.compare(jid, false)) { - const VCard vcard = VCardFactory::instance()->vcard(j); + const auto vcard = VCardFactory::instance()->vcard(j); if (vcard) { vcardPhotoUpdate(vcard.photo()); } @@ -810,17 +808,30 @@ public slots: void dialogRegister(QWidget *w, const Jid &jid) { - connect(w, &QWidget::destroyed, this, &Private::forceDialogUnregister); - item_dialog2 *i = new item_dialog2; - i->widget = w; - i->jid = jid; - dialogList.append(i); + item_dialog2 *i; + if (connect(w, &QWidget::destroyed, this, &Private::forceDialogUnregister, Qt::UniqueConnection)) { + i = new item_dialog2; + i->widget = w; + dialogList.append(i); + // qDebug("registerd: %s for jid=%s", qPrintable(w->objectName()), qPrintable(jid.full())); + } else { + auto it = std::ranges::find_if(dialogList, [w](auto const &v) { return v->widget == w; }); + if (it == dialogList.end()) { + qWarning("dialog registration inconsistency: %s jid=%s", qPrintable(w->objectName()), + qPrintable(jid.full())); + return; + } + i = *it; + // qDebug("jid updated: %s for jid=%s", qPrintable(w->objectName()), qPrintable(jid.full())); + } + i->jid = jid; } void dialogUnregister(QWidget *w) { for (item_dialog2 *i : std::as_const(dialogList)) { if (i->widget == w) { + disconnect(w, &QWidget::destroyed, this, &Private::forceDialogUnregister); dialogList.removeAll(i); delete i; return; @@ -1066,6 +1077,12 @@ PsiAccount::PsiAccount(const UserAccount &acc, PsiContactList *parent, TabManage // another hack. We rather should have PsiMedia single instance as a member of PsiCon connect(MediaDeviceWatcher::instance(), &MediaDeviceWatcher::availibityChanged, this, &PsiAccount::updateFeatures); +#ifdef WEBKIT + connect(d->psi->themeManager()->provider("chatview"), &PsiThemeProvider::themeChanged, this, + &PsiAccount::updateFeatures); + connect(d->psi->themeManager()->provider("groupchatview"), &PsiThemeProvider::themeChanged, this, + &PsiAccount::updateFeatures); +#endif #ifdef FILETRANSFER d->client->setFileTransferEnabled(true); #else @@ -1538,19 +1555,33 @@ void PsiAccount::updateFeatures() #endif #ifdef USE_PEP - features << "http://jabber.org/protocol/mood" << "http://jabber.org/protocol/activity"; - features << "http://jabber.org/protocol/tune" << "http://jabber.org/protocol/geoloc"; - features << "urn:xmpp:avatar:data" << "urn:xmpp:avatar:metadata"; + features << QLatin1String("http://jabber.org/protocol/mood") << QLatin1String("http://jabber.org/protocol/activity") + << QLatin1String("http://jabber.org/protocol/tune") << QLatin1String("http://jabber.org/protocol/geoloc") + << QLatin1String("urn:xmpp:avatar:data") << QLatin1String("urn:xmpp:avatar:metadata"); #endif if (AvCallManager::isSupported()) { - features << "urn:xmpp:jingle:transports:ice-udp:1"; - features << "urn:xmpp:jingle:transports:ice:0"; - features << "urn:xmpp:jingle:apps:rtp:1"; - features << "urn:xmpp:jingle:apps:rtp:audio"; - features << "urn:xmpp:jingle:apps:rtp:video"; + features << QLatin1String("urn:xmpp:jingle:transports:ice-udp:1"); + features << QLatin1String("urn:xmpp:jingle:transports:ice:0"); + features << QLatin1String("urn:xmpp:jingle:apps:rtp:1"); + features << QLatin1String("urn:xmpp:jingle:apps:rtp:audio"); + features << QLatin1String("urn:xmpp:jingle:apps:rtp:video"); } - features << "jabber:x:conference"; // allow direct invites + features << QLatin1String("jabber:x:conference"); // allow direct invites + +#ifdef WEBKIT + auto gcTheme = psi()->themeManager()->provider("groupchatview")->current(); + auto chatTheme = psi()->themeManager()->provider("chatview")->current(); + if (gcTheme && chatTheme) { + auto themeFeatures = gcTheme.features() + chatTheme.features(); + if (themeFeatures.contains(QStringLiteral("reactions"))) { + features << QLatin1String("urn:xmpp:reactions:0"); + } + if (themeFeatures.contains(QStringLiteral("message-retract"))) { + features << QLatin1String("urn:xmpp:message-retract:1"); + } + } +#endif // TODO reset hash d->client->setFeatures(Features(features)); @@ -1597,7 +1628,7 @@ void PsiAccount::login() const bool tlsSupported = QCA::isSupported("tls"); const bool keyStoreManagerAvailable = !QCA::KeyStoreManager().isBusy(); - if (d->acc.ssl == UserAccount::SSL_Yes || d->acc.ssl == UserAccount::SSL_Legacy) { + if (d->acc.ssl == UserAccount::TLS_Yes || d->acc.ssl == UserAccount::Direct_TLS) { if (!tlsSupported) { QString title; if (d->psi->contactList()->enabledAccounts().count() > 1) { @@ -1630,24 +1661,6 @@ void PsiAccount::login() #endif updateClientVersionInfo(); - if (d->acc.legacy_ssl_probe) { - // disable the feature and display a notice - d->acc.legacy_ssl_probe = false; - emit updatedAccount(); - - QString title; - if (d->psi->contactList()->enabledAccounts().count() > 1) { - title = QString("%1: ").arg(name()); - } - title += tr("Feature Removed"); - QString message = tr("This account was configured to use the \"Probe legacy SSL port\" feature, but this " - "feature is no longer supported. Unless your XMPP server is very outdated, this change " - "should not affect you. If you have trouble connecting, please review your account " - "settings for correctness or contact your XMPP server administrator."); - - psi()->alertManager()->raiseMessageBox(AlertManager::ConnectionError, QMessageBox::Information, title, message); - } - d->jid = d->nextJid; v_isActive = true; @@ -1674,7 +1687,7 @@ void PsiAccount::login() // stream d->conn = new AdvancedConnector; - if (d->acc.ssl != UserAccount::SSL_No && tlsSupported && keyStoreManagerAvailable) { + if (d->acc.ssl != UserAccount::TLS_No && tlsSupported && keyStoreManagerAvailable) { d->tls = new QCA::TLS; d->tls->setTrustedCertificates(CertificateHelpers::allCertificates(ApplicationInfo::getCertificateStoreDirs())); d->tlsHandler = new QCATLSHandler(d->tls); @@ -1682,10 +1695,10 @@ void PsiAccount::login() connect(d->tlsHandler, &QCATLSHandler::tlsHandshaken, this, &PsiAccount::tls_handshaken); } d->conn->setProxy(p); - + d->conn->setOptTlsSrv(d->acc.ssl == UserAccount::TLS_Auto || d->acc.ssl == UserAccount::TLS_Yes); if (useHost) { d->conn->setOptHostPort(host, quint16(port)); - d->conn->setOptSSL(d->acc.ssl == UserAccount::SSL_Legacy); + d->conn->setOptSSL(d->acc.ssl == UserAccount::Direct_TLS); } d->stream = new ClientStream(d->conn, d->tlsHandler); @@ -1837,7 +1850,7 @@ void PsiAccount::cs_needAuthParams(bool user, bool pass, bool realm) if (pass) { d->stream->setPassword(d->acc.pass); - if (d->acc.storeSaltedHashedPassword) + if (d->acc.storeSaltedHashedPassword) // TODO can we keep this in keychain too? d->stream->setSCRAMStoredSaltedHash(d->acc.scramSaltedHashPassword); } if (realm) { @@ -1864,38 +1877,52 @@ void PsiAccount::cs_needAuthParams(bool user, bool pass, bool realm) this); // qApp is kind of wrong, but "this" is not QObject pwJob->setKey(d->jid.bare()); pwJob->setAutoDelete(true); - QObject::connect(pwJob, &QKeychain::ReadPasswordJob::finished, this, [=](QKeychain::Job *job) { - if (job->error() == QKeychain::NoError && d->stream) { - d->stream->setPassword(static_cast(job)->textData()); - if (d->acc.opt_pass) { - // keychain read success. erase from xml if any - d->acc.opt_pass = false; - emit updatedAccount(); + QObject::connect(pwJob, &QKeychain::ReadPasswordJob::finished, this, [this](QKeychain::Job *job) { + if (job->error() == QKeychain::AccessDeniedByUser) { + if (d->stream) { + d->stream->abortAuth(); } - } else if (job->error() == QKeychain::EntryNotFound && d->acc.opt_pass) { - // the password was already set to stream - savePassword(); - } else { - if (job->error() > QKeychain::AccessDeniedByUser) { // unrecoverable + return; + } + if (job->error() == QKeychain::NoError) { + // REVIEW can we protect from mem dumps? + d->acc.pass = static_cast(job)->textData(); + } + if (d->acc.pass.isEmpty()) { + if (job->error() != QKeychain::EntryNotFound && job->error() != QKeychain::NoError) { PsiOptions::instance()->setOption("options.keychain.enabled", false); - } - bool accepted = false; - if (d->stream) { + psi()->popupManager()->doPopup( + this, jid(), IconsetFactory::iconPtr("psi/cancel"), tr("Keychain failure"), QPixmap(), + nullptr, + tr("Psi switched to the internal password storage because system " + "password manager is unavailable (%s).") + .arg(job->errorString()), + false, PopupManager::AlertNone); qWarning("KeyChain error=%d: %s", job->error(), qPrintable(job->errorString())); - accepted = passwordPrompt(); - } else { - // tcp socket reports failure RemoteHostClosedError. - // baically we have to reestablish connection if it's lost here. - qWarning("fixme: stream was unexpectedly cleaned up"); + } + if (!passwordPrompt()) { // this will also save password on success + if (d->stream) { + d->stream->abortAuth(); + } return; } - if (accepted) { - d->stream->setPassword(d->acc.pass); - } else { - d->stream->abortAuth(); + } else if (job->error() == QKeychain::EntryNotFound) { + // the password was already set to stream (see above) + savePassword(); + } + // we have non-empty password here and should continue with login + if (d->stream) { + d->stream->setPassword(d->acc.pass); + if (job->error() == QKeychain::NoError && d->acc.opt_pass) { + // keychain read success. erase from xml if any + d->acc.opt_pass = false; + emit updatedAccount(); } + d->stream->continueAfterParams(); + } else { + login(); // eventually we will come here again and rerequest the password. likely without + // interruption for the master password } - d->stream->continueAfterParams(); }); pwJob->start(); } else { @@ -1992,7 +2019,7 @@ void PsiAccount::cs_warning(int w) if (w == ClientStream::WarnSMReconnection) return; - bool showNoTlsWarning = w == ClientStream::WarnNoTLS && d->acc.ssl == UserAccount::SSL_Yes; + bool showNoTlsWarning = w == ClientStream::WarnNoTLS && d->acc.ssl == UserAccount::TLS_Yes; bool doCleanupStream = !d->stream || showNoTlsWarning; if (doCleanupStream) { @@ -2319,7 +2346,7 @@ void PsiAccount::client_rosterRequestFinished(bool success, int, const QString & // PsiOptions::instance()->load(d->client); // we need to have up-to-date photoHash for initial presence - d->vcardChanged(jid()); + d->vcardChanged(jid(), {}); setStatusDirect(d->loginStatus, d->loginWithPriority); emit rosterRequestFinished(); @@ -2327,7 +2354,18 @@ void PsiAccount::client_rosterRequestFinished(bool success, int, const QString & void PsiAccount::resolveContactName(const Jid &j) { - VCardFactory::instance()->getVCard(j, client()->rootTask(), this, [this]() { jt_resolveContactName(); }); + auto req = VCardFactory::instance()->getVCard(this, j, {}); + connect(req, &VCardRequest::finished, this, [this, req]() { + if (req->success() && req->vcard()) { + QString nick = req->vcard().nickName().preferred().data.value(0); + QString full = req->vcard().fullName().preferred(); + if (!nick.isEmpty()) { + actionRename(req->jid(), nick); + } else if (!full.isEmpty()) { + actionRename(req->jid(), full); + } + } + }); } void PsiAccount::setClientVersionInfoMap(const QVariantMap &info) @@ -2336,20 +2374,6 @@ void PsiAccount::setClientVersionInfoMap(const QVariantMap &info) updateClientVersionInfo(); } -void PsiAccount::jt_resolveContactName() -{ - JT_VCard *j = static_cast(sender()); - if (j->success()) { - QString nick = j->vcard().nickName(); - QString full = j->vcard().fullName(); - if (!nick.isEmpty()) { - actionRename(j->jid(), nick); - } else if (!full.isEmpty()) { - actionRename(j->jid(), full); - } - } -} - void PsiAccount::serverFeaturesChanged() { setPEPAvailable(d->client->serverInfoManager()->hasPEP()); @@ -2362,20 +2386,21 @@ void PsiAccount::serverFeaturesChanged() d->client->carbonsManager()->setEnabled(true); } - if (d->client->serverInfoManager()->features().hasVCard() && !d->vcardChecked) { + if (d->client->serverInfoManager()->serverFeatures().hasVCard() && !d->vcardChecked) { // Get the vcard - const VCard vcard = VCardFactory::instance()->vcard(d->jid); + const auto vcard = VCardFactory::instance()->vcard(d->jid); +#if 0 // feels not needed with pubsub vcards. commented out on 2024-06-13. TODO remove options? if (PsiOptions::instance()->getOption("options.vcard.query-own-vcard-on-login").toBool() || vcard.isEmpty() || (vcard.nickName().isEmpty() && vcard.fullName().isEmpty())) { - VCardFactory::instance()->getVCard(d->jid, d->client->rootTask(), this, [this]() { + auto req = VCardFactory::instance()->getVCard(this, d->jid); + connect(req, &VCardRequest::finished, this, [this, req]() { if (!isConnected() || !isActive()) return; - QString nick = d->jid.node(); - JT_VCard *j = static_cast(sender()); - VCard vcard = j->vcard(); - bool changeOwn; - if (j->success()) { + QString nick = d->jid.node(); + auto vcard = req->vcard(); + bool changeOwn; + if (vcard) { if (!vcard.nickName().isEmpty()) { d->nickFromVCard = true; nick = vcard.nickName(); @@ -2384,20 +2409,23 @@ void PsiAccount::serverFeaturesChanged() nick = vcard.fullName(); } if (!vcard.photo().isEmpty()) { - d->vcardPhotoUpdate(j->vcard().photo()); + d->vcardPhotoUpdate(vcard.photo()); } setNick(nick); changeOwn = vcard.isEmpty(); } else { - changeOwn = (j->statusCode() == Task::ErrDisc + 1 || j->statusCode() == 404); + // if vcard is null because of not found, likely unpublished (still good) + changeOwn = req->success(); } if (changeOwn && PsiOptions::instance()->getOption("options.vcard.query-own-vcard-on-login").toBool()) { changeVCard(); } }); - } else { + } else +#endif + { d->nickFromVCard = true; // if we get here, one of these fields is non-empty if (!vcard.nickName().isEmpty()) { @@ -2818,14 +2846,16 @@ void PsiAccount::processIncomingMessage(const Message &_m) bool selfMessage = _m.forwarded().type() == Forwarding::ForwardedCarbonsSent; Message dm = _m.displayMessage(); // skip empty messages, but not if the message contains a data form - if (dm.body().isEmpty() && dm.urlList().isEmpty() && dm.invite().isEmpty() && !dm.containsEvents() - && dm.chatState() == StateNone && dm.subject().isNull() && dm.rosterExchangeItems().isEmpty() - && dm.mucInvites().isEmpty() && dm.getForm().fields().empty() && dm.messageReceipt() == ReceiptNone - && dm.getMUCStatuses().isEmpty()) + if (dm.type() != Message::Type::Error && dm.body().isEmpty() && dm.urlList().isEmpty() && dm.invite().isEmpty() + && !dm.containsEvents() && dm.chatState() == StateNone && dm.subject().isNull() + && dm.rosterExchangeItems().isEmpty() && dm.mucInvites().isEmpty() && dm.getForm().fields().empty() + && dm.messageReceipt() == ReceiptNone && dm.getMUCStatuses().isEmpty() && dm.reactions().targetId.isEmpty() + && dm.retraction().isEmpty()) return; // skip headlines? - if (dm.type() == "headline" && PsiOptions::instance()->getOption("options.messages.ignore-headlines").toBool()) + if (dm.type() == Message::Type::Headline + && PsiOptions::instance()->getOption("options.messages.ignore-headlines").toBool()) return; if (dm.getForm().registrarType() == "urn:xmpp:captcha") { @@ -2858,7 +2888,7 @@ void PsiAccount::processIncomingMessage(const Message &_m) } #ifdef GROUPCHAT - if (dm.type() == "groupchat") { + if (dm.type() == Message::Type::Groupchat) { MessageEvent::Ptr me(new MessageEvent(_m, this)); me->setOriginLocal(false); handleEvent(me, IncomingStanza); @@ -2871,7 +2901,7 @@ void PsiAccount::processIncomingMessage(const Message &_m) if (!selfMessage) { ul = findRelevant(dj); if (!ul.isEmpty()) { - if (dm.type() == QLatin1String("chat")) + if (dm.type() == Message::Type::Chat) ul.first()->setLastMessageType(1); else ul.first()->setLastMessageType(0); @@ -2909,7 +2939,7 @@ void PsiAccount::processIncomingMessage(const Message &_m) if(!c) c = findChatDialog(m.from().full());*/ - if (dm2.type() == "error") { + if (dm2.type() == Message::Type::Error) { Stanza::Error err = dm2.error(); QPair desc = err.description(); QString msg = desc.first + ".\n" + desc.second; @@ -2931,13 +2961,13 @@ void PsiAccount::processIncomingMessage(const Message &_m) } // change the type? - if (dm2.type() != "headline" && dm2.invite().isEmpty() && dm2.mucInvites().isEmpty()) { + if (dm2.type() != Message::Type::Headline && dm2.invite().isEmpty() && dm2.mucInvites().isEmpty()) { const QString type = PsiOptions::instance()->getOption("options.messages.force-incoming-message-type").toString(); if (type == "message") - dm2.setType(""); + dm2.setType(Message::Type::Normal); else if (type == "chat") - dm2.setType("chat"); + dm2.setType(Message::Type::Chat); else if (type == "current-open") { c = nullptr; const auto &dlgs = findChatDialogs(dj, false); @@ -2949,9 +2979,9 @@ void PsiAccount::processIncomingMessage(const Message &_m) } } if (c != nullptr && !c->isHidden()) - dm2.setType("chat"); + dm2.setType(Message::Type::Chat); else - dm2.setType(""); + dm2.setType(Message::Type::Normal); } } @@ -3116,9 +3146,8 @@ void PsiAccount::setStatus(const Status &_s, bool withPriority, bool isManualSta // Block all transports' contacts' status change popups from popping { for (const auto &i : std::as_const(d->acc.roster)) { - if (i.jid() - .node() - .isEmpty() /*&& i.jid().resource() == "registered"*/) // it is very likely then, that it's transport + if (i.jid().node().isEmpty() /*&& i.jid().resource() == "registered"*/) // it is very likely then, + // that it's transport new BlockTransportPopup(d->blockTransportPopupList, i.jid()); // FIXME this code looks like a source for memory leak } @@ -3148,6 +3177,7 @@ void PsiAccount::setStatus(const Status &_s, bool withPriority, bool isManualSta else { if (!isConnected()) { cleanupStream(); + v_isActive = false; login(); } if (rosterDone) { @@ -3155,8 +3185,8 @@ void PsiAccount::setStatus(const Status &_s, bool withPriority, bool isManualSta } if (s.isInvisible()) { //&&Pass invis to transports KEVIN - // this is a nasty hack to let the transports know we're invisible, since they get an offline packet - // when we go invisible + // this is a nasty hack to let the transports know we're invisible, since they get an offline + // packet when we go invisible for (UserListItem *u : std::as_const(d->userList)) { if (u->isTransport()) { JT_Presence *j = new JT_Presence(d->client->rootTask()); @@ -3275,8 +3305,9 @@ void PsiAccount::setStatusActual(const Status &_s) } // Add vcard photo hash if available - if (!d->photoHash.isEmpty()) { - s.setPhotoHash(d->photoHash); + if (d->photoHash.has_value()) { + s.setPhotoHash(*d->photoHash); + d->photoHash = {}; } // Set the status @@ -3463,6 +3494,7 @@ ChatDlg *PsiAccount::findChatDialogEx(const Jid &jid, bool ignoreResource) const ChatDlg *cm1 = nullptr; ChatDlg *cm2 = nullptr; const auto &dialogs = findChatDialogs(jid, false); + ignoreResource = ignoreResource || !d->groupchats.contains(jid.bare()); for (ChatDlg *cl : dialogs) { if (cl->autoSelectContact() || ignoreResource) return cl; @@ -3647,7 +3679,7 @@ void PsiAccount::actionSendStatus(const Jid &jid) StatusSetDlg *w = new StatusSetDlg(psi(), makeLastStatus(status().type()), lastPriorityNotEmpty()); w->setJid(jid); connect(w, qOverload(&StatusSetDlg::setJid), this, - [this, w](const Jid &j, const Status &s) { + [this](const Jid &j, const Status &s) { JT_Presence *p = new JT_Presence(client()->rootTask()); p->pres(j, s); p->go(true); @@ -3864,6 +3896,28 @@ void PsiAccount::itemPublished(const Jid &j, const QString &n, const PubSubItem u->setGeoLocation(geoloc); cpUpdate(*u); } + } else if (n == QLatin1String("urn:ietf:params:xml:ns:vcard-4.0")) { + // we are interested only in our own at the moment + if (j.compare(d->jid, false)) { + VCard4::VCard vcard(item.payload()); + QString nick = d->jid.node(); + bool changeOwn; + if (vcard) { + if (!vcard.nickName().isEmpty()) { + d->nickFromVCard = true; + nick = vcard.nickName(); + } else if (!vcard.fullName().isEmpty()) { + d->nickFromVCard = true; + nick = vcard.fullName(); + } + if (!vcard.photo().isEmpty()) { + d->vcardPhotoUpdate(vcard.photo()); + } + setNick(nick); + + changeOwn = vcard.isEmpty(); + } + } } } @@ -4474,12 +4528,16 @@ void PsiAccount::actionAgentSetStatus(const Jid &j, const Status &s) void PsiAccount::actionInfo(const Jid &_j, bool showStatusInfo) { bool isMucMember = false; + bool isSelf = _j.compare(d->jid, false); + bool isMuc = false; Jid j; - if (findGCContact(_j)) { + if (!isSelf && findGCContact(_j)) { isMucMember = true; j = _j; } else { - j = _j.bare(); + auto js = _j.bare(); + j = js; + isMuc = d->groupchats.contains(js); } InfoDlg *w = findDialog(j); @@ -4488,17 +4546,17 @@ void PsiAccount::actionInfo(const Jid &_j, bool showStatusInfo) w->infoWidget()->setStatusVisibility(showStatusInfo); bringToFront(w); } else { - VCard vcard; + VCard4::VCard vcard; if (isMucMember) { vcard = VCardFactory::instance()->mucVcard(j); } else { vcard = VCardFactory::instance()->vcard(j); } - - w = new InfoDlg(j.compare(d->jid) ? InfoWidget::Self + w = new InfoDlg(isSelf ? InfoWidget::Self : isMucMember ? InfoWidget::MucContact + : isMuc ? InfoWidget::MucView : InfoWidget::Contact, - j, vcard, this, nullptr, true); + j, vcard, this, nullptr); w->infoWidget()->setStatusVisibility(showStatusInfo); w->show(); @@ -4704,10 +4762,11 @@ void PsiAccount::openUri(const QUrl &uriToOpen) // TODO: default case - be more smart!! ;-) // if (QMessageBox::question(0, tr("Hmm.."), QString(tr("So, you'd like to open %1 URI, right?\n" - // "Unfortunately, this URI only identifies an entity, but it doesn't say what action to perform (or at least - // Psi cannot understand it). " "So it's pretty much like if I said \"John\" to you - you'd immediately ask - // \"But what about John?\".\n" "So... What about %1??\n" "At worst, you may send a message to %2 to ask what - // to do (and maybe complain about this URI ;)) " "Would you like to do this + // "Unfortunately, this URI only identifies an entity, but it doesn't say what action to perform (or + // at least Psi cannot understand it). " "So it's pretty much like if I said \"John\" to you - you'd + // immediately ask + // \"But what about John?\".\n" "So... What about %1??\n" "At worst, you may send a message to %2 to + // ask what to do (and maybe complain about this URI ;)) " "Would you like to do this // now?")).arg(uri).arg(entity.full()), QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes) == // QMessageBox::Yes) actionSendMessage(entity); @@ -4723,10 +4782,10 @@ void PsiAccount::dj_sendMessage(Message &m, bool log) if (u) { switch (u->lastMessageType()) { case 0: - m.setType(""); + m.setType(Message::Type::Normal); break; case 1: - m.setType("chat"); + m.setType(Message::Type::Chat); break; } } @@ -4737,7 +4796,7 @@ void PsiAccount::dj_sendMessage(Message &m, bool log) QString body = m.body(); QString subject = m.subject(); - if (PluginManager::instance()->processOutgoingMessage(this, m.to().full(), body, m.type(), subject)) + if (PluginManager::instance()->processOutgoingMessage(this, m.to().full(), body, m.typeStr(), subject)) return; if (body != m.body()) { m.setBody(body); @@ -4765,7 +4824,7 @@ void PsiAccount::dj_sendMessage(Message &m, bool log) // don't log groupchat or encrypted messages if (log) { - if (m.type() != "groupchat" && m.xencrypted().isEmpty()) { + if (m.type() != Message::Type::Groupchat && m.xencrypted().isEmpty()) { int type = findGCContact(m.to()) ? EDB::GroupChatContact : EDB::Contact; MessageEvent::Ptr me(new MessageEvent(m, this)); me->setOriginLocal(true); @@ -4782,11 +4841,11 @@ void PsiAccount::dj_sendMessage(Message &m, bool log) } // don't sound when sending groupchat messages or message events - if (m.type() != "groupchat" && !m.body().isEmpty() && log) + if (m.type() != Message::Type::Groupchat && !m.body().isEmpty() && log) playSound(eSend); // auto close an open messagebox (if non-chat) - if (m.type() != "chat" && !m.body().isEmpty()) { + if (m.type() != Message::Type::Chat && !m.body().isEmpty()) { UserListItem *u = findFirstRelevant(m.to()); if (u) { EventDlg *e = findDialog(u->jid()); @@ -5060,7 +5119,7 @@ void PsiAccount::handleEvent(const PsiEvent::Ptr &e, ActivationType activationTy if (!me || !me->message().displayMessage().body().isEmpty()) { bool isMuc = false; #ifdef GROUPCHAT - if (me && me->message().displayMessage().type() == QLatin1String("groupchat")) + if (me && me->message().displayMessage().type() == Message::Type::Groupchat) isMuc = true; #endif if (!isMuc) { @@ -5103,7 +5162,8 @@ void PsiAccount::handleEvent(const PsiEvent::Ptr &e, ActivationType activationTy } // Pass message events to chat window - if ((dm.containsEvents() || dm.chatState() != StateNone) && dm.body().isEmpty() && dm.type() != "groupchat") { + if ((dm.containsEvents() || dm.chatState() != StateNone) && dm.body().isEmpty() + && dm.type() != Message::Type::Groupchat) { if (selfMessage) { return; // ignore own composing for carbon. TODO should we? } @@ -5124,13 +5184,14 @@ void PsiAccount::handleEvent(const PsiEvent::Ptr &e, ActivationType activationTy } // pass chat messages directly to a chat window if possible (and deal with sound) - else if (dm.type() == "chat") { + else if (dm.type() == Message::Type::Chat) { Jid chatJid = m.displayJid(); if (selfMessage) e->setOriginLocal(true); // throw away carbons sent for MUC private messages - // server sends them to all resources so we have to explicitly skip them or we'll have a lot of duplicates + // server sends them to all resources so we have to explicitly skip them or we'll have a lot of + // duplicates if (m.forwarded().type() == Forwarding::ForwardedCarbonsReceived && m.hasMUCUser()) { return; } @@ -5162,13 +5223,13 @@ void PsiAccount::handleEvent(const PsiEvent::Ptr &e, ActivationType activationTy popupType = PopupManager::AlertChat; } } // /chat - else if (dm.type() == "headline") { + else if (dm.type() == Message::Type::Headline) { soundType = eHeadline; doPopup = true; popupType = PopupManager::AlertHeadline; } // /headline #ifdef GROUPCHAT - else if (dm.type() == "groupchat") { + else if (dm.type() == Message::Type::Groupchat) { putToQueue = false; bool allowMucEvents = o->getOption("options.ui.muc.allow-highlight-events").toBool(); if (activationType != FromXml) { @@ -5182,7 +5243,7 @@ void PsiAccount::handleEvent(const PsiEvent::Ptr &e, ActivationType activationTy putToQueue = true; } // /groupchat #endif - else if (dm.type().isEmpty()) { + else if (dm.type() == Message::Type::Normal) { soundType = eMessage; doPopup = true; popupType = PopupManager::AlertMessage; @@ -5196,7 +5257,7 @@ void PsiAccount::handleEvent(const PsiEvent::Ptr &e, ActivationType activationTy soundType = eNone; putToQueue = false; } - if (dm.type() == "error") { + if (dm.type() == Message::Type::Error) { // FIXME: handle message errors // msg.text = QString(tr("[Error Message]
%1").arg(plain2rich(msg.text))); } @@ -5387,7 +5448,7 @@ void PsiAccount::queueEvent(const PsiEvent::Ptr &e, ActivationType activationTyp nick = ae->nick(); } else if (e->type() == PsiEvent::Message) { MessageEvent::Ptr me = e.staticCast(); - if (me->message().displayMessage().type() != QLatin1String("error")) + if (me->message().displayMessage().type() != Message::Type::Error) nick = me->nick(); } @@ -5417,9 +5478,9 @@ void PsiAccount::queueEvent(const PsiEvent::Ptr &e, ActivationType activationTyp if (e->type() == PsiEvent::Message) { MessageEvent::Ptr me = e.staticCast(); const Message dm = me->message().displayMessage(); - if (dm.type() == "chat") + if (dm.type() == Message::Type::Chat) doPopup = PsiOptions::instance()->getOption("options.ui.chat.auto-popup").toBool(); - else if (dm.type() == "headline") + else if (dm.type() == Message::Type::Headline) doPopup = PsiOptions::instance()->getOption("options.ui.message.auto-popup-headlines").toBool(); else doPopup = PsiOptions::instance()->getOption("options.ui.message.auto-popup").toBool(); @@ -5568,11 +5629,11 @@ void PsiAccount::processReadNext(const UserListItem &u) #endif if (e->type() == PsiEvent::Message) { MessageEvent::Ptr me = e.staticCast(); - const Message dm = me->message().displayMessage(); - if (dm.type() == QLatin1String("chat") && dm.getForm().fields().empty()) + const Message &dm = me->message().displayMessage(); + if (dm.type() == Message::Type::Chat && dm.getForm().fields().empty()) isChat = true; #ifdef GROUPCHAT - else if (dm.type() == QLatin1String("groupchat")) + else if (dm.type() == Message::Type::Groupchat) isMuc = true; #endif } @@ -5767,7 +5828,8 @@ void PsiAccount::shareImage(const Jid &target, const QImage &image, const QStrin Q_UNUSED(target) Q_UNUSED(description) - // this method is intended to use xep-0385. But let's do something simple first. xep-0385 will be implemented later + // this method is intended to use xep-0385. But let's do something simple first. xep-0385 will be + // implemented later auto buffer = new QBuffer(); buffer->open(QIODevice::ReadWrite); image.save(buffer, "PNG"); @@ -6124,12 +6186,13 @@ void PsiAccount::pgp_decryptFinished() if (loggedIn() && m.forwarded().type() != Forwarding::ForwardedCarbonsSent) { Message m; m.setTo(dm.displayJid()); - m.setType("error"); + m.setType(Message::Type::Error); if (!dm.id().isEmpty()) m.setId(dm.id()); m.setBody(dm.body()); - m.setError(Stanza::Error(Stanza::Error::Modify, Stanza::Error::NotAcceptable, "Unable to decrypt")); + m.setError(Stanza::Error(Stanza::Error::ErrorType::Modify, Stanza::Error::ErrorCond::NotAcceptable, + "Unable to decrypt")); d->client->sendMessage(m); } } diff --git a/src/psiaccount.h b/src/psiaccount.h index 6732b8076b..c206086f06 100644 --- a/src/psiaccount.h +++ b/src/psiaccount.h @@ -441,7 +441,6 @@ private slots: void cs_warning(int); void cs_error(int); void client_rosterRequestFinished(bool, int, const QString &); - void jt_resolveContactName(); void client_rosterItemAdded(const RosterItem &); void client_rosterItemUpdated(const RosterItem &); void client_rosterItemRemoved(const RosterItem &); diff --git a/src/psiactionlist.cpp b/src/psiactionlist.cpp index ac611b786b..e6d7ff5a07 100644 --- a/src/psiactionlist.cpp +++ b/src/psiactionlist.cpp @@ -277,8 +277,7 @@ void PsiActionList::Private::createMainWin() = new IconAction(tr("Change Profile"), "psi/profile", tr("&Change Profile"), 0, this); IconAction *actPlaySounds - = new IconAction(tr("Play Sounds"), "psi/playSounds", tr("Play &Sounds"), 0, this, nullptr, true); - actPlaySounds->setToolTip(tr("Toggles whether sound should be played or not")); + = new IconAction(QString(), "psi/playSounds", tr("Play &Sounds"), 0, this, nullptr, true); IconAction *actQuit = new IconAction(tr("Quit"), "psi/quit", tr("&Quit"), 0, this); actQuit->setMenuRole(QAction::QuitRole); diff --git a/src/psichatdlg.cpp b/src/psichatdlg.cpp index 5d65103a21..9fd6cb560b 100644 --- a/src/psichatdlg.cpp +++ b/src/psichatdlg.cpp @@ -317,6 +317,7 @@ void PsiChatDlg::initUi() addAction(act_mini_cmd_); connect(ui_.log, &ChatView::quote, ui_.mle->chatEdit(), &ChatEdit::insertAsQuote); + connect(ui_.log, &ChatView::editMessageRequested, ui_.mle->chatEdit(), &ChatEdit::startCorrection); act_pastesend_ = new IconAction(tr("Paste and Send"), "psi/action_paste_and_send", tr("Paste and Send"), 0, this); connect(act_pastesend_, SIGNAL(triggered()), SLOT(doPasteAndSend())); @@ -374,7 +375,6 @@ void PsiChatDlg::setLooks() int s = PsiIconset::instance()->system().iconSize(); ui_.lb_status->setFixedSize(s, s); - ui_.lb_client->setFixedSize(s, s); ui_.tb_pgp->hide(); if (smallChat_) { @@ -384,9 +384,7 @@ void PsiChatDlg::setLooks() ui_.tb_emoticons->hide(); ui_.toolbar->hide(); ui_.tb_voice->hide(); - ui_.lb_client->hide(); } else { - ui_.lb_client->show(); ui_.lb_status->show(); ui_.le_jid->show(); if (PsiOptions::instance()->getOption("options.ui.contactlist.toolbars.m0.visible").toBool()) { @@ -824,9 +822,8 @@ void PsiChatDlg::contactUpdated(UserListItem *u, int status, const QString &stat if (!client.isEmpty()) { const QPixmap &pix = IconsetFactory::iconPixmap("clients/" + client, int(fontInfo().pixelSize() * EqTextIconK + .5)); - ui_.lb_client->setPixmap(pix); } - ui_.lb_client->setToolTip(r.versionString()); + ui_.le_jid->setToolTip(r.versionString()); } } } @@ -973,16 +970,14 @@ void PsiChatDlg::actPgpToggled(bool b) void PsiChatDlg::doClearButton() { if (PsiOptions::instance()->getOption("options.ui.chat.warn-before-clear").toBool()) { - switch (QMessageBox::warning( + auto result = QMessageBox::warning( this, tr("Warning"), tr("Are you sure you want to clear the chat window?\n(note: does not affect saved history)"), - QMessageBox::Yes | QMessageBox::YesAll | QMessageBox::No)) { - case QMessageBox::No: - break; - case QMessageBox::YesAll: + QMessageBox::Yes | QMessageBox::YesToAll | QMessageBox::No); + if (result == QMessageBox::YesToAll) { PsiOptions::instance()->setOption("options.ui.chat.warn-before-clear", false); - // fall-through - case QMessageBox::Yes: + } + if (result == QMessageBox::YesToAll || result == QMessageBox::Yes) { doClear(); } } else { diff --git a/src/psicli.h b/src/psicli.h index 6a6962cd68..21783a0949 100644 --- a/src/psicli.h +++ b/src/psicli.h @@ -45,6 +45,13 @@ class PsiCli : public SimpleCli { defineParam("status-message", tr("MSG", "translate in UPPER_CASE with no spaces"), tr("Set status message. Must be used together with --status.", "do not translate --status")); + defineSwitch( + "swrender", + tr("Use software widgets rendering. In some cases default hardware rendering may lead to graphical " + "glitches and crashes. This option may help.")); + + defineSwitch("quit", tr("Quit the application")); + defineSwitch("help", tr("Show this help message and exit.")); defineAlias("h", "help"); defineAlias("?", "help"); diff --git a/src/psicon.cpp b/src/psicon.cpp index 2486e94894..42ff17e756 100644 --- a/src/psicon.cpp +++ b/src/psicon.cpp @@ -67,6 +67,7 @@ #include "psicontactlist.h" #include "psievent.h" #include "psiiconset.h" +#include "psirosterwidget.h" #ifdef PSIMNG #include "psimng.h" #endif @@ -99,13 +100,12 @@ #endif #ifdef PSI_PLUGINS #include "filesharingmanager.h" -#include "filesharingnamproxy.h" +#include "filesharingproxy.h" #include "pluginmanager.h" #endif #ifdef WEBKIT #include "avatars.h" #include "chatviewthemeprovider.h" -#include "webview.h" #endif #ifdef HAVE_SPARKLE #include "AutoUpdater/SparkleAutoUpdater.h" @@ -519,6 +519,8 @@ bool PsiCon::init() d->iconSelect->setEmojiSortingEnabled(true); connect(PsiIconset::instance(), SIGNAL(emoticonsChanged()), d, SLOT(updateIconSelect())); d->updateIconSelect(); + d->iconSelect->setRecent( + PsiOptions::instance()->getOption("options.ui.emoticons.recent", QStringList()).toStringList()); const QString css = options->getOption("options.ui.chat.css").toString(); if (!css.isEmpty()) @@ -538,7 +540,8 @@ bool PsiCon::init() std::tie(acc, id) = d->uriToShareSource(req->url().path()); if (!acc) return false; - return d->fileSharingManager->downloadHttpRequest(acc, id, req, res); + FileSharingProxy::proxify(acc, id, req, res); + return true; }); #endif d->nam->route("/psi/account/", [this](const QNetworkRequest &req) -> QNetworkReply * { @@ -547,7 +550,7 @@ bool PsiCon::init() std::tie(acc, id) = d->uriToShareSource(req.url().path()); if (id.isNull() || !acc) return nullptr; - return new FileSharingNAMReply(acc, id, req); + return FileSharingProxy::proxify(acc, id, req); }); d->themeManager = new PsiThemeManager(this); @@ -556,11 +559,22 @@ bool PsiCon::init() d->themeManager->registerProvider(new GroupChatViewThemeProvider(this), true); #endif - if (!d->themeManager->loadAll()) { + const auto reportThemeError = [this]() { QMessageBox::critical(nullptr, tr("Error"), - tr("Unable to load theme! Please make sure Psi is properly installed.")); + tr("Unable to load \"%1\" theme! Please make sure Psi is properly installed.") + .arg(d->themeManager->failedId())); + }; + auto themeLoadResult = d->themeManager->loadAll(); + if (themeLoadResult == PsiThemeProvider::LoadFailure) { + reportThemeError(); result = false; } + if (themeLoadResult == PsiThemeProvider::LoadInProgress) { + connect(d->themeManager, &PsiThemeManager::currentLoadFailed, this, [this, reportThemeError]() { + reportThemeError(); + closeProgram(); + }); + } if (!d->actionList) d->actionList = new PsiActionList(this); @@ -716,6 +730,7 @@ bool PsiCon::init() SLOT(setStatusFromCommandline(const QString &, const QString &))); connect(ActiveProfiles::instance(), SIGNAL(openUriRequested(const QString &)), SLOT(openUri(const QString &))); connect(ActiveProfiles::instance(), SIGNAL(raiseRequested()), SLOT(raiseMainwin())); + connect(ActiveProfiles::instance(), SIGNAL(quitRequested()), SLOT(closeProgram())); DesktopUtil::setUrlHandler("xmpp", this, "openUri"); DesktopUtil::setUrlHandler("x-psi-atstyle", this, "openAtStyleUri"); @@ -742,6 +757,7 @@ void PsiCon::updateStatusPresets() { emit statusPresetsChanged(); } void PsiCon::deinit() { + PsiOptions::instance()->setOption("options.ui.emoticons.recent", d->iconSelect->recent()); // this deletes all dialogs except for mainwin deleteAllDialogs(); @@ -851,9 +867,13 @@ QStringList PsiCon::xmppFatures() const #ifdef GROUPCHAT << "http://jabber.org/protocol/muc" #endif - << "http://jabber.org/protocol/mood" << "http://jabber.org/protocol/activity" - << "http://jabber.org/protocol/tune" << "http://jabber.org/protocol/geoloc" - << "urn:xmpp:avatar:data" << "urn:xmpp:avatar:metadata"; + << "http://jabber.org/protocol/mood" + << "http://jabber.org/protocol/activity" + << "http://jabber.org/protocol/tune" + << "http://jabber.org/protocol/geoloc" + << "urn:ietf:params:xml:ns:vcard-4.0" + << "urn:xmpp:avatar:data" + << "urn:xmpp:avatar:metadata"; static QList fmap = QList() << OptFeatureMap("options.service-discovery.last-activity", QStringList() << "jabber:iq:last") @@ -863,7 +883,8 @@ QStringList PsiCon::xmppFatures() const << "http://jabber.org/protocol/activity+notify" << "http://jabber.org/protocol/tune+notify" << "http://jabber.org/protocol/geoloc+notify" - << "urn:xmpp:avatar:metadata+notify") + << "urn:xmpp:avatar:metadata+notify" + << "urn:xmpp:contacts+notify") << OptFeatureMap("options.messages.send-composing-events", QStringList() << "http://jabber.org/protocol/chatstates") << OptFeatureMap("options.ui.notifications.send-receipts", QStringList() << "urn:xmpp:receipts"); @@ -1026,11 +1047,11 @@ AccountsComboBox *PsiCon::accountsComboBox(QWidget *parent, bool online_only) } PsiAccount *PsiCon::createAccount(const QString &name, const Jid &j, const QString &pass, bool opt_host, - const QString &host, int port, bool legacy_ssl_probe, UserAccount::SSLFlag ssl, - QString proxy, const QString &tlsOverrideDomain, const QByteArray &tlsOverrideCert) + const QString &host, int port, UserAccount::SSLFlag ssl, QString proxy, + const QString &tlsOverrideDomain, const QByteArray &tlsOverrideCert) { - return d->contactList->createAccount(name, j, pass, opt_host, host, port, legacy_ssl_probe, ssl, proxy, - tlsOverrideDomain, tlsOverrideCert); + return d->contactList->createAccount(name, j, pass, opt_host, host, port, ssl, proxy, tlsOverrideDomain, + tlsOverrideCert); } PsiAccount *PsiCon::createAccount(const UserAccount &_acc) @@ -1776,13 +1797,13 @@ void PsiCon::processEvent(const PsiEvent::Ptr &e, ActivationType activationType) MessageEvent::Ptr me = e.staticCast(); const Message dm = me->message().displayMessage(); #ifdef GROUPCHAT - if (dm.type() == "groupchat") { + if (dm.type() == Message::Type::Groupchat) { isMuc = true; } else { #endif bool emptyForm = dm.getForm().fields().empty(); // FIXME: Refactor this, PsiAccount and PsiEvent out - if (dm.type() == "chat" && emptyForm) { + if (dm.type() == Message::Type::Chat && emptyForm) { isChat = true; sentToChatWindow = me->sentToChatWindow(); } @@ -1910,8 +1931,8 @@ void PsiCon::promptUserToCreateAccount() AccountRegDlg w(this); int n = w.exec(); if (n == QDialog::Accepted) { - contactList()->createAccount(w.jid().node(), w.jid(), w.pass(), w.useHost(), w.host(), w.port(), false, - w.ssl(), w.proxy(), w.tlsOverrideDomain(), w.tlsOverrideCert()); + contactList()->createAccount(w.jid().node(), w.jid(), w.pass(), w.useHost(), w.host(), w.port(), w.ssl(), + w.proxy(), w.tlsOverrideDomain(), w.tlsOverrideCert()); } } } @@ -1965,4 +1986,27 @@ int PsiCon::idle() const { return d->idleSettings_.secondsIdle; } ContactUpdatesManager *PsiCon::contactUpdatesManager() const { return contactUpdatesManager_; } +void PsiCon::invokeForwardMessage(const Jid &from, const QString &text) +{ + auto dlg = new QDialog(d->mainwin); + dlg->resize(200, 600); + dlg->setAttribute(Qt::WA_DeleteOnClose); + dlg->setWindowIcon(IconsetFactory::icon("psi/action_contacts_manager").icon()); + dlg->setWindowTitle(tr("Forward...")); + auto layout = new QVBoxLayout(dlg); + + auto roster = new PsiRosterWidget(d->mainwin); + roster->setContactList(d->contactList); + roster->setPickContactMode(true); + connect(roster, &PsiRosterWidget::contactPick, this, + [](PsiContact *c) { qDebug("TODO forwarding to: %s", qPrintable(c->jid().full())); }); + + auto btn = new QPushButton(tr("Forward")); + connect(btn, &QPushButton::clicked, dlg, [this, roster](bool) { qDebug("TODO: implement forwarding"); }); + + layout->addWidget(roster); + layout->addWidget(btn); + dlg->show(); +} + #include "psicon.moc" diff --git a/src/psicon.h b/src/psicon.h index cb03b3fb59..13e41c3ca4 100644 --- a/src/psicon.h +++ b/src/psicon.h @@ -96,8 +96,8 @@ class PsiCon : public QObject { ContactUpdatesManager *contactUpdatesManager() const; PsiAccount *createAccount(const QString &name, const Jid &j = "", const QString &pass = "", bool opt_host = false, - const QString &host = "", int port = 5222, bool legacy_ssl_probe = false, - UserAccount::SSLFlag ssl = UserAccount::SSL_Auto, QString proxy = "", + const QString &host = "", int port = 5222, + UserAccount::SSLFlag ssl = UserAccount::TLS_Auto, QString proxy = "", const QString &tlsOverrideDomain = "", const QByteArray &tlsOverrideCert = QByteArray()); PsiAccount *createAccount(const UserAccount &); // void createAccount(const QString &, const QString &host="", int port=5222, bool ssl=false, const QString @@ -137,6 +137,8 @@ class PsiCon : public QObject { PopupManager *popupManager() const; QStringList xmppFatures() const; + void invokeForwardMessage(const Jid &from, const QString &text); + signals: void quit(int); void accountAdded(PsiAccount *); @@ -182,7 +184,6 @@ public slots: void openAtStyleUri(const QUrl &uri); void raiseMainwin(); -private slots: void saveAccounts(); void optionChanged(const QString &option); void forceSavePreferences(QSessionManager &); diff --git a/src/psicontact.cpp b/src/psicontact.cpp index cc712756e9..3d1b00ce90 100644 --- a/src/psicontact.cpp +++ b/src/psicontact.cpp @@ -119,7 +119,13 @@ PsiContact::PsiContact(const UserListItem &u, PsiAccount *account, bool isSelf) if (d->account_) { connect(d->account_->avatarFactory(), &AvatarFactory::avatarChanged, this, &PsiContact::avatarChanged); } - connect(VCardFactory::instance(), &VCardFactory::vcardChanged, this, &PsiContact::vcardChanged); + connect(VCardFactory::instance(), &VCardFactory::vcardChanged, this, + [this](const Jid &j, VCardFactory::Flags flags) { + if (!j.compare(jid(), flags & VCardFactory::MucUser)) + return; + + emit updated(); + }); update(u); // updateParent(); @@ -686,16 +692,6 @@ void PsiContact::avatarChanged(const Jid &j) emit updated(); } -void PsiContact::rereadVCard() { vcardChanged(jid()); } - -void PsiContact::vcardChanged(const Jid &j) -{ - if (!j.compare(jid(), false)) - return; - - emit updated(); -} - // bool PsiContact::compare(const ContactListItem* other) const //{ // const ContactListGroup* group = dynamic_cast(other); diff --git a/src/psicontact.h b/src/psicontact.h index bbe08b25c5..4a1f04eb45 100644 --- a/src/psicontact.h +++ b/src/psicontact.h @@ -98,8 +98,6 @@ class PsiContact : public QObject { QIcon picture() const; QIcon alertPicture() const; - void rereadVCard(); - bool groupOperationPermitted(const QString &oldGroupName, const QString &newGroupName) const; QStringList groups() const; void setGroups(QStringList); @@ -144,7 +142,6 @@ public slots: private slots: void avatarChanged(const Jid &); - void vcardChanged(const Jid &); void blockContactConfirmation(const QString &id, bool confirmed); void blockContactConfirmationHelper(bool block); diff --git a/src/psicontactlist.cpp b/src/psicontactlist.cpp index 1b49ace887..a641ec64db 100644 --- a/src/psicontactlist.cpp +++ b/src/psicontactlist.cpp @@ -157,9 +157,8 @@ PsiAccount *PsiContactList::defaultAccount() const * Creates new PsiAccount based on some initial settings. This is used by AccountAddDlg. */ PsiAccount *PsiContactList::createAccount(const QString &name, const Jid &j, const QString &pass, bool opt_host, - const QString &host, int port, bool legacy_ssl_probe, - UserAccount::SSLFlag ssl, QString proxyID, const QString &tlsOverrideDomain, - const QByteArray &tlsOverrideCert) + const QString &host, int port, UserAccount::SSLFlag ssl, QString proxyID, + const QString &tlsOverrideDomain, const QByteArray &tlsOverrideCert) { UserAccount acc; acc.name = name; @@ -170,12 +169,11 @@ PsiAccount *PsiContactList::createAccount(const QString &name, const Jid &j, con acc.pass = pass; } - acc.opt_host = opt_host; - acc.host = host; - acc.port = quint16(port); - acc.ssl = ssl; - acc.proxyID = proxyID; - acc.legacy_ssl_probe = legacy_ssl_probe; + acc.opt_host = opt_host; + acc.host = host; + acc.port = quint16(port); + acc.ssl = ssl; + acc.proxyID = proxyID; acc.tog_offline = showOffline(); acc.tog_agents = showAgents(); diff --git a/src/psicontactlist.h b/src/psicontactlist.h index 7a1418f134..7e67e5cbd3 100644 --- a/src/psicontactlist.h +++ b/src/psicontactlist.h @@ -67,8 +67,8 @@ class PsiContactList : public QObject { void setAccountsOrder(QList accounts); PsiAccount *createAccount(const QString &name, const Jid &j = "", const QString &pass = "", bool opt_host = false, - const QString &host = "", int port = 5222, bool legacy_ssl_probe = true, - UserAccount::SSLFlag ssl = UserAccount::SSL_Auto, QString proxyID = "", + const QString &host = "", int port = 5222, + UserAccount::SSLFlag ssl = UserAccount::TLS_Auto, QString proxyID = "", const QString &tlsOverrideDomain = "", const QByteArray &tlsOverrideCert = QByteArray()); void createAccount(const UserAccount &); void removeAccount(PsiAccount *); diff --git a/src/psicontactmenu.cpp b/src/psicontactmenu.cpp index 0c6c983083..6949ed4b8f 100644 --- a/src/psicontactmenu.cpp +++ b/src/psicontactmenu.cpp @@ -186,7 +186,7 @@ PsiContactMenu::Private::Private(PsiContactMenu *menu, PsiContact *_contact) : connect(_copyMucJid, SIGNAL(triggered(bool)), SLOT(copyJid())); mucAddToBookmarks = new IconAction(tr("Add To Bookmarks"), this, "psi/bookmark_add"); - QObject::connect(mucAddToBookmarks, &QAction::triggered, this, [=](bool) { + QObject::connect(mucAddToBookmarks, &QAction::triggered, this, [this](bool) { GCMainDlg *gc = contact_->account()->findDialog(contact_->jid()); if (gc) gc->doBookmark(); @@ -389,7 +389,7 @@ void PsiContactMenu::Private::mucLeave() { GCMainDlg *gc = contact_->account()->findDialog(contact_->jid()); if (gc) - gc->close(); + gc->leave(); } void PsiContactMenu::Private::rename() diff --git a/src/psidbusnotifier.cpp b/src/psidbusnotifier.cpp index c0eecd93fe..fbff870610 100644 --- a/src/psidbusnotifier.cpp +++ b/src/psidbusnotifier.cpp @@ -375,6 +375,7 @@ void PsiDBusNotifier::asyncCallFinished(QDBusPendingCallWatcher *watcher) void PsiDBusNotifier::popupClosed(uint id, uint reason) { + Q_UNUSED(reason) if (id_ != 0 && id_ == id) { readyToDie(); } diff --git a/src/psidbusnotifier.h b/src/psidbusnotifier.h index 4909ebdc82..731b4d0667 100644 --- a/src/psidbusnotifier.h +++ b/src/psidbusnotifier.h @@ -73,9 +73,9 @@ class PsiDBusNotifierPlugin : public QObject, public PsiPopupPluginInterface { Q_INTERFACES(PsiPopupPluginInterface) public: - virtual QString name() const { return "DBus"; } - virtual PsiPopupInterface *popup(QObject *p) { return new PsiDBusNotifier(p); } - virtual bool isAvailable() { return PsiDBusNotifier::isAvailable(); } + QString name() const override { return "DBus"; } + PsiPopupInterface *popup(QObject *p) override { return new PsiDBusNotifier(p); } + bool isAvailable() const override { return PsiDBusNotifier::isAvailable(); } }; #endif // PSIDBUSNOTIFIER_H diff --git a/src/psievent.cpp b/src/psievent.cpp index a88c4fe721..953b271790 100644 --- a/src/psievent.cpp +++ b/src/psievent.cpp @@ -187,7 +187,7 @@ int MessageEvent::type() const { return Message; } Jid MessageEvent::from() const { #ifdef GROUPCHAT - if (v_m.displayMessage().type() == "groupchat") + if (v_m.displayMessage().type() == Message::Type::Groupchat) return v_m.displayJid().bare(); #endif return v_m.displayJid(); @@ -248,9 +248,9 @@ bool MessageEvent::fromXml(PsiCon *psi, PsiAccount *account, const QDomElement * int MessageEvent::priority() const { - if (v_m.displayMessage().type() == QLatin1String("headline")) + if (v_m.displayMessage().type() == Message::Type::Headline) return eventPriorityHeadline; - else if (v_m.displayMessage().type() == QLatin1String("chat")) + else if (v_m.displayMessage().type() == Message::Type::Chat) return eventPriorityChat; return eventPriorityMessage; @@ -768,7 +768,7 @@ PsiEvent::Ptr EventQueue::peekFirstChat(const Jid &j, bool compareRes) const PsiEvent::Ptr e = i->event(); if (e->type() == PsiEvent::Message) { MessageEvent::Ptr me = e.staticCast(); - if (j.compare(me->from(), compareRes) && me->message().displayMessage().type() == QLatin1String("chat")) + if (j.compare(me->from(), compareRes) && me->message().displayMessage().type() == Message::Type::Chat) return e; } } @@ -789,8 +789,7 @@ void EventQueue::extractChats(QList *el, const Jid &j, bool compa if (e->type() == PsiEvent::Message) { MessageEvent::Ptr me = e.staticCast(); if (j.compare(me->from(), compareRes) - && me->message().displayMessage().type() - == QLatin1String("chat")) { // FIXME: refactor-refactor-refactor + && me->message().displayMessage().type() == Message::Type::Chat) { // FIXME: refactor-refactor-refactor extract = true; } } diff --git a/src/psiiconset.cpp b/src/psiiconset.cpp index 52c94e1896..b938f37175 100644 --- a/src/psiiconset.cpp +++ b/src/psiiconset.cpp @@ -48,13 +48,35 @@ struct ClientIconCheck { * psi-plus psi+,psi#fork#plus * psi-ny psi#ny * - * First column is icon name in the iconpack and remaining is a set of caps/clientName search spec. - * This mean for clients with caps node starting with: psi+ and also for nodes starting with psi and having - * word "fork" or "plus" somewhere inside, "psi-plus" icons will be used. For psi-ny (New Year edition) icon - * caps whould start with "psi" and have "ny" somewhere in the middle. + * The first column is an icon name in the iconpack and the remaining is a set of caps/clientName search spec. + * This mean for clients with caps node or client name (i.e. taken from disco#info or other sources) starting + * with: `psi+` and also for nodes/names starting with `psi` and having words "fork" or "plus" somewhere inside, + * "psi-plus" icons will be used. For psi-ny (New Year edition) icon caps/client-name whould start with "psi" + * and have "ny" somewhere in the middle. * - * The structire below will look like + * === How to add new icons === + * In general you are gonna need to discover the client name and update one of iconpacks. * + * To discover the client do next: + * 1. Find any alive contact using interested you client + * 2. Open xml console, and insert the current JID together with current online resource into the filter line + * 3. Press Dump Ringbuf to see some stanzas. + * 4. Find stanza and remember "ver" attribute from element + * 5. Open caps.xml file (~/.cache/psi/caps.xml, "C:/Users//AppData/Local/psi/cache") and search for + * value of `ver`. + * 6. You should see a line like + * 7. You can take the most important unique part of "TheClientName 1.0 O_o" from the beginning (e.g. TheClient) + * 8. Come up with an icon name in lower case (e.g. "theclient") + * 9. Add a line to client_icons.txt with the format described above, all in lower case. + * "theclient theclient" + * 10. If it conflicts with anything else try to add something unique from its name/node to the client lookup spec. + * "theclient theclient#O_o" + * 11. Next you need to update an iconpack. We have at least two iconpacks for icons. One comes in this repo and + * it's quite minimal just co cover popular clients. And there is also a larger iconpack in resources repo. + * Choose one wisely and and the new "theclient" icon there. + * + * + * The QMap below will look like: * { * "psi" => [ * {"psi-plus",["fork", "plus"]} @@ -67,15 +89,15 @@ struct ClientIconCheck { * psi-plus/psi-ny - icon name * fork/plus/ny - parts of caps/client name * - * Now for example we need to lookup icon for caps node "psiplus.com". The most still mathing item here is "psi", + * Now for example we need to lookup icon for caps node "psiplus.com". The most still matching item here is "psi", * (psi+ won't match because psiplus.com doesn't start with psi+). And we don't have anything like "psip" or "psipl".. * So we review just "psi" (all its items consequently) * Both items in "psi" have clarification list. For the first item we take its clarification list ["fork", "plus"] * and review if any item is in "psiplus.com". The "plus" will be found, so the icon "psi-plus" will be returned. * - * Note: It's quite regular for caps node to start with "https" but current client_icons.txt almost doesn't have - * such records. It just means it heavily rely on detected client names instead of caps. - * Client name from its side maybe taken from caps node when there is not other way to detect. + * Note: It's quite regular for caps node to start with "https" but current client_icons.txt almost never have + * such records. It just means it relies heavily on discovered client names instead of caps nodes. + * Client name from its side may be taken from caps node when there is no other way to detect. * Example: * caps node = https://www.psi-im.org/helloworld/caps * resulting client name = psi-im.org/helloworld @@ -815,11 +837,11 @@ PsiIcon *PsiIconset::event2icon(const PsiEvent::Ptr &e) if (e->type() == PsiEvent::Message) { MessageEvent::Ptr me = e.staticCast(); const Message dm = me->message().displayMessage(); - if (dm.type() == QLatin1String("headline")) + if (dm.type() == Message::Type::Headline) icon = "psi/headline"; - else if (dm.type() == QLatin1String("chat") || dm.type() == QLatin1String("groupchat")) + else if (dm.type() == Message::Type::Chat || dm.type() == Message::Type::Groupchat) icon = "psi/chat"; - else if (dm.type() == "error") + else if (dm.type() == Message::Type::Error) icon = "psi/system"; else icon = "psi/message"; diff --git a/src/psioptionseditor.cpp b/src/psioptionseditor.cpp index b111348a12..e43f07057e 100644 --- a/src/psioptionseditor.cpp +++ b/src/psioptionseditor.cpp @@ -136,11 +136,10 @@ PsiOptionsEditor::PsiOptionsEditor(QWidget *parent) : QWidget(parent) tpm_->setSourceModel(tm_); QVBoxLayout *layout = new QVBoxLayout(this); - layout->setSpacing(0); - layout->setContentsMargins(0, 0, 0, 0); QHBoxLayout *filterLayout = new QHBoxLayout; - le_filter = new QLineEdit(this); + filterLayout->setSpacing(5); + le_filter = new QLineEdit(this); le_filter->setProperty("isOption", false); le_filter->setToolTip(tr("Options filter")); lb_filter = new QLabel(tr("Filter"), this); @@ -206,6 +205,14 @@ PsiOptionsEditor::PsiOptionsEditor(QWidget *parent) : QWidget(parent) buttonLine->addStretch(1); + pb_new = new QPushButton(tr("Add..."), this); + buttonLine->addWidget(pb_new); + connect(pb_new, SIGNAL(clicked()), SLOT(add())); + + pb_edit = new QPushButton(tr("Edit..."), this); + buttonLine->addWidget(pb_edit); + connect(pb_edit, SIGNAL(clicked()), SLOT(edit())); + pb_delete = new QPushButton(tr("Delete..."), this); buttonLine->addWidget(pb_delete); connect(pb_delete, SIGNAL(clicked()), SLOT(deleteit())); @@ -214,14 +221,6 @@ PsiOptionsEditor::PsiOptionsEditor(QWidget *parent) : QWidget(parent) buttonLine->addWidget(pb_reset); connect(pb_reset, SIGNAL(clicked()), SLOT(resetit())); - pb_edit = new QPushButton(tr("Edit..."), this); - buttonLine->addWidget(pb_edit); - connect(pb_edit, SIGNAL(clicked()), SLOT(edit())); - - pb_new = new QPushButton(tr("Add..."), this); - buttonLine->addWidget(pb_new); - connect(pb_new, SIGNAL(clicked()), SLOT(add())); - if (parent) { pb_detach = new QToolButton(this); pb_detach->setIcon(IconsetFactory::iconPtr("psi/advanced")->icon()); @@ -353,7 +352,11 @@ void PsiOptionsEditor::resetit() } } -void PsiOptionsEditor::detach() { new PsiOptionsEditor(); } +void PsiOptionsEditor::detach() +{ + auto dlg = new PsiOptionsEditor(); + dlg->resize(pointToPixel(800), pointToPixel(600)); +} void PsiOptionsEditor::bringToFront() { ::bringToFront(this, true); } diff --git a/src/psipopup.h b/src/psipopup.h index a784807c45..86956b0e8d 100644 --- a/src/psipopup.h +++ b/src/psipopup.h @@ -64,9 +64,9 @@ class PsiPopupPlugin : public QObject, public PsiPopupPluginInterface { Q_INTERFACES(PsiPopupPluginInterface) public: - virtual ~PsiPopupPlugin() { PsiPopup::deleteAll(); } - virtual QString name() const { return "Classic"; } - virtual PsiPopupInterface *popup(QObject *p) { return new PsiPopup(p); } + ~PsiPopupPlugin() override { PsiPopup::deleteAll(); } + QString name() const override { return QLatin1String("Classic"); } + PsiPopupInterface *popup(QObject *p) override { return new PsiPopup(p); } }; #endif // PSIPOPUP_H diff --git a/src/psipopupinterface.h b/src/psipopupinterface.h index f36abf6d76..917305080d 100644 --- a/src/psipopupinterface.h +++ b/src/psipopupinterface.h @@ -27,6 +27,7 @@ class PsiPopupInterface; class PsiPopupPluginInterface { public: + virtual ~PsiPopupPluginInterface() = default; virtual QString name() const = 0; virtual bool isAvailable() const { return true; } virtual PsiPopupInterface *popup(QObject *) = 0; diff --git a/src/psirosterwidget.cpp b/src/psirosterwidget.cpp index 43989746aa..928a4b13bc 100644 --- a/src/psirosterwidget.cpp +++ b/src/psirosterwidget.cpp @@ -151,6 +151,7 @@ PsiRosterWidget::PsiRosterWidget(QWidget *parent) : connect(filterPageView_, SIGNAL(quitFilteringMode()), SLOT(quitFilteringMode())); filterPageView_->installEventFilter(this); filterPageLayout->addWidget(filterPageView_); + connect(filterPageView_, &ContactListView::contactSelected, this, &PsiRosterWidget::contactPick); } PsiRosterWidget::~PsiRosterWidget() { } @@ -176,8 +177,9 @@ void PsiRosterWidget::setContactList(PsiContactList *contactList) contactListModel_ = new ContactListDragModel(contactList_, this); contactListModel_->invalidateLayout(); - contactListModel_->setGroupsEnabled(PsiOptions::instance()->getOption(enableGroupsOptionPath).toBool()); contactListModel_->setAccountsEnabled(true); + contactListModel_->setGroupsEnabled(PsiOptions::instance()->getOption(enableGroupsOptionPath).toBool()); + // connect #ifdef MODELTEST new ModelTest(contactListModel_, this); #endif @@ -249,9 +251,19 @@ void PsiRosterWidget::filterEditTextChanged(const QString &text) #endif } -void PsiRosterWidget::quitFilteringMode() { setFilterModeEnabled(false); } +void PsiRosterWidget::quitFilteringMode() +{ + if (!pickContactMode_) { + setFilterModeEnabled(false); + } +} -void PsiRosterWidget::updateFilterMode() { setFilterModeEnabled(!filterEdit_->text().isEmpty()); } +void PsiRosterWidget::updateFilterMode() +{ + if (!pickContactMode_) { + setFilterModeEnabled(!filterEdit_->text().isEmpty()); + } +} void PsiRosterWidget::setFilterModeEnabled(bool enabled) { @@ -297,9 +309,20 @@ void PsiRosterWidget::setFilterModeEnabled(bool enabled) delete selection; } +void PsiRosterWidget::setPickContactMode(bool value) +{ + pickContactMode_ = value; + if (value) { + setFilterModeEnabled(true); + } + filterPageView_->setActivateAction(value ? ContactListView::SignalSelected : ContactListView::Activate); +} + +QList PsiRosterWidget::selectedContacts() const { return {}; } + void PsiRosterWidget::clearFilterEdit() { - if (filterEdit_->text().isEmpty()) + if (filterEdit_->text().isEmpty() && !pickContactMode_) setFilterModeEnabled(false); else filterEdit_->setText(""); @@ -332,7 +355,7 @@ bool PsiRosterWidget::eventFilter(QObject *obj, QEvent *e) filterEdit_->setText(text); return true; } - } else if (ke->key() == Qt::Key_F3) { + } else if (ke->key() == Qt::Key_F3 && !pickContactMode_) { setFilterModeEnabled(!(filterEdit_->isVisible() && filterEdit_->text().isEmpty())); filterEdit_->setText(""); return true; @@ -340,11 +363,15 @@ bool PsiRosterWidget::eventFilter(QObject *obj, QEvent *e) if (obj == filterEdit_ || obj == filterPageView_ || obj == filterPage_) { if (ke->key() == Qt::Key_Escape) { - setFilterModeEnabled(false); + if (pickContactMode_) { + deleteLater(); + } else { + setFilterModeEnabled(false); + } return true; } - if (ke->key() == Qt::Key_Backspace && filterEdit_->text().isEmpty()) { + if (ke->key() == Qt::Key_Backspace && filterEdit_->text().isEmpty() && !pickContactMode_) { setFilterModeEnabled(false); return true; } diff --git a/src/psirosterwidget.h b/src/psirosterwidget.h index 7db4102d19..63c9ecb186 100644 --- a/src/psirosterwidget.h +++ b/src/psirosterwidget.h @@ -27,6 +27,7 @@ class ContactListDragModel; class PsiContactList; class PsiContactListView; class PsiFilteredContactListView; +class PsiContact; class QLineEdit; class QMimeData; class QSortFilterProxyModel; @@ -35,17 +36,23 @@ class QStackedWidget; class PsiRosterWidget : public QWidget { Q_OBJECT public: - PsiRosterWidget(QWidget *parent); + PsiRosterWidget(QWidget *parent = nullptr); ~PsiRosterWidget(); void setContactList(PsiContactList *contactList); + void setPickContactMode(bool); + QList selectedContacts() const; +signals: + void contactPick(PsiContact *); + public slots: void filterEditTextChanged(const QString &); void quitFilteringMode(); void updateFilterMode(); void setFilterModeEnabled(bool enabled); void clearFilterEdit(); + void setShowStatusMsg(bool); private slots: void optionChanged(const QString &option); @@ -53,7 +60,6 @@ private slots: void showHiddenChanged(bool); void showSelfChanged(bool); void showOfflineChanged(bool); - void setShowStatusMsg(bool); protected: bool eventFilter(QObject *obj, QEvent *e); @@ -69,6 +75,7 @@ private slots: ContactListDragModel *contactListModel_; QSortFilterProxyModel *filterModel_; + bool pickContactMode_ = false; }; #endif // PSIROSTERWIDGET_H diff --git a/src/psithememanager.cpp b/src/psithememanager.cpp index 07a1dcd367..946362f3ea 100644 --- a/src/psithememanager.cpp +++ b/src/psithememanager.cpp @@ -28,6 +28,9 @@ class PsiThemeManager::Private { public: QMap providers; QSet required; + QString failedId; + QString errorString; + PsiThemeProvider::LoadRestult loadResult = PsiThemeProvider::LoadNotStarted; }; //--------------------------------------------------------- @@ -61,13 +64,56 @@ PsiThemeProvider *PsiThemeManager::provider(const QString &type) { return d->pro QList PsiThemeManager::registeredProviders() const { return d->providers.values(); } -bool PsiThemeManager::loadAll() +PsiThemeProvider::LoadRestult PsiThemeManager::loadAll() { - const auto &types = d->providers.keys(); - for (const QString &type : types) { - if (!d->providers[type]->loadCurrent() && d->required.contains(type)) { - return false; + d->loadResult = PsiThemeProvider::LoadInProgress; + QObject *ctx = nullptr; + auto pending = std::shared_ptr> { new QList() }; + auto cleanup = [this, pending](QObject *ctx, PsiThemeProvider *provider, bool failure) { + if (failure) { + d->failedId = QLatin1String(provider->type()); + for (auto p : *pending) { + p->cancelCurrentLoading(); + p->disconnect(ctx); + } + ctx->deleteLater(); + d->loadResult = PsiThemeProvider::LoadFailure; + emit currentLoadFailed(); + return; } + pending->removeOne(provider); + provider->disconnect(ctx); + if (pending->isEmpty()) { + ctx->deleteLater(); + d->loadResult = PsiThemeProvider::LoadSuccess; + emit currentLoadSuccess(); + } + }; + for (auto it = d->providers.begin(); it != d->providers.end(); ++it) { + auto provider = it.value(); + auto required = d->required.contains(it.key()); + auto status = provider->loadCurrent(); + if (status == PsiThemeProvider::LoadFailure && required) { + d->failedId = QLatin1String(provider->type()); + d->loadResult = PsiThemeProvider::LoadFailure; + return PsiThemeProvider::LoadFailure; + } + if (status == PsiThemeProvider::LoadSuccess) { + continue; + } + // in progress + pending->append(provider); + + if (!ctx) { + ctx = new QObject(this); + } + connect(provider, &PsiThemeProvider::themeChanged, ctx, + [ctx, provider, cleanup]() { cleanup(ctx, provider, false); }); + connect(provider, &PsiThemeProvider::currentLoadFailed, ctx, + [ctx, provider, required, cleanup]() { cleanup(ctx, provider, required); }); } - return true; + d->loadResult = pending->isEmpty() ? PsiThemeProvider::LoadSuccess : PsiThemeProvider::LoadInProgress; + return d->loadResult; } + +QString PsiThemeManager::failedId() const { return d->failedId; } diff --git a/src/psithememanager.h b/src/psithememanager.h index 850a40b8ed..a2274563df 100644 --- a/src/psithememanager.h +++ b/src/psithememanager.h @@ -32,12 +32,18 @@ class PsiThemeManager : public QObject { PsiThemeManager(QObject *parent); ~PsiThemeManager(); - void registerProvider(PsiThemeProvider *provider, bool required = false); - PsiThemeProvider *unregisterProvider(const QString &type); - PsiThemeProvider *provider(const QString &type); - QList registeredProviders() const; - bool loadAll(); - + void registerProvider(PsiThemeProvider *provider, bool required = false); + PsiThemeProvider *unregisterProvider(const QString &type); + PsiThemeProvider *provider(const QString &type); + QList registeredProviders() const; + PsiThemeProvider::LoadRestult loadAll(); + QString failedId() const; + +signals: + void currentLoadSuccess(); + void currentLoadFailed(); + +private: class Private; Private *d; }; diff --git a/src/psithememodel.cpp b/src/psithememodel.cpp index 3abe2f2c3e..33d9888a47 100644 --- a/src/psithememodel.cpp +++ b/src/psithememodel.cpp @@ -45,7 +45,7 @@ struct PsiThemeModel::Loader { ThemeItemInfo ti; ti.id = id; Theme theme = provider->theme(id); - if (theme.load()) { + if (theme.load({})) { // load with default style fillThemeInfo(ti, theme); } else { ti.title = ":-("; @@ -55,11 +55,11 @@ struct PsiThemeModel::Loader { return ti; } - void asyncLoad(const QString &id, std::function loadCallback) + void asyncLoad(const QString &id, const QString &style, std::function loadCallback) { Theme theme = provider->theme(id); themes.insert(id, theme); - if (!theme.isValid() || !theme.load([this, theme, loadCallback](bool success) { + if (!theme.isValid() || !theme.load(style, [this, theme, loadCallback](bool success) { qDebug("%s theme loading status: %s", qPrintable(theme.id()), success ? "success" : "failure"); // TODO invent something smarter @@ -75,13 +75,16 @@ struct PsiThemeModel::Loader { void fillThemeInfo(ThemeItemInfo &ti, const Theme &theme) const { - ti.id = theme.id(); - ti.title = theme.title(); - ti.version = theme.version(); - ti.description = theme.description(); - ti.authors = theme.authors(); - ti.creation = theme.creation(); - ti.homeUrl = theme.homeUrl(); + ti.id = theme.id(); + ti.title = theme.title(); + ti.version = theme.version(); + ti.description = theme.description(); + ti.authors = theme.authors(); + ti.creation = theme.creation(); + ti.homeUrl = theme.homeUrl(); + ti.features = theme.features(); + ti.stylesList = theme.stylesList(); + ti.currentStyle = theme.style(); ti.hasPreview = theme.hasPreview(); ti.isValid = true; @@ -131,7 +134,12 @@ void PsiThemeModel::load() QStringList ids = provider->themeIds(); qDebug() << ids; for (const QString &id : ids) { - loader->asyncLoad(id, [this](const ThemeItemInfo &ti) { + QString style; + if (id == provider->current().id()) { + style = provider->current().style(); + } + + loader->asyncLoad(id, style, [this](const ThemeItemInfo &ti) { if (ti.isValid) { beginInsertRows(QModelIndex(), themesInfo.size(), themesInfo.size()); themesInfo.append(ti); @@ -195,6 +203,10 @@ QVariant PsiThemeModel::data(const QModelIndex &index, int role) const } case IsCurrent: return themesInfo[index.row()].isCurrent; + case StylesListRole: + return themesInfo[index.row()].stylesList; + case CurrentStyleRole: + return themesInfo[index.row()].currentStyle; } return QVariant(); } @@ -206,12 +218,9 @@ bool PsiThemeModel::setData(const QModelIndex &index, const QVariant &value, int } if (role == IsCurrent) { - if (value.toBool()) { - provider->setCurrentTheme(themesInfo[index.row()].id); - return true; - } else { - // TODO set fallback / default - } + auto style = value.toString(); + provider->setCurrentTheme(themesInfo[index.row()].id, style); + return true; } return false; diff --git a/src/psithememodel.h b/src/psithememodel.h index f7750ca6d6..8431853482 100644 --- a/src/psithememodel.h +++ b/src/psithememodel.h @@ -36,6 +36,9 @@ struct ThemeItemInfo { QStringList authors; QString creation; QString homeUrl; + QStringList features; + QStringList stylesList; + QString currentStyle; bool hasPreview; bool isValid = false; @@ -46,7 +49,14 @@ class PsiThemeModel : public QAbstractListModel { Q_OBJECT public: - enum ThemeRoles { IdRole = Qt::UserRole + 1, HasPreviewRole, TitleRole, IsCurrent }; + enum ThemeRoles { + IdRole = Qt::UserRole + 1, + HasPreviewRole, + TitleRole, + IsCurrent, + CurrentStyleRole, + StylesListRole + }; PsiThemeModel(PsiThemeProvider *provider, QObject *parent); ~PsiThemeModel(); diff --git a/src/psithemeprovider.h b/src/psithemeprovider.h index 205213acc6..d44ed661b3 100644 --- a/src/psithemeprovider.h +++ b/src/psithemeprovider.h @@ -23,7 +23,6 @@ #include "theme.h" #include -#include class PsiCon; @@ -33,17 +32,20 @@ class PsiThemeProvider : public QObject { PsiCon *_psi; public: + enum LoadRestult { LoadNotStarted, LoadSuccess, LoadFailure, LoadInProgress }; + PsiThemeProvider(PsiCon *parent); inline PsiCon *psi() const { return _psi; } - virtual const char *type() const = 0; - virtual Theme theme(const QString &id) = 0; // make new theme - virtual const QStringList themeIds() const = 0; - virtual bool loadCurrent() = 0; - virtual void unloadCurrent() = 0; - virtual Theme current() const = 0; - virtual void setCurrentTheme(const QString &) = 0; + virtual const char *type() const = 0; + virtual Theme theme(const QString &id) = 0; // make new theme + virtual const QStringList themeIds() const = 0; + virtual LoadRestult loadCurrent() = 0; + virtual void unloadCurrent() = 0; + virtual void cancelCurrentLoading() = 0; + virtual Theme current() const = 0; + virtual void setCurrentTheme(const QString &, const QString &style = {}) = 0; virtual bool threadedLoading() const; virtual int screenshotWidth() const; @@ -52,6 +54,10 @@ class PsiThemeProvider : public QObject { virtual QString optionsDescription() const = 0; static QString themePath(const QString &name); + +signals: + void themeChanged(); + void currentLoadFailed(); // loading of the current therme has failed }; #endif // PSITHEMEPROVIDER_H diff --git a/src/src.cmake b/src/src.cmake index d817f72c3d..3d3ed16e6c 100644 --- a/src/src.cmake +++ b/src/src.cmake @@ -139,7 +139,7 @@ list(APPEND HEADERS filesharingdownloader.h filesharingitem.h filesharingmanager.h - filesharingnamproxy.h + filesharingproxy.h filetransdlg.h fileutil.h gcuserview.h @@ -263,10 +263,8 @@ list(APPEND HEADERS if(UNIX OR IS_WEBENGINE) list(APPEND SOURCES - filesharinghttpproxy.cpp webserver.cpp) list(APPEND HEADERS - filesharinghttpproxy.h webserver.h) endif() @@ -386,7 +384,7 @@ list(APPEND SOURCES filesharingdownloader.cpp filesharingitem.cpp filesharingmanager.cpp - filesharingnamproxy.cpp + filesharingproxy.cpp filetransdlg.cpp fileutil.cpp gcuserview.cpp diff --git a/src/sxe/sxemanager.cpp b/src/sxe/sxemanager.cpp index a1ecad380f..588968f495 100644 --- a/src/sxe/sxemanager.cpp +++ b/src/sxe/sxemanager.cpp @@ -91,14 +91,14 @@ void SxeManager::recordDetectedSession(const Message &message) // check if a record of the session exists for (const DetectedSession &d : std::as_const(DetectedSessions_)) { if (d.session == message.sxe().attribute("session") - && d.jid.compare(message.from(), message.type() != "groupchat")) + && d.jid.compare(message.from(), message.type() != Message::Type::Groupchat)) return; } // store a record of a detected session DetectedSession detected; detected.session = message.sxe().attribute("session"); - if (message.type() == "groupchat") + if (message.type() == Message::Type::Groupchat) detected.jid = message.from().bare(); else detected.jid = message.from(); @@ -368,7 +368,7 @@ QPointer SxeManager::processNegotiationMessage(const Message &messag { if (PsiOptions::instance()->getOption("options.messages.ignore-non-roster-contacts").toBool() - && message.type() != "groupchat") { + && message.type() != Message::Type::Groupchat) { // Ignore the message if contact not in roster if (!pa_->find(message.from())) { qDebug("SXE invitation received from contact that is not in roster."); @@ -534,7 +534,7 @@ SxeManager::SxeNegotiation *SxeManager::createNegotiation(const Message &message negotiation->role = SxeNegotiation::Joiner; negotiation->state = SxeNegotiation::NotStarted; - if (message.type() == "groupchat") { + if (message.type() == Message::Type::Groupchat) { // If we're being invited from a groupchat, // ownJid is determined based on the bare part of ownJids_ @@ -677,7 +677,7 @@ void SxeManager::sendSxe(QDomElement sxe, const Jid &receiver, bool groupChat) // QDomElement el = sxe.ownerDocument() == *clientDoc ? sxe : clientDoc->importNode(sxe, true).toElement(); m.setSxe(clientDoc->importNode(sxe, true).toElement()); if (groupChat && receiver.resource().isEmpty()) - m.setType("groupchat"); + m.setType(Message::Type::Groupchat); if (client_->isActive()) { // send queued messages first diff --git a/src/tabcompletion.cpp b/src/tabcompletion.cpp index 7bd56ab49c..6267c69f0b 100644 --- a/src/tabcompletion.cpp +++ b/src/tabcompletion.cpp @@ -12,7 +12,7 @@ * 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 . + * along with this program. If not, see . * */ diff --git a/src/tabcompletion.h b/src/tabcompletion.h index f3d272b0fd..52d7c64166 100644 --- a/src/tabcompletion.h +++ b/src/tabcompletion.h @@ -12,7 +12,7 @@ * 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 . + * along with this program. If not, see . * */ diff --git a/src/tasklist.h b/src/tasklist.h index f9e1a942ed..66c871db7e 100644 --- a/src/tasklist.h +++ b/src/tasklist.h @@ -13,7 +13,7 @@ * 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 . + * along with this program. If not, see . * */ diff --git a/src/textutil.cpp b/src/textutil.cpp index 9f2095e65e..d0f278008b 100644 --- a/src/textutil.cpp +++ b/src/textutil.cpp @@ -1,7 +1,6 @@ #include "textutil.h" #include "coloropt.h" -#include "common.h" #include "emojiregistry.h" #include "psiiconset.h" #include "psioptions.h" @@ -87,7 +86,6 @@ QString TextUtil::quote(const QString &toquote, int width, bool quoteEmpty) QString TextUtil::plain2rich(const QString &plain) { QString rich; - int col = 0; for (int i = 0; i < int(plain.length()); ++i) { #ifdef Q_OS_WIN @@ -96,7 +94,6 @@ QString TextUtil::plain2rich(const QString &plain) #endif if (plain[i] == '\n') { rich += "
"; - col = 0; } else if (plain[i] == ' ' && !rich.isEmpty() && rich[rich.size() - 1] == ' ') rich += " "; // instead of pre-wrap, which prewraps \n as well else if (plain[i] == '\t') @@ -113,7 +110,6 @@ QString TextUtil::plain2rich(const QString &plain) rich += "&"; else rich += plain[i]; - ++col; } return rich; @@ -293,15 +289,14 @@ static void emojiconifyPlainText(RTParse &p, const QString &in) QStringView ref; auto dump_emoji = [&p, &emojisStartIdx, &in, &idx]() { + auto code = QStringView { in }.mid(emojisStartIdx, idx - emojisStartIdx).toString(); #if defined(WEBKIT) || defined(WEBENGINE) - p.putRich(QLatin1String(R"html()html") + p.putRich(QLatin1String(R"html()html") + code + QLatin1String("")); #else // FIXME custom style here is a hack. This supposed to be handled via style resource in PsiTextView - p.putRich( - QLatin1String( - R"html()html") + p.putRich(QString(R"()") + .arg(code)); #endif - + QStringView { in }.mid(emojisStartIdx, idx - emojisStartIdx).toString() + QLatin1String("")); }; int position; while (std::tie(ref, position) = reg.findEmoji(in, idx), !ref.isEmpty()) { @@ -539,16 +534,12 @@ QString TextUtil::emoticonify(const QString &in) int n = match.capturedStart(); if (ePos == -1 || n < ePos || (match.capturedLength() > foundLen && n < ePos + foundLen)) { -#if 0 -// this logic is commented out being rather harmful with unicode emoji bool leftSpace = n == 0 || (n > 0 && str[n - 1].isSpace()); bool rightSpace = (n + match.capturedLength() == int(str.length())) || (n + match.capturedLength() < int(str.length()) && str[n + match.capturedLength()].isSpace()); // there must be whitespace at least on one side of the emoticon - if (leftSpace || rightSpace) { -#endif - if (true) { + if (leftSpace || rightSpace || EmojiRegistry::instance().isEmoji(match.captured())) { ePos = n; closest = icon; @@ -576,9 +567,10 @@ QString TextUtil::emoticonify(const QString &in) if (!closest) break; - p.putRich(QString("") - .arg(TextUtil::escape(closest->name()), TextUtil::escape(str.mid(foundPos, foundLen)), - QString::number(-1.4))); + p.putRich( + QString( + R"()") + .arg(TextUtil::escape(closest->name()), TextUtil::escape(str.mid(foundPos, foundLen)))); i = foundPos + foundLen; } } diff --git a/src/theme.cpp b/src/theme.cpp index 23b380013e..db51fc07b5 100644 --- a/src/theme.cpp +++ b/src/theme.cpp @@ -63,15 +63,15 @@ Theme::State Theme::state() const bool Theme::exists() { return d && d->exists(); } -bool Theme::load() +bool Theme::load(const QString &style) { if (!d) { return false; } - return d->load(); + return d->load(style); } -bool Theme::load(std::function loadCallback) { return d->load(loadCallback); } +bool Theme::load(const QString &style, std::function loadCallback) { return d->load(style, loadCallback); } bool Theme::hasPreview() const { return d->hasPreview(); } @@ -80,7 +80,8 @@ QWidget *Theme::previewWidget() { return d->previewWidget(); } bool Theme::isCompressed(const QFileInfo &fi) { QString sfx = fi.suffix(); - return fi.isDir() && (sfx == QLatin1String("jisp") || sfx == QLatin1String("zip") || sfx == QLatin1String("theme")); + return fi.isFile() + && (sfx == QLatin1String("jisp") || sfx == QLatin1String("zip") || sfx == QLatin1String("theme")); } bool Theme::isCompressed() const { return isCompressed(QFileInfo(d->filepath)); } @@ -144,15 +145,14 @@ QByteArray Theme::loadData(const QString &fileName, const QString &themePath, bo z.setCaseSensitivity(UnZip::CS_Insensitive); } - QString n = fi.completeBaseName() + '/' + fileName; - if (!z.readFile(n, &ba)) { - n = "/" + fileName; - if (loaded) { - *loaded = z.readFile(n, &ba); - } else { - z.readFile(n, &ba); + if (!z.readFile(fileName, &ba)) { + if (!z.readFile("/" + fileName, &ba)) { + return {}; } } + if (loaded) { + *loaded = true; + } } #endif @@ -187,6 +187,12 @@ const QString &Theme::creation() const { return d->creation; } const QString &Theme::homeUrl() const { return d->homeUrl; } +const QStringList &Theme::features() const { return d->features; } + +const QStringList &Theme::stylesList() const { return d->stylesList; } + +const QString &Theme::style() const { return d->style; } + PsiThemeProvider *Theme::themeProvider() const { return d->provider; } /** diff --git a/src/theme.h b/src/theme.h index 3fe78a2c8f..226cf1ce4b 100644 --- a/src/theme.h +++ b/src/theme.h @@ -50,8 +50,9 @@ class Theme { public: virtual ~ResourceLoader(); - virtual QByteArray loadData(const QString &fileName) = 0; - virtual bool fileExists(const QString &fileName) = 0; + virtual QByteArray loadData(const QString &fileName) = 0; + virtual bool fileExists(const QString &fileName) = 0; + virtual QStringList listAll() = 0; }; enum class State : char { Invalid, NotLoaded, Loading, Loaded }; @@ -62,13 +63,14 @@ class Theme { Theme &operator=(const Theme &other); virtual ~Theme(); - bool isValid() const; - State state() const; + bool isValid() const; + inline operator bool() const { return isValid(); } + State state() const; // previously virtual bool exists(); - bool load(); // synchronous load - bool load(std::function loadCallback); // asynchronous load + bool load(const QString &style); // synchronous load + bool load(const QString &style, std::function loadCallback); // asynchronous load bool hasPreview() const; QWidget *previewWidget(); // this hack must be replaced with something widget based @@ -91,6 +93,9 @@ class Theme { const QStringList &authors() const; const QString &creation() const; const QString &homeUrl() const; + const QStringList &features() const; + const QStringList &stylesList() const; + const QString &style() const; PsiThemeProvider *themeProvider() const; const QString &filePath() const; diff --git a/src/theme_p.cpp b/src/theme_p.cpp index 92c7bff7b3..17d4ce0c3c 100644 --- a/src/theme_p.cpp +++ b/src/theme_p.cpp @@ -22,6 +22,14 @@ #include #include +#ifndef NO_Theme_ZIP +#define Theme_ZIP +#endif + +#ifdef Theme_ZIP +#include "zip/zip.h" +#endif + ThemePrivate::ThemePrivate(PsiThemeProvider *provider) : provider(provider), name(QObject::tr("Unnamed")), caseInsensitiveFS(false) { @@ -29,9 +37,9 @@ ThemePrivate::ThemePrivate(PsiThemeProvider *provider) : ThemePrivate::~ThemePrivate() { } -bool ThemePrivate::load() { return false; } +bool ThemePrivate::load(const QString &style) { return false; } -bool ThemePrivate::load(std::function loadCallback) +bool ThemePrivate::load(const QString &style, std::function loadCallback) { Q_UNUSED(loadCallback); return false; @@ -108,6 +116,16 @@ class FSResourceLoader : public Theme::ResourceLoader { } return false; } + + QStringList listAll() + { + QString base = baseDir.path() + QLatin1Char('/'); + QDirIterator it(baseDir.path(), QDir::Files, QDirIterator::Subdirectories); + QStringList ret; + while (it.hasNext()) + ret << it.next(); + return ret; + } }; #ifdef Theme_ZIP @@ -139,6 +157,8 @@ class ZipResourceLoader : public Theme::ResourceLoader { } return z.fileExists(n.mid(baseName.size())); } + + QStringList listAll() { return z.list(); } }; #endif @@ -152,7 +172,7 @@ Theme::ResourceLoader *ThemePrivate::resourceLoader() const } #ifdef Theme_ZIP else if (Theme::isCompressed(fi)) { - UnZip z; + UnZip z(fi.filePath()); if (z.open()) { if (caseInsensitiveFS) { z.setCaseSensitivity(UnZip::CS_Insensitive); diff --git a/src/theme_p.h b/src/theme_p.h index 3e29674bdb..a9d87d77f0 100644 --- a/src/theme_p.h +++ b/src/theme_p.h @@ -32,8 +32,10 @@ class ThemePrivate : public QSharedData { Theme::State state = Theme::State::NotLoaded; // metadata - QString id, name, version, description, creation, homeUrl; - QStringList authors; + QString id, name, version, description, creation, homeUrl; + QStringList authors, features, stylesList; + QString style; + QHash info; // runtime info @@ -45,8 +47,8 @@ class ThemePrivate : public QSharedData { virtual ~ThemePrivate(); virtual bool exists() = 0; - virtual bool load(); // synchronous load - virtual bool load(std::function loadCallback); // asynchronous load + virtual bool load(const QString &style); // synchronous load + virtual bool load(const QString &style, std::function loadCallback); // asynchronous load virtual bool hasPreview() const; virtual QWidget *previewWidget(); // this hack must be replaced with something widget based diff --git a/src/tipdlg.cpp b/src/tipdlg.cpp index fe0b841940..3d3234d69b 100644 --- a/src/tipdlg.cpp +++ b/src/tipdlg.cpp @@ -92,7 +92,8 @@ TipDlg::TipDlg(PsiCon *psi) : QDialog(0), psi_(psi) "features on an unstable server, you can do that -- without running a second client to connect to your " "stable server. Just click Add in the Account Setup screen."), "Hal Rottenberg"); - addTip(tr("Do you chat on third-party IM networks such as AIM and ICQ? Try enabling the \"transport-specific " + addTip(tr("Do you chat on third-party IM networks such as Telegram and WhatsApp? Try enabling the " + "\"transport-specific " "icons\" option. This will allow you to quickly see at a glance which network your buddy is using. " "Then you can convince him to switch to XMPP. "), "Hal Rottenberg"); diff --git a/src/tools/CMakeLists.txt b/src/tools/CMakeLists.txt index cedd2a72ed..8455b6c645 100644 --- a/src/tools/CMakeLists.txt +++ b/src/tools/CMakeLists.txt @@ -154,3 +154,12 @@ target_include_directories(tools PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/iconset ${CMAKE_CURRENT_SOURCE_DIR}/tunecontroller ) +if(LINUX AND USE_X11) + target_compile_definitions(tools PRIVATE HAVE_X11) + if(LIMIT_X11_USAGE) + target_compile_definitions(tools PRIVATE LIMIT_X11_USAGE) + endif() + if(USE_XSS) + target_compile_definitions(tools PRIVATE HAVE_XSS) + endif() +endif() diff --git a/src/tools/emojimodel.cpp b/src/tools/emojimodel.cpp index 5490046953..41a5cb0617 100644 --- a/src/tools/emojimodel.cpp +++ b/src/tools/emojimodel.cpp @@ -251,7 +251,7 @@ QModelIndex EmojiModel::index(int row, int column, const QModelIndex &parent) co auto groupId = groupInternalId >> 24; auto const &group = EmojiRegistry::instance().groups[groupId]; - auto relColumn = column; + std::size_t relColumn = column; for (auto const &subGroup : group.subGroups) { if (relColumn < subGroup.emojis.size()) { auto subGroupId = &subGroup - &group.subGroups[0]; @@ -262,8 +262,9 @@ QModelIndex EmojiModel::index(int row, int column, const QModelIndex &parent) co } } else { auto const &groups = EmojiRegistry::instance().groups; - if (row < groups.size()) { - return createIndex(row, column, (quintptr(row) << 24) | 0x00ffffff + 1); // 0x00ffffff - refers group itself + quintptr rowx = row; + if (rowx < groups.size()) { + return createIndex(row, column, (rowx << 24) | 0x00ffffff + 1); // 0x00ffffff - refers group itself } } return QModelIndex(); @@ -295,7 +296,7 @@ int EmojiModel::rowCount(const QModelIndex &parent) const int sum = 0; auto const &group = EmojiRegistry::instance().groups[id >> 24]; for (auto const &subGroup : group.subGroups) { - sum += subGroup.emojis.size(); + sum += int(subGroup.emojis.size()); } return sum; } diff --git a/src/tools/globalshortcut/globalshortcutmanager_win.cpp b/src/tools/globalshortcut/globalshortcutmanager_win.cpp index abfcf02d30..2978e39709 100644 --- a/src/tools/globalshortcut/globalshortcutmanager_win.cpp +++ b/src/tools/globalshortcut/globalshortcutmanager_win.cpp @@ -104,16 +104,21 @@ class GlobalShortcutManager::KeyTrigger::Impl : public QWidget { static bool convertKeySequence(const QKeySequence &ks, UINT *mod_, UINT *key_) { - int code = ks[0]; - +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) + auto modifiers = Qt::KeyboardModifiers(ks[0] & Qt::KeyboardModifierMask); + int code = modifiers & ~Qt::KeyboardModifierMask; +#else + auto modifiers = ks[0].keyboardModifiers(); + int code = ks[0].key(); +#endif UINT mod = 0; - if (code & Qt::META) + if (modifiers & Qt::MetaModifier) mod |= MOD_WIN; - if (code & Qt::SHIFT) + if (modifiers & Qt::ShiftModifier) mod |= MOD_SHIFT; - if (code & Qt::CTRL) + if (modifiers & Qt::ControlModifier) mod |= MOD_CONTROL; - if (code & Qt::ALT) + if (modifiers & Qt::AltModifier) mod |= MOD_ALT; UINT key = 0; diff --git a/src/tools/globalshortcut/globalshortcutmanager_x11.cpp b/src/tools/globalshortcut/globalshortcutmanager_x11.cpp index dd5843a65a..7fc06455c9 100644 --- a/src/tools/globalshortcut/globalshortcutmanager_x11.cpp +++ b/src/tools/globalshortcut/globalshortcutmanager_x11.cpp @@ -33,9 +33,6 @@ #include #ifdef KeyPress -// defined by X11 headers -const int XKeyPress = KeyPress; -const int XKeyRelease = KeyRelease; #undef KeyPress #endif diff --git a/src/tools/iconset/iconset.h b/src/tools/iconset/iconset.h index 18a0755020..5461189d71 100644 --- a/src/tools/iconset/iconset.h +++ b/src/tools/iconset/iconset.h @@ -181,6 +181,8 @@ public slots: class Iconset { public: + using value_type = PsiIcon *; + enum class Format { Psi, KdeEmoticons }; Iconset(); diff --git a/src/tools/libpsi_tools.cmake b/src/tools/libpsi_tools.cmake index 1c2c384e34..b767ffdd1d 100644 --- a/src/tools/libpsi_tools.cmake +++ b/src/tools/libpsi_tools.cmake @@ -1,4 +1,4 @@ -set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD 20) if(APPLE) if(USE_GROWL) @@ -118,11 +118,6 @@ if(APPLE) systemwatch/systemwatch_mac.cpp ) elseif(WIN32) - list(APPEND HEADERS - # spellchecker - spellchecker/hunspellchecker.h - ) - list(APPEND SOURCES #idle idle/idle_win.cpp @@ -132,9 +127,6 @@ elseif(WIN32) # globalshortcut globalshortcut/globalshortcutmanager_win.cpp - - # spellchecker - spellchecker/hunspellchecker.cpp ) elseif(HAIKU) list(APPEND HEADERS diff --git a/src/userlist.cpp b/src/userlist.cpp index 3a2868ebf2..a56e381f5d 100644 --- a/src/userlist.cpp +++ b/src/userlist.cpp @@ -410,12 +410,10 @@ QString UserListItem::makeBareTip(bool trim, bool doLinkify) const // you most probably want to wrap it with TextUtil::escape() QString str; - int iconSize = PsiIconset::instance()->system().iconSize(); str += QString("") - .arg(iconSize + 2); + .layer1 { white-space:pre; } \ + .layer2 { white-space:normal;} \ + "); QString imgTag = "icon name"; // or 'img src' if appropriate QMimeSourceFactory is installed. but mblsha noticed // that QMimeSourceFactory unloads sometimes @@ -437,9 +435,9 @@ QString UserListItem::makeBareTip(bool trim, bool doLinkify) const str += ""; if (useAvatar) { - str += QString(""; + str += QString(""; } str += "
").arg(QFontInfo(QApplication::font()).pixelSize() * 5); - str += QString(R"()").arg(mucItem ? jid().full() : jid().bare()); - str += ""); + str += QString(R"()").arg(mucItem ? jid().full() : jid().bare()); + str += ""; diff --git a/src/vcardfactory.cpp b/src/vcardfactory.cpp index 8a9e39b861..d3e19765d7 100644 --- a/src/vcardfactory.cpp +++ b/src/vcardfactory.cpp @@ -24,9 +24,15 @@ #include "iris/xmpp_tasks.h" #include "iris/xmpp_vcard.h" #include "jidutil.h" +#include "pepmanager.h" #include "profiles.h" #include "psiaccount.h" +// #include "xmpp/xmpp-im/xmpp_caps.h" +#include "xmpp/xmpp-im/xmpp_pubsubitem.h" +#include "xmpp/xmpp-im/xmpp_serverinfomanager.h" +#include "xmpp/xmpp-im/xmpp_vcard4.h" + #include #include #include @@ -35,12 +41,51 @@ #include #include -#include +// #define VCF_DEBUG 1 + +#define PEP_VCARD4_NODE "urn:xmpp:vcard4" +#define CONTACTS_NODE "urn:xmpp:contacts" +#define PEP_VCARD4_NS "urn:ietf:params:xml:ns:vcard-4.0" + +using VCardRequestQueue = QList; + +class VCardFactory::QueuedLoader : public QObject { + Q_OBJECT + + static const int VcardReqInterval = 500; // query vcards + + VCardFactory *q; + VCardRequestQueue queue_; + QHash jid2req; + QTimer timer_; + +signals: + void vcardReceived(const VCardRequest *); + +public: + enum Priority { HighPriority, NormalPriority }; + + QueuedLoader(VCardFactory *vcf); + ~QueuedLoader(); + VCardRequest *enqueue(PsiAccount *acc, const Jid &jid, Flags flags, Priority prio); +}; /** * \brief Factory for retrieving and changing VCards. */ -VCardFactory::VCardFactory() : QObject(qApp), dictSize_(5) { } +VCardFactory::VCardFactory() : QObject(qApp), dictSize_(5), queuedLoader_(new QueuedLoader(this)) +{ + connect(queuedLoader_, &QueuedLoader::vcardReceived, this, [this](const VCardRequest *request) { + if (request->success()) { + saveVCard(request->jid(), request->vcard(), request->flags()); + } +#ifdef VCF_DEBUG + else { + qDebug() << "vcard query failed for " << request->jid().full() << ": " << request->errorString(); + } +#endif + }); +} /** * \brief Destroys all cached VCards. @@ -61,7 +106,7 @@ VCardFactory *VCardFactory::instance() /** * Adds a vcard to the cache (and removes other items if necessary) */ -void VCardFactory::checkLimit(const QString &jid, const VCard &vcard) +void VCardFactory::checkLimit(const QString &jid, const VCard4::VCard &vcard) { if (vcardList_.contains(jid)) { vcardList_.removeAll(jid); @@ -75,45 +120,32 @@ void VCardFactory::checkLimit(const QString &jid, const VCard &vcard) vcardList_.push_front(jid); } -void VCardFactory::taskFinished() +void VCardFactory::saveVCard(const Jid &j, const VCard4::VCard &vcard, Flags flags) { - JT_VCard *task = static_cast(sender()); - bool notifyPhoto = task->property("phntf").toBool(); - if (task->success()) { - Jid j = task->jid(); - - saveVCard(j, task->vcard(), notifyPhoto); - } -} +#ifdef VCF_DEBUG + qDebug() << "VCardFactory::saveVCard" << j.full(); +#endif + if (flags & MucUser) { -void VCardFactory::mucTaskFinished() -{ - JT_VCard *task = static_cast(sender()); - bool notifyPhoto = task->property("phntf").toBool(); - if (task->success()) { - Jid j = task->jid(); auto &nick2vcard = mucVcardDict_[j.bare()]; auto nickIt = nick2vcard.find(j.resource()); if (nickIt == nick2vcard.end()) { - nick2vcard.insert(j.resource(), task->vcard()); + nick2vcard.insert(j.resource(), vcard); auto &resQueue = lastMucVcards_[j.bare()]; resQueue.enqueue(j.resource()); while (resQueue.size() > 3) { // keep max 3 vcards per muc nick2vcard.remove(resQueue.dequeue()); } } else { - *nickIt = task->vcard(); + *nickIt = vcard; } - emit vcardChanged(j); - if (notifyPhoto && !task->vcard().photo().isEmpty()) { - emit vcardPhotoAvailable(j, true); + if (!(flags & Silent)) { + emit vcardChanged(j, flags); } + return; } -} -void VCardFactory::saveVCard(const Jid &j, const VCard &vcard, bool notifyPhoto) -{ checkLimit(j.bare(), vcard); // save vCard to disk @@ -124,44 +156,41 @@ void VCardFactory::saveVCard(const Jid &j, const VCard &vcard, bool notifyPhoto) if (!v.exists()) p.mkdir("vcard"); - QFile file(ApplicationInfo::vCardDir() + '/' + JIDUtil::encode(j.bare()).toLower() + ".xml"); - file.open(QIODevice::WriteOnly); - QTextStream out(&file); -#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) - out.setEncoding(QStringConverter::Utf8); -#else - out.setCodec("UTF-8"); -#endif - QDomDocument doc; - doc.appendChild(vcard.toXml(&doc)); - out << doc.toString(4); - - Jid jid = j; - emit vcardChanged(jid); + auto filename = ApplicationInfo::vCardDir() + '/' + JIDUtil::encode(j.bare()).toLower() + ".xml"; + if (vcard) { + vcard.save(filename); + } else { + QFile::remove(filename); + } - if (notifyPhoto && !vcard.photo().isEmpty()) { - emit vcardPhotoAvailable(jid, false); + Jid jid = j; + if (!(flags & Silent)) { + emit vcardChanged(jid, flags); } } /** * \brief Call this, when you need a runtime cached vCard. */ -const VCard VCardFactory::mucVcard(const Jid &j) const +const VCard4::VCard VCardFactory::mucVcard(const Jid &j) const { - QHash d = mucVcardDict_.value(j.bare()); - QHash::ConstIterator it = d.constFind(j.resource()); + QHash d = mucVcardDict_.value(j.bare()); + QHash::ConstIterator it = d.constFind(j.resource()); if (it != d.constEnd()) { return *it; } - return VCard(); + return {}; } /** * \brief Call this, when you need a cached vCard. */ -VCard VCardFactory::vcard(const Jid &j) +VCard4::VCard VCardFactory::vcard(const Jid &j, Flags flags) { + if (flags & MucUser) { + return mucVcard(j); + } + // first, try to get vCard from runtime cache if (vcardDict_.contains(j.bare())) { return vcardDict_[j.bare()]; @@ -173,15 +202,21 @@ VCard VCardFactory::vcard(const Jid &j) // REVIEW we can cache which files were really missed. or maybe set a flag for the contact return {}; } - QDomDocument doc; - if (doc.setContent(&file, false)) { - VCard vcard = VCard::fromXml(doc.documentElement()); - if (!vcard.isNull()) { - checkLimit(j.bare(), vcard); - return vcard; + VCard4::VCard v4 = VCard4::VCard::fromDevice(&file); + if (!v4) { + file.seek(0); + QDomDocument doc; + if (doc.setContent(&file, false)) { + VCard vcard = VCard::fromXml(doc.documentElement()); + if (!vcard.isNull()) { + v4.fromVCardTemp(vcard); + } } } + if (v4) { + checkLimit(j.bare(), v4); + } return {}; } @@ -189,66 +224,282 @@ VCard VCardFactory::vcard(const Jid &j) /** * \brief Call this when you need to update vCard in cache. */ -void VCardFactory::setVCard(const Jid &j, const VCard &v, bool notifyPhoto) { saveVCard(j, v, notifyPhoto); } +// void VCardFactory::setVCard(const Jid &j, const VCard &v) { saveVCard(j, v); } /** * \brief Updates vCard on specified \a account. */ -void VCardFactory::setVCard(const PsiAccount *account, const VCard &v, QObject *obj, const char *slot) +Task *VCardFactory::setVCard(PsiAccount *account, const VCard4::VCard &v, const Jid &targetJid, Flags flags) { - JT_VCard *jtVCard_ = new JT_VCard(account->client()->rootTask()); - if (obj) - connect(jtVCard_, SIGNAL(finished()), obj, slot); - connect(jtVCard_, SIGNAL(finished()), SLOT(updateVCardFinished())); - jtVCard_->set(account->jid(), v); - jtVCard_->go(true); + if ((flags & MucRoom) || (flags & ForceVCardTemp)) { + // || !account->client()->serverInfoManager()->hasPersistentStorage()) { + JT_VCard *jtVCard_ = new JT_VCard(account->client()->rootTask()); + connect(jtVCard_, &JT_VCard::finished, this, [this, v, jtVCard_, flags]() { + if (jtVCard_->success()) { + saveVCard(jtVCard_->jid(), v, flags); + } + }); + jtVCard_->set(targetJid.isNull() ? account->jid() : targetJid, v.toVCardTemp(), !targetJid.isNull()); + jtVCard_->go(true); + return jtVCard_; + } + QDomDocument *doc = account->client()->doc(); + QDomElement el = v.toXmlElement(*doc); + + return account->pepManager()->publish(QLatin1String(PEP_VCARD4_NODE), PubSubItem(QLatin1String("current"), el)); } /** - * \brief Updates vCard on specified \a account. + * \brief Call this when you need to retrieve fresh vCard from server (and store it in cache afterwards) */ -void VCardFactory::setTargetVCard(const PsiAccount *account, const VCard &v, const Jid &mucJid, QObject *obj, - const char *slot) +VCardRequest *VCardFactory::getVCard(PsiAccount *account, const Jid &jid, Flags flags) { - JT_VCard *jtVCard_ = new JT_VCard(account->client()->rootTask()); - if (obj) - connect(jtVCard_, SIGNAL(finished()), obj, slot); - connect(jtVCard_, SIGNAL(finished()), SLOT(updateVCardFinished())); - jtVCard_->set(mucJid, v, true); - jtVCard_->go(true); + return queuedLoader_->enqueue(account, jid, flags, QueuedLoader::HighPriority); } -void VCardFactory::updateVCardFinished() +void VCardFactory::setPhoto(const Jid &j, const QByteArray &photo, Flags flags) { - JT_VCard *jtVCard = static_cast(sender()); - if (jtVCard && jtVCard->success()) { - setVCard(jtVCard->jid(), jtVCard->vcard()); + VCard4::VCard vc; + Jid sj; + if (flags & MucUser) { + sj = j; + vc = mucVcard(j); + } else { + sj = j.withResource({}); + vc = vcard(sj); } - if (jtVCard) { - jtVCard->deleteLater(); + if (vc && vc.photo() != photo) { + vc.setPhoto(VCard4::UriValue {}); + saveVCard(sj, vc, flags); } } -/** - * \brief Call this when you need to retrieve fresh vCard from server (and store it in cache afterwards) - */ -JT_VCard *VCardFactory::getVCard(const Jid &jid, Task *rootTask, const QObject *obj, std::function &&cb, - bool cacheVCard, bool isMuc, bool notifyPhoto) +void VCardFactory::deletePhoto(const Jid &j, Flags flags) { - JT_VCard *task = new JT_VCard(rootTask); - if (notifyPhoto) { - task->setProperty("phntf", true); + VCard4::VCard vc; + Jid sj; + if (flags & MucUser) { + sj = j; + vc = mucVcard(j); + } else { + sj = j.withResource({}); + vc = vcard(sj); } - if (cacheVCard) { - if (isMuc) - task->connect(task, SIGNAL(finished()), this, SLOT(mucTaskFinished())); - else - task->connect(task, SIGNAL(finished()), this, SLOT(taskFinished())); + if (vc && !vc.photo().isEmpty()) { + vc.setPhoto(VCard4::UriValue {}); + saveVCard(sj, vc, flags); + } +} + +void VCardFactory::ensureVCardPhotoUpdated(PsiAccount *acc, const Jid &jid, Flags flags, const QByteArray &newPhotoHash) +{ + VCard4::VCard vc; + if (flags & MucUser) { + vc = mucVcard(jid); + } else { + vc = vcard(jid); + } + if (newPhotoHash.isEmpty()) { + if (!vc || vc.photo().isEmpty()) { // we didn't have it and still don't + return; + } + vc.setPhoto(VCard4::UriValue {}); // reset photo; + saveVCard(jid, vc, flags); + + return; + } + auto oldHash = vc ? QCryptographicHash::hash(QByteArray(vc.photo()), QCryptographicHash::Sha1) : QByteArray(); + if (oldHash != newPhotoHash) { +#ifdef VCF_DEBUG + qDebug() << "hash mismatch. old=" << oldHash.toHex() << "new=" << photoHash.toHex(); +#endif + if (!newPhotoHash.isEmpty()) { + flags |= VCardFactory::ForceVCardTemp; + } + queuedLoader_->enqueue(acc, jid, flags, QueuedLoader::NormalPriority); } - task->connect(task, &JT_VCard::finished, obj, cb); - task->get(Jid(jid.full())); - task->go(true); - return task; } VCardFactory *VCardFactory::instance_ = nullptr; + +class VCardRequest::Private { +public: + QList> accounts; + Jid jid; + VCardFactory::Flags flags; + + using ErrorPtr = std::unique_ptr; + VCard4::VCard vcard; + ErrorPtr error; + QString statusString; + + Private(PsiAccount *pa, const Jid &jid, VCardFactory::Flags flags) : + accounts({ { pa } }), jid(jid), flags(flags) { } +}; + +VCardRequest::VCardRequest(PsiAccount *account, const Jid &jid, VCardFactory::Flags flags) : + d(new Private(account, jid, flags)) +{ +} + +Jid &VCardRequest::jid() const { return d->jid; } + +VCardFactory::Flags VCardRequest::flags() const { return d->flags; } + +bool VCardRequest::execute() +{ + auto paIt = std::ranges::find_if(d->accounts, [](auto pa) { return pa && pa->isConnected(); }); + if (paIt == d->accounts.end()) + return false; + + bool doTemp = (d->flags & VCardFactory::ForceVCardTemp) || (d->flags & VCardFactory::MucRoom) + || (d->flags & VCardFactory::MucUser); + auto pa = (*paIt); + + // auto cm = client->capsManager(); + // if (!doTemp) { + // if (d->jid.compare(pa->jid(), false)) { + // // we can assume persistent storage is always there + // doTemp = !client->serverInfoManager()->hasPersistentStorage(); + // } else { + // // neither clients nor servers follow the XEP. so this check rather breaks stuff. + // doTemp = !cm->features(d->jid).hasVCard4(); + // } + // } + + if (doTemp) { + executeVCardTemp(pa); + } else { + executePubSub(pa); + } + return true; +} + +void VCardRequest::executeVCardTemp(PsiAccount *pa) +{ + JT_VCard *task = new JT_VCard(pa->client()->rootTask()); + task->connect(task, &JT_VCard::finished, this, [this, task]() { + if (task->success()) { + d->vcard.fromVCardTemp(task->vcard()); + } else if (!task->error().isCancel() + || task->error().condition != XMPP::Stanza::Error::ErrorCond::ItemNotFound) { + + d->error.reset(new Stanza::Error(task->error())); + d->statusString = task->statusString(); + } + emit finished(); + deleteLater(); + }); + task->get(d->jid); + task->go(true); +} + +void VCardRequest::executePubSub(PsiAccount *pa) +{ + auto task = pa->pepManager()->get(d->jid, QLatin1String(PEP_VCARD4_NODE), QLatin1String("current")); + task->connect(task, &PEPGetTask::finished, this, [this, task, ppa = QPointer(pa)]() { + if (task->success()) { + if (!task->items().empty()) { + d->vcard = VCard4::VCard(task->items().last().payload()); + } else { + if (ppa) { + executeVCardTemp(ppa); + return; + } + } + } else if (!task->error().isCancel() + || task->error().condition != XMPP::Stanza::Error::ErrorCond::ItemNotFound) { + // consider not found vcard as not an error. maybe user removed their vcard intentionally + d->error.reset(new Stanza::Error(task->error())); + d->statusString = task->statusString(); + } else { + // we can still try vcard-temp as a fallback + if (ppa) { + executeVCardTemp(ppa); + return; + } + } + emit finished(); + deleteLater(); + }); +} + +void VCardRequest::merge(PsiAccount *account, const Jid &, VCardFactory::Flags flags) +{ + d->flags |= flags; + if (!std::any_of(d->accounts.begin(), d->accounts.end(), [account](auto const &p) { return p == account; })) { + d->accounts << QPointer(account); + } +} + +bool VCardRequest::success() const { return d->error == nullptr; } + +VCard4::VCard VCardRequest::vcard() const { return d->vcard; } + +QString VCardRequest::errorString() const { return d->statusString.isEmpty() ? d->error->toString() : d->statusString; } + +VCardRequest::~VCardRequest() = default; + +VCardFactory::QueuedLoader::QueuedLoader(VCardFactory *vcf) : QObject(vcf), q(vcf) +{ + timer_.setSingleShot(false); + timer_.setInterval(VcardReqInterval); + QObject::connect(&timer_, &QTimer::timeout, this, [this]() { + if (queue_.isEmpty()) { + timer_.stop(); + return; + } + auto request = queue_.takeFirst(); + connect(request, &VCardRequest::finished, this, [this, request]() { +#ifdef VCF_DEBUG + qDebug() << "received VCardRequest" << request->jid().full(); +#endif + emit vcardReceived(request); + jid2req.remove(request->jid()); + request->deleteLater(); + }); + auto started = request->execute(); + if (!started) { + jid2req.remove(request->jid()); + request->deleteLater(); + } + if (queue_.isEmpty()) { + timer_.stop(); + } + }); +} + +VCardFactory::QueuedLoader::~QueuedLoader() { qDeleteAll(jid2req); } + +VCardRequest *VCardFactory::QueuedLoader::enqueue(PsiAccount *acc, const Jid &jid, Flags flags, Priority prio) +{ + auto sanitized_jid = jid; + if (!(flags & MucUser)) { + sanitized_jid = jid.withResource({}); + } + auto req = jid2req[sanitized_jid]; + if (!req) { +#ifdef VCF_DEBUG + qDebug() << "new VCardRequest" << sanitized_jid.full() << flags; +#endif + req = new VCardRequest(acc, sanitized_jid, flags); + jid2req[sanitized_jid] = req; + if (prio == HighPriority) { + queue_.prepend(req); + } else { + queue_.append(req); + } + } else { +#ifdef VCF_DEBUG + qDebug() << "merge VCardRequest" << sanitized_jid.full() << flags; +#endif + req->merge(acc, sanitized_jid, flags); + } + + if (!timer_.isActive()) { + timer_.start(); + } + return req; +} + +#include "vcardfactory.moc" diff --git a/src/vcardfactory.h b/src/vcardfactory.h index a77374ad8c..e387f5766e 100644 --- a/src/vcardfactory.h +++ b/src/vcardfactory.h @@ -24,7 +24,8 @@ #include #include #include -#include + +#include class PsiAccount; @@ -33,49 +34,92 @@ class JT_VCard; class Jid; class Task; class VCard; +namespace VCard4 { + class VCard; +} } using namespace XMPP; +class VCardRequest; + class VCardFactory : public QObject { Q_OBJECT public: + enum Flag { MucRoom = 0x1, MucUser = 0x2, Cache = 0x4, ForceVCardTemp = 0x8, Silent = 0x10 }; + Q_DECLARE_FLAGS(Flags, Flag); + static VCardFactory *instance(); - VCard vcard(const Jid &); - const VCard mucVcard(const Jid &j) const; - void setVCard(const Jid &, const VCard &, bool notifyPhoto = true); - void setVCard(const PsiAccount *account, const VCard &v, QObject *obj = nullptr, const char *slot = nullptr); - void setTargetVCard(const PsiAccount *account, const VCard &v, const Jid &mucJid, QObject *obj, const char *slot); - JT_VCard *getVCard(const Jid &, Task *rootTask, const QObject *, std::function &&cb, bool cacheVCard = true, - bool isMuc = false, bool notifyPhoto = true); + VCard4::VCard vcard(const Jid &, Flags flags = {}); + const VCard4::VCard mucVcard(const Jid &j) const; + + Task *setVCard(PsiAccount *account, const VCard4::VCard &v, const Jid &targetJid, VCardFactory::Flags flags); + VCardRequest *getVCard(PsiAccount *account, const Jid &, VCardFactory::Flags flags = {}); + + void setPhoto(const Jid &j, const QByteArray &photo, Flags flags); + void deletePhoto(const Jid &j, Flags flags); + + // 1. check if it's needed to do a request, + // 2. enqueue request if necessary (no vcard, or if hash doesn't match) + // 3. vcardChanged() will be sent as usually when vcard is updated + void ensureVCardPhotoUpdated(PsiAccount *acc, const Jid &jid, Flags flags, const QByteArray &newPhotoHash); signals: - void vcardChanged(const Jid &); - void vcardPhotoAvailable( - const Jid &, - bool isMuc); // dedicated for AvatarFactory. it will almost always work except requests from AvatarFactory + void vcardChanged(const Jid &, VCardFactory::Flags); protected: - void checkLimit(const QString &jid, const VCard &vcard); - -private slots: - void updateVCardFinished(); - void taskFinished(); - void mucTaskFinished(); + void checkLimit(const QString &jid, const VCard4::VCard &vcard); private: VCardFactory(); ~VCardFactory(); + friend class VCardRequest; + void saveVCard(const Jid &, const VCard4::VCard &, VCardFactory::Flags flags); + + static VCardFactory *instance_; + const int dictSize_; + QStringList vcardList_; + QMap vcardDict_; - static VCardFactory *instance_; - const int dictSize_; - QStringList vcardList_; - QMap vcardDict_; - QMap> mucVcardDict_; // QHash in case of big mucs mucBareJid => {resoure => vcard} - QMap> - lastMucVcards_; // to limit the hash above. this one keeps ordered resource. mucBareJid => resource_list + // QHash in case of big mucs mucBareJid => {resoure => vcard} + QMap> mucVcardDict_; + + // to limit the hash above. this one keeps ordered resource. mucBareJid => resource_list + QMap> lastMucVcards_; + + class QueuedLoader; + QueuedLoader *queuedLoader_; +}; + +Q_DECLARE_OPERATORS_FOR_FLAGS(VCardFactory::Flags) + +class VCardRequest : public QObject { + Q_OBJECT +public: + VCardRequest(PsiAccount *account, const Jid &, VCardFactory::Flags flags); + ~VCardRequest(); + + Jid &jid() const; + VCardFactory::Flags flags() const; + + bool execute(); + void merge(PsiAccount *account, const Jid &, VCardFactory::Flags flags); + + // result stuff + bool success() const; // item-not-found is considered success but vcard will be null + VCard4::VCard vcard() const; + QString errorString() const; + +signals: + void finished(); + +private: + void executeVCardTemp(PsiAccount *pa); + void executePubSub(PsiAccount *pa); - void saveVCard(const Jid &, const VCard &, bool notifyPhoto); + class Private; + friend class QueuedLoader; + std::unique_ptr d; }; #endif // VCARDFACTORY_H diff --git a/src/webview.cpp b/src/webview.cpp index a71e162687..becdf58e3f 100644 --- a/src/webview.cpp +++ b/src/webview.cpp @@ -32,6 +32,7 @@ #ifdef WEBENGINE #include #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) +#include "iconset.h" #include #elif QT_VERSION >= QT_VERSION_CHECK(5, 7, 0) #include @@ -53,7 +54,9 @@ WebView::WebView(QWidget *parent) : setAcceptDrops(false); #ifdef WEBENGINE - setAttribute(Qt::WA_NativeWindow); +#ifdef Q_OS_WIN + setAttribute(Qt::WA_NativeWindow); // see https://bugreports.qt.io/browse/QTBUG-119221 (potentially unstable) +#endif settings()->setAttribute(QWebEngineSettings::PluginsEnabled, false); settings()->setAttribute(QWebEngineSettings::LocalStorageEnabled, false); // TODO cache cotrol @@ -70,7 +73,7 @@ WebView::WebView(QWidget *parent) : connectPageActions(); } -WebView::~WebView() { qDebug("WebView::~WebView"); } +WebView::~WebView() { /*qDebug("WebView::~WebView");*/ } void WebView::linkClickedEvent(const QUrl &url) { diff --git a/src/webview.h b/src/webview.h index 6d315f5ef3..a495bcad11 100644 --- a/src/webview.h +++ b/src/webview.h @@ -20,9 +20,6 @@ #ifndef WEBVIEW_H #define WEBVIEW_H -#include "iconset.h" -#include "networkaccessmanager.h" - #include #include #include diff --git a/src/widgets/CMakeLists.txt b/src/widgets/CMakeLists.txt index 06436cb4a6..96f0a1754a 100644 --- a/src/widgets/CMakeLists.txt +++ b/src/widgets/CMakeLists.txt @@ -33,6 +33,7 @@ set(HEADERS actionlineedit.h busywidget.h emojiregistry.h + emojidb.h eventnotifier.h fancylabel.h fancypopup.h @@ -62,17 +63,13 @@ set(HEADERS stretchwidget.h ) -if(WIN32 AND (${QT_DEFAULT_MAJOR_VERSION} LESS_EQUAL 5)) - find_package(Qt${QT_DEFAULT_MAJOR_VERSION} COMPONENTS WinExtras REQUIRED) - list(APPEND QT_LIBRARIES - Qt${QT_DEFAULT_MAJOR_VERSION}::WinExtras - ) - list(APPEND SOURCES - thumbnailtoolbar.cpp - ) - list(APPEND HEADERS - thumbnailtoolbar.h - ) +if(USE_TASKBARNOTIFIER) + list(APPEND SOURCES taskbarnotifier.cpp) + list(APPEND HEADERS taskbarnotifier.h) + if(WIN32 AND (${QT_DEFAULT_MAJOR_VERSION} VERSION_LESS "6")) + find_package(Qt${QT_DEFAULT_MAJOR_VERSION} 5.10 REQUIRED COMPONENTS WinExtras) + list(APPEND QT_LIBRARIES Qt${QT_DEFAULT_MAJOR_VERSION}::WinExtras) + endif() endif() set(FORMS @@ -94,5 +91,17 @@ endif() qt_wrap_ui(UI_FORMS ${FORMS}) add_library(widgets STATIC ${SOURCES} ${HEADERS} ${UI_FORMS}) +if(WIN32) +target_link_libraries(widgets shlwapi) +endif() target_link_libraries(widgets ${QT_LIBRARIES} ${iris_LIB} tools) target_include_directories(widgets PUBLIC ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_BINARY_DIR} ${CMAKE_CURRENT_LIST_DIR}) +if(IS_WEBKIT OR IS_WEBENGINE) + target_compile_definitions(widgets PRIVATE WEBKIT) + if(IS_WEBENGINE) + target_compile_definitions(widgets PRIVATE WEBENGINE) + endif() +endif() +if(LINUX AND USE_X11) + target_compile_definitions(widgets PRIVATE HAVE_X11) +endif() diff --git a/src/widgets/emojidb.cpp b/src/widgets/emojidb.cpp deleted file mode 100644 index 052cf62979..0000000000 --- a/src/widgets/emojidb.cpp +++ /dev/null @@ -1,8294 +0,0 @@ -// This is a generated file. See emoji.py for details -// clang-format off -static std::vector db = { - { - QT_TR_NOOP("Smileys & Emotion"), - { - { - "face-smiling", - { - { - "😀", - "grinning face" - }, - { - "😃", - "grinning face with big eyes" - }, - { - "😄", - "grinning face with smiling eyes" - }, - { - "😁", - "beaming face with smiling eyes" - }, - { - "😆", - "grinning squinting face" - }, - { - "😅", - "grinning face with sweat" - }, - { - "🤣", - "rolling on the floor laughing" - }, - { - "😂", - "face with tears of joy" - }, - { - "🙂", - "slightly smiling face" - }, - { - "🙃", - "upside-down face" - }, - { - "🫠", - "melting face" - }, - { - "😉", - "winking face" - }, - { - "😊", - "smiling face with smiling eyes" - }, - { - "😇", - "smiling face with halo" - } - } - }, - { - "face-affection", - { - { - "🥰", - "smiling face with hearts" - }, - { - "😍", - "smiling face with heart-eyes" - }, - { - "🤩", - "star-struck" - }, - { - "😘", - "face blowing a kiss" - }, - { - "😗", - "kissing face" - }, - { - "☺️", - "smiling face" - }, - { - "😚", - "kissing face with closed eyes" - }, - { - "😙", - "kissing face with smiling eyes" - }, - { - "🥲", - "smiling face with tear" - } - } - }, - { - "face-tongue", - { - { - "😋", - "face savoring food" - }, - { - "😛", - "face with tongue" - }, - { - "😜", - "winking face with tongue" - }, - { - "🤪", - "zany face" - }, - { - "😝", - "squinting face with tongue" - }, - { - "🤑", - "money-mouth face" - } - } - }, - { - "face-hand", - { - { - "🤗", - "smiling face with open hands" - }, - { - "🤭", - "face with hand over mouth" - }, - { - "🫢", - "face with open eyes and hand over mouth" - }, - { - "🫣", - "face with peeking eye" - }, - { - "🤫", - "shushing face" - }, - { - "🤔", - "thinking face" - }, - { - "🫡", - "saluting face" - } - } - }, - { - "face-neutral-skeptical", - { - { - "🤐", - "zipper-mouth face" - }, - { - "🤨", - "face with raised eyebrow" - }, - { - "😐", - "neutral face" - }, - { - "😑", - "expressionless face" - }, - { - "😶", - "face without mouth" - }, - { - "🫥", - "dotted line face" - }, - { - "😶‍🌫️", - "face in clouds" - }, - { - "😏", - "smirking face" - }, - { - "😒", - "unamused face" - }, - { - "🙄", - "face with rolling eyes" - }, - { - "😬", - "grimacing face" - }, - { - "😮‍💨", - "face exhaling" - }, - { - "🤥", - "lying face" - }, - { - "🫨", - "shaking face" - }, - { - "🙂‍↔️", - "head shaking horizontally" - }, - { - "🙂‍↕️", - "head shaking vertically" - } - } - }, - { - "face-sleepy", - { - { - "😌", - "relieved face" - }, - { - "😔", - "pensive face" - }, - { - "😪", - "sleepy face" - }, - { - "🤤", - "drooling face" - }, - { - "😴", - "sleeping face" - } - } - }, - { - "face-unwell", - { - { - "😷", - "face with medical mask" - }, - { - "🤒", - "face with thermometer" - }, - { - "🤕", - "face with head-bandage" - }, - { - "🤢", - "nauseated face" - }, - { - "🤮", - "face vomiting" - }, - { - "🤧", - "sneezing face" - }, - { - "🥵", - "hot face" - }, - { - "🥶", - "cold face" - }, - { - "🥴", - "woozy face" - }, - { - "😵", - "face with crossed-out eyes" - }, - { - "😵‍💫", - "face with spiral eyes" - }, - { - "🤯", - "exploding head" - } - } - }, - { - "face-hat", - { - { - "🤠", - "cowboy hat face" - }, - { - "🥳", - "partying face" - }, - { - "🥸", - "disguised face" - } - } - }, - { - "face-glasses", - { - { - "😎", - "smiling face with sunglasses" - }, - { - "🤓", - "nerd face" - }, - { - "🧐", - "face with monocle" - } - } - }, - { - "face-concerned", - { - { - "😕", - "confused face" - }, - { - "🫤", - "face with diagonal mouth" - }, - { - "😟", - "worried face" - }, - { - "🙁", - "slightly frowning face" - }, - { - "☹️", - "frowning face" - }, - { - "😮", - "face with open mouth" - }, - { - "😯", - "hushed face" - }, - { - "😲", - "astonished face" - }, - { - "😳", - "flushed face" - }, - { - "🥺", - "pleading face" - }, - { - "🥹", - "face holding back tears" - }, - { - "😦", - "frowning face with open mouth" - }, - { - "😧", - "anguished face" - }, - { - "😨", - "fearful face" - }, - { - "😰", - "anxious face with sweat" - }, - { - "😥", - "sad but relieved face" - }, - { - "😢", - "crying face" - }, - { - "😭", - "loudly crying face" - }, - { - "😱", - "face screaming in fear" - }, - { - "😖", - "confounded face" - }, - { - "😣", - "persevering face" - }, - { - "😞", - "disappointed face" - }, - { - "😓", - "downcast face with sweat" - }, - { - "😩", - "weary face" - }, - { - "😫", - "tired face" - }, - { - "🥱", - "yawning face" - } - } - }, - { - "face-negative", - { - { - "😤", - "face with steam from nose" - }, - { - "😡", - "enraged face" - }, - { - "😠", - "angry face" - }, - { - "🤬", - "face with symbols on mouth" - }, - { - "😈", - "smiling face with horns" - }, - { - "👿", - "angry face with horns" - }, - { - "💀", - "skull" - }, - { - "☠️", - "skull and crossbones" - } - } - }, - { - "face-costume", - { - { - "💩", - "pile of poo" - }, - { - "🤡", - "clown face" - }, - { - "👹", - "ogre" - }, - { - "👺", - "goblin" - }, - { - "👻", - "ghost" - }, - { - "👽", - "alien" - }, - { - "👾", - "alien monster" - }, - { - "🤖", - "robot" - } - } - }, - { - "cat-face", - { - { - "😺", - "grinning cat" - }, - { - "😸", - "grinning cat with smiling eyes" - }, - { - "😹", - "cat with tears of joy" - }, - { - "😻", - "smiling cat with heart-eyes" - }, - { - "😼", - "cat with wry smile" - }, - { - "😽", - "kissing cat" - }, - { - "🙀", - "weary cat" - }, - { - "😿", - "crying cat" - }, - { - "😾", - "pouting cat" - } - } - }, - { - "monkey-face", - { - { - "🙈", - "see-no-evil monkey" - }, - { - "🙉", - "hear-no-evil monkey" - }, - { - "🙊", - "speak-no-evil monkey" - } - } - }, - { - "heart", - { - { - "💌", - "love letter" - }, - { - "💘", - "heart with arrow" - }, - { - "💝", - "heart with ribbon" - }, - { - "💖", - "sparkling heart" - }, - { - "💗", - "growing heart" - }, - { - "💓", - "beating heart" - }, - { - "💞", - "revolving hearts" - }, - { - "💕", - "two hearts" - }, - { - "💟", - "heart decoration" - }, - { - "❣️", - "heart exclamation" - }, - { - "💔", - "broken heart" - }, - { - "❤️‍🔥", - "heart on fire" - }, - { - "❤️‍🩹", - "mending heart" - }, - { - "❤️", - "red heart" - }, - { - "🩷", - "pink heart" - }, - { - "🧡", - "orange heart" - }, - { - "💛", - "yellow heart" - }, - { - "💚", - "green heart" - }, - { - "💙", - "blue heart" - }, - { - "🩵", - "light blue heart" - }, - { - "💜", - "purple heart" - }, - { - "🤎", - "brown heart" - }, - { - "🖤", - "black heart" - }, - { - "🩶", - "grey heart" - }, - { - "🤍", - "white heart" - } - } - }, - { - "emotion", - { - { - "💋", - "kiss mark" - }, - { - "💯", - "hundred points" - }, - { - "💢", - "anger symbol" - }, - { - "💥", - "collision" - }, - { - "💫", - "dizzy" - }, - { - "💦", - "sweat droplets" - }, - { - "💨", - "dashing away" - }, - { - "🕳️", - "hole" - }, - { - "💬", - "speech balloon" - }, - { - "👁️‍🗨️", - "eye in speech bubble" - }, - { - "🗨️", - "left speech bubble" - }, - { - "🗯️", - "right anger bubble" - }, - { - "💭", - "thought balloon" - }, - { - "💤", - "ZZZ" - } - } - } - } - }, - { - QT_TR_NOOP("People & Body"), - { - { - "hand-fingers-open", - { - { - "👋", - "waving hand" - }, - { - "🤚", - "raised back of hand" - }, - { - "🖐️", - "hand with fingers splayed" - }, - { - "✋", - "raised hand" - }, - { - "🖖", - "vulcan salute" - }, - { - "🫱", - "rightwards hand" - }, - { - "🫲", - "leftwards hand" - }, - { - "🫳", - "palm down hand" - }, - { - "🫴", - "palm up hand" - }, - { - "🫷", - "leftwards pushing hand" - }, - { - "🫸", - "rightwards pushing hand" - } - } - }, - { - "hand-fingers-partial", - { - { - "👌", - "OK hand" - }, - { - "🤌", - "pinched fingers" - }, - { - "🤏", - "pinching hand" - }, - { - "✌️", - "victory hand" - }, - { - "🤞", - "crossed fingers" - }, - { - "🫰", - "hand with index finger and thumb crossed" - }, - { - "🤟", - "love-you gesture" - }, - { - "🤘", - "sign of the horns" - }, - { - "🤙", - "call me hand" - } - } - }, - { - "hand-single-finger", - { - { - "👈", - "backhand index pointing left" - }, - { - "👉", - "backhand index pointing right" - }, - { - "👆", - "backhand index pointing up" - }, - { - "🖕", - "middle finger" - }, - { - "👇", - "backhand index pointing down" - }, - { - "☝️", - "index pointing up" - }, - { - "🫵", - "index pointing at the viewer" - } - } - }, - { - "hand-fingers-closed", - { - { - "👍", - "thumbs up" - }, - { - "👎", - "thumbs down" - }, - { - "✊", - "raised fist" - }, - { - "👊", - "oncoming fist" - }, - { - "🤛", - "left-facing fist" - }, - { - "🤜", - "right-facing fist" - } - } - }, - { - "hands", - { - { - "👏", - "clapping hands" - }, - { - "🙌", - "raising hands" - }, - { - "🫶", - "heart hands" - }, - { - "👐", - "open hands" - }, - { - "🤲", - "palms up together" - }, - { - "🤝", - "handshake" - }, - { - "🙏", - "folded hands" - } - } - }, - { - "hand-prop", - { - { - "✍️", - "writing hand" - }, - { - "💅", - "nail polish" - }, - { - "🤳", - "selfie" - } - } - }, - { - "body-parts", - { - { - "💪", - "flexed biceps" - }, - { - "🦾", - "mechanical arm" - }, - { - "🦿", - "mechanical leg" - }, - { - "🦵", - "leg" - }, - { - "🦶", - "foot" - }, - { - "👂", - "ear" - }, - { - "🦻", - "ear with hearing aid" - }, - { - "👃", - "nose" - }, - { - "🧠", - "brain" - }, - { - "🫀", - "anatomical heart" - }, - { - "🫁", - "lungs" - }, - { - "🦷", - "tooth" - }, - { - "🦴", - "bone" - }, - { - "👀", - "eyes" - }, - { - "👁️", - "eye" - }, - { - "👅", - "tongue" - }, - { - "👄", - "mouth" - }, - { - "🫦", - "biting lip" - } - } - }, - { - "person", - { - { - "👶", - "baby" - }, - { - "🧒", - "child" - }, - { - "👦", - "boy" - }, - { - "👧", - "girl" - }, - { - "🧑", - "person" - }, - { - "👱", - "person" - }, - { - "👨", - "man" - }, - { - "🧔", - "person" - }, - { - "🧔‍♂️", - "man" - }, - { - "🧔‍♀️", - "woman" - }, - { - "👨‍🦰", - "man" - }, - { - "👨‍🦱", - "man" - }, - { - "👨‍🦳", - "man" - }, - { - "👨‍🦲", - "man" - }, - { - "👩", - "woman" - }, - { - "👩‍🦰", - "woman" - }, - { - "🧑‍🦰", - "person" - }, - { - "👩‍🦱", - "woman" - }, - { - "🧑‍🦱", - "person" - }, - { - "👩‍🦳", - "woman" - }, - { - "🧑‍🦳", - "person" - }, - { - "👩‍🦲", - "woman" - }, - { - "🧑‍🦲", - "person" - }, - { - "👱‍♀️", - "woman" - }, - { - "👱‍♂️", - "man" - }, - { - "🧓", - "older person" - }, - { - "👴", - "old man" - }, - { - "👵", - "old woman" - } - } - }, - { - "person-gesture", - { - { - "🙍", - "person frowning" - }, - { - "🙍‍♂️", - "man frowning" - }, - { - "🙍‍♀️", - "woman frowning" - }, - { - "🙎", - "person pouting" - }, - { - "🙎‍♂️", - "man pouting" - }, - { - "🙎‍♀️", - "woman pouting" - }, - { - "🙅", - "person gesturing NO" - }, - { - "🙅‍♂️", - "man gesturing NO" - }, - { - "🙅‍♀️", - "woman gesturing NO" - }, - { - "🙆", - "person gesturing OK" - }, - { - "🙆‍♂️", - "man gesturing OK" - }, - { - "🙆‍♀️", - "woman gesturing OK" - }, - { - "💁", - "person tipping hand" - }, - { - "💁‍♂️", - "man tipping hand" - }, - { - "💁‍♀️", - "woman tipping hand" - }, - { - "🙋", - "person raising hand" - }, - { - "🙋‍♂️", - "man raising hand" - }, - { - "🙋‍♀️", - "woman raising hand" - }, - { - "🧏", - "deaf person" - }, - { - "🧏‍♂️", - "deaf man" - }, - { - "🧏‍♀️", - "deaf woman" - }, - { - "🙇", - "person bowing" - }, - { - "🙇‍♂️", - "man bowing" - }, - { - "🙇‍♀️", - "woman bowing" - }, - { - "🤦", - "person facepalming" - }, - { - "🤦‍♂️", - "man facepalming" - }, - { - "🤦‍♀️", - "woman facepalming" - }, - { - "🤷", - "person shrugging" - }, - { - "🤷‍♂️", - "man shrugging" - }, - { - "🤷‍♀️", - "woman shrugging" - } - } - }, - { - "person-role", - { - { - "🧑‍⚕️", - "health worker" - }, - { - "👨‍⚕️", - "man health worker" - }, - { - "👩‍⚕️", - "woman health worker" - }, - { - "🧑‍🎓", - "student" - }, - { - "👨‍🎓", - "man student" - }, - { - "👩‍🎓", - "woman student" - }, - { - "🧑‍🏫", - "teacher" - }, - { - "👨‍🏫", - "man teacher" - }, - { - "👩‍🏫", - "woman teacher" - }, - { - "🧑‍⚖️", - "judge" - }, - { - "👨‍⚖️", - "man judge" - }, - { - "👩‍⚖️", - "woman judge" - }, - { - "🧑‍🌾", - "farmer" - }, - { - "👨‍🌾", - "man farmer" - }, - { - "👩‍🌾", - "woman farmer" - }, - { - "🧑‍🍳", - "cook" - }, - { - "👨‍🍳", - "man cook" - }, - { - "👩‍🍳", - "woman cook" - }, - { - "🧑‍🔧", - "mechanic" - }, - { - "👨‍🔧", - "man mechanic" - }, - { - "👩‍🔧", - "woman mechanic" - }, - { - "🧑‍🏭", - "factory worker" - }, - { - "👨‍🏭", - "man factory worker" - }, - { - "👩‍🏭", - "woman factory worker" - }, - { - "🧑‍💼", - "office worker" - }, - { - "👨‍💼", - "man office worker" - }, - { - "👩‍💼", - "woman office worker" - }, - { - "🧑‍🔬", - "scientist" - }, - { - "👨‍🔬", - "man scientist" - }, - { - "👩‍🔬", - "woman scientist" - }, - { - "🧑‍💻", - "technologist" - }, - { - "👨‍💻", - "man technologist" - }, - { - "👩‍💻", - "woman technologist" - }, - { - "🧑‍🎤", - "singer" - }, - { - "👨‍🎤", - "man singer" - }, - { - "👩‍🎤", - "woman singer" - }, - { - "🧑‍🎨", - "artist" - }, - { - "👨‍🎨", - "man artist" - }, - { - "👩‍🎨", - "woman artist" - }, - { - "🧑‍✈️", - "pilot" - }, - { - "👨‍✈️", - "man pilot" - }, - { - "👩‍✈️", - "woman pilot" - }, - { - "🧑‍🚀", - "astronaut" - }, - { - "👨‍🚀", - "man astronaut" - }, - { - "👩‍🚀", - "woman astronaut" - }, - { - "🧑‍🚒", - "firefighter" - }, - { - "👨‍🚒", - "man firefighter" - }, - { - "👩‍🚒", - "woman firefighter" - }, - { - "👮", - "police officer" - }, - { - "👮‍♂️", - "man police officer" - }, - { - "👮‍♀️", - "woman police officer" - }, - { - "🕵️", - "detective" - }, - { - "🕵️‍♂️", - "man detective" - }, - { - "🕵️‍♀️", - "woman detective" - }, - { - "💂", - "guard" - }, - { - "💂‍♂️", - "man guard" - }, - { - "💂‍♀️", - "woman guard" - }, - { - "🥷", - "ninja" - }, - { - "👷", - "construction worker" - }, - { - "👷‍♂️", - "man construction worker" - }, - { - "👷‍♀️", - "woman construction worker" - }, - { - "🫅", - "person with crown" - }, - { - "🤴", - "prince" - }, - { - "👸", - "princess" - }, - { - "👳", - "person wearing turban" - }, - { - "👳‍♂️", - "man wearing turban" - }, - { - "👳‍♀️", - "woman wearing turban" - }, - { - "👲", - "person with skullcap" - }, - { - "🧕", - "woman with headscarf" - }, - { - "🤵", - "person in tuxedo" - }, - { - "🤵‍♂️", - "man in tuxedo" - }, - { - "🤵‍♀️", - "woman in tuxedo" - }, - { - "👰", - "person with veil" - }, - { - "👰‍♂️", - "man with veil" - }, - { - "👰‍♀️", - "woman with veil" - }, - { - "🤰", - "pregnant woman" - }, - { - "🫃", - "pregnant man" - }, - { - "🫄", - "pregnant person" - }, - { - "🤱", - "breast-feeding" - }, - { - "👩‍🍼", - "woman feeding baby" - }, - { - "👨‍🍼", - "man feeding baby" - }, - { - "🧑‍🍼", - "person feeding baby" - } - } - }, - { - "person-fantasy", - { - { - "👼", - "baby angel" - }, - { - "🎅", - "Santa Claus" - }, - { - "🤶", - "Mrs. Claus" - }, - { - "🧑‍🎄", - "mx claus" - }, - { - "🦸", - "superhero" - }, - { - "🦸‍♂️", - "man superhero" - }, - { - "🦸‍♀️", - "woman superhero" - }, - { - "🦹", - "supervillain" - }, - { - "🦹‍♂️", - "man supervillain" - }, - { - "🦹‍♀️", - "woman supervillain" - }, - { - "🧙", - "mage" - }, - { - "🧙‍♂️", - "man mage" - }, - { - "🧙‍♀️", - "woman mage" - }, - { - "🧚", - "fairy" - }, - { - "🧚‍♂️", - "man fairy" - }, - { - "🧚‍♀️", - "woman fairy" - }, - { - "🧛", - "vampire" - }, - { - "🧛‍♂️", - "man vampire" - }, - { - "🧛‍♀️", - "woman vampire" - }, - { - "🧜", - "merperson" - }, - { - "🧜‍♂️", - "merman" - }, - { - "🧜‍♀️", - "mermaid" - }, - { - "🧝", - "elf" - }, - { - "🧝‍♂️", - "man elf" - }, - { - "🧝‍♀️", - "woman elf" - }, - { - "🧞", - "genie" - }, - { - "🧞‍♂️", - "man genie" - }, - { - "🧞‍♀️", - "woman genie" - }, - { - "🧟", - "zombie" - }, - { - "🧟‍♂️", - "man zombie" - }, - { - "🧟‍♀️", - "woman zombie" - }, - { - "🧌", - "troll" - } - } - }, - { - "person-activity", - { - { - "💆", - "person getting massage" - }, - { - "💆‍♂️", - "man getting massage" - }, - { - "💆‍♀️", - "woman getting massage" - }, - { - "💇", - "person getting haircut" - }, - { - "💇‍♂️", - "man getting haircut" - }, - { - "💇‍♀️", - "woman getting haircut" - }, - { - "🚶", - "person walking" - }, - { - "🚶‍♂️", - "man walking" - }, - { - "🚶‍♀️", - "woman walking" - }, - { - "🚶‍➡️", - "person walking facing right" - }, - { - "🚶‍♀️‍➡️", - "woman walking facing right" - }, - { - "🚶‍♂️‍➡️", - "man walking facing right" - }, - { - "🧍", - "person standing" - }, - { - "🧍‍♂️", - "man standing" - }, - { - "🧍‍♀️", - "woman standing" - }, - { - "🧎", - "person kneeling" - }, - { - "🧎‍♂️", - "man kneeling" - }, - { - "🧎‍♀️", - "woman kneeling" - }, - { - "🧎‍➡️", - "person kneeling facing right" - }, - { - "🧎‍♀️‍➡️", - "woman kneeling facing right" - }, - { - "🧎‍♂️‍➡️", - "man kneeling facing right" - }, - { - "🧑‍🦯", - "person with white cane" - }, - { - "🧑‍🦯‍➡️", - "person with white cane facing right" - }, - { - "👨‍🦯", - "man with white cane" - }, - { - "👨‍🦯‍➡️", - "man with white cane facing right" - }, - { - "👩‍🦯", - "woman with white cane" - }, - { - "👩‍🦯‍➡️", - "woman with white cane facing right" - }, - { - "🧑‍🦼", - "person in motorized wheelchair" - }, - { - "🧑‍🦼‍➡️", - "person in motorized wheelchair facing right" - }, - { - "👨‍🦼", - "man in motorized wheelchair" - }, - { - "👨‍🦼‍➡️", - "man in motorized wheelchair facing right" - }, - { - "👩‍🦼", - "woman in motorized wheelchair" - }, - { - "👩‍🦼‍➡️", - "woman in motorized wheelchair facing right" - }, - { - "🧑‍🦽", - "person in manual wheelchair" - }, - { - "🧑‍🦽‍➡️", - "person in manual wheelchair facing right" - }, - { - "👨‍🦽", - "man in manual wheelchair" - }, - { - "👨‍🦽‍➡️", - "man in manual wheelchair facing right" - }, - { - "👩‍🦽", - "woman in manual wheelchair" - }, - { - "👩‍🦽‍➡️", - "woman in manual wheelchair facing right" - }, - { - "🏃", - "person running" - }, - { - "🏃‍♂️", - "man running" - }, - { - "🏃‍♀️", - "woman running" - }, - { - "🏃‍➡️", - "person running facing right" - }, - { - "🏃‍♀️‍➡️", - "woman running facing right" - }, - { - "🏃‍♂️‍➡️", - "man running facing right" - }, - { - "💃", - "woman dancing" - }, - { - "🕺", - "man dancing" - }, - { - "🕴️", - "person in suit levitating" - }, - { - "👯", - "people with bunny ears" - }, - { - "👯‍♂️", - "men with bunny ears" - }, - { - "👯‍♀️", - "women with bunny ears" - }, - { - "🧖", - "person in steamy room" - }, - { - "🧖‍♂️", - "man in steamy room" - }, - { - "🧖‍♀️", - "woman in steamy room" - }, - { - "🧗", - "person climbing" - }, - { - "🧗‍♂️", - "man climbing" - }, - { - "🧗‍♀️", - "woman climbing" - } - } - }, - { - "person-sport", - { - { - "🤺", - "person fencing" - }, - { - "🏇", - "horse racing" - }, - { - "⛷️", - "skier" - }, - { - "🏂", - "snowboarder" - }, - { - "🏌️", - "person golfing" - }, - { - "🏌️‍♂️", - "man golfing" - }, - { - "🏌️‍♀️", - "woman golfing" - }, - { - "🏄", - "person surfing" - }, - { - "🏄‍♂️", - "man surfing" - }, - { - "🏄‍♀️", - "woman surfing" - }, - { - "🚣", - "person rowing boat" - }, - { - "🚣‍♂️", - "man rowing boat" - }, - { - "🚣‍♀️", - "woman rowing boat" - }, - { - "🏊", - "person swimming" - }, - { - "🏊‍♂️", - "man swimming" - }, - { - "🏊‍♀️", - "woman swimming" - }, - { - "⛹️", - "person bouncing ball" - }, - { - "⛹️‍♂️", - "man bouncing ball" - }, - { - "⛹️‍♀️", - "woman bouncing ball" - }, - { - "🏋️", - "person lifting weights" - }, - { - "🏋️‍♂️", - "man lifting weights" - }, - { - "🏋️‍♀️", - "woman lifting weights" - }, - { - "🚴", - "person biking" - }, - { - "🚴‍♂️", - "man biking" - }, - { - "🚴‍♀️", - "woman biking" - }, - { - "🚵", - "person mountain biking" - }, - { - "🚵‍♂️", - "man mountain biking" - }, - { - "🚵‍♀️", - "woman mountain biking" - }, - { - "🤸", - "person cartwheeling" - }, - { - "🤸‍♂️", - "man cartwheeling" - }, - { - "🤸‍♀️", - "woman cartwheeling" - }, - { - "🤼", - "people wrestling" - }, - { - "🤼‍♂️", - "men wrestling" - }, - { - "🤼‍♀️", - "women wrestling" - }, - { - "🤽", - "person playing water polo" - }, - { - "🤽‍♂️", - "man playing water polo" - }, - { - "🤽‍♀️", - "woman playing water polo" - }, - { - "🤾", - "person playing handball" - }, - { - "🤾‍♂️", - "man playing handball" - }, - { - "🤾‍♀️", - "woman playing handball" - }, - { - "🤹", - "person juggling" - }, - { - "🤹‍♂️", - "man juggling" - }, - { - "🤹‍♀️", - "woman juggling" - } - } - }, - { - "person-resting", - { - { - "🧘", - "person in lotus position" - }, - { - "🧘‍♂️", - "man in lotus position" - }, - { - "🧘‍♀️", - "woman in lotus position" - }, - { - "🛀", - "person taking bath" - }, - { - "🛌", - "person in bed" - } - } - }, - { - "family", - { - { - "🧑‍🤝‍🧑", - "people holding hands" - }, - { - "👭", - "women holding hands" - }, - { - "👫", - "woman and man holding hands" - }, - { - "👬", - "men holding hands" - }, - { - "💏", - "kiss" - }, - { - "👩‍❤️‍💋‍👨", - "kiss" - }, - { - "👨‍❤️‍💋‍👨", - "kiss" - }, - { - "👩‍❤️‍💋‍👩", - "kiss" - }, - { - "💑", - "couple with heart" - }, - { - "👩‍❤️‍👨", - "couple with heart" - }, - { - "👨‍❤️‍👨", - "couple with heart" - }, - { - "👩‍❤️‍👩", - "couple with heart" - }, - { - "👨‍👩‍👦", - "family" - }, - { - "👨‍👩‍👧", - "family" - }, - { - "👨‍👩‍👧‍👦", - "family" - }, - { - "👨‍👩‍👦‍👦", - "family" - }, - { - "👨‍👩‍👧‍👧", - "family" - }, - { - "👨‍👨‍👦", - "family" - }, - { - "👨‍👨‍👧", - "family" - }, - { - "👨‍👨‍👧‍👦", - "family" - }, - { - "👨‍👨‍👦‍👦", - "family" - }, - { - "👨‍👨‍👧‍👧", - "family" - }, - { - "👩‍👩‍👦", - "family" - }, - { - "👩‍👩‍👧", - "family" - }, - { - "👩‍👩‍👧‍👦", - "family" - }, - { - "👩‍👩‍👦‍👦", - "family" - }, - { - "👩‍👩‍👧‍👧", - "family" - }, - { - "👨‍👦", - "family" - }, - { - "👨‍👦‍👦", - "family" - }, - { - "👨‍👧", - "family" - }, - { - "👨‍👧‍👦", - "family" - }, - { - "👨‍👧‍👧", - "family" - }, - { - "👩‍👦", - "family" - }, - { - "👩‍👦‍👦", - "family" - }, - { - "👩‍👧", - "family" - }, - { - "👩‍👧‍👦", - "family" - }, - { - "👩‍👧‍👧", - "family" - } - } - }, - { - "person-symbol", - { - { - "🗣️", - "speaking head" - }, - { - "👤", - "bust in silhouette" - }, - { - "👥", - "busts in silhouette" - }, - { - "🫂", - "people hugging" - }, - { - "👪", - "family" - }, - { - "🧑‍🧑‍🧒", - "family" - }, - { - "🧑‍🧑‍🧒‍🧒", - "family" - }, - { - "🧑‍🧒", - "family" - }, - { - "🧑‍🧒‍🧒", - "family" - }, - { - "👣", - "footprints" - } - } - } - } - }, - { - QT_TR_NOOP("Animals & Nature"), - { - { - "animal-mammal", - { - { - "🐵", - "monkey face" - }, - { - "🐒", - "monkey" - }, - { - "🦍", - "gorilla" - }, - { - "🦧", - "orangutan" - }, - { - "🐶", - "dog face" - }, - { - "🐕", - "dog" - }, - { - "🦮", - "guide dog" - }, - { - "🐕‍🦺", - "service dog" - }, - { - "🐩", - "poodle" - }, - { - "🐺", - "wolf" - }, - { - "🦊", - "fox" - }, - { - "🦝", - "raccoon" - }, - { - "🐱", - "cat face" - }, - { - "🐈", - "cat" - }, - { - "🐈‍⬛", - "black cat" - }, - { - "🦁", - "lion" - }, - { - "🐯", - "tiger face" - }, - { - "🐅", - "tiger" - }, - { - "🐆", - "leopard" - }, - { - "🐴", - "horse face" - }, - { - "🫎", - "moose" - }, - { - "🫏", - "donkey" - }, - { - "🐎", - "horse" - }, - { - "🦄", - "unicorn" - }, - { - "🦓", - "zebra" - }, - { - "🦌", - "deer" - }, - { - "🦬", - "bison" - }, - { - "🐮", - "cow face" - }, - { - "🐂", - "ox" - }, - { - "🐃", - "water buffalo" - }, - { - "🐄", - "cow" - }, - { - "🐷", - "pig face" - }, - { - "🐖", - "pig" - }, - { - "🐗", - "boar" - }, - { - "🐽", - "pig nose" - }, - { - "🐏", - "ram" - }, - { - "🐑", - "ewe" - }, - { - "🐐", - "goat" - }, - { - "🐪", - "camel" - }, - { - "🐫", - "two-hump camel" - }, - { - "🦙", - "llama" - }, - { - "🦒", - "giraffe" - }, - { - "🐘", - "elephant" - }, - { - "🦣", - "mammoth" - }, - { - "🦏", - "rhinoceros" - }, - { - "🦛", - "hippopotamus" - }, - { - "🐭", - "mouse face" - }, - { - "🐁", - "mouse" - }, - { - "🐀", - "rat" - }, - { - "🐹", - "hamster" - }, - { - "🐰", - "rabbit face" - }, - { - "🐇", - "rabbit" - }, - { - "🐿️", - "chipmunk" - }, - { - "🦫", - "beaver" - }, - { - "🦔", - "hedgehog" - }, - { - "🦇", - "bat" - }, - { - "🐻", - "bear" - }, - { - "🐻‍❄️", - "polar bear" - }, - { - "🐨", - "koala" - }, - { - "🐼", - "panda" - }, - { - "🦥", - "sloth" - }, - { - "🦦", - "otter" - }, - { - "🦨", - "skunk" - }, - { - "🦘", - "kangaroo" - }, - { - "🦡", - "badger" - }, - { - "🐾", - "paw prints" - } - } - }, - { - "animal-bird", - { - { - "🦃", - "turkey" - }, - { - "🐔", - "chicken" - }, - { - "🐓", - "rooster" - }, - { - "🐣", - "hatching chick" - }, - { - "🐤", - "baby chick" - }, - { - "🐥", - "front-facing baby chick" - }, - { - "🐦", - "bird" - }, - { - "🐧", - "penguin" - }, - { - "🕊️", - "dove" - }, - { - "🦅", - "eagle" - }, - { - "🦆", - "duck" - }, - { - "🦢", - "swan" - }, - { - "🦉", - "owl" - }, - { - "🦤", - "dodo" - }, - { - "🪶", - "feather" - }, - { - "🦩", - "flamingo" - }, - { - "🦚", - "peacock" - }, - { - "🦜", - "parrot" - }, - { - "🪽", - "wing" - }, - { - "🐦‍⬛", - "black bird" - }, - { - "🪿", - "goose" - }, - { - "🐦‍🔥", - "phoenix" - } - } - }, - { - "animal-amphibian", - { - { - "🐸", - "frog" - } - } - }, - { - "animal-reptile", - { - { - "🐊", - "crocodile" - }, - { - "🐢", - "turtle" - }, - { - "🦎", - "lizard" - }, - { - "🐍", - "snake" - }, - { - "🐲", - "dragon face" - }, - { - "🐉", - "dragon" - }, - { - "🦕", - "sauropod" - }, - { - "🦖", - "T-Rex" - } - } - }, - { - "animal-marine", - { - { - "🐳", - "spouting whale" - }, - { - "🐋", - "whale" - }, - { - "🐬", - "dolphin" - }, - { - "🦭", - "seal" - }, - { - "🐟", - "fish" - }, - { - "🐠", - "tropical fish" - }, - { - "🐡", - "blowfish" - }, - { - "🦈", - "shark" - }, - { - "🐙", - "octopus" - }, - { - "🐚", - "spiral shell" - }, - { - "🪸", - "coral" - }, - { - "🪼", - "jellyfish" - } - } - }, - { - "animal-bug", - { - { - "🐌", - "snail" - }, - { - "🦋", - "butterfly" - }, - { - "🐛", - "bug" - }, - { - "🐜", - "ant" - }, - { - "🐝", - "honeybee" - }, - { - "🪲", - "beetle" - }, - { - "🐞", - "lady beetle" - }, - { - "🦗", - "cricket" - }, - { - "🪳", - "cockroach" - }, - { - "🕷️", - "spider" - }, - { - "🕸️", - "spider web" - }, - { - "🦂", - "scorpion" - }, - { - "🦟", - "mosquito" - }, - { - "🪰", - "fly" - }, - { - "🪱", - "worm" - }, - { - "🦠", - "microbe" - } - } - }, - { - "plant-flower", - { - { - "💐", - "bouquet" - }, - { - "🌸", - "cherry blossom" - }, - { - "💮", - "white flower" - }, - { - "🪷", - "lotus" - }, - { - "🏵️", - "rosette" - }, - { - "🌹", - "rose" - }, - { - "🥀", - "wilted flower" - }, - { - "🌺", - "hibiscus" - }, - { - "🌻", - "sunflower" - }, - { - "🌼", - "blossom" - }, - { - "🌷", - "tulip" - }, - { - "🪻", - "hyacinth" - } - } - }, - { - "plant-other", - { - { - "🌱", - "seedling" - }, - { - "🪴", - "potted plant" - }, - { - "🌲", - "evergreen tree" - }, - { - "🌳", - "deciduous tree" - }, - { - "🌴", - "palm tree" - }, - { - "🌵", - "cactus" - }, - { - "🌾", - "sheaf of rice" - }, - { - "🌿", - "herb" - }, - { - "☘️", - "shamrock" - }, - { - "🍀", - "four leaf clover" - }, - { - "🍁", - "maple leaf" - }, - { - "🍂", - "fallen leaf" - }, - { - "🍃", - "leaf fluttering in wind" - }, - { - "🪹", - "empty nest" - }, - { - "🪺", - "nest with eggs" - }, - { - "🍄", - "mushroom" - } - } - } - } - }, - { - QT_TR_NOOP("Food & Drink"), - { - { - "food-fruit", - { - { - "🍇", - "grapes" - }, - { - "🍈", - "melon" - }, - { - "🍉", - "watermelon" - }, - { - "🍊", - "tangerine" - }, - { - "🍋", - "lemon" - }, - { - "🍋‍🟩", - "lime" - }, - { - "🍌", - "banana" - }, - { - "🍍", - "pineapple" - }, - { - "🥭", - "mango" - }, - { - "🍎", - "red apple" - }, - { - "🍏", - "green apple" - }, - { - "🍐", - "pear" - }, - { - "🍑", - "peach" - }, - { - "🍒", - "cherries" - }, - { - "🍓", - "strawberry" - }, - { - "🫐", - "blueberries" - }, - { - "🥝", - "kiwi fruit" - }, - { - "🍅", - "tomato" - }, - { - "🫒", - "olive" - }, - { - "🥥", - "coconut" - } - } - }, - { - "food-vegetable", - { - { - "🥑", - "avocado" - }, - { - "🍆", - "eggplant" - }, - { - "🥔", - "potato" - }, - { - "🥕", - "carrot" - }, - { - "🌽", - "ear of corn" - }, - { - "🌶️", - "hot pepper" - }, - { - "🫑", - "bell pepper" - }, - { - "🥒", - "cucumber" - }, - { - "🥬", - "leafy green" - }, - { - "🥦", - "broccoli" - }, - { - "🧄", - "garlic" - }, - { - "🧅", - "onion" - }, - { - "🥜", - "peanuts" - }, - { - "🫘", - "beans" - }, - { - "🌰", - "chestnut" - }, - { - "🫚", - "ginger root" - }, - { - "🫛", - "pea pod" - }, - { - "🍄‍🟫", - "brown mushroom" - } - } - }, - { - "food-prepared", - { - { - "🍞", - "bread" - }, - { - "🥐", - "croissant" - }, - { - "🥖", - "baguette bread" - }, - { - "🫓", - "flatbread" - }, - { - "🥨", - "pretzel" - }, - { - "🥯", - "bagel" - }, - { - "🥞", - "pancakes" - }, - { - "🧇", - "waffle" - }, - { - "🧀", - "cheese wedge" - }, - { - "🍖", - "meat on bone" - }, - { - "🍗", - "poultry leg" - }, - { - "🥩", - "cut of meat" - }, - { - "🥓", - "bacon" - }, - { - "🍔", - "hamburger" - }, - { - "🍟", - "french fries" - }, - { - "🍕", - "pizza" - }, - { - "🌭", - "hot dog" - }, - { - "🥪", - "sandwich" - }, - { - "🌮", - "taco" - }, - { - "🌯", - "burrito" - }, - { - "🫔", - "tamale" - }, - { - "🥙", - "stuffed flatbread" - }, - { - "🧆", - "falafel" - }, - { - "🥚", - "egg" - }, - { - "🍳", - "cooking" - }, - { - "🥘", - "shallow pan of food" - }, - { - "🍲", - "pot of food" - }, - { - "🫕", - "fondue" - }, - { - "🥣", - "bowl with spoon" - }, - { - "🥗", - "green salad" - }, - { - "🍿", - "popcorn" - }, - { - "🧈", - "butter" - }, - { - "🧂", - "salt" - }, - { - "🥫", - "canned food" - } - } - }, - { - "food-asian", - { - { - "🍱", - "bento box" - }, - { - "🍘", - "rice cracker" - }, - { - "🍙", - "rice ball" - }, - { - "🍚", - "cooked rice" - }, - { - "🍛", - "curry rice" - }, - { - "🍜", - "steaming bowl" - }, - { - "🍝", - "spaghetti" - }, - { - "🍠", - "roasted sweet potato" - }, - { - "🍢", - "oden" - }, - { - "🍣", - "sushi" - }, - { - "🍤", - "fried shrimp" - }, - { - "🍥", - "fish cake with swirl" - }, - { - "🥮", - "moon cake" - }, - { - "🍡", - "dango" - }, - { - "🥟", - "dumpling" - }, - { - "🥠", - "fortune cookie" - }, - { - "🥡", - "takeout box" - } - } - }, - { - "food-marine", - { - { - "🦀", - "crab" - }, - { - "🦞", - "lobster" - }, - { - "🦐", - "shrimp" - }, - { - "🦑", - "squid" - }, - { - "🦪", - "oyster" - } - } - }, - { - "food-sweet", - { - { - "🍦", - "soft ice cream" - }, - { - "🍧", - "shaved ice" - }, - { - "🍨", - "ice cream" - }, - { - "🍩", - "doughnut" - }, - { - "🍪", - "cookie" - }, - { - "🎂", - "birthday cake" - }, - { - "🍰", - "shortcake" - }, - { - "🧁", - "cupcake" - }, - { - "🥧", - "pie" - }, - { - "🍫", - "chocolate bar" - }, - { - "🍬", - "candy" - }, - { - "🍭", - "lollipop" - }, - { - "🍮", - "custard" - }, - { - "🍯", - "honey pot" - } - } - }, - { - "drink", - { - { - "🍼", - "baby bottle" - }, - { - "🥛", - "glass of milk" - }, - { - "☕", - "hot beverage" - }, - { - "🫖", - "teapot" - }, - { - "🍵", - "teacup without handle" - }, - { - "🍶", - "sake" - }, - { - "🍾", - "bottle with popping cork" - }, - { - "🍷", - "wine glass" - }, - { - "🍸", - "cocktail glass" - }, - { - "🍹", - "tropical drink" - }, - { - "🍺", - "beer mug" - }, - { - "🍻", - "clinking beer mugs" - }, - { - "🥂", - "clinking glasses" - }, - { - "🥃", - "tumbler glass" - }, - { - "🫗", - "pouring liquid" - }, - { - "🥤", - "cup with straw" - }, - { - "🧋", - "bubble tea" - }, - { - "🧃", - "beverage box" - }, - { - "🧉", - "mate" - }, - { - "🧊", - "ice" - } - } - }, - { - "dishware", - { - { - "🥢", - "chopsticks" - }, - { - "🍽️", - "fork and knife with plate" - }, - { - "🍴", - "fork and knife" - }, - { - "🥄", - "spoon" - }, - { - "🔪", - "kitchen knife" - }, - { - "🫙", - "jar" - }, - { - "🏺", - "amphora" - } - } - } - } - }, - { - QT_TR_NOOP("Travel & Places"), - { - { - "place-map", - { - { - "🌍", - "globe showing Europe-Africa" - }, - { - "🌎", - "globe showing Americas" - }, - { - "🌏", - "globe showing Asia-Australia" - }, - { - "🌐", - "globe with meridians" - }, - { - "🗺️", - "world map" - }, - { - "🗾", - "map of Japan" - }, - { - "🧭", - "compass" - } - } - }, - { - "place-geographic", - { - { - "🏔️", - "snow-capped mountain" - }, - { - "⛰️", - "mountain" - }, - { - "🌋", - "volcano" - }, - { - "🗻", - "mount fuji" - }, - { - "🏕️", - "camping" - }, - { - "🏖️", - "beach with umbrella" - }, - { - "🏜️", - "desert" - }, - { - "🏝️", - "desert island" - }, - { - "🏞️", - "national park" - } - } - }, - { - "place-building", - { - { - "🏟️", - "stadium" - }, - { - "🏛️", - "classical building" - }, - { - "🏗️", - "building construction" - }, - { - "🧱", - "brick" - }, - { - "🪨", - "rock" - }, - { - "🪵", - "wood" - }, - { - "🛖", - "hut" - }, - { - "🏘️", - "houses" - }, - { - "🏚️", - "derelict house" - }, - { - "🏠", - "house" - }, - { - "🏡", - "house with garden" - }, - { - "🏢", - "office building" - }, - { - "🏣", - "Japanese post office" - }, - { - "🏤", - "post office" - }, - { - "🏥", - "hospital" - }, - { - "🏦", - "bank" - }, - { - "🏨", - "hotel" - }, - { - "🏩", - "love hotel" - }, - { - "🏪", - "convenience store" - }, - { - "🏫", - "school" - }, - { - "🏬", - "department store" - }, - { - "🏭", - "factory" - }, - { - "🏯", - "Japanese castle" - }, - { - "🏰", - "castle" - }, - { - "💒", - "wedding" - }, - { - "🗼", - "Tokyo tower" - }, - { - "🗽", - "Statue of Liberty" - } - } - }, - { - "place-religious", - { - { - "⛪", - "church" - }, - { - "🕌", - "mosque" - }, - { - "🛕", - "hindu temple" - }, - { - "🕍", - "synagogue" - }, - { - "⛩️", - "shinto shrine" - }, - { - "🕋", - "kaaba" - } - } - }, - { - "place-other", - { - { - "⛲", - "fountain" - }, - { - "⛺", - "tent" - }, - { - "🌁", - "foggy" - }, - { - "🌃", - "night with stars" - }, - { - "🏙️", - "cityscape" - }, - { - "🌄", - "sunrise over mountains" - }, - { - "🌅", - "sunrise" - }, - { - "🌆", - "cityscape at dusk" - }, - { - "🌇", - "sunset" - }, - { - "🌉", - "bridge at night" - }, - { - "♨️", - "hot springs" - }, - { - "🎠", - "carousel horse" - }, - { - "🛝", - "playground slide" - }, - { - "🎡", - "ferris wheel" - }, - { - "🎢", - "roller coaster" - }, - { - "💈", - "barber pole" - }, - { - "🎪", - "circus tent" - } - } - }, - { - "transport-ground", - { - { - "🚂", - "locomotive" - }, - { - "🚃", - "railway car" - }, - { - "🚄", - "high-speed train" - }, - { - "🚅", - "bullet train" - }, - { - "🚆", - "train" - }, - { - "🚇", - "metro" - }, - { - "🚈", - "light rail" - }, - { - "🚉", - "station" - }, - { - "🚊", - "tram" - }, - { - "🚝", - "monorail" - }, - { - "🚞", - "mountain railway" - }, - { - "🚋", - "tram car" - }, - { - "🚌", - "bus" - }, - { - "🚍", - "oncoming bus" - }, - { - "🚎", - "trolleybus" - }, - { - "🚐", - "minibus" - }, - { - "🚑", - "ambulance" - }, - { - "🚒", - "fire engine" - }, - { - "🚓", - "police car" - }, - { - "🚔", - "oncoming police car" - }, - { - "🚕", - "taxi" - }, - { - "🚖", - "oncoming taxi" - }, - { - "🚗", - "automobile" - }, - { - "🚘", - "oncoming automobile" - }, - { - "🚙", - "sport utility vehicle" - }, - { - "🛻", - "pickup truck" - }, - { - "🚚", - "delivery truck" - }, - { - "🚛", - "articulated lorry" - }, - { - "🚜", - "tractor" - }, - { - "🏎️", - "racing car" - }, - { - "🏍️", - "motorcycle" - }, - { - "🛵", - "motor scooter" - }, - { - "🦽", - "manual wheelchair" - }, - { - "🦼", - "motorized wheelchair" - }, - { - "🛺", - "auto rickshaw" - }, - { - "🚲", - "bicycle" - }, - { - "🛴", - "kick scooter" - }, - { - "🛹", - "skateboard" - }, - { - "🛼", - "roller skate" - }, - { - "🚏", - "bus stop" - }, - { - "🛣️", - "motorway" - }, - { - "🛤️", - "railway track" - }, - { - "🛢️", - "oil drum" - }, - { - "⛽", - "fuel pump" - }, - { - "🛞", - "wheel" - }, - { - "🚨", - "police car light" - }, - { - "🚥", - "horizontal traffic light" - }, - { - "🚦", - "vertical traffic light" - }, - { - "🛑", - "stop sign" - }, - { - "🚧", - "construction" - } - } - }, - { - "transport-water", - { - { - "⚓", - "anchor" - }, - { - "🛟", - "ring buoy" - }, - { - "⛵", - "sailboat" - }, - { - "🛶", - "canoe" - }, - { - "🚤", - "speedboat" - }, - { - "🛳️", - "passenger ship" - }, - { - "⛴️", - "ferry" - }, - { - "🛥️", - "motor boat" - }, - { - "🚢", - "ship" - } - } - }, - { - "transport-air", - { - { - "✈️", - "airplane" - }, - { - "🛩️", - "small airplane" - }, - { - "🛫", - "airplane departure" - }, - { - "🛬", - "airplane arrival" - }, - { - "🪂", - "parachute" - }, - { - "💺", - "seat" - }, - { - "🚁", - "helicopter" - }, - { - "🚟", - "suspension railway" - }, - { - "🚠", - "mountain cableway" - }, - { - "🚡", - "aerial tramway" - }, - { - "🛰️", - "satellite" - }, - { - "🚀", - "rocket" - }, - { - "🛸", - "flying saucer" - } - } - }, - { - "hotel", - { - { - "🛎️", - "bellhop bell" - }, - { - "🧳", - "luggage" - } - } - }, - { - "time", - { - { - "⌛", - "hourglass done" - }, - { - "⏳", - "hourglass not done" - }, - { - "⌚", - "watch" - }, - { - "⏰", - "alarm clock" - }, - { - "⏱️", - "stopwatch" - }, - { - "⏲️", - "timer clock" - }, - { - "🕰️", - "mantelpiece clock" - }, - { - "🕛", - "twelve o’clock" - }, - { - "🕧", - "twelve-thirty" - }, - { - "🕐", - "one o’clock" - }, - { - "🕜", - "one-thirty" - }, - { - "🕑", - "two o’clock" - }, - { - "🕝", - "two-thirty" - }, - { - "🕒", - "three o’clock" - }, - { - "🕞", - "three-thirty" - }, - { - "🕓", - "four o’clock" - }, - { - "🕟", - "four-thirty" - }, - { - "🕔", - "five o’clock" - }, - { - "🕠", - "five-thirty" - }, - { - "🕕", - "six o’clock" - }, - { - "🕡", - "six-thirty" - }, - { - "🕖", - "seven o’clock" - }, - { - "🕢", - "seven-thirty" - }, - { - "🕗", - "eight o’clock" - }, - { - "🕣", - "eight-thirty" - }, - { - "🕘", - "nine o’clock" - }, - { - "🕤", - "nine-thirty" - }, - { - "🕙", - "ten o’clock" - }, - { - "🕥", - "ten-thirty" - }, - { - "🕚", - "eleven o’clock" - }, - { - "🕦", - "eleven-thirty" - } - } - }, - { - "sky & weather", - { - { - "🌑", - "new moon" - }, - { - "🌒", - "waxing crescent moon" - }, - { - "🌓", - "first quarter moon" - }, - { - "🌔", - "waxing gibbous moon" - }, - { - "🌕", - "full moon" - }, - { - "🌖", - "waning gibbous moon" - }, - { - "🌗", - "last quarter moon" - }, - { - "🌘", - "waning crescent moon" - }, - { - "🌙", - "crescent moon" - }, - { - "🌚", - "new moon face" - }, - { - "🌛", - "first quarter moon face" - }, - { - "🌜", - "last quarter moon face" - }, - { - "🌡️", - "thermometer" - }, - { - "☀️", - "sun" - }, - { - "🌝", - "full moon face" - }, - { - "🌞", - "sun with face" - }, - { - "🪐", - "ringed planet" - }, - { - "⭐", - "star" - }, - { - "🌟", - "glowing star" - }, - { - "🌠", - "shooting star" - }, - { - "🌌", - "milky way" - }, - { - "☁️", - "cloud" - }, - { - "⛅", - "sun behind cloud" - }, - { - "⛈️", - "cloud with lightning and rain" - }, - { - "🌤️", - "sun behind small cloud" - }, - { - "🌥️", - "sun behind large cloud" - }, - { - "🌦️", - "sun behind rain cloud" - }, - { - "🌧️", - "cloud with rain" - }, - { - "🌨️", - "cloud with snow" - }, - { - "🌩️", - "cloud with lightning" - }, - { - "🌪️", - "tornado" - }, - { - "🌫️", - "fog" - }, - { - "🌬️", - "wind face" - }, - { - "🌀", - "cyclone" - }, - { - "🌈", - "rainbow" - }, - { - "🌂", - "closed umbrella" - }, - { - "☂️", - "umbrella" - }, - { - "☔", - "umbrella with rain drops" - }, - { - "⛱️", - "umbrella on ground" - }, - { - "⚡", - "high voltage" - }, - { - "❄️", - "snowflake" - }, - { - "☃️", - "snowman" - }, - { - "⛄", - "snowman without snow" - }, - { - "☄️", - "comet" - }, - { - "🔥", - "fire" - }, - { - "💧", - "droplet" - }, - { - "🌊", - "water wave" - } - } - } - } - }, - { - QT_TR_NOOP("Activities"), - { - { - "event", - { - { - "🎃", - "jack-o-lantern" - }, - { - "🎄", - "Christmas tree" - }, - { - "🎆", - "fireworks" - }, - { - "🎇", - "sparkler" - }, - { - "🧨", - "firecracker" - }, - { - "✨", - "sparkles" - }, - { - "🎈", - "balloon" - }, - { - "🎉", - "party popper" - }, - { - "🎊", - "confetti ball" - }, - { - "🎋", - "tanabata tree" - }, - { - "🎍", - "pine decoration" - }, - { - "🎎", - "Japanese dolls" - }, - { - "🎏", - "carp streamer" - }, - { - "🎐", - "wind chime" - }, - { - "🎑", - "moon viewing ceremony" - }, - { - "🧧", - "red envelope" - }, - { - "🎀", - "ribbon" - }, - { - "🎁", - "wrapped gift" - }, - { - "🎗️", - "reminder ribbon" - }, - { - "🎟️", - "admission tickets" - }, - { - "🎫", - "ticket" - } - } - }, - { - "award-medal", - { - { - "🎖️", - "military medal" - }, - { - "🏆", - "trophy" - }, - { - "🏅", - "sports medal" - }, - { - "🥇", - "1st place medal" - }, - { - "🥈", - "2nd place medal" - }, - { - "🥉", - "3rd place medal" - } - } - }, - { - "sport", - { - { - "⚽", - "soccer ball" - }, - { - "⚾", - "baseball" - }, - { - "🥎", - "softball" - }, - { - "🏀", - "basketball" - }, - { - "🏐", - "volleyball" - }, - { - "🏈", - "american football" - }, - { - "🏉", - "rugby football" - }, - { - "🎾", - "tennis" - }, - { - "🥏", - "flying disc" - }, - { - "🎳", - "bowling" - }, - { - "🏏", - "cricket game" - }, - { - "🏑", - "field hockey" - }, - { - "🏒", - "ice hockey" - }, - { - "🥍", - "lacrosse" - }, - { - "🏓", - "ping pong" - }, - { - "🏸", - "badminton" - }, - { - "🥊", - "boxing glove" - }, - { - "🥋", - "martial arts uniform" - }, - { - "🥅", - "goal net" - }, - { - "⛳", - "flag in hole" - }, - { - "⛸️", - "ice skate" - }, - { - "🎣", - "fishing pole" - }, - { - "🤿", - "diving mask" - }, - { - "🎽", - "running shirt" - }, - { - "🎿", - "skis" - }, - { - "🛷", - "sled" - }, - { - "🥌", - "curling stone" - } - } - }, - { - "game", - { - { - "🎯", - "bullseye" - }, - { - "🪀", - "yo-yo" - }, - { - "🪁", - "kite" - }, - { - "🔫", - "water pistol" - }, - { - "🎱", - "pool 8 ball" - }, - { - "🔮", - "crystal ball" - }, - { - "🪄", - "magic wand" - }, - { - "🎮", - "video game" - }, - { - "🕹️", - "joystick" - }, - { - "🎰", - "slot machine" - }, - { - "🎲", - "game die" - }, - { - "🧩", - "puzzle piece" - }, - { - "🧸", - "teddy bear" - }, - { - "🪅", - "piñata" - }, - { - "🪩", - "mirror ball" - }, - { - "🪆", - "nesting dolls" - }, - { - "♠️", - "spade suit" - }, - { - "♥️", - "heart suit" - }, - { - "♦️", - "diamond suit" - }, - { - "♣️", - "club suit" - }, - { - "♟️", - "chess pawn" - }, - { - "🃏", - "joker" - }, - { - "🀄", - "mahjong red dragon" - }, - { - "🎴", - "flower playing cards" - } - } - }, - { - "arts & crafts", - { - { - "🎭", - "performing arts" - }, - { - "🖼️", - "framed picture" - }, - { - "🎨", - "artist palette" - }, - { - "🧵", - "thread" - }, - { - "🪡", - "sewing needle" - }, - { - "🧶", - "yarn" - }, - { - "🪢", - "knot" - } - } - } - } - }, - { - QT_TR_NOOP("Objects"), - { - { - "clothing", - { - { - "👓", - "glasses" - }, - { - "🕶️", - "sunglasses" - }, - { - "🥽", - "goggles" - }, - { - "🥼", - "lab coat" - }, - { - "🦺", - "safety vest" - }, - { - "👔", - "necktie" - }, - { - "👕", - "t-shirt" - }, - { - "👖", - "jeans" - }, - { - "🧣", - "scarf" - }, - { - "🧤", - "gloves" - }, - { - "🧥", - "coat" - }, - { - "🧦", - "socks" - }, - { - "👗", - "dress" - }, - { - "👘", - "kimono" - }, - { - "🥻", - "sari" - }, - { - "🩱", - "one-piece swimsuit" - }, - { - "🩲", - "briefs" - }, - { - "🩳", - "shorts" - }, - { - "👙", - "bikini" - }, - { - "👚", - "woman’s clothes" - }, - { - "🪭", - "folding hand fan" - }, - { - "👛", - "purse" - }, - { - "👜", - "handbag" - }, - { - "👝", - "clutch bag" - }, - { - "🛍️", - "shopping bags" - }, - { - "🎒", - "backpack" - }, - { - "🩴", - "thong sandal" - }, - { - "👞", - "man’s shoe" - }, - { - "👟", - "running shoe" - }, - { - "🥾", - "hiking boot" - }, - { - "🥿", - "flat shoe" - }, - { - "👠", - "high-heeled shoe" - }, - { - "👡", - "woman’s sandal" - }, - { - "🩰", - "ballet shoes" - }, - { - "👢", - "woman’s boot" - }, - { - "🪮", - "hair pick" - }, - { - "👑", - "crown" - }, - { - "👒", - "woman’s hat" - }, - { - "🎩", - "top hat" - }, - { - "🎓", - "graduation cap" - }, - { - "🧢", - "billed cap" - }, - { - "🪖", - "military helmet" - }, - { - "⛑️", - "rescue worker’s helmet" - }, - { - "📿", - "prayer beads" - }, - { - "💄", - "lipstick" - }, - { - "💍", - "ring" - }, - { - "💎", - "gem stone" - } - } - }, - { - "sound", - { - { - "🔇", - "muted speaker" - }, - { - "🔈", - "speaker low volume" - }, - { - "🔉", - "speaker medium volume" - }, - { - "🔊", - "speaker high volume" - }, - { - "📢", - "loudspeaker" - }, - { - "📣", - "megaphone" - }, - { - "📯", - "postal horn" - }, - { - "🔔", - "bell" - }, - { - "🔕", - "bell with slash" - } - } - }, - { - "music", - { - { - "🎼", - "musical score" - }, - { - "🎵", - "musical note" - }, - { - "🎶", - "musical notes" - }, - { - "🎙️", - "studio microphone" - }, - { - "🎚️", - "level slider" - }, - { - "🎛️", - "control knobs" - }, - { - "🎤", - "microphone" - }, - { - "🎧", - "headphone" - }, - { - "📻", - "radio" - } - } - }, - { - "musical-instrument", - { - { - "🎷", - "saxophone" - }, - { - "🪗", - "accordion" - }, - { - "🎸", - "guitar" - }, - { - "🎹", - "musical keyboard" - }, - { - "🎺", - "trumpet" - }, - { - "🎻", - "violin" - }, - { - "🪕", - "banjo" - }, - { - "🥁", - "drum" - }, - { - "🪘", - "long drum" - }, - { - "🪇", - "maracas" - }, - { - "🪈", - "flute" - } - } - }, - { - "phone", - { - { - "📱", - "mobile phone" - }, - { - "📲", - "mobile phone with arrow" - }, - { - "☎️", - "telephone" - }, - { - "📞", - "telephone receiver" - }, - { - "📟", - "pager" - }, - { - "📠", - "fax machine" - } - } - }, - { - "computer", - { - { - "🔋", - "battery" - }, - { - "🪫", - "low battery" - }, - { - "🔌", - "electric plug" - }, - { - "💻", - "laptop" - }, - { - "🖥️", - "desktop computer" - }, - { - "🖨️", - "printer" - }, - { - "⌨️", - "keyboard" - }, - { - "🖱️", - "computer mouse" - }, - { - "🖲️", - "trackball" - }, - { - "💽", - "computer disk" - }, - { - "💾", - "floppy disk" - }, - { - "💿", - "optical disk" - }, - { - "📀", - "dvd" - }, - { - "🧮", - "abacus" - } - } - }, - { - "light & video", - { - { - "🎥", - "movie camera" - }, - { - "🎞️", - "film frames" - }, - { - "📽️", - "film projector" - }, - { - "🎬", - "clapper board" - }, - { - "📺", - "television" - }, - { - "📷", - "camera" - }, - { - "📸", - "camera with flash" - }, - { - "📹", - "video camera" - }, - { - "📼", - "videocassette" - }, - { - "🔍", - "magnifying glass tilted left" - }, - { - "🔎", - "magnifying glass tilted right" - }, - { - "🕯️", - "candle" - }, - { - "💡", - "light bulb" - }, - { - "🔦", - "flashlight" - }, - { - "🏮", - "red paper lantern" - }, - { - "🪔", - "diya lamp" - } - } - }, - { - "book-paper", - { - { - "📔", - "notebook with decorative cover" - }, - { - "📕", - "closed book" - }, - { - "📖", - "open book" - }, - { - "📗", - "green book" - }, - { - "📘", - "blue book" - }, - { - "📙", - "orange book" - }, - { - "📚", - "books" - }, - { - "📓", - "notebook" - }, - { - "📒", - "ledger" - }, - { - "📃", - "page with curl" - }, - { - "📜", - "scroll" - }, - { - "📄", - "page facing up" - }, - { - "📰", - "newspaper" - }, - { - "🗞️", - "rolled-up newspaper" - }, - { - "📑", - "bookmark tabs" - }, - { - "🔖", - "bookmark" - }, - { - "🏷️", - "label" - } - } - }, - { - "money", - { - { - "💰", - "money bag" - }, - { - "🪙", - "coin" - }, - { - "💴", - "yen banknote" - }, - { - "💵", - "dollar banknote" - }, - { - "💶", - "euro banknote" - }, - { - "💷", - "pound banknote" - }, - { - "💸", - "money with wings" - }, - { - "💳", - "credit card" - }, - { - "🧾", - "receipt" - }, - { - "💹", - "chart increasing with yen" - } - } - }, - { - "mail", - { - { - "✉️", - "envelope" - }, - { - "📧", - "e-mail" - }, - { - "📨", - "incoming envelope" - }, - { - "📩", - "envelope with arrow" - }, - { - "📤", - "outbox tray" - }, - { - "📥", - "inbox tray" - }, - { - "📦", - "package" - }, - { - "📫", - "closed mailbox with raised flag" - }, - { - "📪", - "closed mailbox with lowered flag" - }, - { - "📬", - "open mailbox with raised flag" - }, - { - "📭", - "open mailbox with lowered flag" - }, - { - "📮", - "postbox" - }, - { - "🗳️", - "ballot box with ballot" - } - } - }, - { - "writing", - { - { - "✏️", - "pencil" - }, - { - "✒️", - "black nib" - }, - { - "🖋️", - "fountain pen" - }, - { - "🖊️", - "pen" - }, - { - "🖌️", - "paintbrush" - }, - { - "🖍️", - "crayon" - }, - { - "📝", - "memo" - } - } - }, - { - "office", - { - { - "💼", - "briefcase" - }, - { - "📁", - "file folder" - }, - { - "📂", - "open file folder" - }, - { - "🗂️", - "card index dividers" - }, - { - "📅", - "calendar" - }, - { - "📆", - "tear-off calendar" - }, - { - "🗒️", - "spiral notepad" - }, - { - "🗓️", - "spiral calendar" - }, - { - "📇", - "card index" - }, - { - "📈", - "chart increasing" - }, - { - "📉", - "chart decreasing" - }, - { - "📊", - "bar chart" - }, - { - "📋", - "clipboard" - }, - { - "📌", - "pushpin" - }, - { - "📍", - "round pushpin" - }, - { - "📎", - "paperclip" - }, - { - "🖇️", - "linked paperclips" - }, - { - "📏", - "straight ruler" - }, - { - "📐", - "triangular ruler" - }, - { - "✂️", - "scissors" - }, - { - "🗃️", - "card file box" - }, - { - "🗄️", - "file cabinet" - }, - { - "🗑️", - "wastebasket" - } - } - }, - { - "lock", - { - { - "🔒", - "locked" - }, - { - "🔓", - "unlocked" - }, - { - "🔏", - "locked with pen" - }, - { - "🔐", - "locked with key" - }, - { - "🔑", - "key" - }, - { - "🗝️", - "old key" - } - } - }, - { - "tool", - { - { - "🔨", - "hammer" - }, - { - "🪓", - "axe" - }, - { - "⛏️", - "pick" - }, - { - "⚒️", - "hammer and pick" - }, - { - "🛠️", - "hammer and wrench" - }, - { - "🗡️", - "dagger" - }, - { - "⚔️", - "crossed swords" - }, - { - "💣", - "bomb" - }, - { - "🪃", - "boomerang" - }, - { - "🏹", - "bow and arrow" - }, - { - "🛡️", - "shield" - }, - { - "🪚", - "carpentry saw" - }, - { - "🔧", - "wrench" - }, - { - "🪛", - "screwdriver" - }, - { - "🔩", - "nut and bolt" - }, - { - "⚙️", - "gear" - }, - { - "🗜️", - "clamp" - }, - { - "⚖️", - "balance scale" - }, - { - "🦯", - "white cane" - }, - { - "🔗", - "link" - }, - { - "⛓️‍💥", - "broken chain" - }, - { - "⛓️", - "chains" - }, - { - "🪝", - "hook" - }, - { - "🧰", - "toolbox" - }, - { - "🧲", - "magnet" - }, - { - "🪜", - "ladder" - } - } - }, - { - "science", - { - { - "⚗️", - "alembic" - }, - { - "🧪", - "test tube" - }, - { - "🧫", - "petri dish" - }, - { - "🧬", - "dna" - }, - { - "🔬", - "microscope" - }, - { - "🔭", - "telescope" - }, - { - "📡", - "satellite antenna" - } - } - }, - { - "medical", - { - { - "💉", - "syringe" - }, - { - "🩸", - "drop of blood" - }, - { - "💊", - "pill" - }, - { - "🩹", - "adhesive bandage" - }, - { - "🩼", - "crutch" - }, - { - "🩺", - "stethoscope" - }, - { - "🩻", - "x-ray" - } - } - }, - { - "household", - { - { - "🚪", - "door" - }, - { - "🛗", - "elevator" - }, - { - "🪞", - "mirror" - }, - { - "🪟", - "window" - }, - { - "🛏️", - "bed" - }, - { - "🛋️", - "couch and lamp" - }, - { - "🪑", - "chair" - }, - { - "🚽", - "toilet" - }, - { - "🪠", - "plunger" - }, - { - "🚿", - "shower" - }, - { - "🛁", - "bathtub" - }, - { - "🪤", - "mouse trap" - }, - { - "🪒", - "razor" - }, - { - "🧴", - "lotion bottle" - }, - { - "🧷", - "safety pin" - }, - { - "🧹", - "broom" - }, - { - "🧺", - "basket" - }, - { - "🧻", - "roll of paper" - }, - { - "🪣", - "bucket" - }, - { - "🧼", - "soap" - }, - { - "🫧", - "bubbles" - }, - { - "🪥", - "toothbrush" - }, - { - "🧽", - "sponge" - }, - { - "🧯", - "fire extinguisher" - }, - { - "🛒", - "shopping cart" - } - } - }, - { - "other-object", - { - { - "🚬", - "cigarette" - }, - { - "⚰️", - "coffin" - }, - { - "🪦", - "headstone" - }, - { - "⚱️", - "funeral urn" - }, - { - "🧿", - "nazar amulet" - }, - { - "🪬", - "hamsa" - }, - { - "🗿", - "moai" - }, - { - "🪧", - "placard" - }, - { - "🪪", - "identification card" - } - } - } - } - }, - { - QT_TR_NOOP("Symbols"), - { - { - "transport-sign", - { - { - "🏧", - "ATM sign" - }, - { - "🚮", - "litter in bin sign" - }, - { - "🚰", - "potable water" - }, - { - "♿", - "wheelchair symbol" - }, - { - "🚹", - "men’s room" - }, - { - "🚺", - "women’s room" - }, - { - "🚻", - "restroom" - }, - { - "🚼", - "baby symbol" - }, - { - "🚾", - "water closet" - }, - { - "🛂", - "passport control" - }, - { - "🛃", - "customs" - }, - { - "🛄", - "baggage claim" - }, - { - "🛅", - "left luggage" - } - } - }, - { - "warning", - { - { - "⚠️", - "warning" - }, - { - "🚸", - "children crossing" - }, - { - "⛔", - "no entry" - }, - { - "🚫", - "prohibited" - }, - { - "🚳", - "no bicycles" - }, - { - "🚭", - "no smoking" - }, - { - "🚯", - "no littering" - }, - { - "🚱", - "non-potable water" - }, - { - "🚷", - "no pedestrians" - }, - { - "📵", - "no mobile phones" - }, - { - "🔞", - "no one under eighteen" - }, - { - "☢️", - "radioactive" - }, - { - "☣️", - "biohazard" - } - } - }, - { - "arrow", - { - { - "⬆️", - "up arrow" - }, - { - "↗️", - "up-right arrow" - }, - { - "➡️", - "right arrow" - }, - { - "↘️", - "down-right arrow" - }, - { - "⬇️", - "down arrow" - }, - { - "↙️", - "down-left arrow" - }, - { - "⬅️", - "left arrow" - }, - { - "↖️", - "up-left arrow" - }, - { - "↕️", - "up-down arrow" - }, - { - "↔️", - "left-right arrow" - }, - { - "↩️", - "right arrow curving left" - }, - { - "↪️", - "left arrow curving right" - }, - { - "⤴️", - "right arrow curving up" - }, - { - "⤵️", - "right arrow curving down" - }, - { - "🔃", - "clockwise vertical arrows" - }, - { - "🔄", - "counterclockwise arrows button" - }, - { - "🔙", - "BACK arrow" - }, - { - "🔚", - "END arrow" - }, - { - "🔛", - "ON! arrow" - }, - { - "🔜", - "SOON arrow" - }, - { - "🔝", - "TOP arrow" - } - } - }, - { - "religion", - { - { - "🛐", - "place of worship" - }, - { - "⚛️", - "atom symbol" - }, - { - "🕉️", - "om" - }, - { - "✡️", - "star of David" - }, - { - "☸️", - "wheel of dharma" - }, - { - "☯️", - "yin yang" - }, - { - "✝️", - "latin cross" - }, - { - "☦️", - "orthodox cross" - }, - { - "☪️", - "star and crescent" - }, - { - "☮️", - "peace symbol" - }, - { - "🕎", - "menorah" - }, - { - "🔯", - "dotted six-pointed star" - }, - { - "🪯", - "khanda" - } - } - }, - { - "zodiac", - { - { - "♈", - "Aries" - }, - { - "♉", - "Taurus" - }, - { - "♊", - "Gemini" - }, - { - "♋", - "Cancer" - }, - { - "♌", - "Leo" - }, - { - "♍", - "Virgo" - }, - { - "♎", - "Libra" - }, - { - "♏", - "Scorpio" - }, - { - "♐", - "Sagittarius" - }, - { - "♑", - "Capricorn" - }, - { - "♒", - "Aquarius" - }, - { - "♓", - "Pisces" - }, - { - "⛎", - "Ophiuchus" - } - } - }, - { - "av-symbol", - { - { - "🔀", - "shuffle tracks button" - }, - { - "🔁", - "repeat button" - }, - { - "🔂", - "repeat single button" - }, - { - "▶️", - "play button" - }, - { - "⏩", - "fast-forward button" - }, - { - "⏭️", - "next track button" - }, - { - "⏯️", - "play or pause button" - }, - { - "◀️", - "reverse button" - }, - { - "⏪", - "fast reverse button" - }, - { - "⏮️", - "last track button" - }, - { - "🔼", - "upwards button" - }, - { - "⏫", - "fast up button" - }, - { - "🔽", - "downwards button" - }, - { - "⏬", - "fast down button" - }, - { - "⏸️", - "pause button" - }, - { - "⏹️", - "stop button" - }, - { - "⏺️", - "record button" - }, - { - "⏏️", - "eject button" - }, - { - "🎦", - "cinema" - }, - { - "🔅", - "dim button" - }, - { - "🔆", - "bright button" - }, - { - "📶", - "antenna bars" - }, - { - "🛜", - "wireless" - }, - { - "📳", - "vibration mode" - }, - { - "📴", - "mobile phone off" - } - } - }, - { - "gender", - { - { - "♀️", - "female sign" - }, - { - "♂️", - "male sign" - }, - { - "⚧️", - "transgender symbol" - } - } - }, - { - "math", - { - { - "✖️", - "multiply" - }, - { - "➕", - "plus" - }, - { - "➖", - "minus" - }, - { - "➗", - "divide" - }, - { - "🟰", - "heavy equals sign" - }, - { - "♾️", - "infinity" - } - } - }, - { - "punctuation", - { - { - "‼️", - "double exclamation mark" - }, - { - "⁉️", - "exclamation question mark" - }, - { - "❓", - "red question mark" - }, - { - "❔", - "white question mark" - }, - { - "❕", - "white exclamation mark" - }, - { - "❗", - "red exclamation mark" - }, - { - "〰️", - "wavy dash" - } - } - }, - { - "currency", - { - { - "💱", - "currency exchange" - }, - { - "💲", - "heavy dollar sign" - } - } - }, - { - "other-symbol", - { - { - "⚕️", - "medical symbol" - }, - { - "♻️", - "recycling symbol" - }, - { - "⚜️", - "fleur-de-lis" - }, - { - "🔱", - "trident emblem" - }, - { - "📛", - "name badge" - }, - { - "🔰", - "Japanese symbol for beginner" - }, - { - "⭕", - "hollow red circle" - }, - { - "✅", - "check mark button" - }, - { - "☑️", - "check box with check" - }, - { - "✔️", - "check mark" - }, - { - "❌", - "cross mark" - }, - { - "❎", - "cross mark button" - }, - { - "➰", - "curly loop" - }, - { - "➿", - "double curly loop" - }, - { - "〽️", - "part alternation mark" - }, - { - "✳️", - "eight-spoked asterisk" - }, - { - "✴️", - "eight-pointed star" - }, - { - "❇️", - "sparkle" - }, - { - "©️", - "copyright" - }, - { - "®️", - "registered" - }, - { - "™️", - "trade mark" - } - } - }, - { - "keycap", - { - { - "#️⃣", - "keycap" - }, - { - "*️⃣", - "keycap" - }, - { - "0️⃣", - "keycap" - }, - { - "1️⃣", - "keycap" - }, - { - "2️⃣", - "keycap" - }, - { - "3️⃣", - "keycap" - }, - { - "4️⃣", - "keycap" - }, - { - "5️⃣", - "keycap" - }, - { - "6️⃣", - "keycap" - }, - { - "7️⃣", - "keycap" - }, - { - "8️⃣", - "keycap" - }, - { - "9️⃣", - "keycap" - }, - { - "🔟", - "keycap" - } - } - }, - { - "alphanum", - { - { - "🔠", - "input latin uppercase" - }, - { - "🔡", - "input latin lowercase" - }, - { - "🔢", - "input numbers" - }, - { - "🔣", - "input symbols" - }, - { - "🔤", - "input latin letters" - }, - { - "🅰️", - "A button (blood type)" - }, - { - "🆎", - "AB button (blood type)" - }, - { - "🅱️", - "B button (blood type)" - }, - { - "🆑", - "CL button" - }, - { - "🆒", - "COOL button" - }, - { - "🆓", - "FREE button" - }, - { - "ℹ️", - "information" - }, - { - "🆔", - "ID button" - }, - { - "Ⓜ️", - "circled M" - }, - { - "🆕", - "NEW button" - }, - { - "🆖", - "NG button" - }, - { - "🅾️", - "O button (blood type)" - }, - { - "🆗", - "OK button" - }, - { - "🅿️", - "P button" - }, - { - "🆘", - "SOS button" - }, - { - "🆙", - "UP! button" - }, - { - "🆚", - "VS button" - }, - { - "🈁", - "Japanese “here” button" - }, - { - "🈂️", - "Japanese “service charge” button" - }, - { - "🈷️", - "Japanese “monthly amount” button" - }, - { - "🈶", - "Japanese “not free of charge” button" - }, - { - "🈯", - "Japanese “reserved” button" - }, - { - "🉐", - "Japanese “bargain” button" - }, - { - "🈹", - "Japanese “discount” button" - }, - { - "🈚", - "Japanese “free of charge” button" - }, - { - "🈲", - "Japanese “prohibited” button" - }, - { - "🉑", - "Japanese “acceptable” button" - }, - { - "🈸", - "Japanese “application” button" - }, - { - "🈴", - "Japanese “passing grade” button" - }, - { - "🈳", - "Japanese “vacancy” button" - }, - { - "㊗️", - "Japanese “congratulations” button" - }, - { - "㊙️", - "Japanese “secret” button" - }, - { - "🈺", - "Japanese “open for business” button" - }, - { - "🈵", - "Japanese “no vacancy” button" - } - } - }, - { - "geometric", - { - { - "🔴", - "red circle" - }, - { - "🟠", - "orange circle" - }, - { - "🟡", - "yellow circle" - }, - { - "🟢", - "green circle" - }, - { - "🔵", - "blue circle" - }, - { - "🟣", - "purple circle" - }, - { - "🟤", - "brown circle" - }, - { - "⚫", - "black circle" - }, - { - "⚪", - "white circle" - }, - { - "🟥", - "red square" - }, - { - "🟧", - "orange square" - }, - { - "🟨", - "yellow square" - }, - { - "🟩", - "green square" - }, - { - "🟦", - "blue square" - }, - { - "🟪", - "purple square" - }, - { - "🟫", - "brown square" - }, - { - "⬛", - "black large square" - }, - { - "⬜", - "white large square" - }, - { - "◼️", - "black medium square" - }, - { - "◻️", - "white medium square" - }, - { - "◾", - "black medium-small square" - }, - { - "◽", - "white medium-small square" - }, - { - "▪️", - "black small square" - }, - { - "▫️", - "white small square" - }, - { - "🔶", - "large orange diamond" - }, - { - "🔷", - "large blue diamond" - }, - { - "🔸", - "small orange diamond" - }, - { - "🔹", - "small blue diamond" - }, - { - "🔺", - "red triangle pointed up" - }, - { - "🔻", - "red triangle pointed down" - }, - { - "💠", - "diamond with a dot" - }, - { - "🔘", - "radio button" - }, - { - "🔳", - "white square button" - }, - { - "🔲", - "black square button" - } - } - } - } - }, - { - QT_TR_NOOP("Flags"), - { - { - "flag", - { - { - "🏁", - "chequered flag" - }, - { - "🚩", - "triangular flag" - }, - { - "🎌", - "crossed flags" - }, - { - "🏴", - "black flag" - }, - { - "🏳️", - "white flag" - }, - { - "🏳️‍🌈", - "rainbow flag" - }, - { - "🏳️‍⚧️", - "transgender flag" - }, - { - "🏴‍☠️", - "pirate flag" - } - } - }, - { - "country-flag", - { - { - "🇦🇨", - "flag" - }, - { - "🇦🇩", - "flag" - }, - { - "🇦🇪", - "flag" - }, - { - "🇦🇫", - "flag" - }, - { - "🇦🇬", - "flag" - }, - { - "🇦🇮", - "flag" - }, - { - "🇦🇱", - "flag" - }, - { - "🇦🇲", - "flag" - }, - { - "🇦🇴", - "flag" - }, - { - "🇦🇶", - "flag" - }, - { - "🇦🇷", - "flag" - }, - { - "🇦🇸", - "flag" - }, - { - "🇦🇹", - "flag" - }, - { - "🇦🇺", - "flag" - }, - { - "🇦🇼", - "flag" - }, - { - "🇦🇽", - "flag" - }, - { - "🇦🇿", - "flag" - }, - { - "🇧🇦", - "flag" - }, - { - "🇧🇧", - "flag" - }, - { - "🇧🇩", - "flag" - }, - { - "🇧🇪", - "flag" - }, - { - "🇧🇫", - "flag" - }, - { - "🇧🇬", - "flag" - }, - { - "🇧🇭", - "flag" - }, - { - "🇧🇮", - "flag" - }, - { - "🇧🇯", - "flag" - }, - { - "🇧🇱", - "flag" - }, - { - "🇧🇲", - "flag" - }, - { - "🇧🇳", - "flag" - }, - { - "🇧🇴", - "flag" - }, - { - "🇧🇶", - "flag" - }, - { - "🇧🇷", - "flag" - }, - { - "🇧🇸", - "flag" - }, - { - "🇧🇹", - "flag" - }, - { - "🇧🇻", - "flag" - }, - { - "🇧🇼", - "flag" - }, - { - "🇧🇾", - "flag" - }, - { - "🇧🇿", - "flag" - }, - { - "🇨🇦", - "flag" - }, - { - "🇨🇨", - "flag" - }, - { - "🇨🇩", - "flag" - }, - { - "🇨🇫", - "flag" - }, - { - "🇨🇬", - "flag" - }, - { - "🇨🇭", - "flag" - }, - { - "🇨🇮", - "flag" - }, - { - "🇨🇰", - "flag" - }, - { - "🇨🇱", - "flag" - }, - { - "🇨🇲", - "flag" - }, - { - "🇨🇳", - "flag" - }, - { - "🇨🇴", - "flag" - }, - { - "🇨🇵", - "flag" - }, - { - "🇨🇷", - "flag" - }, - { - "🇨🇺", - "flag" - }, - { - "🇨🇻", - "flag" - }, - { - "🇨🇼", - "flag" - }, - { - "🇨🇽", - "flag" - }, - { - "🇨🇾", - "flag" - }, - { - "🇨🇿", - "flag" - }, - { - "🇩🇪", - "flag" - }, - { - "🇩🇬", - "flag" - }, - { - "🇩🇯", - "flag" - }, - { - "🇩🇰", - "flag" - }, - { - "🇩🇲", - "flag" - }, - { - "🇩🇴", - "flag" - }, - { - "🇩🇿", - "flag" - }, - { - "🇪🇦", - "flag" - }, - { - "🇪🇨", - "flag" - }, - { - "🇪🇪", - "flag" - }, - { - "🇪🇬", - "flag" - }, - { - "🇪🇭", - "flag" - }, - { - "🇪🇷", - "flag" - }, - { - "🇪🇸", - "flag" - }, - { - "🇪🇹", - "flag" - }, - { - "🇪🇺", - "flag" - }, - { - "🇫🇮", - "flag" - }, - { - "🇫🇯", - "flag" - }, - { - "🇫🇰", - "flag" - }, - { - "🇫🇲", - "flag" - }, - { - "🇫🇴", - "flag" - }, - { - "🇫🇷", - "flag" - }, - { - "🇬🇦", - "flag" - }, - { - "🇬🇧", - "flag" - }, - { - "🇬🇩", - "flag" - }, - { - "🇬🇪", - "flag" - }, - { - "🇬🇫", - "flag" - }, - { - "🇬🇬", - "flag" - }, - { - "🇬🇭", - "flag" - }, - { - "🇬🇮", - "flag" - }, - { - "🇬🇱", - "flag" - }, - { - "🇬🇲", - "flag" - }, - { - "🇬🇳", - "flag" - }, - { - "🇬🇵", - "flag" - }, - { - "🇬🇶", - "flag" - }, - { - "🇬🇷", - "flag" - }, - { - "🇬🇸", - "flag" - }, - { - "🇬🇹", - "flag" - }, - { - "🇬🇺", - "flag" - }, - { - "🇬🇼", - "flag" - }, - { - "🇬🇾", - "flag" - }, - { - "🇭🇰", - "flag" - }, - { - "🇭🇲", - "flag" - }, - { - "🇭🇳", - "flag" - }, - { - "🇭🇷", - "flag" - }, - { - "🇭🇹", - "flag" - }, - { - "🇭🇺", - "flag" - }, - { - "🇮🇨", - "flag" - }, - { - "🇮🇩", - "flag" - }, - { - "🇮🇪", - "flag" - }, - { - "🇮🇱", - "flag" - }, - { - "🇮🇲", - "flag" - }, - { - "🇮🇳", - "flag" - }, - { - "🇮🇴", - "flag" - }, - { - "🇮🇶", - "flag" - }, - { - "🇮🇷", - "flag" - }, - { - "🇮🇸", - "flag" - }, - { - "🇮🇹", - "flag" - }, - { - "🇯🇪", - "flag" - }, - { - "🇯🇲", - "flag" - }, - { - "🇯🇴", - "flag" - }, - { - "🇯🇵", - "flag" - }, - { - "🇰🇪", - "flag" - }, - { - "🇰🇬", - "flag" - }, - { - "🇰🇭", - "flag" - }, - { - "🇰🇮", - "flag" - }, - { - "🇰🇲", - "flag" - }, - { - "🇰🇳", - "flag" - }, - { - "🇰🇵", - "flag" - }, - { - "🇰🇷", - "flag" - }, - { - "🇰🇼", - "flag" - }, - { - "🇰🇾", - "flag" - }, - { - "🇰🇿", - "flag" - }, - { - "🇱🇦", - "flag" - }, - { - "🇱🇧", - "flag" - }, - { - "🇱🇨", - "flag" - }, - { - "🇱🇮", - "flag" - }, - { - "🇱🇰", - "flag" - }, - { - "🇱🇷", - "flag" - }, - { - "🇱🇸", - "flag" - }, - { - "🇱🇹", - "flag" - }, - { - "🇱🇺", - "flag" - }, - { - "🇱🇻", - "flag" - }, - { - "🇱🇾", - "flag" - }, - { - "🇲🇦", - "flag" - }, - { - "🇲🇨", - "flag" - }, - { - "🇲🇩", - "flag" - }, - { - "🇲🇪", - "flag" - }, - { - "🇲🇫", - "flag" - }, - { - "🇲🇬", - "flag" - }, - { - "🇲🇭", - "flag" - }, - { - "🇲🇰", - "flag" - }, - { - "🇲🇱", - "flag" - }, - { - "🇲🇲", - "flag" - }, - { - "🇲🇳", - "flag" - }, - { - "🇲🇴", - "flag" - }, - { - "🇲🇵", - "flag" - }, - { - "🇲🇶", - "flag" - }, - { - "🇲🇷", - "flag" - }, - { - "🇲🇸", - "flag" - }, - { - "🇲🇹", - "flag" - }, - { - "🇲🇺", - "flag" - }, - { - "🇲🇻", - "flag" - }, - { - "🇲🇼", - "flag" - }, - { - "🇲🇽", - "flag" - }, - { - "🇲🇾", - "flag" - }, - { - "🇲🇿", - "flag" - }, - { - "🇳🇦", - "flag" - }, - { - "🇳🇨", - "flag" - }, - { - "🇳🇪", - "flag" - }, - { - "🇳🇫", - "flag" - }, - { - "🇳🇬", - "flag" - }, - { - "🇳🇮", - "flag" - }, - { - "🇳🇱", - "flag" - }, - { - "🇳🇴", - "flag" - }, - { - "🇳🇵", - "flag" - }, - { - "🇳🇷", - "flag" - }, - { - "🇳🇺", - "flag" - }, - { - "🇳🇿", - "flag" - }, - { - "🇴🇲", - "flag" - }, - { - "🇵🇦", - "flag" - }, - { - "🇵🇪", - "flag" - }, - { - "🇵🇫", - "flag" - }, - { - "🇵🇬", - "flag" - }, - { - "🇵🇭", - "flag" - }, - { - "🇵🇰", - "flag" - }, - { - "🇵🇱", - "flag" - }, - { - "🇵🇲", - "flag" - }, - { - "🇵🇳", - "flag" - }, - { - "🇵🇷", - "flag" - }, - { - "🇵🇸", - "flag" - }, - { - "🇵🇹", - "flag" - }, - { - "🇵🇼", - "flag" - }, - { - "🇵🇾", - "flag" - }, - { - "🇶🇦", - "flag" - }, - { - "🇷🇪", - "flag" - }, - { - "🇷🇴", - "flag" - }, - { - "🇷🇸", - "flag" - }, - { - "🇷🇺", - "flag" - }, - { - "🇷🇼", - "flag" - }, - { - "🇸🇦", - "flag" - }, - { - "🇸🇧", - "flag" - }, - { - "🇸🇨", - "flag" - }, - { - "🇸🇩", - "flag" - }, - { - "🇸🇪", - "flag" - }, - { - "🇸🇬", - "flag" - }, - { - "🇸🇭", - "flag" - }, - { - "🇸🇮", - "flag" - }, - { - "🇸🇯", - "flag" - }, - { - "🇸🇰", - "flag" - }, - { - "🇸🇱", - "flag" - }, - { - "🇸🇲", - "flag" - }, - { - "🇸🇳", - "flag" - }, - { - "🇸🇴", - "flag" - }, - { - "🇸🇷", - "flag" - }, - { - "🇸🇸", - "flag" - }, - { - "🇸🇹", - "flag" - }, - { - "🇸🇻", - "flag" - }, - { - "🇸🇽", - "flag" - }, - { - "🇸🇾", - "flag" - }, - { - "🇸🇿", - "flag" - }, - { - "🇹🇦", - "flag" - }, - { - "🇹🇨", - "flag" - }, - { - "🇹🇩", - "flag" - }, - { - "🇹🇫", - "flag" - }, - { - "🇹🇬", - "flag" - }, - { - "🇹🇭", - "flag" - }, - { - "🇹🇯", - "flag" - }, - { - "🇹🇰", - "flag" - }, - { - "🇹🇱", - "flag" - }, - { - "🇹🇲", - "flag" - }, - { - "🇹🇳", - "flag" - }, - { - "🇹🇴", - "flag" - }, - { - "🇹🇷", - "flag" - }, - { - "🇹🇹", - "flag" - }, - { - "🇹🇻", - "flag" - }, - { - "🇹🇼", - "flag" - }, - { - "🇹🇿", - "flag" - }, - { - "🇺🇦", - "flag" - }, - { - "🇺🇬", - "flag" - }, - { - "🇺🇲", - "flag" - }, - { - "🇺🇳", - "flag" - }, - { - "🇺🇸", - "flag" - }, - { - "🇺🇾", - "flag" - }, - { - "🇺🇿", - "flag" - }, - { - "🇻🇦", - "flag" - }, - { - "🇻🇨", - "flag" - }, - { - "🇻🇪", - "flag" - }, - { - "🇻🇬", - "flag" - }, - { - "🇻🇮", - "flag" - }, - { - "🇻🇳", - "flag" - }, - { - "🇻🇺", - "flag" - }, - { - "🇼🇫", - "flag" - }, - { - "🇼🇸", - "flag" - }, - { - "🇽🇰", - "flag" - }, - { - "🇾🇪", - "flag" - }, - { - "🇾🇹", - "flag" - }, - { - "🇿🇦", - "flag" - }, - { - "🇿🇲", - "flag" - }, - { - "🇿🇼", - "flag" - } - } - }, - { - "subdivision-flag", - { - { - "🏴󠁧󠁢󠁥󠁮󠁧󠁿", - "flag" - }, - { - "🏴󠁧󠁢󠁳󠁣󠁴󠁿", - "flag" - }, - { - "🏴󠁧󠁢󠁷󠁬󠁳󠁿", - "flag" - } - } - } - } - } -}; - -static std::map ranges = { - {35, 35}, - {42, 42}, - {48, 57}, - {169, 169}, - {174, 174}, - {8252, 8252}, - {8265, 8265}, - {8482, 8482}, - {8505, 8505}, - {8596, 8601}, - {8617, 8618}, - {8986, 8987}, - {9000, 9000}, - {9167, 9167}, - {9193, 9203}, - {9208, 9210}, - {9410, 9410}, - {9642, 9643}, - {9654, 9654}, - {9664, 9664}, - {9723, 9726}, - {9728, 9732}, - {9742, 9742}, - {9745, 9745}, - {9748, 9749}, - {9752, 9752}, - {9757, 9757}, - {9760, 9760}, - {9762, 9763}, - {9766, 9766}, - {9770, 9770}, - {9774, 9775}, - {9784, 9786}, - {9792, 9792}, - {9794, 9794}, - {9800, 9811}, - {9823, 9824}, - {9827, 9827}, - {9829, 9830}, - {9832, 9832}, - {9851, 9851}, - {9854, 9855}, - {9874, 9879}, - {9881, 9881}, - {9883, 9884}, - {9888, 9889}, - {9895, 9895}, - {9898, 9899}, - {9904, 9905}, - {9917, 9918}, - {9924, 9925}, - {9928, 9928}, - {9934, 9935}, - {9937, 9937}, - {9939, 9940}, - {9961, 9962}, - {9968, 9973}, - {9975, 9978}, - {9981, 9981}, - {9986, 9986}, - {9989, 9989}, - {9992, 9997}, - {9999, 9999}, - {10002, 10002}, - {10004, 10004}, - {10006, 10006}, - {10013, 10013}, - {10017, 10017}, - {10024, 10024}, - {10035, 10036}, - {10052, 10052}, - {10055, 10055}, - {10060, 10060}, - {10062, 10062}, - {10067, 10069}, - {10071, 10071}, - {10083, 10084}, - {10133, 10135}, - {10145, 10145}, - {10160, 10160}, - {10175, 10175}, - {10548, 10549}, - {11013, 11015}, - {11035, 11036}, - {11088, 11088}, - {11093, 11093}, - {12336, 12336}, - {12349, 12349}, - {12951, 12951}, - {12953, 12953}, - {126980, 126980}, - {127183, 127183}, - {127344, 127345}, - {127358, 127359}, - {127374, 127374}, - {127377, 127386}, - {127462, 127487}, - {127489, 127490}, - {127514, 127514}, - {127535, 127535}, - {127538, 127546}, - {127568, 127569}, - {127744, 127777}, - {127780, 127891}, - {127894, 127895}, - {127897, 127899}, - {127902, 127984}, - {127987, 127989}, - {127991, 127994}, - {128000, 128253}, - {128255, 128317}, - {128329, 128334}, - {128336, 128359}, - {128367, 128368}, - {128371, 128378}, - {128391, 128391}, - {128394, 128397}, - {128400, 128400}, - {128405, 128406}, - {128420, 128421}, - {128424, 128424}, - {128433, 128434}, - {128444, 128444}, - {128450, 128452}, - {128465, 128467}, - {128476, 128478}, - {128481, 128481}, - {128483, 128483}, - {128488, 128488}, - {128495, 128495}, - {128499, 128499}, - {128506, 128591}, - {128640, 128709}, - {128715, 128722}, - {128725, 128727}, - {128732, 128741}, - {128745, 128745}, - {128747, 128748}, - {128752, 128752}, - {128755, 128764}, - {128992, 129003}, - {129008, 129008}, - {129292, 129338}, - {129340, 129349}, - {129351, 129455}, - {129460, 129535}, - {129648, 129660}, - {129664, 129672}, - {129680, 129725}, - {129727, 129733}, - {129742, 129755}, - {129760, 129768}, - {129776, 129784} -}; - -// clang-format on diff --git a/src/widgets/emojidb.h b/src/widgets/emojidb.h new file mode 100644 index 0000000000..ff133e0fba --- /dev/null +++ b/src/widgets/emojidb.h @@ -0,0 +1,8317 @@ +// This is a generated file. See emoji.py for details +// clang-format off + +#include "emojiregistry.h" + +#include + +EmojiRegistry::Group emoji_Smileys___Emotion { + QT_TR_NOOP("Smileys & Emotion"), + { + { + "face-smiling", + { + { + "😀", + "grinning face" + }, + { + "😃", + "grinning face with big eyes" + }, + { + "😄", + "grinning face with smiling eyes" + }, + { + "😁", + "beaming face with smiling eyes" + }, + { + "😆", + "grinning squinting face" + }, + { + "😅", + "grinning face with sweat" + }, + { + "🤣", + "rolling on the floor laughing" + }, + { + "😂", + "face with tears of joy" + }, + { + "🙂", + "slightly smiling face" + }, + { + "🙃", + "upside-down face" + }, + { + "🫠", + "melting face" + }, + { + "😉", + "winking face" + }, + { + "😊", + "smiling face with smiling eyes" + }, + { + "😇", + "smiling face with halo" + } + } + }, + { + "face-affection", + { + { + "🥰", + "smiling face with hearts" + }, + { + "😍", + "smiling face with heart-eyes" + }, + { + "🤩", + "star-struck" + }, + { + "😘", + "face blowing a kiss" + }, + { + "😗", + "kissing face" + }, + { + "☺️", + "smiling face" + }, + { + "😚", + "kissing face with closed eyes" + }, + { + "😙", + "kissing face with smiling eyes" + }, + { + "🥲", + "smiling face with tear" + } + } + }, + { + "face-tongue", + { + { + "😋", + "face savoring food" + }, + { + "😛", + "face with tongue" + }, + { + "😜", + "winking face with tongue" + }, + { + "🤪", + "zany face" + }, + { + "😝", + "squinting face with tongue" + }, + { + "🤑", + "money-mouth face" + } + } + }, + { + "face-hand", + { + { + "🤗", + "smiling face with open hands" + }, + { + "🤭", + "face with hand over mouth" + }, + { + "🫢", + "face with open eyes and hand over mouth" + }, + { + "🫣", + "face with peeking eye" + }, + { + "🤫", + "shushing face" + }, + { + "🤔", + "thinking face" + }, + { + "🫡", + "saluting face" + } + } + }, + { + "face-neutral-skeptical", + { + { + "🤐", + "zipper-mouth face" + }, + { + "🤨", + "face with raised eyebrow" + }, + { + "😐", + "neutral face" + }, + { + "😑", + "expressionless face" + }, + { + "😶", + "face without mouth" + }, + { + "🫥", + "dotted line face" + }, + { + "😶‍🌫️", + "face in clouds" + }, + { + "😏", + "smirking face" + }, + { + "😒", + "unamused face" + }, + { + "🙄", + "face with rolling eyes" + }, + { + "😬", + "grimacing face" + }, + { + "😮‍💨", + "face exhaling" + }, + { + "🤥", + "lying face" + }, + { + "🫨", + "shaking face" + }, + { + "🙂‍↔️", + "head shaking horizontally" + }, + { + "🙂‍↕️", + "head shaking vertically" + } + } + }, + { + "face-sleepy", + { + { + "😌", + "relieved face" + }, + { + "😔", + "pensive face" + }, + { + "😪", + "sleepy face" + }, + { + "🤤", + "drooling face" + }, + { + "😴", + "sleeping face" + } + } + }, + { + "face-unwell", + { + { + "😷", + "face with medical mask" + }, + { + "🤒", + "face with thermometer" + }, + { + "🤕", + "face with head-bandage" + }, + { + "🤢", + "nauseated face" + }, + { + "🤮", + "face vomiting" + }, + { + "🤧", + "sneezing face" + }, + { + "🥵", + "hot face" + }, + { + "🥶", + "cold face" + }, + { + "🥴", + "woozy face" + }, + { + "😵", + "face with crossed-out eyes" + }, + { + "😵‍💫", + "face with spiral eyes" + }, + { + "🤯", + "exploding head" + } + } + }, + { + "face-hat", + { + { + "🤠", + "cowboy hat face" + }, + { + "🥳", + "partying face" + }, + { + "🥸", + "disguised face" + } + } + }, + { + "face-glasses", + { + { + "😎", + "smiling face with sunglasses" + }, + { + "🤓", + "nerd face" + }, + { + "🧐", + "face with monocle" + } + } + }, + { + "face-concerned", + { + { + "😕", + "confused face" + }, + { + "🫤", + "face with diagonal mouth" + }, + { + "😟", + "worried face" + }, + { + "🙁", + "slightly frowning face" + }, + { + "☹️", + "frowning face" + }, + { + "😮", + "face with open mouth" + }, + { + "😯", + "hushed face" + }, + { + "😲", + "astonished face" + }, + { + "😳", + "flushed face" + }, + { + "🥺", + "pleading face" + }, + { + "🥹", + "face holding back tears" + }, + { + "😦", + "frowning face with open mouth" + }, + { + "😧", + "anguished face" + }, + { + "😨", + "fearful face" + }, + { + "😰", + "anxious face with sweat" + }, + { + "😥", + "sad but relieved face" + }, + { + "😢", + "crying face" + }, + { + "😭", + "loudly crying face" + }, + { + "😱", + "face screaming in fear" + }, + { + "😖", + "confounded face" + }, + { + "😣", + "persevering face" + }, + { + "😞", + "disappointed face" + }, + { + "😓", + "downcast face with sweat" + }, + { + "😩", + "weary face" + }, + { + "😫", + "tired face" + }, + { + "🥱", + "yawning face" + } + } + }, + { + "face-negative", + { + { + "😤", + "face with steam from nose" + }, + { + "😡", + "enraged face" + }, + { + "😠", + "angry face" + }, + { + "🤬", + "face with symbols on mouth" + }, + { + "😈", + "smiling face with horns" + }, + { + "👿", + "angry face with horns" + }, + { + "💀", + "skull" + }, + { + "☠️", + "skull and crossbones" + } + } + }, + { + "face-costume", + { + { + "💩", + "pile of poo" + }, + { + "🤡", + "clown face" + }, + { + "👹", + "ogre" + }, + { + "👺", + "goblin" + }, + { + "👻", + "ghost" + }, + { + "👽", + "alien" + }, + { + "👾", + "alien monster" + }, + { + "🤖", + "robot" + } + } + }, + { + "cat-face", + { + { + "😺", + "grinning cat" + }, + { + "😸", + "grinning cat with smiling eyes" + }, + { + "😹", + "cat with tears of joy" + }, + { + "😻", + "smiling cat with heart-eyes" + }, + { + "😼", + "cat with wry smile" + }, + { + "😽", + "kissing cat" + }, + { + "🙀", + "weary cat" + }, + { + "😿", + "crying cat" + }, + { + "😾", + "pouting cat" + } + } + }, + { + "monkey-face", + { + { + "🙈", + "see-no-evil monkey" + }, + { + "🙉", + "hear-no-evil monkey" + }, + { + "🙊", + "speak-no-evil monkey" + } + } + }, + { + "heart", + { + { + "💌", + "love letter" + }, + { + "💘", + "heart with arrow" + }, + { + "💝", + "heart with ribbon" + }, + { + "💖", + "sparkling heart" + }, + { + "💗", + "growing heart" + }, + { + "💓", + "beating heart" + }, + { + "💞", + "revolving hearts" + }, + { + "💕", + "two hearts" + }, + { + "💟", + "heart decoration" + }, + { + "❣️", + "heart exclamation" + }, + { + "💔", + "broken heart" + }, + { + "❤️‍🔥", + "heart on fire" + }, + { + "❤️‍🩹", + "mending heart" + }, + { + "❤️", + "red heart" + }, + { + "🩷", + "pink heart" + }, + { + "🧡", + "orange heart" + }, + { + "💛", + "yellow heart" + }, + { + "💚", + "green heart" + }, + { + "💙", + "blue heart" + }, + { + "🩵", + "light blue heart" + }, + { + "💜", + "purple heart" + }, + { + "🤎", + "brown heart" + }, + { + "🖤", + "black heart" + }, + { + "🩶", + "grey heart" + }, + { + "🤍", + "white heart" + } + } + }, + { + "emotion", + { + { + "💋", + "kiss mark" + }, + { + "💯", + "hundred points" + }, + { + "💢", + "anger symbol" + }, + { + "💥", + "collision" + }, + { + "💫", + "dizzy" + }, + { + "💦", + "sweat droplets" + }, + { + "💨", + "dashing away" + }, + { + "🕳️", + "hole" + }, + { + "💬", + "speech balloon" + }, + { + "👁️‍🗨️", + "eye in speech bubble" + }, + { + "🗨️", + "left speech bubble" + }, + { + "🗯️", + "right anger bubble" + }, + { + "💭", + "thought balloon" + }, + { + "💤", + "ZZZ" + } + } + } + } +}; + +EmojiRegistry::Group emoji_People___Body { + QT_TR_NOOP("People & Body"), + { + { + "hand-fingers-open", + { + { + "👋", + "waving hand" + }, + { + "🤚", + "raised back of hand" + }, + { + "🖐️", + "hand with fingers splayed" + }, + { + "✋", + "raised hand" + }, + { + "🖖", + "vulcan salute" + }, + { + "🫱", + "rightwards hand" + }, + { + "🫲", + "leftwards hand" + }, + { + "🫳", + "palm down hand" + }, + { + "🫴", + "palm up hand" + }, + { + "🫷", + "leftwards pushing hand" + }, + { + "🫸", + "rightwards pushing hand" + } + } + }, + { + "hand-fingers-partial", + { + { + "👌", + "OK hand" + }, + { + "🤌", + "pinched fingers" + }, + { + "🤏", + "pinching hand" + }, + { + "✌️", + "victory hand" + }, + { + "🤞", + "crossed fingers" + }, + { + "🫰", + "hand with index finger and thumb crossed" + }, + { + "🤟", + "love-you gesture" + }, + { + "🤘", + "sign of the horns" + }, + { + "🤙", + "call me hand" + } + } + }, + { + "hand-single-finger", + { + { + "👈", + "backhand index pointing left" + }, + { + "👉", + "backhand index pointing right" + }, + { + "👆", + "backhand index pointing up" + }, + { + "🖕", + "middle finger" + }, + { + "👇", + "backhand index pointing down" + }, + { + "☝️", + "index pointing up" + }, + { + "🫵", + "index pointing at the viewer" + } + } + }, + { + "hand-fingers-closed", + { + { + "👍", + "thumbs up" + }, + { + "👎", + "thumbs down" + }, + { + "✊", + "raised fist" + }, + { + "👊", + "oncoming fist" + }, + { + "🤛", + "left-facing fist" + }, + { + "🤜", + "right-facing fist" + } + } + }, + { + "hands", + { + { + "👏", + "clapping hands" + }, + { + "🙌", + "raising hands" + }, + { + "🫶", + "heart hands" + }, + { + "👐", + "open hands" + }, + { + "🤲", + "palms up together" + }, + { + "🤝", + "handshake" + }, + { + "🙏", + "folded hands" + } + } + }, + { + "hand-prop", + { + { + "✍️", + "writing hand" + }, + { + "💅", + "nail polish" + }, + { + "🤳", + "selfie" + } + } + }, + { + "body-parts", + { + { + "💪", + "flexed biceps" + }, + { + "🦾", + "mechanical arm" + }, + { + "🦿", + "mechanical leg" + }, + { + "🦵", + "leg" + }, + { + "🦶", + "foot" + }, + { + "👂", + "ear" + }, + { + "🦻", + "ear with hearing aid" + }, + { + "👃", + "nose" + }, + { + "🧠", + "brain" + }, + { + "🫀", + "anatomical heart" + }, + { + "🫁", + "lungs" + }, + { + "🦷", + "tooth" + }, + { + "🦴", + "bone" + }, + { + "👀", + "eyes" + }, + { + "👁️", + "eye" + }, + { + "👅", + "tongue" + }, + { + "👄", + "mouth" + }, + { + "🫦", + "biting lip" + } + } + }, + { + "person", + { + { + "👶", + "baby" + }, + { + "🧒", + "child" + }, + { + "👦", + "boy" + }, + { + "👧", + "girl" + }, + { + "🧑", + "person" + }, + { + "👱", + "person" + }, + { + "👨", + "man" + }, + { + "🧔", + "person" + }, + { + "🧔‍♂️", + "man" + }, + { + "🧔‍♀️", + "woman" + }, + { + "👨‍🦰", + "man" + }, + { + "👨‍🦱", + "man" + }, + { + "👨‍🦳", + "man" + }, + { + "👨‍🦲", + "man" + }, + { + "👩", + "woman" + }, + { + "👩‍🦰", + "woman" + }, + { + "🧑‍🦰", + "person" + }, + { + "👩‍🦱", + "woman" + }, + { + "🧑‍🦱", + "person" + }, + { + "👩‍🦳", + "woman" + }, + { + "🧑‍🦳", + "person" + }, + { + "👩‍🦲", + "woman" + }, + { + "🧑‍🦲", + "person" + }, + { + "👱‍♀️", + "woman" + }, + { + "👱‍♂️", + "man" + }, + { + "🧓", + "older person" + }, + { + "👴", + "old man" + }, + { + "👵", + "old woman" + } + } + }, + { + "person-gesture", + { + { + "🙍", + "person frowning" + }, + { + "🙍‍♂️", + "man frowning" + }, + { + "🙍‍♀️", + "woman frowning" + }, + { + "🙎", + "person pouting" + }, + { + "🙎‍♂️", + "man pouting" + }, + { + "🙎‍♀️", + "woman pouting" + }, + { + "🙅", + "person gesturing NO" + }, + { + "🙅‍♂️", + "man gesturing NO" + }, + { + "🙅‍♀️", + "woman gesturing NO" + }, + { + "🙆", + "person gesturing OK" + }, + { + "🙆‍♂️", + "man gesturing OK" + }, + { + "🙆‍♀️", + "woman gesturing OK" + }, + { + "💁", + "person tipping hand" + }, + { + "💁‍♂️", + "man tipping hand" + }, + { + "💁‍♀️", + "woman tipping hand" + }, + { + "🙋", + "person raising hand" + }, + { + "🙋‍♂️", + "man raising hand" + }, + { + "🙋‍♀️", + "woman raising hand" + }, + { + "🧏", + "deaf person" + }, + { + "🧏‍♂️", + "deaf man" + }, + { + "🧏‍♀️", + "deaf woman" + }, + { + "🙇", + "person bowing" + }, + { + "🙇‍♂️", + "man bowing" + }, + { + "🙇‍♀️", + "woman bowing" + }, + { + "🤦", + "person facepalming" + }, + { + "🤦‍♂️", + "man facepalming" + }, + { + "🤦‍♀️", + "woman facepalming" + }, + { + "🤷", + "person shrugging" + }, + { + "🤷‍♂️", + "man shrugging" + }, + { + "🤷‍♀️", + "woman shrugging" + } + } + }, + { + "person-role", + { + { + "🧑‍⚕️", + "health worker" + }, + { + "👨‍⚕️", + "man health worker" + }, + { + "👩‍⚕️", + "woman health worker" + }, + { + "🧑‍🎓", + "student" + }, + { + "👨‍🎓", + "man student" + }, + { + "👩‍🎓", + "woman student" + }, + { + "🧑‍🏫", + "teacher" + }, + { + "👨‍🏫", + "man teacher" + }, + { + "👩‍🏫", + "woman teacher" + }, + { + "🧑‍⚖️", + "judge" + }, + { + "👨‍⚖️", + "man judge" + }, + { + "👩‍⚖️", + "woman judge" + }, + { + "🧑‍🌾", + "farmer" + }, + { + "👨‍🌾", + "man farmer" + }, + { + "👩‍🌾", + "woman farmer" + }, + { + "🧑‍🍳", + "cook" + }, + { + "👨‍🍳", + "man cook" + }, + { + "👩‍🍳", + "woman cook" + }, + { + "🧑‍🔧", + "mechanic" + }, + { + "👨‍🔧", + "man mechanic" + }, + { + "👩‍🔧", + "woman mechanic" + }, + { + "🧑‍🏭", + "factory worker" + }, + { + "👨‍🏭", + "man factory worker" + }, + { + "👩‍🏭", + "woman factory worker" + }, + { + "🧑‍💼", + "office worker" + }, + { + "👨‍💼", + "man office worker" + }, + { + "👩‍💼", + "woman office worker" + }, + { + "🧑‍🔬", + "scientist" + }, + { + "👨‍🔬", + "man scientist" + }, + { + "👩‍🔬", + "woman scientist" + }, + { + "🧑‍💻", + "technologist" + }, + { + "👨‍💻", + "man technologist" + }, + { + "👩‍💻", + "woman technologist" + }, + { + "🧑‍🎤", + "singer" + }, + { + "👨‍🎤", + "man singer" + }, + { + "👩‍🎤", + "woman singer" + }, + { + "🧑‍🎨", + "artist" + }, + { + "👨‍🎨", + "man artist" + }, + { + "👩‍🎨", + "woman artist" + }, + { + "🧑‍✈️", + "pilot" + }, + { + "👨‍✈️", + "man pilot" + }, + { + "👩‍✈️", + "woman pilot" + }, + { + "🧑‍🚀", + "astronaut" + }, + { + "👨‍🚀", + "man astronaut" + }, + { + "👩‍🚀", + "woman astronaut" + }, + { + "🧑‍🚒", + "firefighter" + }, + { + "👨‍🚒", + "man firefighter" + }, + { + "👩‍🚒", + "woman firefighter" + }, + { + "👮", + "police officer" + }, + { + "👮‍♂️", + "man police officer" + }, + { + "👮‍♀️", + "woman police officer" + }, + { + "🕵️", + "detective" + }, + { + "🕵️‍♂️", + "man detective" + }, + { + "🕵️‍♀️", + "woman detective" + }, + { + "💂", + "guard" + }, + { + "💂‍♂️", + "man guard" + }, + { + "💂‍♀️", + "woman guard" + }, + { + "🥷", + "ninja" + }, + { + "👷", + "construction worker" + }, + { + "👷‍♂️", + "man construction worker" + }, + { + "👷‍♀️", + "woman construction worker" + }, + { + "🫅", + "person with crown" + }, + { + "🤴", + "prince" + }, + { + "👸", + "princess" + }, + { + "👳", + "person wearing turban" + }, + { + "👳‍♂️", + "man wearing turban" + }, + { + "👳‍♀️", + "woman wearing turban" + }, + { + "👲", + "person with skullcap" + }, + { + "🧕", + "woman with headscarf" + }, + { + "🤵", + "person in tuxedo" + }, + { + "🤵‍♂️", + "man in tuxedo" + }, + { + "🤵‍♀️", + "woman in tuxedo" + }, + { + "👰", + "person with veil" + }, + { + "👰‍♂️", + "man with veil" + }, + { + "👰‍♀️", + "woman with veil" + }, + { + "🤰", + "pregnant woman" + }, + { + "🫃", + "pregnant man" + }, + { + "🫄", + "pregnant person" + }, + { + "🤱", + "breast-feeding" + }, + { + "👩‍🍼", + "woman feeding baby" + }, + { + "👨‍🍼", + "man feeding baby" + }, + { + "🧑‍🍼", + "person feeding baby" + } + } + }, + { + "person-fantasy", + { + { + "👼", + "baby angel" + }, + { + "🎅", + "Santa Claus" + }, + { + "🤶", + "Mrs. Claus" + }, + { + "🧑‍🎄", + "mx claus" + }, + { + "🦸", + "superhero" + }, + { + "🦸‍♂️", + "man superhero" + }, + { + "🦸‍♀️", + "woman superhero" + }, + { + "🦹", + "supervillain" + }, + { + "🦹‍♂️", + "man supervillain" + }, + { + "🦹‍♀️", + "woman supervillain" + }, + { + "🧙", + "mage" + }, + { + "🧙‍♂️", + "man mage" + }, + { + "🧙‍♀️", + "woman mage" + }, + { + "🧚", + "fairy" + }, + { + "🧚‍♂️", + "man fairy" + }, + { + "🧚‍♀️", + "woman fairy" + }, + { + "🧛", + "vampire" + }, + { + "🧛‍♂️", + "man vampire" + }, + { + "🧛‍♀️", + "woman vampire" + }, + { + "🧜", + "merperson" + }, + { + "🧜‍♂️", + "merman" + }, + { + "🧜‍♀️", + "mermaid" + }, + { + "🧝", + "elf" + }, + { + "🧝‍♂️", + "man elf" + }, + { + "🧝‍♀️", + "woman elf" + }, + { + "🧞", + "genie" + }, + { + "🧞‍♂️", + "man genie" + }, + { + "🧞‍♀️", + "woman genie" + }, + { + "🧟", + "zombie" + }, + { + "🧟‍♂️", + "man zombie" + }, + { + "🧟‍♀️", + "woman zombie" + }, + { + "🧌", + "troll" + } + } + }, + { + "person-activity", + { + { + "💆", + "person getting massage" + }, + { + "💆‍♂️", + "man getting massage" + }, + { + "💆‍♀️", + "woman getting massage" + }, + { + "💇", + "person getting haircut" + }, + { + "💇‍♂️", + "man getting haircut" + }, + { + "💇‍♀️", + "woman getting haircut" + }, + { + "🚶", + "person walking" + }, + { + "🚶‍♂️", + "man walking" + }, + { + "🚶‍♀️", + "woman walking" + }, + { + "🚶‍➡️", + "person walking facing right" + }, + { + "🚶‍♀️‍➡️", + "woman walking facing right" + }, + { + "🚶‍♂️‍➡️", + "man walking facing right" + }, + { + "🧍", + "person standing" + }, + { + "🧍‍♂️", + "man standing" + }, + { + "🧍‍♀️", + "woman standing" + }, + { + "🧎", + "person kneeling" + }, + { + "🧎‍♂️", + "man kneeling" + }, + { + "🧎‍♀️", + "woman kneeling" + }, + { + "🧎‍➡️", + "person kneeling facing right" + }, + { + "🧎‍♀️‍➡️", + "woman kneeling facing right" + }, + { + "🧎‍♂️‍➡️", + "man kneeling facing right" + }, + { + "🧑‍🦯", + "person with white cane" + }, + { + "🧑‍🦯‍➡️", + "person with white cane facing right" + }, + { + "👨‍🦯", + "man with white cane" + }, + { + "👨‍🦯‍➡️", + "man with white cane facing right" + }, + { + "👩‍🦯", + "woman with white cane" + }, + { + "👩‍🦯‍➡️", + "woman with white cane facing right" + }, + { + "🧑‍🦼", + "person in motorized wheelchair" + }, + { + "🧑‍🦼‍➡️", + "person in motorized wheelchair facing right" + }, + { + "👨‍🦼", + "man in motorized wheelchair" + }, + { + "👨‍🦼‍➡️", + "man in motorized wheelchair facing right" + }, + { + "👩‍🦼", + "woman in motorized wheelchair" + }, + { + "👩‍🦼‍➡️", + "woman in motorized wheelchair facing right" + }, + { + "🧑‍🦽", + "person in manual wheelchair" + }, + { + "🧑‍🦽‍➡️", + "person in manual wheelchair facing right" + }, + { + "👨‍🦽", + "man in manual wheelchair" + }, + { + "👨‍🦽‍➡️", + "man in manual wheelchair facing right" + }, + { + "👩‍🦽", + "woman in manual wheelchair" + }, + { + "👩‍🦽‍➡️", + "woman in manual wheelchair facing right" + }, + { + "🏃", + "person running" + }, + { + "🏃‍♂️", + "man running" + }, + { + "🏃‍♀️", + "woman running" + }, + { + "🏃‍➡️", + "person running facing right" + }, + { + "🏃‍♀️‍➡️", + "woman running facing right" + }, + { + "🏃‍♂️‍➡️", + "man running facing right" + }, + { + "💃", + "woman dancing" + }, + { + "🕺", + "man dancing" + }, + { + "🕴️", + "person in suit levitating" + }, + { + "👯", + "people with bunny ears" + }, + { + "👯‍♂️", + "men with bunny ears" + }, + { + "👯‍♀️", + "women with bunny ears" + }, + { + "🧖", + "person in steamy room" + }, + { + "🧖‍♂️", + "man in steamy room" + }, + { + "🧖‍♀️", + "woman in steamy room" + }, + { + "🧗", + "person climbing" + }, + { + "🧗‍♂️", + "man climbing" + }, + { + "🧗‍♀️", + "woman climbing" + } + } + }, + { + "person-sport", + { + { + "🤺", + "person fencing" + }, + { + "🏇", + "horse racing" + }, + { + "⛷️", + "skier" + }, + { + "🏂", + "snowboarder" + }, + { + "🏌️", + "person golfing" + }, + { + "🏌️‍♂️", + "man golfing" + }, + { + "🏌️‍♀️", + "woman golfing" + }, + { + "🏄", + "person surfing" + }, + { + "🏄‍♂️", + "man surfing" + }, + { + "🏄‍♀️", + "woman surfing" + }, + { + "🚣", + "person rowing boat" + }, + { + "🚣‍♂️", + "man rowing boat" + }, + { + "🚣‍♀️", + "woman rowing boat" + }, + { + "🏊", + "person swimming" + }, + { + "🏊‍♂️", + "man swimming" + }, + { + "🏊‍♀️", + "woman swimming" + }, + { + "⛹️", + "person bouncing ball" + }, + { + "⛹️‍♂️", + "man bouncing ball" + }, + { + "⛹️‍♀️", + "woman bouncing ball" + }, + { + "🏋️", + "person lifting weights" + }, + { + "🏋️‍♂️", + "man lifting weights" + }, + { + "🏋️‍♀️", + "woman lifting weights" + }, + { + "🚴", + "person biking" + }, + { + "🚴‍♂️", + "man biking" + }, + { + "🚴‍♀️", + "woman biking" + }, + { + "🚵", + "person mountain biking" + }, + { + "🚵‍♂️", + "man mountain biking" + }, + { + "🚵‍♀️", + "woman mountain biking" + }, + { + "🤸", + "person cartwheeling" + }, + { + "🤸‍♂️", + "man cartwheeling" + }, + { + "🤸‍♀️", + "woman cartwheeling" + }, + { + "🤼", + "people wrestling" + }, + { + "🤼‍♂️", + "men wrestling" + }, + { + "🤼‍♀️", + "women wrestling" + }, + { + "🤽", + "person playing water polo" + }, + { + "🤽‍♂️", + "man playing water polo" + }, + { + "🤽‍♀️", + "woman playing water polo" + }, + { + "🤾", + "person playing handball" + }, + { + "🤾‍♂️", + "man playing handball" + }, + { + "🤾‍♀️", + "woman playing handball" + }, + { + "🤹", + "person juggling" + }, + { + "🤹‍♂️", + "man juggling" + }, + { + "🤹‍♀️", + "woman juggling" + } + } + }, + { + "person-resting", + { + { + "🧘", + "person in lotus position" + }, + { + "🧘‍♂️", + "man in lotus position" + }, + { + "🧘‍♀️", + "woman in lotus position" + }, + { + "🛀", + "person taking bath" + }, + { + "🛌", + "person in bed" + } + } + }, + { + "family", + { + { + "🧑‍🤝‍🧑", + "people holding hands" + }, + { + "👭", + "women holding hands" + }, + { + "👫", + "woman and man holding hands" + }, + { + "👬", + "men holding hands" + }, + { + "💏", + "kiss" + }, + { + "👩‍❤️‍💋‍👨", + "kiss" + }, + { + "👨‍❤️‍💋‍👨", + "kiss" + }, + { + "👩‍❤️‍💋‍👩", + "kiss" + }, + { + "💑", + "couple with heart" + }, + { + "👩‍❤️‍👨", + "couple with heart" + }, + { + "👨‍❤️‍👨", + "couple with heart" + }, + { + "👩‍❤️‍👩", + "couple with heart" + }, + { + "👨‍👩‍👦", + "family" + }, + { + "👨‍👩‍👧", + "family" + }, + { + "👨‍👩‍👧‍👦", + "family" + }, + { + "👨‍👩‍👦‍👦", + "family" + }, + { + "👨‍👩‍👧‍👧", + "family" + }, + { + "👨‍👨‍👦", + "family" + }, + { + "👨‍👨‍👧", + "family" + }, + { + "👨‍👨‍👧‍👦", + "family" + }, + { + "👨‍👨‍👦‍👦", + "family" + }, + { + "👨‍👨‍👧‍👧", + "family" + }, + { + "👩‍👩‍👦", + "family" + }, + { + "👩‍👩‍👧", + "family" + }, + { + "👩‍👩‍👧‍👦", + "family" + }, + { + "👩‍👩‍👦‍👦", + "family" + }, + { + "👩‍👩‍👧‍👧", + "family" + }, + { + "👨‍👦", + "family" + }, + { + "👨‍👦‍👦", + "family" + }, + { + "👨‍👧", + "family" + }, + { + "👨‍👧‍👦", + "family" + }, + { + "👨‍👧‍👧", + "family" + }, + { + "👩‍👦", + "family" + }, + { + "👩‍👦‍👦", + "family" + }, + { + "👩‍👧", + "family" + }, + { + "👩‍👧‍👦", + "family" + }, + { + "👩‍👧‍👧", + "family" + } + } + }, + { + "person-symbol", + { + { + "🗣️", + "speaking head" + }, + { + "👤", + "bust in silhouette" + }, + { + "👥", + "busts in silhouette" + }, + { + "🫂", + "people hugging" + }, + { + "👪", + "family" + }, + { + "🧑‍🧑‍🧒", + "family" + }, + { + "🧑‍🧑‍🧒‍🧒", + "family" + }, + { + "🧑‍🧒", + "family" + }, + { + "🧑‍🧒‍🧒", + "family" + }, + { + "👣", + "footprints" + } + } + } + } +}; + +EmojiRegistry::Group emoji_Animals___Nature { + QT_TR_NOOP("Animals & Nature"), + { + { + "animal-mammal", + { + { + "🐵", + "monkey face" + }, + { + "🐒", + "monkey" + }, + { + "🦍", + "gorilla" + }, + { + "🦧", + "orangutan" + }, + { + "🐶", + "dog face" + }, + { + "🐕", + "dog" + }, + { + "🦮", + "guide dog" + }, + { + "🐕‍🦺", + "service dog" + }, + { + "🐩", + "poodle" + }, + { + "🐺", + "wolf" + }, + { + "🦊", + "fox" + }, + { + "🦝", + "raccoon" + }, + { + "🐱", + "cat face" + }, + { + "🐈", + "cat" + }, + { + "🐈‍⬛", + "black cat" + }, + { + "🦁", + "lion" + }, + { + "🐯", + "tiger face" + }, + { + "🐅", + "tiger" + }, + { + "🐆", + "leopard" + }, + { + "🐴", + "horse face" + }, + { + "🫎", + "moose" + }, + { + "🫏", + "donkey" + }, + { + "🐎", + "horse" + }, + { + "🦄", + "unicorn" + }, + { + "🦓", + "zebra" + }, + { + "🦌", + "deer" + }, + { + "🦬", + "bison" + }, + { + "🐮", + "cow face" + }, + { + "🐂", + "ox" + }, + { + "🐃", + "water buffalo" + }, + { + "🐄", + "cow" + }, + { + "🐷", + "pig face" + }, + { + "🐖", + "pig" + }, + { + "🐗", + "boar" + }, + { + "🐽", + "pig nose" + }, + { + "🐏", + "ram" + }, + { + "🐑", + "ewe" + }, + { + "🐐", + "goat" + }, + { + "🐪", + "camel" + }, + { + "🐫", + "two-hump camel" + }, + { + "🦙", + "llama" + }, + { + "🦒", + "giraffe" + }, + { + "🐘", + "elephant" + }, + { + "🦣", + "mammoth" + }, + { + "🦏", + "rhinoceros" + }, + { + "🦛", + "hippopotamus" + }, + { + "🐭", + "mouse face" + }, + { + "🐁", + "mouse" + }, + { + "🐀", + "rat" + }, + { + "🐹", + "hamster" + }, + { + "🐰", + "rabbit face" + }, + { + "🐇", + "rabbit" + }, + { + "🐿️", + "chipmunk" + }, + { + "🦫", + "beaver" + }, + { + "🦔", + "hedgehog" + }, + { + "🦇", + "bat" + }, + { + "🐻", + "bear" + }, + { + "🐻‍❄️", + "polar bear" + }, + { + "🐨", + "koala" + }, + { + "🐼", + "panda" + }, + { + "🦥", + "sloth" + }, + { + "🦦", + "otter" + }, + { + "🦨", + "skunk" + }, + { + "🦘", + "kangaroo" + }, + { + "🦡", + "badger" + }, + { + "🐾", + "paw prints" + } + } + }, + { + "animal-bird", + { + { + "🦃", + "turkey" + }, + { + "🐔", + "chicken" + }, + { + "🐓", + "rooster" + }, + { + "🐣", + "hatching chick" + }, + { + "🐤", + "baby chick" + }, + { + "🐥", + "front-facing baby chick" + }, + { + "🐦", + "bird" + }, + { + "🐧", + "penguin" + }, + { + "🕊️", + "dove" + }, + { + "🦅", + "eagle" + }, + { + "🦆", + "duck" + }, + { + "🦢", + "swan" + }, + { + "🦉", + "owl" + }, + { + "🦤", + "dodo" + }, + { + "🪶", + "feather" + }, + { + "🦩", + "flamingo" + }, + { + "🦚", + "peacock" + }, + { + "🦜", + "parrot" + }, + { + "🪽", + "wing" + }, + { + "🐦‍⬛", + "black bird" + }, + { + "🪿", + "goose" + }, + { + "🐦‍🔥", + "phoenix" + } + } + }, + { + "animal-amphibian", + { + { + "🐸", + "frog" + } + } + }, + { + "animal-reptile", + { + { + "🐊", + "crocodile" + }, + { + "🐢", + "turtle" + }, + { + "🦎", + "lizard" + }, + { + "🐍", + "snake" + }, + { + "🐲", + "dragon face" + }, + { + "🐉", + "dragon" + }, + { + "🦕", + "sauropod" + }, + { + "🦖", + "T-Rex" + } + } + }, + { + "animal-marine", + { + { + "🐳", + "spouting whale" + }, + { + "🐋", + "whale" + }, + { + "🐬", + "dolphin" + }, + { + "🦭", + "seal" + }, + { + "🐟", + "fish" + }, + { + "🐠", + "tropical fish" + }, + { + "🐡", + "blowfish" + }, + { + "🦈", + "shark" + }, + { + "🐙", + "octopus" + }, + { + "🐚", + "spiral shell" + }, + { + "🪸", + "coral" + }, + { + "🪼", + "jellyfish" + } + } + }, + { + "animal-bug", + { + { + "🐌", + "snail" + }, + { + "🦋", + "butterfly" + }, + { + "🐛", + "bug" + }, + { + "🐜", + "ant" + }, + { + "🐝", + "honeybee" + }, + { + "🪲", + "beetle" + }, + { + "🐞", + "lady beetle" + }, + { + "🦗", + "cricket" + }, + { + "🪳", + "cockroach" + }, + { + "🕷️", + "spider" + }, + { + "🕸️", + "spider web" + }, + { + "🦂", + "scorpion" + }, + { + "🦟", + "mosquito" + }, + { + "🪰", + "fly" + }, + { + "🪱", + "worm" + }, + { + "🦠", + "microbe" + } + } + }, + { + "plant-flower", + { + { + "💐", + "bouquet" + }, + { + "🌸", + "cherry blossom" + }, + { + "💮", + "white flower" + }, + { + "🪷", + "lotus" + }, + { + "🏵️", + "rosette" + }, + { + "🌹", + "rose" + }, + { + "🥀", + "wilted flower" + }, + { + "🌺", + "hibiscus" + }, + { + "🌻", + "sunflower" + }, + { + "🌼", + "blossom" + }, + { + "🌷", + "tulip" + }, + { + "🪻", + "hyacinth" + } + } + }, + { + "plant-other", + { + { + "🌱", + "seedling" + }, + { + "🪴", + "potted plant" + }, + { + "🌲", + "evergreen tree" + }, + { + "🌳", + "deciduous tree" + }, + { + "🌴", + "palm tree" + }, + { + "🌵", + "cactus" + }, + { + "🌾", + "sheaf of rice" + }, + { + "🌿", + "herb" + }, + { + "☘️", + "shamrock" + }, + { + "🍀", + "four leaf clover" + }, + { + "🍁", + "maple leaf" + }, + { + "🍂", + "fallen leaf" + }, + { + "🍃", + "leaf fluttering in wind" + }, + { + "🪹", + "empty nest" + }, + { + "🪺", + "nest with eggs" + }, + { + "🍄", + "mushroom" + } + } + } + } +}; + +EmojiRegistry::Group emoji_Food___Drink { + QT_TR_NOOP("Food & Drink"), + { + { + "food-fruit", + { + { + "🍇", + "grapes" + }, + { + "🍈", + "melon" + }, + { + "🍉", + "watermelon" + }, + { + "🍊", + "tangerine" + }, + { + "🍋", + "lemon" + }, + { + "🍋‍🟩", + "lime" + }, + { + "🍌", + "banana" + }, + { + "🍍", + "pineapple" + }, + { + "🥭", + "mango" + }, + { + "🍎", + "red apple" + }, + { + "🍏", + "green apple" + }, + { + "🍐", + "pear" + }, + { + "🍑", + "peach" + }, + { + "🍒", + "cherries" + }, + { + "🍓", + "strawberry" + }, + { + "🫐", + "blueberries" + }, + { + "🥝", + "kiwi fruit" + }, + { + "🍅", + "tomato" + }, + { + "🫒", + "olive" + }, + { + "🥥", + "coconut" + } + } + }, + { + "food-vegetable", + { + { + "🥑", + "avocado" + }, + { + "🍆", + "eggplant" + }, + { + "🥔", + "potato" + }, + { + "🥕", + "carrot" + }, + { + "🌽", + "ear of corn" + }, + { + "🌶️", + "hot pepper" + }, + { + "🫑", + "bell pepper" + }, + { + "🥒", + "cucumber" + }, + { + "🥬", + "leafy green" + }, + { + "🥦", + "broccoli" + }, + { + "🧄", + "garlic" + }, + { + "🧅", + "onion" + }, + { + "🥜", + "peanuts" + }, + { + "🫘", + "beans" + }, + { + "🌰", + "chestnut" + }, + { + "🫚", + "ginger root" + }, + { + "🫛", + "pea pod" + }, + { + "🍄‍🟫", + "brown mushroom" + } + } + }, + { + "food-prepared", + { + { + "🍞", + "bread" + }, + { + "🥐", + "croissant" + }, + { + "🥖", + "baguette bread" + }, + { + "🫓", + "flatbread" + }, + { + "🥨", + "pretzel" + }, + { + "🥯", + "bagel" + }, + { + "🥞", + "pancakes" + }, + { + "🧇", + "waffle" + }, + { + "🧀", + "cheese wedge" + }, + { + "🍖", + "meat on bone" + }, + { + "🍗", + "poultry leg" + }, + { + "🥩", + "cut of meat" + }, + { + "🥓", + "bacon" + }, + { + "🍔", + "hamburger" + }, + { + "🍟", + "french fries" + }, + { + "🍕", + "pizza" + }, + { + "🌭", + "hot dog" + }, + { + "🥪", + "sandwich" + }, + { + "🌮", + "taco" + }, + { + "🌯", + "burrito" + }, + { + "🫔", + "tamale" + }, + { + "🥙", + "stuffed flatbread" + }, + { + "🧆", + "falafel" + }, + { + "🥚", + "egg" + }, + { + "🍳", + "cooking" + }, + { + "🥘", + "shallow pan of food" + }, + { + "🍲", + "pot of food" + }, + { + "🫕", + "fondue" + }, + { + "🥣", + "bowl with spoon" + }, + { + "🥗", + "green salad" + }, + { + "🍿", + "popcorn" + }, + { + "🧈", + "butter" + }, + { + "🧂", + "salt" + }, + { + "🥫", + "canned food" + } + } + }, + { + "food-asian", + { + { + "🍱", + "bento box" + }, + { + "🍘", + "rice cracker" + }, + { + "🍙", + "rice ball" + }, + { + "🍚", + "cooked rice" + }, + { + "🍛", + "curry rice" + }, + { + "🍜", + "steaming bowl" + }, + { + "🍝", + "spaghetti" + }, + { + "🍠", + "roasted sweet potato" + }, + { + "🍢", + "oden" + }, + { + "🍣", + "sushi" + }, + { + "🍤", + "fried shrimp" + }, + { + "🍥", + "fish cake with swirl" + }, + { + "🥮", + "moon cake" + }, + { + "🍡", + "dango" + }, + { + "🥟", + "dumpling" + }, + { + "🥠", + "fortune cookie" + }, + { + "🥡", + "takeout box" + } + } + }, + { + "food-marine", + { + { + "🦀", + "crab" + }, + { + "🦞", + "lobster" + }, + { + "🦐", + "shrimp" + }, + { + "🦑", + "squid" + }, + { + "🦪", + "oyster" + } + } + }, + { + "food-sweet", + { + { + "🍦", + "soft ice cream" + }, + { + "🍧", + "shaved ice" + }, + { + "🍨", + "ice cream" + }, + { + "🍩", + "doughnut" + }, + { + "🍪", + "cookie" + }, + { + "🎂", + "birthday cake" + }, + { + "🍰", + "shortcake" + }, + { + "🧁", + "cupcake" + }, + { + "🥧", + "pie" + }, + { + "🍫", + "chocolate bar" + }, + { + "🍬", + "candy" + }, + { + "🍭", + "lollipop" + }, + { + "🍮", + "custard" + }, + { + "🍯", + "honey pot" + } + } + }, + { + "drink", + { + { + "🍼", + "baby bottle" + }, + { + "🥛", + "glass of milk" + }, + { + "☕", + "hot beverage" + }, + { + "🫖", + "teapot" + }, + { + "🍵", + "teacup without handle" + }, + { + "🍶", + "sake" + }, + { + "🍾", + "bottle with popping cork" + }, + { + "🍷", + "wine glass" + }, + { + "🍸", + "cocktail glass" + }, + { + "🍹", + "tropical drink" + }, + { + "🍺", + "beer mug" + }, + { + "🍻", + "clinking beer mugs" + }, + { + "🥂", + "clinking glasses" + }, + { + "🥃", + "tumbler glass" + }, + { + "🫗", + "pouring liquid" + }, + { + "🥤", + "cup with straw" + }, + { + "🧋", + "bubble tea" + }, + { + "🧃", + "beverage box" + }, + { + "🧉", + "mate" + }, + { + "🧊", + "ice" + } + } + }, + { + "dishware", + { + { + "🥢", + "chopsticks" + }, + { + "🍽️", + "fork and knife with plate" + }, + { + "🍴", + "fork and knife" + }, + { + "🥄", + "spoon" + }, + { + "🔪", + "kitchen knife" + }, + { + "🫙", + "jar" + }, + { + "🏺", + "amphora" + } + } + } + } +}; + +EmojiRegistry::Group emoji_Travel___Places { + QT_TR_NOOP("Travel & Places"), + { + { + "place-map", + { + { + "🌍", + "globe showing Europe-Africa" + }, + { + "🌎", + "globe showing Americas" + }, + { + "🌏", + "globe showing Asia-Australia" + }, + { + "🌐", + "globe with meridians" + }, + { + "🗺️", + "world map" + }, + { + "🗾", + "map of Japan" + }, + { + "🧭", + "compass" + } + } + }, + { + "place-geographic", + { + { + "🏔️", + "snow-capped mountain" + }, + { + "⛰️", + "mountain" + }, + { + "🌋", + "volcano" + }, + { + "🗻", + "mount fuji" + }, + { + "🏕️", + "camping" + }, + { + "🏖️", + "beach with umbrella" + }, + { + "🏜️", + "desert" + }, + { + "🏝️", + "desert island" + }, + { + "🏞️", + "national park" + } + } + }, + { + "place-building", + { + { + "🏟️", + "stadium" + }, + { + "🏛️", + "classical building" + }, + { + "🏗️", + "building construction" + }, + { + "🧱", + "brick" + }, + { + "🪨", + "rock" + }, + { + "🪵", + "wood" + }, + { + "🛖", + "hut" + }, + { + "🏘️", + "houses" + }, + { + "🏚️", + "derelict house" + }, + { + "🏠", + "house" + }, + { + "🏡", + "house with garden" + }, + { + "🏢", + "office building" + }, + { + "🏣", + "Japanese post office" + }, + { + "🏤", + "post office" + }, + { + "🏥", + "hospital" + }, + { + "🏦", + "bank" + }, + { + "🏨", + "hotel" + }, + { + "🏩", + "love hotel" + }, + { + "🏪", + "convenience store" + }, + { + "🏫", + "school" + }, + { + "🏬", + "department store" + }, + { + "🏭", + "factory" + }, + { + "🏯", + "Japanese castle" + }, + { + "🏰", + "castle" + }, + { + "💒", + "wedding" + }, + { + "🗼", + "Tokyo tower" + }, + { + "🗽", + "Statue of Liberty" + } + } + }, + { + "place-religious", + { + { + "⛪", + "church" + }, + { + "🕌", + "mosque" + }, + { + "🛕", + "hindu temple" + }, + { + "🕍", + "synagogue" + }, + { + "⛩️", + "shinto shrine" + }, + { + "🕋", + "kaaba" + } + } + }, + { + "place-other", + { + { + "⛲", + "fountain" + }, + { + "⛺", + "tent" + }, + { + "🌁", + "foggy" + }, + { + "🌃", + "night with stars" + }, + { + "🏙️", + "cityscape" + }, + { + "🌄", + "sunrise over mountains" + }, + { + "🌅", + "sunrise" + }, + { + "🌆", + "cityscape at dusk" + }, + { + "🌇", + "sunset" + }, + { + "🌉", + "bridge at night" + }, + { + "♨️", + "hot springs" + }, + { + "🎠", + "carousel horse" + }, + { + "🛝", + "playground slide" + }, + { + "🎡", + "ferris wheel" + }, + { + "🎢", + "roller coaster" + }, + { + "💈", + "barber pole" + }, + { + "🎪", + "circus tent" + } + } + }, + { + "transport-ground", + { + { + "🚂", + "locomotive" + }, + { + "🚃", + "railway car" + }, + { + "🚄", + "high-speed train" + }, + { + "🚅", + "bullet train" + }, + { + "🚆", + "train" + }, + { + "🚇", + "metro" + }, + { + "🚈", + "light rail" + }, + { + "🚉", + "station" + }, + { + "🚊", + "tram" + }, + { + "🚝", + "monorail" + }, + { + "🚞", + "mountain railway" + }, + { + "🚋", + "tram car" + }, + { + "🚌", + "bus" + }, + { + "🚍", + "oncoming bus" + }, + { + "🚎", + "trolleybus" + }, + { + "🚐", + "minibus" + }, + { + "🚑", + "ambulance" + }, + { + "🚒", + "fire engine" + }, + { + "🚓", + "police car" + }, + { + "🚔", + "oncoming police car" + }, + { + "🚕", + "taxi" + }, + { + "🚖", + "oncoming taxi" + }, + { + "🚗", + "automobile" + }, + { + "🚘", + "oncoming automobile" + }, + { + "🚙", + "sport utility vehicle" + }, + { + "🛻", + "pickup truck" + }, + { + "🚚", + "delivery truck" + }, + { + "🚛", + "articulated lorry" + }, + { + "🚜", + "tractor" + }, + { + "🏎️", + "racing car" + }, + { + "🏍️", + "motorcycle" + }, + { + "🛵", + "motor scooter" + }, + { + "🦽", + "manual wheelchair" + }, + { + "🦼", + "motorized wheelchair" + }, + { + "🛺", + "auto rickshaw" + }, + { + "🚲", + "bicycle" + }, + { + "🛴", + "kick scooter" + }, + { + "🛹", + "skateboard" + }, + { + "🛼", + "roller skate" + }, + { + "🚏", + "bus stop" + }, + { + "🛣️", + "motorway" + }, + { + "🛤️", + "railway track" + }, + { + "🛢️", + "oil drum" + }, + { + "⛽", + "fuel pump" + }, + { + "🛞", + "wheel" + }, + { + "🚨", + "police car light" + }, + { + "🚥", + "horizontal traffic light" + }, + { + "🚦", + "vertical traffic light" + }, + { + "🛑", + "stop sign" + }, + { + "🚧", + "construction" + } + } + }, + { + "transport-water", + { + { + "⚓", + "anchor" + }, + { + "🛟", + "ring buoy" + }, + { + "⛵", + "sailboat" + }, + { + "🛶", + "canoe" + }, + { + "🚤", + "speedboat" + }, + { + "🛳️", + "passenger ship" + }, + { + "⛴️", + "ferry" + }, + { + "🛥️", + "motor boat" + }, + { + "🚢", + "ship" + } + } + }, + { + "transport-air", + { + { + "✈️", + "airplane" + }, + { + "🛩️", + "small airplane" + }, + { + "🛫", + "airplane departure" + }, + { + "🛬", + "airplane arrival" + }, + { + "🪂", + "parachute" + }, + { + "💺", + "seat" + }, + { + "🚁", + "helicopter" + }, + { + "🚟", + "suspension railway" + }, + { + "🚠", + "mountain cableway" + }, + { + "🚡", + "aerial tramway" + }, + { + "🛰️", + "satellite" + }, + { + "🚀", + "rocket" + }, + { + "🛸", + "flying saucer" + } + } + }, + { + "hotel", + { + { + "🛎️", + "bellhop bell" + }, + { + "🧳", + "luggage" + } + } + }, + { + "time", + { + { + "⌛", + "hourglass done" + }, + { + "⏳", + "hourglass not done" + }, + { + "⌚", + "watch" + }, + { + "⏰", + "alarm clock" + }, + { + "⏱️", + "stopwatch" + }, + { + "⏲️", + "timer clock" + }, + { + "🕰️", + "mantelpiece clock" + }, + { + "🕛", + "twelve o’clock" + }, + { + "🕧", + "twelve-thirty" + }, + { + "🕐", + "one o’clock" + }, + { + "🕜", + "one-thirty" + }, + { + "🕑", + "two o’clock" + }, + { + "🕝", + "two-thirty" + }, + { + "🕒", + "three o’clock" + }, + { + "🕞", + "three-thirty" + }, + { + "🕓", + "four o’clock" + }, + { + "🕟", + "four-thirty" + }, + { + "🕔", + "five o’clock" + }, + { + "🕠", + "five-thirty" + }, + { + "🕕", + "six o’clock" + }, + { + "🕡", + "six-thirty" + }, + { + "🕖", + "seven o’clock" + }, + { + "🕢", + "seven-thirty" + }, + { + "🕗", + "eight o’clock" + }, + { + "🕣", + "eight-thirty" + }, + { + "🕘", + "nine o’clock" + }, + { + "🕤", + "nine-thirty" + }, + { + "🕙", + "ten o’clock" + }, + { + "🕥", + "ten-thirty" + }, + { + "🕚", + "eleven o’clock" + }, + { + "🕦", + "eleven-thirty" + } + } + }, + { + "sky & weather", + { + { + "🌑", + "new moon" + }, + { + "🌒", + "waxing crescent moon" + }, + { + "🌓", + "first quarter moon" + }, + { + "🌔", + "waxing gibbous moon" + }, + { + "🌕", + "full moon" + }, + { + "🌖", + "waning gibbous moon" + }, + { + "🌗", + "last quarter moon" + }, + { + "🌘", + "waning crescent moon" + }, + { + "🌙", + "crescent moon" + }, + { + "🌚", + "new moon face" + }, + { + "🌛", + "first quarter moon face" + }, + { + "🌜", + "last quarter moon face" + }, + { + "🌡️", + "thermometer" + }, + { + "☀️", + "sun" + }, + { + "🌝", + "full moon face" + }, + { + "🌞", + "sun with face" + }, + { + "🪐", + "ringed planet" + }, + { + "⭐", + "star" + }, + { + "🌟", + "glowing star" + }, + { + "🌠", + "shooting star" + }, + { + "🌌", + "milky way" + }, + { + "☁️", + "cloud" + }, + { + "⛅", + "sun behind cloud" + }, + { + "⛈️", + "cloud with lightning and rain" + }, + { + "🌤️", + "sun behind small cloud" + }, + { + "🌥️", + "sun behind large cloud" + }, + { + "🌦️", + "sun behind rain cloud" + }, + { + "🌧️", + "cloud with rain" + }, + { + "🌨️", + "cloud with snow" + }, + { + "🌩️", + "cloud with lightning" + }, + { + "🌪️", + "tornado" + }, + { + "🌫️", + "fog" + }, + { + "🌬️", + "wind face" + }, + { + "🌀", + "cyclone" + }, + { + "🌈", + "rainbow" + }, + { + "🌂", + "closed umbrella" + }, + { + "☂️", + "umbrella" + }, + { + "☔", + "umbrella with rain drops" + }, + { + "⛱️", + "umbrella on ground" + }, + { + "⚡", + "high voltage" + }, + { + "❄️", + "snowflake" + }, + { + "☃️", + "snowman" + }, + { + "⛄", + "snowman without snow" + }, + { + "☄️", + "comet" + }, + { + "🔥", + "fire" + }, + { + "💧", + "droplet" + }, + { + "🌊", + "water wave" + } + } + } + } +}; + +EmojiRegistry::Group emoji_Activities { + QT_TR_NOOP("Activities"), + { + { + "event", + { + { + "🎃", + "jack-o-lantern" + }, + { + "🎄", + "Christmas tree" + }, + { + "🎆", + "fireworks" + }, + { + "🎇", + "sparkler" + }, + { + "🧨", + "firecracker" + }, + { + "✨", + "sparkles" + }, + { + "🎈", + "balloon" + }, + { + "🎉", + "party popper" + }, + { + "🎊", + "confetti ball" + }, + { + "🎋", + "tanabata tree" + }, + { + "🎍", + "pine decoration" + }, + { + "🎎", + "Japanese dolls" + }, + { + "🎏", + "carp streamer" + }, + { + "🎐", + "wind chime" + }, + { + "🎑", + "moon viewing ceremony" + }, + { + "🧧", + "red envelope" + }, + { + "🎀", + "ribbon" + }, + { + "🎁", + "wrapped gift" + }, + { + "🎗️", + "reminder ribbon" + }, + { + "🎟️", + "admission tickets" + }, + { + "🎫", + "ticket" + } + } + }, + { + "award-medal", + { + { + "🎖️", + "military medal" + }, + { + "🏆", + "trophy" + }, + { + "🏅", + "sports medal" + }, + { + "🥇", + "1st place medal" + }, + { + "🥈", + "2nd place medal" + }, + { + "🥉", + "3rd place medal" + } + } + }, + { + "sport", + { + { + "⚽", + "soccer ball" + }, + { + "⚾", + "baseball" + }, + { + "🥎", + "softball" + }, + { + "🏀", + "basketball" + }, + { + "🏐", + "volleyball" + }, + { + "🏈", + "american football" + }, + { + "🏉", + "rugby football" + }, + { + "🎾", + "tennis" + }, + { + "🥏", + "flying disc" + }, + { + "🎳", + "bowling" + }, + { + "🏏", + "cricket game" + }, + { + "🏑", + "field hockey" + }, + { + "🏒", + "ice hockey" + }, + { + "🥍", + "lacrosse" + }, + { + "🏓", + "ping pong" + }, + { + "🏸", + "badminton" + }, + { + "🥊", + "boxing glove" + }, + { + "🥋", + "martial arts uniform" + }, + { + "🥅", + "goal net" + }, + { + "⛳", + "flag in hole" + }, + { + "⛸️", + "ice skate" + }, + { + "🎣", + "fishing pole" + }, + { + "🤿", + "diving mask" + }, + { + "🎽", + "running shirt" + }, + { + "🎿", + "skis" + }, + { + "🛷", + "sled" + }, + { + "🥌", + "curling stone" + } + } + }, + { + "game", + { + { + "🎯", + "bullseye" + }, + { + "🪀", + "yo-yo" + }, + { + "🪁", + "kite" + }, + { + "🔫", + "water pistol" + }, + { + "🎱", + "pool 8 ball" + }, + { + "🔮", + "crystal ball" + }, + { + "🪄", + "magic wand" + }, + { + "🎮", + "video game" + }, + { + "🕹️", + "joystick" + }, + { + "🎰", + "slot machine" + }, + { + "🎲", + "game die" + }, + { + "🧩", + "puzzle piece" + }, + { + "🧸", + "teddy bear" + }, + { + "🪅", + "piñata" + }, + { + "🪩", + "mirror ball" + }, + { + "🪆", + "nesting dolls" + }, + { + "♠️", + "spade suit" + }, + { + "♥️", + "heart suit" + }, + { + "♦️", + "diamond suit" + }, + { + "♣️", + "club suit" + }, + { + "♟️", + "chess pawn" + }, + { + "🃏", + "joker" + }, + { + "🀄", + "mahjong red dragon" + }, + { + "🎴", + "flower playing cards" + } + } + }, + { + "arts & crafts", + { + { + "🎭", + "performing arts" + }, + { + "🖼️", + "framed picture" + }, + { + "🎨", + "artist palette" + }, + { + "🧵", + "thread" + }, + { + "🪡", + "sewing needle" + }, + { + "🧶", + "yarn" + }, + { + "🪢", + "knot" + } + } + } + } +}; + +EmojiRegistry::Group emoji_Objects { + QT_TR_NOOP("Objects"), + { + { + "clothing", + { + { + "👓", + "glasses" + }, + { + "🕶️", + "sunglasses" + }, + { + "🥽", + "goggles" + }, + { + "🥼", + "lab coat" + }, + { + "🦺", + "safety vest" + }, + { + "👔", + "necktie" + }, + { + "👕", + "t-shirt" + }, + { + "👖", + "jeans" + }, + { + "🧣", + "scarf" + }, + { + "🧤", + "gloves" + }, + { + "🧥", + "coat" + }, + { + "🧦", + "socks" + }, + { + "👗", + "dress" + }, + { + "👘", + "kimono" + }, + { + "🥻", + "sari" + }, + { + "🩱", + "one-piece swimsuit" + }, + { + "🩲", + "briefs" + }, + { + "🩳", + "shorts" + }, + { + "👙", + "bikini" + }, + { + "👚", + "woman’s clothes" + }, + { + "🪭", + "folding hand fan" + }, + { + "👛", + "purse" + }, + { + "👜", + "handbag" + }, + { + "👝", + "clutch bag" + }, + { + "🛍️", + "shopping bags" + }, + { + "🎒", + "backpack" + }, + { + "🩴", + "thong sandal" + }, + { + "👞", + "man’s shoe" + }, + { + "👟", + "running shoe" + }, + { + "🥾", + "hiking boot" + }, + { + "🥿", + "flat shoe" + }, + { + "👠", + "high-heeled shoe" + }, + { + "👡", + "woman’s sandal" + }, + { + "🩰", + "ballet shoes" + }, + { + "👢", + "woman’s boot" + }, + { + "🪮", + "hair pick" + }, + { + "👑", + "crown" + }, + { + "👒", + "woman’s hat" + }, + { + "🎩", + "top hat" + }, + { + "🎓", + "graduation cap" + }, + { + "🧢", + "billed cap" + }, + { + "🪖", + "military helmet" + }, + { + "⛑️", + "rescue worker’s helmet" + }, + { + "📿", + "prayer beads" + }, + { + "💄", + "lipstick" + }, + { + "💍", + "ring" + }, + { + "💎", + "gem stone" + } + } + }, + { + "sound", + { + { + "🔇", + "muted speaker" + }, + { + "🔈", + "speaker low volume" + }, + { + "🔉", + "speaker medium volume" + }, + { + "🔊", + "speaker high volume" + }, + { + "📢", + "loudspeaker" + }, + { + "📣", + "megaphone" + }, + { + "📯", + "postal horn" + }, + { + "🔔", + "bell" + }, + { + "🔕", + "bell with slash" + } + } + }, + { + "music", + { + { + "🎼", + "musical score" + }, + { + "🎵", + "musical note" + }, + { + "🎶", + "musical notes" + }, + { + "🎙️", + "studio microphone" + }, + { + "🎚️", + "level slider" + }, + { + "🎛️", + "control knobs" + }, + { + "🎤", + "microphone" + }, + { + "🎧", + "headphone" + }, + { + "📻", + "radio" + } + } + }, + { + "musical-instrument", + { + { + "🎷", + "saxophone" + }, + { + "🪗", + "accordion" + }, + { + "🎸", + "guitar" + }, + { + "🎹", + "musical keyboard" + }, + { + "🎺", + "trumpet" + }, + { + "🎻", + "violin" + }, + { + "🪕", + "banjo" + }, + { + "🥁", + "drum" + }, + { + "🪘", + "long drum" + }, + { + "🪇", + "maracas" + }, + { + "🪈", + "flute" + } + } + }, + { + "phone", + { + { + "📱", + "mobile phone" + }, + { + "📲", + "mobile phone with arrow" + }, + { + "☎️", + "telephone" + }, + { + "📞", + "telephone receiver" + }, + { + "📟", + "pager" + }, + { + "📠", + "fax machine" + } + } + }, + { + "computer", + { + { + "🔋", + "battery" + }, + { + "🪫", + "low battery" + }, + { + "🔌", + "electric plug" + }, + { + "💻", + "laptop" + }, + { + "🖥️", + "desktop computer" + }, + { + "🖨️", + "printer" + }, + { + "⌨️", + "keyboard" + }, + { + "🖱️", + "computer mouse" + }, + { + "🖲️", + "trackball" + }, + { + "💽", + "computer disk" + }, + { + "💾", + "floppy disk" + }, + { + "💿", + "optical disk" + }, + { + "📀", + "dvd" + }, + { + "🧮", + "abacus" + } + } + }, + { + "light & video", + { + { + "🎥", + "movie camera" + }, + { + "🎞️", + "film frames" + }, + { + "📽️", + "film projector" + }, + { + "🎬", + "clapper board" + }, + { + "📺", + "television" + }, + { + "📷", + "camera" + }, + { + "📸", + "camera with flash" + }, + { + "📹", + "video camera" + }, + { + "📼", + "videocassette" + }, + { + "🔍", + "magnifying glass tilted left" + }, + { + "🔎", + "magnifying glass tilted right" + }, + { + "🕯️", + "candle" + }, + { + "💡", + "light bulb" + }, + { + "🔦", + "flashlight" + }, + { + "🏮", + "red paper lantern" + }, + { + "🪔", + "diya lamp" + } + } + }, + { + "book-paper", + { + { + "📔", + "notebook with decorative cover" + }, + { + "📕", + "closed book" + }, + { + "📖", + "open book" + }, + { + "📗", + "green book" + }, + { + "📘", + "blue book" + }, + { + "📙", + "orange book" + }, + { + "📚", + "books" + }, + { + "📓", + "notebook" + }, + { + "📒", + "ledger" + }, + { + "📃", + "page with curl" + }, + { + "📜", + "scroll" + }, + { + "📄", + "page facing up" + }, + { + "📰", + "newspaper" + }, + { + "🗞️", + "rolled-up newspaper" + }, + { + "📑", + "bookmark tabs" + }, + { + "🔖", + "bookmark" + }, + { + "🏷️", + "label" + } + } + }, + { + "money", + { + { + "💰", + "money bag" + }, + { + "🪙", + "coin" + }, + { + "💴", + "yen banknote" + }, + { + "💵", + "dollar banknote" + }, + { + "💶", + "euro banknote" + }, + { + "💷", + "pound banknote" + }, + { + "💸", + "money with wings" + }, + { + "💳", + "credit card" + }, + { + "🧾", + "receipt" + }, + { + "💹", + "chart increasing with yen" + } + } + }, + { + "mail", + { + { + "✉️", + "envelope" + }, + { + "📧", + "e-mail" + }, + { + "📨", + "incoming envelope" + }, + { + "📩", + "envelope with arrow" + }, + { + "📤", + "outbox tray" + }, + { + "📥", + "inbox tray" + }, + { + "📦", + "package" + }, + { + "📫", + "closed mailbox with raised flag" + }, + { + "📪", + "closed mailbox with lowered flag" + }, + { + "📬", + "open mailbox with raised flag" + }, + { + "📭", + "open mailbox with lowered flag" + }, + { + "📮", + "postbox" + }, + { + "🗳️", + "ballot box with ballot" + } + } + }, + { + "writing", + { + { + "✏️", + "pencil" + }, + { + "✒️", + "black nib" + }, + { + "🖋️", + "fountain pen" + }, + { + "🖊️", + "pen" + }, + { + "🖌️", + "paintbrush" + }, + { + "🖍️", + "crayon" + }, + { + "📝", + "memo" + } + } + }, + { + "office", + { + { + "💼", + "briefcase" + }, + { + "📁", + "file folder" + }, + { + "📂", + "open file folder" + }, + { + "🗂️", + "card index dividers" + }, + { + "📅", + "calendar" + }, + { + "📆", + "tear-off calendar" + }, + { + "🗒️", + "spiral notepad" + }, + { + "🗓️", + "spiral calendar" + }, + { + "📇", + "card index" + }, + { + "📈", + "chart increasing" + }, + { + "📉", + "chart decreasing" + }, + { + "📊", + "bar chart" + }, + { + "📋", + "clipboard" + }, + { + "📌", + "pushpin" + }, + { + "📍", + "round pushpin" + }, + { + "📎", + "paperclip" + }, + { + "🖇️", + "linked paperclips" + }, + { + "📏", + "straight ruler" + }, + { + "📐", + "triangular ruler" + }, + { + "✂️", + "scissors" + }, + { + "🗃️", + "card file box" + }, + { + "🗄️", + "file cabinet" + }, + { + "🗑️", + "wastebasket" + } + } + }, + { + "lock", + { + { + "🔒", + "locked" + }, + { + "🔓", + "unlocked" + }, + { + "🔏", + "locked with pen" + }, + { + "🔐", + "locked with key" + }, + { + "🔑", + "key" + }, + { + "🗝️", + "old key" + } + } + }, + { + "tool", + { + { + "🔨", + "hammer" + }, + { + "🪓", + "axe" + }, + { + "⛏️", + "pick" + }, + { + "⚒️", + "hammer and pick" + }, + { + "🛠️", + "hammer and wrench" + }, + { + "🗡️", + "dagger" + }, + { + "⚔️", + "crossed swords" + }, + { + "💣", + "bomb" + }, + { + "🪃", + "boomerang" + }, + { + "🏹", + "bow and arrow" + }, + { + "🛡️", + "shield" + }, + { + "🪚", + "carpentry saw" + }, + { + "🔧", + "wrench" + }, + { + "🪛", + "screwdriver" + }, + { + "🔩", + "nut and bolt" + }, + { + "⚙️", + "gear" + }, + { + "🗜️", + "clamp" + }, + { + "⚖️", + "balance scale" + }, + { + "🦯", + "white cane" + }, + { + "🔗", + "link" + }, + { + "⛓️‍💥", + "broken chain" + }, + { + "⛓️", + "chains" + }, + { + "🪝", + "hook" + }, + { + "🧰", + "toolbox" + }, + { + "🧲", + "magnet" + }, + { + "🪜", + "ladder" + } + } + }, + { + "science", + { + { + "⚗️", + "alembic" + }, + { + "🧪", + "test tube" + }, + { + "🧫", + "petri dish" + }, + { + "🧬", + "dna" + }, + { + "🔬", + "microscope" + }, + { + "🔭", + "telescope" + }, + { + "📡", + "satellite antenna" + } + } + }, + { + "medical", + { + { + "💉", + "syringe" + }, + { + "🩸", + "drop of blood" + }, + { + "💊", + "pill" + }, + { + "🩹", + "adhesive bandage" + }, + { + "🩼", + "crutch" + }, + { + "🩺", + "stethoscope" + }, + { + "🩻", + "x-ray" + } + } + }, + { + "household", + { + { + "🚪", + "door" + }, + { + "🛗", + "elevator" + }, + { + "🪞", + "mirror" + }, + { + "🪟", + "window" + }, + { + "🛏️", + "bed" + }, + { + "🛋️", + "couch and lamp" + }, + { + "🪑", + "chair" + }, + { + "🚽", + "toilet" + }, + { + "🪠", + "plunger" + }, + { + "🚿", + "shower" + }, + { + "🛁", + "bathtub" + }, + { + "🪤", + "mouse trap" + }, + { + "🪒", + "razor" + }, + { + "🧴", + "lotion bottle" + }, + { + "🧷", + "safety pin" + }, + { + "🧹", + "broom" + }, + { + "🧺", + "basket" + }, + { + "🧻", + "roll of paper" + }, + { + "🪣", + "bucket" + }, + { + "🧼", + "soap" + }, + { + "🫧", + "bubbles" + }, + { + "🪥", + "toothbrush" + }, + { + "🧽", + "sponge" + }, + { + "🧯", + "fire extinguisher" + }, + { + "🛒", + "shopping cart" + } + } + }, + { + "other-object", + { + { + "🚬", + "cigarette" + }, + { + "⚰️", + "coffin" + }, + { + "🪦", + "headstone" + }, + { + "⚱️", + "funeral urn" + }, + { + "🧿", + "nazar amulet" + }, + { + "🪬", + "hamsa" + }, + { + "🗿", + "moai" + }, + { + "🪧", + "placard" + }, + { + "🪪", + "identification card" + } + } + } + } +}; + +EmojiRegistry::Group emoji_Symbols { + QT_TR_NOOP("Symbols"), + { + { + "transport-sign", + { + { + "🏧", + "ATM sign" + }, + { + "🚮", + "litter in bin sign" + }, + { + "🚰", + "potable water" + }, + { + "♿", + "wheelchair symbol" + }, + { + "🚹", + "men’s room" + }, + { + "🚺", + "women’s room" + }, + { + "🚻", + "restroom" + }, + { + "🚼", + "baby symbol" + }, + { + "🚾", + "water closet" + }, + { + "🛂", + "passport control" + }, + { + "🛃", + "customs" + }, + { + "🛄", + "baggage claim" + }, + { + "🛅", + "left luggage" + } + } + }, + { + "warning", + { + { + "⚠️", + "warning" + }, + { + "🚸", + "children crossing" + }, + { + "⛔", + "no entry" + }, + { + "🚫", + "prohibited" + }, + { + "🚳", + "no bicycles" + }, + { + "🚭", + "no smoking" + }, + { + "🚯", + "no littering" + }, + { + "🚱", + "non-potable water" + }, + { + "🚷", + "no pedestrians" + }, + { + "📵", + "no mobile phones" + }, + { + "🔞", + "no one under eighteen" + }, + { + "☢️", + "radioactive" + }, + { + "☣️", + "biohazard" + } + } + }, + { + "arrow", + { + { + "⬆️", + "up arrow" + }, + { + "↗️", + "up-right arrow" + }, + { + "➡️", + "right arrow" + }, + { + "↘️", + "down-right arrow" + }, + { + "⬇️", + "down arrow" + }, + { + "↙️", + "down-left arrow" + }, + { + "⬅️", + "left arrow" + }, + { + "↖️", + "up-left arrow" + }, + { + "↕️", + "up-down arrow" + }, + { + "↔️", + "left-right arrow" + }, + { + "↩️", + "right arrow curving left" + }, + { + "↪️", + "left arrow curving right" + }, + { + "⤴️", + "right arrow curving up" + }, + { + "⤵️", + "right arrow curving down" + }, + { + "🔃", + "clockwise vertical arrows" + }, + { + "🔄", + "counterclockwise arrows button" + }, + { + "🔙", + "BACK arrow" + }, + { + "🔚", + "END arrow" + }, + { + "🔛", + "ON! arrow" + }, + { + "🔜", + "SOON arrow" + }, + { + "🔝", + "TOP arrow" + } + } + }, + { + "religion", + { + { + "🛐", + "place of worship" + }, + { + "⚛️", + "atom symbol" + }, + { + "🕉️", + "om" + }, + { + "✡️", + "star of David" + }, + { + "☸️", + "wheel of dharma" + }, + { + "☯️", + "yin yang" + }, + { + "✝️", + "latin cross" + }, + { + "☦️", + "orthodox cross" + }, + { + "☪️", + "star and crescent" + }, + { + "☮️", + "peace symbol" + }, + { + "🕎", + "menorah" + }, + { + "🔯", + "dotted six-pointed star" + }, + { + "🪯", + "khanda" + } + } + }, + { + "zodiac", + { + { + "♈", + "Aries" + }, + { + "♉", + "Taurus" + }, + { + "♊", + "Gemini" + }, + { + "♋", + "Cancer" + }, + { + "♌", + "Leo" + }, + { + "♍", + "Virgo" + }, + { + "♎", + "Libra" + }, + { + "♏", + "Scorpio" + }, + { + "♐", + "Sagittarius" + }, + { + "♑", + "Capricorn" + }, + { + "♒", + "Aquarius" + }, + { + "♓", + "Pisces" + }, + { + "⛎", + "Ophiuchus" + } + } + }, + { + "av-symbol", + { + { + "🔀", + "shuffle tracks button" + }, + { + "🔁", + "repeat button" + }, + { + "🔂", + "repeat single button" + }, + { + "▶️", + "play button" + }, + { + "⏩", + "fast-forward button" + }, + { + "⏭️", + "next track button" + }, + { + "⏯️", + "play or pause button" + }, + { + "◀️", + "reverse button" + }, + { + "⏪", + "fast reverse button" + }, + { + "⏮️", + "last track button" + }, + { + "🔼", + "upwards button" + }, + { + "⏫", + "fast up button" + }, + { + "🔽", + "downwards button" + }, + { + "⏬", + "fast down button" + }, + { + "⏸️", + "pause button" + }, + { + "⏹️", + "stop button" + }, + { + "⏺️", + "record button" + }, + { + "⏏️", + "eject button" + }, + { + "🎦", + "cinema" + }, + { + "🔅", + "dim button" + }, + { + "🔆", + "bright button" + }, + { + "📶", + "antenna bars" + }, + { + "🛜", + "wireless" + }, + { + "📳", + "vibration mode" + }, + { + "📴", + "mobile phone off" + } + } + }, + { + "gender", + { + { + "♀️", + "female sign" + }, + { + "♂️", + "male sign" + }, + { + "⚧️", + "transgender symbol" + } + } + }, + { + "math", + { + { + "✖️", + "multiply" + }, + { + "➕", + "plus" + }, + { + "➖", + "minus" + }, + { + "➗", + "divide" + }, + { + "🟰", + "heavy equals sign" + }, + { + "♾️", + "infinity" + } + } + }, + { + "punctuation", + { + { + "‼️", + "double exclamation mark" + }, + { + "⁉️", + "exclamation question mark" + }, + { + "❓", + "red question mark" + }, + { + "❔", + "white question mark" + }, + { + "❕", + "white exclamation mark" + }, + { + "❗", + "red exclamation mark" + }, + { + "〰️", + "wavy dash" + } + } + }, + { + "currency", + { + { + "💱", + "currency exchange" + }, + { + "💲", + "heavy dollar sign" + } + } + }, + { + "other-symbol", + { + { + "⚕️", + "medical symbol" + }, + { + "♻️", + "recycling symbol" + }, + { + "⚜️", + "fleur-de-lis" + }, + { + "🔱", + "trident emblem" + }, + { + "📛", + "name badge" + }, + { + "🔰", + "Japanese symbol for beginner" + }, + { + "⭕", + "hollow red circle" + }, + { + "✅", + "check mark button" + }, + { + "☑️", + "check box with check" + }, + { + "✔️", + "check mark" + }, + { + "❌", + "cross mark" + }, + { + "❎", + "cross mark button" + }, + { + "➰", + "curly loop" + }, + { + "➿", + "double curly loop" + }, + { + "〽️", + "part alternation mark" + }, + { + "✳️", + "eight-spoked asterisk" + }, + { + "✴️", + "eight-pointed star" + }, + { + "❇️", + "sparkle" + }, + { + "©️", + "copyright" + }, + { + "®️", + "registered" + }, + { + "™️", + "trade mark" + } + } + }, + { + "keycap", + { + { + "#️⃣", + "keycap" + }, + { + "*️⃣", + "keycap" + }, + { + "0️⃣", + "keycap" + }, + { + "1️⃣", + "keycap" + }, + { + "2️⃣", + "keycap" + }, + { + "3️⃣", + "keycap" + }, + { + "4️⃣", + "keycap" + }, + { + "5️⃣", + "keycap" + }, + { + "6️⃣", + "keycap" + }, + { + "7️⃣", + "keycap" + }, + { + "8️⃣", + "keycap" + }, + { + "9️⃣", + "keycap" + }, + { + "🔟", + "keycap" + } + } + }, + { + "alphanum", + { + { + "🔠", + "input latin uppercase" + }, + { + "🔡", + "input latin lowercase" + }, + { + "🔢", + "input numbers" + }, + { + "🔣", + "input symbols" + }, + { + "🔤", + "input latin letters" + }, + { + "🅰️", + "A button (blood type)" + }, + { + "🆎", + "AB button (blood type)" + }, + { + "🅱️", + "B button (blood type)" + }, + { + "🆑", + "CL button" + }, + { + "🆒", + "COOL button" + }, + { + "🆓", + "FREE button" + }, + { + "ℹ️", + "information" + }, + { + "🆔", + "ID button" + }, + { + "Ⓜ️", + "circled M" + }, + { + "🆕", + "NEW button" + }, + { + "🆖", + "NG button" + }, + { + "🅾️", + "O button (blood type)" + }, + { + "🆗", + "OK button" + }, + { + "🅿️", + "P button" + }, + { + "🆘", + "SOS button" + }, + { + "🆙", + "UP! button" + }, + { + "🆚", + "VS button" + }, + { + "🈁", + "Japanese “here” button" + }, + { + "🈂️", + "Japanese “service charge” button" + }, + { + "🈷️", + "Japanese “monthly amount” button" + }, + { + "🈶", + "Japanese “not free of charge” button" + }, + { + "🈯", + "Japanese “reserved” button" + }, + { + "🉐", + "Japanese “bargain” button" + }, + { + "🈹", + "Japanese “discount” button" + }, + { + "🈚", + "Japanese “free of charge” button" + }, + { + "🈲", + "Japanese “prohibited” button" + }, + { + "🉑", + "Japanese “acceptable” button" + }, + { + "🈸", + "Japanese “application” button" + }, + { + "🈴", + "Japanese “passing grade” button" + }, + { + "🈳", + "Japanese “vacancy” button" + }, + { + "㊗️", + "Japanese “congratulations” button" + }, + { + "㊙️", + "Japanese “secret” button" + }, + { + "🈺", + "Japanese “open for business” button" + }, + { + "🈵", + "Japanese “no vacancy” button" + } + } + }, + { + "geometric", + { + { + "🔴", + "red circle" + }, + { + "🟠", + "orange circle" + }, + { + "🟡", + "yellow circle" + }, + { + "🟢", + "green circle" + }, + { + "🔵", + "blue circle" + }, + { + "🟣", + "purple circle" + }, + { + "🟤", + "brown circle" + }, + { + "⚫", + "black circle" + }, + { + "⚪", + "white circle" + }, + { + "🟥", + "red square" + }, + { + "🟧", + "orange square" + }, + { + "🟨", + "yellow square" + }, + { + "🟩", + "green square" + }, + { + "🟦", + "blue square" + }, + { + "🟪", + "purple square" + }, + { + "🟫", + "brown square" + }, + { + "⬛", + "black large square" + }, + { + "⬜", + "white large square" + }, + { + "◼️", + "black medium square" + }, + { + "◻️", + "white medium square" + }, + { + "◾", + "black medium-small square" + }, + { + "◽", + "white medium-small square" + }, + { + "▪️", + "black small square" + }, + { + "▫️", + "white small square" + }, + { + "🔶", + "large orange diamond" + }, + { + "🔷", + "large blue diamond" + }, + { + "🔸", + "small orange diamond" + }, + { + "🔹", + "small blue diamond" + }, + { + "🔺", + "red triangle pointed up" + }, + { + "🔻", + "red triangle pointed down" + }, + { + "💠", + "diamond with a dot" + }, + { + "🔘", + "radio button" + }, + { + "🔳", + "white square button" + }, + { + "🔲", + "black square button" + } + } + } + } +}; + +EmojiRegistry::Group emoji_Flags { + QT_TR_NOOP("Flags"), + { + { + "flag", + { + { + "🏁", + "chequered flag" + }, + { + "🚩", + "triangular flag" + }, + { + "🎌", + "crossed flags" + }, + { + "🏴", + "black flag" + }, + { + "🏳️", + "white flag" + }, + { + "🏳️‍🌈", + "rainbow flag" + }, + { + "🏳️‍⚧️", + "transgender flag" + }, + { + "🏴‍☠️", + "pirate flag" + } + } + }, + { + "country-flag", + { + { + "🇦🇨", + "flag" + }, + { + "🇦🇩", + "flag" + }, + { + "🇦🇪", + "flag" + }, + { + "🇦🇫", + "flag" + }, + { + "🇦🇬", + "flag" + }, + { + "🇦🇮", + "flag" + }, + { + "🇦🇱", + "flag" + }, + { + "🇦🇲", + "flag" + }, + { + "🇦🇴", + "flag" + }, + { + "🇦🇶", + "flag" + }, + { + "🇦🇷", + "flag" + }, + { + "🇦🇸", + "flag" + }, + { + "🇦🇹", + "flag" + }, + { + "🇦🇺", + "flag" + }, + { + "🇦🇼", + "flag" + }, + { + "🇦🇽", + "flag" + }, + { + "🇦🇿", + "flag" + }, + { + "🇧🇦", + "flag" + }, + { + "🇧🇧", + "flag" + }, + { + "🇧🇩", + "flag" + }, + { + "🇧🇪", + "flag" + }, + { + "🇧🇫", + "flag" + }, + { + "🇧🇬", + "flag" + }, + { + "🇧🇭", + "flag" + }, + { + "🇧🇮", + "flag" + }, + { + "🇧🇯", + "flag" + }, + { + "🇧🇱", + "flag" + }, + { + "🇧🇲", + "flag" + }, + { + "🇧🇳", + "flag" + }, + { + "🇧🇴", + "flag" + }, + { + "🇧🇶", + "flag" + }, + { + "🇧🇷", + "flag" + }, + { + "🇧🇸", + "flag" + }, + { + "🇧🇹", + "flag" + }, + { + "🇧🇻", + "flag" + }, + { + "🇧🇼", + "flag" + }, + { + "🇧🇾", + "flag" + }, + { + "🇧🇿", + "flag" + }, + { + "🇨🇦", + "flag" + }, + { + "🇨🇨", + "flag" + }, + { + "🇨🇩", + "flag" + }, + { + "🇨🇫", + "flag" + }, + { + "🇨🇬", + "flag" + }, + { + "🇨🇭", + "flag" + }, + { + "🇨🇮", + "flag" + }, + { + "🇨🇰", + "flag" + }, + { + "🇨🇱", + "flag" + }, + { + "🇨🇲", + "flag" + }, + { + "🇨🇳", + "flag" + }, + { + "🇨🇴", + "flag" + }, + { + "🇨🇵", + "flag" + }, + { + "🇨🇷", + "flag" + }, + { + "🇨🇺", + "flag" + }, + { + "🇨🇻", + "flag" + }, + { + "🇨🇼", + "flag" + }, + { + "🇨🇽", + "flag" + }, + { + "🇨🇾", + "flag" + }, + { + "🇨🇿", + "flag" + }, + { + "🇩🇪", + "flag" + }, + { + "🇩🇬", + "flag" + }, + { + "🇩🇯", + "flag" + }, + { + "🇩🇰", + "flag" + }, + { + "🇩🇲", + "flag" + }, + { + "🇩🇴", + "flag" + }, + { + "🇩🇿", + "flag" + }, + { + "🇪🇦", + "flag" + }, + { + "🇪🇨", + "flag" + }, + { + "🇪🇪", + "flag" + }, + { + "🇪🇬", + "flag" + }, + { + "🇪🇭", + "flag" + }, + { + "🇪🇷", + "flag" + }, + { + "🇪🇸", + "flag" + }, + { + "🇪🇹", + "flag" + }, + { + "🇪🇺", + "flag" + }, + { + "🇫🇮", + "flag" + }, + { + "🇫🇯", + "flag" + }, + { + "🇫🇰", + "flag" + }, + { + "🇫🇲", + "flag" + }, + { + "🇫🇴", + "flag" + }, + { + "🇫🇷", + "flag" + }, + { + "🇬🇦", + "flag" + }, + { + "🇬🇧", + "flag" + }, + { + "🇬🇩", + "flag" + }, + { + "🇬🇪", + "flag" + }, + { + "🇬🇫", + "flag" + }, + { + "🇬🇬", + "flag" + }, + { + "🇬🇭", + "flag" + }, + { + "🇬🇮", + "flag" + }, + { + "🇬🇱", + "flag" + }, + { + "🇬🇲", + "flag" + }, + { + "🇬🇳", + "flag" + }, + { + "🇬🇵", + "flag" + }, + { + "🇬🇶", + "flag" + }, + { + "🇬🇷", + "flag" + }, + { + "🇬🇸", + "flag" + }, + { + "🇬🇹", + "flag" + }, + { + "🇬🇺", + "flag" + }, + { + "🇬🇼", + "flag" + }, + { + "🇬🇾", + "flag" + }, + { + "🇭🇰", + "flag" + }, + { + "🇭🇲", + "flag" + }, + { + "🇭🇳", + "flag" + }, + { + "🇭🇷", + "flag" + }, + { + "🇭🇹", + "flag" + }, + { + "🇭🇺", + "flag" + }, + { + "🇮🇨", + "flag" + }, + { + "🇮🇩", + "flag" + }, + { + "🇮🇪", + "flag" + }, + { + "🇮🇱", + "flag" + }, + { + "🇮🇲", + "flag" + }, + { + "🇮🇳", + "flag" + }, + { + "🇮🇴", + "flag" + }, + { + "🇮🇶", + "flag" + }, + { + "🇮🇷", + "flag" + }, + { + "🇮🇸", + "flag" + }, + { + "🇮🇹", + "flag" + }, + { + "🇯🇪", + "flag" + }, + { + "🇯🇲", + "flag" + }, + { + "🇯🇴", + "flag" + }, + { + "🇯🇵", + "flag" + }, + { + "🇰🇪", + "flag" + }, + { + "🇰🇬", + "flag" + }, + { + "🇰🇭", + "flag" + }, + { + "🇰🇮", + "flag" + }, + { + "🇰🇲", + "flag" + }, + { + "🇰🇳", + "flag" + }, + { + "🇰🇵", + "flag" + }, + { + "🇰🇷", + "flag" + }, + { + "🇰🇼", + "flag" + }, + { + "🇰🇾", + "flag" + }, + { + "🇰🇿", + "flag" + }, + { + "🇱🇦", + "flag" + }, + { + "🇱🇧", + "flag" + }, + { + "🇱🇨", + "flag" + }, + { + "🇱🇮", + "flag" + }, + { + "🇱🇰", + "flag" + }, + { + "🇱🇷", + "flag" + }, + { + "🇱🇸", + "flag" + }, + { + "🇱🇹", + "flag" + }, + { + "🇱🇺", + "flag" + }, + { + "🇱🇻", + "flag" + }, + { + "🇱🇾", + "flag" + }, + { + "🇲🇦", + "flag" + }, + { + "🇲🇨", + "flag" + }, + { + "🇲🇩", + "flag" + }, + { + "🇲🇪", + "flag" + }, + { + "🇲🇫", + "flag" + }, + { + "🇲🇬", + "flag" + }, + { + "🇲🇭", + "flag" + }, + { + "🇲🇰", + "flag" + }, + { + "🇲🇱", + "flag" + }, + { + "🇲🇲", + "flag" + }, + { + "🇲🇳", + "flag" + }, + { + "🇲🇴", + "flag" + }, + { + "🇲🇵", + "flag" + }, + { + "🇲🇶", + "flag" + }, + { + "🇲🇷", + "flag" + }, + { + "🇲🇸", + "flag" + }, + { + "🇲🇹", + "flag" + }, + { + "🇲🇺", + "flag" + }, + { + "🇲🇻", + "flag" + }, + { + "🇲🇼", + "flag" + }, + { + "🇲🇽", + "flag" + }, + { + "🇲🇾", + "flag" + }, + { + "🇲🇿", + "flag" + }, + { + "🇳🇦", + "flag" + }, + { + "🇳🇨", + "flag" + }, + { + "🇳🇪", + "flag" + }, + { + "🇳🇫", + "flag" + }, + { + "🇳🇬", + "flag" + }, + { + "🇳🇮", + "flag" + }, + { + "🇳🇱", + "flag" + }, + { + "🇳🇴", + "flag" + }, + { + "🇳🇵", + "flag" + }, + { + "🇳🇷", + "flag" + }, + { + "🇳🇺", + "flag" + }, + { + "🇳🇿", + "flag" + }, + { + "🇴🇲", + "flag" + }, + { + "🇵🇦", + "flag" + }, + { + "🇵🇪", + "flag" + }, + { + "🇵🇫", + "flag" + }, + { + "🇵🇬", + "flag" + }, + { + "🇵🇭", + "flag" + }, + { + "🇵🇰", + "flag" + }, + { + "🇵🇱", + "flag" + }, + { + "🇵🇲", + "flag" + }, + { + "🇵🇳", + "flag" + }, + { + "🇵🇷", + "flag" + }, + { + "🇵🇸", + "flag" + }, + { + "🇵🇹", + "flag" + }, + { + "🇵🇼", + "flag" + }, + { + "🇵🇾", + "flag" + }, + { + "🇶🇦", + "flag" + }, + { + "🇷🇪", + "flag" + }, + { + "🇷🇴", + "flag" + }, + { + "🇷🇸", + "flag" + }, + { + "🇷🇺", + "flag" + }, + { + "🇷🇼", + "flag" + }, + { + "🇸🇦", + "flag" + }, + { + "🇸🇧", + "flag" + }, + { + "🇸🇨", + "flag" + }, + { + "🇸🇩", + "flag" + }, + { + "🇸🇪", + "flag" + }, + { + "🇸🇬", + "flag" + }, + { + "🇸🇭", + "flag" + }, + { + "🇸🇮", + "flag" + }, + { + "🇸🇯", + "flag" + }, + { + "🇸🇰", + "flag" + }, + { + "🇸🇱", + "flag" + }, + { + "🇸🇲", + "flag" + }, + { + "🇸🇳", + "flag" + }, + { + "🇸🇴", + "flag" + }, + { + "🇸🇷", + "flag" + }, + { + "🇸🇸", + "flag" + }, + { + "🇸🇹", + "flag" + }, + { + "🇸🇻", + "flag" + }, + { + "🇸🇽", + "flag" + }, + { + "🇸🇾", + "flag" + }, + { + "🇸🇿", + "flag" + }, + { + "🇹🇦", + "flag" + }, + { + "🇹🇨", + "flag" + }, + { + "🇹🇩", + "flag" + }, + { + "🇹🇫", + "flag" + }, + { + "🇹🇬", + "flag" + }, + { + "🇹🇭", + "flag" + }, + { + "🇹🇯", + "flag" + }, + { + "🇹🇰", + "flag" + }, + { + "🇹🇱", + "flag" + }, + { + "🇹🇲", + "flag" + }, + { + "🇹🇳", + "flag" + }, + { + "🇹🇴", + "flag" + }, + { + "🇹🇷", + "flag" + }, + { + "🇹🇹", + "flag" + }, + { + "🇹🇻", + "flag" + }, + { + "🇹🇼", + "flag" + }, + { + "🇹🇿", + "flag" + }, + { + "🇺🇦", + "flag" + }, + { + "🇺🇬", + "flag" + }, + { + "🇺🇲", + "flag" + }, + { + "🇺🇳", + "flag" + }, + { + "🇺🇸", + "flag" + }, + { + "🇺🇾", + "flag" + }, + { + "🇺🇿", + "flag" + }, + { + "🇻🇦", + "flag" + }, + { + "🇻🇨", + "flag" + }, + { + "🇻🇪", + "flag" + }, + { + "🇻🇬", + "flag" + }, + { + "🇻🇮", + "flag" + }, + { + "🇻🇳", + "flag" + }, + { + "🇻🇺", + "flag" + }, + { + "🇼🇫", + "flag" + }, + { + "🇼🇸", + "flag" + }, + { + "🇽🇰", + "flag" + }, + { + "🇾🇪", + "flag" + }, + { + "🇾🇹", + "flag" + }, + { + "🇿🇦", + "flag" + }, + { + "🇿🇲", + "flag" + }, + { + "🇿🇼", + "flag" + } + } + }, + { + "subdivision-flag", + { + { + "🏴󠁧󠁢󠁥󠁮󠁧󠁿", + "flag" + }, + { + "🏴󠁧󠁢󠁳󠁣󠁴󠁿", + "flag" + }, + { + "🏴󠁧󠁢󠁷󠁬󠁳󠁿", + "flag" + } + } + } + } +}; + +static std::array db { + std::move(emoji_Smileys___Emotion), + std::move(emoji_People___Body), + std::move(emoji_Animals___Nature), + std::move(emoji_Food___Drink), + std::move(emoji_Travel___Places), + std::move(emoji_Activities), + std::move(emoji_Objects), + std::move(emoji_Symbols), + std::move(emoji_Flags) +}; + +static std::map ranges = { + {35, 35}, + {42, 42}, + {48, 57}, + {169, 169}, + {174, 174}, + {8252, 8252}, + {8265, 8265}, + {8482, 8482}, + {8505, 8505}, + {8596, 8601}, + {8617, 8618}, + {8986, 8987}, + {9000, 9000}, + {9167, 9167}, + {9193, 9203}, + {9208, 9210}, + {9410, 9410}, + {9642, 9643}, + {9654, 9654}, + {9664, 9664}, + {9723, 9726}, + {9728, 9732}, + {9742, 9742}, + {9745, 9745}, + {9748, 9749}, + {9752, 9752}, + {9757, 9757}, + {9760, 9760}, + {9762, 9763}, + {9766, 9766}, + {9770, 9770}, + {9774, 9775}, + {9784, 9786}, + {9792, 9792}, + {9794, 9794}, + {9800, 9811}, + {9823, 9824}, + {9827, 9827}, + {9829, 9830}, + {9832, 9832}, + {9851, 9851}, + {9854, 9855}, + {9874, 9879}, + {9881, 9881}, + {9883, 9884}, + {9888, 9889}, + {9895, 9895}, + {9898, 9899}, + {9904, 9905}, + {9917, 9918}, + {9924, 9925}, + {9928, 9928}, + {9934, 9935}, + {9937, 9937}, + {9939, 9940}, + {9961, 9962}, + {9968, 9973}, + {9975, 9978}, + {9981, 9981}, + {9986, 9986}, + {9989, 9989}, + {9992, 9997}, + {9999, 9999}, + {10002, 10002}, + {10004, 10004}, + {10006, 10006}, + {10013, 10013}, + {10017, 10017}, + {10024, 10024}, + {10035, 10036}, + {10052, 10052}, + {10055, 10055}, + {10060, 10060}, + {10062, 10062}, + {10067, 10069}, + {10071, 10071}, + {10083, 10084}, + {10133, 10135}, + {10145, 10145}, + {10160, 10160}, + {10175, 10175}, + {10548, 10549}, + {11013, 11015}, + {11035, 11036}, + {11088, 11088}, + {11093, 11093}, + {12336, 12336}, + {12349, 12349}, + {12951, 12951}, + {12953, 12953}, + {126980, 126980}, + {127183, 127183}, + {127344, 127345}, + {127358, 127359}, + {127374, 127374}, + {127377, 127386}, + {127462, 127487}, + {127489, 127490}, + {127514, 127514}, + {127535, 127535}, + {127538, 127546}, + {127568, 127569}, + {127744, 127777}, + {127780, 127891}, + {127894, 127895}, + {127897, 127899}, + {127902, 127984}, + {127987, 127989}, + {127991, 127994}, + {128000, 128253}, + {128255, 128317}, + {128329, 128334}, + {128336, 128359}, + {128367, 128368}, + {128371, 128378}, + {128391, 128391}, + {128394, 128397}, + {128400, 128400}, + {128405, 128406}, + {128420, 128421}, + {128424, 128424}, + {128433, 128434}, + {128444, 128444}, + {128450, 128452}, + {128465, 128467}, + {128476, 128478}, + {128481, 128481}, + {128483, 128483}, + {128488, 128488}, + {128495, 128495}, + {128499, 128499}, + {128506, 128591}, + {128640, 128709}, + {128715, 128722}, + {128725, 128727}, + {128732, 128741}, + {128745, 128745}, + {128747, 128748}, + {128752, 128752}, + {128755, 128764}, + {128992, 129003}, + {129008, 129008}, + {129292, 129338}, + {129340, 129349}, + {129351, 129455}, + {129460, 129535}, + {129648, 129660}, + {129664, 129672}, + {129680, 129725}, + {129727, 129733}, + {129742, 129755}, + {129760, 129768}, + {129776, 129784} +}; + +// clang-format on diff --git a/src/widgets/emojiregistry.cpp b/src/widgets/emojiregistry.cpp index 3c70e1b419..d12f721519 100644 --- a/src/widgets/emojiregistry.cpp +++ b/src/widgets/emojiregistry.cpp @@ -21,7 +21,7 @@ #include "emojiregistry.h" -#include "emojidb.cpp" // don't move this line to other headers +#include "emojidb.h" // don't move this line to other headers const EmojiRegistry &EmojiRegistry::instance() { @@ -32,7 +32,7 @@ const EmojiRegistry &EmojiRegistry::instance() bool EmojiRegistry::isEmoji(const QString &code) const { auto cat = startCategory(code); - return cat == Category::Emoji || cat == Category::SkinTone; + return cat == Category::Emoji || cat == Category::SkinTone || cat == Category::HairStyle; // TODO check the whole code is emoji. not just start } @@ -70,6 +70,8 @@ EmojiRegistry::Category EmojiRegistry::startCategory(QStringView in) const if (found) { if (ucs >= 0x1f3fb && ucs <= 0x1f3ff) return Category::SkinTone; + if (ucs >= 0x1f9b0 && ucs <= 0x1f9b2) + return Category::HairStyle; return Category::Emoji; // there more cases to review. like emoji tags/flags etc } return Category::None; @@ -92,6 +94,7 @@ std::pair EmojiRegistry::findEmoji(const QString &in, int idx) bool gotEmoji = false; bool gotSkin = false; + bool gotHair = false; bool gotFQ = false; for (; idx < in.size(); idx++) { auto category = startCategory(QStringView { in }.mid(idx, in.size() - idx)); @@ -103,16 +106,21 @@ std::pair EmojiRegistry::findEmoji(const QString &in, int idx) } else if (category == Category::FullQualify) { if (gotFQ) break; // double qualification is an error - gotSkin = true; // we can't get skin false after fill qualification + gotSkin = true; // we can't get skin false after full qualification + gotHair = true; gotFQ = true; } else if (category == Category::SkinTone) { if (gotSkin) break; // can't have 2 skin tones in the same time gotSkin = true; + } else if (category == Category::HairStyle) { + if (gotHair) + break; // can't have 2 hair styles in the same time + gotHair = true; } else break; // TODO review other categories when implemented } else if (!gotEmoji - && (category == Category::Emoji || category == Category::SkinTone + && (category == Category::Emoji || category == Category::SkinTone || category == Category::HairStyle || category == Category::SimpleKeycap)) { if (emojiStart == -1) emojiStart = idx; diff --git a/src/widgets/emojiregistry.h b/src/widgets/emojiregistry.h index 4ea6630b03..5910d2e83d 100644 --- a/src/widgets/emojiregistry.h +++ b/src/widgets/emojiregistry.h @@ -22,12 +22,13 @@ #include +#include #include #include class EmojiRegistry { public: - enum class Category { None, Emoji, SkinTone, ZWJ, FullQualify, SimpleKeycap }; + enum class Category { None, Emoji, SkinTone, HairStyle, ZWJ, FullQualify, SimpleKeycap }; struct Emoji { const QString code; @@ -44,7 +45,7 @@ class EmojiRegistry { const std::vector subGroups; }; - const std::vector groups; + const std::array groups; static const EmojiRegistry &instance(); @@ -63,6 +64,12 @@ class EmojiRegistry { int count() const; struct iterator { + using iterator_category = std::forward_iterator_tag; + using difference_type = std::size_t; + using value_type = EmojiRegistry::Emoji; + using pointer = EmojiRegistry::Emoji *; // or also value_type* + using reference = EmojiRegistry::Emoji &; // or also value_type& + int group_idx = 0; int subgroup_idx = 0; int emoji_idx = 0; @@ -77,6 +84,9 @@ class EmojiRegistry { { return EmojiRegistry::instance().groups[group_idx].subGroups[subgroup_idx].emojis[emoji_idx]; } + + const Group &group() const { return EmojiRegistry::instance().groups[group_idx]; } + const SubGroup &subGroup() const { return EmojiRegistry::instance().groups[group_idx].subGroups[subgroup_idx]; } }; inline iterator begin() const { return {}; } diff --git a/src/widgets/iconaction.h b/src/widgets/iconaction.h index 2e925a2e19..e9194d6753 100644 --- a/src/widgets/iconaction.h +++ b/src/widgets/iconaction.h @@ -36,12 +36,16 @@ class IconAction : public QAction { IconAction(QObject *parent, const QString &name = QString()); IconAction(const QString &statusTip, const QString &icon, const QString &text, QKeySequence accel, QObject *parent, const QString &name = QString(), bool checkable = false); + IconAction(const QString &statusTip, const QString &icon, const QString &text, QList accel, QObject *parent, const QString &name = QString(), bool checkable = false); + IconAction(const QString &statusTip, const QString &text, QKeySequence accel, QObject *parent, const QString &name = QString(), bool checkable = false); + IconAction(const QString &statusTip, const QString &text, QList accel, QObject *parent, const QString &name = QString(), bool checkable = false); + IconAction(const QString &text, QObject *parent, const QString &icon); ~IconAction(); diff --git a/src/widgets/iconselect.cpp b/src/widgets/iconselect.cpp index d4ef96de79..75f9a2f792 100644 --- a/src/widgets/iconselect.cpp +++ b/src/widgets/iconselect.cpp @@ -23,6 +23,7 @@ #include "emojiregistry.h" #include "iconaction.h" #include "iconset.h" +// #include "qdebug.h" #include #include @@ -31,6 +32,7 @@ #include #include #include +#include #include #include #include @@ -39,6 +41,38 @@ #include #include +#include +#include + +namespace { +struct Item { + PsiIcon *icon = nullptr; + const EmojiRegistry::Emoji *emoji = nullptr; + + bool operator==(const Item &other) const + { + if (emoji) + return emoji == other.emoji; + if (icon != nullptr && other.icon != nullptr) + return icon->name() == other.icon->name(); + return false; + } +}; +using List = std::deque; + +QHash hashedIconset(const Iconset &is) +{ + QHash isIcons; + isIcons.reserve(is.count() * 5); + for (auto const &icon : is) { + for (auto const &text : icon->text()) { + isIcons.insert(text.text, icon); + } + } + return isIcons; +} + +} //---------------------------------------------------------------------------- // IconSelectButton @@ -53,46 +87,47 @@ class IconSelectButton : public QAbstractButton { Q_OBJECT private: - PsiIcon *ic; - QSize s; - QSize maxIconSize; - bool animated; + Item item_; + QSize s; + QSize maxIconSize; + bool animated; public: IconSelectButton(QWidget *parent) : QAbstractButton(parent) { - ic = nullptr; + item_ = {}; animated = false; - connect(this, SIGNAL(clicked()), SLOT(iconClicked())); } ~IconSelectButton() { iconStop(); - if (ic) { - delete ic; - ic = nullptr; + if (item_.icon) { + delete item_.icon; + item_.icon = nullptr; } } - void setIcon(const PsiIcon *i, const QSize &maxSize = QSize()) + // const PsiIcon *icon() const { return ic; } + + void setItem(const Item &newItem, const QSize &maxSize = {}) { iconStop(); - - if (ic) { - delete ic; - ic = nullptr; + if (item_.icon) { + delete item_.icon; } - + item_ = newItem; maxIconSize = maxSize; - if (i) - ic = new PsiIcon(*(const_cast(i))); - else - ic = nullptr; + if (item_.icon) { + item_.icon = new PsiIcon(*(const_cast(item_.icon))); + } else { + setText(item_.emoji->code); + setToolTip(item_.emoji->name); + } } - const PsiIcon *icon() const { return ic; } + const Item &item() const { return item_; } QSize sizeHint() const { return s; } void setSizeHint(QSize sh) { s = sh; } @@ -104,17 +139,17 @@ class IconSelectButton : public QAbstractButton { private: void iconStart() { - if (ic) { - connect(ic, SIGNAL(pixmapChanged()), SLOT(iconUpdated())); + if (item_.icon) { + connect(item_.icon, SIGNAL(pixmapChanged()), SLOT(iconUpdated())); if (!animated) { - ic->activated(false); + item_.icon->activated(false); animated = true; } - if (!ic->text().isEmpty()) { + if (!item_.icon->text().isEmpty()) { // and list of possible variants in the ToolTip QStringList toolTip; - for (const PsiIcon::IconText &t : ic->text()) { + for (const PsiIcon::IconText &t : item_.icon->text()) { toolTip += t.text; } @@ -129,10 +164,10 @@ class IconSelectButton : public QAbstractButton { void iconStop() { - if (ic) { - disconnect(ic, nullptr, this, nullptr); + if (item_.icon) { + disconnect(item_.icon, nullptr, this, nullptr); if (animated) { - ic->stop(); + item_.icon->stop(); animated = false; } } @@ -158,17 +193,6 @@ class IconSelectButton : public QAbstractButton { private slots: void iconUpdated() { update(); } - void iconClicked() - { - clearFocus(); - if (ic) { - emit iconSelected(ic); - emit textSelected(ic->defaultText()); - } else { - emit textSelected(text()); - } - } - private: // reimplemented void paintEvent(QPaintEvent *) @@ -186,8 +210,8 @@ private slots: opt.rect = rect(); style()->drawControl(QStyle::CE_MenuItem, &opt, &p, this); - if (ic) { - QPixmap pix = ic->pixmap(maxIconSize); + if (item_.icon) { + QPixmap pix = item_.icon->pixmap(maxIconSize); if (pix.width() > maxIconSize.width() || pix.height() > maxIconSize.height()) { pix = pix.scaled(maxIconSize, Qt::KeepAspectRatio, Qt::SmoothTransformation); } @@ -208,30 +232,47 @@ class IconSelect : public QWidget { private: IconSelectPopup *menu; - Iconset is; - QGridLayout *grid; - QString titleFilter; - bool shown; - bool emojiSorting; + + Iconset is; + std::optional icons_; // when set this will rendered instead of Iconset + + std::optional rowSize_; // explicit row size in columns + QSizeF preferredIconSize_; + + QGridLayout *grid; + QString titleFilter; + bool shown; + bool emojiSorting; signals: void updatedGeometry(); + void selected(IconSelectButton *); public: - IconSelect(IconSelectPopup *parentMenu); + IconSelect(IconSelectPopup *parentMenu, const char *objectName); ~IconSelect(); - void setIconset(const Iconset &); - const Iconset &iconset() const; + void setIconset(const Iconset &); + inline const Iconset &iconset() const { return is; } - void setEmojiSortingEnabled(bool enabled); - void setTitleFilter(const QString &title); + void setIcons(const List &); + inline const std::optional &icons() const { return icons_; } + + inline void setEmojiSortingEnabled(bool enabled) { emojiSorting = enabled; } + void setTitleFilter(const QString &title); + + inline void setRowSize(int rs) { rowSize_ = rs; } + inline std::optional rowSize() const { return rowSize_; } + + inline QSizeF preferredIconSize() const { return preferredIconSize_; } + inline void setPreferredIconSize(const QSizeF &size) { preferredIconSize_ = size; } protected: - QList sortEmojis() const; - void noIcons(); - void createLayout(); - void updateGrid(); + QList sortEmojis() const; + void noIcons(); + void createLayout(); + void updateGrid(); + std::pair computeIconSize() const; void paintEvent(QPaintEvent *) { @@ -247,8 +288,9 @@ protected slots: void closeMenu(); }; -IconSelect::IconSelect(IconSelectPopup *parentMenu) : QWidget(parentMenu) +IconSelect::IconSelect(IconSelectPopup *parentMenu, const char *objectName) : QWidget(parentMenu) { + setObjectName(QLatin1String(objectName)); menu = parentMenu; connect(menu, SIGNAL(textSelected(QString)), SLOT(closeMenu())); @@ -291,8 +333,18 @@ void IconSelect::noIcons() void IconSelect::setIconset(const Iconset &iconset) { - is = iconset; - shown = false; // we need to recompute geometry + rowSize_ = {}; + preferredIconSize_ = {}; + is = iconset; + shown = false; // we need to recompute geometry + updateGrid(); +} + +void IconSelect::setIcons(const List &l) +{ + // rowSize_ = {}; + icons_ = l; + shown = false; updateGrid(); } @@ -308,117 +360,89 @@ void IconSelect::updateGrid() qDeleteAll(list); } - bool fontEmojiMode = is.count() == 0; - // first we need to find optimal size for elements and don't forget about // taking too much screen space - float w = 0, h = 0; -#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) - const auto fontSz = qApp->fontMetrics().height(); + auto [iconSize, maxPrefSize] = computeIconSize(); + preferredIconSize_ = iconSize; + + // make emoji font + auto font = qApp->font(); +#if defined(Q_OS_WIN) + font.setFamily("Segoe UI Emoji"); +#elif defined(Q_OS_MAC) + font.setFamily("Apple Color Emoji"); #else - const auto fontSz = QFontMetrics(qApp->font()).height(); + font.setFamily("Noto Color Emoji"); #endif - int maxPrefTileHeight = fontSz * 3; - auto maxPrefSize = QSize(maxPrefTileHeight, maxPrefTileHeight); - - double count; // the 'double' type is somewhat important for MSVC.NET here - QList emojis; - if (fontEmojiMode) { - for (auto const &emoji : EmojiRegistry::instance()) { + font.setPixelSize(std::round(iconSize.height())); + + List toRender; + if (icons_.has_value()) { + toRender = *icons_; + } else if (is.count() > 0) { + auto sorted = sortEmojis(); + std::transform(sorted.begin(), sorted.end(), std::back_inserter(toRender), + [](auto icon) { return Item { icon }; }); + } else { + QSizeF totalSize; + auto const &er = EmojiRegistry::instance(); + for (auto it = er.begin(); it != er.end(); ++it) { +#ifdef Q_OS_WIN + if (it.subGroup().name == QStringLiteral("country-flag")) { + continue; + } +#endif + auto const &emoji = *it; if (titleFilter.isEmpty() || emoji.name.contains(titleFilter)) { - emojis.append(&emoji); - if (!titleFilter.isEmpty() && emojis.size() == 40) { - break; + auto rect = QFontMetrics(font).boundingRect(emoji.code); + if (!rect.isEmpty() && rect.width() < rect.height() * 1.4 && rect.height() < rect.width() * 1.4) { + totalSize += rect.size(); + toRender.emplace_back(nullptr, &emoji); + if (!titleFilter.isEmpty() && toRender.size() == 40) { + break; + } } } } - - count = emojis.count(); - w = fontSz * 2.5; - h = fontSz * 2.5; - } else { - QListIterator it = is.iterator(); - for (count = 0; it.hasNext(); count++) { - PsiIcon *icon = it.next(); - auto pix = icon->pixmap(maxPrefSize); - auto pixSize = pix.size(); - if (pix.width() > maxPrefSize.width() || pix.height() > maxPrefSize.height()) { - pixSize.scale(maxPrefSize, Qt::KeepAspectRatio); - } - w += pixSize.width(); - h += pixSize.height(); + if (!toRender.empty()) { + iconSize = totalSize / toRender.size(); } - w /= float(count); - h /= float(count); } const int margin = 2; - int tileSize = int(qMax(w, h)) + 2 * margin; + int tileSize = int(qMax(iconSize.width(), iconSize.height())) + 2 * margin; QRect r = menu->screen()->availableGeometry(); int maxSize = qMin(r.width(), r.height()) / 3; - int size = int(ceil(std::sqrt(count))); - int maxColumns = int(maxSize / tileSize); - size = size > maxColumns ? maxColumns : size; + if (!rowSize_.has_value()) { + rowSize_ = int(ceil(std::sqrt(double(toRender.size())))); + int maxColumns = int(maxSize / tileSize); + rowSize_ = *rowSize_ > maxColumns ? maxColumns : *rowSize_; + } + + // qDebug() << objectName() << " tileSize=" << tileSize << " rowSize=" << *rowSize_; // now, fill grid with elements createLayout(); int row = 0; int column = 0; - - if (fontEmojiMode) { - auto font = qApp->font(); - if (font.pointSize() == -1) - font.setPixelSize(font.pixelSize() * 2.5); - else - font.setPointSize(font.pointSize() * 2.5); -#if defined(Q_OS_WIN) - font.setFamily("Segoe UI Emoji"); -#elif defined(Q_OS_MAC) - font.setFamily("Apple Color Emoji"); -#else - font.setFamily("Noto Color Emoji"); -#endif - for (auto const &emoji : emojis) { - IconSelectButton *b = new IconSelectButton(this); - b->setFont(font); - grid->addWidget(b, row, column); - b->setText(emoji->code); - b->setToolTip(emoji->name); - b->setSizeHint(QSize(tileSize, tileSize)); - connect(b, qOverload(&IconSelectButton::iconSelected), menu, - &IconSelectPopup::iconSelected); - connect(b, &IconSelectButton::textSelected, menu, &IconSelectPopup::textSelected); - - if (++column >= size) { - ++row; - column = 0; - } - } - } else { - QListIterator it = is.iterator(); - QList sortIcons; - if (emojiSorting) { - sortIcons = sortEmojis(); - it = QListIterator(sortIcons); - } - while (it.hasNext()) { - IconSelectButton *b = new IconSelectButton(this); - grid->addWidget(b, row, column); - b->setIcon(it.next(), maxPrefSize); - b->setSizeHint(QSize(tileSize, tileSize)); - connect(b, qOverload(&IconSelectButton::iconSelected), menu, - &IconSelectPopup::iconSelected); - connect(b, &IconSelectButton::textSelected, menu, &IconSelectPopup::textSelected); - - if (++column >= size) { - ++row; - column = 0; - } + for (auto const &item : toRender) { + IconSelectButton *b = new IconSelectButton(this); + b->setFont(font); + b->setItem(item, maxPrefSize); + b->setFixedSize(QSize(tileSize, tileSize)); + // b->setSizeHint(QSize(tileSize, tileSize)); + connect(b, &QAbstractButton::clicked, this, [b, this]() { emit selected(b); }); + grid->addWidget(b, row, column); + + if (++column >= *rowSize_) { + ++row; + column = 0; } } + blockSignals(false); if (!shown) { shown = true; @@ -426,9 +450,53 @@ void IconSelect::updateGrid() } } -const Iconset &IconSelect::iconset() const { return is; } +std::pair IconSelect::computeIconSize() const +{ + QSizeF iconSize; +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) + const auto fontSz = qApp->fontMetrics().height(); +#else + const auto fontSz = QFontMetrics(qApp->font()).height(); +#endif + int maxPrefTileHeight = fontSz * 3; + auto maxPrefSize = QSize(maxPrefTileHeight, maxPrefTileHeight); -void IconSelect::setEmojiSortingEnabled(bool enabled) { emojiSorting = enabled; } + auto scaledIconSize = [&maxPrefSize](auto icon) { + auto pix = icon->pixmap(maxPrefSize); + auto pixSize = pix.size(); + if (pix.width() > maxPrefSize.width() || pix.height() > maxPrefSize.height()) { + pixSize.scale(maxPrefSize, Qt::KeepAspectRatio); + } + return pixSize; + }; + + if (is.count() > 0) { + for (auto const icon : is) { + iconSize += scaledIconSize(icon); + } + iconSize /= is.count(); + } else if (icons_.has_value()) { + int cnt = 0; + for (auto const &icon : std::as_const(*icons_)) { + if (icon.icon) { + iconSize += scaledIconSize(icon.icon); + cnt++; + } + } + if (cnt) { + iconSize /= cnt; + } + } + + if (iconSize.isEmpty()) { + if (!preferredIconSize_.isEmpty()) { + iconSize = preferredIconSize_; + } else { + iconSize = { fontSz * 2.0, fontSz * 2.0 }; + } + } + return { iconSize, maxPrefSize }; +} void IconSelect::setTitleFilter(const QString &title) { @@ -461,7 +529,7 @@ QList IconSelect::sortEmojis() const for (auto const &group : EmojiRegistry::instance().groups) { for (auto const &subgroup : group.subGroups) { for (auto const &emoji : subgroup.emojis) { - auto icon = cp2icon.value(emoji.code); + auto icon = cp2icon.take(emoji.code); if (icon) { ret.append(icon); } @@ -469,7 +537,10 @@ QList IconSelect::sortEmojis() const } } + auto unused = cp2icon.values(); + auto unique = QSet(unused.begin(), unused.end()); ret += notEmoji; + std::copy(unique.begin(), unique.end(), std::back_inserter(ret)); return ret; } @@ -479,70 +550,198 @@ QList IconSelect::sortEmojis() const class IconSelectPopup::Private : public QObject { Q_OBJECT + public: - Private(IconSelectPopup *parent) : QObject(parent), parent_(parent), icsel_(nullptr), emotsAction_(nullptr) { } + Private(IconSelectPopup *parent) : QObject(parent), parent_(parent) { } + ~Private() + { + for (auto const &item : recent) { + if (item.icon) { + delete item.icon; + } + } + } IconSelectPopup *parent_; - IconSelect *icsel_; - QWidgetAction *emotsAction_; - QScrollArea *scrollArea_; - ActionLineEdit *findBar_; - IconAction *findAct_; - QWidgetAction *findAction_; + + Iconset recentIconset; + IconSelect *recentSel_ = nullptr; + QWidgetAction *recentAction_ = nullptr; + // QScrollArea *recentScrollArea_ = nullptr; + + IconSelect *emotsSel_ = nullptr; + QWidgetAction *emotsAction_ = nullptr; + QScrollArea *emotsScrollArea_ = nullptr; + + ActionLineEdit *findBar_ = nullptr; + IconAction *findAct_ = nullptr; + QWidgetAction *findAction_ = nullptr; + List recent; public slots: void updatedGeometry() { - emotsAction_->setDefaultWidget(scrollArea_); - QRect r = scrollArea_->screen()->availableGeometry(); - int maxSize = qMin(r.width(), r.height()) / 3; - int vBarWidth - = scrollArea_->verticalScrollBar()->isEnabled() ? scrollArea_->verticalScrollBar()->sizeHint().rwidth() : 0; - scrollArea_->setMinimumWidth(icsel_->sizeHint().rwidth() + vBarWidth); - scrollArea_->setMinimumHeight(qMin(icsel_->sizeHint().rheight(), maxSize)); - scrollArea_->setFrameStyle(QFrame::Plain); + emotsAction_->setDefaultWidget(emotsScrollArea_); + QRect r = emotsScrollArea_->screen()->availableGeometry(); + int maxSize = qMin(r.width(), r.height()) / 3; + int vBarWidth = emotsScrollArea_->verticalScrollBar()->isEnabled() + ? emotsScrollArea_->verticalScrollBar()->sizeHint().rwidth() + : 0; + emotsScrollArea_->setMinimumWidth(emotsSel_->sizeHint().rwidth() + vBarWidth); + emotsScrollArea_->setMinimumHeight(qMin(emotsSel_->sizeHint().rheight(), maxSize)); + emotsScrollArea_->setFrameStyle(QFrame::Plain); + + if (emotsSel_->rowSize()) { + recentSel_->setRowSize(*emotsSel_->rowSize()); + } + recentAction_->setDefaultWidget(recentSel_); parent_->removeAction(emotsAction_); - parent_->addAction(emotsAction_); // add menu item - findAct_->setPsiIcon("psi/search"); + parent_->removeAction(recentAction_); + + parent_->addAction(recentAction_); + parent_->addAction(emotsAction_); + findBar_->setFocus(); + } + + void updateRecentGeometry() + { + parent_->removeAction(recentAction_); + parent_->insertAction(emotsAction_, recentAction_); + } + + void selected(IconSelectButton *btn) + { + btn->clearFocus(); + + auto const &item = btn->item(); + auto it = std::find_if(recent.begin(), recent.end(), [&item](auto const &r) { return item == r; }); + + auto rotated = false; + if (it != recent.end()) { + auto idx = std::distance(recent.begin(), it); + if (idx != 0) { + std::rotate(recent.begin(), recent.begin() + idx, recent.begin() + idx + 1); + rotated = true; + } + } else if (emotsSel_->rowSize()) { + auto copyItem = item; + if (copyItem.icon) { + copyItem.icon = new PsiIcon(*copyItem.icon); + } + recent.push_front(copyItem); + if (recent.size() > *emotsSel_->rowSize() * 3) { + if (recent.back().icon) { + delete recent.back().icon; + } + recent.pop_back(); + } + rotated = true; + } + + if (item.icon) { + emit parent_->iconSelected(item.icon); + emit parent_->textSelected(item.icon->defaultText()); + } else { + emit parent_->textSelected(btn->text()); + } + + if (rotated) { + recentSel_->setIcons(recent); + } } - void setTitleFilter(const QString &filter) { icsel_->setTitleFilter(filter); } + void setTitleFilter(const QString &filter) { emotsSel_->setTitleFilter(filter); } }; -IconSelectPopup::IconSelectPopup(QWidget *parent) : QMenu(parent) +IconSelectPopup::IconSelectPopup(QWidget *parent) : QMenu(parent), d(new Private(this)) { - d = new Private(this); - d->icsel_ = new IconSelect(this); - d->findAction_ = new QWidgetAction(this); - d->findBar_ = new ActionLineEdit(nullptr); + d->findBar_ = new ActionLineEdit(this); d->findAct_ = new IconAction(d->findBar_); + d->findAct_->setPsiIcon("psi/search"); d->findBar_->addAction(d->findAct_); d->findAction_->setDefaultWidget(d->findBar_); - addAction(d->findAction_); + d->parent_->addAction(d->findAction_); + connect(d->findBar_, &QLineEdit::textChanged, d, &Private::setTitleFilter); - d->emotsAction_ = new QWidgetAction(this); - d->scrollArea_ = new QScrollArea(this); - d->scrollArea_->setWidget(d->icsel_); - d->scrollArea_->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - d->scrollArea_->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); - d->scrollArea_->setWidgetResizable(true); - connect(d->icsel_, &IconSelect::updatedGeometry, d, &IconSelectPopup::Private::updatedGeometry); + d->recentSel_ = new IconSelect(this, "recentSelect"); + d->recentAction_ = new QWidgetAction(this); + connect(d->recentSel_, &IconSelect::updatedGeometry, d, &IconSelectPopup::Private::updateRecentGeometry); + connect(d->recentSel_, &IconSelect::selected, d, &IconSelectPopup::Private::selected); + + d->emotsSel_ = new IconSelect(this, "emotsSelect"); + d->emotsSel_->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + d->emotsAction_ = new QWidgetAction(this); + d->emotsScrollArea_ = new QScrollArea(this); + d->emotsScrollArea_->setWidget(d->emotsSel_); + d->emotsScrollArea_->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + d->emotsScrollArea_->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); + d->emotsScrollArea_->setWidgetResizable(true); + d->emotsScrollArea_->setSizeAdjustPolicy(QAbstractScrollArea::AdjustToContents); + connect(d->emotsSel_, &IconSelect::updatedGeometry, d, &IconSelectPopup::Private::updatedGeometry); + connect(d->emotsSel_, &IconSelect::selected, d, &IconSelectPopup::Private::selected); + d->updatedGeometry(); } -IconSelectPopup::~IconSelectPopup() +IconSelectPopup::~IconSelectPopup() { } + +void IconSelectPopup::setIconset(const Iconset &i) { - d->findAction_->setDefaultWidget(nullptr); - delete d->findBar_; + auto prev_recent = recent(); + for (auto const &r : d->recent) { + if (r.icon != nullptr) + delete r.icon; + } + d->recent.clear(); + d->emotsSel_->setIconset(i); + setRecent(prev_recent); } -void IconSelectPopup::setIconset(const Iconset &i) { d->icsel_->setIconset(i); } +const Iconset &IconSelectPopup::iconset() const { return d->emotsSel_->iconset(); } -const Iconset &IconSelectPopup::iconset() const { return d->icsel_->iconset(); } +void IconSelectPopup::setEmojiSortingEnabled(bool enabled) { d->emotsSel_->setEmojiSortingEnabled(enabled); } + +void IconSelectPopup::setRecent(const QStringList &recent) +{ + List list; + auto const &er = EmojiRegistry::instance(); -void IconSelectPopup::setEmojiSortingEnabled(bool enabled) { d->icsel_->setEmojiSortingEnabled(enabled); } + auto isIcons = hashedIconset(d->emotsSel_->iconset()); + + for (auto const &text : std::as_const(recent)) { + auto icon = isIcons.value(text); + if (icon) { + list.emplace_back(const_cast(icon->copy()), nullptr); + continue; + } + auto it = std::find_if(er.begin(), er.end(), [text](const EmojiRegistry::Emoji &e) { return e.code == text; }); + if (it != er.end()) { + list.emplace_back(nullptr, &*it); + } + } + d->recent = list; + d->recentSel_->setPreferredIconSize(d->emotsSel_->preferredIconSize()); + d->recentSel_->setIcons(d->recent); +} + +QStringList IconSelectPopup::recent() const +{ + QStringList ret; + for (auto const &r : d->recent) { + QString txt; + if (r.icon) { + txt = r.icon->defaultText(); + } else { + txt = r.emoji->code; + } + if (!ret.contains(txt)) { + ret << txt; + } + } + return ret; +} /** It's used by child widget to close the menu by simulating a @@ -551,4 +750,10 @@ void IconSelectPopup::setEmojiSortingEnabled(bool enabled) { d->icsel_->setEmoji */ void IconSelectPopup::mousePressEvent(QMouseEvent *e) { QMenu::mousePressEvent(e); } +void IconSelectPopup::showEvent(QShowEvent *e) +{ + QWidget::showEvent(e); + d->findBar_->setFocus(); +} + #include "iconselect.moc" diff --git a/src/widgets/iconselect.h b/src/widgets/iconselect.h index b40d0cd936..fe5437950b 100644 --- a/src/widgets/iconselect.h +++ b/src/widgets/iconselect.h @@ -22,6 +22,8 @@ #include +#include + class Iconset; class PsiIcon; @@ -35,10 +37,12 @@ class IconSelectPopup : public QMenu { void setIconset(const Iconset &); const Iconset &iconset() const; void setEmojiSortingEnabled(bool enabled); + void setRecent(const QStringList &recent); + QStringList recent() const; // reimplemented - void mousePressEvent(QMouseEvent *e); - + void mousePressEvent(QMouseEvent *e) override; + void showEvent(QShowEvent *e) override; signals: void iconSelected(const PsiIcon *); void textSelected(QString); diff --git a/src/widgets/iconsetselect.h b/src/widgets/iconsetselect.h index 1acbdbdf5a..60d7f3e2fe 100644 --- a/src/widgets/iconsetselect.h +++ b/src/widgets/iconsetselect.h @@ -36,8 +36,11 @@ class IconsetSelect : public QListWidget { const Iconset *iconset() const; QListWidgetItem *lastItem() const; - +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) QStyleOptionViewItem viewOptions() const; +#else + void initViewItemOption(QStyleOptionViewItem *option) const; +#endif public slots: void moveItemUp(); diff --git a/src/widgets/iconwidget.cpp b/src/widgets/iconwidget.cpp index ca1ce4b40f..a2e5e56082 100644 --- a/src/widgets/iconwidget.cpp +++ b/src/widgets/iconwidget.cpp @@ -397,17 +397,17 @@ const Iconset *IconsetSelect::iconset() const QListWidgetItem *IconsetSelect::lastItem() const { return item(count() - 1); } +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) QStyleOptionViewItem IconsetSelect::viewOptions() const { QStyleOptionViewItem o; -#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) - initViewItemOption(&o); -#else - o = QListWidget::viewOptions(); -#endif + o = QListWidget::viewOptions(); o.showDecorationSelected = true; return o; } +#else +void IconsetSelect::initViewItemOption(QStyleOptionViewItem *option) const { option->showDecorationSelected = true; } +#endif //---------------------------------------------------------------------------- // IconsetDisplay diff --git a/src/widgets/psirichtext.cpp b/src/widgets/psirichtext.cpp index a2ed669ef6..81c8e5cae3 100644 --- a/src/widgets/psirichtext.cpp +++ b/src/widgets/psirichtext.cpp @@ -37,11 +37,8 @@ #include #include -#include - #ifndef WIDGET_PLUGIN #include "iconset.h" -#include "qite.h" #include "textutil.h" #else class Iconset; @@ -52,31 +49,114 @@ static const int IconFormatType = QTextFormat::UserObject; static const int MarkerFormatType = QTextFormat::UserObject + 1; static QStringList allowedImageDirs; +struct HtmlSize { + enum Unit { Em, Pt, Px }; + + float size; + Unit unit; +}; +Q_DECLARE_METATYPE(HtmlSize) + +namespace { +std::optional parseSize(QStringView view) +{ + HtmlSize hsize { 0.0f, HtmlSize::Px }; + bool ok = true; + if (view.endsWith(QLatin1String("em"))) { + view.chop(2); + hsize.size = view.toFloat(&ok); + hsize.unit = HtmlSize::Em; + } else if (view.endsWith(QLatin1String("pt"))) { + view.chop(2); + hsize.size = view.toInt(&ok); + hsize.unit = HtmlSize::Pt; + } else { + if (view.endsWith(QLatin1String("px"))) { + view.chop(2); + } + hsize.size = view.toInt(&ok); + } + if (ok) { + return hsize; + } + return {}; +} + +int htmlSizeToPixels(const HtmlSize &size, const QTextCharFormat &format) +{ + if (size.unit == HtmlSize::Pt) { + return pointToPixel(size.size); + } + if (size.unit == HtmlSize::Em) { + auto fs = format.fontPointSize() ? format.fontPointSize() : format.font().pointSize(); + return int(size.size * pointToPixel(fs) + 0.5); + } + return size.size; +} + +} + //---------------------------------------------------------------------------- // TextIconFormat //---------------------------------------------------------------------------- class TextIconFormat : public QTextCharFormat { public: - TextIconFormat(const QString &iconName, const QString &text, qreal size = -1); + TextIconFormat(const QString &iconName, const QString &text, std::optional width = {}, + std::optional height = {}, std::optional minWidth = {}, + std::optional minHeight = {}, std::optional maxWidth = {}, + std::optional maxHeight = {}, std::optional valign = {}, + std::optional fontSize = {}); enum Property { - IconName = QTextFormat::UserProperty + 1, - IconText = QTextFormat::UserProperty + 2, - IconSize = QTextFormat::UserProperty + 3 + IconName = QTextFormat::UserProperty + 1, + IconText = QTextFormat::UserProperty + 2, + IconWidth = QTextFormat::UserProperty + 3, + IconHeight = QTextFormat::UserProperty + 4, + IconMinWidth = QTextFormat::UserProperty + 5, + IconMinHeight = QTextFormat::UserProperty + 6, + IconMaxWidth = QTextFormat::UserProperty + 7, + IconMaxHeight = QTextFormat::UserProperty + 8, + IconFontSize = QTextFormat::UserProperty + 9 }; }; -TextIconFormat::TextIconFormat(const QString &iconName, const QString &text, qreal size) : QTextCharFormat() +TextIconFormat::TextIconFormat(const QString &iconName, const QString &text, std::optional width, + std::optional height, std::optional minWidth, + std::optional minHeight, std::optional maxWidth, + std::optional maxHeight, + std::optional valign, + std::optional fontSize) : QTextCharFormat() { Q_UNUSED(text); setObjectType(IconFormatType); QTextFormat::setProperty(IconName, iconName); QTextFormat::setProperty(IconText, text); - QTextFormat::setProperty(IconSize, size); - - setVerticalAlignment(size < -1 ? QTextCharFormat::AlignBottom : QTextCharFormat::AlignNormal); + if (width) { + QTextFormat::setProperty(IconWidth, QVariant::fromValue(*width)); + } + if (height) { + QTextFormat::setProperty(IconHeight, QVariant::fromValue(*height)); + } + if (minWidth) { + QTextFormat::setProperty(IconMinWidth, QVariant::fromValue(*minWidth)); + } + if (minHeight) { + QTextFormat::setProperty(IconMinHeight, QVariant::fromValue(*minHeight)); + } + if (maxWidth) { + QTextFormat::setProperty(IconMaxWidth, QVariant::fromValue(*maxWidth)); + } + if (maxHeight) { + QTextFormat::setProperty(IconMaxHeight, QVariant::fromValue(*maxHeight)); + } + if (valign) { + setVerticalAlignment(*valign); + } + if (fontSize) { + QTextFormat::setProperty(IconFontSize, QVariant::fromValue(*fontSize)); + } // TODO: handle animations } @@ -96,6 +176,59 @@ class TextIconHandler : public QObject, public QTextObjectInterface { virtual QSizeF intrinsicSize(QTextDocument *doc, int posInDocument, const QTextFormat &format); virtual void drawObject(QPainter *painter, const QRectF &rect, QTextDocument *doc, int posInDocument, const QTextFormat &format); + +private: + QFont adjustFontSize(const QTextCharFormat charFormat) + { + auto propHeight = charFormat.property(TextIconFormat::IconHeight); + auto propMinHeight = charFormat.property(TextIconFormat::IconMinHeight); + auto propMaxHeight = charFormat.property(TextIconFormat::IconMaxHeight); + auto propFontSize = charFormat.property(TextIconFormat::IconFontSize); + + std::optional height; + std::optional minHeight; + std::optional maxHeight; + + if (propMinHeight.isValid()) { + minHeight = htmlSizeToPixels(propMinHeight.value(), charFormat); + } + if (propMaxHeight.isValid()) { + maxHeight = htmlSizeToPixels(propMaxHeight.value(), charFormat); + } + if (propHeight.isValid()) { // we want to scale ignoring aspect ratio + int limitMin = minHeight ? *minHeight : 8; + int limitMax = maxHeight ? *maxHeight : 1080; + height = htmlSizeToPixels(propHeight.value(), charFormat); + height = qMax(qMin(limitMax, *height), limitMin); + } + + auto font = charFormat.font(); + int ps = font.pixelSize(); + if (ps == -1) { + ps = pointToPixel(font.pointSizeF()); + } + if (propFontSize.isValid()) { + auto fontSize = propFontSize.value(); + if (fontSize.unit == HtmlSize::Em) { + ps *= fontSize.size; + } else if (fontSize.unit == HtmlSize::Px) { + ps = fontSize.size; + } else if (fontSize.unit == HtmlSize::Pt) { + ps = pointToPixel(fontSize.size); + } + } + if (height) { + ps = *height; + } else if (minHeight && maxHeight) { + ps = (*minHeight + *maxHeight) / 2; + } else if (minHeight && ps < *minHeight) { + ps = *minHeight; + } else if (maxHeight && ps > *maxHeight) { + ps = *maxHeight; + } + font.setPixelSize(ps); + return font; + } }; TextIconHandler::TextIconHandler(QObject *parent) : QObject(parent) { } @@ -104,37 +237,118 @@ QSizeF TextIconHandler::intrinsicSize(QTextDocument *doc, int posInDocument, con { Q_UNUSED(doc); Q_UNUSED(posInDocument) + QSizeF ret; const QTextCharFormat charFormat = format.toCharFormat(); - /* - * >0 - size in points - * 0 - original size - * <0 - scale factor relative to font size after converting to absolute(positive) value - */ - auto htmlSize = charFormat.doubleProperty(TextIconFormat::IconSize); - auto iconName = charFormat.stringProperty(TextIconFormat::IconName); - - QSizeF ret; - auto icon = IconsetFactory::iconPtr(iconName); - if (!icon) { - // qWarning("invalid icon: %s", qPrintable(iconName)); - ret = QSizeF(); - } else if (htmlSize > 0) { - auto pxSize = pointToPixel(htmlSize); - ret = icon->size(QSize(pxSize, pxSize)); - } else if (htmlSize == 0) { - ret = icon->size(); - } else { - auto relSize = QFontInfo(charFormat.font()).pixelSize() * std::fabs(double(htmlSize)); - if (icon->isScalable()) { - ret = icon->size(QSize(0, relSize)); - } else if (icon->size().height() > relSize * HugeIconTextViewK) { // still too huge - ret = icon->size().scaled(QSize(icon->size().width(), relSize), Qt::KeepAspectRatio); - } else { - ret = icon->size(); + auto iconName = charFormat.stringProperty(TextIconFormat::IconName); + auto iconText = charFormat.stringProperty(TextIconFormat::IconText); + const PsiIcon *icon = nullptr; + bool doScaling = true; + if (!iconName.isEmpty()) { + icon = IconsetFactory::iconPtr(iconName); + if (!icon) { + // qWarning("invalid icon: %s", qPrintable(iconName)); + return {}; + } + ret = icon->size(); + doScaling = icon->isScalable(); + } else if (!iconText.isEmpty()) { + auto font = adjustFontSize(charFormat); + return QFontMetricsF(font).tightBoundingRect(iconText).size() * 1.16; // 1.16 - magic for Windows + } + if (ret.isEmpty()) { + // something went wrong with this icon + return ret; + } + + auto propWidth = charFormat.property(TextIconFormat::IconWidth); + auto propHeight = charFormat.property(TextIconFormat::IconHeight); + auto propMinWidth = charFormat.property(TextIconFormat::IconMinWidth); + auto propMinHeight = charFormat.property(TextIconFormat::IconMinHeight); + auto propMaxWidth = charFormat.property(TextIconFormat::IconMaxWidth); + auto propMaxHeight = charFormat.property(TextIconFormat::IconMaxHeight); + + std::optional width; + std::optional height; + std::optional minWidth; + std::optional minHeight; + std::optional maxWidth; + std::optional maxHeight; + QSize maxSize { 20000, 20000 }; // should be enough fow a few decades + + if (propMinWidth.isValid()) { + minWidth = htmlSizeToPixels(propMinWidth.value(), charFormat); + } + if (propMinHeight.isValid()) { + minHeight = htmlSizeToPixels(propMinHeight.value(), charFormat); + } + if (propMaxWidth.isValid()) { + maxSize.setWidth(htmlSizeToPixels(propMaxWidth.value(), charFormat)); + maxWidth = maxSize.width(); + } + if (propMaxHeight.isValid()) { + maxSize.setHeight(htmlSizeToPixels(propMaxHeight.value(), charFormat)); + maxHeight = maxSize.height(); + } + if (propWidth.isValid()) { + int limitMin = minWidth ? *minWidth : 8; + width = qMax(qMin(maxSize.width(), htmlSizeToPixels(propWidth.value(), charFormat)), limitMin); + } + if (propHeight.isValid()) { // we want to scale ignoring aspect ratio + int limitMin = minHeight ? *minHeight : 8; + height = qMax(qMin(maxSize.height(), htmlSizeToPixels(propHeight.value(), charFormat)), limitMin); + } + + if (width || height) { + QSize scaledTo; + if (width) { + if (height) { + scaledTo = { *width, *height }; + } else { + scaledTo = { *width, maxSize.height() }; + } + } else { // scaling by height by not by width + scaledTo = { maxSize.width(), *height }; } + ret.scale(scaledTo, Qt::KeepAspectRatio); + doScaling = false; + } else if (!doScaling) { + // check where scaling is required even for non-scalable + doScaling = (ret.width() > maxSize.width() || ret.height() > maxSize.height()) + || (minWidth && ret.width() < *minWidth) || (minHeight && ret.height() < *minHeight); } + if (doScaling) { + QSizeF desiredSize { 0, 0 }; + if (minWidth) { + if (maxWidth) { + desiredSize.setWidth((*minWidth + *maxWidth) / 2.0); + } else { + desiredSize.setWidth(qMax(qreal(*minWidth), ret.width())); + } + } else if (maxWidth) { + desiredSize.setWidth(qMin(qreal(*maxWidth), ret.width())); + } + if (minHeight) { + if (maxHeight) { + desiredSize.setHeight((*minHeight + *maxHeight) / 2.0); + } else { + desiredSize.setHeight(qMax(qreal(*minHeight), ret.height())); + } + } else if (maxHeight) { + desiredSize.setHeight(qMin(qreal(*maxHeight), ret.height())); + } + if (desiredSize.width() && !desiredSize.height()) { + desiredSize.setHeight(desiredSize.width() / ret.width() * ret.height()); + } + if (!desiredSize.width() && desiredSize.height()) { + desiredSize.setWidth(desiredSize.height() / ret.height() * ret.width()); + } + if (!desiredSize.isEmpty()) { + ret = desiredSize; + } + } + // qDebug() << ret; return ret; } @@ -146,18 +360,28 @@ void TextIconHandler::drawObject(QPainter *painter, const QRectF &rect, QTextDoc const QTextCharFormat charFormat = format.toCharFormat(); auto const iconName = charFormat.stringProperty(TextIconFormat::IconName); + auto const iconText = charFormat.stringProperty(TextIconFormat::IconText); if (rect.isNull()) { - qWarning("Null rect for drawing icon %s", qPrintable(iconName)); + qWarning("Null rect for drawing icon %s: %s", qPrintable(iconName), qPrintable(iconText)); return; } - auto pixmap = IconsetFactory::iconPixmap(iconName, rect.size().toSize()); - auto alignedSize = rect.size().toSize(); - if (alignedSize != pixmap.size()) { - pixmap = pixmap.scaled(alignedSize, Qt::KeepAspectRatio, Qt::SmoothTransformation); + // qDebug() << "render icon " << iconText << iconName << " in " << rect; + if (iconName.isEmpty()) { + auto font = adjustFontSize(charFormat); + font.setPixelSize(font.pixelSize()); + painter->setFont(font); + painter->drawText(rect, Qt::AlignCenter, iconText); + } else { + auto pixmap = IconsetFactory::iconPixmap(iconName, rect.size().toSize()); + auto alignedSize = rect.size().toSize(); + if (alignedSize != pixmap.size()) { + pixmap = pixmap.scaled(alignedSize, Qt::KeepAspectRatio, Qt::SmoothTransformation); + } + // qDebug() << "render icon " << iconName << " size " << pixmap.size() << " in " << rect; + painter->drawPixmap(rect, pixmap, pixmap.rect()); } - painter->drawPixmap(rect, pixmap, pixmap.rect()); } #endif // WIDGET_PLUGIN @@ -282,8 +506,9 @@ static QStringView preserveOriginalObjectReplacementCharacters(const QStringView static QString convertIconsToObjectReplacementCharacters(const QStringView &text, TextCharFormatQueue *queue, int insertedAfter, const PsiRichText::ParsersMap &parsers) { - QString result; - QStringView work(text); + QString result; + QStringView work(text); + static QStringList emojiFontFamilies = { "Apple Color Emoji", "Noto Color Emoji", "Segoe UI Emoji" }; int start = -1; forever @@ -291,43 +516,80 @@ static QString convertIconsToObjectReplacementCharacters(const QStringView &text start = work.indexOf(QLatin1Char('<'), start + 1); if (start == -1) break; - if (work.mid(start + 1, 4) == QLatin1String { "icon" }) { + if (work.mid(start + 1, 5) == QLatin1String { "icon " }) { // Format: - static QRegularExpression rxName("name=\"([^\"]+)\""); - static QRegularExpression rxText("text=\"([^\"]+)\""); - static QRegularExpression rxSize("size=\"([^\"]+)\""); + static QRegularExpression rxName("([a-z-]+)=\"([^\"]+)\""); result += preserveOriginalObjectReplacementCharacters(work.left(start), queue); int end = work.indexOf(QLatin1Char { '>' }, start); Q_ASSERT(end != -1); - QStringView fragment = work.mid(start, end - start); - auto matchName = rxName.match(fragment); - if (matchName.hasMatch()) { -#ifndef WIDGET_PLUGIN - QString iconName = TextUtil::unescape(matchName.capturedTexts().at(1)); - QString iconText; - auto matchText = rxText.match(fragment); - if (matchText.hasMatch()) { - iconText = TextUtil::unescape(matchText.capturedTexts().at(1)); + QStringView fragment = work.mid(start + 6, end - start); + + std::optional width; + std::optional height; + std::optional minWidth; + std::optional minHeight; + std::optional maxWidth; + std::optional maxHeight; + std::optional fontSize; + QString iconName; + QString iconText; + QString iconType; + + std::optional valign; + + QRegularExpressionMatchIterator i = rxName.globalMatch(fragment); + while (i.hasNext()) { + auto match = i.next(); + if (match.capturedView(1) == QLatin1String("name")) { + iconName = match.captured(2); + } else if (match.capturedView(1) == QLatin1String("text")) { + iconText = match.capturedView(2).contains(QLatin1Char('%')) + ? QUrl::fromPercentEncoding(match.capturedView(2).toUtf8()) + : match.capturedView(2).toString(); + } else if (match.capturedView(1) == QLatin1String("width")) { + width = parseSize(match.capturedView(2)); + } else if (match.capturedView(1) == QLatin1String("height")) { + height = parseSize(match.capturedView(2)); + } else if (match.capturedView(1) == QLatin1String("min-width")) { + minWidth = parseSize(match.capturedView(2)); + } else if (match.capturedView(1) == QLatin1String("min-height")) { + minHeight = parseSize(match.capturedView(2)); + } else if (match.capturedView(1) == QLatin1String("max-width")) { + maxWidth = parseSize(match.capturedView(2)); + } else if (match.capturedView(1) == QLatin1String("max-height")) { + maxHeight = parseSize(match.capturedView(2)); + } else if (match.capturedView(1) == QLatin1String("valign")) { + static QHash vaMap { + { { QLatin1String("normal"), QTextCharFormat::AlignNormal }, + { QLatin1String("superscript"), QTextCharFormat::AlignSuperScript }, + { QLatin1String("subscript"), QTextCharFormat::AlignSubScript }, + { QLatin1String("middle"), QTextCharFormat::AlignMiddle }, + { QLatin1String("bottom"), QTextCharFormat::AlignBottom }, + { QLatin1String("top"), QTextCharFormat::AlignTop }, + { QLatin1String("baseline"), QTextCharFormat::AlignBaseline } } + }; + auto it = vaMap.find(match.capturedView(2).toString()); + if (it != vaMap.end()) { + valign = *it; + } + } else if (match.capturedView(1) == QLatin1String("type")) { + iconType = match.captured(2); + } else if (match.capturedView(1) == QLatin1String("font-size")) { + fontSize = parseSize(match.capturedView(2)); } + } - double iconSize = -1; // not defined. will be resized to be aligned with text if necessary - auto matchSize = rxSize.match(fragment); - if (matchSize.hasMatch()) { - auto szText = matchSize.capturedTexts().at(1); - if (szText == QLatin1String("original")) - iconSize = 0; // use original size - else - iconSize = szText.toDouble(); // size in points + if (!iconName.isEmpty() || !iconText.isEmpty()) { + auto format = new TextIconFormat(iconName, iconText, std::move(width), std::move(height), + std::move(minWidth), std::move(minHeight), std::move(maxWidth), + std::move(maxHeight), std::move(valign), std::move(fontSize)); + if (iconType == QLatin1String("smiley") && iconName.isEmpty()) { + format->setFontFamilies(emojiFontFamilies); } -#else - QString iconName = matchName.capturedTexts()[1]; - QString iconText = matchText.capturedTexts()[1]; - QString iconSize = matchSize.capturedTexts()[1]; -#endif - queue->enqueue(new TextIconFormat(iconName, iconText, iconSize)); + queue->enqueue(format); result += QChar::ObjectReplacementCharacter; } diff --git a/src/widgets/psitabwidget.cpp b/src/widgets/psitabwidget.cpp index d4a0d98a0c..502550af73 100644 --- a/src/widgets/psitabwidget.cpp +++ b/src/widgets/psitabwidget.cpp @@ -45,6 +45,7 @@ PsiTabWidget::PsiTabWidget(QWidget *parent) : QWidget(parent) tabBar_->setMultiRow(multiRow); tabBar_->setUsesScrollButtons(!multiRow); tabBar_->setCurrentIndexAlwaysAtBottom(currentIndexAlwaysAtBottom); + tabBar_->setExpanding(false); layout_ = new QVBoxLayout(this); layout_->setContentsMargins(0, 0, 0, 0); layout_->setSpacing(0); @@ -265,7 +266,34 @@ void PsiTabWidget::setTabText(QWidget *widget, const QString &label) { int index = widgets_.indexOf(widget); if (index != -1) { - tabBar_->setTabText(index, label); + auto shortLabel = label; + auto labelSize = label.size(); + if (labelSize > 40) { +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) + shortLabel = label.left(37) + "..."; +#else + shortLabel = label.first(37) + "..."; +#endif + int maxLength = 80; + // If label is longer than 80 symbols make this label multiline + if (labelSize > maxLength) { + QStringList sentences; + for (int i = 0; i < labelSize;) { + if (i + maxLength < labelSize) { + auto lastSpace = QStringView { label }.mid(i, maxLength).lastIndexOf(QString(" ")) + 1; + sentences << label.mid(i, lastSpace); + i += lastSpace; + } else { + sentences << label.mid(i, labelSize - i); + break; + } + } + tabBar_->setTabToolTip(index, sentences.join("\n")); + } else { + tabBar_->setTabToolTip(index, label); + } + } + tabBar_->setTabText(index, shortLabel); } } diff --git a/src/widgets/psitextview.cpp b/src/widgets/psitextview.cpp index 9ebc70269c..c333d0a3d8 100644 --- a/src/widgets/psitextview.cpp +++ b/src/widgets/psitextview.cpp @@ -48,6 +48,7 @@ class PsiTextView::Private : public QObject { QString anchorOnMousePress; bool hadSelectionOnMousePress = false; + bool keepAtBottom = true; FileSharingDeviceOpener *mediaOpener = nullptr; ITEAudioController *voiceMsgCtrl = nullptr; @@ -172,16 +173,21 @@ span.emojis { QTimer::singleShot(0, this, &PsiTextView::scrollToBottom); setTextCursor(prevCur); }); - auto downloader = item->download(false, 0, 0); + auto downloader = item->download(); downloader->setSelfDelete(true); // read just for cache connect(downloader, &FileShareDownloader::readyRead, this, [downloader]() { downloader->read(downloader->bytesAvailable()); }); downloader->open(); - qlonglong div; - QString unit = TextUtil::sizeUnit(qlonglong(item->fileSize()), &div); - QString sizeStr = TextUtil::roundedNumber(qint64(item->fileSize()), div) + unit; + QString sizeStr; + if (item->fileSize()) { + qlonglong div; + QString unit = TextUtil::sizeUnit(qlonglong(*item->fileSize()), &div); + sizeStr = TextUtil::roundedNumber(qint64(*item->fileSize()), div) + unit; + } else { + sizeStr = QLatin1String("stream"); + } QUrl simpleUrl = item->simpleSource(); if (simpleUrl.isValid()) { @@ -222,14 +228,18 @@ QMenu *PsiTextView::createStandardContextMenu(const QPoint &position) QMenu *menu; QString anc = anchorAt(position); if (!anc.isEmpty()) { - menu = URLObject::getInstance()->createPopupMenu(anc); - - int posInBlock = textcursor.position() - textcursor.block().position(); - QString textblock = textcursor.block().text(); - int begin = textcursor.block().position() + textblock.lastIndexOf(QRegularExpression("\\s|^"), posInBlock) + 1; - int end = textcursor.block().position() + textblock.indexOf(QRegularExpression("\\s|$"), posInBlock); - textcursor.setPosition(begin); - textcursor.setPosition(end, QTextCursor::KeepAnchor); + // menu = URLObject::getInstance()->createPopupMenu(anc); + const QString href = textcursor.charFormat().anchorHref(); + auto clickPos = textcursor.position(); + // we rely on cow to quickly find boundaries + while (textcursor.charFormat().isAnchor() && textcursor.charFormat().anchorHref() == href) { + textcursor.movePosition(QTextCursor::PreviousCharacter); + } + textcursor.setPosition(clickPos + 1, QTextCursor::KeepAnchor); + while (textcursor.charFormat().isAnchor() && textcursor.charFormat().anchorHref() == href) { + textcursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor); + } + textcursor.movePosition(QTextCursor::PreviousCharacter, QTextCursor::KeepAnchor); setTextCursor(textcursor); menu = URLObject::getInstance()->createPopupMenu(anc); @@ -246,6 +256,8 @@ QMenu *PsiTextView::createStandardContextMenu(const QPoint &position) return menu; } +void PsiTextView::setKeepAtBottom(bool v) { d->keepAtBottom = v; } + bool PsiTextView::isSelectedBlock() { if (textCursor().hasSelection()) { @@ -428,6 +440,10 @@ QMimeData *PsiTextView::createMimeDataFromSelection() const */ void PsiTextView::resizeEvent(QResizeEvent *e) { + if (!d->keepAtBottom) { + QTextEdit::resizeEvent(e); + return; + } bool atEnd = verticalScrollBar()->value() == verticalScrollBar()->maximum(); bool atStart = verticalScrollBar()->value() == verticalScrollBar()->minimum(); double value = 0; diff --git a/src/widgets/psitextview.h b/src/widgets/psitextview.h index bd1d9e0e45..fe8a2663fd 100644 --- a/src/widgets/psitextview.h +++ b/src/widgets/psitextview.h @@ -35,6 +35,7 @@ class PsiTextView : public QTextEdit { // Reimplemented QMenu *createStandardContextMenu(const QPoint &position); + void setKeepAtBottom(bool v = true); // keep scrolls at bottom on resize bool atBottom(); virtual void appendText(const QString &text); diff --git a/src/widgets/psitiplabel.cpp b/src/widgets/psitiplabel.cpp index 23bbb5a23f..94e095d7da 100644 --- a/src/widgets/psitiplabel.cpp +++ b/src/widgets/psitiplabel.cpp @@ -212,6 +212,7 @@ void PsiTipLabel::enterEvent(QEvent *e) void PsiTipLabel::enterEvent(QEnterEvent *e) #endif { + Q_UNUSED(e); hideTip(); } diff --git a/src/widgets/tabbar.cpp b/src/widgets/tabbar.cpp index 3d05d7a21f..6b9acfadee 100644 --- a/src/widgets/tabbar.cpp +++ b/src/widgets/tabbar.cpp @@ -34,7 +34,7 @@ #include #include -#define PINNED_CHARS 12 +#define PINNED_CHARS 16 class CloseButton : public QAbstractButton { Q_OBJECT @@ -323,8 +323,8 @@ void TabBar::Private::layoutTabs() LayoutSf layout; if (rows == 1 || hackedTabs.size() == 1) { // Only one row in bar - rows = 1; - normalRows = 1; + rows = 1; + // normalRows = 1; layout << RowSf(); layout[0].number = 0; layout[0].sf = qMin(1.5, sf); diff --git a/src/widgets/taskbarnotifier.cpp b/src/widgets/taskbarnotifier.cpp new file mode 100644 index 0000000000..c8a7b5b1fd --- /dev/null +++ b/src/widgets/taskbarnotifier.cpp @@ -0,0 +1,411 @@ +/* + * taskbarnotifier.cpp - Taskbar notifications class + * Copyright (C) 2024 Vitaly Tonkacheyev + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + */ + +#include "taskbarnotifier.h" + +// #include //Maybe it will be needed for macOS to set application icon +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef USE_DBUS +#include "applicationinfo.h" + +#include +#include +#include +#include +#include +#endif + +#ifdef Q_OS_WIN +#include + +#include "applicationinfo.h" +#include "psiiconset.h" + +#include + +#include +#include +#include +#include + +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) +#include +#endif +#else +#include +#endif + +#ifdef USE_DBUS +// UnityLauncher dbus +static const QLatin1String ULAUNCHER_SERV("com.canonical.Unity"); +static const QLatin1String ULAUNCHER_PATH("/"); +static const QLatin1String ULAUNCHER_IFACE("com.canonical.Unity.LauncherEntry"); +// UnityLauncher functions +static const QLatin1String ULAUNCHER_CMD("Update"); +#endif + +class TaskBarNotifier::Private { +public: + Private() = default; + ~Private(); + + bool active() const; + void setIconCount(uint count = 0); + void restoreDefaultIcon(); + void setParent(QWidget *parent); +#ifdef USE_DBUS + void setDesktopPath(const QString &appName); +#endif +#ifdef Q_OS_WIN + void setFlashWindow(bool enabled); + void addJumpListItem(); +#endif +private: +#ifdef Q_OS_WIN + void setTaskBarIcon(const HICON &icon = {}); + HICON makeIconCaption(const QString &number) const; + HICON getHICONfromQImage(const QImage &image) const; + void doFlashTaskbarIcon(); + IShellLink *createShellLink(const QString &path, const QString &name, const QString &tooltip, const QString &args, + const QString &icon); +#else + QIcon setImageCountCaption(uint count = 0); +#ifdef USE_DBUS + bool checkDBusSeviceAvailable(); + void sendDBusSignal(bool isVisible, uint number = 0); +#endif +#endif + +private: + bool urgent_ = false; + bool active_ = false; + QWidget *parent_; +#ifdef Q_OS_WIN + HWND hwnd_; + HICON icon_; + bool flashWindow_ = false; +#else + QImage *image_; +#endif + int devicePixelRatio_; +}; + +TaskBarNotifier::Private::~Private() +{ +#ifdef Q_OS_WIN + if (icon_) + DestroyIcon(icon_); +#else + if (image_) + delete image_; +#endif +} + +bool TaskBarNotifier::Private::active() const { return active_; } + +void TaskBarNotifier::Private::setIconCount(uint count) +{ + urgent_ = true; +#ifdef Q_OS_WIN + setTaskBarIcon(makeIconCaption(QString::number(count))); + doFlashTaskbarIcon(); +#else +#ifdef USE_DBUS + if (checkDBusSeviceAvailable()) + sendDBusSignal(true, count); + else +#endif + parent_->setWindowIcon(setImageCountCaption(count)); // qApp->setWindowIcon(setImageCountCaption(count)); +#endif + active_ = true; +} + +void TaskBarNotifier::Private::restoreDefaultIcon() +{ + urgent_ = false; +#ifdef Q_OS_WIN + setTaskBarIcon(); + doFlashTaskbarIcon(); +#else +#ifdef USE_DBUS + if (checkDBusSeviceAvailable()) + sendDBusSignal(false, 0); + else +#endif + parent_->setWindowIcon( + QIcon(QPixmap::fromImage(*image_))); // qApp->setWindowIcon(QIcon(QPixmap::fromImage(*image_))); +#endif + active_ = false; +} + +void TaskBarNotifier::Private::setParent(QWidget *parent) +{ + parent_ = parent; + devicePixelRatio_ = parent->devicePixelRatio(); +#ifdef Q_OS_WIN + hwnd_ = reinterpret_cast(parent->winId()); +#else + image_ = new QImage(parent->windowIcon().pixmap({ 128, 128 }).toImage()); +#endif +} + +#ifndef Q_OS_WIN +QIcon TaskBarNotifier::Private::setImageCountCaption(uint count) +{ + auto imSize = image_->size() * devicePixelRatio_; + QImage img = *image_; + auto number = QString::number(count); + auto letters = number.length(); + auto text = (letters < 3) ? QStaticText(number) : QStaticText("∞"); + auto textDelta = (letters <= 2) ? 3 : 4; + + QPainter p(&img); + p.setRenderHints(QPainter::Antialiasing | QPainter::TextAntialiasing); + auto font = QFont(parent_->font().defaultFamily(), imSize.height() / textDelta, QFont::Bold); + auto fm = QFontMetrics(font); + auto fh = fm.height(); + auto fw = fm.horizontalAdvance(text.text()); + auto radius = fh / 2; + + Qt::BrushStyle style = Qt::SolidPattern; + QBrush brush(Qt::black, style); + p.setBrush(brush); + p.setPen(QPen(Qt::NoPen)); + QRect rect(imSize.width() - fw - radius, radius / 4, fw + radius, fh); + p.drawRoundedRect(rect, radius, radius); + + p.setFont(font); + p.setPen(QPen(Qt::white)); + auto offset = rect.width() / ((letters + 1) * 2); + p.drawStaticText(rect.x() + offset, rect.y(), text); + + p.end(); + return QIcon(QPixmap::fromImage(img)); +} +#endif + +#ifdef USE_DBUS +bool TaskBarNotifier::Private::checkDBusSeviceAvailable() +{ + const auto services = QDBusConnection::sessionBus().interface()->registeredServiceNames().value(); + for (const auto &service : services) { + if (service.contains(ULAUNCHER_SERV, Qt::CaseInsensitive)) + return true; + } + return false; +} + +void TaskBarNotifier::Private::sendDBusSignal(bool isVisible, uint number) +{ + auto appName = ApplicationInfo::desktopFileBaseName(); +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) + auto desktopPath_ = QLatin1String("application://%1").arg(appName); +#else + auto desktopPath_ = QLatin1String("application://%1.desktop").arg(appName); +#endif + QDBusMessage signal = QDBusMessage::createSignal(ULAUNCHER_PATH, ULAUNCHER_IFACE, ULAUNCHER_CMD); + signal << desktopPath_; + QVariantMap args; + args["count-visible"] = isVisible; + args["count"] = number; + args["urgent"] = urgent_; + signal << args; + QDBusConnection::sessionBus().send(signal); +} + +#elif defined(Q_OS_WIN) +void TaskBarNotifier::Private::setFlashWindow(bool enabled) { flashWindow_ = enabled; } + +void TaskBarNotifier::Private::setTaskBarIcon(const HICON &icon) +{ + if (icon_) + DestroyIcon(icon_); + + if (icon) + icon_ = icon; + + ITaskbarList3 *tbList; + if (SUCCEEDED(CoCreateInstance(CLSID_TaskbarList, nullptr, CLSCTX_INPROC_SERVER, IID_ITaskbarList3, + reinterpret_cast(&tbList)))) { + tbList->SetOverlayIcon(hwnd_, icon_, (icon_) ? L"Incoming events" : 0); + tbList->Release(); + } +} + +HICON TaskBarNotifier::Private::makeIconCaption(const QString &number) const +{ + auto imSize = QSize(GetSystemMetrics(SM_CXICON), GetSystemMetrics(SM_CYICON)) * devicePixelRatio_; + QImage img(imSize, QImage::Format_ARGB32_Premultiplied); + img.fill(Qt::transparent); + QString text = (number.length() < 3) ? number : "∞"; + auto letters = text.length(); + auto textDelta = (letters <= 2) ? 2 : 3; + + QPainter p(&img); + p.setRenderHints(QPainter::Antialiasing | QPainter::TextAntialiasing); + auto font = QFont(parent_->font().defaultFamily(), imSize.height() / textDelta, QFont::Bold); + auto fm = QFontMetrics(font); + auto fh = fm.height(); + auto radius = fh / 2; + + Qt::BrushStyle style = Qt::SolidPattern; + QBrush brush(Qt::black, style); + p.setBrush(brush); + p.setPen(QPen(Qt::NoPen)); + QRect rect(0, 0, imSize.width() - 2, imSize.height() - 2); + p.drawRoundedRect(rect, radius, radius); + + p.setFont(font); + p.setPen(QPen(Qt::white)); + p.drawText(rect, Qt::AlignCenter, text); + + p.end(); + return std::move(getHICONfromQImage(img)); +} + +HICON TaskBarNotifier::Private::getHICONfromQImage(const QImage &image) const +{ + if (image.isNull()) + return {}; +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) + return std::move(QtWin::toHICON(QPixmap::fromImage(image))); +#else + return std::move(image.toHICON()); +#endif + return {}; +} + +void TaskBarNotifier::Private::doFlashTaskbarIcon() +{ + FLASHWINFO fi; + fi.cbSize = sizeof(FLASHWINFO); + fi.hwnd = hwnd_; + if (urgent_) + fi.dwFlags = ((flashWindow_) ? FLASHW_ALL : FLASHW_TRAY) | FLASHW_TIMER; + else + fi.dwFlags = FLASHW_STOP; + fi.uCount = 0; + fi.dwTimeout = 0; + FlashWindowEx(&fi); +} + +void TaskBarNotifier::Private::addJumpListItem() +{ + // Create an object collection + IObjectCollection *pCollection = nullptr; + if (SUCCEEDED(CoCreateInstance(CLSID_EnumerableObjectCollection, nullptr, CLSCTX_INPROC_SERVER, + IID_PPV_ARGS(&pCollection)))) { + // Create shell link object + auto path = qApp->applicationFilePath(); + auto nameString = parent_->tr("Quit %1 application").arg(qApp->applicationName()); + auto cachedIconFile + = ApplicationInfo::homeDir(ApplicationInfo::CacheLocation) + QStringLiteral("/quit_icon.ico"); + auto pixmap = PsiIconset::instance() + ->system() + .icon("psi/quit") + ->pixmap(QSize(GetSystemMetrics(SM_CXICON), GetSystemMetrics(SM_CYICON)) * devicePixelRatio_); + pixmap.save(cachedIconFile, "ICO"); + IShellLink *quitShellLink + = createShellLink(path, nameString, nameString, QStringLiteral("--quit"), cachedIconFile); + if (quitShellLink != nullptr) { + pCollection->AddObject(quitShellLink); + quitShellLink->Release(); + // Create custom Jump list + ICustomDestinationList *destinationList = nullptr; + if (SUCCEEDED(CoCreateInstance(CLSID_DestinationList, NULL, CLSCTX_INPROC_SERVER, + IID_ICustomDestinationList, + reinterpret_cast(&(destinationList))))) { + IObjectArray *objectArray = nullptr; + UINT cMaxSlots; + // Init Jump list and add items to it + if (SUCCEEDED(destinationList->BeginList(&cMaxSlots, IID_IObjectArray, + reinterpret_cast(&(objectArray))))) { + destinationList->AddUserTasks(pCollection); + destinationList->CommitList(); + objectArray->Release(); + } + destinationList->Release(); + } + } + pCollection->Release(); + } +} + +IShellLink *TaskBarNotifier::Private::createShellLink(const QString &path, const QString &name, const QString &tooltip, + const QString &args, const QString &icon) +{ + IShellLink *pShellLink = nullptr; + if (SUCCEEDED(CoCreateInstance(CLSID_ShellLink, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pShellLink)))) { + auto wPath = reinterpret_cast(path.utf16()); + auto wName = reinterpret_cast(name.utf16()); + auto wDesc = reinterpret_cast(tooltip.utf16()); + auto wArgs = reinterpret_cast(args.utf16()); + auto wIcon = reinterpret_cast(icon.utf16()); + pShellLink->SetPath(wPath); + pShellLink->SetArguments(wArgs); + pShellLink->SetDescription(wDesc); + pShellLink->SetIconLocation(wIcon, 0); + // Change shell link object name + IPropertyStore *propertyStore = nullptr; + if (SUCCEEDED(pShellLink->QueryInterface(IID_IPropertyStore, (LPVOID *)&propertyStore))) { + PROPVARIANT pv; + if (SUCCEEDED(InitPropVariantFromString(wName, &pv))) { + if (SUCCEEDED(propertyStore->SetValue(PKEY_Title, pv))) + propertyStore->Commit(); + PropVariantClear(&pv); + } + propertyStore->Release(); + } + } + return pShellLink; +} +#endif + +TaskBarNotifier::TaskBarNotifier(QWidget *parent) +{ + d = std::make_unique(Private()); + d->setParent(parent); +#ifdef Q_OS_WIN + d->addJumpListItem(); +#endif +} + +TaskBarNotifier::~TaskBarNotifier() = default; + +void TaskBarNotifier::setIconCountCaption(int count) { d->setIconCount(count); } + +void TaskBarNotifier::removeIconCountCaption() { d->restoreDefaultIcon(); } + +bool TaskBarNotifier::isActive() { return d->active(); } + +#ifdef Q_OS_WIN +void TaskBarNotifier::enableFlashWindow(bool enabled) +{ + if (d) + d->setFlashWindow(enabled); +} +#endif diff --git a/src/widgets/taskbarnotifier.h b/src/widgets/taskbarnotifier.h new file mode 100644 index 0000000000..d85f846bb5 --- /dev/null +++ b/src/widgets/taskbarnotifier.h @@ -0,0 +1,44 @@ +/* + * taskbarnotifier.h - Taskbar notifications class + * Copyright (C) 2024 Vitaly Tonkacheyev + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + */ + +#ifndef TASKBARNOTIFIER_H +#define TASKBARNOTIFIER_H + +#include +#include + +class QWidget; + +class TaskBarNotifier { +public: + explicit TaskBarNotifier(QWidget *parent = nullptr); + ~TaskBarNotifier(); + void setIconCountCaption(int count); + void removeIconCountCaption(); + bool isActive(); +#ifdef Q_OS_WIN + void enableFlashWindow(bool enabled); +#endif + +private: + class Private; + std::unique_ptr d; +}; + +#endif // TASKBARNOTIFIER_H diff --git a/src/widgets/thumbnailtoolbar.cpp b/src/widgets/thumbnailtoolbar.cpp deleted file mode 100644 index ae83f14351..0000000000 --- a/src/widgets/thumbnailtoolbar.cpp +++ /dev/null @@ -1,64 +0,0 @@ -/* - * thumbnailtoolbar.cpp - Thumbnail Toolbar Widget - * Copyright (C) 2021 Vitaly Tonkacheyev - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library 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 - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program. If not, see . - * - */ - -#include "thumbnailtoolbar.h" -#include "iconset.h" - -#include -#include - -PsiThumbnailToolBar::PsiThumbnailToolBar(QObject *parent, QWindow *parentWindow) : - QWinThumbnailToolBar(parent), optionsBtn_(new QWinThumbnailToolButton(this)), - onlineStatusBtn_(new QWinThumbnailToolButton(this)), offlineStatusBtn_(new QWinThumbnailToolButton(this)), - eventsBtn_(new QWinThumbnailToolButton(this)), taskbarBtn_(new QWinTaskbarButton(this)) -{ - // ToolTips - optionsBtn_->setToolTip(tr("Options")); - onlineStatusBtn_->setToolTip(tr("Online")); - offlineStatusBtn_->setToolTip(tr("Offline")); - eventsBtn_->setToolTip(tr("Show Next Event")); - // Icons - optionsBtn_->setIcon(IconsetFactory::iconPtr("psi/options")->icon()); - onlineStatusBtn_->setIcon(IconsetFactory::iconPtr("status/online")->icon()); - offlineStatusBtn_->setIcon(IconsetFactory::iconPtr("status/offline")->icon()); - eventsBtn_->setIcon(IconsetFactory::iconPtr("psi/events")->icon()); - // Set dismiss - optionsBtn_->setDismissOnClick(true); - onlineStatusBtn_->setDismissOnClick(true); - offlineStatusBtn_->setDismissOnClick(true); - eventsBtn_->setDismissOnClick(true); - - updateToolBar(false); - - connect(optionsBtn_, &QWinThumbnailToolButton::clicked, this, &PsiThumbnailToolBar::openOptions); - connect(onlineStatusBtn_, &QWinThumbnailToolButton::clicked, this, &PsiThumbnailToolBar::setOnline); - connect(offlineStatusBtn_, &QWinThumbnailToolButton::clicked, this, &PsiThumbnailToolBar::setOffline); - connect(eventsBtn_, &QWinThumbnailToolButton::clicked, this, [this]() { - if (eventsBtn_->isEnabled()) - emit runActiveEvent(); - }); - addButton(eventsBtn_); - addButton(onlineStatusBtn_); - addButton(offlineStatusBtn_); - addButton(optionsBtn_); - setWindow(parentWindow); - taskbarBtn_->setWindow(parentWindow); -} - -void PsiThumbnailToolBar::updateToolBar(bool hasEvents) { eventsBtn_->setEnabled(hasEvents); } diff --git a/src/widgets/thumbnailtoolbar.h b/src/widgets/thumbnailtoolbar.h deleted file mode 100644 index d3ae87f87f..0000000000 --- a/src/widgets/thumbnailtoolbar.h +++ /dev/null @@ -1,47 +0,0 @@ -/* - * thumbnailtoolbar.h - Thumbnail Toolbar Widget - * Copyright (C) 2021 Vitaly Tonkacheyev - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library 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 - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program. If not, see . - * - */ -#ifndef THUMBNAIL_TOOLBAR_H -#define THUMBNAIL_TOOLBAR_H - -#include -#include -#include - -class QWindow; - -class PsiThumbnailToolBar : public QWinThumbnailToolBar { - Q_OBJECT -public: - explicit PsiThumbnailToolBar(QObject *parent = 0, QWindow *parentWindow = 0); - void updateToolBar(bool hasEvents); - -signals: - void openOptions(); - void setOnline(); - void setOffline(); - void runActiveEvent(); - -private: - QWinThumbnailToolButton *optionsBtn_; - QWinThumbnailToolButton *onlineStatusBtn_; - QWinThumbnailToolButton *offlineStatusBtn_; - QWinThumbnailToolButton *eventsBtn_; - QWinTaskbarButton *taskbarBtn_; -}; -#endif // THUMBNAIL_TOOLBAR_H diff --git a/src/widgets/typeaheadfind.cpp b/src/widgets/typeaheadfind.cpp index 9815e9dc51..20be174a47 100644 --- a/src/widgets/typeaheadfind.cpp +++ b/src/widgets/typeaheadfind.cpp @@ -190,7 +190,11 @@ void TypeAheadFindBar::init() addAction(d->act_next); d->cb_case = new QCheckBox(tr("&Case sensitive"), this); - connect(d->cb_case, SIGNAL(stateChanged(int)), SLOT(caseToggled(int))); +#if QT_VERSION < QT_VERSION_CHECK(6,7,0) + connect(d->cb_case, &QCheckBox::stateChanged, this , [this](int state) { d->caseSensitive = (state == Qt::Checked); }); +#else + connect(d->cb_case, &QCheckBox::checkStateChanged, this, [this](Qt::CheckState state) { d->caseSensitive = (state == Qt::Checked); }); +#endif addWidget(d->cb_case); addWidget(new StretchWidget(this)); @@ -299,7 +303,3 @@ void TypeAheadFindBar::findNext() { d->doFind(); } */ void TypeAheadFindBar::findPrevious() { d->doFind(true); } -/** - * \brief Private slot activated when case-sensitive box is toggled. - */ -void TypeAheadFindBar::caseToggled(int state) { d->caseSensitive = (state == Qt::Checked); } diff --git a/src/widgets/typeaheadfind.h b/src/widgets/typeaheadfind.h index d7a08525bc..7bcac5c054 100644 --- a/src/widgets/typeaheadfind.h +++ b/src/widgets/typeaheadfind.h @@ -53,7 +53,6 @@ private slots: void textChanged(const QString &); void findNext(); void findPrevious(); - void caseToggled(int); private: class Private; diff --git a/src/x11windowsystem.cpp b/src/x11windowsystem.cpp index ac18f61a61..539cd404c2 100644 --- a/src/x11windowsystem.cpp +++ b/src/x11windowsystem.cpp @@ -94,7 +94,7 @@ auto getRootWindow() bool X11WindowSystem::isValid() const { return ::isPlatformX11(); } -void X11WindowSystem::x11wmClass(WId wid, QString resName) +void X11WindowSystem::x11wmClass(QWidget *widget, QString resName) { #if defined(LIMIT_X11_USAGE) return; @@ -103,16 +103,18 @@ void X11WindowSystem::x11wmClass(WId wid, QString resName) if (!isValid()) // Avoid crashes if launched in Wayland return; + auto winId = widget->winId(); + Display *dsp = getDisplay(); // get the display // WId win = winId(); // get the window XClassHint classhint; // class hints // Get old class hint. It is important to save old class name - XGetClassHint(dsp, wid, &classhint); + XGetClassHint(dsp, winId, &classhint); XFree(classhint.res_name); const QByteArray latinResName = resName.toLatin1(); classhint.res_name = const_cast(latinResName.data()); // res_name - XSetClassHint(dsp, wid, &classhint); // set the class hints + XSetClassHint(dsp, winId, &classhint); // set the class hints XFree(classhint.res_class); } diff --git a/src/x11windowsystem.h b/src/x11windowsystem.h index 50e7d2131e..f87845b84d 100644 --- a/src/x11windowsystem.h +++ b/src/x11windowsystem.h @@ -42,7 +42,7 @@ class X11WindowSystem { bool windowHasAnyOfStates(Window win, const QSet &filteredStates); bool currentDesktop(long *desktop); bool desktopOfWindow(Window *window, long *desktop); - void x11wmClass(WId wid, QString resName); + void x11wmClass(QWidget *widget, QString resName); void bringToFront(QWidget *w); ulong getDesktopRootWindow(); }; diff --git a/src/xdata_widget.cpp b/src/xdata_widget.cpp index 99d0de39c8..6529639a4f 100644 --- a/src/xdata_widget.cpp +++ b/src/xdata_widget.cpp @@ -133,6 +133,8 @@ class XDataField { virtual XData::Field field() const { return _field; } + virtual QWidget *focusProxy() const { return nullptr; } + QString labelText(QString str = ": ") const { QString text = _field.label(); @@ -299,6 +301,8 @@ class XDataField_TextSingle : public XDataField { return f; } + QWidget *focusProxy() const { return edit; } + protected: QLineEdit *edit; }; @@ -388,6 +392,8 @@ class XDataField_ListSingle : public XDataField { return f; } + QWidget *focusProxy() const { return combo; } + private: QComboBox *combo; }; @@ -457,6 +463,8 @@ class XDataField_ListMulti : public XDataField { return f; } + QWidget *focusProxy() const { return list; } + private: QListWidget *list; }; @@ -506,6 +514,8 @@ class XDataField_TextMulti : public XDataField { return f; } + QWidget *focusProxy() const { return edit; } + private: QTextEdit *edit; }; @@ -614,7 +624,8 @@ void XDataWidget::setForm(const XMPP::XData &d, bool withInstructions) consistent_ = false; } if (!consistent_) { - consistencyError_ = Stanza::Error(Stanza::Error::Modify, Stanza::Error::NotAcceptable); + consistencyError_ + = Stanza::Error(Stanza::Error::ErrorType::Modify, Stanza::Error::ErrorCond::NotAcceptable); } // TODO check if captcha was sent too late (more than 2 minutes) } else { @@ -645,6 +656,7 @@ void XDataWidget::setFields(const XData::FieldList &f) // FIXME QGridLayout *grid = new QGridLayout(fields); grid->setSpacing(3); + bool focusSet = false; XData::FieldList::ConstIterator it = f.begin(); for (; it != f.end(); ++it) { @@ -682,6 +694,14 @@ void XDataWidget::setFields(const XData::FieldList &f) f = new XDataField_TextSingle(*it, grid, this); } fields_.append(f); + auto proxy = f->focusProxy(); + if (proxy) { + if (!focusSet) { + setFocusProxy(proxy); + focusSet = true; + } + lastFocusableWidget_ = proxy; + } } } } diff --git a/src/xdata_widget.h b/src/xdata_widget.h index 05ea72b8f1..32d84223e9 100644 --- a/src/xdata_widget.h +++ b/src/xdata_widget.h @@ -54,6 +54,8 @@ class XDataWidget : public QWidget { XMPP::XData::FieldList fields() const; XDataField *fieldByVar(const QString &) const; + inline QWidget *lastFocusabelWidget() const { return lastFocusableWidget_; } + protected slots: void linkActivated(const QString &); @@ -71,6 +73,7 @@ protected slots: XMPP::Jid owner_; bool consistent_; XMPP::Stanza::Error consistencyError_; + QWidget *lastFocusableWidget_ = nullptr; }; #endif // XDATAWIDGET_H diff --git a/tests/travis-ci/install-build-depends.sh b/tests/travis-ci/install-build-depends.sh index 3bb4da47bd..450295754e 100755 --- a/tests/travis-ci/install-build-depends.sh +++ b/tests/travis-ci/install-build-depends.sh @@ -21,7 +21,7 @@ then libqca-qt5-2-dev \ libqt5svg5-dev \ libqt5x11extras5-dev \ - libsignal-protocol-c-dev \ + libomemo-c-dev \ libsm-dev \ libssl-dev \ libtidy-dev \ @@ -50,7 +50,7 @@ then tidy-html5 \ libgpg-error \ libotr \ - libsignal-protocol-c \ + libomemo-c \ " brew install ${PACKAGES} elif [ "${TARGET}" = "windows64" ] @@ -67,7 +67,6 @@ then ${PREFIX}-hunspell \ ${PREFIX}-minizip \ ${PREFIX}-libotr \ - ${PREFIX}-libsignal-protocol-c \ ${PREFIX}-tidy-html5 \ ${PREFIX}-qtbase \ ${PREFIX}-qttools \ diff --git a/themes/chatview.qrc b/themes/chatview.qrc index aecc5c3652..ce483f2b44 100644 --- a/themes/chatview.qrc +++ b/themes/chatview.qrc @@ -1,8 +1,10 @@ - + chatview/adium/Template.html chatview/adium/adapter.js chatview/psi/psi.css + chatview/psi/bubble/index.html + chatview/psi/bubble/load.js chatview/psi/LunnaCat_Classic/index.html chatview/psi/LunnaCat_Classic/images/HR.png chatview/psi/LunnaCat_Classic/images/ChatLog.png @@ -21,5 +23,6 @@ chatview/psi/adapter.js chatview/moment-with-locales.js chatview/util.js + chatview/psi/bubble/Dark.css diff --git a/themes/chatview/adium/Template.html b/themes/chatview/adium/Template.html index 067b6b2965..dfb08ecb90 100644 --- a/themes/chatview/adium/Template.html +++ b/themes/chatview/adium/Template.html @@ -320,7 +320,7 @@ .actionMessageBody:before { content:"*"; } .actionMessageBody:after { content:"*"; } * { word-wrap:break-word; text-rendering: optimizelegibility; } - img.scaledToFitImage { height: auto; max-width: 100%%; } + img.scaledToFitImage { height: auto; max-width: 100%; } diff --git a/themes/chatview/adium/adapter.js b/themes/chatview/adium/adapter.js index 9c8b40744b..ca40152441 100644 --- a/themes/chatview/adium/adapter.js +++ b/themes/chatview/adium/adapter.js @@ -22,26 +22,40 @@ function psiThemeAdapter(chat) { var adapter = { - loadTheme : function() { + loadTheme : function(style) { //var chat = chat; var loader = window.srvLoader; + var baseDir = ""; + loader.toCache("variant", style); //chat.console("DEBUG: loading " ); loader.setCaseInsensitiveFS(true); loader.setPrepareSessionHtml(true); - loader.setHttpResourcePath("/Contents/Resources"); - //chat.console("DEBUG: loading " + loader.themeId); - var resources = ["FileTransferRequest.html", - "Footer.html", "Header.html", "Status.html", "Topic.html", "Content.html", - "Incoming/Content.html", "Incoming/NextContent.html", - "Incoming/Context.html", "Incoming/NextContext.html", - "Outgoing/Content.html", "Outgoing/NextContent.html", - "Outgoing/Context.html", "Outgoing/NextContext.html"]; - - var toCache = {}; - for (var i=0; i !(path.startsWith("__MACOSX") || path.startsWith(".") || path.endsWith("DS_Store"))); + const value = filesList.find((path)=>path.split("/")[0].toLowerCase().endsWith("adiummessagestyle")); + if (value) { + baseDir = value.split("/")[0] + "/"; + } + loader.setHttpResourcePath(baseDir + "Contents/Resources"); + + //chat.console("DEBUG: loading " + loader.themeId); + var resources = ["FileTransferRequest.html", + "Footer.html", "Header.html", "Status.html", "Topic.html", "Content.html", + "Incoming/Content.html", "Incoming/NextContent.html", + "Incoming/Context.html", "Incoming/NextContext.html", + "Outgoing/Content.html", "Outgoing/NextContent.html", + "Outgoing/Context.html", "Outgoing/NextContext.html"]; + + var toCache = {}; + for (var i=0; i { window.scrollBy(0, value); }); + if (QWebChannel) { + // define compatibility hack for webengine + Object.defineProperty(document.body, "scrollTop", { + set: function(x) { document.documentElement.scrollTop = x; }, + get: function() { return document.documentElement.scrollTop; } + }); + Object.defineProperty(document.body, "scrollHeight", { + set: function(x) { document.documentElement.scrollHeight = x; }, + get: function() { return document.documentElement.scrollHeight; } + }); + } + chat.util.rereadOptions(); session.signalInited(); } diff --git a/themes/chatview/psi/adapter.js b/themes/chatview/psi/adapter.js index 09d75d92c4..d3916b74b0 100644 --- a/themes/chatview/psi/adapter.js +++ b/themes/chatview/psi/adapter.js @@ -21,13 +21,30 @@ function psiThemeAdapter(chat) { chat.console("Psi adapter is ready"); return { - loadTheme : function() { + loadTheme : function(style) { + const finishSetup = (html, js, scripts) => { + var hasStyles = html.indexOf("%styles%") !== -1; + var hasStylesVariant = html.indexOf("%stylesVariant%") !== -1; + var styles = ''; + var stylesVariant = ``; + if (style && !hasStylesVariant) { + styles += stylesVariant; + } + html = html.replace("%scripts%", scripts + (hasStyles?"":"%styles%")); + html = html.replace("%styles%", styles); + if (hasStylesVariant) { + html = html.replace("%stylesVariant%", style? stylesVariant : ""); + } + srvLoader.setHtml(html); + eval(js); + srvLoader.finishThemeLoading(); + }; + if (chat.async) { srvLoader.getFileContents("index.html", function(html){ // FIXME we have a lot of copies of this html everywhere. should be rewritten somehow // probably it's a good idea if adapter will send to Psi a list of required scripts - var hasStyles = html.indexOf("%styles%") !== -1; - html = html.replace("%scripts%", "\n \ + var scripts = "\n \ \n \ \n \ \n \ @@ -37,24 +54,16 @@ function psiThemeAdapter(chat) { window.srvUtil = channel.objects.srvUtil;\n \ var shared = initPsiTheme().adapter.initSession();\n \ });\n \ -" + (hasStyles?"":"%styles%")); - html = html.replace("%styles%", ''); - srvLoader.setHtml(html); +"; srvLoader.getFileContents("load.js", function(js){ - eval(js); - srvLoader.finishThemeLoading(); + finishSetup(html, js, scripts) }) }); } else { var html = srvLoader.getFileContents("index.html"); - html = html.replace("%scripts%", "" + (html.indexOf("%styles%") === -1?"%styles%":"") ); - html = html.replace("%styles%", ''); - - srvLoader.setHtml(html); - eval(srvLoader.getFileContents("load.js")); - srvLoader.finishThemeLoading(); + var js = srvLoader.getFileContents("load.js"); + var scripts = ``; + finishSetup(html, js, scripts); } }, initSession : function() { @@ -88,6 +97,14 @@ function psiThemeAdapter(chat) { this.formatter = new chat.DateTimeFormatter(format || shared.dateFormat); }, + TemplateTemplateVar : function(template_name) { + this.template_name = template_name; + }, + + TemplateEscapeUriVar : function(name) { + this.name = name; // cdata member name + }, + Template : function(raw) { var splitted = raw.split(/(%[\w]+(?:\{[^\{]+\})?%)/), i; this.parts = []; @@ -95,9 +112,12 @@ function psiThemeAdapter(chat) { for (i = 0; i < splitted.length; i++) { var m = splitted[i].match(/%([\w]+)(?:\{([^\{]+)\})?%/); if (m) { - this.parts.push(m[1] == "time" - ? new shared.TemplateTimeVar(m[1], m[2]) - : new shared.TemplateVar(m[1], m[2])); + switch (m[1]) { + case "time": this.parts.push(new shared.TemplateTimeVar(m[1], m[2])); break; + case "template": this.parts.push(new shared.TemplateTemplateVar(m[2])); break; + case "escapeURI": this.parts.push(new shared.TemplateEscapeUriVar(m[2])); break; + default: this.parts.push(new shared.TemplateVar(m[1], m[2])); break; + } } else { this.parts.push(splitted[i]); } @@ -108,17 +128,21 @@ function psiThemeAdapter(chat) { if (typeof(scroll) == 'boolean') { shared.scroller.atBottom = scroll; } + var el; if (nextEl) { - chat.util.siblingHtml(nextEl, html); + el = chat.util.siblingHtml(nextEl, html); } else { - chat.util.appendHtml(shared.chatElement, html); + el = chat.util.appendHtml(shared.chatElement, html, shared.isMuc? shared.cdata.sender : ""); } shared.scroller.invalidate(); + return el; }, stopGroupping : function() { if (shared.prevGrouppingData) { - shared.prevGrouppingData.nextEl.parentNode.removeChild(shared.prevGrouppingData.nextEl) + if (shared.prevGrouppingData.nextEl) { + shared.prevGrouppingData.nextEl.parentNode.removeChild(shared.prevGrouppingData.nextEl); + } shared.prevGrouppingData = null; } }, @@ -135,20 +159,20 @@ function psiThemeAdapter(chat) { shared.groupping = config.groupping || shared.groupping; proxy = config.proxy; shared.varHandlers = config.varHandlers || {}; + shared.msgElPostProcess = config.postProcess; for (var tname in config.templates) { if (config.templates[tname]) { - t[tname] = new shared.Template(config.templates[tname]); + t[tname] = new shared.Template(config.templates[tname].trim()); } } t.message = t.message || "%message%"; t.sys = t.sys || "%message%"; t.sysMessage = t.sysMessage || t.sys; t.sysMessageUT = t.sysMessageUT || t.sysMessage; - t.statusMessageUT = t.statusMessageUT || (t.statusMessage || t.sysMessageUT); t.statusMessage = t.statusMessage || t.sysMessage; + t.statusMessageUT = t.statusMessageUT || (t.statusMessage || t.sysMessageUT); t.sentMessage = t.sentMessage || t.message; t.receivedMessage = t.receivedMessage || t.message; - t.spooledMessage = t.spooledMessage || t.message; t.receivedMessageGroupping = t.receivedMessageGroupping || t.messageGroupping; t.sentMessageGroupping = t.sentMessageGroupping || t.messageGroupping; t.lastMsgDate = t.lastMsgDate || t.sys; @@ -197,8 +221,9 @@ function psiThemeAdapter(chat) { shared.TemplateVar.prototype = { toString : function() { - if (shared.varHandlers[this.name]) { - return shared.varHandlers[this.name](); + const varHandler = shared.varHandlers[this.name]; + if (varHandler) { + return varHandler(); } var d = shared.cdata[this.name]; if (this.name == "sender") { //may not be html @@ -238,11 +263,19 @@ function psiThemeAdapter(chat) { this.formatter.format(new Date()); } + shared.TemplateTemplateVar.prototype.toString = function() { + var tt = shared.templates[this.template_name]; + return "" + tt; + } + + shared.TemplateEscapeUriVar.prototype.toString = function() { + return encodeURIComponent(shared.cdata[this.name]); + } + shared.Template.prototype.toString = function() { return this.parts.join(""); } - chat.adapter.receiveObject = function(data) { shared.cdata = data; if (!inited) { @@ -274,11 +307,7 @@ function psiThemeAdapter(chat) { } if (!template) { data.nextOfGroup = false; //can't group w/o template - if (data.spooled) { - template = shared.templates.spooledMessage; - } else { - template = data.local?shared.templates.sentMessage:shared.templates.receivedMessage; - } + template = data.local?shared.templates.sentMessage:shared.templates.receivedMessage; } break; case "join": @@ -306,8 +335,11 @@ function psiThemeAdapter(chat) { break; } if (template) { - shared.appendHtml(template.toString(), data.local?true:null, data.nextOfGroup? + var el = shared.appendHtml(template.toString(), data.local?true:null, data.nextOfGroup? shared.prevGrouppingData.nextEl:null); //force scroll on local messages + if (shared.msgElPostProcess) { + shared.msgElPostProcess(el); + } shared.stopGroupping();// safe clean up previous data if (shared.cdata.nextEl) { //convert to DOM shared.cdata.nextEl = document.getElementById(shared.cdata.nextEl); diff --git a/themes/chatview/psi/bubble/Dark.css b/themes/chatview/psi/bubble/Dark.css new file mode 100644 index 0000000000..d1ee41ec97 --- /dev/null +++ b/themes/chatview/psi/bubble/Dark.css @@ -0,0 +1,71 @@ +body { + background-color: #180808; + color: white; +} + +.sysmsg { + background-color: #233; + color: #bbb; +} + +.sysmsg .usertext { + color: white; +} + +.msg { + background-color: #282828; +} + +.msg::before { + border-color: #282828; +} + +.mymsg { + background-color: #346; +} + +.mymsg::before { + border-color: #346; +} + +.msgtext { + color: #eee; +} + +.grnext:hover { + background: linear-gradient(90deg, #FFFFFF00 0%, #FFFFFF0F 10%, #FFFFFF0F 90%, #FFFFFF00 100%) +} + +.time { + color: #aaa +} + +.reactions>span { + border-color: #777 +} + +blockquote { + background: #f0f0f080; + border-color: #444; + color: #ccc; +} + +blockquote>div { + background-color: #444; +} + +.reactions_selector { + background-color: #033; +} + +.context_menu { + background-color: #033; +} + +.context_menu>div+div { + border-color: #555; +} + +.context_menu>div:hover { + background-color: #441; +} \ No newline at end of file diff --git a/themes/chatview/psi/bubble/index.html b/themes/chatview/psi/bubble/index.html new file mode 100644 index 0000000000..a888250cf7 --- /dev/null +++ b/themes/chatview/psi/bubble/index.html @@ -0,0 +1,581 @@ + + + + + + +Psi Bubble +%scripts% +%styles% + + +%stylesVariant% + + + + + + diff --git a/themes/chatview/psi/bubble/load.js b/themes/chatview/psi/bubble/load.js new file mode 100644 index 0000000000..279cd82a4a --- /dev/null +++ b/themes/chatview/psi/bubble/load.js @@ -0,0 +1,9 @@ +srvLoader.setMetaData({ + name: "Bubble", + version: "1.0", + authors: ["Sergei Ilinykh "], + description: "Bubble style.", + url: "https://psi-im.org", + features: ["reactions", "message-retract"], + stylesList: ["Light", "Dark"] +}); diff --git a/themes/chatview/psi/classic/index.html b/themes/chatview/psi/classic/index.html index 3af1a468b1..4853aa790d 100644 --- a/themes/chatview/psi/classic/index.html +++ b/themes/chatview/psi/classic/index.html @@ -1,4 +1,4 @@ - + %scripts% diff --git a/themes/chatview/psi/new_classic/index.html b/themes/chatview/psi/new_classic/index.html index ea9644048f..17731a4236 100644 --- a/themes/chatview/psi/new_classic/index.html +++ b/themes/chatview/psi/new_classic/index.html @@ -1,9 +1,24 @@ - + %styles% %scripts% @@ -118,26 +165,27 @@ scroller : new chat.WindowScroller(true), groupping: true, templates : { - message: shared.isMuc? - "
%icon%[%time%] %sender% %alertedmessage%%next%
" - : "
%sender%
%message%
%next%", - messageGroupping: shared.isMuc?null : "
%time% %message%
%next%", - messageNC: "
%icon%[%time%] %sender% %message%
", - spooledMessage: "
%icon%[%time%] %sender% %message%
", - sys: "
%icon%%message%
", - sysMessage: "
%icon%[%time%] *** %message%
", - sysMessageUT: "
%icon%[%time%] *** %message%: %usertext%
", - lastMsgDate: "
%icon%*** %time{LL}%
", + message: "
" + ( + shared.isMuc? "%sender%" : "%sender%") + + "%time%
%alertedmessage%
%next%", + messageGroupping: "
%template{left_time}%%alertedmessage%
%next%", + messageNC: "
%template{left_time}%%icon% %sender% %message%
", + sys: "
%icon%%message%
", + sysMessage: "
%template{left_time}%
%icon% %message%
", + sysMessageUT: "
%template{left_time}%
%icon% %message%:
%usertext%
", + lastMsgDate: "
%icon% %time{LL}%
", subject: shared.isMuc? - "
%icon%[%time%] %message%
%usertext%
" - : "
%icon%*** %usertext%
", - trackbar: '
' + "
%template{left_time}%
%icon% %message%
%usertext%
" + : "
%icon% %usertext%
", + trackbar: '
', + + left_time: '
%time%
' }, dateFormat : "HH:mm", proxy : function() { //optional if (shared.cdata.mtype == "message") { - return shared.cdata.emote && shared.templates.messageNC || - (shared.cdata.spooled && shared.templates.message || null); + return shared.cdata.emote && shared.templates.messageNC || null; + //(shared.cdata.spooled && shared.templates.message || null); } if (shared.cdata.type == "settings") { applyPsiSettings(); @@ -156,7 +204,7 @@ nick = shared.cdata.mtype == "message" && shared.isMuc? ''+nick+'' : nick; - return shared.cdata["emote"]?"*"+nick:(shared.isMuc?"<"+nick+">":nick+":"); + return shared.cdata["emote"]?"*"+nick:nick; }, alertedmessage : function() { var msg = shared.cdata.alert?""+shared.cdata.message+"":shared.cdata.message; diff --git a/themes/chatview/psi/psi.css b/themes/chatview/psi/psi.css index cc39f2788f..22df786217 100644 --- a/themes/chatview/psi/psi.css +++ b/themes/chatview/psi/psi.css @@ -95,7 +95,7 @@ } img.psi-icon { - vertical-align:bottom; + vertical-align:middle; max-height: 1em; max-width: 1em; } diff --git a/themes/chatview/util.js b/themes/chatview/util.js index 1831da2f18..65d3390a90 100644 --- a/themes/chatview/util.js +++ b/themes/chatview/util.js @@ -29,6 +29,85 @@ function initPsiTheme() { this.stop = function() { if (this.id){clearInterval(this.id); this.id = null;}}; } + function DateTimeFormatter(formatStr) { + function convertToTr35(format) + { + var ret="" + var i = 0; + var m = {M: "mm", H: "HH", S: "ss", c: "EEEE', 'MMMM' 'd', 'yyyy' 'G", + A: "EEEE", I: "hh", p: "a", Y: "yyyy"}; // if you want me, report it. + + var txtAcc = ""; + while (i < format.length) { + var c; + if (format[i] === "'" || + (format[i] === "%" && i < (format.length - 1) && (c = m[format[i+1]]))) + { + if (txtAcc) { + ret += "'" + txtAcc + "'"; + txtAcc = ""; + } + if (format[i] === "'") { + ret += "''"; + } else { + ret += c; + i++; + } + } else { + txtAcc += format[i]; + } + i++; + } + if (txtAcc) { + ret += "'" + txtAcc + "'"; + txtAcc = ""; + } + return ret; + } + + function convertToMoment(format) { + var inTxt = false; + var i; + var m = {j:"h"}; // sadly "j" is not supported + var ret = ""; + for (i = 0; i < format.length; i++) { + if (format[i] == "'") { + ret += (inTxt? ']' : '['); + inTxt = !inTxt; + } else { + var c; + if (!inTxt && (c = m[format[i]])) { + ret += c; + } else { + ret += format[i]; + } + } + } + if (inTxt) { + ret += "]"; + } + + ret = ret.replace("EEEE", "dddd"); + ret = ret.replace("EEE", "ddd"); + + return ret; + } + + formatStr = formatStr || "j:mm"; + if (formatStr.indexOf('%') !== -1) { + formatStr = convertToTr35(formatStr); + } + + formatStr = convertToMoment(formatStr); + + this.format = function(val) { + if (val instanceof String) { + val = Date.parse(val); + } + return moment(val).format(formatStr); // FIXME we could speedup it by keeping fomatter instances + } + } + function AudioMessage(el) { var playing = false; @@ -114,649 +193,783 @@ function initPsiTheme() { return that; } - var chat = { - async : async, - console : server.console, - server : server, - session : session, - hooks: [], + function WindowScroller(animate) { + var o=this, state, timerId + var ignoreNextScroll = false; + o.animate = animate; + o.atBottom = true; //just a state of aspiration + + var animationStep = function() { + timerId = null; + var before = document.body.clientHeight - (window.innerHeight+window.pageYOffset); + var step = before; + if (o.animate) { + step = step>200?200:(step<8?step:Math.floor(step/1.7)); + } + ignoreNextScroll = true; + window.scrollTo(0, document.body.clientHeight - window.innerHeight - before + step); + if (before>0) { + timerId = setTimeout(animationStep, 70); //next step in 250ms even if we are already at bottom (control shot) + } + } - util: { - console : server.console, - showCriticalError : function(text) { - var e=document.body || document.documentElement.appendChild(document.createElement("body")); - var er = e.appendChild(document.createElement("div")) - er.style.cssText = "background-color:red;color:white;border:1px solid black;padding:1em;margin:1em;font-weight:bold"; - er.innerHTML = chat.util.escapeHtml(text).replace(/\n/, "
"); - }, + var startAnimation = function() { + if (timerId) return; + if (document.body.clientHeight > window.innerHeight) { //if we have what to scroll + timerId = setTimeout(animationStep, 0); + } + } - // just for debug - escapeHtml : function(html) { - html += ""; //hack - return html.split("&").join("&").split( "<").join("<").split(">").join(">"); - }, + var stopAnimation = function() { + if (timerId) { + clearTimeout(timerId); + timerId = null; + } + } - // just for debug - props : function(e, rec) { - var ret=''; - for (var i in e) { - var gotValue = true; - var val = null; - try { - val = e[i]; - } catch(err) { - val = err.toString(); - gotValue = false; - } - if (gotValue) { - if (val instanceof Object && rec && val.constructor != Date) { - ret+=i+" = "+val.constructor.name+"{"+chat.util.props(val, rec)+"}\n"; - } else { - if (val instanceof Function) { - ret+=i+" = Function: "+i+"\n"; - } else { - ret+=i+" = "+(val === null?"null\n":val.constructor.name+"(\""+val+"\")\n"); - } - } - } else { - ret+=i+" = [CAN'T GET VALUE: "+val+"]\n"; - } + // ensure we at bottom on window resize + if (typeof ResizeObserver === 'undefined') { + + // next code is copied from www.backalleycoder.com/2013/03/18/cross-browser-event-based-element-resize-detection/ on 7 Dec 2018 + (function(){ + var attachEvent = document.attachEvent; + var isIE = navigator.userAgent.match(/Trident/); + //console.log(isIE); + var requestFrame = (function(){ + var raf = window.requestAnimationFrame || window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame || + function(fn){ return window.setTimeout(fn, 20); }; + return function(fn){ return raf(fn); }; + })(); + + var cancelFrame = (function(){ + var cancel = window.cancelAnimationFrame || window.mozCancelAnimationFrame || window.webkitCancelAnimationFrame || + window.clearTimeout; + return function(id){ return cancel(id); }; + })(); + + function resizeListener(e){ + var win = e.target || e.srcElement; + if (win.__resizeRAF__) cancelFrame(win.__resizeRAF__); + win.__resizeRAF__ = requestFrame(function(){ + var trigger = win.__resizeTrigger__; + trigger.__resizeListeners__.forEach(function(fn){ + fn.call(trigger, e); + }); + }); + } + + function objectLoad(e){ + this.contentDocument.defaultView.__resizeTrigger__ = this.__resizeElement__; + this.contentDocument.defaultView.addEventListener('resize', resizeListener); + } + + window.addResizeListener = function(element, fn){ + if (!element.__resizeListeners__) { + element.__resizeListeners__ = []; + if (attachEvent) { + element.__resizeTrigger__ = element; + element.attachEvent('onresize', resizeListener); + } + else { + if (getComputedStyle(element).position == 'static') element.style.position = 'relative'; + var obj = element.__resizeTrigger__ = document.createElement('object'); + obj.setAttribute('style', 'display: block; position: absolute; top: 0; left: 0; height: 100%; width: 100%; overflow: hidden; pointer-events: none; z-index: -1;'); + obj.__resizeElement__ = element; + obj.onload = objectLoad; + obj.type = 'text/html'; + if (isIE) element.appendChild(obj); + obj.data = 'about:blank'; + if (!isIE) element.appendChild(obj); + } } - return ret; - }, - - startSessionTransaction: function(starter, finisher) { - var tId = "st" + (++nextServerTransaction); - serverTransctions[tId] = finisher; - starter(tId); - }, - - _remoteCallEval : function(func, args, cb) { - function ecb(val) { val = eval("[" + val + "][0]"); cb(val); } - - if (chat.async) { - args.push(ecb) - func.apply(this, args) - } else { - var val = func.apply(this, args); - ecb(val); + element.__resizeListeners__.push(fn); + }; + + window.removeResizeListener = function(element, fn){ + element.__resizeListeners__.splice(element.__resizeListeners__.indexOf(fn), 1); + if (!element.__resizeListeners__.length) { + if (attachEvent) element.detachEvent('onresize', resizeListener); + else { + element.__resizeTrigger__.contentDocument.defaultView.removeEventListener('resize', resizeListener); + element.__resizeTrigger__ = !element.removeChild(element.__resizeTrigger__); + } } - }, + } + })(); + // end of copied code + addResizeListener(document.body, function(){ + o.invalidate(); + }); + } else { + const ro = new ResizeObserver(function(entries) { + o.invalidate(); + }); - _remoteCall : function(func, args, cb) { - if (chat.async) { - args.push(cb) - func.apply(this, args) - } else { - var val = func.apply(this, args); - cb(val); - } - }, + // Observe the scrollingElement for when the window gets resized + ro.observe(document.scrollingElement); + // Observe the timeline to process new messages + // ro.observe(timeline); - psiOption : function(option, cb) { chat.util._remoteCallEval(server.psiOption, [option], cb); }, - colorOption : function(option, cb) { chat.util._remoteCallEval(server.colorOption, [option], cb); }, - getFont : function(cb) { chat.util._remoteCallEval(session.getFont, [], cb); }, - getPaletteColor : function(name, cb) { chat.util._remoteCall(session.getPaletteColor, [name], cb); }, - connectOptionChange: function(option, cb) { - if (typeof optionChangeHandlers[option] == 'undefined') { - optionChangeHandlers[option] = {value: undefined, handlers:[]}; - } - optionChangeHandlers[option].handlers.push(cb); - }, - rereadOptions: function() { - onOptionsChanged(Object.getOwnPropertyNames(optionChangeHandlers)); - }, + } - // replaces - // with - icon2img : function (obj) { - var img = document.createElement('img'); - img.src = "/psi/icon/" + obj.getAttribute("name"); - img.title = obj.getAttribute("text"); - img.className = "psi-" + (obj.getAttribute("type") || "icon"); - // ignore size attribute. it's up to css style how to size. - obj.parentNode.replaceChild(img, obj); - }, + //let's consider scroll may happen only by user action + window.addEventListener("scroll", function(){ + if (ignoreNextScroll) { + ignoreNextScroll = false; + return; + } + stopAnimation(); + o.atBottom = document.body.clientHeight == (window.innerHeight+window.pageYOffset); + }, false); + + //EXTERNAL API + // checks current state of scroll and wish and activates necessary actions + o.invalidate = function() { + if (o.atBottom) { + startAnimation(); + } + } - // replaces all occurrence of by function above - replaceIcons : function(el) { - var els = el.querySelectorAll("icon"); // frozen list - for (var i=0; i < els.length; i++) { - chat.util.icon2img(els[i]); - } - }, + o.force = function() { + o.atBottom = true; + o.invalidate(); + } + + o.cancel = stopAnimation; // stops any current in-progress autoscroll + } - replaceBob : function(el) { - var els = el.querySelectorAll("img"); // frozen list - for (var i=0; i < els.length; i++) { - if (els[i].src.indexOf('cid:') == 0) { - els[i].src = "/psibob/" + els[i].src.slice(4); + function LikeButton(reactionsSelector, chatElement, emojiIcon) { + var likeButton = document.createElement("div"); + likeButton.classList.add("like_button"); + likeButton.textContent = emojiIcon || "❤️"; + const pdata = {}; + likeButton.addEventListener("click", () => reactionsSelector.show(pdata.parent.id, likeButton, chatElement)); + + that = { + setupForMessageElement: function(el) { + el.addEventListener("mouseleave", function () { + if (pdata.timer) { // if we were going to show it + clearTimeout(pdata.timer); + pdata.timer = null; } - } - }, + likeButton.classList.remove('noopacity'); + pdata.parent = null; + }); + el.addEventListener("mouseenter", function () { + pdata.timer = setTimeout(function () { + if (likeButton.parentNode != el) { + el.appendChild(likeButton); + } + setTimeout(()=>{likeButton.classList.add('noopacity');},10); + pdata.timer = null; + pdata.parent = el; + }, 500); + }); + } + } - updateObject : function(object, update) { - for (var i in update) { - object[i] = update[i] - } - }, + return that; + } - findStyleSheet : function (sheet, selector) { - for (var i=0; i { + const em = rs.appendChild(document.createElement("em")); + em.textContent = emoji; + }); - createHtmlNode : function(html, context) { - var range = document.createRange(); - range.selectNode(context || document.body); - return range.createContextualFragment(html); - }, + var selector = this; + rs.addEventListener("click", function (event) { + if (event.target.nodeName == "EM") { + session.react(selector.currentMessage, event.target.textContent); + event.target.parentNode.style.display = "none"; + } + event.stopPropagation(); + }); + rs.addEventListener("mouseleave", function (event) { + rs.style.display = "none"; + event.stopPropagation(); + }); - replaceYoutube : function(linkEl) { - var baseLink = "https://www.youtube.com/embed/"; - var link; - - if (linkEl.hostname == "youtu.be") { - link = baseLink + linkEl.pathname.slice(1); - } else if (linkEl.pathname.indexOf("/embed/") != 0) { - var m = linkEl.href.match(/^.*[?&]v=([a-zA-Z0-9_-]+).*$/); - var code = m && m[1]; - if (code) { - link = baseLink + code; - } - } else { - link = linkEl.href; - } + this.show = function(messageId, nearEl, scrollEl) { + document.body.appendChild(rs); + this.currentMessage = messageId; + const nbr = nearEl.getBoundingClientRect(); + rs.style.left = "0px"; + rs.style.top = (nbr.top + scrollEl.scrollTop + document.documentElement.scrollTop) + "px"; + rs.style.display = "flex"; + const selectorRect = rs.getBoundingClientRect(); + const scrollRect = scrollEl.getBoundingClientRect(); + if (nbr.left + selectorRect.width > scrollRect.right) { + rs.style.left = (nbr.right - selectorRect.width) + "px"; + } else { + rs.style.left = nbr.left + "px"; + } + } + this.hide = function() { + rs.style.display = "none"; + } + } - if (link) { - var iframe = chat.util.createHtmlNode('
'); - linkEl.parentNode.insertBefore(iframe, linkEl.nextSibling); - } - }, + function ContextMenu() { + this.items = []; + this.providers = []; - replaceImage : function(linkEl) - { - var img = chat.util.createHtmlNode('
'); - linkEl.parentNode.insertBefore(img, linkEl.nextSibling); - }, + var menu = this; + document.addEventListener("contextmenu", function (event) { + var all_items = menu.items.slice(); + try { + for (let i = 0; i < menu.providers.length; i++) { + all_items = all_items.concat(menu.providers[i](event)); + } + } catch(e) { + chat.console(e+""); + } + if (!all_items.length) { + return true; + } - replaceAudio : function(linkEl) - { - var audio = chat.util.createHtmlNode('
'); - linkEl.parentNode.insertBefore(audio, linkEl.nextSibling); - }, + event.stopPropagation(); + event.preventDefault(); - replaceVideo : function(linkEl) - { - var audio = chat.util.createHtmlNode('
'); - linkEl.parentNode.insertBefore(audio, linkEl.nextSibling); - }, + menu.show(event.pageX, event.pageY, all_items).catch(()=>{}); + }); + } - replaceLinkAsync : function(linkEl) - { - chat.util.startSessionTransaction(function(tId) { - session.getUrlHeaders(tId, linkEl.href); - },function(result) { - //chat.console("result ready " + chat.util.props(result, true)); - var ct = result['content-type']; - if ((typeof(ct) == "string") && (ct != "application/octet-stream")) { - ct = ct.split("/")[0].trim(); - switch (ct) { - case "image": - chat.util.replaceImage(linkEl); - break; - case "audio": - chat.util.replaceAudio(linkEl); - break; - case "video": - chat.util.replaceVideo(linkEl); - break; - } - } else { // fallback when no content type - //chat.console("fallback") - var imageExts = ["png", "jpg", "jpeg", "gif", "webp"]; - var audioExts = ["mp3", "ogg", "aac", "flac", "wav", "m4a"]; - var videoExts = ["mp4", "webm", "mkv", "mov", "avi", "ogv"]; - var lpath = linkEl.pathname.toLowerCase().split('#')[0].split('?')[0]; - function checkExt(exts, replacer) { - for (var i = 0; i < exts.length; i++) { - if (lpath.slice(lpath.length - exts[i].length - 1) == ("." + exts[i])) { - replacer(linkEl); - break; - } + ContextMenu.prototype = { + addItem : function(text, action) { + this.items.push({text: text, action: action}); + }, + addItemProvider : function(itemProvider) { + this.providers.push(itemProvider); + }, + show : function(x, y, items) { + if (window.activeMenu) { + window.activeMenu.destroyMenu(); + } + const menu = document.body.appendChild(document.createElement("div")); + menu.classList.add("context_menu"); + return new Promise((resolve, reject) => { + for (let i = 0; i < items.length; i++) { + const item = menu.appendChild(document.createElement("div")); + item.textContent = items[i].text; + const action = items[i].action; + item.addEventListener("click", (event) => { + event.stopPropagation(); + menu.destroyMenu(); + try { + if (action instanceof Function) { + action(); } + } finally { + resolve(action); } - checkExt(imageExts, chat.util.replaceImage); - checkExt(audioExts, chat.util.replaceAudio); - checkExt(videoExts, chat.util.replaceVideo); - } - }); - }, - - handleLinks : function(el) - { - if (!previewsEnabled) - return; - var links = el.querySelectorAll("a"); - var youtube = ["youtu.be", "www.youtube.com", "youtube.com", "m.youtube.com"]; - for (var li = 0; li < links.length; li++) { - var linkEl = links[li]; - if (youtube.indexOf(linkEl.hostname) != -1) { - chat.util.replaceYoutube(linkEl); - } else if ((linkEl.protocol == "http:" || linkEl.protocol == "https:" || linkEl.protocol == "file:") && linkEl.hostname != "psi") { - chat.util.replaceLinkAsync(linkEl); - } + }, { "once": true }); } - }, - - handleShares : function(el) { - var shares = el.querySelectorAll("share"); - for (var li = 0; li < shares.length; li++) { - var share = shares[li]; - var info = ""; // TODO - var source = share.getAttribute("id"); - var type = share.getAttribute("type"); - if (type.startsWith("audio")) { - var hg = share.getAttribute("amplitudes"); - if (hg && hg.length) - hg.split(",").forEach(v => { info += `` }); - var playerFragment = chat.util.createHtmlNode(`
-
-
-
- ${info} -
-
- - -
`); - var player = playerFragment.firstChild; - if (share.nextSibling) - share.parentNode.insertBefore(playerFragment, share.nextSibling); - else - share.parentNode.appendChild(playerFragment); - new AudioMessage(player); - } - else if (type.startsWith("image")) { - let img = chat.util.createHtmlNode(`
`); - if (share.nextSibling) - share.parentNode.insertBefore(img, share.nextSibling); - else - share.parentNode.appendChild(img); - } + menu.destroyMenu = () => { + document.removeEventListener("click", menu.destroyMenu); + menu.parentNode.removeChild(menu); + window.activeMenu = undefined; + reject(); + }; + document.addEventListener("click", menu.destroyMenu, { "once": true }); + + if (x + menu.clientWidth > document.body.clientWidth) { + x = document.body.clientWidth - menu.clientWidth - 5; + if (x < 0) x = 0; } - }, - - prepareContents : function(html) { - htmlSource.innerHTML = html; - chat.util.replaceBob(htmlSource); - chat.util.handleLinks(htmlSource); - chat.util.replaceIcons(htmlSource); - chat.util.handleShares(htmlSource); - }, + menu.style.left = x + "px"; + + const docEl = document.documentElement; + const bottom = docEl.clientHeight + docEl.scrollTop + document.body.scrollTop; + if (y + menu.clientHeight > bottom) { + y = bottom - menu.clientHeight; + if (y < docEl.scrollTop) y = docEl.scrollTop; + } + menu.style.top = y + "px"; + + window.activeMenu = menu; + }); + } + } - appendHtml : function(dest, html) { - chat.util.prepareContents(html); - while (htmlSource.firstChild) dest.appendChild(htmlSource.firstChild); - }, + var util = { + console : server.console, + showCriticalError : function(text) { + var e=document.body || document.documentElement.appendChild(document.createElement("body")); + var er = e.appendChild(document.createElement("div")) + er.style.cssText = "background-color:red;color:white;border:1px solid black;padding:1em;margin:1em;font-weight:bold"; + er.innerHTML = chat.util.escapeHtml(text).replace(/\n/, "
"); + }, - siblingHtml : function(dest, html) { - chat.util.prepareContents(html); - while (htmlSource.firstChild) dest.parentNode.insertBefore(htmlSource.firstChild, dest); - }, + // just for debug + escapeHtml : function(html) { + html += ""; //hack + return html.split("&").join("&").split( "<").join("<").split(">").join(">"); + }, - ensureDeleted : function(id) { - if (id) { - var el = document.getElementById(id); - if (el) { - el.parentNode.removeChild(el); - } + // just for debug + props : function(e, rec) { + var ret=''; + for (var i in e) { + var gotValue = true; + var val = null; + try { + val = e[i]; + } catch(err) { + val = err.toString(); + gotValue = false; } - }, - - loadXML : function(path, callback) { - function cb(text){ - if (!text) { - throw new Error("File " + path + " is empty. can't parse xml"); - } - var xml; - try { - xml = new DOMParser().parseFromString(text, "text/xml"); - } catch (e) { - server.console("failed to parse xml from file " + path); - throw e; + if (gotValue) { + if (val instanceof Object && rec && val.constructor != Date) { + ret+=i+" = "+val.constructor.name+"{"+chat.util.props(val, rec)+"}\n"; + } else { + if (val instanceof Function) { + ret+=i+" = Function: "+i+"\n"; + } else { + ret+=i+" = "+(val === null?"null\n":val.constructor.name+"(\""+val+"\")\n"); + } } - callback(xml); - } - if (chat.async) { - //server.console("loading xml async: " + path); - loader.getFileContents(path, cb); } else { - //server.console("loading xml sync: " + path); - cb(loader.getFileContents(path)); + ret+=i+" = [CAN'T GET VALUE: "+val+"]\n"; } - }, - - dateFormat : function(val, format) { - return (new chat.DateTimeFormatter(format)).format(val); - }, + } + return ret; + }, - avatarForNick : function(nick) { - var u = usersMap[nick]; - return u && u.avatar; - }, + startSessionTransaction: function(starter, finisher) { + var tId = "st" + (++nextServerTransaction); + serverTransctions[tId] = finisher; + starter(tId); + }, - nickColor : function(nick) { - var u = usersMap[nick]; - return u && u.nickcolor; - }, + _remoteCallEval : function(func, args, cb) { + function ecb(val) { val = eval("[" + val + "][0]"); cb(val); } - replaceableMessage : function(isMuc, isLocal, nick, msgId, text) { - // if we have an id then this is a replacable message. - // next weird logic is as follows: - // - wrapping to some element may break elements flow - // - using well know elements may break styles - // - setting just starting mark is useless (we will never find correct end) - var uniqId; - if (isMuc) { - var u = usersMap[nick]; - if (!u) { - return text; - } + if (chat.async) { + args.push(ecb) + func.apply(this, args) + } else { + var val = func.apply(this, args); + ecb(val); + } + }, - uniqId = "pmr"+uniqReplId.toString(36); // pmr - psi message replace :-) - //chat.console("Sender:"+nick); - usersMap[nick].msgs[msgId] = uniqId; - } else { - var uId = isLocal?"l":"r"; - uniqId = "pmr"+uId+uniqReplId.toString(36); - if (!usersMap[uId]) { - usersMap[uId]={msgs:{}}; - } - usersMap[uId].msgs[msgId] = uniqId; - } + _remoteCall : function(func, args, cb) { + if (chat.async) { + args.push(cb) + func.apply(this, args) + } else { + var val = func.apply(this, args); + cb(val); + } + }, - uniqReplId++; - // TODO better remember elements themselves instead of some id. - return "" + text + ""; - }, + psiOption : function(option, cb) { chat.util._remoteCallEval(server.psiOption, [option], cb); }, + colorOption : function(option, cb) { chat.util._remoteCallEval(server.colorOption, [option], cb); }, + getFont : function(cb) { chat.util._remoteCallEval(session.getFont, [], cb); }, + getPaletteColor : function(name, cb) { chat.util._remoteCall(session.getPaletteColor, [name], cb); }, + connectOptionChange: function(option, cb) { + if (typeof optionChangeHandlers[option] == 'undefined') { + optionChangeHandlers[option] = {value: undefined, handlers:[]}; + } + optionChangeHandlers[option].handlers.push(cb); + }, + rereadOptions: function() { + onOptionsChanged(Object.getOwnPropertyNames(optionChangeHandlers)); + }, - replaceMessage : function(parentEl, isMuc, isLocal, nick, msgId, newId, text) { - var u - if (isMuc) { - u = usersMap[nick]; - } else { - u = usersMap[isLocal?"l":"r"]; - } - //chat.console(isMuc + " " + isLocal + " " + nick + " " + msgId + " " + chat.util.props(u, true)); + // replaces + // with + icon2img : function (obj) { + var img = document.createElement('img'); + img.src = "/psi/icon/" + obj.getAttribute("name"); + img.title = obj.getAttribute("text"); + img.className = "psi-" + (obj.getAttribute("type") || "icon"); + // ignore size attribute. it's up to css style how to size. + obj.parentNode.replaceChild(img, obj); + }, - var uniqId = u && u.msgs[msgId]; - if (!uniqId) - return false; // replacing something we didn't use replaceableMessage for? hm. + // replaces all occurrence of by function above + replaceIcons : function(el) { + var els = el.querySelectorAll("icon"); // frozen list + for (var i=0; i < els.length; i++) { + chat.util.icon2img(els[i]); + } + }, - var se =parentEl.querySelector("psims[mid='"+uniqId+"']"); - var ee =parentEl.querySelector("psime[mid='"+uniqId+"']"); -// chat.console("Replace: start: " + (se? "found, ":"not found, ") + -// "end: " + (ee? "found, ":"not found, ") + -// "parent match: " + ((se && ee && se.parentNode === ee.parentNode)?"yes":"no")); - if (se && ee && se.parentNode === ee.parentNode) { - delete u.msgs[msgId]; // no longer used. will be replaced with newId. - while (se.nextSibling !== ee) { - se.parentNode.removeChild(se.nextSibling); + replaceBob : function(el, sender) { + var els = el.querySelectorAll("img"); // frozen list + for (var i=0; i < els.length; i++) { + if (els[i].src.indexOf('cid:') == 0) { + els[i].src = "psibob/" + els[i].src.slice(4); + if (sender) { + els[i].src += "?sender=" + encodeURIComponent(sender); } - var node = chat.util.createHtmlNode(chat.util.replaceableMessage(isMuc, isLocal, nick, newId, text + '')); - //chat.console(chat.util.props(node)); - chat.util.handleLinks(node); - chat.util.replaceIcons(node); - ee.parentNode.insertBefore(node, ee); - se.parentNode.removeChild(se); - ee.parentNode.removeChild(ee); - return true; } - return false; - }, + } + }, - CSSString2CSSStyleSheet : function(css) { - const style = document.createElement ( 'style' ); - style.innerText = css; - document.head.appendChild ( style ); - const {sheet} = style; - document.head.removeChild ( style ); - return sheet; + updateObject : function(object, update) { + for (var i in update) { + object[i] = update[i] } }, - WindowScroller : function(animate) { - var o=this, state, timerId - var ignoreNextScroll = false; - o.animate = animate; - o.atBottom = true; //just a state of aspiration + findStyleSheet : function (sheet, selector) { + for (var i=0; i200?200:(step<8?step:Math.floor(step/1.7)); - } - ignoreNextScroll = true; - window.scrollTo(0, document.body.clientHeight - window.innerHeight - before + step); - if (before>0) { - timerId = setTimeout(animationStep, 70); //next step in 250ms even if we are already at bottom (control shot) - } + createHtmlNode : function(html, context, stripIndents=true) { + var range = document.createRange(); + range.selectNode(context || document.body); + if (stripIndents) { + var re = new RegExp("^ +", "gm"); + html = html.replace(re, ""); } + return range.createContextualFragment(html); + }, - var startAnimation = function() { - if (timerId) return; - if (document.body.clientHeight > window.innerHeight) { //if we have what to scroll - timerId = setTimeout(animationStep, 0); + videoPreview: function(source) { + const html = `
+ +
`; + return chat.util.createHtmlNode(html); + }, + + replaceYoutube : function(linkEl) { + var baseLink = "https://www.youtube.com/embed/"; + var link; + + if (linkEl.hostname == "youtu.be") { + link = baseLink + linkEl.pathname.slice(1); + } else if (linkEl.pathname.indexOf("/embed/") != 0) { + var m = linkEl.href.match(/^.*[?&]v=([a-zA-Z0-9_-]+).*$/); + var code = m && m[1]; + if (code) { + link = baseLink + code; } + } else { + link = linkEl.href; } - var stopAnimation = function() { - if (timerId) { - clearTimeout(timerId); - timerId = null; - } + if (link) { + var iframe = chat.util.createHtmlNode('
'); + linkEl.parentNode.insertBefore(iframe, linkEl.nextSibling); } + }, - // ensure we at bottom on window resize - if (typeof ResizeObserver === 'undefined') { - - // next code is copied from www.backalleycoder.com/2013/03/18/cross-browser-event-based-element-resize-detection/ on 7 Dec 2018 - (function(){ - var attachEvent = document.attachEvent; - var isIE = navigator.userAgent.match(/Trident/); - //console.log(isIE); - var requestFrame = (function(){ - var raf = window.requestAnimationFrame || window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame || - function(fn){ return window.setTimeout(fn, 20); }; - return function(fn){ return raf(fn); }; - })(); - - var cancelFrame = (function(){ - var cancel = window.cancelAnimationFrame || window.mozCancelAnimationFrame || window.webkitCancelAnimationFrame || - window.clearTimeout; - return function(id){ return cancel(id); }; - })(); - - function resizeListener(e){ - var win = e.target || e.srcElement; - if (win.__resizeRAF__) cancelFrame(win.__resizeRAF__); - win.__resizeRAF__ = requestFrame(function(){ - var trigger = win.__resizeTrigger__; - trigger.__resizeListeners__.forEach(function(fn){ - fn.call(trigger, e); - }); - }); - } + replaceImage : function(linkEl) + { + var img = chat.util.createHtmlNode('
'); + linkEl.parentNode.insertBefore(img, linkEl.nextSibling); + }, - function objectLoad(e){ - this.contentDocument.defaultView.__resizeTrigger__ = this.__resizeElement__; - this.contentDocument.defaultView.addEventListener('resize', resizeListener); - } + replaceAudio : function(linkEl) + { + var audio = chat.util.createHtmlNode('
'); + linkEl.parentNode.insertBefore(audio, linkEl.nextSibling); + }, + + replaceVideo : function(linkEl) + { + const video = chat.util.videoPreview(linkEl.href); + linkEl.parentNode.insertBefore(video, linkEl.nextSibling); + }, - window.addResizeListener = function(element, fn){ - if (!element.__resizeListeners__) { - element.__resizeListeners__ = []; - if (attachEvent) { - element.__resizeTrigger__ = element; - element.attachEvent('onresize', resizeListener); - } - else { - if (getComputedStyle(element).position == 'static') element.style.position = 'relative'; - var obj = element.__resizeTrigger__ = document.createElement('object'); - obj.setAttribute('style', 'display: block; position: absolute; top: 0; left: 0; height: 100%; width: 100%; overflow: hidden; pointer-events: none; z-index: -1;'); - obj.__resizeElement__ = element; - obj.onload = objectLoad; - obj.type = 'text/html'; - if (isIE) element.appendChild(obj); - obj.data = 'about:blank'; - if (!isIE) element.appendChild(obj); - } + replaceLinkAsync : function(linkEl) + { + chat.util.startSessionTransaction(function(tId) { + session.getUrlHeaders(tId, linkEl.href); + },function(result) { + //chat.console("result ready " + chat.util.props(result, true)); + var ct = result['content-type']; + if ((typeof(ct) == "string") && (ct != "application/octet-stream")) { + ct = ct.split("/")[0].trim(); + switch (ct) { + case "image": + chat.util.replaceImage(linkEl); + break; + case "audio": + chat.util.replaceAudio(linkEl); + break; + case "video": + chat.util.replaceVideo(linkEl); + break; } - element.__resizeListeners__.push(fn); - }; - - window.removeResizeListener = function(element, fn){ - element.__resizeListeners__.splice(element.__resizeListeners__.indexOf(fn), 1); - if (!element.__resizeListeners__.length) { - if (attachEvent) element.detachEvent('onresize', resizeListener); - else { - element.__resizeTrigger__.contentDocument.defaultView.removeEventListener('resize', resizeListener); - element.__resizeTrigger__ = !element.removeChild(element.__resizeTrigger__); - } + } else { // fallback when no content type + //chat.console("fallback") + var imageExts = ["png", "jpg", "jpeg", "gif", "webp"]; + var audioExts = ["mp3", "ogg", "aac", "flac", "wav", "m4a"]; + var videoExts = ["mp4", "webm", "mkv", "mov", "avi", "ogv"]; + var lpath = linkEl.pathname.toLowerCase().split('#')[0].split('?')[0]; + function checkExt(exts, replacer) { + for (var i = 0; i < exts.length; i++) { + if (lpath.slice(lpath.length - exts[i].length - 1) == ("." + exts[i])) { + replacer(linkEl); + break; + } + } } - } - })(); - // end of copied code - addResizeListener(document.body, function(){ - o.invalidate(); - }); - } else { - const ro = new ResizeObserver(function(entries) { - o.invalidate(); - }); - - // Observe the scrollingElement for when the window gets resized - ro.observe(document.scrollingElement); - // Observe the timeline to process new messages - // ro.observe(timeline); + checkExt(imageExts, chat.util.replaceImage); + checkExt(audioExts, chat.util.replaceAudio); + checkExt(videoExts, chat.util.replaceVideo); + } + }); + }, + handleLinks : function(el) + { + if (!previewsEnabled) + return; + var links = el.querySelectorAll("a"); + var youtube = ["youtu.be", "www.youtube.com", "youtube.com", "m.youtube.com"]; + for (var li = 0; li < links.length; li++) { + var linkEl = links[li]; + if (youtube.indexOf(linkEl.hostname) != -1) { + chat.util.replaceYoutube(linkEl); + } else if ((linkEl.protocol == "http:" || linkEl.protocol == "https:" || linkEl.protocol == "file:") && linkEl.hostname != "psi") { + chat.util.replaceLinkAsync(linkEl); + } } + }, - //let's consider scroll may happen only by user action - window.addEventListener("scroll", function(){ - if (ignoreNextScroll) { - ignoreNextScroll = false; - return; + handleShares : function(el) { + var shares = el.querySelectorAll("share"); + for (var li = 0; li < shares.length; li++) { + var share = shares[li]; + var info = ""; // TODO + var source = share.getAttribute("id"); + var type = share.getAttribute("type"); + if (type.startsWith("audio")) { + var hg = share.getAttribute("amplitudes"); + if (hg && hg.length) + hg.split(",").forEach(v => { info += `` }); + var playerFragment = chat.util.createHtmlNode(`
+
+
+
+${info} +
+
+ + +
`); + var player = playerFragment.firstChild; + if (share.nextSibling) + share.parentNode.insertBefore(playerFragment, share.nextSibling); + else + share.parentNode.appendChild(playerFragment); + new AudioMessage(player); } - stopAnimation(); - o.atBottom = document.body.clientHeight == (window.innerHeight+window.pageYOffset); - }, false); - - //EXTERNAL API - // checks current state of scroll and wish and activates necessary actions - o.invalidate = function() { - if (o.atBottom) { - startAnimation(); + else if (type.startsWith("image")) { + let img = chat.util.createHtmlNode(`
`); + if (share.nextSibling) + share.parentNode.insertBefore(img, share.nextSibling); + else + share.parentNode.appendChild(img); + } else if (type.startsWith("video")) { + player = chat.util.videoPreview(`/psi/account/${session.account}/sharedfile/${source}`); + if (share.nextSibling) + share.parentNode.insertBefore(player, share.nextSibling); + else + share.parentNode.appendChild(player); } } + }, - o.force = function() { - o.atBottom = true; - o.invalidate(); - } + prepareContents : function(html, sender) { + htmlSource.innerHTML = html; + chat.util.replaceBob(htmlSource, sender); + chat.util.handleLinks(htmlSource); + chat.util.replaceIcons(htmlSource); + chat.util.handleShares(htmlSource); + }, - o.cancel = stopAnimation; // stops any current in-progress autoscroll + appendHtml : function(dest, html, sender) { + chat.util.prepareContents(html, sender); + var firstAdded = htmlSource.firstChild; + while (htmlSource.firstChild) dest.appendChild(htmlSource.firstChild); + return firstAdded; }, - DateTimeFormatter : function(formatStr) { - function convertToTr35(format) - { - var ret="" - var i = 0; - var m = {M: "mm", H: "HH", S: "ss", c: "EEEE', 'MMMM' 'd', 'yyyy' 'G", - A: "EEEE", I: "hh", p: "a", Y: "yyyy"}; // if you want me, report it. + siblingHtml : function(dest, html, sourceUser) { + chat.util.prepareContents(html, sourceUser); + var firstAdded = htmlSource.firstChild; + while (htmlSource.firstChild) dest.parentNode.insertBefore(htmlSource.firstChild, dest); + return firstAdded; + }, - var txtAcc = ""; - while (i < format.length) { - var c; - if (format[i] === "'" || - (format[i] === "%" && i < (format.length - 1) && (c = m[format[i+1]]))) - { - if (txtAcc) { - ret += "'" + txtAcc + "'"; - txtAcc = ""; - } - if (format[i] === "'") { - ret += "''"; - } else { - ret += c; - i++; - } - } else { - txtAcc += format[i]; - } - i++; + ensureDeleted : function(id) { + if (id) { + var el = document.getElementById(id); + if (el) { + el.parentNode.removeChild(el); } - if (txtAcc) { - ret += "'" + txtAcc + "'"; - txtAcc = ""; - } - return ret; - } - - function convertToMoment(format) { - var inTxt = false; - var i; - var m = {j:"h"}; // sadly "j" is not supported - var ret = ""; - for (i = 0; i < format.length; i++) { - if (format[i] == "'") { - ret += (inTxt? ']' : '['); - inTxt = !inTxt; - } else { - var c; - if (!inTxt && (c = m[format[i]])) { - ret += c; - } else { - ret += format[i]; - } - } + } + }, + + listAllFiles : function(callback) { + if (chat.async) { + loader.listFiles(callback); + } else { + callback(loader.listFiles()); + } + }, + + loadXML : function(path, callback) { + function cb(text){ + if (!text) { + throw new Error("File " + path + " is empty. can't parse xml"); } - if (inTxt) { - ret += "]"; + var xml; + try { + xml = new DOMParser().parseFromString(text, "text/xml"); + } catch (e) { + server.console("failed to parse xml from file " + path); + throw e; } + callback(xml); + } + if (chat.async) { + //server.console("loading xml async: " + path); + loader.getFileContents(path, cb); + } else { + //server.console("loading xml sync: " + path); + cb(loader.getFileContents(path)); + } + }, - ret = ret.replace("EEEE", "dddd"); - ret = ret.replace("EEE", "ddd"); + dateFormat : function(val, format) { + return (new chat.DateTimeFormatter(format)).format(val); + }, - return ret; + avatarForNick : function(nick) { + var u = usersMap[nick]; + return u && u.avatar; + }, + + nickColor : function(nick) { + var u = usersMap[nick]; + return u && u.nickcolor || "#888"; + }, + + replaceableMessage : function(isMuc, isLocal, nick, msgId, text) { + // if we have an id then this is a replacable message. + // next weird logic is as follows: + // - wrapping to some element may break elements flow + // - using well known elements may break styles + // - setting just starting mark is useless (we will never find the correct end) + var uniqId; + if (isMuc) { + var u = usersMap[nick]; + if (!u) { + return text; + } + + uniqId = "pmr"+uniqReplId.toString(36); // pmr - psi message replace :-) + //chat.console("Sender:"+nick); + usersMap[nick].msgs[msgId] = uniqId; + } else { + var uId = isLocal?"l":"r"; + uniqId = "pmr"+uId+uniqReplId.toString(36); + if (!usersMap[uId]) { + usersMap[uId]={msgs:{}}; + } + usersMap[uId].msgs[msgId] = uniqId; } - formatStr = formatStr || "j:mm"; - if (formatStr.indexOf('%') !== -1) { - formatStr = convertToTr35(formatStr); + uniqReplId++; + // TODO better remember elements themselves instead of some id. + return "" + text + ""; + }, + + replaceMessage : function(parentEl, isMuc, isLocal, nick, msgId, newId, text) { + var u + if (isMuc) { + u = usersMap[nick]; + } else { + u = usersMap[isLocal?"l":"r"]; } + //chat.console(isMuc + " " + isLocal + " " + nick + " " + msgId + " " + chat.util.props(u, true)); - formatStr = convertToMoment(formatStr); + var uniqId = u && u.msgs[msgId]; + if (!uniqId) + return false; // replacing something we didn't use replaceableMessage for? hm. - this.format = function(val) { - if (val instanceof String) { - val = Date.parse(val); + var se =parentEl.querySelector("psims[mid='"+uniqId+"']"); + var ee =parentEl.querySelector("psime[mid='"+uniqId+"']"); +// chat.console("Replace: start: " + (se? "found, ":"not found, ") + +// "end: " + (ee? "found, ":"not found, ") + +// "parent match: " + ((se && ee && se.parentNode === ee.parentNode)?"yes":"no")); + if (se && ee && se.parentNode === ee.parentNode) { + delete u.msgs[msgId]; // no longer used. will be replaced with newId. + while (se.nextSibling !== ee) { + se.parentNode.removeChild(se.nextSibling); } - return moment(val).format(formatStr); // FIXME we could speedup it by keeping fomatter instances + var node = chat.util.createHtmlNode(chat.util.replaceableMessage(isMuc, isLocal, nick, newId, text + '')); + //chat.console(chat.util.props(node)); + chat.util.handleLinks(node); + chat.util.replaceIcons(node); + ee.parentNode.insertBefore(node, ee); + se.parentNode.removeChild(se); + ee.parentNode.removeChild(ee); + return true; } + return false; }, + CSSString2CSSStyleSheet : function(css) { + const style = document.createElement ( 'style' ); + style.innerText = css; + document.head.appendChild ( style ); + const {sheet} = style; + document.head.removeChild ( style ); + return sheet; + } + } + + var chat = { + async : async, + console : server.console, + server : server, + session : session, + hooks: [], + + util: util, + WindowScroller: WindowScroller, + LikeButton: LikeButton, + ReactionsSelector: ReactionsSelector, + ContextMenu: ContextMenu, + DateTimeFormatter : DateTimeFormatter, AudioMessage : AudioMessage, receiveObject : function(data) {