Skip to content

Commit af7003e

Browse files
committed
Add QR code generation and action box features to room menu
- Implemented QR code generation for rooms using an external API. - Added an action box layout for displaying interactive QR code features. - Introduced new settings toggle for enabling QR code generation. - Updated menu interaction logic to support dynamic action boxes. - Extended room data processing to include canonical aliases. - Updated styles in `stylesheet.css` for new UI elements. - Optimized menu rendering to prevent unnecessary rebuilds, improving performance.
1 parent 52f1d93 commit af7003e

File tree

4 files changed

+263
-18
lines changed

4 files changed

+263
-18
lines changed

extension.js

Lines changed: 191 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import GObject from 'gi://GObject';
1717
import St from 'gi://St';
1818
import Clutter from 'gi://Clutter';
1919
import Soup from 'gi://Soup';
20+
import Shell from 'gi://Shell';
2021
import { Extension } from 'resource:///org/gnome/shell/extensions/extension.js';
2122
import * as PanelMenu from 'resource:///org/gnome/shell/ui/panelMenu.js';
2223
import * as PopupMenu from 'resource:///org/gnome/shell/ui/popupMenu.js';
@@ -47,6 +48,7 @@ const MatrixIndicator = GObject.registerClass(
4748

4849
this.add_child(this.icon);
4950
this._lastRooms = [];
51+
this._openQrRoomId = null;
5052
this._buildMenu([]);
5153
}
5254

@@ -112,6 +114,142 @@ const MatrixIndicator = GObject.registerClass(
112114
clipboard.set_text(St.ClipboardType.CLIPBOARD, text);
113115
}
114116

