summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--.ci/windows/build.bat2
-rw-r--r--.gitlab-ci.yml7
-rw-r--r--CHANGELOG.md197
-rw-r--r--CMakeLists.txt12
-rw-r--r--README.md87
-rw-r--r--appveyor.yml134
-rw-r--r--im.nheko.Nheko.yaml73
-rw-r--r--resources/AppxManifest.xml2
-rw-r--r--resources/Nheko.appinstaller10
-rw-r--r--resources/NhekoNightly.appinstaller2
-rw-r--r--resources/langs/nheko_de.ts14
-rw-r--r--resources/langs/nheko_en.ts13
-rw-r--r--resources/nheko.appdata.xml.in7
-rw-r--r--resources/qml/MessageView.qml20
-rw-r--r--resources/qml/RoomList.qml2
-rw-r--r--resources/qml/TimelineBubbleMessageStyle.qml1
-rw-r--r--resources/qml/TimelineDefaultMessageStyle.qml5
-rw-r--r--resources/qml/TimelineEvent.qml1
-rw-r--r--resources/qml/TimelineView.qml4
-rw-r--r--resources/qml/TopBar.qml2
-rw-r--r--resources/qml/dialogs/InputDialog.qml2
-rw-r--r--src/ChatPage.cpp19
-rw-r--r--src/LoginPage.cpp3
-rw-r--r--src/MainWindow.cpp2
-rw-r--r--src/RegisterPage.cpp3
-rw-r--r--src/TrayIcon.cpp25
-rw-r--r--src/TrayIcon.h13
-rw-r--r--src/UserSettingsPage.cpp3
-rw-r--r--src/UsersModel.cpp13
-rw-r--r--src/Utils.cpp35
-rw-r--r--src/Utils.h3
-rw-r--r--src/main.cpp50
-rw-r--r--src/timeline/InputBar.cpp30
-rw-r--r--src/timeline/InputBar.h16
-rw-r--r--src/timeline/TimelineModel.cpp31
-rw-r--r--src/timeline/TimelineViewManager.cpp11
-rw-r--r--src/ui/NhekoGlobalObject.cpp4
-rw-r--r--src/voip/WebRTCSession.cpp13
38 files changed, 542 insertions, 329 deletions
diff --git a/.ci/windows/build.bat b/.ci/windows/build.bat
index 4d392cb7..7e952237 100644
--- a/.ci/windows/build.bat
+++ b/.ci/windows/build.bat
@@ -4,7 +4,7 @@
 if defined CI_COMMIT_TAG (

 	set VERSION=%CI_COMMIT_TAG%

 ) else (

-	set VERSION=v0.11.3

+	set VERSION=v0.12.0

 )

 set INSTVERSION=%VERSION:~1%

 set WINVERSION=%VERSION:~1%.%CI_JOB_ID%

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 464d0f86..13fe25dd 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -96,9 +96,14 @@ pages:
     - apk add curl jq
   script:
     - export LATEST_WINDOWS_NIGHTLY=$(curl "https://nheko.im/api/v4/projects/2/packages?package_name=windows-nightly&order_by=version&sort=desc" | jq -r '.[0].version')
-    - sed "s/0.11.3.4/${LATEST_WINDOWS_NIGHTLY}/g" -i resources/NhekoNightly.appinstaller
+    #- export LATEST_WINDOWS=$(curl "https://nheko.im/api/v4/projects/2/packages?package_name=windows&order_by=version&sort=desc" | jq -r '.[0].version')
+    # hardcoded to avoid fuzzy matching
+    - export LATEST_WINDOWS='0.12.0.35798'
+    - sed "s/0.12.0.0/${LATEST_WINDOWS_NIGHTLY}/g" -i resources/NhekoNightly.appinstaller
+    - sed "s/0.12.0.0/${LATEST_WINDOWS}/g" -i resources/Nheko.appinstaller
     - mkdir public
     - mv resources/NhekoNightly.appinstaller public
+    - mv resources/Nheko.appinstaller public
   needs:
     - job: upload-windows
       optional: true
diff --git a/CHANGELOG.md b/CHANGELOG.md
index aa0ed778..955dcb04 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,202 @@
 # Changelog
 
+## [0.12.0] -- 2024-06-12
+
+### Notes
+
+The packages for windows are split into an appinstaller file with autoupdates
+and an msix, which can be installed directly and won't check for updates. The
+appimage is currently disabled until someone ports it to Qt6. The flatpak appid
+changed and you will need to migrate manually.
+
+For packagers most of our dependencies have been changed or updated. Most
+significantly Nheko now depends on KDSingleApplication and Qt6.
+
+### Highlights
+
+- Qt6 6️⃣
+  - This release migrates to version 6 of the Qt toolkit.
+  - This brings various changes to Nheko. Scrolling might feel slower on some
+      platforms. Theming will look different. You have been warned!
+  - We left out all the fixes we had to do for this from the changelog.
+      Otherwise it might have been twice as long...
+- Intentional mentions 🔔
+  - You can now decide if a message should mention someone or not.
+  - Replies now also include an explicit mention (MSC4142).
+- Event expiration ⏲️
+  - You can now make Nheko delete messages regularly.
+  - Configure either a time or a maximum number of messages.
+  - Can be configured per room or globally.
+- Ignoring users (NepNep) 🔕
+  - You can now ignore other users.
+  - This will hide their messages from the timeline.
+  - You can either user the `/ignore` command, the button in their profile or
+      the button in the Nheko settings.
+
+### Features
+
+- Support for video calls (including screenshare) on Windows. This support is
+    currently not available in prebuilt packages. (checkraisefold)
+- `/glitch` commands to garble your text. (LorenDB)
+- Deleting sticker packs. (FallenValkyrie)
+- Settings for your current profile state. (online, offline, unavailable, auto)
+- "Goto this event" button in the timeline when searching.
+- Right click menu entry to go to event when searching. (Sateallia)
+- Remember the last used filter (community, tag, etc) between restarts. (Sateallia)
+- Render reactions with custom emoji.
+- Add custom emoji to the emoji popup.
+- Add edit button to sticker and emoji popups.
+- Optionally sort the room list alphabetically. (Sateallia)
+- Allow sending custom message types. (LorenDB)
+- Rainfall effect. (LorenDB)
+- Screenshare using XDG desktop portals (like on Wayland). (David Elsing)
+- Show which rooms you share with another user.
+
+### Improvements
+
+- Support MSC3916 for authenticated media.
+- Disable workarounds for inline images for Qt6.7 and up.
+- Various improvements around window activation on Wayland. (q234rty)
+- Update emoji shortcodes. (TheDrawingCoder-Gamer)
+- Windows and Apple Silicon builds on our own hardware. (Thanks Thulinma for
+    sponsoring access the Apple Silicon box)
+- MSIX builds and appinstaller for Windows with valid signatures and automatic updates.
+- Add environment variable to dump the video call pipeline.
+- Bump supported maximum Matrix version to 1.10.
+- Various fixes around blurry graphics on HiDPI systems. (q234rty)
+- Build instructions for Qt6 version on Debian Trixie. (enigma9o7)
+- Allow reporting messages to your server admin. (LorenDB)
+- Matrix URI handling on macOS. (LorenDB)
+- Disable endless pagination for threads.
+- Deinit gstreamer appropriately. (NepNep)
+- Support the "fixed" mac method during verification.
+- Show/hide password button on login page. (Bubu)
+- Faster blurhash decode.
+- Speedup room switching.
+- Setting to disable swipe navigation. (duarm)
+- Click handling for Windows notifications.
+- Update gstreamer in flatpak. (Francesco Gazzetta)
+- Activation token handling for notifications.
+- Improve Haiku support. (Begasus)
+- Switch to KDSingleApplication for single instance handling.
+- Trust handling for received megolm sessions.
+- Highlight spaces in bold in the quick switcher.
+- Throttle sync processing when the window is unfocused.
+- Allow hiding unsupported events via the hidden events dialog.
+- Change appid to im.nheko.Nheko. (Miika Tuominen)
+- .editorconfig and .gitattributes. (Aminda Suomalainen)
+- Remove fetched messages only on startup.
+- Focus message input after drag and dropping a file. (Sateallia)
+- Add extra styling for effect messages.
+- Reduce CPU usage from animated images not currently visible.
+- Close and open buttons for the room directory. (LorenDB)
+- Touch scrolling for text. (LorenDB)
+- Unify our usage of `@user:example.com`. (LorenDB)
+- Explicit default font options. (LorenDB)
+- Show powerlevel of mods and admins in the timeline.
+- Mark room as read from the room list.
+- Focus input bar after selecting a file. (Sateallia)
+- Rework history settings.
+- Show server ACL changes.
+- Show inviter on invites.
+- Sections for the sticker picker.
+- Automatically strip file extensions of images in sticker picker.
+- Focus the input bar on key presses.
+- Search rooms in quick switcher by 'activity'.
+- Make tombstoned rooms italic in the quick switcher.
+- Allow uploading multiple files at once via the file picker. (Sateallia)
+- Combine notifications above a certain count. (LcsTen)
+- Allow querying the status msg over dbus (if enabled).
+- Allow `#` character in url fragments (to work around clients not escaping
+    matrix.to links).
+- Improve state event redaction.
+- Hide inaccessible rooms in communities. (LcsTen)
+- Update community metadata automatically.
+- Include ACLs in via calculation.
+- Focus message input on "scroll to bottom". (tastytea)
+- Warn on invalid /command. (LorenDB)
+- Cleanup table rendering.
+- Blurhash images on privacy screen. (LorenDB)
+- Improve OpenBSD support. (Klemens Nanni)
+- Show full status mesage in profile and on hover. (Bubu)
+- Animate transition from blurhash. (LorenDB)
+
+### Translations
+
+- Portugese (Tmpod)
+- Polish (Przemysław Romanik, Vaxry)
+- Dutch (Jaron Viëtor, Ruben De Smet)
+- Turkish (Tennouji Misaki, Emilia)
+- Chinese (Traditional) (AdrianL40)
+- Chinese (Simplified) (Poesty Li, Eric, Integral, Estela ad Astra)
+- Russian (pizdjuk)
+- Indonesian (Linerly)
+- Esperanto (Tirifto)
+- Estonian (Priit Jõerüüt)
+- French (val, luilegeant, CB, Guillaume Girol, Pixead, Mohamad Damaj, Tonus,
+    Mayeul Cantan)
+- German
+- Italian (DynamoFox, Elia Tomasi)
+- Spanish (CM0use)
+- Finnish (Lurkki14, Aminda Suomalainen)
+- Arabic (nk)
+- Persian (Farooq Karimi Zadeh)
+- Ukrainian (NullPointerException)
+
+### Bugfixes
+
+- Prevent shortcuts from inserting unprintable characters.
+- Display emojis in avatars properly.
+- Prevent opening empty Nheko profiles by accident.
+- DMs created in Nheko were not marked as DMs properly.
+- Prevent opening user profiles for empty mxids.
+- Fix crash during video calls on Linux. (checkraisefold)
+- Fix validation errors in Linux appdata.xml. (Echo J)
+- Properly copy images to the clipboard on Windows. (NepNep)
+- Prevent emoji verification and room settings from being clipped by default.
+    (Brayd)
+- Properly remove attributes on del tags.
+- Properly scope presence setting to profiles.
+- Animated images first rendered outside the visible area shouldn't be
+    invisible anymore.
+- Correctly handle of multiple devices in parallel.
+- Avoid lag when media messages are shown from enumerating audio devices.
+- Hidden topic for spaces.
+- Url encoding for widget urls.
+- Profile argument parsing for `-p=`. (LorenDB)
+- Unset hidden space setting when leaving a space.
+- Round images are square.
+- Don't freeze after stopping a call on Wayland. (GStreamer frees the EGL
+    context...)
+- Database name length limitation on some filesystems with long userids.
+- Pagination in search.
+- Save profile also when no setting is modified.
+- Fix decrypt notification setting not being stored properly.
+- Calculate the name of rooms with 3 members correctly.
+- Crash on database migration. (mips64-el)
+- `<hr>` tag escaping.
+- Confetti being left over after a celebration.
+- Powerlevel indicator size in timeline.
+- Duplicate qml ids. (ShootingStarDragons)
+- Presence updates in the timeline.
+- Pagination in rooms only containing redactions.
+- Set a pack avatar.
+- Make settings comboboxes dependent on content width.
+- Don't lose message draft history after an edit.
+- Workaround some WM specific behaviour regarding the focus during search.
+    (Sateallia)
+- Handle network errors better when marking a mssage as read.
+- Name and attributions of image packs should be plain text.
+- Displaying encrypted thumbnails.
+- 0 size dialogs. (0xDEADCADE)
+- Loading image packs in unjoined communities.
+- Show encryption dialog once, not twice. (LorenDB)
+- Elide nicks and userids in various dialogs. (LorenDB)
+- macOS builds (1000x).
+- Disable qml disk cache by default.
+- QT_SCALE_FACTOR on OpenBSD. (Klemens Nanni)
+- Deduplicate reactions.
+
 ## [0.11.3] -- 2023-02-23
 
 ### Bugfix
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 5054be05..04738bda 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -113,8 +113,8 @@ include(QtCommon)
 include(GNUInstallDirs)
 
 set(CPACK_PACKAGE_VERSION_MAJOR "0")
