Compare commits

...

13 commits

Author SHA1 Message Date
Loren Burkholder
7e1f46579c
Merge 52346d972e into 1a00d91316 2024-11-05 15:38:59 -06: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
Loren Burkholder
52346d972e
Make the image overlay behave better
Previously, the image would not try to stay centered; furthermore,
on mobile devices, it was impossible to zoom images without dropping
them at a slightly off rotation. This bothered me enough that I've
clamped images to the center of the screen (as long as they aren't
zoomed larger than the screen) and snapped rotations to 45-degree
increments (with a nice animation to boot).
2024-04-18 20:19:21 -04:00
20 changed files with 753 additions and 597 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

@ -48,8 +48,10 @@ Window {
Item {
id: imgContainer
property int imgSrcWidth: (imageOverlay.originalWidth && imageOverlay.originalWidth > 100) ? imageOverlay.originalWidth : Screen.width
property int imgSrcHeight: imageOverlay.proportionalHeight ? imgSrcWidth * imageOverlay.proportionalHeight : Screen.height
property int imgSrcWidth: (imageOverlay.originalWidth && imageOverlay.originalWidth > 100) ? imageOverlay.originalWidth : imageOverlay.width
property int imgSrcHeight: imageOverlay.proportionalHeight ? imgSrcWidth * imageOverlay.proportionalHeight : imageOverlay.height
readonly property int physicalWidth: width * scale
readonly property int physicalHeight: height * scale
property double initialScale: Math.min(Window.height/imgSrcHeight, Window.width/imgSrcWidth, 1.0)
@ -59,6 +61,22 @@ Window {
x: (parent.width - width) / 2
y: (parent.height - height) / 2
onXChanged: {
if (physicalWidth < imageOverlay.width)
x = (parent.width - width) / 2;
}
onYChanged: {
if (physicalHeight < imageOverlay.height)
y = (parent.height - height) / 2;
}
Behavior on rotation {
NumberAnimation {
duration: 100
easing.type: Easing.InOutQuad
}
}
Image {
id: img
@ -89,13 +107,30 @@ Window {
}
Item {
anchors.fill: parent
id: handlerContainer
function snapImageRotation()
{
// snap to 15-degree angles
let rotationOffset = imgContainer.rotation % 15;
if (rotationOffset != 0)
{
if (rotationOffset < 7.5)
imgContainer.rotation -= rotationOffset;
else
imgContainer.rotation += rotationOffset;
}
}
anchors.fill: parent
PinchHandler {
target: imgContainer
maximumScale: 10
minimumScale: 0.1
onGrabChanged: handlerContainer.snapImageRotation()
}
WheelHandler {
@ -105,10 +140,16 @@ Window {
// and we don't yet distinguish mice and trackpads on Wayland either
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
target: imgContainer
onWheel: handlerContainer.snapImageRotation()
}
DragHandler {
target: imgContainer
xAxis.enabled: imgContainer.physicalWidth > imageOverlay.width
yAxis.enabled: imgContainer.physicalHeight > imageOverlay.height
onGrabChanged: handlerContainer.snapImageRotation()
}
HoverHandler {

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>();
// 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>
@ -578,8 +580,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";

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;
}