1 //////////////////////////////////////////////////////////////////////
2 //
3 // BeeBEEP Copyright (C) 2010-2021 Marco Mastroddi
4 //
5 // BeeBEEP is free software: you can redistribute it and/or modify
6 // it under the terms of the GNU General Public License as published
7 // by the Free Software Foundation, either version 3 of the License,
8 // or (at your option) any later version.
9 //
10 // BeeBEEP is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 // GNU General Public License for more details.
14 //
15 // You should have received a copy of the GNU General Public License
16 // along with BeeBEEP. If not, see <http://www.gnu.org/licenses/>.
17 //
18 // Author: Marco Mastroddi <marco.mastroddi(AT)gmail.com>
19 //
20 // $Id: GuiFloatingChat.cpp 1471 2021-01-06 12:10:07Z mastroddi $
21 //
22 //////////////////////////////////////////////////////////////////////
23
24 #include "BeeUtils.h"
25 #include "ChatManager.h"
26 #include "Core.h"
27 #include "GuiFloatingChat.h"
28 #include "GuiEmoticons.h"
29 #include "GuiPresetMessageList.h"
30 #ifdef BEEBEEP_USE_VOICE_CHAT
31 #include "GuiRecordVoiceMessage.h"
32 #include "VoicePlayer.h"
33 #endif
34 #include "GuiUserList.h"
35 #include "IconManager.h"
36 #include "Settings.h"
37 #include "ShortcutManager.h"
38 #include "UserManager.h"
39 #ifdef Q_OS_WIN
40 #include <Windows.h>
41 #endif
42
43
GuiFloatingChat(QWidget * parent)44 GuiFloatingChat::GuiFloatingChat( QWidget *parent )
45 : QMainWindow( parent )
46 {
47 setObjectName( "GuiFloatingChat" );
48 m_mainWindowIcon = IconManager::instance().icon( "chat.png" );
49 setMainIcon( false );
50 mp_chat = new GuiChat( this );
51
52 mp_actGroupMenu = new QAction( IconManager::instance().icon( "group-edit.png" ), tr( "Edit group chat" ), this );
53 connect( mp_actGroupMenu, SIGNAL( triggered() ), this, SLOT( showGroupMenu() ) );
54
55 mp_barMembers = new QToolBar( tr( "Show the bar of members" ), this );
56 addToolBar( Qt::RightToolBarArea, mp_barMembers );
57 mp_barMembers->setObjectName( "GuiFloatingChatMemberToolBar" );
58 mp_barMembers->setIconSize( Settings::instance().avatarIconSize() );
59 mp_barMembers->setAllowedAreas( Qt::AllToolBarAreas );
60 mp_barMembers->setFloatable( false );
61 mp_barMembers->toggleViewAction()->setVisible( false );
62
63 mp_barChat = new QToolBar( tr( "Show chat toolbar" ), this );
64 addToolBar( Qt::BottomToolBarArea, mp_barChat );
65 mp_barChat->setObjectName( "GuiFloatingChatToolBar" );
66 mp_barChat->setIconSize( Settings::instance().mainBarIconSize() );
67 mp_barChat->setAllowedAreas( Qt::AllToolBarAreas );
68 mp_chat->setupToolBar( mp_barChat );
69 mp_barChat->setVisible( Settings::instance().showChatToolbar() );
70 mp_barChat->insertSeparator( mp_barChat->actions().first() );
71 mp_barChat->setFloatable( false );
72
73 mp_dockPresetMessageList = new QDockWidget( tr( "Preset messages" ), this );
74 mp_dockPresetMessageList->setObjectName( "GuiDockPresetMessageList" );
75 mp_presetMessageListWidget = new GuiPresetMessageList( this );
76 mp_presetMessageListWidget->loadFromSettings();
77 connect( mp_presetMessageListWidget, SIGNAL( presetMessageSelected( const QString& ) ), mp_chat, SLOT( addText( const QString& ) ) );
78 mp_dockPresetMessageList->setWidget( mp_presetMessageListWidget );
79 mp_dockPresetMessageList->setAllowedAreas( Qt::AllDockWidgetAreas );
80 addDockWidget( Qt::LeftDockWidgetArea, mp_dockPresetMessageList );
81 QAction* actViewPresetMessageList = mp_dockPresetMessageList->toggleViewAction();
82 actViewPresetMessageList->setIcon( IconManager::instance().icon( "preset-message.png" ) );
83 actViewPresetMessageList->setToolTip( tr( "Show the preset messages panel" ) );
84 mp_barChat->insertAction( mp_barChat->actions().first(), actViewPresetMessageList );
85 mp_actSaveWindowGeometry = mp_barChat->addAction( IconManager::instance().icon( "save-window.png" ), tr( "Save window's geometry" ), this, SLOT( saveGeometryAndState() ) );
86
87 mp_dockEmoticons = new QDockWidget( tr( "Emoticons" ), this );
88 mp_dockEmoticons->setObjectName( "GuiDockEmoticons" );
89 mp_emoticonsWidget = new GuiEmoticons( this );
90 updateEmoticons();
91 connect( mp_emoticonsWidget, SIGNAL( emoticonSelected( const Emoticon& ) ), mp_chat, SLOT( addEmoticon( const Emoticon& ) ) );
92 mp_dockEmoticons->setWidget( mp_emoticonsWidget );
93 mp_dockEmoticons->setAllowedAreas( Qt::AllDockWidgetAreas );
94 addDockWidget( Qt::LeftDockWidgetArea, mp_dockEmoticons );
95 QAction* mp_actViewEmoticons = mp_dockEmoticons->toggleViewAction();
96 mp_actViewEmoticons->setIcon( IconManager::instance().icon( "emoticon.png" ) );
97 mp_actViewEmoticons->setText( tr( "Show the emoticon panel" ) );
98 mp_actViewEmoticons->setVisible( !Settings::instance().useOnlyTextEmoticons() );
99 mp_barChat->insertAction( mp_barChat->actions().first(), mp_actViewEmoticons );
100
101 setCentralWidget( mp_chat );
102 statusBar();
103 m_chatIsVisible = false;
104 m_prevActivatedState = false;
105
106 connect( mp_chat, SIGNAL( toggleVisibilityEmoticonsPanelRequest() ), this, SLOT( toggleVisibilityEmoticonPanel() ) );
107 connect( mp_chat, SIGNAL( toggleVisibilityPresetMessagesPanelRequest() ), this, SLOT( toggleVisibilityPresetMessagesPanel() ) );
108 connect( mp_chat, SIGNAL( hideRequest() ), this, SLOT( showMinimized() ) );
109 connect( mp_chat, SIGNAL( closeRequest() ), this, SLOT( close() ) );
110 connect( mp_chat, SIGNAL( updateChatFontRequest() ), this, SIGNAL( updateChatFontRequest() ) );
111 connect( mp_chat, SIGNAL( updateChatColorsRequest() ), this, SIGNAL( updateChatColorsRequest() ) );
112 connect( mp_chat, SIGNAL( showStatusMessageRequest( const QString&, int ) ), this, SLOT( showStatusMessage(const QString&, int ) ) );
113 #ifdef BEEBEEP_USE_VOICE_CHAT
114 connect( mp_chat, SIGNAL( showVoiceMessageDialogRequest() ), this, SLOT( showRecordMessageDialog() ) );
115 #endif
116 connect( qApp, SIGNAL( focusChanged( QWidget*, QWidget* ) ), this, SLOT( onApplicationFocusChanged( QWidget*, QWidget* ) ) );
117 }
118
updateChatTitle(const Chat & c)119 void GuiFloatingChat::updateChatTitle( const Chat& c )
120 {
121 QString chat_title = "";
122
123 if( c.isPrivate() )
124 {
125 VNumber user_id = c.privateUserId();
126 User u = UserManager::instance().findUser( user_id );
127 if( u.isValid() )
128 {
129 QString user_status = u.status() != User::Online ? Bee::userStatusToString( u.status() ) : "";
130 QString user_status_description = u.status() != User::Offline ? u.statusDescription() : "";
131 QString user_name = Bee::userNameToShow( u, false );
132
133 if( !user_status.isEmpty() && !user_status_description.isEmpty() )
134 chat_title = QString( "%1 [%2 - %3]" ).arg( user_name, Bee::userStatusToString( u.status() ), user_status_description );
135 else if( !user_status.isEmpty() )
136 chat_title = QString( "%1 (%2)" ).arg( user_name, user_status );
137 else if( !user_status_description.isEmpty() )
138 chat_title = QString( "%1 [%2]" ).arg( user_name, user_status_description );
139 else
140 chat_title = user_name;
141
142 m_mainWindowIcon = Bee::avatarForUser( u, QSize( 256, 256 ), true );
143 }
144 else
145 {
146 qWarning() << "Invalid user" << user_id << "found for private chat" << c.name();
147 m_mainWindowIcon = IconManager::instance().icon( "chat.png" );
148 chat_title = c.name();
149 }
150 }
151 else if( c.isDefault() )
152 {
153 chat_title = tr( "All users" ).toUpper();
154 m_mainWindowIcon = IconManager::instance().icon( "default-chat-online.png" );
155 }
156 else if( c.isGroup() )
157 {
158 chat_title = c.name();
159 m_mainWindowIcon = IconManager::instance().icon( "group.png" );
160 }
161 else
162 {
163 chat_title = c.name();
164 m_mainWindowIcon = IconManager::instance().icon( "chat.png" );
165 }
166
167 setMainIcon( c.unreadMessages() > 0 );
168 setWindowTitle( QString( "%1 - %2 %3" ).arg( chat_title, Settings::instance().programName(), Settings::instance().version( false, false, false ) ) );
169 }
170
updateChatMember(const Chat & c,const User & u)171 void GuiFloatingChat::updateChatMember( const Chat& c, const User& u )
172 {
173 if( !mp_barMembers->isEnabled() )
174 return;
175
176 if( u.isLocal() )
177 return;
178
179 QAction* act_user = Q_NULLPTR;
180 QList<QAction*> member_actions = mp_barMembers->actions();
181 foreach( QAction* act, member_actions )
182 {
183 if( u.id() == Bee::qVariantToVNumber( act->data() ) )
184 {
185 act_user = act;
186 break;
187 }
188 }
189
190 QString user_name = Bee::userNameToShow( u, false );
191 if( !act_user )
192 {
193 act_user = mp_barMembers->addAction( user_name, this, SLOT( onGroupMemberActionTriggered() ) );
194 act_user->setData( u.id() );
195 act_user->setCheckable( false );
196 }
197 else
198 act_user->setText( user_name );
199
200 int avatar_size = mp_barMembers->iconSize().width();
201 QString user_tooltip = Bee::toolTipForUser( u, true );
202 if( !u.isLocal() && u.protocolVersion() >= 63 && !c.userHasReadMessages( u.id() ) )
203 {
204 user_tooltip += QString( "\n%1" ).arg( tr( "%1 has not read last messages" ).arg( u.name() ) );
205 act_user->setIcon( Bee::avatarForUser( u, QSize( avatar_size, avatar_size ), Settings::instance().showUserPhoto(), User::Away ) );
206 }
207 else
208 act_user->setIcon( Bee::avatarForUser( u, QSize( avatar_size, avatar_size ), Settings::instance().showUserPhoto() ) );
209 act_user->setToolTip( user_tooltip.trimmed() );
210 }
211
updateChatMembers(const Chat & c)212 void GuiFloatingChat::updateChatMembers( const Chat& c )
213 {
214 mp_barMembers->setVisible( !c.isDefault() );
215 mp_barMembers->setEnabled( !c.isDefault() );
216
217 if( mp_barMembers->isEnabled() )
218 {
219 mp_barMembers->clear();
220 mp_barMembers->setIconSize( Settings::instance().avatarIconSize() );
221 mp_barMembers->setVisible( true );
222
223 UserList ul = UserManager::instance().userList().fromUsersId( c.usersId() );
224 foreach( User u, ul.toList() )
225 updateChatMember( c, u );
226
227 if( c.isGroup() )
228 {
229 mp_barMembers->addSeparator();
230 mp_barMembers->addAction( mp_actGroupMenu );
231 }
232 else
233 mp_actGroupMenu->setDisabled( true );
234 }
235 }
236
updateChat(const Chat & c)237 void GuiFloatingChat::updateChat( const Chat& c )
238 {
239 if( mp_chat->updateChat( c ) )
240 {
241 setMainIcon( c.unreadMessages() > 0 );
242 updateChatTitle( c );
243 updateChatMembers( c );
244 mp_chat->updateShortcuts();
245 mp_chat->updateActions( c, beeCore->isConnected(), beeCore->connectedUsers(), beeCore->isFileTransferActive() );
246 }
247 }
248
setChat(const Chat & c)249 bool GuiFloatingChat::setChat( const Chat& c )
250 {
251 if( mp_chat->setChat( c ) )
252 {
253 setMainIcon( c.unreadMessages() > 0 );
254 updateChatTitle( c );
255 updateChatMembers( c );
256 mp_chat->updateShortcuts();
257 mp_chat->updateActions( c, beeCore->isConnected(), beeCore->connectedUsers(), beeCore->isFileTransferActive() );
258 return true;
259 }
260 else
261 return false;
262 }
263
updateActions(bool is_connected,int connected_users,bool file_transfer_is_active)264 void GuiFloatingChat::updateActions( bool is_connected, int connected_users, bool file_transfer_is_active )
265 {
266 Chat c = ChatManager::instance().chat( mp_chat->chatId() );
267 if( !c.isValid() )
268 return;
269 mp_chat->updateActions( c, is_connected, connected_users, file_transfer_is_active );
270 }
271
updateUser(const User & u)272 void GuiFloatingChat::updateUser( const User& u )
273 {
274 Chat c = ChatManager::instance().chat( mp_chat->chatId() );
275 if( !c.hasUser( u.id() ) )
276 return;
277
278 if( c.isPrivateForUser( u.id() ) )
279 updateChatTitle( c );
280
281 updateChatMember( c, u );
282 }
283
closeEvent(QCloseEvent * e)284 void GuiFloatingChat::closeEvent( QCloseEvent* e )
285 {
286 #ifdef BEEBEEP_USE_VOICE_CHAT
287 if( beeCore->voicePlayer()->chatId() == mp_chat->chatId() && beeCore->voicePlayer()->isPlaying() )
288 beeCore->voicePlayer()->stop();
289 #endif
290 if( Settings::instance().floatingChatState().isEmpty() )
291 Settings::instance().setShowEmoticonMenu( mp_dockEmoticons->isVisible() );
292 QMainWindow::closeEvent( e );
293 emit chatIsAboutToClose( mp_chat->chatId() );
294 e->accept();
295 }
296
setWindowFlagsAndGeometry()297 void GuiFloatingChat::setWindowFlagsAndGeometry()
298 {
299 setAttribute( Qt::WA_ShowWithoutActivating );
300 Bee::setWindowStaysOnTop( this, Settings::instance().stayOnTop() );
301
302 if( Settings::instance().floatingChatGeometry().isEmpty() )
303 {
304 resize( 600, 400 );
305 if( !QApplication::activeWindow() )
306 move( QApplication::desktop()->availableGeometry().width() - frameGeometry().width() - 30, 40 );
307 }
308 else
309 restoreGeometry( Settings::instance().floatingChatGeometry() );
310
311 if( Settings::instance().floatingChatState().isEmpty() )
312 {
313 mp_dockEmoticons->setVisible( Settings::instance().showEmoticonMenu() );
314 mp_dockPresetMessageList->hide();
315 }
316 else
317 restoreState( Settings::instance().floatingChatState() );
318
319 QSplitter* chat_splitter = mp_chat->chatSplitter();
320 if( Settings::instance().floatingChatSplitterState().isEmpty() )
321 {
322 int central_widget_height = centralWidget()->size().height();
323 QList<int> splitter_size_list;
324 splitter_size_list.append( central_widget_height - 80 );
325 splitter_size_list.append( 80 );
326 chat_splitter->setSizes( splitter_size_list );
327 }
328 else
329 chat_splitter->restoreState( Settings::instance().floatingChatSplitterState() );
330 }
331
showUp()332 void GuiFloatingChat::showUp()
333 {
334 Bee::showUp( this );
335 mp_chat->ensureLastMessageVisible();
336 }
337
raiseOnTop()338 void GuiFloatingChat::raiseOnTop()
339 {
340 Bee::raiseOnTop( this );
341 }
342
setFocusInChat()343 void GuiFloatingChat::setFocusInChat()
344 {
345 QWidget* w = QApplication::activeWindow();
346 if( !w )
347 {
348 #ifdef BEEBEEP_DEBUG
349 qWarning() << "Unable to set focus in chat: application has not the focus";
350 #endif
351 return;
352 }
353
354 QApplication::setActiveWindow( this );
355 mp_chat->ensureFocusInChat();
356 }
357
onApplicationFocusChanged(QWidget * old,QWidget * now)358 void GuiFloatingChat::onApplicationFocusChanged( QWidget* old, QWidget* now )
359 {
360 if( old == Q_NULLPTR && isAncestorOf( now ) )
361 {
362 #ifdef BEEBEEP_DEBUG
363 qDebug() << "Floating chat" << mp_chat->chatId() << "has grab focus";
364 #endif
365 m_chatIsVisible = true;
366 m_prevActivatedState = true;
367 setWindowOpacity( Settings::instance().chatActiveWindowOpacityLevel() / 100.0 );
368 mp_chat->updateActionsOnFocusChanged();
369 emit readAllMessages( mp_chat->chatId() );
370 mp_chat->ensureFocusInChat();
371 return;
372 }
373
374 if( isAncestorOf( old ) && now == Q_NULLPTR )
375 {
376 #ifdef BEEBEEP_DEBUG
377 qDebug() << "Floating chat" << mp_chat->chatId() << "has lost focus";
378 #endif
379 m_chatIsVisible = false;
380 m_prevActivatedState = false;
381 setWindowOpacity( Settings::instance().chatInactiveWindowOpacityLevel() / 100.0 );
382 return;
383 }
384
385 bool current_state = isActiveWindow();
386 if( current_state != m_prevActivatedState )
387 {
388 m_prevActivatedState = current_state;
389 if( current_state )
390 {
391 #ifdef BEEBEEP_DEBUG
392 qDebug() << "Floating chat" << mp_chat->chatId() << "has grab focus (active)";
393 #endif
394 m_chatIsVisible = true;
395 setWindowOpacity( Settings::instance().chatActiveWindowOpacityLevel() / 100.0 );
396 mp_chat->updateActionsOnFocusChanged();
397 emit readAllMessages( mp_chat->chatId() );
398 mp_chat->ensureFocusInChat();
399 }
400 else
401 {
402 #ifdef BEEBEEP_DEBUG
403 qDebug() << "Floating chat" << mp_chat->chatId() << "has lost focus (inactive)";
404 #endif
405 m_chatIsVisible = false;
406 setWindowOpacity( Settings::instance().chatInactiveWindowOpacityLevel() / 100.0 );
407 }
408 }
409 }
410
saveGeometryAndState()411 void GuiFloatingChat::saveGeometryAndState()
412 {
413 if( isVisible() )
414 {
415 QByteArray ba_state = saveState();
416 #if QT_VERSION == 0x050906
417 int default_button = mp_dockEmoticons->isVisible() || mp_dockPresetMessageList->isVisible() ? 0 : 1;
418 int ret_code = QMessageBox::warning( this, Settings::instance().programName(),
419 tr( "Qt libraries have a bug on saving the window's state." ) + QString( " " ) +
420 tr( "If you have layout problem please save only geometry." ),
421 tr( "Save all" ), tr( "Save only geometry" ), tr( "Cancel" ), default_button, 2 );
422 switch( ret_code )
423 {
424 case 0:
425 break;
426 case 1:
427 ba_state = QByteArray();
428 break;
429 default:
430 return;
431 }
432 #endif
433 Settings::instance().setFloatingChatGeometry( saveGeometry() );
434 Settings::instance().setFloatingChatState( ba_state );
435 QSplitter* chat_splitter = mp_chat->chatSplitter();
436 Settings::instance().setFloatingChatSplitterState( chat_splitter->saveState() );
437 Settings::instance().setShowEmoticonMenu( mp_dockEmoticons->isVisible() );
438 Settings::instance().save();
439 if( ba_state.isEmpty() )
440 statusBar()->showMessage( tr( "Window geometry saved" ), 5000 );
441 else
442 statusBar()->showMessage( tr( "Window geometry and state saved" ), 5000 );
443 }
444 else
445 qWarning() << "Unable to save floating chat geometry and state (window is not visible)";
446 }
447
keyPressEvent(QKeyEvent * e)448 void GuiFloatingChat::keyPressEvent( QKeyEvent* e )
449 {
450 if( e->key() == Qt::Key_Escape )
451 {
452 if( Settings::instance().keyEscapeMinimizeInTray() )
453 QMetaObject::invokeMethod( this, "close", Qt::QueuedConnection );
454 else
455 QMetaObject::invokeMethod( this, "showMinimized", Qt::QueuedConnection );
456 e->accept();
457 return;
458 }
459
460 QMainWindow::keyPressEvent( e );
461 }
462
setMainIcon(bool with_message)463 void GuiFloatingChat::setMainIcon( bool with_message )
464 {
465 if( with_message )
466 setWindowIcon( IconManager::instance().icon( "beebeep-message.png" ) );
467 else
468 setWindowIcon( m_mainWindowIcon );
469 }
470
updateEmoticons()471 void GuiFloatingChat::updateEmoticons()
472 {
473 QMetaObject::invokeMethod( mp_emoticonsWidget, "updateEmoticons", Qt::QueuedConnection );
474 }
475
toggleVisibilityEmoticonPanel()476 void GuiFloatingChat::toggleVisibilityEmoticonPanel()
477 {
478 if( mp_dockEmoticons->isVisible() )
479 mp_dockEmoticons->hide();
480 else
481 mp_dockEmoticons->show();
482 }
483
toggleVisibilityPresetMessagesPanel()484 void GuiFloatingChat::toggleVisibilityPresetMessagesPanel()
485 {
486 if( mp_dockPresetMessageList->isVisible() )
487 mp_dockPresetMessageList->hide();
488 else
489 mp_dockPresetMessageList->show();
490 }
491
onGroupMemberActionTriggered()492 void GuiFloatingChat::onGroupMemberActionTriggered()
493 {
494 QAction *act = qobject_cast<QAction*>( sender() );
495 if( act )
496 {
497 VNumber user_id = Bee::qVariantToVNumber( act->data() );
498 emit showVCardRequest( user_id );
499 }
500 }
501
setChatReadByUser(const Chat & c,const User & u)502 void GuiFloatingChat::setChatReadByUser( const Chat& c, const User& u )
503 {
504 if( c.id() == mp_chat->chatId() )
505 updateChatMember( c, u );
506 }
507
showChatMessage(const Chat & c,const ChatMessage & cm)508 void GuiFloatingChat::showChatMessage( const Chat& c, const ChatMessage& cm )
509 {
510 if( mp_chat->appendChatMessage( c, cm ) )
511 {
512 if( cm.isFromLocalUser() )
513 {
514 updateChatMembers( c );
515 }
516 else
517 {
518 if( !cm.isFromSystem() )
519 statusBar()->showMessage( "" ); // reset writing message
520 }
521 }
522 }
523
showGroupMenu()524 void GuiFloatingChat::showGroupMenu()
525 {
526 mp_chat->editChatMembers();
527 }
528
onTickEvent(int ticks)529 void GuiFloatingChat::onTickEvent( int ticks )
530 {
531 mp_chat->onTickEvent( ticks );
532 }
533
showStatusMessage(const QString & msg,int timeout)534 void GuiFloatingChat::showStatusMessage( const QString& msg, int timeout )
535 {
536 statusBar()->showMessage( msg, timeout );
537 QApplication::processEvents();
538 }
539
loadSavedMessages()540 void GuiFloatingChat::loadSavedMessages()
541 {
542 QTimer::singleShot( 0, mp_chat, SLOT( loadSavedMessages() ) );
543 }
544
545 #ifdef BEEBEEP_USE_VOICE_CHAT
showRecordMessageDialog()546 void GuiFloatingChat::showRecordMessageDialog()
547 {
548 if( Settings::instance().disableVoiceMessages() )
549 return;
550 GuiRecordVoiceMessage* grvm = new GuiRecordVoiceMessage( this );
551 grvm->setModal( true );
552 grvm->setRecipient( ChatManager::instance().chatName( mp_chat->chatId() ) );
553 grvm->show();
554 if( grvm->exec() == QDialog::Accepted )
555 {
556 emit sendVoiceMessageRequest( mp_chat->chatId(), grvm->filePath(), grvm->duration() );
557 }
558 // deleted in Close Event to bypass crash if you close a modal dialog with QUIT
559 }
560 #endif
561