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:
Jonathan Rainville 2020-07-10 11:37:23 -04:00 committed by Iuri Matias
parent 69ba3c4468
commit 7d178b355e
10 changed files with 148 additions and 40 deletions

View File

@ -25,6 +25,7 @@ type
ResponseTo = UserRole + 14
PlainText = UserRole + 15
Index = UserRole + 16
ImageUrls = UserRole + 17
QtObject:
type
@ -82,6 +83,7 @@ QtObject:
of ChatMessageRoles.OutgoingStatus: result = newQVariant(message.outgoingStatus)
of ChatMessageRoles.ResponseTo: result = newQVariant(message.responseTo)
of ChatMessageRoles.Index: result = newQVariant(index.row)
of ChatMessageRoles.ImageUrls: result = newQVariant(message.imageUrls)
method roleNames(self: ChatMessageList): Table[int, string] =
{
@ -100,7 +102,8 @@ QtObject:
ChatMessageRoles.Id.int: "messageId",
ChatMessageRoles.OutgoingStatus.int: "outgoingStatus",
ChatMessageRoles.ResponseTo.int: "responseTo",
ChatMessageRoles.Index.int: "index"
ChatMessageRoles.Index.int: "index",
ChatMessageRoles.ImageUrls.int: "imageUrls"
}.toTable
proc getMessageIndex(self: ChatMessageList, messageId: string): int {.slot.} =

View File

@ -1,4 +1,4 @@
import json, random, sequtils, sugar
import json, random, re, strutils, sequtils, sugar
import json_serialization
import ../status/libstatus/accounts as status_accounts
import ../status/libstatus/settings as status_settings
@ -140,7 +140,12 @@ proc toTextItem*(jsonText: JsonNode): TextItem =
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,
chatId: jsonMsg{"localChatId"}.getStr,
clock: jsonMsg{"clock"}.getInt,
@ -162,8 +167,14 @@ proc toMessage*(jsonMsg: JsonNode): Message =
outgoingStatus: $jsonMsg{"outgoingStatus"}.getStr,
isCurrentUser: $jsonMsg{"outgoingStatus"}.getStr == "sending" or $jsonMsg{"outgoingStatus"}.getStr == "sent",
stickerHash: "",
parsedText: @[]
parsedText: @[],
imageUrls: ""
)
if imageUrls.len > 0:
message.imageUrls = imageUrls.join(" ")
result = message
if jsonMsg["parsedText"].kind != JNull:
for text in jsonMsg["parsedText"]:

View File

@ -42,6 +42,7 @@ type Message* = object
isCurrentUser*: bool
stickerHash*: string
outgoingStatus*: string
imageUrls*: string
proc `$`*(self: Message): string =
result = fmt"Message(id:{self.id}, chatId:{self.chatId}, clock:{self.clock}, from:{self.fromAuthor}, type:{self.contentType})"

View File

