@@ -17,6 +17,7 @@ import GObject from 'gi://GObject';
1717import St from 'gi://St' ;
1818import Clutter from 'gi://Clutter' ;
1919import Soup from 'gi://Soup' ;
20+ import Shell from 'gi://Shell' ;
2021import { Extension } from 'resource:///org/gnome/shell/extensions/extension.js' ;
2122import * as PanelMenu from 'resource:///org/gnome/shell/ui/panelMenu.js' ;
2223import * 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
0 commit comments