diff --git a/CMakeLists.txt b/CMakeLists.txt index 9280f7a..4348e81 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -279,7 +279,6 @@ set(SRC_FILES src/ui/ThemeManager.cpp src/ui/UserProfile.cpp - src/ActiveCallBar.cpp src/AvatarProvider.cpp src/BlurhashProvider.cpp src/Cache.cpp @@ -492,7 +491,6 @@ qt5_wrap_cpp(MOC_HEADERS src/notifications/Manager.h - src/ActiveCallBar.h src/AvatarProvider.h src/BlurhashProvider.h src/Cache_p.h diff --git a/resources/langs/nheko_pt_PT.ts b/resources/langs/nheko_pt_PT.ts new file mode 100644 index 0000000..66e07bb --- /dev/null +++ b/resources/langs/nheko_pt_PT.ts @@ -0,0 +1,1993 @@ + + + + + Cache + + + You joined this room. + + + + + ChatPage + + + Failed to invite user: %1 + + + + + + Invited user: %1 + + + + + Migrating the cache to the current version failed. This can have different reasons. Please open an issue and try to use an older version in the mean time. Alternatively you can try deleting the cache manually. + + + + + Room %1 created. + + + + + Confirm invite + + + + + Do you really want to invite %1 (%2)? + + + + + Failed to invite %1 to %2: %3 + + + + + Confirm kick + + + + + Do you really want to kick %1 (%2)? + + + + + Failed to kick %1 to %2: %3 + + + + + Kicked user: %1 + + + + + Confirm ban + + + + + Do you really want to ban %1 (%2)? + + + + + Failed to ban %1 in %2: %3 + + + + + Banned user: %1 + + + + + Confirm unban + + + + + Do you really want to unban %1 (%2)? + + + + + Failed to unban %1 in %2: %3 + + + + + Unbanned user: %1 + + + + + Failed to upload media. Please try again. + + + + + Cache migration failed! + + + + + Incompatible cache version + + + + + The cache on your disk is newer than this version of Nheko supports. Please update or clear your cache. + + + + + Failed to restore OLM account. Please login again. + + + + + Failed to restore save data. Please login again. + + + + + Failed to setup encryption keys. Server response: %1 %2. Please try again later. + + + + + + Please try to login again: %1 + + + + + Failed to join room: %1 + + + + + You joined the room + + + + + Failed to remove invite: %1 + + + + + Room creation failed: %1 + + + + + Failed to leave room: %1 + + + + + CommunitiesListItem + + + All rooms + + + + + Favourite rooms + + + + + Low priority rooms + + + + + Server Notices + Tag translation for m.server_notice + + + + + + (tag) + + + + + (community) + + + + + EditModal + + + Apply + + + + + Cancel + + + + + Name + + + + + Topic + + + + + EmojiPicker + + + + Search + + + + + People + + + + + Nature + + + + + Food + + + + + Activity + + + + + Travel + + + + + Objects + + + + + Symbols + + + + + Flags + + + + + EncryptionIndicator + + + Encrypted + + + + + This message is not encrypted! + + + + + EventStore + + + -- Encrypted Event (No keys found for decryption) -- + Placeholder, when the message was not decrypted yet or can't be decrypted. + + + + + -- Decryption Error (failed to retrieve megolm keys from db) -- + Placeholder, when the message can't be decrypted, because the DB access failed. + + + + + -- Decryption Error (%1) -- + Placeholder, when the message can't be decrypted. In this case, the Olm decrytion returned an error, which is passed as %1. + + + + + -- Encrypted Event (Unknown event type) -- + Placeholder, when the message was decrypted, but we couldn't parse it, because Nheko/mtxclient don't support that event type yet. + + + + + -- Replay attack! This message index was reused! -- + + + + + -- Message by unverified device! -- + + + + + InviteeItem + + + Remove + + + + + LoginPage + + + Matrix ID + + + + + e.g @joe:matrix.org + + + + + Your login name. A mxid should start with @ followed by the user id. After the user id you need to include your server name after a :. +You can also put your homeserver address there, if your server doesn't support .well-known lookup. +Example: @user:server.my +If Nheko fails to discover your homeserver, it will show you a field to enter the server manually. + + + + + Password + + + + + Device name + + + + + A name for this device, which will be shown to others, when verifying your devices. If none is provided a default is used. + + + + + The address that can be used to contact you homeservers client API. +Example: https://server.my:8787 + + + + + + LOGIN + + + + + Autodiscovery failed. Received malformed response. + + + + + Autodiscovery failed. Unknown error when requesting .well-known. + + + + + The required endpoints were not found. Possibly not a Matrix server. + + + + + Received malformed response. Make sure the homeserver domain is valid. + + + + + An unknown error occured. Make sure the homeserver domain is valid. + + + + + SSO LOGIN + + + + + Empty password + + + + + SSO login failed + + + + + MemberList + + + Room members + + + + + OK + + + + + MessageDelegate + + + + redacted + + + + + Encryption enabled + + + + + room name changed to: %1 + + + + + removed room name + + + + + topic changed to: %1 + + + + + removed topic + + + + + %1 created and configured room: %2 + + + + + %1 placed a voice call. + + + + + %1 placed a video call. + + + + + %1 placed a call. + + + + + Negotiating call... + + + + + %1 answered the call. + + + + + %1 ended the call. + + + + + Placeholder + + + unimplemented event: + + + + + QuickSwitcher + + + Search for a room... + + + + + RegisterPage + + + Username + + + + + The username must not be empty, and must contain only the characters a-z, 0-9, ., _, =, -, and /. + + + + + Password + + + + + Please choose a secure password. The exact requirements for password strength may depend on your server. + + + + + Password confirmation + + + + + Homeserver + + + + + A server that allows registration. Since matrix is decentralized, you need to first find a server you can register on or host your own. + + + + + REGISTER + + + + + No supported registration flows! + + + + + Invalid username + + + + + Password is not long enough (min 8 chars) + + + + + Passwords don't match + + + + + Invalid server name + + + + + RoomInfo + + + no version stored + + + + + RoomInfoListItem + + + Leave room + + + + + Tag room as: + + + + + Favourite + Standard matrix tag for favourites + + + + + Low Priority + Standard matrix tag for low priority rooms + + + + + Server Notice + Standard matrix tag for server notices + + + + + Adds or removes the specified tag. + WhatsThis hint for tag menu actions + + + + + New tag... + Add a new tag to the room + + + + + New Tag + Tag name prompt title + + + + + Tag: + Tag name prompt + + + + + Accept + + + + + Decline + + + + + SideBarActions + + + User settings + + + + + Create new room + + + + + Join a room + + + + + Start a new chat + + + + + Room directory + + + + + StatusIndicator + + + Failed + + + + + Sent + + + + + Received + + + + + Read + + + + + TextInputWidget + + + Send a file + + + + + + Write a message... + + + + + Send a message + + + + + Emoji + + + + + Select a file + + + + + All Files (*) + + + + + Place a call + + + + + Hang up + + + + + Connection lost. Nheko is trying to re-connect... + + + + + TimelineModel + + + Message redaction failed: %1 + + + + + + + + Failed to encrypt event, sending aborted! + + + + + Save image + + + + + Save video + + + + + Save audio + + + + + Save file + + + + + %1 and %2 are typing. + Multiple users are typing. First argument is a comma separated list of potentially multiple users. Second argument is the last user of that list. (If only one user is typing, %1 is empty. You should still use it in your string though to silence Qt warnings.) + + + + + + + + %1 opened the room to the public. + + + + + %1 made this room require and invitation to join. + + + + + %1 made the room open to guests. + + + + + %1 has closed the room to guest access. + + + + + %1 made the room history world readable. Events may be now read by non-joined people. + + + + + %1 set the room history visible to members from this point on. + + + + + %1 set the room history visible to members since they were invited. + + + + + %1 set the room history visible to members since they joined the room. + + + + + %1 has changed the room's permissions. + + + + + %1 was invited. + + + + + %1 changed their display name and avatar. + + + + + %1 changed their display name. + + + + + %1 changed their avatar. + + + + + %1 changed some profile info. + + + + + %1 joined. + + + + + %1 rejected their invite. + + + + + Revoked the invite to %1. + + + + + %1 left the room. + + + + + Kicked %1. + + + + + Unbanned %1. + + + + + %1 was banned. + + + + + %1 redacted their knock. + + + + + You joined this room. + + + + + Rejected the knock from %1. + + + + + %1 left after having already left! + This is a leave event after the user already left and shouldn't happen apart from state resets + + + + + Reason: %1 + + + + + %1 knocked. + + + + + TimelineRow + + + React + + + + + Reply + + + + + Options + + + + + TimelineView + + + React + + + + + Reply + + + + + Read receipts + + + + + Mark as read + + + + + View raw message + + + + + View decrypted raw message + + + + + Redact message + + + + + Save as + + + + + No room open + + + + + Back to room list + + + + + + No room selected + + + + + Room options + + + + + Invite users + + + + + Members + + + + + Leave room + + + + + Settings + + + + + Close + + + + + TrayIcon + + + Show + + + + + Quit + + + + + UserInfoWidget + + + Logout + + + + + Set custom status message + + + + + Custom status message + + + + + Status: + + + + + Set presence automatically + + + + + Online + + + + + Unavailable + + + + + Offline + + + + + UserSettingsPage + + + Minimize to tray + + + + + Start in tray + + + + + Group's sidebar + + + + + Circular Avatars + + + + + CALLS + + + + + Keep the application running in the background after closing the client window. + + + + + Start the application in the background without showing the client window. + + + + + Change the appearance of user avatars in chats. +OFF - square, ON - Circle. + + + + + Show a column containing groups and tags next to the room list. + + + + + Decrypt messages in sidebar + + + + + Decrypt the messages shown in the sidebar. +Only affects messages in encrypted chats. + + + + + Show buttons in timeline + + + + + Show buttons to quickly reply, react or access additional options next to each message. + + + + + Limit width of timeline + + + + + Set the max width of messages in the timeline (in pixels). This can help readability on wide screen, when Nheko is maximised + + + + + Typing notifications + + + + + Show who is typing in a room. +This will also enable or disable sending typing notifications to others. + + + + + Sort rooms by unreads + + + + + Display rooms with new messages first. +If this is off, the list of rooms will only be sorted by the timestamp of the last message in a room. +If this is on, rooms which have active notifications (the small circle with a number in it) will be sorted on top. Rooms, that you have muted, will still be sorted by timestamp, since you don't seem to consider them as important as the other rooms. + + + + + Read receipts + + + + + Show if your message was read. +Status is displayed next to timestamps. + + + + + Send messages as Markdown + + + + + Allow using markdown in messages. +When disabled, all messages are sent as a plain text. + + + + + Desktop notifications + + + + + Notify about received message when the client is not currently focused. + + + + + Alert on notification + + + + + Show an alert when a message is received. +This usually causes the application icon in the task bar to animate in some fashion. + + + + + Highlight message on hover + + + + + Change the background color of messages when you hover over them. + + + + + Large Emoji in timeline + + + + + Make font size larger if messages with only a few emojis are displayed. + + + + + Scale factor + + + + + Change the scale factor of the whole user interface. + + + + + Font size + + + + + Font Family + + + + + Theme + + + + + Allow fallback call assist server + + + + + Will use turn.matrix.org as assist when your home server does not offer one. + + + + + Device ID + + + + + Device Fingerprint + + + + + Session Keys + + + + + IMPORT + + + + + EXPORT + + + + + ENCRYPTION + + + + + GENERAL + + + + + INTERFACE + + + + + Emoji Font Family + + + + + Open Sessions File + + + + + + + + + + + + + + Error + + + + + + File Password + + + + + Enter the passphrase to decrypt the file: + + + + + + The password cannot be empty + + + + + Enter passphrase to encrypt your session keys: + + + + + File to save the exported session keys + + + + + WelcomePage + + + Welcome to nheko! The desktop client for the Matrix protocol. + + + + + Enjoy your stay! + + + + + REGISTER + + + + + LOGIN + + + + + descriptiveTime + + + Yesterday + + + + + dialogs::AcceptCall + + + Accept + + + + + Reject + + + + + dialogs::CreateRoom + + + Create room + + + + + Cancel + + + + + Name + + + + + Topic + + + + + Alias + + + + + Room Visibility + + + + + Room Preset + + + + + Direct Chat + + + + + dialogs::FallbackAuth + + + Open Fallback in Browser + + + + + Cancel + + + + + Confirm + + + + + Open the fallback, follow the steps and confirm after completing them. + + + + + dialogs::InviteUsers + + + Cancel + + + + + User ID to invite + + + + + dialogs::JoinRoom + + + Join + + + + + Cancel + + + + + Room ID or alias + + + + + dialogs::LeaveRoom + + + Cancel + + + + + Are you sure you want to leave? + + + + + dialogs::Logout + + + Cancel + + + + + Logout. Are you sure? + + + + + dialogs::PlaceCall + + + Voice + + + + + Cancel + + + + + dialogs::PreviewUploadOverlay + + + Upload + + + + + Cancel + + + + + Media type: %1 +Media size: %2 + + + + + + dialogs::ReCaptcha + + + Cancel + + + + + Confirm + + + + + Solve the reCAPTCHA and press the confirm button + + + + + dialogs::ReadReceipts + + + Read receipts + + + + + Close + + + + + dialogs::ReceiptItem + + + Today %1 + + + + + Yesterday %1 + + + + + dialogs::RoomSettings + + + Settings + + + + + Info + + + + + Internal ID + + + + + Room Version + + + + + Notifications + + + + + Muted + + + + + Mentions only + + + + + All messages + + + + + Room access + + + + + Anyone and guests + + + + + Anyone + + + + + Invited users + + + + + Encryption + + + + + End-to-End Encryption + + + + + Encryption is currently experimental and things might break unexpectedly. <br>Please take note that it can't be disabled afterwards. + + + + + Respond to key requests + + + + + Whether or not the client should respond automatically with the session keys + upon request. Use with caution, this is a temporary measure to test the + E2E implementation until device verification is completed. + + + + + %n member(s) + + + + + + + + Failed to enable encryption: %1 + + + + + Select an avatar + + + + + All Files (*) + + + + + The selected file is not an image + + + + + Error while reading file: %1 + + + + + + Failed to upload image: %s + + + + + dialogs::UserProfile + + + Ban the user from the room + + + + + Ignore messages from this user + + + + + Kick the user from the room + + + + + Start a conversation + + + + + Confirm DM + + + + + Do you really want to invite %1 (%2) to a direct chat? + + + + + Devices + + + + + emoji::Panel + + + Smileys & People + + + + + Animals & Nature + + + + + Food & Drink + + + + + Activity + + + + + Travel & Places + + + + + Objects + + + + + Symbols + + + + + Flags + + + + + message-description sent: + + + You sent an audio clip + + + + + %1 sent an audio clip + + + + + You sent an image + + + + + %1 sent an image + + + + + You sent a file + + + + + %1 sent a file + + + + + You sent a video + + + + + %1 sent a video + + + + + You sent a sticker + + + + + %1 sent a sticker + + + + + You sent a notification + + + + + %1 sent a notification + + + + + You: %1 + + + + + %1: %2 + + + + + You sent an encrypted message + + + + + %1 sent an encrypted message + + + + + You placed a call + + + + + %1 placed a call + + + + + You answered a call + + + + + %1 answered a call + + + + + You ended a call + + + + + %1 ended a call + + + + + popups::UserMentions + + + This Room + + + + + All Rooms + + + + + utils + + + Unknown Message Type + + + + diff --git a/resources/qml/ActiveCallBar.qml b/resources/qml/ActiveCallBar.qml new file mode 100644 index 0000000..6148462 --- /dev/null +++ b/resources/qml/ActiveCallBar.qml @@ -0,0 +1,111 @@ +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 { + target: TimelineManager + 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 = "" + } + } + } + + Timer { + id: callTimer + property int startTime + 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) + } + + function pad(n) { + return (n < 10) ? ("0" + n) : n + } + } + + 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 + } + } +} diff --git a/resources/qml/Avatar.qml b/resources/qml/Avatar.qml index 67c3e00..df3dd08 100644 --- a/resources/qml/Avatar.qml +++ b/resources/qml/Avatar.qml @@ -16,7 +16,7 @@ Rectangle { Label { anchors.fill: parent - text: TimelineManager.escapeEmoji(String.fromCodePoint(displayName.codePointAt(0))) + text: TimelineManager.escapeEmoji(displayName ? String.fromCodePoint(displayName.codePointAt(0)) : "") textFormat: Text.RichText font.pixelSize: avatar.height/2 verticalAlignment: Text.AlignVCenter diff --git a/resources/qml/ImageButton.qml b/resources/qml/ImageButton.qml index dd67d59..54399ae 100644 --- a/resources/qml/ImageButton.qml +++ b/resources/qml/ImageButton.qml @@ -3,6 +3,8 @@ import QtQuick.Controls 2.3 AbstractButton { property string image: undefined + property color highlightColor: colors.highlight + property color buttonTextColor: colors.buttonText width: 16 height: 16 id: button @@ -11,7 +13,7 @@ AbstractButton { id: buttonImg // Workaround, can't get icon.source working for now... anchors.fill: parent - source: "image://colorimage/" + image + "?" + (button.hovered ? colors.highlight : colors.buttonText) + source: "image://colorimage/" + image + "?" + (button.hovered ? highlightColor : buttonTextColor) } MouseArea diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml index 1dbe7c1..5c9ca34 100644 --- a/resources/qml/TimelineView.qml +++ b/resources/qml/TimelineView.qml @@ -516,6 +516,11 @@ Page { } } } + + ActiveCallBar { + Layout.fillWidth: true + z: 3 + } } } } diff --git a/resources/res.qrc b/resources/res.qrc index 64e5b08..87216e3 100644 --- a/resources/res.qrc +++ b/resources/res.qrc @@ -121,6 +121,7 @@ qtquickcontrols2.conf qml/TimelineView.qml + qml/ActiveCallBar.qml qml/Avatar.qml qml/ImageButton.qml qml/MatrixText.qml diff --git a/src/ActiveCallBar.cpp b/src/ActiveCallBar.cpp deleted file mode 100644 index c0d2c13..0000000 --- a/src/ActiveCallBar.cpp +++ /dev/null @@ -1,160 +0,0 @@ -#include - -#include -#include -#include -#include -#include -#include - -#include "ActiveCallBar.h" -#include "ChatPage.h" -#include "Utils.h" -#include "WebRTCSession.h" -#include "ui/Avatar.h" -#include "ui/FlatButton.h" - -ActiveCallBar::ActiveCallBar(QWidget *parent) - : QWidget(parent) -{ - setAutoFillBackground(true); - auto p = palette(); - p.setColor(backgroundRole(), QColor(46, 204, 113)); - setPalette(p); - - QFont f; - f.setPointSizeF(f.pointSizeF()); - - const int fontHeight = QFontMetrics(f).height(); - const int widgetMargin = fontHeight / 3; - const int contentHeight = fontHeight * 3; - - setFixedHeight(contentHeight + widgetMargin); - - layout_ = new QHBoxLayout(this); - layout_->setSpacing(widgetMargin); - layout_->setContentsMargins(2 * widgetMargin, widgetMargin, 2 * widgetMargin, widgetMargin); - - QFont labelFont; - labelFont.setPointSizeF(labelFont.pointSizeF() * 1.1); - labelFont.setWeight(QFont::Medium); - - avatar_ = new Avatar(this, QFontMetrics(f).height() * 2.5); - - callPartyLabel_ = new QLabel(this); - callPartyLabel_->setFont(labelFont); - - stateLabel_ = new QLabel(this); - stateLabel_->setFont(labelFont); - - durationLabel_ = new QLabel(this); - durationLabel_->setFont(labelFont); - durationLabel_->hide(); - - muteBtn_ = new FlatButton(this); - setMuteIcon(false); - muteBtn_->setFixedSize(buttonSize_, buttonSize_); - muteBtn_->setCornerRadius(buttonSize_ / 2); - connect(muteBtn_, &FlatButton::clicked, this, [this]() { - if (WebRTCSession::instance().toggleMuteAudioSrc(muted_)) - setMuteIcon(muted_); - }); - - layout_->addWidget(avatar_, 0, Qt::AlignLeft); - layout_->addWidget(callPartyLabel_, 0, Qt::AlignLeft); - layout_->addWidget(stateLabel_, 0, Qt::AlignLeft); - layout_->addWidget(durationLabel_, 0, Qt::AlignLeft); - layout_->addStretch(); - layout_->addWidget(muteBtn_, 0, Qt::AlignCenter); - layout_->addSpacing(18); - - timer_ = new QTimer(this); - connect(timer_, &QTimer::timeout, this, [this]() { - auto seconds = QDateTime::currentSecsSinceEpoch() - callStartTime_; - int s = seconds % 60; - int m = (seconds / 60) % 60; - int h = seconds / 3600; - char buf[12]; - if (h) - snprintf(buf, sizeof(buf), "%.2d:%.2d:%.2d", h, m, s); - else - snprintf(buf, sizeof(buf), "%.2d:%.2d", m, s); - durationLabel_->setText(buf); - }); - - connect( - &WebRTCSession::instance(), &WebRTCSession::stateChanged, this, &ActiveCallBar::update); -} - -void -ActiveCallBar::setMuteIcon(bool muted) -{ - QIcon icon; - if (muted) { - muteBtn_->setToolTip("Unmute Mic"); - icon.addFile(":/icons/icons/ui/microphone-unmute.png"); - } else { - muteBtn_->setToolTip("Mute Mic"); - icon.addFile(":/icons/icons/ui/microphone-mute.png"); - } - muteBtn_->setIcon(icon); - muteBtn_->setIconSize(QSize(buttonSize_, buttonSize_)); -} - -void -ActiveCallBar::setCallParty(const QString &userid, - const QString &displayName, - const QString &roomName, - const QString &avatarUrl) -{ - callPartyLabel_->setText(" " + (displayName.isEmpty() ? userid : displayName) + " "); - - if (!avatarUrl.isEmpty()) - avatar_->setImage(avatarUrl); - else - avatar_->setLetter(utils::firstChar(roomName)); -} - -void -ActiveCallBar::update(WebRTCSession::State state) -{ - switch (state) { - case WebRTCSession::State::INITIATING: - show(); - stateLabel_->setText("Initiating call..."); - break; - case WebRTCSession::State::INITIATED: - show(); - stateLabel_->setText("Call initiated..."); - break; - case WebRTCSession::State::OFFERSENT: - show(); - stateLabel_->setText("Calling..."); - break; - case WebRTCSession::State::CONNECTING: - show(); - stateLabel_->setText("Connecting..."); - break; - case WebRTCSession::State::CONNECTED: - show(); - callStartTime_ = QDateTime::currentSecsSinceEpoch(); - timer_->start(1000); - stateLabel_->setPixmap( - QIcon(":/icons/icons/ui/place-call.png").pixmap(QSize(buttonSize_, buttonSize_))); - durationLabel_->setText("00:00"); - durationLabel_->show(); - break; - case WebRTCSession::State::ICEFAILED: - case WebRTCSession::State::DISCONNECTED: - hide(); - timer_->stop(); - callPartyLabel_->setText(QString()); - stateLabel_->setText(QString()); - durationLabel_->setText(QString()); - durationLabel_->hide(); - setMuteIcon(false); - break; - default: - break; - } -} diff --git a/src/ActiveCallBar.h b/src/ActiveCallBar.h deleted file mode 100644 index 1e94022..0000000 --- a/src/ActiveCallBar.h +++ /dev/null @@ -1,40 +0,0 @@ -#pragma once - -#include - -#include "WebRTCSession.h" - -class QHBoxLayout; -class QLabel; -class QTimer; -class Avatar; -class FlatButton; - -class ActiveCallBar : public QWidget -{ - Q_OBJECT - -public: - ActiveCallBar(QWidget *parent = nullptr); - -public slots: - void update(WebRTCSession::State); - void setCallParty(const QString &userid, - const QString &displayName, - const QString &roomName, - const QString &avatarUrl); - -private: - QHBoxLayout *layout_ = nullptr; - Avatar *avatar_ = nullptr; - QLabel *callPartyLabel_ = nullptr; - QLabel *stateLabel_ = nullptr; - QLabel *durationLabel_ = nullptr; - FlatButton *muteBtn_ = nullptr; - int buttonSize_ = 22; - bool muted_ = false; - qint64 callStartTime_ = 0; - QTimer *timer_ = nullptr; - - void setMuteIcon(bool muted); -}; diff --git a/src/CallManager.cpp b/src/CallManager.cpp index 7a8d2ca..b1d1a75 100644 --- a/src/CallManager.cpp +++ b/src/CallManager.cpp @@ -52,7 +52,7 @@ CallManager::CallManager(QSharedPointer userSettings) emit newMessage(roomid_, CallInvite{callid_, sdp, 0, timeoutms_}); emit newMessage(roomid_, CallCandidates{callid_, candidates, 0}); QTimer::singleShot(timeoutms_, this, [this]() { - if (session_.state() == WebRTCSession::State::OFFERSENT) { + if (session_.state() == webrtc::State::OFFERSENT) { hangUp(CallHangUp::Reason::InviteTimeOut); emit ChatPage::instance()->showNotification( "The remote side failed to pick up."); @@ -99,13 +99,13 @@ CallManager::CallManager(QSharedPointer userSettings) turnServerTimer_.setInterval(ttl * 1000 * 0.9); }); - connect(&session_, &WebRTCSession::stateChanged, this, [this](WebRTCSession::State state) { + connect(&session_, &WebRTCSession::stateChanged, this, [this](webrtc::State state) { switch (state) { - case WebRTCSession::State::DISCONNECTED: + case webrtc::State::DISCONNECTED: playRingtone("qrc:/media/media/callend.ogg", false); clear(); break; - case WebRTCSession::State::ICEFAILED: { + case webrtc::State::ICEFAILED: { QString error("Call connection failed."); if (turnURIs_.empty()) error += " Your homeserver has no configured TURN server."; @@ -155,10 +155,9 @@ CallManager::sendInvite(const QString &roomid) std::vector members(cache::getMembers(roomid.toStdString())); const RoomMember &callee = members.front().user_id == utils::localUser() ? members.back() : members.front(); - emit newCallParty(callee.user_id, - callee.display_name, - QString::fromStdString(roomInfo.name), - QString::fromStdString(roomInfo.avatar_url)); + 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."); @@ -193,9 +192,9 @@ CallManager::hangUp(CallHangUp::Reason reason) } bool -CallManager::onActiveCall() +CallManager::onActiveCall() const { - return session_.state() != WebRTCSession::State::DISCONNECTED; + return session_.state() != webrtc::State::DISCONNECTED; } void @@ -259,11 +258,9 @@ CallManager::handleEvent(const RoomEvent &callInviteEvent) std::vector members(cache::getMembers(callInviteEvent.room_id)); const RoomMember &caller = members.front().user_id == utils::localUser() ? members.back() : members.front(); - emit newCallParty(caller.user_id, - caller.display_name, - QString::fromStdString(roomInfo.name), - QString::fromStdString(roomInfo.avatar_url)); - + 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), @@ -376,6 +373,8 @@ void CallManager::clear() { roomid_.clear(); + callPartyName_.clear(); + callPartyAvatarUrl_.clear(); callid_.clear(); remoteICECandidates_.clear(); } diff --git a/src/CallManager.h b/src/CallManager.h index 3a40643..640230a 100644 --- a/src/CallManager.h +++ b/src/CallManager.h @@ -29,7 +29,9 @@ public: void sendInvite(const QString &roomid); void hangUp( mtx::events::msg::CallHangUp::Reason = mtx::events::msg::CallHangUp::Reason::User); - bool onActiveCall(); + bool onActiveCall() const; + QString callPartyName() const { return callPartyName_; } + QString callPartyAvatarUrl() const { return callPartyAvatarUrl_; } void refreshTurnServer(); public slots: @@ -40,11 +42,8 @@ signals: 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 &); - void newCallParty(const QString &userid, - const QString &displayName, - const QString &roomName, - const QString &avatarUrl); private slots: void retrieveTurnServer(); @@ -52,6 +51,8 @@ private slots: private: WebRTCSession &session_; QString roomid_; + QString callPartyName_; + QString callPartyAvatarUrl_; std::string callid_; const uint32_t timeoutms_ = 120000; std::vector remoteICECandidates_; diff --git a/src/ChatPage.cpp b/src/ChatPage.cpp index 87b4c27..8e93c0f 100644 --- a/src/ChatPage.cpp +++ b/src/ChatPage.cpp @@ -22,7 +22,6 @@ #include #include -#include "ActiveCallBar.h" #include "AvatarProvider.h" #include "Cache.h" #include "Cache_p.h" @@ -41,7 +40,6 @@ #include "UserInfoWidget.h" #include "UserSettingsPage.h" #include "Utils.h" -#include "WebRTCSession.h" #include "ui/OverlayModal.h" #include "ui/Theme.h" @@ -130,12 +128,6 @@ ChatPage::ChatPage(QSharedPointer userSettings, QWidget *parent) contentLayout_->addWidget(view_manager_->getWidget()); - activeCallBar_ = new ActiveCallBar(this); - contentLayout_->addWidget(activeCallBar_); - activeCallBar_->hide(); - connect( - &callManager_, &CallManager::newCallParty, activeCallBar_, &ActiveCallBar::setCallParty); - // Splitter splitter->addWidget(sideBar_); splitter->addWidget(content_); diff --git a/src/ChatPage.h b/src/ChatPage.h index c37aa91..f0e12ab 100644 --- a/src/ChatPage.h +++ b/src/ChatPage.h @@ -43,7 +43,6 @@ #include "notifications/Manager.h" #include "popups/UserMentions.h" -class ActiveCallBar; class OverlayModal; class QuickSwitcher; class RoomList; @@ -259,7 +258,6 @@ private: SideBarActions *sidebarActions_; TextInputWidget *text_input_; - ActiveCallBar *activeCallBar_; QTimer connectivityTimer_; std::atomic_bool isConnected_; diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index 6372237..b6ad8bb 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -288,7 +288,7 @@ MainWindow::showChatPage() void MainWindow::closeEvent(QCloseEvent *event) { - if (WebRTCSession::instance().state() != WebRTCSession::State::DISCONNECTED) { + if (WebRTCSession::instance().state() != webrtc::State::DISCONNECTED) { if (QMessageBox::question(this, "nheko", "A call is in progress. Quit?") != QMessageBox::Yes) { event->ignore(); @@ -431,7 +431,7 @@ MainWindow::openLogoutDialog() { auto dialog = new dialogs::Logout(this); connect(dialog, &dialogs::Logout::loggingOut, this, [this]() { - if (WebRTCSession::instance().state() != WebRTCSession::State::DISCONNECTED) { + if (WebRTCSession::instance().state() != webrtc::State::DISCONNECTED) { if (QMessageBox::question( this, "nheko", "A call is in progress. Log out?") != QMessageBox::Yes) { diff --git a/src/TextInputWidget.cpp b/src/TextInputWidget.cpp index d1be7fb..22e8aaf 100644 --- a/src/TextInputWidget.cpp +++ b/src/TextInputWidget.cpp @@ -560,7 +560,7 @@ TextInputWidget::TextInputWidget(QWidget *parent) #ifdef GSTREAMER_AVAILABLE callBtn_ = new FlatButton(this); - changeCallButtonState(WebRTCSession::State::DISCONNECTED); + changeCallButtonState(webrtc::State::DISCONNECTED); connect(&WebRTCSession::instance(), &WebRTCSession::stateChanged, this, @@ -778,11 +778,10 @@ TextInputWidget::paintEvent(QPaintEvent *) } void -TextInputWidget::changeCallButtonState(WebRTCSession::State state) +TextInputWidget::changeCallButtonState(webrtc::State state) { QIcon icon; - if (state == WebRTCSession::State::ICEFAILED || - state == WebRTCSession::State::DISCONNECTED) { + if (state == webrtc::State::ICEFAILED || state == webrtc::State::DISCONNECTED) { callBtn_->setToolTip(tr("Place a call")); icon.addFile(":/icons/icons/ui/place-call.png"); } else { diff --git a/src/TextInputWidget.h b/src/TextInputWidget.h index 092e0ff..7cc73e9 100644 --- a/src/TextInputWidget.h +++ b/src/TextInputWidget.h @@ -164,7 +164,7 @@ public slots: void openFileSelection(); void hideUploadSpinner(); void focusLineEdit() { input_->setFocus(); } - void changeCallButtonState(WebRTCSession::State); + void changeCallButtonState(webrtc::State); private slots: void addSelectedEmoji(const QString &emoji); diff --git a/src/UserSettingsPage.cpp b/src/UserSettingsPage.cpp index f1542ec..7d81e66 100644 --- a/src/UserSettingsPage.cpp +++ b/src/UserSettingsPage.cpp @@ -511,7 +511,6 @@ UserSettingsPage::UserSettingsPage(QSharedPointer settings, QWidge callsLabel->setFixedHeight(callsLabel->minimumHeight() + LayoutTopMargin); callsLabel->setAlignment(Qt::AlignBottom); callsLabel->setFont(font); - useStunServer_ = new Toggle{this}; auto encryptionLabel_ = new QLabel{tr("ENCRYPTION"), this}; encryptionLabel_->setFixedHeight(encryptionLabel_->minimumHeight() + LayoutTopMargin); diff --git a/src/WebRTCSession.cpp b/src/WebRTCSession.cpp index 30a27b6..1c11f75 100644 --- a/src/WebRTCSession.cpp +++ b/src/WebRTCSession.cpp @@ -1,3 +1,4 @@ +#include #include #include "Logging.h" @@ -14,12 +15,17 @@ extern "C" } #endif -Q_DECLARE_METATYPE(WebRTCSession::State) +Q_DECLARE_METATYPE(webrtc::State) + +using webrtc::State; WebRTCSession::WebRTCSession() : QObject() { - qRegisterMetaType(); + qRegisterMetaType(); + qmlRegisterUncreatableMetaObject( + webrtc::staticMetaObject, "im.nheko", 1, 0, "WebRTCState", "Can't instantiate enum"); + connect(this, &WebRTCSession::stateChanged, this, &WebRTCSession::setState); init(); } @@ -246,12 +252,10 @@ iceGatheringStateChanged(GstElement *webrtc, nhlog::ui()->debug("WebRTC: GstWebRTCICEGatheringState -> Complete"); if (isoffering_) { emit WebRTCSession::instance().offerCreated(localsdp_, localcandidates_); - emit WebRTCSession::instance().stateChanged( - WebRTCSession::State::OFFERSENT); + emit WebRTCSession::instance().stateChanged(State::OFFERSENT); } else { emit WebRTCSession::instance().answerCreated(localsdp_, localcandidates_); - emit WebRTCSession::instance().stateChanged( - WebRTCSession::State::ANSWERSENT); + emit WebRTCSession::instance().stateChanged(State::ANSWERSENT); } } } @@ -264,10 +268,10 @@ onICEGatheringCompletion(gpointer timerid) *(guint *)(timerid) = 0; if (isoffering_) { emit WebRTCSession::instance().offerCreated(localsdp_, localcandidates_); - emit WebRTCSession::instance().stateChanged(WebRTCSession::State::OFFERSENT); + emit WebRTCSession::instance().stateChanged(State::OFFERSENT); } else { emit WebRTCSession::instance().answerCreated(localsdp_, localcandidates_); - emit WebRTCSession::instance().stateChanged(WebRTCSession::State::ANSWERSENT); + emit WebRTCSession::instance().stateChanged(State::ANSWERSENT); } return FALSE; } @@ -285,7 +289,7 @@ addLocalICECandidate(GstElement *webrtc G_GNUC_UNUSED, localcandidates_.push_back({"audio", (uint16_t)mlineIndex, candidate}); return; #else - if (WebRTCSession::instance().state() >= WebRTCSession::State::OFFERSENT) { + if (WebRTCSession::instance().state() >= State::OFFERSENT) { emit WebRTCSession::instance().newICECandidate( {"audio", (uint16_t)mlineIndex, candidate}); return; @@ -314,11 +318,11 @@ iceConnectionStateChanged(GstElement *webrtc, switch (newState) { case GST_WEBRTC_ICE_CONNECTION_STATE_CHECKING: nhlog::ui()->debug("WebRTC: GstWebRTCICEConnectionState -> Checking"); - emit WebRTCSession::instance().stateChanged(WebRTCSession::State::CONNECTING); + emit WebRTCSession::instance().stateChanged(State::CONNECTING); break; case GST_WEBRTC_ICE_CONNECTION_STATE_FAILED: nhlog::ui()->error("WebRTC: GstWebRTCICEConnectionState -> Failed"); - emit WebRTCSession::instance().stateChanged(WebRTCSession::State::ICEFAILED); + emit WebRTCSession::instance().stateChanged(State::ICEFAILED); break; default: break; @@ -355,8 +359,7 @@ linkNewPad(GstElement *decodebin G_GNUC_UNUSED, GstPad *newpad, GstElement *pipe if (GST_PAD_LINK_FAILED(gst_pad_link(newpad, queuepad))) nhlog::ui()->error("WebRTC: unable to link new pad"); else { - emit WebRTCSession::instance().stateChanged( - WebRTCSession::State::CONNECTED); + emit WebRTCSession::instance().stateChanged(State::CONNECTED); } gst_object_unref(queuepad); } @@ -632,21 +635,30 @@ WebRTCSession::createPipeline(int opusPayloadType) } bool -WebRTCSession::toggleMuteAudioSrc(bool &isMuted) +WebRTCSession::isMicMuted() const { if (state_ < State::INITIATED) return false; GstElement *srclevel = gst_bin_get_by_name(GST_BIN(pipe_), "srclevel"); - if (!srclevel) + gboolean muted; + g_object_get(srclevel, "mute", &muted, nullptr); + gst_object_unref(srclevel); + return muted; +} + +bool +WebRTCSession::toggleMicMute() +{ + if (state_ < State::INITIATED) return false; + GstElement *srclevel = gst_bin_get_by_name(GST_BIN(pipe_), "srclevel"); gboolean muted; g_object_get(srclevel, "mute", &muted, nullptr); g_object_set(srclevel, "mute", !muted, nullptr); gst_object_unref(srclevel); - isMuted = !muted; - return true; + return !muted; } void @@ -777,7 +789,13 @@ WebRTCSession::createPipeline(int) } bool -WebRTCSession::toggleMuteAudioSrc(bool &) +WebRTCSession::isMicMuted() const +{ + return false; +} + +bool +WebRTCSession::toggleMicMute() { return false; } diff --git a/src/WebRTCSession.h b/src/WebRTCSession.h index 653ec2c..83cabf5 100644 --- a/src/WebRTCSession.h +++ b/src/WebRTCSession.h @@ -9,23 +9,30 @@ typedef struct _GstElement GstElement; +namespace webrtc { +Q_NAMESPACE + +enum class State +{ + DISCONNECTED, + ICEFAILED, + INITIATING, + INITIATED, + OFFERSENT, + ANSWERSENT, + CONNECTING, + CONNECTED + +}; +Q_ENUM_NS(State) + +} + class WebRTCSession : public QObject { Q_OBJECT public: - enum class State - { - DISCONNECTED, - ICEFAILED, - INITIATING, - INITIATED, - OFFERSENT, - ANSWERSENT, - CONNECTING, - CONNECTED - }; - static WebRTCSession &instance() { static WebRTCSession instance; @@ -33,14 +40,15 @@ public: } bool init(std::string *errorMessage = nullptr); - State state() const { return state_; } + webrtc::State state() const { return state_; } bool createOffer(); bool acceptOffer(const std::string &sdp); bool acceptAnswer(const std::string &sdp); void acceptICECandidates(const std::vector &); - bool toggleMuteAudioSrc(bool &isMuted); + bool isMicMuted() const; + bool toggleMicMute(); void end(); void setStunServer(const std::string &stunServer) { stunServer_ = stunServer; } @@ -55,16 +63,16 @@ signals: void answerCreated(const std::string &sdp, const std::vector &); void newICECandidate(const mtx::events::msg::CallCandidates::Candidate &); - void stateChanged(WebRTCSession::State); // explicit qualifier necessary for Qt + void stateChanged(webrtc::State); private slots: - void setState(State state) { state_ = state; } + void setState(webrtc::State state) { state_ = state; } private: WebRTCSession(); bool initialised_ = false; - State state_ = State::DISCONNECTED; + webrtc::State state_ = webrtc::State::DISCONNECTED; GstElement *pipe_ = nullptr; GstElement *webrtc_ = nullptr; unsigned int busWatchId_ = 0; diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp index ed72005..1815117 100644 --- a/src/timeline/TimelineViewManager.cpp +++ b/src/timeline/TimelineViewManager.cpp @@ -8,7 +8,6 @@ #include #include "BlurhashProvider.h" -#include "CallManager.h" #include "ChatPage.h" #include "ColorImageProvider.h" #include "DelegateChooser.h" @@ -233,6 +232,12 @@ TimelineViewManager::TimelineViewManager(QSharedPointer userSettin isInitialSync_ = true; emit initialSyncChanged(true); }); + connect(&WebRTCSession::instance(), + &WebRTCSession::stateChanged, + this, + &TimelineViewManager::callStateChanged); + connect( + callManager_, &CallManager::newCallParty, this, &TimelineViewManager::callPartyChanged); } void @@ -304,6 +309,13 @@ TimelineViewManager::escapeEmoji(QString str) const return utils::replaceEmoji(str); } +void +TimelineViewManager::toggleMicMute() +{ + WebRTCSession::instance().toggleMicMute(); + emit micMuteChanged(); +} + void TimelineViewManager::openImageOverlay(QString mxcUrl, QString eventId) const { diff --git a/src/timeline/TimelineViewManager.h b/src/timeline/TimelineViewManager.h index a8bd2e0..9a2a646 100644 --- a/src/timeline/TimelineViewManager.h +++ b/src/timeline/TimelineViewManager.h @@ -10,16 +10,17 @@ #include #include "Cache.h" +#include "CallManager.h" #include "DeviceVerificationFlow.h" #include "Logging.h" #include "TimelineModel.h" #include "Utils.h" +#include "WebRTCSession.h" #include "emoji/EmojiModel.h" #include "emoji/Provider.h" class MxcImageProvider; class BlurhashProvider; -class CallManager; class ColorImageProvider; class UserSettings; class ChatPage; @@ -34,6 +35,10 @@ class TimelineViewManager : public QObject bool isInitialSync MEMBER isInitialSync_ READ isInitialSync NOTIFY initialSyncChanged) Q_PROPERTY( bool isNarrowView MEMBER isNarrowView_ READ isNarrowView NOTIFY narrowViewChanged) + Q_PROPERTY(webrtc::State callState READ callState NOTIFY callStateChanged) + Q_PROPERTY(QString callPartyName READ callPartyName NOTIFY callPartyChanged) + Q_PROPERTY(QString callPartyAvatarUrl READ callPartyAvatarUrl NOTIFY callPartyChanged) + Q_PROPERTY(bool isMicMuted READ isMicMuted NOTIFY micMuteChanged) public: TimelineViewManager(QSharedPointer userSettings, @@ -49,6 +54,11 @@ public: Q_INVOKABLE TimelineModel *activeTimeline() const { return timeline_; } Q_INVOKABLE bool isInitialSync() const { return isInitialSync_; } bool isNarrowView() const { return isNarrowView_; } + webrtc::State callState() const { return WebRTCSession::instance().state(); } + QString callPartyName() const { return callManager_->callPartyName(); } + QString callPartyAvatarUrl() const { return callManager_->callPartyAvatarUrl(); } + bool isMicMuted() const { return WebRTCSession::instance().isMicMuted(); } + Q_INVOKABLE void toggleMicMute(); Q_INVOKABLE void openImageOverlay(QString mxcUrl, QString eventId) const; Q_INVOKABLE QColor userColor(QString id, QColor background); Q_INVOKABLE QString escapeEmoji(QString str) const; @@ -78,6 +88,9 @@ signals: void inviteUsers(QStringList users); void showRoomList(); void narrowViewChanged(); + void callStateChanged(webrtc::State); + void callPartyChanged(); + void micMuteChanged(); public slots: void updateReadReceipts(const QString &room_id, const std::vector &event_ids);