Switch to new URL unfurl API [sending messages part] (#11476)

This commit is contained in:
Igor Sirotin 2023-07-11 14:30:55 +03:00 committed by GitHub
parent 3a0954c5da
commit d7aa0582be
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 542 additions and 33 deletions

View File

@ -1,11 +1,14 @@
import io_interface
import io_interface, chronicles, tables
import ../../../../../../app_service/service/message/service as message_service
import ../../../../../../app_service/service/community/service as community_service
import ../../../../../../app_service/service/chat/service as chat_service
import ../../../../../../app_service/service/gif/service as gif_service
import ../../../../../../app_service/service/gif/dto
import ../../../../../../app_service/service/message/dto/link_preview
import ../../../../../core/eventemitter
import ../../../../../core/unique_event_emitter
import ./link_preview_cache
type
Controller* = ref object of RootObj
@ -17,6 +20,8 @@ type
communityService: community_service.Service
chatService: chat_service.Service
gifService: gif_service.Service
messageService: message_service.Service
linkPreviewCache: LinkPreviewCache
proc newController*(
delegate: io_interface.AccessInterface,
@ -26,7 +31,8 @@ proc newController*(
belongsToCommunity: bool,
chatService: chat_service.Service,
communityService: community_service.Service,
gifService: gif_service.Service
gifService: gif_service.Service,
messageService: message_service.Service
): Controller =
result = Controller()
result.delegate = delegate
@ -37,6 +43,10 @@ proc newController*(
result.chatService = chatService
result.communityService = communityService
result.gifService = gifService
result.messageService = messageService
result.linkPreviewCache = newLinkPreiewCache()
proc onUrlsUnfurled(self: Controller, args: LinkPreviewV2DataArgs)
proc delete*(self: Controller) =
self.events.disconnect()
@ -70,6 +80,10 @@ proc init*(self: Controller) =
self.events.on(SIGNAL_SEARCH_GIFS_ERROR) do(e:Args):
self.delegate.searchGifsError()
self.events.on(SIGNAL_URLS_UNFURLED) do(e:Args):
let args = LinkPreviewV2DataArgs(e)
self.onUrlsUnfurled(args)
proc getChatId*(self: Controller): string =
return self.chatId
@ -86,7 +100,16 @@ proc sendChatMessage*(
contentType: int,
preferredUsername: string = "") =
self.chatService.sendChatMessage(self.chatId, msg, replyTo, contentType, preferredUsername)
let urls = self.messageService.getTextUrls(msg)
let linkPreviews = self.linkPreviewCache.linkPreviewsSeq(urls)
self.chatService.sendChatMessage(self.chatId,
msg,
replyTo,
contentType,
preferredUsername,
linkPreviews
)
proc requestAddressForTransaction*(self: Controller, fromAddress: string, amount: string, tokenAddress: string) =
self.chatService.requestAddressForTransaction(self.chatId, fromAddress, amount, tokenAddress)
@ -132,3 +155,20 @@ proc addToRecentsGif*(self: Controller, item: GifDto) =
proc isFavorite*(self: Controller, item: GifDto): bool =
return self.gifService.isFavorite(item)
proc setText*(self: Controller, text: string) =
let urls = self.messageService.getTextUrls(text)
self.delegate.setUrls(urls)
if len(urls) > 0:
let newUrls = self.linkPreviewCache.unknownUrls(urls)
self.messageService.asyncUnfurlUrls(newUrls)
proc linkPreviewsFromCache*(self: Controller, urls: seq[string]): Table[string, LinkPreview] =
return self.linkPreviewCache.linkPreviews(urls)
proc clearLinkPreviewCache*(self: Controller) =
self.linkPreviewCache.clear()
proc onUrlsUnfurled(self: Controller, args: LinkPreviewV2DataArgs) =
let urls = self.linkPreviewCache.add(args.linkPreviews)
self.delegate.updateLinkPreviewsFromCache(urls)

View File

@ -1,6 +1,7 @@
import NimQml
import NimQml, tables
import ../../../../../../app_service/service/gif/dto
import ../../../../../../app_service/service/message/dto/link_preview
type
AccessInterface* {.pure inheritable.} = ref object of RootObj
@ -94,3 +95,18 @@ method isFavorite*(self: AccessInterface, item: GifDto): bool {.base.} =
method viewDidLoad*(self: AccessInterface) {.base.} =
raise newException(ValueError, "No implementation available")
method setText*(self: AccessInterface, text: string) {.base.} =
raise newException(ValueError, "No implementation available")
method setUrls*(self: AccessInterface, urls: seq[string]) {.base.} =
raise newException(ValueError, "No implementation available")
method updateLinkPreviewsFromCache*(self: AccessInterface, urls: seq[string]) {.base.} =
raise newException(ValueError, "No implementation available")
method clearLinkPreviewCache*(self: AccessInterface) {.base.} =
raise newException(ValueError, "No implementation available")
method linkPreviewsFromCache*(self: AccessInterface, urls: seq[string]): Table[string, LinkPreview] {.base.} =
raise newException(ValueError, "No implementation available")

View File

@ -0,0 +1,44 @@
import tables
import ../../../../../../app_service/service/message/dto/link_preview
type
LinkPreviewCache* = ref object
cache: Table[string, LinkPreview]
proc newLinkPreiewCache*(): LinkPreviewCache =
result = LinkPreviewCache()
result.cache = initTable[string, LinkPreview]()
# Returns a table of link previews for given `urls`.
# If url is not found in cache, it's skipped
proc linkPreviews*(self: LinkPreviewCache, urls: seq[string]): Table[string, LinkPreview] =
result = initTable[string, LinkPreview]()
for url in urls:
if self.cache.hasKey(url):
result[url] = self.cache[url]
# Returns list of link previews for given `urls`.
# If url is not found in cache, it's skipped
proc linkPreviewsSeq*(self: LinkPreviewCache, urls: seq[string]): seq[LinkPreview] =
for url in urls:
if self.cache.hasKey(url):
result.add(self.cache[url])
# Adds all given link previews to cache.
# Returns list of urls, for which link preview was updated.
# If a url is already found in cache, correcponding link preview is updated.
proc add*(self: LinkPreviewCache, linkPreviews: Table[string, LinkPreview]): seq[string] =
for key, value in pairs(linkPreviews):
result.add(key)
self.cache[key] = value
# Goes though given `urls` and returns a list
# of urls not found in cache.
proc unknownUrls*(self: LinkPreviewCache, urls: seq[string]): seq[string] =
for url in urls:
if not self.cache.hasKey(url):
result.add(url)
# Clears link preview cache
proc clear*(self: LinkPreviewCache) =
self.cache.clear()

View File

@ -0,0 +1,22 @@
import strformat
import ../../../../../../app_service/service/message/dto/link_preview
type
Item* = ref object
unfurled*: bool
linkPreview*: LinkPreview
proc delete*(self: Item) =
self.linkPreview.delete
proc linkPreview*(self: Item): LinkPreview {.inline.} =
return self.linkPreview
proc `linkPreview=`*(self: Item, linkPreview: LinkPreview) {.inline.} =
self.linkPreview = linkPreview
proc `$`*(self: Item): string =
result = fmt"""LinkPreviewItem(
unfurled: {self.unfurled},
linkPreview: {self.linkPreview},
)"""

View File

@ -0,0 +1,140 @@
import NimQml, strformat, tables
import ./link_preview_item
import ../../../../../../app_service/service/message/dto/link_preview
type
ModelRole {.pure.} = enum
Url = UserRole + 1
Unfurled
Hostname
Title
Description
ThumbnailWidth
ThumbnailHeight
ThumbnailUrl
ThumbnailDataUri
QtObject:
type
Model* = ref object of QAbstractListModel
items: seq[Item]
proc delete*(self: Model) =
for i in 0 ..< self.items.len:
self.items[i].delete
self.items = @[]
self.QAbstractListModel.delete
proc setup(self: Model) =
self.QAbstractListModel.setup
proc newLinkPreviewModel*(): Model =
new(result, delete)
result.setup
proc newModel*(): Model =
new(result, delete)
result.setup
proc `$`*(self: Model): string =
for i in 0 ..< self.items.len:
result &= fmt"""
[{i}]:({$self.items[i]})
"""
proc countChanged(self: Model) {.signal.}
proc getCount*(self: Model): int {.slot.} =
self.items.len
QtProperty[int] count:
read = getCount
notify = countChanged
method rowCount(self: Model, index: QModelIndex = nil): int =
return self.items.len
method roleNames(self: Model): Table[int, string] =
{
ModelRole.Url.int:"url",
ModelRole.Unfurled.int:"unfurled",
ModelRole.Hostname.int:"hostname",
ModelRole.Title.int:"title",
ModelRole.Description.int:"description",
ModelRole.ThumbnailWidth.int:"thumbnailWidth",
ModelRole.ThumbnailHeight.int:"thumbnailHeight",
ModelRole.ThumbnailUrl.int:"thumbnailUrl",
ModelRole.ThumbnailDataUri.int:"thumbnailDataUri",
}.toTable
method allLinkPreviewRoles(self: Model): seq[int] =
return @[
Unfurled.int,
Hostname.int,
Title.int,
Description.int,
ThumbnailWidth.int,
ThumbnailHeight.int,
ThumbnailUrl.int,
ThumbnailDataUri.int,
]
method data(self: Model, index: QModelIndex, role: int): QVariant =
if (not index.isValid):
return
if (index.row < 0 or index.row >= self.items.len):
return
let item = self.items[index.row]
let enumRole = role.ModelRole
case enumRole:
of ModelRole.Url:
result = newQVariant(item.linkPreview.url)
of ModelRole.Unfurled:
result = newQVariant(item.unfurled)
of ModelRole.Hostname:
result = newQVariant(item.linkPreview.hostname)
of ModelRole.Title:
result = newQVariant(item.linkPreview.title)
of ModelRole.Description:
result = newQVariant(item.linkPreview.description)
of ModelRole.ThumbnailWidth:
result = newQVariant(item.linkPreview.thumbnail.width)
of ModelRole.ThumbnailHeight:
result = newQVariant(item.linkPreview.thumbnail.height)
of ModelRole.ThumbnailUrl:
result = newQVariant(item.linkPreview.thumbnail.url)
of ModelRole.ThumbnailDataUri:
result = newQVariant(item.linkPreview.thumbnail.dataUri)
proc clearItems*(self: Model) =
self.beginResetModel()
self.items = @[]
self.endResetModel()
self.countChanged()
proc setUrls*(self: Model, urls: seq[string]) =
var items: seq[Item]
for url in urls:
let linkPreview = initLinkPreview(url)
var item = Item()
item.unfurled = false
item.linkPreview = linkPreview
items.add(item)
self.beginResetModel()
self.items = items
self.endResetModel()
self.countChanged()
proc updateLinkPreviews*(self: Model, linkPreviews: Table[string, LinkPreview]) =
for row, item in self.items:
if not linkPreviews.hasKey(item.linkPreview.url):
continue
item.unfurled = true
item.linkPreview = linkPreviews[item.linkPreview.url]
let modelIndex = self.createIndex(row, 0, nil)
defer: modelIndex.delete
self.dataChanged(modelIndex, modelIndex, self.allLinkPreviewRoles())

View File

@ -1,10 +1,12 @@
import NimQml
import NimQml, tables
import io_interface
import ../io_interface as delegate_interface
import view, controller
import ../../../../../global/global_singleton
import ../../../../../core/eventemitter
import ../../../../../../app_service/service/message/service as message_service
import ../../../../../../app_service/service/message/dto/link_preview
import ../../../../../../app_service/service/chat/service as chat_service
import ../../../../../../app_service/service/community/service as community_service
import ../../../../../../app_service/service/gif/service as gif_service
@ -29,13 +31,14 @@ proc newModule*(
chatService: chat_service.Service,
communityService: community_service.Service,
gifService: gif_service.Service,
messageService: message_service.Service
):
Module =
result = Module()
result.delegate = delegate
result.view = view.newView(result)
result.viewVariant = newQVariant(result.view)
result.controller = controller.newController(result, events, sectionId, chatId, belongsToCommunity, chatService, communityService, gifService)
result.controller = controller.newController(result, events, sectionId, chatId, belongsToCommunity, chatService, communityService, gifService, messageService)
result.moduleLoaded = false
method delete*(self: Module) =
@ -147,3 +150,18 @@ method addToRecentsGif*(self: Module, item: GifDto) =
method isFavorite*(self: Module, item: GifDto): bool =
return self.controller.isFavorite(item)
method setText*(self: Module, text: string) =
self.controller.setText(text)
method clearLinkPreviewCache*(self: Module) {.slot.} =
self.controller.clearLinkPreviewCache()
method updateLinkPreviewsFromCache*(self: Module, urls: seq[string]) =
self.view.updateLinkPreviewsFromCache(urls)
method setUrls*(self: Module, urls: seq[string]) =
self.view.setUrls(urls)
method linkPreviewsFromCache*(self: Module, urls: seq[string]): Table[string, LinkPreview] =
return self.controller.linkPreviewsFromCache(urls)

View File

@ -16,7 +16,7 @@ QtObject:
proc newPreservedProperties*(): PreservedProperties =
new(result, delete)
result.QObject.setup
result.setup
proc textChanged*(self: PreservedProperties) {.signal.}
proc setText*(self: PreservedProperties, value: string) {.slot.} =

View File

@ -2,6 +2,7 @@ import NimQml
import ./io_interface
import ./gif_column_model
import ./preserved_properties
import ./link_preview_model as link_preview_model
import ../../../../../../app_service/service/gif/dto
QtObject:
@ -14,6 +15,8 @@ QtObject:
gifLoading: bool
preservedProperties: PreservedProperties
preservedPropertiesVariant: QVariant
linkPreviewModel: link_preview_model.Model
linkPreviewModelVariant: QVariant
proc delete*(self: View) =
self.QObject.delete
@ -22,6 +25,8 @@ QtObject:
self.gifColumnCModel.delete
self.preservedProperties.delete
self.preservedPropertiesVariant.delete
self.linkPreviewModel.delete
self.linkPreviewModelVariant.delete
proc newView*(delegate: io_interface.AccessInterface): View =
new(result, delete)
@ -33,6 +38,8 @@ QtObject:
result.gifLoading = false
result.preservedProperties = newPreservedProperties()
result.preservedPropertiesVariant = newQVariant(result.preservedProperties)
result.linkPreviewModel = newLinkPreviewModel()
result.linkPreviewModelVariant = newQVariant(result.linkPreviewModel)
proc load*(self: View) =
self.delegate.viewDidLoad()
@ -183,3 +190,25 @@ QtObject:
QtProperty[QVariant] preservedProperties:
read = getPreservedProperties
proc getLinkPreviewModel*(self: View): QVariant {.slot.} =
return self.linkPreviewModelVariant
QtProperty[QVariant] linkPreviewModel:
read = getLinkPreviewModel
# Currently used to fetch link previews, but could be used elsewhere
proc setText*(self: View, text: string) {.slot.} =
self.delegate.setText(text)
proc updateLinkPreviewsFromCache*(self: View, urls: seq[string]) =
let linkPreviews = self.delegate.linkPreviewsFromCache(urls)
self.linkPreviewModel.updateLinkPreviews(linkPreviews)
proc setUrls*(self: View, urls: seq[string]) =
self.linkPreviewModel.setUrls(urls)
self.updateLinkPreviewsFromCache(urls)
proc clearLinkPreviewCache*(self: View) {.slot.} =
self.delegate.clearLinkPreviewCache()

View File

@ -55,7 +55,8 @@ proc newModule*(delegate: delegate_interface.AccessInterface, events: EventEmitt
isUsersListAvailable, settingsService, nodeConfigurationService, contactService, chatService, communityService, messageService)
result.moduleLoaded = false
result.inputAreaModule = input_area_module.newModule(result, events, sectionId, chatId, belongsToCommunity, chatService, communityService, gifService)
result.inputAreaModule = input_area_module.newModule(result, events, sectionId, chatId, belongsToCommunity,
chatService, communityService, gifService, messageService)
result.messagesModule = messages_module.newModule(result, events, sectionId, chatId, belongsToCommunity,
contactService, communityService, chatService, messageService, mailserversService)
result.usersModule =

View File

@ -4,6 +4,7 @@ import std/[times, os]
import ../../../app/core/tasks/[qt, threadpool]
import ./dto/chat as chat_dto
import ../message/dto/message as message_dto
import ../message/dto/link_preview
import ../activity_center/dto/notification as notification_dto
import ../community/dto/community as community_dto
import ../contacts/service as contact_service
@ -508,6 +509,7 @@ QtObject:
replyTo: string,
contentType: int,
preferredUsername: string = "",
linkPreviews: seq[LinkPreview] = @[],
communityId: string = "") =
try:
let allKnownContacts = self.contactService.getContactsByGroup(ContactsGroup.AllKnownContacts)
@ -519,6 +521,7 @@ QtObject:
replyTo,
contentType,
preferredUsername,
linkPreviews,
communityId) # Only send a community ID for the community invites
let (chats, messages) = self.processMessageUpdateAfterSend(response)

View File

@ -284,3 +284,31 @@ const asyncGetFirstUnseenMessageIdForTaskArg: Task = proc(argEncoded: string) {.
responseJson["error"] = %e.msg
arg.finish(responseJson)
#################################################
# Async unfurl urls
#################################################
type
AsyncUnfurlUrlsTaskArg = ref object of QObjectTaskArg
urls*: seq[string]
const asyncUnfurlUrlsTask: Task = proc(argEncoded: string) {.gcsafe, nimcall.} =
let arg = decode[AsyncUnfurlUrlsTaskArg](argEncoded)
try:
let response = status_go.unfurlUrls(arg.urls)
let output = %*{
"error": (if response.error != nil: response.error.message else: ""),
"response": response.result,
"requestedUrls": %*arg.urls
}
arg.finish(output)
except Exception as e:
error "unfurlUrlsTask failed", message = e.msg
let output = %*{
"error": e.msg,
"response": "",
"requestedUrls": %*arg.urls
}
arg.finish(output)

View File

@ -0,0 +1,60 @@
import json, strformat, tables
include ../../../common/json_utils
type
LinkPreviewThumbnail* = object
width*: int
height*: int
url*: string
dataUri*: string
type
LinkPreview* = ref object
url*: string
hostname*: string
title*: string
description*: string
thumbnail*: LinkPreviewThumbnail
proc delete*(self: LinkPreview) =
discard
proc initLinkPreview*(url: string): LinkPreview =
result = LinkPreview()
result.url = url
proc toLinkPreviewThumbnail*(jsonObj: JsonNode): LinkPreviewThumbnail =
result = LinkPreviewThumbnail()
discard jsonObj.getProp("width", result.width)
discard jsonObj.getProp("height", result.height)
discard jsonObj.getProp("url", result.url)
discard jsonObj.getProp("dataUri", result.dataUri)
proc toLinkPreview*(jsonObj: JsonNode): LinkPreview =
result = LinkPreview()
discard jsonObj.getProp("url", result.url)
discard jsonObj.getProp("hostname", result.hostname)
discard jsonObj.getProp("title", result.title)
discard jsonObj.getProp("description", result.description)
discard jsonObj.getProp("hostname", result.hostname)
var thumbnail: JsonNode
if jsonObj.getProp("thumbnail", thumbnail):
result.thumbnail = toLinkPreviewThumbnail(thumbnail)
proc `$`*(self: LinkPreviewThumbnail): string =
result = fmt"""LinkPreviewThumbnail(
width: {self.width},
height: {self.height},
url: {self.url},
dataUri: {self.dataUri}
)"""
proc `$`*(self: LinkPreview): string =
result = fmt"""LinkPreview(
url: {self.url},
hostname: {self.hostname},
title: {self.title},
description: {self.description},
thumbnail: {self.thumbnail}
)"""

View File

@ -17,6 +17,7 @@ import ./dto/reaction as reaction_dto
import ../chat/dto/chat as chat_dto
import ./dto/pinned_message_update as pinned_msg_update_dto
import ./dto/removed_message as removed_msg_dto
import ./dto/link_preview
import ./message_cursor
import ../../common/message as message_common
@ -58,6 +59,7 @@ const SIGNAL_ENVELOPE_SENT* = "envelopeSent"
const SIGNAL_ENVELOPE_EXPIRED* = "envelopeExpired"
const SIGNAL_MESSAGE_LINK_PREVIEW_DATA_LOADED* = "messageLinkPreviewDataLoaded"
const SIGNAL_RELOAD_MESSAGES* = "reloadMessages"
const SIGNAL_URLS_UNFURLED* = "urlsUnfurled"
include async_tasks
@ -121,6 +123,9 @@ type
response*: JsonNode
uuid*: string
LinkPreviewV2DataArgs* = ref object of Args
linkPreviews*: Table[string, LinkPreview]
ReloadMessagesArgs* = ref object of Args
communityId*: string
@ -777,10 +782,65 @@ QtObject:
unfurlImages: unfurlImages,
uuid: uuid
)
self.threadpool.start(arg)
return $genOid()
proc getTextUrls*(self: Service, text: string): seq[string] =
try:
let response = status_go.getTextUrls(text)
if response.result.kind != JArray:
warn "expected response is not an array", methodName = "getTextUrls"
return
return map(response.result.getElems(), proc(x: JsonNode): string = x.getStr())
except Exception as e:
error "getTextUrls failed", errName = e.name, errDesription = e.msg
proc onAsyncUnfurlUrlsFinished*(self: Service, response: string) {.slot.}=
let responseObj = response.parseJson
if responseObj.kind != JObject:
warn "expected response is not a json object", methodName = "onAsyncUnfurlUrlsFinished"
return
let errMessage = responseObj["error"].getStr
if errMessage != "":
error "asyncUnfurlUrls failed", errMessage
return
var requestedUrlsArr: JsonNode
var requestedUrls: seq[string]
if responseObj.getProp("requestedUrls", requestedUrlsArr):
requestedUrls = map(requestedUrlsArr.getElems(), proc(x: JsonNode): string = x.getStr)
var linkPreviewsArr: JsonNode
var linkPreviews: Table[string, LinkPreview]
if responseObj.getProp("response", linkPreviewsArr):
for element in linkPreviewsArr.getElems():
let linkPreview = element.toLinkPreview()
linkPreviews[linkPreview.url] = linkPreview
for url in requestedUrls:
if not linkPreviews.hasKey(url):
linkPreviews[url] = initLinkPreview(url)
let args = LinkPreviewV2DataArgs(
linkPreviews: linkPreviews
)
self.events.emit(SIGNAL_URLS_UNFURLED, args)
proc asyncUnfurlUrls*(self: Service, urls: seq[string]) =
if len(urls) == 0:
return
let arg = AsyncUnfurlUrlsTaskArg(
tptr: cast[ByteAddress](asyncUnfurlUrlsTask),
vptr: cast[ByteAddress](self.vptr),
slot: "onAsyncUnfurlUrlsFinished",
urls: urls
)
self.threadpool.start(arg)
# See render-inline in status-mobile/src/status_im/ui/screens/chat/message/message.cljs
proc renderInline(self: Service, parsedText: ParsedText, communityChats: seq[ChatDto]): string =
let value = escape_html(parsedText.literal)

View File

@ -435,7 +435,8 @@ QtObject:
replyTo,
ContentType.Sticker.int,
preferredUsername,
communityId = "", # communityId is not ncessary when sending a sticker
linkPreviews = @[],
communityId = "", # communityId is not necessary when sending a sticker
sticker.hash,
sticker.packId)
discard self.chatService.processMessageUpdateAfterSend(response)

