Welcome to mirror list, hosted at ThFree Co, Russian Federation.

dev.gajim.org/gajim/gajim.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--gajim/command_system/implementation/middleware.py8
-rw-r--r--gajim/common/const.py5
-rw-r--r--gajim/common/modules/httpupload.py3
-rw-r--r--gajim/common/modules/muc.py22
-rw-r--r--gajim/common/setting_values.py2
-rw-r--r--gajim/common/storage/archive.py28
-rw-r--r--gajim/common/structs.py14
-rw-r--r--gajim/data/gui/chat_banner.ui274
-rw-r--r--gajim/data/gui/chat_control.ui477
-rw-r--r--gajim/data/gui/chat_paned.ui3
-rw-r--r--gajim/data/gui/groupchat_control.ui1700
-rw-r--r--gajim/data/gui/main.ui2
-rw-r--r--gajim/data/gui/message_actions_box.ui249
-rw-r--r--gajim/data/gui/shortcuts_window.ui28
-rw-r--r--gajim/data/other/shortcuts.json2
-rw-r--r--gajim/data/style/gajim.css6
-rw-r--r--gajim/gtk/account_page.py2
-rw-r--r--gajim/gtk/accounts.py2
-rw-r--r--gajim/gtk/application.py2
-rw-r--r--gajim/gtk/builder.pyi118
-rw-r--r--gajim/gtk/chat_action_processor.py32
-rw-r--r--gajim/gtk/chat_banner.py319
-rw-r--r--gajim/gtk/chat_function_page.py419
-rw-r--r--gajim/gtk/chat_list_stack.py2
-rw-r--r--gajim/gtk/chat_page.py3
-rw-r--r--gajim/gtk/chat_stack.py629
-rw-r--r--gajim/gtk/const.py40
-rw-r--r--gajim/gtk/control_stack.py54
-rw-r--r--gajim/gtk/controls/base.py1013
-rw-r--r--gajim/gtk/controls/chat.py428
-rw-r--r--gajim/gtk/controls/groupchat.py839
-rw-r--r--gajim/gtk/controls/private.py83
-rw-r--r--gajim/gtk/conversation/jump_to_end_button.py1
-rw-r--r--gajim/gtk/conversation/rows/message.py23
-rw-r--r--gajim/gtk/conversation/rows/widgets.py51
-rw-r--r--gajim/gtk/conversation/view.py32
-rw-r--r--gajim/gtk/groupchat_details.py5
-rw-r--r--gajim/gtk/groupchat_manage.py15
-rw-r--r--gajim/gtk/groupchat_nick_completion.py98
-rw-r--r--gajim/gtk/groupchat_roster.py1
-rw-r--r--gajim/gtk/main.py98
-rw-r--r--gajim/gtk/menus.py132
-rw-r--r--gajim/gtk/message_actions_box.py698
-rw-r--r--gajim/gtk/message_input.py87
-rw-r--r--gajim/gtk/notification_manager.py2
-rw-r--r--gajim/gtk/roster.py95
-rw-r--r--gajim/gtk/roster_item_exchange.py5
-rw-r--r--gajim/gtk/security_label_selector.py50
-rw-r--r--gajim/gtk/util.py13
-rw-r--r--gajim/gui_interface.py67
50 files changed, 3359 insertions, 4922 deletions
diff --git a/gajim/command_system/implementation/middleware.py b/gajim/command_system/implementation/middleware.py
index d03dc3a37..d1f9bb9c5 100644
--- a/gajim/command_system/implementation/middleware.py
+++ b/gajim/command_system/implementation/middleware.py
@@ -56,7 +56,6 @@ class ChatCommandProcessor(CommandProcessor):
parents = super(ChatCommandProcessor, self)
flag = parents.process_as_command(text)
if flag and self.command_succeeded:
- self.add_history(text)
self.clear_input()
return flag
@@ -147,13 +146,6 @@ class CommandTools:
"""
self.set_input(str())
- def add_history(self, text):
- """
- Add given text to the input history, so user can scroll through
- it using ctrl + up/down arrow keys.
- """
- self.save_message(text, 'sent')
-
@property
def connection(self):
"""
diff --git a/gajim/common/const.py b/gajim/common/const.py
index 15feaffe0..b92962d9d 100644
--- a/gajim/common/const.py
+++ b/gajim/common/const.py
@@ -246,6 +246,7 @@ class MUCJoinedState(Enum):
CREATING = 'creating'
CAPTCHA_REQUEST = 'captcha in progress'
CAPTCHA_FAILED = 'captcha failed'
+ PASSWORD_REQUEST = 'password request'
def __str__(self):
return self.name
@@ -274,6 +275,10 @@ class MUCJoinedState(Enum):
def is_captcha_failed(self):
return self == MUCJoinedState.CAPTCHA_FAILED
+ @property
+ def is_password_request(self):
+ return self == MUCJoinedState.PASSWORD_REQUEST
+
class ClientState(IntEnum):
DISCONNECTING = 0
diff --git a/gajim/common/modules/httpupload.py b/gajim/common/modules/httpupload.py
index a0bbc3052..8114eaa16 100644
--- a/gajim/common/modules/httpupload.py
+++ b/gajim/common/modules/httpupload.py
@@ -95,9 +95,6 @@ class HTTPUpload(BaseModule):
GLib.FormatSizeFlags.IEC_UNITS)
self._log.info('Component has a maximum file size of: %s', size)
- for ctrl in app.window.get_controls(account=self._account):
- ctrl.update_actions()
-
def make_transfer(self,
path: str,
encryption: Optional[str],
diff --git a/gajim/common/modules/muc.py b/gajim/common/modules/muc.py
index 2a8a94ac3..36ccefd34 100644
--- a/gajim/common/modules/muc.py
+++ b/gajim/common/modules/muc.py
@@ -34,6 +34,7 @@ from nbxmpp.namespaces import Namespace
from nbxmpp.protocol import JID
from nbxmpp.protocol import Message
from nbxmpp.protocol import Presence
+from nbxmpp.structs import CommonError
from nbxmpp.structs import DiscoInfo
from nbxmpp.structs import MessageProperties
from nbxmpp.structs import PresenceProperties
@@ -267,7 +268,8 @@ class MUC(BaseModule):
# join a different nickname, so update MUCData here.
muc_data.nick = nick
- if not muc_data.state.is_not_joined:
+ if muc_data.state not in (MUCJoinedState.NOT_JOINED,
+ MUCJoinedState.PASSWORD_REQUEST):
self._log.warning('Can’t join MUC %s, state: %s',
jid, muc_data.state)
return
@@ -503,21 +505,31 @@ class MUC(BaseModule):
elif properties.error.condition == 'not-authorized':
self._remove_rejoin_timeout(room_jid)
- self._set_muc_state(room_jid, MUCJoinedState.NOT_JOINED)
+ self._set_muc_state(room_jid, MUCJoinedState.PASSWORD_REQUEST)
room.notify('room-password-required', properties)
else:
self._set_muc_state(room_jid, MUCJoinedState.NOT_JOINED)
if room_jid not in self._rejoin_muc:
+ muc_data.error = 'join-failed'
+ assert isinstance(properties.error, CommonError)
+ muc_data.error_text = helpers.to_user_string(
+ properties.error)
room.notify('room-join-failed', properties.error)
elif muc_data.state == MUCJoinedState.CREATING:
self._set_muc_state(room_jid, MUCJoinedState.NOT_JOINED)
+ muc_data.error = 'creation-failed'
+ assert isinstance(properties.error, CommonError)
+ muc_data.error_text = helpers.to_user_string(properties.error)
room.notify('room-creation-failed', properties)
elif muc_data.state == MUCJoinedState.CAPTCHA_REQUEST:
self._set_muc_state(room_jid, MUCJoinedState.CAPTCHA_FAILED)
self._set_muc_state(room_jid, MUCJoinedState.NOT_JOINED)
+ muc_data.error = 'captcha-failed'
+ assert isinstance(properties.error, CommonError)
+ muc_data.error_text = helpers.to_user_string(properties.error)
room.notify('room-captcha-error', properties.error)
elif muc_data.state == MUCJoinedState.CAPTCHA_FAILED:
@@ -767,6 +779,9 @@ class MUC(BaseModule):
room = self._get_contact(room_jid)
room.notify('room-joined')
+ def cancel_password_request(self, room_jid: str) -> None:
+ self._set_muc_state(room_jid, MUCJoinedState.NOT_JOINED)
+
def _room_join_complete(self, muc_data: MUCData):
self._remove_join_timeout(muc_data.jid)
self._set_muc_state(muc_data.jid, MUCJoinedState.JOINED)
@@ -820,8 +835,11 @@ class MUC(BaseModule):
return
self._log.info('Captcha challenge received from %s', properties.jid)
+
+ assert properties.captcha is not None
store_bob_data(properties.captcha.bob_data)
muc_data.captcha_id = properties.id
+ muc_data.captcha_form = properties.captcha.form
self._set_muc_state(properties.jid, MUCJoinedState.CAPTCHA_REQUEST)
self._remove_rejoin_timeout(properties.jid)
diff --git a/gajim/common/setting_values.py b/gajim/common/setting_values.py
index 74d49fae0..ccf14f1da 100644
--- a/gajim/common/setting_values.py
+++ b/gajim/common/setting_values.py
@@ -385,13 +385,13 @@ BoolGroupChatSettings = Literal[
'notify_on_all_messages',
'print_join_left',
'print_status',
- 'send_chatstate',
'send_marker',
]
StringGroupChatSettings = Literal[
'encryption',
'speller_language',
+ 'send_chatstate',
]
IntGroupChatSettings = Literal[
diff --git a/gajim/common/storage/archive.py b/gajim/common/storage/archive.py
index e3dac697e..bf8961911 100644
--- a/gajim/common/storage/archive.py
+++ b/gajim/common/storage/archive.py
@@ -993,6 +993,34 @@ class MessageArchiveStorage(SqliteStorage):
return False
@timeit
+ def get_last_correctable_message(self,
+ account: str,
+ jid: JID,
+ message_id: str
+ ) -> Optional[LastConversationRow]:
+ '''
+ Load the last correctable message of a conversation by message_id.
+ Conditions: max 5 min old
+ '''
+ jids = [jid]
+ account_id = self.get_account_id(account)
+ min_time = time.time() - 5 * 60
+
+ sql = '''
+ SELECT contact_name, time, kind, message, stanza_id, message_id,
+ additional_data
+ FROM logs NATURAL JOIN jids WHERE jid IN ({jids})
+ AND account_id = {account_id}
+ AND message_id = ?
+ AND time > ?
+ '''.format(jids=', '.join('?' * len(jids)),
+ account_id=account_id)
+
+ return self._con.execute(
+ sql,
+ tuple(jids) + (message_id, min_time)).fetchone()
+
+ @timeit
def store_message_correction(self,
account: str,
jid: JID,
diff --git a/gajim/common/structs.py b/gajim/common/structs.py
index 8b0a43bef..a02a99334 100644
--- a/gajim/common/structs.py
+++ b/gajim/common/structs.py
@@ -31,7 +31,10 @@ from nbxmpp.const import Affiliation
from nbxmpp.const import Chatstate
from nbxmpp.const import PresenceShow
from nbxmpp.const import Role
+from nbxmpp.modules.dataforms import SimpleDataForm
+from nbxmpp.modules.security_labels import SecurityLabel
from nbxmpp.protocol import JID
+from nbxmpp.structs import MucSubject
from nbxmpp.structs import PresenceProperties
from gajim.common import types
@@ -55,7 +58,7 @@ class MUCData:
def __init__(self,
room_jid: str,
nick: str,
- password: str,
+ password: Optional[str],
config: Optional[dict[str, Any]] = None
) -> None:
@@ -65,8 +68,11 @@ class MUCData:
self.password = password
self.state = MUCJoinedState.NOT_JOINED
# Message id of the captcha challenge
- self.captcha_id = None
- self.subject = None
+ self.captcha_id: Optional[str] = None
+ self.captcha_form: Optional[SimpleDataForm] = None
+ self.error: Optional[str] = None
+ self.error_text: Optional[str] = None
+ self.subject: Optional[MucSubject] = None
@property
def jid(self) -> JID:
@@ -92,7 +98,7 @@ class OutgoingMessage:
marker: Optional[tuple[str, str]] = None,
resource: Optional[str] = None,
user_nick: Optional[str] = None,
- label: Optional[str] = None,
+ label: Optional[SecurityLabel] = None,
control: Optional[Any] = None,
attention: Optional[bool] = None,
correct_id: Optional[str] = None,
diff --git a/gajim/data/gui/chat_banner.ui b/gajim/data/gui/chat_banner.ui
new file mode 100644
index 000000000..953d5661a
--- /dev/null
+++ b/gajim/data/gui/chat_banner.ui
@@ -0,0 +1,274 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Generated with glade 3.38.2 -->
+<interface>
+ <requires lib="gtk+" version="3.24"/>
+ <object class="GtkBox" id="banner_box">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="spacing">12</property>
+ <child>
+ <object class="GtkImage" id="avatar_image">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="spacing">12</property>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="valign">center</property>
+ <property name="orientation">vertical</property>
+ <property name="spacing">3</property>
+ <child>
+ <object class="GtkLabel" id="name_label">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="halign">start</property>
+ <property name="hexpand">True</property>
+ <property name="ellipsize">end</property>
+ <property name="single-line-mode">True</property>
+ <property name="xalign">0</property>
+ <style>
+ <class name="large-header"/>
+ </style>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkImage" id="phone_image">
+ <property name="can-focus">False</property>
+ <property name="no-show-all">True</property>
+ <property name="tooltip-text" translatable="yes">The last message was written on a mobile client</property>
+ <property name="halign">start</property>
+ <property name="margin-end">4</property>
+ <property name="icon-name">phone-apple-iphone-symbolic</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton" id="toggle_roster_button">
+ <property name="can-focus">True</property>
+ <property name="receives-default">True</property>
+ <property name="no-show-all">True</property>
+ <property name="tooltip-text" translatable="yes">Toggle participants list</property>
+ <property name="valign">center</property>
+ <property name="relief">none</property>
+ <signal name="clicked" handler="_on_toggle_roster_clicked" swapped="no"/>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="spacing">3</property>
+ <child>
+ <object class="GtkImage">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="icon-name">system-users-symbolic</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkImage" id="toggle_roster_image">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="icon-name">go-previous-symbolic</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ </child>
+ <style>
+ <class name="flat"/>
+ </style>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="pack-type">end</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="account_badge_box">
+ <property name="can-focus">False</property>
+ <property name="no-show-all">True</property>
+ <property name="halign">end</property>
+ <property name="valign">center</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <placeholder/>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="pack-type">end</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="visitor_box">
+ <property name="can-focus">False</property>
+ <property name="no-show-all">True</property>
+ <property name="spacing">12</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="label" translatable="yes">You are a visitor</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkMenuButton" id="visitor_menu_button">
+ <property name="visible">True</property>
+ <property name="can-focus">True</property>
+ <property name="focus-on-click">False</property>
+ <property name="receives-default">True</property>
+ <property name="tooltip-text" translatable="yes">What does this mean?</property>
+ <property name="valign">center</property>
+ <property name="popover">visitor_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>
+ <style>
+ <class name="suggested-action"/>
+ </style>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="pack-type">end</property>
+ <property name="position">3</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <style>
+ <class name="gajim-banner"/>
+ <class name="padding-6"/>
+ </style>
+ </object>
+ <object class="GtkPopover" id="visitor_popover">
+ <property name="can-focus">False</property>
+ <property name="relative-to">visitor_menu_button</property>
+ <property name="position">bottom</property>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="orientation">vertical</property>
+ <property name="spacing">6</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="label" translatable="yes">You are a visitor</property>
+ <style>
+ <class name="bold"/>
+ </style>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="label" translatable="yes">In order to write messages in this chat, you need to request voice first.
+A moderator will process your request.</property>
+ <property name="wrap">True</property>
+ <property name="max-width-chars">30</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton">
+ <property name="label" translatable="yes">_Request</property>
+ <property name="visible">True</property>
+ <property name="can-focus">True</property>
+ <property name="receives-default">False</property>
+ <property name="halign">center</property>
+ <property name="use-underline">True</property>
+ <signal name="clicked" handler="_on_request_voice_clicked" swapped="no"/>
+ <style>
+ <class name="suggested-action"/>
+ </style>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="pack-type">end</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ <style>
+ <class name="padding-12"/>
+ </style>
+ </object>
+ </child>
+ </object>
+</interface>
diff --git a/gajim/data/gui/chat_control.ui b/gajim/data/gui/chat_control.ui
index 9312b9e1c..3e8f18905 100644
--- a/gajim/data/gui/chat_control.ui
+++ b/gajim/data/gui/chat_control.ui
@@ -2,485 +2,34 @@
<!-- Generated with glade 3.38.2 -->
<interface>
<requires lib="gtk+" version="3.24"/>
- <object class="GtkBox" id="drop_area">
- <property name="name">DropArea</property>
- <property name="visible">True</property>
- <property name="can-focus">False</property>
- <property name="no-show-all">True</property>
- <property name="opacity">0.90</property>
- <property name="hexpand">True</property>
- <property name="vexpand">True</property>
- <property name="orientation">vertical</property>
- <property name="spacing">18</property>
- <child>
- <object class="GtkImage">
- <property name="visible">True</property>
- <property name="can-focus">False</property>
- <property name="valign">end</property>
- <property name="vexpand">True</property>
- <property name="icon-name">mail-attachment-symbolic</property>
- <property name="icon_size">6</property>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">True</property>
- <property name="position">0</property>
- </packing>
- </child>
- <child>
- <object class="GtkLabel">
- <property name="visible">True</property>
- <property name="can-focus">False</property>
- <property name="valign">start</property>
- <property name="vexpand">True</property>
- <property name="label" translatable="yes">Drop files or contacts</property>
- <property name="wrap">True</property>
- <property name="max-width-chars">40</property>
- <style>
- <class name="bold16"/>
- </style>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">True</property>
- <property name="position">1</property>
- </packing>
- </child>
- </object>
- <object class="GtkMenu" id="formattings_menu">
- <property name="visible">True</property>
- <property name="can-focus">False</property>
- <child>
- <object class="GtkMenuItem" id="bold">
- <property name="name">bold</property>
- <property name="visible">True</property>
- <property name="can-focus">False</property>
- <property name="label" translatable="yes">Bold</property>
- <signal name="activate" handler="_on_formatting_menuitem_activate" swapped="no"/>
- </object>
- </child>
- <child>
- <object class="GtkMenuItem" id="italic">
- <property name="name">italic</property>
- <property name="visible">True</property>
- <property name="can-focus">False</property>
- <property name="label" translatable="yes">Italic</property>
- <signal name="activate" handler="_on_formatting_menuitem_activate" swapped="no"/>
- </object>
- </child>
- <child>
- <object class="GtkMenuItem" id="strike">
- <property name="name">strike</property>
- <property name="visible">True</property>
- <property name="can-focus">False</property>
- <property name="label" translatable="yes">Strike</property>
- <signal name="activate" handler="_on_formatting_menuitem_activate" swapped="no"/>
- </object>
- </child>
- </object>
- <object class="GtkBox" id="chat_control_hbox">
+ <object class="GtkBox" id="control_box">
<property name="can-focus">True</property>
- <property name="margin-start">7</property>
- <property name="margin-end">7</property>
- <property name="margin-top">5</property>
- <property name="margin-bottom">7</property>
- <property name="spacing">1</property>
<child>
- <object class="GtkOverlay" id="overlay">
- <property name="visible">True</property>
- <property name="can-focus">False</property>
+ <object class="GtkBox" id="conv_view_box">
+ <property name="can-focus">True</property>
+ <property name="margin-start">7</property>
+ <property name="margin-end">7</property>
+ <property name="spacing">3</property>
<child>
- <object class="GtkBox" id="textview_box">
+ <object class="GtkOverlay" id="conv_view_overlay">
<property name="visible">True</property>
<property name="can-focus">False</property>
- <property name="orientation">vertical</property>
- <child>
- <object class="GtkEventBox" id="banner_eventbox">
- <property name="name">ChatControl-BannerEventBox</property>
- <property name="can-focus">False</property>
- <property name="no-show-all">True</property>
- <child>
- <object class="GtkBox">
- <property name="visible">True</property>
- <property name="can-focus">False</property>
- <property name="spacing">6</property>
- <child>
- <object class="GtkEventBox" id="avatar_eventbox">
- <property name="visible">True</property>
- <property name="can-focus">False</property>
- <property name="visible-window">False</property>
- <child>
- <object class="GtkImage" id="avatar_image">
- <property name="width-request">48</property>
- <property name="height-request">48</property>
- <property name="visible">True</property>
- <property name="can-focus">False</property>
- </object>
- </child>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">False</property>
- <property name="position">0</property>
- </packing>
- </child>
- <child>
- <!-- n-columns=3 n-rows=3 -->
- <object class="GtkGrid">
- <property name="visible">True</property>
- <property name="can-focus">False</property>
- <property name="valign">center</property>
- <property name="hexpand">True</property>
- <property name="row-spacing">3</property>
- <property name="column-spacing">3</property>
- <child>
- <object class="GtkLabel" id="banner_name_label">
- <property name="name">ChatControl-BannerNameLabel</property>
- <property name="visible">True</property>
- <property name="can-focus">False</property>
- <property name="halign">start</property>
- <property name="hexpand">True</property>
- <property name="label">Contact name</property>
- <property name="ellipsize">end</property>
- <property name="xalign">0</property>
- <style>
- <class name="large-header"/>
- </style>
- </object>
- <packing>
- <property name="left-attach">0</property>
- <property name="top-attach">0</property>
- <property name="width">2</property>
- </packing>
- </child>
- <child>
- <object class="GtkLabel" id="banner_label">
- <property name="name">ChatControl-BannerLabel</property>
- <property name="visible">True</property>
- <property name="can-focus">False</property>
- <property name="halign">start</property>
- <property name="hexpand">True</property>
- <property name="label">label</property>
- <property name="selectable">True</property>
- <property name="ellipsize">end</property>
- <property name="xalign">0</property>
- </object>
- <packing>
- <property name="left-attach">1</property>
- <property name="top-attach">1</property>
- </packing>
- </child>
- <child>
- <object class="GtkImage" id="phone_image">
- <property name="can-focus">False</property>
- <property name="no-show-all">True</property>
- <property name="tooltip-text" translatable="yes">The last message was written on a mobile client</property>
- <property name="halign">start</property>
- <property name="margin-end">4</property>
- <property name="icon-name">phone-apple-iphone-symbolic</property>
- </object>
- <packing>
- <property name="left-attach">0</property>
- <property name="top-attach">1</property>
- </packing>
- </child>
- <child>
- <object class="GtkBox" id="account_badge_box">
- <property name="can-focus">False</property>
- <property name="no-show-all">True</property>
- <property name="halign">end</property>
- <property name="valign">center</property>
- <property name="orientation">vertical</property>
- <child>
- <placeholder/>
- </child>
- </object>
- <packing>
- <property name="left-attach">2</property>
- <property name="top-attach">0</property>
- </packing>
- </child>
- <child>
- <placeholder/>
- </child>
- <child>
- <placeholder/>
- </child>
- <child>
- <placeholder/>
- </child>
- <child>
- <placeholder/>
- </child>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">True</property>
- <property name="position">1</property>
- </packing>
- </child>
- <child>
- <placeholder/>
- </child>
- </object>
- </child>
- <style>
- <class name="gajim-banner"/>
- </style>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">True</property>
- <property name="position">0</property>
- </packing>
- </child>
- <child>
- <object class="GtkSeparator">
- <property name="visible">True</property>
- <property name="can-focus">False</property>
- <style>
- <class name="chatcontrol-separator-top"/>
- </style>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">True</property>
- <property name="position">1</property>
- </packing>
- </child>
- <child>
- <object class="GtkOverlay" id="conv_view_overlay">
- <property name="visible">True</property>
- <property name="can-focus">False</property>
- <child>
- <placeholder/>
- </child>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">True</property>
- <property name="position">2</property>
- </packing>
- </child>
- <child>
- <object class="GtkSeparator">
- <property name="visible">True</property>
- <property name="can-focus">False</property>
- <style>
- <class name="chatcontrol-separator"/>
- </style>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">True</property>
- <property name="position">3</property>
- </packing>
- </child>
+ <property name="hexpand">True</property>
<child>
- <object class="GtkBox" id="hbox">
- <property name="visible">True</property>
- <property name="can-focus">False</property>
- <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
- <child>
- <object class="GtkMenuButton" id="emoticons_button">
- <property name="visible">True</property>
- <property name="can-focus">True</property>
- <property name="receives-default">True</property>
- <property name="tooltip-text" translatable="yes">Show a list of emojis</property>
- <property name="relief">none</property>
- <child>
- <object class="GtkImage">
- <property name="visible">True</property>
- <property name="can-focus">False</property>
- <property name="icon-name">face-smile-symbolic</property>
- </object>
- </child>
- <style>
- <class name="chatcontrol-actionbar-button"/>
- </style>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">True</property>
- <property name="position">0</property>
- </packing>
- </child>
- <child>
- <object class="GtkMenuButton" id="formattings_button">
- <property name="visible">True</property>
- <property name="can-focus">True</property>
- <property name="focus-on-click">False</property>
- <property name="receives-default">True</property>
- <property name="tooltip-text" translatable="yes">Format your message…</property>
- <property name="relief">none</property>
- <property name="popup">formattings_menu</property>
- <property name="direction">up</property>
- <child>
- <object class="GtkImage">
- <property name="visible">True</property>
- <property name="can-focus">False</property>
- <property name="icon-name">format-text-bold-symbolic</property>
- <property name="icon_size">1</property>
- </object>
- </child>
- <style>
- <class name="chatcontrol-actionbar-button"/>
- </style>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">True</property>
- <property name="position">1</property>
- </packing>
- </child>
- <child>
- <placeholder/>
- </child>
- <child>
- <object class="GtkMenuButton" id="settings_menu">
- <property name="visible">True</property>
- <property name="can-focus">True</property>
- <property name="receives-default">True</property>
- <property name="relief">none</property>
- <child>
- <object class="GtkImage">
- <property name="visible">True</property>
- <property name="can-focus">False</property>
- <property name="icon-name">open-menu-symbolic</property>
- </object>
- </child>
- <style>
- <class name="chatcontrol-actionbar-button"/>
- </style>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">True</property>
- <property name="pack-type">end</property>
- <property name="position">4</property>
- </packing>
- </child>
- <child>
- <object class="GtkButton" id="authentication_button">
- <property name="can-focus">True</property>
- <property name="focus-on-click">False</property>
- <property name="receives-default">True</property>
- <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
- <property name="no-show-all">True</property>
- <property name="relief">none</property>
- <signal name="clicked" handler="_on_authentication_button_clicked" swapped="no"/>
- <child>
- <object class="GtkImage" id="lock_image">
- <property name="visible">True</property>
- <property name="can-focus">False</property>
- <property name="icon_size">1</property>
- </object>
- </child>
- <style>
- <class name="chatcontrol-actionbar-button"/>
- </style>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">False</property>
- <property name="pack-type">end</property>
- <property name="position">5</property>
- </packing>
- </child>
- <child>
- <object class="GtkMenuButton" id="encryption_menu">
- <property name="visible">True</property>
- <property name="can-focus">True</property>
- <property name="receives-default">True</property>
- <property name="tooltip-text" translatable="yes">Choose encryption</property>
- <property name="relief">none</property>
- <child>
- <placeholder/>
- </child>
- <style>
- <class name="chatcontrol-actionbar-button"/>
- </style>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">True</property>
- <property name="pack-type">end</property>
- <property name="position">6</property>
- </packing>
- </child>
- <child>
- <object class="GtkButton" id="sendfile_button">
- <property name="visible">True</property>
- <property name="can-focus">True</property>
- <property name="focus-on-click">False</property>
- <property name="receives-default">True</property>
- <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
- <property name="relief">none</property>
- <child>
- <object class="GtkImage">
- <property name="visible">True</property>
- <property name="can-focus">False</property>
- <property name="icon-name">mail-attachment-symbolic</property>
- <property name="icon_size">1</property>
- </object>
- </child>
- <style>
- <class name="chatcontrol-actionbar-button"/>
- </style>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">False</property>
- <property name="pack-type">end</property>
- <property name="position">7</property>
- </packing>
- </child>
- <child>
- <object class="GtkButton" id="send_message_button">
- <property name="can-focus">True</property>
- <property name="focus-on-click">False</property>
- <property name="receives-default">True</property>
- <property name="no-show-all">True</property>
- <property name="tooltip-text" translatable="yes">Send Message</property>
- <property name="relief">none</property>
- <child>
- <object class="GtkImage">
- <property name="visible">True</property>
- <property name="can-focus">False</property>
- <property name="icon-name">document-send</property>
- </object>
- </child>
- <style>
- <class name="chatcontrol-actionbar-button"/>
- </style>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">False</property>
- <property name="pack-type">end</property>
- <property name="position">8</property>
- </packing>
- </child>
- <child>
- <placeholder/>
- </child>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">False</property>
- <property name="position">4</property>
- </packing>
+ <placeholder/>
</child>
</object>
<packing>
- <property name="index">-1</property>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
</packing>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
- <property name="position">1</property>
+ <property name="position">0</property>
</packing>
</child>
</object>
diff --git a/gajim/data/gui/chat_paned.ui b/gajim/data/gui/chat_paned.ui
index d89e34c63..35c050f46 100644
--- a/gajim/data/gui/chat_paned.ui
+++ b/gajim/data/gui/chat_paned.ui
@@ -22,6 +22,7 @@
<property name="row-spacing">3</property>
<child>
<object class="GtkBox">
+ <property name="height-request">39</property>
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="spacing">12</property>
@@ -31,6 +32,7 @@
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<property name="tooltip-text" translatable="yes">Edit workspace…</property>
+ <property name="valign">center</property>
<signal name="clicked" handler="_on_edit_workspace_clicked" swapped="no"/>
<child>
<object class="GtkImage">
@@ -69,6 +71,7 @@
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<property name="tooltip-text" translatable="yes">Filter Chats…</property>
+ <property name="valign">center</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
diff --git a/gajim/data/gui/groupchat_control.ui b/gajim/data/gui/groupchat_control.ui
deleted file mode 100644
index 0579ff21e..000000000
--- a/gajim/data/gui/groupchat_control.ui
+++ /dev/null
@@ -1,1700 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- Generated with glade 3.38.2 -->
-<interface>
- <requires lib="gtk+" version="3.24"/>
- <object class="GtkBox" id="drop_area">
- <property name="name">DropArea</property>
- <property name="visible">True</property>
- <property name="can-focus">False</property>
- <property name="no-show-all">True</property>
- <property name="opacity">0.90</property>
- <property name="hexpand">True</property>
- <property name="vexpand">True</property>
- <property name="orientation">vertical</property>
- <property name="spacing">18</property>
- <child>
- <object class="GtkImage">
- <property name="visible">True</property>
- <property name="can-focus">False</property>
- <property name="valign">end</property>
- <property name="vexpand">True</property>
- <property name="icon-name">mail-attachment-symbolic</property>
- <property name="icon_size">6</property>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">True</property>
- <property name="position">0</property>
- </packing>
- </child>
- <child>
- <object class="GtkLabel">
- <property name="visible">True</property>
- <property name="can-focus">False</property>
- <property name="valign">start</property>
- <property name="vexpand">True</property>
- <property name="label" translatable="yes">Drop Files or Contacts</property>
- <property name="wrap">True</property>
- <property name="max-width-chars">40</property>
- <style>
- <class name="bold16"/>
- </style>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">True</property>
- <property name="position">1</property>
- </packing>
- </child>
- </object>
- <object class="GtkMenu" id="formattings_menu">
- <property name="visible">True</property>
- <property name="can-focus">False</property>
- <child>
- <object class="GtkMenuItem" id="bold">
- <property name="name">bold</property>
- <property name="visible">True</property>
- <property name="can-focus">False</property>
- <property name="label" translatable="yes">Bold</property>
- <signal name="activate" handler="_on_formatting_menuitem_activate" swapped="no"/>
- </object>
- </child>
- <child>
- <object class="GtkMenuItem" id="italic">
- <property name="name">italic</property>
- <property name="visible">True</property>
- <property name="can-focus">False</property>
- <property name="label" translatable="yes">Italic</property>
- <signal name="activate" handler="_on_formatting_menuitem_activate" swapped="no"/>
- </object>
- </child>
- <child>
- <object class="GtkMenuItem" id="strike">
- <property name="name">strike</property>
- <property name="visible">True</property>
- <property name="can-focus">False</property>
- <property name="label" translatable="yes">Strike</property>
- <signal name="activate" handler="_on_formatting_menuitem_activate" swapped="no"/>
- </object>
- </child>
- </object>
- <object class="GtkBox" id="groupchat_control_hbox">
- <property name="can-focus">True</property>
- <child>
- <object class="GtkOverlay" id="overlay">
- <property name="visible">True</property>
- <property name="can-focus">False</property>
- <child>
- <object class="GtkStack" id="stack">
- <property name="visible">True</property>
- <property name="can-focus">False</property>
- <signal name="notify::visible-child-name" handler="_on_page_change" swapped="no"/>
- <child>
- <object class="GtkBox" id="groupchat_control_vbox">
- <property name="can-focus">True</property>
- <property name="margin-start">7</property>
- <property name="margin-end">7</property>
- <property name="margin-top">5</property>
- <property name="margin-bottom">7</property>
- <property name="spacing">3</property>
- <child>
- <object class="GtkBox" id="textview_box">
- <property name="visible">True</property>
- <property name="can-focus">False</property>
- <property name="margin-end">4</property>
- <property name="hexpand">True</property>
- <property name="orientation">vertical</property>
- <child>
- <object class="GtkEventBox" id="banner_eventbox">
- <property name="name">GroupChatControl-BannerEventBox</property>
- <property name="can-focus">False</property>
- <property name="no-show-all">True</property>
- <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="avatar_image">
- <property name="width-request">48</property>
- <property name="height-request">48</property>
- <property name="visible">True</property>
- <property name="can-focus">False</property>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">True</property>
- <property name="position">0</property>
- </packing>
- </child>
- <child>
- <object class="GtkLabel" id="banner_name_label">
- <property name="name">GroupChatControl-BannerNameLabel</property>
- <property name="visible">True</property>
- <property name="can-focus">False</property>
- <property name="hexpand">True</property>
- <property name="label">Group Chat Name</property>
- <property name="selectable">True</property>
- <property name="ellipsize">end</property>
- <property name="single-line-mode">True</property>
- <property name="xalign">0</property>
- <style>
- <class name="large-header"/>
- </style>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">True</property>
- <property name="position">1</property>
- </packing>
- </child>
- <child>
- <object class="GtkBox" id="visitor_box">
- <property name="can-focus">False</property>
- <property name="no-show-all">True</property>
- <property name="spacing">12</property>
- <child>
- <object class="GtkLabel">
- <property name="visible">True</property>
- <property name="can-focus">False</property>
- <property name="label" translatable="yes">You are a visitor</property>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">True</property>
- <property name="position">0</property>
- </packing>
- </child>
- <child>
- <object class="GtkMenuButton" id="visitor_menu_button">
- <property name="visible">True</property>
- <property name="can-focus">True</property>
- <property name="focus-on-click">False</property>
- <property name="receives-default">True</property>
- <property name="tooltip-text" translatable="yes">What does this mean?</property>
- <property name="valign">center</property>
- <property name="popover">visitor_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>
- <style>
- <class name="suggested-action"/>
- </style>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">True</property>
- <property name="position">1</property>
- </packing>
- </child>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">True</property>
- <property name="position">2</property>
- </packing>
- </child>
- <child>
- <object class="GtkBox" id="account_badge_box">
- <property name="can-focus">False</property>
- <property name="no-show-all">True</property>
- <property name="halign">end</property>
- <property name="valign">center</property>
- <property name="margin-end">6</property>
- <property name="hexpand">True</property>
- <property name="orientation">vertical</property>
- <child>
- <placeholder/>
- </child>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">True</property>
- <property name="position">3</property>
- </packing>
- </child>
- <child>
- <object class="GtkButton" id="toggle_roster_button">
- <property name="visible">True</property>
- <property name="can-focus">True</property>
- <property name="receives-default">True</property>
- <property name="tooltip-text" translatable="yes">Toggle participants list</property>
- <property name="valign">center</property>
- <signal name="clicked" handler="_show_roster" swapped="no"/>
- <child>
- <object class="GtkBox">
- <property name="visible">True</property>
- <property name="can-focus">False</property>
- <property name="spacing">3</property>
- <child>
- <object class="GtkImage">
- <property name="visible">True</property>
- <property name="can-focus">False</property>
- <property name="icon-name">system-users-symbolic</property>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">True</property>
- <property name="position">0</property>
- </packing>
- </child>
- <child>
- <object class="GtkImage" id="toggle_roster_image">
- <property name="visible">True</property>
- <property name="can-focus">False</property>
- <property name="icon-name">go-previous-symbolic</property>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">True</property>
- <property name="position">1</property>
- </packing>
- </child>
- <style>
- <class name="dim-label"/>
- </style>
- </object>
- </child>
- <style>
- <class name="flat"/>
- </style>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">True</property>
- <property name="position">4</property>
- </packing>
- </child>
- </object>
- </child>
- <style>
- <class name="gajim-banner"/>
- </style>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">True</property>
- <property name="position">0</property>
- </packing>
- </child>
- <child>
- <object class="GtkSeparator">
- <property name="visible">True</property>
- <property name="can-focus">False</property>
- <style>
- <class name="chatcontrol-separator-top"/>
- </style>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">True</property>
- <property name="position">1</property>
- </packing>
- </child>
- <child>
- <object class="GtkOverlay" id="conv_view_overlay">
- <property name="visible">True</property>
- <property name="can-focus">False</property>
- <child>
- <placeholder/>
- </child>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">True</property>
- <property name="position">2</property>
- </packing>
- </child>
- <child>
- <object class="GtkSeparator">
- <property name="visible">True</property>
- <property name="can-focus">False</property>
- <style>
- <class name="chatcontrol-separator"/>
- </style>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">True</property>
- <property name="position">3</property>
- </packing>
- </child>
- <child>
- <object class="GtkBox" id="hbox">
- <property name="visible">True</property>
- <property name="can-focus">False</property>
- <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
- <child>
- <object class="GtkButton" id="quick_invite_button">
- <property name="visible">True</property>
- <property name="can-focus">True</property>
- <property name="receives-default">True</property>
- <property name="tooltip-text" translatable="yes">Invite Contacts…</property>
- <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>
- <style>
- <class name="chatcontrol-actionbar-button"/>
- <class name="flat"/>
- </style>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">True</property>
- <property name="pack-type">end</property>
- <property name="position">0</property>
- </packing>
- </child>
- <child>
- <object class="GtkMenuButton" id="settings_menu">
- <property name="visible">True</property>
- <property name="can-focus">True</property>
- <property name="receives-default">True</property>
- <property name="relief">none</property>
- <child>
- <object class="GtkImage">
- <property name="visible">True</property>
- <property name="can-focus">False</property>
- <property name="icon-name">open-menu-symbolic</property>
- </object>
- </child>
- <style>
- <class name="chatcontrol-actionbar-button"/>
- </style>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">True</property>
- <property name="pack-type">end</property>
- <property name="position">0</property>
- </packing>
- </child>
- <child>
- <object class="GtkButton" id="authentication_button">
- <property name="can-focus">True</property>
- <property name="focus-on-click">False</property>
- <property name="receives-default">True</property>
- <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
- <property name="no-show-all">True</property>
- <property name="relief">none</property>
- <signal name="clicked" handler="_on_authentication_button_clicked" swapped="no"/>
- <child>
- <object class="GtkImage" id="lock_image">
- <property name="visible">True</property>
- <property name="can-focus">False</property>
- <property name="icon_size">1</property>
- </object>
- </child>
- <style>
- <class name="chatcontrol-actionbar-button"/>
- </style>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">False</property>
- <property name="pack-type">end</property>
- <property name="position">1</property>
- </packing>
- </child>
- <child>
- <object class="GtkMenuButton" id="encryption_menu">
- <property name="visible">True</property>
- <property name="can-focus">True</property>
- <property name="receives-default">True</property>
- <property name="tooltip-text" translatable="yes">Choose encryption</property>
- <property name="relief">none</property>
- <child>
- <placeholder/>
- </child>
- <style>
- <class name="chatcontrol-actionbar-button"/>
- </style>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">True</property>
- <property name="pack-type">end</property>
- <property name="position">2</property>
- </packing>
- </child>
- <child>
- <object class="GtkButton" id="sendfile_button">
- <property name="visible">True</property>
- <property name="can-focus">True</property>
- <property name="focus-on-click">False</property>
- <property name="receives-default">True</property>
- <property name="relief">none</property>
- <child>
- <object class="GtkImage">
- <property name="visible">True</property>
- <property name="can-focus">False</property>
- <property name="icon-name">mail-attachment-symbolic</property>
- </object>
- </child>
- <style>
- <class name="chatcontrol-actionbar-button"/>
- </style>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">False</property>
- <property name="pack-type">end</property>
- <property name="position">3</property>
- </packing>
- </child>
- <child>
- <object class="GtkMenuButton" id="emoticons_button">
- <property name="visible">True</property>
- <property name="can-focus">True</property>
- <property name="receives-default">True</property>
- <property name="tooltip-text" translatable="yes">Show a list of emojis</property>
- <property name="relief">none</property>
- <child>
- <object class="GtkImage">
- <property name="visible">True</property>
- <property name="can-focus">False</property>
- <property name="icon-name">face-smile-symbolic</property>
- </object>
- </child>
- <style>
- <class name="chatcontrol-actionbar-button"/>
- </style>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">True</property>
- <property name="position">5</property>
- </packing>
- </child>
- <child>
- <object class="GtkButton" id="send_message_button">
- <property name="can-focus">True</property>
- <property name="focus-on-click">False</property>
- <property name="receives-default">True</property>
- <property name="no-show-all">True</property>
- <property name="tooltip-text" translatable="yes">Send Message</property>
- <property name="relief">none</property>
- <child>
- <object class="GtkImage">
- <property name="visible">True</property>
- <property name="can-focus">False</property>
- <property name="icon-name">document-send</property>
- </object>
- </child>
- <style>
- <class name="chatcontrol-actionbar-button"/>
- </style>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">False</property>
- <property name="pack-type">end</property>
- <property name="position">6</property>
- </packing>
- </child>
- <child>
- <object class="GtkMenuButton" id="formattings_button">
- <property name="visible">True</property>
- <property name="can-focus">True</property>
- <property name="focus-on-click">False</property>
- <property name="receives-default">True</property>
- <property name="tooltip-text" translatable="yes">Format your message</property>
- <property name="relief">none</property>
- <property name="popup">formattings_menu</property>
- <property name="direction">up</property>
- <child>
- <object class="GtkImage">
- <property name="visible">True</property>
- <property name="can-focus">False</property>
- <property name="icon-name">format-text-bold-symbolic</property>
- <property name="icon_size">1</property>
- </object>
- </child>
- <style>
- <class name="chatcontrol-actionbar-button"/>
- </style>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">True</property>
- <property name="position">7</property>
- </packing>
- </child>
- <child>
- <placeholder/>
- </child>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">False</property>
- <property name="position">4</property>
- </packing>
- </child>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">True</property>
- <property name="position">0</property>
- </packing>
- </child>
- <child>
- <object class="GtkRevealer" id="roster_revealer">
- <property name="visible">True</property>
- <property name="can-focus">False</property>
- <property name="transition-type">none</property>
- <child>
- <placeholder/>
- </child>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">True</property>
- <property name="position">1</property>
- </packing>
- </child>
- </object>
- <packing>
- <property name="name">groupchat</property>
- </packing>
- </child>
- <child>
- <object class="GtkBox">
- <property name="visible">True</property>
- <property name="can-focus">False</property>
- <property name="orientation">vertical</property>
- <child>
- <!-- n-columns=1 n-rows=3 -->
- <object class="GtkGrid">
- <property name="visible">True</property>
- <property name="can-focus">False</property>
- <property name="halign">center</property>
- <property name="valign">center</property>
- <property name="hexpand">True</property>
- <property name="vexpand">True</property>
- <property name="row-spacing">12</property>
- <child>
- <object class="GtkLabel">
- <property name="visible">True</property>
- <property name="can-focus">False</property>
- <property name="label" translatable="yes">Enter Nickname</property>
- <property name="xalign">0</property>
- <style>
- <class name="bold16"/>
- </style>
- </object>
- <packing>
- <property name="left-attach">0</property>
- <property name="top-attach">0</property>
- </packing>
- </child>
- <child>
- <object class="GtkEntry" id="nickname_entry">
- <property name="width-request">200</property>
- <property name="visible">True</property>
- <property name="can-focus">True</property>
- <property name="activates-default">True</property>
- <property name="caps-lock-warning">False</property>
- <property name="show-emoji-icon">True</property>
- <signal name="notify::text" handler="_on_nickname_text_changed" swapped="no"/>
- </object>
- <packing>
- <property name="left-attach">0</property>
- <property name="top-attach">1</property>
- </packing>
- </child>
- <child>
- <object class="GtkBox">
- <property name="visible">True</property>
- <property name="can-focus">False</property>
- <property name="homogeneous">True</property>
- <child>
- <object class="GtkButton">
- <property name="label" translatable="yes">_Cancel</property>
- <property name="visible">True</property>
- <property name="can-focus">True</property>
- <property name="receives-default">True</property>
- <property name="halign">start</property>
- <property name="valign">start</property>
- <property name="use-underline">True</property>
- <signal name="clicked" handler="_on_page_cancel_clicked" swapped="no"/>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">True</property>
- <property name="position">0</property>
- </packing>
- </child>
- <child>
- <object class="GtkButton" id="nickname_change_button">
- <property name="label" translatable="yes">Ch_ange</property>
- <property name="visible">True</property>
- <property name="sensitive">False</property>
- <property name="can-focus">True</property>
- <property name="can-default">True</property>
- <property name="receives-default">True</property>
- <property name="halign">end</property>
- <property name="valign">start</property>
- <property name="use-underline">True</property>
- <signal name="clicked" handler="_on_nickname_change_clicked" swapped="no"/>
- <style>
- <class name="suggested-action"/>
- </style>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">True</property>
- <property name="position">1</property>
- </packing>
- </child>
- </object>
- <packing>
- <property name="left-attach">0</property>
- <property name="top-attach">2</property>
- </packing>
- </child>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">True</property>
- <property name="position">0</property>
- </packing>
- </child>
- </object>
- <packing>
- <property name="name">nickname</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="orientation">vertical</property>
- <child>
- <!-- n-columns=1 n-rows=3 -->
- <object class="GtkGrid">
- <property name="visible">True</property>
- <property name="can-focus">False</property>
- <property name="halign">center</property>
- <property name="valign">center</property>
- <property name="hexpand">True</property>
- <property name="vexpand">True</property>
- <property name="row-spacing">12</property>
- <child>
- <object class="GtkLabel">
- <property name="visible">True</property>
- <property name="can-focus">False</property>
- <property name="label" translatable="yes">Enter Password</property>
- <property name="xalign">0</property>
- <style>
- <class name="bold16"/>
- </style>
- </object>
- <packing>
- <property name="left-attach">0</property>
- <property name="top-attach">0</property>
- </packing>
- </child>
- <child>
- <object class="GtkBox">
- <property name="visible">True</property>
- <property name="can-focus">False</property>
- <property name="homogeneous">True</property>
- <child>
- <object class="GtkButton">
- <property name="label" translatable="yes">_Cancel</property>
- <property name="visible">True</property>
- <property name="can-focus">True</property>
- <property name="receives-default">True</property>
- <property name="halign">start</property>
- <property name="valign">start</property>
- <property name="use-underline">True</property>
- <signal name="clicked" handler="_on_password_cancel_clicked" swapped="no"/>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">True</property>
- <property name="position">0</property>
- </packing>
- </child>
- <child>
- <object class="GtkButton" id="password_set_button">
- <property name="label" translatable="yes">_Join</property>
- <property name="visible">True</property>
- <property name="sensitive">False</property>
- <property name="can-focus">True</property>
- <property name="can-default">True</property>
- <property name="receives-default">True</property>
- <property name="halign">end</property>
- <property name="valign">start</property>
- <property name="use-underline">True</property>
- <signal name="clicked" handler="_on_password_set_clicked" swapped="no"/>
- <style>
- <class name="suggested-action"/>
- </style>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">True</property>
- <property name="position">1</property>
- </packing>
- </child>
- </object>
- <packing>
- <property name="left-attach">0</property>
- <property name="top-attach">2</property>
- </packing>
- </child>
- <child>
- <object class="GtkEntry" id="password_entry">
- <property name="visible">True</property>
- <property name="can-focus">True</property>
- <property name="visibility">False</property>
- <property name="invisible-char">*</property>
- <property name="activates-default">True</property>
- <property name="input-purpose">password</property>
- <signal name="notify::text" handler="_on_password_changed" swapped="no"/>
- </object>
- <packing>
- <property name="left-attach">0</property>
- <property name="top-attach">1</property>
- </packing>
- </child>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">True</property>
- <property name="position">0</property>
- </packing>
- </child>
- </object>
- <packing>
- <property name="name">password</property>
- <property name="position">2</property>
- </packing>
- </child>
- <child>
- <object class="GtkBox">
- <property name="visible">True</property>
- <property name="can-focus">False</property>
- <property name="orientation">vertical</property>
- <child>
- <!-- n-columns=1 n-rows=2 -->
- <object class="GtkGrid">
- <property name="visible">True</property>
- <property name="can-focus">False</property>
- <property name="halign">center</property>
- <property name="valign">center</property>
- <property name="hexpand">True</property>
- <property name="vexpand">True</property>
- <property name="row-spacing">12</property>
- <child>
- <object class="GtkBox">
- <property name="visible">True</property>
- <property name="can-focus">False</property>
- <property name="homogeneous">True</property>
- <child>
- <object class="GtkButton">
- <property name="label" translatable="yes">_Cancel</property>
- <property name="visible">True</property>
- <property name="can-focus">True</property>
- <property name="receives-default">True</property>
- <property name="halign">start</property>
- <property name="valign">start</property>
- <property name="use-underline">True</property>
- <signal name="clicked" handler="_on_captcha_cancel_clicked" swapped="no"/>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">True</property>
- <property name="position">0</property>
- </packing>
- </child>
- <child>
- <object class="GtkButton" id="captcha_set_button">
- <property name="label" translatable="yes">_Join</property>
- <property name="visible">True</property>
- <property name="sensitive">False</property>
- <property name="can-focus">True</property>
- <property name="can-default">True</property>
- <property name="receives-default">True</property>
- <property name="halign">end</property>
- <property name="valign">start</property>
- <property name="use-underline">True</property>
- <signal name="clicked" handler="_on_captcha_set_clicked" swapped="no"/>
- <style>
- <class name="suggested-action"/>
- </style>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">True</property>
- <property name="position">1</property>
- </packing>
- </child>
- </object>
- <packing>
- <property name="left-attach">0</property>
- <property name="top-attach">1</property>
- </packing>
- </child>
- <child>
- <object class="GtkBox" id="captcha_box">
- <property name="visible">True</property>
- <property name="can-focus">False</property>
- <property name="orientation">vertical</property>
- <child>
- <placeholder/>
- </child>
- </object>
- <packing>
- <property name="left-attach">0</property>
- <property name="top-attach">0</property>
- </packing>
- </child>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">True</property>
- <property name="position">0</property>
- </packing>
- </child>
- </object>
- <packing>
- <property name="name">captcha</property>
- <property name="position">3</property>
- </packing>
- </child>
- <child>
- <object class="GtkBox">
- <property name="visible">True</property>
- <property name="can-focus">False</property>
- <property name="orientation">vertical</property>
- <child>
- <!-- n-columns=1 n-rows=4 -->
- <object class="GtkGrid">
- <property name="visible">True</property>
- <property name="can-focus">False</property>
- <property name="halign">center</property>
- <property name="valign">center</property>
- <property name="hexpand">True</property>
- <property name="vexpand">True</property>
- <property name="row-spacing">12</property>
- <child>
- <object class="GtkImage">
- <property name="visible">True</property>
- <property name="can-focus">False</property>
- <property name="icon-name">dialog-error-symbolic</property>
- <property name="icon_size">6</property>
- <style>
- <class name="error-color"/>
- </style>
- </object>
- <packing>
- <property name="left-attach">0</property>
- <property name="top-attach">0</property>
- </packing>
- </child>
- <child>
- <object class="GtkBox">
- <property name="visible">True</property>
- <property name="can-focus">False</property>
- <property name="halign">center</property>
- <property name="spacing">6</property>
- <child>
- <object class="GtkButton" id="remove_bookmark_button">
- <property name="label" translatable="yes">_Forget Group Chat</property>
- <property name="can-focus">True</property>
- <property name="receives-default">False</property>
- <property name="no-show-all">True</property>
- <property name="tooltip-text" translatable="yes">Gajim will not try to join this group chat again</property>
- <property name="halign">start</property>
- <property name="valign">start</property>
- <property name="use-underline">True</property>
- <signal name="clicked" handler="_on_remove_bookmark_button_clicked" swapped="no"/>
- <style>
- <class name="destructive-action"/>
- </style>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">True</property>
- <property name="position">0</property>
- </packing>
- </child>
- <child>
- <object class="GtkButton" id="retry_button">
- <property name="label" translatable="yes">T_ry Again</property>
- <property name="visible">True</property>
- <property name="can-focus">True</property>
- <property name="receives-default">True</property>
- <property name="use-underline">True</property>
- <signal name="clicked" handler="_on_retry_join_clicked" swapped="no"/>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">True</property>
- <property name="position">1</property>
- </packing>
- </child>
- <child>
- <object class="GtkButton" id="close_button">
- <property name="label" translatable="yes">_Close</property>
- <property name="visible">True</property>
- <property name="can-focus">True</property>
- <property name="can-default">True</property>
- <property name="receives-default">True</property>
- <property name="halign">end</property>
- <property name="valign">start</property>
- <property name="use-underline">True</property>
- <signal name="clicked" handler="_on_page_close_clicked" swapped="no"/>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">True</property>
- <property name="position">2</property>
- </packing>
- </child>
- </object>
- <packing>
- <property name="left-attach">0</property>
- <property name="top-attach">3</property>
- </packing>
- </child>
- <child>
- <object class="GtkLabel" id="error_label">
- <property name="visible">True</property>
- <property name="can-focus">False</property>
- <property name="justify">center</property>
- <property name="wrap">True</property>
- <property name="max-width-chars">40</property>
- </object>
- <packing>
- <property name="left-attach">0</property>
- <property name="top-attach">2</property>
- </packing>
- </child>
- <child>
- <object class="GtkLabel" id="error_heading">
- <property name="visible">True</property>
- <property name="can-focus">False</property>
- <property name="label" translatable="yes">An Error Occurred</property>
- <property name="justify">center</property>
- <property name="wrap">True</property>
- <property name="max-width-chars">40</property>
- <style>
- <class name="bold16"/>
- </style>
- </object>
- <packing>
- <property name="left-attach">0</property>
- <property name="top-attach">1</property>
- </packing>
- </child>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">True</property>
- <property name="position">0</property>
- </packing>
- </child>
- </object>
- <packing>
- <property name="name">error</property>
- <property name="position">4</property>
- </packing>
- </child>
- <child>
- <object class="GtkBox">
- <property name="visible">True</property>
- <property name="can-focus">False</property>
- <property name="orientation">vertical</property>
- <child>
- <!-- n-columns=1 n-rows=3 -->
- <object class="GtkGrid">
- <property name="visible">True</property>
- <property name="can-focus">False</property>
- <property name="halign">center</property>
- <property name="valign">center</property>
- <property name="hexpand">True</property>
- <property name="vexpand">True</property>
- <property name="row-spacing">12</property>
- <child>
- <object class="GtkBox">
- <property name="visible">True</property>
- <property name="can-focus">False</property>
- <property name="homogeneous">True</property>
- <child>
- <object class="GtkButton" id="captcha_close_button">
- <property name="label" translatable="yes">_Close</property>
- <property name="visible">True</property>
- <property name="can-focus">True</property>
- <property name="can-default">True</property>
- <property name="receives-default">True</property>
- <property name="halign">center</property>
- <property name="valign">start</property>
- <property name="use-underline">True</property>
- <signal name="clicked" handler="_on_page_close_clicked" swapped="no"/>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">True</property>
- <property name="position">0</property>
- </packing>
- </child>
- <child>
- <object class="GtkButton" id="captcha_try_again_button">
- <property name="label" translatable="yes">_Try Again</property>
- <property name="visible">True</property>
- <property name="can-focus">True</property>
- <property name="can-default">True</property>
- <property name="receives-default">True</property>
- <property name="use-underline">True</property>
- <signal name="clicked" handler="_on_captcha_try_again_clicked" swapped="no"/>
- <style>
- <class name="suggested-action"/>
- </style>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">True</property>
- <property name="position">1</property>
- </packing>
- </child>
- </object>
- <packing>
- <property name="left-attach">0</property>
- <property name="top-attach">2</property>
- </packing>
- </child>
- <child>
- <object class="GtkImage">
- <property name="visible">True</property>
- <property name="can-focus">False</property>
- <property name="icon-name">dialog-error-symbolic</property>
- <property name="icon_size">6</property>
- <style>
- <class name="error-color"/>
- </style>
- </object>
- <packing>
- <property name="left-attach">0</property>
- <property name="top-attach">0</property>
- </packing>
- </child>
- <child>
- <object class="GtkLabel" id="captcha_error_label">
- <property name="visible">True</property>
- <property name="can-focus">False</property>
- <property name="justify">center</property>
- <property name="wrap">True</property>
- <property name="max-width-chars">40</property>
- <style>
- <class name="bold16"/>
- </style>
- </object>
- <packing>
- <property name="left-attach">0</property>
- <property name="top-attach">1</property>
- </packing>
- </child>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">True</property>
- <property name="position">0</property>
- </packing>
- </child>
- </object>
- <packing>
- <property name="name">captcha-error</property>
- <property name="position">5</property>
- </packing>
- </child>
- <child>
- <object class="GtkBox">
- <property name="visible">True</property>
- <property name="can-focus">False</property>
- <property name="orientation">vertical</property>
- <child>
- <!-- n-columns=1 n-rows=4 -->
- <object class="GtkGrid">
- <property name="visible">True</property>
- <property name="can-focus">False</property>
- <property name="halign">center</property>
- <property name="valign">center</property>
- <property name="hexpand">True</property>
- <property name="vexpand">True</property>
- <property name="row-spacing">6</property>
- <child>
- <object class="GtkLabel" id="kick_label">
- <property name="visible">True</property>
- <property name="can-focus">False</property>
- <property name="margin-bottom">6</property>
- <property name="label" translatable="yes">Kick Participant</property>
- <property name="ellipsize">end</property>
- <property name="single-line-mode">True</property>
- <property name="max-width-chars">22</property>
- <property name="xalign">0</property>
- <style>
- <class name="bold16"/>
- </style>
- </object>
- <packing>
- <property name="left-attach">0</property>
- <property name="top-attach">0</property>
- </packing>
- </child>
- <child>
- <object class="GtkBox">
- <property name="visible">True</property>
- <property name="can-focus">False</property>
- <property name="homogeneous">True</property>
- <child>
- <object class="GtkButton">
- <property name="label" translatable="yes">_Cancel</property>
- <property name="visible">True</property>
- <property name="can-focus">True</property>
- <property name="receives-default">True</property>
- <property name="halign">start</property>
- <property name="valign">start</property>
- <property name="use-underline">True</property>
- <signal name="clicked" handler="_on_page_cancel_clicked" swapped="no"/>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">True</property>
- <property name="position">0</property>
- </packing>
- </child>
- <child>
- <object class="GtkButton" id="kick_participant_button">
- <property name="label" translatable="yes">_Kick</property>
- <property name="visible">True</property>
- <property name="can-focus">True</property>
- <property name="can-default">True</property>
- <property name="receives-default">True</property>
- <property name="halign">end</property>
- <property name="valign">start</property>
- <property name="use-underline">True</property>
- <signal name="clicked" handler="_on_kick_participant_clicked" swapped="no"/>
- <style>
- <class name="destructive-action"/>
- </style>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">True</property>
- <property name="position">1</property>
- </packing>
- </child>
- </object>
- <packing>
- <property name="left-attach">0</property>
- <property name="top-attach">3</property>
- </packing>
- </child>
- <child>
- <object class="GtkEntry" id="kick_reason_entry">
- <property name="width-request">200</property>
- <property name="visible">True</property>
- <property name="can-focus">True</property>
- <property name="margin-bottom">6</property>
- <property name="activates-default">True</property>
- <property name="caps-lock-warning">False</property>
- <property name="secondary-icon-tooltip-text" translatable="yes">Insert Emoji</property>
- <property name="show-emoji-icon">True</property>
- </object>
- <packing>
- <property name="left-attach">0</property>
- <property name="top-attach">2</property>
- </packing>
- </child>
- <child>
- <object class="GtkLabel">
- <property name="visible">True</property>
- <property name="can-focus">False</property>
- <property name="halign">start</property>
- <property name="label" translatable="yes">Reason (optional)</property>
- <style>
- <class name="dim-label"/>
- </style>
- </object>
- <packing>
- <property name="left-attach">0</property>
- <property name="top-attach">1</property>
- </packing>
- </child>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">True</property>
- <property name="position">0</property>
- </packing>
- </child>
- </object>
- <packing>
- <property name="name">kick</property>
- <property name="position">6</property>
- </packing>
- </child>
- <child>
- <object class="GtkBox">
- <property name="visible">True</property>
- <property name="can-focus">False</property>
- <property name="orientation">vertical</property>
- <child>
- <!-- n-columns=1 n-rows=4 -->
- <object class="GtkGrid">
- <property name="visible">True</property>
- <property name="can-focus">False</property>
- <property name="halign">center</property>
- <property name="valign">center</property>
- <property name="hexpand">True</property>
- <property name="vexpand">True</property>
- <property name="row-spacing">6</property>
- <child>
- <object class="GtkLabel" id="ban_label">
- <property name="visible">True</property>
- <property name="can-focus">False</property>
- <property name="margin-bottom">6</property>
- <property name="label" translatable="yes">Ban Participant</property>
- <property name="ellipsize">end</property>
- <property name="single-line-mode">True</property>
- <property name="max-width-chars">22</property>
- <property name="xalign">0</property>
- <style>
- <class name="bold16"/>
- </style>
- </object>
- <packing>
- <property name="left-attach">0</property>
- <property name="top-attach">0</property>
- </packing>
- </child>
- <child>
- <object class="GtkBox">
- <property name="visible">True</property>
- <property name="can-focus">False</property>
- <property name="homogeneous">True</property>
- <child>
- <object class="GtkButton">
- <property name="label" translatable="yes">_Cancel</property>
- <property name="visible">True</property>
- <property name="can-focus">True</property>
- <property name="receives-default">True</property>
- <property name="halign">start</property>
- <property name="valign">start</property>
- <property name="use-underline">True</property>
- <signal name="clicked" handler="_on_page_cancel_clicked" swapped="no"/>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">True</property>
- <property name="position">0</property>
- </packing>
- </child>
- <child>
- <object class="GtkButton" id="ban_participant_button">
- <property name="label" translatable="yes">_Ban</property>
- <property name="visible">True</property>
- <property name="can-focus">True</property>
- <property name="can-default">True</property>
- <property name="receives-default">True</property>
- <property name="halign">end</property>
- <property name="valign">start</property>
- <property name="use-underline">True</property>
- <signal name="clicked" handler="_on_ban_participant_clicked" swapped="no"/>
- <style>
- <class name="destructive-action"/>
- </style>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">True</property>
- <property name="position">1</property>
- </packing>
- </child>
- </object>
- <packing>
- <property name="left-attach">0</property>
- <property name="top-attach">3</property>
- </packing>
- </child>
- <child>
- <object class="GtkEntry" id="ban_reason_entry">
- <property name="width-request">200</property>
- <property name="visible">True</property>
- <property name="can-focus">True</property>
- <property name="margin-bottom">6</property>
- <property name="activates-default">True</property>
- <property name="caps-lock-warning">False</property>
- <property name="secondary-icon-tooltip-text" translatable="yes">Insert Emoji</property>
- <property name="show-emoji-icon">True</property>
- </object>
- <packing>
- <property name="left-attach">0</property>
- <property name="top-attach">2</property>
- </packing>
- </child>
- <child>
- <object class="GtkLabel">
- <property name="visible">True</property>
- <property name="can-focus">False</property>
- <property name="halign">start</property>
- <property name="label" translatable="yes">Reason (optional)</property>
- <style>
- <class name="dim-label"/>
- </style>
- </object>
- <packing>
- <property name="left-attach">0</property>
- <property name="top-attach">1</property>
- </packing>
- </child>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">True</property>
- <property name="position">0</property>
- </packing>
- </child>
- </object>
- <packing>
- <property name="name">ban</property>
- <property name="position">7</property>
- </packing>
- </child>
- <child>
- <object class="GtkBox">
- <property name="visible">True</property>
- <property name="can-focus">False</property>
- <property name="orientation">vertical</property>
- <child>
- <!-- n-columns=1 n-rows=6 -->
- <object class="GtkGrid">
- <property name="visible">True</property>
- <property name="can-focus">False</property>
- <property name="halign">center</property>
- <property name="valign">center</property>
- <property name="hexpand">True</property>
- <property name="vexpand">True</property>
- <property name="row-spacing">6</property>
- <child>
- <object class="GtkLabel">
- <property name="visible">True</property>
- <property name="can-focus">False</property>
- <property name="margin-bottom">6</property>
- <property name="label" translatable="yes">Destroy This Chat</property>
- <property name="ellipsize">end</property>
- <property name="single-line-mode">True</property>
- <property name="max-width-chars">22</property>
- <property name="xalign">0</property>
- <style>
- <class name="bold16"/>
- </style>
- </object>
- <packing>
- <property name="left-attach">0</property>
- <property name="top-attach">0</property>
- </packing>
- </child>
- <child>
- <object class="GtkBox">
- <property name="visible">True</property>
- <property name="can-focus">False</property>
- <property name="homogeneous">True</property>
- <child>
- <object class="GtkButton">
- <property name="label" translatable="yes">_Cancel</property>
- <property name="visible">True</property>
- <property name="can-focus">True</property>
- <property name="receives-default">True</property>
- <property name="halign">start</property>
- <property name="valign">start</property>
- <property name="use-underline">True</property>
- <signal name="clicked" handler="_on_page_cancel_clicked" swapped="no"/>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">True</property>
- <property name="position">0</property>
- </packing>
- </child>
- <child>
- <object class="GtkButton" id="destroy_button">
- <property name="label" translatable="yes">_Destroy</property>
- <property name="visible">True</property>
- <property name="can-focus">True</property>
- <property name="can-default">True</property>
- <property name="receives-default">True</property>
- <property name="halign">end</property>
- <property name="valign">start</property>
- <property name="use-underline">True</property>
- <signal name="clicked" handler="_on_destroy_confirm" swapped="no"/>
- <style>
- <class name="destructive-action"/>
- </style>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">True</property>
- <property name="position">1</property>
- </packing>
- </child>
- </object>
- <packing>
- <property name="left-attach">0</property>
- <property name="top-attach">5</property>
- </packing>
- </child>
- <child>
- <object class="GtkEntry" id="destroy_alternate_entry">
- <property name="width-request">200</property>
- <property name="visible">True</property>
- <property name="can-focus">True</property>
- <property name="margin-bottom">6</property>
- <property name="activates-default">True</property>
- <property name="caps-lock-warning">False</property>
- <property name="secondary-icon-tooltip-text" translatable="yes">Insert Emoji</property>
- <property name="placeholder-text" translatable="yes">Alternate venue (optional)...</property>
- <signal name="notify::text" handler="_on_destroy_alternate_changed" swapped="no"/>
- </object>
- <packing>
- <property name="left-attach">0</property>
- <property name="top-attach">4</property>
- </packing>
- </child>
- <child>
- <object class="GtkEntry" id="destroy_reason_entry">
- <property name="width-request">200</property>
- <property name="visible">True</property>
- <property name="can-focus">True</property>
- <property name="margin-bottom">6</property>
- <property name="activates-default">True</property>
- <property name="caps-lock-warning">False</property>
- <property name="secondary-icon-tooltip-text" translatable="yes">Insert Emoji</property>
- <property name="placeholder-text" translatable="yes">Reason (optional)...</property>
- <property name="show-emoji-icon">True</property>
- </object>
- <packing>
- <property name="left-attach">0</property>
- <property name="top-attach">2</property>
- </packing>
- </child>
- <child>
- <object class="GtkLabel">
- <property name="visible">True</property>
- <property name="can-focus">False</property>
- <property name="halign">start</property>
- <property name="label" translatable="yes">Reason for destruction</property>
- <style>
- <class name="dim-label"/>
- </style>
- </object>
- <packing>
- <property name="left-attach">0</property>
- <property name="top-attach">1</property>
- </packing>
- </child>
- <child>
- <object class="GtkLabel">
- <property name="visible">True</property>
- <property name="can-focus">False</property>
- <property name="halign">start</property>
- <property name="label" translatable="yes">Where participants should go</property>
- <style>
- <class name="dim-label"/>
- </style>
- </object>
- <packing>
- <property name="left-attach">0</property>
- <property name="top-attach">3</property>
- </packing>
- </child>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">True</property>
- <property name="position">0</property>
- </packing>
- </child>
- </object>
- <packing>
- <property name="name">destroy</property>
- <property name="position">8</property>
- </packing>
- </child>
- <child>
- <object class="GtkBox">
- <property name="visible">True</property>
- <property name="can-focus">False</property>
- <property name="orientation">vertical</property>
- <child>
- <!-- n-columns=1 n-rows=2 -->
- <object class="GtkGrid" id="invite_grid">
- <property name="visible">True</property>
- <property name="can-focus">False</property>
- <property name="halign">center</property>
- <property name="hexpand">True</property>
- <property name="vexpand">True</property>
- <property name="border-width">18</property>
- <property name="row-spacing">6</property>
- <property name="column-spacing">12</property>
- <child>
- <object class="GtkBox">
- <property name="visible">True</property>
- <property name="can-focus">False</property>
- <property name="margin-top">6</property>
- <property name="spacing">12</property>
- <child>
- <object class="GtkButton">
- <property name="label" translatable="yes">_Cancel</property>
- <property name="visible">True</property>
- <property name="can-focus">True</property>
- <property name="receives-default">True</property>
- <property name="use-underline">True</property>
- <signal name="clicked" handler="_on_page_cancel_clicked" swapped="no"/>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">True</property>
- <property name="position">0</property>
- </packing>
- </child>
- <child>
- <object class="GtkButton" id="invite_button">
- <property name="label" translatable="yes">_Invite</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_invite_clicked" swapped="no"/>
- <style>
- <class name="suggested-action"/>
- </style>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">True</property>
- <property name="pack-type">end</property>
- <property name="position">1</property>
- </packing>
- </child>
- </object>
- <packing>
- <property name="left-attach">0</property>
- <property name="top-attach">1</property>
- </packing>
- </child>
- <child>
- <placeholder/>
- </child>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">True</property>
- <property name="position">1</property>
- </packing>
- </child>
- </object>
- <packing>
- <property name="name">invite</property>
- <property name="position">9</property>
- </packing>
- </child>
- <child>
- <placeholder/>
- </child>
- <child>
- <placeholder/>
- </child>
- </object>
- <packing>
- <property name="index">-1</property>
- </packing>
- </child>
- </object>
- <packing>
- <property name="expand">True</property>
- <property name="fill">True</property>
- <property name="position">1</property>
- </packing>
- </child>
- </object>
- <object class="GtkPopover" id="visitor_popover">
- <property name="can-focus">False</property>
- <property name="relative-to">visitor_menu_button</property>
- <property name="position">bottom</property>
- <child>
- <object class="GtkBox">
- <property name="visible">True</property>
- <property name="can-focus">False</property>
- <property name="orientation">vertical</property>
- <property name="spacing">6</property>
- <child>
- <object class="GtkLabel">
- <property name="visible">True</property>
- <property name="can-focus">False</property>
- <property name="label" translatable="yes">You are a visitor</property>
- <style>
- <class name="bold"/>
- </style>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">True</property>
- <property name="position">0</property>
- </packing>
- </child>
- <child>
- <object class="GtkLabel">
- <property name="visible">True</property>
- <property name="can-focus">False</property>
- <property name="label" translatable="yes">In order to write messages in this chat, you need to request voice first.
-A moderator will process your request.</property>
- <property name="wrap">True</property>
- <property name="max-width-chars">30</property>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">True</property>
- <property name="position">1</property>
- </packing>
- </child>
- <child>
- <object class="GtkButton">
- <property name="label" translatable="yes">_Request</property>
- <property name="visible">True</property>
- <property name="can-focus">True</property>
- <property name="receives-default">True</property>
- <property name="halign">center</property>
- <property name="use-underline">True</property>
- <signal name="clicked" handler="_on_request_voice_clicked" swapped="no"/>
- <style>
- <class name="suggested-action"/>
- </style>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">True</property>
- <property name="pack-type">end</property>
- <property name="position">2</property>
- </packing>
- </child>
- <style>
- <class name="padding-6"/>
- </style>
- </object>
- </child>
- </object>
-</interface>
diff --git a/gajim/data/gui/main.ui b/gajim/data/gui/main.ui
index 6a4609f49..f13ecd504 100644
--- a/gajim/data/gui/main.ui
+++ b/gajim/data/gui/main.ui
@@ -42,7 +42,7 @@
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="orientation">vertical</property>
- <property name="spacing">12</property>
+ <property name="spacing">5</property>
<child>
<placeholder/>
</child>
diff --git a/gajim/data/gui/message_actions_box.ui b/gajim/data/gui/message_actions_box.ui
new file mode 100644
index 000000000..29e94a00b
--- /dev/null
+++ b/gajim/data/gui/message_actions_box.ui
@@ -0,0 +1,249 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Generated with glade 3.38.2 -->
+<interface>
+ <requires lib="gtk+" version="3.24"/>
+ <object class="GtkBox" id="box">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
+ <property name="hexpand">True</property>
+ <property name="spacing">2</property>
+ <child>
+ <object class="GtkButton" id="quick_invite_button">
+ <property name="visible">True</property>
+ <property name="can-focus">True</property>
+ <property name="receives-default">True</property>
+ <property name="tooltip-text" translatable="yes">Invite Contacts…</property>
+ <property name="action-name">win.invite</property>
+ <property name="relief">none</property>
+ <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>
+ <style>
+ <class name="chatcontrol-actionbar-button"/>
+ <class name="flat"/>
+ </style>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="pack-type">end</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkMenuButton" id="settings_menu">
+ <property name="visible">True</property>
+ <property name="can-focus">True</property>
+ <property name="receives-default">True</property>
+ <property name="relief">none</property>
+ <child>
+ <object class="GtkImage">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="icon-name">open-menu-symbolic</property>
+ </object>
+ </child>
+ <style>
+ <class name="chatcontrol-actionbar-button"/>
+ </style>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="pack-type">end</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton" id="encryption_details_button">
+ <property name="can-focus">True</property>
+ <property name="focus-on-click">False</property>
+ <property name="receives-default">True</property>
+ <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
+ <property name="no-show-all">True</property>
+ <property name="relief">none</property>
+ <signal name="clicked" handler="_on_encryption_details_clicked" swapped="no"/>
+ <child>
+ <object class="GtkImage" id="encryption_details_image">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="icon_size">1</property>
+ </object>
+ </child>
+ <style>
+ <class name="chatcontrol-actionbar-button"/>
+ </style>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="pack-type">end</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkMenuButton" id="encryption_menu_button">
+ <property name="visible">True</property>
+ <property name="can-focus">True</property>
+ <property name="receives-default">True</property>
+ <property name="tooltip-text" translatable="yes">Choose encryption</property>
+ <property name="relief">none</property>
+ <child>
+ <object class="GtkImage" id="encryption_image">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="icon-name">channel-insecure-symbolic</property>
+ </object>
+ </child>
+ <style>
+ <class name="chatcontrol-actionbar-button"/>
+ </style>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="pack-type">end</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton" id="sendfile_button">
+ <property name="visible">True</property>
+ <property name="can-focus">True</property>
+ <property name="focus-on-click">False</property>
+ <property name="receives-default">True</property>
+ <property name="action-name">win.send-file</property>
+ <property name="relief">none</property>
+ <child>
+ <object class="GtkImage">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="icon-name">mail-attachment-symbolic</property>
+ </object>
+ </child>
+ <style>
+ <class name="chatcontrol-actionbar-button"/>
+ </style>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="pack-type">end</property>
+ <property name="position">3</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkMenuButton" id="emoticons_button">
+ <property name="visible">True</property>
+ <property name="can-focus">True</property>
+ <property name="receives-default">True</property>
+ <property name="tooltip-text" translatable="yes">Show a list of emojis</property>
+ <property name="relief">none</property>
+ <child>
+ <object class="GtkImage">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="icon-name">face-smile-symbolic</property>
+ </object>
+ </child>
+ <style>
+ <class name="chatcontrol-actionbar-button"/>
+ </style>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">5</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton" id="send_message_button">
+ <property name="can-focus">True</property>
+ <property name="focus-on-click">False</property>
+ <property name="receives-default">True</property>
+ <property name="no-show-all">True</property>
+ <property name="tooltip-text" translatable="yes">Send Message</property>
+ <property name="action-name">win.send-message</property>
+ <property name="relief">none</property>
+ <child>
+ <object class="GtkImage">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="icon-name">document-send</property>
+ </object>
+ </child>
+ <style>
+ <class name="chatcontrol-actionbar-button"/>
+ </style>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="pack-type">end</property>
+ <property name="position">6</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkMenuButton" id="formattings_button">
+ <property name="visible">True</property>
+ <property name="can-focus">True</property>
+ <property name="focus-on-click">False</property>
+ <property name="receives-default">True</property>
+ <property name="tooltip-text" translatable="yes">Format your message</property>
+ <property name="relief">none</property>
+ <property name="direction">up</property>
+ <child>
+ <object class="GtkImage">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="icon-name">format-text-bold-symbolic</property>
+ <property name="icon_size">1</property>
+ </object>
+ </child>
+ <style>
+ <class name="chatcontrol-actionbar-button"/>
+ </style>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">7</property>
+ </packing>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <object class="GtkScrolledWindow" id="input_scrolled">
+ <property name="visible">True</property>
+ <property name="can-focus">True</property>
+ <property name="margin-start">3</property>
+ <property name="margin-end">3</property>
+ <property name="shadow-type">in</property>
+ <property name="overlay-scrolling">False</property>
+ <property name="max-content-height">100</property>
+ <property name="propagate-natural-height">True</property>
+ <child>
+ <placeholder/>
+ </child>
+ <style>
+ <class name="message-input-border"/>
+ <class name="scrolled-no-border"/>
+ <class name="no-scroll-indicator"/>
+ <class name="scrollbar-style"/>
+ <class name="one-line-scrollbar"/>
+ </style>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">9</property>
+ </packing>
+ </child>
+ </object>
+</interface>
diff --git a/gajim/data/gui/shortcuts_window.ui b/gajim/data/gui/shortcuts_window.ui
index 8b173e7fc..fd4c4743c 100644
--- a/gajim/data/gui/shortcuts_window.ui
+++ b/gajim/data/gui/shortcuts_window.ui
@@ -103,34 +103,6 @@
<child>
<object class="GtkShortcutsShortcut">
<property name="visible">1</property>
- <property name="accelerator">&lt;primary&gt;Up</property>
- <property name="title" translatable="yes">Previously sent message</property>
- </object>
- </child>
- <child>
- <object class="GtkShortcutsShortcut">
- <property name="visible">1</property>
- <property name="accelerator">&lt;primary&gt;Down</property>
- <property name="title" translatable="yes">Next sent messages</property>
- </object>
- </child>
- <child>
- <object class="GtkShortcutsShortcut">
- <property name="visible">1</property>
- <property name="accelerator">&lt;primary&gt;&lt;shift&gt;Up</property>
- <property name="title" translatable="yes">Quote previous message</property>
- </object>
- </child>
- <child>
- <object class="GtkShortcutsShortcut">
- <property name="visible">1</property>
- <property name="accelerator">&lt;primary&gt;&lt;shift&gt;Down</property>
- <property name="title" translatable="yes">Quote next message</property>
- </object>
- </child>
- <child>
- <object class="GtkShortcutsShortcut">
- <property name="visible">1</property>
<property name="accelerator">&lt;primary&gt;u</property>
<property name="title" translatable="yes">Clear message entry</property>
</object>
diff --git a/gajim/data/other/shortcuts.json b/gajim/data/other/shortcuts.json
index 2cc0887e8..9c3feeb64 100644
--- a/gajim/data/other/shortcuts.json
+++ b/gajim/data/other/shortcuts.json
@@ -15,7 +15,7 @@
"win.show-contact-info": ["<Primary>I"],
"win.show-emoji-chooser": ["<Primary><Shift>M"],
"win.clear-chat": ["<Primary>L"],
- "win.delete-line": ["<Primary>U"],
+ "win.input-clear": ["<Primary>U"],
"win.close-tab": ["<Primary>W"],
"win.switch-next-tab": ["<Primary>Page_Down"],
"win.switch-prev-tab": ["<Primary>Page_Up"],
diff --git a/gajim/data/style/gajim.css b/gajim/data/style/gajim.css
index 58c4ee7f6..24ec7eed7 100644
--- a/gajim/data/style/gajim.css
+++ b/gajim/data/style/gajim.css
@@ -6,6 +6,8 @@
border: none;
}
+.message-action-box { margin: 8px 13px 8px 8px }
+
.no-border { border: none; }
.scrolled-no-border { border: none; }
.no-scroll-indicator undershoot.top, undershoot.bottom { background-image: none; }
@@ -184,10 +186,10 @@ row:backdrop { transition: none; }
padding: 4px;
}
.app-page-box {
- margin-top: 6px;
+ margin-top: 3px;
}
.app-page-box > separator {
- background-color: darker(@theme_bg_color);
+ background-color: @unfocused_borders;
margin-left: 12px;
margin-right: 12px;
}
diff --git a/gajim/gtk/account_page.py b/gajim/gtk/account_page.py
index 6909e748b..bccf11b42 100644
--- a/gajim/gtk/account_page.py
+++ b/gajim/gtk/account_page.py
@@ -75,7 +75,7 @@ class AccountPage(Gtk.Box, EventHelper):
roster_menu = Gio.Menu()
for action, label in ROSTER_MENU_DICT.items():
- roster_menu.append(label, f'win.{action}-{account}')
+ roster_menu.append(label, f'win.{action}')
self._ui.roster_menu_button.set_menu_model(roster_menu)
self._ui.connect_signals(self)
diff --git a/gajim/gtk/accounts.py b/gajim/gtk/accounts.py
index bd68ff189..8a17044e8 100644
--- a/gajim/gtk/accounts.py
+++ b/gajim/gtk/accounts.py
@@ -872,8 +872,6 @@ class PrivacyPage(GenericSettingPage):
app.settings.set_contact_settings('send_marker', None)
app.settings.set_group_chat_settings(
'send_marker', None, context='private')
- for ctrl in app.window.get_controls(account=self._account):
- ctrl.update_actions()
class ConnectionPage(GenericSettingPage):
diff --git a/gajim/gtk/application.py b/gajim/gtk/application.py
index 89ed1ce01..c3d6dd106 100644
--- a/gajim/gtk/application.py
+++ b/gajim/gtk/application.py
@@ -526,6 +526,8 @@ class GajimApplication(Gtk.Application, CoreApplication):
elif event.feature == Namespace.BLOCKING:
action = '%s-blocking' % event.account
self.set_action_state(action, True)
+ action = '%s-block-contact' % event.account
+ self.set_action_state(action, True)
# Action Callbacks
diff --git a/gajim/gtk/builder.pyi b/gajim/gtk/builder.pyi
index 690e78b7d..4b2b926f1 100644
--- a/gajim/gtk/builder.pyi
+++ b/gajim/gtk/builder.pyi
@@ -195,32 +195,26 @@ class CertificateBuilder(Builder):
image1: Gtk.Image
-class ChatControlBuilder(Builder):
- drop_area: Gtk.Box
- formattings_menu: Gtk.Menu
- bold: Gtk.MenuItem
- italic: Gtk.MenuItem
- strike: Gtk.MenuItem
- chat_control_hbox: Gtk.Box
- overlay: Gtk.Overlay
- textview_box: Gtk.Box
- banner_eventbox: Gtk.EventBox
- avatar_eventbox: Gtk.EventBox
+class ChatBannerBuilder(Builder):
+ banner_box: Gtk.Box
avatar_image: Gtk.Image
- banner_name_label: Gtk.Label
- banner_label: Gtk.Label
+ name_label: Gtk.Label
phone_image: Gtk.Image
+ toggle_roster_button: Gtk.Button
+ toggle_roster_image: Gtk.Image
account_badge_box: Gtk.Box
+ visitor_box: Gtk.Box
+ visitor_menu_button: Gtk.MenuButton
+ visitor_popover: Gtk.Popover
+
+
+class ChatControlBuilder(Builder):
+ control_box: Gtk.Box
+ overlay: Gtk.Overlay
+ conv_view_box: Gtk.Box
conv_view_overlay: Gtk.Overlay
- hbox: Gtk.Box
- emoticons_button: Gtk.MenuButton
- formattings_button: Gtk.MenuButton
- settings_menu: Gtk.MenuButton
- authentication_button: Gtk.Button
- lock_image: Gtk.Image
- encryption_menu: Gtk.MenuButton
- sendfile_button: Gtk.Button
- send_message_button: Gtk.Button
+ roster_revealer: Gtk.Revealer
+ drop_area: Gtk.Box
class ChatListRowBuilder(Builder):
@@ -395,65 +389,6 @@ class GroupchatConfigBuilder(Builder):
error_label: Gtk.Label
-class GroupchatControlBuilder(Builder):
- drop_area: Gtk.Box
- formattings_menu: Gtk.Menu
- bold: Gtk.MenuItem
- italic: Gtk.MenuItem
- strike: Gtk.MenuItem
- groupchat_control_hbox: Gtk.Box
- overlay: Gtk.Overlay
- stack: Gtk.Stack
- groupchat_control_vbox: Gtk.Box
- textview_box: Gtk.Box
- banner_eventbox: Gtk.EventBox
- avatar_image: Gtk.Image
- banner_name_label: Gtk.Label
- visitor_box: Gtk.Box
- visitor_menu_button: Gtk.MenuButton
- account_badge_box: Gtk.Box
- toggle_roster_button: Gtk.Button
- toggle_roster_image: Gtk.Image
- conv_view_overlay: Gtk.Overlay
- hbox: Gtk.Box
- quick_invite_button: Gtk.Button
- settings_menu: Gtk.MenuButton
- authentication_button: Gtk.Button
- lock_image: Gtk.Image
- encryption_menu: Gtk.MenuButton
- sendfile_button: Gtk.Button
- emoticons_button: Gtk.MenuButton
- send_message_button: Gtk.Button
- formattings_button: Gtk.MenuButton
- roster_revealer: Gtk.Revealer
- nickname_entry: Gtk.Entry
- nickname_change_button: Gtk.Button
- password_set_button: Gtk.Button
- password_entry: Gtk.Entry
- captcha_set_button: Gtk.Button
- captcha_box: Gtk.Box
- remove_bookmark_button: Gtk.Button
- retry_button: Gtk.Button
- close_button: Gtk.Button
- error_label: Gtk.Label
- error_heading: Gtk.Label
- captcha_close_button: Gtk.Button
- captcha_try_again_button: Gtk.Button
- captcha_error_label: Gtk.Label
- kick_label: Gtk.Label
- kick_participant_button: Gtk.Button
- kick_reason_entry: Gtk.Entry
- ban_label: Gtk.Label
- ban_participant_button: Gtk.Button
- ban_reason_entry: Gtk.Entry
- destroy_button: Gtk.Button
- destroy_alternate_entry: Gtk.Entry
- destroy_reason_entry: Gtk.Entry
- invite_grid: Gtk.Grid
- invite_button: Gtk.Button
- visitor_popover: Gtk.Popover
-
-
class GroupchatCreationBuilder(Builder):
account_liststore: Gtk.ListStore
stack: Gtk.Stack
@@ -652,6 +587,21 @@ class ManageSoundsBuilder(Builder):
filechooser: Gtk.FileChooserButton
+class MessageActionsBoxBuilder(Builder):
+ box: Gtk.Box
+ quick_invite_button: Gtk.Button
+ settings_menu: Gtk.MenuButton
+ encryption_details_button: Gtk.Button
+ encryption_details_image: Gtk.Image
+ encryption_menu_button: Gtk.MenuButton
+ encryption_image: Gtk.Image
+ sendfile_button: Gtk.Button
+ emoticons_button: Gtk.MenuButton
+ send_message_button: Gtk.Button
+ formattings_button: Gtk.MenuButton
+ input_scrolled: Gtk.ScrolledWindow
+
+
class PasswordDialogBuilder(Builder):
pass_box: Gtk.Box
header: Gtk.Label
@@ -1002,6 +952,8 @@ def get_builder(file_name: Literal['call_window.ui'], widgets: list[str] = ...)
@overload
def get_builder(file_name: Literal['certificate.ui'], widgets: list[str] = ...) -> CertificateBuilder: ...
@overload
+def get_builder(file_name: Literal['chat_banner.ui'], widgets: list[str] = ...) -> ChatBannerBuilder: ...
+@overload
def get_builder(file_name: Literal['chat_control.ui'], widgets: list[str] = ...) -> ChatControlBuilder: ...
@overload
def get_builder(file_name: Literal['chat_list_row.ui'], widgets: list[str] = ...) -> ChatListRowBuilder: ...
@@ -1026,8 +978,6 @@ def get_builder(file_name: Literal['groupchat_affiliation.ui'], widgets: list[st
@overload
def get_builder(file_name: Literal['groupchat_config.ui'], widgets: list[str] = ...) -> GroupchatConfigBuilder: ...
@overload
-def get_builder(file_name: Literal['groupchat_control.ui'], widgets: list[str] = ...) -> GroupchatControlBuilder: ...
-@overload
def get_builder(file_name: Literal['groupchat_creation.ui'], widgets: list[str] = ...) -> GroupchatCreationBuilder: ...
@overload
def get_builder(file_name: Literal['groupchat_details.ui'], widgets: list[str] = ...) -> GroupchatDetailsBuilder: ...
@@ -1062,6 +1012,8 @@ def get_builder(file_name: Literal['manage_proxies.ui'], widgets: list[str] = ..
@overload
def get_builder(file_name: Literal['manage_sounds.ui'], widgets: list[str] = ...) -> ManageSoundsBuilder: ...
@overload
+def get_builder(file_name: Literal['message_actions_box.ui'], widgets: list[str] = ...) -> MessageActionsBoxBuilder: ...
+@overload
def get_builder(file_name: Literal['password_dialog.ui'], widgets: list[str] = ...) -> PasswordDialogBuilder: ...
@overload
def get_builder(file_name: Literal['plugins.ui'], widgets: list[str] = ...) -> PluginsBuilder: ...
diff --git a/gajim/gtk/chat_action_processor.py b/gajim/gtk/chat_action_processor.py
index 6dbe82571..d745e9f4e 100644
--- a/gajim/gtk/chat_action_processor.py
+++ b/gajim/gtk/chat_action_processor.py
@@ -38,11 +38,7 @@ MAX_ENTRIES = 5
class ChatActionProcessor(Gtk.Popover):
- def __init__(self,
- account: str,
- contact: types.ChatContactT,
- message_input: MessageInputTextView
- ) -> None:
+ def __init__(self, message_input: MessageInputTextView) -> None:
Gtk.Popover.__init__(self)
self._menu = Gio.Menu()
self.bind_model(self._menu)
@@ -53,23 +49,28 @@ class ChatActionProcessor(Gtk.Popover):
self.connect('closed', self._on_popover_closed)
self.connect('destroy', self._on_destroy)
+ self._account: Optional[str] = None
+ self._contact: Optional[types.ChatContactT] = None
+
self._message_input = message_input
self._message_input.connect('key-press-event', self._on_key_press)
self._buf = message_input.get_buffer()
self._buf.connect('changed', self._on_changed)
- self._nick_completion: Optional[GroupChatNickCompletion] = None
- if contact.is_groupchat:
- assert isinstance(contact, GroupchatContact)
- self._nick_completion = GroupChatNickCompletion(
- account, contact, message_input)
+ self._nick_completion = GroupChatNickCompletion()
self._start_mark: Optional[Gtk.TextMark] = None
self._current_iter: Optional[Gtk.TextIter] = None
self._active = False
+ def switch_contact(self, contact: types.ChatContactT) -> None:
+ self._account = contact.account
+ self._contact = contact
+ if isinstance(contact, GroupchatContact):
+ self._nick_completion.switch_contact(contact)
+
def _on_destroy(self, _popover: Gtk.Popover) -> None:
app.check_finalize(self)
@@ -77,7 +78,7 @@ class ChatActionProcessor(Gtk.Popover):
textview: MessageInputTextView,
event: Gdk.EventKey
) -> bool:
- if self._nick_completion is not None:
+ if isinstance(self._contact, GroupchatContact):
res = self._nick_completion.process_key_press(textview, event)
if res:
return True
@@ -122,8 +123,9 @@ class ChatActionProcessor(Gtk.Popover):
def _get_commands(self) -> list[str]:
commands: list[str] = []
- control = app.window.get_control(
- self._message_input.account, self._message_input.contact.jid)
+ assert self._account
+ assert self._contact
+ control = app.window.get_control(self._account, self._contact.jid)
assert control is not None
for command in control.list_commands():
for name in command.names:
@@ -324,7 +326,3 @@ class ChatActionProcessor(Gtk.Popover):
def _item_has_focus(item: Gtk.ModelButton) -> bool:
flags = item.get_state_flags()
return 'GTK_STATE_FLAG_FOCUSED' in str(flags)
-
- def process_outgoing_message(self, contact: str, highlight: bool) -> None:
- if self._nick_completion is not None:
- self._nick_completion.record_message(contact, highlight)
diff --git a/gajim/gtk/chat_banner.py b/gajim/gtk/chat_banner.py
new file mode 100644
index 000000000..b69f87e35
--- /dev/null
+++ b/gajim/gtk/chat_banner.py
@@ -0,0 +1,319 @@
+# 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 Any
+from typing import Optional
+
+from gi.repository import Gtk
+
+import cairo
+
+from nbxmpp.protocol import JID
+from nbxmpp.structs import PresenceProperties
+
+from gajim.common import app
+from gajim.common import ged
+from gajim.common import types
+from gajim.common.const import AvatarSize
+from gajim.common.const import SimpleClientState
+from gajim.common.events import AccountEnabled
+from gajim.common.events import BookmarksReceived
+from gajim.common.events import MessageReceived
+from gajim.common.events import MucDiscoUpdate
+from gajim.common.ged import EventHelper
+from gajim.common.helpers import get_uf_chatstate
+from gajim.common.i18n import _
+from gajim.common.modules.contacts import BareContact
+from gajim.common.modules.contacts import GroupchatContact
+from gajim.common.modules.contacts import GroupchatParticipant
+
+from .builder import get_builder
+from .util import AccountBadge
+
+
+class ChatBanner(Gtk.Box, EventHelper):
+ def __init__(self) -> None:
+ Gtk.Box.__init__(self)
+ EventHelper.__init__(self)
+
+ self._client: Optional[types.Client] = None
+ self._contact: Optional[types.ChatContactT] = None
+
+ self._ui = get_builder('chat_banner.ui')
+ self.add(self._ui.banner_box)
+ self._ui.connect_signals(self)
+
+ self._account_badge: Optional[AccountBadge] = None
+
+ hide_roster = app.settings.get('hide_groupchat_occupants_list')
+ self._set_toggle_roster_button_icon(hide_roster)
+ app.settings.connect_signal(
+ 'hide_groupchat_occupants_list',
+ self._set_toggle_roster_button_icon)
+
+ self.show_all()
+
+ def clear(self) -> None:
+ if self._contact is not None:
+ self._contact.disconnect_all_from_obj(self)
+ self._contact = None
+
+ if self._client is not None:
+ self._client.disconnect_all_from_obj(self)
+ self._client = None
+
+ self.unregister_events()
+
+ self._contact = None
+ self._client = None
+
+ def switch_contact(self,
+ account: str,
+ jid: JID
+ ) -> None:
+
+ self._update_account_badge(account)
+
+ if self._client is not None:
+ self._client.disconnect_all_from_obj(self)
+
+ self._client = app.get_client(account)
+ self._client.connect_signal('state-changed',
+ self._on_client_state_changed)
+
+ if self._contact is not None:
+ self._contact.disconnect_all_from_obj(self)
+
+ self._contact = self._client.get_module('Contacts').get_contact(jid)
+ self._contact.multi_connect({
+ 'chatstate-update': self._on_chatstate_update,
+ 'nickname-update': self._on_nickname_update,
+ 'avatar-update': self._on_avatar_update,
+ 'presence-update': self._on_presence_update,
+ 'caps-update': self._on_caps_update
+ })
+
+ if isinstance(self._contact, GroupchatContact):
+ self._contact.multi_connect({
+ 'user-role-changed': self._on_user_role_changed,
+ 'state-changed': self._on_muc_state_changed
+ })
+ self._ui.toggle_roster_button.show()
+ hide_banner = app.settings.get('hide_groupchat_banner')
+ else:
+ self._ui.toggle_roster_button.hide()
+ hide_banner = app.settings.get('hide_chat_banner')
+
+ if isinstance(self._contact, GroupchatParticipant):
+ self._contact.multi_connect({
+ 'user-joined': self._on_user_state_changed,
+ 'user-left': self._on_user_state_changed,
+ 'user-avatar-update': self._on_user_avatar_update,
+ 'user-nickname-changed': self._on_user_nickname_changed
+ })
+
+ self.register_events([
+ ('message-received', ged.GUI1, self._on_message_received),
+ ('bookmarks-received', ged.GUI1, self._on_bookmarks_received),
+ ('muc-disco-update', ged.GUI1, self._on_muc_disco_update),
+ ('account-enabled', ged.GUI2, self._on_account_changed),
+ ('account-disabled', ged.GUI2, self._on_account_changed)
+ ])
+
+ if hide_banner:
+ self.set_no_show_all(True)
+ self.hide()
+ else:
+ self.set_no_show_all(False)
+ self.show_all()
+
+ self._update_avatar()
+ self._update_content()
+
+ def _on_client_state_changed(self,
+ _client: types.Client,
+ _signal_name: str,
+ state: SimpleClientState):
+ self._update_avatar()
+ self._update_content()
+
+ def _on_presence_update(self,
+ _contact: types.BareContact,
+ _signal_name: str
+ ) -> None:
+ self._update_avatar()
+
+ def _on_chatstate_update(self,
+ _contact: types.BareContact,
+ _signal_name: str
+ ) -> None:
+ self._update_content()
+
+ def _on_nickname_update(self,
+ _contact: types.BareContact,
+ _signal_name: str
+ ) -> None:
+ self._update_content()
+
+ def _on_avatar_update(self,
+ _contact: types.BareContact,
+ _signal_name: str
+ ) -> None:
+ self._update_avatar()
+
+ def _on_caps_update(self,
+ _contact: types.BareContact,
+ _signal_name: str
+ ) -> None:
+ self._update_avatar()
+
+ def _on_muc_state_changed(self,
+ contact: GroupchatContact,
+ _signal_name: str
+ ) -> None:
+ if contact.is_joined:
+ self._update_content()
+
+ def _on_user_role_changed(self,
+ _contact: GroupchatContact,
+ _signal_name: str,
+ user_contact: GroupchatParticipant,
+ properties: PresenceProperties
+ ) -> None:
+ self._update_content()
+
+ def _on_user_nickname_changed(self,
+ user_contact: GroupchatParticipant,
+ _signal_name: str,
+ properties: PresenceProperties,
+ ) -> None:
+ if user_contact.name != properties.muc_nickname:
+ return
+
+ self._update_content()
+
+ def _on_user_state_changed(self, *args: Any) -> None:
+ self._update_avatar()
+
+ def _on_user_avatar_update(self, *args: Any) -> None:
+ self._update_avatar()
+
+ def _on_bookmarks_received(self, _event: BookmarksReceived) -> None:
+ self._update_content()
+
+ def _on_muc_disco_update(self, event: MucDiscoUpdate) -> None:
+ if self._contact is None or event.jid != self._contact.jid:
+ return
+
+ self._update_content()
+
+ def _on_account_changed(self, event: AccountEnabled) -> None:
+ self._update_account_badge(event.account)
+
+ def _on_message_received(self, event: MessageReceived) -> None:
+ if not event.msgtxt:
+ return
+
+ if event.properties.is_sent_carbon:
+ return
+
+ assert self._contact is not None
+
+ if event.jid != self._contact.jid:
+ return
+
+ self._ui.phone_image.set_visible(False)
+
+ if isinstance(self._contact, (
+ GroupchatContact, GroupchatParticipant)):
+ return
+
+ kind = 'outgoing' if event.properties.is_sent_carbon else 'incoming'
+ if kind == 'outgoing':
+ return
+
+ if event.resource is not None:
+ resource_contact = self._contact.get_resource(event.resource)
+ self._ui.phone_image.set_visible(resource_contact.is_phone)
+
+ def _update_avatar(self) -> None:
+ scale = app.window.get_scale_factor()
+ assert self._contact
+ surface = self._contact.get_avatar(AvatarSize.CHAT, scale)
+ assert isinstance(surface, cairo.Surface)
+ self._ui.avatar_image.set_from_surface(surface)
+
+ def _update_content(self) -> None:
+ if self._client is None or self._contact is None:
+ return
+
+ name = self._contact.name
+
+ if self._contact.jid.bare_match(self._client.get_own_jid()):
+ name = _('Note to myself')
+
+ if self._contact.is_pm_contact:
+ gc_contact = self._client.get_module('Contacts').get_contact(
+ self._contact.jid.bare)
+ name = f'{name} ({gc_contact.name})'
+
+ label_text = f'<span>{name}</span>'
+ label_tooltip = name
+ show_chatstate = app.settings.get('show_chatstate_in_banner')
+ if (show_chatstate and isinstance(
+ self._contact, (BareContact, GroupchatParticipant))):
+ chatstate = self._contact.chatstate
+ if chatstate is not None:
+ chatstate = get_uf_chatstate(chatstate.value)
+ else:
+ chatstate = ''
+
+ label_text = f'<span>{name}</span>' \
+ f'<span size="x-small" weight="light">' \
+ f' {chatstate}</span>'
+ label_tooltip = f'{name} {chatstate}'
+
+ self._ui.name_label.set_markup(label_text)
+ self._ui.name_label.set_tooltip_text(label_tooltip)
+
+ if isinstance(self._contact, GroupchatContact):
+ self_contact = self._contact.get_self()
+ if self_contact:
+ self._ui.visitor_box.set_visible(self_contact.role.is_visitor)
+
+ def _update_account_badge(self, account: str) -> None:
+ if self._account_badge is not None:
+ self._account_badge.destroy()
+
+ 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)
+
+ def _on_request_voice_clicked(self, _button: Gtk.Button) -> None:
+ self._ui.visitor_popover.popdown()
+ app.window.activate_action('muc-request-voice', None)
+
+ def _on_toggle_roster_clicked(self, _button: Gtk.Button) -> None:
+ state = app.settings.get('hide_groupchat_occupants_list')
+ app.settings.set('hide_groupchat_occupants_list', not state)
+
+ def _set_toggle_roster_button_icon(self,
+ show_roster: bool,
+ *args: Any) -> None:
+ icon = 'go-next-symbolic' if show_roster else 'go-previous-symbolic'
+ self._ui.toggle_roster_image.set_from_icon_name(
+ icon, Gtk.IconSize.BUTTON)
diff --git a/gajim/gtk/chat_function_page.py b/gajim/gtk/chat_function_page.py
new file mode 100644
index 000000000..53fe2fa82
--- /dev/null
+++ b/gajim/gtk/chat_function_page.py
@@ -0,0 +1,419 @@
+# 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 Optional
+
+from gi.repository import GObject
+from gi.repository import Gtk
+from gi.repository import Pango
+
+from nbxmpp.protocol import InvalidJid
+from nbxmpp.protocol import JID
+from nbxmpp.protocol import validate_resourcepart
+
+from gajim.common import app
+from gajim.common.client import Client
+from gajim.common.i18n import _
+from gajim.common.modules.contacts import GroupchatContact
+from gajim.common.types import ChatContactT
+
+from .dataform import DataFormWidget
+from .groupchat_inviter import GroupChatInviter
+
+
+class ChatFunctionPage(Gtk.Box):
+
+ __gsignals__ = {
+ 'finish': (GObject.SignalFlags.RUN_LAST, None, (bool,)),
+ 'message': (GObject.SignalFlags.RUN_LAST, None, (str,)),
+ }
+
+ def __init__(self) -> None:
+ Gtk.Box.__init__(self,
+ orientation=Gtk.Orientation.VERTICAL,
+ spacing=18)
+ self.set_halign(Gtk.Align.CENTER)
+ self.set_valign(Gtk.Align.CENTER)
+ self.get_style_context().add_class('padding-18')
+
+ self._client: Optional[Client] = None
+ self._contact: Optional[ChatContactT] = None
+ self._mode: Optional[str] = None
+
+ self._widget: Optional[Gtk.Widget] = None
+
+ self._heading = Gtk.Label()
+ self._heading.set_max_width_chars(30)
+ self._heading.set_ellipsize(Pango.EllipsizeMode.END)
+ self._heading.get_style_context().add_class('large-header')
+ self.add(self._heading)
+
+ self._content_box = Gtk.Box()
+ self._content_box.set_halign(Gtk.Align.CENTER)
+ self.add(self._content_box)
+
+ cancel_button = Gtk.Button(label=_('Cancel'))
+ cancel_button.connect('clicked', self._on_cancel_clicked)
+
+ self._forget_button = Gtk.Button(label=_('Forget Group Chat'))
+ self._forget_button.set_no_show_all(True)
+ self._forget_button.get_style_context().add_class(
+ 'destructive-action')
+ self._forget_button.connect('clicked', self._on_forget_clicked)
+
+ self._confirm_button = Gtk.Button()
+ self._confirm_button.set_can_default(True)
+ self._confirm_button.connect('clicked', self._on_confirm_clicked)
+
+ button_box = Gtk.Box(spacing=18)
+ button_box.pack_start(cancel_button, False, True, 0)
+ button_box.pack_end(self._forget_button, False, True, 0)
+ button_box.pack_end(self._confirm_button, False, True, 0)
+
+ self.add(button_box)
+
+ def process_escape(self) -> None:
+ close_control = self._mode in (
+ 'join-failed',
+ 'creation-failed',
+ 'config-failed',
+ 'captcha-error')
+ self.emit('finish', close_control)
+
+ def _clear(self) -> None:
+ for child in self._content_box.get_children():
+ child.destroy()
+
+ def set_mode(self,
+ contact: ChatContactT,
+ mode: str,
+ data: Optional[str] = None
+ ) -> None:
+ self._clear()
+ self._confirm_button.get_style_context().remove_class(
+ 'destructive-action')
+ self._confirm_button.get_style_context().remove_class(
+ 'suggested-action')
+ self._confirm_button.set_sensitive(False)
+ self._confirm_button.grab_default()
+
+ self._forget_button.hide()
+
+ self._contact = contact
+ self._heading.set_text(self._contact.name)
+ self._client = app.get_client(contact.account)
+ self._mode = mode
+ self._data = data
+
+ if self._widget is not None:
+ self._widget.destroy()
+
+ if mode == 'invite':
+ self._confirm_button.set_label(_('Invite'))
+ self._confirm_button.get_style_context().add_class(
+ 'suggested-action')
+ self._widget = GroupChatInviter(str(contact.jid))
+ self._widget.set_size_request(-1, 500)
+ self._widget.connect('listbox-changed', self._on_ready)
+ self._widget.load_contacts()
+
+ elif mode == 'change-nickname':
+ self._confirm_button.set_label(_('Change'))
+ self._confirm_button.get_style_context().add_class(
+ 'suggested-action')
+ self._widget = InputWidget(self._contact, mode)
+ self._widget.connect('changed', self._on_ready)
+
+ elif mode == 'kick':
+ self._confirm_button.set_label(_('Kick'))
+ self._confirm_button.set_sensitive(True)
+ self._confirm_button.get_style_context().add_class(
+ 'destructive-action')
+ self._widget = InputWidget(self._contact, mode, data)
+
+ elif mode == 'ban':
+ self._confirm_button.set_label(_('Ban'))
+ self._confirm_button.set_sensitive(True)
+ self._confirm_button.get_style_context().add_class(
+ 'destructive-action')
+ self._widget = InputWidget(self._contact, mode, data)
+
+ elif mode == 'password-request':
+ self._confirm_button.set_label(_('Join'))
+ self._confirm_button.get_style_context().add_class(
+ 'suggested-action')
+ self._widget = InputWidget(self._contact, mode)
+ self._widget.connect('changed', self._on_ready)
+
+ elif mode == 'captcha-request':
+ self._confirm_button.set_label(_('Join'))
+ self._confirm_button.get_style_context().add_class(
+ 'suggested-action')
+ muc_data = self._client.get_module('MUC').get_muc_data(
+ self._contact.jid)
+ form = muc_data.captcha_form
+ options = {'no-scrolling': True,
+ 'entry-activates-default': True}
+ self._widget = DataFormWidget(form, options=options)
+ self._widget.set_valign(Gtk.Align.START)
+ self._widget.show_all()
+ self._widget.connect('is-valid', self._on_ready)
+
+ elif mode == 'captcha-error':
+ self._confirm_button.set_label(_('Try Again'))
+ self._confirm_button.set_sensitive(True)
+ self._confirm_button.get_style_context().add_class(
+ 'suggested-action')
+ self._widget = ErrorWidget(error_text=data)
+
+ elif mode in ('join-failed', 'creation-failed', 'config-failed'):
+ self._confirm_button.set_label(_('Try Again'))
+ self._confirm_button.set_sensitive(True)
+ self._confirm_button.get_style_context().add_class(
+ 'suggested-action')
+ if mode == 'join-failed':
+ is_bookmark = self._client.get_module('Bookmarks').is_bookmark(
+ self._contact.jid)
+ self._forget_button.set_visible(is_bookmark)
+ self._widget = ErrorWidget(error_mode=mode, error_text=data)
+
+ assert self._widget is not None
+ self._content_box.add(self._widget)
+ if isinstance(self._widget, InputWidget):
+ self._widget.focus()
+ elif isinstance(self._widget, DataFormWidget):
+ self._widget.focus_first_entry()
+
+ def _on_ready(self,
+ _widget: Gtk.Widget,
+ state: bool
+ ) -> None:
+ self._confirm_button.set_sensitive(state)
+
+ def _on_confirm_clicked(self, _button: Gtk.Button) -> None:
+ if self._mode == 'invite':
+ assert isinstance(self._widget, GroupChatInviter)
+ invitees = self._widget.get_invitees()
+ for jid in invitees:
+ self._invite(JID.from_string(jid))
+
+ elif self._mode == 'change-nickname':
+ assert isinstance(self._widget, InputWidget)
+ nickname = self._widget.get_text()
+ assert self._client is not None
+ assert self._contact is not None
+ self._client.get_module('MUC').change_nick(
+ self._contact.jid, nickname)
+
+ elif self._mode == 'kick':
+ assert isinstance(self._widget, InputWidget)
+ reason = self._widget.get_text()
+ assert self._client is not None
+ assert self._contact is not None
+ self._client.get_module('MUC').set_role(
+ self._contact.jid, self._data, 'none', reason)
+
+ elif self._mode == 'ban':
+ assert isinstance(self._widget, InputWidget)
+ reason = self._widget.get_text()
+ assert self._client is not None
+ assert self._contact is not None
+ self._client.get_module('MUC').set_affiliation(
+ self._contact.jid,
+ {self._data: {'affiliation': 'outcast', 'reason': reason}})
+
+ elif self._mode == 'password-request':
+ assert isinstance(self._widget, InputWidget)
+ password = self._widget.get_text()
+ assert self._client is not None
+ assert self._contact is not None
+ self._client.get_module('MUC').set_password(
+ self._contact.jid, password)
+ self._client.get_module('MUC').join(self._contact.jid)
+
+ elif self._mode == 'captcha-request':
+ assert isinstance(self._widget, DataFormWidget)
+ form_node = self._widget.get_submit_form()
+ assert self._client is not None
+ assert self._contact is not None
+ self._client.get_module('MUC').send_captcha(
+ self._contact.jid, form_node)
+
+ elif self._mode in ('join-failed', 'captcha-error'):
+ assert self._client is not None
+ assert self._contact is not None
+ self._client.get_module('MUC').join(self._contact.jid)
+
+ self.emit('finish', False)
+
+ def _on_cancel_clicked(self, _button: Gtk.Button) -> None:
+ assert self._client is not None
+ assert self._contact is not None
+
+ if self._mode == 'captcha-request':
+ self._client.get_module('MUC').cancel_captcha(
+ self._contact.jid)
+ self.emit('finish', True)
+ return
+
+ if self._mode == 'password-request':
+ self._client.get_module('MUC').cancel_password_request(
+ self._contact.jid)
+ self.emit('finish', True)
+ return
+
+ if self._mode in (
+ 'join-failed',
+ 'creation-failed',
+ 'config-failed',
+ 'captcha-error'):
+ self.emit('finish', True)
+ return
+
+ self.emit('finish', False)
+
+ def _on_forget_clicked(self, _button: Gtk.Button) -> None:
+ assert self._client is not None
+ assert self._contact is not None
+ self._client.get_module('Bookmarks').remove(self._contact.jid)
+ self.emit('finish', True)
+
+ def _invite(self, invited_jid: JID) -> None:
+ assert self._contact is not None
+ client = app.get_client(self._contact.account)
+ client.get_module('MUC').invite(
+ self._contact.jid, invited_jid)
+ invited_contact = client.get_module('Contacts').get_contact(
+ invited_jid)
+ self.emit(
+ 'message',
+ _('%s has been invited to this group chat') % invited_contact.name)
+
+
+class InputWidget(Gtk.Box):
+
+ __gsignals__ = {
+ 'changed': (GObject.SignalFlags.RUN_LAST, None, (bool,)),
+ }
+
+ def __init__(self,
+ contact: ChatContactT,
+ mode: str,
+ data: Optional[str] = None
+ ) -> None:
+ Gtk.Box.__init__(self,
+ orientation=Gtk.Orientation.VERTICAL,
+ spacing=12)
+ self._contact = contact
+ self._mode = mode
+
+ heading_label = Gtk.Label()
+ heading_label.set_xalign(0)
+ heading_label.get_style_context().add_class('bold16')
+ self.add(heading_label)
+
+ sub_label = Gtk.Label()
+ sub_label.set_xalign(0)
+ sub_label.get_style_context().add_class('dim-label')
+ self.add(sub_label)
+
+ self._entry = Gtk.Entry()
+ self._entry.set_activates_default(True)
+ self._entry.set_size_request(300, -1)
+ self._entry.connect('changed', self._on_entry_changed)
+ self.add(self._entry)
+
+ if mode == 'change-nickname':
+ heading_label.set_text(_('Change Nickname'))
+ sub_label.set_text(_('Enter your new nickname'))
+ assert isinstance(self._contact, GroupchatContact)
+ self._entry.set_text(self._contact.nickname or '')
+
+ elif mode == 'kick':
+ heading_label.set_text(_('Kick %s') % data)
+ sub_label.set_text(_('Reason (optional)'))
+
+ elif mode == 'ban':
+ heading_label.set_text(_('Ban %s') % data)
+ sub_label.set_text(_('Reason (optional)'))
+
+ elif mode == 'password-request':
+ heading_label.set_text(_('Password Required'))
+ sub_label.set_text(_('Enter a password to join this chat'))
+ self._entry.set_input_purpose(Gtk.InputPurpose.PASSWORD)
+ self._entry.set_visibility(False)
+
+ self.connect('destroy', self._on_destroy)
+ self.show_all()
+
+ def _on_destroy(self, _widget: InputWidget) -> None:
+ app.check_finalize(self)
+
+ def _on_entry_changed(self, entry: Gtk.Entry) -> None:
+ text = entry.get_text()
+
+ if self._mode == 'change-nickname':
+ assert isinstance(self._contact, GroupchatContact)
+ if not text or text == self._contact.nickname:
+ self.emit('changed', False)
+ return
+ try:
+ validate_resourcepart(text)
+ except InvalidJid:
+ self.emit('changed', False)
+ return
+
+ self.emit('changed', bool(text))
+
+ def focus(self) -> None:
+ self._entry.grab_focus()
+
+ def get_text(self) -> str:
+ return self._entry.get_text()
+
+
+class ErrorWidget(Gtk.Box):
+ def __init__(self,
+ error_mode: Optional[str] = None,
+ error_text: Optional[str] = None
+ ) -> None:
+ Gtk.Box.__init__(self,
+ orientation=Gtk.Orientation.VERTICAL,
+ spacing=12)
+ image = Gtk.Image.new_from_icon_name(
+ 'dialog-error-symbolic', Gtk.IconSize.DIALOG)
+ image.get_style_context().add_class('error-color')
+
+ heading = Gtk.Label()
+ heading.get_style_context().add_class('bold16')
+ heading_text = _('An Error Occurred')
+ if error_mode == 'join-failed':
+ heading_text = _('Failed to Join Group Chat')
+ elif error_mode == 'creation-failed':
+ heading_text = _('Failed to Create Group Chat')
+ elif error_mode == 'config-failed':
+ heading_text = _('Failed to Configure Group Chat')
+ heading.set_text(heading_text)
+
+ label = Gtk.Label()
+ label.set_max_width_chars(40)
+ if error_text is not None:
+ label.set_text(error_text)
+
+ self.add(image)
+ self.add(heading)
+ self.add(label)
+ self.show_all()
diff --git a/gajim/gtk/chat_list_stack.py b/gajim/gtk/chat_list_stack.py
index 2b43a74b4..b91bc9f49 100644
--- a/gajim/gtk/chat_list_stack.py
+++ b/gajim/gtk/chat_list_stack.py
@@ -269,7 +269,7 @@ class ChatListStack(Gtk.Stack):
_('Leave Group Chat'),
_('Are you sure you want to leave this group chat?'),
_('If you close this chat, you will leave '
- '\'%s\'.') % contact.name,
+ '"%s".') % contact.name,
_('_Do not ask me again'),
[DialogButton.make('Cancel'),
DialogButton.make('Accept',
diff --git a/gajim/gtk/chat_page.py b/gajim/gtk/chat_page.py
index 0504b409b..b6d39f6fc 100644
--- a/gajim/gtk/chat_page.py
+++ b/gajim/gtk/chat_page.py
@@ -121,6 +121,9 @@ class ChatPage(Gtk.Box):
def get_chat_list_stack(self) -> ChatListStack:
return self._chat_list_stack
+ def get_chat_stack(self) -> ChatStack:
+ return self._chat_stack
+
def get_control_stack(self) -> ControlStack:
return self._control_stack
diff --git a/gajim/gtk/chat_stack.py b/gajim/gtk/chat_stack.py
index 16b296f17..f5474577c 100644
--- a/gajim/gtk/chat_stack.py
+++ b/gajim/gtk/chat_stack.py
@@ -12,15 +12,42 @@
# 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 Any
+from typing import Optional
+import sys
import logging
+from gi.repository import Gdk
+from gi.repository import GLib
+from gi.repository import Gio
from gi.repository import Gtk
+
+from nbxmpp.errors import StanzaError
from nbxmpp.protocol import JID
+from nbxmpp.structs import MessageProperties
+from nbxmpp.structs import PresenceProperties
+from gajim.common import app
+from gajim.common import helpers
+from gajim.common.events import MucDiscoUpdate
+from gajim.common.i18n import _
+from gajim.common.const import CallType
+from gajim.common.modules.contacts import BareContact
+from gajim.common.modules.contacts import GroupchatContact
+from gajim.common.modules.contacts import GroupchatParticipant
+from gajim.common.structs import OutgoingMessage
+from gajim.common.types import ChatContactT
+
+from .chat_banner import ChatBanner
+from .chat_function_page import ChatFunctionPage
+from .const import TARGET_TYPE_URI_LIST
from .control_stack import ControlStack
-from .util import EventHelper
+from .controls.groupchat import GroupchatControl
+from .message_actions_box import MessageActionsBox
+from .util import EventHelper, open_window
log = logging.getLogger('gajim.gui.chatstack')
@@ -33,23 +60,621 @@ class ChatStack(Gtk.Stack, EventHelper):
self.set_vexpand(True)
self.set_hexpand(True)
+ self._current_contact: Optional[ChatContactT] = None
+
+ self.add_named(ChatPlaceholderBox(), 'empty')
+
+ self._chat_function_page = ChatFunctionPage()
+ self._chat_function_page.connect('finish', self._on_function_finished)
+ self._chat_function_page.connect('message', self._on_function_message)
+ self.add_named(self._chat_function_page, 'function')
+
+ self._chat_banner = ChatBanner()
self._control_stack = ControlStack()
+ self._message_action_box = MessageActionsBox()
box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
+ box.add(self._chat_banner)
+ box.add(Gtk.Separator())
box.add(self._control_stack)
+ box.add(Gtk.Separator())
+ box.add(self._message_action_box)
+
+ dnd_icon = Gtk.Image.new_from_icon_name(
+ 'mail-attachment-symbolic', Gtk.IconSize.DIALOG)
+ dnd_icon.set_vexpand(True)
+ dnd_icon.set_valign(Gtk.Align.END)
+ dnd_label = Gtk.Label(label=_('Drop files or contacts'))
+ dnd_label.set_max_width_chars(40)
+ dnd_label.set_vexpand(True)
+ dnd_label.set_valign(Gtk.Align.START)
+ dnd_label.get_style_context().add_class('bold16')
- self.add_named(box, 'controls')
+ self._drop_area = Gtk.Box(
+ orientation=Gtk.Orientation.VERTICAL, spacing=18)
+ self._drop_area.set_no_show_all(True)
+ self._drop_area.set_hexpand(True)
+ self._drop_area.set_vexpand(True)
+ self._drop_area.set_name('DropArea')
+ self._drop_area.add(dnd_icon)
+ self._drop_area.add(dnd_label)
+ overlay = Gtk.Overlay()
+ overlay.add_overlay(self._drop_area)
+ overlay.add(box)
+ overlay.connect('drag-data-received', self._on_drag_data_received)
+ overlay.connect('drag-motion', self._on_drag_motion)
+ overlay.connect('drag-leave', self._on_drag_leave)
+
+ uri_entry = Gtk.TargetEntry.new(
+ 'text/uri-list',
+ Gtk.TargetFlags.OTHER_APP,
+ TARGET_TYPE_URI_LIST)
+ dnd_list = [uri_entry,
+ Gtk.TargetEntry.new(
+ 'OBJECT_DROP',
+ Gtk.TargetFlags.SAME_APP,
+ 0)]
+ dst_targets = Gtk.TargetList.new([uri_entry])
+ dst_targets.add_text_targets(0)
+
+ overlay.drag_dest_set(
+ Gtk.DestDefaults.ALL,
+ dnd_list,
+ Gdk.DragAction.COPY | Gdk.DragAction.MOVE)
+ overlay.drag_dest_set_target_list(dst_targets)
+
+ self.add_named(overlay, 'controls')
+
+ self._connect_actions()
self.show_all()
+ def _get_current_contact(self) -> ChatContactT:
+ assert self._current_contact is not None
+ return self._current_contact
+
+ def process_escape(self) -> bool:
+ if self.get_visible_child_name() == 'function':
+ self._chat_function_page.process_escape()
+ return True
+ return False
+
def get_control_stack(self) -> ControlStack:
return self._control_stack
+ def get_message_action_box(self) -> MessageActionsBox:
+ return self._message_action_box
+
def show_chat(self, account: str, jid: JID) -> None:
+ # Store (preserve) primary clipboard and restore it after switching
+ clipboard = Gtk.Clipboard.get(Gdk.SELECTION_PRIMARY)
+ old_primary_clipboard = clipboard.wait_for_text()
+
+ if self._current_contact is not None:
+ self._current_contact.disconnect_all_from_obj(self)
+
+ client = app.get_client(account)
+ self._current_contact = client.get_module('Contacts').get_contact(jid)
+
+ self._chat_banner.switch_contact(account, jid)
self._control_stack.show_chat(account, jid)
+ self._message_action_box.switch_contact(self._current_contact)
+
+ self._update_base_actions(self._current_contact)
+ if isinstance(self._current_contact, GroupchatContact):
+ self._current_contact.multi_connect({
+ 'user-joined': self._on_user_joined,
+ 'user-role-changed': self._on_user_role_changed,
+ 'user-affiliation-changed': self._on_user_affiliation_changed,
+ 'state-changed': self._on_muc_state_changed,
+ 'room-password-required': self._on_room_password_required,
+ 'room-captcha-challenge': self._on_room_captcha_challenge,
+ 'room-captcha-error': self._on_room_captcha_error,
+ 'room-creation-failed': self._on_room_creation_failed,
+ 'room-join-failed': self._on_room_join_failed,
+ 'room-config-failed': self._on_room_config_failed,
+ })
+ self._update_group_chat_actions(self._current_contact)
+
+ elif isinstance(self._current_contact, GroupchatParticipant):
+ self._update_participant_actions(self._current_contact)
+
+ else:
+ self._update_chat_actions(self._current_contact)
+
+ if isinstance(self._current_contact, GroupchatContact):
+ muc_data = client.get_module('MUC').get_muc_data(
+ str(self._current_contact.jid))
+ if muc_data is not None:
+ if muc_data.state.is_captcha_request:
+ self._show_chat_function_page('captcha-request')
+ return
+
+ if muc_data.state.is_password_request:
+ self._show_chat_function_page('password-request')
+ return
+
+ if not muc_data.state.is_joined:
+ if muc_data.error == 'captcha-failed':
+ self._show_chat_function_page(
+ 'captcha-error', muc_data.error_text)
+ return
+ if muc_data.error in ('join-failed', 'creation-failed'):
+ self._show_chat_function_page(
+ muc_data.error, muc_data.error_text)
+ return
+
+ self.set_transition_type(Gtk.StackTransitionType.NONE)
+ self.set_visible_child_name('controls')
+
+ if old_primary_clipboard is not None:
+ GLib.idle_add(clipboard.set_text, # pyright: ignore
+ old_primary_clipboard,
+ -1)
+
+ GLib.idle_add(self._message_action_box.msg_textview.grab_focus)
+
+ def _on_room_password_required(self,
+ _contact: GroupchatContact,
+ _signal_name: str,
+ _properties: MessageProperties
+ ) -> None:
+ self._show_chat_function_page('password-request')
+
+ def _on_room_captcha_challenge(self,
+ contact: GroupchatContact,
+ _signal_name: str,
+ properties: MessageProperties
+ ) -> None:
+ self._show_chat_function_page('captcha-request')
+
+ def _on_room_captcha_error(self,
+ _contact: GroupchatContact,
+ _signal_name: str,
+ error: StanzaError
+ ) -> None:
+ error_text = helpers.to_user_string(error)
+ self._show_chat_function_page('captcha-error', error_text)
+
+ def _on_room_creation_failed(self,
+ _contact: GroupchatContact,
+ _signal_name: str,
+ properties: MessageProperties
+ ) -> None:
+ assert properties.error is not None
+ error_text = helpers.to_user_string(properties.error)
+ self._show_chat_function_page('creation-failed', error_text)
+
+ def _on_room_join_failed(self,
+ _contact: GroupchatContact,
+ _signal_name: str,
+ error: StanzaError
+ ) -> None:
+ self._show_chat_function_page(
+ 'join-failed', helpers.to_user_string(error))
+
+ def _on_room_config_failed(self,
+ _contact: GroupchatContact,
+ _signal_name: str,
+ error: StanzaError
+ ) -> None:
+ self._show_chat_function_page(
+ 'config-failed', helpers.to_user_string(error))
+
+ def _on_muc_state_changed(self,
+ contact: GroupchatContact,
+ _signal_name: str
+ ) -> None:
+ self._update_group_chat_actions(contact)
+
+ def _on_user_joined(self,
+ contact: GroupchatContact,
+ _signal_name: str,
+ _user_contact: GroupchatParticipant,
+ _properties: PresenceProperties
+ ) -> None:
+ self._update_group_chat_actions(contact)
+
+ def _on_user_role_changed(self,
+ contact: GroupchatContact,
+ _signal_name: str,
+ _user_contact: GroupchatParticipant,
+ _properties: PresenceProperties
+ ) -> None:
+ self._update_group_chat_actions(contact)
+
+ def _on_user_affiliation_changed(self,
+ contact: GroupchatContact,
+ _signal_name: str,
+ _user_contact: GroupchatParticipant,
+ _properties: PresenceProperties
+ ) -> None:
+ self._update_group_chat_actions(contact)
+
+ def _on_muc_disco_update(self, event: MucDiscoUpdate) -> None:
+ if self._current_contact is None:
+ return
+
+ if event.jid != self._current_contact.jid:
+ return
+
+ if isinstance(self._current_contact, GroupchatContact):
+ self._update_group_chat_actions(self._current_contact)
+
+ def _connect_actions(self) -> None:
+ actions = [
+ 'add-to-roster',
+ 'clear-chat',
+ 'invite-contacts',
+ 'send-file',
+ 'send-file-httpupload',
+ 'send-file-jingle',
+ 'show-contact-info',
+ 'start-video-call',
+ 'start-voice-call',
+ 'send-message',
+ 'muc-change-nickname',
+ 'muc-invite',
+ 'muc-contact-info',
+ 'muc-execute-command',
+ 'muc-ban',
+ 'muc-kick',
+ 'muc-change-role',
+ 'muc-change-affiliation',
+ 'muc-request-voice',
+ ]
+
+ for action in actions:
+ action = app.window.lookup_action(action)
+ assert action is not None
+ action.connect('activate', self._on_action)
+
+ def _update_base_actions(self, contact: ChatContactT) -> None:
+ client = app.get_client(contact.account)
+ online = app.account_is_connected(contact.account)
+
+ has_text = self._message_action_box.msg_textview.has_text
+ app.window.get_action('send-message').set_enabled(
+ online and has_text)
+
+ httpupload = app.window.get_action('send-file-httpupload')
+ httpupload.set_enabled(online and
+ client.get_module('HTTPUpload').available)
+
+ jingle = app.window.get_action('send-file-jingle')
+ jingle.set_enabled(online and contact.is_jingle_available)
+
+ app.window.get_action('send-file').set_enabled(
+ jingle.get_enabled() or
+ httpupload.get_enabled())
+
+ app.window.get_action('show-contact-info').set_enabled(online)
+ app.window.get_action('correct-message').set_enabled(online)
+
+ app.window.get_action('input-bold').set_enabled(True)
+ app.window.get_action('input-italic').set_enabled(True)
+ app.window.get_action('input-strike').set_enabled(True)
+ app.window.get_action('input-clear').set_enabled(True)
+ app.window.get_action('clear-chat').set_enabled(True)
+
+ def _update_chat_actions(self, contact: BareContact) -> None:
+ account = contact.account
+ online = app.account_is_connected(account)
+
+ app.window.get_action('start-voice-call').set_enabled(
+ online and contact.supports_audio() and
+ sys.platform != 'win32')
+ app.window.get_action('start-video-call').set_enabled(
+ online and contact.supports_video() and
+ sys.platform != 'win32')
+
+ app.window.get_action('quote').set_enabled(online)
+ app.window.get_action('mention').set_enabled(online)
+
+ def _update_group_chat_actions(self, contact: GroupchatContact) -> None:
+ joined = contact.is_joined
+ is_visitor = False
+ if joined:
+ self_contact = contact.get_self()
+ assert self_contact
+ is_visitor = self_contact.role.is_visitor
+
+ app.window.get_action('muc-change-nickname').set_enabled(
+ joined and not contact.is_irc)
+
+ app.window.get_action('muc-contact-info').set_enabled(joined)
+ app.window.get_action('muc-execute-command').set_enabled(joined)
+ app.window.get_action('muc-ban').set_enabled(joined)
+ app.window.get_action('muc-kick').set_enabled(joined)
+ app.window.get_action('muc-change-role').set_enabled(joined)
+ app.window.get_action('muc-change-affiliation').set_enabled(joined)
+ app.window.get_action('muc-invite').set_enabled(joined)
+
+ app.window.get_action('muc-request-voice').set_enabled(is_visitor)
+
+ app.window.get_action('quote').set_enabled(joined)
+ app.window.get_action('mention').set_enabled(joined)
+
+ def _update_participant_actions(self,
+ contact: GroupchatParticipant) -> None:
+ pass
+
+ def _on_action(self,
+ action: Gio.SimpleAction,
+ param: Optional[GLib.Variant]) -> None:
+
+ action_name = action.get_name()
+ contact = self._current_contact
+ assert contact is not None
+ account = contact.account
+ client = app.get_client(account)
+ jid = contact.jid
+ current_control = self._control_stack.get_current_control()
+
+ if action_name == 'send-message':
+ self._on_send_message()
+
+ elif action_name == 'start-voice-call':
+ app.call_manager.start_call(account, jid, CallType.AUDIO)
+
+ elif action_name == 'start-video-call':
+ app.call_manager.start_call(account, jid, CallType.VIDEO)
+
+ elif action_name.startswith('send-file'):
+ name = action.get_name()
+ if 'httpupload' in name:
+ app.interface.start_file_transfer(contact, method='httpupload')
+ return
+
+ if 'jingle' in name:
+ app.interface.start_file_transfer(contact, method='jingle')
+ return
+
+ app.interface.start_file_transfer(contact)
+
+ elif action_name == 'invite-contacts':
+ open_window('AdhocMUC', account=account, contact=contact)
+
+ elif action_name == 'add-to-roster':
+ if (isinstance(contact, GroupchatParticipant) and
+ contact.real_jid is not None):
+ jid = contact.real_jid
+ open_window('AddContact', account=account, jid=jid)
+
+ elif action_name == 'clear-chat':
+ assert current_control is not None
+ current_control.reset_view()
+
+ elif action_name == 'show-contact-info':
+ if isinstance(contact, GroupchatContact):
+ open_window('GroupchatDetails', contact=contact)
+ else:
+ app.window.contact_info(account, str(jid))
+
+ elif action_name == 'muc-contact-info':
+ assert param is not None
+ nick = param.get_string()
+ assert isinstance(contact, GroupchatContact)
+ resource_contact = contact.get_resource(nick)
+ open_window(
+ 'ContactInfo', account=account, contact=resource_contact)
+
+ elif action_name == 'muc-invite':
+ self._show_chat_function_page('invite')
+
+ elif action_name == 'muc-change-nickname':
+ self._show_chat_function_page('change-nickname')
+
+ elif action_name == 'muc-execute-command':
+ assert isinstance(current_control, GroupchatControl)
+ nick = None
+ if param is not None:
+ nick = param.get_string()
+ if nick:
+ assert isinstance(contact, GroupchatContact)
+ resource_contact = contact.get_resource(nick)
+ jid = resource_contact.jid
+ open_window('AdHocCommands', account=account, jid=jid)
+
+ elif action_name == 'muc-kick':
+ assert isinstance(current_control, GroupchatControl)
+ assert param is not None
+ kick_nick = param.get_string()
+ self._show_chat_function_page('kick', data=kick_nick)
+
+ elif action_name == 'muc-ban':
+ assert isinstance(current_control, GroupchatControl)
+ assert param is not None
+ ban_jid = param.get_string()
+ self._show_chat_function_page('ban', data=ban_jid)
+
+ elif action_name == 'muc-change-role':
+ assert param is not None
+ nick, role = param.get_strv()
+ client.get_module('MUC').set_role(contact.jid, nick, role)
+
+ elif action_name == 'muc-change-affiliation':
+ assert param is not None
+ jid, affiliation = param.get_strv()
+ client.get_module('MUC').set_affiliation(
+ contact.jid,
+ {jid: {'affiliation': affiliation}})
+
+ elif action_name == 'muc-request-voice':
+ client.get_module('MUC').request_voice(contact.jid)
+
+ def _on_drag_data_received(self,
+ _widget: Gtk.Widget,
+ _context: Gdk.DragContext,
+ _x_coord: int,
+ _y_coord: int,
+ selection: Gtk.SelectionData,
+ target_type: int,
+ _timestamp: int
+ ) -> None:
+ if not selection.get_data():
+ return
+
+ log.debug('Drop received: %s, %s', selection.get_data(), target_type)
+
+ # TODO: Contact drag and drop for invitations (AdHoc MUC/MUC)
+ if target_type == TARGET_TYPE_URI_LIST:
+ control = app.window.get_active_control()
+ if control is not None:
+ control.drag_data_file_transfer(selection)
+
+ def _on_drag_leave(self,
+ _widget: Gtk.Widget,
+ _context: Gdk.DragContext,
+ _time: int
+ ) -> None:
+ self._drop_area.set_no_show_all(True)
+ self._drop_area.hide()
+
+ def _on_drag_motion(self,
+ _widget: Gtk.Widget,
+ _context: Gdk.DragContext,
+ _x_coord: int,
+ _y_coord: int,
+ _time: int
+ ) -> bool:
+ self._drop_area.set_no_show_all(False)
+ self._drop_area.show_all()
+ return True
+
+ def _show_chat_function_page(self,
+ function: str,
+ data: Optional[str] = None
+ ) -> None:
+ assert self._current_contact is not None
+ self._chat_function_page.set_mode(
+ self._current_contact, function, data)
+ self.set_transition_type(Gtk.StackTransitionType.SLIDE_UP_DOWN)
+ self.set_visible_child_name('function')
+
+ def _on_function_finished(self,
+ _function_page: ChatFunctionPage,
+ close_control: bool
+ ) -> None:
+ if close_control:
+ self._close_control()
+ self.set_visible_child_name('empty')
+ self.set_transition_type(Gtk.StackTransitionType.NONE)
+ return
+
+ self.set_visible_child_name('controls')
+ self.set_transition_type(Gtk.StackTransitionType.NONE)
+
+ def _on_function_message(self,
+ _function_page: ChatFunctionPage,
+ message: str
+ ) -> None:
+ control = self._control_stack.get_current_control()
+ assert control is not None
+ control.add_info_message(message)
+
+ def _on_send_message(self) -> None:
+ self._message_action_box.msg_textview.replace_emojis()
+ message = self._message_action_box.msg_textview.get_text()
+
+ control = self._control_stack.get_current_control()
+ assert control is not None
+
+ contact = self._current_contact
+ assert contact is not None
+
+ encryption = contact.settings.get('encryption')
+ if encryption:
+ self.sendmessage = True
+ app.plugin_manager.extension_point(
+ 'send_message' + encryption,
+ control)
+ if not self.sendmessage:
+ return
+
+ client = app.get_client(contact.account)
+
+ message = helpers.remove_invalid_xml_chars(message)
+ 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
+ if self._message_action_box.is_correcting:
+ correct_id = self._message_action_box.last_message_id.get(
+ (contact.account, contact.jid))
+ self._message_action_box.is_correcting = False
+
+ chatstate = client.get_module('Chatstate').get_active_chatstate(
+ contact)
+
+ type_ = 'chat'
+ if isinstance(contact, GroupchatContact):
+ type_ = 'groupchat'
+
+ message_ = OutgoingMessage(account=contact.account,
+ contact=contact,
+ message=message,
+ type_=type_,
+ chatstate=chatstate,
+ label=label,
+ control=control,
+ correct_id=correct_id)
+
+ client.send_message(message_)
+
+ self._message_action_box.msg_textview.clear()
+
+ def get_last_message_id(self, account: str, jid: JID) -> Optional[str]:
+ return self._message_action_box.last_message_id.get((account, jid))
+
+ def _close_control(self) -> None:
+ assert self._current_contact is not None
+ app.window.activate_action(
+ 'remove-chat',
+ GLib.Variant('as',
+ [self._current_contact.account,
+ str(self._current_contact.jid)]))
def clear(self) -> None:
+ if self._current_contact is not None:
+ self._current_contact.disconnect_all_from_obj(self)
+
+ self.set_visible_child_name('empty')
+ self._chat_banner.clear()
+ self._message_action_box.clear()
self._control_stack.clear()
def process_event(self, event: Any) -> None:
+ if isinstance(event, MucDiscoUpdate):
+ self._on_muc_disco_update(event)
self._control_stack.process_event(event)
+ self._message_action_box.process_event(event)
+
+
+class ChatPlaceholderBox(Gtk.Box):
+ def __init__(self):
+ Gtk.Box.__init__(self, orientation=Gtk.Orientation.VERTICAL,
+ spacing=18)
+ self.set_valign(Gtk.Align.CENTER)
+ pixbuf = Gtk.IconTheme.load_icon_for_scale(
+ Gtk.IconTheme.get_default(),
+ 'org.gajim.Gajim-symbolic',
+ 100,
+ self.get_scale_factor(),
+ Gtk.IconLookupFlags.FORCE_SIZE)
+ image = Gtk.Image.new_from_pixbuf(pixbuf)
+ image.get_style_context().add_class('dim-label')
+ self.add(image)
+
+ button = Gtk.Button(label=_('Start Chatting…'))
+ button.set_halign(Gtk.Align.CENTER)
+ button.connect('clicked', self._on_start_chatting)
+ self.add(button)
+
+ def _on_start_chatting(self, _button: Gtk.Button) -> None:
+ app.app.activate_action('start-chat', GLib.Variant('s', ''))
diff --git a/gajim/gtk/const.py b/gajim/gtk/const.py
index 52d2f1447..97226f6fc 100644
--- a/gajim/gtk/const.py
+++ b/gajim/gtk/const.py
@@ -187,8 +187,44 @@ APP_ACTIONS = [
]
+MAIN_WIN_ACTIONS = [
+ # action name, variant type, enabled
+ ('input-bold', None, False),
+ ('input-italic', None, False),
+ ('input-strike', None, False),
+ ('input-clear', None, False),
+ ('show-emoji-chooser', None, False),
+ ('correct-message', None, False),
+ ('quote', 's', False),
+ ('mention', 's', False),
+ ('send-file-httpupload', None, False),
+ ('send-file-jingle', None, False),
+ ('send-file', None, False),
+ ('invite-contacts', None, False),
+ ('add-to-roster', None, True),
+ ('start-voice-call', None, False),
+ ('start-video-call', None, False),
+ ('clear-chat', None, False),
+ ('show-contact-info', None, False),
+ ('send-message', None, False),
+ ('muc-change-nickname', None, False),
+ ('muc-invite', None, False),
+ ('muc-contact-info', 's', False),
+ ('muc-execute-command', 's', False),
+ ('muc-ban', 's', False),
+ ('muc-kick', 's', False),
+ ('muc-change-role', 'as', False),
+ ('muc-change-affiliation', 'as', False),
+ ('muc-request-voice', None, False),
+]
+
+
ACCOUNT_ACTIONS = [
('add-contact', 'as'),
+ ('block-contact', 's'),
+ ('remove-contact', 's'),
+ ('execute-command', 's'),
+ ('modify-gateway', 's'),
('archive', 's'),
('blocking', 's'),
('bookmarks', 's'),
@@ -214,6 +250,9 @@ ALWAYS_ACCOUNT_ACTIONS = {
ONLINE_ACCOUNT_ACTIONS = {
'add-contact',
+ 'remove-contact',
+ 'execute-command',
+ 'modify-gateway',
'bookmarks',
'import-contacts',
'open-chat',
@@ -228,4 +267,5 @@ ONLINE_ACCOUNT_ACTIONS = {
FEATURE_ACCOUNT_ACTIONS = {
'archive',
'blocking',
+ 'block-contact',
}
diff --git a/gajim/gtk/control_stack.py b/gajim/gtk/control_stack.py
index 24661c10f..43fe58cfc 100644
--- a/gajim/gtk/control_stack.py
+++ b/gajim/gtk/control_stack.py
@@ -18,43 +18,30 @@ from typing import Generator
import logging
-from gi.repository import GLib
from gi.repository import Gtk
from nbxmpp import JID
-from gajim.common import app
-from gajim.common import ged
-from gajim.common.i18n import _
-
from .controls.chat import ChatControl
from .controls.groupchat import GroupchatControl
from .controls.private import PrivateChatControl
from .types import ControlT
-from .util import EventHelper
log = logging.getLogger('gajim.gui.controlstack')
-class ControlStack(Gtk.Stack, EventHelper):
+class ControlStack(Gtk.Stack):
def __init__(self):
Gtk.Stack.__init__(self)
- EventHelper.__init__(self)
-
self.set_vexpand(True)
self.set_hexpand(True)
- self.add_named(ChatPlaceholderBox(), 'empty')
-
- self.register_events([
- ('account-enabled', ged.GUI2, self._on_account_changed),
- ('account-disabled', ged.GUI2, self._on_account_changed),
- ])
+ self.add_named(Gtk.Box(), 'empty')
self.show_all()
self._controls: dict[tuple[str, JID], ControlT] = {}
- self._current_control = None
+ self._current_control: Optional[ControlT] = None
def get_control(self, account: str, jid: JID) -> Optional[ControlT]:
try:
@@ -62,6 +49,9 @@ class ControlStack(Gtk.Stack, EventHelper):
except KeyError:
return None
+ def get_current_control(self) -> Optional[ControlT]:
+ return self._current_control
+
def get_controls(self, account: Optional[str]
) -> Generator[ControlT, None, None]:
if account is None:
@@ -117,14 +107,12 @@ class ControlStack(Gtk.Stack, EventHelper):
return
if self._current_control is not None:
- self._current_control.set_control_active(False)
self._current_control.reset_view()
- control.set_control_active(True)
+ control.load_messages()
self._current_control = control
self.set_visible_child_name(new_name)
- GLib.idle_add(control.focus)
def is_chat_loaded(self, account: str, jid: JID) -> bool:
control = self.get_control(account, jid)
@@ -147,31 +135,3 @@ class ControlStack(Gtk.Stack, EventHelper):
if chat_account != account:
continue
self.remove_chat(account, jid)
-
- def _on_account_changed(self, *args: Any) -> None:
- for control in self._controls.values():
- control.update_account_badge()
-
-
-class ChatPlaceholderBox(Gtk.Box):
- def __init__(self):
- Gtk.Box.__init__(self, orientation=Gtk.Orientation.VERTICAL,
- spacing=18)
- self.set_valign(Gtk.Align.CENTER)
- pixbuf = Gtk.IconTheme.load_icon_for_scale(
- Gtk.IconTheme.get_default(),
- 'org.gajim.Gajim-symbolic',
- 100,
- self.get_scale_factor(),
- Gtk.IconLookupFlags.FORCE_SIZE)
- image = Gtk.Image.new_from_pixbuf(pixbuf)
- image.get_style_context().add_class('dim-label')
- self.add(image)
-
- button = Gtk.Button(label=_('Start Chatting…'))
- button.set_halign(Gtk.Align.CENTER)
- button.connect('clicked', self._on_start_chatting)
- self.add(button)
-
- def _on_start_chatting(self, _button: Gtk.Button) -> None:
- app.app.activate_action('start-chat', GLib.Variant('s', ''))
diff --git a/gajim/gtk/controls/base.py b/gajim/gtk/controls/base.py
index 43f6d15da..4b3bf694e 100644
--- a/gajim/gtk/controls/base.py
+++ b/gajim/gtk/controls/base.py
@@ -28,59 +28,40 @@ from __future__ import annotations
from typing import Any
from typing import Optional
from typing import Union
+from typing import cast
import os
import logging
-import sys
import time
import uuid
-import tempfile
-from functools import partial
from gi.repository import Gtk
-from gi.repository import Gdk
-from gi.repository import GdkPixbuf
from gi.repository import GLib
-from gi.repository import Gio
from nbxmpp import JID
-from nbxmpp.const import Chatstate
from nbxmpp.modules.security_labels import Displaymarking
from gajim.common import app
from gajim.common import events
from gajim.common import ged
-from gajim.common import i18n
from gajim.common.helpers import message_needs_highlight
from gajim.common.helpers import get_file_path_from_dnd_dropped_uri
from gajim.common.i18n import _
from gajim.common.ged import EventHelper
from gajim.common.helpers import AdditionalDataDict
from gajim.common.helpers import get_retraction_text
-from gajim.common.const import Direction, KindConstant
+from gajim.common.const import KindConstant
from gajim.common.modules.httpupload import HTTPFileTransfer
from gajim.common.preview_helpers import filename_from_uri
from gajim.common.preview_helpers import guess_simple_file_type
from gajim.common.storage.archive import ConversationRow
-from gajim.common.structs import OutgoingMessage
from gajim.gui.conversation.view import ConversationView
from gajim.gui.conversation.scrolled import ScrolledView
from gajim.gui.conversation.jump_to_end_button import JumpToEndButton
-from gajim.gui.dialogs import DialogButton
-from gajim.gui.dialogs import ErrorDialog
-from gajim.gui.dialogs import PastePreviewDialog
-from gajim.gui.file_transfer_send import SendFileDialog
-from gajim.gui.message_input import MessageInputTextView
-from gajim.gui.security_label_selector import SecurityLabelSelector
-from gajim.gui.util import get_hardware_key_codes
from gajim.gui.builder import get_builder
from gajim.gui.util import set_urgency_hint
-from gajim.gui.util import scroll_to_end
-from gajim.gui.util import AccountBadge
from gajim.gui.const import ControlType
-from gajim.gui.const import TARGET_TYPE_URI_LIST
-from gajim.gui.emoji_chooser import emoji_chooser
from gajim.command_system.implementation.middleware import ChatCommandProcessor
from gajim.command_system.implementation.middleware import CommandTools
@@ -92,39 +73,19 @@ from gajim.command_system.implementation.middleware import CommandTools
from gajim.command_system.implementation import standard # noqa: F401
from gajim.command_system.implementation import execute # noqa: F401
-if app.is_installed('GSPELL'):
- from gi.repository import Gspell # pylint: disable=ungrouped-imports
-
-# This is needed so copying text from the conversation textview
-# works with different language layouts. Pressing the key c on a russian
-# layout yields another keyval than with the english layout.
-# So we match hardware keycodes instead of keyvals.
-# Multiple hardware keycodes can trigger a keyval like Gdk.KEY_c.
-KEYCODES_KEY_C = get_hardware_key_codes(Gdk.KEY_c)
-
-if sys.platform == 'darwin':
- COPY_MODIFIER = Gdk.ModifierType.META_MASK
- COPY_MODIFIER_KEYS = (Gdk.KEY_Meta_L, Gdk.KEY_Meta_R)
-else:
- COPY_MODIFIER = Gdk.ModifierType.CONTROL_MASK
- COPY_MODIFIER_KEYS = (Gdk.KEY_Control_L, Gdk.KEY_Control_R)
log = logging.getLogger('gajim.gui.controls.base')
-################################################################################
class BaseControl(ChatCommandProcessor, CommandTools, EventHelper):
- """
+ '''
A base class containing a banner, ConversationView, MessageInputTextView
- """
+ '''
_type: Optional[ControlType] = None
def __init__(self, widget_name: str, account: str, jid: JID) -> None:
EventHelper.__init__(self)
- # Undo needs this variable to know if space has been pressed.
- # Initialize it to True so empty textview is saved in undo list
- self.space_pressed: bool = True
self.handlers: dict[int, Any] = {}
@@ -137,60 +98,13 @@ class BaseControl(ChatCommandProcessor, CommandTools, EventHelper):
jid, groupchat=groupchat)
self._connect_contact_signals()
- # control_id is a unique id for the control,
- # its used as action name for actions that belong to a control
self.control_id: str = str(uuid.uuid4())
- self.session = None
- if app.settings.get('use_kib_mib'):
- self._units = GLib.FormatSizeFlags.IEC_UNITS
- else:
- self._units = GLib.FormatSizeFlags.DEFAULT
-
- self.xml = get_builder(f'{widget_name}.ui')
- self.xml.connect_signals(self)
- self.widget: Gtk.Box = self.xml.get_object(f'{widget_name}_hbox')
-
- self._account_badge = AccountBadge(self.account)
- self.xml.account_badge_box.add(self._account_badge)
- show_account_badge = len(app.settings.get_active_accounts()) > 1
- self.xml.account_badge_box.set_visible(show_account_badge)
-
- # Drag and drop
- self.xml.overlay.add_overlay(self.xml.drop_area)
- self.xml.drop_area.hide()
- self.xml.overlay.connect(
- 'drag-data-received', self._on_drag_data_received)
- self.xml.overlay.connect('drag-motion', self._on_drag_motion)
- self.xml.overlay.connect('drag-leave', self._on_drag_leave)
-
- uri_entry = Gtk.TargetEntry.new(
- 'text/uri-list',
- Gtk.TargetFlags.OTHER_APP,
- TARGET_TYPE_URI_LIST)
- dst_targets = Gtk.TargetList.new([uri_entry])
- dst_targets.add_text_targets(0)
- self._dnd_list = [uri_entry,
- Gtk.TargetEntry.new(
- 'MY_TREE_MODEL_ROW',
- Gtk.TargetFlags.SAME_APP,
- 0)]
-
- self.xml.overlay.drag_dest_set(
- Gtk.DestDefaults.ALL,
- self._dnd_list,
- Gdk.DragAction.COPY | Gdk.DragAction.MOVE)
- self.xml.overlay.drag_dest_set_target_list(dst_targets)
+ self.xml = get_builder('chat_control.ui')
+ self.widget = cast(Gtk.Box, self.xml.get_object('control_box'))
# Create ConversationView and connect signals
self.conversation_view = ConversationView(self.account, self.contact)
- self.conversation_view.connect('quote', self._on_quote)
- self.conversation_view.connect('mention', self._on_mention)
- self.conversation_view.connect('scroll-to-end', self._on_scroll_to_end)
-
- id_ = self.conversation_view.connect(
- 'key-press-event', self._on_conversation_view_key_press)
- self.handlers[id_] = self.conversation_view
self._scrolled_view = ScrolledView()
self._scrolled_view.add(self.conversation_view)
@@ -207,67 +121,16 @@ class BaseControl(ChatCommandProcessor, CommandTools, EventHelper):
self._scrolled_view.connect('request-history',
self.fetch_n_lines_history, 20)
- self._security_label_selector = SecurityLabelSelector(
- account, self.contact)
- self.xml.hbox.pack_start(self._security_label_selector, False, True, 0)
-
- self.msg_textview = MessageInputTextView(self.account, self.contact)
- self.msg_textview.connect('paste-clipboard',
- self._on_message_textview_paste_event)
- self.msg_textview.connect('key-press-event',
- self._on_message_textview_key_press_event)
- self.msg_textview.connect('populate-popup',
- self._on_msg_textview_populate_popup)
- self.msg_textview.get_buffer().connect(
- 'changed', self._on_message_tv_buffer_changed)
-
- # Send message button
- self.xml.send_message_button.set_action_name(
- f'win.send-message-{self.control_id}')
- self.xml.send_message_button.set_visible(
- app.settings.get('show_send_message_button'))
- app.settings.bind_signal(
- 'show_send_message_button',
- self.xml.send_message_button,
- 'set_visible')
-
- self.msg_scrolledwindow = ScrolledWindow()
- self.msg_scrolledwindow.set_margin_start(3)
- self.msg_scrolledwindow.set_margin_end(3)
- self.msg_scrolledwindow.get_style_context().add_class(
- 'message-input-border')
- self.msg_scrolledwindow.add(self.msg_textview)
-
- self.xml.hbox.pack_start(self.msg_scrolledwindow, True, True, 0)
-
# Keeps track of whether the ConversationView is populated
self._chat_loaded: bool = False
- # the following vars are used to keep history of user's messages
- self.sent_history: list[str] = []
- self.sent_history_pos: int = 0
- self.received_history: list[str] = []
- self.received_history_pos: int = 0
- self.orig_msg: Optional[str] = None
-
# XEP-0333 Chat Markers
self.last_msg_id: Optional[str] = None
- # XEP-0308 Message Correction
- self.correcting: bool = False
- self.last_sent_msg: Optional[str] = None
-
- self.set_emoticon_popover()
-
- # Attach speller
- self.set_speller()
-
# XEP-0172 User Nickname
# TODO:
self.user_nick: Optional[str] = None
- self.sendmessage: bool = True
-
self._client.get_module('Chatstate').set_active(self.contact)
self.encryption: Optional[str] = self.get_encryption_state()
@@ -278,9 +141,9 @@ class BaseControl(ChatCommandProcessor, CommandTools, EventHelper):
app.plugin_manager.gui_extension_point('chat_control_base', self)
self.register_events([
- ('ping-sent', ged.GUI1, self._nec_ping),
- ('ping-reply', ged.GUI1, self._nec_ping),
- ('ping-error', ged.GUI1, self._nec_ping),
+ ('ping-sent', ged.GUI1, self._on_ping_event),
+ ('ping-reply', ged.GUI1, self._on_ping_event),
+ ('ping-error', ged.GUI1, self._on_ping_event),
])
# This is basically a very nasty hack to surpass the inability
@@ -288,10 +151,9 @@ class BaseControl(ChatCommandProcessor, CommandTools, EventHelper):
CommandTools.__init__(self)
def _connect_contact_signals(self) -> None:
- raise NotImplementedError
-
- def _get_action(self, name: str) -> Gio.SimpleAction:
- return app.window.lookup_action(f'{name}{self.control_id}')
+ '''
+ Derived types MAY implement this
+ '''
def process_event(self, event: events.ApplicationEvent) -> None:
if event.account != self.account:
@@ -305,24 +167,50 @@ class BaseControl(ChatCommandProcessor, CommandTools, EventHelper):
events.FileRequestSent
)
- if self.is_chat:
- if isinstance(event, file_transfer_events):
- self.add_jingle_file_transfer(event=event)
- return
- if isinstance(event, events.JingleRequestReceived):
- active_jid = app.call_manager.get_active_call_jid()
- # Don't add a second row if contact upgrades to video
- if active_jid is None:
- self.add_call_message(event=event)
- return
- if isinstance(event, events.CallStopped):
- self.conversation_view.update_call_rows()
- return
+ if isinstance(event, file_transfer_events):
+ self.add_jingle_file_transfer(event=event)
+ return
+
+ if isinstance(event, events.JingleRequestReceived):
+ active_jid = app.call_manager.get_active_call_jid()
+ # Don't add a second row if contact upgrades to video
+ if active_jid is None:
+ self.add_call_message(event=event)
+ return
+
+ if isinstance(event, events.CallStopped):
+ self.conversation_view.update_call_rows()
+ return
+
+ if isinstance(event, events.MessageReceived) and not self.is_groupchat:
+ self._on_message_received(event)
+ return
+
+ if isinstance(event, events.MessageSent):
+ self._on_message_sent(event)
+ return
+
+ if isinstance(event, events.MessageError):
+ self._on_message_error(event)
+ return
method_name = event.name.replace('-', '_')
method_name = f'_on_{method_name}'
getattr(self, method_name)(event)
+ def _on_message_sent(self, event: events.MessageSent) -> None:
+ '''
+ Derived types MAY implement this
+ '''
+
+ def _on_message_received(self, event: events.MessageReceived) -> None:
+ '''
+ Derived types MAY implement this
+ '''
+
+ def _on_message_error(self, event: events.MessageError) -> None:
+ self.conversation_view.show_error(event.message_id, event.error)
+
def _on_message_updated(self, event: events.MessageUpdated) -> None:
self.conversation_view.correct_message(
event.correct_id, event.msgtxt, event.nickname)
@@ -335,181 +223,29 @@ class BaseControl(ChatCommandProcessor, CommandTools, EventHelper):
self.conversation_view.show_message_retraction(
event.moderation.stanza_id, text)
- def _on_conversation_view_key_press(self,
- _listbox: ConversationView,
- event: Gdk.EventKey
- ) -> int:
- if event.get_state() & Gdk.ModifierType.SHIFT_MASK:
- if event.keyval in (Gdk.KEY_Page_Down, Gdk.KEY_Page_Up):
- return Gdk.EVENT_PROPAGATE
-
- if event.keyval in COPY_MODIFIER_KEYS:
- # Don’t route modifier keys for copy action to the Message Input
- # otherwise pressing CTRL/META + c (the next event after that)
- # will not reach the textview (because the Message Input would get
- # focused).
- return Gdk.EVENT_PROPAGATE
-
- # if event.get_state() & COPY_MODIFIER:
- # TODO
- # # Don’t reroute the event if it is META + c and the
- # # textview has a selection
- # if event.hardware_keycode in KEYCODES_KEY_C:
- # if textview.get_buffer().props.has_selection:
- # return Gdk.EVENT_PROPAGATE
-
- if not self.msg_textview.get_sensitive():
- # If the input textview is not sensitive it can’t get the focus.
- # In that case propagate_key_event() would send the event again
- # to the conversation textview. This would mean a recursion.
- return Gdk.EVENT_PROPAGATE
-
- # Focus the Message Input and resend the event
- self.msg_textview.grab_focus()
- self.msg_textview.get_toplevel().propagate_key_event(event)
- return Gdk.EVENT_STOP
-
@property
def type(self) -> ControlType:
+ assert self._type is not None
return self._type
@property
def is_chat(self) -> bool:
+ assert self._type is not None
return self._type.is_chat
@property
def is_privatechat(self) -> bool:
+ assert self._type is not None
return self._type.is_privatechat
@property
def is_groupchat(self) -> bool:
+ assert self._type is not None
return self._type.is_groupchat
- def focus(self) -> None:
+ def _on_ping_event(self, event: events.PingEventT) -> None:
raise NotImplementedError
- def update_actions(self) -> None:
- """
- Derived types MAY implement this
- """
-
- def draw_banner(self) -> None:
- """
- Draw the fat line at the top of the window
- that houses the icon, jid, etc
-
- Derived types MAY implement this.
- """
- self.draw_banner_text()
-
- def update_toolbar(self) -> None:
- """
- update state of buttons in toolbar
- """
- self._update_toolbar()
- app.plugin_manager.gui_extension_point(
- 'chat_control_base_update_toolbar', self)
-
- def draw_banner_text(self) -> None:
- """
- Derived types SHOULD implement this
- """
-
- def update_ui(self) -> None:
- """
- Derived types SHOULD implement this
- """
- self.draw_banner()
-
- def repaint_themed_widgets(self) -> None:
- """
- Derived types MAY implement this
- """
- self.draw_banner()
-
- def _update_toolbar(self) -> None:
- """
- Derived types MAY implement this
- """
-
- def set_session(self, session):
- oldsession = None
- if hasattr(self, 'session'):
- oldsession = self.session
-
- if oldsession and session == oldsession:
- return
-
- self.session = session
-
- if session:
- session.control = self
-
- if session and oldsession:
- oldsession.control = None
-
- def remove_session(self, session):
- if session != self.session:
- return
- self.session.control = None
- self.session = None
-
- def _nec_ping(self, obj):
- raise NotImplementedError
-
- def delegate_action(self, action: str) -> int:
- if action == 'clear-chat':
- self.reset_view()
- return Gdk.EVENT_STOP
-
- if action == 'delete-line':
- self.msg_textview.clear()
- return Gdk.EVENT_STOP
-
- if action == 'show-emoji-chooser':
- if sys.platform == 'darwin':
- # TODO: Remove if colored emoji rendering works well on
- # Windows and MacOS
- self.xml.emoticons_button.get_popover().show()
- return Gdk.EVENT_STOP
- self.msg_textview.emit('insert-emoji')
- return Gdk.EVENT_STOP
-
- return Gdk.EVENT_PROPAGATE
-
- def add_actions(self) -> None:
- action = Gio.SimpleAction.new_stateful(
- f'set-encryption-{self.control_id}',
- GLib.VariantType.new('s'),
- GLib.Variant('s', self.encryption or 'disabled'))
- action.connect('change-state', self.change_encryption)
- app.window.add_action(action)
-
- actions = {
- 'send-message-%s': self._on_send_message,
- 'send-file-%s': self._on_send_file,
- 'send-file-httpupload-%s': self._on_send_file,
- 'send-file-jingle-%s': self._on_send_file,
- }
-
- for name, func in actions.items():
- action = Gio.SimpleAction.new(name % self.control_id, None)
- action.connect('activate', func)
- action.set_enabled(False)
- app.window.add_action(action)
-
- def remove_actions(self) -> None:
- actions = [
- 'send-message-',
- 'set-encryption-',
- 'send-file-',
- 'send-file-httpupload-',
- 'send-file-jingle-',
- ]
-
- for action in actions:
- app.window.remove_action(f'{action}{self.control_id}')
-
def mark_as_read(self, send_marker: bool = True) -> None:
self._jump_to_end_button.reset_unread_count()
@@ -521,58 +257,6 @@ class BaseControl(ChatCommandProcessor, CommandTools, EventHelper):
str(self._type))
self.last_msg_id = None
- def change_encryption(self,
- action: Gio.SimpleAction,
- param: GLib.Variant
- ) -> None:
- encryption = param.get_string()
- if encryption == 'disabled':
- encryption = None
-
- if self.encryption == encryption:
- return
-
- if encryption:
- plugin = app.plugin_manager.encryption_plugins[encryption]
- if not plugin.activate_encryption(self):
- return
-
- action.set_state(param)
- self.set_encryption_state(encryption)
- self.set_encryption_menu_icon()
- self.set_lock_image()
-
- def set_lock_image(self) -> None:
- encryption_state = {'visible': self.encryption is not None,
- 'enc_type': self.encryption,
- 'authenticated': False}
-
- if self.encryption:
- app.plugin_manager.extension_point(
- 'encryption_state' + self.encryption, self, encryption_state)
-
- visible, enc_type, authenticated = encryption_state.values()
-
- if authenticated:
- authenticated_string = _('and authenticated')
- self.xml.lock_image.set_from_icon_name(
- 'security-high-symbolic', Gtk.IconSize.MENU)
- else:
- authenticated_string = _('and NOT authenticated')
- self.xml.lock_image.set_from_icon_name(
- 'security-low-symbolic', Gtk.IconSize.MENU)
-
- tooltip = _('%(type)s encryption is active %(authenticated)s.') % {
- 'type': enc_type, 'authenticated': authenticated_string}
-
- self.xml.authentication_button.set_tooltip_text(tooltip)
- self.xml.authentication_button.set_visible(visible)
- self.xml.lock_image.set_sensitive(visible)
-
- def _on_authentication_button_clicked(self, _button: Gtk.Button) -> None:
- app.plugin_manager.extension_point(
- 'encryption_dialog' + self.encryption, self)
-
def set_encryption_state(self, encryption: Optional[str]) -> None:
self.encryption = encryption
self.conversation_view.encryption_enabled = encryption is not None
@@ -587,60 +271,12 @@ class BaseControl(ChatCommandProcessor, CommandTools, EventHelper):
return None
return state
- def set_encryption_menu_icon(self) -> None:
- image = self.xml.encryption_menu.get_image()
- if image is None:
- image = Gtk.Image()
- self.xml.encryption_menu.set_image(image)
- if not self.encryption:
- image.set_from_icon_name('channel-insecure-symbolic',
- Gtk.IconSize.MENU)
- else:
- image.set_from_icon_name('channel-secure-symbolic',
- Gtk.IconSize.MENU)
-
- def set_speller(self) -> None:
- if (not app.is_installed('GSPELL') or
- not app.settings.get('use_speller')):
- return
-
- gspell_lang = self.get_speller_language()
- spell_checker = Gspell.Checker.new(gspell_lang)
- spell_buffer = Gspell.TextBuffer.get_from_gtk_text_buffer(
- self.msg_textview.get_buffer())
- spell_buffer.set_spell_checker(spell_checker)
- spell_view = Gspell.TextView.get_from_gtk_text_view(self.msg_textview)
- spell_view.set_inline_spell_checking(False)
- spell_view.set_enable_language_menu(True)
-
- spell_checker.connect('notify::language', self.on_language_changed)
-
- def get_speller_language(self) -> Optional[Gspell.Language]:
- lang = self.contact.settings.get('speller_language')
- if not lang:
- # use the default one
- lang = app.settings.get('speller_language')
- if not lang:
- lang = i18n.LANG
- gspell_lang = Gspell.language_lookup(lang)
- if gspell_lang is None:
- gspell_lang = Gspell.language_get_default()
- return gspell_lang
-
- def on_language_changed(self, checker: Gspell.Checker, _param: Any) -> None:
- gspell_lang = checker.get_language()
- if gspell_lang is not None:
- self.contact.settings.set('speller_language',
- gspell_lang.get_code())
-
def shutdown(self) -> None:
# remove_gui_extension_point() is called on shutdown, but also when
# a plugin is getting disabled. Plugins don’t know the difference.
# Plugins might want to remove their widgets on
# remove_gui_extension_point(), so delete the objects only afterwards.
app.plugin_manager.remove_gui_extension_point('chat_control_base', self)
- app.plugin_manager.remove_gui_extension_point(
- 'chat_control_base_update_toolbar', self)
self._client.disconnect_all_from_obj(self)
self.contact.disconnect_all_from_obj(self)
@@ -655,8 +291,6 @@ class BaseControl(ChatCommandProcessor, CommandTools, EventHelper):
del self.conversation_view
del self._scrolled_view
- del self.msg_textview
- del self.msg_scrolledwindow
self.widget.destroy()
del self.widget
@@ -665,246 +299,6 @@ class BaseControl(ChatCommandProcessor, CommandTools, EventHelper):
self.unregister_events()
- def _on_msg_textview_populate_popup(self,
- _textview: MessageInputTextView,
- menu: Gtk.Widget
- ) -> None:
- """
- Override the default context menu and we prepend an option to switch
- languages
- """
- assert isinstance(menu, Gtk.Menu)
- item = Gtk.MenuItem.new_with_mnemonic(_('_Undo'))
- menu.prepend(item)
- id_ = item.connect('activate', self.msg_textview.undo)
- self.handlers[id_] = item
-
- item = Gtk.SeparatorMenuItem()
- menu.prepend(item)
-
- item = Gtk.MenuItem.new_with_mnemonic(_('_Clear'))
- menu.prepend(item)
- id_ = item.connect('activate', self.msg_textview.clear)
- self.handlers[id_] = item
-
- paste_item = Gtk.MenuItem.new_with_label(_('Paste as quote'))
- id_ = paste_item.connect('activate', self.paste_clipboard_as_quote)
- self.handlers[id_] = paste_item
- menu.append(paste_item)
-
- menu.show_all()
-
- def insert_as_quote(self, text: str) -> None:
- text = '> ' + text.replace('\n', '\n> ') + '\n'
- message_buffer = self.msg_textview.get_buffer()
- message_buffer.insert_at_cursor(text)
- self.msg_textview.grab_focus()
-
- def paste_clipboard_as_quote(self, _item: Gtk.MenuItem) -> None:
- clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
- text = clipboard.wait_for_text()
- if text is None:
- return
- self.insert_as_quote(text)
-
- def _on_quote(self, _view: ConversationView, text: str) -> None:
- self.insert_as_quote(text)
-
- def _on_mention(self, _view: ConversationView, name: str) -> None:
- gc_refer_to_nick_char = app.settings.get('gc_refer_to_nick_char')
- text = f'{name}{gc_refer_to_nick_char} '
- message_buffer = self.msg_textview.get_buffer()
- message_buffer.insert_at_cursor(text)
- GLib.idle_add(self.msg_textview.grab_focus)
-
- def _on_scroll_to_end(self, _view: ConversationView) -> None:
- scroll_to_end(self._scrolled_view)
-
- def _on_message_textview_paste_event(self,
- _texview: MessageInputTextView
- ) -> None:
- clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
- image = clipboard.wait_for_image()
- if image is not None:
- if not app.settings.get('confirm_paste_image'):
- self._paste_event_confirmed(True, image)
- return
- PastePreviewDialog(
- _('Paste Image'),
- _('You are trying to paste an image'),
- _('Are you sure you want to paste your '
- 'clipboard\'s image into the chat window?'),
- _('_Do not ask me again'),
- image,
- [DialogButton.make('Cancel'),
- DialogButton.make('Accept',
- text=_('_Paste'),
- callback=self._paste_event_confirmed,
- args=[image])]).show()
-
- def _paste_event_confirmed(self,
- is_checked: bool,
- image: GdkPixbuf.Pixbuf
- ) -> None:
- if is_checked:
- app.settings.set('confirm_paste_image', False)
-
- dir_ = tempfile.gettempdir()
- path = os.path.join(dir_, f'{uuid.uuid4()}.png')
- if image is None:
- self.add_info_message(_('Error: Could not process image'))
- return
-
- image.savev(path, 'png', [], [])
-
- self._start_filetransfer(path)
-
- def _get_pref_ft_method(self) -> Optional[str]:
- ft_pref = app.settings.get_account_setting(self.account,
- 'filetransfer_preference')
- httpupload = app.window.lookup_action(
- f'send-file-httpupload-{self.control_id}')
- jingle = app.window.lookup_action(
- f'send-file-jingle-{self.control_id}')
-
- if self.is_groupchat:
- if httpupload.get_enabled():
- return 'httpupload'
- return None
-
- if httpupload.get_enabled() and jingle.get_enabled():
- return ft_pref
-
- if httpupload.get_enabled():
- return 'httpupload'
-
- if jingle.get_enabled():
- return 'jingle'
- return None
-
- def _start_filetransfer(self, path: str) -> None:
- method = self._get_pref_ft_method()
- if method is None:
- return
-
- if method == 'httpupload':
- app.interface.send_httpupload(self, path)
- else:
- send_callback = partial(
- app.interface.instances['file_transfers'].send_file,
- self.account,
- self.contact)
- SendFileDialog(self.contact, send_callback, app.window, [path])
-
- def _on_send_file(self,
- action: Gio.SimpleAction,
- _param: Optional[GLib.Variant]
- ) -> None:
- name = action.get_name()
- if 'httpupload' in name:
- app.interface.send_httpupload(self)
- return
-
- if 'jingle' in name:
- app.interface.instances['file_transfers'].show_file_send_request(
- self.account, self.contact)
- return
-
- method = self._get_pref_ft_method()
- if method is None:
- return
-
- if method == 'httpupload':
- app.interface.send_httpupload(self)
- else:
- app.interface.instances['file_transfers'].show_file_send_request(
- self.account, self.contact)
-
- def _on_message_textview_key_press_event(self,
- textview: MessageInputTextView,
- event: Gdk.EventKey
- ) -> bool:
- # pylint: disable=too-many-nested-blocks
- if event.keyval == Gdk.KEY_space:
- self.space_pressed = True
-
- elif ((self.space_pressed or self.msg_textview.undo_pressed) and
- event.keyval not in (Gdk.KEY_Control_L, Gdk.KEY_Control_R) and
- not (event.keyval == Gdk.KEY_z and
- event.get_state() & Gdk.ModifierType.CONTROL_MASK)):
- # If the space key has been pressed and now it hasn't,
- # we save the buffer into the undo list. But be careful we're not
- # pressing Control again (as in ctrl+z)
- _buffer = textview.get_buffer()
- start_iter, end_iter = _buffer.get_bounds()
- self.msg_textview.save_undo(_buffer.get_text(start_iter,
- end_iter,
- True))
- self.space_pressed = False
-
- # Ctrl [+ Shift] + Tab are not forwarded to notebook. We handle it here
- if event.get_state() & Gdk.ModifierType.SHIFT_MASK:
- if (event.get_state() & Gdk.ModifierType.CONTROL_MASK and
- event.keyval == Gdk.KEY_ISO_Left_Tab):
- app.window.select_next_chat(Direction.PREV, unread_first=True)
- return True
-
- if event.keyval in (Gdk.KEY_Page_Down, Gdk.KEY_Page_Up):
- self.conversation_view.event(event)
- return True
-
- if event.get_state() & Gdk.ModifierType.CONTROL_MASK:
- if event.keyval == Gdk.KEY_Tab:
- app.window.select_next_chat(Direction.NEXT, unread_first=True)
- return True
-
- message_buffer = self.msg_textview.get_buffer()
- event_state = event.get_state()
- if event.keyval == Gdk.KEY_Up:
- if event_state & Gdk.ModifierType.CONTROL_MASK:
- if event_state & Gdk.ModifierType.SHIFT_MASK: # Ctrl+Shift+UP
- self.scroll_messages('up', message_buffer, 'received')
- else: # Ctrl+UP
- self.scroll_messages('up', message_buffer, 'sent')
- return True
- elif event.keyval == Gdk.KEY_Down:
- if event_state & Gdk.ModifierType.CONTROL_MASK:
- if event_state & Gdk.ModifierType.SHIFT_MASK: # Ctrl+Shift+Down
- self.scroll_messages('down', message_buffer, 'received')
- else: # Ctrl+Down
- self.scroll_messages('down', message_buffer, 'sent')
- return True
- elif event.keyval in (Gdk.KEY_Return, Gdk.KEY_KP_Enter): # ENTER
- if event_state & Gdk.ModifierType.SHIFT_MASK:
- textview.insert_newline()
- return True
-
- if event_state & Gdk.ModifierType.CONTROL_MASK:
- if not app.settings.get('send_on_ctrl_enter'):
- textview.insert_newline()
- return True
- else:
- if app.settings.get('send_on_ctrl_enter'):
- textview.insert_newline()
- return True
-
- if not app.account_is_available(self.account):
- # we are not connected
- ErrorDialog(
- _('No Connection Available'),
- _('Your message can not be sent until you are connected.'))
- return True
-
- self._on_send_message()
- return True
-
- elif event.keyval == Gdk.KEY_z: # CTRL+z
- if event_state & Gdk.ModifierType.CONTROL_MASK:
- self.msg_textview.undo()
- return True
-
- return False
-
def _on_autoscroll_changed(self,
_widget: ScrolledView,
autoscroll: bool
@@ -918,40 +312,7 @@ class BaseControl(ChatCommandProcessor, CommandTools, EventHelper):
app.window.mark_as_read(self.account, self.contact.jid)
def _on_jump_to_end(self, _button: Gtk.Button) -> None:
- self.scroll_to_end(force=True)
- self._jump_to_end_button.reset_unread_count()
-
- def _on_drag_data_received(self,
- widget: Gtk.Widget,
- context: Gdk.DragContext,
- x_coord: int,
- y_coord: int,
- selection: Gtk.SelectionData,
- target_type: int,
- timestamp: int
- ) -> None:
- """
- Derived types SHOULD implement this
- """
-
- def _on_drag_leave(self,
- _widget: Gtk.Widget,
- _context: Gdk.DragContext,
- _time: int
- ) -> None:
- self.xml.drop_area.set_no_show_all(True)
- self.xml.drop_area.hide()
-
- def _on_drag_motion(self,
- _widget: Gtk.Widget,
- _context: Gdk.DragContext,
- _x_coord: int,
- _y_coord: int,
- _time: int
- ) -> bool:
- self.xml.drop_area.set_no_show_all(False)
- self.xml.drop_area.show_all()
- return True
+ self.reset_view()
def drag_data_file_transfer(self, selection: Gtk.SelectionData) -> None:
# we may have more than one file dropped
@@ -964,114 +325,11 @@ class BaseControl(ChatCommandProcessor, CommandTools, EventHelper):
'not uploaded: %s') % path)
continue
- self._start_filetransfer(path)
-
- def get_seclabel(self) -> Optional[str]:
- return self._security_label_selector.get_seclabel()
+ app.interface.start_file_transfer(self.contact, path)
def get_our_nick(self) -> str:
return app.nicks[self.account]
- def _on_send_message(self, *args: Any) -> None:
- self.msg_textview.replace_emojis()
- message = self.msg_textview.get_text()
- self.send_message(message)
-
- def send_message(self,
- message: str,
- type_: str = 'chat',
- resource: Optional[str] = None,
- process_commands: bool = True,
- attention: bool = False
- ) -> None:
- """
- Send the given message to the active tab. Doesn't return None if error
- """
- if not message or message == '\n':
- return None
-
- if process_commands and self.process_as_command(message):
- return
-
- label = self.get_seclabel()
-
- correct_id: Optional[str] = None
- if self.correcting and self.last_sent_msg:
- correct_id = self.last_sent_msg
- else:
- correct_id = None
-
- chatstate = self._client.get_module('Chatstate').get_active_chatstate(
- self.contact)
-
- message_ = OutgoingMessage(account=self.account,
- contact=self.contact,
- message=message,
- type_=type_,
- chatstate=chatstate,
- resource=resource,
- user_nick=self.user_nick,
- label=label,
- control=self,
- attention=attention,
- correct_id=correct_id)
-
- self._client.send_message(message_)
-
- # Record the history of sent messages
- self.save_message(message, 'sent')
-
- # Be sure to send user nickname only once according to JEP-0172
- self.user_nick = None
-
- self.msg_textview.clear()
-
- def _on_message_tv_buffer_changed(self,
- textbuffer: Gtk.TextBuffer
- ) -> None:
- has_text = self.msg_textview.has_text()
- app.window.lookup_action(
- f'send-message-{self.control_id}').set_enabled(has_text)
-
- if textbuffer.get_char_count() and self.encryption:
- app.plugin_manager.extension_point(
- 'typing' + self.encryption, self)
-
- self._client.get_module('Chatstate').set_keyboard_activity(
- self.contact)
- if not has_text:
- self._client.get_module('Chatstate').set_chatstate_delayed(
- self.contact, Chatstate.ACTIVE)
- return
- self._client.get_module('Chatstate').set_chatstate(
- self.contact, Chatstate.COMPOSING)
-
- def save_message(self, message: str, msg_type: str) -> None:
- # save the message, so user can scroll though the list with key up/down
- if msg_type == 'sent':
- history = self.sent_history
- pos = self.sent_history_pos
- else:
- history = self.received_history
- pos = self.received_history_pos
- size = len(history)
- scroll = pos != size
- # we don't want size of the buffer to grow indefinitely
- max_size = app.settings.get('key_up_lines')
- for _i in range(size - max_size + 1):
- if pos == 0:
- break
- history.pop(0)
- pos -= 1
- history.append(message)
- if not scroll or msg_type == 'sent':
- pos = len(history)
- if msg_type == 'sent':
- self.sent_history_pos = pos
- self.orig_msg = None
- else:
- self.received_history_pos = pos
-
def _allow_add_message(self) -> bool:
# Only add messages if the view is already populated
return self.is_chat_loaded and self._scrolled_view.get_lower_complete()
@@ -1128,7 +386,7 @@ class BaseControl(ChatCommandProcessor, CommandTools, EventHelper):
if not self._scrolled_view.get_autoscroll():
if kind == 'outgoing':
- self.scroll_to_end()
+ self.reset_view()
else:
self._jump_to_end_button.add_unread_count()
else:
@@ -1141,11 +399,8 @@ class BaseControl(ChatCommandProcessor, CommandTools, EventHelper):
self.last_msg_id = message_id
if kind == 'incoming':
- # Record the history of received messages
- self.save_message(text, 'received')
-
- # Issue notification
if notify:
+ # Issue notification
self._notify(name, text, tim, additional_data)
if not chat_active and notify:
@@ -1201,8 +456,10 @@ class BaseControl(ChatCommandProcessor, CommandTools, EventHelper):
return
if self.is_privatechat:
+ room_contact = self._client.get_module('Contacts').get_contact(
+ self.contact.jid.bare)
msg_type = 'private-chat-message'
- title += f' (private in {self.room_name})'
+ title += f' (private in {room_contact.name})'
sound = 'first_message_received'
# Is it a history message? Don't want sound-floods when we join.
@@ -1222,65 +479,11 @@ class BaseControl(ChatCommandProcessor, CommandTools, EventHelper):
text=text,
sound=sound))
- def toggle_emoticons(self) -> None:
- """
- Hide show emoticons_button
- """
- if app.settings.get('emoticons_theme'):
- self.xml.emoticons_button.set_no_show_all(False)
- self.xml.emoticons_button.show()
- else:
- self.xml.emoticons_button.set_no_show_all(True)
- self.xml.emoticons_button.hide()
-
- def set_emoticon_popover(self) -> None:
- if not app.settings.get('emoticons_theme'):
- return
-
- if sys.platform == 'darwin':
- # TODO: Remove if colored emoji rendering works well on
- # Windows and MacOS
- emoji_chooser.text_widget = self.msg_textview
- self.xml.emoticons_button.set_popover(emoji_chooser)
+ def load_messages(self) -> None:
+ if self._chat_loaded:
return
- self.xml.emoticons_button.set_sensitive(True)
- self.xml.emoticons_button.connect('clicked',
- self._on_emoticon_button_clicked)
-
- def _on_emoticon_button_clicked(self, _widget: Gtk.Button) -> None:
- # Present GTK emoji chooser (not cross platform compatible)
- self.msg_textview.emit('insert-emoji')
- self.xml.emoticons_button.set_active(False)
-
- def _on_formatting_menuitem_activate(self,
- menu_item: Gtk.CheckMenuItem
- ) -> None:
- formatting = menu_item.get_name()
- self.msg_textview.apply_formatting(formatting)
-
- def set_control_active(self, state: bool) -> None:
- if not self._chat_loaded:
- self.fetch_n_lines_history(self._scrolled_view, True, 20)
-
- if state:
- self.set_emoticon_popover()
- self._security_label_selector.update()
-
- if self.msg_textview.has_text():
- self._client.get_module('Chatstate').set_chatstate(
- self.contact, Chatstate.PAUSED)
- else:
- self._client.get_module('Chatstate').set_chatstate(
- self.contact, Chatstate.ACTIVE)
- else:
- self._client.get_module('Chatstate').set_chatstate(
- self.contact, Chatstate.INACTIVE)
-
- def set_message_input_state(self, state: bool) -> None:
- self.xml.formattings_button.set_sensitive(state)
- self.msg_textview.set_sensitive(state)
- self.msg_textview.set_editable(state)
+ self.fetch_n_lines_history(self._scrolled_view, True, 20)
@property
def is_chat_loaded(self) -> bool:
@@ -1294,11 +497,6 @@ class BaseControl(ChatCommandProcessor, CommandTools, EventHelper):
def get_autoscroll(self) -> bool:
return self._scrolled_view.get_autoscroll()
- def scroll_to_end(self, force: bool = False) -> None:
- # Clear view and reload conversation
- self.reset_view()
- self.conversation_view.scroll_to_end(force)
-
def scroll_to_message(self, log_line_id: int, timestamp: float) -> None:
row = self.conversation_view.get_row_by_log_line_id(log_line_id)
if row is None:
@@ -1412,82 +610,3 @@ class BaseControl(ChatCommandProcessor, CommandTools, EventHelper):
log_line_id=msg.log_line_id,
marker=msg.marker,
error=msg.error)
-
- def has_focus(self) -> bool:
- if app.window.get_property('has-toplevel-focus'):
- if self == app.window.get_active_control():
- return True
- return False
-
- def scroll_messages(self,
- direction: str,
- msg_buf: Gtk.TextBuffer,
- msg_type: str,
- ) -> None:
- # pylint: disable=too-many-boolean-expressions
- if msg_type == 'sent':
- history = self.sent_history
- pos = self.sent_history_pos
- self.received_history_pos = len(self.received_history)
- else:
- history = self.received_history
- pos = self.received_history_pos
- self.sent_history_pos = len(self.sent_history)
- size = len(history)
- if self.orig_msg is None:
- # user was typing something and then went into history, so save
- # whatever is already typed
- start_iter = msg_buf.get_start_iter()
- end_iter = msg_buf.get_end_iter()
- self.orig_msg = msg_buf.get_text(start_iter, end_iter, False)
- if (pos == size and size > 0 and direction == 'up' and
- msg_type == 'sent' and not self.correcting and (
- not history[pos - 1].startswith('/') or
- history[pos - 1].startswith('/me'))):
- if self.last_sent_msg is not None:
- self.correcting = True
- self.msg_textview.get_style_context().add_class(
- 'gajim-msg-correcting')
- message = history[pos - 1]
- msg_buf.set_text(message)
- return
- if self.correcting:
- # We were previously correcting
- self.msg_textview.get_style_context().remove_class(
- 'gajim-msg-correcting')
- self.correcting = False
- pos += -1 if direction == 'up' else +1
- if pos == -1:
- return
- if pos >= size:
- pos = size
- message = self.orig_msg
- self.orig_msg = None
- else:
- message = history[pos]
- if msg_type == 'sent':
- self.sent_history_pos = pos
- else:
- self.received_history_pos = pos
- if self.orig_msg is not None:
- message = '> %s\n' % message.replace('\n', '\n> ')
- msg_buf.set_text(message)
-
- def update_account_badge(self) -> None:
- show = len(app.settings.get_active_accounts()) > 1
- self.xml.account_badge_box.set_visible(show)
-
-
-class ScrolledWindow(Gtk.ScrolledWindow):
- def __init__(self, *args: Any, **kwargs: Any) -> None:
- Gtk.ScrolledWindow.__init__(self, *args, **kwargs)
-
- self.set_overlay_scrolling(False)
- self.set_max_content_height(100)
- self.set_propagate_natural_height(True)
- self.get_style_context().add_class('scrolled-no-border')
- self.get_style_context().add_class('no-scroll-indicator')
- self.get_style_context().add_class('scrollbar-style')
- self.get_style_context().add_class('one-line-scrollbar')
- self.set_shadow_type(Gtk.ShadowType.IN)
- self.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
diff --git a/gajim/gtk/controls/chat.py b/gajim/gtk/controls/chat.py
index 74cd59e28..ab32532d1 100644
--- a/gajim/gtk/controls/chat.py
+++ b/gajim/gtk/controls/chat.py
@@ -30,51 +30,31 @@ from typing import Type
from typing import Optional
import logging
-import sys
-
-from gi.repository import Gtk
-from gi.repository import Gio
-from gi.repository import GLib
-from gi.repository import Gdk
from nbxmpp import JID
from nbxmpp.const import Chatstate
from nbxmpp.modules.security_labels import Displaymarking
-from nbxmpp.namespaces import Namespace
from gajim.common import app
from gajim.common import events
-from gajim.common import helpers
-from gajim.common import types
-from gajim.common.client import Client
from gajim.common.i18n import _
from gajim.common.helpers import AdditionalDataDict
-from gajim.common.const import AvatarSize
-from gajim.common.const import CallType
-from gajim.common.const import SimpleClientState
from gajim.common.const import KindConstant
from gajim.common.modules.contacts import BareContact
from gajim.gui.controls.base import BaseControl
-from gajim.gui.const import TARGET_TYPE_URI_LIST
from gajim.gui.const import ControlType
-from gajim.gui.util import open_window
from gajim.command_system.implementation.hosts import ChatCommands
from gajim.command_system.framework import CommandHost
-from ..menus import get_encryption_menu
-from ..menus import get_private_chat_menu
-from ..menus import get_self_contact_menu
-from ..menus import get_singlechat_menu
-
log = logging.getLogger('gajim.gui.controls.chat')
class ChatControl(BaseControl):
- """
+ '''
A control for standard 1-1 chat
- """
+ '''
_type = ControlType.CHAT
# Set a command host to bound to. Every command given through a chat will be
@@ -86,267 +66,14 @@ class ChatControl(BaseControl):
'chat_control',
account,
jid)
-
- self.sendmessage: bool = True
-
- # XEP-0308 Message Correction
- self.correcting: bool = False
- self.last_sent_msg: Optional[str] = None
-
- self.toggle_emoticons()
-
- if self._type == ControlType.CHAT:
- self._client.connect_signal('state-changed',
- self._on_client_state_changed)
-
- if not app.settings.get('hide_chat_banner'):
- self.xml.banner_eventbox.set_no_show_all(False)
-
- self.xml.sendfile_button.set_action_name(
- f'win.send-file-{self.control_id}')
-
- # Menu for the HeaderBar
- if self._type == ControlType.CHAT:
- if self.contact.is_self:
- self.control_menu = get_self_contact_menu(self.control_id,
- self.contact)
- else:
- self.control_menu = get_singlechat_menu(self.control_id,
- self.contact)
- else:
- self.control_menu = get_private_chat_menu(self.control_id,
- self.contact)
-
- # Settings menu
- self.xml.settings_menu.set_menu_model(self.control_menu)
-
- self.update_toolbar()
- self._update_avatar()
-
- self.add_actions()
- self.update_ui()
- self.set_lock_image()
-
- self.xml.encryption_menu.set_menu_model(get_encryption_menu(
- self.control_id, self._type))
- self.set_encryption_menu_icon()
- self.msg_textview.grab_focus()
-
# PluginSystem: adding GUI extension point for this ChatControl
# instance object
app.plugin_manager.gui_extension_point('chat_control', self)
- self.update_actions()
-
- def _connect_contact_signals(self) -> None:
- self.contact.multi_connect({
- 'presence-update': self._on_presence_update,
- 'chatstate-update': self._on_chatstate_update,
- 'nickname-update': self._on_nickname_update,
- 'avatar-update': self._on_avatar_update,
- 'caps-update': self._on_caps_update,
- })
@property
def jid(self) -> JID:
return self.contact.jid
- def add_actions(self) -> None:
- super().add_actions()
- actions = [
- ('invite-contacts-', self._on_invite_contacts),
- ('add-to-roster-', self._on_add_to_roster),
- ('block-contact-', self._on_block_contact),
- ('information-', self._on_information),
- ('start-voice-call-', self._on_start_voice_call),
- ('start-video-call-', self._on_start_video_call),
- ]
-
- for action in actions:
- action_name, func = action
- act = Gio.SimpleAction.new(action_name + self.control_id, None)
- act.connect('activate', func)
- app.window.add_action(act)
-
- chatstate = self.contact.settings.get('send_chatstate')
-
- act = Gio.SimpleAction.new_stateful(
- 'send-chatstate-' + self.control_id,
- GLib.VariantType.new("s"),
- GLib.Variant("s", chatstate))
- act.connect('change-state', self._on_send_chatstate)
- app.window.add_action(act)
-
- marker = self.contact.settings.get('send_marker')
-
- act = Gio.SimpleAction.new_stateful(
- f'send-marker-{self.control_id}',
- None,
- GLib.Variant.new_boolean(marker))
- act.connect('change-state', self._on_send_marker)
- app.window.add_action(act)
-
- def update_actions(self) -> None:
- online = app.account_is_connected(self.account)
-
- if self.type.is_chat:
- self._get_action('add-to-roster-').set_enabled(
- not self.contact.is_in_roster)
-
- # Block contact
- self._get_action('block-contact-').set_enabled(
- online and self._client.get_module('Blocking').supported)
-
- # Jingle AV
- if self.type.is_chat:
- self._get_action('start-voice-call-').set_enabled(
- online and self.contact.supports_audio() and
- sys.platform != 'win32')
- self._get_action('start-video-call-').set_enabled(
- online and self.contact.supports_video() and
- sys.platform != 'win32')
-
- # Send message
- has_text = self.msg_textview.has_text()
- self._get_action('send-message-').set_enabled(online and has_text)
-
- # Send file (HTTP File Upload)
- httpupload = self._get_action('send-file-httpupload-')
- httpupload.set_enabled(online and
- self._client.get_module('HTTPUpload').available)
-
- # Send file (Jingle)
- jingle = self._get_action('send-file-jingle-')
- jingle.set_enabled(online and self.contact.is_jingle_available)
-
- # Send file
- self._get_action('send-file-').set_enabled(jingle.get_enabled() or
- httpupload.get_enabled())
-
- # Set File Transfer Button tooltip
- if online and (httpupload.get_enabled() or jingle.get_enabled()):
- tooltip_text = _('Send File…')
- else:
- tooltip_text = _('No File Transfer available')
- self.xml.sendfile_button.set_tooltip_text(tooltip_text)
-
- # Chat markers
- state = GLib.Variant.new_boolean(
- self.contact.settings.get('send_marker'))
- self._get_action('send-marker-').change_state(state)
-
- # Convert to GC
- enabled = self.contact.supports(Namespace.MUC) and online
- self._get_action('invite-contacts-').set_enabled(enabled)
-
- # Information
- self._get_action('information-').set_enabled(online)
-
- def remove_actions(self) -> None:
- super().remove_actions()
- actions = [
- 'invite-contacts-',
- 'add-to-roster-',
- 'block-contact-',
- 'information-',
- 'start-voice-call-',
- 'start-video-call-',
- 'send-chatstate-',
- 'send-marker-',
- ]
- for action in actions:
- app.window.remove_action(f'{action}{self.control_id}')
-
- def focus(self) -> None:
- if not hasattr(self, 'msg_textview'):
- # focus() is called sometimes with GLib.idle_add()
- # This means there is the possibility that shutdown() was called
- # before focus is executed.
- return
- self.msg_textview.grab_focus()
-
- def delegate_action(self, action: str) -> int:
- res = super().delegate_action(action)
- if res == Gdk.EVENT_STOP:
- return res
-
- if action == 'show-contact-info':
- self._get_action('information-').activate()
- return Gdk.EVENT_STOP
-
- if action == 'send-file':
- self._get_action('send-file-').activate()
- return Gdk.EVENT_STOP
-
- return Gdk.EVENT_PROPAGATE
-
- def _on_add_to_roster(self,
- _action: Gio.SimpleAction,
- _param: Optional[GLib.Variant]
- ) -> None:
- jid = self.contact.jid
- if self.type.is_privatechat and self.contact.real_jid is not None:
- jid = self.contact.real_jid
- open_window('AddContact', account=self.account, jid=jid)
-
- def _on_block_contact(self,
- _action: Gio.SimpleAction,
- _param: Optional[GLib.Variant]
- ) -> None:
- app.window.block_contact(self.account, self.contact.jid)
-
- def _on_information(self,
- _action: Gio.SimpleAction,
- _param: Optional[GLib.Variant]
- ) -> None:
- app.window.contact_info(self.account, self.contact.jid)
-
- def _on_invite_contacts(self,
- _action: Gio.SimpleAction,
- _param: Optional[GLib.Variant]
- ) -> None:
- open_window('AdhocMUC', account=self.account, contact=self.contact)
-
- def _on_send_chatstate(self,
- action: Gio.SimpleAction,
- param: GLib.Variant
- ) -> None:
- action.set_state(param)
- self.contact.settings.set('send_chatstate', param.get_string())
-
- def _on_send_marker(self,
- action: Gio.SimpleAction,
- param: GLib.Variant
- ) -> None:
- action.set_state(param)
- self.contact.settings.set('send_marker', param.get_boolean())
-
- def _on_nickname_received(self, _event):
- self.update_ui()
-
- def _on_chatstate_update(self,
- _contact: types.BareContact,
- _signal_name: str
- ) -> None:
- self.draw_banner_text()
-
- def _on_nickname_update(self,
- _contact: types.BareContact,
- _signal_name: str
- ) -> None:
- self.draw_banner_text()
-
- def _on_presence_update(self,
- _contact: types.BareContact,
- _signal_name: str
- ) -> None:
- self._update_avatar()
-
- def _on_caps_update(self,
- _contact: types.BareContact,
- _signal_name: str
- ) -> None:
- self.update_ui()
-
def _on_mam_message_received(self,
event: events.MamMessageReceived) -> None:
if event.properties.is_muc_pm:
@@ -376,13 +103,6 @@ class ChatControl(BaseControl):
if event.properties.is_sent_carbon:
kind = 'outgoing'
- visible = False
- if event.resource is not None:
- resource_contact = self.contact.get_resource(event.resource)
- visible = resource_contact.is_phone
-
- self.xml.phone_image.set_visible(visible)
-
self.add_message(event.msgtxt,
kind,
tim=event.properties.timestamp,
@@ -392,30 +112,16 @@ class ChatControl(BaseControl):
stanza_id=event.stanza_id,
additional_data=event.additional_data)
- def _on_message_error(self, event: events.MessageError) -> None:
- self.conversation_view.show_error(event.message_id, event.error)
-
def _on_message_sent(self, event: events.MessageSent) -> None:
if not event.message:
return
- if event.correct_id is None:
- oob_url = event.additional_data.get_value('gajim', 'oob_url')
- if oob_url == event.message:
- self.last_sent_msg = None
- else:
- self.last_sent_msg = event.message_id
-
message_id = event.message_id
if event.label:
displaymarking = event.label.displaymarking
else:
displaymarking = None
- if self.correcting:
- self.correcting = False
- self.msg_textview.get_style_context().remove_class(
- 'gajim-msg-correcting')
if event.correct_id:
self.conversation_view.correct_message(
@@ -435,7 +141,7 @@ class ChatControl(BaseControl):
def _on_displayed_received(self, event: events.DisplayedReceived) -> None:
self.conversation_view.set_read_marker(event.marker_id)
- def _nec_ping(self, event: events.ApplicationEvent):
+ def _on_ping_event(self, event: events.PingEventT) -> None:
if self.contact != event.contact:
return
if isinstance(event, events.PingSent):
@@ -443,89 +149,8 @@ class ChatControl(BaseControl):
elif isinstance(event, events.PingReply):
self.add_info_message(
_('Pong! (%s seconds)') % event.seconds)
- elif isinstance(event, events.PingError):
- self.add_info_message(event.error)
-
- def _on_start_voice_call(self,
- _action: Gio.SimpleAction,
- _param: Optional[GLib.Variant]
- ) -> None:
- app.call_manager.start_call(self.account, self.jid, CallType.AUDIO)
-
- def _on_start_video_call(self,
- _action: Gio.SimpleAction,
- _param: Optional[GLib.Variant]
- ) -> None:
- app.call_manager.start_call(self.account, self.jid, CallType.VIDEO)
-
- def update_ui(self) -> None:
- BaseControl.update_ui(self)
- self.update_toolbar()
- self._update_avatar()
- self.update_actions()
-
- def draw_banner_text(self) -> None:
- """
- Draws the chat banner's text (e.g. name, chat state) in the top of the
- chat window
- """
- contact = self.contact
- name = contact.name
-
- if self.jid == self._client.get_own_jid().bare:
- name = _('Note to myself')
-
- if self._type.is_privatechat:
- name = f'{name} ({self.room_name})'
-
- chatstate = self.contact.chatstate
- if chatstate is not None:
- chatstate = chatstate.value
-
- if app.settings.get('show_chatstate_in_banner'):
- chatstate = helpers.get_uf_chatstate(chatstate)
-
- label_text = f'<span>{name}</span>' \
- f'<span size="x-small" weight="light">' \
- f' {chatstate}</span>'
- label_tooltip = f'{name} {chatstate}'
else:
- label_text = f'<span>{name}</span>'
- label_tooltip = name
-
- status_text = ''
- self.xml.banner_label.hide()
- self.xml.banner_label.set_no_show_all(True)
- self.xml.banner_label.set_markup(status_text)
-
- self.xml.banner_name_label.set_markup(label_text)
- self.xml.banner_name_label.set_tooltip_text(label_tooltip)
-
- def send_message(self,
- message: str,
- process_commands: bool = True,
- attention: bool = False
- ) -> None:
- """
- Send a message to contact
- """
-
- if self.encryption:
- self.sendmessage = True
- app.plugin_manager.extension_point('send_message' + self.encryption,
- self)
- if not self.sendmessage:
- return
-
- message = helpers.remove_invalid_xml_chars(message)
- if message in ('', None, '\n'):
- return
-
- BaseControl.send_message(self,
- message,
- type_='chat',
- process_commands=process_commands,
- attention=attention)
+ self.add_info_message(event.error)
def add_message(self,
text: str,
@@ -561,8 +186,6 @@ class ChatControl(BaseControl):
# instance object
app.plugin_manager.remove_gui_extension_point('chat_control', self)
- self.remove_actions()
-
# Send 'gone' chatstate
self._client.get_module('Chatstate').set_chatstate(
self.contact, Chatstate.GONE)
@@ -570,50 +193,7 @@ class ChatControl(BaseControl):
super(ChatControl, self).shutdown()
app.check_finalize(self)
- def _on_avatar_update(self,
- _contact: types.BareContact,
- _signal_name: str
- ) -> None:
- self._update_avatar()
-
- def _update_avatar(self) -> None:
- scale = app.window.get_scale_factor()
- surface = self.contact.get_avatar(AvatarSize.CHAT, scale)
- self.xml.avatar_image.set_from_surface(surface)
-
- def _on_drag_data_received(self,
- _widget: Gtk.Widget,
- _context: Gdk.DragContext,
- _x_coord: int,
- _y_coord: int,
- selection: Gtk.SelectionData,
- target_type: int,
- _timestamp: int
- ) -> None:
- if not selection.get_data():
- return
-
- log.debug('Drop received: %s, %s', selection.get_data(), target_type)
-
- # TODO: Contact drag and drop for AdHocMUC
- if target_type == TARGET_TYPE_URI_LIST:
- # File drag and drop (handled in chat_control_base)
- self.drag_data_file_transfer(selection)
-
- def _on_client_state_changed(self,
- _client: Client,
- _signal_name: str,
- state: SimpleClientState):
- self.set_message_input_state(state.is_connected)
-
- self._update_avatar()
- self.update_toolbar()
- self.draw_banner()
- self.update_actions()
-
def _on_presence_received(self, event: events.PresenceReceived) -> None:
- self.update_ui()
-
if not app.settings.get('print_status_in_chats'):
return
diff --git a/gajim/gtk/controls/groupchat.py b/gajim/gtk/controls/groupchat.py
index 33096eb5f..1f012d157 100644
--- a/gajim/gtk/controls/groupchat.py
+++ b/gajim/gtk/controls/groupchat.py
@@ -30,27 +30,17 @@ from typing import Optional
import logging
from nbxmpp import JID
-from nbxmpp.protocol import InvalidJid
-from nbxmpp.protocol import validate_resourcepart
from nbxmpp.const import StatusCode
-from nbxmpp.errors import StanzaError
from nbxmpp.modules.security_labels import Displaymarking
from nbxmpp.structs import PresenceProperties
from nbxmpp.structs import MessageProperties
from nbxmpp.structs import MucSubject
from gi.repository import Gtk
-from gi.repository import Gdk
-from gi.repository import GLib
-from gi.repository import Gio
from gajim.common import app
from gajim.common import events
-from gajim.common import ged
from gajim.common import helpers
-from gajim.common.client import Client
-from gajim.common.const import AvatarSize
-from gajim.common.const import SimpleClientState
from gajim.common.helpers import AdditionalDataDict
from gajim.common.helpers import event_filter
from gajim.common.helpers import to_user_string
@@ -58,24 +48,16 @@ from gajim.common.helpers import to_user_string
from gajim.common.i18n import _
from gajim.common.modules.contacts import GroupchatContact
from gajim.common.modules.contacts import GroupchatParticipant
-from gajim.common.structs import OutgoingMessage
from gajim.gui.controls.base import BaseControl
from gajim.command_system.implementation.hosts import GroupChatCommands
from gajim.gui.const import ControlType
-from gajim.gui.const import TARGET_TYPE_URI_LIST
from gajim.gui.dialogs import DialogButton
from gajim.gui.dialogs import ConfirmationDialog
-from gajim.gui.dataform import DataFormWidget
-from gajim.gui.groupchat_inviter import GroupChatInviter
from gajim.gui.groupchat_roster import GroupchatRoster
from gajim.gui.groupchat_state import GroupchatState
-from gajim.gui.util import open_window
-
-from ..menus import get_encryption_menu
-from ..menus import get_groupchat_menu
log = logging.getLogger('gajim.gui.controls.groupchat')
@@ -94,111 +76,29 @@ class GroupchatControl(BaseControl):
account,
jid)
- self._client.connect_signal('state-changed',
- self._on_client_state_changed)
-
self.is_anonymous: bool = True
- self.toggle_emoticons()
-
- self.room_jid: str = str(self.contact.jid)
-
- # Stores nickname we want to kick
- self._kick_nick: Optional[str] = None
-
- # Stores nickname we want to ban
- self._ban_jid: Optional[str] = None
-
- # Last sent message text
- self.last_sent_txt: str = ''
-
- # Attribute, encryption plugins use to signal the message can be sent
- self.sendmessage: bool = False
+ self.room_jid = str(self.contact.jid)
- # XEP-0308 Message Correction
- self.correcting: bool = False
- self.last_sent_msg: Optional[str] = None
-
- self._groupchat_state = GroupchatState()
- self._groupchat_state.connect('join-clicked',
- self._on_groupchat_state_join_clicked)
- self._groupchat_state.connect('abort-clicked',
- self._on_groupchat_state_abort_clicked)
+ self._groupchat_state = GroupchatState(self.contact)
self.xml.conv_view_overlay.add_overlay(self._groupchat_state)
self.roster = GroupchatRoster(self.account, self.room_jid, self)
- self.xml.roster_revealer.add(self.roster)
-
- show_roster = app.settings.get('hide_groupchat_occupants_list')
- self.xml.roster_revealer.set_reveal_child(show_roster)
- app.settings.bind_signal(
- 'hide_groupchat_occupants_list',
- self.xml.roster_revealer,
- 'set_reveal_child')
- self._set_toggle_roster_button_icon(show_roster)
- app.settings.connect_signal(
- 'hide_groupchat_occupants_list',
- self._set_toggle_roster_button_icon)
self.roster.connect('row-activated', self._on_roster_row_activated)
- self.add_actions()
- GLib.idle_add(self.update_actions)
-
- if not app.settings.get('hide_groupchat_banner'):
- self.xml.banner_eventbox.set_no_show_all(False)
-
- # muc attention flag (when we are mentioned in a muc)
- # if True, the room has mentioned us
- self.attention_flag: bool = False
-
- # Send file
- self.xml.sendfile_button.set_action_name(
- f'win.send-file-{self.control_id}')
-
- # Encryption
- self.set_lock_image()
-
- self.xml.encryption_menu.set_menu_model(get_encryption_menu(
- self.control_id, self._type))
- self.set_encryption_menu_icon()
-
- self._update_avatar()
+ show_roster = app.settings.get('hide_groupchat_occupants_list')
+ self._roster_revealer = Gtk.Revealer(no_show_all=not show_roster)
+ self._roster_revealer.add(self.roster)
+ self._roster_revealer.set_reveal_child(show_roster)
+ self.xml.conv_view_box.add(self._roster_revealer)
- # Holds CaptchaRequest widget
- self._captcha_request: Optional[DataFormWidget] = None
+ app.settings.connect_signal(
+ 'hide_groupchat_occupants_list', self._show_roster)
self._subject_text = ''
- # Groupchat invite
- self.xml.quick_invite_button.set_action_name(
- f'win.invite-{self.control_id}')
-
- self._invite_box = GroupChatInviter(self.room_jid)
- self.xml.invite_grid.attach(self._invite_box, 0, 0, 1, 1)
- self._invite_box.connect('listbox-changed', self._on_invite_ready)
-
- self.control_menu = get_groupchat_menu(self.control_id,
- self.account,
- self.contact.jid)
-
- self.xml.settings_menu.set_menu_model(self.control_menu)
-
- app.settings.connect_signal('gc_print_join_left_default',
- self.update_actions)
- app.settings.connect_signal('gc_print_status_default',
- self.update_actions)
-
- self.register_events([
- ('bookmarks-received', ged.GUI1, self._on_bookmarks_received),
- ])
-
self._set_control_inactive()
- # Stack
- self.xml.stack.show_all()
- self.xml.stack.set_visible_child_name('groupchat')
-
- self.update_ui()
self.widget.show_all()
# PluginSystem: adding GUI extension point for this GroupchatControl
@@ -208,7 +108,6 @@ class GroupchatControl(BaseControl):
def _connect_contact_signals(self) -> None:
self.contact.multi_connect({
'state-changed': self._on_muc_state_changed,
- 'avatar-update': self._on_avatar_update,
'user-joined': self._on_user_joined,
'user-left': self._on_user_left,
'user-affiliation-changed': self._on_user_affiliation_changed,
@@ -218,189 +117,28 @@ class GroupchatControl(BaseControl):
'room-kicked': self._on_room_kicked,
'room-destroyed': self._on_room_destroyed,
'room-config-finished': self._on_room_config_finished,
- 'room-config-failed': self._on_room_config_failed,
'room-config-changed': self._on_room_config_changed,
- 'room-password-required': self._on_room_password_required,
- 'room-creation-failed': self._on_room_creation_failed,
'room-presence-error': self._on_room_presence_error,
'room-voice-request': self._on_room_voice_request,
- 'room-captcha-challenge': self._on_room_captcha_challenge,
- 'room-captcha-error': self._on_room_captcha_error,
'room-subject': self._on_room_subject,
- 'room-joined': self._on_room_joined,
- 'room-join-failed': self._on_room_join_failed,
})
def _on_muc_state_changed(self,
_contact: GroupchatContact,
_signal_name: str
) -> None:
- if self.contact.is_joining:
- self._groupchat_state.set_joining()
-
if self.contact.is_joined:
self._set_control_active()
- self._groupchat_state.set_joined()
elif self.contact.is_not_joined:
self._set_control_inactive()
- def _on_client_state_changed(self,
- _client: Client,
- _signal_name: str,
- state: SimpleClientState
- ) -> None:
+ def _on_muc_disco_update(self, event: events.MucDiscoUpdate) -> None:
pass
- @property
- def disco_info(self):
- return app.storage.cache.get_last_disco_info(self.contact.jid)
-
- def add_actions(self) -> None:
- super().add_actions()
- actions = [
- ('change-nickname-', None, self._on_change_nick),
- ('destroy-', None, self._on_destroy_room),
- ('request-voice-', None, self._on_request_voice),
- ('groupchat-details-', None, self._on_details),
- ('invite-', None, self._on_invite),
- ('contact-information-', 's', self._on_contact_information),
- ('execute-command-', 's', self._on_execute_command),
- ('ban-', 's', self._on_ban),
- ('kick-', 's', self._on_kick),
- ('change-role-', 'as', self._on_change_role),
- ('change-affiliation-', 'as', self._on_change_affiliation),
- ]
-
- for action in actions:
- action_name, variant, func = action
- if variant is not None:
- variant = GLib.VariantType.new(variant)
- act = Gio.SimpleAction.new(action_name + self.control_id, variant)
- act.connect("activate", func)
- app.window.add_action(act)
-
- def update_actions(self, *args: Any) -> None:
- request_voice = False
- joined = self.contact.is_joined
- if joined:
- contact = self.contact.get_self()
- request_voice = contact.role.is_visitor
-
- self._get_action('request-voice-').set_enabled(request_voice)
-
- # Change Nick
- self._get_action('change-nickname-').set_enabled(
- joined and not self.is_irc())
-
- # Execute command
- self._get_action('execute-command-').set_enabled(joined)
-
- # Send message
- has_text = self.msg_textview.has_text()
- self._get_action('send-message-').set_enabled(
- joined and has_text)
-
- # Send file (HTTP File Upload)
- httpupload = self._get_action(
- 'send-file-httpupload-')
- httpupload.set_enabled(joined and
- self._client.get_module('HTTPUpload').available)
- self._get_action('send-file-').set_enabled(httpupload.get_enabled())
-
- if joined and httpupload.get_enabled():
- tooltip_text = _('Send File…')
- max_file_size = self._client.get_module('HTTPUpload').max_file_size
- if max_file_size is not None:
- max_file_size = GLib.format_size_full(max_file_size,
- self._units)
- tooltip_text = _('Send File (max. %s)…') % max_file_size
- else:
- tooltip_text = _('No File Transfer available')
- self.xml.sendfile_button.set_tooltip_text(tooltip_text)
-
- self._get_action('contact-information-').set_enabled(joined)
-
- self._get_action('groupchat-details-').set_enabled(joined)
-
- self._get_action('execute-command-').set_enabled(joined)
-
- self._get_action('ban-').set_enabled(joined)
-
- self._get_action('kick-').set_enabled(joined)
-
- self._get_action('invite-').set_enabled(joined)
-
- def remove_actions(self) -> None:
- super().remove_actions()
- actions = [
- 'change-nickname-',
- 'destroy-',
- 'configure-',
- 'request-voice-',
- 'groupchat-details-',
- 'invite-',
- 'contact-information-',
- 'execute-command-',
- 'ban-',
- 'kick-',
- 'change-role-',
- 'change-affiliation-',
- ]
-
- for action in actions:
- app.window.remove_action(f'{action}{self.control_id}')
-
- def is_irc(self) -> bool:
- if self.disco_info is None:
- return False
- return self.disco_info.is_irc
-
- def _show_page(self, name: str) -> None:
- transition = Gtk.StackTransitionType.SLIDE_DOWN
- if name == 'groupchat':
- transition = Gtk.StackTransitionType.SLIDE_UP
- active_control = app.window.get_active_control()
- if active_control == self:
- self.msg_textview.grab_focus()
- self.xml.stack.set_visible_child_full(name, transition)
-
- def _get_current_page(self) -> str:
- return self.xml.stack.get_visible_child_name()
-
- def _on_muc_disco_update(self, _event: events.MucDiscoUpdate) -> None:
- self.update_actions()
- self.draw_banner_text()
-
# Actions
- def _on_details(self,
- _action: Gio.SimpleAction,
- _param: Optional[GLib.Variant]
- ) -> None:
- open_window('GroupchatDetails',
- contact=self.contact,
- subject=self._subject_text)
-
- def _on_invite(self,
- _action: Gio.SimpleAction,
- _param: Optional[GLib.Variant]
- ) -> None:
- self._invite_box.load_contacts()
- self._show_page('invite')
-
- def _on_invite_ready(self,
- _listbox: GroupChatInviter,
- invitable: bool
- ) -> None:
- self.xml.invite_button.set_sensitive(invitable)
-
- def _on_invite_clicked(self, _button: Gtk.Button) -> None:
- invitees = self._invite_box.get_invitees()
- for jid in invitees:
- self.invite(JID.from_string(jid))
- self._show_page('groupchat')
-
def invite(self, invited_jid: JID) -> None:
+ # TODO: Remove, used by command system
self._client.get_module('MUC').invite(
self.contact.jid, invited_jid)
invited_contact = self._client.get_module('Contacts').get_contact(
@@ -408,128 +146,14 @@ class GroupchatControl(BaseControl):
self.add_info_message(
_('%s has been invited to this group chat') % invited_contact.name)
- def _on_destroy_room(self, _button: Gtk.Button) -> None:
- self.xml.destroy_reason_entry.grab_focus()
- self.xml.destroy_button.grab_default()
- self._show_page('destroy')
-
- def _on_destroy_alternate_changed(self,
- entry: Gtk.Entry,
- _param: Any
- ) -> None:
- jid = entry.get_text()
- if jid:
- try:
- jid = helpers.validate_jid(jid)
- except Exception:
- icon = 'dialog-warning-symbolic'
- text = _('Invalid XMPP Address')
- self.xml.destroy_alternate_entry.set_icon_from_icon_name(
- Gtk.EntryIconPosition.SECONDARY, icon)
- self.xml.destroy_alternate_entry.set_icon_tooltip_text(
- Gtk.EntryIconPosition.SECONDARY, text)
- self.xml.destroy_button.set_sensitive(False)
- return
- self.xml.destroy_alternate_entry.set_icon_from_icon_name(
- Gtk.EntryIconPosition.SECONDARY, None)
- self.xml.destroy_button.set_sensitive(True)
-
- def _on_destroy_confirm(self, _button: Gtk.Button) -> None:
- reason = self.xml.destroy_reason_entry.get_text()
- jid = self.xml.destroy_alternate_entry.get_text()
- self._client.get_module('MUC').destroy(self.room_jid, reason, jid)
- self._show_page('groupchat')
-
- def _on_request_voice_clicked(self, _button: Gtk.Button) -> None:
- self._request_voice()
- self.xml.visitor_popover.popdown()
-
- def _on_request_voice(self,
- _action: Gio.SimpleAction,
- _param: Optional[GLib.Variant]
- ) -> None:
- self._request_voice()
-
- def _request_voice(self) -> None:
- self._client.get_module('MUC').request_voice(self.room_jid)
-
- def _on_execute_command(self,
- _action: Gio.SimpleAction,
- param: GLib.Variant
- ) -> None:
- jid = self.room_jid
- nick = param.get_string()
- if nick:
- jid += '/' + nick
- open_window('AdHocCommands', account=self.account, jid=jid)
-
- def _on_contact_information(self,
- _action: Gio.SimpleAction,
- param: GLib.Variant
- ) -> None:
- nick = param.get_string()
- contact = self.contact.get_resource(nick)
- open_window('ContactInfo', account=self.account, contact=contact)
-
- def _on_kick(self,
- _action: Gio.SimpleAction,
- param: GLib.Variant
- ) -> None:
- nick = param.get_string()
- self._kick_nick = nick
- text = _('Kick %s') % nick
- self.xml.kick_label.set_text(text)
- self.xml.kick_label.set_tooltip_text(text)
- self.xml.kick_reason_entry.grab_focus()
- self.xml.kick_participant_button.grab_default()
- self._show_page('kick')
-
- def _on_ban(self,
- _action: Gio.SimpleAction,
- param: GLib.Variant
- ) -> None:
- jid = param.get_string()
- self._ban_jid = jid
- text = _('Ban %s') % jid
- self.xml.ban_label.set_text(text)
- self.xml.ban_label.set_tooltip_text(text)
- self.xml.ban_reason_entry.grab_focus()
- self.xml.ban_participant_button.grab_default()
- self._show_page('ban')
-
- def _on_change_role(self,
- _action: Gio.SimpleAction,
- param: GLib.Variant
- ) -> None:
- nick, role = param.get_strv()
- self._client.get_module('MUC').set_role(self.room_jid, nick, role)
-
- def _on_change_affiliation(self,
- _action: Gio.SimpleAction,
- param: GLib.Variant
- ) -> None:
- jid, affiliation = param.get_strv()
- self._client.get_module('MUC').set_affiliation(
- self.room_jid,
- {jid: {'affiliation': affiliation}})
-
- def _set_toggle_roster_button_icon(self,
- show_roster: bool,
- *args: Any) -> None:
- icon = 'go-next-symbolic' if show_roster else 'go-previous-symbolic'
- self.xml.toggle_roster_image.set_from_icon_name(
- icon, Gtk.IconSize.BUTTON)
-
- def _show_roster(self, *args: Any) -> None:
- show = not self.xml.roster_revealer.get_reveal_child()
- self._set_toggle_roster_button_icon(show)
-
+ def _show_roster(self, show_roster: bool, *args: Any) -> None:
transition = Gtk.RevealerTransitionType.SLIDE_RIGHT
- if show:
+ if show_roster:
+ self._roster_revealer.set_no_show_all(False)
+ self._roster_revealer.show_all()
transition = Gtk.RevealerTransitionType.SLIDE_LEFT
- self.xml.roster_revealer.set_transition_type(transition)
- self.xml.roster_revealer.set_reveal_child(show)
- app.settings.set('hide_groupchat_occupants_list', show)
+ self._roster_revealer.set_transition_type(transition)
+ self._roster_revealer.set_reveal_child(show_roster)
def _on_roster_row_activated(self,
_roster: GroupchatRoster,
@@ -547,33 +171,13 @@ class GroupchatControl(BaseControl):
contact = self.contact.get_resource(nick)
app.window.add_private_chat(self.account, contact.jid, select=True)
- def _on_avatar_update(self,
- _contact: GroupchatContact,
- _signal_name: str
- ) -> None:
- self._update_avatar()
-
- def _update_avatar(self) -> None:
- surface = self.contact.get_avatar(
- AvatarSize.CHAT, app.window.get_scale_factor())
- self.xml.avatar_image.set_from_surface(surface)
-
- def draw_banner_text(self) -> None:
- """
- Draw the text in the fat line at the top of the window that houses the
- room jid
- """
- self.xml.banner_name_label.set_text(self.contact.name)
-
- def _on_bookmarks_received(self, _event: events.BookmarksReceived) -> None:
- self.draw_banner_text()
-
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(
@@ -590,10 +194,6 @@ class GroupchatControl(BaseControl):
callback=on_approve)],
modal=False).show()
- def _on_message_received(self, event: events.MessageReceived) -> None:
- if not event.msgtxt:
- return
-
def _on_mam_message_received(self,
event: events.MamMessageReceived
) -> None:
@@ -617,9 +217,6 @@ class GroupchatControl(BaseControl):
displaymarking=event.displaymarking,
additional_data=event.additional_data)
else:
- if event.properties.muc_nickname == self.contact.nickname:
- self.last_sent_txt = event.msgtxt
-
self.add_message(event.msgtxt,
contact=event.properties.muc_nickname,
tim=event.properties.timestamp,
@@ -627,10 +224,6 @@ class GroupchatControl(BaseControl):
message_id=event.properties.id,
stanza_id=event.stanza_id,
additional_data=event.additional_data)
- event.needs_highlight = helpers.message_needs_highlight(
- event.msgtxt,
- self.contact.nickname,
- self._client.get_own_jid().bare)
def add_message(self,
text: str,
@@ -649,12 +242,6 @@ class GroupchatControl(BaseControl):
kind = 'incoming'
# muc-specific chatstate
- if kind == 'incoming':
- highlight = helpers.message_needs_highlight(
- text, self.contact.nickname, self._client.get_own_jid().bare)
-
- self.msg_textview.process_outgoing_message(contact, highlight)
-
BaseControl.add_message(self,
text,
kind,
@@ -693,6 +280,7 @@ class GroupchatControl(BaseControl):
# http://www.xmpp.org/extensions/xep-0045.html#roomconfig-notify
status_codes = properties.muc_status_codes
+ assert status_codes is not None
changes: list[str] = []
if StatusCode.SHOWING_UNAVAILABLE in status_codes:
@@ -730,7 +318,7 @@ class GroupchatControl(BaseControl):
self.add_info_message(change)
@event_filter(['account'])
- def _nec_ping(self, event: events.ApplicationEvent) -> None:
+ def _on_ping_event(self, event: events.PingEventT) -> None:
if not event.contact.is_groupchat:
return
@@ -741,43 +329,27 @@ class GroupchatControl(BaseControl):
if isinstance(event, events.PingSent):
self.add_info_message(_('Ping? (%s)') % nick)
elif isinstance(event, events.PingReply):
- self.add_info_message(
- _('Pong! (%(nick)s %(delay)s s.)') % {'nick': nick,
- 'delay': event.seconds})
- elif isinstance(event, events.PingError):
+ self.add_info_message(_('Pong! (%(nick)s %(delay)s s.)') % {
+ 'nick': nick,
+ 'delay': event.seconds})
+ else:
self.add_info_message(event.error)
def _set_control_active(self) -> None:
- contact = self.contact.get_self()
- if contact.role.is_visitor:
- self.xml.visitor_box.show()
- else:
- self.set_message_input_state(True)
-
+ assert self.roster is not None
self.roster.initial_draw()
self.conversation_view.update_avatars()
- self.update_actions()
-
def _set_control_inactive(self) -> None:
- self.set_message_input_state(False)
- self.xml.visitor_box.hide()
-
+ assert self.roster is not None
self.roster.enable_sort(False)
self.roster.clear()
self._client.get_module('Chatstate').remove_delay_timeout(self.contact)
- self.update_actions()
-
def rejoin(self) -> None:
self._client.get_module('MUC').join(self.room_jid)
- # def send_pm(self, nick, message=None):
- # ctrl = self._start_private_message(nick)
- # if message is not None:
- # ctrl.send_message(message)
-
def _on_user_joined(self,
_contact: GroupchatContact,
_signal_name: str,
@@ -809,25 +381,12 @@ class GroupchatControl(BaseControl):
_('The server has assigned or modified your nickname in this '
'group chat'))
- # Update Actions
- self.update_actions()
-
def _on_room_config_finished(self,
_contact: GroupchatContact,
_signal_name: str
) -> None:
- self._show_page('groupchat')
self.add_info_message(_('A new group chat has been created'))
- def _on_room_config_failed(self,
- _contact: GroupchatContact,
- _signal_name: str,
- error: StanzaError
- ) -> None:
- self.xml.error_heading.set_text(_('Failed to Configure Group Chat'))
- self.xml.error_label.set_text(to_user_string(error))
- self._show_page('error')
-
def _on_user_nickname_changed(self,
_contact: GroupchatContact,
_signal_name: str,
@@ -835,6 +394,8 @@ class GroupchatControl(BaseControl):
properties: MessageProperties
) -> None:
nick = user_contact.name
+
+ assert properties.muc_user is not None
new_nick = properties.muc_user.nick
if properties.is_muc_self_presence:
message = _('You are now known as %s') % new_nick
@@ -865,6 +426,8 @@ class GroupchatControl(BaseControl):
) -> None:
affiliation = helpers.get_uf_affiliation(user_contact.affiliation)
nick = user_contact.name
+
+ assert properties.muc_user is not None
reason = properties.muc_user.reason
reason = '' if reason is None else f': {reason}'
@@ -887,7 +450,6 @@ class GroupchatControl(BaseControl):
reason=reason)
self.add_info_message(message)
- self.update_actions()
def _on_user_role_changed(self,
_contact: GroupchatContact,
@@ -897,6 +459,8 @@ class GroupchatControl(BaseControl):
) -> None:
role = helpers.get_uf_role(user_contact.role)
nick = user_contact.name
+
+ assert properties.muc_user is not None
reason = properties.muc_user.reason
reason = '' if reason is None else f': {reason}'
@@ -905,11 +469,6 @@ class GroupchatControl(BaseControl):
actor = '' if actor is None else _(' by {actor}').format(actor=actor)
if properties.is_muc_self_presence:
- if user_contact.role.is_visitor:
- self.xml.visitor_box.show()
- else:
- self.xml.visitor_box.hide()
- self.set_message_input_state(not user_contact.role.is_visitor)
message = _('** Your Role has been set to '
'{role}{actor}{reason}').format(role=role,
actor=actor,
@@ -922,7 +481,6 @@ class GroupchatControl(BaseControl):
reason=reason)
self.add_info_message(message)
- self.update_actions()
def _on_room_kicked(self,
_contact: GroupchatContact,
@@ -931,6 +489,7 @@ class GroupchatControl(BaseControl):
) -> None:
status_codes = properties.muc_status_codes or []
+ assert properties.muc_user is not None
reason = properties.muc_user.reason
reason = '' if reason is None else f': {reason}'
@@ -989,6 +548,7 @@ class GroupchatControl(BaseControl):
status_codes = properties.muc_status_codes or []
nick = user_contact.name
+ assert properties.muc_user is not None
reason = properties.muc_user.reason
reason = '' if reason is None else f': {reason}'
@@ -1034,39 +594,6 @@ class GroupchatControl(BaseControl):
self.conversation_view.add_muc_user_left(
nick, properties.muc_user.reason)
- def _on_room_joined(self,
- _contact: GroupchatContact,
- _signal_name: str
- ) -> None:
- self._show_page('groupchat')
-
- def _on_room_password_required(self,
- _contact: GroupchatContact,
- _signal_name: str,
- _properties: MessageProperties):
- self._show_page('password')
-
- def _on_room_join_failed(self,
- _contact: GroupchatContact,
- _signal_name: str,
- error: StanzaError
- ) -> None:
- if self._client.get_module('Bookmarks').is_bookmark(self.room_jid):
- self.xml.remove_bookmark_button.show()
-
- self.xml.error_heading.set_text(_('Failed to Join Group Chat'))
- self.xml.error_label.set_text(to_user_string(error))
- self._show_page('error')
-
- def _on_room_creation_failed(self,
- _contact: GroupchatContact,
- _signal_name: str,
- properties: MessageProperties
- ) -> None:
- self.xml.error_heading.set_text(_('Failed to Create Group Chat'))
- self.xml.error_label.set_text(to_user_string(properties.error))
- self._show_page('error')
-
def _on_room_presence_error(self,
_contact: GroupchatContact,
_signal_name: str,
@@ -1094,74 +621,6 @@ class GroupchatControl(BaseControl):
'instead: xmpp:%s?join') % str(alternate)
self.add_info_message(join_message)
- def _on_message_sent(self, event: events.MessageSent) -> None:
- if not event.message:
- return
- # we'll save sent message text when we'll receive it in
- # _nec_gc_message_received
- if event.correct_id is None:
- oob_url = event.additional_data.get_value('gajim', 'oob_url')
- if oob_url == event.message:
- self.last_sent_msg = None
- else:
- self.last_sent_msg = event.message_id
- if self.correcting:
- self.correcting = False
- self.msg_textview.get_style_context().remove_class(
- 'gajim-msg-correcting')
-
- def send_message(self,
- message: str,
- process_commands: bool = True
- ) -> None:
- """
- Call this function to send our message
- """
- if not message:
- return
-
- if self.encryption:
- self.sendmessage = True
- app.plugin_manager.extension_point(
- 'send_message' + self.encryption, self)
- if not self.sendmessage:
- return
-
- if process_commands and self.process_as_command(message):
- return
-
- message = helpers.remove_invalid_xml_chars(message)
-
- if not message:
- return
-
- label = self.get_seclabel()
- if message != '' or message != '\n':
- self.save_message(message, 'sent')
-
- if self.correcting and self.last_sent_msg:
- correct_id = self.last_sent_msg
- else:
- correct_id = None
- chatstate = self._client.get_module('Chatstate')\
- .get_active_chatstate(self.contact)
-
- # Send the message
- message_ = OutgoingMessage(account=self.account,
- contact=self.contact,
- message=message,
- type_='groupchat',
- label=label,
- chatstate=chatstate,
- correct_id=correct_id)
- self._client.send_message(message_)
-
- self.msg_textview.get_buffer().set_text('')
- self.msg_textview.grab_focus()
-
- def _on_message_error(self, event: events.MessageError) -> None:
- self.conversation_view.show_error(event.message_id, event.error)
-
def shutdown(self, reason: Optional[str] = None) -> None:
app.settings.disconnect_signals(self)
self.contact.disconnect(self)
@@ -1174,235 +633,5 @@ class GroupchatControl(BaseControl):
self.roster.destroy()
self.roster = None
- self.remove_actions()
-
super(GroupchatControl, self).shutdown()
app.check_finalize(self)
-
- def _close_control(self) -> None:
- app.window.activate_action(
- 'remove-chat',
- GLib.Variant('as', [self.account, str(self.room_jid)]))
-
- def set_control_active(self, state: bool) -> None:
- self.attention_flag = False
- BaseControl.set_control_active(self, state)
-
- def _on_drag_data_received(self,
- _widget: Gtk.Widget,
- _context: Gdk.DragContext,
- _x_coord: int,
- _y_coord: int,
- selection: Gtk.SelectionData,
- target_type: int,
- _timestamp: int
- ) -> None:
- if not selection.get_data():
- return
-
- log.debug('Drop received: %s, %s', selection.get_data(), target_type)
-
- # TODO: Contact drag and drop for Invitations
- if target_type == TARGET_TYPE_URI_LIST:
- # File drag and drop (handled in chat_control_base)
- self.drag_data_file_transfer(selection)
-
- def delegate_action(self, action: str) -> int:
- res = super().delegate_action(action)
- if res == Gdk.EVENT_STOP:
- return res
-
- if action == 'change-nickname':
- control_action = f'{action}-{self.control_id}'
- app.window.lookup_action(control_action).activate()
- return Gdk.EVENT_STOP
-
- if action == 'escape':
- if self._get_current_page() == 'groupchat':
- return Gdk.EVENT_PROPAGATE
-
- if self._get_current_page() == 'password':
- self._on_password_cancel_clicked()
- elif self._get_current_page() == 'captcha':
- self._on_captcha_cancel_clicked()
- elif self._get_current_page() in ('error', 'captcha-error'):
- self._on_page_close_clicked()
- else:
- self._show_page('groupchat')
- return Gdk.EVENT_STOP
-
- if action == 'change-subject':
- open_window('GroupchatDetails',
- contact=self.contact,
- subject=self._subject_text,
- page='manage')
- return Gdk.EVENT_STOP
-
- if action == 'show-contact-info':
- app.window.lookup_action(
- f'groupchat-details-{self.control_id}').activate()
- return Gdk.EVENT_STOP
-
- return Gdk.EVENT_PROPAGATE
-
- def focus(self) -> None:
- page_name = self._get_current_page()
- if page_name == 'groupchat':
- self.msg_textview.grab_focus()
- elif page_name == 'password':
- self.xml.password_entry.grab_focus_without_selecting()
- elif page_name == 'nickname':
- self.xml.nickname_entry.grab_focus_without_selecting()
- elif page_name == 'captcha':
- self._captcha_request.focus_first_entry()
- elif page_name == 'invite':
- self._invite_box.focus_search_entry()
- elif page_name == 'destroy':
- self.xml.destroy_reason_entry.grab_focus_without_selecting()
-
- def _on_kick_participant_clicked(self, _button: Gtk.Button) -> None:
- reason = self.xml.kick_reason_entry.get_text()
- self._client.get_module('MUC').set_role(
- self.room_jid, self._kick_nick, 'none', reason)
- self._show_page('groupchat')
-
- def _on_ban_participant_clicked(self, _button: Gtk.Button) -> None:
- reason = self.xml.ban_reason_entry.get_text()
- self._client.get_module('MUC').set_affiliation(
- self.room_jid,
- {self._ban_jid: {'affiliation': 'outcast', 'reason': reason}})
- self._show_page('groupchat')
-
- def _on_page_change(self, stack: Gtk.Stack, _param: Any) -> None:
- page_name = stack.get_visible_child_name()
- if page_name == 'groupchat':
- pass
- elif page_name == 'password':
- self.xml.password_entry.set_text('')
- self.xml.password_entry.grab_focus()
- self.xml.password_set_button.grab_default()
- elif page_name == 'captcha':
- self.xml.captcha_set_button.grab_default()
- elif page_name == 'captcha-error':
- self.xml.captcha_try_again_button.grab_default()
- elif page_name == 'error':
- self.xml.close_button.grab_default()
-
- def _on_change_nick(self,
- _action: Gio.SimpleAction,
- _param: Optional[GLib.Variant]
- ) -> None:
- if self._get_current_page() != 'groupchat':
- return
- self.xml.nickname_entry.set_text(self.contact.nickname)
- self.xml.nickname_entry.grab_focus()
- self.xml.nickname_change_button.grab_default()
- self._show_page('nickname')
-
- def _on_nickname_text_changed(self, entry: Gtk.Entry, _param: Any) -> None:
- text = entry.get_text()
- if not text or text == self.contact.nickname:
- self.xml.nickname_change_button.set_sensitive(False)
- else:
- try:
- validate_resourcepart(text)
- except InvalidJid:
- self.xml.nickname_change_button.set_sensitive(False)
- else:
- self.xml.nickname_change_button.set_sensitive(True)
-
- def _on_nickname_change_clicked(self, _button: Gtk.Button) -> None:
- new_nick = self.xml.nickname_entry.get_text()
- self._client.get_module('MUC').change_nick(self.room_jid, new_nick)
- self._show_page('groupchat')
-
- def _on_password_set_clicked(self, _button: Gtk.Button) -> None:
- password = self.xml.password_entry.get_text()
- self._client.get_module('MUC').set_password(self.room_jid, password)
- self._client.get_module('MUC').join(self.room_jid)
- self._show_page('groupchat')
-
- def _on_password_changed(self, entry, _param):
- self.xml.password_set_button.set_sensitive(bool(entry.get_text()))
-
- def _on_password_cancel_clicked(self, *args: Any) -> None:
- self._close_control()
-
- def _on_room_captcha_challenge(self,
- _contact: GroupchatContact,
- _signal_name: str,
- properties: MessageProperties
- ) -> None:
- self._remove_captcha_request()
- form = properties.captcha.form
-
- options = {'no-scrolling': True,
- 'entry-activates-default': True}
- self._captcha_request = DataFormWidget(form, options=options)
- self._captcha_request.connect('is-valid', self._on_captcha_changed)
- self._captcha_request.set_valign(Gtk.Align.START)
- self._captcha_request.show_all()
- self.xml.captcha_box.add(self._captcha_request)
-
- self._show_page('captcha')
- self._captcha_request.focus_first_entry()
-
- def _on_room_captcha_error(self,
- _contact: GroupchatContact,
- _signal_name: str,
- error: StanzaError
- ) -> None:
- error_text = to_user_string(error)
- self.xml.captcha_error_label.set_text(error_text)
- self._show_page('captcha-error')
-
- def _remove_captcha_request(self) -> None:
- if self._captcha_request is None:
- return
- if self._captcha_request in self.xml.captcha_box.get_children():
- self.xml.captcha_box.remove(self._captcha_request)
- self._captcha_request.destroy()
- self._captcha_request = None
-
- def _on_captcha_changed(self,
- _widget: DataFormWidget,
- is_valid: bool
- ) -> None:
- self.xml.captcha_set_button.set_sensitive(is_valid)
-
- def _on_captcha_set_clicked(self, _button):
- form_node = self._captcha_request.get_submit_form()
- self._client.get_module('MUC').send_captcha(self.room_jid, form_node)
- self._remove_captcha_request()
- self._show_page('groupchat')
-
- def _on_captcha_cancel_clicked(self, *args: Any) -> None:
- self._client.get_module('MUC').cancel_captcha(self.room_jid)
- self._remove_captcha_request()
- self._close_control()
-
- def _on_captcha_try_again_clicked(self, *args: Any) -> None:
- self._client.get_module('MUC').join(self.room_jid)
- self._show_page('groupchat')
-
- def _on_remove_bookmark_button_clicked(self, *args: Any) -> None:
- self._client.get_module('Bookmarks').remove(self.room_jid)
- self._close_control()
-
- def _on_retry_join_clicked(self, *args: Any) -> None:
- self._client.get_module('MUC').join(self.room_jid)
- self._show_page('groupchat')
-
- def _on_page_cancel_clicked(self, *args: Any) -> None:
- self._show_page('groupchat')
-
- def _on_page_close_clicked(self, *args: Any) -> None:
- self._close_control()
-
- def _on_groupchat_state_abort_clicked(self, _button: Gtk.Button) -> None:
- self._close_control()
-
- def _on_groupchat_state_join_clicked(self,
- _groupchat_state: GroupchatState
- ) -> None:
- self._client.get_module('MUC').join(self.room_jid)
diff --git a/gajim/gtk/controls/private.py b/gajim/gtk/controls/private.py
index bada80ef1..c91295f62 100644
--- a/gajim/gtk/controls/private.py
+++ b/gajim/gtk/controls/private.py
@@ -24,22 +24,17 @@
# 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 nbxmpp import JID
-from nbxmpp.structs import MessageProperties
from nbxmpp.structs import PresenceProperties
from gajim.common import app
from gajim.common import helpers
from gajim.common.i18n import _
-from gajim.common.modules.contacts import GroupchatContact
from gajim.common.modules.contacts import GroupchatParticipant
from gajim.gui.controls.chat import ChatControl
from gajim.command_system.implementation.hosts import PrivateChatCommands
-from gajim.gui.dialogs import ErrorDialog
from gajim.gui.const import ControlType
@@ -60,22 +55,12 @@ class PrivateChatControl(ChatControl):
def _connect_contact_signals(self) -> None:
self.contact.multi_connect({
- 'user-avatar-update': self._on_user_avatar_update,
- 'user-joined': self._on_user_joined,
- 'user-left': self._on_user_left,
'user-status-show-changed': self._on_user_status_show_changed,
'user-nickname-changed': self._on_user_nickname_changed,
# 'room-kicked': self._on_room_kicked,
# 'room-destroyed': self._on_room_destroyed,
- 'room-joined': self._on_room_joined,
- # 'room-left': self._on_room_left
- 'chatstate-update': self._on_chatstate_update,
})
- @property
- def room_name(self) -> str:
- return self._room_contact.name
-
def get_our_nick(self) -> str:
muc_data = self._client.get_module('MUC').get_muc_data(
self._room_contact.jid)
@@ -88,6 +73,8 @@ class PrivateChatControl(ChatControl):
) -> None:
# TODO
nick = properties.muc_nickname
+
+ assert properties.muc_user is not None
new_nick = properties.muc_user.nick
if properties.is_muc_self_presence:
message = _('You are now known as %s') % new_nick
@@ -97,9 +84,6 @@ class PrivateChatControl(ChatControl):
self.add_info_message(message)
- self.draw_banner()
- self.update_ui()
-
def _on_user_status_show_changed(self,
_user_contact: GroupchatParticipant,
_signal_name: str,
@@ -112,7 +96,6 @@ class PrivateChatControl(ChatControl):
show = helpers.get_uf_show(properties.show.value)
if not self._room_contact.settings.get('print_status'):
- self.update_ui()
return
if properties.is_muc_self_presence:
@@ -124,65 +107,3 @@ class PrivateChatControl(ChatControl):
show=show,
status=status)
self.add_info_message(message)
- self.update_ui()
-
- # def _on_disconnected(self, event):
- # if event.properties.jid != self.contact.jid:
- # return
- # self.set_message_input_state(False)
-
- def _on_user_left(self,
- _user_contact: GroupchatParticipant,
- _signal_name: str,
- _properties: MessageProperties
- ) -> None:
- self.set_message_input_state(False)
-
- def _on_user_joined(self,
- _user_contact: GroupchatParticipant,
- _signal_name: str,
- _properties: MessageProperties
- ) -> None:
- self.set_message_input_state(True)
-
- def _on_room_joined(self,
- _contact: GroupchatContact,
- _signal_name: str
- ) -> None:
- if not self.contact.is_available:
- return
- self.set_message_input_state(True)
-
- def send_message(self,
- message: str,
- process_commands: bool = True,
- attention: bool = False
- ) -> None:
- """
- Call this method to send the message
- """
- message = helpers.remove_invalid_xml_chars(message)
- if not message:
- return
-
- # We need to make sure that we can still send through the room and that
- # the recipient did not go away
- if not self.contact.is_available:
- ErrorDialog(
- _('Sending private message failed'),
- _('You are no longer joined "%(room)s" or '
- '"%(nick)s" has left the chat.') % {
- 'room': self.room_name,
- 'nick': self.contact.name})
- return
-
- ChatControl.send_message(self,
- message,
- process_commands=process_commands,
- attention=attention)
-
- def update_ui(self) -> None:
- ChatControl.update_ui(self)
-
- def _on_user_avatar_update(self, *args: Any) -> None:
- self._update_avatar()
diff --git a/gajim/gtk/conversation/jump_to_end_button.py b/gajim/gtk/conversation/jump_to_end_button.py
index 2a1019af5..6795c3f85 100644
--- a/gajim/gtk/conversation/jump_to_end_button.py
+++ b/gajim/gtk/conversation/jump_to_end_button.py
@@ -77,6 +77,7 @@ class JumpToEndButton(Gtk.Overlay):
self.set_no_show_all(True)
def _on_jump_clicked(self, _button: Gtk.Button) -> None:
+ self.reset_unread_count()
self.emit('clicked')
def toggle(self, visible: bool) -> None:
diff --git a/gajim/gtk/conversation/rows/message.py b/gajim/gtk/conversation/rows/message.py
index 4aa49cf8a..21cc87dd2 100644
--- a/gajim/gtk/conversation/rows/message.py
+++ b/gajim/gtk/conversation/rows/message.py
@@ -25,7 +25,6 @@ from gi.repository import Gdk
from gi.repository import GdkPixbuf
from gi.repository import GLib
from gi.repository import Gtk
-from gi.repository import GObject
from gi.repository import Pango
import cairo
@@ -62,20 +61,6 @@ MERGE_TIMEFRAME = timedelta(seconds=120)
class MessageRow(BaseRow):
-
- __gsignals__ = {
- 'mention': (
- GObject.SignalFlags.RUN_LAST,
- None,
- (str,)
- ),
- 'quote': (
- GObject.SignalFlags.RUN_LAST,
- None,
- (str,)
- ),
- }
-
def __init__(self,
account: str,
contact: ChatContactT,
@@ -273,7 +258,7 @@ class MessageRow(BaseRow):
name: str
) -> int:
if event.type == Gdk.EventType.BUTTON_PRESS and event.button == 1:
- self.emit('mention', name)
+ app.window.activate_action('mention', GLib.Variant('s', name))
return Gdk.EVENT_STOP
@staticmethod
@@ -335,7 +320,11 @@ class MessageRow(BaseRow):
clip.set_text(f'{timestamp_formatted} - {self.name}: {text}', -1)
def on_quote_message(self, _widget: Gtk.Widget) -> None:
- self.emit('quote', self._message_widget.get_text())
+ app.window.activate_action(
+ 'quote', GLib.Variant('s', self._message_widget.get_text()))
+
+ def on_correct_message(self, _widget: Gtk.Widget) -> None:
+ app.window.activate_action('correct-message', None)
def on_retract_message(self, _widget: Gtk.Widget) -> None:
def _on_retract(reason: str) -> None:
diff --git a/gajim/gtk/conversation/rows/widgets.py b/gajim/gtk/conversation/rows/widgets.py
index 2093ddc1f..6e47c4508 100644
--- a/gajim/gtk/conversation/rows/widgets.py
+++ b/gajim/gtk/conversation/rows/widgets.py
@@ -14,8 +14,7 @@
from __future__ import annotations
-from typing import Union
-from typing import Any
+from typing import TYPE_CHECKING
from gi.repository import Gtk
from gi.repository import Pango
@@ -24,13 +23,13 @@ from gi.repository import GLib
from gajim.common import app
from gajim.common.i18n import _
from gajim.common.helpers import is_retraction_allowed
-from gajim.common.modules.contacts import BareContact
from gajim.common.modules.contacts import GroupchatContact
-from gajim.common.modules.contacts import GroupchatParticipant
+from gajim.common.types import ChatContactT
-from ...util import wrap_with_event_box
+if TYPE_CHECKING:
+ from .message import MessageRow
-ContactT = Union[BareContact, GroupchatContact, GroupchatParticipant]
+from ...util import wrap_with_event_box
class SimpleLabel(Gtk.Label):
@@ -45,8 +44,8 @@ class SimpleLabel(Gtk.Label):
@wrap_with_event_box
class MoreMenuButton(Gtk.Button):
def __init__(self,
- row: Any,
- contact: ContactT,
+ row: MessageRow,
+ contact: ChatContactT,
name: str
) -> None:
@@ -70,23 +69,33 @@ class MoreMenuButton(Gtk.Button):
show_retract = False
if isinstance(self._contact, GroupchatContact):
if not self._contact.is_joined:
- self._create_popover(False)
+ self._create_popover(show_retract=False)
return
- disco_info = app.storage.cache.get_last_disco_info(
- self._contact.jid)
- assert disco_info is not None
-
contact = self._contact.get_resource(self._name)
self_contact = self._contact.get_self()
assert self_contact is not None
-
is_allowed = is_retraction_allowed(self_contact, contact)
+
+ disco_info = app.storage.cache.get_last_disco_info(
+ self._contact.jid)
+ assert disco_info is not None
+
if disco_info.has_message_moderation and is_allowed:
show_retract = True
- self._create_popover(show_retract)
- def _create_popover(self, show_retract: bool) -> None:
+ show_correction = False
+ if self._row.message_id is not None:
+ show_correction = app.window.is_message_correctable(
+ self._contact.account, self._contact.jid, self._row.message_id)
+
+ self._create_popover(show_retract=show_retract,
+ show_correction=show_correction)
+
+ def _create_popover(self,
+ show_retract: bool = False,
+ show_correction: bool = False
+ ) -> None:
menu_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
menu_box.get_style_context().add_class('padding-6')
@@ -116,6 +125,16 @@ class MoreMenuButton(Gtk.Button):
'edit-copy-symbolic', Gtk.IconSize.MENU))
menu_box.add(copy_button)
+ if show_correction:
+ correct_button = Gtk.ModelButton()
+ correct_button.set_halign(Gtk.Align.START)
+ correct_button.connect(
+ 'clicked', self._row.on_correct_message)
+ correct_button.set_label(_('Correct'))
+ correct_button.set_image(Gtk.Image.new_from_icon_name(
+ 'document-edit-symbolic', Gtk.IconSize.MENU))
+ menu_box.add(correct_button)
+
if show_retract:
retract_button = Gtk.ModelButton()
retract_button.set_halign(Gtk.Align.START)
diff --git a/gajim/gtk/conversation/view.py b/gajim/gtk/conversation/view.py
index d4a2ab111..5e9167af1 100644
--- a/gajim/gtk/conversation/view.py
+++ b/gajim/gtk/conversation/view.py
@@ -27,7 +27,6 @@ from datetime import datetime
from datetime import timedelta
from gi.repository import GLib
-from gi.repository import GObject
from gi.repository import Gtk
from nbxmpp.errors import StanzaError
@@ -68,25 +67,6 @@ log = logging.getLogger('gajim.gui.conversation_view')
class ConversationView(Gtk.ListBox):
-
- __gsignals__ = {
- 'quote': (
- GObject.SignalFlags.RUN_LAST | GObject.SignalFlags.ACTION,
- None,
- (str, )
- ),
- 'mention': (
- GObject.SignalFlags.RUN_LAST | GObject.SignalFlags.ACTION,
- None,
- (str, )
- ),
- 'scroll-to-end': (
- GObject.SignalFlags.RUN_LAST | GObject.SignalFlags.ACTION,
- None,
- ()
- ),
- }
-
def __init__(self, account: str, contact: ChatContactT) -> None:
Gtk.ListBox.__init__(self)
self.set_selection_mode(Gtk.SelectionMode.NONE)
@@ -273,8 +253,6 @@ class ConversationView(Gtk.ListBox):
error=error,
encryption_enabled=self.encryption_enabled,
log_line_id=log_line_id)
- message_row.connect('mention', self._on_mention)
- message_row.connect('quote', self._on_quote)
if message_id is not None:
self._message_id_row_map[message_id] = message_row
@@ -488,10 +466,6 @@ class ConversationView(Gtk.ListBox):
if isinstance(row, MessageRow):
row.update_avatar()
- def scroll_to_end(self, force: bool = False) -> None:
- if self.autoscroll or force:
- GLib.idle_add(self.emit, 'scroll-to-end')
-
def correct_message(self,
correct_id: str,
text: str,
@@ -518,12 +492,6 @@ class ConversationView(Gtk.ListBox):
message_row.set_error(to_user_string(error))
message_row.set_merged(False)
- def _on_quote(self, _message_row: MessageRow, text: str) -> None:
- self.emit('quote', text)
-
- def _on_mention(self, _message_row: MessageRow, name: str) -> None:
- self.emit('mention', name)
-
def _on_contact_setting_changed(self,
value: Any,
setting: str,
diff --git a/gajim/gtk/groupchat_details.py b/gajim/gtk/groupchat_details.py
index f663f83c1..0bdeeccf3 100644
--- a/gajim/gtk/groupchat_details.py
+++ b/gajim/gtk/groupchat_details.py
@@ -41,7 +41,6 @@ from .structs import RemoveHistoryActionParams
class GroupchatDetails(Gtk.ApplicationWindow):
def __init__(self,
contact: GroupchatContact,
- subject: str,
page: Optional[str] = None
) -> None:
Gtk.ApplicationWindow.__init__(self)
@@ -55,7 +54,6 @@ class GroupchatDetails(Gtk.ApplicationWindow):
self.account = contact.account
self._client = app.get_client(contact.account)
- self._subject_text = subject
self._contact = contact
self._contact.connect('avatar-update', self._on_avatar_update)
@@ -136,8 +134,7 @@ class GroupchatDetails(Gtk.ApplicationWindow):
def _add_groupchat_manage(self) -> None:
self._groupchat_manage = GroupchatManage(self.account,
- self._contact,
- self._subject_text)
+ self._contact)
self._ui.manage_box.add(self._groupchat_manage)
def _add_groupchat_info(self) -> None:
diff --git a/gajim/gtk/groupchat_manage.py b/gajim/gtk/groupchat_manage.py
index dd6da9d43..480278617 100644
--- a/gajim/gtk/groupchat_manage.py
+++ b/gajim/gtk/groupchat_manage.py
@@ -43,7 +43,6 @@ class GroupchatManage(Gtk.Box):
def __init__(self,
account: str,
contact: GroupchatContact,
- subject: str
) -> None:
Gtk.Box.__init__(self)
self._account = account
@@ -51,8 +50,6 @@ class GroupchatManage(Gtk.Box):
self._contact = contact
self._contact.connect('room-subject', self._on_room_subject)
- self._subject_text = subject
-
self._room_config_form = None
self._ui = get_builder('groupchat_manage.ui')
@@ -81,7 +78,11 @@ class GroupchatManage(Gtk.Box):
return app.storage.cache.get_last_disco_info(self._contact.jid)
def _prepare_subject(self) -> None:
- self._ui.subject_textview.get_buffer().set_text(self._subject_text)
+ text = ''
+ if self._contact.subject is not None:
+ text = self._contact.subject.text
+
+ self._ui.subject_textview.get_buffer().set_text(text)
joined = self._contact.is_joined
change_allowed = self._is_subject_change_allowed()
@@ -103,8 +104,11 @@ class GroupchatManage(Gtk.Box):
text = buffer_.get_text(buffer_.get_start_iter(),
buffer_.get_end_iter(),
False)
+
+ assert self._contact.subject is not None
+
self._ui.subject_change_button.set_sensitive(
- text != self._subject_text)
+ text != self._contact.subject.text)
def _on_subject_change_clicked(self, _button: Gtk.Button) -> None:
buffer_ = self._ui.subject_textview.get_buffer()
@@ -121,7 +125,6 @@ class GroupchatManage(Gtk.Box):
if subject is None:
return
- self._subject_text = subject.text
self._ui.subject_textview.get_buffer().set_text(subject.text)
def _prepare_manage(self) -> None:
diff --git a/gajim/gtk/groupchat_nick_completion.py b/gajim/gtk/groupchat_nick_completion.py
index fdb309539..32f74a1ee 100644
--- a/gajim/gtk/groupchat_nick_completion.py
+++ b/gajim/gtk/groupchat_nick_completion.py
@@ -14,6 +14,7 @@
from __future__ import annotations
+from typing import Optional
from typing import TYPE_CHECKING
import logging
@@ -23,8 +24,12 @@ from nbxmpp.structs import PresenceProperties
from gi.repository import Gdk
from gajim.common import app
+from gajim.common import ged
from gajim.common import types
+from gajim.common.events import GcMessageReceived
+from gajim.common.ged import EventHelper
from gajim.common.helpers import jid_is_blocked
+from gajim.common.helpers import message_needs_highlight
if TYPE_CHECKING:
from .message_input import MessageInputTextView
@@ -32,23 +37,44 @@ if TYPE_CHECKING:
log = logging.getLogger('gajim.gui.groupchat_nick_completion')
-class GroupChatNickCompletion:
- def __init__(self,
- account: str,
- contact: types.GroupchatContactT,
- message_input: MessageInputTextView
- ) -> None:
- self._account = account
+class GroupChatNickCompletion(EventHelper):
+ def __init__(self) -> None:
+ EventHelper.__init__(self)
- self._contact = contact
- self._contact.connect(
- 'user-nickname-changed', self._on_user_nickname_changed)
+ self._account: Optional[str] = None
+ self._contact: Optional[types.GroupchatContactT] = None
self._sender_list: list[str] = []
- self._attention_list: list[str] = []
+ self._highlight_list: list[str] = []
self._nick_hits: list[str] = []
self._last_key_tab = False
+ self._nick_data: dict[str, tuple[list[str], list[str]]] = {}
+
+ self.register_event(
+ 'gc-message-received', ged.GUI1, self._on_gc_message_received)
+
+ def switch_contact(self, contact: types.GroupchatContactT) -> None:
+ self._nick_hits.clear()
+ self._last_key_tab = False
+
+ if self._contact is not None:
+ self._contact.disconnect_all_from_obj(self)
+ self._nick_data[str(self._contact.jid)] = (
+ self._sender_list, self._highlight_list)
+
+ nick_data = self._nick_data.get(str(contact.jid))
+ if nick_data is None:
+ self._sender_list.clear()
+ self._highlight_list.clear()
+ else:
+ self._sender_list, self._highlight_list = nick_data
+
+ self._account = contact.account
+ self._contact = contact
+ self._contact.connect(
+ 'user-nickname-changed', self._on_user_nickname_changed)
+
def _on_user_nickname_changed(self,
_contact: types.GroupchatContact,
_signal_name: str,
@@ -63,27 +89,46 @@ class GroupChatNickCompletion:
new_name = properties.muc_user.nick
assert new_name is not None
log.debug('Contact %s renamed to %s', old_name, new_name)
- for lst in (self._attention_list, self._sender_list):
+ for lst in (self._highlight_list, self._sender_list):
for idx, contact in enumerate(lst):
if contact == old_name:
lst[idx] = new_name
- def record_message(self, contact_name: str, highlight: bool) -> None:
- if contact_name == self._contact.nickname:
+ def _on_gc_message_received(self, event: GcMessageReceived) -> None:
+ if event.properties.muc_nickname is None:
+ # Message from server
return
- log.debug('Recorded a message from %s, highlight; %s',
- contact_name,
- highlight)
+ client = app.get_client(event.account)
+ gc_contact = client.get_module('Contacts').get_contact(
+ event.room_jid)
+
+ participant_nick = event.properties.muc_nickname
+ if participant_nick == gc_contact.nickname:
+ return
+
+ highlight = message_needs_highlight(
+ event.msgtxt, gc_contact.nickname, client.get_own_jid().bare)
+ self._process_message(participant_nick, highlight, event.room_jid)
+
+ def _process_message(self,
+ participant_nick: str,
+ highlight: bool,
+ room_jid: str
+ ) -> None:
+ nick_data = self._nick_data.get(room_jid)
+ if nick_data is None:
+ return
+ sender_list, highlight_list = nick_data
if highlight:
try:
- self._attention_list.remove(contact_name)
+ highlight_list.remove(participant_nick)
except ValueError:
pass
- if len(self._attention_list) > 6:
- self._attention_list.pop(0) # remove older
- self._attention_list.append(contact_name)
+ if len(highlight_list) > 6:
+ highlight_list.pop(0) # remove older
+ highlight_list.append(participant_nick)
# TODO implement it in a more efficient way
# Currently it's O(n*m + n*s), where n is the number of participants and
@@ -95,10 +140,10 @@ class GroupChatNickCompletion:
# for each suggestion (currently generating the suggestions is O(n))
# this would give the expected complexity of O(m + s * n log n)
try:
- self._sender_list.remove(contact_name)
+ sender_list.remove(participant_nick)
except ValueError:
pass
- self._sender_list.append(contact_name)
+ sender_list.append(participant_nick)
def _generate_suggestions(self,
nicks: list[str],
@@ -111,12 +156,13 @@ class GroupChatNickCompletion:
`beginning` is the text already typed by the user
'''
def _nick_matching(nick: str) -> bool:
+ assert self._contact
return (nick != self._contact.nickname and
nick.lower().startswith(beginning.lower()))
if beginning == '':
# empty message, so just suggest recent mentions
- potential_matches = self._attention_list
+ potential_matches = self._highlight_list
else:
# nick partially typed, try completing it
potential_matches = self._sender_list
@@ -179,6 +225,7 @@ class GroupChatNickCompletion:
self._nick_hits.append(self._nick_hits[0])
begin = self._nick_hits.pop(0)
else:
+ assert self._contact
list_nick = self._contact.get_user_nicknames()
list_nick = list(filter(self._jid_not_blocked, list_nick))
@@ -215,6 +262,7 @@ class GroupChatNickCompletion:
else:
start_iter.backward_chars(len(begin))
+ assert self._account
client = app.get_client(self._account)
client.get_module('Chatstate').block_chatstates(
self._contact, True)
@@ -255,6 +303,8 @@ class GroupChatNickCompletion:
return True
def _jid_not_blocked(self, resource: str) -> bool:
+ assert self._account
+ assert self._contact
resource_contact = self._contact.get_resource(resource)
return not jid_is_blocked(
self._account, str(resource_contact.jid))
diff --git a/gajim/gtk/groupchat_roster.py b/gajim/gtk/groupchat_roster.py
index 171db7f71..f0168f78d 100644
--- a/gajim/gtk/groupchat_roster.py
+++ b/gajim/gtk/groupchat_roster.py
@@ -378,7 +378,6 @@ class GroupchatRoster(Gtk.ScrolledWindow, EventHelper):
assert self_contact is not None
contact = self._group_chat_contact.get_resource(nick)
menu = get_groupchat_roster_menu(self._account,
- self._control_id,
self_contact,
contact)
diff --git a/gajim/gtk/main.py b/gajim/gtk/main.py
index 40038c90b..131d90763 100644
--- a/gajim/gtk/main.py
+++ b/gajim/gtk/main.py
@@ -38,6 +38,7 @@ from gajim.common.const import SimpleClientState
from gajim.common.ged import EventHelper
from gajim.common.i18n import _
from gajim.common.modules.bytestream import is_transfer_active
+from gajim.gtk.const import MAIN_WIN_ACTIONS
from gajim.plugins.pluginmanager import PluginManifest
from gajim.plugins.repository import PluginRepository
@@ -76,6 +77,10 @@ class MainWindow(Gtk.ApplicationWindow, EventHelper):
app.window = self
+ self._add_actions()
+ self._add_actions2()
+ self._add_stateful_actions()
+
self._startup_finished: bool = False
self._ui = get_builder('main.ui')
@@ -142,8 +147,6 @@ class MainWindow(Gtk.ApplicationWindow, EventHelper):
self._check_for_account()
self._load_chats()
self._load_unread_counts()
- self._add_actions()
- self._add_actions2()
self._prepare_window()
@@ -155,6 +158,11 @@ class MainWindow(Gtk.ApplicationWindow, EventHelper):
client.connect_signal('state-changed',
self._on_client_state_changed)
+ def get_action(self, name: str) -> Gio.SimpleAction:
+ action = self.lookup_action(name)
+ assert action is not None
+ return action
+
def is_minimized(self) -> bool:
if app.is_display(Display.WAYLAND):
# There is no way to discover if a window is minimized on wayland
@@ -319,16 +327,44 @@ class MainWindow(Gtk.ApplicationWindow, EventHelper):
act.connect('activate', func)
self.add_action(act)
+ def _add_stateful_actions(self) -> None:
+ action = Gio.SimpleAction.new_stateful(
+ 'show-offline',
+ None,
+ GLib.Variant('b', app.settings.get('showoffline')))
+
+ action.connect('change-state', self._on_show_offline)
+
+ self.add_action(action)
+
+ action = Gio.SimpleAction.new_stateful(
+ 'sort-by-show',
+ None,
+ GLib.Variant('b', app.settings.get('sort_by_show_in_roster')))
+
+ action.connect('change-state', self._on_sort_by_show)
+
+ self.add_action(action)
+
+ action = Gio.SimpleAction.new_stateful(
+ 'set-encryption',
+ GLib.VariantType('s'),
+ GLib.Variant('s', 'disabled'))
+
+ self.add_action(action)
+
def _add_actions2(self) -> None:
+ for action, variant_type, enabled in MAIN_WIN_ACTIONS:
+ if variant_type is not None:
+ variant_type = GLib.VariantType(variant_type)
+ act = Gio.SimpleAction.new(action, variant_type)
+ act.set_enabled(enabled)
+ self.add_action(act)
+
actions = [
'change-nickname',
'change-subject',
'escape',
- 'send-file',
- 'show-contact-info',
- 'show-emoji-chooser',
- 'clear-chat',
- 'delete-line',
'close-tab',
'switch-next-tab',
'switch-prev-tab',
@@ -347,7 +383,6 @@ class MainWindow(Gtk.ApplicationWindow, EventHelper):
]
disabled_for_emacs = (
- 'send-file',
'close-tab'
)
@@ -370,12 +405,21 @@ class MainWindow(Gtk.ApplicationWindow, EventHelper):
if action_name == 'escape' and self._chat_page.hide_search():
return None
+ chat_stack = self._chat_page.get_chat_stack()
+ if action_name == 'escape' and chat_stack.process_escape():
+ return None
+
control = self.get_active_control()
if control is not None:
+ if action_name == 'change-nickname':
+ app.window.activate_action('muc-change-nickname', None)
+ return None
- res = control.delegate_action(action_name)
- if res != Gdk.EVENT_PROPAGATE:
- return res
+ if action_name == 'change-subject':
+ open_window('GroupchatDetails',
+ contact=control.contact,
+ page='manage')
+ return None
if action_name == 'escape':
if app.settings.get('escape_key_closes'):
@@ -409,6 +453,20 @@ class MainWindow(Gtk.ApplicationWindow, EventHelper):
return None
+ def _on_show_offline(self,
+ action: Gio.SimpleAction,
+ value: GLib.Variant) -> None:
+
+ action.set_state(value)
+ app.settings.set('showoffline', value.get_boolean())
+
+ def _on_sort_by_show(self,
+ action: Gio.SimpleAction,
+ value: GLib.Variant) -> None:
+
+ action.set_state(value)
+ app.settings.set('sort_by_show_in_roster', value.get_boolean())
+
def _toggle_chat_list(self) -> None:
chat_list_stack = self._chat_page.get_chat_list_stack()
chat_list = chat_list_stack.get_current_chat_list()
@@ -435,8 +493,10 @@ class MainWindow(Gtk.ApplicationWindow, EventHelper):
if self.get_property('has-toplevel-focus'):
client = app.get_client(control.account)
+ chat_stack = self._chat_page.get_chat_stack()
+ msg_action_box = chat_stack.get_message_action_box()
client.get_module('Chatstate').set_mouse_activity(
- control.contact, control.msg_textview.has_text())
+ control.contact, msg_action_box.msg_textview.has_text)
def _on_window_delete(self,
_widget: Gtk.ApplicationWindow,
@@ -715,6 +775,20 @@ class MainWindow(Gtk.ApplicationWindow, EventHelper):
def chat_exists(self, account: str, jid: JID) -> bool:
return self._chat_page.chat_exists(account, jid)
+ def is_message_correctable(self,
+ account: str,
+ jid: JID,
+ message_id: str
+ ) -> bool:
+ chat_stack = self._chat_page.get_chat_stack()
+ last_message_id = chat_stack.get_last_message_id(account, jid)
+ if last_message_id is None or last_message_id != message_id:
+ return False
+
+ message_row = app.storage.archive.get_last_correctable_message(
+ account, jid, last_message_id)
+ return message_row is not None
+
def get_total_unread_count(self) -> int:
chat_list_stack = self._chat_page.get_chat_list_stack()
return chat_list_stack.get_total_unread_count()
diff --git a/gajim/gtk/menus.py b/gajim/gtk/menus.py
index 86e5278a8..0f3d045a0 100644
--- a/gajim/gtk/menus.py
+++ b/gajim/gtk/menus.py
@@ -41,6 +41,7 @@ from gajim.common.const import URIType
from gajim.common.const import URIAction
from gajim.common.structs import URI
from gajim.common.structs import VariantMixin
+from gajim.common.modules.contacts import GroupchatContact
from gajim.common.modules.contacts import can_add_to_roster
from gajim.gui.structs import AddChatActionParams
@@ -48,22 +49,20 @@ from gajim.gui.structs import AccountJidParam
from gajim.gui.structs import ChatListEntryParam
from gajim.gui.structs import RemoveHistoryActionParams
from gajim.gui.util import GajimMenu
-from gajim.gui.const import ControlType
MenuValueT = Union[None, str, GLib.Variant, VariantMixin]
MenuItemListT = list[tuple[str, str, MenuValueT]]
-def get_self_contact_menu(control_id: str,
- contact: types.BareContact) -> GajimMenu:
+def get_self_contact_menu(contact: types.BareContact) -> GajimMenu:
account = contact.account
jid = contact.jid
menu = GajimMenu()
menu.add_item(_('Profile'), f'app.{account}-profile', f'"{account}"')
- submenu = get_send_file_submenu(control_id)
+ submenu = get_send_file_submenu()
menu.append_submenu(_('Send File'), submenu)
params = RemoveHistoryActionParams(account=account, jid=jid)
@@ -72,60 +71,55 @@ def get_self_contact_menu(control_id: str,
return menu
-def get_singlechat_menu(control_id: str,
- contact: Union[types.BareContact,
- types.GroupchatParticipant]
- ) -> GajimMenu:
+def get_singlechat_menu(contact: types.BareContact) -> GajimMenu:
+ account = contact.account
menu = GajimMenu()
- menu.add_item(_('Details'), f'win.information-{control_id}')
- menu.add_item(_('Block Contact…'), f'win.block-contact-{control_id}')
+ menu.add_item(_('Details'), 'win.show-contact-info')
+ menu.add_item(_('Block Contact…'), f'app.{account}-block-contact')
- submenu = get_send_file_submenu(control_id)
+ submenu = get_send_file_submenu()
menu.append_submenu(_('Send File'), submenu)
- menu.add_item(_('Start Voice Call…'), f'win.start-voice-call-{control_id}')
- menu.add_item(_('Start Video Call…'), f'win.start-video-call-{control_id}')
- menu.add_item(_('Search…'), 'win.search-history')
+ menu.add_item(_('Start Voice Call…'), 'win.start-voice-call')
+ menu.add_item(_('Start Video Call…'), 'win.start-video-call')
+
+ menu.add_item(_('Search…'), 'win.search-history', None)
if can_add_to_roster(contact):
- menu.append(_('Add to Contact List…'),
- f'win.add-to-roster-{control_id}')
+ params = AccountJidParam(account=account, jid=contact.jid)
+ menu.add_item(_('Add to Contact List…'), 'win.add-to-roster', params)
return menu
-def get_private_chat_menu(control_id: str,
- contact: types.GroupchatParticipant
- ) -> GajimMenu:
-
+def get_private_chat_menu(contact: types.GroupchatParticipant) -> GajimMenu:
menu = GajimMenu()
- menu.add_item(_('Details'), f'win.information-{control_id}')
- menu.add_item(_('Upload File…'), f'win.send-file-httpupload-{control_id}')
+ menu.add_item(_('Details'), 'win.show-contact-info')
+ menu.add_item(_('Upload File…'), 'win.send-file-httpupload')
menu.add_item(_('Search…'), 'win.search-history')
if can_add_to_roster(contact):
- menu.append(_('Add to Contact List…'),
- f'win.add-to-roster-{control_id}')
+ params = AccountJidParam(account=contact.account, jid=contact.jid)
+ menu.add_item(_('Add to Contact List…'), 'win.add-to-roster', params)
return menu
-def get_send_file_submenu(control_id: str) -> GajimMenu:
+def get_send_file_submenu() -> GajimMenu:
menu = GajimMenu()
- menu.add_item(_('Upload File…'), f'win.send-file-httpupload-{control_id}')
- menu.add_item(_('Send File Directly…'),
- f'win.send-file-jingle-{control_id}')
+ menu.add_item(_('Upload File…'), 'win.send-file-httpupload')
+ menu.add_item(_('Send File Directly…'), 'win.send-file-jingle')
return menu
-def get_groupchat_menu(control_id: str, account: str, jid: JID) -> GajimMenu:
+def get_groupchat_menu(contact: GroupchatContact) -> GajimMenu:
menuitems: MenuItemListT = [
- (_('Details'), f'win.groupchat-details-{control_id}', None),
- (_('Change Nickname…'), f'win.change-nickname-{control_id}', None),
- (_('Request Voice'), f'win.request-voice-{control_id}', None),
- (_('Execute Command…'), f'win.execute-command-{control_id}', '""'),
+ (_('Details'), 'win.show-contact-info', None),
+ (_('Change Nickname…'), 'win.muc-change-nickname', None),
+ (_('Request Voice'), 'win.muc-request-voice', None),
+ (_('Execute Command…'), 'win.muc-execute-command', '""'),
(_('Search…'), 'win.search-history', None)
]
@@ -199,24 +193,16 @@ def build_accounts_menu() -> None:
menubar.insert_submenu(menu_position, _('Accounts'), acc_menu)
-def get_encryption_menu(control_id: str,
- control_type: ControlType,
- ) -> Optional[GajimMenu]:
- menu = GajimMenu()
- action = f'win.set-encryption-{control_id}'
- menu.add_item(_('Disabled'), action, '"disabled"')
- for name, plugin in app.plugin_manager.encryption_plugins.items():
- if control_type.is_groupchat:
- if not hasattr(plugin, 'allow_groupchat'):
- continue
- if control_type.is_privatechat:
- if not hasattr(plugin, 'allow_privatechat'):
- continue
- menu.add_item(name, action, f'"{name}"')
-
- if menu.get_n_items() == 1:
- return None
- return menu
+def get_encryption_menu() -> GajimMenu:
+
+ menuitems: MenuItemListT = [
+ (_('Disabled'), 'win.set-encryption', '""'),
+ ('OMEMO', 'win.set-encryption', '"OMEMO"'),
+ ('OpenPGP', 'win.set-encryption', '"OpenPGP"'),
+ ('PGP', 'win.set-encryption', '"PGP"'),
+ ]
+
+ return GajimMenu.from_list(menuitems)
def get_conv_action_context_menu(account: str,
@@ -373,15 +359,15 @@ def get_roster_menu(account: str, jid: str, gateway: bool = False) -> GajimMenu:
value = f'"{jid}"'
menuitems: MenuItemListT = [
- (_('Details'), f'win.contact-info-{account}', value),
- (_('Execute Command…'), f'win.execute-command-{account}', value),
- (block_label, f'win.block-contact-{account}', value),
- (_('Remove…'), f'win.remove-contact-{account}', value),
+ (_('Details'), f'app.{account}-contact-info', value),
+ (_('Execute Command…'), f'app.{account}-execute-command', value),
+ (block_label, f'app.{account}-block-contact', value),
+ (_('Remove…'), f'app.{account}-remove-contact', value),
]
if gateway:
menuitems.insert(
- 1, (_('Modify Gateway…'), f'win.modify-gateway-{account}', value))
+ 1, (_('Modify Gateway…'), f'app.{account}-modify-gateway', value))
return GajimMenu.from_list(menuitems)
@@ -466,13 +452,12 @@ def get_workspace_params(current_workspace_id: str,
yield name, params
-def get_groupchat_admin_menu(control_id: str,
- self_contact: types.GroupchatParticipant,
+def get_groupchat_admin_menu(self_contact: types.GroupchatParticipant,
contact: types.GroupchatParticipant) -> GajimMenu:
menu = GajimMenu()
- action = f'win.change-affiliation-{control_id}'
+ action = 'win.muc-change-affiliation'
if is_affiliation_change_allowed(self_contact, contact, 'owner'):
value = f'["{contact.real_jid}", "owner"]'
@@ -492,7 +477,7 @@ def get_groupchat_admin_menu(control_id: str,
if is_affiliation_change_allowed(self_contact, contact, 'outcast'):
value = f'"{contact.real_jid}"'
- menu.add_item(_('Ban…'), f'win.ban-{control_id}', value)
+ menu.add_item(_('Ban…'), 'win.muc-ban', value)
if not menu.get_n_items():
menu.add_item(_('Not Available'), 'dummy', None)
@@ -500,8 +485,7 @@ def get_groupchat_admin_menu(control_id: str,
return menu
-def get_groupchat_mod_menu(control_id: str,
- self_contact: types.GroupchatParticipant,
+def get_groupchat_mod_menu(self_contact: types.GroupchatParticipant,
contact: types.GroupchatParticipant
) -> GajimMenu:
@@ -509,9 +493,9 @@ def get_groupchat_mod_menu(control_id: str,
if is_role_change_allowed(self_contact, contact):
value = f'"{contact.name}"'
- menu.add_item(_('Kick…'), f'win.kick-{control_id}', value)
+ menu.add_item(_('Kick…'), 'win.muc-kick', value)
- action = f'win.change-role-{control_id}'
+ action = 'win.muc-change-role'
if is_role_change_allowed(self_contact, contact):
if contact.role.is_visitor:
@@ -528,7 +512,6 @@ def get_groupchat_mod_menu(control_id: str,
def get_groupchat_roster_menu(account: str,
- control_id: str,
self_contact: types.GroupchatParticipant,
contact: types.GroupchatParticipant
) -> GajimMenu:
@@ -536,8 +519,8 @@ def get_groupchat_roster_menu(account: str,
value = f'"{contact.name}"'
general_items: MenuItemListT = [
- (_('Details'), f'win.contact-information-{control_id}', value),
- (_('Execute Command…'), f'win.execute-command-{control_id}', value),
+ (_('Details'), 'win.muc-contact-info', value),
+ (_('Execute Command…'), 'win.muc-execute-command', value),
]
real_contact = contact.get_real_contact()
@@ -546,8 +529,8 @@ def get_groupchat_roster_menu(account: str,
action = f'app.{account}-add-contact'
general_items.insert(1, (_('Add to Contact List…'), action, value))
- mod_menu = get_groupchat_mod_menu(control_id, self_contact, contact)
- admin_menu = get_groupchat_admin_menu(control_id, self_contact, contact)
+ mod_menu = get_groupchat_mod_menu(self_contact, contact)
+ admin_menu = get_groupchat_admin_menu(self_contact, contact)
menu = GajimMenu.from_list(general_items)
menu.append_section(_('Moderation'), mod_menu)
@@ -577,6 +560,17 @@ def get_component_search_menu(jid: Optional[str], copy_text: str) -> Gio.Menu:
return menu
+def get_format_menu() -> GajimMenu:
+
+ menuitems: MenuItemListT = [
+ (_('bold'), 'win.input-bold', None),
+ (_('italic'), 'win.input-italic', None),
+ (_('strike'), 'win.input-strike', None),
+ ]
+
+ return GajimMenu.from_list(menuitems)
+
+
def escape_mnemonic(label: Optional[str]) -> Optional[str]:
if label is None:
return None
diff --git a/gajim/gtk/message_actions_box.py b/gajim/gtk/message_actions_box.py
new file mode 100644
index 000000000..eba274aa7
--- /dev/null
+++ b/gajim/gtk/message_actions_box.py
@@ -0,0 +1,698 @@
+# 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 Any
+from typing import Optional
+
+import os
+import sys
+import tempfile
+import logging
+import uuid
+
+from gi.repository import GLib
+from gi.repository import Gdk
+from gi.repository import GdkPixbuf
+from gi.repository import Gio
+from gi.repository import Gtk
+from gi.repository import GObject
+
+from nbxmpp.const import Chatstate
+from nbxmpp.modules.security_labels import SecurityLabel
+from nbxmpp.protocol import JID
+
+from gajim.common import app
+from gajim.common import events
+from gajim.common import i18n
+from gajim.common.i18n import _
+from gajim.common.client import Client
+from gajim.common.const import SimpleClientState
+from gajim.common.const import Direction
+from gajim.common.modules.contacts import GroupchatContact
+from gajim.common.modules.contacts import GroupchatParticipant
+from gajim.common.types import ChatContactT
+from gajim.gui.security_label_selector import SecurityLabelSelector
+
+from .builder import get_builder
+from .dialogs import DialogButton
+from .dialogs import ErrorDialog
+from .dialogs import PastePreviewDialog
+from .emoji_chooser import emoji_chooser
+from .menus import get_encryption_menu
+from .menus import get_format_menu
+from .menus import get_groupchat_menu
+from .menus import get_private_chat_menu
+from .menus import get_self_contact_menu
+from .menus import get_singlechat_menu
+from .message_input import MessageInputTextView
+
+if app.is_installed('GSPELL'):
+ from gi.repository import Gspell # pylint: disable=ungrouped-imports
+
+log = logging.getLogger('gajim.gui.messageactionsbox')
+
+
+class MessageActionsBox(Gtk.Grid):
+ def __init__(self) -> None:
+ Gtk.Grid.__init__(self)
+
+ self._client: Optional[Client] = None
+ self._contact: Optional[ChatContactT] = None
+
+ self._ui = get_builder('message_actions_box.ui')
+ self.get_style_context().add_class('message-action-box')
+
+ self.attach(self._ui.box, 0, 0, 1, 1)
+
+ # For undo
+ self.space_pressed = False
+
+ # XEP-0308 Message Correction
+ self.is_correcting = False
+ self.last_message_id: dict[tuple[str, JID], Optional[str]] = {}
+
+ self._ui.send_message_button.set_visible(
+ app.settings.get('show_send_message_button'))
+ app.settings.bind_signal('show_send_message_button',
+ self._ui.send_message_button,
+ 'set_visible')
+
+ self._security_label_selector = SecurityLabelSelector()
+ self._ui.box.pack_start(self._security_label_selector, False, True, 0)
+
+ self.msg_textview = MessageInputTextView()
+ self.msg_textview.get_buffer().connect('changed',
+ self._on_buffer_changed)
+ self.msg_textview.connect('key-press-event',
+ self._on_msg_textview_key_press_event)
+ self.msg_textview.connect('paste-clipboard',
+ self._on_paste_clipboard)
+
+ self._ui.input_scrolled.add(self.msg_textview)
+
+ self._ui.sendfile_button.set_tooltip_text(
+ _('No File Transfer available'))
+ self._ui.formattings_button.set_menu_model(get_format_menu())
+ self._ui.encryption_menu_button.set_menu_model(get_encryption_menu())
+
+ self._ui.quick_invite_button.set_action_name('win.muc-invite')
+
+ self._init_emoticon_popover()
+ # TODO init spellchecker uses a contact on callback, contact might
+ # be None
+ self._language_handler_id = self._init_spell_checker()
+
+ self.show_all()
+ self._ui.connect_signals(self)
+
+ self._connect_actions()
+
+ def _get_encryption_state(self) -> tuple[bool, str]:
+ assert self._contact is not None
+ encryption = self._contact.settings.get('encryption')
+ return bool(encryption), encryption
+
+ def get_current_contact(self) -> ChatContactT:
+ assert self._contact is not None
+ return self._contact
+
+ def get_seclabel(self) -> Optional[SecurityLabel]:
+ return self._security_label_selector.get_seclabel()
+
+ def _connect_actions(self) -> None:
+ actions = [
+ 'input-bold',
+ 'input-italic',
+ 'input-strike',
+ 'input-clear',
+ 'show-emoji-chooser',
+ 'quote',
+ 'mention',
+ 'correct-message',
+ ]
+
+ for action in actions:
+ action = app.window.get_action(action)
+ action.connect('activate', self._on_action)
+
+ action = app.window.get_action('set-encryption')
+ action.connect('change-state', self._change_encryption)
+
+ action = app.window.get_action('send-file-jingle')
+ action.connect('notify::enabled', self._on_send_file_enabled_changed)
+
+ action = app.window.get_action('send-file-httpupload')
+ action.connect('notify::enabled', self._on_send_file_enabled_changed)
+
+ def _on_action(self,
+ action: Gio.SimpleAction,
+ param: Optional[GLib.Variant]) -> Optional[int]:
+
+ if self._contact is None:
+ return
+
+ action_name = action.get_name()
+ log.info('Activate action: %s', action_name)
+
+ if action_name == 'input-clear':
+ self._on_clear()
+
+ elif action_name.startswith('input-'):
+ self._on_format(action_name)
+
+ elif action_name == 'show-emoji-chooser':
+ self._on_show_emoji_chooser()
+
+ elif action_name == 'quote':
+ assert param
+ self.msg_textview.insert_as_quote(param.get_string())
+
+ elif action_name == 'mention':
+ assert param
+ self.msg_textview.mention_participant(param.get_string())
+
+ elif action_name == 'correct-message':
+ self.toggle_message_correction()
+
+ def switch_contact(self, contact: ChatContactT) -> None:
+ if self._client is not None:
+ self._set_chatstate(False)
+ self._client.disconnect_all_from_obj(self)
+
+ if self._contact is not None:
+ self._contact.disconnect_all_from_obj(self)
+
+ self._client = app.get_client(contact.account)
+ self._client.connect_signal(
+ 'state-changed', self._on_client_state_changed)
+
+ self._contact = contact
+
+ if isinstance(self._contact, GroupchatContact):
+ self._ui.quick_invite_button.show()
+ self._contact.multi_connect({
+ 'state-changed': self._on_muc_state_changed,
+ 'user-role-changed': self._on_muc_state_changed,
+ })
+ elif isinstance(self._contact, GroupchatParticipant):
+ self._ui.quick_invite_button.hide()
+ self._contact.multi_connect({
+ 'user-joined': self._on_user_state_changed,
+ 'user-left': self._on_user_state_changed,
+ 'room-joined': self._on_user_state_changed,
+ 'room-left': self._on_user_state_changed,
+ })
+ else:
+ self._ui.quick_invite_button.hide()
+
+ self._set_settings_menu(contact)
+
+ encryption = self._contact.settings.get('encryption')
+ self._set_encryption_state(encryption)
+ self._set_encryption_details(encryption)
+
+ self._set_spell_checker_language(contact)
+ self._set_chatstate(True)
+
+ self.msg_textview.switch_contact(contact)
+ self._security_label_selector.switch_contact(contact)
+
+ self._update_message_input_state()
+
+ def clear(self) -> None:
+ if self._client is not None:
+ self._set_chatstate(False)
+ self._client.disconnect_all_from_obj(self)
+
+ if self._contact is not None:
+ self._contact.disconnect_all_from_obj(self)
+
+ self._contact = None
+ self._client = None
+
+ def _on_client_state_changed(self,
+ _client: Client,
+ _signal_name: str,
+ state: SimpleClientState
+ ) -> None:
+ self._update_message_input_state()
+
+ def _on_muc_state_changed(self, *args: Any) -> None:
+ self._update_message_input_state()
+
+ def _on_user_state_changed(self, *args: Any) -> None:
+ self._update_message_input_state()
+
+ def _update_message_input_state(self) -> None:
+ assert self._client
+ state = self._client.state.is_available
+
+ if isinstance(self._contact, GroupchatContact):
+ state = self._contact.is_joined
+ if self._contact.is_joined:
+ self_contact = self._contact.get_self()
+ assert self_contact
+ state = not self_contact.role.is_visitor
+
+ if isinstance(self._contact, GroupchatParticipant):
+ state = self._contact.is_available
+
+ self._ui.emoticons_button.set_sensitive(state)
+ self._ui.formattings_button.set_sensitive(state)
+ self.msg_textview.set_sensitive(state)
+ self.msg_textview.set_editable(state)
+
+ def _set_chatstate(self, state: bool) -> None:
+ assert self._client is not None
+ if state:
+ if self.msg_textview.has_text:
+ self._client.get_module('Chatstate').set_chatstate(
+ self._contact, Chatstate.PAUSED)
+ else:
+ self._client.get_module('Chatstate').set_chatstate(
+ self._contact, Chatstate.ACTIVE)
+ else:
+ self._client.get_module('Chatstate').set_chatstate(
+ self._contact, Chatstate.INACTIVE)
+
+ def _set_encryption_state(self, state: str) -> None:
+ action = app.window.get_action('set-encryption')
+ action.set_state(GLib.Variant('s', state))
+
+ if state in ('OMEMO', 'OpenPGP', 'PGP'):
+ icon_name = 'channel-secure-symbolic'
+ else:
+ icon_name = 'channel-insecure-symbolic'
+
+ self._ui.encryption_image.set_from_icon_name(icon_name,
+ Gtk.IconSize.MENU)
+
+ def _change_encryption(self,
+ action: Gio.SimpleAction,
+ param: GLib.Variant
+ ) -> None:
+
+ new_state = param.get_string()
+ action_state = action.get_state()
+ assert action_state is not None
+ current_state = action_state.get_string()
+ if current_state == new_state:
+ return
+
+ if new_state:
+ plugin = app.plugin_manager.encryption_plugins.get(new_state)
+ if plugin is None:
+ # TODO: Add GUI error here
+ return
+
+ if not plugin.activate_encryption(app.window.get_active_control()):
+ return
+
+ self._set_encryption_state(new_state)
+ # self.conversation_view.encryption_enabled = encryption is not None
+ contact = self.get_current_contact()
+ contact.settings.set('encryption', new_state)
+
+ self._set_encryption_details(new_state)
+
+ def _set_encryption_details(self, state: str) -> None:
+ encryption_state = {'visible': bool(state),
+ 'enc_type': state,
+ 'authenticated': False}
+
+ if state:
+ app.plugin_manager.extension_point(
+ f'encryption_state{state}', self, encryption_state)
+
+ visible, enc_type, authenticated = encryption_state.values()
+ assert isinstance(visible, bool)
+
+ if authenticated:
+ authenticated_string = _('and authenticated')
+ self._ui.encryption_details_image.set_from_icon_name(
+ 'security-high-symbolic', Gtk.IconSize.MENU)
+ else:
+ authenticated_string = _('and NOT authenticated')
+ self._ui.encryption_details_image.set_from_icon_name(
+ 'security-low-symbolic', Gtk.IconSize.MENU)
+
+ tooltip = _('%(type)s encryption is active %(authenticated)s.') % {
+ 'type': enc_type, 'authenticated': authenticated_string}
+
+ self._ui.encryption_details_button.set_tooltip_text(tooltip)
+ self._ui.encryption_details_button.set_visible(visible)
+ self._ui.encryption_details_image.set_sensitive(visible)
+
+ def _on_encryption_details_clicked(self, _button: Gtk.Button) -> None:
+ contact = self.get_current_contact()
+ encryption = contact.settings.get('encryption')
+ app.plugin_manager.extension_point(
+ f'encryption_dialog{encryption}', app.window.get_active_control())
+
+ def _set_settings_menu(self, contact: ChatContactT) -> None:
+ if isinstance(contact, GroupchatContact):
+ menu = get_groupchat_menu(contact)
+ elif isinstance(contact, GroupchatParticipant):
+ menu = get_private_chat_menu(contact)
+ elif contact.is_self:
+ menu = get_self_contact_menu(contact)
+ else:
+ menu = get_singlechat_menu(contact)
+ self._ui.settings_menu.set_menu_model(menu)
+
+ def _init_emoticon_popover(self) -> None:
+ if not app.settings.get('emoticons_theme'):
+ return
+
+ if sys.platform == 'darwin':
+ emoji_chooser.text_widget = self.msg_textview
+ self._ui.emoticons_button.set_popover(emoji_chooser)
+ return
+
+ self._ui.emoticons_button.set_sensitive(True)
+ self._ui.emoticons_button.connect('clicked',
+ self._on_emoticon_button_clicked)
+
+ def toggle_emoticons(self) -> None:
+ if app.settings.get('emoticons_theme'):
+ self._ui.emoticons_button.set_no_show_all(False)
+ self._ui.emoticons_button.show()
+ else:
+ self._ui.emoticons_button.set_no_show_all(True)
+ self._ui.emoticons_button.hide()
+
+ def _on_emoticon_button_clicked(self, _widget: Gtk.Button) -> None:
+ self.msg_textview.emit('insert-emoji')
+ self._ui.emoticons_button.set_active(False)
+
+ def _on_format(self, name: str) -> None:
+ name = name.removeprefix('input-')
+ self.msg_textview.apply_formatting(name)
+
+ def _on_clear(self) -> None:
+ self.msg_textview.clear()
+
+ def _on_show_emoji_chooser(self) -> None:
+ if sys.platform == 'darwin':
+ popover = self._ui.emoticons_button.get_popover()
+ assert popover
+ popover.show()
+ else:
+ self.msg_textview.emit('insert-emoji')
+
+ def _init_spell_checker(self) -> int:
+ if not app.is_installed('GSPELL'):
+ return 0
+
+ checker = Gspell.Checker.new(Gspell.language_get_default())
+
+ buffer = Gspell.TextBuffer.get_from_gtk_text_buffer(
+ self.msg_textview.get_buffer())
+ buffer.set_spell_checker(checker)
+
+ use_spell_check = app.settings.get('use_speller')
+ view = Gspell.TextView.get_from_gtk_text_view(self.msg_textview)
+ view.set_inline_spell_checking(use_spell_check)
+ view.set_enable_language_menu(True)
+
+ return checker.connect('notify::language', self._on_language_changed)
+
+ def toggle_spell_checker(self) -> None:
+ if not app.is_installed('GSPELL'):
+ return
+ use_spell_check = app.settings.get('use_speller')
+ view = Gspell.TextView.get_from_gtk_text_view(self.msg_textview)
+ view.set_inline_spell_checking(use_spell_check)
+
+ def _set_spell_checker_language(self, contact: ChatContactT) -> None:
+ if not app.is_installed('GSPELL'):
+ return
+
+ buffer = Gspell.TextBuffer.get_from_gtk_text_buffer(
+ self.msg_textview.get_buffer())
+ checker = buffer.get_spell_checker()
+ assert checker is not None
+ lang = self._get_spell_checker_language(contact)
+
+ with checker.handler_block(self._language_handler_id):
+ checker.set_language(lang)
+
+ def _get_spell_checker_language(self,
+ contact: ChatContactT
+ ) -> Optional[Gspell.Language]:
+
+ lang = contact.settings.get('speller_language')
+ if not lang:
+ # use the default one
+ lang = app.settings.get('speller_language')
+ if not lang:
+ lang = i18n.LANG
+
+ assert isinstance(lang, str)
+ lang = Gspell.language_lookup(lang)
+ if lang is None:
+ lang = Gspell.language_get_default()
+ return lang
+
+ def _on_language_changed(self,
+ checker: Gspell.Checker,
+ _param: Any) -> None:
+
+ gspell_lang = checker.get_language()
+ if gspell_lang is not None:
+ contact = self.get_current_contact()
+ contact.settings.set('speller_language', gspell_lang.get_code())
+
+ def _on_send_file_enabled_changed(self,
+ action: Gio.SimpleAction,
+ _param: GObject.ParamSpec) -> None:
+
+ self._update_send_file_button_tooltip()
+
+ def _update_send_file_button_tooltip(self):
+ httpupload = app.window.get_action_enabled('send-file-httpupload')
+ jingle = app.window.get_action_enabled('send-file-jingle')
+
+ if not httpupload and not jingle:
+ tooltip_text = _('No File Transfer available')
+ self._ui.sendfile_button.set_tooltip_text(tooltip_text)
+ return
+
+ assert self._contact is not None
+ client = app.get_client(self._contact.account)
+
+ tooltip_text = _('Send File…')
+ if httpupload and not jingle:
+ max_file_size = client.get_module('HTTPUpload').max_file_size
+ if max_file_size is not None:
+ if app.settings.get('use_kib_mib'):
+ units = GLib.FormatSizeFlags.IEC_UNITS
+ else:
+ units = GLib.FormatSizeFlags.DEFAULT
+ max_file_size = GLib.format_size_full(max_file_size, units)
+ tooltip_text = _('Send File (max. %s)…') % max_file_size
+
+ self._ui.sendfile_button.set_tooltip_text(tooltip_text)
+
+ def _on_buffer_changed(self, textbuffer: Gtk.TextBuffer) -> None:
+ has_text = self.msg_textview.has_text
+ send_message_action = app.window.get_action('send-message')
+ send_message_action.set_enabled(has_text)
+
+ encryption_enabled, encryption_name = self._get_encryption_state()
+
+ if has_text and encryption_enabled:
+ app.plugin_manager.extension_point('typing' + encryption_name, self)
+
+ assert self._contact
+ client = app.get_client(self._contact.account)
+ client.get_module('Chatstate').set_keyboard_activity(self._contact)
+ if not has_text:
+ client.get_module('Chatstate').set_chatstate_delayed(
+ self._contact, Chatstate.ACTIVE)
+ return
+
+ client.get_module('Chatstate').set_chatstate(
+ self._contact, Chatstate.COMPOSING)
+
+ def _on_msg_textview_key_press_event(self,
+ textview: MessageInputTextView,
+ event: Gdk.EventKey
+ ) -> bool:
+ # pylint: disable=too-many-nested-blocks
+ if event.keyval == Gdk.KEY_space:
+ self.space_pressed = True
+
+ elif ((self.space_pressed or self.msg_textview.undo_pressed) and
+ event.keyval not in (Gdk.KEY_Control_L, Gdk.KEY_Control_R) and
+ not (event.keyval == Gdk.KEY_z and
+ event.get_state() & Gdk.ModifierType.CONTROL_MASK)):
+ # If the space key has been pressed and now it hasn't,
+ # we save the buffer into the undo list. But be careful we're not
+ # pressing Control again (as in ctrl+z)
+ _buffer = textview.get_buffer()
+ start_iter, end_iter = _buffer.get_bounds()
+ self.msg_textview.save_undo(_buffer.get_text(start_iter,
+ end_iter,
+ True))
+ self.space_pressed = False
+
+ event_state = event.get_state()
+ if event_state & Gdk.ModifierType.SHIFT_MASK:
+ if event_state & Gdk.ModifierType.CONTROL_MASK:
+ if event.keyval == Gdk.KEY_ISO_Left_Tab:
+ app.window.select_next_chat(
+ Direction.PREV, unread_first=True)
+ return True
+
+ if event.keyval in (Gdk.KEY_Page_Down, Gdk.KEY_Page_Up):
+ control = app.window.get_active_control()
+ if control is not None:
+ control.conversation_view.event(event)
+ return True
+
+ if event_state & Gdk.ModifierType.CONTROL_MASK:
+ if event.keyval == Gdk.KEY_Tab:
+ app.window.select_next_chat(Direction.NEXT, unread_first=True)
+ return True
+
+ if event.keyval == Gdk.KEY_z:
+ self.msg_textview.undo()
+ return True
+
+ if event.keyval == Gdk.KEY_Up:
+ self.toggle_message_correction()
+ return True
+
+ if event.keyval in (Gdk.KEY_Return, Gdk.KEY_KP_Enter):
+ if event_state & Gdk.ModifierType.SHIFT_MASK:
+ textview.insert_newline()
+ return True
+
+ if event_state & Gdk.ModifierType.CONTROL_MASK:
+ if not app.settings.get('send_on_ctrl_enter'):
+ textview.insert_newline()
+ return True
+ else:
+ if app.settings.get('send_on_ctrl_enter'):
+ textview.insert_newline()
+ return True
+
+ assert self._contact is not None
+ if not app.account_is_available(self._contact.account):
+ # we are not connected
+ ErrorDialog(
+ _('No Connection Available'),
+ _('Your message can not be sent until you are connected.'))
+ return True
+
+ app.window.activate_action('send-message', None)
+ return True
+
+ return False
+
+ def _on_paste_clipboard(self,
+ _texview: MessageInputTextView
+ ) -> None:
+
+ clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
+ image = clipboard.wait_for_image()
+ if image is None:
+ return
+
+ if not app.settings.get('confirm_paste_image'):
+ self._paste_event_confirmed(True, image)
+ return
+
+ PastePreviewDialog(
+ _('Paste Image'),
+ _('You are trying to paste an image'),
+ _('Are you sure you want to paste your '
+ "clipboard's image into the chat window?"),
+ _('_Do not ask me again'),
+ image,
+ [DialogButton.make('Cancel'),
+ DialogButton.make('Accept',
+ text=_('_Paste'),
+ callback=self._paste_event_confirmed,
+ args=[image])]).show()
+
+ def _paste_event_confirmed(self,
+ is_checked: bool,
+ image: GdkPixbuf.Pixbuf
+ ) -> None:
+ if is_checked:
+ app.settings.set('confirm_paste_image', False)
+
+ dir_ = tempfile.gettempdir()
+ path = os.path.join(dir_, f'{uuid.uuid4()}.png')
+ if image is None:
+ log.error('Could not process pasted image')
+ return
+
+ image.savev(path, 'png', [], [])
+
+ assert self._contact is not None
+ app.interface.start_file_transfer(self._contact, path)
+
+ def toggle_message_correction(self) -> None:
+ if self.is_correcting:
+ self.is_correcting = False
+ self.msg_textview.clear()
+ self.msg_textview.get_style_context().remove_class(
+ 'gajim-msg-correcting')
+ self.msg_textview.grab_focus()
+ return
+
+ assert self._contact is not None
+ last_message_id = self.last_message_id.get(
+ (self._contact.account, self._contact.jid))
+ if last_message_id is None:
+ return
+
+ message_row = app.storage.archive.get_last_correctable_message(
+ self._contact.account, self._contact.jid, last_message_id)
+ if message_row is None:
+ return
+
+ self.is_correcting = True
+ self.msg_textview.clear()
+ self.msg_textview.insert_text(message_row.message)
+ self.msg_textview.get_style_context().add_class('gajim-msg-correcting')
+ self.msg_textview.grab_focus()
+
+ def _on_message_sent(self, event: events.MessageSent) -> None:
+ if not event.message:
+ return
+
+ if event.correct_id is None:
+ # This wasn't a corrected message
+ assert self._contact is not None
+ oob_url = event.additional_data.get_value('gajim', 'oob_url')
+ account = self._contact.account
+ jid = self._contact.jid
+ if oob_url == event.message:
+ # Don't allow to correct HTTP Upload file transfer URLs
+ self.last_message_id[(account, jid)] = None
+ else:
+ self.last_message_id[(account, jid)] = event.message_id
+
+ self.msg_textview.get_style_context().remove_class(
+ 'gajim-msg-correcting')
+
+ def process_event(self, event: events.ApplicationEvent) -> None:
+ if isinstance(event, events.MessageSent):
+ self._on_message_sent(event)
diff --git a/gajim/gtk/message_input.py b/gajim/gtk/message_input.py
index 0251c8cbc..ff2c6f72a 100644
--- a/gajim/gtk/message_input.py
+++ b/gajim/gtk/message_input.py
@@ -17,7 +17,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 Any
+from typing import Optional
import sys
@@ -27,7 +30,10 @@ from gi.repository import GdkPixbuf
from gi.repository import GLib
from gi.repository import Pango
+from nbxmpp.protocol import JID
+
from gajim.common import app
+from gajim.common.i18n import _
from gajim.common.styling import process
from gajim.common.styling import PlainBlock
from gajim.common.types import ChatContactT
@@ -52,11 +58,8 @@ class MessageInputTextView(Gtk.TextView):
'''
A Gtk.Textview for chat message input
'''
- def __init__(self, account: str, contact: ChatContactT) -> None:
+ def __init__(self) -> None:
Gtk.TextView.__init__(self)
- self.account = account
- self.contact = contact
-
self.set_border_width(3)
self.set_accepts_tab(True)
self.set_editable(True)
@@ -73,8 +76,10 @@ class MessageInputTextView(Gtk.TextView):
self._undo_list: list[str] = []
self.undo_pressed: bool = False
- self._chat_action_processor = ChatActionProcessor(
- account, contact, self)
+ self._contact: Optional[ChatContactT] = None
+ self._drafts: dict[tuple[str, JID], str] = {}
+
+ self._chat_action_processor = ChatActionProcessor(self)
self.get_buffer().create_tag('strong', weight=Pango.Weight.BOLD)
self.get_buffer().create_tag('emphasis', style=Pango.Style.ITALIC)
@@ -86,6 +91,24 @@ class MessageInputTextView(Gtk.TextView):
self.connect('focus-in-event', self._on_focus_in)
self.connect('focus-out-event', self._on_focus_out)
self.connect('destroy', self._on_destroy)
+ self.connect('populate-popup', self._on_populate_popup)
+
+ def switch_contact(self, contact: ChatContactT) -> None:
+ if self._contact is not None:
+ account = self._contact.account
+ jid = self._contact.jid
+ if self.has_text:
+ self._drafts[(account, jid)] = self.get_text()
+ else:
+ self._drafts.pop((account, jid), None)
+
+ self.clear()
+ draft = self._drafts.get((contact.account, contact.jid))
+ if draft is not None:
+ self.insert_text(draft)
+
+ self._contact = contact
+ self._chat_action_processor.switch_contact(contact)
def _on_destroy(self, _widget: Gtk.Widget) -> None:
# We restore the TextView’s drag destination to avoid a GTK warning
@@ -117,7 +140,7 @@ class MessageInputTextView(Gtk.TextView):
scrolled = self.get_parent()
assert scrolled
scrolled.get_style_context().remove_class('message-input-focus')
- if not self.has_text():
+ if not self.has_text:
self.toggle_speller(False)
return False
@@ -167,6 +190,7 @@ class MessageInputTextView(Gtk.TextView):
if buf.get_end_iter().equal(iter_):
GLib.idle_add(scroll_to_end, self.get_parent())
+ @property
def has_text(self) -> bool:
buf = self.get_buffer()
start, end = buf.get_bounds()
@@ -179,6 +203,9 @@ class MessageInputTextView(Gtk.TextView):
text = self.get_buffer().get_text(start, end, True)
return text
+ def get_draft(self, account: str, jid: JID) -> Optional[str]:
+ return self._drafts.get((account, jid))
+
def toggle_speller(self, activate: bool) -> None:
if app.is_installed('GSPELL') and app.settings.get('use_speller'):
spell_view = Gspell.TextView.get_from_gtk_text_view(self)
@@ -302,6 +329,7 @@ class MessageInputTextView(Gtk.TextView):
buf = self.get_buffer()
start, end = buf.get_bounds()
buf.delete(start, end)
+ self._undo_list.clear()
def save_undo(self, text: str) -> None:
self._undo_list.append(text)
@@ -315,9 +343,42 @@ class MessageInputTextView(Gtk.TextView):
buf.set_text(self._undo_list.pop())
self.undo_pressed = True
- def process_outgoing_message(self,
- contact_name: str,
- highlight: bool
- ) -> None:
- self._chat_action_processor.process_outgoing_message(
- contact_name, highlight)
+ def _on_populate_popup(self,
+ _textview: MessageInputTextView,
+ menu: Gtk.Widget
+ ) -> None:
+ assert isinstance(menu, Gtk.Menu)
+ item = Gtk.MenuItem.new_with_mnemonic(_('_Undo'))
+ menu.prepend(item)
+ item.connect('activate', self.undo)
+
+ item = Gtk.SeparatorMenuItem()
+ menu.prepend(item)
+
+ item = Gtk.MenuItem.new_with_mnemonic(_('_Clear'))
+ menu.prepend(item)
+ item.connect('activate', self.clear)
+
+ paste_item = Gtk.MenuItem.new_with_label(_('Paste as quote'))
+ paste_item.connect('activate', self._paste_clipboard_as_quote)
+ menu.append(paste_item)
+
+ menu.show_all()
+
+ def mention_participant(self, name: str) -> None:
+ gc_refer_to_nick_char = app.settings.get('gc_refer_to_nick_char')
+ text = f'{name}{gc_refer_to_nick_char} '
+ self.insert_text(text)
+ self.grab_focus()
+
+ def insert_as_quote(self, text: str) -> None:
+ text = '> ' + text.replace('\n', '\n> ') + '\n'
+ self.insert_text(text)
+ self.grab_focus()
+
+ def _paste_clipboard_as_quote(self, _item: Gtk.MenuItem) -> None:
+ clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
+ text = clipboard.wait_for_text()
+ if text is None:
+ return
+ self.insert_as_quote(text)
diff --git a/gajim/gtk/notification_manager.py b/gajim/gtk/notification_manager.py
index f8bd58d85..a5f140a86 100644
--- a/gajim/gtk/notification_manager.py
+++ b/gajim/gtk/notification_manager.py
@@ -378,7 +378,7 @@ class UnsubscribedRow(NotificationRow):
remove_button.set_valign(Gtk.Align.CENTER)
remove_button.set_tooltip_text('Remove from contact list')
remove_button.set_action_name(
- f'win.remove-contact-{self._account}')
+ f'win.{self._account}-remove-contact')
remove_button.set_action_target_value(GLib.Variant('s', str(self.jid)))
self.grid.attach(remove_button, 3, 1, 1, 2)
diff --git a/gajim/gtk/roster.py b/gajim/gtk/roster.py
index e91de30f8..baddd859e 100644
--- a/gajim/gtk/roster.py
+++ b/gajim/gtk/roster.py
@@ -117,8 +117,6 @@ class Roster(Gtk.ScrolledWindow, EventHelper):
self._ui.connect_signals(self)
self.register_events([
- ('account-connected', ged.CORE, self._on_account_state),
- ('account-disconnected', ged.CORE, self._on_account_state),
('roster-received', ged.CORE, self._on_roster_received),
('theme-update', ged.GUI2, self._on_theme_update),
('roster-push', ged.GUI2, self._on_roster_push),
@@ -135,69 +133,29 @@ class Roster(Gtk.ScrolledWindow, EventHelper):
self._filter_enabled = False
self._filter_string = ''
- self._add_actions()
+ app.settings.connect_signal('showoffline', self._on_setting_changed)
+ app.settings.connect_signal('sort_by_show_in_roster',
+ self._on_setting_changed)
+
+ self._connect_actions()
self._initial_draw()
- def _add_actions(self):
- actions = [
- ('contact-info', self._on_contact_info),
- ('modify-gateway', self._on_modify_gateway),
- ('execute-command', self._on_execute_command),
- ('block-contact', self._on_block_contact),
- ('remove-contact', self._on_remove_contact),
+ def _connect_actions(self):
+ app_actions = [
+ (f'{self._account}-modify-gateway', self._on_modify_gateway),
+ (f'{self._account}-execute-command', self._on_execute_command),
+ (f'{self._account}-block-contact', self._on_block_contact),
+ (f'{self._account}-remove-contact', self._on_remove_contact),
]
- for action in actions:
+
+ for action in app_actions:
action_name, func = action
- act = Gio.SimpleAction.new(
- f'{action_name}-{self._account}', GLib.VariantType.new('s'))
- act.connect('activate', func)
- app.window.add_action(act)
-
- action = Gio.SimpleAction.new_stateful(
- f'show-offline-{self._account}',
- None,
- GLib.Variant.new_boolean(app.settings.get('showoffline')))
- action.connect('change-state', self._on_show_offline)
- app.window.add_action(action)
-
- action = Gio.SimpleAction.new_stateful(
- f'sort-by-show-{self._account}',
- None,
- GLib.Variant.new_boolean(
- app.settings.get('sort_by_show_in_roster')))
- action.connect('change-state', self._on_sort_by_show)
- app.window.add_action(action)
-
- def update_actions(self):
- online = app.account_is_connected(self._account)
- blocking_support = self._client.get_module('Blocking').supported
-
- actions = [('contact-info', online),
- ('modify-gateway', online),
- ('execute-command', online),
- ('block-contact', online and blocking_support),
- ('remove-contact', online)]
-
- for action, enabled in actions:
- act = app.window.lookup_action(f'{action}-{self._account}')
- assert act is not None
- act.set_enabled(enabled)
-
- def _remove_actions(self):
- actions = [
- 'contact-info',
- 'modify-gateway',
- 'execute-command',
- 'block-contact',
- 'remove-contact',
- 'show-offline',
- 'sort-by-show',
- ]
- for action in actions:
- app.window.remove_action(f'{action}-{self._account}')
+ action = app.app.lookup_action(action_name)
+ assert action is not None
+ action.connect('activate', func)
- def _on_account_state(self, _event: ApplicationEvent) -> None:
- self.update_actions()
+ def _get_contact(self, jid: str) -> types.BareContact:
+ return self._client.get_module('Contacts').get_contact(jid)
def _on_theme_update(self, _event: ApplicationEvent) -> None:
self.redraw()
@@ -304,20 +262,7 @@ class Roster(Gtk.ScrolledWindow, EventHelper):
tooltip.set_custom(widget)
return value
- def _on_show_offline(self,
- action: Gio.SimpleAction,
- value: Optional[GLib.Variant]) -> None:
-
- assert value is not None
- action.set_state(value)
- app.settings.set('showoffline', value.get_boolean())
- self._refilter()
-
- def _on_sort_by_show(self,
- action: Gio.SimpleAction,
- value: GLib.Variant) -> None:
- action.set_state(value)
- app.settings.set('sort_by_show_in_roster', value.get_boolean())
+ def _on_setting_changed(self, *args: Any) -> None:
self._refilter()
def _on_contact_info(self,
@@ -787,7 +732,7 @@ class Roster(Gtk.ScrolledWindow, EventHelper):
return
def _on_destroy(self, _roster: Roster) -> None:
- self._remove_actions()
+ app.settings.disconnect_signals(self)
self._contact_refs.clear()
self._group_refs.clear()
self._unset_model()
diff --git a/gajim/gtk/roster_item_exchange.py b/gajim/gtk/roster_item_exchange.py
index bebd75d7f..3c4efbf87 100644
--- a/gajim/gtk/roster_item_exchange.py
+++ b/gajim/gtk/roster_item_exchange.py
@@ -243,11 +243,6 @@ class RosterItemExchange(Gtk.ApplicationWindow):
self._client.get_module('Roster').set_item(
jid, model[iter_][2], groups)
- # Update opened chats
- ctrl = app.window.get_control(self.account, jid)
- if ctrl:
- ctrl.update_ui()
-
iter_ = model.iter_next(iter_)
elif self._action == 'delete':
count = 0
diff --git a/gajim/gtk/security_label_selector.py b/gajim/gtk/security_label_selector.py
index 42469e809..0ad967591 100644
--- a/gajim/gtk/security_label_selector.py
+++ b/gajim/gtk/security_label_selector.py
@@ -19,6 +19,7 @@ from typing import Optional
from gi.repository import Gtk
from gi.repository import Pango
+from nbxmpp.modules.security_labels import SecurityLabel
from nbxmpp.protocol import JID
from gajim.common import app
@@ -31,11 +32,11 @@ from gajim.common.i18n import _
class SecurityLabelSelector(Gtk.ComboBox):
- def __init__(self, account: str, contact: ChatContactT) -> None:
+ def __init__(self) -> None:
Gtk.ComboBox.__init__(self, no_show_all=True)
- self._account = account
- self._client = app.get_client(account)
- self._contact = contact
+ self._account: Optional[str] = None
+ self._client: Optional[Client] = None
+ self._contact: Optional[ChatContactT] = None
self.set_valign(Gtk.Align.CENTER)
self.set_tooltip_text(_('Select a security label for your message…'))
@@ -51,14 +52,25 @@ class SecurityLabelSelector(Gtk.ComboBox):
app.ged.register_event_handler(
'sec-catalog-received', ged.GUI1, self._sec_labels_received)
+ self.connect('changed', self._on_changed)
+ self.connect('destroy', self._on_destroy)
+
+ def switch_contact(self, contact: ChatContactT) -> None:
+ app.settings.disconnect_signals(self)
+ if self._client is not None:
+ self._client.disconnect_all_from_obj(self)
+
+ self._account = contact.account
+ self._client = app.get_client(contact.account)
+ self._contact = contact
+
app.settings.connect_signal(
'enable_security_labels',
self._on_setting_changed,
account=self._account)
self._client.connect_signal(
'state-changed', self._on_client_state_changed)
- self.connect('changed', self._on_changed)
- self.connect('destroy', self._on_destroy)
+ self._update()
def _on_destroy(self, _widget: SecurityLabelSelector) -> None:
app.ged.remove_event_handler('sec-catalog-received',
@@ -79,13 +91,14 @@ class SecurityLabelSelector(Gtk.ComboBox):
def _on_client_state_changed(self,
_client: Client,
_signal_name: str,
- _state: SimpleClientState):
- self.update()
+ _state: SimpleClientState
+ ) -> None:
+ self._update()
def _on_setting_changed(self,
state: bool,
_name: str,
- account: str,
+ _account: Optional[str],
_jid: Optional[JID]
) -> None:
self.set_no_show_all(not state)
@@ -93,9 +106,13 @@ class SecurityLabelSelector(Gtk.ComboBox):
self.show_all()
else:
self.hide()
+ self._update()
def _sec_labels_received(self, event: SecCatalogReceived) -> None:
- if event.account != self._account:
+ if self._account is None or self._account != event.account:
+ return
+
+ if self._contact is None:
return
if event.jid != self._contact.jid.bare:
@@ -120,11 +137,13 @@ class SecurityLabelSelector(Gtk.ComboBox):
self.set_no_show_all(False)
self.show_all()
- def get_seclabel(self) -> Optional[str]:
+ def get_seclabel(self) -> Optional[SecurityLabel]:
index = self.get_active()
if index == -1:
return None
+ assert self._contact is not None
+ assert self._client is not None
jid = self._contact.jid.bare
catalog = self._client.get_module('SecLabels').get_catalog(jid)
if catalog is None:
@@ -132,10 +151,13 @@ class SecurityLabelSelector(Gtk.ComboBox):
labels, label_list = catalog.labels, catalog.get_label_names()
label_name = label_list[index]
- label = labels[label_name]
- return label
+ return labels[label_name]
+
+ def _update(self) -> None:
+ assert self._account is not None
+ assert self._client is not None
+ assert self._contact is not None
- def update(self) -> None:
chat_active = app.window.is_chat_active(
self._account, self._contact.jid)
if chat_active:
diff --git a/gajim/gtk/util.py b/gajim/gtk/util.py
index 73c822857..2e5eebd66 100644
--- a/gajim/gtk/util.py
+++ b/gajim/gtk/util.py
@@ -23,6 +23,7 @@ from typing import Union
import logging
import math
+import sys
import textwrap
from io import BytesIO
from importlib import import_module
@@ -358,6 +359,18 @@ def get_primary_accel_mod() -> Optional[Gdk.ModifierType]:
return Gtk.accelerator_parse('<Primary>')[1]
+def get_copy_modifier() -> Gdk.ModifierType:
+ if sys.platform == 'darwin':
+ return Gdk.ModifierType.META_MASK
+ return Gdk.ModifierType.CONTROL_MASK
+
+
+def get_copy_modifier_keys() -> tuple[int, int]:
+ if sys.platform == 'darwin':
+ return Gdk.KEY_Meta_L, Gdk.KEY_Meta_R
+ return Gdk.KEY_Control_L, Gdk.KEY_Control_R
+
+
def get_hardware_key_codes(keyval: int) -> list[int]:
display = Gdk.Display.get_default()
assert display is not None
diff --git a/gajim/gui_interface.py b/gajim/gui_interface.py
index cc0841bae..0f1d4b8f2 100644
--- a/gajim/gui_interface.py
+++ b/gajim/gui_interface.py
@@ -67,16 +67,20 @@ from gajim.common.events import FileCompleted
from gajim.common.events import FileHashError
from gajim.common.events import FileProgress
from gajim.common.events import FileError
+from gajim.common.modules.contacts import BareContact
+from gajim.common.modules.contacts import GroupchatContact
from gajim.common.structs import OutgoingMessage
from gajim.common.i18n import _
from gajim.common.client import Client
from gajim.common.const import FTState
from gajim.common.file_props import FileProp
+from gajim.common.types import ChatContactT
from gajim.common.modules.httpupload import HTTPFileTransfer
from gajim.gui.dialogs import ErrorDialog
from gajim.gui.filechoosers import FileChooserDialog
+from gajim.gui.file_transfer_send import SendFileDialog
from gajim.gui.filetransfer import FileTransfersWindow
from gajim.gui.menus import build_accounts_menu
from gajim.gui.util import get_app_window
@@ -125,7 +129,6 @@ class Interface:
# pylint: disable=line-too-long
self.handlers = {
'signed-in': [self.handle_event_signed_in],
- 'presence-received': [self.handle_event_presence],
'message-sent': [self.handle_event_msgsent],
'message-not-sent': [self.handle_event_msgnotsent],
}
@@ -166,14 +169,6 @@ class Interface:
if app.settings.get('ask_online_status'):
app.window.show_account_page(account)
- def handle_event_presence(self, event):
- account = event.conn.name
- jid = event.jid
-
- ctrl = app.window.get_control(account, jid)
- if ctrl and ctrl.session and len(event.contact_list) > 1:
- ctrl.remove_session(ctrl.session)
-
@staticmethod
def handle_event_msgsent(event):
if not event.play_sound:
@@ -377,6 +372,60 @@ class Interface:
client = app.get_client(transfer.account)
client.get_module('HTTPUpload').cancel_transfer(transfer)
+ def _get_pref_ft_method(self, contact: ChatContactT) -> Optional[str]:
+ ft_pref = app.settings.get_account_setting(contact.account,
+ 'filetransfer_preference')
+ httpupload = app.window.get_action_enabled('send-file-httpupload')
+ jingle = app.window.get_action_enabled('send-file-jingle')
+
+ if isinstance(contact, GroupchatContact):
+ if httpupload:
+ return 'httpupload'
+ return None
+
+ if httpupload and jingle:
+ return ft_pref
+
+ if httpupload:
+ return 'httpupload'
+
+ if jingle:
+ return 'jingle'
+ return None
+
+ def start_file_transfer(self,
+ contact: ChatContactT,
+ path: Optional[str] = None,
+ method: Optional[str] = None
+ ) -> None:
+ if method is None:
+ method = self._get_pref_ft_method(contact)
+ if method is None:
+ return
+
+ current_control = app.window.get_active_control()
+ if current_control is None:
+ return
+
+ if path is None:
+ if method == 'httpupload':
+ self.send_httpupload(current_control)
+ return
+ if method == 'jingle':
+ self.instances['file_transfers'].show_file_send_request(
+ contact.account, contact)
+ return
+
+ if method == 'httpupload':
+ self.send_httpupload(current_control, path)
+ else:
+ assert isinstance(contact, BareContact)
+ send_callback = partial(
+ self.instances['file_transfers'].send_file,
+ contact.account,
+ contact)
+ SendFileDialog(contact, send_callback, app.window, [path])
+
@staticmethod
def create_groupchat(account: str,
room_jid: str,