Compare commits

...

13 commits

Author SHA1 Message Date
lymkwi
e6153b03d4
Merge 50cdcc0376 into 1a00d91316 2024-11-05 21:37:06 +02:00
DeepBlueV7.X
1a00d91316
Merge pull request #1833 from Integral-Tech/refactor-string-conversion
refactor: use fmt lib to avoid back-and-forth conversion
2024-10-30 01:10:07 +00:00
Integral
b7a5d714c6
refactor: use fmt lib to avoid back-and-forth conversion 2024-10-19 16:49:12 +08:00
DeepBlueV7.X
2f967978f2
Merge pull request #1825 from Integral-Tech/fix-tooltip
Add profile name to tooltip & fix message count
2024-10-13 22:19:09 +00:00
Integral
3b0df06629
Add profile name to tooltip & fix message count 2024-10-13 20:55:58 +08:00
Nicolas Werner
27683bedc4
Fix media deletion of animated files 2024-10-09 03:39:19 +02:00
Nicolas Werner
80a39cca17
Disable http3 support by default and warn if users enable it 2024-10-08 23:49:29 +02:00
Nicolas Werner
5523460f4e
Fix menu positions 2024-10-08 23:35:13 +02:00
Nicolas Werner
65c6e96e24
Get rid of platform dialogs/menus now that Qt6.8 supports native menus without them
This will look bad on some platforms and older versions for now, but
should fix a lot of crashes and we can report the rest as bugs.
2024-10-08 23:04:41 +02:00
Nicolas Werner
3a3c3def7c
Bump qt version in apple silicon build 2024-10-08 22:37:00 +02:00
Nicolas Werner
da2d7861d7
Move more templates out of the cache private header 2024-10-08 20:18:47 +02:00
Nicolas Werner
db68281a28
Limit status messages to 255 bytes 2024-10-08 16:55:07 +02:00
lymkwi
50cdcc0376
Implement attachment captioning
Starting with Client-Server API v1.10 [0], the `body` field in messages of type `m.image`,
`m.audio`, `m.video` and `m.file` can be used as the caption of the attachment. This is fact the way
that Nheko rends captions on images, for example.

This commit introduces a field in the `UploadHandle`s awaiting upload on the timeline's `InputBar`
which holds a caption taken from the input text area. The decision is as follows:
 - If text bar is empty or full of blanks, send all media with no caption
 - If the text is an incomplete command, fail
 - If there are no pending uploads, proceed as done previously (if there is no command recognized
   send the text, or try and execute the command and if it fails send the text)
 - If there are pending uploads, only accept uploads if nothing resembling a command name is in the
   text area. That text becomes the caption for all pending media. Otherwise, try and execute the
   command, and, if it fails, send it as text.

While this workflow for captioning so far is a bit jank, it is the least effort implementation.

Links:
[0]: https://spec.matrix.org/v1.10/client-server-api/#mimage

Signed-off-by: lymkwi <lymkwi@vulpinecitrus.info>
2024-05-01 13:02:47 +02:00
20 changed files with 757 additions and 605 deletions

View file

@ -151,7 +151,7 @@ build-clazy:
- apt-get -y install --no-install-suggests --no-install-recommends ca-certificates build-essential ninja-build cmake gcc make automake ccache liblmdb-dev
libssl-dev libqt5multimedia5-plugins libqt5multimediagsttools5 libqt5multimediaquick5 libqt5svg5-dev
qtmultimedia5-dev qtquickcontrols2-5-dev qttools5-dev qttools5-dev-tools qtdeclarative5-dev
qml-module-qtmultimedia qml-module-qtquick-controls2 qml-module-qtquick-layouts qml-module-qt-labs-platform
qml-module-qtmultimedia qml-module-qtquick-controls2 qml-module-qtquick-layouts
qt5keychain-dev ccache libcurl4-openssl-dev libevent-dev libspdlog-dev git nlohmann-json3-dev libcmark-dev asciidoc time # libolm-dev
# need recommended deps for wget
- apt-get -y install wget
@ -282,7 +282,7 @@ build-macos-as:
- pipx ensurepath
- . ~/.zshrc
- mkdir $HOME/Qt
- aqt install-qt --outputdir $HOME/qt mac desktop 6.6 clang_64 -m qtlocation qtimageformats qtmultimedia qtpositioning qtshadertools
- aqt install-qt --outputdir $HOME/qt mac desktop 6.8 clang_64 -m qtlocation qtimageformats qtmultimedia qtpositioning qtshadertools
script:
- export QTPATH=($HOME/qt/6.*/macos/bin)
- export PATH="$QTPATH:${PATH}"
@ -394,7 +394,7 @@ build-flatpak:
- apt-get -y install --no-install-suggests --no-install-recommends ca-certificates build-essential ninja-build cmake gcc make automake ccache liblmdb-dev
libssl-dev libqt5multimedia5-plugins libqt5multimediagsttools5 libqt5multimediaquick5 libqt5svg5-dev
qtmultimedia5-dev qtquickcontrols2-5-dev qttools5-dev qttools5-dev-tools qtdeclarative5-dev
qml-module-qtmultimedia qml-module-qtquick-controls2 qml-module-qtquick-layouts qml-module-qt-labs-platform
qml-module-qtmultimedia qml-module-qtquick-controls2 qml-module-qtquick-layouts
qt5keychain-dev ccache libcurl4-openssl-dev libevent-dev libspdlog-dev nlohmann-json3-dev libcmark-dev asciidoc libre2-dev libgtest-dev libgl1-mesa-dev qml-module-qtquick-particles2
# Installing the packages needed to build AppImage

View file

