Compare commits
1 Commits
master
...
debug-pagi
Author | SHA1 | Date |
---|---|---|
Adasauce | 48c00f7a57 | 5 years ago |
@ -1,21 +0,0 @@ |
||||
$file = "nheko_win_64.zip" |
||||
$fileName = "nheko-${env:APPVEYOR_REPO_BRANCH}-${env:APPVEYOR_REPO_COMMIT}-win64.zip" |
||||
|
||||
$response = Invoke-RestMethod -uri "https://matrix.neko.dev/_matrix/media/r0/upload?filename=$fileName" -Method Post -Infile "$file" -ContentType 'application/x-compressed' -Headers @{"Authorization"="Bearer ${env:MATRIX_ACCESS_TOKEN}"} |
||||
|
||||
$txId = [DateTimeOffset]::Now.ToUnixTimeSeconds() |
||||
$fileSize = (Get-Item $file).Length |
||||
$body = @{ |
||||
"body" = "${fileName}" |
||||
"filename"= "${fileName}" |
||||
"info" = @{ |
||||
"mimetype" = "application/x-compressed" |
||||
"size" = ${fileSize} |
||||
} |
||||
"msgtype" = "m.file" |
||||
"url" = ${response}.content_uri |
||||
} | ConvertTo-Json |
||||
$room = "!TshDrgpBNBDmfDeEGN:neko.dev" |
||||
|
||||
Invoke-RestMethod -uri "https://matrix.neko.dev/_matrix/client/r0/rooms/${room}/send/m.room.message/${txid}" -Method Put -Body "$body" -ContentType 'application/json' -Headers @{"Authorization"="Bearer ${env:MATRIX_ACCESS_TOKEN}"} |
||||
|
@ -1,9 +0,0 @@ |
||||
#!/bin/sh |
||||
|
||||
file=$(find artifacts/ -type f -exec basename {} \;) |
||||
fileName="nheko-${TRAVIS_BRANCH}-${file#nheko-}" |
||||
|
||||
uri=$(curl -H "Authorization: Bearer ${MATRIX_ACCESS_TOKEN}" -H "Content-Type: application/x-compressed" -X POST --data-binary "@artifacts/${file}" "https://matrix.neko.dev/_matrix/media/r0/upload?filename=${fileName}" --http1.1 | python -c "import sys, json; print(json.load(sys.stdin)['content_uri'])") |
||||
echo "Uploaded to ${uri}" |
||||
|
||||
curl -H "Authorization: Bearer ${MATRIX_ACCESS_TOKEN}" -H "Content-Type: application/json" -X PUT -d "{ \"body\": \"${fileName}\", \"filename\": \"${fileName}\", \"info\": { \"mimetype\": \"application/x-compressed\", \"size\": $(wc -c < artifacts/${file}) }, \"msgtype\": \"m.file\", \"url\": \"${uri}\" }" "https://matrix.neko.dev/_matrix/client/r0/rooms/${ROOM}/send/m.room.message/$(date +%s)" |
@ -1,20 +0,0 @@ |
||||
--- |
||||
name: Feature request |
||||
about: Suggest an idea for this project |
||||
title: '' |
||||
labels: enhancement |
||||
assignees: '' |
||||
|
||||
--- |
||||
|
||||
**Is your feature request related to a problem? Please describe.** |
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] |
||||
|
||||
**Describe the solution you'd like** |
||||
A clear and concise description of what you want to happen. |
||||
|
||||
**Describe alternatives you've considered** |
||||
A clear and concise description of any alternative solutions or features you've considered. |
||||
|
||||
**Additional context** |
||||
Add any other context or screenshots about the feature request here. |
@ -1,5 +1,5 @@ |
||||
hunter_config( |
||||
Boost |
||||
VERSION "1.70.0-p1" |
||||
VERSION "1.70.0-p0" |
||||
CMAKE_ARGS IOSTREAMS_NO_BZIP2=1 |
||||
) |
||||
|
Before Width: | Height: | Size: 643 B |
Before Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 759 B |
Before Width: | Height: | Size: 573 B |
Before Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 385 B |
Before Width: | Height: | Size: 741 B |
@ -1 +0,0 @@ |
||||
*.qm |
@ -1,5 +0,0 @@ |
||||
The below media files were obtained from https://github.com/matrix-org/matrix-react-sdk/tree/develop/res/media |
||||
|
||||
callend.ogg |
||||
ringback.ogg |
||||
ring.ogg |
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 5.2 KiB |
@ -1,113 +0,0 @@ |
||||
import QtQuick 2.9 |
||||
import QtQuick.Controls 2.3 |
||||
import QtQuick.Layouts 1.2 |
||||
import im.nheko 1.0 |
||||
|
||||
Rectangle { |
||||
id: activeCallBar |
||||
|
||||
visible: TimelineManager.callState != WebRTCState.DISCONNECTED |
||||
color: "#2ECC71" |
||||
implicitHeight: rowLayout.height + 8 |
||||
|
||||
RowLayout { |
||||
id: rowLayout |
||||
|
||||
anchors.left: parent.left |
||||
anchors.right: parent.right |
||||
anchors.verticalCenter: parent.verticalCenter |
||||
anchors.leftMargin: 8 |
||||
|
||||
Avatar { |
||||
width: avatarSize |
||||
height: avatarSize |
||||
url: TimelineManager.callPartyAvatarUrl.replace("mxc://", "image://MxcImage/") |
||||
displayName: TimelineManager.callPartyName |
||||
} |
||||
|
||||
Label { |
||||
font.pointSize: fontMetrics.font.pointSize * 1.1 |
||||
text: " " + TimelineManager.callPartyName + " " |
||||
} |
||||
|
||||
Image { |
||||
Layout.preferredWidth: 24 |
||||
Layout.preferredHeight: 24 |
||||
source: "qrc:/icons/icons/ui/place-call.png" |
||||
} |
||||
|
||||
Label { |
||||
id: callStateLabel |
||||
|
||||
font.pointSize: fontMetrics.font.pointSize * 1.1 |
||||
} |
||||
|
||||
Connections { |
||||
function onCallStateChanged(state) { |
||||
switch (state) { |
||||
case WebRTCState.INITIATING: |
||||
callStateLabel.text = qsTr("Initiating..."); |
||||
break; |
||||
case WebRTCState.OFFERSENT: |
||||
callStateLabel.text = qsTr("Calling..."); |
||||
break; |
||||
case WebRTCState.CONNECTING: |
||||
callStateLabel.text = qsTr("Connecting..."); |
||||
break; |
||||
case WebRTCState.CONNECTED: |
||||
callStateLabel.text = "00:00"; |
||||
var d = new Date(); |
||||
callTimer.startTime = Math.floor(d.getTime() / 1000); |
||||
break; |
||||
case WebRTCState.DISCONNECTED: |
||||
callStateLabel.text = ""; |
||||
} |
||||
} |
||||
|
||||
target: TimelineManager |
||||
} |
||||
|
||||
Timer { |
||||
id: callTimer |
||||
|
||||
property int startTime |
||||
|
||||
function pad(n) { |
||||
return (n < 10) ? ("0" + n) : n; |
||||
} |
||||
|
||||
interval: 1000 |
||||
running: TimelineManager.callState == WebRTCState.CONNECTED |
||||
repeat: true |
||||
onTriggered: { |
||||
var d = new Date(); |
||||
let seconds = Math.floor(d.getTime() / 1000 - startTime); |
||||
let s = Math.floor(seconds % 60); |
||||
let m = Math.floor(seconds / 60) % 60; |
||||
let h = Math.floor(seconds / 3600); |
||||
callStateLabel.text = (h ? (pad(h) + ":") : "") + pad(m) + ":" + pad(s); |
||||
} |
||||
} |
||||
|
||||
Item { |
||||
Layout.fillWidth: true |
||||
} |
||||
|
||||
ImageButton { |
||||
width: 24 |
||||
height: 24 |
||||
buttonTextColor: "#000000" |
||||
image: TimelineManager.isMicMuted ? ":/icons/icons/ui/microphone-unmute.png" : ":/icons/icons/ui/microphone-mute.png" |
||||
hoverEnabled: true |
||||
ToolTip.visible: hovered |
||||
ToolTip.text: TimelineManager.isMicMuted ? qsTr("Unmute Mic") : qsTr("Mute Mic") |
||||
onClicked: TimelineManager.toggleMicMute() |
||||
} |
||||
|
||||
Item { |
||||
implicitWidth: 16 |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
@ -1,75 +1,52 @@ |
||||
import QtGraphicalEffects 1.0 |
||||
import QtQuick 2.6 |
||||
import QtQuick.Controls 2.3 |
||||
import im.nheko 1.0 |
||||
import QtGraphicalEffects 1.0 |
||||
import Qt.labs.settings 1.0 |
||||
|
||||
Rectangle { |
||||
id: avatar |
||||
width: 48 |
||||
height: 48 |
||||
radius: settings.avatar_circles ? height/2 : 3 |
||||
|
||||
Settings { |
||||
id: settings |
||||
category: "user" |
||||
property bool avatar_circles: true |
||||
} |
||||
|
||||
property alias url: img.source |
||||
property string userid |
||||
property string displayName |
||||
|
||||
width: 48 |
||||
height: 48 |
||||
radius: Settings.avatarCircles ? height / 2 : 3 |
||||
color: colors.base |
||||
|
||||
Label { |
||||
Text { |
||||
anchors.fill: parent |
||||
text: TimelineManager.escapeEmoji(displayName ? String.fromCodePoint(displayName.codePointAt(0)) : "") |
||||
text: chat.model.escapeEmoji(String.fromCodePoint(displayName.codePointAt(0))) |
||||
textFormat: Text.RichText |
||||
font.pixelSize: avatar.height / 2 |
||||
color: colors.text |
||||
font.pixelSize: avatar.height/2 |
||||
verticalAlignment: Text.AlignVCenter |
||||
horizontalAlignment: Text.AlignHCenter |
||||
visible: img.status != Image.Ready |
||||
color: colors.text |
||||
} |
||||
|
||||
Image { |
||||
id: img |
||||
|
||||
anchors.fill: parent |
||||
asynchronous: true |
||||
fillMode: Image.PreserveAspectCrop |
||||
mipmap: true |
||||
smooth: false |
||||
|
||||
sourceSize.width: avatar.width |
||||
sourceSize.height: avatar.height |
||||
layer.enabled: true |
||||
|
||||
layer.enabled: true |
||||
layer.effect: OpacityMask { |
||||
|
||||
maskSource: Rectangle { |
||||
anchors.fill: parent |
||||
width: avatar.width |
||||
height: avatar.height |
||||
radius: Settings.avatarCircles ? height / 2 : 3 |
||||
} |
||||
|
||||
radius: settings.avatar_circles ? height/2 : 3 |
||||
} |
||||
|
||||
} |
||||
|
||||
Rectangle { |
||||
anchors.bottom: avatar.bottom |
||||
anchors.right: avatar.right |
||||
visible: !!userid |
||||
height: avatar.height / 6 |
||||
width: height |
||||
radius: Settings.avatarCircles ? height / 2 : height / 4 |
||||
color: { |
||||
switch (TimelineManager.userPresence(userid)) { |
||||
case "online": |
||||
return "#00cc66"; |
||||
case "unavailable": |
||||
return "#ff9933"; |
||||
case "offline": |
||||
default: |
||||
// return "#a82353" don't show anything if offline, since it is confusing, if presence is disabled |
||||
"transparent"; |
||||
} |
||||
} |
||||
} |
||||
|
||||
color: colors.base |
||||
} |
||||
|
@ -1,30 +1,29 @@ |
||||
import QtQuick 2.3 |
||||
import QtQuick.Controls 2.3 |
||||
|
||||
AbstractButton { |
||||
Button { |
||||
property string image: undefined |
||||
|
||||
id: button |
||||
|
||||
property string image: undefined |
||||
property color highlightColor: colors.highlight |
||||
property color buttonTextColor: colors.buttonText |
||||
flat: true |
||||
|
||||
width: 16 |
||||
height: 16 |
||||
// disable background, because we don't want a border on hover |
||||
background: Item { |
||||
} |
||||
|
||||
Image { |
||||
id: buttonImg |
||||
|
||||
// Workaround, can't get icon.source working for now... |
||||
anchors.fill: parent |
||||
source: "image://colorimage/" + image + "?" + (button.hovered ? highlightColor : buttonTextColor) |
||||
source: "image://colorimage/" + image + "?" + (button.hovered ? colors.highlight : colors.buttonText) |
||||
} |
||||
|
||||
MouseArea { |
||||
MouseArea |
||||
{ |
||||
id: mouseArea |
||||
|
||||
anchors.fill: parent |
||||
onPressed: mouse.accepted = false |
||||
cursorShape: Qt.PointingHandCursor |
||||
} |
||||
|
||||
} |
||||
|
@ -1,37 +1,31 @@ |
||||
import QtQuick 2.5 |
||||
import QtQuick.Controls 2.3 |
||||
import im.nheko 1.0 |
||||
|
||||
TextEdit { |
||||
textFormat: TextEdit.RichText |
||||
readOnly: true |
||||
wrapMode: Text.Wrap |
||||
selectByMouse: true |
||||
activeFocusOnPress: false |
||||
color: colors.text |
||||
|
||||
onLinkActivated: { |
||||
if (/^https:\/\/matrix.to\/#\/(@.*)$/.test(link)) { |
||||
chat.model.openUserProfile(/^https:\/\/matrix.to\/#\/(@.*)$/.exec(link)[1]); |
||||
} else if (/^https:\/\/matrix.to\/#\/(![^\/]*)$/.test(link)) { |
||||
TimelineManager.setHistoryView(/^https:\/\/matrix.to\/#\/(!.*)$/.exec(link)[1]); |
||||
} else if (/^https:\/\/matrix.to\/#\/(![^\/]*)\/(\$.*)$/.test(link)) { |
||||
var match = /^https:\/\/matrix.to\/#\/(![^\/]*)\/(\$.*)$/.exec(link); |
||||
TimelineManager.setHistoryView(match[1]); |
||||
chat.positionViewAtIndex(chat.model.idToIndex(match[2]), ListView.Contain); |
||||
} else { |
||||
TimelineManager.openLink(link); |
||||
if (/^https:\/\/matrix.to\/#\/(@.*)$/.test(link)) chat.model.openUserProfile(/^https:\/\/matrix.to\/#\/(@.*)$/.exec(link)[1]) |
||||
else if (/^https:\/\/matrix.to\/#\/(![^\/]*)$/.test(link)) timelineManager.setHistoryView(/^https:\/\/matrix.to\/#\/(!.*)$/.exec(link)[1]) |
||||
else if (/^https:\/\/matrix.to\/#\/(![^\/]*)\/(\$.*)$/.test(link)) { |
||||
var match = /^https:\/\/matrix.to\/#\/(![^\/]*)\/(\$.*)$/.exec(link) |
||||
timelineManager.setHistoryView(match[1]) |
||||
chat.positionViewAtIndex(chat.model.idToIndex(match[2]), ListView.Contain) |
||||
} |
||||
else Qt.openUrlExternally(link) |
||||
} |
||||
ToolTip.visible: hoveredLink |
||||
ToolTip.text: hoveredLink |
||||
|
||||
MouseArea { |
||||
MouseArea |
||||
{ |
||||
id: ma |
||||
|
||||
anchors.fill: parent |
||||
propagateComposedEvents: true |
||||
acceptedButtons: Qt.NoButton |
||||
onPressed: mouse.accepted = false |
||||
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor |
||||
} |
||||
|
||||
ToolTip.visible: hoveredLink |
||||
ToolTip.text: hoveredLink |
||||
} |
||||
|
@ -1,95 +0,0 @@ |
||||
import QtQuick 2.6 |
||||
import QtQuick.Controls 2.2 |
||||
import im.nheko 1.0 |
||||
|
||||
// This class is for showing Reactions in the timeline row, not for |
||||
// adding new reactions via the emoji picker |
||||
Flow { |
||||
id: reactionFlow |
||||
|
||||
// highlight colors for selfReactedEvent background |
||||
property real highlightHue: colors.highlight.hslHue |
||||
property real highlightSat: colors.highlight.hslSaturation |
||||
property real highlightLight: colors.highlight.hslLightness |
||||
property string eventId |
||||
property alias reactions: repeater.model |
||||
|
||||
anchors.left: parent.left |
||||
anchors.right: parent.right |
||||
spacing: 4 |
||||
|
||||
Repeater { |
||||
id: repeater |
||||
|
||||
delegate: AbstractButton { |
||||
id: reaction |
||||
|
||||
hoverEnabled: true |
||||
implicitWidth: contentItem.childrenRect.width + contentItem.leftPadding * 2 |
||||
implicitHeight: contentItem.childrenRect.height |
||||
ToolTip.visible: hovered |
||||
ToolTip.text: modelData.users |
||||
onClicked: { |
||||
console.debug("Picked " + modelData.key + "in response to " + reactionFlow.eventId + ". selfReactedEvent: " + modelData.selfReactedEvent); |
||||
TimelineManager.queueReactionMessage(reactionFlow.eventId, modelData.key); |
||||
} |
||||
|
||||
contentItem: Row { |
||||
anchors.centerIn: parent |
||||
spacing: reactionText.implicitHeight / 4 |
||||
leftPadding: reactionText.implicitHeight / 2 |
||||
rightPadding: reactionText.implicitHeight / 2 |
||||
|
||||
TextMetrics { |
||||
id: textMetrics |
||||
|
||||
font.family: Settings.emojiFont |
||||
elide: Text.ElideRight |
||||
elideWidth: 150 |
||||
text: modelData.key |
||||
} |
||||
|
||||
Text { |
||||
id: reactionText |
||||
|
||||
anchors.baseline: reactionCounter.baseline |
||||
text: textMetrics.elidedText + (textMetrics.elidedText == modelData.key ? "" : "…") |
||||
font.family: Settings.emojiFont |
||||
color: reaction.hovered ? colors.highlight : colors.text |
||||
maximumLineCount: 1 |
||||
} |
||||
|
||||
Rectangle { |
||||
id: divider |
||||
|
||||
height: Math.floor(reactionCounter.implicitHeight * 1.4) |
||||
width: 1 |
||||
color: (reaction.hovered || modelData.selfReactedEvent !== '') ? colors.highlight : colors.text |
||||
} |
||||
|
||||
Text { |
||||
id: reactionCounter |
||||
|
||||
anchors.verticalCenter: divider.verticalCenter |
||||
text: modelData.count |
||||
font: reaction.font |
||||
color: reaction.hovered ? colors.highlight : colors.text |
||||
} |
||||
|
||||
} |
||||
|
||||
background: Rectangle { |
||||
anchors.centerIn: parent |
||||
implicitWidth: reaction.implicitWidth |
||||
implicitHeight: reaction.implicitHeight |
||||
border.color: (reaction.hovered || modelData.selfReactedEvent !== '') ? colors.highlight : colors.text |
||||
color: modelData.selfReactedEvent !== '' ? Qt.hsla(highlightHue, highlightSat, highlightLight, 0.2) : colors.base |
||||
border.width: 1 |
||||
radius: reaction.height / 2 |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
||||
|
||||
} |
@ -1,108 +0,0 @@ |
||||
/* |
||||
* Copyright (C) 2016 Michael Bohlender, <michael.bohlender@kdemail.net> |
||||
* Copyright (C) 2017 Christian Mollekopf, <mollekopf@kolabsystems.com> |
||||
* |
||||
* This program is free software; you can redistribute it and/or modify |
||||
* it under the terms of the GNU General Public License as published by |
||||
* the Free Software Foundation; either version 2 of the License, or |
||||
* (at your option) any later version. |
||||
* |
||||
* This program is distributed in the hope that it will be useful, |
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
* GNU General Public License for more details. |
||||
* |
||||
* You should have received a copy of the GNU General Public License along |
||||
* with this program; if not, write to the Free Software Foundation, Inc., |
||||
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||||
*/ |
||||
/* |
||||
* Shamelessly stolen from: |
||||
* https://cgit.kde.org/kube.git/tree/framework/qml/ScrollHelper.qml |
||||
* |
||||
* The MouseArea + interactive: false + maximumFlickVelocity are required |
||||
* to fix scrolling for desktop systems where we don't want flicking behaviour. |
||||
* |
||||
* See also: |
||||
* ScrollView.qml in qtquickcontrols |
||||
* qquickwheelarea.cpp in qtquickcontrols |
||||
*/ |
||||
|
||||
import QtQuick 2.9 |
||||
import QtQuick.Controls 2.3 |
||||
|
||||
MouseArea { |
||||
// console.warn("Delta: ", wheel.pixelDelta.y); |
||||
// console.warn("Old position: ", flickable.contentY); |
||||
// console.warn("New position: ", newPos); |
||||
|
||||
id: root |
||||
|
||||
property Flickable flickable |
||||
property alias enabled: root.enabled |
||||
|
||||
function calculateNewPosition(flickableItem, wheel) { |
||||
//Nothing to scroll |
||||
if (flickableItem.contentHeight < flickableItem.height) |
||||
return flickableItem.contentY; |
||||
|
||||
//Ignore 0 events (happens at least with Christians trackpad) |
||||
if (wheel.pixelDelta.y == 0 && wheel.angleDelta.y == 0) |
||||
return flickableItem.contentY; |
||||
|
||||
//pixelDelta seems to be the same as angleDelta/8 |
||||
var pixelDelta = 0; |
||||
//The pixelDelta is a smaller number if both are provided, so pixelDelta can be 0 while angleDelta is still something. So we check the angleDelta |
||||
if (wheel.angleDelta.y) { |
||||
var wheelScrollLines = 3; //Default value of QApplication wheelScrollLines property |
||||
var pixelPerLine = 20; //Default value in Qt, originally comes from QTextEdit |
||||
var ticks = (wheel.angleDelta.y / 8) / 15; //Divide by 8 gives us pixels typically come in 15pixel steps. |
||||
pixelDelta = ticks * pixelPerLine * wheelScrollLines; |
||||
} else { |
||||
pixelDelta = wheel.pixelDelta.y; |
||||
} |
||||
pixelDelta = Math.round(pixelDelta); |
||||
if (!pixelDelta) |
||||
return flickableItem.contentY; |
||||
|
||||
var minYExtent = flickableItem.originY + flickableItem.topMargin; |
||||
var maxYExtent = (flickableItem.contentHeight + flickableItem.bottomMargin + flickableItem.originY) - flickableItem.height; |
||||
if (typeof (flickableItem.headerItem) !== "undefined" && flickableItem.headerItem) |
||||
minYExtent += flickableItem.headerItem.height; |
||||
|
||||
//Avoid overscrolling |
||||
return Math.max(minYExtent, Math.min(maxYExtent, flickableItem.contentY - pixelDelta)); |
||||
} |
||||
|
||||
propagateComposedEvents: true |
||||
//Place the mouse area under the flickable |
||||
z: -1 |
||||
onFlickableChanged: { |
||||
if (enabled) { |
||||
flickable.maximumFlickVelocity = 100000; |
||||
flickable.boundsBehavior = Flickable.StopAtBounds; |
||||
root.parent = flickable; |
||||
} |
||||
} |
||||
acceptedButtons: Qt.NoButton |
||||
onWheel: { |
||||
var newPos = calculateNewPosition(flickable, wheel); |
||||
// Show the scrollbars |
||||
flickable.flick(0, 0); |
||||
flickable.contentY = newPos; |
||||
cancelFlickStateTimer.start(); |
||||
} |
||||
|
||||
Timer { |
||||
id: cancelFlickStateTimer |
||||
|
||||
//How long the scrollbar will remain visible |
||||
interval: 500 |
||||
// Hide the scrollbars |
||||
onTriggered: { |
||||
flickable.cancelFlick(); |
||||
flickable.movementEnded(); |
||||
} |
||||
} |
||||
|
||||
} |
@ -1,175 +0,0 @@ |
||||
import "./device-verification" |
||||
import QtQuick 2.9 |
||||
import QtQuick.Controls 2.3 |
||||
import QtQuick.Layouts 1.2 |
||||
import QtQuick.Window 2.3 |
||||
import im.nheko 1.0 |
||||
|
||||
ApplicationWindow { |
||||
id: userProfileDialog |
||||
|
||||
property var profile |
||||
|
||||
height: 650 |
||||
width: 420 |
||||
minimumHeight: 420 |
||||
palette: colors |
||||
|
||||
Component { |
||||
id: deviceVerificationDialog |
||||
|
||||
DeviceVerification { |
||||
} |
||||
|
||||
} |
||||
|
||||
ColumnLayout { |
||||
id: contentL |
||||
|
||||
anchors.fill: parent |
||||
anchors.margins: 10 |
||||
spacing: 10 |
||||
|
||||
Avatar { |
||||
url: profile.avatarUrl.replace("mxc://", "image://MxcImage/") |
||||
height: 130 |
||||
width: 130 |
||||
displayName: profile.displayName |
||||
userid: profile.userid |
||||
Layout.alignment: Qt.AlignHCenter |
||||
} |
||||
|
||||
Label { |
||||
text: profile.displayName |
||||
fontSizeMode: Text.HorizontalFit |
||||
font.pixelSize: 20 |
||||
color: TimelineManager.userColor(profile.userid, colors.window) |
||||
font.bold: true |
||||
Layout.alignment: Qt.AlignHCenter |
||||
} |
||||
|
||||
MatrixText { |
||||
text: profile.userid |
||||
font.pixelSize: 15 |
||||
Layout.alignment: Qt.AlignHCenter |
||||
} |
||||
|
||||
Button { |
||||
id: verifyUserButton |
||||
|
||||
text: qsTr("Verify") |
||||
Layout.alignment: Qt.AlignHCenter |
||||
enabled: !profile.isUserVerified |
||||
visible: !profile.isUserVerified |
||||
onClicked: profile.verify() |
||||
} |
||||
|
||||
RowLayout { |
||||
Layout.alignment: Qt.AlignHCenter |
||||
spacing: 8 |
||||
|
||||
ImageButton { |
||||
image: ":/icons/icons/ui/do-not-disturb-rounded-sign.png" |
||||
hoverEnabled: true |
||||
ToolTip.visible: hovered |
||||
ToolTip.text: qsTr("Ban the user") |
||||
onClicked: profile.banUser() |
||||
} |
||||
// ImageButton{ |
||||
|
||||
// image:":/icons/icons/ui/volume-off-indicator.png" |
||||
// Layout.margins: { |
||||
// left: 5 |
||||
// right: 5 |
||||
// } |
||||
// ToolTip.visible: hovered |
||||
// ToolTip.text: qsTr("Ignore messages from this user") |
||||
// onClicked : { |
||||
// profile.ignoreUser() |
||||
// } |
||||
// } |
||||
ImageButton { |
||||
image: ":/icons/icons/ui/black-bubble-speech.png" |
||||
hoverEnabled: true |
||||
ToolTip.visible: hovered |
||||
ToolTip.text: qsTr("Start a private chat") |
||||
onClicked: profile.startChat() |
||||
} |
||||
|
||||
ImageButton { |
||||
image: ":/icons/icons/ui/round-remove-button.png" |
||||
hoverEnabled: true |
||||
ToolTip.visible: hovered |
||||
ToolTip.text: qsTr("Kick the user") |
||||
onClicked: profile.kickUser() |
||||
} |
||||
|
||||
} |
||||
|
||||
ListView { |
||||
id: devicelist |
||||
|
||||
Layout.fillHeight: true |
||||
Layout.minimumHeight: 200 |
||||
Layout.fillWidth: true |
||||
clip: true |
||||
spacing: 8 |
||||
boundsBehavior: Flickable.StopAtBounds |
||||
model: profile.deviceList |
||||
|
||||
delegate: RowLayout { |
||||
width: devicelist.width |
||||
spacing: 4 |
||||
|
||||
ColumnLayout { |
||||
spacing: 0 |
||||
|
||||
Text { |
||||
Layout.fillWidth: true |
||||
Layout.alignment: Qt.AlignLeft |
||||
elide: Text.ElideRight |
||||
font.bold: true |
||||
color: colors.text |
||||
text: model.deviceId |
||||
} |
||||
|
||||
Text { |
||||
Layout.fillWidth: true |
||||
Layout.alignment: Qt.AlignRight |
||||
elide: Text.ElideRight |
||||
color: colors.text |
||||
text: model.deviceName |
||||
} |
||||
|
||||
} |
||||
|
||||
Image { |
||||
Layout.preferredHeight: 16 |
||||
Layout.preferredWidth: 16 |
||||
source: ((model.verificationStatus == VerificationStatus.VERIFIED) ? "image://colorimage/:/icons/icons/ui/lock.png?green" : ((model.verificationStatus == VerificationStatus.UNVERIFIED) ? "image://colorimage/:/icons/icons/ui/unlock.png?yellow" : "image://colorimage/:/icons/icons/ui/unlock.png?red")) |
||||
} |
||||
|
||||
Button { |
||||
id: verifyButton |
||||
|
||||
text: (model.verificationStatus != VerificationStatus.VERIFIED) ? "Verify" : "Unverify" |
||||
onClicked: { |
||||
if (model.verificationStatus == VerificationStatus.VERIFIED) |
||||
profile.unverify(model.deviceId); |
||||
else |
||||
profile.verify(model.deviceId); |
||||
} |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
||||
|
||||
} |
||||
|
||||
footer: DialogButtonBox { |
||||
standardButtons: DialogButtonBox.Ok |
||||
onAccepted: userProfileDialog.close() |
||||
} |
||||
|
||||
} |
@ -1,70 +1,28 @@ |
||||
import QtQuick 2.6 |
||||
|
||||
import im.nheko 1.0 |
||||
|
||||
Item { |
||||
property double tempWidth: Math.min(parent ? parent.width : undefined, model.data.width < 1 ? parent.width : model.data.width) |
||||
property double tempWidth: Math.min(parent ? parent.width : undefined, model.data.width) |
||||
property double tempHeight: tempWidth * model.data.proportionalHeight |
||||
property double divisor: model.isReply ? 4 : 2 |
||||
property bool tooHigh: tempHeight > timelineRoot.height / divisor |
||||
|
||||
height: Math.round(tooHigh ? timelineRoot.height / divisor : tempHeight) |
||||
width: Math.round(tooHigh ? (timelineRoot.height / divisor) / model.data.proportionalHeight : tempWidth) |
||||
|
||||
Image { |
||||
id: blurhash |
||||
property bool tooHigh: tempHeight > timelineRoot.height / 2 |
||||
|
||||
anchors.fill: parent |
||||
visible: img.status != Image.Ready |
||||
source: model.data.blurhash ? ("image://blurhash/" + model.data.blurhash) : ("image://colorimage/:/icons/icons/ui/do-not-disturb-rounded-sign@2x.png?" + colors.buttonText) |
||||
asynchronous: true |
||||
fillMode: Image.PreserveAspectFit |
||||
sourceSize.width: parent.width |
||||
sourceSize.height: parent.height |
||||
} |
||||
height: tooHigh ? timelineRoot.height / 2 : tempHeight |
||||
width: tooHigh ? (timelineRoot.height / 2) / model.data.proportionalHeight : tempWidth |
||||
|
||||
Image { |
||||
id: img |
||||
|
||||
anchors.fill: parent |
||||
|
||||
source: model.data.url.replace("mxc://", "image://MxcImage/") |
||||
asynchronous: true |
||||
fillMode: Image.PreserveAspectFit |
||||
|
||||
MouseArea { |
||||
id: mouseArea |
||||
enabled: model.data.type == MtxEvent.ImageMessage && img.status == Image.Ready |
||||
hoverEnabled: true |
||||
enabled: model.data.type == MtxEvent.ImageMessage |
||||
anchors.fill: parent |
||||
onClicked: TimelineManager.openImageOverlay(model.data.url, model.data.id) |
||||
} |
||||
|
||||
Item { |
||||
id: overlay |
||||
|
||||
anchors.fill: parent |
||||
visible: mouseArea.containsMouse |
||||
|
||||
Rectangle { |
||||
id: container |
||||
|
||||
width: parent.width |
||||
implicitHeight: imgcaption.implicitHeight |
||||
anchors.bottom: overlay.bottom |
||||
color: colors.window |
||||
opacity: 0.75 |
||||
} |
||||
|
||||
Text { |
||||
id: imgcaption |
||||
|
||||
anchors.fill: container |
||||
elide: Text.ElideMiddle |
||||
horizontalAlignment: Text.AlignHCenter |
||||
verticalAlignment: Text.AlignVCenter |
||||
// See this MSC: https://github.com/matrix-org/matrix-doc/pull/2530 |
||||
text: model.data.filename ? model.data.filename : model.data.body |
||||
color: colors.text |
||||
} |
||||
onClicked: timelineManager.openImageOverlay(model.data.url, model.data.id) |
||||
} |
||||
} |
||||
} |
||||
|
@ -1,6 +1,4 @@ |
||||
TextMessage { |
||||
font.italic: true |
||||
color: colors.buttonText |
||||
height: isReply ? Math.min(chat.height / 8, implicitHeight) : undefined |
||||
clip: true |
||||
color: inactiveColors.text |
||||
} |
||||
|
@ -1,12 +1,7 @@ |
||||
import ".." |
||||
import im.nheko 1.0 |
||||
|
||||
MatrixText { |
||||
property string formatted: model.data.formattedBody |
||||
|
||||
text: "<style type=\"text/css\">a { color:" + colors.link + ";}</style>" + formatted.replace("<pre>", "<pre style='white-space: pre-wrap'>") |
||||
text: formatted.replace("<pre>", "<pre style='white-space: pre-wrap'>") |
||||
width: parent ? parent.width : undefined |
||||
height: isReply ? Math.round(Math.min(timelineRoot.height / 8, implicitHeight)) : undefined |
||||
clip: true |
||||
font.pointSize: (Settings.enlargeEmojiOnlyMessages && model.data.isOnlyEmoji > 0 && model.data.isOnlyEmoji < 4) ? Settings.fontSize * 3 : Settings.fontSize |
||||
} |
||||
|
@ -1,46 +0,0 @@ |
||||
import QtQuick 2.3 |
||||
import QtQuick.Controls 2.10 |
||||
import QtQuick.Layouts 1.10 |
||||
import im.nheko 1.0 |
||||
|
||||
Pane { |
||||
property string title: qsTr("Awaiting Confirmation") |
||||
|
||||
ColumnLayout { |
||||
spacing: 16 |
||||
|
||||
Label { |
||||
id: content |
||||
|
||||
Layout.maximumWidth: 400 |
||||
Layout.fillHeight: true |
||||
Layout.fillWidth: true |
||||
wrapMode: Text.Wrap |
||||
text: qsTr("Waiting for other side to complete verification.") |
||||
color: colors.text |
||||
verticalAlignment: Text.AlignVCenter |
||||
} |
||||
|
||||
BusyIndicator { |
||||
Layout.alignment: Qt.AlignHCenter |
||||
} |
||||
|
||||
RowLayout { |
||||
Button { |
||||
Layout.alignment: Qt.AlignLeft |
||||
text: qsTr("Cancel") |
||||
onClicked: { |
||||
flow.cancel(); |
||||
dialog.close(); |
||||
} |
||||
} |
||||
|
||||
Item { |
||||
Layout.fillWidth: true |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
||||
|
||||
} |
@ -1,144 +0,0 @@ |
||||
import QtQuick 2.10 |
||||
import QtQuick.Controls 2.10 |
||||
import QtQuick.Window 2.10 |
||||
import im.nheko 1.0 |
||||
|
||||
ApplicationWindow { |
||||
id: dialog |
||||
|
||||
property var flow |
||||
|
||||
onClosing: TimelineManager.removeVerificationFlow(flow) |
||||
title: stack.currentItem.title |
||||
flags: Qt.Dialog |
||||
palette: colors |
||||
height: stack.implicitHeight |
||||
width: stack.implicitWidth |
||||
|
||||
StackView { |
||||
id: stack |
||||
|
||||
initialItem: newVerificationRequest |
||||
implicitWidth: currentItem.implicitWidth |
||||
implicitHeight: currentItem.implicitHeight |
||||
} |
||||
|
||||
Component { |
||||
id: newVerificationRequest |
||||
|
||||
NewVerificationRequest { |
||||
} |
||||
|
||||
} |
||||
|
||||
Component { |
||||
id: waiting |
||||
|
||||
Waiting { |
||||
} |
||||
|
||||
} |
||||
|
||||
Component { |
||||
id: success |
||||
|
||||
Success { |
||||
} |
||||
|
||||
} |
||||
|
||||
Component { |
||||
id: failed |
||||
|
||||
Failed { |
||||
} |
||||
|
||||
} |
||||
|
||||
Component { |
||||
id: digitVerification |
||||
|
||||
DigitVerification { |
||||
} |
||||
|
||||
} |
||||
|
||||
Component { |
||||
id: emojiVerification |
||||
|
||||
EmojiVerification { |
||||
} |
||||
|
||||
} |
||||
|
||||
Item { |
||||
state: flow.state |
||||
states: [ |
||||
State { |
||||
name: "PromptStartVerification" |
||||
|
||||
StateChangeScript { |
||||
script: stack.replace(newVerificationRequest) |
||||
} |
||||
|
||||
}, |
||||
State { |
||||
name: "CompareEmoji" |
||||
|
||||
StateChangeScript { |
||||
script: stack.replace(emojiVerification) |
||||
} |
||||
|
||||
}, |
||||
State { |
||||
name: "CompareNumber" |
||||
|
||||
StateChangeScript { |
||||
script: stack.replace(digitVerification) |
||||
} |
||||
|
||||
}, |
||||
State { |
||||
name: "WaitingForKeys" |
||||
|
||||
StateChangeScript { |
||||
script: stack.replace(waiting) |
||||
} |
||||
|
||||
}, |
||||
State { |
||||
name: "WaitingForOtherToAccept" |
||||
|
||||
StateChangeScript { |
||||
script: stack.replace(waiting) |
||||
} |
||||
|
||||
}, |
||||
State { |
||||
name: "WaitingForMac" |
||||
|
||||
StateChangeScript { |
||||
script: stack.replace(waiting) |
||||
} |
||||
|
||||
}, |
||||
State { |
||||
name: "Success" |
||||
|
||||
StateChangeScript { |
||||
script: stack.replace(success) |
||||
} |
||||
|
||||
}, |
||||
State { |
||||
name: "Failed" |
||||
|
||||
StateChangeScript { |
||||
script: stack.replace(failed) |
||||
} |
||||
|
||||
} |
||||
] |
||||
} |
||||
|
||||
} |
@ -1,69 +0,0 @@ |
||||
import QtQuick 2.3 |
||||
import QtQuick.Controls 2.10 |
||||
import QtQuick.Layouts 1.10 |
||||
import im.nheko 1.0 |
||||
|
||||
Pane { |
||||
property string title: qsTr("Verification Code") |
||||
|
||||
ColumnLayout { |
||||
spacing: 16 |
||||
|
||||
Label { |
||||
Layout.maximumWidth: 400 |
||||
Layout.fillHeight: true |
||||
Layout.fillWidth: true |
||||
wrapMode: Text.Wrap |
||||
text: qsTr("Please verify the following digits. You should see the same numbers on both sides. If they differ, please press 'They do not match!' to abort verification!") |
||||
color: colors.text |
||||
verticalAlignment: Text.AlignVCenter |
||||
} |
||||
|
||||
RowLayout { |
||||
Layout.alignment: Qt.AlignHCenter |
||||
|
||||
Label { |
||||
font.pixelSize: Qt.application.font.pixelSize * 2 |
||||
text: flow.sasList[0] |
||||
color: colors.text |
||||
} |
||||
|
||||
Label { |
||||
font.pixelSize: Qt.application.font.pixelSize * 2 |
||||
text: flow.sasList[1] |
||||
color: colors.text |
||||
} |
||||
|
||||
Label { |
||||
font.pixelSize: Qt.application.font.pixelSize * 2 |
||||
text: flow.sasList[2] |
||||
color: colors.text |
||||
} |
||||
|
||||
} |
||||
|
||||
RowLayout { |
||||
Button { |
||||
Layout.alignment: Qt.AlignLeft |
||||
text: qsTr("They do not match!") |
||||
onClicked: { |
||||
flow.cancel(); |
||||
dialog.close(); |
||||
} |
||||
} |
||||
|
||||
Item { |
||||
Layout.fillWidth: true |
||||
} |
||||
|
||||
Button { |
||||
Layout.alignment: Qt.AlignRight |
||||
text: qsTr("They match!") |
||||
onClicked: flow.next() |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
||||
|
||||
} |
@ -1,33 +0,0 @@ |
||||
import QtQuick 2.3 |
||||
import QtQuick.Controls 2.10 |
||||
import QtQuick.Layouts 1.10 |
||||
|
||||
Rectangle { |
||||
color: "red" |
||||
implicitHeight: Qt.application.font.pixelSize * 4 |
||||
implicitWidth: col.width |
||||
height: Qt.application.font.pixelSize * 4 |
||||
width: col.width |
||||
|
||||
ColumnLayout { |
||||
id: col |
||||
|
||||
property var emoji: emojis.mapping[Math.floor(Math.random() * 64)] |
||||
|
||||
anchors.bottom: parent.bottom |
||||
|
||||
Label { |
||||
height: font.pixelSize * 2 |
||||
Layout.alignment: Qt.AlignHCenter |
||||
text: col.emoji.emoji |
||||
font.pixelSize: Qt.application.font.pixelSize * 2 |
||||
} |
||||
|
||||
Label { |
||||
Layout.alignment: Qt.AlignHCenter | Qt.AlignBottom |
||||
text: col.emoji.description |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
@ -1,56 +0,0 @@ |
||||
import QtQuick 2.3 |
||||
import QtQuick.Controls 2.10 |
||||
import QtQuick.Layouts 1.10 |
||||
import im.nheko 1.0 |
||||
|
||||
Pane { |
||||
property string title: qsTr("Verification failed") |
||||
|
||||
ColumnLayout { |
||||
spacing: 16 |
||||
|
||||
Text { |
||||
id: content |
||||
|
||||
Layout.maximumWidth: 400 |
||||
Layout.fillHeight: true |
||||
Layout.fillWidth: true |
||||
wrapMode: Text.Wrap |
||||
text: { |
||||
switch (flow.error) { |
||||
case DeviceVerificationFlow.UnknownMethod: |
||||
return qsTr("Other client does not support our verification protocol."); |
||||
case DeviceVerificationFlow.MismatchedCommitment: |
||||
case DeviceVerificationFlow.MismatchedSAS: |
||||
case DeviceVerificationFlow.KeyMismatch: |
||||
return qsTr("Key mismatch detected!"); |
||||
case DeviceVerificationFlow.Timeout: |
||||
return qsTr("Device verification timed out."); |
||||
case DeviceVerificationFlow.User: |
||||
return qsTr("Other party canceled the verification."); |
||||
case DeviceVerificationFlow.OutOfOrder: |
||||
return qsTr("Device verification timed out."); |
||||
default: |
||||
return "Unknown verification error."; |
||||
} |
||||
} |
||||
color: colors.text |
||||
verticalAlignment: Text.AlignVCenter |
||||
} |
||||
|
||||
RowLayout { |
||||
Item { |
||||
Layout.fillWidth: true |
||||
} |
||||
|
||||
Button { |
||||
Layout.alignment: Qt.AlignRight |
||||
text: qsTr("Close") |
||||
onClicked: dialog.close() |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
||||
|
||||
} |
@ -1,46 +0,0 @@ |
||||
import QtQuick 2.3 |
||||
import QtQuick.Controls 2.10 |
||||
import QtQuick.Layouts 1.10 |
||||
import im.nheko 1.0 |
||||
|
||||
Pane { |
||||
property string title: flow.sender ? qsTr("Send Device Verification Request") : qsTr("Recieved Device Verification Request") |
||||
|
||||
ColumnLayout { |
||||
spacing: 16 |
||||
|
||||
Label { |
||||
Layout.maximumWidth: 400 |
||||
Layout.fillHeight: true |
||||
Layout.fillWidth: true |
||||
wrapMode: Text.Wrap |
||||
text: flow.sender ? qsTr("To ensure that no malicious user can eavesdrop on your encrypted communications, you can verify this device.") : qsTr("The device was requested to be verified") |
||||
color: colors.text |
||||
verticalAlignment: Text.AlignVCenter |
||||
} |
||||
|
||||
RowLayout { |
||||
Button { |
||||
Layout.alignment: Qt.AlignLeft |
||||
text: flow.sender ? qsTr("Cancel") : qsTr("Deny") |
||||
onClicked: { |
||||
flow.cancel(); |
||||
dialog.close(); |
||||
} |
||||
} |
||||
|
||||
Item { |
||||
Layout.fillWidth: true |
||||
} |
||||
|
||||
Button { |
||||
Layout.alignment: Qt.AlignRight |
||||
text: flow.sender ? qsTr("Start verification") : qsTr("Accept") |
||||
onClicked: flow.next() |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
||||
|
||||
} |
@ -1,38 +0,0 @@ |
||||
import QtQuick 2.3 |
||||
import QtQuick.Controls 2.10 |
||||
import QtQuick.Layouts 1.10 |
||||
|
||||
Pane { |
||||
property string title: qsTr("Successful Verification") |
||||
|
||||
ColumnLayout { |
||||
spacing: 16 |
||||
|
||||
Label { |
||||
id: content |
||||
|
||||
Layout.maximumWidth: 400 |
||||
Layout.fillHeight: true |
||||
Layout.fillWidth: true |
||||
wrapMode: Text.Wrap |
||||
text: qsTr("Verification successful! Both sides verified their devices!") |
||||
color: colors.text |
||||
verticalAlignment: Text.AlignVCenter |
||||
} |
||||
|
||||
RowLayout { |
||||
Item { |
||||
Layout.fillWidth: true |
||||
} |
||||
|
||||
Button { |
||||
Layout.alignment: Qt.AlignRight |
||||
text: qsTr("Close") |
||||
onClicked: dialog.close() |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
||||
|
||||
} |
@ -1,56 +0,0 @@ |
||||
import QtQuick 2.3 |
||||
import QtQuick.Controls 2.10 |
||||
import QtQuick.Layouts 1.10 |
||||
import im.nheko 1.0 |
||||
|
||||
Pane { |
||||
property string title: qsTr("Waiting for other party") |
||||
|
||||
ColumnLayout { |
||||
spacing: 16 |
||||
|
||||
Label { |
||||
id: content |
||||
|
||||
Layout.maximumWidth: 400 |
||||
Layout.fillHeight: true |
||||
Layout.fillWidth: true |
||||
wrapMode: Text.Wrap |
||||
text: { |
||||
switch (flow.state) { |
||||
case "WaitingForOtherToAccept": |
||||
return qsTr("Waiting for other side to accept the verification request."); |
||||
case "WaitingForKeys": |
||||
return qsTr("Waiting for other side to continue the verification request."); |
||||
case "WaitingForMac": |
||||
return qsTr("Waiting for other side to complete the verification request."); |
||||
} |
||||
} |
||||
color: colors.text |
||||
verticalAlignment: Text.AlignVCenter |
||||
} |
||||
|
||||
BusyIndicator { |
||||
Layout.alignment: Qt.AlignHCenter |
||||
palette: colors |
||||
} |
||||
|
||||
RowLayout { |
||||
Button { |
||||
Layout.alignment: Qt.AlignLeft |
||||
text: qsTr("Cancel") |
||||
onClicked: { |
||||
flow.cancel(); |
||||
dialog.close(); |
||||
} |
||||
} |
||||
|
||||
Item { |
||||
Layout.fillWidth: true |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
||||
|
||||
} |
@ -1,16 +0,0 @@ |
||||
import "../" |
||||
import QtQuick 2.10 |
||||
import QtQuick.Controls 2.1 |
||||
import im.nheko 1.0 |
||||
import im.nheko.EmojiModel 1.0 |
||||
|
||||
ImageButton { |
||||
id: emojiButton |
||||
|
||||
property var colors: currentActivePalette |
||||
property var emojiPicker |
||||
property string event_id |
||||
|
||||
image: ":/icons/icons/ui/smile.png" |
||||
onClicked: emojiPicker.visible ? emojiPicker.close() : emojiPicker.show(emojiButton, event_id) |
||||
} |
@ -1,332 +0,0 @@ |
||||
import "../" |
||||
import QtGraphicalEffects 1.0 |
||||
import QtQuick 2.9 |
||||
import QtQuick.Controls 2.3 |
||||
import QtQuick.Layouts 1.3 |
||||
import im.nheko 1.0 |
||||
import im.nheko.EmojiModel 1.0 |
||||
|
||||
Popup { |
||||
id: emojiPopup |
||||
|
||||
property string event_id |
||||
property var colors |
||||
property alias model: gridView.model |
||||
property var textArea |
||||
property string emojiCategory: "people" |
||||
property real highlightHue: colors.highlight.hslHue |
||||
property real highlightSat: colors.highlight.hslSaturation |
||||
property real highlightLight: colors.highlight.hslLightness |
||||
|
||||
function show(showAt, event_id) { |
||||
console.debug("Showing emojiPicker for " + event_id); |
||||
if (showAt) { |
||||
parent = showAt; |
||||
x = Math.round((showAt.width - width) / 2); |
||||
y = showAt.height; |
||||
} |
||||
emojiPopup.event_id = event_id; |
||||
open(); |
||||
} |
||||
|
||||
margins: 0 |
||||
bottomPadding: 1 |
||||
leftPadding: 1 |
||||
rightPadding: 1 |
||||
modal: true |
||||
focus: true |
||||
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside |
||||
|
||||
ColumnLayout { |
||||
id: columnView |
||||
|
||||
anchors.fill: parent |
||||
spacing: 0 |
||||
Layout.bottomMargin: 0 |
||||
Layout.leftMargin: 3 |
||||
Layout.rightMargin: 3 |
||||
Layout.topMargin: 2 |
||||
|
||||
// emoji grid |
||||
GridView { |
||||
id: gridView |
||||
|
||||
Layout.preferredHeight: emojiPopup.height |
||||
Layout.fillWidth: true |
||||
Layout.fillHeight: true |
||||
Layout.leftMargin: 4 |
||||
cellWidth: 52 |
||||
cellHeight: 52 |
||||
boundsBehavior: Flickable.StopAtBounds |
||||
clip: true |
||||
currentIndex: -1 // prevent sorting from stealing focus |
||||
|
||||
// Individual emoji |
||||
delegate: AbstractButton { |
||||
width: 48 |
||||
height: 48 |
||||
hoverEnabled: true |
||||
ToolTip.text: model.shortName |
||||
ToolTip.visible: hovered |
||||
// TODO: maybe add favorites at some point? |
||||
onClicked: { |
||||
console.debug("Picked " + model.unicode + "in response to " + emojiPopup.event_id); |
||||
emojiPopup.close(); |
||||
TimelineManager.queueReactionMessage(emojiPopup.event_id, model.unicode); |
||||
} |
||||
|
||||
// give the emoji a little oomf |
||||
DropShadow { |
||||
width: parent.width |
||||
height: parent.height |
||||
horizontalOffset: 3 |
||||
verticalOffset: 3 |
||||
radius: 8 |
||||
samples: 17 |
||||
color: "#80000000" |
||||
source: parent.contentItem |
||||
} |
||||
|
||||
contentItem: Text { |
||||
horizontalAlignment: Text.AlignHCenter |
||||
verticalAlignment: Text.AlignVCenter |
||||
font.family: Settings.emojiFont |
||||
font.pixelSize: 36 |
||||
text: model.unicode |
||||
} |
||||
|
||||
background: Rectangle { |
||||
anchors.fill: parent |
||||
color: hovered ? colors.highlight : 'transparent' |
||||
radius: 5 |
||||
} |
||||
|
||||
} |
||||
|
||||
// Search field |
||||
header: TextField { |
||||
id: emojiSearch |
||||
|
||||
anchors.left: parent.left |
||||
anchors.right: parent.right |
||||
anchors.rightMargin: emojiScroll.width + 4 |
||||
placeholderText: qsTr("Search") |
||||
selectByMouse: true |
||||
rightPadding: clearSearch.width |
||||
onTextChanged: searchTimer.restart() |
||||
onVisibleChanged: { |
||||
if (visible) |
||||
forceActiveFocus(); |
||||
|
||||
} |
||||
|
||||
Timer { |
||||
id: searchTimer |
||||
|
||||
interval: 350 // tweak as needed? |
||||
onTriggered: { |
||||
emojiPopup.model.filter = emojiSearch.text; |
||||
emojiPopup.model.category = EmojiCategory.Search; |
||||
} |
||||
} |
||||
|
||||
ToolButton { |
||||
id: clearSearch |
||||
|
||||
visible: emojiSearch.text !== '' |
||||
icon.source: "image://colorimage/:/icons/icons/ui/round-remove-button.png?" + (clearSearch.hovered ? colors.highlight : colors.buttonText) |
||||
focusPolicy: Qt.NoFocus |
||||
onClicked: emojiSearch.clear() |
||||
|
||||
anchors { |
||||
verticalCenter: parent.verticalCenter |
||||
right: parent.right |
||||
} |
||||
// clear the default hover effects. |
||||
|
||||
background: Item { |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
||||
|
||||
ScrollBar.vertical: ScrollBar { |
||||
id: emojiScroll |
||||
} |
||||
|
||||
} |
||||
|
||||
// Separator |
||||
Rectangle { |
||||
Layout.fillWidth: true |
||||
Layout.preferredHeight: 1 |
||||
color: emojiPopup.colors.dark |
||||
} |
||||
|
||||
// Category picker row |
||||
RowLayout { |
||||
Layout.bottomMargin: 0 |
||||
Layout.preferredHeight: 42 |
||||
implicitHeight: 42 |
||||
Layout.alignment: Qt.AlignHCenter | Qt.AlignBottom |
||||
|
||||
// Display the normal categories |
||||
Repeater { |
||||
|
||||
model: ListModel { |
||||
// TODO: Would like to get 'simple' icons for the categories |
||||
ListElement { |
||||
image: ":/icons/icons/emoji-categories/people.png" |
||||
category: EmojiCategory.People |
||||
} |
||||
|
||||
ListElement { |
||||
image: ":/icons/icons/emoji-categories/nature.png" |
||||
category: EmojiCategory.Nature |
||||
} |
||||
|
||||
ListElement { |
||||
image: ":/icons/icons/emoji-categories/foods.png" |
||||
category: EmojiCategory.Food |
||||
} |
||||
|
||||
ListElement { |
||||
image: ":/icons/icons/emoji-categories/activity.png" |
||||
category: EmojiCategory.Activity |
||||
} |
||||
|
||||
ListElement { |
||||
image: ":/icons/icons/emoji-categories/travel.png" |
||||
category: EmojiCategory.Travel |
||||
} |
||||
|
||||
ListElement { |
||||
image: ":/icons/icons/emoji-categories/objects.png" |
||||
category: EmojiCategory.Objects |
||||
} |
||||
|
||||
ListElement { |
||||
image: ":/icons/icons/emoji-categories/symbols.png" |
||||
category: EmojiCategory.Symbols |
||||
} |
||||
|
||||
ListElement { |
||||
image: ":/icons/icons/emoji-categories/flags.png" |
||||
category: EmojiCategory.Flags |
||||
} |
||||
|
||||
} |
||||
|
||||
delegate: AbstractButton { |
||||
Layout.preferredWidth: 36 |
||||
Layout.preferredHeight: 36 |
||||
hoverEnabled: true |
||||
ToolTip.text: { |
||||
switch (model.category) { |
||||
case EmojiCategory.People: |
||||
return qsTr('People'); |
||||
case EmojiCategory.Nature: |
||||
return qsTr('Nature'); |
||||
case EmojiCategory.Food: |
||||
return qsTr('Food'); |
||||
case EmojiCategory.Activity: |
||||
return qsTr('Activity'); |
||||
case EmojiCategory.Travel: |
||||
return qsTr('Travel'); |
||||
case EmojiCategory.Objects: |
||||
return qsTr('Objects'); |
||||
case EmojiCategory.Symbols: |
||||
return qsTr('Symbols'); |
||||
case EmojiCategory.Flags: |
||||
return qsTr('Flags'); |
||||
} |
||||
} |
||||
ToolTip.visible: hovered |
||||
onClicked: { |
||||
emojiPopup.model.category = model.category; |
||||
} |
||||
|
||||
MouseArea { |
||||
id: mouseArea |
||||
|
||||
anchors.fill: parent |
||||
onPressed: mouse.accepted = false |
||||
cursorShape: Qt.PointingHandCursor |
||||
} |
||||
|
||||
contentItem: Image { |
||||
horizontalAlignment: Image.AlignHCenter |
||||
verticalAlignment: Image.AlignVCenter |
||||
fillMode: Image.Pad |
||||
sourceSize.width: 32 |
||||
sourceSize.height: 32 |
||||
source: "image://colorimage/" + model.image + "?" + (hovered ? colors.highlight : colors.buttonText) |
||||
} |
||||
|
||||
background: Rectangle { |
||||
anchors.fill: parent |
||||
color: emojiPopup.model.category === model.category ? Qt.hsla(highlightHue, highlightSat, highlightLight, 0.2) : 'transparent' |
||||
radius: 5 |
||||
border.color: emojiPopup.model.category === model.category ? colors.highlight : 'transparent' |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
||||
|
||||
// Separator |
||||
Rectangle { |
||||
Layout.fillHeight: true |
||||
Layout.preferredWidth: 1 |
||||
implicitWidth: 1 |
||||
height: parent.height |
||||
color: emojiPopup.colors.dark |
||||
} |
||||
|
||||
// Search Button is special |
||||
AbstractButton { |
||||
id: searchBtn |
||||
|
||||
hoverEnabled: true |
||||
Layout.alignment: Qt.AlignRight |
||||
Layout.bottomMargin: 0 |
||||
ToolTip.text: qsTr("Search") |
||||
ToolTip.visible: hovered |
||||
onClicked: { |
||||
// clear any filters |
||||
emojiPopup.model.category = EmojiCategory.Search; |
||||
gridView.positionViewAtBeginning(); |
||||
emojiSearch.forceActiveFocus(); |
||||
} |
||||
Layout.preferredWidth: 36 |
||||
Layout.preferredHeight: 36 |
||||
implicitWidth: 36 |
||||
implicitHeight: 36 |
||||
|
||||
MouseArea { |
||||
id: mouseArea |
||||
|
||||
anchors.fill: parent |
||||
onPressed: mouse.accepted = false |
||||
cursorShape: Qt.PointingHandCursor |
||||
} |
||||
|
||||
contentItem: Image { |
||||
anchors.right: parent.right |
||||
horizontalAlignment: Image.AlignHCenter |
||||
verticalAlignment: Image.AlignVCenter |
||||
sourceSize.width: 32 |
||||
sourceSize.height: 32 |
||||
fillMode: Image.Pad |
||||
smooth: true |
||||
source: "image://colorimage/:/icons/icons/ui/search.png?" + (parent.hovered ? colors.highlight : colors.buttonText) |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
||||
|
||||
} |
||||
|
||||
} |
@ -1,2 +0,0 @@ |
||||
[Controls] |
||||
FallbackStyle=Fusion |
@ -1,38 +0,0 @@ |
||||
#include "BlurhashProvider.h" |
||||
|
||||
#include <algorithm> |
||||
|
||||
#include <QUrl> |
||||
|
||||
#include "blurhash.hpp" |
||||
|
||||
void |
||||
BlurhashResponse::run() |
||||
{ |
||||
if (m_requestedSize.width() < 0 || m_requestedSize.height() < 0) { |
||||
m_error = QStringLiteral("Blurhash needs size request"); |
||||
emit finished(); |
||||
return; |
||||
} |
||||
if (m_requestedSize.width() == 0 || m_requestedSize.height() == 0) { |
||||
m_image = QImage(m_requestedSize, QImage::Format_RGB32); |
||||
m_image.fill(QColor(0, 0, 0)); |
||||
emit finished(); |
||||
return; |
||||
} |
||||
|
||||
auto decoded = blurhash::decode(QUrl::fromPercentEncoding(m_id.toUtf8()).toStdString(), |
||||
m_requestedSize.width(), |
||||
m_requestedSize.height(), |
||||
4); |
||||
if (decoded.image.empty()) { |
||||
m_error = QStringLiteral("Failed decode!"); |
||||
emit finished(); |
||||
return; |
||||
} |
||||
|
||||
QImage image(decoded.image.data(), decoded.width, decoded.height, QImage::Format_RGB32); |
||||
|
||||
m_image = image.copy(); |
||||
emit finished(); |
||||
} |
@ -1,51 +0,0 @@ |
||||
#pragma once |
||||
|
||||
#include <QQuickAsyncImageProvider> |
||||
#include <QQuickImageResponse> |
||||
|
||||
#include <QImage> |
||||
#include <QThreadPool> |
||||
|
||||
class BlurhashResponse |
||||
: public QQuickImageResponse |
||||
, public QRunnable |
||||
{ |
||||
public: |
||||
BlurhashResponse(const QString &id, const QSize &requestedSize) |
||||
|
||||
: m_id(id) |
||||
, m_requestedSize(requestedSize) |
||||
{ |
||||
setAutoDelete(false); |
||||
} |
||||
|
||||
QQuickTextureFactory *textureFactory() const override |
||||
{ |
||||
return QQuickTextureFactory::textureFactoryForImage(m_image); |
||||
} |
||||
QString errorString() const override { return m_error; } |
||||
|
||||
void run() override; |
||||
|
||||
QString m_id, m_error; |
||||
QSize m_requestedSize; |
||||
QImage m_image; |
||||
}; |
||||
|
||||
class BlurhashProvider |
||||
: public QObject |
||||
, public QQuickAsyncImageProvider |
||||
{ |
||||
Q_OBJECT |
||||
public slots: |
||||
QQuickImageResponse *requestImageResponse(const QString &id, |
||||
const QSize &requestedSize) override |
||||
{ |
||||
BlurhashResponse *response = new BlurhashResponse(id, requestedSize); |
||||
pool.start(response); |
||||
return response; |
||||
} |
||||
|
||||
private: |
||||
QThreadPool pool; |
||||
}; |
@ -1,457 +0,0 @@ |
||||
#include <algorithm> |
||||
#include <cctype> |
||||
#include <chrono> |
||||
#include <cstdint> |
||||
|
||||
#include <QMediaPlaylist> |
||||
#include <QUrl> |
||||
|
||||
#include "Cache.h" |
||||
#include "CallManager.h" |
||||
#include "ChatPage.h" |
||||
#include "Logging.h" |
||||
#include "MainWindow.h" |
||||
#include "MatrixClient.h" |
||||
#include "UserSettingsPage.h" |
||||
#include "WebRTCSession.h" |
||||
#include "dialogs/AcceptCall.h" |
||||
|
||||
#include "mtx/responses/turn_server.hpp" |
||||
|
||||
Q_DECLARE_METATYPE(std::vector<mtx::events::msg::CallCandidates::Candidate>) |
||||
Q_DECLARE_METATYPE(mtx::events::msg::CallCandidates::Candidate) |
||||
Q_DECLARE_METATYPE(mtx::responses::TurnServer) |
||||
|
||||
using namespace mtx::events; |
||||
using namespace mtx::events::msg; |
||||
|
||||
// https://github.com/vector-im/riot-web/issues/10173
|
||||
#define STUN_SERVER "stun://turn.matrix.org:3478"
|
||||
|
||||
namespace { |
||||
std::vector<std::string> |
||||
getTurnURIs(const mtx::responses::TurnServer &turnServer); |
||||
} |
||||
|
||||
CallManager::CallManager(QSharedPointer<UserSettings> userSettings) |
||||
: QObject() |
||||
, session_(WebRTCSession::instance()) |
||||
, turnServerTimer_(this) |
||||
, settings_(userSettings) |
||||
{ |
||||
qRegisterMetaType<std::vector<mtx::events::msg::CallCandidates::Candidate>>(); |
||||
qRegisterMetaType<mtx::events::msg::CallCandidates::Candidate>(); |
||||
qRegisterMetaType<mtx::responses::TurnServer>(); |
||||
|
||||
connect( |
||||
&session_, |
||||
&WebRTCSession::offerCreated, |
||||
this, |
||||
[this](const std::string &sdp, const std::vector<CallCandidates::Candidate> &candidates) { |
||||
nhlog::ui()->debug("WebRTC: call id: {} - sending offer", callid_); |
||||
emit newMessage(roomid_, CallInvite{callid_, sdp, 0, timeoutms_}); |
||||
emit newMessage(roomid_, CallCandidates{callid_, candidates, 0}); |
||||
QTimer::singleShot(timeoutms_, this, [this]() { |
||||
if (session_.state() == webrtc::State::OFFERSENT) { |
||||
hangUp(CallHangUp::Reason::InviteTimeOut); |
||||
emit ChatPage::instance()->showNotification( |
||||
"The remote side failed to pick up."); |
||||
} |
||||
}); |
||||
}); |
||||
|
||||
connect( |
||||
&session_, |
||||
&WebRTCSession::answerCreated, |
||||
this, |
||||
[this](const std::string &sdp, const std::vector<CallCandidates::Candidate> &candidates) { |
||||
nhlog::ui()->debug("WebRTC: call id: {} - sending answer", callid_); |
||||
emit newMessage(roomid_, CallAnswer{callid_, sdp, 0}); |
||||
emit newMessage(roomid_, CallCandidates{callid_, candidates, 0}); |
||||
}); |
||||
|
||||
connect(&session_, |
||||
&WebRTCSession::newICECandidate, |
||||
this, |
||||
[this](const CallCandidates::Candidate &candidate) { |
||||
nhlog::ui()->debug("WebRTC: call id: {} - sending ice candidate", callid_); |
||||
emit newMessage(roomid_, CallCandidates{callid_, {candidate}, 0}); |
||||
}); |
||||
|
||||
connect(&turnServerTimer_, &QTimer::timeout, this, &CallManager::retrieveTurnServer); |
||||
|
||||
connect(this, |
||||
&CallManager::turnServerRetrieved, |
||||
this, |
||||
[this](const mtx::responses::TurnServer &res) { |
||||
nhlog::net()->info("TURN server(s) retrieved from homeserver:"); |
||||
nhlog::net()->info("username: {}", res.username); |
||||
nhlog::net()->info("ttl: {} seconds", res.ttl); |
||||
for (const auto &u : res.uris) |
||||
nhlog::net()->info("uri: {}", u); |
||||
|
||||
// Request new credentials close to expiry
|
||||
// See https://tools.ietf.org/html/draft-uberti-behave-turn-rest-00
|
||||
turnURIs_ = getTurnURIs(res); |
||||
uint32_t ttl = std::max(res.ttl, UINT32_C(3600)); |
||||
if (res.ttl < 3600) |
||||
nhlog::net()->warn("Setting ttl to 1 hour"); |
||||
turnServerTimer_.setInterval(ttl * 1000 * 0.9); |
||||
}); |
||||
|
||||
connect(&session_, &WebRTCSession::stateChanged, this, [this](webrtc::State state) { |
||||
switch (state) { |
||||
case webrtc::State::DISCONNECTED: |
||||
playRingtone("qrc:/media/media/callend.ogg", false); |
||||
clear(); |
||||
break; |
||||
case webrtc::State::ICEFAILED: { |
||||
QString error("Call connection failed."); |
||||
if (turnURIs_.empty()) |
||||
error += " Your homeserver has no configured TURN server."; |
||||
emit ChatPage::instance()->showNotification(error); |
||||
hangUp(CallHangUp::Reason::ICEFailed); |
||||
break; |
||||
} |
||||
default: |
||||
break; |
||||
} |
||||
}); |
||||
|
||||
connect(&player_, |
||||
&QMediaPlayer::mediaStatusChanged, |
||||
this, |
||||
[this](QMediaPlayer::MediaStatus status) { |
||||
if (status == QMediaPlayer::LoadedMedia) |
||||
player_.play(); |
||||
}); |
||||
} |
||||
|
||||
void |
||||
CallManager::sendInvite(const QString &roomid) |
||||
{ |
||||
if (onActiveCall()) |
||||
return; |
||||
|
||||
auto roomInfo = cache::singleRoomInfo(roomid.toStdString()); |
||||
if (roomInfo.member_count != 2) { |
||||
emit ChatPage::instance()->showNotification( |
||||
"Voice calls are limited to 1:1 rooms."); |
||||
return; |
||||
} |
||||
|
||||
std::string errorMessage; |
||||
if (!session_.init(&errorMessage)) { |
||||
emit ChatPage::instance()->showNotification(QString::fromStdString(errorMessage)); |
||||
return; |
||||
} |
||||
|
||||
roomid_ = roomid; |
||||
session_.setStunServer(settings_->useStunServer() ? STUN_SERVER : ""); |
||||
session_.setTurnServers(turnURIs_); |
||||
|
||||
generateCallID(); |
||||
nhlog::ui()->debug("WebRTC: call id: {} - creating invite", callid_); |
||||
std::vector<RoomMember> members(cache::getMembers(roomid.toStdString())); |
||||
const RoomMember &callee = |
||||
members.front().user_id == utils::localUser() ? members.back() : members.front(); |
||||
callPartyName_ = callee.display_name.isEmpty() ? callee.user_id : callee.display_name; |
||||
callPartyAvatarUrl_ = QString::fromStdString(roomInfo.avatar_url); |
||||
emit newCallParty(); |
||||
playRingtone("qrc:/media/media/ringback.ogg", true); |
||||
if (!session_.createOffer()) { |
||||
emit ChatPage::instance()->showNotification("Problem setting up call."); |
||||
endCall(); |
||||
} |
||||
} |
||||
|
||||
namespace { |
||||
std::string |
||||
callHangUpReasonString(CallHangUp::Reason reason) |
||||
{ |
||||
switch (reason) { |
||||
case CallHangUp::Reason::ICEFailed: |
||||
return "ICE failed"; |
||||
case CallHangUp::Reason::InviteTimeOut: |
||||
return "Invite time out"; |
||||
default: |
||||
return "User"; |
||||
} |
||||
} |
||||
} |
||||
|
||||
void |
||||
CallManager::hangUp(CallHangUp::Reason reason) |
||||
{ |
||||
if (!callid_.empty()) { |
||||
nhlog::ui()->debug( |
||||
"WebRTC: call id: {} - hanging up ({})", callid_, callHangUpReasonString(reason)); |
||||
emit newMessage(roomid_, CallHangUp{callid_, 0, reason}); |
||||
endCall(); |
||||
} |
||||
} |
||||
|
||||
bool |
||||
CallManager::onActiveCall() const |
||||
{ |
||||
return session_.state() != webrtc::State::DISCONNECTED; |
||||
} |
||||
|
||||
void |
||||
CallManager::syncEvent(const mtx::events::collections::TimelineEvents &event) |
||||
{ |
||||
#ifdef GSTREAMER_AVAILABLE |
||||
if (handleEvent_<CallInvite>(event) || handleEvent_<CallCandidates>(event) || |
||||
handleEvent_<CallAnswer>(event) || handleEvent_<CallHangUp>(event)) |
||||
return; |
||||
#else |
||||
(void)event; |
||||
#endif |
||||
} |
||||
|
||||
template<typename T> |
||||
bool |
||||
CallManager::handleEvent_(const mtx::events::collections::TimelineEvents &event) |
||||
{ |
||||
if (std::holds_alternative<RoomEvent<T>>(event)) { |
||||
handleEvent(std::get<RoomEvent<T>>(event)); |
||||
return true; |
||||
} |
||||
return false; |
||||
} |
||||
|
||||
void |
||||
CallManager::handleEvent(const RoomEvent<CallInvite> &callInviteEvent) |
||||
{ |
||||
const char video[] = "m=video"; |
||||
const std::string &sdp = callInviteEvent.content.sdp; |
||||
bool isVideo = std::search(sdp.cbegin(), |
||||
sdp.cend(), |
||||
std::cbegin(video), |
||||
std::cend(video) - 1, |
||||
[](unsigned char c1, unsigned char c2) { |
||||
return std::tolower(c1) == std::tolower(c2); |
||||
}) != sdp.cend(); |
||||
|
||||
nhlog::ui()->debug("WebRTC: call id: {} - incoming {} CallInvite from {}", |
||||
callInviteEvent.content.call_id, |
||||
(isVideo ? "video" : "voice"), |
||||
callInviteEvent.sender); |
||||
|
||||
if (callInviteEvent.content.call_id.empty()) |
||||
return; |
||||
|
||||
auto roomInfo = cache::singleRoomInfo(callInviteEvent.room_id); |
||||
if (onActiveCall() || roomInfo.member_count != 2 || isVideo) { |
||||
emit newMessage(QString::fromStdString(callInviteEvent.room_id), |
||||
CallHangUp{callInviteEvent.content.call_id, |
||||
0, |
||||
CallHangUp::Reason::InviteTimeOut}); |
||||
return; |
||||
} |
||||
|
||||
playRingtone("qrc:/media/media/ring.ogg", true); |
||||
roomid_ = QString::fromStdString(callInviteEvent.room_id); |
||||
callid_ = callInviteEvent.content.call_id; |
||||
remoteICECandidates_.clear(); |
||||
|
||||
std::vector<RoomMember> members(cache::getMembers(callInviteEvent.room_id)); |
||||
const RoomMember &caller = |
||||
members.front().user_id == utils::localUser() ? members.back() : members.front(); |
||||
callPartyName_ = caller.display_name.isEmpty() ? caller.user_id : caller.display_name; |
||||
callPartyAvatarUrl_ = QString::fromStdString(roomInfo.avatar_url); |
||||
emit newCallParty(); |
||||
auto dialog = new dialogs::AcceptCall(caller.user_id, |
||||
caller.display_name, |
||||
QString::fromStdString(roomInfo.name), |
||||
QString::fromStdString(roomInfo.avatar_url), |
||||
settings_, |
||||
MainWindow::instance()); |
||||
connect(dialog, &dialogs::AcceptCall::accept, this, [this, callInviteEvent]() { |
||||
MainWindow::instance()->hideOverlay(); |
||||
answerInvite(callInviteEvent.content); |
||||
}); |
||||
connect(dialog, &dialogs::AcceptCall::reject, this, [this]() { |
||||
MainWindow::instance()->hideOverlay(); |
||||
hangUp(); |
||||
}); |
||||
MainWindow::instance()->showSolidOverlayModal(dialog); |
||||
} |
||||
|
||||
void |
||||
CallManager::answerInvite(const CallInvite &invite) |
||||
{ |
||||
stopRingtone(); |
||||
std::string errorMessage; |
||||
if (!session_.init(&errorMessage)) { |
||||
emit ChatPage::instance()->showNotification(QString::fromStdString(errorMessage)); |
||||
hangUp(); |
||||
return; |
||||
} |
||||
|
||||
session_.setStunServer(settings_->useStunServer() ? STUN_SERVER : ""); |
||||
session_.setTurnServers(turnURIs_); |
||||
|
||||
if (!session_.acceptOffer(invite.sdp)) { |
||||
emit ChatPage::instance()->showNotification("Problem setting up call."); |
||||
hangUp(); |
||||
return; |
||||
} |
||||
session_.acceptICECandidates(remoteICECandidates_); |
||||
remoteICECandidates_.clear(); |
||||
} |
||||
|
||||
void |
||||
CallManager::handleEvent(const RoomEvent<CallCandidates> &callCandidatesEvent) |
||||
{ |
||||
if (callCandidatesEvent.sender == utils::localUser().toStdString()) |
||||
return; |
||||
|
||||
nhlog::ui()->debug("WebRTC: call id: {} - incoming CallCandidates from {}", |
||||
callCandidatesEvent.content.call_id, |
||||
callCandidatesEvent.sender); |
||||
|
||||
if (callid_ == callCandidatesEvent.content.call_id) { |
||||
if (onActiveCall()) |
||||
session_.acceptICECandidates(callCandidatesEvent.content.candidates); |
||||
else { |
||||
// CallInvite has been received and we're awaiting localUser to accept or
|
||||
// reject the call
|
||||
for (const auto &c : callCandidatesEvent.content.candidates) |
||||
remoteICECandidates_.push_back(c); |
||||
} |
||||
} |
||||
} |
||||
|
||||
void |
||||
CallManager::handleEvent(const RoomEvent<CallAnswer> &callAnswerEvent) |
||||
{ |
||||
nhlog::ui()->debug("WebRTC: call id: {} - incoming CallAnswer from {}", |
||||
callAnswerEvent.content.call_id, |
||||
callAnswerEvent.sender); |
||||
|
||||
if (!onActiveCall() && callAnswerEvent.sender == utils::localUser().toStdString() && |
||||
callid_ == callAnswerEvent.content.call_id) { |
||||
emit ChatPage::instance()->showNotification("Call answered on another device."); |
||||
stopRingtone(); |
||||
MainWindow::instance()->hideOverlay(); |
||||
return; |
||||
} |
||||
|
||||
if (onActiveCall() && callid_ == callAnswerEvent.content.call_id) { |
||||
stopRingtone(); |
||||
if (!session_.acceptAnswer(callAnswerEvent.content.sdp)) { |
||||
emit ChatPage::instance()->showNotification("Problem setting up call."); |
||||
hangUp(); |
||||
} |
||||
} |
||||
} |
||||
|
||||
void |
||||
CallManager::handleEvent(const RoomEvent<CallHangUp> &callHangUpEvent) |
||||
{ |
||||
nhlog::ui()->debug("WebRTC: call id: {} - incoming CallHangUp ({}) from {}", |
||||
callHangUpEvent.content.call_id, |
||||
callHangUpReasonString(callHangUpEvent.content.reason), |
||||
callHangUpEvent.sender); |
||||
|
||||
if (callid_ == callHangUpEvent.content.call_id) { |
||||
MainWindow::instance()->hideOverlay(); |
||||
endCall(); |
||||
} |
||||
} |
||||
|
||||
void |
||||
CallManager::generateCallID() |
||||
{ |
||||
using namespace std::chrono; |
||||
uint64_t ms = duration_cast<milliseconds>(system_clock::now().time_since_epoch()).count(); |
||||
callid_ = "c" + std::to_string(ms); |
||||
} |
||||
|
||||
void |
||||
CallManager::clear() |
||||
{ |
||||
roomid_.clear(); |
||||
callPartyName_.clear(); |
||||
callPartyAvatarUrl_.clear(); |
||||
callid_.clear(); |
||||
remoteICECandidates_.clear(); |
||||
} |
||||
|
||||
void |
||||
CallManager::endCall() |
||||
{ |
||||
stopRingtone(); |
||||
clear(); |
||||
session_.end(); |
||||
} |
||||
|
||||
void |
||||
CallManager::refreshTurnServer() |
||||
{ |
||||
turnURIs_.clear(); |
||||
turnServerTimer_.start(2000); |
||||
} |
||||
|
||||
void |
||||
CallManager::retrieveTurnServer() |
||||
{ |
||||
http::client()->get_turn_server( |
||||
[this](const mtx::responses::TurnServer &res, mtx::http::RequestErr err) { |
||||
if (err) { |
||||
turnServerTimer_.setInterval(5000); |
||||
return; |
||||
} |
||||
emit turnServerRetrieved(res); |
||||
}); |
||||
} |
||||
|
||||
void |
||||
CallManager::playRingtone(const QString &ringtone, bool repeat) |
||||
{ |
||||
static QMediaPlaylist playlist; |
||||
playlist.clear(); |
||||
playlist.setPlaybackMode(repeat ? QMediaPlaylist::CurrentItemInLoop |
||||
: QMediaPlaylist::CurrentItemOnce); |
||||
playlist.addMedia(QUrl(ringtone)); |
||||
player_.setVolume(100); |
||||
player_.setPlaylist(&playlist); |
||||
} |
||||
|
||||
void |
||||
CallManager::stopRingtone() |
||||
{ |
||||
player_.setPlaylist(nullptr); |
||||
} |
||||
|
||||
namespace { |
||||
std::vector<std::string> |
||||
getTurnURIs(const mtx::responses::TurnServer &turnServer) |
||||
{ |
||||
// gstreamer expects: turn(s)://username:password@host:port?transport=udp(tcp)
|
||||
// where username and password are percent-encoded
|
||||
std::vector<std::string> ret; |
||||
for (const auto &uri : turnServer.uris) { |
||||
if (auto c = uri.find(':'); c == std::string::npos) { |
||||
nhlog::ui()->error("Invalid TURN server uri: {}", uri); |
||||
continue; |
||||
} else { |
||||
std::string scheme = std::string(uri, 0, c); |
||||
if (scheme != "turn" && scheme != "turns") { |
||||
nhlog::ui()->error("Invalid TURN server uri: {}", uri); |
||||
continue; |
||||
} |
||||
|
||||
QString encodedUri = |
||||
QString::fromStdString(scheme) + "://" + |
||||
QUrl::toPercentEncoding(QString::fromStdString(turnServer.username)) + |
||||
":" + |
||||
QUrl::toPercentEncoding(QString::fromStdString(turnServer.password)) + |
||||
"@" + QString::fromStdString(std::string(uri, ++c)); |
||||
ret.push_back(encodedUri.toStdString()); |
||||
} |
||||
} |
||||
return ret; |
||||
} |
||||
} |
@ -1,76 +0,0 @@ |
||||
#pragma once |
||||
|
||||
#include <string> |
||||
#include <vector> |
||||
|
||||
#include <QMediaPlayer> |
||||
#include <QObject> |
||||
#include <QSharedPointer> |
||||
#include <QString> |
||||
#include <QTimer> |
||||
|
||||
#include "mtx/events/collections.hpp" |
||||
#include "mtx/events/voip.hpp" |
||||
|
||||
namespace mtx::responses { |
||||
struct TurnServer; |
||||
} |
||||
|
||||
class UserSettings; |
||||
class WebRTCSession; |
||||
|
||||
class CallManager : public QObject |
||||
{ |
||||
Q_OBJECT |
||||
|
||||
public: |
||||
CallManager(QSharedPointer<UserSettings>); |
||||
|
||||
void sendInvite(const QString &roomid); |
||||
void hangUp( |
||||
mtx::events::msg::CallHangUp::Reason = mtx::events::msg::CallHangUp::Reason::User); |
||||
bool onActiveCall() const; |
||||
QString callPartyName() const { return callPartyName_; } |
||||
QString callPartyAvatarUrl() const { return callPartyAvatarUrl_; } |
||||
void refreshTurnServer(); |
||||
|
||||
public slots: |
||||
void syncEvent(const mtx::events::collections::TimelineEvents &event); |
||||
|
||||
signals: |
||||
void newMessage(const QString &roomid, const mtx::events::msg::CallInvite &); |
||||
void newMessage(const QString &roomid, const mtx::events::msg::CallCandidates &); |
||||
void newMessage(const QString &roomid, const mtx::events::msg::CallAnswer &); |
||||
void newMessage(const QString &roomid, const mtx::events::msg::CallHangUp &); |
||||
void newCallParty(); |
||||
void turnServerRetrieved(const mtx::responses::TurnServer &); |
||||
|
||||
private slots: |
||||
void retrieveTurnServer(); |
||||
|
||||
private: |
||||
WebRTCSession &session_; |
||||
QString roomid_; |
||||
QString callPartyName_; |
||||
QString callPartyAvatarUrl_; |
||||
std::string callid_; |
||||
const uint32_t timeoutms_ = 120000; |
||||
std::vector<mtx::events::msg::CallCandidates::Candidate> remoteICECandidates_; |
||||
std::vector<std::string> turnURIs_; |
||||
QTimer turnServerTimer_; |
||||
QSharedPointer<UserSettings> settings_; |
||||
QMediaPlayer player_; |
||||
|
||||
template<typename T> |
||||
bool handleEvent_(const mtx::events::collections::TimelineEvents &event); |
||||
void handleEvent(const mtx::events::RoomEvent<mtx::events::msg::CallInvite> &); |
||||
void handleEvent(const mtx::events::RoomEvent<mtx::events::msg::CallCandidates> &); |
||||
void handleEvent(const mtx::events::RoomEvent<mtx::events::msg::CallAnswer> &); |
||||
void handleEvent(const mtx::events::RoomEvent<mtx::events::msg::CallHangUp> &); |
||||
void answerInvite(const mtx::events::msg::CallInvite &); |
||||
void generateCallID(); |
||||
void clear(); |
||||
void endCall(); |
||||
void playRingtone(const QString &ringtone, bool repeat); |
||||
void stopRingtone(); |
||||
}; |