summaryrefslogtreecommitdiff
path: root/poezio/tabs
diff options
context:
space:
mode:
Diffstat (limited to 'poezio/tabs')
-rw-r--r--poezio/tabs/basetabs.py59
-rw-r--r--poezio/tabs/bookmarkstab.py2
-rw-r--r--poezio/tabs/conversationtab.py45
-rw-r--r--poezio/tabs/muclisttab.py3
-rw-r--r--poezio/tabs/muctab.py170
-rw-r--r--poezio/tabs/privatetab.py21
-rw-r--r--poezio/tabs/rostertab.py4
-rw-r--r--poezio/tabs/xmltab.py1
8 files changed, 222 insertions, 83 deletions
diff --git a/poezio/tabs/basetabs.py b/poezio/tabs/basetabs.py
index 508465e3..793eae62 100644
--- a/poezio/tabs/basetabs.py
+++ b/poezio/tabs/basetabs.py
@@ -170,15 +170,15 @@ class Tab:
return 1
@property
- def info_win(self):
+ def info_win(self) -> windows.TextWin:
return self.core.information_win
@property
- def color(self):
+ def color(self) -> Union[Tuple[int, int], Tuple[int, int, 'str']]:
return STATE_COLORS[self._state]()
@property
- def vertical_color(self):
+ def vertical_color(self) -> Union[Tuple[int, int], Tuple[int, int, 'str']]:
return VERTICAL_STATE_COLORS[self._state]()
@property
@@ -351,7 +351,7 @@ class Tab:
if hasattr(self.input, "reset_completion"):
self.input.reset_completion()
if asyncio.iscoroutinefunction(func):
- asyncio.ensure_future(func(arg))
+ asyncio.create_task(func(arg))
else:
func(arg)
return True
@@ -390,12 +390,6 @@ class Tab:
"""
return self.name
- def get_text_window(self) -> Optional[windows.TextWin]:
- """
- Returns the principal TextWin window, if there's one
- """
- return None
-
def on_input(self, key: str, raw: bool):
"""
raw indicates if the key should activate the associated command or not.
@@ -498,7 +492,7 @@ class GapTab(Tab):
return 0
@property
- def name(self):
+ def name(self) -> str:
return ''
def refresh(self):
@@ -520,6 +514,7 @@ class ChatTab(Tab):
timed_event_paused: Optional[DelayedEvent]
timed_event_not_paused: Optional[DelayedEvent]
mam_filler: Optional[MAMFiller]
+ e2e_encryption: Optional[str] = None
def __init__(self, core, jid: Union[JID, str]):
Tab.__init__(self, core)
@@ -675,7 +670,9 @@ class ChatTab(Tab):
if not self.execute_command(txt):
if txt.startswith('//'):
txt = txt[1:]
- self.command_say(xhtml.convert_simple_to_full_colors(txt))
+ asyncio.ensure_future(
+ self.command_say(xhtml.convert_simple_to_full_colors(txt))
+ )
self.cancel_paused_delay()
@command_args_parser.raw
@@ -800,7 +797,7 @@ class ChatTab(Tab):
self.last_sent_message = msg
@command_args_parser.raw
- def command_correct(self, line: str) -> None:
+ async def command_correct(self, line: str) -> None:
"""
/correct <fixed message>
"""
@@ -810,7 +807,7 @@ class ChatTab(Tab):
if not self.last_sent_message:
self.core.information('There is no message to correct.', 'Error')
return
- self.command_say(line, correct=True)
+ await self.command_say(line, correct=True)
def completion_correct(self, the_input):
if self.last_sent_message and the_input.get_argument_position() == 1:
@@ -844,7 +841,7 @@ class ChatTab(Tab):
self.state = 'scrolled'
@command_args_parser.raw
- def command_say(self, line: str, attention: bool = False, correct: bool = False):
+ async def command_say(self, line: str, attention: bool = False, correct: bool = False):
pass
def goto_build_lines(self, new_date):
@@ -979,7 +976,7 @@ class ChatTab(Tab):
def on_scroll_up(self):
if not self.query_status:
from poezio.log_loader import LogLoader
- asyncio.ensure_future(
+ asyncio.create_task(
LogLoader(logger, self, config.getbool('use_log')).scroll_requested()
)
return self.text_win.scroll_up(self.text_win.height - 1)
@@ -1004,6 +1001,7 @@ class OneToOneTab(ChatTab):
self.__status = Status("", "")
self.last_remote_message = datetime.now()
+ self._initial_log = asyncio.Event()
# Set to true once the first disco is done
self.__initial_disco = False
@@ -1018,9 +1016,9 @@ class OneToOneTab(ChatTab):
shortdesc='Request the attention.',
desc='Attention: Request the attention of the contact. Can also '
'send a message along with the attention.')
- self.init_logs(initial=initial)
+ asyncio.create_task(self.init_logs(initial=initial))
- def init_logs(self, initial=None) -> None:
+ async def init_logs(self, initial: Optional[SMessage] = None) -> None:
use_log = config.get_by_tabname('use_log', self.jid)
mam_sync = config.get_by_tabname('mam_sync', self.jid)
if use_log and mam_sync:
@@ -1031,19 +1029,16 @@ class OneToOneTab(ChatTab):
if initial is not None:
# If there is an initial message, throw it back into the
# text buffer if it cannot be fetched from mam
- async def fallback_no_mam():
- await mam_filler.done.wait()
- if mam_filler.result == 0:
- self.handle_message(initial)
-
- asyncio.ensure_future(fallback_no_mam())
+ await mam_filler.done.wait()
+ if mam_filler.result == 0:
+ await self.handle_message(initial)
elif use_log and initial:
- self.handle_message(initial, display=False)
- asyncio.ensure_future(
- LogLoader(logger, self, use_log).tab_open()
- )
+ await self.handle_message(initial, display=False)
+ elif initial:
+ await self.handle_message(initial)
+ await LogLoader(logger, self, use_log, self._initial_log).tab_open()
- def handle_message(self, msg: SMessage, display: bool = True):
+ async def handle_message(self, msg: SMessage, display: bool = True):
pass
def remote_user_color(self):
@@ -1116,10 +1111,10 @@ class OneToOneTab(ChatTab):
self.refresh()
@command_args_parser.raw
- def command_attention(self, message):
+ async def command_attention(self, message):
"""/attention [message]"""
if message != '':
- self.command_say(message, attention=True)
+ await self.command_say(message, attention=True)
else:
msg = self.core.xmpp.make_message(self.get_dest_jid())
msg['type'] = 'chat'
@@ -1127,7 +1122,7 @@ class OneToOneTab(ChatTab):
msg.send()
@command_args_parser.raw
- def command_say(self, line: str, attention: bool = False, correct: bool = False):
+ async def command_say(self, line: str, attention: bool = False, correct: bool = False):
pass
@command_args_parser.ignored
diff --git a/poezio/tabs/bookmarkstab.py b/poezio/tabs/bookmarkstab.py
index 10c7c0ce..d21b5630 100644
--- a/poezio/tabs/bookmarkstab.py
+++ b/poezio/tabs/bookmarkstab.py
@@ -96,7 +96,7 @@ class BookmarksTab(Tab):
if bm in self.bookmarks:
self.bookmarks.remove(bm)
- asyncio.ensure_future(
+ asyncio.create_task(
self.save_routine()
)
diff --git a/poezio/tabs/conversationtab.py b/poezio/tabs/conversationtab.py
index 9ddb6fc1..de1f988a 100644
--- a/poezio/tabs/conversationtab.py
+++ b/poezio/tabs/conversationtab.py
@@ -11,6 +11,7 @@ There are two different instances of a ConversationTab:
the time.
"""
+import asyncio
import curses
import logging
from datetime import datetime
@@ -21,7 +22,6 @@ from slixmpp import JID, InvalidJID, Message as SMessage
from poezio.tabs.basetabs import OneToOneTab, Tab
from poezio import common
-from poezio import tabs
from poezio import windows
from poezio import xhtml
from poezio.config import config, get_image_cache
@@ -83,8 +83,8 @@ class ConversationTab(OneToOneTab):
self.update_keys()
@property
- def general_jid(self):
- return self.jid.bare
+ def general_jid(self) -> JID:
+ return JID(self.jid.bare)
def get_info_header(self):
raise NotImplementedError
@@ -105,16 +105,25 @@ class ConversationTab(OneToOneTab):
def completion(self):
self.complete_commands(self.input)
- def handle_message(self, message: SMessage, display: bool = True):
+ async def handle_message(self, message: SMessage, display: bool = True):
"""Handle a received message.
The message can come from us (carbon copy).
"""
+
+ # Prevent messages coming from our own devices (1:1) to be reflected
+ if message['to'].bare == self.core.xmpp.boundjid.bare and \
+ message['from'].bare == self.core.xmpp.boundjid.bare:
+ _, index = self._text_buffer._find_message(message['id'])
+ if index != -1:
+ return
+
use_xhtml = config.get_by_tabname(
'enable_xhtml_im',
message['from'].bare
)
tmp_dir = get_image_cache()
+
# normal message, we are the recipient
if message['to'].bare == self.core.xmpp.boundjid.bare:
conv_jid = message['from']
@@ -132,7 +141,7 @@ class ConversationTab(OneToOneTab):
else:
return
- self.core.events.trigger('conversation_msg', message, self)
+ await self.core.events.trigger_async('conversation_msg', message, self)
if not message['body']:
return
@@ -172,7 +181,8 @@ class ConversationTab(OneToOneTab):
@refresh_wrapper.always
@command_args_parser.raw
- def command_say(self, line: str, attention: bool = False, correct: bool = False):
+ async def command_say(self, line: str, attention: bool = False, correct: bool = False):
+ await self._initial_log.wait()
msg: SMessage = self.core.xmpp.make_message(
mto=self.get_dest_jid(),
mfrom=self.core.xmpp.boundjid
@@ -189,7 +199,6 @@ class ConversationTab(OneToOneTab):
self.core.events.trigger('conversation_say', msg, self)
if not msg['body']:
return
- replaced = False
if correct or msg['replace']['id']:
msg['replace']['id'] = self.last_sent_message['id'] # type: ignore
else:
@@ -209,10 +218,10 @@ class ConversationTab(OneToOneTab):
if not msg['body']:
return
self.set_last_sent_message(msg, correct=correct)
- self.core.handler.on_normal_message(msg)
- # Our receipts slixmpp hack
msg._add_receipt = True # type: ignore
msg.send()
+ await self.core.handler.on_normal_message(msg)
+ # Our receipts slixmpp hack
self.cancel_paused_delay()
@command_args_parser.quoted(0, 1)
@@ -277,16 +286,9 @@ class ConversationTab(OneToOneTab):
else:
resource = None
if resource:
- status = (
- 'Status: %s' % resource.status) if resource.status else ''
- self.add_message(
- InfoMessage(
- "Show: %(show)s, %(status)s" % {
- 'show': resource.presence or 'available',
- 'status': status,
- }
- ),
- )
+ status = (f', Status: {resource.status}') if resource.status else ''
+ show = f"Show: {resource.presence or 'available'}"
+ self.add_message(InfoMessage(f'{show}{status}'))
return True
self.add_message(
InfoMessage("No information available"),
@@ -436,9 +438,6 @@ class ConversationTab(OneToOneTab):
1, self.width, self.height - 2 - self.core.information_win_size -
Tab.tab_win_height(), 0)
- def get_text_window(self):
- return self.text_win
-
def on_close(self):
Tab.on_close(self)
if config.get_by_tabname('send_chat_states', self.general_jid):
@@ -543,7 +542,7 @@ class StaticConversationTab(ConversationTab):
self.update_commands()
self.update_keys()
- def init_logs(self, initial=None) -> None:
+ async def init_logs(self, initial=None) -> None:
# Disable local logs because…
pass
diff --git a/poezio/tabs/muclisttab.py b/poezio/tabs/muclisttab.py
index f6b3fc35..53fce727 100644
--- a/poezio/tabs/muclisttab.py
+++ b/poezio/tabs/muclisttab.py
@@ -4,6 +4,7 @@ A MucListTab is a tab listing the rooms on a conference server.
It has no functionality except scrolling the list, and allowing the
user to join the rooms.
"""
+import asyncio
import logging
from typing import Dict, Callable
@@ -74,4 +75,4 @@ class MucListTab(ListTab):
row = self.listview.get_selected_row()
if not row:
return
- self.core.command.join(row[1])
+ asyncio.ensure_future(self.core.command.join(row[1]))
diff --git a/poezio/tabs/muctab.py b/poezio/tabs/muctab.py
index acc145af..e2d546c9 100644
--- a/poezio/tabs/muctab.py
+++ b/poezio/tabs/muctab.py
@@ -18,6 +18,7 @@ import random
import re
import functools
from copy import copy
+from dataclasses import dataclass
from datetime import datetime
from typing import (
cast,
@@ -44,12 +45,13 @@ from poezio import timed_events
from poezio import windows
from poezio import xhtml
from poezio.common import to_utc
-from poezio.config import config
+from poezio.config import config, get_image_cache
from poezio.core.structs import Command
from poezio.decorators import refresh_wrapper, command_args_parser
from poezio.logger import logger
from poezio.log_loader import LogLoader, MAMFiller
from poezio.roster import roster
+from poezio.text_buffer import CorrectionError
from poezio.theming import get_theme, dump_tuple
from poezio.user import User
from poezio.core.structs import Completion, Status
@@ -73,6 +75,18 @@ NS_MUC_USER = 'http://jabber.org/protocol/muc#user'
COMPARE_USERS_LAST_TALKED = lambda x: x.last_talked
+@dataclass
+class MessageData:
+ message: SMessage
+ delayed: bool
+ date: Optional[datetime]
+ nick: str
+ user: Optional[User]
+ room_from: str
+ body: str
+ is_history: bool
+
+
class MucTab(ChatTab):
"""
The tab containing a multi-user-chat room.
@@ -154,14 +168,14 @@ class MucTab(ChatTab):
"""
The user do not want to send their config, send an iq cancel
"""
- asyncio.ensure_future(self.core.xmpp['xep_0045'].cancel_config(self.jid))
+ asyncio.create_task(self.core.xmpp['xep_0045'].cancel_config(self.jid))
self.core.close_tab()
def send_config(self, form: Form) -> None:
"""
The user sends their config to the server
"""
- asyncio.ensure_future(self.core.xmpp['xep_0045'].set_room_config(self.jid, form))
+ asyncio.create_task(self.core.xmpp['xep_0045'].set_room_config(self.jid, form))
self.core.close_tab()
def join(self) -> None:
@@ -233,6 +247,8 @@ class MucTab(ChatTab):
message)
self.core.disable_private_tabs(self.jid.bare, reason=msg)
else:
+ self.presence_buffer = []
+ self.users = []
muc.leave_groupchat(self.core.xmpp, self.jid, self.own_nick,
message)
@@ -450,9 +466,6 @@ class MucTab(ChatTab):
# TODO: send the disco#info identity name here, if it exists.
return self.jid.node
- def get_text_window(self) -> windows.TextWin:
- return self.text_win
-
def on_lose_focus(self) -> None:
if self.joined:
if self.input.text:
@@ -480,6 +493,126 @@ class MucTab(ChatTab):
self.general_jid) and not self.input.get_text():
self.send_chat_state('active')
+ async def handle_message(self, message: SMessage) -> bool:
+ """Parse an incoming message
+
+ Returns False if the message was dropped silently.
+ """
+ room_from = message['from'].bare
+ nick_from = message['mucnick']
+ user = self.get_user_by_name(nick_from)
+ if user and user in self.ignores:
+ return False
+
+ await self.core.events.trigger_async('muc_msg', message, self)
+ use_xhtml = config.get_by_tabname('enable_xhtml_im', room_from)
+ tmp_dir = get_image_cache()
+ body = xhtml.get_body_from_message_stanza(
+ message, use_xhtml=use_xhtml, extract_images_to=tmp_dir)
+
+ # TODO: #3314. Is this a MUC reflection?
+ # Is this an encrypted message? Is so ignore.
+ # It is not possible in the OMEMO case to decrypt these messages
+ # since we don't encrypt for our own device (something something
+ # forward secrecy), but even for non-FS encryption schemes anyway
+ # messages shouldn't have changed after a round-trip to the room.
+ # Otherwire replace the matching message we sent.
+ if not body:
+ return False
+
+ old_state = self.state
+ delayed, date = common.find_delayed_tag(message)
+ is_history = not self.joined and delayed
+
+ mdata = MessageData(
+ message, delayed, date, nick_from, user, room_from, body,
+ is_history
+ )
+
+ replaced = False
+ if message.xml.find('{urn:xmpp:message-correct:0}replace') is not None:
+ replaced = await self._handle_correction_message(mdata)
+ if not replaced:
+ await self._handle_normal_message(mdata)
+ if mdata.nick == self.own_nick:
+ self.set_last_sent_message(message, correct=replaced)
+ self._refresh_after_message(old_state)
+ return True
+
+ def _refresh_after_message(self, old_state: str) -> None:
+ """Refresh the appropriate UI after a message is received"""
+ if self is self.core.tabs.current_tab:
+ self.refresh()
+ elif self.state != old_state:
+ self.core.refresh_tab_win()
+ current = self.core.tabs.current_tab
+ current.refresh_input()
+ self.core.doupdate()
+
+ async def _handle_correction_message(self, message: MessageData) -> bool:
+ """Process a correction message.
+
+ Returns true if a message was actually corrected.
+ """
+ replaced_id = message.message['replace']['id']
+ if replaced_id != '' and config.get_by_tabname(
+ 'group_corrections', JID(message.room_from)):
+ try:
+ delayed_date = message.date or datetime.now()
+ modify_hl = self.modify_message(
+ message.body,
+ replaced_id,
+ message.message['id'],
+ time=delayed_date,
+ delayed=message.delayed,
+ nickname=message.nick,
+ user=message.user
+ )
+ if modify_hl:
+ await self.core.events.trigger_async(
+ 'highlight',
+ message.message,
+ self
+ )
+ return True
+ except CorrectionError:
+ log.debug('Unable to correct a message', exc_info=True)
+ return False
+
+ async def _handle_normal_message(self, message: MessageData) -> None:
+ """
+ Process the non-correction groupchat message.
+ """
+ ui_msg: Union[InfoMessage, Message]
+ # Messages coming from MUC barejid (Server maintenance, IRC mode
+ # changes from biboumi, etc.) have no nick/resource and are displayed
+ # as info messages.
+ highlight = False
+ if message.nick:
+ highlight = self.message_is_highlight(
+ message.body, message.nick, message.is_history
+ )
+ ui_msg = Message(
+ txt=message.body,
+ time=message.date,
+ nickname=message.nick,
+ history=message.is_history,
+ delayed=message.delayed,
+ identifier=message.message['id'],
+ jid=message.message['from'],
+ user=message.user,
+ highlight=highlight,
+ )
+ else:
+ ui_msg = InfoMessage(
+ txt=message.body,
+ time=message.date,
+ identifier=message.message['id'],
+ )
+ self.add_message(ui_msg)
+ if highlight:
+ await self.core.events.trigger_async('highlight', message, self)
+
def handle_presence(self, presence: Presence) -> None:
"""Handle MUC presence"""
self.reset_lag()
@@ -610,7 +743,7 @@ class MucTab(ChatTab):
},
),
)
- asyncio.ensure_future(LogLoader(
+ asyncio.create_task(LogLoader(
logger, self, config.get_by_tabname('use_log', self.general_jid)
).tab_open())
@@ -662,6 +795,17 @@ class MucTab(ChatTab):
elif typ == 'unavailable':
self.on_user_leave_groupchat(user, jid, status, from_nick,
JID(from_room), server_initiated)
+ ns = 'http://jabber.org/protocol/muc#user'
+ if presence.xml.find(f'{{{ns}}}x/{{{ns}}}destroy') is not None:
+ info = f'Room {self.jid} was destroyed.'
+ if presence['muc']['destroy']:
+ reason = presence['muc']['destroy']['reason']
+ altroom = presence['muc']['destroy']['jid']
+ if reason:
+ info += f' “{reason}”.'
+ if altroom:
+ info += f' The new address now is {altroom}.'
+ self.core.information(info, 'Info')
# status change
else:
self.on_user_change_status(user, from_nick, from_room, affiliation,
@@ -1513,7 +1657,7 @@ class MucTab(ChatTab):
bookmark = self.core.bookmarks[self.jid]
if bookmark:
bookmark.autojoin = False
- asyncio.ensure_future(
+ asyncio.create_task(
self.core.bookmarks.save(self.core.xmpp)
)
self.core.close_tab(self)
@@ -1538,8 +1682,10 @@ class MucTab(ChatTab):
r = self.core.open_private_window(self.jid.bare, user.nick)
if r and len(args) == 2:
msg = args[1]
- r.command_say(
- xhtml.convert_simple_to_full_colors(msg)
+ asyncio.ensure_future(
+ r.command_say(
+ xhtml.convert_simple_to_full_colors(msg)
+ )
)
if not r:
self.core.information("Cannot find user: %s" % nick, 'Error')
@@ -1712,7 +1858,7 @@ class MucTab(ChatTab):
return None
@command_args_parser.raw
- def command_say(self, line: str, attention: bool = False, correct: bool = False):
+ async def command_say(self, line: str, attention: bool = False, correct: bool = False):
"""
/say <message>
Or normal input + enter
@@ -2198,7 +2344,7 @@ class MucTab(ChatTab):
'shortdesc':
'Fix a color for a nick.',
'completion':
- self.completion_recolor
+ self.completion_color
}, {
'name':
'cycle',
diff --git a/poezio/tabs/privatetab.py b/poezio/tabs/privatetab.py
index fb89d8e6..1909e3c1 100644
--- a/poezio/tabs/privatetab.py
+++ b/poezio/tabs/privatetab.py
@@ -10,6 +10,7 @@ both participant’s nicks. It also has slightly different features than
the ConversationTab (such as tab-completion on nicks from the room).
"""
+import asyncio
import curses
import logging
from datetime import datetime
@@ -47,7 +48,7 @@ class PrivateTab(OneToOneTab):
additional_information: Dict[str, Callable[[str], str]] = {}
def __init__(self, core, jid, nick, initial=None):
- OneToOneTab.__init__(self, core, jid)
+ OneToOneTab.__init__(self, core, jid, initial)
self.own_nick = nick
self.info_header = windows.PrivateInfoWin()
self.input = windows.MessageInput()
@@ -84,14 +85,14 @@ class PrivateTab(OneToOneTab):
return super().remote_user_color()
@property
- def general_jid(self):
+ def general_jid(self) -> JID:
return self.jid
- def get_dest_jid(self):
+ def get_dest_jid(self) -> JID:
return self.jid
@property
- def nick(self):
+ def nick(self) -> str:
return self.get_nick()
def ack_message(self, msg_id: str, msg_jid: JID):
@@ -141,7 +142,7 @@ class PrivateTab(OneToOneTab):
and not self.input.get_text().startswith('//'))
self.send_composing_chat_state(empty_after)
- def handle_message(self, message: SMessage, display: bool = True):
+ async def handle_message(self, message: SMessage, display: bool = True):
sent = message['from'].bare == self.core.xmpp.boundjid.bare
jid = message['to'] if sent else message['from']
with_nick = jid.resource
@@ -155,7 +156,7 @@ class PrivateTab(OneToOneTab):
)
tmp_dir = get_image_cache()
if not sent:
- self.core.events.trigger('private_msg', message, self)
+ await self.core.events.trigger_async('private_msg', message, self)
body = xhtml.get_body_from_message_stanza(
message, use_xhtml=use_xhtml, extract_images_to=tmp_dir)
if not body or not self:
@@ -201,9 +202,10 @@ class PrivateTab(OneToOneTab):
@refresh_wrapper.always
@command_args_parser.raw
- def command_say(self, line: str, attention: bool = False, correct: bool = False) -> None:
+ async def command_say(self, line: str, attention: bool = False, correct: bool = False) -> None:
if not self.on:
return
+ await self._initial_log.wait()
our_jid = JID(self.jid.bare)
our_jid.resource = self.own_nick
msg: SMessage = self.core.xmpp.make_message(
@@ -239,7 +241,7 @@ class PrivateTab(OneToOneTab):
if not msg['body']:
return
self.set_last_sent_message(msg, correct=correct)
- self.core.handler.on_groupchat_private_message(msg, sent=True)
+ await self.core.handler.on_groupchat_private_message(msg, sent=True)
# Our receipts slixmpp hack
msg._add_receipt = True # type: ignore
msg.send()
@@ -358,9 +360,6 @@ class PrivateTab(OneToOneTab):
1, self.width, self.height - 2 - self.core.information_win_size -
Tab.tab_win_height(), 0)
- def get_text_window(self):
- return self.text_win
-
@refresh_wrapper.conditional
def rename_user(self, old_nick, user):
"""
diff --git a/poezio/tabs/rostertab.py b/poezio/tabs/rostertab.py
index 66aff2b1..18334c20 100644
--- a/poezio/tabs/rostertab.py
+++ b/poezio/tabs/rostertab.py
@@ -14,7 +14,7 @@ import ssl
from functools import partial
from os import getenv, path
from pathlib import Path
-from typing import Dict, Callable
+from typing import Dict, Callable, Union
from slixmpp import JID, InvalidJID
from slixmpp.exceptions import IqError, IqTimeout
@@ -199,7 +199,7 @@ class RosterInfoTab(Tab):
completion=self.completion_cert_fetch)
@property
- def selected_row(self):
+ def selected_row(self) -> Union[Contact, Resource]:
return self.roster_win.get_selected_row()
@command_args_parser.ignored
diff --git a/poezio/tabs/xmltab.py b/poezio/tabs/xmltab.py
index 9501c6d3..939af67d 100644
--- a/poezio/tabs/xmltab.py
+++ b/poezio/tabs/xmltab.py
@@ -10,7 +10,6 @@ log = logging.getLogger(__name__)
import curses
import os
-from typing import Union, Optional
from slixmpp import JID, InvalidJID
from slixmpp.xmlstream import matcher, StanzaBase
from slixmpp.xmlstream.tostring import tostring