Philipp Hörist pushed to branch master at gajim / gajim
Commits: 56aa0693 by lovetox at 2022-01-26T22:17:37+01:00 GroupchatDetails: Add GroupchatOutcasts Page
- - - - -
6 changed files:
- gajim/data/gui/groupchat_details.ui - + gajim/data/gui/groupchat_outcast.ui - gajim/gtk/builder.pyi - gajim/gtk/groupchat_details.py - + gajim/gtk/groupchat_outcasts.py - pyrightconfig.json
Changes:
===================================== gajim/data/gui/groupchat_details.ui ===================================== @@ -147,6 +147,25 @@ <property name="position">2</property> </packing> </child> + <child> + <object class="GtkBox" id="outcasts_box"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="orientation">vertical</property> + <child> + <placeholder/> + </child> + <style> + <class name="padding-18"/> + </style> + </object> + <packing> + <property name="name">outcasts</property> + <property name="title" translatable="yes">Outcasts</property> + <property name="icon-name">system-users-symbolic</property> + <property name="position">3</property> + </packing> + </child> <style> <class name="stack-border"/> </style>
===================================== gajim/data/gui/groupchat_outcast.ui ===================================== @@ -0,0 +1,266 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Generated with glade 3.38.2 --> +<interface> + <requires lib="gtk+" version="3.24"/> + <object class="GtkPopover" id="info_popover"> + <property name="can-focus">False</property> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="label" translatable="yes"><b>XMPP Address</b> +&lt;user@domain/resource&gt; (only that resource matches) +&lt;user@domain&gt; (any resource matches) +&lt;domain/resource&gt; (only that resource matches) +&lt;domain&gt; (the domain itself matches, as does any user@domain or domain/resource) +</property> + <property name="use-markup">True</property> + <style> + <class name="padding-6"/> + </style> + </object> + </child> + </object> + <object class="GtkListStore" id="outcast_store"> + <columns> + <!-- column-name address --> + <column type="gchararray"/> + <!-- column-name reason --> + <column type="gchararray"/> + <!-- column-name editable --> + <column type="gboolean"/> + </columns> + </object> + <object class="GtkBox" id="main_box"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkScrolledWindow" id="outcast_scrolled"> + <property name="visible">True</property> + <property name="can-focus">True</property> + <property name="hexpand">True</property> + <property name="vexpand">True</property> + <property name="shadow-type">in</property> + <child> + <object class="GtkTreeView" id="outcast_treeview"> + <property name="visible">True</property> + <property name="can-focus">True</property> + <property name="model">outcast_store</property> + <property name="search-column">0</property> + <property name="show-expanders">False</property> + <property name="enable-grid-lines">horizontal</property> + <child internal-child="selection"> + <object class="GtkTreeSelection"> + <property name="mode">multiple</property> + <signal name="changed" handler="_on_selection_changed" swapped="no"/> + </object> + </child> + <child> + <object class="GtkTreeViewColumn"> + <property name="resizable">True</property> + <property name="sizing">fixed</property> + <property name="min-width">150</property> + <property name="title">XMPP Address</property> + <property name="expand">True</property> + <property name="clickable">True</property> + <property name="sort-indicator">True</property> + <property name="sort-column-id">0</property> + <child> + <object class="GtkCellRendererText"> + <property name="ellipsize">end</property> + <property name="placeholder-text">user@example.org</property> + <signal name="edited" handler="_on_jid_edited" swapped="no"/> + </object> + <attributes> + <attribute name="editable">2</attribute> + <attribute name="text">0</attribute> + </attributes> + </child> + </object> + </child> + <child> + <object class="GtkTreeViewColumn"> + <property name="resizable">True</property> + <property name="sizing">fixed</property> + <property name="min-width">120</property> + <property name="title" translatable="yes">Reason</property> + <property name="clickable">True</property> + <property name="sort-indicator">True</property> + <property name="sort-column-id">1</property> + <child> + <object class="GtkCellRendererText"> + <property name="placeholder-text">Spam</property> + <signal name="edited" handler="_on_reason_edited" swapped="no"/> + </object> + <attributes> + <attribute name="editable">2</attribute> + <attribute name="text">1</attribute> + </attributes> + </child> + </object> + </child> + </object> + </child> + <style> + <class name="margin-12"/> + </style> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkBox" id="button_box"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="spacing">12</property> + <child> + <object class="GtkBox" id="add_remove_button_box"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="valign">start</property> + <property name="vexpand">False</property> + <property name="spacing">6</property> + <child> + <object class="GtkButton" id="add_button"> + <property name="visible">True</property> + <property name="sensitive">False</property> + <property name="can-focus">True</property> + <property name="receives-default">True</property> + <property name="tooltip-text" translatable="yes">Only Admins and Owners can modify the affiliation</property> + <property name="halign">start</property> + <signal name="clicked" handler="_on_add" swapped="no"/> + <child> + <object class="GtkImage"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="icon-name">list-add-symbolic</property> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkButton" id="remove_button"> + <property name="visible">True</property> + <property name="sensitive">False</property> + <property name="can-focus">True</property> + <property name="receives-default">True</property> + <property name="tooltip-text" translatable="yes">Only Admins and Owners can modify the affiliation</property> + <signal name="clicked" handler="_on_remove" swapped="no"/> + <child> + <object class="GtkImage"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="icon-name">list-remove-symbolic</property> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkMenuButton" id="info_button"> + <property name="visible">True</property> + <property name="can-focus">True</property> + <property name="receives-default">True</property> + <property name="popover">info_popover</property> + <child> + <object class="GtkImage"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="icon-name">dialog-information-symbolic</property> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="spacing">6</property> + <child> + <object class="GtkImage" id="status_image"> + <property name="can-focus">False</property> + <property name="no-show-all">True</property> + <property name="stock">gtk-missing-image</property> + <property name="icon_size">1</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkSpinner" id="spinner"> + <property name="can-focus">False</property> + <property name="no-show-all">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkButton" id="apply_button"> + <property name="label">_Apply</property> + <property name="visible">True</property> + <property name="sensitive">False</property> + <property name="can-focus">True</property> + <property name="receives-default">True</property> + <property name="use-underline">True</property> + <signal name="clicked" handler="_on_apply" swapped="no"/> + <style> + <class name="suggested-action"/> + </style> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">2</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="pack-type">end</property> + <property name="position">2</property> + </packing> + </child> + <style> + <class name="margin-12"/> + </style> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> +</interface>
===================================== gajim/gtk/builder.pyi ===================================== @@ -499,6 +499,7 @@ class GroupchatDetailsBuilder(Builder): info_box: Gtk.Box settings_box: Gtk.Box affiliation_box: Gtk.Box + outcasts_box: Gtk.Box
class GroupchatInfoScrolledBuilder(Builder): @@ -541,6 +542,22 @@ class GroupchatNickChooserBuilder(Builder): apply_button: Gtk.Button
+class GroupchatOutcastBuilder(Builder): + info_popover: Gtk.Popover + outcast_store: Gtk.ListStore + main_box: Gtk.Box + outcast_scrolled: Gtk.ScrolledWindow + outcast_treeview: Gtk.TreeView + button_box: Gtk.Box + add_remove_button_box: Gtk.Box + add_button: Gtk.Button + remove_button: Gtk.Button + info_button: Gtk.MenuButton + status_image: Gtk.Image + spinner: Gtk.Spinner + apply_button: Gtk.Button + + class GroupchatRosterBuilder(Builder): participant_store: Gtk.TreeStore roster_treeview: Gtk.TreeView @@ -808,7 +825,6 @@ class ServerInfoBuilder(Builder): no_addresses_label: Gtk.Label connection_type: Gtk.Label proxy_type: Gtk.Label - cert_button: Gtk.Button proxy_host: Gtk.Label domain_label: Gtk.Label dns_label: Gtk.Label @@ -1020,6 +1036,8 @@ def get_builder(file_name: Literal['groupchat_inviter.ui'], widgets: list[str] = @overload def get_builder(file_name: Literal['groupchat_nick_chooser.ui'], widgets: list[str] = ...) -> GroupchatNickChooserBuilder: ... @overload +def get_builder(file_name: Literal['groupchat_outcast.ui'], widgets: list[str] = ...) -> GroupchatOutcastBuilder: ... +@overload def get_builder(file_name: Literal['groupchat_roster.ui'], widgets: list[str] = ...) -> GroupchatRosterBuilder: ... @overload def get_builder(file_name: Literal['groupchat_roster_tooltip.ui'], widgets: list[str] = ...) -> GroupchatRosterTooltipBuilder: ...
===================================== gajim/gtk/groupchat_details.py ===================================== @@ -25,6 +25,7 @@ from gajim.common.i18n import _ from gajim.common.modules.contacts import GroupchatContact from gajim.gtk.groupchat_affiliation import GroupchatAffiliation +from gajim.gtk.groupchat_outcasts import GroupchatOutcasts
from .builder import get_builder from .groupchat_info import GroupChatInfoScrolled @@ -63,6 +64,7 @@ def __init__(self, self._add_groupchat_info() self._add_groupchat_settings() self._add_affiliations() + self._add_outcasts()
self._load_avatar() self._ui.name_entry.set_text(contact.name) @@ -120,6 +122,10 @@ def _add_affiliations(self) -> None: affiliations = GroupchatAffiliation(self._client, self._contact) self._ui.affiliation_box.add(affiliations)
+ def _add_outcasts(self) -> None: + affiliations = GroupchatOutcasts(self._client, self._contact) + self._ui.outcasts_box.add(affiliations) + def _on_key_press(self, _widget: GroupchatDetails, event: Gdk.EventKey) -> None:
===================================== gajim/gtk/groupchat_outcasts.py ===================================== @@ -0,0 +1,259 @@ +# 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 NamedTuple +from typing import Optional +from typing import cast + +import logging +from enum import IntEnum + +from gi.repository import Gtk +from nbxmpp.errors import StanzaError +from nbxmpp.structs import AffiliationResult +from nbxmpp.task import Task + +from gajim.common.client import Client +from gajim.common.i18n import _ +from gajim.common.modules.contacts import GroupchatContact + +from gajim.gui.builder import get_builder +from gajim.gui.dialogs import ErrorDialog + + +log = logging.getLogger('gajim.gui.groupchat_outcasts') + + +class OutcastRow(NamedTuple): + jid: str + reason: Optional[str] + + +class Column(IntEnum): + JID = 0 + REASON = 1 + + +class GroupchatOutcasts(Gtk.Box): + def __init__(self, client: Client, contact: GroupchatContact) -> None: + Gtk.Box.__init__(self, orientation=Gtk.Orientation.VERTICAL) + + self._client = client + self._contact = contact + self._current_rows: set[OutcastRow] = set() + + self_contact = contact.get_self() + assert self_contact is not None + self._own_affiliation = self_contact.affiliation.value + + self._ui = get_builder('groupchat_outcast.ui') + + self._treeview = self._ui.outcast_treeview + self._store = self._ui.outcast_store + + self.add(self._ui.main_box) + + if self._own_affiliation in ('admin', 'owner'): + self._ui.add_button.set_sensitive(True) + self._ui.add_button.set_tooltip_text('') + + self._ui.connect_signals(self) + + self._client.get_module('MUC').get_affiliation( + self._contact.jid, + 'outcast', + callback=self._on_outcasts_received) + + def _on_apply(self, _button: Gtk.Button) -> None: + self._begin_progress() + self._set_outcasts() + + def _begin_progress(self) -> None: + self._ui.spinner.show() + self._ui.spinner.start() + self._ui.apply_button.set_sensitive(False) + self._ui.outcast_scrolled.set_sensitive(False) + self._ui.add_remove_button_box.set_sensitive(False) + + def _end_progress(self, result: str) -> None: + self._ui.spinner.stop() + self._ui.spinner.hide() + self._ui.apply_button.set_sensitive(result == 'error') + self._ui.outcast_scrolled.set_sensitive(True) + self._ui.add_remove_button_box.set_sensitive(True) + + def _on_add(self, _button: Gtk.Button) -> None: + iter_ = self._store.append([None, None, True]) + + path = self._store.get_path(iter_) + self._treeview.scroll_to_cell(path, None, False, 0, 0) + self._treeview.get_selection().unselect_all() + self._treeview.get_selection().select_path(path) + + self._update_apply_button_state() + + def _on_remove(self, _button: Gtk.Button) -> None: + _model, paths = self._treeview.get_selection().get_selected_rows() + + references: list[Gtk.TreeRowReference] = [] + for path in paths: + references.append(Gtk.TreeRowReference.new(self._store, path)) + + for ref in references: + path = ref.get_path() + assert path is not None + iter_ = self._store.get_iter(path) + self._store.remove(iter_) + + self._update_apply_button_state() + + def _on_jid_edited(self, + _renderer: Gtk.CellRendererText, + path: str, + new_text: str) -> None: + + old_text = self._store[path][Column.JID] + if new_text == old_text: + return + + if self._jid_exists(new_text): + self._raise_error() + return + + self._store[path][Column.JID] = new_text + self._update_apply_button_state() + + def _on_reason_edited(self, + _renderer: Gtk.CellRendererText, + path: str, + new_text: str) -> None: + self._store[path][Column.REASON] = new_text or None + self._update_apply_button_state() + + def _on_selection_changed(self, tree_selection: Gtk.TreeSelection) -> None: + sensitive = bool(tree_selection.count_selected_rows()) + self._set_remove_button_state(sensitive) + + def _jid_exists(self, jid: str) -> bool: + for row in self._store: + if row[Column.JID] == jid: + return True + return False + + def _set_remove_button_state(self, sensitive: bool) -> None: + value = self._own_affiliation in ('admin', 'owner') + self._ui.remove_button.set_sensitive(sensitive and value) + + def _update_apply_button_state(self): + if self._own_affiliation not in ('admin', 'owner'): + return + new_rows = self._get_new_rows() + self._ui.apply_button.set_sensitive(new_rows != self._current_rows) + self._ui.status_image.hide() + + def _allowed_to_edit(self) -> bool: + return self._own_affiliation in ('owner', 'admin') + + def _get_new_rows(self) -> set[OutcastRow]: + rows: set[OutcastRow] = set() + + for row in self._store: + if not row[Column.JID]: + continue + + rows.add(OutcastRow(jid=row[Column.JID], + reason=row[Column.REASON])) + return rows + + def _get_diff(self) -> list[OutcastRow]: + + new_rows = self._get_new_rows() + + before = {row.jid for row in self._current_rows} + after = {row.jid for row in new_rows} + + removed = before - after + removed_rows = [row for row in self._current_rows if row.jid in removed] + + added = after - before + added_rows = [row for row in new_rows if row.jid in added] + + same = after - removed - added + same_rows = {row for row in new_rows if row.jid in same} + modified_rows = list(same_rows - self._current_rows) + + return removed_rows + added_rows + modified_rows + + def _set_outcasts(self) -> None: + diff_rows = self._get_diff() + + outcasts = {} + for row in diff_rows: + outcasts[row.jid] = {'affiliation': 'outcast'} + if row.reason: + outcasts[row.jid]['reason'] = row.reason + + self._client.get_module('MUC').set_affiliation( + self._contact.jid, + outcasts, + callback=self._on_affiliation_finished) + + def _on_affiliation_finished(self, task: Task) -> None: + try: + task.finish() + except StanzaError as error: + log.info('Error while setting outcasts: %s', error) + self._end_progress('error') + self._set_status_image('error', tooltip_text=str(error)) + return + + self._current_rows = self._get_new_rows() + self._set_status_image('success') + self._end_progress('success') + + def _on_outcasts_received(self, task: Task) -> None: + try: + result = cast(AffiliationResult, task.finish()) + except StanzaError as error: + log.info('Error while requesting outcasts: %s', error.condition) + return + + editable = self._allowed_to_edit() + for jid, attrs in result.users.items(): + reason = attrs.get('reason') + + jid = str(jid) + self._store.append([jid, reason, editable]) + + self._current_rows.add(OutcastRow(jid=jid, reason=reason)) + + def _set_status_image(self, state: str, tooltip_text: str = '') -> None: + icon_name = 'feather-check-symbolic' + css_class = 'success-color' + + if state == 'error': + icon_name = 'dialog-warning-symbolic' + css_class = 'warning-color' + + self._ui.status_image.set_from_icon_name(icon_name, Gtk.IconSize.MENU) + self._ui.status_image.get_style_context().add_class(css_class) + self._ui.status_image.set_tooltip_text(tooltip_text) + self._ui.status_image.show() + + @staticmethod + def _raise_error() -> None: + ErrorDialog(_('Error'), + _('An entry with this XMPP Address already exists'))
===================================== pyrightconfig.json ===================================== @@ -46,6 +46,7 @@ "gajim/gtk/groupchat_invitation.py", "gajim/gtk/groupchat_inviter.py", "gajim/gtk/groupchat_nick.py", + "gajim/gtk/groupchat_outcasts.py", "gajim/gtk/groupchat_settings.py", "gajim/gtk/groupchat_state.py", "gajim/gtk/main.py",
View it on GitLab: https://dev.gajim.org/gajim/gajim/-/commit/56aa069322bd4e87176190ba688c2859b...