@ -365,7 +365,7 @@ 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 libqt6svg5-dev qt6keychain-dev qml-module-qt{gstreamer,multimedia,quick-extras,-labs-settings,-labs-platform,graphicaleffects,quick-controls2,quick-particles2} \
qt{base,declarative,tools,multimedia,quickcontrols2-}5-dev libqt6svg5-dev qt6keychain-dev qml-module-qt{gstreamer,multimedia,quick-extras,-labs-settings,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.

View file

@ -3,7 +3,6 @@
// SPDX-License-Identifier: GPL-3.0-or-later
import "./components"
import Qt.labs.platform 1.1 as Platform
import QtQml
import QtQuick
import QtQuick.Controls
@ -101,17 +100,18 @@ Page {
]
onClicked: Communities.setCurrentTagId(model.id)
onPressAndHold: communityContextMenu.show(model.id, model.hidden, model.muted)
onPressAndHold: communityContextMenu.show(communityItem, model.id, model.hidden, model.muted)
Item {
anchors.fill: parent
TapHandler {
id: rth
acceptedButtons: Qt.RightButton
acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus | PointerDevice.TouchPad
gesturePolicy: TapHandler.ReleaseWithinBounds
onSingleTapped: communityContextMenu.show(model.id, model.hidden, model.muted)
onSingleTapped: communityContextMenu.show(rth, model.id, model.hidden, model.muted)
}
}
RowLayout {
@ -195,28 +195,28 @@ Page {
}
}
Platform.Menu {
Menu {
id: communityContextMenu
property bool hidden
property bool muted
property string tagId
function show(id_, hidden_, muted_) {
function show(parent, id_, hidden_, muted_) {
tagId = id_;
hidden = hidden_;
muted = muted_;
open();
popup(parent);
}
Platform.MenuItem {
MenuItem {
checkable: true
checked: communityContextMenu.muted
text: qsTr("Do not show notification counts for this community or tag.")
onTriggered: Communities.toggleTagMute(communityContextMenu.tagId)
}
Platform.MenuItem {
MenuItem {
checkable: true
checked: communityContextMenu.hidden
text: qsTr("Hide rooms with this tag or from this community by default.")

View file

@ -4,7 +4,6 @@
import "./ui"
import "./dialogs"
import Qt.labs.platform 1.1 as Platform
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.2
@ -393,7 +392,7 @@ Item {
}
}
}
Platform.Menu {
Menu {
id: messageContextMenuC
property string eventId
@ -421,9 +420,9 @@ Item {
else
link = "";
if (showAt_)
open(showAt_);
popup(showAt_);
else
open();
popup();
}
Component {
@ -448,7 +447,7 @@ Item {
ReportMessage {}
}
Platform.MenuItem {
MenuItem {
enabled: visible
text: qsTr("Go to &message")
visible: filteredTimeline.filterByContent
@ -458,21 +457,21 @@ Item {
room.showEvent(messageContextMenuC.eventId);
}
}
Platform.MenuItem {
MenuItem {
enabled: visible
text: qsTr("&Copy")
visible: messageContextMenuC.text
onTriggered: Clipboard.text = messageContextMenuC.text
}
Platform.MenuItem {
MenuItem {
enabled: visible
text: qsTr("Copy &link location")
visible: messageContextMenuC.link
onTriggered: Clipboard.text = messageContextMenuC.link
}
Platform.MenuItem {
MenuItem {
id: reactionOption
text: qsTr("Re&act")
@ -483,39 +482,39 @@ Item {
TimelineManager.focusMessageInput();
})
}
Platform.MenuItem {
MenuItem {
text: qsTr("Repl&y")
visible: room ? room.permissions.canSend(MtxEvent.TextMessage) : false
onTriggered: room.reply = (messageContextMenuC.eventId)
}
Platform.MenuItem {
MenuItem {
enabled: visible
text: qsTr("&Edit")
visible: messageContextMenuC.isEditable && (room ? room.permissions.canSend(MtxEvent.TextMessage) : false)
onTriggered: room.edit = (messageContextMenuC.eventId)
}
Platform.MenuItem {
MenuItem {
enabled: visible
text: qsTr("&Thread")
visible: (room ? room.permissions.canSend(MtxEvent.TextMessage) : false)
onTriggered: room.thread = (messageContextMenuC.threadId || messageContextMenuC.eventId)
}
Platform.MenuItem {
MenuItem {
enabled: visible
text: visible && room.pinnedMessages.includes(messageContextMenuC.eventId) ? qsTr("Un&pin") : qsTr("&Pin")
visible: (room ? room.permissions.canChange(MtxEvent.PinnedEvents) : false)
onTriggered: visible && room.pinnedMessages.includes(messageContextMenuC.eventId) ? room.unpin(messageContextMenuC.eventId) : room.pin(messageContextMenuC.eventId)
}
Platform.MenuItem {
MenuItem {
text: qsTr("&Read receipts")
onTriggered: room.showReadReceipts(messageContextMenuC.eventId)
}
Platform.MenuItem {
MenuItem {
text: qsTr("&Forward")
visible: messageContextMenuC.eventType == MtxEvent.ImageMessage || messageContextMenuC.eventType == MtxEvent.VideoMessage || messageContextMenuC.eventType == MtxEvent.AudioMessage || messageContextMenuC.eventType == MtxEvent.FileMessage || messageContextMenuC.eventType == MtxEvent.Sticker || messageContextMenuC.eventType == MtxEvent.TextMessage || messageContextMenuC.eventType == MtxEvent.LocationMessage || messageContextMenuC.eventType == MtxEvent.EmoteMessage || messageContextMenuC.eventType == MtxEvent.NoticeMessage
@ -526,15 +525,15 @@ Item {
timelineRoot.destroyOnClose(forwardMess);
}
}
Platform.MenuItem {
MenuItem {
text: qsTr("&Mark as read")
}
Platform.MenuItem {
MenuItem {
text: qsTr("View raw message")
onTriggered: room.viewRawMessage(messageContextMenuC.eventId)
}
Platform.MenuItem {
MenuItem {
enabled: visible
text: qsTr("View decrypted raw message")
// TODO(Nico): Fix this still being iterated over, when using keyboard to select options
@ -542,7 +541,7 @@ Item {
onTriggered: room.viewDecryptedRawMessage(messageContextMenuC.eventId)
}
Platform.MenuItem {
MenuItem {
text: qsTr("Remo&ve message")
visible: (room ? room.permissions.canRedact() : false) || messageContextMenuC.isSender
@ -554,7 +553,7 @@ Item {
timelineRoot.destroyOnClose(dialog);
}
}
Platform.MenuItem {
MenuItem {
text: qsTr("Report message")
enabled: visible
onTriggered: function () {
@ -564,21 +563,21 @@ Item {
timelineRoot.destroyOnClose(dialog);
}
}
Platform.MenuItem {
MenuItem {
enabled: visible
text: qsTr("&Save as")
visible: messageContextMenuC.eventType == MtxEvent.ImageMessage || messageContextMenuC.eventType == MtxEvent.VideoMessage || messageContextMenuC.eventType == MtxEvent.AudioMessage || messageContextMenuC.eventType == MtxEvent.FileMessage || messageContextMenuC.eventType == MtxEvent.Sticker
onTriggered: room.saveMedia(messageContextMenuC.eventId)
}
Platform.MenuItem {
MenuItem {
enabled: visible
text: qsTr("&Open in external program")
visible: messageContextMenuC.eventType == MtxEvent.ImageMessage || messageContextMenuC.eventType == MtxEvent.VideoMessage || messageContextMenuC.eventType == MtxEvent.AudioMessage || messageContextMenuC.eventType == MtxEvent.FileMessage || messageContextMenuC.eventType == MtxEvent.Sticker
onTriggered: room.openMedia(messageContextMenuC.eventId)
}
Platform.MenuItem {
MenuItem {
enabled: visible
text: qsTr("Copy link to eve&nt")
visible: messageContextMenuC.eventId
@ -592,7 +591,7 @@ Item {
ForwardCompleter {
}
}
Platform.Menu {
Menu {
id: replyContextMenuC
property string eventId
@ -606,21 +605,21 @@ Item {
open();
}
Platform.MenuItem {
MenuItem {
enabled: visible
text: qsTr("&Copy")
visible: replyContextMenuC.text
onTriggered: Clipboard.text = replyContextMenuC.text
}
Platform.MenuItem {
MenuItem {
enabled: visible
text: qsTr("Copy &link location")
visible: replyContextMenuC.link
onTriggered: Clipboard.text = replyContextMenuC.link
}
Platform.MenuItem {
MenuItem {
enabled: visible
text: qsTr("&Go to quoted message")
visible: true

View file

@ -5,8 +5,6 @@
import "./components"
import "./dialogs"
import "./ui"
import Qt.labs.platform 1.1 as Platform
import QtQml
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
@ -43,6 +41,8 @@ Page {
id: buttonRow
ImageButton {
id: startChatButton
Layout.fillWidth: true
Layout.margins: Nheko.paddingMedium
ToolTip.delay: Nheko.tooltipDelay
@ -53,17 +53,17 @@ Page {
hoverEnabled: true
image: ":/icons/icons/ui/add-square-button.svg"
onClicked: roomJoinCreateMenu.open(parent)
onClicked: roomJoinCreateMenu.popup(startChatButton)
Platform.Menu {
Menu {
id: roomJoinCreateMenu
Platform.MenuItem {
MenuItem {
text: qsTr("Join a room")
onTriggered: Nheko.openJoinRoomDialog()
}
Platform.MenuItem {
MenuItem {
text: qsTr("Create a new room")
onTriggered: {
@ -72,7 +72,7 @@ Page {
timelineRoot.destroyOnClose(createRoom);
}
}
Platform.MenuItem {
MenuItem {
text: qsTr("Start a direct chat")
onTriggered: {
@ -81,7 +81,7 @@ Page {
timelineRoot.destroyOnClose(createDirect);
}
}
Platform.MenuItem {
MenuItem {
text: qsTr("Create a new community")
onTriggered: {
@ -255,68 +255,72 @@ Page {
Nheko.setStatusMessage(text);
}
}
Platform.Menu {
Menu {
id: userInfoMenu
Platform.MenuItem {
MenuItem {
text: qsTr("Profile settings")
onTriggered: userInfoPanel.openUserProfile()
}
Platform.MenuItem {
MenuItem {
text: qsTr("Set status message")
onTriggered: statusDialog.show()
}
Platform.MenuSeparator {
MenuSeparator {
}
Platform.MenuItemGroup {
ButtonGroup {
id: onlineStateGroup
}
Platform.MenuItem {
MenuItem {
text: qsTr("Automatic online status")
group: onlineStateGroup
ButtonGroup.group: onlineStateGroup
checkable: true
checked: Settings.presence == Settings.AutomaticPresence
onTriggered: if (checked) Settings.presence = Settings.AutomaticPresence
}
Platform.MenuItem {
MenuItem {
text: qsTr("Online")
group: onlineStateGroup
ButtonGroup.group: onlineStateGroup
checkable: true
checked: Settings.presence == Settings.Online
onTriggered: if (checked) Settings.presence = Settings.Online
}
Platform.MenuItem {
MenuItem {
text: qsTr("Unavailable")
group: onlineStateGroup
ButtonGroup.group: onlineStateGroup
checkable: true
checked: Settings.presence == Settings.Unavailable
onTriggered: if (checked) Settings.presence = Settings.Unavailable
}
Platform.MenuItem {
MenuItem {
text: qsTr("Offline")
group: onlineStateGroup
ButtonGroup.group: onlineStateGroup
checkable: true
checked: Settings.presence == Settings.Offline
onTriggered: if (checked) Settings.presence = Settings.Offline
}
}
TapHandler {
id: userTapHandler
acceptedButtons: Qt.LeftButton
gesturePolicy: TapHandler.ReleaseWithinBounds
margin: -Nheko.paddingSmall
onLongPressed: userInfoMenu.open()
onLongPressed: userInfoMenu.popup(userTapHandler)
onSingleTapped: userInfoPanel.openUserProfile()
}
TapHandler {
id: userTapHandler2
acceptedButtons: Qt.RightButton
gesturePolicy: TapHandler.ReleaseWithinBounds
margin: -Nheko.paddingSmall
onSingleTapped: userInfoMenu.open()
onSingleTapped: userInfoMenu.popup(userTapHandler2)
}
}
Rectangle {
@ -525,7 +529,7 @@ Page {
}
onPressAndHold: {
if (!isInvite)
roomContextMenu.show(roomId, tags);
roomContextMenu.show(roomItem, roomId, tags);
}
Ripple {
@ -538,13 +542,15 @@ Page {
anchors.margins: 1
TapHandler {
id: roomItemTh
acceptedButtons: Qt.RightButton
acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus | PointerDevice.TouchPad
gesturePolicy: TapHandler.ReleaseWithinBounds
onSingleTapped: {
if (!TimelineManager.isInvite)
roomContextMenu.show(roomId, tags);
roomContextMenu.show(roomItemTh, roomId, tags);
}
}
}
@ -734,16 +740,16 @@ Page {
roomid: roomContextMenu.roomid
}
}
Platform.Menu {
Menu {
id: roomContextMenu
property string roomid
property var tags
function show(roomid_, tags_) {
function show(parent, roomid_, tags_) {
roomid = roomid_;
tags = tags_;
open();
popup(parent);
}
InputDialog {
@ -756,7 +762,7 @@ Page {
Rooms.toggleTag(roomContextMenu.roomid, "u." + text, true);
}
}
Platform.MenuItem {
MenuItem {
text: qsTr("Open separately")
onTriggered: {
@ -768,27 +774,27 @@ Page {
destroyOnClose(roomWindow);
}
}
Platform.MenuItem {
MenuItem {
text: qsTr("Mark as read")
onTriggered: Rooms.getRoomById(roomContextMenu.roomid).markRoomAsRead()
}
Platform.MenuItem {
MenuItem {
text: qsTr("Room settings")
onTriggered: TimelineManager.openRoomSettings(roomContextMenu.roomid)
}
Platform.MenuItem {
MenuItem {
text: qsTr("Leave room")
onTriggered: TimelineManager.openLeaveRoomDialog(roomContextMenu.roomid)
}
Platform.MenuItem {
MenuItem {
text: qsTr("Copy room link")
onTriggered: Rooms.copyLink(roomContextMenu.roomid)
}
Platform.Menu {
Menu {
id: tagsMenu
title: qsTr("Tag room as:")
@ -796,7 +802,7 @@ Page {
Instantiator {
model: Communities.tagsWithDefault
delegate: Platform.MenuItem {
delegate: MenuItem {
property string t: modelData
checkable: true
@ -820,7 +826,7 @@ Page {
onObjectAdded: (index, object) => tagsMenu.insertItem(index, object)
onObjectRemoved: (index, object) => tagsMenu.removeItem(object)
}
Platform.MenuItem {
MenuItem {
text: qsTr("Create new tag...")
onTriggered: newTag.show()

View file

@ -5,10 +5,10 @@
import "./dialogs"
import "./pages"
import "./ui"
import Qt.labs.platform 1.1 as Platform
import QtQuick
import QtQuick.Controls
import QtQuick.Window
import QtQuick.Dialogs
import im.nheko
Pane {
@ -342,15 +342,13 @@ Pane {
return UIA.submit3pidToken(t);
}
}
Platform.MessageDialog {
MessageDialog {
id: uiaConfirmationLinkDialog
buttons: Platform.MessageDialog.Ok
buttons: MessageDialog.Ok
text: qsTr("Wait for the confirmation link to arrive, then continue.")
// Broken on macos, see https://bugreports.qt.io/browse/QTBUG-102078
//onAccepted: UIA.continue3pidReceived()
onOkClicked: UIA.continue3pidReceived()
onAccepted: UIA.continue3pidReceived()
}
Connections {
function onConfirm3pidToken() {

View file

@ -2,7 +2,6 @@
//
// SPDX-License-Identifier: GPL-3.0-or-later
import Qt.labs.platform 1.1 as Platform
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.2
@ -235,28 +234,28 @@ Pane {
image: ":/icons/icons/ui/options.svg"
visible: !!room
onClicked: roomOptionsMenu.open(roomOptionsButton)
onClicked: roomOptionsMenu.popup(roomOptionsButton)
Platform.Menu {
Menu {
id: roomOptionsMenu
Platform.MenuItem {
MenuItem {
text: qsTr("Invite users")
visible: room ? room.permissions.canInvite() : false
onTriggered: TimelineManager.openInviteUsers(roomId)
}
Platform.MenuItem {
MenuItem {
text: qsTr("Members")
onTriggered: TimelineManager.openRoomMembers(room)
}
Platform.MenuItem {
MenuItem {
text: qsTr("Leave room")
onTriggered: TimelineManager.openLeaveRoomDialog(roomId)
}
Platform.MenuItem {
MenuItem {
text: qsTr("Settings")
onTriggered: TimelineManager.openRoomSettings(roomId)

View file

@ -2,11 +2,11 @@
//
// SPDX-License-Identifier: GPL-3.0-or-later
import QtQuick 2.15
import Qt.labs.platform 1.1 as Platform
import im.nheko 1.0
import QtQuick
import QtQuick.Controls
import im.nheko
Platform.Menu {
Menu {
id: spacesMenu
property string roomid
@ -19,56 +19,61 @@ Platform.Menu {
onAboutToShow: loadChildren = true
//onAboutToHide: loadChildren = false
Platform.MenuItemGroup {
ButtonGroup {
id: modificationGroup
visible: position != -1
//visible: position != -1
}
Platform.MenuItem {
MenuItem {
text: qsTr("Official community for this room")
group: modificationGroup
ButtonGroup.group: modificationGroup
visible: position != -1
checkable: true
checked: spacesMenu.position >= 0 && (modelData.childValid && modelData.parentValid && modelData.canonical)
enabled: spacesMenu.position >= 0 && (modelData.canEditChild && modelData.canEditParent)
onTriggered: if (checked) Communities.updateSpaceStatus(modelData.roomid, spacesMenu.roomid, true, true, true)
}
Platform.MenuItem {
MenuItem {
text: qsTr("Affiliated community for this room")
group: modificationGroup
ButtonGroup.group: modificationGroup
visible: position != -1
checkable: true
checked: spacesMenu.position >= 0 && (modelData.childValid && modelData.parentValid && !modelData.canonical)
enabled: spacesMenu.position >= 0 && (modelData.canEditChild && modelData.canEditParent)
onTriggered: if (checked) Communities.updateSpaceStatus(modelData.roomid, spacesMenu.roomid, true, true, false)
}
Platform.MenuItem {
MenuItem {
text: qsTr("Listed only for community members")
group: modificationGroup
ButtonGroup.group: modificationGroup
visible: position != -1
checkable: true
checked: spacesMenu.position >= 0 && (modelData.childValid && !modelData.parentValid)
enabled: spacesMenu.position >= 0 && ((modelData.canEditChild || modelData.childValid) && (!modelData.parentValid || modelData.canEditParent))
onTriggered: if (checked) Communities.updateSpaceStatus(modelData.roomid, spacesMenu.roomid, false, true, false)
}
Platform.MenuItem {
MenuItem {
text: qsTr("Listed only for room members")
group: modificationGroup
ButtonGroup.group: modificationGroup
visible: position != -1
checkable: true
checked: spacesMenu.position >= 0 && (!modelData.childValid && modelData.parentValid)
enabled: spacesMenu.position >= 0 && ((modelData.canEditChild) && (modelData.parentValid || modelData.canEditParent))
onTriggered: if (checked) Communities.updateSpaceStatus(modelData.roomid, spacesMenu.roomid, true, false, false)
}
Platform.MenuItem {
MenuItem {
text: qsTr("Not related")
group: modificationGroup
ButtonGroup.group: modificationGroup
visible: position != -1
checkable: true
checked: spacesMenu.position >= 0 && (!modelData.childValid && !modelData.parentValid)
enabled: spacesMenu.position >= 0 && ((modelData.canEditChild || !modelData.childValid) && (!modelData.parentValid || modelData.canEditParent))
onTriggered: if (checked) Communities.updateSpaceStatus(modelData.roomid, spacesMenu.roomid, false, false, false)
}
Platform.MenuSeparator {
text: qsTr("Subcommunities")
group: modificationGroup
visible: modificationGroup.visible && inst.model != undefined
MenuSeparator {
//text: qsTr("Subcommunities")
ButtonGroup.group: modificationGroup
visible: position != -1 && inst.model != undefined
}
Instantiator {

View file

@ -4,11 +4,11 @@
import ".."
import "../ui"
import Qt.labs.platform 1.1 as Platform
import QtQuick 2.15
import QtQuick.Controls 2.3
import QtQuick.Layouts 1.2
import QtQuick.Window 2.13
import QtQuick.Dialogs
import im.nheko 1.0
ApplicationWindow {
@ -580,26 +580,23 @@ ApplicationWindow {
Layout.alignment: Qt.AlignRight
}
Platform.MessageDialog {
MessageDialog {
id: confirmEncryptionDialog
title: qsTr("End-to-End Encryption")
text: qsTr(`Encryption is currently experimental and things might break unexpectedly. <br>
Please take note that it can't be disabled afterwards.`)
modality: Qt.NonModal
// Broken on macos, see https://bugreports.qt.io/browse/QTBUG-102078
//onAccepted: {
onOkClicked: {
onAccepted: {
if (roomSettings.isEncryptionEnabled)
return ;
roomSettings.enableEncryption();
}
//onRejected: {
onCancelClicked: {
onRejected: {
encryptionToggle.checked = false;
}
buttons: Platform.MessageDialog.Ok | Platform.MessageDialog.Cancel
buttons: MessageDialog.Ok | MessageDialog.Cancel
}
Label {

File diff suppressed because it is too large Load diff

View file

@ -9,12 +9,6 @@
#include <QDateTime>
#include <QString>
#if __has_include(<lmdbxx/lmdb++.h>)
#include <lmdbxx/lmdb++.h>
#else
#include <lmdb++.h>
#endif
#include <mtx/events/collections.hpp>
#include <mtx/responses/notifications.hpp>
#include <mtx/responses/sync.hpp>
@ -29,12 +23,20 @@ struct Messages;
struct StateEvents;
}
namespace lmdb {
class txn;
class dbi;
}
struct CacheDb;
class Cache final : public QObject
{
Q_OBJECT
public:
Cache(const QString &userId, QObject *parent = nullptr);
~Cache() noexcept;
std::string displayName(const std::string &room_id, const std::string &user_id);
QString displayName(const QString &room_id, const QString &user_id);
@ -97,19 +99,11 @@ public:
//! Get a specific state event
template<typename T>
std::optional<mtx::events::StateEvent<T>>
getStateEvent(const std::string &room_id, std::string_view state_key = "")
{
auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY);
return getStateEvent<T>(txn, room_id, state_key);
}
getStateEvent(const std::string &room_id, std::string_view state_key = "");
template<typename T>
std::vector<mtx::events::StateEvent<T>>
getStateEventsWithType(const std::string &room_id,
mtx::events::EventType type = mtx::events::state_content_to_type<T>)
{
auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY);
return getStateEventsWithType<T>(txn, room_id, type);
}
mtx::events::EventType type = mtx::events::state_content_to_type<T>);
//! retrieve a specific event from account data
//! pass empty room_id for global account data
@ -304,20 +298,6 @@ public:
std::is_same_v<std::remove_cv_t<std::remove_reference_t<T>>,
mtx::events::StateEvent<decltype(std::declval<T>().content)>>;
static int compare_state_key(const MDB_val *a, const MDB_val *b)
{
auto get_skey = [](const MDB_val *v) {
auto temp = std::string_view(static_cast<const char *>(v->mv_data), v->mv_size);
// allow only passing the state key, in which case no null char will be in it and we
// return the whole string because rfind returns npos.
// We search from the back, because state keys could include nullbytes, event ids can
// not.
return temp.substr(0, temp.rfind('\0'));
};
return get_skey(a).compare(get_skey(b));
}
signals:
void newReadReceipts(const QString &room_id, const std::vector<QString> &event_ids);
void roomReadStatus(const std::map<QString, bool> &status);
@ -401,107 +381,44 @@ private:
//! Sends signals for the rooms that are removed.
void
removeLeftRooms(lmdb::txn &txn, const std::map<std::string, mtx::responses::LeftRoom> &rooms)
{
for (const auto &room : rooms) {
removeRoom(txn, room.first);
// Clean up leftover invites.
removeInvite(txn, room.first);
}
}
removeLeftRooms(lmdb::txn &txn, const std::map<std::string, mtx::responses::LeftRoom> &rooms);
void updateSpaces(lmdb::txn &txn,
const std::set<std::string> &spaces_with_updates,
std::set<std::string> rooms_with_updates);
lmdb::dbi getEventsDb(lmdb::txn &txn, const std::string &room_id)
{
return lmdb::dbi::open(txn, std::string(room_id + "/events").c_str(), MDB_CREATE);
}
lmdb::dbi getEventsDb(lmdb::txn &txn, const std::string &room_id);
lmdb::dbi getEventOrderDb(lmdb::txn &txn, const std::string &room_id)
{
return lmdb::dbi::open(
txn, std::string(room_id + "/event_order").c_str(), MDB_CREATE | MDB_INTEGERKEY);
}
lmdb::dbi getEventOrderDb(lmdb::txn &txn, const std::string &room_id);
// inverse of EventOrderDb
lmdb::dbi getEventToOrderDb(lmdb::txn &txn, const std::string &room_id)
{
return lmdb::dbi::open(txn, std::string(room_id + "/event2order").c_str(), MDB_CREATE);
}
lmdb::dbi getEventToOrderDb(lmdb::txn &txn, const std::string &room_id);
lmdb::dbi getMessageToOrderDb(lmdb::txn &txn, const std::string &room_id)
{
return lmdb::dbi::open(txn, std::string(room_id + "/msg2order").c_str(), MDB_CREATE);
}
lmdb::dbi getMessageToOrderDb(lmdb::txn &txn, const std::string &room_id);
lmdb::dbi getOrderToMessageDb(lmdb::txn &txn, const std::string &room_id)
{
return lmdb::dbi::open(
txn, std::string(room_id + "/order2msg").c_str(), MDB_CREATE | MDB_INTEGERKEY);
}
lmdb::dbi getOrderToMessageDb(lmdb::txn &txn, const std::string &room_id);
lmdb::dbi getPendingMessagesDb(lmdb::txn &txn, const std::string &room_id)
{
return lmdb::dbi::open(
txn, std::string(room_id + "/pending").c_str(), MDB_CREATE | MDB_INTEGERKEY);
}
lmdb::dbi getPendingMessagesDb(lmdb::txn &txn, const std::string &room_id);
lmdb::dbi getRelationsDb(lmdb::txn &txn, const std::string &room_id)
{
return lmdb::dbi::open(
txn, std::string(room_id + "/related").c_str(), MDB_CREATE | MDB_DUPSORT);
}
lmdb::dbi getRelationsDb(lmdb::txn &txn, const std::string &room_id);
lmdb::dbi getInviteStatesDb(lmdb::txn &txn, const std::string &room_id)
{
return lmdb::dbi::open(txn, std::string(room_id + "/invite_state").c_str(), MDB_CREATE);
}
lmdb::dbi getInviteStatesDb(lmdb::txn &txn, const std::string &room_id);
lmdb::dbi getInviteMembersDb(lmdb::txn &txn, const std::string &room_id)
{
return lmdb::dbi::open(txn, std::string(room_id + "/invite_members").c_str(), MDB_CREATE);
}
lmdb::dbi getInviteMembersDb(lmdb::txn &txn, const std::string &room_id);
lmdb::dbi getStatesDb(lmdb::txn &txn, const std::string &room_id)
{
return lmdb::dbi::open(txn, std::string(room_id + "/state").c_str(), MDB_CREATE);
}
lmdb::dbi getStatesDb(lmdb::txn &txn, const std::string &room_id);
lmdb::dbi getStatesKeyDb(lmdb::txn &txn, const std::string &room_id)
{
auto db = lmdb::dbi::open(
txn, std::string(room_id + "/states_key").c_str(), MDB_CREATE | MDB_DUPSORT);
lmdb::dbi_set_dupsort(txn, db, compare_state_key);
return db;
}
lmdb::dbi getStatesKeyDb(lmdb::txn &txn, const std::string &room_id);
lmdb::dbi getAccountDataDb(lmdb::txn &txn, const std::string &room_id)
{
return lmdb::dbi::open(txn, std::string(room_id + "/account_data").c_str(), MDB_CREATE);
}
lmdb::dbi getAccountDataDb(lmdb::txn &txn, const std::string &room_id);
lmdb::dbi getMembersDb(lmdb::txn &txn, const std::string &room_id)
{
return lmdb::dbi::open(txn, std::string(room_id + "/members").c_str(), MDB_CREATE);
}
lmdb::dbi getMembersDb(lmdb::txn &txn, const std::string &room_id);
lmdb::dbi getUserKeysDb(lmdb::txn &txn) { return lmdb::dbi::open(txn, "user_key", MDB_CREATE); }
lmdb::dbi getUserKeysDb(lmdb::txn &txn);
lmdb::dbi getVerificationDb(lmdb::txn &txn)
{
return lmdb::dbi::open(txn, "verified", MDB_CREATE);
}
lmdb::dbi getVerificationDb(lmdb::txn &txn);
QString getDisplayName(const mtx::events::StateEvent<mtx::events::state::Member> &event)
{
if (!event.content.display_name.empty())
return QString::fromStdString(event.content.display_name);
return QString::fromStdString(event.state_key);
}
QString getDisplayName(const mtx::events::StateEvent<mtx::events::state::Member> &event);
std::optional<VerificationCache> verificationCache(const std::string &user_id, lmdb::txn &txn);
VerificationStatus verificationStatus_(const std::string &user_id, lmdb::txn &txn);
@ -509,27 +426,6 @@ private:
void setNextBatchToken(lmdb::txn &txn, const std::string &token);
lmdb::env env_;
lmdb::dbi syncStateDb_;
lmdb::dbi roomsDb_;
lmdb::dbi spacesChildrenDb_, spacesParentsDb_;
lmdb::dbi invitesDb_;
lmdb::dbi readReceiptsDb_;
lmdb::dbi notificationsDb_;
lmdb::dbi presenceDb_;
lmdb::dbi devicesDb_;
lmdb::dbi deviceKeysDb_;
lmdb::dbi inboundMegolmSessionDb_;
lmdb::dbi outboundMegolmSessionDb_;
lmdb::dbi megolmSessionDataDb_;
lmdb::dbi olmSessionDb_;
lmdb::dbi encryptedRooms_;
lmdb::dbi eventExpiryBgJob_;
QString localUserId_;
QString cacheDirectory_;
@ -538,6 +434,8 @@ private:
VerificationStorage verification_storage;
bool databaseReady_ = false;
std::unique_ptr<CacheDb> db;
};
namespace cache {
@ -546,11 +444,12 @@ client();
}
#define NHEKO_CACHE_GET_STATE_EVENT_FORWARD(Content) \
extern template std::optional<mtx::events::StateEvent<Content>> Cache::getStateEvent( \
lmdb::txn &txn, const std::string &room_id, std::string_view state_key); \
extern template std::optional<mtx::events::StateEvent<Content>> Cache::getStateEvent<Content>( \
const std::string &room_id, std::string_view state_key); \
\
extern template std::vector<mtx::events::StateEvent<Content>> Cache::getStateEventsWithType( \
lmdb::txn &txn, const std::string &room_id, mtx::events::EventType type);
extern template std::vector<mtx::events::StateEvent<Content>> \
Cache::getStateEventsWithType<Content>(const std::string &room_id, \
mtx::events::EventType type);
NHEKO_CACHE_GET_STATE_EVENT_FORWARD(mtx::events::state::Aliases)
NHEKO_CACHE_GET_STATE_EVENT_FORWARD(mtx::events::state::Avatar)

View file

@ -13,6 +13,9 @@
#include <mtx/responses.hpp>
#include "Logging.h"
#include "UserSettingsPage.h"
namespace http {
mtx::http::Client *
@ -20,9 +23,15 @@ client()
{
static auto client_ = [] {
auto c = std::make_shared<mtx::http::Client>();
c->alt_svc_cache_path((QStandardPaths::writableLocation(QStandardPaths::CacheLocation) +
"/curl_alt_svc_cache.txt")
.toStdString());
// Disabled by default until CPU usage and reliability improves
if (UserSettings::instance()->qsettings()->value("enable_http3").toBool()) {
nhlog::net()->warn("Enabling http3 support. This is currently usually a worse "
"experience, so you are on your own.");
c->alt_svc_cache_path((QStandardPaths::writableLocation(QStandardPaths::CacheLocation) +
"/curl_alt_svc_cache.txt")
.toStdString());
}
return c;
}();
return client_.get();

View file

@ -32,16 +32,17 @@ MxcImageProvider::MxcImageProvider()
timer->setInterval(std::chrono::hours(1));
connect(timer, &QTimer::timeout, this, [] {
QThreadPool::globalInstance()->start([] {
nhlog::net()->debug("Running media purge");
QDir dir(QStandardPaths::writableLocation(QStandardPaths::CacheLocation) +
"/media_cache",
"",
QDir::SortFlags(QDir::Name | QDir::IgnoreCase),
QDir::Filter::Writable | QDir::Filter::NoDotAndDotDot | QDir::Filter::Files);
QDir::Filter::Writable | QDir::Filter::NoDotAndDotDot | QDir::Filter::Files |
QDir::Filter::Dirs);
auto files = dir.entryInfoList();
for (const auto &fileInfo : std::as_const(files)) {
auto handleFile = [](const QFileInfo &fileInfo) {
if (fileInfo.fileTime(QFile::FileTime::FileAccessTime)
.daysTo(QDateTime::currentDateTime()) > 30) {
.daysTo(QDateTime::currentDateTime()) > 14) {
if (QFile::remove(fileInfo.absoluteFilePath()))
nhlog::net()->debug("Deleted stale media '{}'",
fileInfo.absoluteFilePath().toStdString());
@ -49,6 +50,24 @@ MxcImageProvider::MxcImageProvider()
nhlog::net()->warn("Failed to delete stale media '{}'",
fileInfo.absoluteFilePath().toStdString());
}
};
auto files = dir.entryInfoList();
for (const auto &fileInfo : std::as_const(files)) {
if (fileInfo.isDir()) {
// handle one level of legacy directories
auto nestedDir = QDir(fileInfo.absoluteFilePath(),
"",
QDir::SortFlags(QDir::Name | QDir::IgnoreCase),
QDir::Filter::Writable | QDir::Filter::NoDotAndDotDot |
QDir::Filter::Files)
.entryInfoList();
for (const auto &nestedFile : std::as_const(nestedDir)) {
handleFile(nestedFile);
}
} else {
handleFile(fileInfo);
}
}
});
});

View file

@ -11,6 +11,7 @@
#include <QWindow>
#include "TrayIcon.h"
#include "UserSettingsPage.h"
MsgCountComposedIcon::MsgCountComposedIcon(const QIcon &icon)
: QIconEngine()
@ -114,12 +115,30 @@ TrayIcon::TrayIcon(const QString &filename, QWindow *parent)
menu->addAction(viewAction_);
menu->addAction(quitAction_);
QString toolTip = QLatin1String("nheko");
QString profile = UserSettings::instance()->profile();
if (!profile.isEmpty())
toolTip.append(QStringLiteral(" | %1").arg(profile));
setToolTip(toolTip);
}
void
TrayIcon::setUnreadCount(int count)
{
qGuiApp->setBadgeNumber(count);
if (count != previousCount) {
QString toolTip = QLatin1String("nheko");
QString profile = UserSettings::instance()->profile();
if (!profile.isEmpty())
toolTip.append(QStringLiteral(" | %1").arg(profile));
if (count != 0)
toolTip.append(tr("\n%n unread message(s)", "", count));
setToolTip(toolTip);
}
#if !defined(Q_OS_MACOS) && !defined(Q_OS_WIN)
if (count != previousCount) {
@ -131,13 +150,6 @@ TrayIcon::setUnreadCount(int count)
#else
(void)previousCount;
#endif
QString toolTip = QLatin1String("nheko");
if (count > 0) {
toolTip.append(tr("\n%n unread message(s)", "", count));
}
setToolTip(toolTip);
}
#include "moc_TrayIcon.cpp"

View file

@ -20,6 +20,8 @@
#include <QVideoFrame>
#include <QVideoSink>
#include <fmt/format.h>
#include <nlohmann/json.hpp>
#include <mtx/responses/common.hpp>
@ -389,6 +391,9 @@ InputBar::send()
{
QInputMethod *im = QGuiApplication::inputMethod();
im->commit();
// If the input from the UI is only blanks or no text, this trigger should
// be used to confirm media upload. If that is not the case however, but
// but there are pending uploads, we fall into one of the cases seen later.
if (text().trimmed().isEmpty()) {
acceptUploads();
return;
@ -402,8 +407,18 @@ InputBar::send()
updateTextContentProperties(text());
if (containsIncompleteCommand_)
return;
if (commandName.isEmpty() || !command(commandName, args))
message(text());
if (unconfirmedUploads.empty()) {
if (commandName.isEmpty() || !command(commandName, args)) {
message(text());
}
} else {
if (commandName.isEmpty()) {
// This is a set of uploads with text
acceptUploadsWithCaption(text());
} else if (!command(commandName, args)) {
message(text());
}
}
if (!wasEdit) {
history_.push_front(QLatin1String(""));
@ -578,8 +593,7 @@ InputBar::message(const QString &msg, MarkdownOverride useMarkdown, bool rainbow
}
}
text.body =
QStringLiteral("%1\n%2").arg(body, QString::fromStdString(text.body)).toStdString();
text.body = fmt::format("{}\n{}", body.toStdString(), text.body);
// NOTE(Nico): rich replies always need a formatted_body!
text.format = "org.matrix.custom.html";
@ -717,6 +731,7 @@ void
InputBar::image(const QString &filename,
const std::optional<mtx::crypto::EncryptedFile> &file,
const QString &url,
const std::optional<QString> &caption,
const QString &mime,
uint64_t dsize,
const QSize &dimensions,
@ -730,9 +745,10 @@ InputBar::image(const QString &filename,
image.info.mimetype = mime.toStdString();
image.info.size = dsize;
image.info.blurhash = blurhash.toStdString();
image.body = filename.toStdString();
image.info.h = dimensions.height();
image.info.w = dimensions.width();
// Depending on the input bar's situation, retrieve the text
image.body = caption.has_value() ? caption.value().toStdString() : filename.toStdString();
image.info.h = dimensions.height();
image.info.w = dimensions.width();
if (file)
image.file = file;
@ -761,13 +777,14 @@ void
InputBar::file(const QString &filename,
const std::optional<mtx::crypto::EncryptedFile> &encryptedFile,
const QString &url,
const std::optional<QString> &caption,
const QString &mime,
uint64_t dsize)
{
mtx::events::msg::File file;
file.info.mimetype = mime.toStdString();
file.info.size = dsize;
file.body = filename.toStdString();
file.body = caption.has_value() ? caption.value().toStdString() : filename.toStdString();
if (encryptedFile)
file.file = encryptedFile;
@ -784,6 +801,7 @@ void
InputBar::audio(const QString &filename,
const std::optional<mtx::crypto::EncryptedFile> &file,
const QString &url,
const std::optional<QString> &caption,
const QString &mime,
uint64_t dsize,
uint64_t duration)
@ -791,8 +809,8 @@ InputBar::audio(const QString &filename,
mtx::events::msg::Audio audio;
audio.info.mimetype = mime.toStdString();
audio.info.size = dsize;
audio.body = filename.toStdString();
audio.url = url.toStdString();
audio.body = caption.has_value() ? caption.value().toStdString() : filename.toStdString();
audio.url = url.toStdString();
if (duration > 0)
audio.info.duration = duration;
@ -812,6 +830,7 @@ void
InputBar::video(const QString &filename,
const std::optional<mtx::crypto::EncryptedFile> &file,
const QString &url,
const std::optional<QString> &caption,
const QString &mime,
uint64_t dsize,
uint64_t duration,
@ -826,7 +845,7 @@ InputBar::video(const QString &filename,
video.info.mimetype = mime.toStdString();
video.info.size = dsize;
video.info.blurhash = blurhash.toStdString();
video.body = filename.toStdString();
video.body = caption.has_value() ? caption.value().toStdString() : filename.toStdString();
if (duration > 0)
video.info.duration = duration;
@ -1298,10 +1317,12 @@ InputBar::finalizeUpload(MediaUpload *upload, const QString &url)
auto mimeClass = upload->mimeClass();
auto size = upload->size();
auto encryptedFile = upload->encryptedFile_();
auto caption = upload->caption();
if (mimeClass == u"image")
image(filename,
encryptedFile,
url,
caption,
mime,
size,
upload->dimensions(),
@ -1311,11 +1332,12 @@ InputBar::finalizeUpload(MediaUpload *upload, const QString &url)
upload->thumbnailImg().size(),
upload->blurhash());
else if (mimeClass == u"audio")
audio(filename, encryptedFile, url, mime, size, upload->duration());
audio(filename, encryptedFile, url, caption, mime, size, upload->duration());
else if (mimeClass == u"video")
video(filename,
encryptedFile,
url,
caption,
mime,
size,
upload->duration(),
@ -1326,7 +1348,7 @@ InputBar::finalizeUpload(MediaUpload *upload, const QString &url)
upload->thumbnailImg().size(),
upload->blurhash());
else
file(filename, encryptedFile, url, mime, size);
file(filename, encryptedFile, url, caption, mime, size);
removeRunUpload(upload);
}
@ -1421,6 +1443,15 @@ InputBar::acceptUploads()
}
}
void
InputBar::acceptUploadsWithCaption(QString caption)
{
for (UploadHandle &upload : unconfirmedUploads) {
upload->caption_ = std::optional(caption);
}
acceptUploads();
}
void
InputBar::declineUploads()
{

View file

@ -86,6 +86,7 @@ public:
return MediaType::File;
}
[[nodiscard]] QString url() const { return url_; }
[[nodiscard]] std::optional<QString> caption() const { return caption_; }
[[nodiscard]] QString mimetype() const { return mimetype_; }
[[nodiscard]] QString mimeClass() const { return mimeClass_; }
[[nodiscard]] QString filename() const { return originalFilename_; }
@ -143,6 +144,7 @@ public:
QString blurhash_;
QString thumbnailUrl_;
QString url_;
std::optional<QString> caption_;
std::optional<mtx::crypto::EncryptedFile> encryptedFile, thumbnailEncryptedFile;
QImage thumbnail_;
@ -241,6 +243,7 @@ public slots:
void sticker(QStringList descriptor);
void acceptUploads();
void acceptUploadsWithCaption(QString);
void declineUploads();
private slots:
@ -269,6 +272,7 @@ private:
void image(const QString &filename,
const std::optional<mtx::crypto::EncryptedFile> &file,
const QString &url,
const std::optional<QString> &caption,
const QString &mime,
uint64_t dsize,
const QSize &dimensions,
@ -280,17 +284,20 @@ private:
void file(const QString &filename,
const std::optional<mtx::crypto::EncryptedFile> &encryptedFile,
const QString &url,
const std::optional<QString> &caption,
const QString &mime,
uint64_t dsize);
void audio(const QString &filename,
const std::optional<mtx::crypto::EncryptedFile> &file,
const QString &url,
const std::optional<QString> &caption,
const QString &mime,
uint64_t dsize,
uint64_t duration);
void video(const QString &filename,
const std::optional<mtx::crypto::EncryptedFile> &file,
const QString &url,
const std::optional<QString> &caption,
const QString &mime,
uint64_t dsize,
uint64_t duration,

View file

@ -37,8 +37,14 @@ static CacheEntry *
pullPresence(const QString &id)
{
auto p = cache::presence(id.toStdString());
auto c = new CacheEntry{
utils::replaceEmoji(QString::fromStdString(p.status_msg).toHtmlEscaped()), p.presence};
auto statusMsg = QString::fromStdString(p.status_msg);
if (statusMsg.size() > 255) {
statusMsg.truncate(255);
statusMsg.append(u'');
}
auto c = new CacheEntry{utils::replaceEmoji(std::move(statusMsg).toHtmlEscaped()), p.presence};
presences.insert(id, c);
return c;
}

View file

@ -2073,10 +2073,12 @@ TimelineModel::cacheMedia(const QString &eventId,
const auto url = mxcUrl.toStdString();
const auto name = QString(mxcUrl).remove(QStringLiteral("mxc://"));
QFileInfo filename(
QStringLiteral("%1/media_cache/%2.%3")
.arg(QStandardPaths::writableLocation(QStandardPaths::CacheLocation), name, suffix));
if (QDir::cleanPath(name) != name) {
QFileInfo filename(QStringLiteral("%1/media_cache/%2.%3")
.arg(QStandardPaths::writableLocation(QStandardPaths::CacheLocation),
QString::fromUtf8(name.toUtf8().toBase64(
QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals)),
suffix));
if (QDir::cleanPath(filename.filePath()) != filename.filePath()) {
nhlog::net()->warn("mxcUrl '{}' is not safe, not downloading file", url);
return;
}

View file

@ -53,10 +53,12 @@ MxcAnimatedImage::startDownload()
const auto url = mxcUrl.toStdString();
const auto name = QString(mxcUrl).remove(QStringLiteral("mxc://"));
QFileInfo filename(
QStringLiteral("%1/media_cache/media/%2.%3")
.arg(QStandardPaths::writableLocation(QStandardPaths::CacheLocation), name, suffix));
if (QDir::cleanPath(name) != name) {
QFileInfo filename(QStringLiteral("%1/media_cache/media/%2.%3")
.arg(QStandardPaths::writableLocation(QStandardPaths::CacheLocation),
QString::fromUtf8(name.toUtf8().toBase64(
QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals)),
suffix));
if (QDir::cleanPath(filename.filePath()) != filename.filePath()) {
nhlog::net()->warn("mxcUrl '{}' is not safe, not downloading file", url);
return;
}

View file

@ -96,10 +96,12 @@ MxcMediaProxy::startDownload(bool onlyCached)
const auto url = mxcUrl.toStdString();
const auto name = QString(mxcUrl).remove(QStringLiteral("mxc://"));
QFileInfo filename(
QStringLiteral("%1/media_cache/media/%2.%3")
.arg(QStandardPaths::writableLocation(QStandardPaths::CacheLocation), name, suffix));
if (QDir::cleanPath(name) != name) {
QFileInfo filename(QStringLiteral("%1/media_cache/media/%2.%3")
.arg(QStandardPaths::writableLocation(QStandardPaths::CacheLocation),
QString::fromUtf8(name.toUtf8().toBase64(
QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals)),
suffix));
if (QDir::cleanPath(filename.filePath()) != filename.filePath()) {
nhlog::net()->warn("mxcUrl '{}' is not safe, not downloading file", url);
return;
}