Compare commits

...

4 Commits

Author SHA1 Message Date
c7c8e2e8f6 misc: show "deleted message" for deleted message
So that users can know when a message is deleted and can check
previous version like any edited version.

Change-Id: If3c1daeec930bd4b0f359573b971790199397489
2023-02-01 13:39:26 -05:00
b16131e63a windows sharing: use windows id instead of name
Change-Id: Ie1708ae4be049206fb8337db1211b7844e45726f
2023-02-01 11:28:40 -05:00
86b84ea17e feature: save participant's view
Change-Id: I790f10542aed306a7416a4ce79f2eaf7a770135a
Gitlab: #698
2023-01-31 15:26:58 -05:00
9ffbf4ae1c macOS: update signature for Sparkle
This patch sets up app updates to use EdDSA signature as now
it is required by Sparkle

Change-Id: I68a581e21850f04a819f4fe7ea49a33766031e01
2023-01-30 11:10:46 -05:00
40 changed files with 459 additions and 160 deletions

View File

@ -449,12 +449,6 @@ else() # APPLE
HINTS ${sparkle_dir})
add_definitions(-DENABLE_SPARKLE)
message("Sparkle is here:" ${SPARKLE_FRAMEWORK})
set(PUBLIC_KEY_PATH "${sparkle_dir}/dsa_pub.pem")
set_source_files_properties(
${PUBLIC_KEY_PATH}
PROPERTIES
MACOSX_PACKAGE_LOCATION Resources)
set(PUBLIC_KEY ${PUBLIC_KEY_PATH})
endif()
if(BETA)
message(STATUS "Beta config enabled")
@ -722,7 +716,7 @@ else()
${CMAKE_CURRENT_SOURCE_DIR}/resources/images/jami.icns)
set(libs ${QT_LIBS} ${SYSTEM_CONFIGURATUION} qrencode ${LIBCLIENT_NAME})
if(ENABLE_SPARKLE)
set(resources ${resources} ${PUBLIC_KEY} ${SPARKLE_FRAMEWORK})
set(resources ${resources} ${SPARKLE_FRAMEWORK})
set(libs ${libs} ${SPARKLE_FRAMEWORK})
endif(ENABLE_SPARKLE)
target_sources(${PROJECT_NAME} PRIVATE ${resources})
@ -766,6 +760,7 @@ else()
else()
set_target_properties(${PROJECT_NAME} PROPERTIES
SPARKLE_URL "${SPARKLE_URL}"
SPARKLE_PUBLIC_KEY "${SPARKLE_PUBLIC_KEY}"
XCODE_ATTRIBUTE_CODE_SIGN_ENTITLEMENTS "${CMAKE_CURRENT_SOURCE_DIR}/resources/entitlements/Jami.entitlements"
XCODE_ATTRIBUTE_ENABLE_HARDENED_RUNTIME TRUE)
endif()

2
daemon

Submodule daemon updated: 33f089ef50...5e35e71d09

View File

@ -1,36 +0,0 @@
-----BEGIN PUBLIC KEY-----
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
ZnO5P9x/rPIJLCXY4xQYBryQCMu6JC5ibWzP
-----END PUBLIC KEY-----

View File

@ -1,11 +0,0 @@
#!/bin/bash
set -e
set -o pipefail
if [ "$#" -ne 2 ]; then
echo "Usage: $0 update_archive private_key"
exit 1
fi
openssl=/usr/bin/openssl
$openssl dgst -sha1 -binary < "$1" | $openssl dgst -dss1 -sign "$2" | base64 $BASE64_OPTS

View File

@ -6,13 +6,12 @@ REPO_FOLDER=$1
SPARKLE_FILE=$2
REPO_URL=$3
PACKAGE=$4
DSA_KEY=$5
CHANNEL_NAME=$6
VERSION=$7
BUILD=$8
CHANNEL_NAME=$5
VERSION=$6
BUILD=$7
if [ ! -f ${PACKAGE} -o ! -f ${DSA_KEY} ]; then
echo "Can't find package or dsa key, aborting..."
if [ ! -f ${PACKAGE} ]; then
echo "Can't find package, aborting..."
exit 1
fi
@ -20,7 +19,6 @@ if [ -f ${REPO_FOLDER}/${SPARKLE_FILE} ]; then
ITEMS=$(sed -n "/<item>/,/<\/item>/p" ${REPO_FOLDER}/${SPARKLE_FILE})
fi
PACKAGE_SIZE=`stat -f%z ${PACKAGE}`
DATE_RFC2822=`date "+%a, %d %b %Y %T %z"`
cat << EOFILE > ${REPO_FOLDER}/${SPARKLE_FILE}
@ -37,7 +35,7 @@ cat << EOFILE > ${REPO_FOLDER}/${SPARKLE_FILE}
<sparkle:version>${BUILD}</sparkle:version>
<sparkle:shortVersionString>${VERSION}</sparkle:shortVersionString>
<sparkle:minimumSystemVersion>10.15.0</sparkle:minimumSystemVersion>
<enclosure url="${REPO_URL}/$(basename ${PACKAGE})" length="$PACKAGE_SIZE" type="application/octet-stream" sparkle:dsaSignature="$(./sign_update.sh ${PACKAGE} ${DSA_KEY})" />
<enclosure url="${REPO_URL}/$(basename ${PACKAGE})" type="application/octet-stream" $(./sign_update ${PACKAGE}) />
</item>
$(echo -e "${ITEMS}")
</channel>

View File

