1
0
Fork 0
mirror of https://git.jami.net/savoirfairelinux/jami-client-qt.git synced 2025-08-21 15:24:01 +02:00

Menu: refactor menu

GitLab: #1388

Change-Id: Ia168dce60ffdafa1ab4d08905c46f47f98625916
This commit is contained in:
lcoursodon 2023-10-18 13:01:22 -04:00 committed by Adrien Béraud
parent f144b27db8
commit 493addcbd8
17 changed files with 567 additions and 539 deletions

View file

@ -37,6 +37,7 @@ ContextMenuAutoLoader {
canTrigger: lineEditObj.selectedText.length canTrigger: lineEditObj.selectedText.length
itemName: JamiStrings.copy itemName: JamiStrings.copy
hasIcon: false
onClicked: { onClicked: {
lineEditObj.copy(); lineEditObj.copy();
} }
@ -46,7 +47,7 @@ ContextMenuAutoLoader {
canTrigger: lineEditObj.selectedText.length && !selectOnly canTrigger: lineEditObj.selectedText.length && !selectOnly
itemName: JamiStrings.cut itemName: JamiStrings.cut
hasIcon: false
onClicked: { onClicked: {
lineEditObj.cut(); lineEditObj.cut();
} }
@ -56,6 +57,7 @@ ContextMenuAutoLoader {
canTrigger: !selectOnly canTrigger: !selectOnly
itemName: JamiStrings.paste itemName: JamiStrings.paste
hasIcon: false
onClicked: { onClicked: {
if (customizePaste) if (customizePaste)
root.contextMenuRequirePaste(); root.contextMenuRequirePaste();
@ -76,10 +78,6 @@ ContextMenuAutoLoader {
lineEditObj.select(selectionStart, selectionEnd); lineEditObj.select(selectionStart, selectionEnd);
} }
contextMenuItemPreferredHeight: JamiTheme.lineEditContextMenuItemsHeight
contextMenuItemPreferredWidth: JamiTheme.lineEditContextMenuItemsWidth
contextMenuSeparatorPreferredHeight: JamiTheme.lineEditContextMenuSeparatorsHeight
Connections { Connections {
target: root.item target: root.item
enabled: root.status === Loader.Ready enabled: root.status === Loader.Ready

View file

@ -1,309 +0,0 @@
/*
* Copyright (C) 2022-2023 Savoir-faire Linux Inc.
* Author: Nicolas Vengeon <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 QtQuick.Controls
import Qt5Compat.GraphicalEffects
import QtQuick.Layouts
import net.jami.Models 1.1
import net.jami.Adapters 1.1
import net.jami.Constants 1.1
Popup {
id: root
width: emojiColumn.width + JamiTheme.emojiMargins
height: emojiColumn.height + JamiTheme.emojiMargins
padding: 0
background.visible: false
required property var emojiReactions
property var emojiReplied: emojiReactions.ownEmojis
required property string msgId
required property string msgBody
required property bool isOutgoing
required property int type
required property string transferName
required property Item msgBubble
required property ListView listView
property string transferId: msgId
property string location: msgBody
property bool closeWithoutAnimation: false
property var emojiPicker
focus: true
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
function xPositionProvider(width) {
// Use the width at function scope to retrigger property evaluation.
const listViewWidth = listView.width
if (isOutgoing) {
const leftMargin = msgBubble.mapToItem(listView, 0, 0).x
return width > leftMargin ? -leftMargin : -width
} else {
const rightMargin = listViewWidth - (msgBubble.x + msgBubble.width)
return width > rightMargin ? msgBubble.width - width : msgBubble.width
}
}
function yPositionProvider(height) {
const topOffset = msgBubble.mapToItem(listView, 0, 0).y
if (topOffset < 0) return -topOffset
const bottomOffset = topOffset + height - listView.height
if (bottomOffset > 0) return -bottomOffset
return 0
}
x: xPositionProvider(width)
y: yPositionProvider(height)
signal addMoreEmoji
onAddMoreEmoji: {
JamiQmlUtils.updateMessageBarButtonsPoints()
openEmojiPicker()
}
function openEmojiPicker() {
var component = WITH_WEBENGINE ?
Qt.createComponent("qrc:/webengine/emojipicker/EmojiPicker.qml") :
Qt.createComponent("qrc:/nowebengine/EmojiPicker.qml")
emojiPicker = component.createObject(root.parent, { listView: listView })
emojiPicker.emojiIsPicked.connect(function(content) {
if (emojiReplied.includes(content)) {
MessagesAdapter.removeEmojiReaction(CurrentConversation.id, content, msgId)
} else {
MessagesAdapter.addEmojiReaction(CurrentConversation.id, content, msgId)
}
})
if (emojiPicker !== null) {
root.opacity = 0
emojiPicker.closed.connect(() => close())
emojiPicker.x = xPositionProvider(JamiTheme.emojiPickerWidth)
emojiPicker.y = yPositionProvider(JamiTheme.emojiPickerHeight)
emojiPicker.open()
} else {
console.log("Error creating emojiPicker from message options popup");
}
}
// Close the picker when listView vertical properties change.
property real listViewHeight: listView.height
onListViewHeightChanged: close()
property bool isScrolling: listView.verticalScrollBar.active
onIsScrollingChanged: close()
onOpened: root.closeWithoutAnimation = false
onClosed: if (emojiPicker) emojiPicker.closeEmojiPicker()
function getModel() {
const defaultModel = ["👍", "👎", "😂"]
const reactedEmojis = Array.isArray(emojiReplied) ? emojiReplied.slice(0, defaultModel.length) : []
const uniqueEmojis = Array.from(new Set(reactedEmojis))
const missingEmojis = defaultModel.filter(emoji => !uniqueEmojis.includes(emoji))
return uniqueEmojis.concat(missingEmojis)
}
Rectangle {
id: bubble
color: JamiTheme.chatviewBgColor
anchors.fill: parent
radius: JamiTheme.modalPopupRadius
ColumnLayout {
id: emojiColumn
anchors.centerIn: parent
RowLayout {
id: emojiRow
Layout.alignment: Qt.AlignCenter
Repeater {
model: root.getModel()
delegate: Button {
id: emojiButton
height: 50
width: 50
text: modelData
font.pointSize: JamiTheme.emojiBubbleSize
Text {
visible: emojiButton.hovered
anchors.centerIn: parent
text: modelData
font.pointSize: JamiTheme.emojiBubbleSizeBig
z: 1
}
background: Rectangle {
anchors.fill: parent
opacity: emojiReplied ? (emojiReplied.includes(modelData) ? 1 : 0) : 0
color: JamiTheme.emojiReactPushButtonColor
radius: 10
}
onClicked: {
if (emojiReplied.includes(modelData))
MessagesAdapter.removeEmojiReaction(CurrentConversation.id,text,msgId)
else
MessagesAdapter.addEmojiReaction(CurrentConversation.id,text,msgId)
close()
}
}
}
PushButton {
toolTipText: JamiStrings.moreEmojis
source: JamiResources.add_reaction_svg
normalColor: JamiTheme.emojiReactBubbleBgColor
imageColor: JamiTheme.emojiReactPushButtonColor
visible: WITH_WEBENGINE
onClicked: {
root.closeWithoutAnimation = true
root.addMoreEmoji()
//close()
}
}
}
Rectangle {
Layout.margins: 5
color: JamiTheme.timestampColor
Layout.fillWidth: true
Layout.preferredHeight: 1
radius: width * 0.5
opacity: 0.6
}
MessageOptionButton {
textButton: JamiStrings.copy
iconSource: JamiResources.copy_svg
Layout.fillWidth: true
Layout.margins: 5
onClicked: {
UtilsAdapter.setClipboardText(msgBody)
close()
}
}
MessageOptionButton {
visible: type === Interaction.Type.DATA_TRANSFER
textButton: JamiStrings.saveFile
iconSource: JamiResources.save_file_svg
Layout.fillWidth: true
Layout.margins: 5
onClicked: {
MessagesAdapter.copyToDownloads(root.transferId, root.transferName)
close()
}
}
MessageOptionButton {
visible: type === Interaction.Type.DATA_TRANSFER
textButton: JamiStrings.openLocation
iconSource: JamiResources.round_folder_24dp_svg
Layout.fillWidth: true
Layout.margins: 5
onClicked: {
MessagesAdapter.openDirectory(root.location)
close()
}
}
MessageOptionButton {
visible: type === Interaction.Type.DATA_TRANSFER && Status === Interaction.Status.TRANSFER_FINISHED
textButton: JamiStrings.removeLocally
iconSource: JamiResources.trash_black_24dp_svg
Layout.fillWidth: true
Layout.margins: 5
onClicked: {
MessagesAdapter.removeFile(msgId, root.location)
close()
}
}
MessageOptionButton {
id: buttonEdit
visible: root.isOutgoing && type === Interaction.Type.TEXT
textButton: JamiStrings.editMessage
iconSource: JamiResources.edit_svg
Layout.fillWidth: true
Layout.margins: 5
onClicked: {
MessagesAdapter.replyToId = ""
MessagesAdapter.editId = root.msgId
close()
}
}
MessageOptionButton {
visible: root.isOutgoing && type === Interaction.Type.TEXT
textButton: JamiStrings.deleteMessage
iconSource: JamiResources.delete_svg
Layout.fillWidth: true
Layout.margins: 5
onClicked: {
MessagesAdapter.editMessage(CurrentConversation.id, "", root.msgId)
close()
}
}
}
}
Overlay.modal: Rectangle {
color: JamiTheme.transparentColor
// Color animation for overlay when pop up is shown.
ColorAnimation on color {
to: JamiTheme.popupOverlayColor
duration: 500
}
}
DropShadow {
z: -1
width: bubble.width
height: bubble.height
horizontalOffset: 3.0
verticalOffset: 3.0
radius: bubble.radius * 4
color: JamiTheme.shadowColor
source: bubble
transparentBorder: true
samples: radius + 1
}
enter: Transition {
NumberAnimation {
properties: "opacity"; from: 0.0; to: 1.0
duration: JamiTheme.shortFadeDuration
}
}
exit: Transition {
NumberAnimation {
properties: "opacity"; from: 1.0; to: 0.0
duration: root.closeWithoutAnimation ? 0 : JamiTheme.shortFadeDuration
}
}
}

View file

@ -53,6 +53,7 @@ AbstractButton {
property alias toolTipText: toolTip.text property alias toolTipText: toolTip.text
property alias hasShortcut: toolTip.hasShortcut property alias hasShortcut: toolTip.hasShortcut
property alias shortcutKey: toolTip.shortcutKey property alias shortcutKey: toolTip.shortcutKey
property int buttonTextFontSize: 12
// State colors // State colors
property string pressedColor: JamiTheme.pressedButtonColor property string pressedColor: JamiTheme.pressedButtonColor
@ -143,7 +144,7 @@ AbstractButton {
color: JamiTheme.primaryForegroundColor color: JamiTheme.primaryForegroundColor
font.kerning: true font.kerning: true
font.pixelSize: 12 font.pixelSize: buttonTextFontSize
elide: Qt.ElideRight elide: Qt.ElideRight
} }

View file

@ -37,6 +37,7 @@ ContextMenuAutoLoader {
canTrigger: root.transferId !== "" canTrigger: root.transferId !== ""
itemName: JamiStrings.saveFile itemName: JamiStrings.saveFile
iconSource: JamiResources.save_file_svg
onClicked: MessagesAdapter.copyToDownloads(root.transferId, root.transferName) onClicked: MessagesAdapter.copyToDownloads(root.transferId, root.transferName)
}, },
GeneralMenuItem { GeneralMenuItem {
@ -44,6 +45,7 @@ ContextMenuAutoLoader {
canTrigger: root.transferId !== "" canTrigger: root.transferId !== ""
itemName: JamiStrings.openLocation itemName: JamiStrings.openLocation
iconSource: JamiResources.round_folder_24dp_svg
onClicked: { onClicked: {
MessagesAdapter.openDirectory(root.location); MessagesAdapter.openDirectory(root.location);
} }
@ -52,6 +54,7 @@ ContextMenuAutoLoader {
id: reply id: reply
itemName: JamiStrings.reply itemName: JamiStrings.reply
iconSource: JamiResources.reply_svg
onClicked: { onClicked: {
MessagesAdapter.editId = ""; MessagesAdapter.editId = "";
MessagesAdapter.replyToId = root.msgId; MessagesAdapter.replyToId = root.msgId;
@ -62,6 +65,7 @@ ContextMenuAutoLoader {
canTrigger: transferId === "" && isOutgoing canTrigger: transferId === "" && isOutgoing
itemName: JamiStrings.edit itemName: JamiStrings.edit
iconSource: JamiResources.edit_svg
onClicked: { onClicked: {
MessagesAdapter.replyToId = ""; MessagesAdapter.replyToId = "";
MessagesAdapter.editId = root.msgId; MessagesAdapter.editId = root.msgId;
@ -73,6 +77,7 @@ ContextMenuAutoLoader {
canTrigger: transferId === "" && isOutgoing canTrigger: transferId === "" && isOutgoing
itemName: JamiStrings.optionDelete itemName: JamiStrings.optionDelete
iconSource: JamiResources.delete_svg
onClicked: { onClicked: {
MessagesAdapter.editMessage(CurrentConversation.id, "", root.msgId); MessagesAdapter.editMessage(CurrentConversation.id, "", root.msgId);
} }

View file

@ -277,7 +277,7 @@ Control {
anchors.rightMargin: isOutgoing ? 10 : 0 anchors.rightMargin: isOutgoing ? 10 : 0
anchors.leftMargin: !isOutgoing ? 10 : 0 anchors.leftMargin: !isOutgoing ? 10 : 0
imageColor: JamiTheme.emojiReactPushButtonColor imageColor: hovered ? JamiTheme.chatViewFooterImgHoverColor : JamiTheme.chatViewFooterImgColor
normalColor: JamiTheme.primaryBackgroundColor normalColor: JamiTheme.primaryBackgroundColor
toolTipText: JamiStrings.moreOptions toolTipText: JamiStrings.moreOptions
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
@ -290,7 +290,7 @@ Control {
circled: false circled: false
onClicked: { onClicked: {
var component = Qt.createComponent("qrc:/commoncomponents/MessageOptionsPopup.qml"); var component = Qt.createComponent("qrc:/commoncomponents/ShowMoreMenu.qml");
var obj = component.createObject(bubble, { var obj = component.createObject(bubble, {
"emojiReactions": emojiReactions, "emojiReactions": emojiReactions,
"isOutgoing": isOutgoing, "isOutgoing": isOutgoing,
@ -308,7 +308,8 @@ Control {
PushButton { PushButton {
id: reply id: reply
imageColor: JamiTheme.emojiReactPushButtonColor circled: false
imageColor: hovered ? JamiTheme.chatViewFooterImgHoverColor : JamiTheme.chatViewFooterImgColor
normalColor: JamiTheme.primaryBackgroundColor normalColor: JamiTheme.primaryBackgroundColor
toolTipText: JamiStrings.reply toolTipText: JamiStrings.reply
source: JamiResources.reply_svg source: JamiResources.reply_svg
@ -319,7 +320,6 @@ Control {
anchors.right: isOutgoing ? more.left : undefined anchors.right: isOutgoing ? more.left : undefined
anchors.left: !isOutgoing ? more.right : undefined anchors.left: !isOutgoing ? more.right : undefined
visible: CurrentAccount.type !== Profile.Type.SIP && Body !== "" && (bubbleArea.bubbleHovered || hovered || more.hovered || bgHandler.hovered) visible: CurrentAccount.type !== Profile.Type.SIP && Body !== "" && (bubbleArea.bubbleHovered || hovered || more.hovered || bgHandler.hovered)
circled: false
onClicked: { onClicked: {
MessagesAdapter.editId = ""; MessagesAdapter.editId = "";

View file

@ -0,0 +1,65 @@
/*
* Copyright (C) 2020-2023 Savoir-faire Linux Inc.
*
* 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
import "contextmenu"
BaseContextMenu {
id: root
property var modelList
signal audioRecordMessageButtonClicked
signal videoRecordMessageButtonClicked
signal showMapClicked
property list<GeneralMenuItem> menuItems: [
GeneralMenuItem {
id: audioMessage
canTrigger: true
iconSource: JamiResources.message_audio_black_24dp_svg
itemName: JamiStrings.leaveAudioMessage
onClicked: {
root.audioRecordMessageButtonClicked();
}
},
GeneralMenuItem {
id: videoMessage
canTrigger: true
iconSource: JamiResources.message_video_black_24dp_svg
itemName: JamiStrings.leaveVideoMessage
onClicked: {
root.videoRecordMessageButtonClicked();
}
},
GeneralMenuItem {
id: shareLocation
canTrigger: true
iconSource: JamiResources.localisation_sharing_send_pin_svg
itemName: JamiStrings.shareLocation
onClicked: {
root.showMapClicked();
}
}
]
Component.onCompleted: {
root.loadMenuItems(menuItems);
}
}

View file

@ -1,149 +0,0 @@
/*
* Copyright (C) 2023 Savoir-faire Linux Inc.
*
* 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 QtQuick.Layouts
import QtQuick.Controls
import Qt.labs.platform
import Qt5Compat.GraphicalEffects
import net.jami.Models 1.1
import net.jami.Adapters 1.1
import net.jami.Constants 1.1
import "../mainview/components"
Popup {
id: root
padding: 0
property list<Action> menuMoreButton
height: childrenRect.height
width: childrenRect.width
focus: true
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
Rectangle {
id: rect
color: JamiTheme.primaryBackgroundColor
border.color: JamiTheme.chatViewFooterRectangleBorderColor
border.width: 2
radius: 5
height: listViewMoreButton.childrenRect.height + 16
width: listViewMoreButton.childrenRect.width + 16
ListView {
id: listViewMoreButton
anchors.centerIn: parent
orientation: ListView.Vertical
spacing: 0
width: contentItem.childrenRect.width
height: contentHeight
model: menuMoreButton
Rectangle {
z: -1
anchors.fill: parent
color: "transparent"
}
onCountChanged: {
for (var i = 0; i < count; i++) {
var item = listViewMoreButton.itemAtIndex(i);
item.width = listViewMoreButton.width;
}
}
delegate: ItemDelegate {
id: control
text: modelData.toolTip
contentItem: RowLayout {
Rectangle {
id: image
width: 20
height: 20
radius: 5
color: JamiTheme.transparentColor
ResponsiveImage {
anchors.fill: parent
source: modelData.iconSrc
color: control.hovered ? JamiTheme.chatViewFooterImgHoverColor : JamiTheme.chatViewFooterImgColor
}
}
Text {
Layout.alignment: Qt.AlignLeft
text: control.text
color: JamiTheme.chatViewFooterImgHoverColor
font.pixelSize: JamiTheme.menuFontSize
}
}
background: Rectangle {
color: control.hovered ? JamiTheme.showMoreButtonOpenColor : JamiTheme.transparentColor
}
action: modelData
onClicked: {
root.close();
}
}
}
}
DropShadow {
z: -1
width: rect.width
height: rect.height
horizontalOffset: 3.0
verticalOffset: 3.0
radius: rect.radius * 4
color: JamiTheme.shadowColor
source: rect
transparentBorder: true
samples: radius + 1
}
background: Rectangle {
anchors.fill: parent
color: JamiTheme.transparentColor
radius: 5
z: -1
}
enter: Transition {
NumberAnimation {
properties: "opacity"
from: 0.0
to: 1.0
duration: JamiTheme.shortFadeDuration
}
}
exit: Transition {
NumberAnimation {
properties: "opacity"
from: 1.0
to: 0.0
duration: JamiTheme.shortFadeDuration
}
}
}

View file

@ -0,0 +1,191 @@
/*
* Copyright (C) 2020-2023 Savoir-faire Linux Inc.
*
* 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
import Qt5Compat.GraphicalEffects
import QtQuick.Controls
import net.jami.Models 1.1
import net.jami.Adapters 1.1
import "contextmenu"
BaseContextMenu {
id: root
required property var emojiReactions
property var emojiReplied: emojiReactions.ownEmojis
required property string msgId
required property string msgBody
required property bool isOutgoing
required property int type
required property string transferName
required property Item msgBubble
required property ListView listView
property string location: msgBody
property bool closeWithoutAnimation: false
property var emojiPicker
function xPositionProvider(width) {
// Use the width at function scope to retrigger property evaluation.
const listViewWidth = listView.width;
if (isOutgoing) {
const leftMargin = msgBubble.mapToItem(listView, 0, 0).x;
return width > leftMargin ? -leftMargin : -width;
} else {
const rightMargin = listViewWidth - (msgBubble.x + msgBubble.width);
return width > rightMargin ? msgBubble.width - width : msgBubble.width;
}
}
function yPositionProvider(height) {
const topOffset = msgBubble.mapToItem(listView, 0, 0).y;
if (topOffset < 0)
return -topOffset;
const bottomOffset = topOffset + height - listView.height;
if (bottomOffset > 0)
return -bottomOffset;
return 0;
}
x: xPositionProvider(width)
y: yPositionProvider(height)
signal addMoreEmoji
onAddMoreEmoji: {
JamiQmlUtils.updateMessageBarButtonsPoints();
openEmojiPicker();
}
function openEmojiPicker() {
var component = WITH_WEBENGINE ? Qt.createComponent("qrc:/webengine/emojipicker/EmojiPicker.qml") : Qt.createComponent("qrc:/nowebengine/EmojiPicker.qml");
emojiPicker = component.createObject(root.parent, {
"listView": listView
});
emojiPicker.emojiIsPicked.connect(function (content) {
if (emojiReplied.includes(content)) {
MessagesAdapter.removeEmojiReaction(CurrentConversation.id, content, msgId);
} else {
MessagesAdapter.addEmojiReaction(CurrentConversation.id, content, msgId);
}
});
if (emojiPicker !== null) {
root.opacity = 0;
emojiPicker.closed.connect(() => close());
emojiPicker.x = xPositionProvider(JamiTheme.emojiPickerWidth);
emojiPicker.y = yPositionProvider(JamiTheme.emojiPickerHeight);
emojiPicker.open();
} else {
console.log("Error creating emojiPicker from message options popup");
}
}
// Close the picker when listView vertical properties change.
property real listViewHeight: listView.height
onListViewHeightChanged: close()
property bool isScrolling: listView.verticalScrollBar.active
onIsScrollingChanged: close()
onOpened: root.closeWithoutAnimation = false
onClosed: if (emojiPicker)
emojiPicker.closeEmojiPicker()
function getModel() {
const defaultModel = ["👍", "👎", "😂"];
const reactedEmojis = Array.isArray(emojiReplied) ? emojiReplied.slice(0, defaultModel.length) : [];
const uniqueEmojis = Array.from(new Set(reactedEmojis));
const missingEmojis = defaultModel.filter(emoji => !uniqueEmojis.includes(emoji));
return uniqueEmojis.concat(missingEmojis);
}
property list<MenuItem> menuItems: [
GeneralMenuItemList {
id: audioMessage
modelList: getModel()
canTrigger: true
iconSource: JamiResources.add_reaction_svg
itemName: JamiStrings.copy
addMenuSeparatorAfter: true
messageId: msgId
},
GeneralMenuItem {
id: copyMessage
canTrigger: true
iconSource: JamiResources.copy_svg
itemName: JamiStrings.copy
onClicked: {
UtilsAdapter.setClipboardText(msgBody);
}
},
GeneralMenuItem {
id: saveFile
canTrigger: type === Interaction.Type.DATA_TRANSFER
iconSource: JamiResources.save_file_svg
itemName: JamiStrings.saveFile
onClicked: {
MessagesAdapter.copyToDownloads(root.msgId, root.transferName);
}
},
GeneralMenuItem {
id: openLocation
canTrigger: type === Interaction.Type.DATA_TRANSFER
iconSource: JamiResources.round_folder_24dp_svg
itemName: JamiStrings.openLocation
onClicked: {
MessagesAdapter.openDirectory(root.location);
}
},
GeneralMenuItem {
id: removeLocally
canTrigger: type === Interaction.Type.DATA_TRANSFER && Status === Interaction.Status.TRANSFER_FINISHED
iconSource: JamiResources.trash_black_24dp_svg
itemName: JamiStrings.removeLocally
onClicked: {
MessagesAdapter.removeFile(msgId, root.location);
;
}
},
GeneralMenuItem {
id: editMessage
canTrigger: root.isOutgoing && type === Interaction.Type.TEXT
iconSource: JamiResources.edit_svg
itemName: JamiStrings.editMessage
onClicked: {
MessagesAdapter.replyToId = "";
MessagesAdapter.editId = root.msgId;
}
},
GeneralMenuItem {
id: deleteMessage
canTrigger: root.isOutgoing && type === Interaction.Type.TEXT
iconSource: JamiResources.delete_svg
itemName: JamiStrings.deleteMessage
onClicked: {
MessagesAdapter.editMessage(CurrentConversation.id, "", root.msgId);
}
}
]
Component.onCompleted: {
root.loadMenuItems(menuItems);
}
}

View file

@ -43,11 +43,9 @@ Menu {
function loadMenuItems(menuItems) { function loadMenuItems(menuItems) {
root.addItem(menuTopBorder); root.addItem(menuTopBorder);
// use the maximum text width as the preferred width for menu
for (var j = 0; j < menuItems.length; ++j) { for (var j = 0; j < menuItems.length; ++j) {
var currentItemWidth = menuItems[j].itemPreferredWidth; var currentItemWidth = menuItems[j].itemPreferredWidth;
if (currentItemWidth !== JamiTheme.menuItemsPreferredWidth && currentItemWidth > menuPreferredWidth) if (currentItemWidth !== JamiTheme.menuItemsPreferredWidth && currentItemWidth > menuPreferredWidth && menuItems[j].canTrigger)
menuPreferredWidth = currentItemWidth; menuPreferredWidth = currentItemWidth;
} }
for (var i = 0; i < menuItems.length; ++i) { for (var i = 0; i < menuItems.length; ++i) {
@ -55,17 +53,28 @@ Menu {
menuItems[i].parentMenu = root; menuItems[i].parentMenu = root;
root.addItem(menuItems[i]); root.addItem(menuItems[i]);
if (menuPreferredWidth) if (menuPreferredWidth)
menuItems[i].itemPreferredWidth = menuPreferredWidth; menuItems[i].itemRealWidth = menuPreferredWidth;
if (menuItemsPreferredHeight) if (menuItemsPreferredHeight)
menuItems[i].itemPreferredHeight = menuItemsPreferredHeight; menuItems[i].itemPreferredHeight = menuItemsPreferredHeight;
} if (i !== menuItems.length - 1) {
if (menuItems[i].addMenuSeparatorAfter) { var menuSeparatorComponent = Qt.createComponent("GeneralMenuSeparator.qml", Component.PreferSynchronous, root);
// If the QML file to be loaded is a local file, var menuSeparatorComponentObj = menuSeparatorComponent.createObject();
// you could omit the finishCreation() function generalMenuSeparatorList.push(menuSeparatorComponentObj);
var menuSeparatorComponent = Qt.createComponent("GeneralMenuSeparator.qml", Component.PreferSynchronous, root); root.addItem(menuSeparatorComponentObj);
var menuSeparatorComponentObj = menuSeparatorComponent.createObject(); }
generalMenuSeparatorList.push(menuSeparatorComponentObj); if (menuItems[i].addMenuSeparatorAfter) {
root.addItem(menuSeparatorComponentObj); var menuSeparatorComponent = Qt.createComponent("GeneralMenuSeparator.qml", Component.PreferSynchronous, root);
var menuSeparatorComponentObj = menuSeparatorComponent.createObject(root, {
"separatorColor": "#DEDEDE",
"separatorPreferredHeight": 0
});
generalMenuSeparatorList.push(menuSeparatorComponentObj);
root.addItem(menuSeparatorComponentObj);
var menuSeparatorComponent = Qt.createComponent("GeneralMenuSeparator.qml", Component.PreferSynchronous, root);
var menuSeparatorComponentObj = menuSeparatorComponent.createObject();
generalMenuSeparatorList.push(menuSeparatorComponentObj);
root.addItem(menuSeparatorComponentObj);
}
} }
} }
root.addItem(menuBottomBorder); root.addItem(menuBottomBorder);
@ -81,24 +90,23 @@ Menu {
Overlay.modal: Rectangle { Overlay.modal: Rectangle {
color: "transparent" color: "transparent"
} }
font.pointSize: JamiTheme.menuFontSize font.pointSize: JamiTheme.menuFontSize
background: Rectangle { background: Rectangle {
id: container
implicitWidth: menuPreferredWidth ? menuPreferredWidth : JamiTheme.menuItemsPreferredWidth implicitWidth: menuPreferredWidth ? menuPreferredWidth : JamiTheme.menuItemsPreferredWidth
border.width: JamiTheme.menuItemsCommonBorderWidth color: JamiTheme.primaryBackgroundColor
border.color: JamiTheme.tabbarBorderColor radius: 5
color: JamiTheme.backgroundColor
layer.enabled: true layer.enabled: true
layer.effect: DropShadow { layer.effect: DropShadow {
z: -1 z: -1
horizontalOffset: 3.0 horizontalOffset: 0.0
verticalOffset: 3.0 verticalOffset: 3.0
radius: 16.0 radius: 6
color: JamiTheme.shadowColor color: "#29000000"
transparentBorder: true transparentBorder: true
samples: radius + 1 samples: radius + 1
} }

View file

@ -30,6 +30,8 @@ Loader {
active: false active: false
visible: false
function openMenu() { function openMenu() {
root.active = true; root.active = true;
root.sourceComponent = menuComponent; root.sourceComponent = menuComponent;

View file

@ -1,6 +1,5 @@
/* /*
* Copyright (C) 2020-2023 Savoir-faire Linux Inc. * Copyright (C) 2020-2023 Savoir-faire Linux Inc.
* Author: Mingrui Zhang <mingrui.zhang@savoirfairelinux.com>
* *
* This program is free software; you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by
@ -32,22 +31,26 @@ MenuItem {
property alias iconSource: contextMenuItemImage.source property alias iconSource: contextMenuItemImage.source
property string iconColor: "" property string iconColor: ""
property bool canTrigger: true property bool canTrigger: true
property bool hasIcon: true
property bool addMenuSeparatorAfter: false property bool addMenuSeparatorAfter: false
property bool autoTextSizeAdjustment: true property bool autoTextSizeAdjustment: true
property bool dangerous: false property bool dangerous: false
property BaseContextMenu parentMenu property BaseContextMenu parentMenu
property int itemPreferredWidth: JamiTheme.menuItemsPreferredWidth property int itemPreferredWidth: hasIcon ? 50 + contextMenuItemText.contentWidth + contextMenuItemImage.width : 35 + contextMenuItemText.contentWidth
property int itemRealWidth: itemPreferredWidth
property int itemPreferredHeight: JamiTheme.menuItemsPreferredHeight property int itemPreferredHeight: JamiTheme.menuItemsPreferredHeight
property int leftBorderWidth: JamiTheme.menuItemsCommonBorderWidth property int leftBorderWidth: JamiTheme.menuItemsCommonBorderWidth
property int rightBorderWidth: JamiTheme.menuItemsCommonBorderWidth property int rightBorderWidth: JamiTheme.menuItemsCommonBorderWidth
property int itemImageLeftMargin: 24 property int itemImageLeftMargin: 18
property int itemTextMargin: 20 property int itemTextMargin: 10
signal clicked signal clicked
property bool itemHovered: menuItemContentRect.hovered property bool itemHovered: menuItemContentRect.hovered
width: itemRealWidth
contentItem: AbstractButton { contentItem: AbstractButton {
id: menuItemContentRect id: menuItemContentRect
@ -55,10 +58,12 @@ MenuItem {
id: background id: background
anchors.fill: parent anchors.fill: parent
anchors.leftMargin: 1 anchors.leftMargin: 6
anchors.rightMargin: 1 anchors.rightMargin: 6
color: menuItemContentRect.hovered ? JamiTheme.hoverColor : JamiTheme.backgroundColor radius: 5
color: menuItemContentRect.hovered ? JamiTheme.hoverColor : JamiTheme.primaryBackgroundColor
} }
anchors.fill: parent anchors.fill: parent
@ -76,41 +81,25 @@ MenuItem {
visible: status === Image.Ready visible: status === Image.Ready
color: iconColor !== "" ? iconColor : JamiTheme.textColor color: menuItemContentRect.hovered ? JamiTheme.textColor : JamiTheme.chatViewFooterImgColor
opacity: 0.7
} }
Text { Item {
id: contextMenuItemText
Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft
Layout.leftMargin: contextMenuItemImage.status === Image.Ready ? itemTextMargin : itemTextMargin / 2 Layout.leftMargin: contextMenuItemImage.status === Image.Ready ? itemTextMargin : itemTextMargin
Layout.rightMargin: contextMenuItemImage.status === Image.Ready ? itemTextMargin : itemTextMargin / 2 Layout.rightMargin: contextMenuItemImage.status === Image.Ready ? itemImageLeftMargin : itemTextMargin
Layout.preferredHeight: itemPreferredHeight Layout.preferredHeight: itemPreferredHeight
Layout.fillWidth: true Layout.fillWidth: true
text: itemName Text {
color: dangerous ? JamiTheme.redColor : JamiTheme.textColor id: contextMenuItemText
font.pointSize: JamiTheme.textFontSize height: parent.height
horizontalAlignment: Text.AlignLeft text: itemName
verticalAlignment: Text.AlignVCenter color: dangerous ? JamiTheme.redColor : JamiTheme.textColor
font.pointSize: JamiTheme.textFontSize
TextMetrics { horizontalAlignment: Text.AlignLeft
id: contextMenuItemTextMetrics verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight
font: contextMenuItemText.font
text: contextMenuItemText.text
onBoundingRectChanged: {
var sizeToCompare = itemPreferredWidth - (contextMenuItemImage.source.toString().length > 0 ? itemTextMargin + itemImageLeftMargin + contextMenuItemImage.width : itemTextMargin / 2);
if (autoTextSizeAdjustment && boundingRect.width > sizeToCompare) {
if (boundingRect.width > JamiTheme.contextMenuItemTextMaxWidth) {
itemPreferredWidth += JamiTheme.contextMenuItemTextMaxWidth - JamiTheme.contextMenuItemTextPreferredWidth + itemTextMargin;
contextMenuItemText.elide = Text.ElideRight;
} else
itemPreferredWidth += boundingRect.width + itemTextMargin - sizeToCompare;
}
}
} }
} }
} }
@ -130,7 +119,9 @@ MenuItem {
anchors.leftMargin: leftBorderWidth anchors.leftMargin: leftBorderWidth
anchors.rightMargin: rightBorderWidth anchors.rightMargin: rightBorderWidth
implicitWidth: itemPreferredWidth color: JamiTheme.primaryBackgroundColor
implicitWidth: itemRealWidth
implicitHeight: itemPreferredHeight implicitHeight: itemPreferredHeight
border.width: 0 border.width: 0
@ -141,7 +132,7 @@ MenuItem {
rBorderwidth: rightBorderWidth rBorderwidth: rightBorderWidth
tBorderwidth: 0 tBorderwidth: 0
bBorderwidth: 0 bBorderwidth: 0
borderColor: JamiTheme.tabbarBorderColor borderColor: JamiTheme.primaryBackgroundColor
} }
} }
} }

View file

@ -0,0 +1,216 @@
/*
* Copyright (C) 2020-2023 Savoir-faire Linux Inc.
* Author: Mingrui Zhang <mingrui.zhang@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 QtQuick.Controls
import QtQuick.Layouts
import net.jami.Constants 1.1
import "../"
import net.jami.Adapters 1.1
// General menu item.
// Can control top, bottom, left, right border width.
// Use onClicked slot to simulate item click event.
// Can have image icon at the left of the text.
MenuItem {
id: menuItem
property var modelList: undefined
property string itemName: ""
property var iconSource: undefined
property string iconColor: ""
property bool canTrigger: true
property bool hasIcon: true
property bool addMenuSeparatorAfter: false
property bool autoTextSizeAdjustment: true
property bool dangerous: false
property BaseContextMenu parentMenu
property string messageId
signal addMoreEmoji
property int itemPreferredWidth: 207
property int itemRealWidth: itemPreferredWidth
property int itemPreferredHeight: JamiTheme.menuItemsPreferredHeight
property int leftBorderWidth: JamiTheme.menuItemsCommonBorderWidth
property int rightBorderWidth: JamiTheme.menuItemsCommonBorderWidth
property int itemImageLeftMargin: 18
signal clicked
width: itemRealWidth
contentItem: Item {
id: menuItemContentRect
anchors.fill: parent
RowLayout {
spacing: 0
anchors.fill: menuItemContentRect
Rectangle {
id: contextMenuItemImage
Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft
Layout.leftMargin: itemImageLeftMargin
height: 36
width: 36
color: emojiReplied.includes(modelList[0]) ? JamiTheme.hoveredButtonColor : JamiTheme.primaryBackgroundColor
radius: 5
Text {
anchors.centerIn: parent
text: modelList[0]
font.pointSize: JamiTheme.emojiBubbleSize
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
onEntered: {
contextMenuItemImage.color = JamiTheme.hoveredButtonColor;
}
onExited: {
contextMenuItemImage.color = emojiReplied.includes(modelList[0]) ? JamiTheme.hoveredButtonColor : JamiTheme.primaryBackgroundColor;
}
onClicked: {
if (emojiReplied.includes(modelList[0])) {
MessagesAdapter.removeEmojiReaction(CurrentConversation.id, modelList[0], msgId);
} else {
MessagesAdapter.addEmojiReaction(CurrentConversation.id, modelList[0], msgId);
}
close();
}
}
}
Rectangle {
id: contextMenuItemImage2
Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft
Layout.leftMargin: itemImageLeftMargin / 2
height: 36
width: 36
color: emojiReplied.includes(modelList[1]) ? JamiTheme.hoveredButtonColor : JamiTheme.primaryBackgroundColor
radius: 5
Text {
anchors.centerIn: parent
text: modelList[1]
font.pointSize: JamiTheme.emojiBubbleSize
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
onEntered: {
contextMenuItemImage2.color = JamiTheme.hoveredButtonColor;
}
onExited: {
contextMenuItemImage2.color = emojiReplied.includes(modelList[1]) ? JamiTheme.hoveredButtonColor : JamiTheme.primaryBackgroundColor;
}
onClicked: {
if (emojiReplied.includes(modelList[1])) {
MessagesAdapter.removeEmojiReaction(CurrentConversation.id, modelList[1], msgId);
} else {
MessagesAdapter.addEmojiReaction(CurrentConversation.id, modelList[1], msgId);
}
close();
}
}
}
Rectangle {
id: contextMenuItemImage3
Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft
Layout.leftMargin: itemImageLeftMargin / 2
height: 36
width: 36
color: emojiReplied.includes(modelList[2]) ? JamiTheme.hoveredButtonColor : JamiTheme.primaryBackgroundColor
radius: 5
Text {
anchors.centerIn: parent
text: modelList[2]
font.pointSize: JamiTheme.emojiBubbleSize
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
onEntered: {
contextMenuItemImage3.color = JamiTheme.hoveredButtonColor;
}
onExited: {
contextMenuItemImage3.color = emojiReplied.includes(modelList[2]) ? JamiTheme.hoveredButtonColor : JamiTheme.primaryBackgroundColor;
}
onClicked: {
if (emojiReplied.includes(modelList[2])) {
MessagesAdapter.removeEmojiReaction(CurrentConversation.id, modelList[2], msgId);
} else {
MessagesAdapter.addEmojiReaction(CurrentConversation.id, modelList[2], msgId);
}
close();
}
}
}
PushButton {
id: contextMenuItemImage4
Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft
Layout.leftMargin: itemImageLeftMargin / 2
Layout.rightMargin: itemImageLeftMargin
height: 36
width: 36
imageColor: hovered ? JamiTheme.chatViewFooterImgHoverColor : JamiTheme.chatViewFooterImgColor
normalColor: JamiTheme.primaryBackgroundColor
radius: 5
source: iconSource
onClicked: {
root.addMoreEmoji();
}
}
}
}
highlighted: true
background: Rectangle {
id: contextMenuBackgroundRect
anchors.fill: parent
anchors.leftMargin: leftBorderWidth
anchors.rightMargin: rightBorderWidth
color: JamiTheme.primaryBackgroundColor
implicitWidth: itemRealWidth
implicitHeight: itemPreferredHeight
border.width: 0
CustomBorder {
commonBorder: false
lBorderwidth: leftBorderWidth
rBorderwidth: rightBorderWidth
tBorderwidth: 0
bBorderwidth: 0
borderColor: JamiTheme.primaryBackgroundColor
}
}
}

View file

@ -23,19 +23,24 @@ MenuSeparator {
id: menuSeparator id: menuSeparator
property int separatorPreferredWidth: JamiTheme.menuItemsPreferredWidth property int separatorPreferredWidth: JamiTheme.menuItemsPreferredWidth
property int separatorPreferredHeight: 1 property int separatorPreferredHeight: 5
property string separatorColor: JamiTheme.tabbarBorderColor property string separatorColor: JamiTheme.primaryBackgroundColor
padding: 0 padding: 0
topPadding: 1 topPadding: 1
bottomPadding: 1 bottomPadding: 1
contentItem: Rectangle { contentItem: Rectangle {
implicitWidth: separatorPreferredWidth implicitWidth: separatorPreferredWidth
implicitHeight: separatorPreferredHeight implicitHeight: separatorPreferredHeight
color: separatorColor color: separatorColor
radius: 5
} }
background: Rectangle { background: Rectangle {
color: JamiTheme.backgroundColor width: parent.width - 10
anchors.horizontalCenter: parent.horizontalCenter
color: separatorColor
radius: 5
} }
} }

View file

@ -367,9 +367,9 @@ Item {
property int avatarReadReceiptSize: 18 property int avatarReadReceiptSize: 18
property int menuItemsPreferredWidth: 220 property int menuItemsPreferredWidth: 220
property int menuItemsPreferredHeight: 48 property int menuItemsPreferredHeight: 36
property int menuItemsCommonBorderWidth: 1 property int menuItemsCommonBorderWidth: 1
property int menuBorderPreferredHeight: 8 property int menuBorderPreferredHeight: 5
property real maximumWidthSettingsView: 516 property real maximumWidthSettingsView: 516
property real settingsHeaderpreferredHeight: 64 property real settingsHeaderpreferredHeight: 64

View file

@ -104,7 +104,6 @@ ContextMenuAutoLoader {
canTrigger: hasCall canTrigger: hasCall
itemName: JamiStrings.endCall itemName: JamiStrings.endCall
iconSource: JamiResources.ic_call_end_white_24dp_svg iconSource: JamiResources.ic_call_end_white_24dp_svg
addMenuSeparatorAfter: contactType !== Profile.Type.SIP && (contactType === Profile.Type.PENDING || !hasCall)
onClicked: CallAdapter.hangUpACall(responsibleAccountId, responsibleConvUid) onClicked: CallAdapter.hangUpACall(responsibleAccountId, responsibleConvUid)
}, },
GeneralMenuItem { GeneralMenuItem {
@ -129,7 +128,6 @@ ContextMenuAutoLoader {
canTrigger: !hasCall && contactType !== Profile.Type.SIP && !root.isBanned && isCoreDialog && root.idText !== CurrentAccount.uri canTrigger: !hasCall && contactType !== Profile.Type.SIP && !root.isBanned && isCoreDialog && root.idText !== CurrentAccount.uri
itemName: JamiStrings.blockContact itemName: JamiStrings.blockContact
iconSource: JamiResources.block_black_24dp_svg iconSource: JamiResources.block_black_24dp_svg
addMenuSeparatorAfter: canTrigger
onClicked: { onClicked: {
var dlg = viewCoordinator.presentDialog(appWindow, "commoncomponents/ConfirmDialog.qml", { var dlg = viewCoordinator.presentDialog(appWindow, "commoncomponents/ConfirmDialog.qml", {
"title": JamiStrings.confirmAction, "title": JamiStrings.confirmAction,
@ -147,7 +145,6 @@ ContextMenuAutoLoader {
canTrigger: root.isBanned canTrigger: root.isBanned
itemName: JamiStrings.reinstateContact itemName: JamiStrings.reinstateContact
iconSource: JamiResources.round_remove_circle_24dp_svg iconSource: JamiResources.round_remove_circle_24dp_svg
addMenuSeparatorAfter: canTrigger
onClicked: MessagesAdapter.unbanConversation(responsibleConvUid) onClicked: MessagesAdapter.unbanConversation(responsibleConvUid)
}, },
GeneralMenuItem { GeneralMenuItem {

View file

@ -108,14 +108,21 @@ RowLayout {
sharePopup.close(); sharePopup.close();
} }
popup: SharePopup { popup: ShareMenu {
id: sharePopup id: sharePopup
onAudioRecordMessageButtonClicked: {
root.audioRecordMessageButtonClicked();
}
onVideoRecordMessageButtonClicked: {
root.videoRecordMessageButtonClicked();
}
onShowMapClicked: {
root.showMapClicked();
}
parent: root
modelList: listViewMoreButton.menuMoreButton
y: -160 y: -160
x: -20 x: -20
menuMoreButton: listViewMoreButton.menuMoreButton
onClosed: messageBar.textAreaObj.forceActiveFocus()
} }
} }
} }

View file

@ -51,7 +51,7 @@ Item {
} }
function getOptionsPopup(isOutgoing, id, body, type, transferName) { function getOptionsPopup(isOutgoing, id, body, type, transferName) {
var component = Qt.createComponent("qrc:/commoncomponents/MessageOptionsPopup.qml"); var component = Qt.createComponent("qrc:/commoncomponents/ShowMoreMenu.qml");
var obj = component.createObject(bubble, { var obj = component.createObject(bubble, {
"emojiReactions": emojiReactions, "emojiReactions": emojiReactions,
"isOutgoing": isOutgoing, "isOutgoing": isOutgoing,