-set(CPACK_PACKAGE_VERSION_MINOR "11")
-set(CPACK_PACKAGE_VERSION_PATCH "3")
+set(CPACK_PACKAGE_VERSION_MINOR "12")
+set(CPACK_PACKAGE_VERSION_PATCH "0")
 set(PROJECT_VERSION_MAJOR ${CPACK_PACKAGE_VERSION_MAJOR})
 set(PROJECT_VERSION_MINOR ${CPACK_PACKAGE_VERSION_MINOR})
 set(PROJECT_VERSION_PATCH ${CPACK_PACKAGE_VERSION_PATCH})
@@ -125,8 +125,8 @@ fix_project_version()
 
 # Set additional project information
 set(COMPANY "Nheko")
-set(COPYRIGHT "Copyright (c) 2023 Nheko Contributors")
-set(IDENTIFIER "io.github.nheko-reborn.nheko")
+set(COPYRIGHT "Copyright (c) 2024 Nheko Contributors")
+set(IDENTIFIER "im.nheko.Nheko")
 
 add_project_meta(META_FILES_TO_INCLUDE)
 
@@ -606,13 +606,13 @@ if(USE_BUNDLED_MTXCLIENT)
     FetchContent_Declare(
         MatrixClient
             GIT_REPOSITORY https://github.com/Nheko-Reborn/mtxclient.git
-            GIT_TAG        8b3c9a34770df147fbd78134dc71a9b27471d153
+            GIT_TAG        v0.10.0
     )
     set(BUILD_LIB_EXAMPLES OFF CACHE INTERNAL "")
     set(BUILD_LIB_TESTS OFF CACHE INTERNAL "")
     FetchContent_MakeAvailable(MatrixClient)
 else()
-    find_package(MatrixClient 0.9.0 REQUIRED)
+    find_package(MatrixClient 0.10.0 REQUIRED)
 endif()
 
 if(VOIP)
