adds edits

This commit is contained in:
gridtime
2023-12-08 21:27:11 +01:00
parent f5691fd8be
commit fc23274c94
11 changed files with 771 additions and 2 deletions

View File

@@ -35,7 +35,9 @@ TextEdit {
focus: false
selectByMouse: true
onLinkActivated: if (enableLinkActivation && link !== '#state-text')
onLinkActivated: if (enableLinkActivation
&& link !== '#state-text'
&& link !== '#replaced-text')
Qt.openUrlExternally(link)
MouseArea {

View File

@@ -51,6 +51,16 @@ HRowLayout {
readonly property var reactions: model.reactions
readonly property var contentHistory: model.content_history
readonly property string replacedText:
`<a href="#replaced-text" style="text-decoration: none">` +
`<font size=${theme.fontSize.small}px><font ` + (
model.replaced ?
`color="${theme.chat.message.readCounter}">&nbsp;🖉` : // U+1F589
">"
) + "</font></font></a>"
readonly property bool pureMedia: ! contentText && linksRepeater.count
readonly property bool hoveredSelectable: contentHover.hovered
@@ -125,6 +135,13 @@ HRowLayout {
id: contentLabel
visible: ! pureMedia
enableLinkActivation: ! eventList.selectedCount
onLinkActivated:
if(link === "#replaced-text") window.makePopup(
"Popups/MessageReplaceHistoryPopup.qml",
{
contentHistory: contentHistory
},
)
selectByMouse:
eventList.selectedCount <= 1 &&
@@ -165,6 +182,7 @@ HRowLayout {
timeText +
"</font>" +
replacedText +
stateText
transform: Translate { x: xOffset }

View File

@@ -284,6 +284,8 @@ Rectangle {
function focusNextVisibleMessage() {
decrementCurrentIndex()
while ( currentIndex > -1 && model.get(currentIndex).hidden ) {
if ( currentIndex === 0 )
currentIndex = -1;
decrementCurrentIndex()
}
}

View File

@@ -0,0 +1,275 @@
// Copyright Mirage authors & contributors <https://github.com/mirukana/mirage>
// SPDX-License-Identifier: LGPL-3.0-or-later
import QtQuick 2.12
import QtQuick.Layouts 1.12
import "../../../Base"
import "../../.."
HRowLayout {
id: historyContent
readonly property var mentions: []
readonly property string mentionsCSS: {
const lines = []
for (const [name, link] of mentions) {
if (! link.match(/^https?:\/\/matrix.to\/#\/@.+/)) continue
lines.push(
`.mention[data-mention='${utils.escapeHtml(name)}'] ` +
`{ color: ${utils.nameColor(name)} }`
)
}
return "<style type='text/css'>" + lines.join("\n") + "</style>"
}
readonly property string diffCSS: {
const lines = [
"del { background-color: #f8d7da; color: #721c24; text-decoration: line-through; }",
"ins { background-color: #d4edda; color: #155724; text-decoration: underline; }",
]
return "<style type='text/css'>" + lines.join("\n") + "</style>"
}
readonly property string senderText: ""
property string contentText: model.content_diff
readonly property string timeText: utils.formatTime(model.date, false)
readonly property bool pureMedia: false
readonly property bool hoveredSelectable: contentHover.hovered
readonly property string hoveredLink:
linksRepeater.lastHovered && linksRepeater.lastHovered.hovered ?
linksRepeater.lastHovered.mediaUrl :
contentLabel.hoveredLink
readonly property alias contentLabel: contentLabel
readonly property int xOffset:
onRight ?
Math.min(
contentColumn.width - contentLabel.paintedWidth -
contentLabel.leftPadding - contentLabel.rightPadding,
contentColumn.width - linksRepeater.widestChild -
(
pureMedia ?
0 : contentLabel.leftPadding + contentLabel.rightPadding
),
) :
0
readonly property int maxMessageWidth:
contentText.includes("<pre>") || contentText.includes("<table>") ?
-1 :
window.settings.Chat.max_messages_line_length < 0 ?
-1 :
Math.ceil(
mainUI.fontMetrics.averageCharacterWidth *
window.settings.Chat.max_messages_line_length
)
readonly property alias selectedText: contentLabel.selectedPlainText
spacing: theme.chat.message.horizontalSpacing
layoutDirection: onRight ? Qt.RightToLeft: Qt.LeftToRight
HColumnLayout {
id: contentColumn
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
HSelectableLabel {
id: contentLabel
visible: ! pureMedia
enableLinkActivation: ! historyList.selectedCount
selectByMouse:
historyList.selectedCount <= 1 &&
historyDelegate.checked &&
textSelectionBlocker.point.scenePosition === Qt.point(0, 0)
topPadding: theme.chat.message.verticalSpacing
bottomPadding: topPadding
leftPadding: historyContent.spacing
rightPadding: leftPadding
color: theme.chat.message.body
font.italic: false
wrapMode: TextEdit.Wrap
textFormat: Text.RichText
text:
// CSS
theme.chat.message.styleInclude + mentionsCSS + diffCSS +
// Sender name & message body
(
compact && contentText.match(/^\s*<(p|h[1-6])>/) ?
contentText.replace(
/(^\s*<(p|h[1-6])>)/, "$1" + senderText,
) :
senderText + contentText
) +
// Time
// For some reason, if there's only one space,
// times will be on their own lines most of the time.
" " +
`<font size=${theme.fontSize.small}px ` +
`color=${theme.chat.message.date}>` +
timeText +
"</font>"
transform: Translate { x: xOffset }
Layout.maximumWidth: historyContent.maxMessageWidth
Layout.fillWidth: true
onSelectedTextChanged: if (selectedPlainText) {
historyList.delegateWithSelectedText = model.id
historyList.selectedText = selectedPlainText
} else if (historyList.delegateWithSelectedText === model.id) {
historyList.delegateWithSelectedText = ""
historyList.selectedText = ""
}
Connections {
target: historyList
onCheckedChanged: contentLabel.deselect()
onDelegateWithSelectedTextChanged: {
if (historyList.delegateWithSelectedText !== model.id)
contentLabel.deselect()
}
}
HoverHandler { id: contentHover }
PointHandler {
id: mousePointHandler
property bool checkedNow: false
acceptedButtons: Qt.LeftButton
acceptedModifiers: Qt.NoModifier
acceptedPointerTypes:
PointerDevice.GenericPointer | PointerDevice.Eraser
onActiveChanged: {
if (active &&
! historyDelegate.checked &&
(! parent.hoveredLink ||
! parent.enableLinkActivation)) {
historyList.check(model.index)
checkedNow = true
}
if (! active && historyDelegate.checked) {
checkedNow ?
checkedNow = false :
historyList.uncheck(model.index)
}
}
}
PointHandler {
id: mouseShiftPointHandler
acceptedButtons: Qt.LeftButton
acceptedModifiers: Qt.ShiftModifier
acceptedPointerTypes:
PointerDevice.GenericPointer | PointerDevice.Eraser
onActiveChanged: {
if (active &&
! historyDelegate.checked &&
(! parent.hoveredLink ||
! parent.enableLinkActivation)) {
historyList.checkFromLastToHere(model.index)
}
}
}
TapHandler {
id: touchTapHandler
acceptedButtons: Qt.LeftButton
acceptedPointerTypes: PointerDevice.Finger | PointerDevice.Pen
onTapped:
if (! parent.hoveredLink || ! parent.enableLinkActivation)
historyDelegate.toggleChecked()
}
TapHandler {
id: textSelectionBlocker
acceptedPointerTypes: PointerDevice.Finger | PointerDevice.Pen
}
Rectangle {
id: contentBackground
width: Math.max(
parent.paintedWidth +
parent.leftPadding + parent.rightPadding,
linksRepeater.summedWidth +
(pureMedia ? 0 : parent.leftPadding + parent.rightPadding),
)
height: contentColumn.height
radius: theme.chat.message.radius
z: -100
color: historyDelegate.checked &&
! contentLabel.selectedPlainText &&
! mousePointHandler.active &&
! mouseShiftPointHandler.active ?
theme.chat.message.checkedBackground :
isOwn?
theme.chat.message.ownBackground :
theme.chat.message.background
}
}
HRepeater {
id: linksRepeater
property EventMediaLoader lastHovered: null
model: {
const links = historyDelegate.currentModel.links
if (historyDelegate.currentModel.media_url)
links.push(historyDelegate.currentModel.media_url)
return links
}
EventMediaLoader {
singleMediaInfo: historyDelegate.currentModel
mediaUrl: modelData
showSender: pureMedia ? senderText : ""
showDate: pureMedia ? timeText : ""
showLocalEcho: pureMedia && (
singleMediaInfo.is_local_echo ||
singleMediaInfo.read_by_count
) ? stateText : ""
transform: Translate { x: xOffset }
onHoveredChanged: if (hovered) linksRepeater.lastHovered = this
Layout.bottomMargin: pureMedia ? 0 : contentLabel.bottomPadding
Layout.leftMargin: pureMedia ? 0 : historyContent.spacing
Layout.rightMargin: pureMedia ? 0 : historyContent.spacing
Layout.preferredWidth: item ? item.width : -1
Layout.preferredHeight: item ? item.height : -1
}
}
}
HSpacer {}
}

View File

@@ -0,0 +1,93 @@
// Copyright Mirage authors & contributors <https://github.com/mirukana/mirage>
// SPDX-License-Identifier: LGPL-3.0-or-later
import QtQuick 2.12
import QtQuick.Layouts 1.12
import Clipboard 0.1
import "../../.."
import "../../../Base"
HColumnLayout {
id: historyDelegate
// Remember timeline goes from newest message at index 0 to oldest
readonly property var previousModel: historyList.model.get(model.index + 1)
readonly property var nextModel: historyList.model.get(model.index - 1)
readonly property QtObject currentModel: model
readonly property bool isFocused: model.index === historyList.currentIndex
readonly property bool compact: window.settings.General.compact
readonly property bool checked: model.id in historyList.checked
readonly property bool isOwn: true
readonly property bool isRedacted: false
readonly property bool onRight: ! historyList.ownEventsOnLeft && isOwn
readonly property bool combine: false
readonly property bool talkBreak: false
readonly property bool dayBreak:
model.index === 0 ? true : historyList.canDayBreak(previousModel, model)
readonly property bool hideNameLine: true
readonly property int cursorShape:
historyContent.hoveredLink ? Qt.PointingHandCursor :
historyContent.hoveredSelectable ? Qt.IBeamCursor :
Qt.ArrowCursor
readonly property int separationSpacing: theme.spacing * (
dayBreak ? 4 :
talkBreak ? 6 :
combine && compact ? 0.25 :
combine ? 0.5 :
compact ? 1 :
2
)
readonly property alias historyContent: historyContent
function toggleChecked() {
historyList.toggleCheck(model.index)
}
width: historyList.width - historyList.leftMargin - historyList.rightMargin
// Needed because of historyList's MouseArea which steals the
// HSelectableLabel's MouseArea hover events
onCursorShapeChanged: historyList.cursorShape = cursorShape
ListView.onRemove: historyList.uncheck(model.id)
DelegateTransitionFixer {}
Item {
Layout.fillWidth: true
visible: model.index !== 0
Layout.preferredHeight: separationSpacing
}
DayBreak {
visible: dayBreak
Layout.fillWidth: true
Layout.minimumWidth: parent.width
Layout.bottomMargin: separationSpacing
}
HistoryContent {
id: historyContent
Layout.fillWidth: true
}
TapHandler {
acceptedButtons: Qt.LeftButton
acceptedModifiers: Qt.NoModifier
onTapped: toggleChecked()
}
TapHandler {
acceptedButtons: Qt.LeftButton
acceptedModifiers: Qt.ShiftModifier
onTapped: historyList.checkFromLastToHere(model.index)
}
}

View File

@@ -0,0 +1,206 @@
// Copyright Mirage authors & contributors <https://github.com/mirukana/mirage>
// SPDX-License-Identifier: LGPL-3.0-or-later
import QtQuick 2.12
import QtQuick.Layouts 1.12
import QtQuick.Window 2.12
import Clipboard 0.1
import "../../.."
import "../../../Base"
import "../../../PythonBridge"
import "../../../ShortcutBundles"
Rectangle {
readonly property alias historyList: historyList
color: theme.chat.eventList.background
HShortcut {
sequences: window.settings.Keys.Messages.unfocus_or_deselect
onActivated: {
historyList.selectedCount ?
historyList.checked = {} :
historyList.currentIndex = -1
}
}
HShortcut {
sequences: window.settings.Keys.Messages.previous
onActivated: historyList.focusPreviousMessage()
}
HShortcut {
sequences: window.settings.Keys.Messages.next
onActivated: historyList.focusNextMessage()
}
HShortcut {
active: historyList.currentItem
sequences: window.settings.Keys.Messages.select
onActivated: historyList.toggleCheck(historyList.currentIndex)
}
HShortcut {
active: historyList.currentItem
sequences: window.settings.Keys.Messages.select_until_here
onActivated:
historyList.checkFromLastToHere(historyList.currentIndex)
}
HShortcut {
sequences: window.settings.Keys.Messages.open_links_files
onActivated: {
const indice =
historyList.getFocusedOrSelectedOrLastMediaEvents(true)
for (const i of Array.from(indice).sort().reverse()) {
const event = historyList.model.get(i)
for (const url of JSON.parse(event.links)) {
utils.getLinkType(url) === Utils.Media.Image ?
historyList.openImageViewer(event, url) :
Qt.openUrlExternally(url)
}
}
}
}
HShortcut {
sequences: window.settings.Keys.Messages.open_links_files_externally
onActivated: {
const indice =
historyList.getFocusedOrSelectedOrLastMediaEvents(true)
for (const i of Array.from(indice).sort().reverse()) {
const event = historyList.model.get(i)
for (const url of JSON.parse(event.links))
Qt.openUrlExternally(url)
}
}
}
HListView {
id: historyList
property bool ownEventsOnLeft: false
property string delegateWithSelectedText: ""
property string selectedText: ""
property bool showFocusedSeenTooltips: false
property alias cursorShape: cursorShapeArea.cursorShape
function focusCenterMessage() {
const previous = highlightRangeMode
highlightRangeMode = HListView.NoHighlightRange
currentIndex = indexAt(0, contentY + height / 2)
highlightRangeMode = previous
}
function focusPreviousMessage() {
currentIndex === -1 && visibleEnd.y < contentHeight - height / 4 ?
focusCenterMessage() :
incrementCurrentIndex()
}
function focusNextMessage() {
currentIndex === -1 && visibleEnd.y < contentHeight - height / 4 ?
focusCenterMessage() :
historyList.currentIndex === 0 ?
historyList.currentIndex = -1 :
decrementCurrentIndex()
}
function copySelectedDelegates() {
if (historyList.selectedText) {
Clipboard.text = historyList.selectedText
return
}
if (! historyList.selectedCount && historyList.currentIndex !== -1) {
const model = historyList.model.get(historyList.currentIndex)
const source = JSON.parse(model.source)
Clipboard.text =
model.media_http_url &&
utils.isEmptyObject(JSON.parse(model.media_crypt_dict)) ?
model.media_http_url :
"body" in source ?
source.body :
utils.stripHtmlTags(utils.processedEventText(model))
return
}
const contents = []
for (const model of historyList.getSortedChecked()) {
const source = JSON.parse(model.source)
contents.push(
model.media_http_url &&
utils.isEmptyObject(JSON.parse(model.media_crypt_dict)) ?
model.media_http_url :
"body" in source ?
source.body :
utils.stripHtmlTags(utils.processedEventText(model))
)
}
Clipboard.text = contents.join("\n\n")
}
function canDayBreak(item, itemAfter) {
if (! item || ! itemAfter || ! item.date || ! itemAfter.date)
return false
return item.date.getDate() !== itemAfter.date.getDate()
}
function getFocusedOrSelectedOrLastMediaEvents(acceptLinks=false) {
if (historyList.selectedCount) return historyList.checkedIndice
if (historyList.currentIndex !== -1) return [historyList.currentIndex]
// Find most recent event that's a media or contains links
for (let i = 0; i < historyList.model.count && i <= 1000; i++) {
const ev = historyList.model.get(i)
const links = JSON.parse(ev.links)
if (ev.media_url || (acceptLinks && links.length)) return [i]
}
}
anchors.fill: parent
clip: true
keyNavigationWraps: false
leftMargin: theme.spacing
rightMargin: theme.spacing
topMargin: theme.spacing
bottomMargin: theme.spacing
// model: ModelStore.get(chat.userRoomId[0], chat.userRoomId[1], "events")
model: []
delegate: HistoryDelegate {}
highlight: Rectangle {
color: theme.chat.message.focusedHighlight
opacity: theme.chat.message.focusedHighlightOpacity
}
MouseArea {
id: cursorShapeArea
anchors.fill: parent
acceptedButtons: Qt.NoButton
}
}
}

View File

@@ -0,0 +1,44 @@
// Copyright Mirage authors & contributors <https://github.com/mirukana/mirage>
// SPDX-License-Identifier: LGPL-3.0-or-later
import QtQuick 2.12
import QtQuick.Controls 2.12
import QtQuick.Layouts 1.12
import "../Base"
import "../Base/Buttons"
import "../Pages/Chat/Timeline"
HColumnPopup {
id: popup
contentWidthLimit:
window.settings.Chat.max_messages_line_length < 0 ?
theme.controls.popup.defaultWidth * 2 :
Math.ceil(
mainUI.fontMetrics.averageCharacterWidth *
window.settings.Chat.max_messages_line_length
)
property var contentHistory
page.footer: AutoDirectionLayout {
CancelButton {
id: cancelButton
onClicked: popup.close()
}
}
onOpened: cancelButton.forceActiveFocus()
SummaryLabel {
text: qsTr("Message History")
textFormat: Text.StyledText
}
HistoryList {
id: historyList
historyList.model: contentHistory
height: 400
Layout.fillWidth: true
Layout.fillHeight: true
}
}