Split message content into its own module
This is laying some groundwork for the rich text chatbar.
177
src/messagecontent/AudioComponent.qml
Normal file
@@ -0,0 +1,177 @@
|
||||
// SPDX-FileCopyrightText: 2022 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtQuick.Layouts
|
||||
import QtMultimedia
|
||||
|
||||
import org.kde.coreaddons
|
||||
import org.kde.kirigami as Kirigami
|
||||
|
||||
import org.kde.neochat
|
||||
|
||||
/**
|
||||
* @brief A component to show audio from a message.
|
||||
*/
|
||||
ColumnLayout {
|
||||
id: root
|
||||
|
||||
/**
|
||||
* @brief The matrix ID of the message event.
|
||||
*/
|
||||
required property string eventId
|
||||
|
||||
/**
|
||||
* @brief The media info for the event.
|
||||
*
|
||||
* This should consist of the following:
|
||||
* - source - The mxc URL for the media.
|
||||
* - mimeType - The MIME type of the media (should be image/xxx for this delegate).
|
||||
* - mimeIcon - The MIME icon name (should be image-xxx).
|
||||
* - size - The file size in bytes.
|
||||
* - width - The width in pixels of the audio media.
|
||||
* - height - The height in pixels of the audio media.
|
||||
* - tempInfo - mediaInfo (with the same properties as this except no tempInfo) for a temporary image while the file downloads.
|
||||
* - filename - original filename of the media
|
||||
*/
|
||||
required property var mediaInfo
|
||||
|
||||
/**
|
||||
* @brief FileTransferInfo for any downloading files.
|
||||
*/
|
||||
required property var fileTransferInfo
|
||||
|
||||
/**
|
||||
* @brief Whether the media has been downloaded.
|
||||
*/
|
||||
readonly property bool downloaded: root.fileTransferInfo && root.fileTransferInfo.completed
|
||||
onDownloadedChanged: if (downloaded) {
|
||||
audio.play();
|
||||
}
|
||||
|
||||
MediaPlayer {
|
||||
id: audio
|
||||
onErrorOccurred: (error, errorString) => console.warn("Audio playback error:" + error + errorString)
|
||||
audioOutput: AudioOutput {}
|
||||
}
|
||||
|
||||
states: [
|
||||
State {
|
||||
name: "notDownloaded"
|
||||
when: !root.fileTransferInfo.completed && !root.fileTransferInfo.active
|
||||
|
||||
PropertyChanges {
|
||||
target: playButton
|
||||
icon.name: "media-playback-start"
|
||||
onClicked: Message.room.downloadFile(root.eventId)
|
||||
}
|
||||
},
|
||||
State {
|
||||
name: "downloading"
|
||||
when: root.fileTransferInfo.active && !root.fileTransferInfo.completed
|
||||
PropertyChanges {
|
||||
target: downloadBar
|
||||
visible: true
|
||||
}
|
||||
PropertyChanges {
|
||||
target: playButton
|
||||
icon.name: "media-playback-stop"
|
||||
onClicked: {
|
||||
Message.room.cancelFileTransfer(root.eventId);
|
||||
}
|
||||
}
|
||||
},
|
||||
State {
|
||||
name: "paused"
|
||||
when: root.fileTransferInfo.completed && (audio.playbackState === MediaPlayer.StoppedState || audio.playbackState === MediaPlayer.PausedState)
|
||||
PropertyChanges {
|
||||
target: playButton
|
||||
icon.name: "media-playback-start"
|
||||
onClicked: {
|
||||
audio.source = root.fileTransferInfo.localPath;
|
||||
MediaManager.startPlayback();
|
||||
audio.play();
|
||||
}
|
||||
}
|
||||
},
|
||||
State {
|
||||
name: "playing"
|
||||
when: root.fileTransferInfo.completed && audio.playbackState === MediaPlayer.PlayingState
|
||||
|
||||
PropertyChanges {
|
||||
target: playButton
|
||||
|
||||
icon.name: "media-playback-pause"
|
||||
|
||||
onClicked: audio.pause()
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
Connections {
|
||||
target: MediaManager
|
||||
function onPlaybackStarted() {
|
||||
if (audio.playbackState === MediaPlayer.PlayingState) {
|
||||
audio.pause();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
spacing: Kirigami.Units.smallSpacing
|
||||
|
||||
QQC2.ToolButton {
|
||||
id: playButton
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
spacing: 0
|
||||
|
||||
QQC2.Label {
|
||||
text: root.mediaInfo.filename
|
||||
wrapMode: Text.Wrap
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
QQC2.Label {
|
||||
text: Format.formatDuration(root.mediaInfo.duration)
|
||||
color: Kirigami.Theme.disabledTextColor
|
||||
visible: !audio.hasAudio
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
}
|
||||
QQC2.ProgressBar {
|
||||
id: downloadBar
|
||||
visible: false
|
||||
Layout.fillWidth: true
|
||||
from: 0
|
||||
to: root.mediaInfo.size
|
||||
value: root.fileTransferInfo.progress
|
||||
}
|
||||
RowLayout {
|
||||
visible: audio.hasAudio
|
||||
|
||||
QQC2.Slider {
|
||||
Layout.fillWidth: true
|
||||
from: 0
|
||||
to: audio.duration
|
||||
value: audio.position
|
||||
onMoved: audio.setPosition(value)
|
||||
}
|
||||
|
||||
QQC2.Label {
|
||||
visible: root.Message.maxContentWidth > Kirigami.Units.gridUnit * 12
|
||||
|
||||
text: Format.formatDuration(audio.position) + "/" + Format.formatDuration(audio.duration)
|
||||
}
|
||||
}
|
||||
QQC2.Label {
|
||||
Layout.alignment: Qt.AlignRight
|
||||
Layout.rightMargin: Kirigami.Units.smallSpacing
|
||||
visible: audio.hasAudio && root.Message.maxContentWidth < Kirigami.Units.gridUnit * 12
|
||||
|
||||
text: Format.formatDuration(audio.position) + "/" + Format.formatDuration(audio.duration)
|
||||
}
|
||||
}
|
||||
96
src/messagecontent/AuthorComponent.qml
Normal file
@@ -0,0 +1,96 @@
|
||||
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtQuick.Layouts
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
|
||||
import org.kde.neochat
|
||||
|
||||
RowLayout {
|
||||
id: root
|
||||
|
||||
/**
|
||||
* @brief The matrix ID of the message event.
|
||||
*/
|
||||
required property string eventId
|
||||
|
||||
/**
|
||||
* @brief The message author.
|
||||
*
|
||||
* A Quotient::RoomMember object.
|
||||
*
|
||||
* @sa Quotient::RoomMember
|
||||
*/
|
||||
required property var author
|
||||
|
||||
/**
|
||||
* @brief The timestamp of the message.
|
||||
*/
|
||||
required property var time
|
||||
|
||||
/**
|
||||
* @brief The timestamp of the message as a string.
|
||||
*/
|
||||
required property string timeString
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.maximumWidth: Message.maxContentWidth
|
||||
|
||||
implicitHeight: Math.max(nameButton.implicitHeight, timeLabel.implicitHeight)
|
||||
|
||||
QQC2.Label {
|
||||
id: nameButton
|
||||
|
||||
text: root.author.disambiguatedName
|
||||
color: root.author.color
|
||||
textFormat: Text.PlainText
|
||||
font.weight: Font.Bold
|
||||
elide: Text.ElideRight
|
||||
|
||||
function openUserMenu(): void {
|
||||
const menu = Qt.createComponent("org.kde.neochat", "UserMenu").createObject(root, {
|
||||
window: QQC2.ApplicationWindow.window as Kirigami.ApplicationWindow,
|
||||
author: root.author,
|
||||
});
|
||||
menu.popup(root.QQC2.Overlay.overlay);
|
||||
}
|
||||
|
||||
// tapping to open profile
|
||||
TapHandler {
|
||||
onTapped: RoomManager.resolveResource(root.author.uri)
|
||||
}
|
||||
|
||||
// right-clicking/long-press for context menu
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
|
||||
acceptedButtons: Qt.RightButton
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
|
||||
onPressed: nameButton.openUserMenu()
|
||||
}
|
||||
TapHandler {
|
||||
acceptedDevices: PointerDevice.TouchScreen
|
||||
onTapped: nameButton.openUserMenu()
|
||||
}
|
||||
}
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
QQC2.Label {
|
||||
id: timeLabel
|
||||
text: root.timeString
|
||||
horizontalAlignment: Text.AlignRight
|
||||
color: Kirigami.Theme.disabledTextColor
|
||||
QQC2.ToolTip.visible: timeHoverHandler.hovered
|
||||
QQC2.ToolTip.text: root.time.toLocaleString(Qt.locale(), Locale.ShortFormat)
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
|
||||
HoverHandler {
|
||||
id: timeHoverHandler
|
||||
}
|
||||
}
|
||||
}
|
||||
185
src/messagecontent/BaseMessageComponentChooser.qml
Normal file
@@ -0,0 +1,185 @@
|
||||
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Qt.labs.qmlmodels
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
|
||||
import org.kde.neochat
|
||||
|
||||
/**
|
||||
* @brief Select a message component based on a MessageComponentType.
|
||||
*/
|
||||
DelegateChooser {
|
||||
id: root
|
||||
|
||||
/**
|
||||
* @brief The user selected text has changed.
|
||||
*/
|
||||
signal selectedTextChanged(string selectedText)
|
||||
|
||||
/**
|
||||
* @brief The user hovered link has changed.
|
||||
*/
|
||||
signal hoveredLinkChanged(string hoveredLink)
|
||||
|
||||
signal removeLinkPreview(int index)
|
||||
|
||||
/**
|
||||
* @brief Request more events in the thread be loaded.
|
||||
*/
|
||||
signal fetchMoreEvents()
|
||||
|
||||
role: "componentType"
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: MessageComponentType.Author
|
||||
delegate: AuthorComponent {}
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: MessageComponentType.Text
|
||||
delegate: TextComponent {
|
||||
onSelectedTextChanged: root.selectedTextChanged(selectedText)
|
||||
onHoveredLinkChanged: root.hoveredLinkChanged(hoveredLink)
|
||||
}
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: MessageComponentType.Image
|
||||
delegate: ImageComponent {}
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: MessageComponentType.Video
|
||||
delegate: VideoComponent {}
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: MessageComponentType.Code
|
||||
delegate: CodeComponent {
|
||||
onSelectedTextChanged: selectedText => {
|
||||
root.selectedTextChanged(selectedText);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: MessageComponentType.Quote
|
||||
delegate: QuoteComponent {
|
||||
onSelectedTextChanged: selectedText => {
|
||||
root.selectedTextChanged(selectedText);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: MessageComponentType.Audio
|
||||
delegate: AudioComponent {}
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: MessageComponentType.File
|
||||
delegate: FileComponent {}
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: MessageComponentType.Itinerary
|
||||
delegate: ItineraryComponent {}
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: MessageComponentType.Pdf
|
||||
delegate: PdfPreviewComponent {}
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: MessageComponentType.Poll
|
||||
delegate: PollComponent {}
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: MessageComponentType.Location
|
||||
delegate: LocationComponent {}
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: MessageComponentType.LiveLocation
|
||||
delegate: LiveLocationComponent {}
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: MessageComponentType.Encrypted
|
||||
delegate: EncryptedComponent {}
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: MessageComponentType.Reply
|
||||
delegate: ReplyComponent {}
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: MessageComponentType.Reaction
|
||||
delegate: ReactionComponent {}
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: MessageComponentType.LinkPreview
|
||||
delegate: LinkPreviewComponent {
|
||||
onRemove: index => root.removeLinkPreview(index)
|
||||
}
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: MessageComponentType.LinkPreviewLoad
|
||||
delegate: LinkPreviewLoadComponent {
|
||||
type: LinkPreviewLoadComponent.LinkPreview
|
||||
onRemove: index => root.removeLinkPreview(index)
|
||||
}
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: MessageComponentType.ChatBar
|
||||
delegate: ChatBarComponent {}
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: MessageComponentType.ReplyButton
|
||||
delegate: ReplyButtonComponent {}
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: MessageComponentType.FetchButton
|
||||
delegate: FetchButtonComponent {
|
||||
onFetchMoreEvents: root.fetchMoreEvents()
|
||||
}
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: MessageComponentType.Verification
|
||||
delegate: MimeComponent {
|
||||
mimeIconSource: "security-high"
|
||||
label: i18n("%1 started a user verification", model.author.htmlSafeDisplayName)
|
||||
}
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: MessageComponentType.Loading
|
||||
delegate: LoadComponent {}
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: MessageComponentType.Separator
|
||||
delegate: Kirigami.Separator {
|
||||
Layout.fillWidth: true
|
||||
Layout.maximumWidth: Message.maxContentWidth
|
||||
}
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: MessageComponentType.Other
|
||||
delegate: Item {}
|
||||
}
|
||||
}
|
||||
107
src/messagecontent/CMakeLists.txt
Normal file
@@ -0,0 +1,107 @@
|
||||
# SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
|
||||
# SPDX-License-Identifier: BSD-2-Clause
|
||||
|
||||
qt_add_library(MessageContent STATIC)
|
||||
ecm_add_qml_module(MessageContent GENERATE_PLUGIN_SOURCE
|
||||
URI org.kde.neochat.messagecontent
|
||||
OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/src/org/kde/neochat/messagecontent
|
||||
QML_FILES
|
||||
BaseMessageComponentChooser.qml
|
||||
MessageComponentChooser.qml
|
||||
ReplyMessageComponentChooser.qml
|
||||
AuthorComponent.qml
|
||||
AudioComponent.qml
|
||||
ChatBarComponent.qml
|
||||
CodeComponent.qml
|
||||
EncryptedComponent.qml
|
||||
FetchButtonComponent.qml
|
||||
FileComponent.qml
|
||||
ImageComponent.qml
|
||||
ItineraryComponent.qml
|
||||
ItineraryReservationComponent.qml
|
||||
JourneySectionStopDelegateLineSegment.qml
|
||||
TransportIcon.qml
|
||||
FoodReservationComponent.qml
|
||||
TrainReservationComponent.qml
|
||||
FlightReservationComponent.qml
|
||||
HotelReservationComponent.qml
|
||||
LinkPreviewComponent.qml
|
||||
LinkPreviewLoadComponent.qml
|
||||
LiveLocationComponent.qml
|
||||
LoadComponent.qml
|
||||
LocationComponent.qml
|
||||
MimeComponent.qml
|
||||
PdfPreviewComponent.qml
|
||||
PollComponent.qml
|
||||
QuoteComponent.qml
|
||||
ReactionComponent.qml
|
||||
ReplyAuthorComponent.qml
|
||||
ReplyButtonComponent.qml
|
||||
ReplyComponent.qml
|
||||
StateComponent.qml
|
||||
TextComponent.qml
|
||||
ThreadBodyComponent.qml
|
||||
VideoComponent.qml
|
||||
SOURCES
|
||||
contentprovider.cpp
|
||||
mediasizehelper.cpp
|
||||
pollhandler.cpp
|
||||
models/itinerarymodel.cpp
|
||||
models/linemodel.cpp
|
||||
models/messagecontentmodel.cpp
|
||||
models/pollanswermodel.cpp
|
||||
models/reactionmodel.cpp
|
||||
models/threadmodel.cpp
|
||||
RESOURCES
|
||||
images/bike.svg
|
||||
images/bus.svg
|
||||
images/cablecar.svg
|
||||
images/car.svg
|
||||
images/coach.svg
|
||||
images/couchettecar.svg
|
||||
images/elevator.svg
|
||||
images/escalator.svg
|
||||
images/ferry.svg
|
||||
images/flight.svg
|
||||
images/foodestablishment.svg
|
||||
images/funicular.svg
|
||||
images/longdistancetrain.svg
|
||||
images/rapidtransit.svg
|
||||
images/seat.svg
|
||||
images/shuttle.svg
|
||||
images/sleepingcar.svg
|
||||
images/stairs.svg
|
||||
images/subway.svg
|
||||
images/taxi.svg
|
||||
images/train.svg
|
||||
images/tramway.svg
|
||||
images/transfer.svg
|
||||
images/wait.svg
|
||||
images/walk.svg
|
||||
DEPENDENCIES
|
||||
QtQuick
|
||||
)
|
||||
|
||||
configure_file(config-neochat.h.in ${CMAKE_CURRENT_BINARY_DIR}/config-neochat.h)
|
||||
|
||||
ecm_qt_declare_logging_category(MessageContent
|
||||
HEADER "messagemodel_logging.h"
|
||||
IDENTIFIER "Message"
|
||||
CATEGORY_NAME "org.kde.neochat.messagemodel"
|
||||
DESCRIPTION "Neochat: messagemodel"
|
||||
DEFAULT_SEVERITY Info
|
||||
EXPORT NEOCHAT
|
||||
)
|
||||
|
||||
target_include_directories(MessageContent PRIVATE ${CMAKE_BINARY_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/models)
|
||||
target_link_libraries(MessageContent PRIVATE
|
||||
Qt::Core
|
||||
Qt::Quick
|
||||
Qt::QuickControls2
|
||||
KF6::Kirigami
|
||||
LibNeoChat
|
||||
)
|
||||
|
||||
if(NOT ANDROID)
|
||||
target_link_libraries(MessageContent PUBLIC KF6::SyntaxHighlighting)
|
||||
endif()
|
||||
281
src/messagecontent/ChatBarComponent.qml
Normal file
@@ -0,0 +1,281 @@
|
||||
// SPDX-FileCopyrightText: 2023 James Graham <james.h.graham@protonmail.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
|
||||
import QtCore
|
||||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtQuick.Layouts
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
|
||||
import org.kde.neochat
|
||||
import org.kde.neochat.chatbar
|
||||
|
||||
/**
|
||||
* @brief A component to show a chat bar in a message bubble.
|
||||
*/
|
||||
QQC2.Control {
|
||||
id: root
|
||||
|
||||
/**
|
||||
* @brief The ChatBarCache to use.
|
||||
*/
|
||||
required property ChatBarCache chatBarCache
|
||||
onChatBarCacheChanged: documentHandler.chatBarCache = chatBarCache
|
||||
|
||||
readonly property bool isBusy: root.Message.room && root.Message.room.hasFileUploading
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.maximumWidth: Message.maxContentWidth
|
||||
|
||||
contentItem: ColumnLayout {
|
||||
Loader {
|
||||
id: paneLoader
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.margins: Kirigami.Units.largeSpacing
|
||||
Layout.preferredHeight: active ? item.implicitHeight : 0
|
||||
|
||||
active: visible
|
||||
visible: root.chatBarCache.replyId.length > 0 || root.chatBarCache.attachmentPath.length > 0
|
||||
sourceComponent: root.chatBarCache.replyId.length > 0 ? replyPane : attachmentPane
|
||||
}
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 0
|
||||
|
||||
QQC2.ScrollView {
|
||||
id: chatBarScrollView
|
||||
Layout.topMargin: Kirigami.Units.smallSpacing
|
||||
Layout.bottomMargin: Kirigami.Units.smallSpacing
|
||||
Layout.leftMargin: Kirigami.Units.largeSpacing
|
||||
Layout.rightMargin: Kirigami.Units.largeSpacing
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.maximumHeight: Kirigami.Units.gridUnit * 8
|
||||
|
||||
QQC2.TextArea {
|
||||
id: textArea
|
||||
|
||||
// Work around for BUG: 503846
|
||||
// Seems to crash when we try to access textArea's text here. Even though we add a slight delay it's still instantaneous in the UI.
|
||||
Component.onCompleted: Qt.callLater(() => _private.updateText())
|
||||
|
||||
Layout.fillWidth: true
|
||||
|
||||
color: Kirigami.Theme.textColor
|
||||
verticalAlignment: TextEdit.AlignVCenter
|
||||
wrapMode: TextEdit.Wrap
|
||||
|
||||
onTextChanged: {
|
||||
root.chatBarCache.text = text;
|
||||
}
|
||||
|
||||
Keys.onEnterPressed: {
|
||||
if (completionMenu.visible) {
|
||||
completionMenu.complete();
|
||||
} else if (event.modifiers & Qt.ShiftModifier) {
|
||||
textArea.insert(cursorPosition, "\n");
|
||||
} else {
|
||||
_private.post();
|
||||
}
|
||||
}
|
||||
Keys.onReturnPressed: {
|
||||
if (completionMenu.visible) {
|
||||
completionMenu.complete();
|
||||
} else if (event.modifiers & Qt.ShiftModifier) {
|
||||
textArea.insert(cursorPosition, "\n");
|
||||
} else {
|
||||
_private.post();
|
||||
}
|
||||
}
|
||||
Keys.onTabPressed: {
|
||||
if (completionMenu.visible) {
|
||||
completionMenu.complete();
|
||||
}
|
||||
}
|
||||
Keys.onPressed: event => {
|
||||
if (event.key === Qt.Key_Up && completionMenu.visible) {
|
||||
completionMenu.decrementIndex();
|
||||
} else if (event.key === Qt.Key_Down && completionMenu.visible) {
|
||||
completionMenu.incrementIndex();
|
||||
}
|
||||
}
|
||||
|
||||
CompletionMenu {
|
||||
id: completionMenu
|
||||
width: Math.max(350, root.width - 1)
|
||||
height: implicitHeight
|
||||
y: -height - 5
|
||||
z: 10
|
||||
connection: root.Message.room.connection
|
||||
chatDocumentHandler: documentHandler
|
||||
margins: 0
|
||||
Behavior on height {
|
||||
NumberAnimation {
|
||||
property: "height"
|
||||
duration: Kirigami.Units.shortDuration
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// opt-out of whatever spell checker a styled TextArea might come with
|
||||
Kirigami.SpellCheck.enabled: false
|
||||
|
||||
ChatDocumentHandler {
|
||||
id: documentHandler
|
||||
document: textArea.textDocument
|
||||
cursorPosition: textArea.cursorPosition
|
||||
selectionStart: textArea.selectionStart
|
||||
selectionEnd: textArea.selectionEnd
|
||||
room: root.Message.room // We don't care about saving for edits so this is OK.
|
||||
mentionColor: Kirigami.Theme.linkColor
|
||||
errorColor: Kirigami.Theme.negativeTextColor
|
||||
}
|
||||
|
||||
TextMetrics {
|
||||
id: textMetrics
|
||||
text: textArea.text
|
||||
}
|
||||
|
||||
Component {
|
||||
id: openFileDialog
|
||||
|
||||
OpenFileDialog {
|
||||
parentWindow: Window.window
|
||||
currentFolder: StandardPaths.standardLocations(StandardPaths.HomeLocation)[0]
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: attachDialog
|
||||
|
||||
AttachDialog {
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
}
|
||||
|
||||
background: null
|
||||
}
|
||||
}
|
||||
PieProgressBar {
|
||||
visible: root.isBusy
|
||||
progress: root.Message.room.fileUploadingProgress
|
||||
}
|
||||
QQC2.ToolButton {
|
||||
visible: !root.isBusy
|
||||
display: QQC2.AbstractButton.IconOnly
|
||||
action: Kirigami.Action {
|
||||
text: i18nc("@action:button", "Attach an image or file")
|
||||
icon.name: "mail-attachment"
|
||||
onTriggered: {
|
||||
let dialog = (Clipboard.hasImage ? attachDialog : openFileDialog).createObject(QQC2.Overlay.overlay);
|
||||
dialog.chosen.connect(path => root.chatBarCache.attachmentPath = path);
|
||||
dialog.open();
|
||||
}
|
||||
}
|
||||
QQC2.ToolTip.text: text
|
||||
QQC2.ToolTip.visible: hovered
|
||||
}
|
||||
QQC2.ToolButton {
|
||||
display: QQC2.AbstractButton.IconOnly
|
||||
action: Kirigami.Action {
|
||||
text: root.chatBarCache.isEditing ? i18nc("@action:button", "Confirm edit") : i18nc("@action:button", "Post message in thread")
|
||||
icon.name: "document-send"
|
||||
onTriggered: {
|
||||
_private.post();
|
||||
}
|
||||
}
|
||||
QQC2.ToolTip.text: text
|
||||
QQC2.ToolTip.visible: hovered
|
||||
}
|
||||
QQC2.ToolButton {
|
||||
display: QQC2.AbstractButton.IconOnly
|
||||
action: Kirigami.Action {
|
||||
text: i18nc("@action:button", "Cancel")
|
||||
icon.name: "dialog-close"
|
||||
onTriggered: {
|
||||
root.chatBarCache.clearRelations();
|
||||
}
|
||||
shortcut: "Escape"
|
||||
}
|
||||
QQC2.ToolTip.text: text
|
||||
QQC2.ToolTip.visible: hovered
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
color: Kirigami.Theme.backgroundColor
|
||||
radius: Kirigami.Units.cornerRadius
|
||||
border {
|
||||
width: 1
|
||||
color: Kirigami.ColorUtils.linearInterpolation(Kirigami.Theme.backgroundColor, Kirigami.Theme.textColor, Kirigami.Theme.frameContrast)
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: replyPane
|
||||
Item {
|
||||
implicitWidth: replyComponent.implicitWidth
|
||||
implicitHeight: replyComponent.implicitHeight
|
||||
ReplyComponent {
|
||||
id: replyComponent
|
||||
replyEventId: root.chatBarCache.replyId
|
||||
replyAuthor: root.chatBarCache.relationAuthor
|
||||
replyContentModel: ContentProvider.contentModelForEvent(root.Message.room, root.chatBarCache.replyId, true)
|
||||
Message.maxContentWidth: paneLoader.item.width
|
||||
}
|
||||
QQC2.Button {
|
||||
id: cancelButton
|
||||
|
||||
anchors.top: parent.top
|
||||
anchors.right: parent.right
|
||||
|
||||
display: QQC2.AbstractButton.IconOnly
|
||||
text: i18nc("@action:button", "Cancel reply")
|
||||
icon.name: "dialog-close"
|
||||
onClicked: {
|
||||
root.chatBarCache.replyId = "";
|
||||
root.chatBarCache.attachmentPath = "";
|
||||
}
|
||||
QQC2.ToolTip.text: text
|
||||
QQC2.ToolTip.visible: hovered
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
}
|
||||
}
|
||||
}
|
||||
Component {
|
||||
id: attachmentPane
|
||||
AttachmentPane {
|
||||
attachmentPath: root.chatBarCache.attachmentPath
|
||||
|
||||
onAttachmentCancelled: {
|
||||
root.chatBarCache.attachmentPath = "";
|
||||
root.forceActiveFocus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QtObject {
|
||||
id: _private
|
||||
|
||||
function updateText() {
|
||||
// This could possibly be undefined due to some esoteric QtQuick issue. Referencing it somewhere in JS is enough.
|
||||
documentHandler.document;
|
||||
if (chatBarCache?.isEditing && chatBarCache.relationMessage.length > 0) {
|
||||
textArea.text = chatBarCache.relationMessage;
|
||||
documentHandler.updateMentions(textArea.textDocument, chatBarCache.editId);
|
||||
textArea.forceActiveFocus();
|
||||
textArea.cursorPosition = textArea.text.length;
|
||||
}
|
||||
}
|
||||
|
||||
function post() {
|
||||
root.chatBarCache.postMessage();
|
||||
textArea.clear();
|
||||
root.chatBarCache.clearRelations();
|
||||
}
|
||||
}
|
||||
}
|
||||
194
src/messagecontent/CodeComponent.qml
Normal file
@@ -0,0 +1,194 @@
|
||||
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtQuick.Layouts
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
import org.kde.syntaxhighlighting
|
||||
|
||||
import org.kde.neochat
|
||||
|
||||
QQC2.Control {
|
||||
id: root
|
||||
|
||||
/**
|
||||
* @brief The matrix ID of the message event.
|
||||
*/
|
||||
required property string eventId
|
||||
|
||||
/**
|
||||
* @brief The message author.
|
||||
*
|
||||
* A Quotient::RoomMember object.
|
||||
*
|
||||
* @sa Quotient::RoomMember
|
||||
*/
|
||||
required property NeochatRoomMember author
|
||||
|
||||
/**
|
||||
* @brief The timestamp of the message.
|
||||
*/
|
||||
required property var time
|
||||
|
||||
/**
|
||||
* @brief The display text of the message.
|
||||
*/
|
||||
required property string display
|
||||
|
||||
/**
|
||||
* @brief The attributes of the component.
|
||||
*/
|
||||
required property var componentAttributes
|
||||
|
||||
/**
|
||||
* @brief The user selected text has changed.
|
||||
*/
|
||||
signal selectedTextChanged(string selectedText)
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
Layout.maximumWidth: Message.maxContentWidth
|
||||
Layout.maximumHeight: Kirigami.Units.gridUnit * 20
|
||||
|
||||
topPadding: 0
|
||||
bottomPadding: 0
|
||||
leftPadding: 0
|
||||
rightPadding: 0
|
||||
|
||||
contentItem: QQC2.ScrollView {
|
||||
id: codeScrollView
|
||||
contentWidth: root.Message.maxContentWidth
|
||||
|
||||
// HACK: Hide unnecessary horizontal scrollbar (https://bugreports.qt.io/browse/QTBUG-83890)
|
||||
QQC2.ScrollBar.horizontal.policy: QQC2.ScrollBar.AlwaysOff
|
||||
|
||||
QQC2.TextArea {
|
||||
id: codeText
|
||||
topPadding: Kirigami.Units.smallSpacing
|
||||
bottomPadding: Kirigami.Units.smallSpacing
|
||||
leftPadding: lineNumberColumn.width + lineNumberColumn.anchors.leftMargin + Kirigami.Units.smallSpacing * 2
|
||||
|
||||
text: root.display
|
||||
readOnly: true
|
||||
textFormat: TextEdit.PlainText
|
||||
wrapMode: TextEdit.Wrap
|
||||
color: Kirigami.Theme.textColor
|
||||
|
||||
font.family: "monospace"
|
||||
|
||||
Kirigami.SpellCheck.enabled: false
|
||||
|
||||
onWidthChanged: lineModel.resetModel()
|
||||
onHeightChanged: lineModel.resetModel()
|
||||
|
||||
onSelectedTextChanged: root.selectedTextChanged(selectedText)
|
||||
|
||||
SyntaxHighlighter {
|
||||
property string definitionName: Repository.definitionForName(root.componentAttributes.class).name
|
||||
textEdit: definitionName == "None" ? null : codeText
|
||||
definition: definitionName
|
||||
}
|
||||
ColumnLayout {
|
||||
id: lineNumberColumn
|
||||
anchors {
|
||||
top: codeText.top
|
||||
topMargin: codeText.topPadding
|
||||
left: codeText.left
|
||||
leftMargin: Kirigami.Units.smallSpacing
|
||||
}
|
||||
spacing: 0
|
||||
Repeater {
|
||||
id: repeater
|
||||
model: LineModel {
|
||||
id: lineModel
|
||||
document: codeText.textDocument
|
||||
}
|
||||
delegate: QQC2.Label {
|
||||
id: label
|
||||
required property int index
|
||||
required property int docLineHeight
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: docLineHeight
|
||||
horizontalAlignment: Text.AlignRight
|
||||
text: index + 1
|
||||
color: Kirigami.Theme.disabledTextColor
|
||||
|
||||
font.family: "monospace"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TapHandler {
|
||||
enabled: root.time.toString() !== "Invalid Date"
|
||||
acceptedButtons: Qt.LeftButton
|
||||
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad | PointerDevice.Stylus
|
||||
onTapped: RoomManager.maximizeCode(root.author, root.time, root.display, root.componentAttributes.class)
|
||||
}
|
||||
|
||||
TapHandler {
|
||||
acceptedDevices: PointerDevice.TouchScreen
|
||||
onLongPressed: RoomManager.viewEventMenu(root.eventId, root.Message.room, root.author, root.Message.selectedText, root.Message.hoveredLink);
|
||||
}
|
||||
|
||||
background: null
|
||||
}
|
||||
}
|
||||
|
||||
Kirigami.Separator {
|
||||
anchors {
|
||||
top: root.top
|
||||
bottom: root.bottom
|
||||
left: root.left
|
||||
leftMargin: lineNumberColumn.width + lineNumberColumn.anchors.leftMargin + Kirigami.Units.smallSpacing
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
anchors {
|
||||
top: parent.top
|
||||
topMargin: Kirigami.Units.smallSpacing
|
||||
right: parent.right
|
||||
rightMargin: (codeScrollView.QQC2.ScrollBar.vertical.visible ? codeScrollView.QQC2.ScrollBar.vertical.width : 0) + Kirigami.Units.smallSpacing
|
||||
}
|
||||
visible: root.hovered
|
||||
spacing: Kirigami.Units.mediumSpacing
|
||||
|
||||
QQC2.Button {
|
||||
icon.name: "edit-copy"
|
||||
text: i18n("Copy to clipboard")
|
||||
display: QQC2.AbstractButton.IconOnly
|
||||
|
||||
onClicked: Clipboard.saveText(root.display);
|
||||
|
||||
QQC2.ToolTip.text: text
|
||||
QQC2.ToolTip.visible: hovered
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
}
|
||||
QQC2.Button {
|
||||
visible: root.time.toString() !== "Invalid Date"
|
||||
icon.name: "view-fullscreen"
|
||||
text: i18nc("@action:button", "Maximize")
|
||||
display: QQC2.AbstractButton.IconOnly
|
||||
|
||||
onClicked: RoomManager.maximizeCode(root.author, root.time, root.display, root.componentAttributes.class);
|
||||
|
||||
QQC2.ToolTip.text: text
|
||||
QQC2.ToolTip.visible: hovered
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
}
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
color: Kirigami.Theme.backgroundColor
|
||||
Kirigami.Theme.colorSet: Kirigami.Theme.View
|
||||
Kirigami.Theme.inherit: false
|
||||
radius: Kirigami.Units.cornerRadius
|
||||
border {
|
||||
width: 1
|
||||
color: Kirigami.Theme.highlightColor
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
27
src/messagecontent/EncryptedComponent.qml
Normal file
@@ -0,0 +1,27 @@
|
||||
// SPDX-FileCopyrightText: 2021 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
|
||||
import org.kde.neochat
|
||||
|
||||
/**
|
||||
* @brief A component for an encrypted message that can't be decrypted.
|
||||
*/
|
||||
TextEdit {
|
||||
Layout.fillWidth: true
|
||||
Layout.maximumWidth: Message.maxContentWidth
|
||||
|
||||
text: i18n("This message is encrypted and the sender has not shared the key with this device.")
|
||||
color: Kirigami.Theme.disabledTextColor
|
||||
selectedTextColor: Kirigami.Theme.highlightedTextColor
|
||||
selectionColor: Kirigami.Theme.highlightColor
|
||||
font.pointSize: Kirigami.Theme.defaultFont.pointSize
|
||||
selectByMouse: !Kirigami.Settings.isMobile
|
||||
readOnly: true
|
||||
wrapMode: Text.WordWrap
|
||||
textFormat: Text.RichText
|
||||
}
|
||||
47
src/messagecontent/FetchButtonComponent.qml
Normal file
@@ -0,0 +1,47 @@
|
||||
// SPDX-FileCopyrightText: 2025 James Graham <james.h.graham@protonmail.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtQuick.Layouts
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
import org.kde.kirigamiaddons.delegates as Delegates
|
||||
|
||||
import org.kde.neochat
|
||||
import org.kde.neochat.chatbar
|
||||
|
||||
/**
|
||||
* @brief A component to show a reply button for threads in a message bubble.
|
||||
*/
|
||||
Delegates.RoundedItemDelegate {
|
||||
id: root
|
||||
|
||||
/**
|
||||
* @brief Request more events in the thread be loaded.
|
||||
*/
|
||||
signal fetchMoreEvents()
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.maximumWidth: Message.maxContentWidth
|
||||
|
||||
leftInset: 0
|
||||
rightInset: 0
|
||||
|
||||
highlighted: true
|
||||
|
||||
icon.name: "arrow-up"
|
||||
icon.width: Kirigami.Units.iconSizes.sizeForLabels
|
||||
icon.height: Kirigami.Units.iconSizes.sizeForLabels
|
||||
text: i18nc("@action:button", "Fetch More Events")
|
||||
|
||||
onClicked: {
|
||||
root.fetchMoreEvents()
|
||||
}
|
||||
|
||||
contentItem: Kirigami.Icon {
|
||||
implicitWidth: root.icon.width
|
||||
implicitHeight: root.icon.height
|
||||
source: root.icon.name
|
||||
}
|
||||
}
|
||||
200
src/messagecontent/FileComponent.qml
Normal file
@@ -0,0 +1,200 @@
|
||||
// SPDX-FileCopyrightText: 2018-2019 Black Hat <bhat@encom.eu.org>
|
||||
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
import QtCore as Core
|
||||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Dialogs as Dialogs
|
||||
import Qt.labs.qmlmodels
|
||||
|
||||
import org.kde.coreaddons
|
||||
import org.kde.kirigami as Kirigami
|
||||
|
||||
import org.kde.neochat
|
||||
|
||||
/**
|
||||
* @brief A component to show a file from a message.
|
||||
*/
|
||||
ColumnLayout {
|
||||
id: root
|
||||
|
||||
/**
|
||||
* @brief The matrix ID of the message event.
|
||||
*/
|
||||
required property string eventId
|
||||
|
||||
/**
|
||||
* @brief The media info for the event.
|
||||
*
|
||||
* This should consist of the following:
|
||||
* - source - The mxc URL for the media.
|
||||
* - mimeType - The MIME type of the media (should be image/xxx for this delegate).
|
||||
* - mimeIcon - The MIME icon name (should be image-xxx).
|
||||
* - size - The file size in bytes.
|
||||
* - width - The width in pixels of the audio media.
|
||||
* - height - The height in pixels of the audio media.
|
||||
* - tempInfo - mediaInfo (with the same properties as this except no tempInfo) for a temporary image while the file downloads.
|
||||
* - filename - original filename of the media
|
||||
*/
|
||||
required property var mediaInfo
|
||||
|
||||
/**
|
||||
* @brief FileTransferInfo for any downloading files.
|
||||
*/
|
||||
required property var fileTransferInfo
|
||||
|
||||
/**
|
||||
* @brief Whether the media has been downloaded.
|
||||
*/
|
||||
readonly property bool downloaded: root.fileTransferInfo && root.fileTransferInfo.completed
|
||||
onDownloadedChanged: {
|
||||
if (autoOpenFile) {
|
||||
openSavedFile();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Whether the file should be automatically opened when downloaded.
|
||||
*/
|
||||
property bool autoOpenFile: false
|
||||
|
||||
function saveFileAs() {
|
||||
const dialog = fileDialog.createObject(QQC2.Overlay.overlay);
|
||||
dialog.selectedFile = Message.room.fileNameToDownload(root.eventId);
|
||||
dialog.open();
|
||||
}
|
||||
|
||||
function openSavedFile() {
|
||||
UrlHelper.openUrl(root.fileTransferInfo.localPath);
|
||||
}
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.maximumWidth: Message.maxContentWidth
|
||||
spacing: Kirigami.Units.largeSpacing
|
||||
|
||||
RowLayout {
|
||||
spacing: Kirigami.Units.largeSpacing
|
||||
|
||||
states: [
|
||||
State {
|
||||
name: "downloadedInstant"
|
||||
when: root.fileTransferInfo.completed && autoOpenFile
|
||||
|
||||
PropertyChanges {
|
||||
target: openButton
|
||||
icon.name: "document-open"
|
||||
onClicked: openSavedFile()
|
||||
}
|
||||
|
||||
PropertyChanges {
|
||||
target: downloadButton
|
||||
icon.name: "download"
|
||||
QQC2.ToolTip.text: i18nc("tooltip for a button on a message; offers ability to download its file", "Download")
|
||||
onClicked: saveFileAs()
|
||||
}
|
||||
},
|
||||
State {
|
||||
name: "downloaded"
|
||||
when: root.fileTransferInfo.completed && !autoOpenFile
|
||||
|
||||
PropertyChanges {
|
||||
target: openButton
|
||||
visible: false
|
||||
}
|
||||
|
||||
PropertyChanges {
|
||||
target: downloadButton
|
||||
icon.name: "document-open"
|
||||
QQC2.ToolTip.text: i18nc("tooltip for a button on a message; offers ability to open its downloaded file with an appropriate application", "Open File")
|
||||
onClicked: openSavedFile()
|
||||
}
|
||||
},
|
||||
State {
|
||||
name: "downloading"
|
||||
when: root.fileTransferInfo.active
|
||||
|
||||
PropertyChanges {
|
||||
target: openButton
|
||||
visible: false
|
||||
}
|
||||
|
||||
PropertyChanges {
|
||||
target: sizeLabel
|
||||
text: i18nc("file download progress", "%1 / %2", Format.formatByteSize(root.fileTransferInfo.progress), Format.formatByteSize(root.fileTransferInfo.total))
|
||||
}
|
||||
PropertyChanges {
|
||||
target: downloadButton
|
||||
icon.name: "media-playback-stop"
|
||||
QQC2.ToolTip.text: i18nc("tooltip for a button on a message; stops downloading the message's file", "Stop Download")
|
||||
onClicked: Message.room.cancelFileTransfer(root.eventId)
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
Kirigami.Icon {
|
||||
source: root.mediaInfo.mimeIcon
|
||||
fallback: "unknown"
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
spacing: 0
|
||||
QQC2.Label {
|
||||
Layout.fillWidth: true
|
||||
text: root.mediaInfo.filename
|
||||
wrapMode: Text.Wrap
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
QQC2.Label {
|
||||
id: sizeLabel
|
||||
Layout.fillWidth: true
|
||||
text: Format.formatByteSize(root.mediaInfo.size)
|
||||
opacity: 0.7
|
||||
elide: Text.ElideRight
|
||||
maximumLineCount: 1
|
||||
}
|
||||
}
|
||||
|
||||
QQC2.Button {
|
||||
id: openButton
|
||||
icon.name: "document-open"
|
||||
onClicked: {
|
||||
autoOpenFile = true;
|
||||
root.Message.room.downloadTempFile(root.eventId);
|
||||
}
|
||||
|
||||
QQC2.ToolTip.text: i18nc("tooltip for a button on a message; offers ability to open its downloaded file with an appropriate application", "Open File")
|
||||
QQC2.ToolTip.visible: hovered
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
}
|
||||
|
||||
QQC2.Button {
|
||||
id: downloadButton
|
||||
icon.name: "download"
|
||||
onClicked: root.saveFileAs()
|
||||
|
||||
QQC2.ToolTip.text: i18nc("tooltip for a button on a message; offers ability to download its file", "Download")
|
||||
QQC2.ToolTip.visible: hovered
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
}
|
||||
|
||||
Component {
|
||||
id: fileDialog
|
||||
|
||||
Dialogs.FileDialog {
|
||||
fileMode: Dialogs.FileDialog.SaveFile
|
||||
currentFolder: NeoChatConfig.lastSaveDirectory.length > 0 ? NeoChatConfig.lastSaveDirectory : Core.StandardPaths.writableLocation(Core.StandardPaths.DownloadLocation)
|
||||
onAccepted: {
|
||||
NeoChatConfig.lastSaveDirectory = currentFolder;
|
||||
NeoChatConfig.save();
|
||||
if (autoOpenFile) {
|
||||
UrlHelper.copyTo(root.fileTransferInfo.localPath, selectedFile);
|
||||
} else {
|
||||
root.Message.room.download(root.eventId, selectedFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
101
src/messagecontent/FlightReservationComponent.qml
Normal file
@@ -0,0 +1,101 @@
|
||||
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Controls as QQC2
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
import org.kde.kirigamiaddons.formcard as FormCard
|
||||
|
||||
/**
|
||||
* @brief A component for a flight itinerary reservation.
|
||||
*/
|
||||
ItineraryReservationComponent {
|
||||
id: root
|
||||
|
||||
/**
|
||||
* @brief The name of the reservation.
|
||||
*/
|
||||
required property string name
|
||||
|
||||
/**
|
||||
* @brief The boarding time of the flight.
|
||||
*
|
||||
* Includes date.
|
||||
*/
|
||||
required property string startTime
|
||||
|
||||
/**
|
||||
* @brief The departure airport of the flight.
|
||||
*/
|
||||
required property string departureLocation
|
||||
|
||||
/**
|
||||
* @brief The address of the departure airport of the flight.
|
||||
*/
|
||||
required property string departureAddress
|
||||
|
||||
/**
|
||||
* @brief The arrival airport of the flight.
|
||||
*/
|
||||
required property string arrivalLocation
|
||||
|
||||
/**
|
||||
* @brief The address of the arrival airport of the flight.
|
||||
*/
|
||||
required property string arrivalAddress
|
||||
|
||||
headerItem: RowLayout {
|
||||
TransportIcon {
|
||||
source: "qrc:/qt/qml/org/kde/neochat/timeline/images/flight.svg"
|
||||
isMask: true
|
||||
size: Kirigami.Units.iconSizes.smallMedium
|
||||
}
|
||||
QQC2.Label {
|
||||
Layout.fillWidth: true
|
||||
text: root.name
|
||||
elide: Text.ElideRight
|
||||
|
||||
Accessible.ignored: true
|
||||
}
|
||||
QQC2.Label {
|
||||
text: root.startTime
|
||||
}
|
||||
}
|
||||
|
||||
contentItem: ColumnLayout {
|
||||
id: topLayout
|
||||
spacing: Kirigami.Units.smallSpacing
|
||||
|
||||
QQC2.Label {
|
||||
Layout.fillWidth: true
|
||||
text: i18nc("flight departure, %1 is airport, %2 is time", "Departure from %1",
|
||||
root.departureLocation)
|
||||
color: Kirigami.Theme.textColor
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
QQC2.Label {
|
||||
Layout.fillWidth: true
|
||||
visible: text !== ""
|
||||
text: root.departureAddress
|
||||
}
|
||||
|
||||
Kirigami.Separator {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
QQC2.Label {
|
||||
Layout.fillWidth: true
|
||||
text: i18nc("flight arrival, %1 is airport, %2 is time", "Arrival at %1",
|
||||
root.arrivalLocation)
|
||||
color: Kirigami.Theme.textColor
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
QQC2.Label {
|
||||
Layout.fillWidth: true
|
||||
visible: text !== ""
|
||||
text: root.arrivalAddress
|
||||
}
|
||||
}
|
||||
}
|
||||
57
src/messagecontent/FoodReservationComponent.qml
Normal file
@@ -0,0 +1,57 @@
|
||||
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Controls as QQC2
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
import org.kde.kirigamiaddons.formcard as FormCard
|
||||
|
||||
/**
|
||||
* @brief A component for a food itinerary reservation.
|
||||
*/
|
||||
ItineraryReservationComponent {
|
||||
id: root
|
||||
|
||||
/**
|
||||
* @brief The name of the reservation.
|
||||
*/
|
||||
required property string name
|
||||
|
||||
/**
|
||||
* @brief The start time of the reservation.
|
||||
*
|
||||
* Includes date.
|
||||
*/
|
||||
required property string startTime
|
||||
|
||||
/**
|
||||
* @brief The address of the hotel.
|
||||
*/
|
||||
required property string address
|
||||
|
||||
headerItem: RowLayout {
|
||||
TransportIcon {
|
||||
source: "qrc:/qt/qml/org/kde/neochat/timeline/images/foodestablishment.svg"
|
||||
isMask: true
|
||||
size: Kirigami.Units.iconSizes.smallMedium
|
||||
}
|
||||
QQC2.Label {
|
||||
Layout.fillWidth: true
|
||||
text: root.name
|
||||
elide: Text.ElideRight
|
||||
|
||||
Accessible.ignored: true
|
||||
}
|
||||
QQC2.Label {
|
||||
text: root.startTime
|
||||
}
|
||||
}
|
||||
|
||||
contentItem: QQC2.Label {
|
||||
visible: text !== ""
|
||||
text: root.address
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
}
|
||||
74
src/messagecontent/HotelReservationComponent.qml
Normal file
@@ -0,0 +1,74 @@
|
||||
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Controls as QQC2
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
import org.kde.kirigamiaddons.formcard as FormCard
|
||||
|
||||
/**
|
||||
* @brief A component for a hotel itinerary reservation.
|
||||
*/
|
||||
ItineraryReservationComponent {
|
||||
id: root
|
||||
|
||||
/**
|
||||
* @brief The name of the reservation.
|
||||
*/
|
||||
required property string name
|
||||
|
||||
/**
|
||||
* @brief The check-in time at the hotel.
|
||||
*
|
||||
* Includes date.
|
||||
*/
|
||||
required property string startTime
|
||||
|
||||
/**
|
||||
* @brief The check-out time at the hotel.
|
||||
*
|
||||
* Includes date.
|
||||
*/
|
||||
required property string endTime
|
||||
|
||||
/**
|
||||
* @brief The address of the hotel.
|
||||
*/
|
||||
required property string address
|
||||
|
||||
headerItem: RowLayout {
|
||||
TransportIcon {
|
||||
source: "go-home-symbolic"
|
||||
isMask: true
|
||||
size: Kirigami.Units.iconSizes.smallMedium
|
||||
}
|
||||
QQC2.Label {
|
||||
text: root.name
|
||||
elide: Text.ElideRight
|
||||
|
||||
Accessible.ignored: true
|
||||
}
|
||||
}
|
||||
|
||||
contentItem: ColumnLayout {
|
||||
spacing: Kirigami.Units.smallSpacing
|
||||
|
||||
QQC2.Label {
|
||||
Layout.fillWidth: true
|
||||
visible: text !== ""
|
||||
text: root.address
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
QQC2.Label {
|
||||
text: i18n("Check-in time: %1", root.startTime)
|
||||
color: Kirigami.Theme.textColor
|
||||
}
|
||||
QQC2.Label {
|
||||
text: i18n("Check-out time: %1", root.endTime)
|
||||
color: Kirigami.Theme.textColor
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
204
src/messagecontent/ImageComponent.qml
Normal file
@@ -0,0 +1,204 @@
|
||||
// SPDX-FileCopyrightText: 2018-2020 Black Hat <bhat@encom.eu.org>
|
||||
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtQuick.Layouts
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
|
||||
import org.kde.neochat
|
||||
|
||||
/**
|
||||
* @brief A component to show the image from a message.
|
||||
*/
|
||||
Item {
|
||||
id: root
|
||||
|
||||
/**
|
||||
* @brief The matrix ID of the message event.
|
||||
*/
|
||||
required property string eventId
|
||||
|
||||
/**
|
||||
* @brief The display text of the message.
|
||||
*/
|
||||
required property string display
|
||||
|
||||
/**
|
||||
* @brief The media info for the event.
|
||||
*
|
||||
* This should consist of the following:
|
||||
* - source - The mxc URL for the media.
|
||||
* - mimeType - The MIME type of the media (should be image/xxx for this delegate).
|
||||
* - mimeIcon - The MIME icon name (should be image-xxx).
|
||||
* - size - The file size in bytes.
|
||||
* - width - The width in pixels of the audio media.
|
||||
* - height - The height in pixels of the audio media.
|
||||
* - tempInfo - mediaInfo (with the same properties as this except no tempInfo) for a temporary image while the file downloads.
|
||||
* - isSticker - Whether the image is a sticker or not
|
||||
*/
|
||||
required property var mediaInfo
|
||||
|
||||
/**
|
||||
* @brief FileTransferInfo for any downloading files.
|
||||
*/
|
||||
required property var fileTransferInfo
|
||||
|
||||
/**
|
||||
* The maximum height of the image. Can be left undefined most of the times. Passed to MediaSizeHelper::contentMaxHeight.
|
||||
*/
|
||||
property var contentMaxHeight: undefined
|
||||
|
||||
implicitWidth: mediaSizeHelper.currentSize.width
|
||||
implicitHeight: mediaSizeHelper.currentSize.height
|
||||
|
||||
QQC2.Button {
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
visible: !_private.hideImage
|
||||
icon.name: "view-hidden"
|
||||
text: i18nc("@action:button", "Hide Image")
|
||||
display: QQC2.Button.IconOnly
|
||||
z: 10
|
||||
onClicked: {
|
||||
_private.hideImage = true;
|
||||
Controller.markImageHidden(root.eventId)
|
||||
}
|
||||
|
||||
QQC2.ToolTip.text: text
|
||||
QQC2.ToolTip.visible: hovered
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: imageLoader
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
active: !root.mediaInfo.animated && !_private.hideImage
|
||||
sourceComponent: Image {
|
||||
source: root.mediaInfo.source
|
||||
sourceSize.width: mediaSizeHelper.currentSize.width * Screen.devicePixelRatio
|
||||
sourceSize.height: mediaSizeHelper.currentSize.height * Screen.devicePixelRatio
|
||||
|
||||
fillMode: Image.PreserveAspectFit
|
||||
autoTransform: true
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: animatedImageLoader
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
active: (root?.mediaInfo.animated ?? false) && !_private.hideImage
|
||||
sourceComponent: AnimatedImage {
|
||||
source: root.mediaInfo.source
|
||||
|
||||
fillMode: Image.PreserveAspectFit
|
||||
autoTransform: true
|
||||
|
||||
paused: !applicationWindow().active
|
||||
}
|
||||
}
|
||||
|
||||
Image {
|
||||
anchors.fill: parent
|
||||
source: visible ? (root?.mediaInfo.tempInfo?.source ?? "") : ""
|
||||
visible: _private.imageItem && _private.imageItem.status !== Image.Ready && !_private.hideImage
|
||||
}
|
||||
|
||||
QQC2.ToolTip.text: root.display
|
||||
QQC2.ToolTip.visible: hoverHandler.hovered
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
|
||||
HoverHandler {
|
||||
id: hoverHandler
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
|
||||
visible: _private.imageItem.status !== Image.Ready || _private.hideImage
|
||||
|
||||
color: "#BB000000"
|
||||
|
||||
QQC2.ProgressBar {
|
||||
anchors.centerIn: parent
|
||||
|
||||
width: parent.width * 0.8
|
||||
visible: !_private.hideImage
|
||||
|
||||
from: 0
|
||||
to: 1.0
|
||||
value: _private.imageItem.progress
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
QQC2.Button {
|
||||
anchors.centerIn: parent
|
||||
text: i18nc("@action:button", "Show Image")
|
||||
visible: _private.hideImage
|
||||
onClicked: {
|
||||
_private.hideImage = false;
|
||||
Controller.markImageShown(root.eventId);
|
||||
}
|
||||
}
|
||||
|
||||
TapHandler {
|
||||
acceptedButtons: Qt.LeftButton
|
||||
gesturePolicy: TapHandler.ReleaseWithinBounds | TapHandler.WithinBounds
|
||||
onTapped: {
|
||||
root.QQC2.ToolTip.hide();
|
||||
if (root.mediaInfo.animated) {
|
||||
_private.imageItem.paused = true;
|
||||
}
|
||||
root.Message.timeline.interactive = false;
|
||||
if (!root.mediaInfo.isSticker) {
|
||||
// We need to make sure the index is that of the MediaMessageFilterModel.
|
||||
if (root.Message.timeline.model instanceof MessageFilterModel) {
|
||||
RoomManager.maximizeMedia(RoomManager.mediaMessageFilterModel.getRowForSourceItem(root.Message.index));
|
||||
} else {
|
||||
RoomManager.maximizeMedia(root.Message.index);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function downloadAndOpen() {
|
||||
if (_private.downloaded) {
|
||||
openSavedFile();
|
||||
} else {
|
||||
openOnFinished = true;
|
||||
Message.room.downloadFile(root.eventId, StandardPaths.writableLocation(StandardPaths.CacheLocation) + "/" + root.eventId.replace(":", "_").replace("/", "_").replace("+", "_") + Message.room.fileNameToDownload(root.eventId));
|
||||
}
|
||||
}
|
||||
|
||||
function openSavedFile() {
|
||||
if (UrlHelper.openUrl(root.fileTransferInfo.localPath))
|
||||
return;
|
||||
if (UrlHelper.openUrl(root.fileTransferInfo.localDir))
|
||||
return;
|
||||
}
|
||||
|
||||
MediaSizeHelper {
|
||||
id: mediaSizeHelper
|
||||
contentMaxWidth: root.Message.maxContentWidth
|
||||
contentMaxHeight: root.contentMaxHeight ?? -1
|
||||
mediaWidth: root?.mediaInfo.isSticker ? 256 : (root?.mediaInfo.width ?? 0)
|
||||
mediaHeight: root?.mediaInfo.isSticker ? 256 : (root?.mediaInfo.height ?? 0)
|
||||
}
|
||||
|
||||
QtObject {
|
||||
id: _private
|
||||
readonly property var imageItem: root.mediaInfo.animated ? animatedImageLoader.item : imageLoader.item
|
||||
|
||||
// The space available for the component after taking away the border
|
||||
readonly property real downloaded: root.fileTransferInfo && root.fileTransferInfo.completed
|
||||
|
||||
property bool hideImage: NeoChatConfig.hideImages && !Controller.isImageShown(root.eventId)
|
||||
}
|
||||
}
|
||||
61
src/messagecontent/ItineraryComponent.qml
Normal file
@@ -0,0 +1,61 @@
|
||||
// SPDX-FileCopyrightText: 2024 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtQuick.Layouts
|
||||
import Qt.labs.qmlmodels
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
|
||||
import org.kde.neochat
|
||||
|
||||
/**
|
||||
* @brief A component to show a preview of a file that can integrate with KDE itinerary.
|
||||
*/
|
||||
ColumnLayout {
|
||||
id: root
|
||||
|
||||
/**
|
||||
* @brief A model with the itinerary preview of the file.
|
||||
*/
|
||||
required property var itineraryModel
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.maximumWidth: Message.maxContentWidth
|
||||
spacing: Kirigami.Units.largeSpacing
|
||||
|
||||
Repeater {
|
||||
id: itinerary
|
||||
model: root.itineraryModel
|
||||
delegate: DelegateChooser {
|
||||
role: "type"
|
||||
DelegateChoice {
|
||||
roleValue: "TrainReservation"
|
||||
delegate: TrainReservationComponent {}
|
||||
}
|
||||
DelegateChoice {
|
||||
roleValue: "LodgingReservation"
|
||||
delegate: HotelReservationComponent {}
|
||||
}
|
||||
DelegateChoice {
|
||||
roleValue: "FoodEstablishmentReservation"
|
||||
delegate: FoodReservationComponent {}
|
||||
}
|
||||
DelegateChoice {
|
||||
roleValue: "FlightReservation"
|
||||
delegate: FlightReservationComponent {}
|
||||
}
|
||||
}
|
||||
}
|
||||
QQC2.Button {
|
||||
icon.name: "map-globe"
|
||||
text: i18nc("@action", "Send to KDE Itinerary")
|
||||
QQC2.ToolTip.visible: hovered
|
||||
QQC2.ToolTip.text: text
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
onClicked: itineraryModel.sendToItinerary()
|
||||
visible: itinerary.count > 0
|
||||
}
|
||||
}
|
||||
46
src/messagecontent/ItineraryReservationComponent.qml
Normal file
@@ -0,0 +1,46 @@
|
||||
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
import org.kde.kirigamiaddons.formcard as FormCard
|
||||
|
||||
/**
|
||||
* @brief A base component for an itinerary reservation.
|
||||
*/
|
||||
FormCard.FormCard {
|
||||
id: root
|
||||
|
||||
/**
|
||||
* @brief An item with the header content.
|
||||
*/
|
||||
property alias headerItem: headerDelegate.contentItem
|
||||
|
||||
/**
|
||||
* @brief An item with the main body content.
|
||||
*/
|
||||
property alias contentItem: content.contentItem
|
||||
|
||||
Layout.fillWidth: true
|
||||
implicitWidth: Math.max(headerDelegate.implicitWidth, content.implicitWidth)
|
||||
|
||||
Component.onCompleted: children[0].radius = Kirigami.Units.cornerRadius
|
||||
|
||||
FormCard.AbstractFormDelegate {
|
||||
id: headerDelegate
|
||||
Layout.fillWidth: true
|
||||
background: Rectangle {
|
||||
color: Kirigami.Theme.backgroundColor
|
||||
Kirigami.Theme.colorSet: Kirigami.Theme.Header
|
||||
Kirigami.Theme.inherit: false
|
||||
}
|
||||
}
|
||||
FormCard.AbstractFormDelegate {
|
||||
id: content
|
||||
Layout.fillWidth: true
|
||||
background: null
|
||||
}
|
||||
}
|
||||
|
||||
80
src/messagecontent/JourneySectionStopDelegateLineSegment.qml
Normal file
@@ -0,0 +1,80 @@
|
||||
// SPDX-FileCopyrightText: 2020 Volker Krause <vkrause@kde.org>
|
||||
// SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Controls as QQC2
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
|
||||
/**
|
||||
* @brief Line segment drawn on the left in the journey section view.
|
||||
*
|
||||
* Can be used in a fully drawn form, or partially drawn as a progress
|
||||
* overlay over the full variant.
|
||||
*/
|
||||
Item {
|
||||
id: root
|
||||
|
||||
/**
|
||||
* @brief Whether the segment is representing the start of the journey.
|
||||
*
|
||||
* I.e. this will show a segment rounded at the top with a stop marker.
|
||||
*/
|
||||
property bool isDeparture: false
|
||||
|
||||
/**
|
||||
* @brief Whether the segment is representing the end of the journey.
|
||||
*
|
||||
* I.e. this will show a segment rounded at the bottom with a stop marker.
|
||||
*/
|
||||
property bool isArrival: false
|
||||
|
||||
/**
|
||||
* @brief The color of the segment.
|
||||
*/
|
||||
property color lineColor: Kirigami.Theme.textColor
|
||||
|
||||
/**
|
||||
* @brief The width of the segment.
|
||||
*/
|
||||
property int lineWidth: Kirigami.Units.smallSpacing *4
|
||||
|
||||
implicitWidth: root.lineWidth * 2
|
||||
clip: true
|
||||
|
||||
Kirigami.ShadowedRectangle {
|
||||
id: line
|
||||
x: root.lineWidth / 2
|
||||
y: isDeparture? parent.height-height:0
|
||||
width: root.lineWidth
|
||||
color: root.lineColor
|
||||
|
||||
corners {
|
||||
topRightRadius: isDeparture ? Math.round(width / 2) : 0
|
||||
topLeftRadius: isDeparture ? Math.round(width / 2) : 0
|
||||
|
||||
bottomRightRadius: isArrival ? Math.round(width / 2) : 0
|
||||
bottomLeftRadius: isArrival ? Math.round(width / 2) : 0
|
||||
}
|
||||
height:
|
||||
if (isArrival) {
|
||||
Math.round(parent.height / 2) + root.lineWidth / 2
|
||||
} else if (isDeparture) {
|
||||
Math.round(parent.height / 2) + root.lineWidth / 2
|
||||
} else {
|
||||
parent.height
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: stopDot
|
||||
x: line.x + (line.width - width) / 2
|
||||
y: parent.height / 2 - width / 2
|
||||
radius: width / 2
|
||||
width: root.lineWidth * 0.6
|
||||
height: width
|
||||
color: Kirigami.Theme.backgroundColor
|
||||
visible: root.isArrival || root.isDeparture
|
||||
}
|
||||
}
|
||||
161
src/messagecontent/LinkPreviewComponent.qml
Normal file
@@ -0,0 +1,161 @@
|
||||
// SPDX-FileCopyrightText: 2022 Bharadwaj Raju <bharadwaj.raju777@protonmail.com>
|
||||
// SPDX-FileCopyrightText: 2023-2024 James Graham <james.h.graham@protonmail.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-or-later OR LicenseRef-KDE-Accepted-GPL
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtQuick.Layouts
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
|
||||
import org.kde.neochat
|
||||
|
||||
/**
|
||||
* @brief A component to show a link preview from a message.
|
||||
*/
|
||||
QQC2.Control {
|
||||
id: root
|
||||
|
||||
/**
|
||||
* @brief The index of the delegate in the model.
|
||||
*/
|
||||
required property int index
|
||||
|
||||
/**
|
||||
* @brief The link preview properties.
|
||||
*
|
||||
* This is a list or object containing the following:
|
||||
* - url - The URL being previewed.
|
||||
* - loaded - Whether the URL preview has been loaded.
|
||||
* - title - the title of the URL preview.
|
||||
* - description - the description of the URL preview.
|
||||
* - imageSource - a source URL for the preview image.
|
||||
*/
|
||||
required property LinkPreviewer linkPreviewer
|
||||
|
||||
/**
|
||||
* @brief Standard height for the link preview.
|
||||
*
|
||||
* When the content of the link preview is larger than this it will be
|
||||
* elided/hidden until maximized.
|
||||
*/
|
||||
property var defaultHeight: Kirigami.Units.gridUnit * 3 + Kirigami.Units.largeSpacing * 2
|
||||
|
||||
property bool truncated: linkPreviewDescription.truncated || !linkPreviewDescription.visible
|
||||
|
||||
/**
|
||||
* @brief Request for this delegate to be removed.
|
||||
*/
|
||||
signal remove(int index)
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.maximumWidth: Message.maxContentWidth
|
||||
Layout.minimumHeight: root.defaultHeight
|
||||
|
||||
leftPadding: 0
|
||||
rightPadding: 0
|
||||
topPadding: 0
|
||||
bottomPadding: 0
|
||||
|
||||
contentItem: RowLayout {
|
||||
id: contentRow
|
||||
spacing: Kirigami.Units.smallSpacing
|
||||
|
||||
Rectangle {
|
||||
id: separator
|
||||
Layout.fillHeight: true
|
||||
width: Kirigami.Units.smallSpacing
|
||||
color: Kirigami.Theme.highlightColor
|
||||
radius: Kirigami.Units.cornerRadius
|
||||
}
|
||||
Image {
|
||||
id: previewImage
|
||||
Layout.preferredWidth: root.defaultHeight
|
||||
Layout.preferredHeight: root.defaultHeight
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
visible: root.linkPreviewer.imageSource.toString().length > 0
|
||||
source: root.linkPreviewer.imageSource
|
||||
fillMode: Image.PreserveAspectFit
|
||||
}
|
||||
ColumnLayout {
|
||||
id: column
|
||||
implicitWidth: Math.max(linkPreviewTitle.implicitWidth, linkPreviewDescription.implicitWidth)
|
||||
spacing: Kirigami.Units.smallSpacing
|
||||
visible: root.linkPreviewer.title.length > 0 || root.linkPreviewer.description.length > 0
|
||||
Kirigami.Heading {
|
||||
id: linkPreviewTitle
|
||||
Layout.fillWidth: true
|
||||
level: 3
|
||||
wrapMode: Text.Wrap
|
||||
textFormat: Text.RichText
|
||||
text: "<style>
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
</style>
|
||||
<a href=\"" + root.linkPreviewer.url + "\">" + (maximizeButton.checked ? root.linkPreviewer.title : titleTextMetrics.elidedText).replace("–", "—") + "</a>"
|
||||
|
||||
TextMetrics {
|
||||
id: titleTextMetrics
|
||||
text: root.linkPreviewer.title
|
||||
font: linkPreviewTitle.font
|
||||
elide: Text.ElideRight
|
||||
elideWidth: linkPreviewTitle.width
|
||||
}
|
||||
}
|
||||
QQC2.Label {
|
||||
id: linkPreviewDescription
|
||||
Layout.fillWidth: true
|
||||
Layout.maximumHeight: maximizeButton.checked ? -1 : root.defaultHeight - linkPreviewTitle.height - column.spacing
|
||||
visible: linkPreviewTitle.height + column.spacing + font.pointSize <= root.defaultHeight || maximizeButton.checked
|
||||
text: linkPreviewer.description
|
||||
wrapMode: Text.Wrap
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
|
||||
TapHandler {
|
||||
acceptedButtons: Qt.LeftButton
|
||||
onTapped: RoomManager.resolveResource(root.linkPreviewer.url, "join")
|
||||
}
|
||||
}
|
||||
|
||||
HoverHandler {
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
}
|
||||
|
||||
QQC2.Button {
|
||||
id: closeButton
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
visible: root.hovered
|
||||
text: i18nc("As in remove the link preview so it's no longer shown", "Remove preview")
|
||||
icon.name: "dialog-close"
|
||||
display: QQC2.AbstractButton.IconOnly
|
||||
|
||||
onClicked: root.remove(root.index)
|
||||
|
||||
QQC2.ToolTip {
|
||||
text: closeButton.text
|
||||
visible: closeButton.hovered
|
||||
delay: Kirigami.Units.toolTipDelay
|
||||
}
|
||||
}
|
||||
QQC2.Button {
|
||||
id: maximizeButton
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
visible: root.hovered && (root.truncated || checked)
|
||||
checkable: true
|
||||
text: checked ? i18n("Shrink preview") : i18n("Expand preview")
|
||||
icon.name: checked ? "go-up" : "go-down"
|
||||
display: QQC2.AbstractButton.IconOnly
|
||||
|
||||
QQC2.ToolTip {
|
||||
text: maximizeButton.text
|
||||
visible: maximizeButton.hovered
|
||||
delay: Kirigami.Units.toolTipDelay
|
||||
}
|
||||
}
|
||||
}
|
||||
88
src/messagecontent/LinkPreviewLoadComponent.qml
Normal file
@@ -0,0 +1,88 @@
|
||||
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-or-later OR LicenseRef-KDE-Accepted-GPL
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtQuick.Layouts
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
|
||||
import org.kde.neochat
|
||||
|
||||
/**
|
||||
* @brief A component to show a link preview loading from a message.
|
||||
*/
|
||||
QQC2.Control {
|
||||
id: root
|
||||
|
||||
/**
|
||||
* @brief The index of the delegate in the model.
|
||||
*/
|
||||
required property int index
|
||||
|
||||
required property int type
|
||||
|
||||
/**
|
||||
* @brief Standard height for the link preview.
|
||||
*
|
||||
* When the content of the link preview is larger than this it will be
|
||||
* elided/hidden until maximized.
|
||||
*/
|
||||
property var defaultHeight: Kirigami.Units.gridUnit * 3 + Kirigami.Units.smallSpacing * 2
|
||||
|
||||
/**
|
||||
* @brief Request for this delegate to be removed.
|
||||
*/
|
||||
signal remove(int index)
|
||||
|
||||
enum Type {
|
||||
Reply,
|
||||
LinkPreview
|
||||
}
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.maximumWidth: Message.maxContentWidth
|
||||
|
||||
contentItem : RowLayout {
|
||||
spacing: Kirigami.Units.smallSpacing
|
||||
|
||||
Rectangle {
|
||||
Layout.fillHeight: true
|
||||
width: Kirigami.Units.smallSpacing
|
||||
color: Kirigami.Theme.highlightColor
|
||||
}
|
||||
QQC2.BusyIndicator {}
|
||||
Kirigami.Heading {
|
||||
Layout.fillWidth: true
|
||||
Layout.minimumHeight: root.defaultHeight
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
level: 2
|
||||
text: {
|
||||
switch (root.type) {
|
||||
case LinkPreviewLoadComponent.Reply:
|
||||
return i18n("Loading reply");
|
||||
case LinkPreviewLoadComponent.LinkPreview:
|
||||
return i18n("Loading URL preview");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QQC2.Button {
|
||||
id: closeButton
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
visible: root.hovered && root.type === LinkPreviewLoadComponent.LinkPreview
|
||||
text: i18nc("As in remove the link preview so it's no longer shown", "Remove preview")
|
||||
icon.name: "dialog-close"
|
||||
display: QQC2.AbstractButton.IconOnly
|
||||
|
||||
onClicked: root.remove(root.index)
|
||||
|
||||
QQC2.ToolTip {
|
||||
text: closeButton.text
|
||||
visible: closeButton.hovered
|
||||
delay: Kirigami.Units.toolTipDelay
|
||||
}
|
||||
}
|
||||
}
|
||||
87
src/messagecontent/LiveLocationComponent.qml
Normal file
@@ -0,0 +1,87 @@
|
||||
// SPDX-FileCopyrightText: 2021 Tobias Fella <fella@posteo.de>
|
||||
// SPDX-FileCopyrightText: 2023 Volker Krause <vkrause@kde.org>
|
||||
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import QtLocation
|
||||
import QtPositioning
|
||||
|
||||
import org.kde.neochat
|
||||
|
||||
/**
|
||||
* @brief A component to show a live location from a message.
|
||||
*/
|
||||
ColumnLayout {
|
||||
id: root
|
||||
|
||||
/**
|
||||
* @brief The matrix ID of the message event.
|
||||
*/
|
||||
required property string eventId
|
||||
|
||||
/**
|
||||
* @brief The display text of the message.
|
||||
*/
|
||||
required property string display
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.maximumWidth: Message.maxContentWidth
|
||||
|
||||
LiveLocationsModel {
|
||||
id: liveLocationModel
|
||||
eventId: root.eventId
|
||||
room: Message.room
|
||||
}
|
||||
MapView {
|
||||
id: mapView
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredWidth: root.Message.maxContentWidth
|
||||
Layout.preferredHeight: root.Message.maxContentWidth / 16 * 9
|
||||
|
||||
map.center: QtPositioning.coordinate(liveLocationModel.boundingBox.y, liveLocationModel.boundingBox.x)
|
||||
map.zoomLevel: 15
|
||||
|
||||
map.plugin: OsmLocationPlugin.plugin
|
||||
|
||||
MapItemView {
|
||||
model: liveLocationModel
|
||||
delegate: LocationMapItem {}
|
||||
}
|
||||
|
||||
TapHandler {
|
||||
acceptedButtons: Qt.LeftButton
|
||||
onTapped: {
|
||||
let map = fullScreenMap.createObject(parent, {
|
||||
liveLocationModel: liveLocationModel
|
||||
});
|
||||
map.open();
|
||||
}
|
||||
}
|
||||
TapHandler {
|
||||
acceptedDevices: PointerDevice.TouchScreen
|
||||
onLongPressed: openMessageContext("")
|
||||
}
|
||||
TapHandler {
|
||||
acceptedButtons: Qt.RightButton
|
||||
onTapped: openMessageContext("")
|
||||
}
|
||||
Connections {
|
||||
target: mapView.map
|
||||
function onCopyrightLinkActivated() {
|
||||
Qt.openUrlExternally(link);
|
||||
}
|
||||
}
|
||||
}
|
||||
Component {
|
||||
id: fullScreenMap
|
||||
FullScreenMap {}
|
||||
}
|
||||
|
||||
TextComponent {
|
||||
display: root.display
|
||||
visible: root.display !== ""
|
||||
}
|
||||
}
|
||||
33
src/messagecontent/LoadComponent.qml
Normal file
@@ -0,0 +1,33 @@
|
||||
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-or-later OR LicenseRef-KDE-Accepted-GPL
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtQuick.Layouts
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
|
||||
import org.kde.neochat
|
||||
|
||||
/**
|
||||
* @brief A component to show that part of a message is loading.
|
||||
*/
|
||||
RowLayout {
|
||||
id: root
|
||||
|
||||
required property string display
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.maximumWidth: Message.maxContentWidth
|
||||
spacing: Kirigami.Units.smallSpacing
|
||||
|
||||
QQC2.BusyIndicator {}
|
||||
Kirigami.Heading {
|
||||
id: loadingText
|
||||
Layout.fillWidth: true
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
level: 2
|
||||
text: root.display.length > 0 ? root.display : i18n("Loading")
|
||||
}
|
||||
}
|
||||
|
||||
142
src/messagecontent/LocationComponent.qml
Normal file
@@ -0,0 +1,142 @@
|
||||
// SPDX-FileCopyrightText: 2021 Tobias Fella <fella@posteo.de>
|
||||
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import QtLocation
|
||||
import QtPositioning
|
||||
|
||||
import org.kde.neochat
|
||||
import org.kde.kirigami as Kirigami
|
||||
|
||||
/**
|
||||
* @brief A component to show a location from a message.
|
||||
*/
|
||||
ColumnLayout {
|
||||
id: root
|
||||
|
||||
/**
|
||||
* @brief The matrix ID of the message event.
|
||||
*/
|
||||
required property string eventId
|
||||
|
||||
/**
|
||||
* @brief The message author.
|
||||
*
|
||||
* A Quotient::RoomMember object.
|
||||
*
|
||||
* @sa Quotient::RoomMember
|
||||
*/
|
||||
required property var author
|
||||
|
||||
/**
|
||||
* @brief The display text of the message.
|
||||
*/
|
||||
required property string display
|
||||
|
||||
/**
|
||||
* @brief The latitude of the location marker in the message.
|
||||
*/
|
||||
required property real latitude
|
||||
|
||||
/**
|
||||
* @brief The longitude of the location marker in the message.
|
||||
*/
|
||||
required property real longitude
|
||||
|
||||
/**
|
||||
* @brief What type of marker the location message is.
|
||||
*
|
||||
* The main options are m.pin for a general location or m.self for a pin to show
|
||||
* a user's location.
|
||||
*/
|
||||
required property string asset
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.maximumWidth: Message.maxContentWidth
|
||||
|
||||
MapView {
|
||||
id: mapView
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredWidth: root.Message.maxContentWidth
|
||||
Layout.preferredHeight: root.Message.maxContentWidth / 16 * 9
|
||||
|
||||
map.center: QtPositioning.coordinate(root.latitude, root.longitude)
|
||||
map.zoomLevel: 15
|
||||
|
||||
map.plugin: OsmLocationPlugin.plugin
|
||||
|
||||
readonly property LocationMapItem locationMapItem: LocationMapItem {
|
||||
latitude: root.latitude
|
||||
longitude: root.longitude
|
||||
asset: root.asset
|
||||
author: root.author
|
||||
isLive: true
|
||||
heading: NaN
|
||||
}
|
||||
|
||||
Component.onCompleted: map.addMapItem(locationMapItem)
|
||||
|
||||
Connections {
|
||||
target: mapView.map
|
||||
function onCopyrightLinkActivated(link: string) {
|
||||
Qt.openUrlExternally(link);
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
anchors {
|
||||
top: parent.top
|
||||
right: parent.right
|
||||
margins: Kirigami.Units.smallSpacing
|
||||
}
|
||||
|
||||
spacing: Kirigami.Units.mediumSpacing
|
||||
|
||||
Button {
|
||||
text: i18nc("@action:button Open the location in an external program", "Open Externally")
|
||||
icon.name: "open-link-symbolic"
|
||||
display: AbstractButton.IconOnly
|
||||
|
||||
onClicked: Qt.openUrlExternally("geo:" + root.latitude + "," + root.longitude)
|
||||
|
||||
ToolTip.text: text
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
}
|
||||
|
||||
Button {
|
||||
icon.name: "view-fullscreen"
|
||||
text: i18nc("@action:button", "Open Fullscreen")
|
||||
display: AbstractButton.IconOnly
|
||||
|
||||
onClicked: {
|
||||
let map = fullScreenMap.createObject(parent, {
|
||||
latitude: root.latitude,
|
||||
longitude: root.longitude,
|
||||
asset: root.asset,
|
||||
author: root.author
|
||||
});
|
||||
map.open();
|
||||
}
|
||||
|
||||
ToolTip.text: text
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
}
|
||||
}
|
||||
}
|
||||
Component {
|
||||
id: fullScreenMap
|
||||
FullScreenMap {}
|
||||
}
|
||||
|
||||
TextComponent {
|
||||
eventId: root.eventId
|
||||
author: root.author
|
||||
display: root.display
|
||||
visible: root.display !== ""
|
||||
}
|
||||
}
|
||||
26
src/messagecontent/MessageComponentChooser.qml
Normal file
@@ -0,0 +1,26 @@
|
||||
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
|
||||
import QtQuick
|
||||
import Qt.labs.qmlmodels
|
||||
|
||||
import org.kde.neochat
|
||||
|
||||
/**
|
||||
* @brief Select a message component based on a MessageComponentType.
|
||||
*/
|
||||
BaseMessageComponentChooser {
|
||||
id: root
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: MessageComponentType.ThreadBody
|
||||
delegate: ThreadBodyComponent {
|
||||
onSelectedTextChanged: selectedText => {
|
||||
root.selectedTextChanged(selectedText);
|
||||
}
|
||||
onHoveredLinkChanged: hoveredLink => {
|
||||
root.hoveredLinkChanged(hoveredLink);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
57
src/messagecontent/MimeComponent.qml
Normal file
@@ -0,0 +1,57 @@
|
||||
// SPDX-FileCopyrightText: 2022 James Graham <james.h.graham@protonmail.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtQuick.Layouts
|
||||
|
||||
import org.kde.coreaddons
|
||||
import org.kde.kirigami as Kirigami
|
||||
|
||||
/**
|
||||
* @brief A component to show media based upon its mime type.
|
||||
*/
|
||||
RowLayout {
|
||||
property alias mimeIconSource: icon.source
|
||||
property alias label: nameLabel.text
|
||||
property string subLabel: ""
|
||||
property int size: 0
|
||||
property int duration: 0
|
||||
|
||||
spacing: Kirigami.Units.largeSpacing
|
||||
|
||||
Kirigami.Icon {
|
||||
id: icon
|
||||
fallback: "unknown"
|
||||
}
|
||||
ColumnLayout {
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.fillWidth: true
|
||||
|
||||
spacing: 0
|
||||
|
||||
QQC2.Label {
|
||||
id: nameLabel
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: caption.visible ? Qt.AlignLeft | Qt.AlignBottom : Qt.AlignLeft | Qt.AlignVCenter
|
||||
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
QQC2.Label {
|
||||
id: caption
|
||||
|
||||
Layout.fillWidth: true
|
||||
|
||||
text: (subLabel || size || duration || '') && [
|
||||
subLabel,
|
||||
size && Format.formatByteSize(size),
|
||||
duration > 0 && Format.formatDuration(duration),
|
||||
].filter(Boolean).join(" | ")
|
||||
|
||||
elide: Text.ElideRight
|
||||
visible: text.length > 0
|
||||
opacity: 0.7
|
||||
}
|
||||
}
|
||||
}
|
||||
39
src/messagecontent/PdfPreviewComponent.qml
Normal file
@@ -0,0 +1,39 @@
|
||||
// SPDX-FileCopyrightText: 2024 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
import org.kde.neochat
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
/**
|
||||
* @brief FileTransferInfo for any downloading files.
|
||||
*/
|
||||
required property var fileTransferInfo
|
||||
|
||||
/**
|
||||
* @brief The attributes of the component.
|
||||
*/
|
||||
required property var componentAttributes
|
||||
|
||||
Layout.preferredWidth: mediaSizeHelper.currentSize.width
|
||||
Layout.preferredHeight: mediaSizeHelper.currentSize.height
|
||||
|
||||
color: "white"
|
||||
|
||||
Image {
|
||||
anchors.fill: root
|
||||
source: root?.fileTransferInfo.localPath ?? ""
|
||||
|
||||
MediaSizeHelper {
|
||||
id: mediaSizeHelper
|
||||
contentMaxWidth: root.Message.maxContentWidth
|
||||
mediaWidth: root.componentAttributes.size.width
|
||||
mediaHeight: root.componentAttributes.size.height
|
||||
}
|
||||
}
|
||||
}
|
||||
132
src/messagecontent/PollComponent.qml
Normal file
@@ -0,0 +1,132 @@
|
||||
// SPDX-FileCopyrightText: 2022 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtQuick.Layouts
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
import org.kde.kirigamiaddons.delegates as Delegates
|
||||
import org.kde.kirigamiaddons.formcard as FormCard
|
||||
|
||||
import org.kde.neochat
|
||||
|
||||
/**
|
||||
* @brief A component to show a poll from a message.
|
||||
*/
|
||||
ColumnLayout {
|
||||
id: root
|
||||
|
||||
/**
|
||||
* @brief The matrix ID of the message event.
|
||||
*/
|
||||
required property string eventId
|
||||
|
||||
/**
|
||||
* @brief The poll handler for this poll.
|
||||
*
|
||||
* This contains the required information like what the question, answers and
|
||||
* current number of votes for each is.
|
||||
*/
|
||||
required property var pollHandler
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.maximumWidth: Message.maxContentWidth
|
||||
Layout.minimumWidth: Message.maxContentWidth
|
||||
|
||||
spacing: 0
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
Kirigami.Icon {
|
||||
implicitWidth: Kirigami.Units.iconSizes.smallMedium
|
||||
implicitHeight: implicitWidth
|
||||
source: "amarok_playcount"
|
||||
}
|
||||
QQC2.Label {
|
||||
id: questionLabel
|
||||
Layout.fillWidth: true
|
||||
topPadding: Kirigami.Units.largeSpacing
|
||||
bottomPadding: Kirigami.Units.largeSpacing
|
||||
|
||||
text: root.pollHandler.question
|
||||
wrapMode: Text.Wrap
|
||||
visible: text.length > 0
|
||||
}
|
||||
}
|
||||
Repeater {
|
||||
model: root.pollHandler.answerModel
|
||||
delegate: Delegates.RoundedItemDelegate {
|
||||
id: answerDelegate
|
||||
|
||||
required property string id
|
||||
required property string answerText
|
||||
required property int count
|
||||
required property bool localChoice
|
||||
required property bool isWinner
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: -Kirigami.Units.largeSpacing - Kirigami.Units.smallSpacing
|
||||
Layout.rightMargin: -Kirigami.Units.largeSpacing - Kirigami.Units.smallSpacing
|
||||
|
||||
highlighted: false
|
||||
|
||||
onClicked: {
|
||||
if (root.pollHandler.hasEnded) {
|
||||
return;
|
||||
}
|
||||
root.pollHandler.sendPollAnswer(root.eventId, answerDelegate.id);
|
||||
}
|
||||
text: answerDelegate.answerText
|
||||
|
||||
topPadding: Kirigami.Units.smallSpacing
|
||||
bottomPadding: Kirigami.Units.smallSpacing
|
||||
|
||||
contentItem: ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
QQC2.CheckBox {
|
||||
enabled: !root.pollHandler.hasEnded
|
||||
checked: answerDelegate.localChoice
|
||||
|
||||
onClicked: answerDelegate.clicked()
|
||||
}
|
||||
QQC2.Label {
|
||||
Layout.fillWidth: true
|
||||
|
||||
text: answerDelegate.answerText
|
||||
}
|
||||
Kirigami.Icon {
|
||||
implicitWidth: Kirigami.Units.iconSizes.small
|
||||
implicitHeight: implicitWidth
|
||||
visible: answerDelegate.isWinner
|
||||
source: "favorite-favorited"
|
||||
}
|
||||
QQC2.Label {
|
||||
visible: root.pollHandler.kind == PollKind.Disclosed || pollHandler.hasEnded
|
||||
horizontalAlignment: Text.AlignRight
|
||||
text: i18np("%1 Vote", "%1 Votes", answerDelegate.count)
|
||||
}
|
||||
}
|
||||
QQC2.ProgressBar {
|
||||
id: voteProgress
|
||||
|
||||
Layout.fillWidth: true
|
||||
to: root.pollHandler.totalCount
|
||||
value: root.pollHandler.kind == PollKind.Disclosed || pollHandler.hasEnded ? answerDelegate.count : 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QQC2.Label {
|
||||
visible: root.pollHandler.kind == PollKind.Disclosed || root.pollHandler.hasEnded
|
||||
text: i18np("Based on votes by %1 user", "Based on votes by %1 users", root.pollHandler.totalCount) + (root.pollHandler.hasEnded ? (" " + i18nc("as in 'this vote has ended'", "(Ended)")) : "")
|
||||
font.pointSize: questionLabel.font.pointSize * 0.8
|
||||
}
|
||||
}
|
||||
74
src/messagecontent/QuoteComponent.qml
Normal file
@@ -0,0 +1,74 @@
|
||||
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtQuick.Layouts
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
|
||||
import org.kde.neochat
|
||||
|
||||
QQC2.Control {
|
||||
id: root
|
||||
|
||||
/**
|
||||
* @brief The matrix ID of the message event.
|
||||
*/
|
||||
required property string eventId
|
||||
|
||||
/**
|
||||
* @brief The message author.
|
||||
*
|
||||
* A Quotient::RoomMember object.
|
||||
*
|
||||
* @sa Quotient::RoomMember
|
||||
*/
|
||||
required property NeochatRoomMember author
|
||||
|
||||
/**
|
||||
* @brief The display text of the message.
|
||||
*/
|
||||
required property string display
|
||||
|
||||
/**
|
||||
* @brief The user selected text has changed.
|
||||
*/
|
||||
signal selectedTextChanged(string selectedText)
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
Layout.maximumWidth: Message.maxContentWidth
|
||||
|
||||
topPadding: 0
|
||||
bottomPadding: 0
|
||||
|
||||
contentItem: TextEdit {
|
||||
id: quoteText
|
||||
Layout.fillWidth: true
|
||||
topPadding: Kirigami.Units.smallSpacing
|
||||
bottomPadding: Kirigami.Units.smallSpacing
|
||||
|
||||
text: root.display
|
||||
readOnly: true
|
||||
textFormat: TextEdit.RichText
|
||||
wrapMode: TextEdit.Wrap
|
||||
color: Kirigami.Theme.textColor
|
||||
|
||||
font.italic: true
|
||||
|
||||
onSelectedTextChanged: root.selectedTextChanged(selectedText)
|
||||
|
||||
TapHandler {
|
||||
enabled: !quoteText.hoveredLink
|
||||
acceptedDevices: PointerDevice.TouchScreen
|
||||
acceptedButtons: Qt.LeftButton
|
||||
onLongPressed: RoomManager.viewEventMenu(root.eventId, root.Message.room, root.author, root.Message.selectedText, root.Message.hoveredLink);
|
||||
}
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
color: Kirigami.Theme.backgroundColor
|
||||
radius: Kirigami.Units.cornerRadius
|
||||
}
|
||||
}
|
||||
133
src/messagecontent/ReactionComponent.qml
Normal file
@@ -0,0 +1,133 @@
|
||||
// SPDX-FileCopyrightText: 2019 Black Hat <bhat@encom.eu.org>
|
||||
// SPDX-FileCopyrightText: 2020 Carl Schwan <carl@carlschwan.eu>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtQuick.Layouts
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
|
||||
import org.kde.neochat
|
||||
|
||||
Flow {
|
||||
id: root
|
||||
|
||||
/**
|
||||
* @brief The matrix ID of the message event.
|
||||
*/
|
||||
required property string eventId
|
||||
|
||||
/**
|
||||
* @brief The reaction model to get the reactions from.
|
||||
*/
|
||||
required property ReactionModel reactionModel
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
Layout.maximumWidth: Message.maxContentWidth
|
||||
|
||||
spacing: Kirigami.Units.smallSpacing
|
||||
|
||||
Repeater {
|
||||
id: reactionRepeater
|
||||
|
||||
model: root.reactionModel
|
||||
|
||||
delegate: QQC2.AbstractButton {
|
||||
id: reactionDelegate
|
||||
|
||||
required property string textContent
|
||||
required property string reaction
|
||||
required property string toolTip
|
||||
required property bool hasLocalMember
|
||||
|
||||
width: Math.max(contentItem.implicitWidth + leftPadding + rightPadding, height)
|
||||
height: Math.round(Kirigami.Units.gridUnit * 1.5)
|
||||
|
||||
contentItem: QQC2.Label {
|
||||
id: reactionLabel
|
||||
anchors.centerIn: parent
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
text: reactionDelegate.textContent
|
||||
background: null
|
||||
wrapMode: TextEdit.NoWrap
|
||||
textFormat: Text.RichText
|
||||
}
|
||||
|
||||
padding: Kirigami.Units.smallSpacing
|
||||
|
||||
background: Kirigami.ShadowedRectangle {
|
||||
Kirigami.Theme.inherit: false
|
||||
Kirigami.Theme.colorSet: NeoChatConfig.compactLayout ? Kirigami.Theme.View : Kirigami.Theme.Window
|
||||
color: reactionDelegate.hasLocalMember ? Kirigami.Theme.positiveBackgroundColor : Kirigami.Theme.backgroundColor
|
||||
radius: height / 2
|
||||
shadow {
|
||||
size: Kirigami.Units.smallSpacing
|
||||
color: !reactionDelegate.hasLocalMember ? Qt.rgba(0.0, 0.0, 0.0, 0.10) : Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.10)
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
root.Message.room.toggleReaction(root.eventId, reactionDelegate.reaction)
|
||||
}
|
||||
|
||||
hoverEnabled: true
|
||||
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
QQC2.ToolTip.visible: hovered
|
||||
QQC2.ToolTip.text: reactionDelegate.toolTip
|
||||
}
|
||||
}
|
||||
|
||||
QQC2.AbstractButton {
|
||||
id: reactButton
|
||||
width: Math.round(Kirigami.Units.gridUnit * 1.5)
|
||||
height: Math.round(Kirigami.Units.gridUnit * 1.5)
|
||||
|
||||
text: i18nc("@button", "React")
|
||||
|
||||
contentItem: Kirigami.Icon {
|
||||
source: "list-add"
|
||||
}
|
||||
|
||||
padding: Kirigami.Units.smallSpacing
|
||||
|
||||
background: Rectangle {
|
||||
color: Kirigami.Theme.backgroundColor
|
||||
radius: height / 2
|
||||
border {
|
||||
width: reactButton.hovered ? 1 : 0
|
||||
color: Kirigami.ColorUtils.linearInterpolation(Kirigami.Theme.backgroundColor, Kirigami.Theme.textColor, Kirigami.Theme.frameContrast)
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
var dialog = emojiDialog.createObject(reactButton);
|
||||
dialog.showStickers = false;
|
||||
dialog.chosen.connect(emoji => {
|
||||
root.Message.room.toggleReaction(root.eventId, emoji);
|
||||
if (!Kirigami.Settings.isMobile) {
|
||||
root.focusChatBar();
|
||||
}
|
||||
});
|
||||
dialog.open();
|
||||
}
|
||||
|
||||
hoverEnabled: true
|
||||
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
QQC2.ToolTip.visible: hovered
|
||||
QQC2.ToolTip.text: reactButton.text
|
||||
}
|
||||
|
||||
Component {
|
||||
id: emojiDialog
|
||||
|
||||
EmojiDialog {
|
||||
currentRoom: root.Message.room
|
||||
showQuickReaction: true
|
||||
}
|
||||
}
|
||||
}
|
||||
54
src/messagecontent/ReplyAuthorComponent.qml
Normal file
@@ -0,0 +1,54 @@
|
||||
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtQuick.Layouts
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
import org.kde.kirigamiaddons.labs.components as KirigamiComponents
|
||||
|
||||
RowLayout {
|
||||
id: root
|
||||
|
||||
/**
|
||||
* @brief The message author.
|
||||
*
|
||||
* A Quotient::RoomMember object.
|
||||
*
|
||||
* @sa Quotient::RoomMember
|
||||
*/
|
||||
required property var author
|
||||
|
||||
/**
|
||||
* @brief The maximum width that the bubble's content can be.
|
||||
*/
|
||||
property real maxContentWidth: -1
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.maximumWidth: root.maxContentWidth
|
||||
|
||||
implicitHeight: Math.max(replyAvatar.implicitHeight, replyName.implicitHeight)
|
||||
spacing: Kirigami.Units.largeSpacing
|
||||
|
||||
KirigamiComponents.Avatar {
|
||||
id: replyAvatar
|
||||
|
||||
implicitWidth: Kirigami.Units.iconSizes.small
|
||||
implicitHeight: Kirigami.Units.iconSizes.small
|
||||
|
||||
source: root.author.avatarUrl
|
||||
name: root.author.displayName
|
||||
color: root.author.color
|
||||
asynchronous: true
|
||||
}
|
||||
QQC2.Label {
|
||||
id: replyName
|
||||
Layout.fillWidth: true
|
||||
|
||||
color: root.author.color
|
||||
text: root.author.disambiguatedName
|
||||
elide: Text.ElideRight
|
||||
textFormat: Text.PlainText
|
||||
}
|
||||
}
|
||||
42
src/messagecontent/ReplyButtonComponent.qml
Normal file
@@ -0,0 +1,42 @@
|
||||
// SPDX-FileCopyrightText: 2023 James Graham <james.h.graham@protonmail.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtQuick.Layouts
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
import org.kde.kirigamiaddons.delegates as Delegates
|
||||
|
||||
import org.kde.neochat
|
||||
import org.kde.neochat.chatbar
|
||||
|
||||
/**
|
||||
* @brief A component to show a reply button for threads in a message bubble.
|
||||
*/
|
||||
Delegates.RoundedItemDelegate {
|
||||
id: root
|
||||
|
||||
/**
|
||||
* @brief The thread root ID.
|
||||
*/
|
||||
required property string threadRoot
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.maximumWidth: Message.maxContentWidth
|
||||
|
||||
leftInset: 0
|
||||
rightInset: 0
|
||||
|
||||
highlighted: true
|
||||
|
||||
icon.name: "mail-reply-custom"
|
||||
text: i18nc("@action:button", "Reply")
|
||||
|
||||
onClicked: {
|
||||
Message.room.threadCache.replyId = "";
|
||||
Message.room.threadCache.threadId = root.threadRoot;
|
||||
Message.room.mainCache.clearRelations();
|
||||
Message.room.editCache.clearRelations();
|
||||
}
|
||||
}
|
||||
84
src/messagecontent/ReplyComponent.qml
Normal file
@@ -0,0 +1,84 @@
|
||||
// SPDX-FileCopyrightText: 2019 Black Hat <bhat@encom.eu.org>
|
||||
// SPDX-FileCopyrightText: 2020 Carl Schwan <carl@carlschwan.eu>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtQuick.Layouts
|
||||
import Qt.labs.qmlmodels
|
||||
|
||||
import org.kde.coreaddons
|
||||
import org.kde.kirigami as Kirigami
|
||||
import org.kde.kirigamiaddons.labs.components as KirigamiComponents
|
||||
|
||||
import org.kde.neochat
|
||||
|
||||
/**
|
||||
* @brief A component to show a message that has been replied to.
|
||||
*
|
||||
* Similar to the main timeline delegate a reply delegate is chosen based on the type
|
||||
* of message being replied to. The main difference is that not all messages can be
|
||||
* show in their original form and are instead visualised with a MIME type delegate
|
||||
* e.g. Videos.
|
||||
*/
|
||||
RowLayout {
|
||||
id: root
|
||||
|
||||
/**
|
||||
* @brief The matrix ID of the reply event.
|
||||
*/
|
||||
required property var replyEventId
|
||||
|
||||
/**
|
||||
* @brief The reply author.
|
||||
*
|
||||
* A Quotient::RoomMember object.
|
||||
*
|
||||
* @sa Quotient::RoomMember
|
||||
*/
|
||||
required property var replyAuthor
|
||||
|
||||
/**
|
||||
* @brief The model to visualise the content of the message replied to.
|
||||
*/
|
||||
required property var replyContentModel
|
||||
|
||||
Layout.fillWidth: true
|
||||
|
||||
spacing: Kirigami.Units.largeSpacing
|
||||
|
||||
Rectangle {
|
||||
id: verticalBorder
|
||||
Layout.fillHeight: true
|
||||
|
||||
implicitWidth: Kirigami.Units.smallSpacing
|
||||
color: root.replyAuthor.color
|
||||
radius: Kirigami.Units.cornerRadius
|
||||
}
|
||||
ColumnLayout {
|
||||
id: contentColumn
|
||||
spacing: Kirigami.Units.smallSpacing
|
||||
|
||||
Message.maxContentWidth: _private.availableContentWidth
|
||||
|
||||
Repeater {
|
||||
id: contentRepeater
|
||||
model: root.replyContentModel
|
||||
delegate: ReplyMessageComponentChooser {
|
||||
onReplyClicked: root.Message.timeline.goToEvent(root.replyEventId)
|
||||
}
|
||||
}
|
||||
}
|
||||
HoverHandler {
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
}
|
||||
TapHandler {
|
||||
acceptedButtons: Qt.LeftButton
|
||||
onTapped: root.Message.timeline.goToEvent(root.replyEventId)
|
||||
}
|
||||
QtObject {
|
||||
id: _private
|
||||
// The space available for the component after taking away the border
|
||||
readonly property real availableContentWidth: root.Message.maxContentWidth - verticalBorder.implicitWidth - root.spacing
|
||||
}
|
||||
}
|
||||
147
src/messagecontent/ReplyMessageComponentChooser.qml
Normal file
@@ -0,0 +1,147 @@
|
||||
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Qt.labs.qmlmodels
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
|
||||
import org.kde.neochat
|
||||
|
||||
/**
|
||||
* @brief Select a message component based on a MessageComponentType.
|
||||
*/
|
||||
DelegateChooser {
|
||||
id: root
|
||||
|
||||
/**
|
||||
* @brief The reply has been clicked.
|
||||
*/
|
||||
signal replyClicked()
|
||||
|
||||
role: "componentType"
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: MessageComponentType.Author
|
||||
delegate: ReplyAuthorComponent {}
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: MessageComponentType.Text
|
||||
delegate: TextComponent {
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.LeftButton
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: root.replyClicked()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: MessageComponentType.Image
|
||||
delegate: ImageComponent {
|
||||
contentMaxHeight: Kirigami.Units.gridUnit * 5
|
||||
}
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: MessageComponentType.Video
|
||||
delegate: MimeComponent {
|
||||
required property var mediaInfo
|
||||
|
||||
mimeIconSource: mediaInfo.mimeIcon
|
||||
size: mediaInfo.size
|
||||
duration: mediaInfo.duration
|
||||
label: mediaInfo.filename
|
||||
}
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: MessageComponentType.Code
|
||||
delegate: CodeComponent {
|
||||
Layout.maximumHeight: Kirigami.Units.gridUnit * 5
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.LeftButton
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: root.replyClicked()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: MessageComponentType.Quote
|
||||
delegate: QuoteComponent {
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.LeftButton
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: root.replyClicked()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: MessageComponentType.Audio
|
||||
delegate: MimeComponent {
|
||||
required property string display
|
||||
required property var mediaInfo
|
||||
|
||||
mimeIconSource: mediaInfo.mimeIcon
|
||||
size: mediaInfo.size
|
||||
duration: mediaInfo.duration
|
||||
label: mediaInfo.filename
|
||||
}
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: MessageComponentType.File
|
||||
delegate: MimeComponent {
|
||||
required property string display
|
||||
required property var mediaInfo
|
||||
|
||||
mimeIconSource: mediaInfo.mimeIcon
|
||||
size: mediaInfo.size
|
||||
label: mediaInfo.filename
|
||||
}
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: MessageComponentType.Poll
|
||||
delegate: PollComponent {}
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: MessageComponentType.Location
|
||||
delegate: MimeComponent {
|
||||
mimeIconSource: "mark-location"
|
||||
label: display
|
||||
}
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: MessageComponentType.LiveLocation
|
||||
delegate: MimeComponent {
|
||||
mimeIconSource: "mark-location"
|
||||
label: display
|
||||
}
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: MessageComponentType.Encrypted
|
||||
delegate: EncryptedComponent {}
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: MessageComponentType.Loading
|
||||
delegate: LoadComponent {}
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: MessageComponentType.Other
|
||||
delegate: Item {}
|
||||
}
|
||||
}
|
||||
74
src/messagecontent/StateComponent.qml
Normal file
@@ -0,0 +1,74 @@
|
||||
// SPDX-FileCopyrightText: 2022 James Graham <james.h.graham@protonmail.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtQuick.Layouts
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
import org.kde.kirigamiaddons.labs.components as KirigamiComponents
|
||||
|
||||
import org.kde.neochat
|
||||
|
||||
/**
|
||||
* @brief A component for visualising a single state event
|
||||
*/
|
||||
RowLayout {
|
||||
id: root
|
||||
|
||||
/**
|
||||
* @brief All model roles as a map with the property names as the keys.
|
||||
*/
|
||||
required property var modelData
|
||||
|
||||
/**
|
||||
* @brief The message author.
|
||||
*
|
||||
* A Quotient::RoomMember object.
|
||||
*
|
||||
* @sa Quotient::RoomMember
|
||||
*/
|
||||
property var author: modelData.author
|
||||
|
||||
/**
|
||||
* @brief The displayname for the event's sender; for name change events, the old displayname.
|
||||
*/
|
||||
property string authorDisplayName: modelData.authorDisplayName
|
||||
|
||||
/**
|
||||
* @brief The display text for the state event.
|
||||
*/
|
||||
property string text: modelData.text
|
||||
|
||||
KirigamiComponents.Avatar {
|
||||
id: stateAvatar
|
||||
|
||||
Layout.preferredWidth: Kirigami.Units.iconSizes.small
|
||||
Layout.preferredHeight: Kirigami.Units.iconSizes.small
|
||||
|
||||
source: root.author?.avatarUrl ?? ""
|
||||
name: root.author?.displayName ?? ""
|
||||
color: root.author?.color ?? Kirigami.Theme.highlightColor
|
||||
asynchronous: true
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
enabled: root.author
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: RoomManager.resolveResource("https://matrix.to/#/" + root.author?.id ?? "")
|
||||
}
|
||||
}
|
||||
|
||||
QQC2.Label {
|
||||
id: label
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.fillWidth: true
|
||||
text: `<style>a {text-decoration: none; color: ${Kirigami.Theme.textColor};}</style><a href="https://matrix.to/#/${root.author?.id ?? ""}">${root.authorDisplayName}</a> ${root.text}`
|
||||
wrapMode: Text.WordWrap
|
||||
textFormat: Text.RichText
|
||||
onLinkActivated: link => RoomManager.resolveResource(link)
|
||||
HoverHandler {
|
||||
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.IBeamCursor
|
||||
}
|
||||
}
|
||||
}
|
||||
163
src/messagecontent/TextComponent.qml
Normal file
@@ -0,0 +1,163 @@
|
||||
// SPDX-FileCopyrightText: 2020 Black Hat <bhat@encom.eu.org>
|
||||
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
|
||||
import org.kde.neochat
|
||||
|
||||
/**
|
||||
* @brief A component to show rich text from a message.
|
||||
*/
|
||||
TextEdit {
|
||||
id: root
|
||||
|
||||
/**
|
||||
* @brief The matrix ID of the message event.
|
||||
*/
|
||||
required property string eventId
|
||||
|
||||
/**
|
||||
* @brief The message author.
|
||||
*
|
||||
* A Quotient::RoomMember object.
|
||||
*
|
||||
* @sa Quotient::RoomMember
|
||||
*/
|
||||
required property NeochatRoomMember author
|
||||
|
||||
/**
|
||||
* @brief The display text of the message.
|
||||
*/
|
||||
required property string display
|
||||
|
||||
/**
|
||||
* @brief Whether this message is replying to another.
|
||||
*/
|
||||
property bool isReply: false
|
||||
|
||||
/**
|
||||
* @brief Regex for detecting a message with a spoiler.
|
||||
*/
|
||||
readonly property var hasSpoiler: /data-mx-spoiler/g
|
||||
|
||||
/**
|
||||
* @brief Whether a spoiler should be revealed.
|
||||
*/
|
||||
property bool spoilerRevealed: !hasSpoiler.test(display)
|
||||
|
||||
/**
|
||||
* @brief The color of spoiler blocks, to be theme-agnostic.
|
||||
*/
|
||||
property color spoilerBlockColor: Kirigami.ColorUtils.tintWithAlpha("#232629", Kirigami.Theme.textColor, 0.15)
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
Layout.maximumWidth: Message.maxContentWidth
|
||||
|
||||
ListView.onReused: Qt.binding(() => !hasSpoiler.test(display))
|
||||
|
||||
persistentSelection: true
|
||||
|
||||
text: "<style>
|
||||
table {
|
||||
width:100%;
|
||||
border-width: 1px;
|
||||
border-collapse: collapse;
|
||||
border-style: solid;
|
||||
}
|
||||
code {
|
||||
background-color:" + Kirigami.Theme.alternateBackgroundColor + ";
|
||||
}
|
||||
table th,
|
||||
table td {
|
||||
border: 1px solid black;
|
||||
padding: 3px;
|
||||
}
|
||||
blockquote {
|
||||
margin: 0;
|
||||
}
|
||||
blockquote table {
|
||||
width: 100%;
|
||||
border-width: 0;
|
||||
background-color:" + Kirigami.Theme.alternateBackgroundColor + ";
|
||||
}
|
||||
blockquote td {
|
||||
width: 100%;
|
||||
padding: " + Kirigami.Units.largeSpacing + ";
|
||||
}
|
||||
pre {
|
||||
white-space: pre-wrap
|
||||
}
|
||||
a{
|
||||
color: " + Kirigami.Theme.linkColor + ";
|
||||
text-decoration: none;
|
||||
}
|
||||
[data-mx-spoiler] a {
|
||||
background: " + root.spoilerBlockColor + ";
|
||||
}
|
||||
[data-mx-spoiler] {
|
||||
background: " + root.spoilerBlockColor + ";
|
||||
}
|
||||
" + (!spoilerRevealed ? "
|
||||
[data-mx-spoiler] a {
|
||||
color: transparent;
|
||||
}
|
||||
[data-mx-spoiler] {
|
||||
color: transparent;
|
||||
}
|
||||
" : "
|
||||
[data-mx-spoiler] a {
|
||||
color: white;
|
||||
}
|
||||
[data-mx-spoiler] {
|
||||
color: white;
|
||||
}
|
||||
") + "
|
||||
</style>" + display
|
||||
|
||||
color: Kirigami.Theme.textColor
|
||||
selectedTextColor: Kirigami.Theme.highlightedTextColor
|
||||
selectionColor: Kirigami.Theme.highlightColor
|
||||
font {
|
||||
pointSize: !root.isReply && QmlUtils.isEmoji(display) ? Kirigami.Theme.defaultFont.pointSize * 4 : Kirigami.Theme.defaultFont.pointSize
|
||||
family: QmlUtils.isEmoji(display) ? 'emoji' : Kirigami.Theme.defaultFont.family
|
||||
}
|
||||
selectByMouse: !Kirigami.Settings.isMobile
|
||||
readOnly: true
|
||||
wrapMode: Text.Wrap
|
||||
textFormat: Text.RichText
|
||||
|
||||
onLinkActivated: link => {
|
||||
spoilerRevealed = true;
|
||||
RoomManager.resolveResource(link, "join");
|
||||
}
|
||||
onHoveredLinkChanged: if (hoveredLink.length > 0 && hoveredLink !== "1") {
|
||||
applicationWindow().hoverLinkIndicator.text = hoveredLink;
|
||||
} else {
|
||||
applicationWindow().hoverLinkIndicator.text = "";
|
||||
}
|
||||
|
||||
HoverHandler {
|
||||
cursorShape: (root.hoveredLink || !spoilerRevealed) ? Qt.PointingHandCursor : Qt.IBeamCursor
|
||||
}
|
||||
TapHandler {
|
||||
enabled: !root.hoveredLink && !spoilerRevealed
|
||||
onTapped: spoilerRevealed = true
|
||||
}
|
||||
TapHandler {
|
||||
enabled: !root.hoveredLink
|
||||
acceptedButtons: Qt.LeftButton
|
||||
acceptedDevices: PointerDevice.TouchScreen
|
||||
onLongPressed: RoomManager.viewEventMenu(root.eventId, root.Message.room, root.author, root.Message.selectedText, root.Message.hoveredLink);
|
||||
}
|
||||
TapHandler {
|
||||
acceptedButtons: Qt.RightButton
|
||||
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad | PointerDevice.Stylus
|
||||
gesturePolicy: TapHandler.WithinBounds
|
||||
onTapped: RoomManager.viewEventMenu(root.eventId, root.Message.room, root.author, root.Message.selectedText, root.Message.hoveredLink);
|
||||
}
|
||||
}
|
||||
54
src/messagecontent/ThreadBodyComponent.qml
Normal file
@@ -0,0 +1,54 @@
|
||||
// SPDX-FileCopyrightText: 2025 James Graham <james.h.graham@protonmail.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
|
||||
import org.kde.neochat
|
||||
|
||||
/**
|
||||
* @brief A component to visualize a ThreadModel.
|
||||
*
|
||||
* @sa ThreadModel
|
||||
*/
|
||||
ColumnLayout {
|
||||
id: root
|
||||
|
||||
/**
|
||||
* @brief The Matrix ID of the root message in the thread, if any.
|
||||
*/
|
||||
required property string threadRoot
|
||||
|
||||
/**
|
||||
* @brief The user selected text has changed.
|
||||
*/
|
||||
signal selectedTextChanged(string selectedText)
|
||||
|
||||
/**
|
||||
* @brief The user hovered link has changed.
|
||||
*/
|
||||
signal hoveredLinkChanged(string hoveredLink)
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
Layout.maximumWidth: Message.maxContentWidth
|
||||
spacing: Kirigami.Units.smallSpacing
|
||||
|
||||
Repeater {
|
||||
id: threadRepeater
|
||||
model: root.Message.contentModel.modelForThread(root.threadRoot);
|
||||
|
||||
delegate: BaseMessageComponentChooser {
|
||||
onSelectedTextChanged: selectedText => {
|
||||
root.selectedTextChanged(selectedText);
|
||||
}
|
||||
onHoveredLinkChanged: hoveredLink => {
|
||||
root.hoveredLinkChanged(hoveredLink);
|
||||
}
|
||||
onRemoveLinkPreview: index => threadRepeater.model.closeLinkPreview(index)
|
||||
onFetchMoreEvents: threadRepeater.model.fetchMoreEvents(5)
|
||||
}
|
||||
}
|
||||
}
|
||||
214
src/messagecontent/TrainReservationComponent.qml
Normal file
@@ -0,0 +1,214 @@
|
||||
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Controls as QQC2
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
import org.kde.kirigamiaddons.formcard as FormCard
|
||||
|
||||
/**
|
||||
* @brief A component for a train itinerary reservation component.
|
||||
*/
|
||||
ItineraryReservationComponent {
|
||||
id: root
|
||||
|
||||
/**
|
||||
* @brief The name of the reservation.
|
||||
*/
|
||||
required property string name
|
||||
|
||||
/**
|
||||
* @brief The departure time of the train.
|
||||
*/
|
||||
required property string departureTime
|
||||
|
||||
/**
|
||||
* @brief The departure station of the train.
|
||||
*/
|
||||
required property string departureLocation
|
||||
|
||||
/**
|
||||
* @brief The address of the departure station of the train.
|
||||
*/
|
||||
required property string departureAddress
|
||||
|
||||
/**
|
||||
* @brief The departure platform of the train.
|
||||
*/
|
||||
required property string departurePlatform
|
||||
|
||||
/**
|
||||
* @brief The arrival time of the train.
|
||||
*/
|
||||
required property string arrivalTime
|
||||
|
||||
/**
|
||||
* @brief The arrival station of the train.
|
||||
*/
|
||||
required property string arrivalLocation
|
||||
|
||||
/**
|
||||
* @brief The address of the arrival station of the train.
|
||||
*/
|
||||
required property string arrivalAddress
|
||||
|
||||
/**
|
||||
* @brief The arrival platform of the train.
|
||||
*/
|
||||
required property string arrivalPlatform
|
||||
|
||||
headerItem: RowLayout {
|
||||
TransportIcon {
|
||||
source: "qrc:/qt/qml/org/kde/neochat/timeline/images/train.svg"
|
||||
isMask: true
|
||||
size: Kirigami.Units.iconSizes.smallMedium
|
||||
}
|
||||
QQC2.Label {
|
||||
Layout.fillWidth: true
|
||||
text: root.name
|
||||
elide: Text.ElideRight
|
||||
horizontalAlignment: Text.AlignLeft
|
||||
|
||||
Accessible.ignored: true
|
||||
}
|
||||
QQC2.Label {
|
||||
text: root.departureTime
|
||||
}
|
||||
}
|
||||
|
||||
contentItem: ColumnLayout {
|
||||
spacing: 0
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
ColumnLayout{
|
||||
id: lineSectionColumn
|
||||
spacing: 0
|
||||
JourneySectionStopDelegateLineSegment {
|
||||
Layout.fillHeight: true
|
||||
isDeparture: true
|
||||
}
|
||||
JourneySectionStopDelegateLineSegment {
|
||||
visible: departureCountryLayout.visible
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout{
|
||||
Layout.bottomMargin: Kirigami.Units.largeSpacing
|
||||
|
||||
spacing:0
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
RowLayout {
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
QQC2.Label {
|
||||
id: depTime
|
||||
text: root.departureTime
|
||||
}
|
||||
QQC2.Label {
|
||||
Layout.fillWidth: true
|
||||
font.bold: true
|
||||
text: root.departureLocation
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
QQC2.Label {
|
||||
Layout.alignment: Qt.AlignRight
|
||||
text: {
|
||||
let platform = root.departurePlatform;
|
||||
|
||||
if (platform) {
|
||||
return i18n("Pl. %1", platform);
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
RowLayout{
|
||||
id: departureCountryLayout
|
||||
visible: departureCountryLabel.text.length > 0
|
||||
Item{
|
||||
Layout.minimumWidth: depTime.width
|
||||
}
|
||||
QQC2.Label {
|
||||
id: departureCountryLabel
|
||||
Layout.fillWidth: true
|
||||
text: root.departureAddress
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
RowLayout{
|
||||
Layout.fillWidth: true
|
||||
JourneySectionStopDelegateLineSegment {
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
Kirigami.Separator {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
ColumnLayout {
|
||||
spacing: 0
|
||||
JourneySectionStopDelegateLineSegment {
|
||||
visible: arrivalCountryLayout.visible
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
JourneySectionStopDelegateLineSegment {
|
||||
Layout.fillHeight: true
|
||||
isArrival: true
|
||||
}
|
||||
}
|
||||
ColumnLayout{
|
||||
Layout.topMargin: Kirigami.Units.largeSpacing
|
||||
|
||||
spacing:0
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
RowLayout {
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
QQC2.Label {
|
||||
text: root.arrivalTime
|
||||
}
|
||||
QQC2.Label {
|
||||
Layout.fillWidth: true
|
||||
font.bold: true
|
||||
text: root.arrivalLocation
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
QQC2.Label {
|
||||
Layout.alignment: Qt.AlignRight
|
||||
text: {
|
||||
let platform = root.arrivalPlatform;
|
||||
|
||||
if (platform) {
|
||||
return i18n("Pl. %1", platform);
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
RowLayout {
|
||||
id: arrivalCountryLayout
|
||||
visible: arrivalCountryLabel.text.length > 0
|
||||
Item{
|
||||
Layout.minimumWidth: depTime.width
|
||||
}
|
||||
QQC2.Label {
|
||||
id: arrivalCountryLabel
|
||||
Layout.fillWidth: true
|
||||
text: root.arrivalAddress
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
46
src/messagecontent/TransportIcon.qml
Normal file
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
SPDX-FileCopyrightText: 2023 Volker Krause <vkrause@kde.org>
|
||||
SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
*/
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import org.kde.kirigami as Kirigami
|
||||
|
||||
/** Displays a transport line or mode logo/icon.
|
||||
* Mainly to hide ugly implementation details of Icon not
|
||||
* handling non-square SVG assets in the way we need it here.
|
||||
*/
|
||||
Item {
|
||||
id: root
|
||||
// properties match those of Icon
|
||||
property string source
|
||||
property alias isMask: __icon.isMask
|
||||
property alias color: __icon.color
|
||||
|
||||
// icon size (height for non-square ones)
|
||||
property int size: Kirigami.Units.iconSizes.small
|
||||
|
||||
property bool __isIcon: !source.startsWith("file:")
|
||||
|
||||
|
||||
implicitWidth: __isIcon ? root.size : Math.round(root.size * __image.implicitWidth / __image.implicitHeight)
|
||||
implicitHeight: root.size
|
||||
|
||||
Layout.preferredWidth: root.implicitWidth
|
||||
Layout.preferredHeight: root.implicitHeight
|
||||
|
||||
Kirigami.Icon {
|
||||
id: __icon
|
||||
source: root.__isIcon ? root.source : ""
|
||||
visible: source !== ""
|
||||
anchors.fill: parent
|
||||
}
|
||||
Image {
|
||||
id: __image
|
||||
source: root.__isIcon ? "" : root.source
|
||||
visible: source !== ""
|
||||
anchors.fill: parent
|
||||
fillMode: Image.PreserveAspectFit
|
||||
}
|
||||
}
|
||||
460
src/messagecontent/VideoComponent.qml
Normal file
@@ -0,0 +1,460 @@
|
||||
// SPDX-FileCopyrightText: 2019 Black Hat <bhat@encom.eu.org>
|
||||
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
import QtCore as Core
|
||||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtQuick.Layouts
|
||||
import QtMultimedia
|
||||
|
||||
import org.kde.coreaddons
|
||||
import org.kde.kirigami as Kirigami
|
||||
|
||||
import org.kde.neochat
|
||||
|
||||
/**
|
||||
* @brief A component to show a video from a message.
|
||||
*/
|
||||
Video {
|
||||
id: root
|
||||
|
||||
/**
|
||||
* @brief The matrix ID of the message event.
|
||||
*/
|
||||
required property string eventId
|
||||
|
||||
/**
|
||||
* @brief The display text of the message.
|
||||
*/
|
||||
required property string display
|
||||
|
||||
/**
|
||||
* @brief The media info for the event.
|
||||
*
|
||||
* This should consist of the following:
|
||||
* - source - The mxc URL for the media.
|
||||
* - mimeType - The MIME type of the media (should be image/xxx for this delegate).
|
||||
* - mimeIcon - The MIME icon name (should be image-xxx).
|
||||
* - size - The file size in bytes.
|
||||
* - width - The width in pixels of the audio media.
|
||||
* - height - The height in pixels of the audio media.
|
||||
* - tempInfo - mediaInfo (with the same properties as this except no tempInfo) for a temporary image while the file downloads.
|
||||
*/
|
||||
required property var mediaInfo
|
||||
|
||||
/**
|
||||
* @brief FileTransferInfo for any downloading files.
|
||||
*/
|
||||
required property var fileTransferInfo
|
||||
|
||||
/**
|
||||
* @brief Whether the media has been downloaded.
|
||||
*/
|
||||
readonly property bool downloaded: root.fileTransferInfo && root.fileTransferInfo.completed
|
||||
onDownloadedChanged: {
|
||||
if (downloaded) {
|
||||
root.source = root.fileTransferInfo.localPath;
|
||||
}
|
||||
if (downloaded && playOnFinished) {
|
||||
playSavedFile();
|
||||
playOnFinished = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Whether the video should be played when downloaded.
|
||||
*/
|
||||
property bool playOnFinished: false
|
||||
|
||||
Layout.preferredWidth: mediaSizeHelper.currentSize.width
|
||||
Layout.preferredHeight: mediaSizeHelper.currentSize.height
|
||||
|
||||
fillMode: VideoOutput.PreserveAspectFit
|
||||
|
||||
Component.onDestruction: root.stop()
|
||||
Component.onCompleted: {
|
||||
if (NeoChatConfig.hideImages && !Controller.isImageShown(root.eventId)) {
|
||||
root.state = "hidden";
|
||||
}
|
||||
}
|
||||
|
||||
states: [
|
||||
State {
|
||||
name: "notDownloaded"
|
||||
when: !root.fileTransferInfo.completed && !root.fileTransferInfo.active
|
||||
PropertyChanges {
|
||||
target: videoLabel
|
||||
visible: true
|
||||
}
|
||||
PropertyChanges {
|
||||
target: mediaThumbnail
|
||||
visible: true
|
||||
}
|
||||
},
|
||||
State {
|
||||
name: "downloading"
|
||||
when: root.fileTransferInfo.active && !root.fileTransferInfo.completed && (Controller.isImageShown(root.eventId) || !NeoChatConfig.hideImages)
|
||||
PropertyChanges {
|
||||
target: downloadBar
|
||||
visible: true
|
||||
}
|
||||
},
|
||||
State {
|
||||
name: "paused"
|
||||
when: root.fileTransferInfo.completed && root.playbackState === MediaPlayer.PausedState && (Controller.isImageShown(root.eventId) || !NeoChatConfig.hideImages)
|
||||
PropertyChanges {
|
||||
target: videoControls
|
||||
stateVisible: true
|
||||
}
|
||||
PropertyChanges {
|
||||
target: playButton
|
||||
icon.name: "media-playback-start"
|
||||
onClicked: {
|
||||
MediaManager.startPlayback();
|
||||
root.play();
|
||||
}
|
||||
}
|
||||
},
|
||||
State {
|
||||
name: "playing"
|
||||
when: root.fileTransferInfo.completed && root.playbackState === MediaPlayer.PlayingState && (Controller.isImageShown(root.eventId) || !NeoChatConfig.hideImages)
|
||||
PropertyChanges {
|
||||
target: videoControls
|
||||
stateVisible: true
|
||||
}
|
||||
PropertyChanges {
|
||||
target: playButton
|
||||
icon.name: "media-playback-pause"
|
||||
onClicked: root.pause()
|
||||
}
|
||||
},
|
||||
State {
|
||||
name: "stopped"
|
||||
when: root.fileTransferInfo.completed && root.playbackState === MediaPlayer.StoppedState && (Controller.isImageShown(root.eventId) || !NeoChatConfig.hideImages)
|
||||
PropertyChanges {
|
||||
target: videoControls
|
||||
stateVisible: true
|
||||
}
|
||||
PropertyChanges {
|
||||
target: mediaThumbnail
|
||||
visible: true
|
||||
}
|
||||
PropertyChanges {
|
||||
target: videoLabel
|
||||
visible: true
|
||||
}
|
||||
PropertyChanges {
|
||||
target: playButton
|
||||
icon.name: "media-playback-start"
|
||||
onClicked: {
|
||||
MediaManager.startPlayback();
|
||||
root.play();
|
||||
}
|
||||
}
|
||||
},
|
||||
State {
|
||||
name: "hidden"
|
||||
PropertyChanges {
|
||||
target: mediaThumbnail
|
||||
visible: false
|
||||
}
|
||||
PropertyChanges {
|
||||
target: videoControls
|
||||
visible: false
|
||||
}
|
||||
PropertyChanges {
|
||||
target: hidden
|
||||
visible: true
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
Connections {
|
||||
target: MediaManager
|
||||
function onPlaybackStarted() {
|
||||
if (root.playbackState === MediaPlayer.PlayingState) {
|
||||
root.pause();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QQC2.Button {
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
visible: root.state !== "hidden"
|
||||
icon.name: "view-hidden"
|
||||
text: i18nc("@action:button", "Hide Image")
|
||||
display: QQC2.Button.IconOnly
|
||||
z: 10
|
||||
onClicked: {
|
||||
root.state = "hidden"
|
||||
Controller.markImageHidden(root.eventId)
|
||||
}
|
||||
|
||||
QQC2.ToolTip.text: text
|
||||
QQC2.ToolTip.visible: hovered
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
}
|
||||
|
||||
Image {
|
||||
id: mediaThumbnail
|
||||
anchors.fill: parent
|
||||
visible: false
|
||||
|
||||
source: visible ? root.mediaInfo.tempInfo.source : ""
|
||||
fillMode: Image.PreserveAspectFit
|
||||
}
|
||||
|
||||
QQC2.Label {
|
||||
id: videoLabel
|
||||
anchors.centerIn: parent
|
||||
|
||||
visible: false
|
||||
color: "white"
|
||||
text: i18n("Video")
|
||||
font.pixelSize: 16
|
||||
|
||||
padding: 8
|
||||
|
||||
background: Rectangle {
|
||||
radius: Kirigami.Units.smallSpacing
|
||||
color: "black"
|
||||
opacity: 0.3
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: downloadBar
|
||||
anchors.fill: parent
|
||||
visible: false
|
||||
|
||||
color: Kirigami.Theme.backgroundColor
|
||||
radius: Kirigami.Units.cornerRadius
|
||||
|
||||
QQC2.ProgressBar {
|
||||
anchors.centerIn: parent
|
||||
|
||||
width: parent.width * 0.8
|
||||
|
||||
from: 0
|
||||
to: root.fileTransferInfo.total
|
||||
value: root.fileTransferInfo.progress
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: hidden
|
||||
anchors.fill: parent
|
||||
|
||||
visible: false
|
||||
color: "#BB000000"
|
||||
|
||||
QQC2.Button {
|
||||
anchors.centerIn: parent
|
||||
text: i18nc("@action:button", "Show Video")
|
||||
onClicked: {
|
||||
root.state = "notDownloaded";
|
||||
Controller.markImageShown(root.eventId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QQC2.Control {
|
||||
id: videoControls
|
||||
property bool stateVisible: false
|
||||
|
||||
anchors.bottom: root.bottom
|
||||
anchors.left: root.left
|
||||
anchors.right: root.right
|
||||
visible: stateVisible && (videoHoverHandler.hovered || volumePopupHoverHandler.hovered || volumeSlider.hovered || videoControlTimer.running)
|
||||
|
||||
contentItem: RowLayout {
|
||||
id: controlRow
|
||||
QQC2.ToolButton {
|
||||
id: playButton
|
||||
}
|
||||
QQC2.Slider {
|
||||
Layout.fillWidth: true
|
||||
from: 0
|
||||
to: root.duration
|
||||
value: root.position
|
||||
onMoved: root.seek(value)
|
||||
}
|
||||
QQC2.Label {
|
||||
text: Format.formatDuration(root.position) + "/" + Format.formatDuration(root.duration)
|
||||
}
|
||||
QQC2.ToolButton {
|
||||
id: volumeButton
|
||||
property var unmuteVolume: root.volume
|
||||
|
||||
icon.name: root.volume <= 0 ? "player-volume-muted" : "player-volume"
|
||||
|
||||
QQC2.ToolTip.visible: hovered
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
QQC2.ToolTip.timeout: Kirigami.Units.toolTipDelay
|
||||
QQC2.ToolTip.text: i18nc("@action:button", "Volume")
|
||||
|
||||
onClicked: {
|
||||
if (root.volume > 0) {
|
||||
root.volume = 0;
|
||||
} else {
|
||||
if (unmuteVolume === 0) {
|
||||
root.volume = 1;
|
||||
} else {
|
||||
root.volume = unmuteVolume;
|
||||
}
|
||||
}
|
||||
}
|
||||
onHoveredChanged: {
|
||||
if (!hovered && (root.state === "paused" || root.state === "stopped" || root.state === "playing")) {
|
||||
videoControlTimer.restart();
|
||||
volumePopupTimer.restart();
|
||||
}
|
||||
}
|
||||
|
||||
QQC2.Popup {
|
||||
id: volumePopup
|
||||
y: -height
|
||||
width: volumeButton.width
|
||||
visible: videoControls.stateVisible && (volumeButton.hovered || volumePopupHoverHandler.hovered || volumeSlider.hovered || volumePopupTimer.running)
|
||||
|
||||
focus: true
|
||||
padding: Kirigami.Units.smallSpacing
|
||||
closePolicy: QQC2.Popup.NoAutoClose
|
||||
|
||||
QQC2.Slider {
|
||||
id: volumeSlider
|
||||
anchors.centerIn: parent
|
||||
implicitHeight: Kirigami.Units.gridUnit * 7
|
||||
orientation: Qt.Vertical
|
||||
padding: 0
|
||||
from: 0
|
||||
to: 1
|
||||
value: root.volume
|
||||
onMoved: {
|
||||
root.volume = value;
|
||||
volumeButton.unmuteVolume = value;
|
||||
}
|
||||
onHoveredChanged: {
|
||||
if (!hovered && (root.state === "paused" || root.state === "stopped" || root.state === "playing")) {
|
||||
videoControlTimer.restart();
|
||||
volumePopupTimer.restart();
|
||||
}
|
||||
}
|
||||
}
|
||||
Timer {
|
||||
id: volumePopupTimer
|
||||
interval: 500
|
||||
}
|
||||
HoverHandler {
|
||||
id: volumePopupHoverHandler
|
||||
onHoveredChanged: {
|
||||
if (!hovered && (root.state === "paused" || root.state === "stopped" || root.state === "playing")) {
|
||||
videoControlTimer.restart();
|
||||
volumePopupTimer.restart();
|
||||
}
|
||||
}
|
||||
}
|
||||
background: Kirigami.ShadowedRectangle {
|
||||
radius: Kirigami.Units.cornerRadius
|
||||
color: Kirigami.Theme.backgroundColor
|
||||
opacity: 0.8
|
||||
|
||||
property color borderColor: Kirigami.Theme.textColor
|
||||
border.color: Qt.rgba(borderColor.r, borderColor.g, borderColor.b, 0.3)
|
||||
border.width: 1
|
||||
|
||||
shadow.xOffset: 0
|
||||
shadow.yOffset: 4
|
||||
shadow.color: Qt.rgba(0, 0, 0, 0.3)
|
||||
shadow.size: 8
|
||||
}
|
||||
}
|
||||
}
|
||||
QQC2.ToolButton {
|
||||
id: maximizeButton
|
||||
display: QQC2.AbstractButton.IconOnly
|
||||
|
||||
action: Kirigami.Action {
|
||||
text: i18n("Maximize")
|
||||
icon.name: "view-fullscreen"
|
||||
onTriggered: {
|
||||
root.Message.timeline.interactive = false;
|
||||
root.pause();
|
||||
// We need to make sure the index is that of the MediaMessageFilterModel.
|
||||
if (root.Message.timeline.model instanceof MessageFilterModel) {
|
||||
RoomManager.maximizeMedia(RoomManager.mediaMessageFilterModel.getRowForSourceItem(root.Message.index));
|
||||
} else {
|
||||
RoomManager.maximizeMedia(root.Message.index);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
background: Kirigami.ShadowedRectangle {
|
||||
radius: Kirigami.Units.cornerRadius
|
||||
color: Kirigami.Theme.backgroundColor
|
||||
opacity: 0.8
|
||||
|
||||
property color borderColor: Kirigami.Theme.textColor
|
||||
border.color: Qt.rgba(borderColor.r, borderColor.g, borderColor.b, 0.3)
|
||||
border.width: 1
|
||||
|
||||
shadow.xOffset: 0
|
||||
shadow.yOffset: 4
|
||||
shadow.color: Qt.rgba(0, 0, 0, 0.3)
|
||||
shadow.size: 8
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: videoControlTimer
|
||||
interval: 1000
|
||||
}
|
||||
HoverHandler {
|
||||
id: videoHoverHandler
|
||||
onHoveredChanged: {
|
||||
if (!hovered && (root.state === "paused" || root.state === "stopped" || root.state === "playing")) {
|
||||
videoControlTimer.restart();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TapHandler {
|
||||
acceptedButtons: Qt.LeftButton
|
||||
gesturePolicy: TapHandler.ReleaseWithinBounds | TapHandler.WithinBounds
|
||||
onTapped: if (root.fileTransferInfo.completed) {
|
||||
if (root.playbackState == MediaPlayer.PlayingState) {
|
||||
root.pause();
|
||||
} else {
|
||||
MediaManager.startPlayback();
|
||||
root.play();
|
||||
}
|
||||
} else {
|
||||
root.downloadAndPlay();
|
||||
}
|
||||
}
|
||||
|
||||
MediaSizeHelper {
|
||||
id: mediaSizeHelper
|
||||
contentMaxWidth: root.Message.maxContentWidth
|
||||
mediaWidth: root.mediaInfo.width
|
||||
mediaHeight: root.mediaInfo.height
|
||||
}
|
||||
|
||||
function downloadAndPlay() {
|
||||
if (root.downloaded) {
|
||||
playSavedFile();
|
||||
} else {
|
||||
playOnFinished = true;
|
||||
Message.room.downloadFile(root.eventId, Core.StandardPaths.writableLocation(Core.StandardPaths.CacheLocation) + "/" + root.eventId.replace(":", "_").replace("/", "_").replace("+", "_") + Message.room.fileNameToDownload(root.eventId));
|
||||
}
|
||||
}
|
||||
|
||||
function playSavedFile() {
|
||||
root.stop();
|
||||
MediaManager.startPlayback();
|
||||
root.play();
|
||||
}
|
||||
}
|
||||
8
src/messagecontent/config-neochat.h.in
Normal file
@@ -0,0 +1,8 @@
|
||||
/*
|
||||
SPDX-FileCopyrightText: 2024 Tobias Fella <tobias.fella@kde.org>
|
||||
SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#define CMAKE_INSTALL_FULL_LIBEXECDIR_KF6 "${KDE_INSTALL_FULL_LIBEXECDIR_KF}"
|
||||
125
src/messagecontent/contentprovider.cpp
Normal file
@@ -0,0 +1,125 @@
|
||||
// SPDX-FileCopyrightText: 2025 James Graham <james.h.graham@protonmail.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
|
||||
#include "contentprovider.h"
|
||||
|
||||
ContentProvider::ContentProvider(QObject *parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
}
|
||||
|
||||
ContentProvider &ContentProvider::self()
|
||||
{
|
||||
static ContentProvider instance;
|
||||
return instance;
|
||||
}
|
||||
|
||||
MessageContentModel *ContentProvider::contentModelForEvent(NeoChatRoom *room, const QString &evtOrTxnId, bool isReply)
|
||||
{
|
||||
if (!room || evtOrTxnId.isEmpty()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (!m_eventContentModels.contains(evtOrTxnId)) {
|
||||
auto model = new MessageContentModel(room, evtOrTxnId, isReply);
|
||||
QQmlEngine::setObjectOwnership(model, QQmlEngine::CppOwnership);
|
||||
m_eventContentModels.insert(evtOrTxnId, model);
|
||||
}
|
||||
|
||||
return m_eventContentModels.object(evtOrTxnId);
|
||||
}
|
||||
|
||||
MessageContentModel *ContentProvider::contentModelForEvent(NeoChatRoom *room, const Quotient::RoomEvent *event, bool isReply)
|
||||
{
|
||||
if (!room) {
|
||||
return nullptr;
|
||||
}
|
||||
const auto roomMessageEvent = eventCast<const Quotient::RoomMessageEvent>(event);
|
||||
if (roomMessageEvent == nullptr) {
|
||||
// If for some reason a model is there remove.
|
||||
if (m_eventContentModels.contains(event->id())) {
|
||||
m_eventContentModels.remove(event->id());
|
||||
}
|
||||
if (m_eventContentModels.contains(event->transactionId())) {
|
||||
m_eventContentModels.remove(event->transactionId());
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (event->isStateEvent() || event->matrixType() == u"org.matrix.msc3672.beacon_info"_s) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto eventId = event->id();
|
||||
const auto txnId = event->transactionId();
|
||||
if (!m_eventContentModels.contains(eventId) && !m_eventContentModels.contains(txnId)) {
|
||||
auto model = new MessageContentModel(room, eventId.isEmpty() ? txnId : eventId, isReply, eventId.isEmpty());
|
||||
QQmlEngine::setObjectOwnership(model, QQmlEngine::CppOwnership);
|
||||
m_eventContentModels.insert(eventId.isEmpty() ? txnId : eventId, model);
|
||||
}
|
||||
|
||||
if (!eventId.isEmpty() && m_eventContentModels.contains(eventId)) {
|
||||
return m_eventContentModels.object(eventId);
|
||||
}
|
||||
|
||||
if (!txnId.isEmpty() && m_eventContentModels.contains(txnId)) {
|
||||
if (eventId.isEmpty()) {
|
||||
return m_eventContentModels.object(txnId);
|
||||
}
|
||||
|
||||
// If we now have an event ID use that as the map key instead of transaction ID.
|
||||
auto txnModel = m_eventContentModels.take(txnId);
|
||||
m_eventContentModels.insert(eventId, txnModel);
|
||||
return m_eventContentModels.object(eventId);
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
ThreadModel *ContentProvider::modelForThread(NeoChatRoom *room, const QString &threadRootId)
|
||||
{
|
||||
if (!room || threadRootId.isEmpty()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (!m_threadModels.contains(threadRootId)) {
|
||||
auto model = new ThreadModel(threadRootId, room);
|
||||
QQmlEngine::setObjectOwnership(model, QQmlEngine::CppOwnership);
|
||||
m_threadModels.insert(threadRootId, model);
|
||||
}
|
||||
|
||||
return m_threadModels.object(threadRootId);
|
||||
}
|
||||
|
||||
static PollHandler *emptyPollHandler = new PollHandler;
|
||||
|
||||
PollHandler *ContentProvider::handlerForPoll(NeoChatRoom *room, const QString &eventId)
|
||||
{
|
||||
if (!room || eventId.isEmpty()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const auto event = room->getEvent(eventId);
|
||||
if (event.first == nullptr || event.second) {
|
||||
return emptyPollHandler;
|
||||
}
|
||||
|
||||
if (!m_pollHandlers.contains(eventId)) {
|
||||
auto pollHandler = new PollHandler(room, eventId);
|
||||
QQmlEngine::setObjectOwnership(pollHandler, QQmlEngine::CppOwnership);
|
||||
m_pollHandlers.insert(eventId, pollHandler);
|
||||
}
|
||||
|
||||
return m_pollHandlers.object(eventId);
|
||||
}
|
||||
|
||||
void ContentProvider::setThreadsEnabled(bool enableThreads)
|
||||
{
|
||||
MessageContentModel::setThreadsEnabled(enableThreads);
|
||||
|
||||
for (const auto &key : m_eventContentModels.keys()) {
|
||||
m_eventContentModels.object(key)->threadsEnabledChanged();
|
||||
}
|
||||
}
|
||||
|
||||
#include "moc_contentprovider.cpp"
|
||||
92
src/messagecontent/contentprovider.h
Normal file
@@ -0,0 +1,92 @@
|
||||
// SPDX-FileCopyrightText: 2025 James Graham <james.h.graham@protonmail.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QCache>
|
||||
#include <QObject>
|
||||
#include <QQmlEngine>
|
||||
|
||||
#include "models/messagecontentmodel.h"
|
||||
#include "models/threadmodel.h"
|
||||
#include "neochatroom.h"
|
||||
#include "pollhandler.h"
|
||||
|
||||
/**
|
||||
* @class ContentProvider
|
||||
*
|
||||
* Store and retrieve models for message content.
|
||||
*/
|
||||
class ContentProvider : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
QML_SINGLETON
|
||||
|
||||
public:
|
||||
/**
|
||||
* Get the global instance of ContentProvider.
|
||||
*/
|
||||
static ContentProvider &self();
|
||||
static ContentProvider *create(QQmlEngine *engine, QJSEngine *)
|
||||
{
|
||||
engine->setObjectOwnership(&self(), QQmlEngine::CppOwnership);
|
||||
return &self();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Returns the content model for the given event ID.
|
||||
*
|
||||
* A model is created if one doesn't exist. Will return nullptr if evtOrTxnId
|
||||
* is empty.
|
||||
*
|
||||
* @warning If a non-empty ID is given it is assumed to be a valid Quotient::RoomMessageEvent
|
||||
* event ID. The caller must ensure that the ID is a real event. A model will be
|
||||
* returned unconditionally.
|
||||
*
|
||||
* @warning Do NOT use for pending events as this function has no way to differentiate.
|
||||
*/
|
||||
Q_INVOKABLE MessageContentModel *contentModelForEvent(NeoChatRoom *room, const QString &evtOrTxnId, bool isReply = false);
|
||||
|
||||
/**
|
||||
* @brief Returns the content model for the given event.
|
||||
*
|
||||
* A model is created if one doesn't exist. Will return nullptr if event is:
|
||||
* - nullptr
|
||||
* - not a Quotient::RoomMessageEvent (e.g a state event)
|
||||
*
|
||||
* @note This method is preferred to the version using just an event ID as it
|
||||
* can perform some basic checks. If a copy of the event is not available,
|
||||
* you may have to use the version that takes an event ID.
|
||||
*
|
||||
* @note This version must be used for pending events as it can differentiate.
|
||||
*/
|
||||
MessageContentModel *contentModelForEvent(NeoChatRoom *room, const Quotient::RoomEvent *event, bool isReply = false);
|
||||
|
||||
/**
|
||||
* @brief Returns the thread model for the given thread root event ID.
|
||||
*
|
||||
* A model is created if one doesn't exist. Will return nullptr if threadRootId
|
||||
* is empty.
|
||||
*/
|
||||
ThreadModel *modelForThread(NeoChatRoom *room, const QString &threadRootId);
|
||||
|
||||
/**
|
||||
* @brief Get a PollHandler object for the given event Id.
|
||||
*
|
||||
* Will return an existing PollHandler if one already exists for the event ID.
|
||||
* A new PollHandler will be created if one doesn't exist.
|
||||
*
|
||||
* @sa PollHandler
|
||||
*/
|
||||
Q_INVOKABLE PollHandler *handlerForPoll(NeoChatRoom *room, const QString &eventId);
|
||||
|
||||
void setThreadsEnabled(bool enableThreads);
|
||||
|
||||
private:
|
||||
explicit ContentProvider(QObject *parent = nullptr);
|
||||
|
||||
QCache<QString, MessageContentModel> m_eventContentModels;
|
||||
QCache<QString, ThreadModel> m_threadModels;
|
||||
QCache<QString, PollHandler> m_pollHandlers;
|
||||
};
|
||||
19
src/messagecontent/images/bike.svg
Normal file
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22">
|
||||
<defs id="defs3051">
|
||||
<style type="text/css" id="current-color-scheme">
|
||||
.ColorScheme-Text {
|
||||
color:#4d4d4d;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<g
|
||||
transform="matrix(0.00938989,0,0,-0.00938989,-1.1929841,18.425545)"
|
||||
id="g4">
|
||||
<path
|
||||
sodipodi:nodetypes="sssccscsssssssssssccsccccsccccssssssssscsccssccsssssssscscccssssccsssssascccssssscssssssccscssssssssscsssccssscssssscccccccscccsccc"
|
||||
style="color:#4d4d4d;fill:currentColor;fill-opacity:1;stroke:none" class="ColorScheme-Text"
|
||||
d="M 2251.5,232 C 2165.8333,146.66667 2063,104 1943,104 c -114,0 -212.8333,38.5 -296.5,115.5 -83.6667,77 -130.1667,170.83333 -139.5,281.5 h -122 c -3.3333,-13.33333 -5.8333,-25.83333 -7.5,-37.5 -1.6667,-11.66667 -4.1667,-23.5 -7.5,-35.5 20.6667,0 37.6667,-4 51,-12 13.3333,-8 20,-22.66667 20,-44 0,-18.66667 -7.6667,-32.5 -23,-41.5 -15.3333,-9 -39,-13.5 -71,-13.5 -10.6667,0 -20.8333,0.33333 -30.5,1 -9.6667,0.66667 -18.1667,1 -25.5,1 h -21 c -22.6667,0 -38.1667,3.83333 -46.5,11.5 -8.3333,7.66667 -12.5,19.83333 -12.5,36.5 0,18 9.3333,33.16667 28,45.5 18.6667,12.33333 38,18.5 58,18.5 h 6 l 5,-1 c 3.3333,13.33333 5.5,25.33333 6.5,36 1,10.66667 3.1667,22.33333 6.5,35 h -14 l -6,1 h -9 c -2,1.33333 -5.5,2.83333 -10.5,4.5 -5,1.66667 -11.1667,7.5 -18.5,17.5 L 962,972 c -4,-15.33333 -8.66667,-31 -14,-47 l -16,-47 c 49.33333,-40.66667 88.5,-90.16667 117.5,-148.5 C 1078.5,671.16667 1093,607.66667 1093,539 1093,420.33333 1050.3333,318.16667 965,232.5 879.66667,146.83333 776.66667,104 656,104 535.33333,104 432.33333,146.83333 347,232.5 261.66667,318.16667 219,420.33333 219,539 c 0,120 43,222.83333 129,308.5 86,85.66667 188.66667,128.5 308,128.5 33.33333,0 65.33333,-3.5 96,-10.5 30.66667,-7 60,-17.16667 88,-30.5 15.33333,44 28,88.6667 38,134 10,45.3333 19.33333,90.3333 28,135 l 4,22 c 14,82 34.83333,131.8333 62.5,149.5 27.6667,17.6667 48.8333,31.3333 63.5,41 14.6667,9.6667 61.3333,14.5 140,14.5 l 61,-1 c 16,0 30.8333,-7.1667 44.5,-21.5 13.6667,-14.3333 20.5,-30.8333 20.5,-49.5 0,-20.6667 -10,-38.8333 -30,-54.5 -20,-15.6667 -52,-23.5 -96,-23.5 h -51 c -9.3333,0 -18.8333,-0.5 -28.5,-1.5 -9.6667,-1 -18.1667,-1.5 -25.5,-1.5 -6.6667,0 -13,0.8333 -19,2.5 -6,1.6667 -11.3333,4.5 -16,8.5 -5.3333,-6.6667 -9.5,-16.1667 -12.5,-28.5 -3,-12.3333 -6.8333,-29.8333 -11.5,-52.5 l -15,-81 h 443 c -34.6667,4 -62.5,12 -83.5,24 -21,12 -31.5,27 -31.5,45 0,16.6667 6,32 18,46 12,14 27.3333,21.8333 46,23.5 18.6667,1.6667 42,5.1667 70,10.5 l 75,13 c 24.6667,4 49,7.5 73,10.5 24,3 47,4.5 69,4.5 22.6667,0 42.5,-8.8333 59.5,-26.5 17,-17.6667 25.5,-38.1667 25.5,-61.5 0,-27.3333 -12.6667,-51.5 -38,-72.5 -25.3333,-21 -53.2102,-26.1605 -82,-31.5 -5.8994,-1.0941 -12.0021,-0.1577 -18,0 -6.3399,0.1667 -12.3333,0.3333 -19,1 v -21 l 126,-170 c 32,17.33333 65.8333,30.66667 101.5,40 35.6667,9.33333 72.8333,14 111.5,14 118.6667,0 221.1667,-42.66667 307.5,-128 86.3333,-85.33333 129.5,-188.33333 129.5,-309 0,-119.33333 -42.8333,-221.66667 -128.5,-307 z M 885,764 C 850.33333,690.66667 811.5,628.83333 768.5,578.5 725.5,528.16667 700,500 692,494 c -8,-6 -19,-9 -33,-9 -14.66667,0 -27.66667,5.16667 -39,15.5 -11.33333,10.33333 -17,23.16667 -17,38.5 0,9.33333 2.16667,18 6.5,26 4.33333,8 11.16667,14.66667 20.5,20 l 7,5 c 36,36 66.83333,73.66667 92.5,113 25.66667,39.33333 48.16667,80.66667 67.5,124 -21.33333,10.66667 -43.83333,18.83333 -67.5,24.5 -23.66667,5.66667 -48.16667,8.5 -73.5,8.5 -86.66667,0 -161.66667,-31.33333 -225,-94 -63.33333,-62.66667 -95,-138.33333 -95,-227 0,-88 31.33333,-163.33333 94,-226 62.66667,-62.66667 138,-94 226,-94 88.66667,0 164.33333,31.66667 227,95 62.66667,63.33333 94,138.33333 94,225 0,44 -8.33333,85 -25,123 -16.66667,38 -39,72 -67,102 z m 1283,1 c -63.3333,63.33333 -138.3333,95 -225,95 -25.3333,0 -50,-2.83333 -74,-8.5 -24,-5.66667 -46.3333,-13.83333 -67,-24.5 l 179,-242 c 6.6667,-9.33333 10,-20 10,-32 0,-14.66667 -5.1667,-27 -15.5,-37 -10.3333,-10 -22.5,-15 -36.5,-15 h -314 c 9.3333,-80.66667 44.3333,-147.83333 105,-201.5 60.6667,-53.66667 131.6667,-80.5 213,-80.5 88,0 163.3333,31.66667 226,95 62.6667,63.33333 94,138.33333 94,225 0,87.33333 -31.6667,162.66667 -95,226 z m -1113,257 244,-362 170,362 z m 507,-50 -171,-365 h 120 c 7.3333,49.33333 22.5,95.5 45.5,138.5 23,43 52.5,81.16667 88.5,114.5 z m 153,-207 c -22,-21.33333 -40.3333,-45.33333 -55,-72 -14.6667,-26.66667 -25,-55.33333 -31,-86 h 205 z"
|
||||
id="path2" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.4 KiB |
2
src/messagecontent/images/bike.svg.license
Normal file
@@ -0,0 +1,2 @@
|
||||
SPDX-License-Identifier: CC0-1.0
|
||||
SPDX-FileCopyrightText: Volker Krause <vkrause@kde.org>
|
||||
16
src/messagecontent/images/bus.svg
Normal file
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22">
|
||||
<defs id="defs3051">
|
||||
<style
|
||||
type="text/css"
|
||||
id="current-color-scheme">
|
||||
.ColorScheme-Text {
|
||||
color:#4d4d4d;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<path
|
||||
style="color:#4d4d4d;fill:currentColor;fill-opacity:1;stroke:none" class="ColorScheme-Text"
|
||||
d="m 18.679188,11.502469 0.02034,0.569593 v 5.197534 q 0,0.23394 -0.16274,0.401766 -0.162741,0.167827 -0.396681,0.167827 h -0.925588 v 1.403639 q 0,0.406852 -0.274626,0.686563 -0.274625,0.279711 -0.681477,0.279711 -0.39668,0 -0.676391,-0.284797 -0.279711,-0.284796 -0.279711,-0.681477 V 17.839189 H 6.7075677 v 1.403639 q 0,0.406852 -0.2847964,0.686563 -0.2847964,0.279711 -0.6713058,0.279711 -0.3966807,0 -0.6763915,-0.284797 Q 4.7953633,19.639509 4.7953633,19.242828 V 17.839189 H 3.869775 q -0.2339399,0 -0.3966807,-0.167827 -0.1627408,-0.167826 -0.1627408,-0.401766 v -4.627942 l 0.010171,-0.569592 q 0,-0.335653 0.020343,-0.569593 L 3.7375481,4.1079337 Q 3.7578907,3.8231373 3.9308028,3.4773131 4.1037149,3.1314889 4.9326758,2.7144656 5.7616368,2.2974423 7.2822461,1.982132 8.8028555,1.6668217 10.908315,1.6668217 q 1.810491,0 3.290415,0.2491969 1.479924,0.2491968 2.558082,0.7069053 1.078158,0.4577085 1.28667,0.813704 0.208511,0.3559955 0.239025,0.6713058 z M 7.2771605,3.0501185 q -0.2644538,0 -0.4526229,0.188169 -0.188169,0.1881691 -0.188169,0.4526229 0,0.2644538 0.188169,0.4475372 Q 7.0127067,4.321531 7.2771605,4.321531 H 14.54964 q 0.264454,0 0.447537,-0.1830834 0.183084,-0.1830834 0.183084,-0.4475372 0,-0.2644538 -0.183084,-0.4526229 -0.183083,-0.188169 -0.447537,-0.188169 z M 4.7445068,12.194117 H 17.275548 L 17.082294,5.1860915 H 4.9275902 Z m 1.8257483,2.130888 q -0.3203959,-0.320396 -0.7577618,-0.320396 -0.4475372,0 -0.7628475,0.325481 -0.3153103,0.325482 -0.3153103,0.752676 0,0.427195 0.3153103,0.752677 0.3153103,0.325481 0.7628475,0.325481 0.4373659,0 0.7577618,-0.320396 0.320396,-0.320396 0.320396,-0.757762 0,-0.437365 -0.320396,-0.757761 z m 10.4611819,0.0051 q -0.31531,-0.325481 -0.752676,-0.325481 -0.447537,0 -0.762847,0.325481 -0.315311,0.325482 -0.315311,0.752676 0,0.427195 0.315311,0.752677 0.31531,0.325481 0.762847,0.325481 0.437366,0 0.752676,-0.325481 0.31531,-0.325482 0.31531,-0.752677 0,-0.427194 -0.31531,-0.752676 z"
|
||||
id="path4749" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.3 KiB |
2
src/messagecontent/images/bus.svg.license
Normal file
@@ -0,0 +1,2 @@
|
||||
SPDX-License-Identifier: CC0-1.0
|
||||
SPDX-FileCopyrightText: Volker Krause <vkrause@kde.org>
|
||||
16
src/messagecontent/images/cablecar.svg
Normal file
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22">
|
||||
<defs id="defs3051">
|
||||
<style
|
||||
type="text/css"
|
||||
id="current-color-scheme">
|
||||
.ColorScheme-Text {
|
||||
color:#4d4d4d;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<path
|
||||
style="color:#4d4d4d;fill:currentColor;fill-opacity:1;stroke:none" class="ColorScheme-Text"
|
||||
d="m 14.879482,5.2406591 q -0.07353,0.1562559 -0.216,0.2527668 -0.142469,0.096511 -0.326299,0.096511 h -1.700431 l 1.645282,5.1104851 h 2.803413 q 0.23898,0 0.436597,0.174639 0.197618,0.174639 0.243576,0.413618 l 0.661789,4.255674 q 0,0.07353 0.0092,0.160852 0.0092,0.08732 0.0092,0.188426 0,0.18383 -0.02298,0.413618 -0.02298,0.229788 -0.06894,0.386044 l -0.523917,1.939411 q -0.06434,0.238979 -0.284937,0.404427 -0.220596,0.165447 -0.459576,0.165447 H 5.0629391 q -0.2481711,0 -0.4733633,-0.165447 Q 4.3643835,18.871683 4.2908514,18.641895 L 3.6842111,16.684102 q -0.045958,-0.147065 -0.068936,-0.349278 -0.022979,-0.202214 -0.022979,-0.404427 0,-0.220597 0.027575,-0.386044 L 4.3643835,11.288679 Q 4.4103411,11.0497 4.6125546,10.875061 4.814768,10.700422 5.0629391,10.700422 h 3.02401 L 9.6035499,5.5899369 H 7.6825223 q -0.1838304,0 -0.3217032,-0.096511 Q 7.2229463,5.396915 7.1494141,5.2406591 H 0.26496562 V 4.6432103 H 7.094265 V 4.5237206 q 0,-0.2389796 0.1746389,-0.4136184 0.1746388,-0.1746389 0.4136184,-0.1746389 h 1.0110672 q 0.248171,0 0.4182141,-0.1746389 Q 9.2818467,3.5861855 9.2818467,3.3380145 V 3.2001417 q 0,-0.2389795 0.1792347,-0.4136184 Q 9.640316,2.6118844 9.8792955,2.6118844 h 2.2703055 q 0.248171,0 0.418214,0.1746389 0.170043,0.1746389 0.170043,0.4136184 v 0.1378728 q 0,0.248171 0.179235,0.4228099 0.179235,0.1746389 0.418214,0.1746389 h 1.001876 q 0.248171,0 0.42281,0.1746389 0.174639,0.1746388 0.174639,0.4136184 v 0.1194897 h 6.829299 v 0.5974488 z m 0.03677,9.9636079 q 0,0.156256 0.101107,0.257362 0.101107,0.101107 0.257363,0.101107 h 1.957794 q 0.128681,0 0.216,-0.07813 0.08732,-0.07813 0.08732,-0.206809 l -0.42281,-2.739073 q -0.02758,-0.156256 -0.142469,-0.257363 -0.114894,-0.101106 -0.261958,-0.101106 h -1.433877 q -0.156256,0 -0.257363,0.105702 -0.101107,0.105703 -0.101107,0.252767 z M 10.56866,4.3766562 q 0.18383,0.1838304 0.450384,0.1838304 0.257363,0 0.441193,-0.1838304 0.18383,-0.1838304 0.18383,-0.4411929 0,-0.2665541 -0.18383,-0.4503845 -0.18383,-0.1838304 -0.441193,-0.1838304 -0.266554,0 -0.450384,0.1838304 -0.183831,0.1838304 -0.183831,0.4503845 0,0.2573625 0.183831,0.4411929 z m -0.42281,3.7685232 h 2.003751 L 11.313173,5.5899369 H 10.908746 Z M 7.112648,12.538726 q 0,-0.147064 -0.1011067,-0.252767 -0.1011067,-0.105702 -0.248171,-0.105702 H 5.4122168 q -0.1562558,0 -0.2757456,0.101106 -0.1194897,0.101107 -0.1470643,0.257363 l -0.477959,2.729881 q 0,0.128682 0.087319,0.211405 0.087319,0.08272 0.2160007,0.08272 h 1.9486023 q 0.1470643,0 0.248171,-0.101107 0.1011067,-0.101106 0.1011067,-0.257362 z m 3.456012,0 q 0,-0.147064 -0.105703,-0.252767 -0.105702,-0.105702 -0.252767,-0.105702 H 8.3718863 q -0.1470644,0 -0.2527668,0.105702 -0.1057025,0.105703 -0.1057025,0.252767 v 2.665541 q 0,0.156256 0.1057025,0.257362 0.1057024,0.101107 0.2527668,0.101107 H 10.21019 q 0.147065,0 0.252767,-0.101107 0.105703,-0.101106 0.105703,-0.257362 z m 3.437628,0 q 0,-0.147064 -0.101107,-0.252767 -0.101106,-0.105702 -0.257362,-0.105702 h -1.829113 q -0.147064,0 -0.248171,0.105702 -0.101107,0.105703 -0.101107,0.252767 v 2.665541 q 0,0.156256 0.101107,0.257362 0.101107,0.101107 0.248171,0.101107 h 1.829113 q 0.156256,0 0.257362,-0.101107 0.101107,-0.101106 0.101107,-0.257362 z M 12.416155,8.9816078 H 9.9068701 L 9.392145,10.700422 h 3.575501 z"
|
||||
id="path4751" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.6 KiB |
2
src/messagecontent/images/cablecar.svg.license
Normal file
@@ -0,0 +1,2 @@
|
||||
SPDX-License-Identifier: CC0-1.0
|
||||
SPDX-FileCopyrightText: Volker Krause <vkrause@kde.org>
|
||||
14
src/messagecontent/images/car.svg
Normal file
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22">
|
||||
<defs id="defs3051">
|
||||
<style type="text/css" id="current-color-scheme">
|
||||
.ColorScheme-Text {
|
||||
color:#4d4d4d;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<path
|
||||
style="color:#4d4d4d;fill:currentColor;fill-opacity:1;stroke:none" class="ColorScheme-Text"
|
||||
d="m 20.363699,14.382151 q -0.682766,0.758106 -1.690435,0.758106 h -0.131845 q 0,-0.875825 -0.640388,-1.520921 -0.640388,-0.645097 -1.544465,-0.645097 -0.89466,0 -1.535048,0.635679 -0.640388,0.635679 -0.640388,1.530339 H 7.5041442 q 0,-0.89466 -0.640388,-1.530339 -0.6403879,-0.635679 -1.5350476,-0.635679 -0.9040771,0 -1.5444651,0.645097 -0.6403879,0.645096 -0.6403879,1.520921 -0.7157278,0 -1.4738341,-0.645097 -0.75810633,-0.645096 -0.75810633,-1.841115 0,-0.809902 0.31548523,-1.497378 Q 1.5428857,10.469192 2.6541471,9.9983184 3.7654086,9.5274449 4.3163306,9.2967169 4.8672526,9.0659889 5.4511357,7.8888051 L 5.7807472,7.2389997 Q 6.4305526,5.9770587 7.4099695,5.3790494 8.3893864,4.78104 9.7172496,4.78104 h 3.5880564 q 1.33728,0 2.415581,0.5885919 1.0783,0.5885919 1.944707,1.954125 l 0.329612,0.5179609 q 0.404951,0.6403879 0.904077,1.1395139 0.499126,0.4991259 1.101844,1.0877173 0.602718,0.588592 0.824028,1.224272 0.221311,0.635679 0.221311,1.257232 0,1.073591 -0.682767,1.831698 z M 10.216376,6.683369 q 0,-0.4520386 -0.117719,-0.621553 -0.1177182,-0.1695145 -0.3719899,-0.1695145 -0.6498054,0 -1.3937856,0.3531551 Q 7.5889014,6.5986117 7.0521056,7.394388 6.5153098,8.1901642 6.5153098,8.7175425 q 0,0.2166018 0.1130097,0.3484464 0.1130096,0.1318446 0.3672813,0.1318446 h 2.5803868 q 0.2731066,0 0.4567474,-0.1412621 0.183641,-0.141262 0.183641,-0.480291 z m 5.725821,2.5144645 q 0.357864,0 0.517961,-0.1695145 0.160097,-0.1695145 0.160097,-0.4426211 0,-0.621553 -0.687475,-1.4597078 Q 15.245305,6.2878352 14.327101,6.0853596 13.408898,5.882884 12.382394,5.882884 q -0.527379,0 -0.739272,0.080049 -0.211893,0.080049 -0.287233,0.2071844 -0.07534,0.1271358 -0.07534,0.5132521 v 1.8929114 q 0,0.3484464 0.277815,0.4849998 0.277815,0.1365533 0.673349,0.1365533 z M 6.4635137,16.289188 q -0.4755822,0.480291 -1.1348051,0.480291 -0.6780578,0 -1.1536401,-0.475582 -0.4755822,-0.475582 -0.4755822,-1.15364 0,-0.659223 0.480291,-1.134805 0.4802909,-0.475582 1.1489313,-0.475582 0.6592229,0 1.1348051,0.475582 0.4755823,0.475582 0.4755823,1.134805 0,0.66864 -0.4755823,1.148931 z m 11.0466923,0.0047 q -0.475582,0.475582 -1.15364,0.475582 -0.678058,0 -1.148931,-0.480291 -0.470874,-0.480291 -0.470874,-1.148931 0,-0.649805 0.470874,-1.130096 0.470873,-0.480291 1.148931,-0.480291 0.66864,0 1.148931,0.475582 0.480291,0.475582 0.480291,1.134805 0,0.678058 -0.475582,1.15364 z"
|
||||
id="path4749" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.7 KiB |
2
src/messagecontent/images/car.svg.license
Normal file
@@ -0,0 +1,2 @@
|
||||
SPDX-License-Identifier: CC0-1.0
|
||||
SPDX-FileCopyrightText: Volker Krause <vkrause@kde.org>
|
||||
16
src/messagecontent/images/coach.svg
Normal file
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22">
|
||||
<defs id="defs3051">
|
||||
<style
|
||||
type="text/css"
|
||||
id="current-color-scheme">
|
||||
.ColorScheme-Text {
|
||||
color:#4d4d4d;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<path
|
||||
style="color:#4d4d4d;fill:currentColor;fill-opacity:1;stroke:none" class="ColorScheme-Text"
|
||||
d="m 21.126606,12.816239 q 0,0.344378 -0.231379,0.721041 -0.231379,0.376663 -0.608042,0.753326 0,0.05381 0.01076,0.112999 0.01076,0.05919 0.01076,0.123761 0,1.194559 -0.274426,1.770316 -0.274426,0.575756 -0.780231,0.575756 -0.30133,0 -0.548851,-0.242141 -0.247522,-0.24214 -0.376663,-0.823277 -0.807135,0.548852 -1.447462,0.963181 -0.640328,0.414329 -1.210703,0.629565 -0.02152,1.086942 -0.355139,1.646556 -0.333616,0.559613 -0.968562,0.559613 -0.559614,0 -0.898611,-0.446615 -0.338996,-0.446614 -0.414329,-1.329082 H 5.7372316 q -0.075333,0.89323 -0.4143293,1.334463 -0.3389967,0.441234 -0.8986103,0.441234 -0.5811372,0 -0.9201339,-0.4789 -0.3389967,-0.4789 -0.3928057,-1.4367 Q 2.2396466,17.433052 1.6746521,16.674345 1.1096576,15.915638 1.1096576,14.979361 V 4.9170782 q 0,-1.0546564 0.7479451,-1.721888 Q 2.6055478,2.5279586 3.7893458,2.5279586 H 18.94196 q 1.183798,0 1.684222,0.591899 0.500424,0.591899 0.500424,1.775697 z M 3.0683052,9.5876994 q 0,0.376663 0.2798068,0.6672316 0.2798068,0.290569 0.6779934,0.290569 h 7.9960176 q 0.398186,0 0.677993,-0.290569 0.279807,-0.2905686 0.279807,-0.6672316 v -4.30472 q 0,-0.4089484 -0.279807,-0.6833743 Q 12.420309,4.3251792 12.022123,4.3251792 H 4.0261054 q -0.3981866,0 -0.6779934,0.2744259 Q 3.0683052,4.874031 3.0683052,5.2829794 Z M 17.403023,5.0569816 q 0,-0.3013304 -0.156046,-0.5165664 -0.156046,-0.215236 -0.392806,-0.215236 h -1.162274 q -0.322854,0 -0.570376,0.2744259 Q 14.874,4.874031 14.874,5.2829794 v 4.30472 q 0,0.2798068 0.129141,0.4358526 0.129142,0.156046 0.344378,0.156046 0.172189,0 0.344378,-0.107618 L 16.854171,9.4262724 Q 17.090931,9.286369 17.246977,8.9796577 17.403023,8.6729464 17.403023,8.371616 Z m 1.431319,2.6258792 q 0,0.215236 0.06457,0.3389967 0.06457,0.1237607 0.161427,0.1237607 0.02152,0 0.05381,-0.010762 l 0.05381,-0.021524 0.527328,-0.3013304 q 0.107618,-0.064571 0.182951,-0.2529023 0.07533,-0.1883315 0.07533,-0.4035675 V 4.8525074 q 0,-0.215236 -0.07533,-0.3712821 -0.07533,-0.1560461 -0.182951,-0.1560461 h -0.527328 q -0.129142,0 -0.231379,0.1829506 -0.102237,0.1829506 -0.102237,0.430472 z m -5.671468,6.8875522 q -0.312093,-0.312092 -0.764088,-0.312092 -0.451996,0 -0.764088,0.312092 -0.312092,0.312092 -0.312092,0.764088 0,0.441233 0.306711,0.758706 0.306711,0.317474 0.769469,0.317474 0.462757,0 0.769469,-0.317474 0.306711,-0.317473 0.306711,-0.758706 0,-0.451996 -0.312092,-0.764088 z m -8.7977719,1.522794 q 0.3067113,-0.317473 0.3067113,-0.758706 0,-0.441234 -0.3174731,-0.758707 -0.3174731,-0.317473 -0.7587069,-0.317473 -0.4519956,0 -0.7640878,0.312092 -0.3120922,0.312092 -0.3120922,0.764088 0,0.451995 0.3120922,0.764087 0.3120922,0.312093 0.7640878,0.312093 0.4627574,0 0.7694687,-0.317474 z"
|
||||
id="path4749" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.1 KiB |
2
src/messagecontent/images/coach.svg.license
Normal file
@@ -0,0 +1,2 @@
|
||||
SPDX-License-Identifier: CC0-1.0
|
||||
SPDX-FileCopyrightText: Volker Krause <vkrause@kde.org>
|
||||
11
src/messagecontent/images/couchettecar.svg
Normal file
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg width="22" height="22" viewBox="0 0 22 22" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<style type="text/css" id="current-color-scheme">
|
||||
.ColorScheme-Text { color:#4d4d4d; }
|
||||
</style>
|
||||
<polygon
|
||||
style="fill:currentColor;stroke-width:0"
|
||||
points="13.843885,3.2142857 9.0837593,6.7294713 0,6.7294713 0,11.785714 1.1319308,11.785714 1.1319308,9.1029246 13.790798,9.1029246 13.790798,11.785714 15,11.785714 15,3.2142857 "
|
||||
id="polygon3"
|
||||
transform="matrix(1.3138289,0,0,1.3138289,1.1118802,1.1489425)" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 636 B |
2
src/messagecontent/images/couchettecar.svg.license
Normal file
@@ -0,0 +1,2 @@
|
||||
SPDX-License-Identifier: CC0-1.0
|
||||
SPDX-FileCopyrightText: Volker Krause <vkrause@kde.org>
|
||||
13
src/messagecontent/images/elevator.svg
Normal file
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22">
|
||||
<defs id="defs3051">
|
||||
<style
|
||||
type="text/css"
|
||||
id="current-color-scheme">
|
||||
.ColorScheme-Text {
|
||||
color:#4d4d4d;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<path d="m 13.891951,2.1595513 v 1.465731 h 2.931463 V 18.282592 H 14.26706 l -0.03966,1.465731 h 5.527474 V 2.1595513 Z M 6.5632967,12.419668 10.96049,19.748323 15.357683,12.419668 H 12.426221 V 9.4882061 h 2.931462 L 10.96049,2.1595513 6.5632967,9.4882061 h 2.931462 v 2.9314619 z m 0.8526688,7.328655 0.059488,-1.465731 H 5.0975654 V 3.6252823 H 8.0290275 V 2.1595513 H 2.1661038 V 19.748323 Z" class="ColorScheme-Text" style="fill:currentColor;"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 750 B |
2
src/messagecontent/images/elevator.svg.license
Normal file
@@ -0,0 +1,2 @@
|
||||
SPDX-License-Identifier: CC0-1.0
|
||||
SPDX-FileCopyrightText: https://github.com/gravitystorm/openstreetmap-carto
|
||||
15
src/messagecontent/images/escalator.svg
Normal file
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22">
|
||||
<defs id="defs3051">
|
||||
<style
|
||||
type="text/css"
|
||||
id="current-color-scheme">
|
||||
.ColorScheme-Text {
|
||||
color:#4d4d4d;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<path
|
||||
style="color:#4d4d4d;fill:currentColor;fill-opacity:1;stroke:none" class="ColorScheme-Text"
|
||||
d="M 15.421821,4.149711 C 14.791977,4.1493251 13.983404,4.4066009 13.50051,4.8906508 L 5.6613053,12.681742 C 5.4318152,12.91046 5.4044789,12.921922 4.952441,12.922308 H 3.4545242 c -1.5034503,-3.86e-4 -2.4044909,1.35083 -2.4056481,2.498665 0.00115,1.143981 0.9019199,2.592455 2.5403643,2.591685 0,0 2.0343344,0.0011 2.6622505,0 0.6290732,0.0011 1.3908996,-0.237424 2.0207445,-0.827542 L 16.066534,9.406854 c 0.166238,-0.1465713 0.260251,-0.1924526 0.545282,-0.1924526 h 1.927725 c 0.924904,0 2.234878,-1.0477952 2.235649,-2.5628173 C 20.77409,5.1543052 19.454577,4.1527968 18.462561,4.149711 Z m 0.182828,1.2926342 h 2.732819 c 0.664169,0.00155 1.113782,0.7440044 1.113012,1.1867861 0.0011,0.4447097 -0.318821,1.2730043 -1.132258,1.2733903 h -1.642256 c -0.697342,-3.86e-4 -1.006209,0.097205 -1.408107,0.4971665 L 7.4158246,16.25814 C 7.096467,16.576339 6.6936595,16.70155 6.1745102,16.700778 H 3.5860329 c -0.6664858,7.77e-4 -1.253373,-0.561595 -1.2541446,-1.29905 7.71e-4,-0.741311 0.5608982,-1.156374 1.1386735,-1.157918 h 1.7769721 c 0.5176063,0.0015 0.8596525,-0.145258 1.1547109,-0.44264 L 14.388997,5.8144197 C 14.719154,5.4865778 15.044619,5.443889 15.604649,5.4423452 Z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
2
src/messagecontent/images/escalator.svg.license
Normal file
@@ -0,0 +1,2 @@
|
||||
SPDX-License-Identifier: CC0-1.0
|
||||
SPDX-FileCopyrightText: Volker Krause <vkrause@kde.org>
|
||||
16
src/messagecontent/images/ferry.svg
Normal file
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22">
|
||||
<defs id="defs3051">
|
||||
<style
|
||||
type="text/css"
|
||||
id="current-color-scheme">
|
||||
.ColorScheme-Text {
|
||||
color:#4d4d4d;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<path
|
||||
style="color:#4d4d4d;fill:currentColor;fill-opacity:1;stroke:none" class="ColorScheme-Text"
|
||||
d="m 19.904344,13.991511 c 0.04329,0 0.286785,0.03246 0.730485,0.09739 0.443714,0.06493 0.665564,0.313843 0.665564,0.746717 0,0.238089 -0.0505,0.487004 -0.151509,0.746731 -0.100998,0.259734 -0.216443,0.519461 -0.346307,0.779195 l -0.400417,0.768369 c -0.144296,0.281372 -0.232669,0.503229 -0.265147,0.665557 -0.03246,0.162328 -0.05591,0.398617 -0.07033,0.708847 H 3.6279025 c 0.036078,-0.05772 0.068542,-0.122651 0.097399,-0.194792 0.028858,-0.07215 0.043283,-0.147909 0.043283,-0.22727 0,-0.245302 -0.091979,-0.532087 -0.2759591,-0.860362 -0.1839798,-0.328262 -0.3138363,-0.597016 -0.3895906,-0.80624 -0.075754,-0.209231 -0.4545329,-0.80264 -1.1363217,-1.78024 -0.6817958,-0.9776 -1.05515432,-1.583628 -1.12008959,-1.818111 -0.0649353,-0.234482 -0.0973994,-0.402216 -0.0973994,-0.503229 0,-0.209224 0.077554,-0.362539 0.23267596,-0.459938 0.15511493,-0.0974 0.34450073,-0.146096 0.56816433,-0.146096 0.093786,0 0.6853951,0.05591 1.7748273,0.167741 1.0894181,0.111831 1.8722197,0.200211 2.3483906,0.26514 0.079361,-0.31745 0.165934,-0.62948 0.2597341,-0.93611 0.093786,-0.306624 0.1803664,-0.61866 0.2597271,-0.93611 l 0.0974,-0.3895946 C 6.347859,9.604946 6.4380387,9.377676 6.5606966,9.1973096 6.6833474,9.0169432 6.9106103,8.9267635 7.2424854,8.9267635 h 0.248908 c 0.3535201,0 0.6998275,0.025251 1.0389222,0.075754 0.3390878,0.050503 0.6817888,0.093786 1.0281033,0.1298636 l 1.7315301,0.2056178 c 0.07937,-0.4256611 0.14069,-0.8567492 0.183973,-1.2932433 0.04329,-0.4364871 0.07936,-0.8820006 0.108225,-1.3365265 0.187586,-0.1515086 0.387791,-0.2579274 0.600629,-0.3192493 0.21283,-0.061329 0.434687,-0.091993 0.665556,-0.091993 0.440101,0 0.901839,0.075754 1.38523,0.22727 0.483391,0.1515086 0.836911,0.331875 1.060567,0.5410992 l -0.08657,2.2726433 c 0,0.1010058 0.0036,0.1984052 0.01081,0.2922052 V 9.9115697 L 16.59278,10.12802 c 0.4401,0.06492 0.900039,0.151502 1.379816,0.259728 0.479785,0.108218 0.761157,0.192992 0.844131,0.254321 0.08297,0.06132 0.173147,0.315636 0.270553,0.762956 0.09739,0.447313 0.146096,0.919877 0.146096,1.417694 v 0.292198 c 0,0.137083 0.01082,0.270553 0.03246,0.400416 l 0.04329,0.40041 z M 11.657901,6.0913689 c 0.01443,-0.1803734 0.07215,-0.4455065 0.173154,-0.7954273 0.101006,-0.3499067 0.295805,-0.569957 0.584397,-0.6601507 0.288591,-0.090173 0.566357,-0.1352695 0.833297,-0.1352695 0.548326,0 1.03171,0.1136314 1.450165,0.3408944 0.418448,0.2272699 0.627687,0.5356932 0.627687,0.9252978 0,0.1009987 -0.0036,0.2002048 -0.01083,0.2976042 -0.0072,0.097399 -0.01082,0.1965984 -0.01082,0.2976042 -0.13709,-0.1515086 -0.452726,-0.3012106 -0.946935,-0.4491198 -0.494217,-0.1478953 -0.918079,-0.22185 -1.271599,-0.22185 -0.367952,0 -0.665556,0.034271 -0.892826,0.1028055 -0.227256,0.068549 -0.40583,0.1677477 -0.535694,0.2976112 z m 3.592938,6.6231241 c 0.180367,0.173161 0.393204,0.259734 0.638506,0.259734 0.252514,0 0.465345,-0.08657 0.638505,-0.259734 0.173154,-0.173154 0.259735,-0.385984 0.259735,-0.638499 0,-0.252521 -0.08658,-0.465351 -0.259735,-0.638505 -0.17316,-0.173154 -0.385991,-0.259734 -0.638505,-0.259734 -0.245302,0 -0.458139,0.08658 -0.638506,0.259734 -0.180366,0.173154 -0.270553,0.385984 -0.270553,0.638505 0,0.252515 0.09019,0.465345 0.270553,0.638499 z M 8.7467595,12.021885 c 0.1731537,0.180367 0.3823779,0.270553 0.6276796,0.270553 0.2525144,0 0.4671516,-0.08839 0.6439119,-0.265147 0.176767,-0.17676 0.265147,-0.391397 0.265147,-0.643911 0,-0.245302 -0.09019,-0.454533 -0.270553,-0.627687 -0.1803667,-0.173147 -0.3932042,-0.259727 -0.6385059,-0.259727 -0.2453017,0 -0.4545259,0.08658 -0.6276796,0.259727 -0.1731537,0.173154 -0.259734,0.382385 -0.259734,0.627687 0,0.245301 0.08658,0.458139 0.259734,0.638505 z m 3.2466235,-0.919878 c -0.173147,0.180367 -0.259727,0.393204 -0.259727,0.638499 0,0.245302 0.09019,0.454526 0.27056,0.627687 0.180366,0.173146 0.393197,0.259727 0.638499,0.259727 0.245301,0 0.454525,-0.08658 0.627679,-0.259727 0.173154,-0.173161 0.259727,-0.382385 0.259727,-0.627687 0,-0.245295 -0.08477,-0.458132 -0.254314,-0.638499 -0.169547,-0.180373 -0.380578,-0.270553 -0.633092,-0.270553 -0.259728,0 -0.476171,0.09018 -0.649332,0.270553 z"
|
||||
id="path4751" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.4 KiB |
2
src/messagecontent/images/ferry.svg.license
Normal file
@@ -0,0 +1,2 @@
|
||||
SPDX-License-Identifier: CC0-1.0
|
||||
SPDX-FileCopyrightText: Volker Krause <vkrause@kde.org>
|
||||
14
src/messagecontent/images/flight.svg
Normal file
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22">
|
||||
<defs id="defs3051">
|
||||
<style type="text/css" id="current-color-scheme">
|
||||
.ColorScheme-Text {
|
||||
color:#4d4d4d;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<path
|
||||
style="color:#4d4d4d;fill:currentColor;fill-opacity:1;stroke:none"
|
||||
d="m 2.3243326,11.281529 v -0.48768 L 2.763251,10.732949 1.9829519,8.0750591 h 1.2192176 l 1.7800603,2.4506299 c 1.1867061,-0.10575 2.9423813,-0.19913 5.2670252,-0.28042 C 9.8184645,8.0019091 8.9081155,5.4090391 7.5182042,2.4666491 h 1.4996403 c 2.1134945,2.74341 3.8331395,4.98965 5.9741715,7.7786199 h 3.218737 c 0.568969,0 1.048529,0.10574 1.438679,0.317 0.2601,0.13817 0.390149,0.29667 0.390149,0.47549 0,0.17883 -0.130044,0.33732 -0.390149,0.4755 -0.39015,0.21133 -0.86971,0.31699 -1.438679,0.31699 h -3.218737 c -2.145078,2.80603 -3.840734,4.99587 -5.9741715,7.77861 H 7.5182042 c 1.3899113,-2.86108 2.3002603,-5.45396 2.7310508,-7.77861 -2.3246439,-0.0894 -4.0803191,-0.18287 -5.2670252,-0.28042 l -1.7800603,2.45061 H 1.9829519 l 0.7802991,-2.65791 z"
|
||||
class="ColorScheme-Text"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
2
src/messagecontent/images/flight.svg.license
Normal file
@@ -0,0 +1,2 @@
|
||||
SPDX-License-Identifier: CC0-1.0
|
||||
SPDX-FileCopyrightText: Volker Krause <vkrause@kde.org>
|
||||
14
src/messagecontent/images/foodestablishment.svg
Normal file
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22">
|
||||
<defs id="defs3051">
|
||||
<style type="text/css" id="current-color-scheme">
|
||||
.ColorScheme-Text {
|
||||
color:#4d4d4d;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<path
|
||||
style="color:#4d4d4d;fill:currentColor;fill-opacity:1;stroke:none"
|
||||
d="m 16.15861,18.818425 c 0,0.264672 -0.118157,0.527767 -0.354472,0.789284 -0.236311,0.261523 -0.486803,0.392284 -0.751475,0.392284 -0.327683,0 -0.58605,-0.189053 -0.775102,-0.567158 -0.15124,-0.30878 -0.22686,-0.645917 -0.22686,-1.011411 V 13.439957 C 13.73562,12.65225 13.57808,11.684944 13.57808,10.538039 V 4.3372017 c 0,-0.5545465 0.110277,-1.0744333 0.330832,-1.5596604 0.283579,-0.6049597 0.671132,-0.9074396 1.16266,-0.9074396 0.724692,0 1.087038,0.5955067 1.087038,1.7865202 z M 10.146822,6.9839006 c 0,0.8948384 -0.5621719,2.2156104 -1.639757,2.6063117 l 0.1368084,2.9612067 c 0.00627,0.126037 -0.1449733,-0.743594 0,0 0.2279252,5.263533 0.2279252,4.53254 0.2279252,5.263533 0,0.724692 0.039954,1.19782 -0.067173,1.512902 -0.1953527,0.554546 -0.1386973,0.738301 -0.7184505,0.738301 -0.5734481,0 -0.4871106,-0.247591 -0.6761628,-0.789532 -0.1134277,-0.385224 -0.09777,-0.685562 -0.076624,-1.391031 0.1577864,-5.264035 0,0 0.2045452,-5.334173 0.2268599,-1.304447 -0.081925,0.579757 0,0 L 7.6279841,9.5902123 C 6.5503984,9.199511 6.034985,7.878739 6.034985,6.9839006 c 0,-0.037808 0.00315,-0.1071252 0.00944,-0.2079516 L 6.1733048,2.8909713 h 0.4253606 l 0.022376,3.724287 c -0.0063,0.1386343 0.051989,0.875934 0.1748671,1.0429299 0.122883,0.1669912 0.2536416,0.2504868 0.3922759,0.2504868 0.1512399,0 0.2946042,-0.086648 0.4300929,-0.2599445 0.1354855,-0.1732963 0.2032283,-0.920049 0.2032283,-1.071289 V 2.8909713 h 0.5387972 v 3.6864702 c 0,0.15124 0.067742,0.8979927 0.2032267,1.071289 0.135484,0.1732963 0.278846,0.2599445 0.430086,0.2599445 0.1386387,0 0.2693993,-0.083496 0.392282,-0.2504868 0.1228833,-0.1669959 0.1811723,-0.9042956 0.174867,-1.0429299 l 0.022377,-3.724287 h 0.4253607 l 0.128865,3.8849777 c 0.0063,0.1008267 0.0095,0.1701439 0.0095,0.2079516 z M 15.61982,11.955917 V 2.8342596 c 0,-0.3465927 -0.15124,-0.519889 -0.45372,-0.519889 -0.308785,0 -0.567155,0.2174069 -0.775109,0.6522206 -0.176447,0.3591938 -0.26467,0.7152373 -0.26467,1.0681306 v 6.7585362 c 0,1.22252 0.242614,1.83378 0.727842,1.83378 0.151239,0 0.316657,-0.07404 0.496254,-0.222127 0.179602,-0.14809 0.269403,-0.297755 0.269403,-0.448994 z"
|
||||
class="ColorScheme-Text"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.5 KiB |
2
src/messagecontent/images/foodestablishment.svg.license
Normal file
@@ -0,0 +1,2 @@
|
||||
SPDX-License-Identifier: CC0-1.0
|
||||
SPDX-FileCopyrightText: Volker Krause <vkrause@kde.org>
|
||||
16
src/messagecontent/images/funicular.svg
Normal file
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22">
|
||||
<defs id="defs3051">
|
||||
<style
|
||||
type="text/css"
|
||||
id="current-color-scheme">
|
||||
.ColorScheme-Text {
|
||||
color:#4d4d4d;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<path
|
||||
style="color:#4d4d4d;fill:currentColor;fill-opacity:1;stroke:none" class="ColorScheme-Text"
|
||||
d="m 16.645728,12.656988 q 0,-2.292227 -1.640349,-3.9453573 -1.640348,-1.6531303 -3.949617,-1.6531303 -1.4912262,0 -2.7949835,0.7498736 Q 6.9570211,8.5582475 6.2114082,9.8620048 5.4657953,11.165762 5.4657953,12.656988 v 5.061646 H 8.9254391 L 7.5620327,19.883042 H 2.9094083 q -0.7754373,0 -1.3378425,-0.562405 Q 1.0091607,18.758232 1.0091607,17.982794 V 9.308121 Q 2.9946212,5.8740411 5.5680508,4.0206606 8.1414804,2.16728 11.055762,2.16728 q 2.871674,0 5.423801,1.8107741 2.552126,1.8107741 4.631321,5.3300669 v 8.674673 q 0,0.775438 -0.562405,1.337843 -0.562405,0.562405 -1.337843,0.562405 H 14.54949 l -1.354885,-2.164408 h 3.451123 z m -3.63007,-3.6215483 q 0.673182,0 1.146114,0.4814529 0.472931,0.4814529 0.472931,1.1461134 v 4.465156 q 0,0.630576 -0.417543,1.086465 -0.417543,0.455889 -1.031076,0.524059 v 0.809522 q 0,0.144862 -0.102255,0.247118 -0.102256,0.102255 -0.255639,0.102255 -0.144862,0 -0.247118,-0.102255 -0.102255,-0.102256 -0.102255,-0.247118 v -0.79248 H 9.6156636 v 0.79248 q 0,0.144862 -0.1065162,0.247118 -0.1065161,0.102255 -0.251378,0.102255 -0.1448619,0 -0.2471174,-0.102255 Q 8.9083965,17.69307 8.9083965,17.548208 V 16.738686 Q 8.2948636,16.670516 7.8773204,16.214627 7.4597772,15.758738 7.4597772,15.128162 v -4.465156 q 0,-0.6731818 0.4814529,-1.150374 Q 8.422683,9.0354397 9.0873436,9.0354397 Z m 0.903257,1.4230553 q 0,-0.357894 -0.255639,-0.6177934 Q 13.407638,9.5808023 13.049743,9.5808023 H 9.0532584 q -0.3664154,0 -0.6220541,0.2556387 -0.2556387,0.255639 -0.2556387,0.622054 v 1.099247 q 0,0.366415 0.2556387,0.622054 0.2556387,0.255638 0.6220541,0.255638 h 3.9964846 q 0.357895,0 0.613533,-0.259899 0.255639,-0.259899 0.255639,-0.617793 z m -0.579448,9.424547 H 8.7720559 L 9.65827,17.718634 h 2.794983 z M 9.1981204,14.71914 q -0.1704258,-0.170426 -0.4175432,-0.170426 -0.2471175,0 -0.4175433,0.170426 -0.1704258,0.170426 -0.1704258,0.417543 0,0.247118 0.1704258,0.417544 0.1704258,0.170425 0.4175433,0.170425 0.2471174,0 0.4175432,-0.170425 0.1704258,-0.170426 0.1704258,-0.417544 0,-0.247117 -0.1704258,-0.417543 z m 4.5333266,0 q -0.170426,-0.170426 -0.417544,-0.170426 -0.247117,0 -0.417543,0.170426 -0.170426,0.170426 -0.170426,0.417543 0,0.247118 0.170426,0.417544 0.170426,0.170425 0.417543,0.170425 0.247118,0 0.417544,-0.170425 0.170425,-0.170426 0.170425,-0.417544 0,-0.247117 -0.170425,-0.417543 z"
|
||||
id="path4751" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.7 KiB |
2
src/messagecontent/images/funicular.svg.license
Normal file
@@ -0,0 +1,2 @@
|
||||
SPDX-License-Identifier: CC0-1.0
|
||||
SPDX-FileCopyrightText: Volker Krause <vkrause@kde.org>
|
||||
15
src/messagecontent/images/longdistancetrain.svg
Normal file
|
After Width: | Height: | Size: 5.3 KiB |
2
src/messagecontent/images/longdistancetrain.svg.license
Normal file
@@ -0,0 +1,2 @@
|
||||
SPDX-License-Identifier: CC0-1.0
|
||||
SPDX-FileCopyrightText: Volker Krause <vkrause@kde.org>
|
||||
14
src/messagecontent/images/rapidtransit.svg
Normal file
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22">
|
||||
<defs id="defs3051">
|
||||
<style type="text/css" id="current-color-scheme">
|
||||
.ColorScheme-Text {
|
||||
color:#4d4d4d;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<path
|
||||
style="color:#4d4d4d;fill:currentColor;fill-opacity:1;stroke:none" class="ColorScheme-Text"
|
||||
d="m 14.514282,4.8066299 q 1.183528,0 2.037358,0.8538307 0.853831,0.8538308 0.853831,2.0373586 v 7.9549978 q 0,1.09899 -0.739705,1.927459 -0.739705,0.82847 -1.838695,0.946823 v 1.428687 q 0,0.262067 -0.181756,0.443823 -0.181756,0.181756 -0.443823,0.181756 -0.262067,0 -0.44805,-0.181756 -0.185983,-0.181756 -0.185983,-0.443823 v -1.41178 H 8.4698362 v 1.41178 q 0,0.262067 -0.1817561,0.443823 -0.181756,0.181756 -0.4438229,0.181756 -0.2620669,0 -0.4438229,-0.181756 Q 7.2186782,20.217853 7.2186782,19.955786 V 18.527099 Q 6.1196881,18.408746 5.3757563,17.584503 4.6318246,16.760261 4.6318246,15.652817 V 7.6978192 q 0,-1.1835278 0.8538307,-2.0373586 Q 6.3394861,4.8066299 7.5230139,4.8066299 H 8.622004 V 2.6171035 Q 8.3007608,2.7354562 8.1063241,2.9510274 7.9118873,3.1665985 7.9118873,3.2173211 q -0.06763,0.1352603 -0.1902098,0.2071174 -0.1225797,0.071857 -0.2662937,0.071857 -0.2197981,0 -0.3592853,-0.1521678 -0.1394872,-0.1521679 -0.1394872,-0.3381508 0,-0.109899 0.050723,-0.2113443 L 7.1341405,2.5748346 Q 7.3539385,2.2028687 7.8738454,1.8689448 8.3937522,1.5350209 9.1207764,1.5350209 h 3.8041966 q 0.710117,0 1.23425,0.3339239 0.524134,0.3339239 0.752386,0.7058898 l 0.126806,0.219798 q 0.04227,0.109899 0.04227,0.2028905 0,0.1944367 -0.139487,0.3508315 -0.139487,0.1563947 -0.359285,0.1563947 -0.143714,0 -0.266294,-0.076084 -0.12258,-0.076084 -0.19021,-0.2113443 0,-0.042269 -0.19021,-0.2620669 -0.190209,-0.219798 -0.519906,-0.3381507 v 2.1895264 z m -4.9031869,0 H 12.426201 V 2.5325657 H 9.6110951 Z M 7.7216775,14.917339 q -0.3085626,-0.304336 -0.7312511,-0.304336 -0.439596,0 -0.7439318,0.304336 -0.3043357,0.304336 -0.3043357,0.743932 0,0.448049 0.3170164,0.748158 0.3170164,0.300109 0.7312511,0.300109 0.405781,0 0.7227973,-0.300109 0.3170164,-0.300109 0.3170164,-0.748158 0,-0.439596 -0.3085626,-0.743932 z m 8.0775775,0 q -0.304336,-0.304336 -0.743932,-0.304336 -0.44805,0 -0.748159,0.317016 -0.300108,0.317017 -0.300108,0.731252 0,0.431142 0.308562,0.739704 0.308563,0.308563 0.739705,0.308563 0.414235,0 0.731251,-0.300109 0.317016,-0.300109 0.317016,-0.748158 0,-0.439596 -0.304335,-0.743932 z m 0.338151,-7.5914857 q 0,-0.6340327 -0.460731,-1.0947632 -0.46073,-0.4607304 -1.094763,-0.4607304 H 7.4722913 q -0.6424865,0 -1.103217,0.4565035 -0.4607305,0.4565036 -0.4607305,1.0989901 v 3.6942977 q 0,0.642486 0.4607305,1.103217 0.4607305,0.46073 1.103217,0.46073 h 7.1096207 q 0.642486,0 1.09899,-0.46073 0.456504,-0.460731 0.456504,-1.103217 z"
|
||||
id="path4749" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.8 KiB |
2
src/messagecontent/images/rapidtransit.svg.license
Normal file
@@ -0,0 +1,2 @@
|
||||
SPDX-License-Identifier: CC0-1.0
|
||||
SPDX-FileCopyrightText: Volker Krause <vkrause@kde.org>
|
||||
19
src/messagecontent/images/seat.svg
Normal file
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22">
|
||||
<defs id="defs3051">
|
||||
<style type="text/css" id="current-color-scheme">
|
||||
.ColorScheme-Text {
|
||||
color:#4d4d4d;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<path
|
||||
d="M 18.270753,3.2659119 14.646412,14.864869 c -0.01672,0.06553 -0.921877,1.686021 -2.174043,1.681772 l -7.3329699,0.01812 c -1.238273,-0.413166 -1.229256,-1.466354 -1.229256,-1.466354 0,0 -0.05503,-1.219046 1.286042,-1.57108 l 6.2575869,0.02125 c 0.289281,-0.142682 0.285418,-0.063 0.426871,-0.415318 L 15.188634,2.4779275 c 0.04419,-0.1814692 0.444638,-1.3733887 1.996173,-1.0635157 1.323434,0.432045 1.134602,1.7542909 1.085946,1.8515001 z"
|
||||
style="fill:#4d4d4d;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
|
||||
<path
|
||||
d="m 12.511479,9.6326174 -4.2465189,-0.0022 c -0.436543,0.00592 -1.180203,0.2731972 -1.278698,1.1330156 -0.09676,0.844716 0.04306,0.825819 0.04306,0.825819 0.0022,0.01975 0.04841,0.126554 0.223252,0.212145 l 4.5915099,-3.04e-4 z"
|
||||
style="fill:#4d4d4d;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
|
||||
<path
|
||||
d="m 12.965467,17.031896 -0.908629,-0.02576 1.001921,2.921594 c 0.05194,0.122275 -0.05837,0.16911 -0.12924,0.174385 l -7.6249769,-0.01411 c -0.07821,-0.01611 -0.111476,-0.103479 -0.06521,-0.192766 l 1.015412,-2.847699 -0.931814,-0.01364 -0.945272,2.697325 c -0.02203,0.614614 0.175621,0.814271 0.175621,0.814271 0,0 0.17226,0.261324 0.538924,0.3735 l 7.9892399,0.0018 c 0.28247,0.01106 0.615134,-0.35466 0.615134,-0.35466 0.04515,-0.01943 0.301887,-0.441164 0.245863,-0.719598 z"
|
||||
style="fill:#4d4d4d;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
2
src/messagecontent/images/seat.svg.license
Normal file
@@ -0,0 +1,2 @@
|
||||
SPDX-License-Identifier: CC0-1.0
|
||||
SPDX-FileCopyrightText: Volker Krause <vkrause@kde.org>
|
||||
16
src/messagecontent/images/shuttle.svg
Normal file
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22">
|
||||
<defs id="defs3051">
|
||||
<style
|
||||
type="text/css"
|
||||
id="current-color-scheme">
|
||||
.ColorScheme-Text {
|
||||
color:#4d4d4d;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<path
|
||||
style="color:#4d4d4d;fill:currentColor;fill-opacity:1;stroke:none" class="ColorScheme-Text"
|
||||
d="m 5.0443854,17.825429 q -0.5100278,-0.505261 -0.5100278,-1.220253 0,-0.705459 0.5052612,-1.215487 0.5052612,-0.510028 1.2297867,-0.510028 0.7245255,0 1.22502,0.519561 0.5004946,0.519561 0.5004946,1.205954 0,0.695925 -0.5052612,1.21072 -0.5052612,0.514794 -1.2202534,0.514794 -0.7149923,0 -1.2250201,-0.505261 z m 9.6619286,0 q -0.505261,-0.505261 -0.505261,-1.220253 0,-0.705459 0.505261,-1.215487 0.505261,-0.510028 1.220253,-0.510028 0.714993,0 1.220254,0.505261 0.505261,0.505261 0.505261,1.220254 0,0.714992 -0.510028,1.220253 -0.510027,0.505261 -1.215487,0.505261 -0.714992,0 -1.220253,-0.505261 z m 6.282399,-5.071678 q 0,0.705459 -0.257398,1.677848 -0.257397,0.97239 -0.891357,1.572983 -0.633959,0.600594 -1.625415,0.600594 0,-0.934257 -0.67686,-1.611116 -0.676859,-0.67686 -1.611116,-0.67686 -0.943789,0 -1.615882,0.67686 -0.672093,0.676859 -0.672093,1.611116 H 8.5478475 q 0,-0.924724 -0.6673261,-1.60635 Q 7.2131953,14.3172 6.2694055,14.3172 q -0.9437898,0 -1.6206491,0.67686 -0.6768593,0.676859 -0.6768593,1.611116 H 3.7144999 q -1.2583864,0 -2.0639443,-0.69116 -0.80555798,-0.691159 -0.80555798,-1.940012 0,-0.667326 0.24309738,-1.415685 0.2430973,-0.748358 0.9199567,-1.244086 L 2.4561135,10.999636 Q 2.9899744,10.62784 3.2330717,10.341844 3.4761691,10.055847 3.7335663,9.6363845 L 4.6296899,8.1968668 Q 5.6592788,6.5380847 6.3933375,5.6753274 7.1273962,4.8125701 7.6421906,4.5885392 8.156985,4.3645083 9.7776341,4.1929101 11.398283,4.021312 13.609993,4.021312 q 2.068711,0 3.613094,0.2430974 1.544383,0.2430973 2.049644,0.643493 0.505261,0.4003957 0.867524,1.2488531 0.362263,0.8484575 0.60536,3.1030664 0.243098,2.2546091 0.243098,3.4939291 z M 8.7194456,9.4361867 q 0,-0.2001979 0.00953,-0.433762 0.00953,-0.2335641 0.00953,-0.4623617 0,-0.7531251 -0.066733,-1.015289 Q 8.6050468,7.2626102 8.4715816,7.1625113 8.3381164,7.0624124 7.8423884,7.0624124 q -1.2011869,0 -1.4967171,0.3527295 Q 6.0501412,7.7678714 5.482914,8.9309255 4.9156868,10.09398 4.9156868,10.322777 q 0,0.162065 0.1048656,0.26693 0.1048655,0.104866 0.2955301,0.104866 h 2.859969 q 0.3050634,0 0.4146955,-0.214498 0.1096321,-0.214497 0.1096321,-0.490961 z M 12.718636,7.1529781 Q 12.313473,7.005213 10.683291,7.005213 q -0.9342566,0 -0.9866893,0.9485564 -0.052433,0.9485564 -0.052433,1.5491499 0,0.8579907 0.1382318,1.0152887 0.1382319,0.157299 1.2250205,0.157299 0.305063,0 0.571994,-0.0095 h 0.409928 l 0.276464,0.0095 q 0.695926,0 0.776958,-0.471895 0.08103,-0.4718952 0.08103,-1.3680188 0,-1.5348501 -0.405162,-1.6826151 z m 6.763826,0.9294899 q -0.181131,-0.8961236 -0.91519,-0.9771561 -0.734059,-0.081032 -2.116377,-0.081032 -0.381329,0 -1.034355,0.00953 -0.653027,0.00953 -0.643493,0.00953 -0.638727,0 -0.719759,0.3002968 -0.08103,0.3002967 -0.08103,1.4919505 0,1.6778488 0.300297,1.8017808 0.300297,0.123932 1.19642,0.123932 0.371796,0 0.800792,-0.0095 l 1.468117,-0.03813 q 0.285997,0 0.400396,-0.0095 0.953323,0 1.23932,-0.176365 0.285997,-0.176364 0.285997,-0.5672267 0,-0.9819227 -0.181132,-1.8780463 z"
|
||||
id="path4751" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.3 KiB |
2
src/messagecontent/images/shuttle.svg.license
Normal file
@@ -0,0 +1,2 @@
|
||||
SPDX-License-Identifier: CC0-1.0
|
||||
SPDX-FileCopyrightText: Volker Krause <vkrause@kde.org>
|
||||
9
src/messagecontent/images/sleepingcar.svg
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg width="22" height="22" viewBox="0 0 22 22" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<style type="text/css" id="current-color-scheme">
|
||||
.ColorScheme-Text { color:#4d4d4d; }
|
||||
</style>
|
||||
<g transform="matrix(1.3102432,0,0,1.3102432,1.2193104,1.1894719)">
|
||||
<path d="m 5.1443418,7.3044641 c 0,-0.528773 -0.0081,-0.8023148 0.1015991,-1.2374509 C 5.3614143,5.6373851 5.5519455,5.2463134 5.8175344,4.8993061 6.071576,4.5688229 6.3991096,4.3125374 6.7051142,4.1307717 7.1671034,3.949414 7.1363263,3.9549633 7.6766568,3.8968406 L 13.845266,3.8729476 V 2.8539579 H 15 V 12.272727 H 13.845266 V 9.6013219 H 1.2124711 V 12.272727 H 0 V 2.7272727 h 1.1374134 v 0.837224 c 0.2078522,0 0.4272517,0.016524 0.6581986,0.060589 0.2309469,0.044065 0.4618938,0.1101611 0.6928407,0.1982899 0.2309468,0.088129 0.4503464,0.203798 0.6466512,0.3414993 0.1963049,0.1321932 0.3579677,0.2919267 0.4849885,0.4681844 0.2598152,0.3580234 0.4445727,0.7601112 0.5600462,1.2117715 0.1212471,0.4461522 0.1789838,0.9308608 0.1789838,1.4596338 z m -4.0069284,0 h 2.2748268 c 0,-0.4516602 -0.040416,-0.842732 -0.1212471,-1.1732151 C 3.204388,5.8007659 3.0715935,5.497823 2.8983834,5.2114043 2.7020785,4.9139695 2.4364896,4.7046635 2.1016166,4.5889944 1.76097,4.4678172 1.443418,4.4072287 1.1374134,4.4072287 Z M 7.9306984,4.7560967 C 7.2845735,4.7638814 6.8853796,4.9041689 6.5505066,5.3833694 6.3657491,5.653264 6.2216915,6.0174407 6.1408601,6.3424158 6.0658023,6.6673908 6.091224,6.8528039 6.091224,7.3044641 h 7.754042 V 4.7322037 Z" style="fill:currentColor;stroke-width:0"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
2
src/messagecontent/images/sleepingcar.svg.license
Normal file
@@ -0,0 +1,2 @@
|
||||
SPDX-License-Identifier: CC0-1.0
|
||||
SPDX-FileCopyrightText: Volker Krause <vkrause@kde.org>
|
||||
13
src/messagecontent/images/stairs.svg
Normal file
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22">
|
||||
<defs id="defs3051">
|
||||
<style
|
||||
type="text/css"
|
||||
id="current-color-scheme">
|
||||
.ColorScheme-Text {
|
||||
color:#4d4d4d;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<path style="stroke-width:1px;fill:none;stroke:currentColor;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" class="ColorScheme-Text" d="m 3.3896829,19 v -4 h 4 V 11 H 11.389683 V 7 h 4 V 3 h 4"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 506 B |
2
src/messagecontent/images/stairs.svg.license
Normal file
@@ -0,0 +1,2 @@
|
||||
SPDX-License-Identifier: CC0-1.0
|
||||
SPDX-FileCopyrightText: Volker Krause <vkrause@kde.org>
|
||||
14
src/messagecontent/images/subway.svg
Normal file
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22" >
|
||||
<defs id="defs3051">
|
||||
<style type="text/css" id="current-color-scheme">
|
||||
.ColorScheme-Text {
|
||||
color:#4d4d4d;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<path
|
||||
style="color:#4d4d4d;fill:currentColor;fill-opacity:1;stroke:none" class="ColorScheme-Text"
|
||||
d="m 19.635166,16.020953 q -1.345816,2.314593 -3.649935,3.634225 -2.304119,1.319632 -4.943383,1.319632 -2.6497377,0 -4.9643304,-1.330106 -2.3145926,-1.330105 -3.6394613,-3.644698 -1.3248686,-2.314592 -1.3248686,-4.96433 0,-2.6392637 1.3301053,-4.9538563 1.3301052,-2.3145927 3.6446979,-3.644698 2.3145927,-1.3301053 4.9538571,-1.3301053 2.62879,0 4.938146,1.319632 2.309356,1.319632 3.655172,3.6289881 1.345815,2.309356 1.345815,4.9800395 0,2.670684 -1.345815,4.985277 z M 16.540314,7.7784894 q 0,-1.0368537 -0.733129,-1.7595094 Q 15.074057,5.2963244 14.058149,5.2963244 H 8.0360191 q -1.0368537,0 -1.764746,0.7331289 -0.7278922,0.7331289 -0.7278922,1.7490361 v 6.8390456 q 0,0.974014 0.6545794,1.686196 0.6545793,0.712183 1.5657538,0.795969 v 1.068273 q 0,0.219939 0.1623357,0.371801 0.1623357,0.151863 0.3927476,0.151863 0.2199387,0 0.3718011,-0.151863 0.1518624,-0.151862 0.1518624,-0.371801 v -1.047327 h 4.3883001 v 1.047327 q 0,0.219939 0.162336,0.371801 0.162335,0.151863 0.382274,0.151863 0.230412,0 0.382274,-0.151863 0.151863,-0.151862 0.151863,-0.371801 V 17.0997 q 0.911174,-0.08379 1.57099,-0.795969 0.659816,-0.712182 0.659816,-1.686196 z M 14.110516,6.1132395 q 0.54461,0 0.937357,0.3927476 0.392748,0.3927476 0.392748,0.9583042 v 1.6861965 q 0,0.5655565 -0.392748,0.9583042 -0.392747,0.392748 -0.937357,0.392748 H 7.9836528 q -0.5655566,0 -0.9478309,-0.397985 Q 6.6535475,9.7055711 6.6535475,9.1504878 V 7.4642913 q 0,-0.5550833 0.3822744,-0.9530676 Q 7.4180962,6.1132395 7.9836528,6.1132395 Z M 8.2088281,15.261641 q -0.2670684,0.277541 -0.6441061,0.277541 -0.3665645,0 -0.6336328,-0.267068 -0.2670684,-0.267068 -0.2670684,-0.633633 0,-0.377037 0.2775416,-0.644106 0.2775417,-0.267068 0.6231596,-0.267068 0.3770377,0 0.6441061,0.267068 0.2670684,0.267069 0.2670684,0.644106 0,0.345618 -0.2670684,0.62316 z m 6.9437779,0 q -0.267068,0.277541 -0.644106,0.277541 -0.356091,0 -0.628396,-0.267068 -0.272305,-0.267068 -0.272305,-0.633633 0,-0.377037 0.261832,-0.644106 0.261831,-0.267068 0.638869,-0.267068 0.377038,0 0.644106,0.267068 0.267068,0.267069 0.267068,0.644106 0,0.345618 -0.267068,0.62316 z"
|
||||
id="path4749" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.5 KiB |
2
src/messagecontent/images/subway.svg.license
Normal file
@@ -0,0 +1,2 @@
|
||||
SPDX-License-Identifier: CC0-1.0
|
||||
SPDX-FileCopyrightText: Volker Krause <vkrause@kde.org>
|
||||
16
src/messagecontent/images/taxi.svg
Normal file
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22">
|
||||
<defs id="defs3051">
|
||||
<style
|
||||
type="text/css"
|
||||
id="current-color-scheme">
|
||||
.ColorScheme-Text {
|
||||
color:#4d4d4d;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<path
|
||||
style="color:#4d4d4d;fill:currentColor;fill-opacity:1;stroke:none" class="ColorScheme-Text"
|
||||
d="m 19.256782,10.803117 q 0.700505,0.155667 1.161021,0.609698 0.460517,0.45403 0.460517,1.024811 v 4.527333 q 0,0.298363 -0.214043,0.512406 -0.214043,0.214043 -0.512406,0.214043 h -0.921033 v 1.660454 q 0,0.596726 -0.415114,1.018326 -0.415113,0.4216 -1.011839,0.4216 -0.596726,0 -1.01184,-0.4216 -0.415113,-0.4216 -0.415113,-1.018326 V 17.691408 H 5.8304519 v 1.660454 q 0,0.596726 -0.4151136,1.018326 -0.4151136,0.4216 -1.0118394,0.4216 -0.6096981,0 -1.0248117,-0.428086 -0.4151136,-0.428086 -0.4151136,-1.01184 V 17.691408 H 1.9517342 q -0.2983629,0 -0.5124058,-0.214043 -0.214043,-0.214043 -0.214043,-0.512406 v -4.527333 q 0,-0.570781 0.4605167,-1.024811 0.4605166,-0.454031 1.1739931,-0.609698 L 4.2737759,5.6012243 Q 4.4294435,5.0174708 4.967794,4.6023572 5.5061444,4.1872436 6.1288148,4.1872436 h 1.9328727 l -0.012972,-0.090806 q 0,-0.038917 -0.012972,-0.090806 v -1.945845 q 0,-0.2983629 0.214043,-0.5124059 0.2140429,-0.2140429 0.5124058,-0.2140429 h 4.5792227 q 0.298363,0 0.512405,0.2140429 0.214043,0.214043 0.214043,0.5124059 v 1.945845 q 0,0.1167507 -0.02594,0.1816122 h 1.945845 q 0.62267,0 1.161021,0.4151136 0.53835,0.4151136 0.694018,0.9988671 z M 12.809549,3.5386286 q 0,0.025945 0.02595,0.025945 h 0.324307 q 0.02595,0 0.02595,-0.025945 V 2.2284263 q 0,-0.025945 -0.02595,-0.025945 h -0.324307 q -0.02595,0 -0.02595,0.025945 z m -1.478842,-0.012972 q 0,0.025945 0.01297,0.038917 h 0.389169 l 0.02595,-0.012972 0.220529,-0.3372798 0.233502,0.3372798 0.02594,0.012972 h 0.376197 q 0.02594,0 0.02594,-0.038917 l -0.42808,-0.635643 0.415113,-0.648615 V 2.215454 L 12.614967,2.202482 H 12.25174 L 12.2258,2.215454 11.979326,2.5657061 11.745825,2.215454 11.719875,2.202482 h -0.363224 q -0.03892,0 -0.03892,0.012972 l 0.01297,0.025945 0.428086,0.648615 z M 10.396702,2.2024817 q -0.02595,0 -0.02595,0.012972 L 9.8778095,3.5256563 q 0,0.038917 0.051889,0.038917 h 0.2983635 l 0.03892,-0.038917 0.06486,-0.2075568 h 0.428086 l 0.07783,0.2075568 q 0,0.038917 0.03892,0.038917 h 0.311335 q 0.02594,0 0.02594,-0.038917 L 10.746954,2.2413986 q 0,-0.038917 -0.03892,-0.038917 z m 0.28539,0.8431995 h -0.259446 l 0.129723,-0.389169 z M 8.9308316,2.4619277 q 0,0.025945 0.038917,0.025945 H 9.2810838 V 3.538629 q 0,0.025945 0.038917,0.025945 h 0.2853906 q 0.025945,0 0.025945,-0.025945 V 2.4878723 h 0.3243075 q 0.025945,0 0.025945,-0.025945 V 2.215454 q 0,-0.025945 -0.025945,-0.025945 H 8.9697485 q -0.038917,0 -0.038917,0.025945 z M 5.4412829,13.014894 q -0.4151136,-0.395655 -0.9729225,-0.395655 -0.5318643,0 -0.9469779,0.389169 -0.4151136,0.389169 -0.4151136,0.972922 0,0.596726 0.4086275,0.985895 0.4086274,0.389169 0.953464,0.389169 0.5578089,0 0.9729225,-0.402141 0.4151136,-0.402142 0.4151136,-0.972923 0,-0.570781 -0.4151136,-0.966436 z M 16.662322,10.686366 q 0.259446,0 0.428086,-0.162154 0.16864,-0.162154 0.16864,-0.408627 0,-0.103779 -0.01297,-0.1556679 L 16.441793,6.2109224 Q 16.376932,5.9125595 16.117486,5.6985165 15.85804,5.4844736 15.546705,5.4844736 H 6.5569007 q -0.3113352,0 -0.5707812,0.2140429 -0.259446,0.214043 -0.3243075,0.5124059 L 4.8575294,9.9599171 q 0,0.051889 -0.012972,0.1426949 0,0.259446 0.1686399,0.4216 0.1686399,0.162154 0.4280859,0.162154 z m 2.127458,2.328528 q -0.415114,-0.395655 -0.972923,-0.395655 -0.544836,0 -0.95995,0.402141 -0.415114,0.402141 -0.415114,0.95995 0,0.570781 0.415114,0.972923 0.415114,0.402141 0.95995,0.402141 0.557809,0 0.972923,-0.402141 0.415113,-0.402142 0.415113,-0.972923 0,-0.570781 -0.415113,-0.966436 z"
|
||||
id="path4751" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.8 KiB |
2
src/messagecontent/images/taxi.svg.license
Normal file
@@ -0,0 +1,2 @@
|
||||
SPDX-License-Identifier: CC0-1.0
|
||||
SPDX-FileCopyrightText: Volker Krause <vkrause@kde.org>
|
||||
14
src/messagecontent/images/train.svg
Normal file
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22">
|
||||
<defs id="defs3051">
|
||||
<style type="text/css" id="current-color-scheme">
|
||||
.ColorScheme-Text {
|
||||
color:#4d4d4d;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<path
|
||||
style="color:#4d4d4d;fill:currentColor;fill-opacity:1;stroke:none" class="ColorScheme-Text"
|
||||
d="m 15.098099,1.8668609 q 1.399482,0 2.400501,1.0010189 1.001019,1.0010189 1.001019,2.4005016 v 9.3687596 q 0,1.302296 -0.879536,2.2693 -0.879536,0.967004 -2.152676,1.103064 v 1.671605 q 0,0.310996 -0.21867,0.529665 -0.218669,0.218669 -0.519946,0.218669 -0.310996,0 -0.529666,-0.218669 -0.218669,-0.218669 -0.218669,-0.529665 V 18.028943 H 7.9937802 v 1.652167 q 0,0.310996 -0.2186692,0.529665 -0.2186692,0.218669 -0.5296653,0.218669 -0.3109962,0 -0.5248061,-0.218669 Q 6.5068298,19.992106 6.5068298,19.68111 V 18.009505 Q 5.214252,17.873445 4.3444346,16.906441 3.4746172,15.939437 3.4746172,14.637141 V 5.2683814 q 0,-1.3994827 1.0010189,-2.4005016 Q 5.476655,1.8668609 6.8761377,1.8668609 Z M 7.1093848,13.757605 q -0.3693079,-0.35473 -0.864958,-0.35473 -0.5150874,0 -0.8746767,0.359589 -0.3595893,0.359589 -0.3595893,0.874677 0,0.505368 0.3595893,0.869817 0.3595893,0.364449 0.8746767,0.364449 0.4956501,0 0.864958,-0.35959 0.369308,-0.359589 0.369308,-0.874676 0,-0.524806 -0.369308,-0.879536 z m 9.4902422,0.0049 q -0.364449,-0.359589 -0.860099,-0.359589 -0.515087,0 -0.874676,0.359589 -0.35959,0.359589 -0.35959,0.874677 0,0.505368 0.35959,0.869817 0.359589,0.364449 0.874676,0.364449 0.505369,0 0.864958,-0.364449 0.35959,-0.364449 0.35959,-0.869817 0,-0.515088 -0.364449,-0.874677 z m 0.393605,-8.9314209 q 0,-0.7386159 -0.544244,-1.2828592 Q 15.904745,3.0039406 15.166129,3.0039406 H 6.8081073 q -0.7483345,0 -1.2925778,0.539384 -0.5442433,0.539384 -0.5442433,1.2877185 v 2.3227525 q 0,0.7483345 0.5442433,1.2925778 0.5442433,0.5442433 1.2925778,0.5442433 h 8.3580217 q 0.748335,0 1.287719,-0.5442433 0.539384,-0.5442433 0.539384,-1.2925778 z"
|
||||
id="path4749" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
2
src/messagecontent/images/train.svg.license
Normal file
@@ -0,0 +1,2 @@
|
||||
SPDX-License-Identifier: CC0-1.0
|
||||
SPDX-FileCopyrightText: Volker Krause <vkrause@kde.org>
|
||||
16
src/messagecontent/images/tramway.svg
Normal file
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22">
|
||||
<defs id="defs3051">
|
||||
<style
|
||||
type="text/css"
|
||||
id="current-color-scheme">
|
||||
.ColorScheme-Text {
|
||||
color:#4d4d4d;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<path
|
||||
style="color:#4d4d4d;fill:currentColor;fill-opacity:1;stroke:none" class="ColorScheme-Text"
|
||||
d="m 20.794826,13.179229 q 0.0173,0.06921 0.02595,0.147079 0.0087,0.07787 0.0087,0.164383 0,0.164383 -0.02596,0.328765 -0.02595,0.164383 -0.06921,0.285507 l -0.5018,1.444837 q 0.147079,0.05191 0.272529,0.173035 0.12545,0.121124 0.12545,0.276855 v 0.917082 q 0,0.19899 -0.09517,0.337417 -0.09517,0.138428 -0.294158,0.138428 H 18.3983 q -0.173035,0.2509 -0.441238,0.402305 -0.268203,0.151405 -0.596969,0.151405 -0.328765,0 -0.601294,-0.151405 -0.27253,-0.151405 -0.436912,-0.402305 h -0.432586 q -0.173035,0.2509 -0.445564,0.402305 -0.272529,0.151405 -0.592643,0.151405 -0.328765,0 -0.601294,-0.151405 -0.272529,-0.151405 -0.445564,-0.402305 H 8.2152252 q -0.1730344,0.2509 -0.4412378,0.402305 -0.2682033,0.151405 -0.5969686,0.151405 -0.3287654,0 -0.5969687,-0.151405 -0.2682033,-0.151405 -0.4412377,-0.402305 h -0.432586 q -0.1730344,0.2509 -0.4455636,0.402305 -0.2725292,0.151405 -0.5926428,0.151405 -0.3287654,0 -0.6012946,-0.151405 Q 3.7941962,17.643517 3.6211618,17.392617 H 1.7264352 q -0.1989896,0 -0.3157878,-0.138428 -0.1167982,-0.138427 -0.1167982,-0.337417 v -0.90843 q 0,-0.19899 0.1470792,-0.333092 0.1470792,-0.134101 0.3374171,-0.151405 L 1.2765457,14.104963 q -0.043259,-0.121124 -0.064888,-0.276855 -0.021629,-0.155731 -0.021629,-0.311462 0,-0.09517 0.00865,-0.181686 0.00865,-0.08652 0.025955,-0.155731 L 2.0638522,9.3119102 Q 2.2455384,8.4986486 2.8944174,7.9016799 3.5432964,7.3047112 4.356558,7.3047112 h 7.84711 L 13.337043,6.1799876 11.892206,4.7264986 H 10.568493 V 3.9651473 q 0,-0.1989896 0.142753,-0.341743 Q 10.854,3.480651 11.052989,3.480651 h 3.028102 q 0.19899,0 0.341743,0.1427533 0.142754,0.1427534 0.142754,0.341743 v 0.7613513 h -1.237196 l 1.090116,1.0901168 q 0.14708,0.1643826 0.14708,0.3633722 0,0.2076413 -0.14708,0.3547205 l -0.770003,0.7700031 h 4.014398 q 0.813262,0 1.462141,0.5969687 0.648879,0.5969687 0.830565,1.4102303 z M 14.67806,9.225393 q 0,-0.1989895 -0.147079,-0.337417 Q 14.383902,8.7495484 14.17626,8.7495484 h -2.119671 q -0.19899,0 -0.350395,0.1384276 -0.151405,0.1384275 -0.151405,0.337417 v 3.322261 q 0,0.198989 0.151405,0.341742 0.151405,0.142754 0.350395,0.142754 h 2.119671 q 0.207642,0 0.354721,-0.142754 0.147079,-0.142753 0.147079,-0.341742 z m -4.222039,0 q 0,-0.1989895 -0.14708,-0.337417 Q 10.161862,8.7495484 9.9542209,8.7495484 H 7.8345495 q -0.1989896,0 -0.3503947,0.1384276 -0.1514051,0.1384275 -0.1514051,0.337417 v 3.322261 q 0,0.198989 0.1514051,0.341742 0.1514051,0.142754 0.3503947,0.142754 h 2.1196714 q 0.2076411,0 0.3547201,-0.142754 0.14708,-0.142753 0.14708,-0.341742 z m 8.279696,3.806757 q 0.181686,0 0.30281,-0.112473 0.121124,-0.112472 0.121124,-0.276855 0,-0.06056 -0.0087,-0.08652 L 18.554031,9.5714618 Q 18.467513,9.1561793 18.203636,8.9355604 17.939758,8.7149416 17.610993,8.7149416 h -1.332365 q -0.198989,0 -0.350394,0.1427533 -0.151406,0.1427534 -0.151406,0.341743 v 3.3482161 q 0,0.198989 0.151406,0.341742 0.151405,0.142754 0.350394,0.142754 z M 6.2339813,9.1994379 q 0,-0.1989896 -0.1470793,-0.341743 Q 5.9398228,8.7149416 5.7321815,8.7149416 H 4.4084684 q -0.3287654,0 -0.588317,0.2206188 Q 3.5605998,9.1561793 3.4740826,9.5714618 L 2.8684622,12.556305 q 0,0.02595 -0.00865,0.08652 0,0.164383 0.1211241,0.276855 0.121124,0.112473 0.3028102,0.112473 h 2.4484367 q 0.2076413,0 0.3547205,-0.142754 0.1470793,-0.142753 0.1470793,-0.341742 z"
|
||||
id="path4749" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.7 KiB |
2
src/messagecontent/images/tramway.svg.license
Normal file
@@ -0,0 +1,2 @@
|
||||
SPDX-License-Identifier: CC0-1.0
|
||||
SPDX-FileCopyrightText: Volker Krause <vkrause@kde.org>
|
||||
16
src/messagecontent/images/transfer.svg
Normal file
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22">
|
||||
<defs id="defs3051">
|
||||
<style
|
||||
type="text/css"
|
||||
id="current-color-scheme">
|
||||
.ColorScheme-Text {
|
||||
color:#4d4d4d;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<path
|
||||
style="color:#4d4d4d;fill:currentColor;fill-opacity:1;stroke:none" class="ColorScheme-Text"
|
||||
d="m 1.9348413,12.612438 q -0.2922305,-0.838985 -0.2922305,-1.725103 0,-2.149308 1.5507069,-3.700015 1.550707,-1.550707 3.7000151,-1.550707 H 16.178721 V 2.7331617 l 4.835943,3.9215447 -4.835943,3.9026906 V 7.4559835 H 6.8933328 q -1.3951649,0 -2.4132582,1.0086666 -1.0180933,1.0086665 -1.0180933,2.4226849 0,0.103695 0.00943,0.197963 l 0.00943,0.207389 z M 20.185107,9.275354 q 0.29223,0.857838 0.29223,1.715676 0,2.149308 -1.54128,3.700015 -1.54128,1.550707 -3.709442,1.550707 H 5.941227 v 2.903451 L 1.1147105,15.233085 5.941227,11.320967 v 3.110841 h 9.285388 q 1.395165,0 2.413258,-1.008666 1.018094,-1.008667 1.018094,-2.432112 0,-0.188536 -0.02828,-0.405352 z"
|
||||
id="path4751" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
2
src/messagecontent/images/transfer.svg.license
Normal file
@@ -0,0 +1,2 @@
|
||||
SPDX-License-Identifier: CC0-1.0
|
||||
SPDX-FileCopyrightText: Volker Krause <vkrause@kde.org>
|
||||
16
src/messagecontent/images/wait.svg
Normal file
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22">
|
||||
<defs id="defs3051">
|
||||
<style
|
||||
type="text/css"
|
||||
id="current-color-scheme">
|
||||
.ColorScheme-Text {
|
||||
color:#4d4d4d;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<path
|
||||
style="color:#4d4d4d;fill:currentColor;fill-opacity:1;stroke:none" class="ColorScheme-Text"
|
||||
d="m 16.575268,17.736668 q 0.345137,0.08026 0.565864,0.349151 0.220728,0.268886 0.220728,0.630076 0,0.417375 -0.288953,0.706328 -0.288952,0.288952 -0.706327,0.288952 H 5.6592955 q -0.4173753,0 -0.7103408,-0.296979 -0.2929654,-0.296979 -0.2929654,-0.698301 0,-0.36119 0.2167142,-0.62205 0.2167141,-0.26086 0.553825,-0.341124 0.056185,-2.303591 0.7906053,-3.648022 0.7344202,-1.34443 1.9343745,-1.950427 1.1999542,-0.605997 1.3042981,-0.826724 0.1043438,-0.220728 0.1043438,-0.381257 0,-0.561851 -0.8548169,-0.927055 Q 7.8505164,9.6540329 7.1040565,8.9798111 6.3575967,8.3055893 5.9041023,7.0735292 5.4506078,5.8414691 5.418502,4.0355179 5.0894176,3.9552534 4.8727035,3.6943938 4.6559893,3.4335341 4.6559893,3.0723439 q 0,-0.4013225 0.2929654,-0.6983011 Q 5.2419202,2.0770641 5.6592955,2.0770641 H 16.36658 q 0.417375,0 0.706327,0.2889522 0.288953,0.2889522 0.288953,0.7063276 0,0.3531638 -0.220728,0.6260631 -0.220727,0.2728993 -0.565864,0.3371109 -0.03211,2.3356969 -0.7585,3.7001934 -0.726394,1.3644966 -1.954441,1.9905597 -1.228046,0.626063 -1.328377,0.850804 -0.100331,0.22474 -0.100331,0.369216 0,0.545799 0.862844,0.919029 0.862843,0.37323 1.59325,1.039425 0.730407,0.666195 1.183901,1.862137 0.453495,1.195941 0.501654,2.969786 z M 6.0525916,17.720615 h 9.8966124 q -0.0321,-1.420682 -0.345137,-2.436027 -0.313031,-1.015346 -0.882909,-1.665489 -0.569878,-0.650142 -1.74174,-1.207981 -1.171862,-0.557838 -1.171862,-1.464827 0,-0.9712001 1.372523,-1.5852235 1.372523,-0.6140234 2.046745,-1.8380571 0.674222,-1.2240336 0.72238,-3.4152545 H 6.0525916 q 0.032106,2.1671415 0.6862615,3.3831487 0.6541556,1.2160072 1.7698322,1.7337132 1.1156765,0.517706 1.3966023,0.8748832 0.2809254,0.357177 0.2809254,0.84679 0,0.955148 -1.3966019,1.577198 -1.3966024,0.62205 -2.0427316,1.866149 -0.6461292,1.2441 -0.6942879,3.330977 z m 8.9976504,-2.343723 q 0.128423,0.369216 0.208688,0.830737 0.08026,0.461521 0.120396,1.039426 h -8.74883 q 0.032106,-0.577905 0.1163835,-1.039426 0.084278,-0.461521 0.2127009,-0.830737 h 3.7804576 v -4.430601 q 0,-0.545798 -0.305005,-1.0313985 Q 10.130028,9.4292923 9.3755419,9.0440227 L 8.95014,8.8273086 Q 8.7334259,8.7149383 8.5046721,8.5744754 8.2759182,8.4340125 8.0431512,8.2413777 H 13.950618 Q 13.573375,8.5544092 12.413553,9.1764591 11.253731,9.798509 11.253731,10.946291 v 4.430601 z"
|
||||
id="path4749" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.7 KiB |
2
src/messagecontent/images/wait.svg.license
Normal file
@@ -0,0 +1,2 @@
|
||||
SPDX-License-Identifier: CC0-1.0
|
||||
SPDX-FileCopyrightText: Volker Krause <vkrause@kde.org>
|
||||
16
src/messagecontent/images/walk.svg
Normal file
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22">
|
||||
<defs id="defs3051">
|
||||
<style
|
||||
type="text/css"
|
||||
id="current-color-scheme">
|
||||
.ColorScheme-Text {
|
||||
color:#4d4d4d;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<path
|
||||
style="color:#4d4d4d;fill:currentColor;fill-opacity:1;stroke:none" class="ColorScheme-Text"
|
||||
d="m 16.09347,20.042218 q -0.585255,0.535733 -1.24254,0.535733 -0.630274,0 -0.985928,-0.616768 -0.355655,-0.616768 -0.814854,-1.346085 l -0.891387,-1.368594 q -0.06303,-0.09904 -0.355655,-0.684297 -0.292627,-0.585254 -0.742822,-0.585254 -0.108047,0 -0.180078,0.05402 l -0.162071,0.108047 q -0.765332,0.423184 -1.5846878,1.535167 -0.8193558,1.111983 -0.8193558,1.33708 0,0.09904 0.036016,0.189082 l 0.1350587,0.378165 q 0.027012,0.09904 0.027012,0.198086 0,0.333144 -0.2701173,0.57625 -0.2701173,0.243105 -0.5942581,0.243105 -0.5852541,0 -1.1525005,-0.612265 -0.5672463,-0.612266 -0.5672463,-1.170509 0,-0.747324 0.7338187,-1.481143 0.7338186,-0.733819 1.1479985,-1.328077 L 8.0259662,15.67082 q 0.2341017,-0.369161 0.3781643,-0.715811 0.1440625,-0.346651 0.1440625,-0.625772 0,-0.378164 -0.049521,-0.742822 -0.049522,-0.364659 -0.049522,-0.742823 0,-0.333145 0.036016,-0.495215 0.036016,-0.16207 0.090039,-0.25211 L 8.65624,11.970213 q 0.036016,-0.06303 0.067529,-0.130557 0.031514,-0.06753 0.031514,-0.139561 0,-0.07203 -0.040518,-0.11705 -0.040518,-0.04502 -0.094541,-0.04502 -0.2791212,0 -0.5222268,0.540235 -0.2431055,0.540234 -0.2431055,0.927402 0,0.08104 0.036016,0.175577 0.036016,0.09454 0.036016,0.544736 0,0.423184 -0.2656153,0.62127 -0.2656154,0.198086 -0.6347757,0.198086 -0.6302737,0 -0.9454105,-0.342149 -0.3151369,-0.342148 -0.3151369,-0.999434 0,-0.639277 0.405176,-1.539668 Q 6.5763367,10.763689 7.2876456,9.9443329 7.9989545,9.1249771 8.6292282,8.6252601 9.2595019,8.1255431 9.2595019,7.738375 q 0,-0.2611134 -0.3736623,-0.5942581 Q 8.5121774,6.8109723 8.2060444,6.131177 7.8999115,5.4513818 7.8999115,4.7760886 q 0,-1.2605474 0.9229008,-2.2149619 0.9229008,-0.9544144 2.2644837,-0.9544144 1.368594,0 2.205958,0.895889 0.837363,0.8958891 0.837363,2.1744443 0,0.3151368 -0.05853,0.6257717 -0.05853,0.3106349 -0.157569,0.6257718 -0.126054,-0.027012 -0.247607,-0.058525 -0.121553,-0.031514 -0.247608,-0.031514 -0.306133,0 -0.468203,0.040518 -0.16207,0.040518 -0.16207,0.1665723 0,0.1440626 0.157568,0.2521095 0.157568,0.1080469 0.364658,0.1800782 l 0.25211,0.081035 Q 13.44632,6.837984 13.243732,7.0675837 13.041144,7.2971834 12.879074,7.3512069 L 12.66298,7.441246 q -0.09904,0.04502 -0.153067,0.1215527 -0.05402,0.076533 -0.05402,0.2656154 0,0.3331447 0.270117,0.6347756 0.270118,0.301631 0.634776,0.8868852 0.364658,0.5852541 0.96792,1.3775981 0.603262,0.792344 0.931905,0.99043 0.328643,0.198086 0.558242,0.504219 0.2296,0.306133 0.2296,0.639278 0,0.36916 -0.288125,0.603262 -0.288125,0.234101 -0.648282,0.234101 -0.279121,0 -0.513222,-0.112548 -0.234102,-0.112549 -0.43669,-0.324141 -0.202588,-0.211592 -0.373662,-0.477207 -0.171075,-0.265616 -0.351153,-0.504219 -0.180078,-0.238604 -0.283623,-0.454698 -0.103545,-0.216094 -0.256611,-0.216094 -0.07203,0 -0.09454,0.08103 -0.02251,0.08104 -0.02251,0.180079 0,0.153066 0.02701,0.333144 0.02701,0.180079 0.02701,0.234102 0,0.171074 0.234102,1.251543 0.234102,1.08047 0.454697,1.530665 0.220596,0.450196 0.612266,1.355089 0.39167,0.904893 0.729317,1.256045 0.337647,0.351153 0.96792,0.351153 0.243106,0 0.544737,0.265615 0.301631,0.265615 0.301631,0.589756 0,0.468203 -0.585254,1.003936 z M 13.014132,5.0011863 q 0.171075,-0.1710742 0.171075,-0.4051759 0,-0.2431056 -0.171075,-0.4141799 -0.171074,-0.1710743 -0.414179,-0.1710743 -0.234102,0 -0.405176,0.1710743 -0.171075,0.1710743 -0.171075,0.4141799 0,0.2341017 0.171075,0.4051759 0.171074,0.1710743 0.405176,0.1710743 0.243105,0 0.414179,-0.1710743 z"
|
||||
id="path4749" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.8 KiB |
2
src/messagecontent/images/walk.svg.license
Normal file
@@ -0,0 +1,2 @@
|
||||
SPDX-License-Identifier: CC0-1.0
|
||||
SPDX-FileCopyrightText: Volker Krause <vkrause@kde.org>
|
||||
170
src/messagecontent/mediasizehelper.cpp
Normal file
@@ -0,0 +1,170 @@
|
||||
// SPDX-FileCopyrightText: 2023 James Graham <james.h.graham@protonmail.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
|
||||
#include "mediasizehelper.h"
|
||||
|
||||
int MediaSizeHelper::m_mediaMaxWidth = 0;
|
||||
int MediaSizeHelper::m_mediaMaxHeight = 0;
|
||||
|
||||
MediaSizeHelper::MediaSizeHelper(QObject *parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
}
|
||||
|
||||
qreal MediaSizeHelper::contentMaxWidth() const
|
||||
{
|
||||
return m_contentMaxWidth;
|
||||
}
|
||||
|
||||
void MediaSizeHelper::setContentMaxWidth(qreal contentMaxWidth)
|
||||
{
|
||||
if (contentMaxWidth < 0.0 || qFuzzyCompare(contentMaxWidth, 0.0)) {
|
||||
m_contentMaxWidth = -1.0;
|
||||
Q_EMIT contentMaxWidthChanged();
|
||||
Q_EMIT currentSizeChanged();
|
||||
return;
|
||||
}
|
||||
if (qFuzzyCompare(contentMaxWidth, m_contentMaxWidth)) {
|
||||
return;
|
||||
}
|
||||
m_contentMaxWidth = contentMaxWidth;
|
||||
Q_EMIT contentMaxWidthChanged();
|
||||
Q_EMIT currentSizeChanged();
|
||||
}
|
||||
|
||||
qreal MediaSizeHelper::contentMaxHeight() const
|
||||
{
|
||||
return m_contentMaxHeight;
|
||||
}
|
||||
|
||||
void MediaSizeHelper::setContentMaxHeight(qreal contentMaxHeight)
|
||||
{
|
||||
if (contentMaxHeight < 0.0 || qFuzzyCompare(contentMaxHeight, 0.0)) {
|
||||
m_contentMaxHeight = -1.0;
|
||||
Q_EMIT contentMaxHeightChanged();
|
||||
Q_EMIT currentSizeChanged();
|
||||
return;
|
||||
}
|
||||
if (qFuzzyCompare(contentMaxHeight, m_contentMaxHeight)) {
|
||||
return;
|
||||
}
|
||||
m_contentMaxHeight = contentMaxHeight;
|
||||
Q_EMIT contentMaxHeightChanged();
|
||||
Q_EMIT currentSizeChanged();
|
||||
}
|
||||
|
||||
qreal MediaSizeHelper::mediaWidth() const
|
||||
{
|
||||
return m_mediaWidth;
|
||||
}
|
||||
|
||||
void MediaSizeHelper::setMediaWidth(qreal mediaWidth)
|
||||
{
|
||||
if (mediaWidth < 0.0 || qFuzzyCompare(mediaWidth, 0.0)) {
|
||||
m_mediaWidth = -1.0;
|
||||
Q_EMIT mediaWidthChanged();
|
||||
Q_EMIT currentSizeChanged();
|
||||
return;
|
||||
}
|
||||
if (qFuzzyCompare(mediaWidth, m_mediaWidth)) {
|
||||
return;
|
||||
}
|
||||
m_mediaWidth = mediaWidth;
|
||||
Q_EMIT mediaWidthChanged();
|
||||
Q_EMIT currentSizeChanged();
|
||||
}
|
||||
|
||||
qreal MediaSizeHelper::mediaHeight() const
|
||||
{
|
||||
return m_mediaHeight;
|
||||
}
|
||||
|
||||
void MediaSizeHelper::setMediaHeight(qreal mediaHeight)
|
||||
{
|
||||
if (mediaHeight < 0.0 || qFuzzyCompare(mediaHeight, 0.0)) {
|
||||
m_mediaHeight = -1.0;
|
||||
Q_EMIT mediaHeightChanged();
|
||||
Q_EMIT currentSizeChanged();
|
||||
return;
|
||||
}
|
||||
if (qFuzzyCompare(mediaHeight, m_mediaHeight)) {
|
||||
return;
|
||||
}
|
||||
m_mediaHeight = mediaHeight;
|
||||
Q_EMIT mediaHeightChanged();
|
||||
Q_EMIT currentSizeChanged();
|
||||
}
|
||||
|
||||
qreal MediaSizeHelper::resolvedMediaWidth() const
|
||||
{
|
||||
if (m_mediaWidth > 0.0) {
|
||||
return m_mediaWidth;
|
||||
}
|
||||
return widthLimit();
|
||||
}
|
||||
|
||||
qreal MediaSizeHelper::resolvedMediaHeight() const
|
||||
{
|
||||
if (m_mediaHeight > 0.0) {
|
||||
return m_mediaHeight;
|
||||
}
|
||||
return widthLimit() / 16.0 * 9.0;
|
||||
}
|
||||
|
||||
qreal MediaSizeHelper::aspectRatio() const
|
||||
{
|
||||
return resolvedMediaWidth() / resolvedMediaHeight();
|
||||
}
|
||||
|
||||
bool MediaSizeHelper::limitWidth() const
|
||||
{
|
||||
// If actual data isn't available we'll be using a placeholder that is width
|
||||
// limited so return true.
|
||||
if (m_mediaWidth < 0.0 || m_mediaHeight < 0.0) {
|
||||
return true;
|
||||
}
|
||||
return m_mediaWidth >= m_mediaHeight;
|
||||
}
|
||||
|
||||
qreal MediaSizeHelper::widthLimit() const
|
||||
{
|
||||
if (m_contentMaxWidth < 0.0) {
|
||||
return m_mediaMaxWidth;
|
||||
}
|
||||
return std::min(m_contentMaxWidth, qreal(m_mediaMaxWidth));
|
||||
}
|
||||
|
||||
qreal MediaSizeHelper::heightLimit() const
|
||||
{
|
||||
if (m_contentMaxHeight < 0.0) {
|
||||
return m_mediaMaxHeight;
|
||||
}
|
||||
return std::min(m_contentMaxHeight, qreal(m_mediaMaxHeight));
|
||||
}
|
||||
|
||||
QSize MediaSizeHelper::currentSize() const
|
||||
{
|
||||
if (limitWidth()) {
|
||||
qreal width = std::min(widthLimit(), resolvedMediaWidth());
|
||||
qreal height = width / aspectRatio();
|
||||
if (height > heightLimit()) {
|
||||
return QSize(qRound(heightLimit() * aspectRatio()), qRound(heightLimit()));
|
||||
}
|
||||
return QSize(qRound(width), qRound(height));
|
||||
} else {
|
||||
qreal height = std::min(heightLimit(), resolvedMediaHeight());
|
||||
qreal width = height * aspectRatio();
|
||||
if (width > widthLimit()) {
|
||||
return QSize(qRound(widthLimit()), qRound(widthLimit() / aspectRatio()));
|
||||
}
|
||||
return QSize(qRound(width), qRound(height));
|
||||
}
|
||||
}
|
||||
|
||||
void MediaSizeHelper::setMaxSize(int width, int height)
|
||||
{
|
||||
MediaSizeHelper::m_mediaMaxWidth = width;
|
||||
MediaSizeHelper::m_mediaMaxHeight = height;
|
||||
}
|
||||
|
||||
#include "moc_mediasizehelper.cpp"
|
||||
110
src/messagecontent/mediasizehelper.h
Normal file
@@ -0,0 +1,110 @@
|
||||
// SPDX-FileCopyrightText: 2023 James Graham <james.h.graham@protonmail.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QQmlEngine>
|
||||
#include <QSize>
|
||||
|
||||
/**
|
||||
* @class MediaSizeHelper
|
||||
*
|
||||
* A class to help calculate the current width of a media item within a chat delegate.
|
||||
*
|
||||
* The only realistic way to guarantee that a media item (e.g. an image or video)
|
||||
* is the correct size in QML is to calculate the size manually.
|
||||
*
|
||||
* The rules for this component work as follows:
|
||||
* - The output will always try to keep the media size if no limits are breached.
|
||||
* - If no media width is set, the current size will be a placeholder at a 16:9 ratio
|
||||
* calcualated from either the configured max width or the contentMaxWidth, whichever
|
||||
* is smaller (if the contentMaxWidth isn't set, the configured max width is used).
|
||||
* - The aspect ratio of the media will always be maintained if set (otherwise 16:9).
|
||||
* - The current size will never be larger than any of the limits in either direction.
|
||||
* - If any limit is breached the image size will be reduced while maintaining aspect
|
||||
* ration, i.e. no stretching or squashing. This can mean that the width or height
|
||||
* is reduced even if that parameter doesn't breach the limit itself.
|
||||
*/
|
||||
class MediaSizeHelper : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
|
||||
/**
|
||||
* @brief The maximum width (in px) the media can be.
|
||||
*
|
||||
* This is the upper limit placed upon the media by the delegate.
|
||||
*/
|
||||
Q_PROPERTY(qreal contentMaxWidth READ contentMaxWidth WRITE setContentMaxWidth NOTIFY contentMaxWidthChanged)
|
||||
|
||||
/**
|
||||
* @brief The maximum height (in px) the media can be.
|
||||
*
|
||||
* This is the upper limit placed upon the media by the delegate.
|
||||
*/
|
||||
Q_PROPERTY(qreal contentMaxHeight READ contentMaxHeight WRITE setContentMaxHeight NOTIFY contentMaxHeightChanged)
|
||||
|
||||
/**
|
||||
* @brief The base width (in px) of the media.
|
||||
*/
|
||||
Q_PROPERTY(qreal mediaWidth READ mediaWidth WRITE setMediaWidth NOTIFY mediaWidthChanged)
|
||||
|
||||
/**
|
||||
* @brief The base height (in px) of the media.
|
||||
*/
|
||||
Q_PROPERTY(qreal mediaHeight READ mediaHeight WRITE setMediaHeight NOTIFY mediaHeightChanged)
|
||||
|
||||
/**
|
||||
* @brief The size (in px) of the component based on the current input.
|
||||
*
|
||||
* Will always try to return a value even if some of the inputs are not set to
|
||||
* account for being called before the parameters are intialised. For any parameters
|
||||
* not set these will just be left out of the calcs.
|
||||
*
|
||||
* If no input values are provided a default placeholder value will be returned.
|
||||
*/
|
||||
Q_PROPERTY(QSize currentSize READ currentSize NOTIFY currentSizeChanged)
|
||||
|
||||
public:
|
||||
explicit MediaSizeHelper(QObject *parent = nullptr);
|
||||
|
||||
qreal contentMaxWidth() const;
|
||||
void setContentMaxWidth(qreal contentMaxWidth);
|
||||
|
||||
qreal contentMaxHeight() const;
|
||||
void setContentMaxHeight(qreal contentMaxHeight);
|
||||
|
||||
qreal mediaWidth() const;
|
||||
void setMediaWidth(qreal mediaWidth);
|
||||
|
||||
qreal mediaHeight() const;
|
||||
void setMediaHeight(qreal mediaHeight);
|
||||
|
||||
QSize currentSize() const;
|
||||
|
||||
static void setMaxSize(int width, int height);
|
||||
|
||||
Q_SIGNALS:
|
||||
void contentMaxWidthChanged();
|
||||
void contentMaxHeightChanged();
|
||||
void mediaWidthChanged();
|
||||
void mediaHeightChanged();
|
||||
void currentSizeChanged();
|
||||
|
||||
private:
|
||||
qreal m_contentMaxWidth = -1.0;
|
||||
qreal m_contentMaxHeight = -1.0;
|
||||
qreal m_mediaWidth = -1.0;
|
||||
qreal m_mediaHeight = -1.0;
|
||||
|
||||
qreal resolvedMediaWidth() const;
|
||||
qreal resolvedMediaHeight() const;
|
||||
qreal aspectRatio() const;
|
||||
bool limitWidth() const;
|
||||
qreal widthLimit() const;
|
||||
qreal heightLimit() const;
|
||||
|
||||
static int m_mediaMaxWidth;
|
||||
static int m_mediaMaxHeight;
|
||||
};
|
||||
206
src/messagecontent/models/itinerarymodel.cpp
Normal file
@@ -0,0 +1,206 @@
|
||||
// SPDX-FileCopyrightText: 2024 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
|
||||
#include "itinerarymodel.h"
|
||||
|
||||
#include <QJsonDocument>
|
||||
#include <QProcess>
|
||||
|
||||
#include "config-neochat.h"
|
||||
|
||||
#ifndef Q_OS_ANDROID
|
||||
#include <KIO/ApplicationLauncherJob>
|
||||
#endif
|
||||
|
||||
using namespace Qt::StringLiterals;
|
||||
|
||||
ItineraryModel::ItineraryModel(QObject *parent)
|
||||
: QAbstractListModel(parent)
|
||||
{
|
||||
}
|
||||
|
||||
QVariant ItineraryModel::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
if (!index.isValid()) {
|
||||
return {};
|
||||
}
|
||||
auto row = index.row();
|
||||
auto data = m_data[row];
|
||||
if (role == NameRole) {
|
||||
if (data["@type"_L1] == u"TrainReservation"_s) {
|
||||
auto trainName = u"%1 %2"_s.arg(data["reservationFor"_L1]["trainName"_L1].toString(), data["reservationFor"_L1]["trainNumber"_L1].toString());
|
||||
if (trainName.trimmed().isEmpty()) {
|
||||
return u"%1 to %2"_s.arg(data["reservationFor"_L1]["departureStation"_L1]["name"_L1].toString(),
|
||||
data["reservationFor"_L1]["arrivalStation"_L1]["name"_L1].toString());
|
||||
;
|
||||
}
|
||||
return trainName;
|
||||
}
|
||||
if (data["@type"_L1] == u"LodgingReservation"_s) {
|
||||
return data["reservationFor"_L1]["name"_L1];
|
||||
}
|
||||
if (data["@type"_L1] == u"FoodEstablishmentReservation"_s) {
|
||||
return data["reservationFor"_L1]["name"_L1];
|
||||
}
|
||||
if (data["@type"_L1] == u"FlightReservation"_s) {
|
||||
return u"%1 %2 %3 → %4"_s.arg(data["reservationFor"_L1]["airline"_L1]["iataCode"_L1].toString(),
|
||||
data["reservationFor"_L1]["flightNumber"_L1].toString(),
|
||||
data["reservationFor"_L1]["departureAirport"_L1]["iataCode"_L1].toString(),
|
||||
data["reservationFor"_L1]["arrivalAirport"_L1]["iataCode"_L1].toString());
|
||||
}
|
||||
}
|
||||
if (role == TypeRole) {
|
||||
return data["@type"_L1];
|
||||
}
|
||||
if (role == DepartureLocationRole) {
|
||||
if (data["@type"_L1] == u"TrainReservation"_s) {
|
||||
return data["reservationFor"_L1]["departureStation"_L1]["name"_L1];
|
||||
}
|
||||
if (data["@type"_L1] == u"FlightReservation"_s) {
|
||||
return data["reservationFor"_L1]["departureAirport"_L1]["iataCode"_L1];
|
||||
}
|
||||
}
|
||||
if (role == DepartureAddressRole) {
|
||||
if (data["@type"_L1] == u"TrainReservation"_s) {
|
||||
return data["reservationFor"_L1]["departureStation"_L1]["address"_L1]["addressCountry"_L1].toString();
|
||||
}
|
||||
if (data["@type"_L1] == u"FlightReservation"_s) {
|
||||
return data["reservationFor"_L1]["departureAirport"_L1]["address"_L1]["addressCountry"_L1].toString();
|
||||
}
|
||||
}
|
||||
if (role == ArrivalLocationRole) {
|
||||
if (data["@type"_L1] == u"TrainReservation"_s) {
|
||||
return data["reservationFor"_L1]["arrivalStation"_L1]["name"_L1];
|
||||
}
|
||||
if (data["@type"_L1] == u"FlightReservation"_s) {
|
||||
return data["reservationFor"_L1]["arrivalAirport"_L1]["iataCode"_L1];
|
||||
}
|
||||
}
|
||||
if (role == ArrivalAddressRole) {
|
||||
if (data["@type"_L1] == u"TrainReservation"_s) {
|
||||
return data["reservationFor"_L1]["arrivalStation"_L1]["address"_L1]["addressCountry"_L1].toString();
|
||||
}
|
||||
if (data["@type"_L1] == u"FlightReservation"_s) {
|
||||
return data["reservationFor"_L1]["arrivalAirport"_L1]["address"_L1]["addressCountry"_L1].toString();
|
||||
}
|
||||
}
|
||||
if (role == DepartureTimeRole) {
|
||||
const auto &time = data["reservationFor"_L1]["departureTime"_L1];
|
||||
auto dateTime = (time.isString() ? time : time["@value"_L1]).toVariant().toDateTime();
|
||||
if (const auto &timeZone = time["timezone"_L1].toString(); timeZone.length() > 0) {
|
||||
dateTime.setTimeZone(QTimeZone(timeZone.toLatin1().data()));
|
||||
}
|
||||
return dateTime.toString(QLocale::system().dateTimeFormat(QLocale::ShortFormat));
|
||||
}
|
||||
if (role == ArrivalTimeRole) {
|
||||
const auto &time = data["reservationFor"_L1]["arrivalTime"_L1];
|
||||
auto dateTime = (time.isString() ? time : time["@value"_L1]).toVariant().toDateTime();
|
||||
if (const auto &timeZone = time["timezone"_L1].toString(); timeZone.length() > 0) {
|
||||
dateTime.setTimeZone(QTimeZone(timeZone.toLatin1().data()));
|
||||
}
|
||||
return dateTime.toString(QLocale::system().dateTimeFormat(QLocale::ShortFormat));
|
||||
}
|
||||
if (role == AddressRole) {
|
||||
const auto &addressData = data["reservationFor"_L1]["address"_L1];
|
||||
return u"%1 - %2 %3 %4"_s.arg(addressData["streetAddress"_L1].toString(),
|
||||
addressData["postalCode"_L1].toString(),
|
||||
addressData["addressLocality"_L1].toString(),
|
||||
addressData["addressCountry"_L1].toString());
|
||||
}
|
||||
if (role == StartTimeRole) {
|
||||
QDateTime dateTime;
|
||||
if (data["@type"_L1] == u"LodgingReservation"_s) {
|
||||
dateTime = data["checkinTime"_L1]["@value"_L1].toVariant().toDateTime();
|
||||
}
|
||||
if (data["@type"_L1] == u"FoodEstablishmentReservation"_s) {
|
||||
dateTime = data["startTime"_L1]["@value"_L1].toVariant().toDateTime();
|
||||
}
|
||||
if (data["@type"_L1] == u"FlightReservation"_s) {
|
||||
dateTime = data["reservationFor"_L1]["boardingTime"_L1]["@value"_L1].toVariant().toDateTime();
|
||||
}
|
||||
return dateTime.toString(QLocale::system().dateTimeFormat(QLocale::ShortFormat));
|
||||
}
|
||||
if (role == EndTimeRole) {
|
||||
auto dateTime = data["checkoutTime"_L1]["@value"_L1].toVariant().toDateTime();
|
||||
return dateTime.toString(QLocale::system().dateTimeFormat(QLocale::ShortFormat));
|
||||
}
|
||||
if (role == DeparturePlatformRole) {
|
||||
return data["reservationFor"_L1]["departurePlatform"_L1];
|
||||
}
|
||||
if (role == ArrivalPlatformRole) {
|
||||
return data["reservationFor"_L1]["arrivalPlatform"_L1];
|
||||
}
|
||||
if (role == CoachRole) {
|
||||
return data["reservedTicket"_L1]["ticketedSeat"_L1]["seatSection"_L1];
|
||||
}
|
||||
if (role == SeatRole) {
|
||||
return data["reservedTicket"_L1]["ticketedSeat"_L1]["seatNumber"_L1];
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
int ItineraryModel::rowCount(const QModelIndex &parent) const
|
||||
{
|
||||
Q_UNUSED(parent);
|
||||
return m_data.size();
|
||||
}
|
||||
|
||||
QHash<int, QByteArray> ItineraryModel::roleNames() const
|
||||
{
|
||||
return {
|
||||
{NameRole, "name"},
|
||||
{TypeRole, "type"},
|
||||
{DepartureLocationRole, "departureLocation"},
|
||||
{DepartureAddressRole, "departureAddress"},
|
||||
{ArrivalLocationRole, "arrivalLocation"},
|
||||
{ArrivalAddressRole, "arrivalAddress"},
|
||||
{DepartureTimeRole, "departureTime"},
|
||||
{ArrivalTimeRole, "arrivalTime"},
|
||||
{AddressRole, "address"},
|
||||
{StartTimeRole, "startTime"},
|
||||
{EndTimeRole, "endTime"},
|
||||
{DeparturePlatformRole, "departurePlatform"},
|
||||
{ArrivalPlatformRole, "arrivalPlatform"},
|
||||
{CoachRole, "coach"},
|
||||
{SeatRole, "seat"},
|
||||
};
|
||||
}
|
||||
|
||||
QString ItineraryModel::path() const
|
||||
{
|
||||
return m_path;
|
||||
}
|
||||
|
||||
void ItineraryModel::setPath(const QString &path)
|
||||
{
|
||||
m_path = path;
|
||||
loadData();
|
||||
}
|
||||
|
||||
void ItineraryModel::loadData()
|
||||
{
|
||||
auto process = new QProcess(this);
|
||||
process->start(QStringLiteral(CMAKE_INSTALL_FULL_LIBEXECDIR_KF6) + u"/kitinerary-extractor"_s, {m_path.mid(7)});
|
||||
connect(process, &QProcess::finished, this, [this, process]() {
|
||||
auto data = process->readAllStandardOutput();
|
||||
beginResetModel();
|
||||
m_data = QJsonDocument::fromJson(data).array();
|
||||
endResetModel();
|
||||
|
||||
Q_EMIT loaded();
|
||||
});
|
||||
connect(process, &QProcess::errorOccurred, this, [this]() {
|
||||
Q_EMIT loadErrorOccurred();
|
||||
});
|
||||
}
|
||||
|
||||
void ItineraryModel::sendToItinerary()
|
||||
{
|
||||
#ifndef Q_OS_ANDROID
|
||||
auto job = new KIO::ApplicationLauncherJob(KService::serviceByDesktopName(u"org.kde.itinerary"_s));
|
||||
job->setUrls({QUrl::fromLocalFile(m_path.mid(7))});
|
||||
job->start();
|
||||
#endif
|
||||
}
|
||||
|
||||
#include "moc_itinerarymodel.cpp"
|
||||
57
src/messagecontent/models/itinerarymodel.h
Normal file
@@ -0,0 +1,57 @@
|
||||
// SPDX-FileCopyrightText: 2024 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QAbstractListModel>
|
||||
#include <QJsonArray>
|
||||
#include <QPointer>
|
||||
#include <QQmlEngine>
|
||||
#include <QString>
|
||||
|
||||
class ItineraryModel : public QAbstractListModel
|
||||
{
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
QML_UNCREATABLE("")
|
||||
|
||||
public:
|
||||
enum Roles {
|
||||
NameRole = Qt::DisplayRole,
|
||||
TypeRole,
|
||||
DepartureLocationRole,
|
||||
ArrivalLocationRole,
|
||||
DepartureTimeRole,
|
||||
DepartureAddressRole,
|
||||
ArrivalTimeRole,
|
||||
ArrivalAddressRole,
|
||||
AddressRole,
|
||||
StartTimeRole,
|
||||
EndTimeRole,
|
||||
DeparturePlatformRole,
|
||||
ArrivalPlatformRole,
|
||||
CoachRole,
|
||||
SeatRole,
|
||||
};
|
||||
Q_ENUM(Roles)
|
||||
explicit ItineraryModel(QObject *parent = nullptr);
|
||||
|
||||
QVariant data(const QModelIndex &index, int role) const override;
|
||||
int rowCount(const QModelIndex &parent = {}) const override;
|
||||
|
||||
QHash<int, QByteArray> roleNames() const override;
|
||||
|
||||
QString path() const;
|
||||
void setPath(const QString &path);
|
||||
|
||||
Q_INVOKABLE void sendToItinerary();
|
||||
|
||||
Q_SIGNALS:
|
||||
void loaded();
|
||||
void loadErrorOccurred();
|
||||
|
||||
private:
|
||||
QJsonArray m_data;
|
||||
QString m_path;
|
||||
void loadData();
|
||||
};
|
||||
66
src/messagecontent/models/linemodel.cpp
Normal file
@@ -0,0 +1,66 @@
|
||||
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
|
||||
#include "linemodel.h"
|
||||
|
||||
LineModel::LineModel(QObject *parent)
|
||||
: QAbstractListModel(parent)
|
||||
{
|
||||
}
|
||||
|
||||
QQuickTextDocument *LineModel::document() const
|
||||
{
|
||||
return m_document;
|
||||
}
|
||||
|
||||
void LineModel::setDocument(QQuickTextDocument *document)
|
||||
{
|
||||
if (document == m_document) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_document = document;
|
||||
Q_EMIT documentChanged();
|
||||
|
||||
resetModel();
|
||||
}
|
||||
|
||||
QVariant LineModel::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
if (!index.isValid()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const auto &row = index.row();
|
||||
if (row < 0 || row > rowCount()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
if (role == LineHeightRole) {
|
||||
auto textDoc = m_document->textDocument();
|
||||
return int(textDoc->documentLayout()->blockBoundingRect(textDoc->findBlockByNumber(row)).height());
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
int LineModel::rowCount(const QModelIndex &parent) const
|
||||
{
|
||||
Q_UNUSED(parent);
|
||||
if (m_document == nullptr) {
|
||||
return 0;
|
||||
}
|
||||
return m_document->textDocument()->blockCount();
|
||||
}
|
||||
|
||||
QHash<int, QByteArray> LineModel::roleNames() const
|
||||
{
|
||||
return {{LineHeightRole, "docLineHeight"}};
|
||||
}
|
||||
|
||||
void LineModel::resetModel()
|
||||
{
|
||||
beginResetModel();
|
||||
endResetModel();
|
||||
}
|
||||
|
||||
#include "moc_linemodel.cpp"
|
||||
80
src/messagecontent/models/linemodel.h
Normal file
@@ -0,0 +1,80 @@
|
||||
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QAbstractListModel>
|
||||
#include <QAbstractTextDocumentLayout>
|
||||
#include <QQmlEngine>
|
||||
#include <QQuickTextDocument>
|
||||
#include <QTextBlock>
|
||||
#include <qtmetamacros.h>
|
||||
|
||||
/**
|
||||
* @class LineModel
|
||||
*
|
||||
* A model to provide line info for a QQuickTextDocument.
|
||||
*
|
||||
* @sa QQuickTextDocument
|
||||
*/
|
||||
class LineModel : public QAbstractListModel
|
||||
{
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
|
||||
/**
|
||||
* @brief The QQuickTextDocument that is being handled.
|
||||
*/
|
||||
Q_PROPERTY(QQuickTextDocument *document READ document WRITE setDocument NOTIFY documentChanged)
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Defines the model roles.
|
||||
*/
|
||||
enum Roles {
|
||||
LineHeightRole = Qt::UserRole + 1, /**< The delegate type of the message. */
|
||||
};
|
||||
Q_ENUM(Roles)
|
||||
|
||||
explicit LineModel(QObject *parent = nullptr);
|
||||
|
||||
[[nodiscard]] QQuickTextDocument *document() const;
|
||||
void setDocument(QQuickTextDocument *document);
|
||||
|
||||
/**
|
||||
* @brief Get the given role value at the given index.
|
||||
*
|
||||
* @sa QAbstractItemModel::data
|
||||
*/
|
||||
[[nodiscard]] QVariant data(const QModelIndex &index, int role) const override;
|
||||
|
||||
/**
|
||||
* @brief Number of rows in the model.
|
||||
*
|
||||
* @sa QAbstractItemModel::rowCount
|
||||
*/
|
||||
[[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||
|
||||
/**
|
||||
* @brief Returns a mapping from Role enum values to role names.
|
||||
*
|
||||
* @sa Roles, QAbstractItemModel::roleNames()
|
||||
*/
|
||||
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
|
||||
|
||||
/**
|
||||
* @brief Reset the model.
|
||||
*
|
||||
* This needs to be called when the QQuickTextDocument container changes width
|
||||
* or height as this may change line heights due to wrapping.
|
||||
*
|
||||
* @sa QQuickTextDocument
|
||||
*/
|
||||
Q_INVOKABLE void resetModel();
|
||||
|
||||
Q_SIGNALS:
|
||||
void documentChanged();
|
||||
|
||||
private:
|
||||
QPointer<QQuickTextDocument> m_document = nullptr;
|
||||
};
|
||||
790
src/messagecontent/models/messagecontentmodel.cpp
Normal file
@@ -0,0 +1,790 @@
|
||||
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
|
||||
#include "messagecontentmodel.h"
|
||||
#include "contentprovider.h"
|
||||
#include "enums/messagecomponenttype.h"
|
||||
#include "eventhandler.h"
|
||||
|
||||
#include <QImageReader>
|
||||
|
||||
#include <Quotient/events/eventcontent.h>
|
||||
#include <Quotient/events/redactionevent.h>
|
||||
#include <Quotient/events/roommessageevent.h>
|
||||
#include <Quotient/events/stickerevent.h>
|
||||
#include <Quotient/qt_connection_util.h>
|
||||
#if Quotient_VERSION_MINOR > 9 || (Quotient_VERSION_MINOR == 9 && Quotient_VERSION_PATCH > 1)
|
||||
#include <Quotient/thread.h>
|
||||
#endif
|
||||
|
||||
#include <KLocalizedString>
|
||||
#include <Kirigami/Platform/PlatformTheme>
|
||||
|
||||
#ifndef Q_OS_ANDROID
|
||||
#include <KSyntaxHighlighting/Definition>
|
||||
#include <KSyntaxHighlighting/Repository>
|
||||
#endif
|
||||
|
||||
#include "chatbarcache.h"
|
||||
#include "contentprovider.h"
|
||||
#include "filetype.h"
|
||||
#include "linkpreviewer.h"
|
||||
#include "models/reactionmodel.h"
|
||||
#include "neochatconnection.h"
|
||||
#include "neochatroom.h"
|
||||
#include "texthandler.h"
|
||||
|
||||
using namespace Quotient;
|
||||
|
||||
bool MessageContentModel::m_threadsEnabled = false;
|
||||
|
||||
MessageContentModel::MessageContentModel(NeoChatRoom *room, const QString &eventId, bool isReply, bool isPending, MessageContentModel *parent)
|
||||
: QAbstractListModel(parent)
|
||||
, m_room(room)
|
||||
, m_eventId(eventId)
|
||||
, m_currentState(isPending ? Pending : Unknown)
|
||||
, m_isReply(isReply)
|
||||
{
|
||||
initializeModel();
|
||||
}
|
||||
|
||||
void MessageContentModel::initializeModel()
|
||||
{
|
||||
Q_ASSERT(m_room != nullptr);
|
||||
Q_ASSERT(!m_eventId.isEmpty());
|
||||
|
||||
connect(m_room, &NeoChatRoom::pendingEventAdded, this, [this]() {
|
||||
if (m_room != nullptr && m_currentState == Unknown) {
|
||||
initializeEvent();
|
||||
updateReplyModel();
|
||||
resetModel();
|
||||
}
|
||||
});
|
||||
connect(m_room, &NeoChatRoom::pendingEventAboutToMerge, this, [this](Quotient::RoomEvent *serverEvent) {
|
||||
if (m_room != nullptr) {
|
||||
if (m_eventId == serverEvent->id() || m_eventId == serverEvent->transactionId()) {
|
||||
m_eventId = serverEvent->id();
|
||||
}
|
||||
}
|
||||
});
|
||||
connect(m_room, &NeoChatRoom::pendingEventMerged, this, [this]() {
|
||||
if (m_room != nullptr && m_currentState == Pending) {
|
||||
initializeEvent();
|
||||
updateReplyModel();
|
||||
resetModel();
|
||||
}
|
||||
});
|
||||
connect(m_room, &NeoChatRoom::addedMessages, this, [this](int fromIndex, int toIndex) {
|
||||
if (m_room != nullptr) {
|
||||
for (int i = fromIndex; i <= toIndex; i++) {
|
||||
if (m_room->findInTimeline(i)->event()->id() == m_eventId) {
|
||||
initializeEvent();
|
||||
updateReplyModel();
|
||||
resetModel();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
connect(m_room, &NeoChatRoom::replacedEvent, this, [this](const Quotient::RoomEvent *newEvent) {
|
||||
if (m_room != nullptr) {
|
||||
if (m_eventId == newEvent->id()) {
|
||||
beginResetModel();
|
||||
initializeEvent();
|
||||
resetContent();
|
||||
endResetModel();
|
||||
}
|
||||
}
|
||||
});
|
||||
connect(m_room, &NeoChatRoom::newFileTransfer, this, [this](const QString &eventId) {
|
||||
if (eventId == m_eventId) {
|
||||
Q_EMIT dataChanged(index(0), index(rowCount() - 1), {FileTransferInfoRole});
|
||||
}
|
||||
});
|
||||
connect(m_room, &NeoChatRoom::fileTransferProgress, this, [this](const QString &eventId) {
|
||||
if (eventId == m_eventId) {
|
||||
Q_EMIT dataChanged(index(0), index(rowCount() - 1), {FileTransferInfoRole});
|
||||
}
|
||||
});
|
||||
connect(m_room, &NeoChatRoom::fileTransferCompleted, this, [this](const QString &eventId) {
|
||||
if (m_room != nullptr && eventId == m_eventId) {
|
||||
resetContent();
|
||||
Q_EMIT dataChanged(index(0), index(rowCount() - 1), {FileTransferInfoRole});
|
||||
}
|
||||
});
|
||||
connect(m_room, &NeoChatRoom::fileTransferFailed, this, [this](const QString &eventId) {
|
||||
if (eventId == m_eventId) {
|
||||
resetContent();
|
||||
Q_EMIT dataChanged(index(0), index(rowCount() - 1), {FileTransferInfoRole});
|
||||
}
|
||||
});
|
||||
connect(m_room->editCache(), &ChatBarCache::relationIdChanged, this, [this](const QString &oldEventId, const QString &newEventId) {
|
||||
if (oldEventId == m_eventId || newEventId == m_eventId) {
|
||||
// HACK: Because DelegateChooser can't switch the delegate on dataChanged it has to think there is a new delegate.
|
||||
beginResetModel();
|
||||
resetContent(newEventId == m_eventId);
|
||||
endResetModel();
|
||||
}
|
||||
});
|
||||
connect(m_room->threadCache(), &ChatBarCache::threadIdChanged, this, [this](const QString &oldThreadId, const QString &newThreadId) {
|
||||
if (oldThreadId == m_eventId || newThreadId == m_eventId) {
|
||||
beginResetModel();
|
||||
resetContent(false, newThreadId == m_eventId);
|
||||
endResetModel();
|
||||
}
|
||||
});
|
||||
connect(m_room, &NeoChatRoom::urlPreviewEnabledChanged, this, [this]() {
|
||||
resetContent();
|
||||
});
|
||||
connect(m_room, &Room::memberNameUpdated, this, [this](RoomMember member) {
|
||||
if (m_room != nullptr) {
|
||||
if (senderId().isEmpty() || senderId() == member.id()) {
|
||||
Q_EMIT dataChanged(index(0, 0), index(rowCount() - 1, 0), {AuthorRole});
|
||||
}
|
||||
}
|
||||
});
|
||||
connect(m_room, &Room::memberAvatarUpdated, this, [this](RoomMember member) {
|
||||
if (m_room != nullptr) {
|
||||
if (senderId().isEmpty() || senderId() == member.id()) {
|
||||
Q_EMIT dataChanged(index(0, 0), index(rowCount() - 1, 0), {AuthorRole});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
connect(this, &MessageContentModel::threadsEnabledChanged, this, [this]() {
|
||||
updateReplyModel();
|
||||
resetModel();
|
||||
});
|
||||
connect(m_room, &Room::updatedEvent, this, [this](const QString &eventId) {
|
||||
if (eventId == m_eventId) {
|
||||
updateReactionModel();
|
||||
}
|
||||
});
|
||||
|
||||
initializeEvent();
|
||||
if (m_currentState == Available || m_currentState == Pending) {
|
||||
updateReplyModel();
|
||||
}
|
||||
resetModel();
|
||||
updateReactionModel();
|
||||
}
|
||||
|
||||
void MessageContentModel::initializeEvent()
|
||||
{
|
||||
if (m_currentState == UnAvailable) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto eventResult = m_room->getEvent(m_eventId);
|
||||
if (eventResult.first == nullptr) {
|
||||
if (m_currentState != Pending) {
|
||||
getEvent();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (eventResult.second) {
|
||||
m_currentState = Pending;
|
||||
} else {
|
||||
m_currentState = Available;
|
||||
}
|
||||
Q_EMIT eventUpdated();
|
||||
}
|
||||
|
||||
void MessageContentModel::getEvent()
|
||||
{
|
||||
Quotient::connectUntil(m_room.get(), &NeoChatRoom::extraEventLoaded, this, [this](const QString &eventId) {
|
||||
if (m_room != nullptr) {
|
||||
if (eventId == m_eventId) {
|
||||
initializeEvent();
|
||||
updateReplyModel();
|
||||
resetModel();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
Quotient::connectUntil(m_room.get(), &NeoChatRoom::extraEventNotFound, this, [this](const QString &eventId) {
|
||||
if (m_room != nullptr) {
|
||||
if (eventId == m_eventId) {
|
||||
m_currentState = UnAvailable;
|
||||
resetModel();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
m_room->downloadEventFromServer(m_eventId);
|
||||
}
|
||||
|
||||
QString MessageContentModel::senderId() const
|
||||
{
|
||||
const auto eventResult = m_room->getEvent(m_eventId);
|
||||
if (eventResult.first == nullptr) {
|
||||
return {};
|
||||
}
|
||||
auto senderId = eventResult.first->senderId();
|
||||
if (senderId.isEmpty()) {
|
||||
senderId = m_room->localMember().id();
|
||||
}
|
||||
return senderId;
|
||||
}
|
||||
|
||||
NeochatRoomMember *MessageContentModel::senderObject() const
|
||||
{
|
||||
const auto eventResult = m_room->getEvent(m_eventId);
|
||||
if (eventResult.first == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
if (eventResult.first->senderId().isEmpty()) {
|
||||
return m_room->qmlSafeMember(m_room->localMember().id());
|
||||
}
|
||||
return m_room->qmlSafeMember(eventResult.first->senderId());
|
||||
}
|
||||
|
||||
static LinkPreviewer *emptyLinkPreview = new LinkPreviewer;
|
||||
|
||||
QVariant MessageContentModel::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
if (!index.isValid()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
if (index.row() >= rowCount()) {
|
||||
qDebug() << "MessageContentModel, something's wrong: index.row() >= rowCount()";
|
||||
return {};
|
||||
}
|
||||
|
||||
const auto component = m_components[index.row()];
|
||||
|
||||
const auto event = m_room->getEvent(m_eventId);
|
||||
if (event.first == nullptr) {
|
||||
if (role == DisplayRole) {
|
||||
if (m_isReply) {
|
||||
return i18n("Loading reply");
|
||||
} else {
|
||||
return i18n("Loading");
|
||||
}
|
||||
}
|
||||
if (role == ComponentTypeRole) {
|
||||
return component.type;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
if (role == DisplayRole) {
|
||||
if (m_currentState == UnAvailable || m_room->connection()->isIgnored(senderId())) {
|
||||
Kirigami::Platform::PlatformTheme *theme =
|
||||
static_cast<Kirigami::Platform::PlatformTheme *>(qmlAttachedPropertiesObject<Kirigami::Platform::PlatformTheme>(this, true));
|
||||
|
||||
QString disabledTextColor;
|
||||
if (theme != nullptr) {
|
||||
disabledTextColor = theme->disabledTextColor().name();
|
||||
} else {
|
||||
disabledTextColor = u"#000000"_s;
|
||||
}
|
||||
return QString(u"<span style=\"color:%1\">"_s.arg(disabledTextColor)
|
||||
+ i18nc("@info", "This message was either not found, you do not have permission to view it, or it was sent by an ignored user")
|
||||
+ u"</span>"_s);
|
||||
}
|
||||
if (component.type == MessageComponentType::Loading) {
|
||||
if (m_isReply) {
|
||||
return i18n("Loading reply");
|
||||
} else {
|
||||
return i18n("Loading");
|
||||
}
|
||||
}
|
||||
if (!component.content.isEmpty()) {
|
||||
return component.content;
|
||||
}
|
||||
return EventHandler::richBody(m_room, event.first);
|
||||
}
|
||||
if (role == ComponentTypeRole) {
|
||||
return component.type;
|
||||
}
|
||||
if (role == ComponentAttributesRole) {
|
||||
return component.attributes;
|
||||
}
|
||||
if (role == EventIdRole) {
|
||||
return event.first->displayId();
|
||||
}
|
||||
if (role == TimeRole) {
|
||||
return EventHandler::time(m_room, event.first, m_currentState == Pending);
|
||||
}
|
||||
if (role == TimeStringRole) {
|
||||
return EventHandler::timeString(m_room, event.first, u"hh:mm"_s, m_currentState == Pending);
|
||||
}
|
||||
if (role == AuthorRole) {
|
||||
return QVariant::fromValue<NeochatRoomMember *>(senderObject());
|
||||
}
|
||||
if (role == MediaInfoRole) {
|
||||
return EventHandler::mediaInfo(m_room, event.first);
|
||||
}
|
||||
if (role == FileTransferInfoRole) {
|
||||
return QVariant::fromValue(m_room->cachedFileTransferInfo(event.first));
|
||||
}
|
||||
if (role == ItineraryModelRole) {
|
||||
return QVariant::fromValue<ItineraryModel *>(m_itineraryModel);
|
||||
}
|
||||
if (role == LatitudeRole) {
|
||||
return EventHandler::latitude(event.first);
|
||||
}
|
||||
if (role == LongitudeRole) {
|
||||
return EventHandler::longitude(event.first);
|
||||
}
|
||||
if (role == AssetRole) {
|
||||
return EventHandler::locationAssetType(event.first);
|
||||
}
|
||||
if (role == PollHandlerRole) {
|
||||
return QVariant::fromValue<PollHandler *>(ContentProvider::self().handlerForPoll(m_room, m_eventId));
|
||||
}
|
||||
if (role == ReplyEventIdRole) {
|
||||
if (const auto roomMessageEvent = eventCast<const RoomMessageEvent>(event.first)) {
|
||||
return roomMessageEvent->replyEventId();
|
||||
}
|
||||
}
|
||||
if (role == ReplyAuthorRole) {
|
||||
return QVariant::fromValue(EventHandler::replyAuthor(m_room, event.first));
|
||||
}
|
||||
if (role == ReplyContentModelRole) {
|
||||
return QVariant::fromValue<MessageContentModel *>(m_replyModel);
|
||||
}
|
||||
if (role == ReactionModelRole) {
|
||||
return QVariant::fromValue<ReactionModel *>(m_reactionModel);
|
||||
;
|
||||
}
|
||||
if (role == ThreadRootRole) {
|
||||
auto roomMessageEvent = eventCast<const RoomMessageEvent>(event.first);
|
||||
#if Quotient_VERSION_MINOR > 9 || (Quotient_VERSION_MINOR == 9 && Quotient_VERSION_PATCH > 1)
|
||||
if (roomMessageEvent && (roomMessageEvent->isThreaded() || m_room->threads().contains(roomMessageEvent->id()))) {
|
||||
#else
|
||||
if (roomMessageEvent && roomMessageEvent->isThreaded()) {
|
||||
#endif
|
||||
return roomMessageEvent->threadRootEventId();
|
||||
}
|
||||
return {};
|
||||
}
|
||||
if (role == LinkPreviewerRole) {
|
||||
if (component.type == MessageComponentType::LinkPreview) {
|
||||
return QVariant::fromValue<LinkPreviewer *>(
|
||||
dynamic_cast<NeoChatConnection *>(m_room->connection())->previewerForLink(component.attributes["link"_L1].toUrl()));
|
||||
} else {
|
||||
return QVariant::fromValue<LinkPreviewer *>(emptyLinkPreview);
|
||||
}
|
||||
}
|
||||
if (role == ChatBarCacheRole) {
|
||||
if (m_room->threadCache()->threadId() == m_eventId) {
|
||||
return QVariant::fromValue<ChatBarCache *>(m_room->threadCache());
|
||||
}
|
||||
return QVariant::fromValue<ChatBarCache *>(m_room->editCache());
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
int MessageContentModel::rowCount(const QModelIndex &parent) const
|
||||
{
|
||||
Q_UNUSED(parent)
|
||||
return m_components.size();
|
||||
}
|
||||
|
||||
QHash<int, QByteArray> MessageContentModel::roleNames() const
|
||||
{
|
||||
return roleNamesStatic();
|
||||
}
|
||||
|
||||
QHash<int, QByteArray> MessageContentModel::roleNamesStatic()
|
||||
{
|
||||
QHash<int, QByteArray> roles;
|
||||
roles[MessageContentModel::DisplayRole] = "display";
|
||||
roles[MessageContentModel::ComponentTypeRole] = "componentType";
|
||||
roles[MessageContentModel::ComponentAttributesRole] = "componentAttributes";
|
||||
roles[MessageContentModel::EventIdRole] = "eventId";
|
||||
roles[MessageContentModel::TimeRole] = "time";
|
||||
roles[MessageContentModel::TimeStringRole] = "timeString";
|
||||
roles[MessageContentModel::AuthorRole] = "author";
|
||||
roles[MessageContentModel::MediaInfoRole] = "mediaInfo";
|
||||
roles[MessageContentModel::FileTransferInfoRole] = "fileTransferInfo";
|
||||
roles[MessageContentModel::ItineraryModelRole] = "itineraryModel";
|
||||
roles[MessageContentModel::LatitudeRole] = "latitude";
|
||||
roles[MessageContentModel::LongitudeRole] = "longitude";
|
||||
roles[MessageContentModel::AssetRole] = "asset";
|
||||
roles[MessageContentModel::PollHandlerRole] = "pollHandler";
|
||||
roles[MessageContentModel::ReplyEventIdRole] = "replyEventId";
|
||||
roles[MessageContentModel::ReplyAuthorRole] = "replyAuthor";
|
||||
roles[MessageContentModel::ReplyContentModelRole] = "replyContentModel";
|
||||
roles[MessageContentModel::ReactionModelRole] = "reactionModel";
|
||||
roles[MessageContentModel::ThreadRootRole] = "threadRoot";
|
||||
roles[MessageContentModel::LinkPreviewerRole] = "linkPreviewer";
|
||||
roles[MessageContentModel::ChatBarCacheRole] = "chatBarCache";
|
||||
return roles;
|
||||
}
|
||||
|
||||
void MessageContentModel::resetModel()
|
||||
{
|
||||
beginResetModel();
|
||||
m_components.clear();
|
||||
|
||||
if (m_room->connection()->isIgnored(senderId()) || m_currentState == UnAvailable) {
|
||||
m_components += MessageComponent{MessageComponentType::Text, QString(), {}};
|
||||
endResetModel();
|
||||
return;
|
||||
}
|
||||
|
||||
const auto event = m_room->getEvent(m_eventId);
|
||||
if (event.first == nullptr) {
|
||||
m_components += MessageComponent{MessageComponentType::Loading, QString(), {}};
|
||||
endResetModel();
|
||||
return;
|
||||
}
|
||||
|
||||
m_components += MessageComponent{MessageComponentType::Author, QString(), {}};
|
||||
|
||||
m_components += messageContentComponents();
|
||||
endResetModel();
|
||||
}
|
||||
|
||||
void MessageContentModel::resetContent(bool isEditing, bool isThreading)
|
||||
{
|
||||
const auto startRow = m_components[0].type == MessageComponentType::Author ? 1 : 0;
|
||||
beginRemoveRows({}, startRow, rowCount() - 1);
|
||||
m_components.remove(startRow, rowCount() - startRow);
|
||||
endRemoveRows();
|
||||
|
||||
const auto newComponents = messageContentComponents(isEditing, isThreading);
|
||||
if (newComponents.size() == 0) {
|
||||
return;
|
||||
}
|
||||
beginInsertRows({}, startRow, startRow + newComponents.size() - 1);
|
||||
m_components += newComponents;
|
||||
endInsertRows();
|
||||
}
|
||||
|
||||
QList<MessageComponent> MessageContentModel::messageContentComponents(bool isEditing, bool isThreading)
|
||||
{
|
||||
const auto event = m_room->getEvent(m_eventId);
|
||||
if (event.first == nullptr) {
|
||||
return {};
|
||||
}
|
||||
|
||||
QList<MessageComponent> newComponents;
|
||||
|
||||
const auto roomMessageEvent = eventCast<const Quotient::RoomMessageEvent>(event.first);
|
||||
if (roomMessageEvent && roomMessageEvent->rawMsgtype() == u"m.key.verification.request"_s) {
|
||||
newComponents += MessageComponent{MessageComponentType::Verification, QString(), {}};
|
||||
return newComponents;
|
||||
}
|
||||
|
||||
if (event.first->isRedacted()) {
|
||||
newComponents += MessageComponent{MessageComponentType::Text, QString(), {}};
|
||||
return newComponents;
|
||||
}
|
||||
|
||||
if (m_replyModel != nullptr) {
|
||||
newComponents += MessageComponent{MessageComponentType::Reply, QString(), {}};
|
||||
}
|
||||
|
||||
if (isEditing) {
|
||||
newComponents += MessageComponent{MessageComponentType::ChatBar, QString(), {}};
|
||||
} else {
|
||||
newComponents.append(componentsForType(MessageComponentType::typeForEvent(*event.first)));
|
||||
}
|
||||
|
||||
if (m_room->urlPreviewEnabled()) {
|
||||
newComponents = addLinkPreviews(newComponents);
|
||||
}
|
||||
|
||||
if ((m_reactionModel && m_reactionModel->rowCount() > 0)) {
|
||||
newComponents += MessageComponent{MessageComponentType::Reaction, QString(), {}};
|
||||
}
|
||||
|
||||
#if Quotient_VERSION_MINOR > 9 || (Quotient_VERSION_MINOR == 9 && Quotient_VERSION_PATCH > 1)
|
||||
if (m_threadsEnabled && roomMessageEvent && (roomMessageEvent->isThreaded() || m_room->threads().contains(roomMessageEvent->id()))
|
||||
&& roomMessageEvent->id() == roomMessageEvent->threadRootEventId()) {
|
||||
#else
|
||||
if (m_threadsEnabled && roomMessageEvent && roomMessageEvent->isThreaded() && roomMessageEvent->id() == roomMessageEvent->threadRootEventId()) {
|
||||
#endif
|
||||
newComponents += MessageComponent{MessageComponentType::Separator, {}, {}};
|
||||
newComponents += MessageComponent{MessageComponentType::ThreadBody, u"Thread Body"_s, {}};
|
||||
}
|
||||
|
||||
// If the event is already threaded the ThreadModel will handle displaying a chat bar.
|
||||
#if Quotient_VERSION_MINOR > 9 || (Quotient_VERSION_MINOR == 9 && Quotient_VERSION_PATCH > 1)
|
||||
if (isThreading && roomMessageEvent && !(roomMessageEvent->isThreaded() || m_room->threads().contains(roomMessageEvent->id()))) {
|
||||
#else
|
||||
if (isThreading && roomMessageEvent && roomMessageEvent->isThreaded()) {
|
||||
#endif
|
||||
newComponents += MessageComponent{MessageComponentType::ChatBar, QString(), {}};
|
||||
}
|
||||
|
||||
return newComponents;
|
||||
}
|
||||
|
||||
void MessageContentModel::updateReplyModel()
|
||||
{
|
||||
const auto event = m_room->getEvent(m_eventId);
|
||||
if (event.first == nullptr || m_isReply) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto roomMessageEvent = eventCast<const Quotient::RoomMessageEvent>(event.first);
|
||||
if (roomMessageEvent == nullptr) {
|
||||
return;
|
||||
}
|
||||
if (!roomMessageEvent->isReply(m_threadsEnabled) || (roomMessageEvent->isThreaded() && m_threadsEnabled)) {
|
||||
if (m_replyModel) {
|
||||
delete m_replyModel;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_replyModel != nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_replyModel = new MessageContentModel(m_room, roomMessageEvent->replyEventId(!m_threadsEnabled), true, false, this);
|
||||
|
||||
connect(m_replyModel, &MessageContentModel::eventUpdated, this, [this]() {
|
||||
Q_EMIT dataChanged(index(0), index(0), {ReplyAuthorRole});
|
||||
});
|
||||
}
|
||||
|
||||
QList<MessageComponent> MessageContentModel::componentsForType(MessageComponentType::Type type)
|
||||
{
|
||||
const auto event = m_room->getEvent(m_eventId);
|
||||
if (event.first == nullptr) {
|
||||
return {};
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
case MessageComponentType::Text: {
|
||||
const auto roomMessageEvent = eventCast<const Quotient::RoomMessageEvent>(event.first);
|
||||
auto body = EventHandler::rawMessageBody(*roomMessageEvent);
|
||||
if (body.trimmed().isEmpty()) {
|
||||
return TextHandler().textComponents(i18n("<i>This event does not have any content.</i>"),
|
||||
Qt::TextFormat::RichText,
|
||||
m_room,
|
||||
roomMessageEvent,
|
||||
roomMessageEvent->isReplaced());
|
||||
} else {
|
||||
return TextHandler().textComponents(body,
|
||||
EventHandler::messageBodyInputFormat(*roomMessageEvent),
|
||||
m_room,
|
||||
roomMessageEvent,
|
||||
roomMessageEvent->isReplaced());
|
||||
}
|
||||
}
|
||||
case MessageComponentType::File: {
|
||||
QList<MessageComponent> components;
|
||||
components += MessageComponent{MessageComponentType::File, QString(), {}};
|
||||
const auto roomMessageEvent = eventCast<const Quotient::RoomMessageEvent>(event.first);
|
||||
|
||||
if (m_emptyItinerary) {
|
||||
if (!m_isReply) {
|
||||
auto fileTransferInfo = m_room->cachedFileTransferInfo(event.first);
|
||||
|
||||
#ifndef Q_OS_ANDROID
|
||||
Q_ASSERT(roomMessageEvent->content() != nullptr && roomMessageEvent->has<EventContent::FileContent>());
|
||||
const QMimeType mimeType = roomMessageEvent->get<EventContent::FileContent>()->mimeType;
|
||||
if (mimeType.name() == u"text/plain"_s || mimeType.parentMimeTypes().contains(u"text/plain"_s)) {
|
||||
QString originalName = roomMessageEvent->get<EventContent::FileContent>()->originalName;
|
||||
if (originalName.isEmpty()) {
|
||||
originalName = roomMessageEvent->plainBody();
|
||||
}
|
||||
KSyntaxHighlighting::Repository repository;
|
||||
KSyntaxHighlighting::Definition definitionForFile = repository.definitionForFileName(originalName);
|
||||
if (!definitionForFile.isValid()) {
|
||||
definitionForFile = repository.definitionForMimeType(mimeType.name());
|
||||
}
|
||||
|
||||
QFile file(fileTransferInfo.localPath.path());
|
||||
file.open(QIODevice::ReadOnly);
|
||||
components += MessageComponent{MessageComponentType::Code,
|
||||
QString::fromStdString(file.readAll().toStdString()),
|
||||
{{u"class"_s, definitionForFile.name()}}};
|
||||
}
|
||||
#endif
|
||||
|
||||
if (FileType::instance().fileHasImage(fileTransferInfo.localPath)) {
|
||||
QImageReader reader(fileTransferInfo.localPath.path());
|
||||
components += MessageComponent{MessageComponentType::Pdf, QString(), {{u"size"_s, reader.size()}}};
|
||||
}
|
||||
}
|
||||
} else if (m_itineraryModel != nullptr) {
|
||||
components += MessageComponent{MessageComponentType::Itinerary, QString(), {}};
|
||||
if (m_itineraryModel->rowCount() > 0) {
|
||||
updateItineraryModel();
|
||||
}
|
||||
} else {
|
||||
updateItineraryModel();
|
||||
}
|
||||
auto body = EventHandler::rawMessageBody(*roomMessageEvent);
|
||||
components += TextHandler().textComponents(body,
|
||||
EventHandler::messageBodyInputFormat(*roomMessageEvent),
|
||||
m_room,
|
||||
roomMessageEvent,
|
||||
roomMessageEvent->isReplaced());
|
||||
return components;
|
||||
}
|
||||
case MessageComponentType::Image:
|
||||
case MessageComponentType::Audio:
|
||||
case MessageComponentType::Video: {
|
||||
if (!event.first->is<StickerEvent>()) {
|
||||
const auto roomMessageEvent = eventCast<const Quotient::RoomMessageEvent>(event.first);
|
||||
const auto fileContent = roomMessageEvent->get<EventContent::FileContentBase>();
|
||||
if (fileContent != nullptr) {
|
||||
const auto fileInfo = fileContent->commonInfo();
|
||||
const auto body = EventHandler::rawMessageBody(*roomMessageEvent);
|
||||
// Do not attach the description to the image, if it's the same as the original filename.
|
||||
if (fileInfo.originalName != body) {
|
||||
QList<MessageComponent> components;
|
||||
components += MessageComponent{type, QString(), {}};
|
||||
components += TextHandler().textComponents(body,
|
||||
EventHandler::messageBodyInputFormat(*roomMessageEvent),
|
||||
m_room,
|
||||
roomMessageEvent,
|
||||
roomMessageEvent->isReplaced());
|
||||
return components;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
[[fallthrough]];
|
||||
default:
|
||||
return {MessageComponent{type, QString(), {}}};
|
||||
}
|
||||
}
|
||||
|
||||
MessageComponent MessageContentModel::linkPreviewComponent(const QUrl &link)
|
||||
{
|
||||
const auto linkPreviewer = dynamic_cast<NeoChatConnection *>(m_room->connection())->previewerForLink(link);
|
||||
if (linkPreviewer == nullptr) {
|
||||
return {};
|
||||
}
|
||||
if (linkPreviewer->loaded()) {
|
||||
return MessageComponent{MessageComponentType::LinkPreview, QString(), {{"link"_L1, link}}};
|
||||
} else {
|
||||
connect(linkPreviewer, &LinkPreviewer::loadedChanged, this, [this, link]() {
|
||||
const auto linkPreviewer = dynamic_cast<NeoChatConnection *>(m_room->connection())->previewerForLink(link);
|
||||
if (linkPreviewer != nullptr && linkPreviewer->loaded()) {
|
||||
for (auto &component : m_components) {
|
||||
if (component.attributes["link"_L1].toUrl() == link) {
|
||||
// HACK: Because DelegateChooser can't switch the delegate on dataChanged it has to think there is a new delegate.
|
||||
beginResetModel();
|
||||
component.type = MessageComponentType::LinkPreview;
|
||||
endResetModel();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
return MessageComponent{MessageComponentType::LinkPreviewLoad, QString(), {{"link"_L1, link}}};
|
||||
}
|
||||
}
|
||||
|
||||
QList<MessageComponent> MessageContentModel::addLinkPreviews(QList<MessageComponent> inputComponents)
|
||||
{
|
||||
int i = 0;
|
||||
while (i < inputComponents.size()) {
|
||||
const auto component = inputComponents.at(i);
|
||||
if (component.type == MessageComponentType::Text || component.type == MessageComponentType::Quote) {
|
||||
if (LinkPreviewer::hasPreviewableLinks(component.content)) {
|
||||
const auto links = LinkPreviewer::linkPreviews(component.content);
|
||||
for (qsizetype j = 0; j < links.size(); ++j) {
|
||||
const auto linkPreview = linkPreviewComponent(links[j]);
|
||||
if (!m_removedLinkPreviews.contains(links[j]) && !linkPreview.isEmpty()) {
|
||||
inputComponents.insert(i + j + 1, linkPreview);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
i++;
|
||||
}
|
||||
|
||||
return inputComponents;
|
||||
}
|
||||
|
||||
void MessageContentModel::closeLinkPreview(int row)
|
||||
{
|
||||
if (row < 0 || row >= m_components.size()) {
|
||||
qWarning() << "closeLinkPreview() called with row" << row << "which does not exist. m_components.size() =" << m_components.size();
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_components[row].type == MessageComponentType::LinkPreview || m_components[row].type == MessageComponentType::LinkPreviewLoad) {
|
||||
beginResetModel();
|
||||
m_removedLinkPreviews += m_components[row].attributes["link"_L1].toUrl();
|
||||
m_components.remove(row);
|
||||
m_components.squeeze();
|
||||
endResetModel();
|
||||
resetContent();
|
||||
}
|
||||
}
|
||||
|
||||
void MessageContentModel::updateItineraryModel()
|
||||
{
|
||||
const auto event = m_room->getEvent(m_eventId);
|
||||
if (m_room == nullptr || event.first == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (auto roomMessageEvent = eventCast<const Quotient::RoomMessageEvent>(event.first)) {
|
||||
if (roomMessageEvent->has<EventContent::FileContent>()) {
|
||||
auto filePath = m_room->cachedFileTransferInfo(event.first).localPath;
|
||||
if (filePath.isEmpty() && m_itineraryModel != nullptr) {
|
||||
delete m_itineraryModel;
|
||||
m_itineraryModel = nullptr;
|
||||
} else if (!filePath.isEmpty()) {
|
||||
if (m_itineraryModel == nullptr) {
|
||||
m_itineraryModel = new ItineraryModel(this);
|
||||
connect(m_itineraryModel, &ItineraryModel::loaded, this, [this]() {
|
||||
if (m_itineraryModel->rowCount() == 0) {
|
||||
m_emptyItinerary = true;
|
||||
m_itineraryModel->deleteLater();
|
||||
m_itineraryModel = nullptr;
|
||||
resetContent();
|
||||
}
|
||||
});
|
||||
connect(m_itineraryModel, &ItineraryModel::loadErrorOccurred, this, [this]() {
|
||||
m_emptyItinerary = true;
|
||||
m_itineraryModel->deleteLater();
|
||||
m_itineraryModel = nullptr;
|
||||
resetContent();
|
||||
});
|
||||
}
|
||||
m_itineraryModel->setPath(filePath.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MessageContentModel::updateReactionModel()
|
||||
{
|
||||
if (m_reactionModel != nullptr && m_reactionModel->rowCount() > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_reactionModel == nullptr) {
|
||||
m_reactionModel = new ReactionModel(this, m_eventId, m_room);
|
||||
connect(m_reactionModel, &ReactionModel::reactionsUpdated, this, &MessageContentModel::updateReactionModel);
|
||||
}
|
||||
|
||||
if (m_reactionModel->rowCount() <= 0) {
|
||||
m_reactionModel->disconnect(this);
|
||||
delete m_reactionModel;
|
||||
m_reactionModel = nullptr;
|
||||
return;
|
||||
}
|
||||
|
||||
resetContent();
|
||||
}
|
||||
|
||||
ThreadModel *MessageContentModel::modelForThread(const QString &threadRootId)
|
||||
{
|
||||
return ContentProvider::self().modelForThread(m_room, threadRootId);
|
||||
}
|
||||
|
||||
void MessageContentModel::setThreadsEnabled(bool enableThreads)
|
||||
{
|
||||
m_threadsEnabled = enableThreads;
|
||||
}
|
||||
|
||||
#include "moc_messagecontentmodel.cpp"
|
||||
159
src/messagecontent/models/messagecontentmodel.h
Normal file
@@ -0,0 +1,159 @@
|
||||
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QAbstractListModel>
|
||||
#include <QQmlEngine>
|
||||
|
||||
#include <Quotient/events/roomevent.h>
|
||||
|
||||
#include "enums/messagecomponenttype.h"
|
||||
#include "itinerarymodel.h"
|
||||
#include "messagecomponent.h"
|
||||
#include "models/reactionmodel.h"
|
||||
#include "neochatroommember.h"
|
||||
|
||||
class ThreadModel;
|
||||
|
||||
/**
|
||||
* @class MessageContentModel
|
||||
*
|
||||
* A model to visualise the components of a single RoomMessageEvent.
|
||||
*/
|
||||
class MessageContentModel : public QAbstractListModel
|
||||
{
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
QML_UNCREATABLE("")
|
||||
|
||||
public:
|
||||
enum MessageState {
|
||||
Unknown, /**< The message state is unknown. */
|
||||
Pending, /**< The message is a new pending message which the server has not yet acknowledged. */
|
||||
Available, /**< The message is available and acknowledged by the server. */
|
||||
UnAvailable, /**< The message can't be retrieved either because it doesn't exist or is blocked. */
|
||||
};
|
||||
Q_ENUM(MessageState)
|
||||
|
||||
/**
|
||||
* @brief Defines the model roles.
|
||||
*/
|
||||
enum Roles {
|
||||
DisplayRole = Qt::DisplayRole, /**< The display text for the message. */
|
||||
ComponentTypeRole = Qt::UserRole, /**< The type of component to visualise the message. */
|
||||
ComponentAttributesRole, /**< The attributes of the component. */
|
||||
EventIdRole, /**< The matrix event ID of the event. */
|
||||
TimeRole, /**< The timestamp for when the event was sent (as a QDateTime). */
|
||||
TimeStringRole, /**< The timestamp for when the event was sent as a string (in QLocale::ShortFormat). */
|
||||
AuthorRole, /**< The author of the event. */
|
||||
MediaInfoRole, /**< The media info for the event. */
|
||||
FileTransferInfoRole, /**< FileTransferInfo for any downloading files. */
|
||||
ItineraryModelRole, /**< The itinerary model for a file. */
|
||||
LatitudeRole, /**< Latitude for a location event. */
|
||||
LongitudeRole, /**< Longitude for a location event. */
|
||||
AssetRole, /**< Type of location event, e.g. self pin of the user location. */
|
||||
PollHandlerRole, /**< The PollHandler for the event, if any. */
|
||||
|
||||
ReplyEventIdRole, /**< The matrix ID of the message that was replied to. */
|
||||
ReplyAuthorRole, /**< The author of the event that was replied to. */
|
||||
ReplyContentModelRole, /**< The MessageContentModel for the reply event. */
|
||||
|
||||
ReactionModelRole, /**< Reaction model for this event. */
|
||||
|
||||
ThreadRootRole, /**< The thread root event ID for the event. */
|
||||
|
||||
LinkPreviewerRole, /**< The link preview details. */
|
||||
ChatBarCacheRole, /**< The ChatBarCache to use. */
|
||||
};
|
||||
Q_ENUM(Roles)
|
||||
|
||||
explicit MessageContentModel(NeoChatRoom *room,
|
||||
const QString &eventId,
|
||||
bool isReply = false,
|
||||
bool isPending = false,
|
||||
MessageContentModel *parent = nullptr);
|
||||
|
||||
/**
|
||||
* @brief Get the given role value at the given index.
|
||||
*
|
||||
* @sa QAbstractItemModel::data
|
||||
*/
|
||||
[[nodiscard]] QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
||||
|
||||
/**
|
||||
* @brief Number of rows in the model.
|
||||
*
|
||||
* @sa QAbstractItemModel::rowCount
|
||||
*/
|
||||
[[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||
|
||||
/**
|
||||
* @brief Returns a mapping from Role enum values to role names.
|
||||
*
|
||||
* @sa Roles, QAbstractItemModel::roleNames()
|
||||
*/
|
||||
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
|
||||
|
||||
static QHash<int, QByteArray> roleNamesStatic();
|
||||
|
||||
/**
|
||||
* @brief Close the link preview at the given index.
|
||||
*
|
||||
* If the given index is not a link preview component, nothing happens.
|
||||
*/
|
||||
Q_INVOKABLE void closeLinkPreview(int row);
|
||||
|
||||
/**
|
||||
* @brief Returns the thread model for the given thread root event ID.
|
||||
*
|
||||
* A model is created is one doesn't exist. Will return nullptr if threadRootId
|
||||
* is empty.
|
||||
*/
|
||||
Q_INVOKABLE ThreadModel *modelForThread(const QString &threadRootId);
|
||||
|
||||
static void setThreadsEnabled(bool enableThreads);
|
||||
|
||||
Q_SIGNALS:
|
||||
void showAuthorChanged();
|
||||
void eventUpdated();
|
||||
|
||||
void threadsEnabledChanged();
|
||||
|
||||
private:
|
||||
QPointer<NeoChatRoom> m_room;
|
||||
QString m_eventId;
|
||||
QString senderId() const;
|
||||
NeochatRoomMember *senderObject() const;
|
||||
|
||||
MessageState m_currentState = Unknown;
|
||||
bool m_isReply;
|
||||
|
||||
void initializeModel();
|
||||
void initializeEvent();
|
||||
void getEvent();
|
||||
|
||||
QList<MessageComponent> m_components;
|
||||
void resetModel();
|
||||
void resetContent(bool isEditing = false, bool isThreading = false);
|
||||
QList<MessageComponent> messageContentComponents(bool isEditing = false, bool isThreading = false);
|
||||
|
||||
QPointer<MessageContentModel> m_replyModel;
|
||||
void updateReplyModel();
|
||||
|
||||
ReactionModel *m_reactionModel = nullptr;
|
||||
ItineraryModel *m_itineraryModel = nullptr;
|
||||
|
||||
QList<MessageComponent> componentsForType(MessageComponentType::Type type);
|
||||
MessageComponent linkPreviewComponent(const QUrl &link);
|
||||
QList<MessageComponent> addLinkPreviews(QList<MessageComponent> inputComponents);
|
||||
|
||||
QList<QUrl> m_removedLinkPreviews;
|
||||
|
||||
void updateItineraryModel();
|
||||
bool m_emptyItinerary = false;
|
||||
|
||||
void updateReactionModel();
|
||||
|
||||
static bool m_threadsEnabled;
|
||||
};
|
||||
80
src/messagecontent/models/pollanswermodel.cpp
Normal file
@@ -0,0 +1,80 @@
|
||||
// SPDX-FileCopyrightText: 2025 James Graham <james.h.graham@protonmail.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
|
||||
#include "pollanswermodel.h"
|
||||
|
||||
#include "neochatroom.h"
|
||||
#include "pollhandler.h"
|
||||
|
||||
PollAnswerModel::PollAnswerModel(PollHandler *parent)
|
||||
: QAbstractListModel(parent)
|
||||
{
|
||||
Q_ASSERT(parent != nullptr);
|
||||
|
||||
connect(parent, &PollHandler::selectionsChanged, this, [this]() {
|
||||
dataChanged(index(0), index(rowCount() - 1), {CountRole, LocalChoiceRole, IsWinnerRole});
|
||||
});
|
||||
connect(parent, &PollHandler::answersChanged, this, [this]() {
|
||||
dataChanged(index(0), index(rowCount() - 1), {TextRole});
|
||||
});
|
||||
}
|
||||
|
||||
QVariant PollAnswerModel::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
Q_ASSERT(checkIndex(index, QAbstractItemModel::CheckIndexOption::IndexIsValid));
|
||||
|
||||
const auto row = index.row();
|
||||
if (row < 0 || row >= rowCount()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const auto pollHandler = dynamic_cast<PollHandler *>(this->parent());
|
||||
if (pollHandler == nullptr) {
|
||||
qWarning() << "PollAnswerModel created with nullptr parent.";
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (role == IdRole) {
|
||||
return pollHandler->answerAtRow(row).id;
|
||||
}
|
||||
if (role == TextRole) {
|
||||
return pollHandler->answerAtRow(row).text;
|
||||
}
|
||||
if (role == CountRole) {
|
||||
return pollHandler->answerCountAtId(pollHandler->answerAtRow(row).id);
|
||||
}
|
||||
if (role == LocalChoiceRole) {
|
||||
const auto room = pollHandler->room();
|
||||
if (room == nullptr) {
|
||||
return {};
|
||||
}
|
||||
return pollHandler->checkMemberSelectedId(room->localMember().id(), pollHandler->answerAtRow(row).id);
|
||||
}
|
||||
if (role == IsWinnerRole) {
|
||||
return pollHandler->winningAnswerIds().contains(pollHandler->answerAtRow(row).id) && pollHandler->hasEnded();
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
int PollAnswerModel::rowCount(const QModelIndex &parent) const
|
||||
{
|
||||
Q_UNUSED(parent);
|
||||
const auto pollHandler = dynamic_cast<PollHandler *>(this->parent());
|
||||
if (pollHandler == nullptr) {
|
||||
qWarning() << "PollAnswerModel created with nullptr parent.";
|
||||
return 0;
|
||||
}
|
||||
|
||||
return pollHandler->numAnswers();
|
||||
}
|
||||
|
||||
QHash<int, QByteArray> PollAnswerModel::roleNames() const
|
||||
{
|
||||
return {
|
||||
{IdRole, "id"},
|
||||
{TextRole, "answerText"},
|
||||
{CountRole, "count"},
|
||||
{LocalChoiceRole, "localChoice"},
|
||||
{IsWinnerRole, "isWinner"},
|
||||
};
|
||||
}
|
||||
57
src/messagecontent/models/pollanswermodel.h
Normal file
@@ -0,0 +1,57 @@
|
||||
// SPDX-FileCopyrightText: 2025 James Graham <james.h.graham@protonmail.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QAbstractListModel>
|
||||
#include <QQmlEngine>
|
||||
|
||||
class PollHandler;
|
||||
|
||||
/**
|
||||
* @class PollAnswerModel
|
||||
*
|
||||
* This class defines the model for visualising a list of answer to a poll.
|
||||
*/
|
||||
class PollAnswerModel : public QAbstractListModel
|
||||
{
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
QML_UNCREATABLE("")
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Defines the model roles.
|
||||
*/
|
||||
enum Roles {
|
||||
IdRole, /**< The ID of the answer. */
|
||||
TextRole, /**< The answer text. */
|
||||
CountRole, /**< The number of people who gave this answer. */
|
||||
LocalChoiceRole, /**< Whether this option was selected by the local user */
|
||||
IsWinnerRole, /**< Whether this option was selected by the local user */
|
||||
};
|
||||
Q_ENUM(Roles)
|
||||
|
||||
explicit PollAnswerModel(PollHandler *parent);
|
||||
|
||||
/**
|
||||
* @brief Get the given role value at the given index.
|
||||
*
|
||||
* @sa QAbstractItemModel::data
|
||||
*/
|
||||
QVariant data(const QModelIndex &index, int role) const override;
|
||||
|
||||
/**
|
||||
* @brief Number of rows in the model.
|
||||
*
|
||||
* @sa QAbstractItemModel::rowCount
|
||||
*/
|
||||
int rowCount(const QModelIndex &parent = {}) const override;
|
||||
|
||||
/**
|
||||
* @brief Returns a mapping from Role enum values to role names.
|
||||
*
|
||||
* @sa Roles, QAbstractItemModel::roleNames()
|
||||
*/
|
||||
QHash<int, QByteArray> roleNames() const override;
|
||||
};
|
||||