@ -7,8 +7,10 @@ import "./components"
import "./ChatColumn"
StackLayout {
id: chatColumnLayout
property int chatGroupsListViewCount: 0
property bool isReply: false
property var appSettings
Layout.fillHeight: true
Layout.fillWidth: true
Layout.minimumWidth: 300
@ -36,6 +38,7 @@ StackLayout {
ChatMessages {
id: chatMessages
messageList: chatsModel.messageList
appSettings: chatColumnLayout.appSettings
}
}

View File

@ -11,6 +11,7 @@ ScrollView {
id: scrollView
property var messageList: MessagesData {}
property var appSettings
property bool loadingMessages: false
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.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 {
id: chatLogView
anchors.fill: parent
spacing: 4
boundsBehavior: Flickable.StopAtBounds
id: chatLogView
Layout.fillWidth: true
Layout.fillHeight: true
@ -36,17 +49,11 @@ ScrollView {
}
onActiveChannelChanged: {
Qt.callLater( chatLogView.positionViewAtEnd )
scrollToBottom(true)
}
onMessagePushed: {
if (!chatLogView.atYEnd) {
// User has scrolled up, we don't want to scroll back
return
}
if(chatLogView.atYEnd)
Qt.callLater( chatLogView.positionViewAtEnd )
scrollToBottom()
}
onMessageNotificationPushed: function(chatId, msg) {
@ -141,6 +148,8 @@ ScrollView {
}
return -1;
}
appSettings: scrollView.appSettings
scrollToBottom: scrollView.scrollToBottom
}
}

View File

@ -28,7 +28,7 @@ Item {
property string authorPrevMsg: "authorPrevMsg"
property bool isEmoji: contentType === Constants.emojiType
property bool isMessage: contentType === Constants.messageType || contentType === Constants.stickerType
property bool isMessage: contentType === Constants.messageType || contentType === Constants.stickerType
property bool isStatusMessage: contentType === Constants.systemMessagePrivateGroupType
property bool isSticker: contentType === Constants.stickerType
@ -37,12 +37,15 @@ Item {
property string repliedMessageContent: replyMessageIndex > -1 ? chatsModel.messageList.getMessageData(replyMessageIndex, "message") : "";
property var profileClick: function () {}
property var scrollToBottom: function () {}
property var appSettings
width: parent.width
anchors.right: !isCurrentUser ? undefined : parent.right
id: messageWrapper
height: {
switch(contentType){
case Constants.chatIdentifier:
return parent.parent.height - 100
return channelIdentifier.height + channelIdentifier.verticalMargin
case Constants.stickerType:
return stickerId.height + 50 + (dateGroupLbl.visible ? 50 : 0)
default:
@ -51,11 +54,11 @@ Item {
}
function linkify(inputText) {
//URLs starting with http://, https://, or ftp://
// URLs starting with http://, https://, or ftp://
var replacePattern1 = /(\b(https?|ftp):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/gim;
var replacedText = inputText.replace(replacePattern1, "<a href='$1'>$1</a>");
//URLs starting with "www." (without // before it, or it'd re-link the ones done above).
// URLs starting with "www." (without // before it, or it'd re-link the ones done above).
var replacePattern2 = /(^|[^\/])(www\.[\S]+(\b|$))/gim;
replacedText = replacedText.replace(replacePattern2, "$1<a href='http://$2'>$2</a>");
@ -68,10 +71,13 @@ Item {
}
Item {
property int verticalMargin: 50
id: channelIdentifier
visible: authorCurrentMsg == ""
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 {
id: circleId
@ -171,7 +177,6 @@ Item {
}
}
}
}
// Private group Messages
@ -363,7 +368,7 @@ Item {
StyledTextEdit {
id: chatText
textFormat: TextEdit.RichText
textFormat: Text.RichText
text: {
if(contentType === Constants.stickerType) return "";
let msg = linkify(message);
@ -468,17 +473,16 @@ Item {
let hours = messageDate.getHours();
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.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
font.pixelSize: 10
readOnly: true
selectByMouse: true
visible: (isEmoji || isMessage || isSticker)
}
StyledTextEdit {
id: sentMessage
@ -496,22 +500,74 @@ Item {
readOnly: true
visible: isCurrentUser && (isEmoji || isMessage || isSticker)
}
// This rectangle's only job is to mask the corner to make it less rounded... yep
Rectangle {
// TODO find a way to show the corner for stickers since they have a border
visible: isMessage || isEmoji
color: chatBox.color
width: 18
height: 18
anchors.bottom: chatBox.bottom
anchors.bottomMargin: 0
anchors.left: !isCurrentUser ? chatBox.left : undefined
anchors.leftMargin: 0
anchors.right: !isCurrentUser ? undefined : chatBox.right
anchors.rightMargin: 0
radius: 4
z: -1
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
Rectangle {
color: parent.color
width: 18
height: 18
anchors.bottom: parent.bottom
anchors.bottomMargin: 0
anchors.left: !isCurrentUser ? parent.left : undefined
anchors.leftMargin: 0
anchors.right: !isCurrentUser ? undefined : parent.right
anchors.rightMargin: 0
radius: 4
z: -1
}
}
}

View File

@ -24,6 +24,7 @@ SplitView {
ChatColumn {
id: chatColumn
chatGroupsListViewCount: contactsColumn.chatGroupsListViewCount
appSettings: chatView.appSettings
}
}

View File

@ -50,7 +50,9 @@ SplitView {
NotificationsContainer {}
AdvancedContainer {}
AdvancedContainer {
appSettings: profileView.appSettings
}
HelpContainer {}

View File

@ -5,6 +5,7 @@ import "../../../../imports"
import "../../../../shared"
Item {
property var appSettings
id: advancedContainer
width: 200
height: 200
@ -96,6 +97,7 @@ Item {
}
RowLayout {
id: nodeTabSettings
anchors.top: browserTabSettings.bottom
anchors.topMargin: 20
anchors.left: parent.left
@ -115,6 +117,25 @@ Item {
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")
}
}
}
/*##^##

View File

@ -61,6 +61,7 @@ ApplicationWindow {
property var chatSplitView
property var walletSplitView
property var profileSplitView
property bool displayChatImages: false
}
SystemTrayIcon {