View File

@ -2,6 +2,7 @@ import json, sequtils, sugar, strutils
import core, ../app_service/common/utils
import response_type
import interpret/cropped_image
import ../app_service/service/message/dto/link_preview
export response_type
@ -60,6 +61,7 @@ proc sendChatMessage*(
replyTo: string,
contentType: int,
preferredUsername: string = "",
linkPreviews: seq[LinkPreview],
communityId: string = "",
stickerHash: string = "",
stickerPack: string = "0",
@ -75,7 +77,8 @@ proc sendChatMessage*(
"pack": parseInt(stickerPack)
},
"contentType": contentType,
"communityId": communityId
"communityId": communityId,
"linkPreviews": linkPreviews
}
])

View File

@ -1,4 +1,4 @@
import json
import json, strformat
import core, ../app_service/common/utils
import response_type
@ -72,3 +72,10 @@ proc firstUnseenMessageID*(chatId: string): RpcResponse[JsonNode] {.raises: [Exc
let payload = %* [chatId]
result = callPrivateRPC("firstUnseenMessageID".prefix, payload)
proc getTextUrls*(text: string): RpcResponse[JsonNode] {.raises: [Exception].} =
let payload = %*[text]
result = callPrivateRPC("getTextURLs".prefix, payload)
proc unfurlUrls*(urls: seq[string]): RpcResponse[JsonNode] {.raises: [Exception].} =
let payload = %*[urls]
result = callPrivateRPC("unfurlURLs".prefix, payload)

View File

@ -134,18 +134,20 @@ QtObject {
return msg
}
function cleanMessageText(formattedMessage) {
const text = globalUtilsInst.plainText(StatusQUtils.Emoji.deparse(formattedMessage))
return interpretMessage(text)
}
function sendMessage(chatId, event, text, replyMessageId, fileUrlsAndSources) {
chatCommunitySectionModule.prepareChatContentModuleForChatId(chatId)
const chatContentModule = chatCommunitySectionModule.getChatContentModule()
var result = false
let textMsg = globalUtilsInst.plainText(StatusQUtils.Emoji.deparse(text))
const textMsg = cleanMessageText(text)
if (textMsg.trim() !== "") {
textMsg = interpretMessage(textMsg)
if (event) {
if (event)
event.accepted = true
}
}
if (fileUrlsAndSources.length > 0) {

View File

@ -3,6 +3,7 @@ import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import QtQml 2.15
import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1
import StatusQ.Components 0.1
import StatusQ.Controls 0.1
@ -178,7 +179,15 @@ Item {
d.restoreInputAttachments()
}
readonly property var updateLinkPreviews: {
return Backpressure.debounce(this, 250, () => {
const messageText = root.rootStore.cleanMessageText(chatInput.textInput.text)
d.activeChatContentModule.inputAreaModule.setText(messageText)
})
}
onActiveChatContentModuleChanged: {
d.activeChatContentModule.inputAreaModule.clearLinkPreviewCache()
// Call later to make sure activeUsersStore and activeMessagesStore bindings are updated
Qt.callLater(d.restoreInputState)
}
@ -238,6 +247,31 @@ Item {
}
}
// This is a non-designed preview of unfurled urls.
// Should be replaced with a proper UI when it's ready.
//
// StatusListView {
// Layout.fillWidth: true
// Layout.maximumHeight: 200
// Layout.margins: Style.current.smallPadding
// // For a vertical list bind the imlicitHeight to contentHeight
// implicitHeight: contentHeight
// spacing: 10
// model: d.activeChatContentModule.inputAreaModule.linkPreviewModel
// delegate: StatusBaseText {
// width: ListView.view.width
// wrapMode: Text.WordWrap
// text: {
// const icon = unfurled ? (hostname !== "" ? '' : '') : '👀'
// const thumbnailInfo = `thumbnail: (${thumbnailWidth}*${thumbnailHeight}, url: ${thumbnailUrl.length} symbols, data: ${thumbnailDataUri.length} symbols)`
// return `${icon} ${url} (hostname: ${hostname}): ${title}\ndescription: ${description}\n${thumbnailInfo}`
// }
// }
// }
RowLayout {
Layout.fillWidth: true
Layout.margins: Style.current.smallPadding
@ -289,6 +323,7 @@ Item {
suggestions.suggestionFilter.addSystemSuggestions: chatType === Constants.chatType.communityChat
textInput.onTextChanged: {
d.updateLinkPreviews()
if (!!d.activeChatContentModule)
d.activeChatContentModule.inputAreaModule.preservedProperties.text = textInput.text
}
@ -334,19 +369,19 @@ Item {
return
}
if (root.rootStore.sendMessage(d.activeChatContentModule.getMyChatId(),
event,
chatInput.getTextWithPublicKeys(),
chatInput.isReply? chatInput.replyMessageId : "",
chatInput.fileUrlsAndSources
))
{
Global.playSendMessageSound()
if (root.rootStore.sendMessage(d.activeChatContentModule.getMyChatId(),
event,
chatInput.getTextWithPublicKeys(),
chatInput.isReply? chatInput.replyMessageId : "",
chatInput.fileUrlsAndSources
))
{
Global.playSendMessageSound()
chatInput.textInput.clear();
chatInput.textInput.textFormat = TextEdit.PlainText;
chatInput.textInput.textFormat = TextEdit.RichText;
}
chatInput.textInput.clear();
chatInput.textInput.textFormat = TextEdit.PlainText;
chatInput.textInput.textFormat = TextEdit.RichText;
}
}
onKeyUpPress: {