diff --git a/README.md b/README.md
index 37ebcf42..41b32886 100644
--- a/README.md
+++ b/README.md
@@ -12,7 +12,7 @@ feels more like a mainstream chat app ([Element], Telegram etc) and less like an
 ### Stable
 
 [![Stable Version](https://img.shields.io/badge/download-stable-green.svg)](https://github.com/Nheko-Reborn/nheko/releases/latest)
-<a href='https://flathub.org/apps/details/io.github.NhekoReborn.Nheko'><img height='32' alt='Download on Flathub' src='https://flathub.org/assets/badges/flathub-badge-en.png'/></a>
+<a href='https://flathub.org/apps/details/im.nheko.Nheko'><img height='32' alt='Download on Flathub' src='https://flathub.org/assets/badges/flathub-badge-en.png'/></a>
 [![Packaging status](https://repology.org/badge/tiny-repos/nheko.svg)](https://repology.org/project/nheko/versions)
 
 For other options and details see the [Installation](#installation) section.
@@ -39,17 +39,19 @@ Most of the features you would expect from a chat application are missing right
 but we are getting close to a more feature complete client.
 Specifically there is support for:
 - E2E encryption.
-- VoIP calls (voice & video).
+- VoIP calls (voice & video, support varies by platform).
 - User registration.
 - Creating, joining & leaving rooms.
 - Sending & receiving invites.
 - Sending & receiving files and emoji (inline widgets for images, audio and file messages).
+- Custom stickers and emoji.
 - Replies with text, images and other media (and actually render them as inline widgets).
 - Typing notifications.
 - Username auto-completion.
 - Message & mention notifications.
 - Redacting messages.
 - Read receipts.
+- Presence and status messages (if enabled on the server side).
 - Basic communities support.
 - Room switcher (ctrl-K).
 - Light, Dark & System themes.
@@ -60,8 +62,9 @@ Specifically there is support for:
 
 ### Releases
 
-Releases for Linux (AppImage), macOS (disk image) & Windows (x64 installer)
+Releases for Linux (Flatpak), macOS (disk image) & Windows (x64 msix or appinstaller)
 can be found in the [GitHub releases](https://github.com/Nheko-Reborn/nheko/releases).
+The appinstaller on Windows will regularly check our servers for updates. The msix won't.
 
 ### Repositories
 
@@ -110,10 +113,38 @@ sudo urpmi nheko
 #### Nix(os)
 
 ```bash
+# Imperatively: (not recommended)
 nix-env -iA nixpkgs.nheko
-# or
+
+# In an ephemeral shell: (recommended if you just want to try it out without committing to a full installation)
 nix-shell -p nheko --run nheko
+# Note: The above command will both install and run Nheko.
+# To stop it from running immediately, just remove the `--run nheko` from the end.
+```
+Alternatively, add it to your config in one of the following ways: (recommended for long-term installation)
+
+**System-wide:**
+```nix
+environment.systemPackages = with pkgs; [
+    # ...
+    nheko
+    # ...
+];
+```
+**User-specific:**
+```nix
+users.users.<user>.packages = with pkgs; [
+    # ...
+    nheko
+    # ...
+];
 ```
+**via `home-manager`:**
+```nix
+programs.nheko.enable = true;
+```
+
+
 
 #### Alpine Linux (and postmarketOS)
 
@@ -143,7 +174,7 @@ sudo zypper install qt-jdenticon
 #### Flatpak
 
 ```
-flatpak install flathub io.github.NhekoReborn.Nheko
+flatpak install flathub im.nheko.Nheko
 ```
 
 #### Guix
@@ -157,8 +188,8 @@ guix install nheko
 Install nheko via the `Discover` app in Desktop Mode (this installs the flatpak). To also make it work in Game Mode you'll have create a wrapper script that starts kwalletd and then nheko. You can create `/home/deck/nheko.sh` with the following content and then add this script as a "Non-Steam Game" to Steam.
 ```bash
 #!/bin/sh
-kwalletd5&
-flatpak run --env=XDG_CURRENT_DESKTOP=KDE --env=KDE_SESSION_VERSION=5 --branch=stable --arch=x86_64 --command=io.github.NhekoReborn.Nheko --file-forwarding io.github.NhekoReborn.Nheko @@u @@
+kwalletd6&
+flatpak run --env=XDG_CURRENT_DESKTOP=KDE --env=KDE_SESSION_VERSION=5 --branch=stable --arch=x86_64 --command=im.nheko.Nheko --file-forwarding im.nheko.Nheko @@u @@
 ```
 
 #### macOS (10.14 and above)
@@ -237,23 +268,23 @@ sharing easier.
 
 **A:** Nheko uses Qt's image plugins to render images. You might need to install
 additional packages to display some image types like webp. Usually those
-packages are called `qt5-image-formats-plugins`, `qt5-imageformats` or similar.
+packages are called `qt6-image-formats-plugins`, `qt6-imageformats` or similar.
 KDE has similar plugins, that can extend the supported image types even more.
 
 ---
 
 ### Build Requirements
 
-- Qt5 (5.15 or greater). Required for overlapping hover handlers in Qml.
+- Qt6 (6.5 or greater). Required for overlapping hover handlers in Qml.
 - CMake 3.15 or greater. (Lower version may work, but may break boost linking)
 - [mtxclient](https://github.com/Nheko-Reborn/mtxclient)
 - [coeurl](https://nheko.im/nheko-reborn/coeurl)
-- [LMDB](https://symas.com/lightning-memory-mapped-database/)
+- [LMDB](https://www.symas.com/lmdb)
 - [lmdb++](https://github.com/hoytech/lmdbxx) (0.9.14 too old)
 - [cmark](https://github.com/commonmark/cmark) 0.29 or greater.
 - [libolm](https://gitlab.matrix.org/matrix-org/olm)
 - [spdlog](https://github.com/gabime/spdlog) (1.8.1 too old)
-- [GStreamer](https://gitlab.freedesktop.org/gstreamer) 1.18.0 or greater (optional, needed for VoIP support. Pass `-DVOIP=OFF` to disable.).
+- [GStreamer](https://gitlab.freedesktop.org/gstreamer) 1.20.0 or greater (optional, needed for VoIP support. Pass `-DVOIP=OFF` to disable.).
     - Installing the gstreamer core library plus gst-plugins-base, gst-plugins-good & gst-plugins-bad
       is often sufficient. The qmlgl plugin though is often packaged separately. The actual plugin requirements
       are as follows:
@@ -265,7 +296,7 @@ KDE has similar plugins, that can extend the supported image types even more.
 - [KDSingleApplication](https://github.com/KDAB/KDSingleApplication) (1.0 or greater with Qt6 support)
 - A compiler that supports C++ 20:
     - Clang 16 (Only clazy 16 is tested in CI)
-    - GCC 11 (tested on Gitlab CI)
+    - GCC 11.3 (tested on Gitlab CI)
     - MSVC 19.13 (tested on AppVeyor)
 
 Nheko can use bundled version for most of those libraries automatically, if the versions in your distro are too old.
@@ -303,17 +334,17 @@ make docker-app-image
 #### Arch Linux
 
 ```bash
-sudo pacman -S qt5-base \
-    qt5-tools \
-    qt5-multimedia \
-    qt5-svg \
+sudo pacman -S qt6-base \
+    qt6-tools \
+    qt6-multimedia \
+    qt6-svg \
     cmake \
     gcc \
     fontconfig \
     lmdb \
     cmark \
     boost \
-    qtkeychain-qt5
+    qtkeychain-qt6
 ```
 
 #### Debian 13 [Testing/Sid] (Nheko QT6 Version)
@@ -335,8 +366,8 @@ cmake --build build
 *Build requirements + qml modules needed at runtime (you may not need all of them, but the following seem to work according to reports):*
 ```bash
 sudo apt install --no-install-recommends g++ cmake make zlib1g-dev libssl-dev libolm-dev liblmdb-dev libcmark-dev nlohmann-json3-dev libspdlog-dev libevent-dev libcurl4-openssl-dev libre2-dev libxcb-ewmh-dev asciidoc-base \
-qt{base,declarative,tools,multimedia,quickcontrols2-}5-dev libqt5svg5-dev qt5keychain-dev qml-module-qt{gstreamer,multimedia,quick-extras,-labs-settings,-labs-platform,graphicaleffects,quick-controls2,quick-particles2} \
-libgstreamer1.0-dev libgstreamer-plugins-{base,bad}1.0-dev qtgstreamer-plugins-qt5 libnice-dev ninja-build
+qt{base,declarative,tools,multimedia,quickcontrols2-}5-dev libqt6svg5-dev qt6keychain-dev qml-module-qt{gstreamer,multimedia,quick-extras,-labs-settings,-labs-platform,graphicaleffects,quick-controls2,quick-particles2} \
+libgstreamer1.0-dev libgstreamer-plugins-{base,bad}1.0-dev qtgstreamer-plugins-qt6 libnice-dev ninja-build
 ```
 lmdb++-dev is too old so bundled lmdbxx must be used.  
 libspdlog-dev in debian bullseye is too old (without backporting) so requires using hunter to use bundled spdlog.  
@@ -365,7 +396,7 @@ guix environment nheko
 
 ```bash
 brew update
-brew install qt5 lmdb cmake llvm spdlog boost cmark libolm qtkeychain
+brew install qt6 lmdb cmake llvm spdlog boost cmark libolm qtkeychain
 ```
 
 #### Windows
@@ -398,18 +429,18 @@ Adapt the USE_BUNDLED_* as needed.
 
 If the build fails with the following error
 ```
-Could not find a package configuration file provided by "Qt5Widgets" with
+Could not find a package configuration file provided by "Qt6Widgets" with
 any of the following names:
 
-Qt5WidgetsConfig.cmake
-qt5widgets-config.cmake
+Qt6WidgetsConfig.cmake
+qt6widgets-config.cmake
 ```
-You might need to pass `-DCMAKE_PREFIX_PATH` to cmake to point it at your qt5 install.
+You might need to pass `-DCMAKE_PREFIX_PATH` to cmake to point it at your qt6 install.
 
 e.g on macOS
 
 ```
-cmake -S. -Bbuild -DCMAKE_BUILD_TYPE=Release -DCMAKE_PREFIX_PATH=$(brew --prefix qt5)
+cmake -S. -Bbuild -DCMAKE_BUILD_TYPE=Release -DCMAKE_PREFIX_PATH=$(brew --prefix qt6)
 cmake --build build
 ```
 
@@ -420,11 +451,11 @@ The `nheko` binary will be located in the `build` directory.
 After installing all dependencies, you need to edit the `CMakeSettings.json` to
 be able to load and compile nheko within Visual Studio.
 
-You need to fill out the paths for the `Qt5_DIR`.
-The Qt5 dir should point to the `lib\cmake\Qt5` dir.
+You need to fill out the paths for the `Qt6_DIR`.
+The Qt6 dir should point to the `lib\cmake\Qt6` dir.
 
 Examples for the paths are:
- - `C:\\Qt\\5.15.1\\msvc2017_64\\lib\\cmake\\Qt5`
+ - `C:\\Qt\\6.5.2\\msvc2017_64\\lib\\cmake\\Qt6`
 
 You should also enable hunter by setting `HUNTER_ENABLED` to `ON` and `BUILD_SHARED_LIBS` to `OFF`.
 
diff --git a/appveyor.yml b/appveyor.yml
deleted file mode 100644
index 084e7179..00000000
--- a/appveyor.yml
+++ /dev/null
@@ -1,134 +0,0 @@
----
-
-version: 0.11.3-{build}
-
-configuration: Release
-image: Visual Studio 2022
-platform: x64
-
-environment:
-    APPVEYOR_SAVE_CACHE_ON_ERROR: true
-    MATRIX_ACCESS_TOKEN:
-        secure: Qoy+QQ8zWXYCQrck9GtXJsoPTv9r/rhgCDUlKJ6ue+gkteYG40E9MxgwP1svn6bse20H4z6Svrxn8kFbcJB7Wg2Cnv1s326/vsJJzhWir2eHFFGK+f4SB992/U0HoQmk3Cq5hPk7dLcA7KqHIa1g1PTSFPfl1VODJ2UqqAyn8nzbC5ym+wwU1buJqoWPlTyHBW7eE8wNe77+qI18XpF7NN8yuOOyg3Tzup9YyXLrI36XiJu/5JD3j3s3V1QiUTpuLyQzqwuBUOf1MHTbzuPwHm3ZwzSM98WD6aL6riaK9qa7mDbSx1aY0ukIYSY9IdAfHNwZY/DEAn+QAVD+ZTvPq04ASv+kmSFpOBKr07kpqfM=
-
-
-
-cache:
-  - c:\hunter\ -> appveyor.yml, CMakeLists.txt, cmake/Hunter/config.cmake
-  - build\_deps -> appveyor.yml, CMakeLists.txt
-
-build:
-    verbosity: minimal
-
-install:
-    - set QT_DIR=C:\Qt\6.5\msvc2019_64
-    - set PATH=C:\Strawberry\perl\bin;C:\Python39-x64;%QT_DIR%\bin;%PATH%
-
-build_script:
-    # VERSION format:     branch-master/branch-1.2
-    # INSTVERSION format: x.y.z
-    # WINVERSION format:  9999.0.0.123/1.2.0.234
-    - if "%APPVEYOR_REPO_TAG%"=="false" set INSTVERSION=0.11.3
-    - if "%APPVEYOR_REPO_TAG%"=="false" set VERSION=0.11.3
-    - if "%APPVEYOR_REPO_TAG%"=="false" set WINVERSION=%INSTVERSION%.%APPVEYOR_BUILD_NUMBER%
-    # VERSION format:     v1.2.3/v1.3.4
-    # INSTVERSION format: 1.2.3/1.3.4
-    # WINVERSION format:  1.2.3.123/1.3.4.234
-    - if "%APPVEYOR_REPO_TAG%"=="true" set VERSION=%APPVEYOR_REPO_TAG_NAME%
-    - if "%APPVEYOR_REPO_TAG%"=="true" set INSTVERSION=%VERSION:~1%
-    - if "%APPVEYOR_REPO_TAG%"=="true" set WINVERSION=%VERSION:~1%.%APPVEYOR_BUILD_NUMBER%
-    - set DATE=%date:~10,4%-%date:~4,2%-%date:~7,2%
-    - echo %VERSION%
-    - echo %INSTVERSION%
-    - echo %DATE%
-
-    # Build nheko
-    - cmake -G "Visual Studio 17 2022" -A x64 -H. -Bbuild
-      -DHUNTER_ROOT="C:\hunter"
-      -DHUNTER_ENABLED=ON -DBUILD_SHARED_LIBS=OFF -DUSE_BUNDLED_OPENSSL=ON -DUSE_BUNDLED_KDSINGLEAPPLICATION=ON
-      -DCMAKE_BUILD_TYPE=Release -DHUNTER_CONFIGURATION_TYPES=Release
-
-    - cmake --build build --config Release
-
-    - call "C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Auxiliary\Build\vcvarsall.bat" x64
-    - git clone https://github.com/Nheko-Reborn/qt-jdenticon.git
-    - cd qt-jdenticon
-    - qmake
-    - nmake
-    - cd ..
-
-after_build:
-    # Variables
-    - set BUILD=%APPVEYOR_BUILD_FOLDER%
-    - echo %BUILD%
-    - mkdir NhekoRelease
-    - copy build\Release\nheko.exe NhekoRelease\nheko.exe
-    - copy qt-jdenticon\release\qtjdenticon0.dll NhekoRelease\qtjdenticon.dll
-    - copy build\_deps\cmark-build\src\Release\cmark.dll NhekoRelease\cmark.dll
-    - windeployqt --qmldir resources\qml\ NhekoRelease\nheko.exe
-
-    - 7z a nheko_win_64.zip .\NhekoRelease\*
-    - ls -lh build\Release\
-    - ls -lh NhekoRelease\
-    - mkdir NhekoData
-    - xcopy .\NhekoRelease\*.* NhekoData\*.* /s /e /c /y
-    #
-    # Create the Qt Installer Framework version
-    #
-    - mkdir installer
-    - mkdir installer\config
-    - mkdir installer\packages
-    - mkdir installer\packages\io.github.nhekoreborn.nheko
-    - mkdir installer\packages\io.github.nhekoreborn.nheko\data
-    - mkdir installer\packages\io.github.nhekoreborn.nheko\meta
-    # Copy installer data
-    - copy %BUILD%\resources\nheko.ico installer\config
-    - copy %BUILD%\resources\nheko.png installer\config
-    - copy %BUILD%\COPYING installer\packages\io.github.nhekoreborn.nheko\meta\license.txt
-    - copy %BUILD%\deploy\installer\config.xml installer\config
-    - copy %BUILD%\deploy\installer\controlscript.qs installer\config
-    - copy %BUILD%\deploy\installer\uninstall.qs installer\packages\io.github.nhekoreborn.nheko\data
-    - copy %BUILD%\deploy\installer\gui\package.xml installer\packages\io.github.nhekoreborn.nheko\meta
-    - copy %BUILD%\deploy\installer\gui\installscript.qs installer\packages\io.github.nhekoreborn.nheko\meta
-    # Amend version and date
-    - sed -i "s/__VERSION__/%VERSION%/" installer\config\config.xml
-    - sed -i "s/__VERSION__/%VERSION%/" installer\packages\io.github.nhekoreborn.nheko\meta\package.xml
-    - sed -i "s/__DATE__/%DATE%/" installer\packages\io.github.nhekoreborn.nheko\meta\package.xml
-    # Copy nheko data
-    - xcopy NhekoData\*.* installer\packages\io.github.nhekoreborn.nheko\data\*.* /s /e /c /y
-    - copy NhekoRelease\nheko.exe installer\packages\io.github.nhekoreborn.nheko\data
-    - mkdir tools
-    - curl -L -O https://download.qt.io/official_releases/qt-installer-framework/4.3.0/QtInstallerFramework-windows-x86-4.3.0.exe
-    - 7z x QtInstallerFramework-windows-x86-4.3.0.exe -otools -aoa
-    - set PATH=%BUILD%\tools\bin;%PATH%
-    - binarycreator.exe -f -c installer\config\config.xml -p installer\packages nheko-installer.exe
-
-    # build an msix
-    - mkdir msix
-    - xcopy .\NhekoRelease\*.* msix\*.* /s /e /c /y
-    - copy %BUILD%\resources\nheko.png msix
-    - copy %BUILD%\resources\AppxManifest.xml msix
-    - del msix\vc_redist*
-    - 'sed -i "s/ Version=[^ ]*/ Version=\"%WINVERSION%\"/" msix\AppxManifest.xml'
-    - '"C:\Program Files (x86)\Windows Kits\10\App Certification Kit\makeappx.exe" pack -d msix -p nheko.msix'
-
-    - copy nheko-installer.exe nheko-%APPVEYOR_REPO_TAG_NAME%-installer.exe
-    - copy nheko-installer.exe nheko-%APPVEYOR_PULL_REQUEST_HEAD_COMMIT%-installer.exe
-    - ps: .\.ci\upload-nightly.ps1
-
-deploy:
-    - description: "Development builds"
-      provider: GitHub
-      auth_token:
-          secure: "ShStWeqp+TkYqJPQr7uFZb+B8ZTgC7Iwth+IkhjfRDCTLhy8gtWvlPzlQilder3E"
-      artifact: nheko-${APPVEYOR_REPO_TAG_NAME}-installer.exe
-      force_update: true
-      prerelease: true
-      on:
-          appveyor_repo_tag: true
-
-artifacts:
-    - path: nheko_win_64.zip
-    - path: nheko.msix
-    - path: nheko-$(APPVEYOR_REPO_TAG_NAME)-installer.exe
-    - path: nheko-$(APPVEYOR_PULL_REQUEST_HEAD_COMMIT)-installer.exe
diff --git a/im.nheko.Nheko.yaml b/im.nheko.Nheko.yaml
index 8150b292..8cdc5edb 100644
--- a/im.nheko.Nheko.yaml
+++ b/im.nheko.Nheko.yaml
@@ -1,21 +1,23 @@
 id: im.nheko.Nheko
 command: im.nheko.Nheko
 runtime: org.kde.Platform
-runtime-version: '6.6'
+runtime-version: '6.7'
 sdk: org.kde.Sdk
 finish-args:
-  - --device=dri
-  # needed for webcams, see #517
+  # needed for webcams, see https://github.com/Nheko-Reborn/nheko/issues/517
   - --device=all
   - --share=ipc
   - --share=network
   - --socket=pulseaudio
-  - --socket=wayland
   - --socket=x11
+  - --socket=wayland
   - --talk-name=org.freedesktop.Notifications
   - --talk-name=org.freedesktop.secrets
+  - --talk-name=org.kde.kwalletd5
+  - --talk-name=org.kde.kwalletd6
   - --talk-name=org.freedesktop.StatusNotifierItem
-  - --talk-name=org.kde.*
+  # needed for tray icon
+  - --talk-name=org.kde.StatusNotifierWatcher
 cleanup:
   - /include
   - /lib/pkgconfig
@@ -83,18 +85,18 @@ modules:
       - sha256: 5197b3147cfcfaa67dd564db7b878e4a4b3d9f3443801722b3915cdeced656cb
         type: archive
         url: https://github.com/gabime/spdlog/archive/v1.8.1.tar.gz
-  - config-opts:
+  - name: olm
+    config-opts:
       - -DCMAKE_BUILD_TYPE=Release
     buildsystem: cmake-ninja
-    name: olm
     sources:
       - commit: 92769cec711c604a1f682b95d6944578d2a1bb3d
         disable-shallow-clone: true
         tag: 3.2.12
         type: git
         url: https://gitlab.matrix.org/matrix-org/olm.git
-  - buildsystem: meson
-    name: libsecret
+  - name: libsecret
+    buildsystem: meson
     config-opts:
       - -Dmanpage=false
       - -Dvapi=false
@@ -137,51 +139,52 @@ modules:
   #      tag: v5.93.0
   #      type: git
   #      url: https://invent.kde.org/frameworks/kimageformats.git
-  - config-opts:
+  - name: QtKeychain
+    config-opts:
       - -DCMAKE_BUILD_TYPE=Release
       - -DBUILD_TEST_APPLICATION=OFF
       - -DQTKEYCHAIN_STATIC=ON
       - -DBUILD_WITH_QT6=ON
     buildsystem: cmake-ninja
-    name: QtKeychain
     sources:
       - commit: 69f993c47efed7e557d79a30a367014d9a27d809
         tag: 0.14.1
         type: git
         url: https://github.com/frankosterfeld/qtkeychain.git
-  - config-opts:
+  - name: nlohmann
+    config-opts:
       - -DJSON_BuildTests=OFF
-    buildsystem: cmake
-    name: nlohmann
+    buildsystem: cmake-ninja
     sources:
       - sha256: d69f9deb6a75e2580465c6c4c5111b89c4dc2fa94e3a85fcd2ffcd9a143d9273
         type: archive
         url: https://github.com/nlohmann/json/archive/v3.11.2.tar.gz
-  - config-opts:
+  - name: kdsingleapplication
+    config-opts:
       - -DKDSingleApplication_EXAMPLES=OFF
       - -DKDSingleApplication_QT6=ON
-    buildsystem: cmake
-    name: kdsingleapplication
+    buildsystem: cmake-ninja
     sources:
       - sha256: c92355dc10f3ebd39363458458fb5bdd9662e080cf77d91f0437763c4d936520
         type: archive
         url: https://github.com/KDAB/KDSingleApplication/releases/download/v1.0.0/kdsingleapplication-1.0.0.tar.gz
-  - buildsystem: simple
+  - name: re2
+    buildsystem: simple
     build-commands:
       - make static
       - make prefix=/app static-install
-    name: re2
     sources:
       - sha256: f89c61410a072e5cbcf8c27e3a778da7d6fd2f2b5b1445cd4f4508bee946ab0f
         type: archive
         url: https://github.com/google/re2/archive/refs/tags/2022-06-01.tar.gz
-  - buildsystem: meson
-    name: gstreamer
+  - name: gstreamer
+    buildsystem: meson
     sources:
-      - commit: 4d13eddc8b6d3f42ba44682ba42048acf170547f
-        tag: 1.22.7
+      - commit: d2c02bb704b5804ca057fc7e6c7b16b4466fd7d5
+        tag: 1.22.12
         type: git
         url: https://gitlab.freedesktop.org/gstreamer/gstreamer.git
+        disable-submodules: true
     config-opts:
       - --auto-features=disabled
       -  -Dgood=enabled
@@ -195,46 +198,46 @@ modules:
       -  -Dgst-plugins-base:gl_winsys=x11,wayland
       -  -Dgst-plugins-base:x11=enabled
       -  -Dgst-plugins-base:xshm=enabled
-  - buildsystem: cmake
-    name: qt-jdenticon
+  - name: qt-jdenticon
+    buildsystem: cmake-ninja
     no-make-install: true
     build-commands:
       - mkdir -p /app/bin/
       - cp libqtjdenticon.so /app/bin/
     sources:
-      - commit: 1e7013d64fd081d76e4ce69f2693129c817fd8f1
-        #tag: v0.3.0
+      - commit: 39cde33d4b23b57aa5b94e94071d6ff18d2bd92a
+        tag: v0.3.1
         type: git
         url: https://github.com/Nheko-Reborn/qt-jdenticon.git
-  - buildsystem: meson
+  - name: coeurl
+    buildsystem: meson
     config-opts:
       - -Ddefault_library=static
-    name: coeurl
     sources:
       - commit: 3007387745cf84138d0855e0f04ff94261fc7175
         #tag: v0.3.0
         type: git
         url: https://nheko.im/nheko-reborn/coeurl.git
-  - config-opts:
+  - name: mtxclient
+    config-opts:
       - -DBUILD_LIB_TESTS=OFF
       - -DBUILD_LIB_EXAMPLES=OFF
       - -DCMAKE_BUILD_TYPE=Release
       - -DBUILD_SHARED_LIBS=OFF
     buildsystem: cmake-ninja
-    name: mtxclient
     sources:
-      - commit: 8b3c9a34770df147fbd78134dc71a9b27471d153
-        #tag: v0.9.2
+      - commit: 457bc52773b40142848f0b2ab025516bf6ed634d
+        tag: v0.10.0
         type: git
         url: https://github.com/Nheko-Reborn/mtxclient.git
-  - config-opts:
+  - name: nheko
+    config-opts:
       - -DCMAKE_BUILD_TYPE=Release
       - -DLMDBXX_INCLUDE_DIR=.deps/lmdbxx
       - -DCOMPILE_QML=ON
       - -DMAN=OFF
       - -DFLATPAK=ON
     buildsystem: cmake-ninja
-    name: nheko
     sources:
       - path: .
         type: dir
diff --git a/resources/AppxManifest.xml b/resources/AppxManifest.xml
index 9f933604..8253f839 100644
--- a/resources/AppxManifest.xml
+++ b/resources/AppxManifest.xml
@@ -4,7 +4,7 @@
   xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
   xmlns:uap10="http://schemas.microsoft.com/appx/manifest/uap/windows10/10"
   xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities">
-  <Identity Name="im.nheko.Nheko" Version="0.11.3.4" Publisher="CN=Nicolas Werner, O=Nicolas Werner, L=Munich, S=Bavaria, C=DE" ProcessorArchitecture="x64"/>
+  <Identity Name="im.nheko.Nheko" Version="0.12.0.0" Publisher="CN=Nicolas Werner, O=Nicolas Werner, L=Munich, S=Bavaria, C=DE" ProcessorArchitecture="x64"/>
   <Properties>
     <DisplayName>Nheko</DisplayName>
     <PublisherDisplayName>Nheko-Reborn</PublisherDisplayName>
diff --git a/resources/Nheko.appinstaller b/resources/Nheko.appinstaller
new file mode 100644
index 00000000..004a0d73
--- /dev/null
+++ b/resources/Nheko.appinstaller
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<AppInstaller Uri="https://nheko-reborn.pages.nheko.im/nheko/Nheko.appinstaller" Version="0.0.0.1" xmlns="http://schemas.microsoft.com/appx/appinstaller/2018">
+  <MainPackage Name="im.nheko.Nheko" Version="0.12.0.0" Publisher="CN=Nicolas Werner, O=Nicolas Werner, L=Munich, S=Bavaria, C=DE" Uri="https://nheko.im/api/v4/projects/2/packages/generic/windows/0.12.0.0/nheko.msix" ProcessorArchitecture="x64" />
+  <UpdateSettings>
+    <!-- We can't set this to check only once a month, so just check once a week. If the user doesn't want that ping, they should install the msix directly. -->
+    <OnLaunch HoursBetweenUpdateChecks="168" ShowPrompt="true" />
+    <ForceUpdateFromAnyVersion>true</ForceUpdateFromAnyVersion>
+  </UpdateSettings>
+</AppInstaller>
+
diff --git a/resources/NhekoNightly.appinstaller b/resources/NhekoNightly.appinstaller
index bd94f5d9..88f1fd8a 100644
--- a/resources/NhekoNightly.appinstaller
+++ b/resources/NhekoNightly.appinstaller
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <AppInstaller Uri="https://nheko-reborn.pages.nheko.im/nheko/NhekoNightly.appinstaller" Version="0.0.0.1" xmlns="http://schemas.microsoft.com/appx/appinstaller/2018">
-  <MainPackage Name="im.nheko.Nheko" Version="0.11.3.4" Publisher="CN=Nicolas Werner, O=Nicolas Werner, L=Munich, S=Bavaria, C=DE" Uri="https://nheko.im/api/v4/projects/2/packages/generic/windows-nightly/0.11.3.4/nheko.msix" ProcessorArchitecture="x64" />
+  <MainPackage Name="im.nheko.Nheko" Version="0.12.0.0" Publisher="CN=Nicolas Werner, O=Nicolas Werner, L=Munich, S=Bavaria, C=DE" Uri="https://nheko.im/api/v4/projects/2/packages/generic/windows-nightly/0.12.0.0/nheko.msix" ProcessorArchitecture="x64" />
   <UpdateSettings>
     <!-- We can't set this to check only once a month, so just check once a week. If the user doesn't want that ping, they should install the msix directly. -->
     <OnLaunch HoursBetweenUpdateChecks="168" ShowPrompt="true" />
diff --git a/resources/langs/nheko_de.ts b/resources/langs/nheko_de.ts
index eb043ca2..a1ba0953 100644
--- a/resources/langs/nheko_de.ts
+++ b/resources/langs/nheko_de.ts
@@ -2117,7 +2117,7 @@ Beispiel: https://deinserver.example.com:8787</translation>
         <location line="+12"/>
         <source>%1 replied with a spoiler.</source>
         <comment>Format a reply in a notification. %1 is the sender.</comment>
-        <translation type="unfinished"></translation>
+        <translation>%1 hat mit einem Spoiler geantwortet.</translation>
     </message>
     <message>
         <location line="+10"/>
@@ -3367,7 +3367,9 @@ Beispiel: https://deinserver.example.com:8787</translation>
         <location line="+1"/>
         <source>Encryption is currently experimental and things might break unexpectedly. &lt;br&gt;
                                 Please take note that it can&apos;t be disabled afterwards.</source>
-        <translation type="unfinished"></translation>
+        <translatorcomment>Verschlüsselung ist aktuell noch experimentell und kann Fehler beinhalten. &lt;br&gt;
+Bitte sei dir bewusst, dass die Verschlüsselung nicht nachträglich deaktiviert werden kann.</translatorcomment>
+        <translation></translation>
     </message>
     <message>
         <location line="+19"/>
@@ -5600,7 +5602,7 @@ Diese Einstellung benötigt einen Neustart von Nheko.</translation>
     <message>
         <location filename="../../src/notifications/ManagerMac.cpp" line="-12"/>
         <source>Message contains spoiler.</source>
-        <translation type="unfinished"></translation>
+        <translation>Nachricht beinhaltet Spoiler.</translation>
     </message>
 </context>
 <context>
@@ -5669,13 +5671,13 @@ Diese Einstellung benötigt einen Neustart von Nheko.</translation>
         <location line="+6"/>
         <location line="+26"/>
         <source>You sent a spoiler.</source>
-        <translation type="unfinished"></translation>
+        <translation>Du hast einen Spoiler gesendet.</translation>
     </message>
     <message>
         <location line="-23"/>
         <location line="+26"/>
         <source>%1 sent a spoiler.</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 hat einen Spoiler gesendet.</translation>
     </message>
     <message>
         <location line="-20"/>
@@ -5702,7 +5704,7 @@ Diese Einstellung benötigt einen Neustart von Nheko.</translation>
     <message>
         <location line="+23"/>
         <source>* %1 spoils something.</source>
-        <translation type="unfinished"></translation>
+        <translation>* %1 spoilert etwas.</translation>
     </message>
     <message>
         <location line="+8"/>
diff --git a/resources/langs/nheko_en.ts b/resources/langs/nheko_en.ts
index 5ea03de2..eb65db49 100644
--- a/resources/langs/nheko_en.ts
+++ b/resources/langs/nheko_en.ts
@@ -2119,7 +2119,7 @@ Example: https://yourserver.example.com:8787</translation>
         <location line="+12"/>
         <source>%1 replied with a spoiler.</source>
         <comment>Format a reply in a notification. %1 is the sender.</comment>
-        <translation type="unfinished"></translation>
+        <translation>%1 replied with a spoiler.</translation>
     </message>
     <message>
         <location line="+10"/>
@@ -3369,7 +3369,8 @@ Example: https://yourserver.example.com:8787</translation>
         <location line="+1"/>
         <source>Encryption is currently experimental and things might break unexpectedly. &lt;br&gt;
                                 Please take note that it can&apos;t be disabled afterwards.</source>
-        <translation type="unfinished"></translation>
+        <translation>Encryption is currently experimental and things might break unexpectedly. &lt;br&gt;
+                                Please take note that it can&apos;t be disabled afterwards.</translation>
     </message>
     <message>
         <location line="+19"/>
@@ -5602,7 +5603,7 @@ This setting will take effect upon restart.</translation>
     <message>
         <location filename="../../src/notifications/ManagerMac.cpp" line="-12"/>
         <source>Message contains spoiler.</source>
-        <translation type="unfinished"></translation>
+        <translation>Message contains spoiler.</translation>
     </message>
 </context>
 <context>
@@ -5671,13 +5672,13 @@ This setting will take effect upon restart.</translation>
         <location line="+6"/>
         <location line="+26"/>
         <source>You sent a spoiler.</source>
-        <translation type="unfinished"></translation>
+        <translation>You sent a spoiler.</translation>
     </message>
     <message>
         <location line="-23"/>
         <location line="+26"/>
         <source>%1 sent a spoiler.</source>
-        <translation type="unfinished"></translation>
+        <translation>%1 sent a spoiler.</translation>
     </message>
     <message>
         <location line="-20"/>
@@ -5704,7 +5705,7 @@ This setting will take effect upon restart.</translation>
     <message>
         <location line="+23"/>
         <source>* %1 spoils something.</source>
-        <translation type="unfinished"></translation>
+        <translation>* %1 spoils something.</translation>
     </message>
     <message>
         <location line="+8"/>
diff --git a/resources/nheko.appdata.xml.in b/resources/nheko.appdata.xml.in
index 69a632a1..77d1c6ad 100644
--- a/resources/nheko.appdata.xml.in
+++ b/resources/nheko.appdata.xml.in
@@ -59,6 +59,13 @@
   <url type="homepage">https://github.com/Nheko-Reborn/nheko</url>
   <update_contact>https://github.com/Nheko-Reborn</update_contact>
   <releases>
+    <release date="2024-06-12" version="0.12.0">
+      <description>
+        <p>This release features a complete port to Qt6, intentional mentions, expiring messages, ignoring users, better sticker and emoji handling and much, much more!</p>
+      </description>
+
+      <url>https://github.com/Nheko-Reborn/nheko/releases/tag/v0.12.0</url>
+    </release>
     <release date="2023-02-23" version="0.11.3"/>
     <release date="2023-02-20" version="0.11.2"/>
     <release date="2023-01-15" version="0.11.1"/>
diff --git a/resources/qml/MessageView.qml b/resources/qml/MessageView.qml
index 5ad414f7..f253b7a8 100644
--- a/resources/qml/MessageView.qml
+++ b/resources/qml/MessageView.qml
@@ -84,6 +84,16 @@ Item {
                 messageContextMenu: messageContextMenuC
                 replyContextMenu: replyContextMenuC
                 scrolledToThis: eventId === room.scrollTarget && (y + height > chat.y + chat.contentY && y < chat.y + chat.height + chat.contentY)
+                data: [
+                    Connections {
+                        function onMovementEnded() {
+                            if (y + height + 2 * chat.spacing > chat.contentY + chat.height && y < chat.contentY + chat.height) {
+                                room.currentIndex = index;
+                            }
+                        }
+                        target: chat
+                    }
+                ]
             }
         }
         Component {
@@ -94,6 +104,16 @@ Item {
                 messageContextMenu: messageContextMenuC
                 replyContextMenu: replyContextMenuC
                 scrolledToThis: eventId === room.scrollTarget && (y + height > chat.y + chat.contentY && y < chat.y + chat.height + chat.contentY)
+                data: [
+                    Connections {
+                        function onMovementEnded() {
+                            if (y + height + 2 * chat.spacing > chat.contentY + chat.height && y < chat.contentY + chat.height) {
+                                room.currentIndex = index;
+                            }
+                        }
+                        target: chat
+                    }
+                ]
             }
         }
 
diff --git a/resources/qml/RoomList.qml b/resources/qml/RoomList.qml
index 350b3846..c82bc43a 100644
--- a/resources/qml/RoomList.qml
+++ b/resources/qml/RoomList.qml
@@ -249,6 +249,8 @@ Page {
                 prompt: qsTr("Enter your status message:")
                 title: qsTr("Status Message")
 
+                text: userInfoGrid.profile ? Presence.userStatus(userInfoGrid.profile.userid) : ""
+
                 onAccepted: function (text) {
                     Nheko.setStatusMessage(text);
                 }
diff --git a/resources/qml/TimelineBubbleMessageStyle.qml b/resources/qml/TimelineBubbleMessageStyle.qml
index 3b0f2d94..dd197264 100644
--- a/resources/qml/TimelineBubbleMessageStyle.qml
+++ b/resources/qml/TimelineBubbleMessageStyle.qml
@@ -45,7 +45,6 @@ TimelineEvent {
     property int avatarMargin: (wrapper.isStateEvent || Settings.smallAvatars ? 0 : (Nheko.avatarSize + 8)) // align bubble with section header
 
     property alias hovered: messageHover.hovered
-    property bool scrolledToThis: false
 
     mainInset: (threadId ? (4 + Nheko.paddingSmall) : 0) + 4
     replyInset: mainInset + 4 + Nheko.paddingSmall
diff --git a/resources/qml/TimelineDefaultMessageStyle.qml b/resources/qml/TimelineDefaultMessageStyle.qml
index 331a5dfe..16db9964 100644
--- a/resources/qml/TimelineDefaultMessageStyle.qml
+++ b/resources/qml/TimelineDefaultMessageStyle.qml
@@ -45,7 +45,6 @@ TimelineEvent {
     property int avatarMargin: (wrapper.isStateEvent || Settings.smallAvatars ? 0 : (Nheko.avatarSize + 8)) // align bubble with section header
 
     property alias hovered: messageHover.hovered
-    property bool scrolledToThis: false
 
     mainInset: (threadId ? (4 + Nheko.paddingSmall) : 0)
     replyInset: mainInset + 4 + Nheko.paddingSmall
@@ -127,7 +126,9 @@ TimelineEvent {
                         to: 0
                     }
                     ScriptAction {
-                        script: wrapper.room.eventShown()
+                        script: {
+                            wrapper.room.eventShown();
+                        }
                     }
                 }
             }
diff --git a/resources/qml/TimelineEvent.qml b/resources/qml/TimelineEvent.qml
index 3cc239c9..ef1a9578 100644
--- a/resources/qml/TimelineEvent.qml
+++ b/resources/qml/TimelineEvent.qml
@@ -13,6 +13,7 @@ EventDelegateChooser {
     id: wrapper
 
     required property bool isStateEvent
+    property bool scrolledToThis: false
 
     // qmllint disable required
     EventDelegateChoice {
diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml
index c29eb537..53b1951c 100644
--- a/resources/qml/TimelineView.qml
+++ b/resources/qml/TimelineView.qml
@@ -27,7 +27,9 @@ Item {
     Keys.onPressed: event => {
         if (event.text && event.key !== Qt.Key_Enter && event.key !== Qt.Key_Return && !topBar.searchHasFocus) {
             TimelineManager.focusMessageInput();
-            room.input.setText(room.input.text + event.text);
+            if (event.modifiers != Qt.ControlModifier) {
+                room.input.setText(room.input.text + event.text);
+            }
         }
     }
     onRoomChanged: if (room != null)
diff --git a/resources/qml/TopBar.qml b/resources/qml/TopBar.qml
index a2bfe414..281f60d5 100644
--- a/resources/qml/TopBar.qml
+++ b/resources/qml/TopBar.qml
@@ -96,7 +96,7 @@ Pane {
                 Layout.column: 1
                 Layout.row: 1
                 Layout.rowSpan: 2
-                displayName: roomName
+                displayName: room ? room.plainRoomName : roomName
                 enabled: false
                 implicitHeight: Nheko.avatarSize
                 implicitWidth: Nheko.avatarSize
diff --git a/resources/qml/dialogs/InputDialog.qml b/resources/qml/dialogs/InputDialog.qml
index bf3cbc9a..c963febb 100644
--- a/resources/qml/dialogs/InputDialog.qml
+++ b/resources/qml/dialogs/InputDialog.qml
@@ -13,6 +13,8 @@ ApplicationWindow {
 
     property alias prompt: promptLabel.text
     property alias echoMode: statusInput.echoMode
+    property alias text: statusInput.text
+
     signal accepted(text: string)
 
     modality: Qt.NonModal
diff --git a/src/ChatPage.cpp b/src/ChatPage.cpp
index be05f8bd..3c663b94 100644
--- a/src/ChatPage.cpp
+++ b/src/ChatPage.cpp
@@ -986,8 +986,15 @@ ChatPage::createRoom(const mtx::requests::CreateRoom &req)
         return;
     }
 
+    bool direct = req.is_direct;
+    std::string direct_user;
+    if (direct && !req.invite.empty())
+        direct_user = req.invite.front();
+
     http::client()->create_room(
-      req, [this](const mtx::responses::CreateRoom &res, mtx::http::RequestErr err) {
+      req,
+      [this, direct, direct_user](const mtx::responses::CreateRoom &res,
+                                  mtx::http::RequestErr err) {
           if (err) {
               const auto err_code = mtx::errors::to_string(err->matrix_error.errcode);
               const auto error    = err->matrix_error.error;
@@ -1000,6 +1007,16 @@ ChatPage::createRoom(const mtx::requests::CreateRoom &req)
           }
 
           QString newRoomId = QString::fromStdString(res.room_id.to_string());
+
+          if (direct && !direct_user.empty()) {
+              utils::markRoomAsDirect(newRoomId,
+                                      {RoomMember{
+                                        .user_id      = QString::fromStdString(direct_user),
+                                        .display_name = "",
+                                        .avatar_url   = "",
+                                      }});
+          }
+
           emit showNotification(tr("Room %1 created.").arg(newRoomId));
           emit newRoom(newRoomId);
           emit changeToRoom(newRoomId);
diff --git a/src/LoginPage.cpp b/src/LoginPage.cpp
index ea295136..9b48730d 100644
--- a/src/LoginPage.cpp
+++ b/src/LoginPage.cpp
@@ -184,13 +184,14 @@ LoginPage::checkHomeserverVersion()
                     "v1.7",
                     "v1.8",
                     "v1.9",
+                    "v1.10",
                   };
                   return supported.count(v) != 0;
               }) == versions.versions.cend()) {
             emit versionErrorCb(
               tr("The selected server does not support a version of the Matrix protocol, that this "
                  "client understands (%1 to %2). You can't sign in.")
-                .arg(u"v1.1", u"v1.9"));
+                .arg(u"v1.1", u"v1.10"));
             return;
         }
 
diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp
index ea9f560d..42a1521d 100644
--- a/src/MainWindow.cpp
+++ b/src/MainWindow.cpp
@@ -60,7 +60,7 @@ MainWindow::MainWindow(QWindow *parent)
 
     connect(chat_page_, &ChatPage::closing, this, [this] { switchToLoginPage(""); });
     connect(chat_page_, &ChatPage::unreadMessages, this, &MainWindow::setWindowTitle);
-    connect(chat_page_, SIGNAL(unreadMessages(int)), trayIcon_, SLOT(setUnreadCount(int)));
+    connect(chat_page_, &ChatPage::unreadMessages, trayIcon_, &TrayIcon::setUnreadCount);
     connect(chat_page_, &ChatPage::showLoginPage, this, &MainWindow::switchToLoginPage);
     connect(chat_page_, &ChatPage::showNotification, this, &MainWindow::showNotification);
 
diff --git a/src/RegisterPage.cpp b/src/RegisterPage.cpp
index 824547bc..93e2cf1b 100644
--- a/src/RegisterPage.cpp
+++ b/src/RegisterPage.cpp
@@ -139,13 +139,14 @@ RegisterPage::versionsCheck()
                     "v1.7",
                     "v1.8",
                     "v1.9",
+                    "v1.10",
                   };
                   return supported.count(v) != 0;
               }) == versions.versions.cend()) {
             emit setHsError(
               tr("The selected server does not support a version of the Matrix protocol that "
                  "this client understands (%1 to %2). You can't register.")
-                .arg(u"v1.1", u"v1.9"));
+                .arg(u"v1.1", u"v1.10"));
             emit hsErrorChanged();
             return;
         }
diff --git a/src/TrayIcon.cpp b/src/TrayIcon.cpp
index c89e69cc..c62fecea 100644
--- a/src/TrayIcon.cpp
+++ b/src/TrayIcon.cpp
@@ -12,9 +12,9 @@
 
 #include "TrayIcon.h"
 
-MsgCountComposedIcon::MsgCountComposedIcon(const QString &filename)
+MsgCountComposedIcon::MsgCountComposedIcon(const QIcon &icon)
   : QIconEngine()
-  , icon_{QIcon{filename}}
+  , icon_{icon}
 {
 }
 
@@ -41,8 +41,8 @@ MsgCountComposedIcon::paint(QPainter *painter,
     brush.setColor(backgroundColor);
 
     QFont f;
-    f.setPointSizeF(8);
-    f.setWeight(QFont::Thin);
+    f.setPixelSize(int(BubbleDiameter * 0.6));
+    f.setWeight(QFont::Bold);
 
     painter->setBrush(brush);
     painter->setPen(Qt::NoPen);
@@ -66,9 +66,6 @@ MsgCountComposedIcon::clone() const
 
 QList<QSize>
 MsgCountComposedIcon::availableSizes(QIcon::Mode mode, QIcon::State state)
-#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
-  const
-#endif
 {
     Q_UNUSED(mode);
     Q_UNUSED(state);
@@ -97,11 +94,12 @@ MsgCountComposedIcon::pixmap(const QSize &size, QIcon::Mode mode, QIcon::State s
 
 TrayIcon::TrayIcon(const QString &filename, QWindow *parent)
   : QSystemTrayIcon(parent)
+  , icon(filename)
 {
 #if defined(Q_OS_MACOS) || defined(Q_OS_WIN)
-    setIcon(QIcon(filename));
+    setIcon(icon);
 #else
-    icon_ = new MsgCountComposedIcon(filename);
+    auto icon_ = new MsgCountComposedIcon(icon);
     setIcon(QIcon(icon_));
 #endif
 
@@ -122,6 +120,15 @@ void
 TrayIcon::setUnreadCount(int count)
 {
     qGuiApp->setBadgeNumber(count);
+
+#if !defined(Q_OS_MACOS) && !defined(Q_OS_WIN)
+    if (count != previousCount) {
+        auto i      = new MsgCountComposedIcon(icon);
+        i->msgCount = count;
+        setIcon(QIcon(i));
+        previousCount = count;
+    }
+#endif
 }
 
 #include "moc_TrayIcon.cpp"
diff --git a/src/TrayIcon.h b/src/TrayIcon.h
index f2009612..7c0bc7b2 100644
--- a/src/TrayIcon.h
+++ b/src/TrayIcon.h
@@ -15,15 +15,11 @@ class QPainter;
 class MsgCountComposedIcon final : public QIconEngine
 {
 public:
-    MsgCountComposedIcon(const QString &filename);
+    MsgCountComposedIcon(const QIcon &icon);
 
     void paint(QPainter *p, const QRect &rect, QIcon::Mode mode, QIcon::State state) override;
     QIconEngine *clone() const override;
-    QList<QSize> availableSizes(QIcon::Mode mode, QIcon::State state)
-#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
-      const
-#endif
-      override;
+    QList<QSize> availableSizes(QIcon::Mode mode, QIcon::State state) override;
     QPixmap pixmap(const QSize &size, QIcon::Mode mode, QIcon::State state) override;
 
     int msgCount = 0;
@@ -47,7 +43,6 @@ private:
     QAction *viewAction_;
     QAction *quitAction_;
 
-#if !defined(Q_OS_MACOS) && !defined(Q_OS_WIN)
-    MsgCountComposedIcon *icon_;
-#endif
+    int previousCount = 0;
+    QIcon icon;
 };
diff --git a/src/UserSettingsPage.cpp b/src/UserSettingsPage.cpp
index f50bc3c0..2172c34c 100644
--- a/src/UserSettingsPage.cpp
+++ b/src/UserSettingsPage.cpp
@@ -1420,7 +1420,8 @@ UserSettingsModel::data(const QModelIndex &index, int role) const
             return tr("Will prevent swipe motions like swiping left/right between Rooms and "
                       "Timeline, or swiping a message to reply.");
         case ScaleFactor:
-            return tr("Change the scale factor of the whole user interface.");
+            return tr("Change the scale factor of the whole user interface. Requires a restart to "
+                      "take effect.");
         case UseStunServer:
             return tr(
               "Will use turn.matrix.org as assist when your home server does not offer one.");
diff --git a/src/UsersModel.cpp b/src/UsersModel.cpp
index a017aa84..61d1bddd 100644
--- a/src/UsersModel.cpp
+++ b/src/UsersModel.cpp
@@ -11,6 +11,7 @@
 #include "CompletionModelRoles.h"
 #include "Logging.h"
 #include "UserSettingsPage.h"
+#include "Utils.h"
 
 UsersModel::UsersModel(const std::string &roomId, QObject *parent)
   : QAbstractListModel(parent)
@@ -24,10 +25,13 @@ UsersModel::UsersModel(const std::string &roomId, QObject *parent)
                   std::get_if<mtx::events::AccountDataEvent<mtx::events::account_data::Direct>>(
                     &e.value())) {
                 for (const auto &[userId, roomIds] : event->content.user_to_rooms) {
+                    if (roomIds.empty())
+                        continue;
+
                     displayNames.push_back(
-                      QString::fromStdString(cache::displayName(roomIds[0], userId)));
+                      QString::fromStdString(cache::displayName(roomIds.at(0), userId)));
                     userids.push_back(QString::fromStdString(userId));
-                    avatarUrls.push_back(cache::avatarUrl(QString::fromStdString(roomIds[0]),
+                    avatarUrls.push_back(cache::avatarUrl(QString::fromStdString(roomIds.at(0)),
                                                           QString::fromStdString(userId)));
                 }
             }
@@ -66,10 +70,7 @@ UsersModel::data(const QModelIndex &index, int role) const
         case CompletionModel::CompletionRole:
             if (UserSettings::instance()->markdown())
                 return QStringLiteral("[%1](https://matrix.to/#/%2)")
-                  .arg(QString(displayNames[index.row()])
-                         .replace("[", "\\[")
-                         .replace("]", "\\]")
-                         .toHtmlEscaped(),
+                  .arg(utils::escapeMentionMarkdown(QString(displayNames[index.row()])),
                        QString(QUrl::toPercentEncoding(userids[index.row()])));
             else
                 return displayNames[index.row()];
diff --git a/src/Utils.cpp b/src/Utils.cpp
index 8b8a11dc..3e7340f4 100644
--- a/src/Utils.cpp
+++ b/src/Utils.cpp
@@ -582,6 +582,34 @@ utils::linkifyMessage(const QString &body)
 }
 
 QString
+utils::escapeMentionMarkdown(QString input)
+{
+    input = input.toHtmlEscaped();
+
+    constexpr std::array<char, 10> markdownChars = {
+      '\\',
+      '`',
+      '*',
+      '_',
+      /*'{', '}',*/ '[',
+      ']',
+      '<',
+      '>',
+      /* '(', ')',  '#', '-', '+', '.', '!', */ '~',
+      '|',
+    };
+
+    QByteArray replacement = "\\\\";
+
+    for (char c : markdownChars) {
+        replacement[1] = c;
+        input.replace(QChar::fromLatin1(c), QLatin1StringView(replacement));
+    }
+
+    return input;
+}
+
+QString
 utils::escapeBlacklistedHtml(const QString &rawStr)
 {
     static const std::set<QByteArray> allowedTags = {
@@ -1139,18 +1167,19 @@ utils::getFormattedQuoteBody(const RelatedInfo &related, const QString &html)
             return QStringLiteral("sent a video");
         }
         default: {
-            return related.quoted_formatted_body;
+            return escapeBlacklistedHtml(related.quoted_formatted_body);
         }
         }
     };
+
     return QStringLiteral("<mx-reply><blockquote><a "
                           "href=\"https://matrix.to/#/%1/%2\">In reply "
                           "to</a> <a href=\"https://matrix.to/#/%3\">%4</a><br"
                           "/>%5</blockquote></mx-reply>")
              .arg(related.room,
                   QString::fromStdString(related.related_event),
-                  related.quoted_user,
-                  related.quoted_user,
+                  QUrl::toPercentEncoding(related.quoted_user),
+                  related.quoted_user.toHtmlEscaped(),
                   getFormattedBody()) +
            html;
 }
diff --git a/src/Utils.h b/src/Utils.h
index 75438a7b..9c5e4713 100644
--- a/src/Utils.h
+++ b/src/Utils.h
@@ -140,6 +140,9 @@ linkifyMessage(const QString &body);
 QString
 markdownToHtml(const QString &text, bool rainbowify = false, bool noExtensions = false);
 
+QString
+escapeMentionMarkdown(QString input);
+
 //! Escape every html tag, that was not whitelisted
 QString
 escapeBlacklistedHtml(const QString &data);
diff --git a/src/main.cpp b/src/main.cpp
index 15519b8a..bdfb84e7 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -21,7 +21,7 @@
 
 // in theory we can enable this everywhere, but the header is missing on some of our CI systems and
 // it is too much effort to install.
-#if defined(Q_OS_UNIX) && !defined(Q_OS_MACOS)
+#if __has_include(<QtGui/qpa/qplatformwindow_p.h>)
 #include <QtGui/qpa/qplatformwindow_p.h>
 #endif
 
@@ -169,7 +169,7 @@ main(int argc, char *argv[])
 
     // this needs to be after setting the application name. Or how would we find our settings
     // file then?
-#if defined(Q_OS_UNIX) && !defined(Q_OS_MACOS)
+#if !defined(Q_OS_MACOS)
     if (qgetenv("QT_SCALE_FACTOR").size() == 0) {
         float factor = utils::scaleFactor();
 
@@ -178,36 +178,16 @@ main(int argc, char *argv[])
     }
 #endif
 
-    // This is some hacky programming, but it's necessary (AFAIK?) to get the unique config name
-    // parsed before the SingleApplication userdata is set.
-    QString userdata{QLatin1String("")};
     QString matrixUri;
     for (int i = 1; i < argc; ++i) {
         QString arg{argv[i]};
-        if (arg.startsWith(QLatin1String("--profile="))) {
-            arg.remove(QStringLiteral("--profile="));
-            userdata = arg;
-        } else if (arg.startsWith(QLatin1String("-p="))) {
-            arg.remove(QStringLiteral("-p="));
-            userdata = arg;
-        } else if (arg == QLatin1String("--profile") || arg == QLatin1String("-p")) {
-            if (i < argc - 1) // if i is less than argc - 1, we still have a parameter
-                              // left to process as the name
-            {
-                ++i; // the next arg is the name, so increment
-                userdata = QString{argv[i]};
-            }
-        } else if (arg.startsWith(QLatin1String("matrix:"))) {
+        if (arg.startsWith(QLatin1String("matrix:"))) {
             matrixUri = arg;
         }
     }
 
     QApplication app(argc, argv);
 
-    KDSingleApplication singleapp(
-      QStringLiteral("im.nheko.nheko-%1")
-        .arg(userdata == QLatin1String("default") ? QLatin1String("") : userdata));
-
     QCommandLineParser parser;
     parser.addHelpOption();
     parser.addVersionOption();
@@ -250,12 +230,27 @@ main(int argc, char *argv[])
     if (parser.isSet(compactDb))
         cache::setNeedsCompactFlag();
 
+    if (parser.isSet(configName))
+        UserSettings::initialize(parser.value(configName));
+    else
+        UserSettings::initialize(std::nullopt);
+
+    auto settings = UserSettings::instance().toWeakRef();
+
+    auto profileName = settings.lock()->profile();
+
+    KDSingleApplication singleapp(
+      QStringLiteral("im.nheko.nheko-%1")
+        .arg(profileName == QLatin1String("default") ? QLatin1String("") : profileName));
+
     // This check needs to happen _after_ process(), so that we actually print help for --help when
     // Nheko is already running.
     if (!singleapp.isPrimaryInstance()) {
         auto token = qgetenv("XDG_ACTIVATION_TOKEN");
 
-#if defined(Q_OS_UNIX) && !defined(Q_OS_MACOS)
+#if __has_include(<QtGui/qpa/qplatformwindow_p.h>) && \
+        ((QT_VERSION >= QT_VERSION_CHECK(6, 7, 0) &&  QT_CONFIG(wayland)) || \
+         (QT_VERSION < QT_VERSION_CHECK(6, 7, 0) && defined(Q_OS_UNIX) && !defined(Q_OS_MACOS)))
         // getting a valid activation token on wayland is a bit of a pain, it works most reliably
         // when you have an actual window, that has the focus...
         auto waylandApp = app.nativeInterface<QNativeInterface::QWaylandApplication>();
@@ -370,13 +365,6 @@ main(int argc, char *argv[])
     auto filter = new NhekoFixupPaletteEventFilter(&app);
     app.installEventFilter(filter);
 
-    if (parser.isSet(configName))
-        UserSettings::initialize(parser.value(configName));
-    else
-        UserSettings::initialize(std::nullopt);
-
-    auto settings = UserSettings::instance().toWeakRef();
-
     QFont font;
     QString userFontFamily = settings.lock()->font();
     if (!userFontFamily.isEmpty() && userFontFamily != QLatin1String("default")) {
diff --git a/src/timeline/InputBar.cpp b/src/timeline/InputBar.cpp
index ac1d47a2..c7687ab6 100644
--- a/src/timeline/InputBar.cpp
+++ b/src/timeline/InputBar.cpp
@@ -187,9 +187,6 @@ InputBar::addMention(QString mention, QString text)
         mentionTexts_.push_back(text);
 
         emit mentionsChanged();
-        if (mention == u"@room") {
-            this->containsAtRoom_ = true;
-        }
     }
 }
 
@@ -200,9 +197,6 @@ InputBar::removeMention(QString mention)
         mentions_.removeAt(idx);
         mentionTexts_.removeAt(idx);
         emit mentionsChanged();
-        if (mention == u"@room") {
-            this->containsAtRoom_ = false;
-        }
     }
 }
 
@@ -244,6 +238,7 @@ InputBar::updateTextContentProperties(const QString &t, bool charDeleted)
     auto roomMention = containsRoomMention(t) && this->room->permissions()->canPingRoom();
 
     if (roomMention != this->containsAtRoom_) {
+        this->containsAtRoom_ = roomMention;
         if (roomMention)
             addMention(QStringLiteral(u"@room"), QStringLiteral(u"@room"));
         else
@@ -462,8 +457,7 @@ replaceMatrixToMarkdownLink(QString input)
         int newline   = input.indexOf('\n', endOfName);
         if (endOfLink > endOfName && (newline == -1 || endOfLink < newline)) {
             auto name = input.mid(startOfName + 1, endOfName - startOfName - 1);
-            name.replace("\\[", "[");
-            name.replace("\\]", "]");
+            name.remove(QChar(u'\\'), Qt::CaseSensitive);
             input.replace(startOfName, endOfLink - startOfName + 1, name);
             replaced = true;
         }
@@ -501,8 +495,11 @@ mtx::common::Mentions
 InputBar::generateMentions()
 {
     std::vector<std::string> userMentions;
+    bool atRoom = false;
     for (const auto &m : mentions_)
-        if (m != u"@room")
+        if (m == u"@room")
+            atRoom = true;
+        else
             userMentions.push_back(m.toStdString());
 
     if (!room->reply().isEmpty()) {
@@ -516,12 +513,14 @@ InputBar::generateMentions()
 
     auto mention = mtx::common::Mentions{
       .user_ids = userMentions,
-      .room     = containsAtRoom_,
+      // We use the atRoom from the mentions list to allow suppressing a room mention
+      .room = atRoom,
     };
 
     // this->containsAtRoom_ = false;
     // this->mentions_.clear();
     // this->mentionTexts_.clear();
+
     return mention;
 }
 
@@ -1031,9 +1030,9 @@ InputBar::command(const QString &command, QString args)
     } else if (command == QLatin1String("converttoroom")) {
         utils::removeDirectFromRoom(this->room->roomId());
     } else if (command == QLatin1String("ignore")) {
-        this->toggleIgnore(args, true);
+        this->toggleIgnore(args.trimmed(), true);
     } else if (command == QLatin1String("unignore")) {
-        this->toggleIgnore(args, false);
+        this->toggleIgnore(args.trimmed(), false);
     } else {
         return false;
     }
@@ -1044,6 +1043,13 @@ InputBar::command(const QString &command, QString args)
 void
 InputBar::toggleIgnore(const QString &user, const bool ignored)
 {
+    if (!user.startsWith(u"@")) {
+        MainWindow::instance()->showNotification(
+          tr("You need to pass a valid mxid when ignoring a user. '%1' is not a valid userid.")
+            .arg(user));
+        return;
+    }
+
     UserProfile *profile = new UserProfile(QString(), user, TimelineViewManager::instance());
     connect(profile, &UserProfile::failedToFetchProfile, [user, profile] {
         MainWindow::instance()->showNotification(tr("Failed to fetch user %1").arg(user));
diff --git a/src/timeline/InputBar.h b/src/timeline/InputBar.h
index c38de662..5e885d4f 100644
--- a/src/timeline/InputBar.h
+++ b/src/timeline/InputBar.h
@@ -194,15 +194,17 @@ public slots:
 
     void storeForEdit()
     {
-        textBeforeEdit     = text();
-        mentionsBefore     = mentions_;
-        mentionTextsBefore = mentionTexts_;
+        textBeforeEdit       = text();
+        mentionsBefore       = mentions_;
+        mentionTextsBefore   = mentionTexts_;
+        containsAtRoomBefore = containsAtRoom_;
         emit mentionsChanged();
     }
     void restoreAfterEdit()
     {
-        mentions_     = mentionsBefore;
-        mentionTexts_ = mentionTextsBefore;
+        mentions_       = mentionsBefore;
+        mentionTexts_   = mentionTextsBefore;
+        containsAtRoom_ = containsAtRoomBefore;
         mentionsBefore.clear();
         mentionTextsBefore.clear();
         setText(textBeforeEdit);
@@ -216,6 +218,9 @@ public slots:
 
         mentions_     = newMentions;
         mentionTexts_ = newMentionTexts;
+
+        this->containsAtRoom_ = newMentions.contains(u"@room");
+
         emit mentionsChanged();
     }
 
@@ -331,6 +336,7 @@ private:
     // store stuff during edits
     QStringList mentionsBefore, mentionTextsBefore;
     QString textBeforeEdit;
+    bool containsAtRoomBefore = false;
 
     using UploadHandle = std::unique_ptr<MediaUpload, DeleteLaterDeleter>;
     std::vector<UploadHandle> unconfirmedUploads;
diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp
index 03606d90..48b58ee5 100644
--- a/src/timeline/TimelineModel.cpp
+++ b/src/timeline/TimelineModel.cpp
@@ -736,7 +736,9 @@ TimelineModel::data(const mtx::events::collections::TimelineEvents &event, int r
                             .arg(displayName(QString::fromStdString(e.sender)))
                             .arg(QStringLiteral("<img height=\"32\" src=\"%1\">")
                                    .arg(QUrl::toPercentEncoding(
-                                     QString::fromStdString(e.content.url))));
+                                     QString::fromStdString(e.content.url)
+                                       .replace("mxc://", "image://MxcImage/"),
+                                     ":/")));
                       else
                           return tr("%1 removed the room avatar.")
                             .arg(displayName(QString::fromStdString(e.sender)));
@@ -2244,8 +2246,9 @@ TimelineModel::getRoomVias(const QString &roomId)
 void
 TimelineModel::copyLinkToEvent(const QString &eventId) const
 {
-    auto link = QStringLiteral("%1/%2?%3")
-                  .arg(getBareRoomLink(room_id_),
+    // Event links shouldn't use an alias, since that can be repointed.
+    auto link = QStringLiteral("https://matrix.to/#/%1/%2?%3")
+                  .arg(QUrl::toPercentEncoding(room_id_),
                        QString(QUrl::toPercentEncoding(eventId)),
                        getRoomVias(room_id_));
     QGuiApplication::clipboard()->setText(link);
@@ -3080,8 +3083,6 @@ TimelineModel::setEdit(const QString &newEdit)
         input()->storeForEdit();
     }
 
-    auto quoted = [](QString in) { return in.replace("[", "\\[").replace("]", "\\]"); };
-
     if (edit_ != newEdit) {
         auto ev = events.get(newEdit.toStdString(), "");
         if (ev && mtx::accessors::sender(*ev) == http::client()->user_id().to_string()) {
@@ -3100,10 +3101,12 @@ TimelineModel::setEdit(const QString &newEdit)
                 for (const auto &user : mentionsList->user_ids) {
                     auto userid = QString::fromStdString(user);
                     mentions.append(userid);
-                    mentionTexts.append(
-                      QStringLiteral("[%1](https://matrix.to/#/%2)")
-                        .arg(displayName(userid).replace("[", "\\[").replace("]", "\\]"),
-                             QString(QUrl::toPercentEncoding(userid))));
+                    mentionTexts.append(QStringLiteral("[%1](https://matrix.to/#/%2)")
+                                          .arg(utils::escapeMentionMarkdown(
+                                                 // not using TimelineModel::displayName here,
+                                                 // because it would double html escape
+                                                 cache::displayName(room_id_, userid)),
+                                               QString(QUrl::toPercentEncoding(userid))));
                 }
             }
 
@@ -3127,7 +3130,9 @@ TimelineModel::setEdit(const QString &newEdit)
 
                     for (const auto &[user, link] : reverseNameMapping) {
                         // TODO(Nico): html unescape the user name
-                        editText.replace(user, QStringLiteral("[%1](%2)").arg(quoted(user), link));
+                        editText.replace(
+                          user,
+                          QStringLiteral("[%1](%2)").arg(utils::escapeMentionMarkdown(user), link));
                     }
                 }
 
@@ -3275,8 +3280,8 @@ TimelineModel::widgetLinks() const
     QStringList list;
 
     auto user = utils::localUser();
-    auto av   = QUrl::toPercentEncoding(
-      QString::fromStdString(http::client()->mxc_to_download_url(avatarUrl(user).toStdString())));
+    // auto av   = QUrl::toPercentEncoding(
+    //   QString::fromStdString(http::client()->mxc_to_download_url(avatarUrl(user).toStdString())));
     auto disp  = QUrl::toPercentEncoding(displayName(user));
     auto theme = UserSettings::instance()->theme();
     if (theme == QStringLiteral("system"))
@@ -3297,7 +3302,7 @@ TimelineModel::widgetLinks() const
         url.replace("$matrix_user_id", user);
         url.replace("$matrix_room_id", QUrl::toPercentEncoding(room_id_));
         url.replace("$matrix_display_name", disp);
-        url.replace("$matrix_avatar_url", av);
+        // url.replace("$matrix_avatar_url", av);
 
         url.replace("$matrix_widget_id",
                     QUrl::toPercentEncoding(QString::fromStdString(p.content.id)));
diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp
index 479fe7e1..1b2635b0 100644
--- a/src/timeline/TimelineViewManager.cpp
+++ b/src/timeline/TimelineViewManager.cpp
@@ -232,6 +232,9 @@ TimelineViewManager::openRoomSettings(QString room_id)
 void
 TimelineViewManager::openInviteUsers(QString roomId)
 {
+    if (!roomId.startsWith('!'))
+        return;
+
     InviteesModel *model = new InviteesModel{rooms_->getRoomById(roomId).data()};
     connect(model, &InviteesModel::accept, this, [this, model, roomId]() {
         emit inviteUsers(roomId, model->mxids());
@@ -243,6 +246,9 @@ TimelineViewManager::openInviteUsers(QString roomId)
 void
 TimelineViewManager::openGlobalUserProfile(QString userId)
 {
+    if (!userId.startsWith('@'))
+        return;
+
     UserProfile *profile = new UserProfile{QString{}, userId, this};
     QQmlEngine::setObjectOwnership(profile, QQmlEngine::JavaScriptOwnership);
     emit openProfile(profile);
@@ -615,11 +621,14 @@ TimelineViewManager::forwardMessageToRoom(mtx::events::collections::TimelineEven
 
 //! WORKAROUND(Nico): for https://bugreports.qt.io/browse/QTBUG-93281
 void
-TimelineViewManager::fixImageRendering(QQuickTextDocument *t, QQuickItem *i)
+TimelineViewManager::fixImageRendering([[maybe_unused]] QQuickTextDocument *t,
+                                       [[maybe_unused]] QQuickItem *i)
 {
+#if QT_VERSION < QT_VERSION_CHECK(6, 7, 0)
     if (t) {
         QObject::connect(t->textDocument(), SIGNAL(imagesLoaded()), i, SLOT(updateWholeDocument()));
     }
+#endif
 }
 
 using IgnoredUsers = mtx::events::EphemeralEvent<mtx::events::account_data::IgnoredUsers>;
diff --git a/src/ui/NhekoGlobalObject.cpp b/src/ui/NhekoGlobalObject.cpp
index 138b4283..e28b1bc1 100644
--- a/src/ui/NhekoGlobalObject.cpp
+++ b/src/ui/NhekoGlobalObject.cpp
@@ -19,7 +19,7 @@
 #include "UserSettingsPage.h"
 #include "Utils.h"
 
-#if XCB_AVAILABLE
+#if XCB_AVAILABLE && QT_CONFIG(xcb)
 #include <xcb/xproto.h>
 #endif
 
@@ -186,7 +186,7 @@ Nheko::createRoom(bool space,
 void
 Nheko::setWindowRole([[maybe_unused]] QWindow *win, [[maybe_unused]] QString newRole) const
 {
-#if XCB_AVAILABLE
+#if XCB_AVAILABLE && QT_CONFIG(xcb)
     const QNativeInterface::QX11Application *x11Interface =
       qGuiApp->nativeInterface<QNativeInterface::QX11Application>();
 
diff --git a/src/voip/WebRTCSession.cpp b/src/voip/WebRTCSession.cpp
index ba1d5424..d55b7c41 100644
--- a/src/voip/WebRTCSession.cpp
+++ b/src/voip/WebRTCSession.cpp
@@ -339,21 +339,16 @@ newVideoSinkChain(GstElement *pipe)
     g_object_set(compositor, "background", 1, nullptr);
     switch (graphicsApi) {
     case QSGRendererInterface::OpenGL: {
-        GstElement *glupload       = gst_element_factory_make("glupload", nullptr);
-        GstElement *glcolorconvert = gst_element_factory_make("glcolorconvert", nullptr);
-        GstElement *qmlglsink      = gst_element_factory_make("qml6glsink", nullptr);
-        GstElement *glsinkbin      = gst_element_factory_make("glsinkbin", nullptr);
+        GstElement *qmlglsink = gst_element_factory_make("qml6glsink", nullptr);
+        GstElement *glsinkbin = gst_element_factory_make("glsinkbin", nullptr);
 
         g_object_set(qmlglsink, "widget", WebRTCSession::instance().getVideoItem(), nullptr);
         g_object_set(glsinkbin, "sink", qmlglsink, nullptr);
-        gst_bin_add_many(
-          GST_BIN(pipe), queue, compositor, glupload, glcolorconvert, glsinkbin, nullptr);
-        gst_element_link_many(queue, compositor, glupload, glcolorconvert, glsinkbin, nullptr);
+        gst_bin_add_many(GST_BIN(pipe), queue, compositor, glsinkbin, nullptr);
+        gst_element_link_many(queue, compositor, glsinkbin, nullptr);
 
         gst_element_sync_state_with_parent(queue);
         gst_element_sync_state_with_parent(compositor);
-        gst_element_sync_state_with_parent(glupload);
-        gst_element_sync_state_with_parent(glcolorconvert);
         gst_element_sync_state_with_parent(glsinkbin);
 
         // to propagate context (hopefully)