117+
_getPrettyId(room) {
118+
return room.dmPartnerId || room.canonicalAlias || room.id;
119+
}
120+
121+
_getMatrixToUrlFor(room) {
122+
const target = this._getPrettyId(room);
123+
return `https://matrix.to/#/${target}`;
124+
}
125+
126+
async _toggleActionBox(room, roomItem) {
127+
try {
128+
// If this room's action box is already shown, close it
129+
if (this._openQrRoomId === room.id) {
130+
if (roomItem._actionItem) {
131+
roomItem._actionItem.destroy();
132+
roomItem._actionItem = null;
133+
}
134+
this._openQrRoomId = null;
135+
return;
136+
}
137+
138+
// Close any other open action box first
139+
if (this._openQrRoomId) {
140+
const items = this.menu._getMenuItems();
141+
for (const item of items) {
142+
if (item._actionItem) {
143+
item._actionItem.destroy();
144+
item._actionItem = null;
145+
146+
// Reset icon of the previous button (set to QR icon as it's now closed)
147+
const btn = item.get_children().find(c => c instanceof St.Button && c.has_style_class_name('matrix-action-button'));
148+
if (btn && btn.child instanceof St.Icon) {
149+
btn.child.icon_name = 'qr-code-symbolic';
150+
}
151+
}
152+
}
153+
}
154+
155+
this._openQrRoomId = room.id;
156+
this._createActionBox(room, roomItem);
157+
}
158+
catch (e) {
159+
console.error(`[Matrix-Status] Action box error: ${e.message}`);
160+
}
161+
}
162+
163+
_createActionBox(room, roomItem, showQrImmediately = false) {
164+
const actionItem = new PopupMenu.PopupBaseMenuItem({ reactive: true, can_focus: false });
165+
actionItem.style_class = 'matrix-action-box-item';
166+
167+
const mainBox = new St.BoxLayout({ vertical: true, x_expand: true });
168+
169+
const qrContainer = new St.BoxLayout({ vertical: true, x_expand: true });
170+
mainBox.add_child(qrContainer);
171+
172+
actionItem.add_child(mainBox);
173+
174+
// Find position to insert (right after the room item)
175+
const items = this.menu._getMenuItems();
176+
const index = items.indexOf(roomItem);
177+
this.menu.addMenuItem(actionItem, index + 1);
178+
179+
roomItem._actionItem = actionItem;
180+
181+
this._fillQrContainer(room, qrContainer);
182+
}
183+
184+
async _fillQrContainer(room, container) {
185+
try {
186+
// Clear container first
187+
container.get_children().forEach(c => c.destroy());
188+
189+
const spinner = new St.Label({ text: 'Generating...', x_align: Clutter.ActorAlign.CENTER });
190+
container.add_child(spinner);
191+
container.visible = true;
192+
193+
const dataUrl = `https://api.qrserver.com/v1/create-qr-code/?size=180x180&data=${encodeURIComponent(this._getMatrixToUrlFor(room))}`;
194+
const message = Soup.Message.new('GET', dataUrl);
195+
const bytes = await this._httpSession.send_and_read_async(
196+
message,
197+
GLib.PRIORITY_DEFAULT,
198+
this._cancellable,
199+
);
200+
201+
spinner.destroy();
202+
203+
if (message.status_code !== 200) {
204+
container.add_child(new St.Label({ text: 'Error generating QR', x_align: Clutter.ActorAlign.CENTER }));
205+
return;
206+
}
207+
208+
// QR Image
209+
const icon = new St.Icon({
210+
gicon: Gio.BytesIcon.new(bytes),
211+
icon_size: 160,
212+
x_align: Clutter.ActorAlign.CENTER,
213+
style_class: 'matrix-qr-image',
214+
});
215+
container.add_child(icon);
216+
217+
// ID row: label and copy button
218+
const idRow = new St.BoxLayout({
219+
x_expand: true,
220+
x_align: Clutter.ActorAlign.CENTER,
221+
style_class: 'matrix-qr-id-row'
222+
});
223+
224+
const idLabel = new St.Label({
225+
text: this._getPrettyId(room),
226+
style_class: 'matrix-qr-id-label',
227+
y_align: Clutter.ActorAlign.CENTER,
228+
});
229+
230+
const copyBtn = new St.Button({
231+
child: new St.Icon({
232+
icon_name: 'edit-copy-symbolic',
233+
icon_size: 14,
234+
}),
235+
style_class: 'button matrix-qr-copy-button',
236+
can_focus: true,
237+
});
238+
239+
copyBtn.connect('clicked', () => {
240+
this._copyToClipboard(this._getPrettyId(room));
241+
this.menu.close();
242+
});
243+
244+
idRow.add_child(idLabel);
245+
idRow.add_child(copyBtn);
246+
container.add_child(idRow);
247+
}
248+
catch (e) {
249+
console.error(`[Matrix-Status] QR generation error: ${e.message}`);
250+
}
251+
}
252+
115253
_buildMenu(rooms = []) {
116254
this.menu.removeAll();
117255
if (rooms.length === 0) {
@@ -136,32 +274,49 @@ const MatrixIndicator = GObject.registerClass(
136274
item.insert_child_at_index(lockIcon, 0);
137275
}
138276

139-
let labelText = room.unread > 0 ? `<b>(${room.unread}) ${room.name}</b>` : room.name;
277+
const labelText = room.unread > 0 ? `<b>(${room.unread}) ${room.name}</b>` : room.name;
140278
item.label.get_clutter_text().set_markup(labelText);
141279
item.label.x_expand = true;
142280

143-
// Copy ID button
144-
const copyButton = new St.Button({
281+
// Action button
282+
const isQrEnabled = this._settings.get_boolean('generate-qr-code-enable');
283+
const initialIconName = isQrEnabled
284+
? (this._openQrRoomId === room.id ? 'view-conceal-symbolic' : 'qr-code-symbolic')
285+
: 'edit-copy-symbolic';
286+
287+
const actionButton = new St.Button({
145288
child: new St.Icon({
146-
icon_name: 'edit-copy-symbolic',
289+
icon_name: initialIconName,
147290
icon_size: 14,
148291
}),
149-
style_class: 'matrix-copy-button',
292+
style_class: 'button matrix-action-button',
150293
can_focus: true,
294+
y_align: Clutter.ActorAlign.CENTER,
151295
});
152296

153-
copyButton.connect('clicked', () => {
154-
this._copyToClipboard(room.dmPartnerId || room.id);
155-
this.menu.close();
297+
actionButton.connect('clicked', () => {
298+
if (isQrEnabled) {
299+
this._toggleActionBox(room, item);
300+
const newIconName = this._openQrRoomId === room.id ? 'view-conceal-symbolic' : 'qr-code-symbolic';
301+
actionButton.child.icon_name = newIconName;
302+
} else {
303+
this._copyToClipboard(this._getPrettyId(room));
304+
this.menu.close();
305+
}
156306
return Clutter.EVENT_STOP;
157307
});
158308

159-
item.add_child(copyButton);
309+
item.add_child(actionButton);
160310

161311
item.connect('activate', () => {
162312
this._openMatrixClient(room.id);
163313
});
164314
this.menu.addMenuItem(item);
315+
316+
// Restore action box if it was open for this room
317+
if (isQrEnabled && this._openQrRoomId === room.id) {
318+
this._createActionBox(room, item, true);
319+
}
165320
});
166321
}
167322

@@ -242,6 +397,19 @@ const MatrixIndicator = GObject.registerClass(
242397
* - Intelligent filtering: only unread or favorite rooms
243398
* - Sorting: based on last event timestamp (desc)
244399
*/
400+
_isSameRoomList(newList) {
401+
if (this._lastRooms.length !== newList.length)
402+
return false;
403+
404+
for (let i = 0; i < newList.length; i++) {
405+
const a = this._lastRooms[i];
406+
const b = newList[i];
407+
if (a.id !== b.id || a.unread !== b.unread || a.name !== b.name || a.encrypted !== b.encrypted)
408+
return false;
409+
}
410+
return true;
411+
}
412+
245413
_processSync(data) {
246414
let roomList = [];
247415
let totalUnread = 0;
@@ -261,11 +429,16 @@ const MatrixIndicator = GObject.registerClass(
261429
if (unread > 0 || hasFavTag) {
262430
let name = null;
263431
let dmPartnerId = null;
432+
let canonicalAlias = null;
264433

265434
const nameEv = roomData.state?.events?.find(e => e.type === 'm.room.name');
266435
if (nameEv?.content?.name)
267436
name = nameEv.content.name;
268437

438+
const aliasEv = roomData.state?.events?.find(e => e.type === 'm.room.canonical_alias');
439+
if (aliasEv?.content?.alias)
440+
canonicalAlias = aliasEv.content.alias;
441+
269442
if (roomData.summary?.['m.heroes']?.length > 0) {
270443
const heroes = roomData.summary['m.heroes'];
271444

@@ -293,6 +466,7 @@ const MatrixIndicator = GObject.registerClass(
293466
name: name || 'Unnamed Room',
294467
id: roomId,
295468
dmPartnerId,
469+
canonicalAlias,
296470
unread,
297471
timestamp,
298472
encrypted: isEncrypted,
@@ -308,8 +482,11 @@ const MatrixIndicator = GObject.registerClass(
308482
this.remove_style_class_name('matrix-pill-active');
309483
}
310484

311-
this._lastRooms = roomList;
312-
this._buildMenu(roomList);
485+
// Only rebuild menu if data changed; avoid unnecessary rebuilds to prevent flicker
486+
if (!this._isSameRoomList(roomList)) {
487+
this._lastRooms = roomList;
488+
this._buildMenu(roomList);
489+
}
313490
}
314491
});
315492

@@ -331,6 +508,9 @@ export default class MatrixExtension extends Extension {
331508
// Rebuild menu immediately to reflect client change (e.g., show/hide Open Element)
332509
this._indicator?._buildMenu(this._indicator?._lastRooms ?? []);
333510
});
511+
this._settings.connect('changed::generate-qr-code-enable', () => {
512+
this._indicator?._buildMenu(this._indicator?._lastRooms ?? []);
513+
});
334514
this._restartTimer();
335515
}
336516

prefs.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,13 @@ export default class MatrixStatusPreferences extends ExtensionPreferences {
7373
});
7474
configGroup.add(clientTypeRow);
7575

76+
const qrRow = new Adw.SwitchRow({
77+
title: 'Enable QR Code Generation',
78+
subtitle: 'Show a button to generate and display room QR codes',
79+
});
80+
settings.bind('generate-qr-code-enable', qrRow, 'active', Gio.SettingsBindFlags.DEFAULT);
81+
configGroup.add(qrRow);
82+
7683
// Separator Group
7784
page.add(new Adw.PreferencesGroup());
7885

schemas/org.gnome.shell.extensions.matrix-status.gschema.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,5 +24,10 @@
2424
<summary>Matrix Client Type</summary>
2525
<description>Choose between Web (matrix.to), Element (element://) and Fractal (matrix:)</description>
2626
</key>
27+
<key name="generate-qr-code-enable" type="b">
28+
<default>false</default>
29+
<summary>Enable QR Code generation</summary>
30+
<description>If enabled, a button to show a QR code for the room/user will be displayed.</description>
31+
</key>
2732
</schema>
2833
</schemalist>

stylesheet.css

Lines changed: 60 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,18 +25,71 @@
2525
padding: 2px;
2626
}
2727

28-
.matrix-copy-button {
28+
.matrix-action-button {
29+
padding: 3px;
30+
border-radius: 8px;
31+
margin-left: 4px;
32+
min-width: 24px;
33+
height: 24px;
34+
transition: background-color 0.2s ease;
35+
}
36+
37+
.matrix-action-button:hover {
38+
background-color: rgba(255, 255, 255, 0.15);
39+
}
40+
41+
.matrix-action-button:active {
42+
background-color: rgba(255, 255, 255, 0.25);
43+
}
44+
45+
.matrix-action-box-item {
46+
background-color: rgba(255, 255, 255, 0.08);
47+
margin: 2px 8px 6px 8px;
48+
border-radius: 12px;
49+
padding: 0px;
50+
}
51+
52+
.matrix-action-row {
2953
padding: 4px;
30-
border-radius: 4px;
31-
transition: background-color 0.2s;
32-
margin-left: 12px;
33-
margin-right: 4px;
54+
spacing: 4px;
3455
}
3556

36-
.matrix-copy-button:hover {
57+
.matrix-action-box-button {
58+
padding: 6px 12px;
59+
border-radius: 6px;
60+
font-size: 0.9em;
61+
font-weight: bold;
62+
background-color: rgba(255, 255, 255, 0.05);
63+
}
64+
65+
.matrix-action-box-button:hover {
66+
background-color: rgba(255, 255, 255, 0.15);
67+
}
68+
69+
.matrix-qr-image {
70+
margin: 10px 0;
71+
border-radius: 8px;
72+
background-color: white;
73+
padding: 10px;
74+
}
75+
76+
.matrix-qr-id-row {
77+
spacing: 8px;
78+
margin-bottom: 12px;
79+
}
80+
81+
.matrix-qr-id-label {
82+
font-size: 0.85em;
83+
font-weight: bold;
84+
color: #ccc;
85+
}
86+
87+
.matrix-qr-copy-button {
88+
padding: 2px 6px;
89+
border-radius: 4px;
3790
background-color: rgba(255, 255, 255, 0.1);
3891
}
3992

40-
.matrix-copy-button:active {
93+
.matrix-qr-copy-button:hover {
4194
background-color: rgba(255, 255, 255, 0.2);
4295
}

0 commit comments

Comments
 (0)