feat(chat): add image support
This commit adds support for rendering images by detecting image URLs inside of a message and attaching them to their corresponding message bubble.
This commit is contained in:
parent
69ba3c4468
commit
7d178b355e
|
@ -25,6 +25,7 @@ type
|
||||||
ResponseTo = UserRole + 14
|
ResponseTo = UserRole + 14
|
||||||
PlainText = UserRole + 15
|
PlainText = UserRole + 15
|
||||||
Index = UserRole + 16
|
Index = UserRole + 16
|
||||||
|
ImageUrls = UserRole + 17
|
||||||
|
|
||||||
QtObject:
|
QtObject:
|
||||||
type
|
type
|
||||||
|
@ -82,6 +83,7 @@ QtObject:
|
||||||
of ChatMessageRoles.OutgoingStatus: result = newQVariant(message.outgoingStatus)
|
of ChatMessageRoles.OutgoingStatus: result = newQVariant(message.outgoingStatus)
|
||||||
of ChatMessageRoles.ResponseTo: result = newQVariant(message.responseTo)
|
of ChatMessageRoles.ResponseTo: result = newQVariant(message.responseTo)
|
||||||
of ChatMessageRoles.Index: result = newQVariant(index.row)
|
of ChatMessageRoles.Index: result = newQVariant(index.row)
|
||||||
|
of ChatMessageRoles.ImageUrls: result = newQVariant(message.imageUrls)
|
||||||
|
|
||||||
method roleNames(self: ChatMessageList): Table[int, string] =
|
method roleNames(self: ChatMessageList): Table[int, string] =
|
||||||
{
|
{
|
||||||
|
@ -100,7 +102,8 @@ QtObject:
|
||||||
ChatMessageRoles.Id.int: "messageId",
|
ChatMessageRoles.Id.int: "messageId",
|
||||||
ChatMessageRoles.OutgoingStatus.int: "outgoingStatus",
|
ChatMessageRoles.OutgoingStatus.int: "outgoingStatus",
|
||||||
ChatMessageRoles.ResponseTo.int: "responseTo",
|
ChatMessageRoles.ResponseTo.int: "responseTo",
|
||||||
ChatMessageRoles.Index.int: "index"
|
ChatMessageRoles.Index.int: "index",
|
||||||
|
ChatMessageRoles.ImageUrls.int: "imageUrls"
|
||||||
}.toTable
|
}.toTable
|
||||||
|
|
||||||
proc getMessageIndex(self: ChatMessageList, messageId: string): int {.slot.} =
|
proc getMessageIndex(self: ChatMessageList, messageId: string): int {.slot.} =
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import json, random, sequtils, sugar
|
import json, random, re, strutils, sequtils, sugar
|
||||||
import json_serialization
|
import json_serialization
|
||||||
import ../status/libstatus/accounts as status_accounts
|
import ../status/libstatus/accounts as status_accounts
|
||||||
import ../status/libstatus/settings as status_settings
|
import ../status/libstatus/settings as status_settings
|
||||||
|
@ -140,7 +140,12 @@ proc toTextItem*(jsonText: JsonNode): TextItem =
|
||||||
|
|
||||||
|
|
||||||
proc toMessage*(jsonMsg: JsonNode): Message =
|
proc toMessage*(jsonMsg: JsonNode): Message =
|
||||||
result = Message(
|
let
|
||||||
|
regex = re(r"(\b(https?|ftp):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|].(?:jpg|jpeg|gif|png|svg))", flags = {reStudy, reIgnoreCase})
|
||||||
|
text = jsonMsg{"text"}.getStr
|
||||||
|
imageUrls = findAll(text, regex)
|
||||||
|
|
||||||
|
var message = Message(
|
||||||
alias: jsonMsg{"alias"}.getStr,
|
alias: jsonMsg{"alias"}.getStr,
|
||||||
chatId: jsonMsg{"localChatId"}.getStr,
|
chatId: jsonMsg{"localChatId"}.getStr,
|
||||||
clock: jsonMsg{"clock"}.getInt,
|
clock: jsonMsg{"clock"}.getInt,
|
||||||
|
@ -162,9 +167,15 @@ proc toMessage*(jsonMsg: JsonNode): Message =
|
||||||
outgoingStatus: $jsonMsg{"outgoingStatus"}.getStr,
|
outgoingStatus: $jsonMsg{"outgoingStatus"}.getStr,
|
||||||
isCurrentUser: $jsonMsg{"outgoingStatus"}.getStr == "sending" or $jsonMsg{"outgoingStatus"}.getStr == "sent",
|
isCurrentUser: $jsonMsg{"outgoingStatus"}.getStr == "sending" or $jsonMsg{"outgoingStatus"}.getStr == "sent",
|
||||||
stickerHash: "",
|
stickerHash: "",
|
||||||
parsedText: @[]
|
parsedText: @[],
|
||||||
|
imageUrls: ""
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if imageUrls.len > 0:
|
||||||
|
message.imageUrls = imageUrls.join(" ")
|
||||||
|
|
||||||
|
result = message
|
||||||
|
|
||||||
if jsonMsg["parsedText"].kind != JNull:
|
if jsonMsg["parsedText"].kind != JNull:
|
||||||
for text in jsonMsg["parsedText"]:
|
for text in jsonMsg["parsedText"]:
|
||||||
result.parsedText.add(text.toTextItem)
|
result.parsedText.add(text.toTextItem)
|
||||||
|
|
|
@ -42,6 +42,7 @@ type Message* = object
|
||||||
isCurrentUser*: bool
|
isCurrentUser*: bool
|
||||||
stickerHash*: string
|
stickerHash*: string
|
||||||
outgoingStatus*: string
|
outgoingStatus*: string
|
||||||
|
imageUrls*: string
|
||||||
|
|
||||||
proc `$`*(self: Message): string =
|
proc `$`*(self: Message): string =
|
||||||
result = fmt"Message(id:{self.id}, chatId:{self.chatId}, clock:{self.clock}, from:{self.fromAuthor}, type:{self.contentType})"
|
result = fmt"Message(id:{self.id}, chatId:{self.chatId}, clock:{self.clock}, from:{self.fromAuthor}, type:{self.contentType})"
|
||||||
|
|
|
@ -7,8 +7,10 @@ import "./components"
|
||||||
import "./ChatColumn"
|
import "./ChatColumn"
|
||||||
|
|
||||||
StackLayout {
|
StackLayout {
|
||||||
|
id: chatColumnLayout
|
||||||
property int chatGroupsListViewCount: 0
|
property int chatGroupsListViewCount: 0
|
||||||
property bool isReply: false
|
property bool isReply: false
|
||||||
|
property var appSettings
|
||||||
Layout.fillHeight: true
|
Layout.fillHeight: true
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.minimumWidth: 300
|
Layout.minimumWidth: 300
|
||||||
|
@ -36,6 +38,7 @@ StackLayout {
|
||||||
ChatMessages {
|
ChatMessages {
|
||||||
id: chatMessages
|
id: chatMessages
|
||||||
messageList: chatsModel.messageList
|
messageList: chatsModel.messageList
|
||||||
|
appSettings: chatColumnLayout.appSettings
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -11,6 +11,7 @@ ScrollView {
|
||||||
id: scrollView
|
id: scrollView
|
||||||
|
|
||||||
property var messageList: MessagesData {}
|
property var messageList: MessagesData {}
|
||||||
|
property var appSettings
|
||||||
property bool loadingMessages: false
|
property bool loadingMessages: false
|
||||||
property real scrollY: chatLogView.visibleArea.yPosition * chatLogView.contentHeight
|
property real scrollY: chatLogView.visibleArea.yPosition * chatLogView.contentHeight
|
||||||
|
|
||||||
|
@ -21,11 +22,23 @@ ScrollView {
|
||||||
ScrollBar.vertical.policy: chatLogView.contentHeight > chatLogView.height ? ScrollBar.AlwaysOn : ScrollBar.AlwaysOff
|
ScrollBar.vertical.policy: chatLogView.contentHeight > chatLogView.height ? ScrollBar.AlwaysOn : ScrollBar.AlwaysOff
|
||||||
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
|
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
|
||||||
|
|
||||||
|
function scrollToBottom(force, caller) {
|
||||||
|
if (!true && !chatLogView.atYEnd) {
|
||||||
|
// User has scrolled up, we don't want to scroll back
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (caller && caller !== chatLogView.itemAtIndex(chatLogView.count - 1)) {
|
||||||
|
// If we have a caller, only accept its request if it's the last message
|
||||||
|
return
|
||||||
|
}
|
||||||
|
Qt.callLater( chatLogView.positionViewAtEnd )
|
||||||
|
}
|
||||||
|
|
||||||
ListView {
|
ListView {
|
||||||
|
id: chatLogView
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
spacing: 4
|
spacing: 4
|
||||||
boundsBehavior: Flickable.StopAtBounds
|
boundsBehavior: Flickable.StopAtBounds
|
||||||
id: chatLogView
|
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.fillHeight: true
|
Layout.fillHeight: true
|
||||||
|
|
||||||
|
@ -36,17 +49,11 @@ ScrollView {
|
||||||
}
|
}
|
||||||
|
|
||||||
onActiveChannelChanged: {
|
onActiveChannelChanged: {
|
||||||
Qt.callLater( chatLogView.positionViewAtEnd )
|
scrollToBottom(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
onMessagePushed: {
|
onMessagePushed: {
|
||||||
if (!chatLogView.atYEnd) {
|
scrollToBottom()
|
||||||
// User has scrolled up, we don't want to scroll back
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if(chatLogView.atYEnd)
|
|
||||||
Qt.callLater( chatLogView.positionViewAtEnd )
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onMessageNotificationPushed: function(chatId, msg) {
|
onMessageNotificationPushed: function(chatId, msg) {
|
||||||
|
@ -141,6 +148,8 @@ ScrollView {
|
||||||
}
|
}
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
appSettings: scrollView.appSettings
|
||||||
|
scrollToBottom: scrollView.scrollToBottom
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -37,12 +37,15 @@ Item {
|
||||||
property string repliedMessageContent: replyMessageIndex > -1 ? chatsModel.messageList.getMessageData(replyMessageIndex, "message") : "";
|
property string repliedMessageContent: replyMessageIndex > -1 ? chatsModel.messageList.getMessageData(replyMessageIndex, "message") : "";
|
||||||
|
|
||||||
property var profileClick: function () {}
|
property var profileClick: function () {}
|
||||||
|
property var scrollToBottom: function () {}
|
||||||
|
property var appSettings
|
||||||
width: parent.width
|
width: parent.width
|
||||||
|
anchors.right: !isCurrentUser ? undefined : parent.right
|
||||||
|
id: messageWrapper
|
||||||
height: {
|
height: {
|
||||||
switch(contentType){
|
switch(contentType){
|
||||||
case Constants.chatIdentifier:
|
case Constants.chatIdentifier:
|
||||||
return parent.parent.height - 100
|
return channelIdentifier.height + channelIdentifier.verticalMargin
|
||||||
case Constants.stickerType:
|
case Constants.stickerType:
|
||||||
return stickerId.height + 50 + (dateGroupLbl.visible ? 50 : 0)
|
return stickerId.height + 50 + (dateGroupLbl.visible ? 50 : 0)
|
||||||
default:
|
default:
|
||||||
|
@ -68,10 +71,13 @@ Item {
|
||||||
}
|
}
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
|
property int verticalMargin: 50
|
||||||
id: channelIdentifier
|
id: channelIdentifier
|
||||||
visible: authorCurrentMsg == ""
|
visible: authorCurrentMsg == ""
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.top: parent.top
|
||||||
|
anchors.topMargin: this.visible ? verticalMargin : 0
|
||||||
|
height: this.visible ? childrenRect.height + verticalMargin : 0
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: circleId
|
id: circleId
|
||||||
|
@ -171,7 +177,6 @@ Item {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Private group Messages
|
// Private group Messages
|
||||||
|
@ -363,7 +368,7 @@ Item {
|
||||||
|
|
||||||
StyledTextEdit {
|
StyledTextEdit {
|
||||||
id: chatText
|
id: chatText
|
||||||
textFormat: TextEdit.RichText
|
textFormat: Text.RichText
|
||||||
text: {
|
text: {
|
||||||
if(contentType === Constants.stickerType) return "";
|
if(contentType === Constants.stickerType) return "";
|
||||||
let msg = linkify(message);
|
let msg = linkify(message);
|
||||||
|
@ -468,10 +473,10 @@ Item {
|
||||||
let hours = messageDate.getHours();
|
let hours = messageDate.getHours();
|
||||||
return (hours < 10 ? "0" + hours : hours) + ":" + (minutes < 10 ? "0" + minutes : minutes)
|
return (hours < 10 ? "0" + hours : hours) + ":" + (minutes < 10 ? "0" + minutes : minutes)
|
||||||
}
|
}
|
||||||
anchors.top: chatBox.bottom
|
anchors.top: messageWrapper.appSettings.displayChatImages && imageUrls != "" ? imageChatBox.bottom : chatBox.bottom
|
||||||
anchors.topMargin: 4
|
anchors.topMargin: 4
|
||||||
anchors.bottomMargin: Style.current.padding
|
anchors.bottomMargin: Style.current.padding
|
||||||
anchors.right: chatBox.right
|
anchors.right: messageWrapper.appSettings.displayChatImages && imageUrls != "" ? imageChatBox.right : chatBox.right
|
||||||
anchors.rightMargin: isCurrentUser ? 5 : Style.current.padding
|
anchors.rightMargin: isCurrentUser ? 5 : Style.current.padding
|
||||||
font.pixelSize: 10
|
font.pixelSize: 10
|
||||||
readOnly: true
|
readOnly: true
|
||||||
|
@ -479,7 +484,6 @@ Item {
|
||||||
visible: (isEmoji || isMessage || isSticker)
|
visible: (isEmoji || isMessage || isSticker)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
StyledTextEdit {
|
StyledTextEdit {
|
||||||
id: sentMessage
|
id: sentMessage
|
||||||
color: Style.current.darkGrey
|
color: Style.current.darkGrey
|
||||||
|
@ -497,23 +501,75 @@ Item {
|
||||||
visible: isCurrentUser && (isEmoji || isMessage || isSticker)
|
visible: isCurrentUser && (isEmoji || isMessage || isSticker)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
property int chatVerticalPadding: 12
|
||||||
|
property int chatHorizontalPadding: 12
|
||||||
|
property int imageWidth: 350
|
||||||
|
|
||||||
|
id: imageChatBox
|
||||||
|
visible: messageWrapper.appSettings.displayChatImages && imageUrls != ""
|
||||||
|
height: {
|
||||||
|
if (!imageChatBox.visible) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
let h = chatVerticalPadding
|
||||||
|
for (let i = 0; i < imageRepeater.count; i++) {
|
||||||
|
h += imageRepeater.itemAt(i).height
|
||||||
|
}
|
||||||
|
return h + chatVerticalPadding * imageRepeater.count
|
||||||
|
}
|
||||||
|
color: isCurrentUser ? Style.current.blue : Style.current.lightBlue
|
||||||
|
border.color: "transparent"
|
||||||
|
width: imageWidth+ 2 * chatHorizontalPadding
|
||||||
|
radius: 16
|
||||||
|
anchors.left: !isCurrentUser ? chatImage.right : undefined
|
||||||
|
anchors.leftMargin: !isCurrentUser ? 8 : 0
|
||||||
|
anchors.right: !isCurrentUser ? undefined : parent.right
|
||||||
|
anchors.rightMargin: !isCurrentUser ? 0 : Style.current.padding
|
||||||
|
anchors.top: messageWrapper.appSettings.displayChatImages && imageUrls != "" ? chatBox.bottom : chatTime.bottom
|
||||||
|
anchors.topMargin: Style.current.smallPadding
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
id: imageRepeater
|
||||||
|
model: messageWrapper.appSettings.displayChatImages && imageUrls != "" ? imageUrls.split(" ") : []
|
||||||
|
|
||||||
|
Image {
|
||||||
|
id: imageMessage
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
anchors.top: (index == 0) ? parent.top: parent.children[index-1].bottom
|
||||||
|
anchors.topMargin: imageChatBox.chatVerticalPadding
|
||||||
|
sourceSize.width: imageChatBox.imageWidth
|
||||||
|
source: modelData
|
||||||
|
onStatusChanged: {
|
||||||
|
if (imageMessage.status == Image.Error) {
|
||||||
|
imageMessage.height = 0
|
||||||
|
imageMessage.visible = false
|
||||||
|
imageChatBox.height = 0
|
||||||
|
imageChatBox.visible = false
|
||||||
|
} else if (imageMessage.status == Image.Ready) {
|
||||||
|
messageWrapper.scrollToBottom(true, messageWrapper)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// This rectangle's only job is to mask the corner to make it less rounded... yep
|
// This rectangle's only job is to mask the corner to make it less rounded... yep
|
||||||
Rectangle {
|
Rectangle {
|
||||||
// TODO find a way to show the corner for stickers since they have a border
|
color: parent.color
|
||||||
visible: isMessage || isEmoji
|
|
||||||
color: chatBox.color
|
|
||||||
width: 18
|
width: 18
|
||||||
height: 18
|
height: 18
|
||||||
anchors.bottom: chatBox.bottom
|
anchors.bottom: parent.bottom
|
||||||
anchors.bottomMargin: 0
|
anchors.bottomMargin: 0
|
||||||
anchors.left: !isCurrentUser ? chatBox.left : undefined
|
anchors.left: !isCurrentUser ? parent.left : undefined
|
||||||
anchors.leftMargin: 0
|
anchors.leftMargin: 0
|
||||||
anchors.right: !isCurrentUser ? undefined : chatBox.right
|
anchors.right: !isCurrentUser ? undefined : parent.right
|
||||||
anchors.rightMargin: 0
|
anchors.rightMargin: 0
|
||||||
radius: 4
|
radius: 4
|
||||||
z: -1
|
z: -1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/*##^##
|
/*##^##
|
||||||
Designer {
|
Designer {
|
||||||
|
|
|
@ -24,6 +24,7 @@ SplitView {
|
||||||
ChatColumn {
|
ChatColumn {
|
||||||
id: chatColumn
|
id: chatColumn
|
||||||
chatGroupsListViewCount: contactsColumn.chatGroupsListViewCount
|
chatGroupsListViewCount: contactsColumn.chatGroupsListViewCount
|
||||||
|
appSettings: chatView.appSettings
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -50,7 +50,9 @@ SplitView {
|
||||||
|
|
||||||
NotificationsContainer {}
|
NotificationsContainer {}
|
||||||
|
|
||||||
AdvancedContainer {}
|
AdvancedContainer {
|
||||||
|
appSettings: profileView.appSettings
|
||||||
|
}
|
||||||
|
|
||||||
HelpContainer {}
|
HelpContainer {}
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,7 @@ import "../../../../imports"
|
||||||
import "../../../../shared"
|
import "../../../../shared"
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
|
property var appSettings
|
||||||
id: advancedContainer
|
id: advancedContainer
|
||||||
width: 200
|
width: 200
|
||||||
height: 200
|
height: 200
|
||||||
|
@ -96,6 +97,7 @@ Item {
|
||||||
}
|
}
|
||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
|
id: nodeTabSettings
|
||||||
anchors.top: browserTabSettings.bottom
|
anchors.top: browserTabSettings.bottom
|
||||||
anchors.topMargin: 20
|
anchors.topMargin: 20
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
|
@ -115,6 +117,25 @@ Item {
|
||||||
text: qsTrId("under-development")
|
text: qsTrId("under-development")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
anchors.top: nodeTabSettings.bottom
|
||||||
|
anchors.topMargin: 20
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: 24
|
||||||
|
StyledText {
|
||||||
|
text: qsTr("Display images in chat automatically")
|
||||||
|
}
|
||||||
|
Switch {
|
||||||
|
checked: appSettings.displayChatImages
|
||||||
|
onCheckedChanged: function(value) {
|
||||||
|
advancedContainer.appSettings.displayChatImages = this.checked
|
||||||
|
}
|
||||||
|
}
|
||||||
|
StyledText {
|
||||||
|
text: qsTr("under development")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*##^##
|
/*##^##
|
||||||
|
|
|
@ -61,6 +61,7 @@ ApplicationWindow {
|
||||||
property var chatSplitView
|
property var chatSplitView
|
||||||
property var walletSplitView
|
property var walletSplitView
|
||||||
property var profileSplitView
|
property var profileSplitView
|
||||||
|
property bool displayChatImages: false
|
||||||
}
|
}
|
||||||
|
|
||||||
SystemTrayIcon {
|
SystemTrayIcon {
|
||||||
|
|
Loading…
Reference in New Issue