1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
|
// SPDX-FileCopyrightText: Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
import Qt.labs.platform 1.1 as Platform
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.2
import QtQuick.Window 2.15
import im.nheko 1.0
import "./delegates"
Pane {
id: topBar
property string avatarUrl: room ? room.roomAvatarUrl : ""
property string directChatOtherUserId: room ? room.directChatOtherUserId : ""
property bool isDirect: room ? room.isDirect : false
property bool isEncrypted: room ? room.isEncrypted : false
property string roomId: room ? room.roomId : ""
property string roomName: room ? room.roomName : qsTr("No room selected")
property string roomTopic: room ? room.roomTopic : ""
property bool searchHasFocus: searchField.focus && searchField.enabled
property string searchString: ""
property bool showBackButton: false
property int trustlevel: room ? room.trustlevel : Crypto.Unverified
Layout.fillWidth: true
implicitHeight: topLayout.height + Nheko.paddingMedium * 2
padding: 0
z: 3
background: Rectangle {
color: palette.window
}
contentItem: Item {
GridLayout {
id: topLayout
anchors.left: parent.left
anchors.margins: Nheko.paddingMedium
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
columnSpacing: Nheko.paddingSmall
rowSpacing: Nheko.paddingSmall
Avatar {
id: communityAvatar
property string avatarUrl: (Settings.groupView && room && room.parentSpace && room.parentSpace.roomAvatarUrl) || ""
property string communityId: (Settings.groupView && room && room.parentSpace && room.parentSpace.roomid) || ""
property string communityName: (Settings.groupView && room && room.parentSpace && room.parentSpace.roomName) || ""
Layout.alignment: Qt.AlignRight
Layout.column: 1
Layout.row: 0
displayName: communityName
enabled: false
height: fontMetrics.lineSpacing
roomid: communityId
url: avatarUrl.replace("mxc://", "image://MxcImage/")
visible: roomid && room.parentSpace.isLoaded && ("space:" + room.parentSpace.roomid != Communities.currentTagId)
width: fontMetrics.lineSpacing
}
Label {
id: communityLabel
Layout.column: 2
Layout.fillWidth: true
Layout.row: 0
color: palette.text
elide: Text.ElideRight
maximumLineCount: 1
text: qsTr("In %1").arg(communityAvatar.displayName)
textFormat: Text.RichText
visible: communityAvatar.visible
}
ImageButton {
id: backToRoomsButton
Layout.alignment: Qt.AlignVCenter
Layout.column: 0
Layout.preferredHeight: Nheko.avatarSize - Nheko.paddingMedium
Layout.preferredWidth: Nheko.avatarSize - Nheko.paddingMedium
Layout.row: 1
Layout.rowSpan: 2
ToolTip.text: qsTr("Back to room list")
ToolTip.visible: hovered
image: ":/icons/icons/ui/angle-arrow-left.svg"
visible: showBackButton
onClicked: Rooms.resetCurrentRoom()
}
Avatar {
Layout.alignment: Qt.AlignVCenter
Layout.column: 1
Layout.row: 1
Layout.rowSpan: 2
displayName: roomName
enabled: false
height: Nheko.avatarSize
roomid: roomId
url: avatarUrl.replace("mxc://", "image://MxcImage/")
userid: isDirect ? directChatOtherUserId : ""
width: Nheko.avatarSize
}
Label {
Layout.column: 2
Layout.fillWidth: true
Layout.row: 1
color: palette.text
elide: Text.ElideRight
font.bold: true
font.pointSize: fontMetrics.font.pointSize * 1.1
maximumLineCount: 1
text: roomName
textFormat: Text.RichText
}
MatrixText {
id: roomTopicC
Layout.column: 2
Layout.fillWidth: true
Layout.maximumHeight: fontMetrics.lineSpacing * 2 // show 2 lines
Layout.row: 2
clip: true
enabled: false
selectByMouse: false
text: roomTopic
}
ImageButton {
id: pinButton
property bool pinsShown: !Settings.hiddenPins.includes(roomId)
Layout.alignment: Qt.AlignVCenter
Layout.column: 3
Layout.preferredHeight: Nheko.avatarSize - Nheko.paddingMedium
Layout.preferredWidth: Nheko.avatarSize - Nheko.paddingMedium
Layout.row: 1
Layout.rowSpan: 2
ToolTip.text: qsTr("Show or hide pinned messages")
ToolTip.visible: hovered
image: pinsShown ? ":/icons/icons/ui/pin.svg" : ":/icons/icons/ui/pin-off.svg"
visible: !!room && room.pinnedMessages.length > 0
onClicked: {
var ps = Settings.hiddenPins;
if (pinsShown) {
ps.push(roomId);
} else {
const index = ps.indexOf(roomId);
if (index > -1) {
ps.splice(index, 1);
}
}
Settings.hiddenPins = ps;
}
}
AbstractButton {
Layout.column: 4
Layout.preferredHeight: Nheko.avatarSize - Nheko.paddingMedium
Layout.preferredWidth: Nheko.avatarSize - Nheko.paddingMedium
Layout.row: 1
Layout.rowSpan: 2
background: null
contentItem: EncryptionIndicator {
ToolTip.delay: Nheko.tooltipDelay
ToolTip.text: {
if (!isEncrypted)
return qsTr("Show room members.");
switch (trustlevel) {
case Crypto.Verified:
return qsTr("This room contains only verified devices.");
case Crypto.TOFU:
return qsTr("This room contains verified devices and devices which have never changed their master key.");
default:
return qsTr("This room contains unverified devices!");
}
}
enabled: false
encrypted: isEncrypted
hovered: parent.hovered
trust: trustlevel
unencryptedColor: palette.buttonText
unencryptedHoverColor: palette.highlight
unencryptedIcon: ":/icons/icons/ui/people.svg"
}
onClicked: TimelineManager.openRoomMembers(room)
}
ImageButton {
id: searchButton
property bool searchActive: false
Layout.alignment: Qt.AlignVCenter
Layout.column: 5
Layout.preferredHeight: Nheko.avatarSize - Nheko.paddingMedium
Layout.preferredWidth: Nheko.avatarSize - Nheko.paddingMedium
Layout.row: 1
Layout.rowSpan: 2
ToolTip.text: qsTr("Search this room")
ToolTip.visible: hovered
image: ":/icons/icons/ui/search.svg"
visible: !!room
onClicked: searchActive = !searchActive
onSearchActiveChanged: {
if (searchActive) {
searchField.forceActiveFocus();
} else {
searchField.clear();
topBar.searchString = "";
}
}
}
ImageButton {
id: roomOptionsButton
Layout.alignment: Qt.AlignVCenter
Layout.column: 6
Layout.preferredHeight: Nheko.avatarSize - Nheko.paddingMedium
Layout.preferredWidth: Nheko.avatarSize - Nheko.paddingMedium
Layout.row: 1
Layout.rowSpan: 2
ToolTip.text: qsTr("Room options")
ToolTip.visible: hovered
image: ":/icons/icons/ui/options.svg"
visible: !!room
onClicked: roomOptionsMenu.open(roomOptionsButton)
Platform.Menu {
id: roomOptionsMenu
Platform.MenuItem {
text: qsTr("Invite users")
visible: room ? room.permissions.canInvite() : false
onTriggered: TimelineManager.openInviteUsers(roomId)
}
Platform.MenuItem {
text: qsTr("Members")
onTriggered: TimelineManager.openRoomMembers(room)
}
Platform.MenuItem {
text: qsTr("Leave room")
onTriggered: TimelineManager.openLeaveRoomDialog(roomId)
}
Platform.MenuItem {
text: qsTr("Settings")
onTriggered: TimelineManager.openRoomSettings(roomId)
}
}
}
ScrollView {
id: pinnedMessages
Layout.column: 2
Layout.columnSpan: 4
Layout.fillWidth: true
Layout.preferredHeight: Math.min(contentHeight, Nheko.avatarSize * 4)
Layout.row: 3
ScrollBar.horizontal.visible: false
clip: true
visible: !!room && room.pinnedMessages.length > 0 && !Settings.hiddenPins.includes(roomId)
ListView {
model: room ? room.pinnedMessages : undefined
spacing: Nheko.paddingSmall
delegate: RowLayout {
required property string modelData
height: implicitHeight
width: ListView.view.width
Reply {
id: reply
property var e: room ? room.getDump(modelData, "pins") : {}
Layout.fillWidth: true
Layout.preferredHeight: height
blurhash: e.blurhash ?? ""
body: e.body ?? ""
encryptionError: e.encryptionError ?? 0
eventId: e.eventId ?? ""
filename: e.filename ?? ""
filesize: e.filesize ?? ""
formattedBody: e.formattedBody ?? ""
isOnlyEmoji: e.isOnlyEmoji ?? false
keepFullText: true
originalWidth: e.originalWidth ?? 0
proportionalHeight: e.proportionalHeight ?? 1
type: e.type ?? MtxEvent.UnknownMessage
typeString: e.typeString ?? ""
url: e.url ?? ""
userColor: TimelineManager.userColor(e.userId, palette.window)
userId: e.userId ?? ""
userName: e.userName ?? ""
Connections {
function onPinnedMessagesChanged() {
reply.e = room.getDump(modelData, "pins");
}
target: room
}
}
ImageButton {
id: deletePinButton
Layout.alignment: Qt.AlignTop | Qt.AlignLeft
Layout.preferredHeight: 16
Layout.preferredWidth: 16
ToolTip.text: qsTr("Unpin")
ToolTip.visible: hovered
hoverEnabled: true
image: ":/icons/icons/ui/dismiss.svg"
visible: room.permissions.canChange(MtxEvent.PinnedEvents)
onClicked: room.unpin(modelData)
}
}
}
}
ScrollView {
id: widgets
Layout.column: 2
Layout.columnSpan: 4
Layout.fillWidth: true
Layout.preferredHeight: Math.min(contentHeight, Nheko.avatarSize * 1.5)
Layout.row: 4
ScrollBar.horizontal.visible: false
clip: true
visible: !!room && room.widgetLinks.length > 0 && !Settings.hiddenWidgets.includes(roomId)
ListView {
model: room ? room.widgetLinks : undefined
spacing: Nheko.paddingSmall
delegate: MatrixText {
required property var modelData
color: palette.text
text: modelData
}
}
}
MatrixTextField {
id: searchField
Layout.column: 2
Layout.columnSpan: 4
Layout.fillWidth: true
Layout.row: 5
enabled: visible
hasClear: true
placeholderText: qsTr("Enter search query")
visible: searchButton.searchActive
onAccepted: topBar.searchString = text
}
}
NhekoCursorShape {
anchors.bottomMargin: (pinnedMessages.visible ? pinnedMessages.height : 0) + (widgets.visible ? widgets.height : 0)
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
}
}
onRoomIdChanged: {
searchString = "";
searchButton.searchActive = false;
searchField.text = "";
}
// HACK: https://bugreports.qt.io/browse/QTBUG-83972, qtwayland cannot auto hide menu
Connections {
function onHideMenu() {
roomOptionsMenu.close();
}
target: MainWindow
}
Shortcut {
sequence: StandardKey.Find
onActivated: searchButton.searchActive = !searchButton.searchActive
}
TapHandler {
gesturePolicy: TapHandler.ReleaseWithinBounds
onSingleTapped: {
if (eventPoint.position.y > topBar.height - (pinnedMessages.visible ? pinnedMessages.height : 0) - (widgets.visible ? widgets.height : 0)) {
eventPoint.accepted = true;
return;
}
if (showBackButton && eventPoint.position.x < Nheko.paddingMedium + backToRoomsButton.width) {
eventPoint.accepted = true;
return;
}
if (eventPoint.position.x > topBar.width - Nheko.paddingMedium - roomOptionsButton.width) {
eventPoint.accepted = true;
return;
}
if (communityLabel.visible && eventPoint.position.y < communityAvatar.height + Nheko.paddingMedium + Nheko.paddingSmall / 2) {
if (!Communities.trySwitchToSpace(room.parentSpace.roomid))
room.parentSpace.promptJoin();
eventPoint.accepted = true;
return;
}
if (room) {
let p = topBar.mapToItem(roomTopicC, eventPoint.position.x, eventPoint.position.y);
let link = roomTopicC.linkAt(p.x, p.y);
if (link) {
Nheko.openLink(link);
} else {
TimelineManager.openRoomSettings(room.roomId);
}
}
eventPoint.accepted = true;
}
}
HoverHandler {
grabPermissions: PointerHandler.TakeOverForbidden | PointerHandler.CanTakeOverFromAnything
}
}
|