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
|
||||
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.} =
|
||||
|
|
|
@ -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,9 +167,15 @@ 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"]:
|
||||
result.parsedText.add(text.toTextItem)
|
||||
|
|
|
@ -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})"
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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,10 +473,10 @@ 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
|
||||
|
@ -479,7 +484,6 @@ Item {
|
|||
visible: (isEmoji || isMessage || isSticker)
|
||||
}
|
||||
|
||||
|
||||
StyledTextEdit {
|
||||
id: sentMessage
|
||||
color: Style.current.darkGrey
|
||||
|
@ -497,21 +501,73 @@ Item {
|
|||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -24,6 +24,7 @@ SplitView {
|
|||
ChatColumn {
|
||||
id: chatColumn
|
||||
chatGroupsListViewCount: contactsColumn.chatGroupsListViewCount
|
||||
appSettings: chatView.appSettings
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -50,7 +50,9 @@ SplitView {
|
|||
|
||||
NotificationsContainer {}
|
||||
|
||||
AdvancedContainer {}
|
||||
AdvancedContainer {
|
||||
appSettings: profileView.appSettings
|
||||
}
|
||||
|
||||
HelpContainer {}
|
||||
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*##^##
|
||||
|
|
|
@ -61,6 +61,7 @@ ApplicationWindow {
|
|||
property var chatSplitView
|
||||
property var walletSplitView
|
||||
property var profileSplitView
|
||||
property bool displayChatImages: false
|
||||
}
|
||||
|
||||
SystemTrayIcon {
|
||||
|
|
Loading…
Reference in New Issue