Daniel Brötzmann pushed to branch voice-request-button at gajim / gajim
Commits: 307b7baf by wurstsalat at 2022-08-29T11:59:48+02:00 imprv: Workspaces: Move chats when removing workspace
Closes #11098
- - - - - d78234b5 by wurstsalat at 2022-08-29T18:42:00+02:00 change: ChatBanner: Add button for managing voice requests
- - - - -
14 changed files:
- gajim/common/modules/muc.py - gajim/data/gui/chat_banner.ui - gajim/data/style/gajim.css - gajim/gtk/builder.pyi - gajim/gtk/chat_banner.py - gajim/gtk/chat_list_stack.py - gajim/gtk/control.py - + gajim/gtk/groupchat_voice_requests_button.py - gajim/gtk/main.py - gajim/gtk/menus.py - gajim/gtk/structs.py - gajim/gtk/workspace_dialog.py - gajim/gtk/workspace_side_bar.py - pyrightconfig.json
Changes:
===================================== gajim/common/modules/muc.py ===================================== @@ -39,6 +39,7 @@ from nbxmpp.structs import MessageProperties from nbxmpp.structs import PresenceProperties from nbxmpp.structs import StanzaHandler +from nbxmpp.structs import VoiceRequest from nbxmpp.task import Task
from gi.repository import GLib @@ -149,6 +150,8 @@ def __init__(self, con: types.Client) -> None: str, dict[str, MUCPresenceData]] = defaultdict(dict) self._mucs: dict[str, MUCData] = {} self._muc_nicknames = {} + self._voice_requests: dict[ + GroupchatContact, list[VoiceRequest]] = defaultdict(list)
def _on_resume_failed(self, _client: types.Client, @@ -808,10 +811,35 @@ def _on_voice_request(self, return
room = self._get_contact(properties.jid.bare) + assert isinstance(room, GroupchatContact) + assert properties.voice_request is not None + + self._voice_requests[room].append(properties.voice_request) room.notify('room-voice-request', properties)
raise nbxmpp.NodeProcessed
+ def get_voice_requests(self, + contact: GroupchatContact + ) -> list[VoiceRequest]: + + return self._voice_requests.get(contact, []) + + def approve_voice_request(self, + contact: GroupchatContact, + voice_request: VoiceRequest + ) -> None: + + self._voice_requests[contact].remove(voice_request) + self._nbxmpp('MUC').approve_voice_request(contact.jid, voice_request) + + def decline_voice_request(self, + contact: GroupchatContact, + voice_request: VoiceRequest + ) -> None: + + self._voice_requests[contact].remove(voice_request) + def _on_captcha_challenge(self, _con: types.xmppClient, _stanza: Message,
===================================== gajim/data/gui/chat_banner.ui ===================================== @@ -1,5 +1,5 @@ <?xml version="1.0" encoding="UTF-8"?> -<!-- Generated with glade 3.38.2 --> +<!-- Generated with glade 3.40.0 --> <interface> <requires lib="gtk+" version="3.24"/> <object class="GtkBox" id="banner_box"> @@ -125,7 +125,7 @@ </packing> </child> <child> - <object class="GtkBox" id="account_badge_box"> + <object class="GtkBox" id="additional_items_box"> <property name="visible">True</property> <property name="can-focus">False</property> <property name="halign">end</property>
===================================== gajim/data/style/gajim.css ===================================== @@ -813,6 +813,18 @@ .badge { font-weight: bold; }
+@keyframes pulse-opacity { + 0% { opacity: 1.0; } + 20% { opacity: 0.5; } + 45% { opacity: 1.0; } +} +.pulse-opacity { + animation-name: pulse-opacity; + animation-duration: 2.0s; + animation-timing-function: ease-in-out; + animation-iteration-count: infinite; +} + @keyframes pulse { 0% { -gtk-icon-transform: scale(1.0); } 20% { -gtk-icon-transform: scale(1.1); }
===================================== gajim/gtk/builder.pyi ===================================== @@ -18,7 +18,6 @@ class AccountPageBuilder(Builder): avatar_image: Gtk.Image account_label: Gtk.Label account_action_box: Gtk.Box - account_settings: Gtk.Button status_message_box: Gtk.Box
@@ -202,7 +201,7 @@ class ChatBannerBuilder(Builder): phone_image: Gtk.Image toggle_roster_button: Gtk.Button toggle_roster_image: Gtk.Image - account_badge_box: Gtk.Box + additional_items_box: Gtk.Box visitor_box: Gtk.Box visitor_menu_button: Gtk.MenuButton visitor_popover: Gtk.Popover
===================================== gajim/gtk/chat_banner.py ===================================== @@ -40,6 +40,7 @@ from gajim.common.modules.contacts import GroupchatParticipant
from .builder import get_builder +from .groupchat_voice_requests_button import VoiceRequestsButton from .util import AccountBadge
@@ -58,6 +59,9 @@ def __init__(self) -> None: self._ui.connect_signals(self)
self._account_badge: Optional[AccountBadge] = None + self._voice_requests_button = VoiceRequestsButton() + self._ui.additional_items_box.pack_start( + self._voice_requests_button, False, True, 0)
hide_roster = app.settings.get('hide_groupchat_occupants_list') self._set_toggle_roster_button_icon(hide_roster) @@ -103,7 +107,8 @@ def switch_contact(self, contact: types.ChatContactT) -> None: if isinstance(self._contact, GroupchatContact): self._contact.multi_connect({ 'user-role-changed': self._on_user_role_changed, - 'state-changed': self._on_muc_state_changed + 'state-changed': self._on_muc_state_changed, + 'room-voice-request': self._on_room_voice_request }) self._ui.toggle_roster_button.show() hide_banner = app.settings.get('hide_groupchat_banner') @@ -130,6 +135,8 @@ def switch_contact(self, contact: types.ChatContactT) -> None: self._ui.phone_image.set_visible( self._contact in self._last_message_from_phone)
+ self._voice_requests_button.switch_contact(self._contact) + if hide_banner: self.set_no_show_all(True) self.hide() @@ -184,6 +191,10 @@ def _on_muc_state_changed(self, if contact.is_joined: self._update_content()
+ def _on_room_voice_request(self, *args: Any) -> None: + self._voice_requests_button.set_no_show_all(False) + self._voice_requests_button.show_all() + def _on_user_role_changed(self, _contact: GroupchatContact, _signal_name: str, @@ -284,7 +295,8 @@ def _update_account_badge(self, account: str) -> None: enabled_accounts = app.get_enabled_accounts_with_labels() if len(enabled_accounts) > 1: self._account_badge = AccountBadge(account) - self._ui.account_badge_box.add(self._account_badge) + self._ui.additional_items_box.pack_end( + self._account_badge, False, True, 0)
def _on_request_voice_clicked(self, _button: Gtk.Button) -> None: self._ui.visitor_popover.popdown()
===================================== gajim/gtk/chat_list_stack.py ===================================== @@ -243,16 +243,17 @@ def _move_chat_to_workspace(self, if not workspace_id: workspace_id = app.window.add_workspace(switch=False)
- current_chatlist = cast(ChatList, self.get_visible_child()) - type_ = current_chatlist.get_chat_type(params.account, params.jid) + source_chatlist = self.get_chatlist(params.source_workspace_id) + type_ = source_chatlist.get_chat_type(params.account, params.jid) if type_ is None: return - current_chatlist.remove_chat(params.account, params.jid) + + source_chatlist.remove_chat(params.account, params.jid)
new_chatlist = self.get_chatlist(workspace_id) new_chatlist.add_chat(params.account, params.jid, type_, False, -1)
- self.store_open_chats(current_chatlist.workspace_id) + self.store_open_chats(source_chatlist.workspace_id) self.store_open_chats(workspace_id)
@structs.actionmethod
===================================== gajim/gtk/control.py ===================================== @@ -57,8 +57,6 @@ from gajim.gui.conversation.scrolled import ScrolledView from gajim.gui.conversation.jump_to_end_button import JumpToEndButton from gajim.gui.builder import get_builder -from gajim.gui.dialogs import DialogButton -from gajim.gui.dialogs import ConfirmationDialog from gajim.gui.groupchat_roster import GroupchatRoster from gajim.gui.groupchat_state import GroupchatState
@@ -200,7 +198,6 @@ def switch_contact(self, contact: Union[BareContact, 'room-config-finished': self._on_room_config_finished, 'room-config-changed': self._on_room_config_changed, 'room-presence-error': self._on_room_presence_error, - 'room-voice-request': self._on_room_voice_request, 'room-subject': self._on_room_subject, })
@@ -779,29 +776,6 @@ def _on_user_status_show_changed(self, status=status) self.add_info_message(message)
- def _on_room_voice_request(self, - _contact: GroupchatContact, - _signal_name: str, - properties: MessageProperties - ) -> None: - voice_request = properties.voice_request - assert voice_request is not None - - def on_approve() -> None: - self.client.get_module('MUC').approve_voice_request( - self.contact.jid, voice_request) - - ConfirmationDialog( - _('Voice Request'), - _('Voice Request'), - _('<b>%(nick)s</b> from <b>%(room_name)s</b> requests voice') % { - 'nick': voice_request.nick, 'room_name': self.contact.name}, - [DialogButton.make('Cancel'), - DialogButton.make('Accept', - text=_('_Approve'), - callback=on_approve)], - modal=False).show() - def add_muc_message(self, text: str, tim: float,
===================================== gajim/gtk/groupchat_voice_requests_button.py ===================================== @@ -0,0 +1,151 @@ +# This file is part of Gajim. +# +# Gajim is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published +# by the Free Software Foundation; version 3 only. +# +# Gajim is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Gajim. If not, see http://www.gnu.org/licenses/. + +from __future__ import annotations + +from typing import cast + +from gi.repository import GLib +from gi.repository import Gtk +from gi.repository import Pango + +from nbxmpp.structs import VoiceRequest + +from gajim.common import app +from gajim.common.types import ChatContactT +from gajim.common.i18n import _ +from gajim.common.modules.contacts import GroupchatContact + + +class VoiceRequestsButton(Gtk.Button): + def __init__(self) -> None: + Gtk.Button.__init__(self) + self.set_tooltip_text(_('Pending Voice Requests')) + self.set_no_show_all(True) + image = Gtk.Image.new_from_icon_name( + 'dialog-question-symbolic', Gtk.IconSize.BUTTON) + self.add(image) + self.get_style_context().add_class('pulse-opacity') + self.get_style_context().add_class('suggested-action') + self.connect('clicked', self._on_button_clicked) + + def switch_contact(self, contact: ChatContactT) -> None: + if not isinstance(contact, GroupchatContact): + self._contact = None + self.set_no_show_all(True) + self.hide() + return + + self._contact = contact + self._update() + + def _update(self) -> None: + assert self._contact is not None + client = app.get_client(self._contact.account) + voice_requests = client.get_module('MUC').get_voice_requests( + self._contact) + self.hide() + if voice_requests: + self.set_no_show_all(False) + self.show_all() + + def _on_button_clicked(self, _button: VoiceRequestsButton) -> None: + assert self._contact is not None + client = app.get_client(self._contact.account) + voice_requests = client.get_module('MUC').get_voice_requests( + self._contact) + if not voice_requests: + return + + menu_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6) + menu_box.get_style_context().add_class('padding-12') + menu_box.set_hexpand(True) + + desc_label = Gtk.Label(label=_('Participants asking for voice:')) + desc_label.get_style_context().add_class('dim-label') + desc_label.set_max_width_chars(35) + desc_label.set_line_wrap(True) + desc_label.set_margin_bottom(6) + menu_box.add(desc_label) + + for request in cast(list[VoiceRequest], voice_requests): + request_box = Gtk.Box(spacing=12) + + name_label = Gtk.Label(label=request.nick) + name_label.set_width_chars(10) + name_label.set_max_width_chars(20) + name_label.set_ellipsize(Pango.EllipsizeMode.END) + name_label.set_xalign(0) + request_box.add(name_label) + + decline_button = Gtk.Button.new_from_icon_name( + 'process-stop-symbolic', Gtk.IconSize.BUTTON) + decline_button.set_tooltip_text(_('Decline')) + decline_button.connect( + 'clicked', + self._on_decline, + request, + self._contact) + request_box.pack_end(decline_button, False, False, 0) + + approve_button = Gtk.Button.new_from_icon_name( + 'feather-check-symbolic', Gtk.IconSize.BUTTON) + approve_button.set_tooltip_text(_('Approve')) + approve_button.connect( + 'clicked', + self._on_approve, + request, + self._contact) + request_box.pack_end(approve_button, False, False, 0) + + if voice_requests.index(request) > 0: + menu_box.add(Gtk.Separator()) + + menu_box.add(request_box) + + menu_box.show_all() + + popover = Gtk.PopoverMenu() + popover.get_style_context().add_class('padding-6') + popover.set_relative_to(self) + popover.set_position(Gtk.PositionType.BOTTOM) + popover.add(menu_box) + popover.connect('closed', self._on_closed) + popover.popup() + + @staticmethod + def _on_closed(popover: Gtk.Popover) -> None: + GLib.idle_add(popover.destroy) + + def _on_approve(self, + _button: Gtk.Button, + voice_request: VoiceRequest, + contact: GroupchatContact + ) -> None: + + client = app.get_client(contact.account) + client.get_module('MUC').approve_voice_request( + contact, voice_request) + self._update() + + def _on_decline(self, + _button: Gtk.Button, + voice_request: VoiceRequest, + contact: GroupchatContact + ) -> None: + + client = app.get_client(contact.account) + client.get_module('MUC').decline_voice_request( + contact, voice_request) + self._update()
===================================== gajim/gtk/main.py ===================================== @@ -62,6 +62,7 @@ from .structs import AccountJidParam from .structs import AddChatActionParams from .structs import actionmethod +from .structs import ChatListEntryParam
if TYPE_CHECKING: from .control import ChatControl @@ -626,18 +627,52 @@ def _remove_workspace(self, self.remove_workspace(workspace_id)
def remove_workspace(self, workspace_id: str) -> None: + if len(app.settings.get_workspaces()) == 1: + log.warning('Tried to remove the only workspace') + return + was_active = self.get_active_workspace() == workspace_id + chat_list = self.get_chat_list(workspace_id) + open_chats = chat_list.get_open_chats() + + def _continue_removing_workspace(): + new_workspace_id = self._workspace_side_bar.get_other_workspace( + workspace_id) + if new_workspace_id is None: + log.warning('No other workspaces found') + return
- success = self._workspace_side_bar.remove_workspace(workspace_id) - if not success: + for open_chat in open_chats: + params = ChatListEntryParam( + workspace_id=new_workspace_id, + source_workspace_id=workspace_id, + account=open_chat['account'], + jid=open_chat['jid']) + self.activate_action('move-chat-to-workspace', + params.to_variant()) + + if was_active: + self.activate_workspace(new_workspace_id) + + self._workspace_side_bar.remove_workspace(workspace_id) + self._chat_page.remove_chat_list(workspace_id) + app.settings.remove_workspace(workspace_id) + + if open_chats: + ConfirmationDialog( + _('Remove Workspace'), + _('Remove Workspace'), + _('This workspace contains chats. All chats will be moved to ' + 'the next workspace. Remove anyway?'), + [DialogButton.make('Cancel', + text=_('_No')), + DialogButton.make('Remove', + callback=_continue_removing_workspace)] + ).show() return
- if was_active: - new_active_id = self._workspace_side_bar.get_first_workspace() - self.activate_workspace(new_active_id) - - self._chat_page.remove_chat_list(workspace_id) - app.settings.remove_workspace(workspace_id) + # No chats in chat list, it is save to remove this workspace + _continue_removing_workspace()
def _activate_workspace(self, _action: Gio.SimpleAction,
===================================== gajim/gtk/menus.py ===================================== @@ -410,6 +410,7 @@ def get_chat_list_row_menu(workspace_id: str, menu = GajimMenu()
params = ChatListEntryParam(workspace_id=workspace_id, + source_workspace_id=workspace_id, account=account, jid=jid)
@@ -423,6 +424,7 @@ def get_chat_list_row_menu(workspace_id: str, submenu.add_item(name, 'win.move-chat-to-workspace', params)
params = ChatListEntryParam(workspace_id='', + source_workspace_id=workspace_id, account=account, jid=jid)
@@ -450,6 +452,7 @@ def get_workspace_params(current_workspace_id: str, continue name = app.settings.get_workspace_setting(workspace_id, 'name') params = ChatListEntryParam(workspace_id=workspace_id, + source_workspace_id=current_workspace_id, account=account, jid=jid) yield name, params @@ -575,9 +578,13 @@ def get_format_menu() -> GajimMenu:
def get_workspace_menu(workspace_id: str) -> GajimMenu: + remove_action = 'win.dummy' + if len(app.settings.get_workspaces()) > 1: + remove_action = 'win.remove-workspace' + menuitems: MenuItemListT = [ (_('Edit…'), 'win.edit-workspace', f'"{workspace_id}"'), - (_('Remove'), 'win.remove-workspace', f'"{workspace_id}"'), + (_('Remove'), remove_action, f'"{workspace_id}"'), ]
return GajimMenu.from_list(menuitems)
===================================== gajim/gtk/structs.py ===================================== @@ -55,6 +55,7 @@ class AddChatActionParams(VariantMixin): @dataclass class ChatListEntryParam(VariantMixin): workspace_id: str + source_workspace_id: str account: str jid: JID
===================================== gajim/gtk/workspace_dialog.py ===================================== @@ -24,8 +24,6 @@
from .avatar import make_workspace_avatar from .avatar_selector import AvatarSelector -from .dialogs import ConfirmationDialog -from .dialogs import DialogButton from .builder import get_builder from .util import rgba_to_float from .util import make_rgba @@ -93,25 +91,9 @@ def _on_key_press(self, _widget: Gtk.Widget, event: Gdk.EventKey) -> None: self.destroy()
def _on_remove_workspace(self, _button: Gtk.Button) -> None: - def _on_remove(): - assert self._workspace_id is not None - app.window.remove_workspace(self._workspace_id) - self.destroy() assert self._workspace_id is not None - chat_list = app.window.get_chat_list(self._workspace_id) - open_chats = chat_list.get_open_chats() - if len(open_chats) > 0: - ConfirmationDialog( - _('Remove Workspace'), - _('Remove Workspace'), - _('This workspace contains chats. Remove anyway?'), - [DialogButton.make('Cancel', - text=_('_No')), - DialogButton.make('Remove', - callback=_on_remove)]).show() - return - # No chats in chat list, it is save to remove workspace - _on_remove() + app.window.remove_workspace(self._workspace_id) + self.destroy()
def _on_cancel(self, _button: Gtk.Button) -> None: self.destroy()
===================================== gajim/gtk/workspace_side_bar.py ===================================== @@ -158,6 +158,7 @@ def _process_chat_list_drop(self, workspace_id = ''
params = ChatListEntryParam(workspace_id=workspace_id, + source_workspace_id='', account=account, jid=jid) app.window.activate_action('move-chat-to-workspace', @@ -221,12 +222,18 @@ def store_workspace_order(self) -> None: order.remove('add') app.settings.set_app_setting('workspace_order', order)
- def remove_workspace(self, workspace_id: str) -> bool: - if len(self._workspaces) == 1: - return False + def remove_workspace(self, workspace_id: str) -> None: row = self._workspaces.pop(workspace_id) self.remove(row) - return True + + def get_other_workspace(self, + exclude_workspace_id: str + ) -> Optional[str]: + + for workspace in self._workspaces.values(): + if workspace.workspace_id != exclude_workspace_id: + return workspace.workspace_id + return None
def activate_workspace(self, workspace_id: str) -> None: row = cast(CommonWorkspace, self.get_selected_row())
===================================== pyrightconfig.json ===================================== @@ -90,6 +90,7 @@ "gajim/gtk/groupchat_roster.py", "gajim/gtk/groupchat_settings.py", "gajim/gtk/groupchat_state.py", + "gajim/gtk/groupchat_voice_requests_button.py", "gajim/gtk/gstreamer.py", "gajim/gtk/history_export.py", "gajim/gtk/history_sync.py",
View it on GitLab: https://dev.gajim.org/gajim/gajim/-/compare/720472e274c64f03b5c8682d8b8f59c2...