@ -24,8 +24,8 @@
<string>public.app-category.social-networking</string>
<key>NSHumanReadableCopyright</key>
<string>${MACOSX_BUNDLE_COPYRIGHT}</string>
<key>SUPublicDSAKeyFile</key>
<string>dsa_pub.pem</string>
<key>SUPublicEDKey</key>
<string>${SPARKLE_PUBLIC_KEY}</string>
<key>SUFeedURL</key>
<string>${SPARKLE_URL}</string>
<key>NSPrincipalClass</key>

View File

@ -36,6 +36,7 @@ extern const QString defaultDownloadPath;
#define KEYS \
X(MinimizeOnClose, true) \
X(DownloadPath, defaultDownloadPath) \
X(ScreenshotPath, {}) \
X(EnableNotifications, true) \
X(EnableTypingIndicator, true) \
X(EnableReadReceipt, true) \

View File

@ -553,7 +553,7 @@ CallAdapter::fillParticipantData(QJsonObject& participant) const
auto uri = participant[URI].toString();
participant[ISLOCAL] = false;
if (uri == accInfo.profileInfo.uri && participant[DEVICE] == getCurrentDeviceId(accInfo)) {
participant[BESTNAME] = tr("me");
participant[BESTNAME] = tr("Me");
participant[ISLOCAL] = true;
} else {
try {
@ -1145,6 +1145,27 @@ CallAdapter::updateAdvancedInformation()
}
}
bool
CallAdapter::takeScreenshot(const QImage& image, const QString& path)
{
QString name = QString("%1 %2")
.arg(tr("Screenshot"))
.arg(QDateTime::currentDateTime().toString(Qt::ISODate));
bool fileAlreadyExists = true;
int nb = 0;
QString filePath = QString("%1%2.png").arg(path).arg(name);
while (fileAlreadyExists) {
filePath = QString("%1%2.png").arg(path).arg(name);
if (nb)
filePath = QString("%1(%2).png").arg(filePath).arg(QString::number(nb));
QFileInfo check_file(filePath);
fileAlreadyExists = check_file.exists() && check_file.isFile();
nb++;
}
return image.save(filePath, "PNG");
}
void
CallAdapter::preventScreenSaver(bool state)
{

View File

@ -98,6 +98,8 @@ public:
Q_INVOKABLE void setCallInfo();
Q_INVOKABLE void updateAdvancedInformation();
Q_INVOKABLE bool takeScreenshot(const QImage &image, const QString &path);
Q_SIGNALS:
void callStatusChanged(int index, const QString& accountId, const QString& convUid);
void callInfosChanged(const QVariant& infos, const QString& accountId, const QString& convUid);

View File

@ -55,8 +55,9 @@ BaseModalDialog {
anchors.centerIn: parent
Text {
Layout.preferredWidth: root.width / 4
Layout.maximumWidth: root.width / 2
Layout.leftMargin: JamiTheme.settingsMarginSize
elide: Text.ElideRight
text: MessagesAdapter.getFormattedDay(modelData.timestamp.toString())
+ " - " + MessagesAdapter.getFormattedTime(modelData.timestamp.toString())
@ -72,7 +73,7 @@ BaseModalDialog {
id: metrics
elide: Text.ElideRight
elideWidth: 3 * rowBody.width / 4 - 2 * JamiTheme.preferredMarginSize
text: modelData.body
text: modelData.body === "" ? JamiStrings.deletedMessage : modelData.body
}
text: metrics.elidedText

View File

@ -31,7 +31,7 @@ Item {
property real contentWidth: bubble.width
property var emojiTexts: ownEmojiList
visible: emojis ? emojis.length : false
visible: emojis.length && Body !== ""
property string emojis: {
var space = ""

View File

@ -88,7 +88,7 @@ Item {
id: metrics
elide: Text.ElideRight
elideWidth: JamiTheme.preferredFieldWidth - JamiTheme.preferredMarginSize
text: ReplyToBody
text: ReplyToBody === "" && ReplyToAuthor !== "" ? "*(Deleted Message)*" : ReplyToBody
}
textFormat: Text.MarkdownText

View File

@ -174,10 +174,10 @@ Control {
anchors.verticalCenter: parent.verticalCenter
anchors.right: isOutgoing ? optionButtonItem.right : undefined
anchors.left: !isOutgoing ? optionButtonItem.left : undefined
visible: bubbleArea.bubbleHovered
visible: Body !== "" && (bubbleArea.bubbleHovered
|| hovered
|| reply.hovered
|| bgHandler.hovered
|| bgHandler.hovered)
source: JamiResources.more_vert_24dp_svg
width: optionButtonItem.width / 2
height: optionButtonItem.height
@ -202,10 +202,10 @@ Control {
anchors.verticalCenter: parent.verticalCenter
anchors.right: isOutgoing ? more.left : undefined
anchors.left: !isOutgoing ? more.right : undefined
visible: bubbleArea.bubbleHovered
visible: Body !== "" && (bubbleArea.bubbleHovered
|| hovered
|| more.hovered
|| bgHandler.hovered
|| bgHandler.hovered)
onClicked: {
MessagesAdapter.editId = ""

View File

@ -58,7 +58,7 @@ SBSMessageBase {
padding: isEmojiOnly ? 0 : JamiTheme.preferredMarginSize
anchors.right: isOutgoing ? parent.right : undefined
text: Body
text: Body === "" ? "*("+ JamiStrings.deletedMessage +")*" : Body
horizontalAlignment: Text.AlignLeft
HoverHandler {

View File

@ -49,6 +49,7 @@ MenuItem {
property int itemTextMargin: 20
signal clicked
property bool itemHovered: menuItemContentRect.hovered
contentItem: AbstractButton {
id: menuItemContentRect

View File

@ -298,6 +298,8 @@ Item {
property string lowerHand: qsTr("Lower hand")
property string raiseHand: qsTr("Raise hand")
property string layoutSettings: qsTr("Layout settings")
property string tileScreenshot: qsTr("Take tile screenshot")
property string screenshotTaken: qsTr("Screenshot saved to %1")
//advanced information
property string renderersInformation: qsTr("Renderers information")
@ -350,6 +352,7 @@ Item {
property string backendError: qsTr("This is the error from the backend: %0")
property string disabledAccount: qsTr("The account is disabled")
property string noNetworkConnectivity: qsTr("No network connectivity")
property string deletedMessage: qsTr("Deleted message")
// Chatview footer
property string jumpToLatest: qsTr("Jump to latest")
@ -508,15 +511,16 @@ Item {
// Context Menu
property string saveFile: qsTr("Save file")
property string openLocation: qsTr("Open location")
property string me: qsTr("Me")
// Updates
property string betaInstall: qsTr("Install beta version")
property string checkForUpdates: qsTr("Check for updates now")
property string enableAutoUpdates: qsTr("Enable/Disable automatic updates")
property string tipAutoUpdate: qsTr("toggle automatic updates")
property string tipAutoUpdate: qsTr("Toggle automatic updates")
property string updatesTitle: qsTr("Updates")
property string updateDialogTitle: qsTr("Update")
property string updateFound: qsTr("A new version of Jami was found\n Would you like to update now?")
property string updateFound: qsTr("A new version of Jami was found\nWould you like to update now?")
property string updateNotFound: qsTr("No new version of Jami was found")
property string updateCheckError: qsTr("An error occured when checking for a new version")
property string updateNetworkError: qsTr("Network error")
@ -538,7 +542,8 @@ Item {
// Recording Settings
property string tipRecordFolder: qsTr("Select a record directory")
property string quality: qsTr("Quality")
property string saveIn: qsTr("Save in")
property string saveRecordingsTo: qsTr("Save recordings to")
property string saveScreenshotsTo: qsTr("Save screenshots to")
property string callRecording: qsTr("Call Recording")
property string alwaysRecordCalls: qsTr("Always record calls")

View File

@ -146,6 +146,11 @@ Item {
property color spinboxBackgroundColor: darkTheme ? editBackgroundColor : selectedColor
property color spinboxBorderColor: darkTheme ? tintedBlue : Qt.rgba(0, 0.34, 0.6, 0.36)
//Toast
property color toastColor: darkTheme ? "#f0f0f0" : "#000000"
property color toastRectColor: !darkTheme ? "#f0f0f0" : "#000000"
property real toastFontSize: calcSize(15)
// Call buttons
property color acceptButtonGreen: "#4caf50"
property color acceptButtonHoverGreen: "#5db761"

View File

@ -174,10 +174,12 @@ MainApplication::init()
Qt::DirectConnection);
auto downloadPath = settingsManager_->getValue(Settings::Key::DownloadPath);
auto screenshotPath = settingsManager_->getValue(Settings::Key::ScreenshotPath);
auto allowTransferFromTrusted = settingsManager_->getValue(Settings::Key::AutoAcceptFiles)
.toBool();
auto acceptTransferBelow = settingsManager_->getValue(Settings::Key::AcceptTransferBelow).toInt();
lrcInstance_->accountModel().downloadDirectory = downloadPath.toString() + "/";
lrcInstance_->accountModel().screenshotDirectory = screenshotPath.toString();
lrcInstance_->accountModel().autoTransferFromTrusted = allowTransferFromTrusted;
lrcInstance_->accountModel().autoTransferSizeThreshold = acceptTransferBelow;

View File

@ -39,6 +39,7 @@ Item {
signal chatButtonClicked
signal fullScreenClicked
signal closeClicked
function closeContextMenuAndRelatedWindows() {
ContactPickerCreation.closeContactPicker()
@ -46,14 +47,22 @@ Item {
SelectScreenWindowCreation.destroySelectScreenWindow()
ScreenRubberBandCreation.destroyScreenRubberBandWindow()
PluginHandlerPickerCreation.closePluginHandlerPicker()
root.closeClicked()
callInformationOverlay.close()
}
// x, y position does not need to be translated
// since they all fill the call page
function openCallViewContextMenuInPos(x, y) {
function openCallViewContextMenuInPos(x, y,
hoveredOverlayUri,
hoveredOverlaySinkId,
hoveredOverVideoMuted)
{
callViewContextMenu.x = x
callViewContextMenu.y = y
callViewContextMenu.hoveredOverlayUri = hoveredOverlayUri
callViewContextMenu.hoveredOverlaySinkId = hoveredOverlaySinkId
callViewContextMenu.hoveredOverVideoMuted = hoveredOverVideoMuted
callViewContextMenu.openMenu()
}
@ -171,10 +180,16 @@ Item {
onTransferCallButtonClicked: openContactPicker(ContactList.TRANSFER)
onPluginItemClicked: openPluginsMenu()
onScreenshotTaken: {
toastManager.instantiateToast();
}
onRecordCallClicked: CallAdapter.recordThisCallToggle()
onOpenSelectionWindow: {
SelectScreenWindowCreation.createSelectScreenWindowObject(appWindow)
SelectScreenWindowCreation.showSelectScreenWindow(callPreviewId, windowSelection)
}
onScreenshotButtonHoveredChanged: {
participantsLayer.screenshotButtonHovered = screenshotButtonHovered
}
}
}

View File

@ -37,6 +37,12 @@ ContextMenuAutoLoader {
signal transferCallButtonClicked
signal recordCallClicked
signal openSelectionWindow
signal screenshotTaken
property bool screenshotButtonHovered: screenShot.itemHovered
property string hoveredOverlayUri: ""
property string hoveredOverlaySinkId: ""
property bool hoveredOverVideoMuted: true
property list<GeneralMenuItem> menuItems: [
GeneralMenuItem {
@ -194,8 +200,34 @@ ContextMenuAutoLoader {
CallAdapter.startTimerInformation();
callInformationOverlay.open()
}
},
GeneralMenuItem {
id: screenShot
canTrigger: hoveredOverlayUri !== "" && hoveredOverVideoMuted === false
itemName: JamiStrings.tileScreenshot
iconSource: JamiResources.baseline_camera_alt_24dp_svg
MaterialToolTip {
id: tooltip
parent: screenShot
visible: screenShot.itemHovered
delay: Qt.styleHints.mousePressAndHoldInterval
property bool isMe: CurrentAccount.uri === hoveredOverlayUri
text: isMe ? JamiStrings.me
: UtilsAdapter.getBestNameForUri(CurrentAccount.id, hoveredOverlayUri)
}
onClicked: {
if (CallAdapter.takeScreenshot(videoProvider.captureRawVideoFrame(hoveredOverlaySinkId),
UtilsAdapter.getDirScreenshot())) {
screenshotTaken()
}
}
}
]
Component.onCompleted: menuItemsToLoad = menuItems
}

View File

@ -62,7 +62,9 @@ RowLayout {
anchors.centerIn: parent
text: shortcut
text: shortcut2 === "" ?
shortcut :
shortcut + " + " + shortcut2
font.pointSize: JamiTheme.textFontSize + 3
font.weight: Font.DemiBold
color: JamiTheme.textColor

View File

@ -40,42 +40,52 @@ Window {
ListElement {
shortcut: "Ctrl + J"
shortcut2: ""
description: qsTr("Open account list")
}
ListElement {
shortcut: "Ctrl + L"
shortcut2: ""
description: qsTr("Focus conversations list")
}
ListElement {
shortcut: "Ctrl + R"
shortcut2: ""
description: qsTr("Requests list")
}
ListElement {
shortcut: "Ctrl + ↑"
shortcut2: ""
description: qsTr("Previous conversation")
}
ListElement {
shortcut: "Ctrl + ↓"
shortcut2: ""
description: qsTr("Next conversation")
}
ListElement {
shortcut: "Ctrl + F"
shortcut2: ""
description: qsTr("Search bar")
}
ListElement {
shortcut: "F11"
shortcut2: ""
description: qsTr("Full screen")
}
ListElement {
shortcut: "Ctrl + +"
shortcut2: ""
description: qsTr("Increase font size")
}
ListElement {
shortcut: "Ctrl + -"
shortcut2: ""
description: qsTr("Decrease font size")
}
ListElement {
shortcut: "Ctrl + 0"
shortcut2: ""
description: qsTr("Reset font size")
}
}
@ -85,34 +95,42 @@ Window {
ListElement {
shortcut: "Ctrl + Shift + C"
shortcut2: ""
description: qsTr("Start an audio call")
}
ListElement {
shortcut: "Ctrl + Shift + X"
shortcut2: ""
description: qsTr("Start a video call")
}
ListElement {
shortcut: "Ctrl + Shift + L"
shortcut2: ""
description: qsTr("Clear history")
}
ListElement {
shortcut: "Ctrl + Shift + B"
shortcut2: ""
description: qsTr("Block contact")
}
ListElement {
shortcut: "Ctrl + Shift + Delete"
shortcut2: ""
description: qsTr("Remove conversation")
}
ListElement {
shortcut: "Shift + Ctrl + A"
shortcut2: ""
description: qsTr("Accept contact request")
}
ListElement {
shortcut: "↑"
shortcut2: ""
description: qsTr("Edit last message")
}
ListElement {
shortcut: "Esc"
shortcut2: ""
description: qsTr("Cancel message edition")
}
}
@ -122,26 +140,32 @@ Window {
ListElement {
shortcut: "Ctrl + M"
shortcut2: ""
description: qsTr("Media settings")
}
ListElement {
shortcut: "Ctrl + G"
shortcut2: ""
description: qsTr("General settings")
}
ListElement {
shortcut: "Ctrl + I"
shortcut2: ""
description: qsTr("Account settings")
}
ListElement {
shortcut: "Ctrl + P"
shortcut2: ""
description: qsTr("Plugin settings")
}
ListElement {
shortcut: "Ctrl + Shift + N"
shortcut2: ""
description: qsTr("Open account creation wizard")
}
ListElement {
shortcut: "F10"
shortcut2: ""
description: qsTr("Open keyboard shortcut table")
}
}
@ -151,24 +175,34 @@ Window {
ListElement {
shortcut: "Ctrl + Y"
shortcut2: ""
description: qsTr("Answer an incoming call")
}
ListElement {
shortcut: "Ctrl + D"
shortcut2: ""
description: qsTr("End call")
}
ListElement {
shortcut: "Ctrl + Shift + D"
shortcut2: ""
description: qsTr("Decline the call request")
}
ListElement {
shortcut: "M"
shortcut2: ""
description: qsTr("Mute microphone")
}
ListElement {
shortcut: "V"
shortcut2: ""
description: qsTr("Stop camera")
}
ListElement {
shortcut: "Ctrl"
shortcut2: qsTr("Mouse middle click")
description: qsTr("Take tile screenshot")
}
}
Rectangle {

View File

@ -215,8 +215,7 @@ JamiListView {
readonly property int mergeType: Interaction.Type.MERGE
readonly property int editedType: Interaction.Type.EDITED
readonly property int reactionType: Interaction.Type.REACTION
expression: Body !== ""
&& Type !== mergeType
expression: Type !== mergeType
&& Type !== editedType
&& Type !== reactionType
}

View File

@ -166,7 +166,10 @@ Rectangle {
onTapped: function (eventPoint, button) {
if (button === Qt.RightButton) {
callOverlay.openCallViewContextMenuInPos(eventPoint.position.x,
eventPoint.position.y)
eventPoint.position.y,
participantsLayer.hoveredOverlayUri,
participantsLayer.hoveredOverlaySinkId,
participantsLayer.hoveredOverVideoMuted)
}
}
}
@ -184,6 +187,7 @@ Rectangle {
ParticipantsLayer {
id: participantsLayer
anchors.fill: parent
anchors.centerIn: parent
anchors.margins: 1
@ -191,9 +195,18 @@ Rectangle {
participantsSide: callOverlay.participantsSide
}
ToastManager {
id: toastManager
anchors.fill: parent
function instantiateToast() {
instantiate(JamiStrings.screenshotTaken.arg(UtilsAdapter.getDirScreenshot()),1000,400)
}
}
LocalVideo {
id: previewRenderer
visible: (CurrentCall.isSharing || !CurrentCall.isVideoMuted)
&& !CurrentCall.isConference
@ -329,6 +342,11 @@ Rectangle {
openInCallConversation()
}
}
onCloseClicked: {
participantsLayer.hoveredOverlayUri = ""
participantsLayer.hoveredOverlaySinkId = ""
participantsLayer.hoveredOverVideoMuted = true
}
onChatButtonClicked: {
inCallMessageWebViewStack.visible ?

View File

@ -67,6 +67,18 @@ Item {
property string muteAlertMessage: ""
property bool muteAlertActive: false
property bool participantHovered: hoverIndicator.hovered
property bool isScreenshotButtonHovered: false
function takeScreenshot() {
if (!hoveredOverVideoMuted) {
if (CallAdapter.takeScreenshot(videoProvider.captureRawVideoFrame(hoveredOverlaySinkId),
UtilsAdapter.getDirScreenshot())) {
toastManager.instantiateToast();
}
}
}
onMuteAlertActiveChanged: {
if (muteAlertActive) {
alertTimer.restart()
@ -94,9 +106,11 @@ Item {
Rectangle {
z: -1
color: JamiTheme.buttonTintedBlue
border.color: JamiTheme.buttonTintedBlue
border.width: 2
color: "transparent"
radius: 10
visible:voiceActive
visible: voiceActive || isScreenshotButtonHovered
width: participantIsActive ? mediaDistRender.contentRect.width + 2 : undefined
height: participantIsActive ? mediaDistRender.contentRect.height + 2 : undefined
anchors.centerIn: participantIsActive ? parent : undefined
@ -109,7 +123,6 @@ Item {
anchors.margins: 2
rendererId: root.sinkId
crop: !participantIsActive
underlayItems: Avatar {
property real componentSize: Math.min(mediaDistRender.contentRect.width / 2, mediaDistRender.contentRect.height / 2)
height: componentSize
@ -140,7 +153,25 @@ Item {
anchors.centerIn: participantIsActive ? parent : undefined
anchors.fill: participantIsActive ? undefined : parent
TapHandler {
acceptedButtons: Qt.MiddleButton
acceptedModifiers: Qt.ControlModifier
onTapped: {
takeScreenshot()
}
}
MultiPointTouchArea {
anchors.fill: parent
minimumTouchPoints: 3
onPressed: {
takeScreenshot()
}
}
HoverHandler {
id: hoverIndicator
onPointChanged: {
participantRect.opacity = 1
fadeOutTimer.restart()
@ -164,6 +195,7 @@ Item {
// Participant buttons for moderation
ParticipantOverlayMenu {
id: overlayMenu
visible: isMe || meModerator
anchors.fill: parent
@ -209,6 +241,7 @@ Item {
RowLayout {
id: participantFootInfo
height: parent.height
anchors.verticalCenter: parent.verticalCenter
Text {

View File

@ -37,6 +37,10 @@ Item {
property bool inLine: CallParticipantsModel.conferenceLayout === CallParticipantsModel.ONE_WITH_SMALL
property bool participantsSide
property bool enableHideSpectators: CallParticipantsModel.count > 1 && CurrentCall.hideSpectators
property string hoveredOverlayUri: ""
property string hoveredOverlaySinkId: ""
property bool hoveredOverVideoMuted: true
property bool screenshotButtonHovered: false
onVisibleChanged: {
CurrentCall.hideSelf = UtilsAdapter.getAppValue(Settings.HideSelf)
@ -51,7 +55,10 @@ Item {
anchors.fill: parent
anchors.leftMargin: leftMargin_
isScreenshotButtonHovered: screenshotButtonHovered && hoveredOverlaySinkId === sinkId_
opacity: screenshotButtonHovered
? hoveredOverlaySinkId !== sinkId ? 0.1 : 1
: 1
sinkId: sinkId_
uri: uri_
deviceId: deviceId_
@ -70,6 +77,14 @@ Item {
participantIsModeratorMuted: audioModeratorMuted_
participantHandIsRaised: isHandRaised_
onParticipantHoveredChanged: {
if (participantHovered) {
hoveredOverlayUri = overlay.uri
hoveredOverlaySinkId = overlay.sinkId
hoveredOverVideoMuted = videoMuted_
}
}
Connections {
id: registeredNameFoundConnection

View File

@ -284,6 +284,7 @@ Window {
border.color: selectedScreenNumber === index ? JamiTheme.screenSelectionBorderColor : JamiTheme.tabbarBorderColor
visible: root.window && JamiStrings.selectScreen !== screens[index] && index >= Qt.application.screens.length
&& screenName2.text != JamiStrings.selectWindow
Text {
id: screenName2

View File

@ -0,0 +1,72 @@
/*
* Copyright (C) 2023 Savoir-faire Linux Inc.
* Author: Vengeon Nicolas <nicolas.vengeon@savoirfairelinux.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 3 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, see <https://www.gnu.org/licenses/>.
*/
import QtQuick
import net.jami.Constants 1.1
Rectangle {
id: root
anchors.top: parent.top
anchors.horizontalCenter: parent.horizontalCenter
width: textMessage.width + 20
height: textMessage.height + 10
anchors.topMargin: 10
radius: 15
color: JamiTheme.toastRectColor
property int duration
property int fadingTime
property string message
Component.onCompleted: {
anim.start();
}
Text {
id: textMessage
anchors.centerIn: root
text: message
font.pointSize: JamiTheme.toastFontSize
color: JamiTheme.toastColor
}
SequentialAnimation on opacity {
id: anim
running: false
NumberAnimation {
to: 0.9
duration: root.fadingTime
}
PauseAnimation {
duration: root.duration
}
NumberAnimation {
to: 0
duration: root.fadingTime
}
onRunningChanged: {
if (!running)
root.destroy();
}
}
}

View File

@ -0,0 +1,28 @@
/*
* Copyright (C) 2023 Savoir-faire Linux Inc.
* Author: Vengeon Nicolas <nicolas.vengeon@savoirfairelinux.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 3 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, see <https://www.gnu.org/licenses/>.
*/
import QtQuick
Item {
id: root
function instantiate(message, duration, fadingTime) {
var component = Qt.createComponent("Toast.qml");
var sprite = component.createObject(root, {message: message, duration: duration, fadingTime: fadingTime});
}
}

View File

@ -28,24 +28,32 @@ import net.jami.Constants 1.1
import "../../commoncomponents"
ColumnLayout {
id:root
id: root
property int itemWidth
property string recordPath: AVModel.getRecordPath()
property string screenshotPath: UtilsAdapter.getDirScreenshot()
onRecordPathChanged: {
if(recordPath === "") return
if(recordPath === "")
return
if(AVModel){
if(AVModel) {
AVModel.setRecordPath(recordPath)
}
}
onScreenshotPathChanged: {
if (screenshotPath === "")
return
UtilsAdapter.setScreenshotPath(screenshotPath)
}
FolderDialog {
id: recordPathDialog
title: JamiStrings.selectFolder
currentFolder: StandardPaths.writableLocation(StandardPaths.HomeLocation)
currentFolder: UtilsAdapter.getDirScreenshot()
options: FolderDialog.ShowDirsOnly
onAccepted: {
@ -54,6 +62,19 @@ ColumnLayout {
}
}
FolderDialog {
id: screenshotPathDialog
title: JamiStrings.selectFolder
currentFolder: StandardPaths.writableLocation(StandardPaths.PicturesLocation)
options: FolderDialog.ShowDirsOnly
onAccepted: {
var dir = UtilsAdapter.getAbsPath(folder.toString())
screenshotPath = dir
}
}
Timer{
id: updateRecordQualityTimer
@ -172,7 +193,7 @@ ColumnLayout {
Layout.fillWidth: true
Layout.fillHeight: true
text: JamiStrings.saveIn
text: JamiStrings.saveRecordingsTo
color: JamiTheme.textColor
font.pointSize: JamiTheme.settingsFontSize
font.kerning: true
@ -199,4 +220,41 @@ ColumnLayout {
onClicked: recordPathDialog.open()
}
}
RowLayout {
Layout.fillWidth: true
Layout.preferredHeight: JamiTheme.preferredFieldHeight
Layout.leftMargin: JamiTheme.preferredMarginSize
Label {
Layout.fillWidth: true
Layout.fillHeight: true
text: JamiStrings.saveScreenshotsTo
color: JamiTheme.textColor
font.pointSize: JamiTheme.settingsFontSize
font.kerning: true
horizontalAlignment: Text.AlignLeft
verticalAlignment: Text.AlignVCenter
}
MaterialButton {
id: screenshotPathButton
Layout.alignment: Qt.AlignRight
preferredWidth: itemWidth
preferredHeight: JamiTheme.preferredFieldHeight
toolTipText: UtilsAdapter.getDirScreenshot()
text: screenshotPath
iconSource: JamiResources.round_folder_24dp_svg
color: JamiTheme.buttonTintedGrey
hoveredColor: JamiTheme.buttonTintedGreyHovered
pressedColor: JamiTheme.buttonTintedGreyPressed
onClicked: screenshotPathDialog.open()
}
}
}

View File

@ -387,6 +387,22 @@ UtilsAdapter::getDirDocument()
QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation));
}
QString
UtilsAdapter::getDirScreenshot()
{
QString screenshotPath = lrcInstance_->accountModel().screenshotDirectory;
if (screenshotPath.isEmpty()) {
QString folderName = "Jami";
auto picture = QStandardPaths::writableLocation(QStandardPaths::PicturesLocation);
QDir dir;
dir.mkdir(picture + QDir::separator() + folderName);
screenshotPath = picture + QDir::separator() + folderName;
setScreenshotPath(screenshotPath);
lrcInstance_->accountModel().screenshotDirectory = screenshotPath;
}
return screenshotPath;
}
QString
UtilsAdapter::getDirDownload()
{
@ -425,6 +441,13 @@ UtilsAdapter::setDownloadPath(QString dir)
lrcInstance_->accountModel().downloadDirectory = dir + "/";
}
void
UtilsAdapter::setScreenshotPath(QString dir)
{
setAppValue(Settings::Key::ScreenshotPath, dir);
lrcInstance_->accountModel().screenshotDirectory = dir + QDir::separator();
}
void
UtilsAdapter::monitor(const bool& continuous)
{

View File

@ -107,9 +107,11 @@ public:
Q_INVOKABLE QVariant getAppValue(const Settings::Key key);
Q_INVOKABLE void setAppValue(const Settings::Key key, const QVariant& value);
Q_INVOKABLE QString getDirDocument();
Q_INVOKABLE QString getDirScreenshot();
Q_INVOKABLE QString getDirDownload();
Q_INVOKABLE void setRunOnStartUp(bool state);
Q_INVOKABLE void setDownloadPath(QString dir);
Q_INVOKABLE void setScreenshotPath(QString dir);
Q_INVOKABLE void monitor(const bool& continuous);
Q_INVOKABLE void clearInteractionsCache(const QString& accountId, const QString& convUid);
Q_INVOKABLE QVariantMap supportedLang();

View File

@ -117,6 +117,13 @@ VideoProvider::frame(const QString& id)
QString
VideoProvider::captureVideoFrame(const QString& id)
{
auto img = captureRawVideoFrame(id);
return Utils::byteArrayToBase64String(Utils::QImageToByteArray(img));
}
QImage
VideoProvider::captureRawVideoFrame(const QString& id)
{
QMutexLocker framesLk(&framesObjsMutex_);
if (auto* videoFrame = frame(id)) {
@ -127,7 +134,7 @@ VideoProvider::captureVideoFrame(const QString& id)
videoFrame->height(),
videoFrame->bytesPerLine(0),
imageFormat);
return Utils::byteArrayToBase64String(Utils::QImageToByteArray(img));
return img;
}
return {};
}

View File

@ -46,6 +46,7 @@ public:
Q_INVOKABLE void registerSink(const QString& id, QVideoSink* obj);
Q_INVOKABLE void unregisterSink(QVideoSink* obj);
Q_INVOKABLE QString captureVideoFrame(const QString& id);
Q_INVOKABLE QImage captureRawVideoFrame(const QString& id);
private Q_SLOTS:
void onRendererStarted(const QString& id, const QSize& size);

View File

@ -73,6 +73,7 @@ public:
* Should contains the full directory with the end marker (/ on linux for example)
*/
QString downloadDirectory;
QString screenshotDirectory;
/**
* Accept transfer from trusted contacts
*/

View File

@ -564,10 +564,13 @@ AVModel::stopPreview(const QString& resource)
BOOL
IsAltTabWindow(HWND hwnd)
{
LONG style = GetWindowLong(hwnd, GWL_STYLE);
if (!((style & WS_DISABLED) != WS_DISABLED)) {
auto styles = (DWORD) GetWindowLongPtr(hwnd, GWL_STYLE);
auto ex_styles = (DWORD) GetWindowLongPtr(hwnd, GWL_EXSTYLE);
if (ex_styles & WS_EX_TOOLWINDOW)
return false;
if (styles & WS_CHILD)
return false;
}
DWORD cloaked = FALSE;
HRESULT hrTemp = DwmGetWindowAttribute(hwnd, DWMWA_CLOAKED, &cloaked, sizeof(cloaked));
@ -591,20 +594,9 @@ IsAltTabWindow(HWND hwnd)
BOOL CALLBACK
CbEnumAltTab(HWND hwnd, LPARAM lParam)
{
// Do not show invisible windows
if (!IsWindowVisible(hwnd))
return TRUE;
// Alt-tab test as described by Raymond Chen
if (!IsAltTabWindow(hwnd))
return TRUE;
const size_t MAX_WINDOW_NAME = 256;
TCHAR windowName[MAX_WINDOW_NAME];
if (hwnd == GetShellWindow())
return TRUE;
else
GetWindowText(hwnd, windowName, MAX_WINDOW_NAME);
GetWindowText(hwnd, windowName, MAX_WINDOW_NAME);
// Do not show windows that has no caption
if (0 == windowName[0])
@ -613,14 +605,27 @@ CbEnumAltTab(HWND hwnd, LPARAM lParam)
std::wstring msg = std::wstring(windowName);
auto name = QString::fromStdWString(msg);
// Do not show invisible windows
if (!IsWindowVisible(hwnd))
return TRUE;
// Alt-tab test as described by Raymond Chen
if (!IsAltTabWindow(hwnd))
return TRUE;
auto isShellWindow = hwnd == GetShellWindow();
if (isShellWindow)
return TRUE;
QMap<QString, QVariant>* windowList = reinterpret_cast<QMap<QString, QVariant>*>(lParam);
auto keys = windowList->keys();
if (keys.indexOf(name) > 0) {
return FALSE;
} else {
DWORD processId;
GetWindowThreadProcessId(hwnd, &processId);
windowList->insert(name, QVariant::fromValue(processId));
std::stringstream ss;
ss << hwnd;
windowList->insert(name, QString::fromStdString(ss.str()));
}
return TRUE;

View File

@ -949,30 +949,6 @@ CallModel::getDisplay(int idx, int x, int y, int w, int h)
.arg(h);
}
#ifdef WIN32
BOOL CALLBACK
EnumWindowsProcMy(HWND hwnd, LPARAM lParam)
{
std::pair<DWORD, QString>* dataPair = reinterpret_cast<std::pair<DWORD, QString>*>(lParam);
DWORD lpdwProcessId;
if (auto parent = GetWindow(hwnd, GW_OWNER))
GetWindowThreadProcessId(parent, &lpdwProcessId);
else
GetWindowThreadProcessId(hwnd, &lpdwProcessId);
int len = GetWindowTextLength(hwnd) + 1;
std::vector<wchar_t> buf(len);
GetWindowText(hwnd, &buf[0], len);
if (lpdwProcessId == dataPair->first) {
if (!IsWindowVisible(hwnd))
return TRUE;
dataPair->second = QString::fromStdWString(&buf[0]);
return FALSE;
}
return TRUE;
}
#endif
QString
CallModel::getDisplay(const QString& windowProcessId, const QString& windowId)
{
@ -986,22 +962,10 @@ CallModel::getDisplay(const QString& windowProcessId, const QString& windowId)
.arg(windowProcessId);
#endif
#ifdef WIN32
// If window changed the name we must look for the parent process window
QString newWindowId = windowId;
auto hwnd = FindWindow(NULL, windowId.toStdWString().c_str());
if (!hwnd) {
std::pair<DWORD, QString> idName(windowProcessId.toInt(), {});
LPARAM lParam = reinterpret_cast<LPARAM>(&idName);
EnumWindows(EnumWindowsProcMy, lParam);
if (!idName.second.isEmpty()) {
newWindowId = idName.second;
}
}
ret = QString("%1%2:+0,0 window-id:title=%3")
.arg(libjami::Media::VideoProtocolPrefix::DISPLAY)
.arg(sep)
.arg(newWindowId);
ret = QString("%1%2:+0,0 window-id:hwnd=%3")
.arg(libjami::Media::VideoProtocolPrefix::DISPLAY)
.arg(sep)
.arg(windowProcessId);
#endif
return ret;
}

View File

@ -394,6 +394,8 @@ MessageListModel::roleNames() const
bool
MessageListModel::isOnlyEmoji(const QString& text) const
{
if (text.isEmpty())
return false;
auto codepointList = text.toUcs4();
for (QList<uint>::iterator it = codepointList.begin(); it != codepointList.end(); it++) {
auto cur = false;
@ -418,7 +420,7 @@ QVariant
MessageListModel::dataForItem(item_t item, int, int role) const
{
QString replyId = item.second.commit["reply-to"];
int repliedMsg;
int repliedMsg = -1;
if (!replyId.isEmpty() && (role == Role::ReplyToAuthor || role == Role::ReplyToBody)) {
repliedMsg = getIndexOfMessage(replyId);
}
@ -695,7 +697,10 @@ MessageListModel::editMessage(const QString& msgId, interaction::Info& info)
}
info.body = it->rbegin()->body;
editedBodies_.erase(it);
emitDataChanged(msgId, {MessageList::Role::Body, MessageList::Role::PreviousBodies});
emitDataChanged(msgId,
{MessageList::Role::Body,
MessageList::Role::PreviousBodies,
MessageList::Role::IsEmojiOnly});
// Body changed, replies should update
for (const auto& replyId : replyTo_[msgId]) {

View File

@ -30,16 +30,16 @@ VideoManagerInterface::VideoManagerInterface()
int width,
int height,
bool isMixer) {
Q_EMIT decodingStarted(QString::fromLatin1(QByteArray::fromStdString(id)),
QString::fromLatin1(QByteArray::fromStdString(shmPath)),
Q_EMIT decodingStarted(QString(id.c_str()),
QString(shmPath.c_str()),
width,
height,
isMixer);
}),
exportable_callback<VideoSignal::DecodingStopped>(
[this](const std::string& id, const std::string& shmPath, bool isMixer) {
Q_EMIT decodingStopped(QString::fromLatin1(QByteArray::fromStdString(id)),
QString::fromLatin1(QByteArray::fromStdString(shmPath)),
Q_EMIT decodingStopped(QString(id.c_str()),
QString(shmPath.c_str()),
isMixer);
})};
#endif

View File

@ -126,7 +126,7 @@ public Q_SLOTS: // METHODS
QString openVideoInput(const QString& resource)
{
#ifdef ENABLE_VIDEO
return QString::fromLatin1(QByteArray::fromStdString(libjami::openVideoInput(resource.toStdString())));
return libjami::openVideoInput(resource.toLatin1().toStdString()).c_str();
#endif
}
@ -150,7 +150,7 @@ public Q_SLOTS: // METHODS
bool registerSinkTarget(const QString& sinkID, const libjami::SinkTarget& target)
{
#ifdef ENABLE_VIDEO
return libjami::registerSinkTarget(sinkID.toLatin1().toStdString(), target);
return libjami::registerSinkTarget(sinkID.toStdString(), target);
#else
Q_UNUSED(sinkID)
Q_UNUSED(target)