Philipp Hörist pushed to branch new-chat-commands at gajim / gajim
Commits: 5c3af508 by wurstsalat at 2022-08-14T19:18:43+02:00 change: Shortcuts: Use Ctrl+F for opening the search bar
Fixes #11073
NOTES: Ctrl+H for opening the search bar has been removed, please use the commonly used Ctrl+F (find). The shortcut for starting a file transfer has been dropped.
- - - - - ef3683f3 by wurstsalat at 2022-08-14T19:49:20+02:00 cfix: ConversationView: Make sure ReadMarkerRow can be finalized
- - - - - 6fd1b99d by wurstsalat at 2022-08-14T20:35:02+02:00 new: Control: Store info messages per contact
- - - - - f21ec271 by wurstsalat at 2022-08-14T22:57:47+02:00 cfix: Control: Use deque for storing info messages
- - - - - b6e481ce by wurstsalat at 2022-08-14T23:18:22+02:00 new: Control: Store muc subject per contact
- - - - - 39f20787 by wurstsalat at 2022-08-15T10:57:20+02:00 cfix: MUC: muc_data for group chat nick is only available if joined
- - - - - cbc05e1b by wurstsalat at 2022-08-15T10:57:59+02:00 cfix: ConversationView: Don’t remove ReadmarkerRow
Fixes #11085
- - - - - 95317a4e by wurstsalat at 2022-08-15T11:03:39+02:00 fix: BlockingList: Don’t fail with placeholder address
Fixes #11084
- - - - - d6b10432 by wurstsalat at 2022-08-15T11:31:41+02:00 cfix: ChatBanner: Remove unnecessary code from clear method
- - - - - ba2f930b by wurstsalat at 2022-08-15T17:00:53+02:00 imprv: AccountPage: Add button for Ad-Hoc commands
- - - - - 8ac2a1bd by wurstsalat at 2022-08-15T18:44:38+02:00 cfix: Shortcuts window: Adapt to recent changes
- - - - - 47137bbb by wurstsalat at 2022-08-15T19:02:44+02:00 fix: Preview: Reset received size before cancelling or downloading
- - - - - 0698a1d5 by lovetox at 2022-08-15T20:14:32+02:00 fix: MUC: Send password and reason correctly on invite
- - - - - f283525a by lovetox at 2022-08-15T20:14:32+02:00 fix: InfoMessage: Don’t escape text
- - - - - 1063e5b0 by lovetox at 2022-08-15T21:00:30+02:00 change: Rework ChatCommands
ChatCommands have been rewritten completely to be more maintainable.
NOTES: Only following commands are now available
- status - invite - ban - affiliate - kick - role
- - - - -
20 changed files:
- gajim/common/app.py - gajim/common/application.py - + gajim/common/commands.py - gajim/common/modules/contacts.py - gajim/common/modules/muc.py - gajim/common/preview.py - gajim/data/gui/account_page.ui - gajim/data/gui/shortcuts_window.ui - gajim/data/other/shortcuts.json - gajim/gtk/account_page.py - gajim/gtk/blocking.py - gajim/gtk/chat_action_processor.py - gajim/gtk/chat_banner.py - gajim/gtk/chat_stack.py - gajim/gtk/control.py - gajim/gtk/conversation/rows/info.py - gajim/gtk/conversation/rows/muc_subject.py - gajim/gtk/conversation/view.py - gajim/gtk/message_actions_box.py - pyrightconfig.json
Changes:
===================================== gajim/common/app.py ===================================== @@ -63,6 +63,7 @@ from gajim.common.call_manager import CallManager from gajim.common.preview import PreviewManager from gajim.common.task_manager import TaskManager + from gajim.common.commands import ChatCommands
interface = cast(types.InterfaceT, None) @@ -77,6 +78,7 @@ ipython_window = None app = None # type: GajimApplication window = None # type: MainWindow +commands = None # type: ChatCommands
ged = ged_module.GlobalEventsDispatcher() # Global Events Dispatcher plugin_manager = cast(types.PluginManagerT, None) # Plugins Manager
===================================== gajim/common/application.py ===================================== @@ -37,6 +37,7 @@ from gajim.common import configpaths from gajim.common import logging_helpers from gajim.common import passwords +from gajim.common.commands import ChatCommands from gajim.common.dbus import logind from gajim.common.events import AccountDisonnected from gajim.common.events import AllowGajimUpdateCheck @@ -68,6 +69,7 @@ def _init_core(self) -> None: app.settings.init()
app.config = LegacyConfig() + app.commands = ChatCommands()
app.storage.cache = CacheStorage() app.storage.cache.init()
===================================== gajim/common/commands.py ===================================== @@ -0,0 +1,295 @@ +# 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 typing import Any +from typing import NoReturn +from typing import Callable +from typing import Optional + +import io +import argparse +import shlex +import operator + +from nbxmpp.protocol import JID + +from gajim.common import app +from gajim.common.i18n import _ +from gajim.common.helpers import Observable, is_role_change_allowed +from gajim.common.helpers import is_affiliation_change_allowed +from gajim.common.modules.contacts import GroupchatContact + + +def split_argument_string(string: str) -> list[str]: + ''' + Split a string with shlex.split + ''' + + lex = shlex.shlex(string, posix=True) + lex.whitespace_split = True + lex.commenters = '' + result: list[str] = [] + + try: + for token in lex: + result.append(token) + except ValueError: + # If end-of-string is reached and there is a invalid state + # ValueError is raised. Still add the partial token to the result. + result.append(lex.token) + + return result + + +def get_usage_from_command(cmd: argparse.ArgumentParser) -> str: + with io.StringIO() as output: + cmd.print_usage(file=output) + usage = output.getvalue() + + usage = usage.split('[-h] ')[1] + return usage.strip() + + +class ArgumentParserError(Exception): + pass + + +class CommandError(Exception): + pass + + +class CommandNotFoundError(Exception): + pass + + +class ArgumentParser(argparse.ArgumentParser): + def error(self, message: str) -> NoReturn: + raise ArgumentParserError(message) + + +class ChatCommands(Observable): + def __init__(self) -> None: + Observable.__init__(self) + self._parser = ArgumentParser(prog='ChatCommands') + self._sub_parser = self._parser.add_subparsers() + self._commands: dict[str, tuple[list[str], str]] = {} + + self._create_commands() + + def get_commands(self, used_in: str) -> list[tuple[str, str]]: + commands: list[tuple[str, str]] = [] + for cmd_name, cmd in self._commands.items(): + if used_in in cmd[0]: + commands.append((cmd_name, cmd[1])) + commands.sort(key=operator.itemgetter(0)) + return commands + + def make_parser(self, + command_name: str, + callback: Callable[..., Any], + **kwargs: Any) -> ArgumentParser: + + '''Add and return a subparser and initialize + it with the command name. + ''' + + parser = self._sub_parser.add_parser(command_name, **kwargs) + parser.set_defaults(command_name=command_name) + self.connect(command_name, callback) + return parser + + def add_command(self, + command_name: str, + used_in: list[str], + cmd: argparse.ArgumentParser + ) -> None: + + usage = get_usage_from_command(cmd) + self._commands[command_name] = (used_in, usage) + + def parse(self, type_: str, arg_string: str) -> None: + arg_list = split_argument_string(arg_string[1:]) + + command_name = arg_list[0] + command = self._commands.get(command_name) + if command is None or type_ not in command[0]: + raise CommandNotFoundError(_('Unknown command: %s' % command_name)) + + args = self._parser.parse_args(arg_list) + + self.notify(args.command_name, args) + + def _create_commands(self) -> None: + parser = self.make_parser('status', self._status_command) + parser.add_argument('status', + choices=['online', 'away', 'xa', 'dnd']) + parser.add_argument('message', default=None, nargs='?') + self.add_command('status', ['chat', 'groupchat', 'pm'], parser) + + parser = self.make_parser('invite', self._invite_command) + parser.add_argument('address') + parser.add_argument('reason', default=None, nargs='?') + self.add_command('invite', ['groupchat'], parser) + + parser = self.make_parser('ban', self._ban_command) + parser.add_argument('who') + parser.add_argument('reason', default=None, nargs='?') + self.add_command('ban', ['groupchat'], parser) + + parser = self.make_parser('affiliate', self._affiliate_command) + parser.add_argument('who') + parser.add_argument('affiliation', + choices=['owner', 'admin', 'member', 'none']) + self.add_command('affiliate', ['groupchat'], parser) + + parser = self.make_parser('kick', self._kick_command) + parser.add_argument('who') + parser.add_argument('reason', default=None, nargs='?') + self.add_command('kick', ['groupchat'], parser) + + parser = self.make_parser('role', self._role_command) + parser.add_argument('who') + parser.add_argument('role', + choices=['moderator', 'participant', 'visitor']) + self.add_command('role', ['groupchat'], parser) + + def _status_command(self, + chat_commands: Any, + signal_name: str, + args: Any + ) -> None: + + for client in app.get_clients(): + if not app.settings.get_account_setting(client.account, + 'sync_with_global_status'): + continue + + if not client.state.is_available: + continue + + client.change_status(args.status, + args.message or client.status_message) + + def _check_if_joined(self) -> GroupchatContact: + contact = app.window.get_control().contact + assert isinstance(contact, GroupchatContact) + if not contact.is_joined: + raise CommandError(_('You are currently not ' + 'joined this group chat')) + return contact + + def _invite_command(self, + chat_commands: Any, + signal_name: str, + args: Any + ) -> None: + + contact = self._check_if_joined() + + try: + jid = JID.from_string(args.address) + except Exception: + raise CommandError(_('Invalid address: %s' % args.address)) + + if jid.is_full or jid.localpart is None: + raise CommandError(_('Invalid address: %s' % args.address)) + + client = app.get_client(contact.account) + client.get_module('MUC').invite(contact.jid, args.address, args.reason) + + def _change_affiliation(self, + nick_or_address: str, + affiliation: str, + reason: Optional[str]) -> None: + + contact = self._check_if_joined() + + nick_list = contact.get_user_nicknames() + if nick_or_address in nick_list: + participant = contact.get_resource(nick_or_address) + self_contact = contact.get_self() + assert self_contact is not None + if not is_affiliation_change_allowed(self_contact, + participant, + 'outcast'): + raise CommandError(_('You have insufficient permissions')) + + jid = participant.real_jid + + else: + try: + jid = JID.from_string(nick_or_address) + except Exception: + raise CommandError(_('Invalid address: %s' % nick_or_address)) + + if jid.is_full or jid.localpart is None: + raise CommandError(_('Invalid address: %s' % nick_or_address)) + + client = app.get_client(contact.account) + client.get_module('MUC').set_affiliation( + contact.jid, + {jid: {'affiliation': affiliation, + 'reason': reason}}) + + def _ban_command(self, + chat_commands: Any, + signal_name: str, + args: Any + ) -> None: + + self._change_affiliation(args.who, 'outcast', args.reason) + + def _affiliate_command(self, + chat_commands: Any, + signal_name: str, + args: Any + ) -> None: + + self._change_affiliation(args.who, args.affiliation, None) + + def _change_role(self, nick: str, role: str, reason: Optional[str]) -> None: + + contact = self._check_if_joined() + + nick_list = contact.get_user_nicknames() + if nick not in nick_list: + raise CommandError(_('User %s not found' % nick)) + + participant = contact.get_resource(nick) + self_contact = contact.get_self() + assert self_contact is not None + if not is_role_change_allowed(self_contact, participant): + raise CommandError(_('You have insufficient permissions')) + + client = app.get_client(contact.account) + client.get_module('MUC').set_role(contact.jid, + nick, + role, + reason) + + def _kick_command(self, + chat_commands: Any, + signal_name: str, + args: Any + ) -> None: + + self._change_role(args.who, 'none', args.reason) + + def _role_command(self, + chat_commands: Any, + signal_name: str, + args: Any + ) -> None: + + self._change_role(args.who, args.role, None)
===================================== gajim/common/modules/contacts.py ===================================== @@ -308,6 +308,10 @@ def _supports(jid: JID, requested_feature: str) -> bool:
return disco_info.supports(requested_feature)
+ @property + def is_chat(self) -> bool: + return False + @property def is_groupchat(self) -> bool: return False @@ -316,6 +320,10 @@ def is_groupchat(self) -> bool: def is_pm_contact(self) -> bool: return False
+ @property + def type_string(self) -> str: + raise NotImplementedError + @property def is_jingle_available(self) -> bool: return False @@ -558,6 +566,14 @@ def is_jingle_available(self) -> bool: return False return self.is_available
+ @property + def is_chat(self) -> bool: + return True + + @property + def type_string(self) -> str: + return 'chat' +
class ResourceContact(CommonContact): def __init__(self, logger: LogAdapter, jid: JID, account: str) -> None: @@ -791,6 +807,10 @@ def can_notify(self) -> bool: room = self.settings.get('notify_on_all_messages') return all_ or room
+ @property + def type_string(self) -> str: + return 'groupchat' +
class GroupchatParticipant(CommonContact): def __init__(self, logger: LogAdapter, jid: JID, account: str) -> None: @@ -936,6 +956,10 @@ def update_avatar(self, *args: Any) -> None: app.app.avatar_storage.invalidate_cache(self._jid) self.notify('user-avatar-update')
+ @property + def type_string(self) -> str: + return 'pm' +
def can_add_to_roster(contact: Union[BareContact, GroupchatContact,
===================================== gajim/common/modules/muc.py ===================================== @@ -974,7 +974,7 @@ def invite(self, password = self._mucs[str(room)].password self._log.info('Invite %s to %s', jid, room) return self._nbxmpp('MUC').invite( - room, jid, reason, password, continue_, type_) + room, jid, password, reason, continue_, type_)
def _on_client_state_changed(self, _client: types.Client,
===================================== gajim/common/preview.py ===================================== @@ -388,6 +388,7 @@ def _on_thumb_load_finished(data: Optional[bytes], def download_content(self, preview: Preview, force: bool = False) -> None: + preview.reset_received_size() if preview.account is None: # History Window can be opened without account context # This means we can not apply proxy settings @@ -552,5 +553,6 @@ def _on_thumb_write_finished(_result: bool, preview.update_widget(data=pixbuf)
def cancel_download(self, preview: Preview) -> None: + preview.reset_received_size() session = self._get_session(preview.account) session.cancel_message(preview.soup_message, Soup.Status.CANCELLED)
===================================== gajim/data/gui/account_page.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="GtkPaned" id="paned"> @@ -207,7 +207,7 @@ <property name="halign">center</property> <property name="spacing">12</property> <child> - <object class="GtkButton" id="account_settings"> + <object class="GtkButton"> <property name="visible">True</property> <property name="can-focus">True</property> <property name="receives-default">True</property> @@ -228,6 +228,27 @@ <property name="position">0</property> </packing> </child> + <child> + <object class="GtkButton"> + <property name="visible">True</property> + <property name="can-focus">True</property> + <property name="receives-default">True</property> + <property name="tooltip-text" translatable="yes">Ad-Hoc Commands…</property> + <signal name="clicked" handler="_on_adhoc_commands" swapped="no"/> + <child> + <object class="GtkImage"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="icon-name">utilities-terminal-symbolic</property> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> <child> <placeholder/> </child>
===================================== gajim/data/gui/shortcuts_window.ui ===================================== @@ -130,7 +130,7 @@ <child> <object class="GtkShortcutsShortcut"> <property name="visible">1</property> - <property name="accelerator"><primary>h</property> + <property name="accelerator"><primary>f</property> <property name="title" translatable="yes">Open search bar</property> </object> </child> @@ -139,7 +139,7 @@ <child> <object class="GtkShortcutsGroup"> <property name="visible">1</property> - <property name="title" translatable="yes">Tabs</property> + <property name="title" translatable="yes">Chat List</property> <child> <object class="GtkShortcutsShortcut"> <property name="visible">1</property>
===================================== gajim/data/other/shortcuts.json ===================================== @@ -11,7 +11,7 @@ "win.change-nickname": ["<Primary><Shift>N"], "win.change-subject": ["<Primary><Shift>S"], "win.escape": ["Escape"], - "win.send-file": ["<Primary>F"], + "win.search-history": ["<Primary>F"], "win.show-contact-info": ["<Primary>I"], "win.show-emoji-chooser": ["<Primary><Shift>M"], "win.input-clear": ["<Primary>U"], @@ -29,6 +29,5 @@ "win.switch-tab-7": ["<Alt>7", "<Alt>KP_7"], "win.switch-tab-8": ["<Alt>8", "<Alt>KP_8"], "win.switch-tab-9": ["<Alt>9", "<Alt>KP_9"], - "win.search-history": ["<Primary>H"], "win.toggle-chat-list": ["<Primary>R"] }
===================================== gajim/gtk/account_page.py ===================================== @@ -18,6 +18,8 @@ from gi.repository import Gio from gi.repository import Gtk
+from nbxmpp.protocol import JID + from gajim.common import app from gajim.common import ged from gajim.common.const import AvatarSize @@ -103,6 +105,10 @@ def _on_account_settings(self, _button: Gtk.Button) -> None: window = open_window('AccountsWindow') window.select_account(self._account)
+ def _on_adhoc_commands(self, _button: Gtk.Button) -> None: + server_jid = JID.from_string(self._jid).domain + open_window('AdHocCommands', account=self._account, jid=server_jid) + def _on_search_changed(self, widget: Gtk.SearchEntry) -> None: text = widget.get_text().lower() self._roster.set_search_string(text)
===================================== gajim/gtk/blocking.py ===================================== @@ -127,6 +127,9 @@ def _on_save(self, _button: Gtk.Button) -> None:
blocked_jids: set[JID] = set() for item in self._ui.blocking_store: + if not item[0]: + # No address/placeholder + continue blocked_jids.add(JID.from_string(item[0].lower()))
unblock_jids = self._prev_blocked_jids - blocked_jids
===================================== gajim/gtk/chat_action_processor.py ===================================== @@ -121,17 +121,9 @@ def _replace_text(self, selected_action: str) -> None: self._buf.delete(start_iter, self._current_iter) self._buf.insert(start_iter, selected_action)
- def _get_commands(self) -> list[str]: - # TODO - # commands: list[str] = [] - # assert self._account - # assert self._contact - # control = app.window.get_control() - # for command in control.list_commands(): - # for name in command.names: - # commands.append(name) - # return commands - return [] + def _get_commands(self) -> list[tuple[str, str]]: + assert self._contact is not None + return app.commands.get_commands(self._contact.type_string)
def _on_changed(self, _textview: MessageInputTextView) -> None: insert = self._buf.get_insert() @@ -181,7 +173,7 @@ def _update_commands_menu(self, self._menu.remove_all() command_list = self._get_commands() num_entries = 0 - for command in command_list: + for command, usage in command_list: if not command.startswith(action_text[1:]): continue if num_entries >= MAX_ENTRIES: @@ -189,7 +181,7 @@ def _update_commands_menu(self,
action_data = GLib.Variant('s', f'/{command}') menu_item = Gio.MenuItem() - menu_item.set_label(f'/{command}') + menu_item.set_label(f'/{command} {usage}') menu_item.set_attribute_value('action-data', action_data) self._menu.append_item(menu_item) num_entries += 1
===================================== gajim/gtk/chat_banner.py ===================================== @@ -78,9 +78,6 @@ def clear(self) -> None:
self.unregister_events()
- self._contact = None - self._client = None - def switch_contact(self, contact: types.ChatContactT) -> None: self._update_account_badge(contact.account)
===================================== gajim/gtk/chat_stack.py ===================================== @@ -76,6 +76,10 @@ def __init__(self): self._chat_banner = ChatBanner() self._chat_control = ChatControl() self._message_action_box = MessageActionsBox() + self._message_action_box.connect('command-error', + self._on_command_error) + self._message_action_box.connect('command-not-found', + self._on_command_not_found)
box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) box.add(self._chat_banner) @@ -659,6 +663,9 @@ def _on_function_message(self, def _on_send_message(self) -> None: self._message_action_box.msg_textview.replace_emojis() message = self._message_action_box.msg_textview.get_text() + if message.startswith('//'): + # Escape sequence for chat commands + message = message[1:]
contact = self._current_contact assert contact is not None @@ -683,9 +690,6 @@ def _on_send_message(self) -> None: if message in ('', None, '\n'): return
- # if process_commands and self.process_as_command(message): - # return - label = self._message_action_box.get_seclabel()
correct_id = None @@ -734,6 +738,19 @@ def clear(self) -> None: self._message_action_box.clear() self._chat_control.clear()
+ def _on_command_error(self, + message_actions_box: MessageActionsBox, + error: str) -> None: + + self._chat_control.add_info_message(error) + + def _on_command_not_found(self, + message_actions_box: MessageActionsBox, + error: str + ) -> None: + + self._chat_control.add_info_message(error) +
class ChatPlaceholderBox(Gtk.Box): def __init__(self):
===================================== gajim/gtk/control.py ===================================== @@ -19,6 +19,9 @@ from typing import Union from typing import cast
+from collections import defaultdict +from collections import deque +from functools import partial import os import logging import time @@ -99,7 +102,14 @@ def __init__(self) -> None: # XEP-0333 Chat Markers self.last_msg_id: Optional[str] = None
- self._subject_text_cache: dict[JID, str] = {} + self._muc_subjects: dict[ + types.ChatContactT, tuple[float, MucSubject]] = {} + + # Store 100 info messages per contact on a FIFO basis + self._info_messages: dict[ + types.ChatContactT, + deque[tuple[float, str]]] = defaultdict( + partial(deque, maxlen=100)) # pyright: ignore
self.widget = cast(Gtk.Box, self._ui.get_object('control_box')) self.widget.show_all() @@ -202,6 +212,17 @@ def switch_contact(self, contact: Union[BareContact, for transfer in transfers: self.add_file_transfer(transfer)
+ for timestamp, message in self._info_messages[contact]: + self.add_info_message(message, timestamp) + + if isinstance(contact, GroupchatContact): + if (app.settings.get('show_subject_on_join') or + not contact.is_joining): + timestamp, subject = self._muc_subjects.get( + contact, (None, None)) + if subject is not None: + self.conversation_view.add_muc_subject(subject, timestamp) + def _register_events(self) -> None: if self.has_events_registered(): return @@ -478,15 +499,24 @@ def get_our_nick(self) -> str: if isinstance(self.contact, GroupchatParticipant): muc_data = self.client.get_module('MUC').get_muc_data( self.contact.jid.bare) - return muc_data.nick + if muc_data is not None: + return muc_data.nick
return app.nicks[self.contact.account]
def _allow_add_message(self) -> bool: return self._scrolled_view.get_lower_complete()
- def add_info_message(self, text: str) -> None: - self.conversation_view.add_info_message(text) + def add_info_message(self, + text: str, + timestamp: Optional[float] = None + ) -> None: + + if timestamp is None: + assert self._contact is not None + self._info_messages[self._contact].append((time.time(), text)) + + self.conversation_view.add_info_message(text, timestamp)
def add_file_transfer(self, transfer: HTTPFileTransfer) -> None: self.conversation_view.add_file_transfer(transfer) @@ -814,11 +844,12 @@ def _on_room_subject(self, if subject is None: return
- if self._subject_text_cache.get(contact.jid) == subject.text: + _timestamp, old_subject = self._muc_subjects.get(contact, (None, None)) + if old_subject is not None and old_subject.text == subject.text: # Probably a rejoin, we already showed that subject return
- self._subject_text_cache[contact.jid] = subject.text + self._muc_subjects[contact] = (time.time(), subject)
if (app.settings.get('show_subject_on_join') or not contact.is_joining):
===================================== gajim/gtk/conversation/rows/info.py ===================================== @@ -12,10 +12,13 @@ # 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 Optional + import time from datetime import datetime
-from gi.repository import GLib from gi.repository import Gtk
from gajim.common.const import AvatarSize @@ -25,15 +28,18 @@
class InfoMessage(BaseRow): - def __init__(self, account: str, text: str) -> None: + def __init__(self, + account: str, + text: str, + timestamp: Optional[float] + ) -> None: + BaseRow.__init__(self, account)
self.type = 'info' - timestamp = time.time() - self.timestamp = datetime.fromtimestamp(timestamp) - self.db_timestamp = timestamp - - text = GLib.markup_escape_text(text) + current_timestamp = timestamp or time.time() + self.timestamp = datetime.fromtimestamp(current_timestamp) + self.db_timestamp = current_timestamp
avatar_placeholder = Gtk.Box() avatar_placeholder.set_size_request(AvatarSize.ROSTER, -1)
===================================== gajim/gtk/conversation/rows/muc_subject.py ===================================== @@ -12,6 +12,10 @@ # 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 Optional + import time from datetime import datetime
@@ -29,12 +33,17 @@ class MUCSubject(BaseRow):
type = 'muc-subject'
- def __init__(self, account: str, subject: MucSubject) -> None: + def __init__(self, + account: str, + subject: MucSubject, + timestamp: Optional[float] = None + ) -> None: + BaseRow.__init__(self, account)
- timestamp = time.time() - self.timestamp = datetime.fromtimestamp(timestamp) - self.db_timestamp = timestamp + current_timestamp = timestamp or time.time() + self.timestamp = datetime.fromtimestamp(current_timestamp) + self.db_timestamp = current_timestamp
self.grid.set_halign(Gtk.Align.START)
===================================== gajim/gtk/conversation/view.py ===================================== @@ -130,6 +130,8 @@ def get_row_at_index(self, index: int) -> BaseRow:
def reset(self) -> None: for row in self.get_children()[2:]: + if isinstance(row, ReadMarkerRow): + continue row.destroy()
self._row_count = 0 @@ -161,8 +163,12 @@ def _sort_func(row1: BaseRow, row2: BaseRow) -> int: return 0 return -1 if row1.timestamp < row2.timestamp else 1
- def add_muc_subject(self, subject: MucSubject) -> None: - muc_subject = MUCSubject(self.contact.account, subject) + def add_muc_subject(self, + subject: MucSubject, + timestamp: Optional[float] = None + ) -> None: + + muc_subject = MUCSubject(self.contact.account, subject, timestamp) self._insert_message(muc_subject)
def add_muc_user_left(self, @@ -192,8 +198,12 @@ def add_user_status(self, name: str, show: str, status: str) -> None: user_status = UserStatus(self.contact.account, name, show, status) self._insert_message(user_status)
- def add_info_message(self, text: str) -> None: - message = InfoMessage(self.contact.account, text) + def add_info_message(self, + text: str, + timestamp: Optional[float] = None + ) -> None: + + message = InfoMessage(self.contact.account, text, timestamp) self._insert_message(message)
def add_file_transfer(self, transfer: HTTPFileTransfer) -> None:
===================================== gajim/gtk/message_actions_box.py ===================================== @@ -38,6 +38,9 @@ from gajim.common import events from gajim.common import i18n from gajim.common import ged +from gajim.common.commands import ArgumentParserError +from gajim.common.commands import CommandError +from gajim.common.commands import CommandNotFoundError from gajim.common.i18n import _ from gajim.common.client import Client from gajim.common.const import SimpleClientState @@ -67,6 +70,12 @@
class MessageActionsBox(Gtk.Grid, ged.EventHelper): + + __gsignals__ = { + 'command-error': (GObject.SignalFlags.RUN_FIRST, None, (str,)), + 'command-not-found': (GObject.SignalFlags.RUN_FIRST, None, (str,)) + } + def __init__(self) -> None: Gtk.Grid.__init__(self) ged.EventHelper.__init__(self) @@ -595,6 +604,21 @@ def _on_msg_textview_key_press_event(self, return True
assert self._contact is not None + + message = self.msg_textview.get_text() + if (message.startswith('/') and + not message.startswith('//') and + not message.startswith('/me ')): + try: + app.commands.parse(self._contact.type_string, message) + except (ArgumentParserError, CommandError) as error: + self.emit('command-error', error) + except CommandNotFoundError as error: + self.emit('command-not-found', error) + else: + self.msg_textview.clear() + return True + if not app.account_is_available(self._contact.account): # we are not connected ErrorDialog(
===================================== pyrightconfig.json ===================================== @@ -22,6 +22,7 @@ "gajim/common/application.py", "gajim/common/call_manager.py", "gajim/common/cert_store.py", + "gajim/common/commands.py", "gajim/common/configpaths.py", "gajim/common/const.py", "gajim/common/events.py",
View it on GitLab: https://dev.gajim.org/gajim/gajim/-/compare/a8fa42d2c37823f771ef38fa72e8b210...