Exemple #1
0
    def mount_sidebar(self, executor):
        yield from asyncio.gather(
            loop.run_in_executor(executor, self.store.load_auth),
            loop.run_in_executor(executor, self.store.load_channels),
            loop.run_in_executor(executor, self.store.load_groups),
            loop.run_in_executor(executor, self.store.load_users))
        profile = Profile(name=self.store.state.auth['user'])
        channels = [
            Channel(id=channel['id'],
                    name=channel['name'],
                    is_private=channel['is_private'])
            for channel in self.store.state.channels
        ]
        dms = []
        max_users_sidebar = self.store.config['sidebar']['max_users']
        dm_users = self.store.state.dms[:max_users_sidebar]

        for dm in dm_users:
            user = self.store.find_user_by_id(dm['user'])
            if user:
                dms.append(
                    Dm(dm['id'],
                       name=user.get('display_name') or user.get('real_name')
                       or user['name'],
                       user=dm['user'],
                       you=user['id'] == self.store.state.auth['user_id']))

        self.sidebar = SideBar(profile,
                               channels,
                               dms,
                               title=self.store.state.auth['team'])
        urwid.connect_signal(self.sidebar, 'go_to_channel', self.go_to_channel)
        loop.create_task(self.get_channels_info(executor, channels))
        loop.create_task(self.get_presences(executor, dms))
Exemple #2
0
 def switch_to_workspace(self, workspace_number):
     self.sidebar = LoadingSideBar()
     self.chatbox = LoadingChatBox('And it becomes worse!')
     self._loading = True
     self.message_box = None
     self.store.switch_to_workspace(workspace_number)
     loop.create_task(self.animate_loading())
     loop.create_task(self.component_did_mount())
Exemple #3
0
class App:
    message_box = None

    def _exception_handler(self, loop, context):
        try:
            exception = context.get('exception')
            if not exception:
                raise Exception
            message = 'Whoops, something went wrong:\n\n' + str(exception) + '\n' + ''.join(traceback.format_tb(exception.__traceback__))
            self.chatbox = LoadingChatBox(message)
        except Exception as exc:
            self.chatbox = LoadingChatBox('Unable to show exception: ' + str(exc))
        return

    def __init__(self, config):
        self._loading = False
        self.config = config
        self.quick_switcher = None
        self.set_snooze_widget = None
        self.workspaces = list(config['workspaces'].items())
        self.store = Store(self.workspaces, self.config)
        Store.instance = self.store
        urwid.set_encoding('UTF-8')
        sidebar = LoadingSideBar()
        chatbox = LoadingChatBox('Everything is terrible!')
        palette = themes.get(config['theme'], themes['default'])

        custom_loop = SclackEventLoop(loop=loop)
        custom_loop.set_exception_handler(self._exception_handler)

        if len(self.workspaces) <= 1:
            self.workspaces_line = None
        else:
            self.workspaces_line = Workspaces(self.workspaces)

        self.columns = urwid.Columns([
            ('fixed', config['sidebar']['width'], urwid.AttrWrap(sidebar, 'sidebar')),
            urwid.AttrWrap(chatbox, 'chatbox')
        ])
        self._body = urwid.Frame(self.columns, header=self.workspaces_line)

        self.urwid_loop = urwid.MainLoop(
            self._body,
            palette=palette,
            event_loop=custom_loop,
            unhandled_input=self.unhandled_input
        )
        self.configure_screen(self.urwid_loop.screen)
        self.last_keypress = (0, None)

    @property
    def sidebar_column(self):
        return self.columns.contents[0]


    def start(self):
        self._loading = True
        loop.create_task(self.animate_loading())
        loop.create_task(self.component_did_mount())
        self.urwid_loop.run()

    def switch_to_workspace(self, workspace_number):
        if not self._loading:
            self._loading = True
            self.sidebar = LoadingSideBar()
            self.chatbox = LoadingChatBox('And it becomes worse!')
            self.message_box = None
            self.store.switch_to_workspace(workspace_number)
            loop.create_task(self.animate_loading())
            loop.create_task(self.component_did_mount())

    @property
    def is_chatbox_rendered(self):
        return not self._loading and self.chatbox and type(self.chatbox) is ChatBox

    @property
    def sidebar(self):
        return self.columns.contents[0][0].original_widget

    @sidebar.setter
    def sidebar(self, sidebar):
        self.columns.contents[0][0].original_widget = sidebar

    @property
    def chatbox(self):
        return self.columns.contents[1][0].original_widget

    @chatbox.setter
    def chatbox(self, chatbox):
        self.columns.contents[1][0].original_widget = chatbox

    @asyncio.coroutine
    def animate_loading(self):
        def update(*args):
            if self._loading:
                self.chatbox.circular_loading.next_frame()
                self.urwid_loop.set_alarm_in(0.2, update)
        update()

    @asyncio.coroutine
    def component_did_mount(self):
        with concurrent.futures.ThreadPoolExecutor(max_workers=20) as executor:
            yield from self.mount_sidebar(executor)
            yield from self.mount_chatbox(executor, self.store.state.channels[0]['id'])

    @asyncio.coroutine
    def mount_sidebar(self, executor):
        yield from asyncio.gather(
            loop.run_in_executor(executor, self.store.load_auth),
            loop.run_in_executor(executor, self.store.load_channels),
            loop.run_in_executor(executor, self.store.load_stars),
            loop.run_in_executor(executor, self.store.load_groups),
            loop.run_in_executor(executor, self.store.load_users),
            loop.run_in_executor(executor, self.store.load_user_dnd),
        )
        profile = Profile(name=self.store.state.auth['user'], is_snoozed=self.store.state.is_snoozed)

        channels = []
        dms = []
        stars = []
        star_user_tmp = []  # To contain user, channel should be on top of list
        stars_user_id = []  # To ignore item in DMs list
        stars_channel_id = []  # To ignore item in channels list
        max_users_sidebar = self.store.config['sidebar']['max_users']

        # Prepare list of Star users and channels
        for dm in self.store.state.stars:
            if is_dm(dm['channel']):
                detail = self.store.get_channel_info(dm['channel'])
                user = self.store.find_user_by_id(detail['user'])

                if user:
                    stars_user_id.append(user['id'])
                    star_user_tmp.append(Dm(
                        dm['channel'],
                        name=self.store.get_user_display_name(user),
                        user=user['id'],
                        you=False
                    ))
            elif is_channel(dm['channel']) or is_group(dm['channel']):
                channel = self.store.get_channel_info(dm['channel'])
                # Group chat (is_mpim) is not supported, prefer to https://github.com/haskellcamargo/sclack/issues/67
                if channel and not channel.get('is_archived', False) and not channel.get('is_mpim', False):
                    stars_channel_id.append(channel['id'])
                    stars.append(Channel(
                        id=channel['id'],
                        name=channel['name'],
                        is_private=channel.get('is_private', True)
                    ))
        stars.extend(star_user_tmp)

        # Prepare list of Channels
        for channel in self.store.state.channels:
            if channel['id'] in stars_channel_id:
                continue
            channels.append(Channel(
                id=channel['id'],
                name=channel['name'],
                is_private=channel['is_private']
            ))

        # Prepare list of DM
        dm_users = self.store.state.dms[:max_users_sidebar]
        for dm in dm_users:
            if dm['user'] in stars_user_id:
                continue
            user = self.store.find_user_by_id(dm['user'])
            if user:
                dms.append(Dm(
                    dm['id'],
                    name=self.store.get_user_display_name(user),
                    user=dm['user'],
                    you=user['id'] == self.store.state.auth['user_id']
                ))

        self.sidebar = SideBar(profile, channels, dms, stars=stars, title=self.store.state.auth['team'])
        urwid.connect_signal(self.sidebar, 'go_to_channel', self.go_to_channel)
        loop.create_task(self.get_channels_info(executor, self.sidebar.get_all_channels()))
        loop.create_task(self.get_presences(executor, self.sidebar.get_all_dms()))
        loop.create_task(self.get_dms_unread(executor, self.sidebar.get_all_dms()))

    @asyncio.coroutine
    def get_presences(self, executor, dm_widgets):
        """
        Compute and return presence because updating UI from another thread is unsafe
        :param executor:
        :param dm_widgets:
        :return:
        """
        def get_presence(dm_widget):
            presence = self.store.get_presence(dm_widget.user)
            return [dm_widget, presence]
        presences = yield from asyncio.gather(*[
            loop.run_in_executor(executor, get_presence, dm_widget)
            for dm_widget in dm_widgets
        ])

        for presence in presences:
            [widget, response] = presence
            if response['ok']:
                widget.set_presence(response['presence'])

    @asyncio.coroutine
    def get_dms_unread(self, executor, dm_widgets):
        """
        Compute and return unread_count_display because updating UI from another thread is unsafe
        :param executor:
        :param dm_widgets:
        :return:
        """
        def get_presence(dm_widget):
            profile_response = self.store.get_channel_info(dm_widget.id)
            return [dm_widget, profile_response]

        responses = yield from asyncio.gather(*[
            loop.run_in_executor(executor, get_presence, dm_widget)
            for dm_widget in dm_widgets
        ])

        for profile_response in responses:
            [widget, response] = profile_response
            if response is not None:
                widget.set_unread(response['unread_count_display'])

    @asyncio.coroutine
    def get_channels_info(self, executor, channels):
        def get_info(channel):
            info = self.store.get_channel_info(channel.id)
            return [channel, info]
        channels_info = yield from asyncio.gather(*[
            loop.run_in_executor(executor, get_info, channel)
            for channel in channels
        ])

        for channel_info in channels_info:
            [widget, response] = channel_info
            widget.set_unread(response.get('unread_count_display', 0))

    @asyncio.coroutine
    def update_chat(self, event):
        """
        Update channel/DM message count badge
        :param event:
        :return:
        """
        self.sidebar.update_items(event)

    @asyncio.coroutine
    def mount_chatbox(self, executor, channel):
        yield from asyncio.gather(
            loop.run_in_executor(executor, self.store.load_channel, channel),
            loop.run_in_executor(executor, self.store.load_messages, channel)
        )
        messages = self.render_messages(self.store.state.messages)
        header = self.render_chatbox_header()
        self._loading = False
        self.sidebar.select_channel(channel)
        self.message_box = MessageBox(
            user=self.store.state.auth['user'],
            is_read_only=self.store.state.channel.get('is_read_only', False)
        )
        self.chatbox = ChatBox(messages, header, self.message_box, self.urwid_loop)
        urwid.connect_signal(self.chatbox, 'set_insert_mode', self.set_insert_mode)
        urwid.connect_signal(self.chatbox, 'mark_read', self.handle_mark_read)
        urwid.connect_signal(self.chatbox, 'open_quick_switcher', self.open_quick_switcher)
        urwid.connect_signal(self.chatbox, 'open_set_snooze', self.open_set_snooze)

        urwid.connect_signal(self.message_box.prompt_widget, 'submit_message', self.submit_message)
        urwid.connect_signal(self.message_box.prompt_widget, 'go_to_last_message', self.go_to_last_message)

        self.real_time_task = loop.create_task(self.start_real_time())

    def edit_message(self, widget, user_id, ts, original_text):
        is_logged_user = self.store.state.auth['user_id'] == user_id
        current_date = datetime.today()
        message_date = datetime.fromtimestamp(float(ts))
        # Only messages sent in the last 5 minutes can be edited
        if is_logged_user and (current_date - message_date).total_seconds() < 60 * 5:
            self.store.state.editing_widget = widget
            self.set_insert_mode()
            self.chatbox.message_box.text = original_text
            widget.set_edit_mode()

    def get_permalink(self, widget, channel_id, ts):
        try:
            permalink = self.store.get_permalink(channel_id, ts)
            if permalink and permalink.get('permalink'):
                text = permalink.get('permalink')
                self.set_insert_mode()
                self.chatbox.message_box.text = text
        except:
            pass

    def delete_message(self, widget, user_id, ts):
        if self.store.state.auth['user_id'] == user_id:
            if self.store.delete_message(self.store.state.channel['id'], ts)['ok']:
                self.chatbox.body.body.remove(widget)

    def go_to_profile(self, user_id):
        if len(self.columns.contents) > 2:
            self.columns.contents.pop()
        if user_id == self.store.state.profile_user_id:
            self.store.state.profile_user_id = None
        else:
            user = self.store.find_user_by_id(user_id)
            if not user:
                return
            self.store.state.profile_user_id = user_id
            profile = ProfileSideBar(
                user.get('display_name') or user.get('real_name') or user['name'],
                user['profile'].get('status_text', None),
                user['profile'].get('tz_label', None),
                user['profile'].get('phone', None),
                user['profile'].get('email', None),
                user['profile'].get('skype', None)
            )
            if self.config['features']['pictures']:
                loop.create_task(self.load_profile_avatar(user['profile'].get('image_512'), profile))
            self.columns.contents.append((profile, ('given', 35, False)))

    def render_chatbox_header(self):

        if self.store.state.channel['id'][0] == 'D':
            user = self.store.find_user_by_id(self.store.state.channel['user'])
            header = ChannelHeader(
                name=user.get('display_name') or user.get('real_name') or user['name'],
                topic=user['profile']['status_text'],
                is_starred=self.store.state.channel.get('is_starred', False),
                is_dm_workaround_please_remove_me=True
            )
        else:
            are_more_members = False
            if self.store.state.members.get('response_metadata', None):
                if self.store.state.members['response_metadata'].get('next_cursor', None):
                    are_more_members = True
            header = ChannelHeader(
                name=self.store.state.channel['name'],
                topic=self.store.state.channel['topic']['value'],
                num_members=len(self.store.state.members['members']),
                more_members=are_more_members,
                pin_count=self.store.state.pin_count,
                is_private=self.store.state.channel.get('is_group', False),
                is_starred=self.store.state.channel.get('is_starred', False)
            )
            urwid.connect_signal(header.topic_widget, 'done', self.on_change_topic)
        return header

    def on_change_topic(self, text):
        self.chatbox.header.original_topic = text
        self.store.set_topic(self.store.state.channel['id'], text)
        self.go_to_sidebar()

    def render_message(self, message, channel_id=None):
        is_app = False
        subtype = message.get('subtype')

        if subtype == SCLACK_SUBTYPE:
            message = Message(
                message['ts'],
                '',
                User('1', 'sclack'),
                MarkdownText(message['text']),
                Indicators(False, False)
            )
            urwid.connect_signal(message, 'go_to_sidebar', self.go_to_sidebar)
            urwid.connect_signal(message, 'quit_application', self.quit_application)
            urwid.connect_signal(message, 'set_insert_mode', self.set_insert_mode)
            urwid.connect_signal(message, 'mark_read', self.handle_mark_read)

            return message

        message_text = message['text']
        files = message.get('files', [])

        # Files uploaded
        if len(files) > 0:
            file_links = ['"{}" <{}>'.format(file.get('title'), file.get('url_private')) for file in message.get('files')]
            file_upload_text = 'File{} uploaded'.format('' if len(files) == 1 else 's')
            file_text = '{} {}'.format(file_upload_text ,', '.join(file_links))

            if message_text == '':
                message_text = file_text
            else:
                message_text = '{}\n{}'.format(message_text, file_text)

        if subtype == 'bot_message':
            bot = (self.store.find_user_by_id(message['bot_id'])
                or self.store.find_or_load_bot(message['bot_id']))
            if bot:
                user_id = message['bot_id']
                user_name = bot.get('profile', {}).get('display_name') or bot.get('name')
                color = bot.get('color')
                is_app = 'app_id' in bot
            else:
                return None
        elif subtype == 'file_comment':
            user = self.store.find_user_by_id(message['comment']['user'])

            # A temporary fix for a null pointer exception for truncated or deleted users
            if user is None:
                return None

            user_id = user['id']
            user_name = user['profile']['display_name'] or user.get('name')
            color = user.get('color')
            if message.get('file'):
                message['file'] = None
        else:
            user = self.store.find_user_by_id(message['user'])

            # A temporary fix for a null pointer exception for truncated or deleted users
            if user is None:
                return None

            user_id = user['id']
            user_name = user['profile']['display_name'] or user.get('name')
            color = user.get('color')

        user = User(user_id, user_name, color, is_app)
        text = MarkdownText(message_text)
        indicators = Indicators('edited' in message, message.get('is_starred', False))
        reactions = [
            Reaction(reaction['name'], reaction['count'])
            for reaction in message.get('reactions', [])
        ]

        attachments = []
        for attachment in message.get('attachments', []):
            attachment_widget = Attachment(
                service_name=attachment.get('service_name'),
                title=attachment.get('title'),
                from_url=attachment.get('from_url'),
                fields=attachment.get('fields'),
                color=attachment.get('color'),
                author_name=attachment.get('author_name') or attachment.get('author_subname'),
                pretext=attachment.get('pretext'),
                text=message_text,
                attachment_text=attachment.get('text'),
                ts=attachment.get('ts'),
                footer=attachment.get('footer')
            )
            image_url = attachment.get('image_url')
            if image_url and self.config['features']['pictures']:
                loop.create_task(self.load_picture_async(
                    image_url,
                    attachment.get('image_width', 500),
                    attachment_widget,
                    auth=False
                ))
            attachments.append(attachment_widget)

        file = message.get('file')

        if file:
            files.append(file)

        message_channel = channel_id if channel_id is not None else message.get('channel')

        message = Message(
            message['ts'],
            message_channel,
            user,
            text,
            indicators,
            attachments=attachments,
            reactions=reactions
        )

        self.lazy_load_images(files, message)

        urwid.connect_signal(message, 'edit_message', self.edit_message)
        urwid.connect_signal(message, 'get_permalink', self.get_permalink)
        urwid.connect_signal(message, 'go_to_profile', self.go_to_profile)
        urwid.connect_signal(message, 'go_to_sidebar', self.go_to_sidebar)
        urwid.connect_signal(message, 'delete_message', self.delete_message)
        urwid.connect_signal(message, 'quit_application', self.quit_application)
        urwid.connect_signal(message, 'set_insert_mode', self.set_insert_mode)
        urwid.connect_signal(message, 'mark_read', self.handle_mark_read)

        return message

    def lazy_load_images(self, files, widget):
        """
        Load images lazily and attache to widget
        :param files:
        :param widget:
        :return:
        """
        if not self.config['features']['pictures']:
            return

        allowed_file_types = ('bmp', 'gif', 'jpeg', 'jpg', 'png')

        for file in files:
            if file.get('filetype') in allowed_file_types:
                loop.create_task(self.load_picture_async(
                    file['url_private'],
                    file.get('original_w', 500),
                    widget,
                    not file.get('is_external', True)
                ))

    def render_messages(self, messages, channel_id=None):
        _messages = []
        previous_date = self.store.state.last_date
        last_read_datetime = datetime.fromtimestamp(float(self.store.state.channel.get('last_read', '0')))
        today = datetime.today().date()
        for message in messages:
            message_datetime = datetime.fromtimestamp(float(message['ts']))
            message_date = message_datetime.date()
            date_text = None
            unread_text = None
            if not previous_date or previous_date != message_date:
                previous_date = message_date
                self.store.state.last_date = previous_date
                if message_date == today:
                    date_text = 'Today'
                else:
                    date_text = message_date.strftime('%A, %B %d')

            # New messages badge
            if (message_datetime > last_read_datetime and not self.store.state.did_render_new_messages
                and (self.store.state.channel.get('unread_count_display', 0) > 0)):
                self.store.state.did_render_new_messages = True
                unread_text = 'new messages'
            if unread_text is not None:
                _messages.append(NewMessagesDivider(unread_text, date=date_text))
            elif date_text is not None:
                _messages.append(TextDivider(('history_date', date_text), 'center'))

            message = self.render_message(message, channel_id)

            if message is not None:
                _messages.append(message)

        return _messages

    def handle_mark_read(self, data):
        """
        Mark as read to bottom
        :return:
        """
        row_index = data if data is not None else -1

        def read(*kwargs):
            loop.create_task(
                self.mark_read_slack(row_index)
            )

        now = time.time()
        if now - self.last_keypress[0] < MARK_READ_ALARM_PERIOD and self.last_keypress[1] is not None:
            self.urwid_loop.remove_alarm(self.last_keypress[1])

        self.last_keypress = (now, self.urwid_loop.set_alarm_in(MARK_READ_ALARM_PERIOD, read))

    def scroll_messages(self, *args):
        index = self.chatbox.body.scroll_to_new_messages()
        loop.create_task(
            self.mark_read_slack(index)
        )

    @asyncio.coroutine
    def mark_read_slack(self, index):
        if not self.is_chatbox_rendered:
            return

        if index is None or index == -1:
            index = len(self.chatbox.body.body) - 1

        if len(self.chatbox.body.body) > index:
            message = self.chatbox.body.body[index]

            # Only apply for message
            if not hasattr(message, 'channel_id'):
                if len(self.chatbox.body.body) > index + 1:
                    message = self.chatbox.body.body[index + 1]
                else:
                    message = self.chatbox.body.body[index - 1]

            if message.channel_id:
                self.store.mark_read(message.channel_id, message.ts)

    @asyncio.coroutine
    def _go_to_channel(self, channel_id):
        with concurrent.futures.ThreadPoolExecutor(max_workers=20) as executor:
            yield from asyncio.gather(
                loop.run_in_executor(executor, self.store.load_channel, channel_id),
                loop.run_in_executor(executor, self.store.load_messages, channel_id)
            )
            self.store.state.last_date = None

            if len(self.store.state.messages) == 0:
                messages = self.render_messages([{
                    'text': "There's no conversation in this channel",
                    'ts': '0',
                    'subtype': SCLACK_SUBTYPE,
                }])
            else:
                messages = self.render_messages(self.store.state.messages, channel_id=channel_id)

            header = self.render_chatbox_header()
            if self.is_chatbox_rendered:
                self.chatbox.body.body[:] = messages
                self.chatbox.header = header
                self.chatbox.message_box.is_read_only = self.store.state.channel.get('is_read_only', False)
                self.sidebar.select_channel(channel_id)
                self.urwid_loop.set_alarm_in(0, self.scroll_messages)

            if len(self.store.state.messages) == 0:
                self.go_to_sidebar()
            else:
                self.go_to_chatbox()

    def go_to_channel(self, channel_id):
        if self.quick_switcher:
            urwid.disconnect_signal(self.quick_switcher, 'go_to_channel', self.go_to_channel)
            self.urwid_loop.widget = self._body
            self.quick_switcher = None
        loop.create_task(self._go_to_channel(channel_id))

    def handle_set_snooze_time(self, snoozed_time):
        loop.create_task(self.dispatch_snooze_time(snoozed_time))

    def handle_close_set_snooze(self):
        """
        Close set_snooze
        :return:
        """
        if self.set_snooze_widget:
            urwid.disconnect_signal(self.set_snooze_widget, 'set_snooze_time', self.handle_set_snooze_time)
            urwid.disconnect_signal(self.set_snooze_widget, 'close_set_snooze', self.handle_close_set_snooze)
            self.urwid_loop.widget = self._body
            self.set_snooze_widget = None

    @asyncio.coroutine
    def dispatch_snooze_time(self, snoozed_time):
        self.store.set_snooze(snoozed_time)

    @asyncio.coroutine
    def load_picture_async(self, url, width, message_widget, auth=True):
        width = min(width, 800)
        bytes_in_cache = self.store.cache.picture.get(url)
        if bytes_in_cache:
            message_widget.file = bytes_in_cache
            return
        with concurrent.futures.ThreadPoolExecutor(max_workers=20) as executor:
            headers = {}
            if auth:
                headers = {'Authorization': 'Bearer {}'.format(self.store.slack_token)}
            bytes = yield from loop.run_in_executor(
                executor,
                functools.partial(requests.get, url, headers=headers)
            )
            file = tempfile.NamedTemporaryFile(delete=False)
            file.write(bytes.content)
            file.close()
            picture = Image(file.name, width=(width / 10))
            message_widget.file = picture

    @asyncio.coroutine
    def load_profile_avatar(self, url, profile):
        bytes_in_cache = self.store.cache.avatar.get(url)
        if bytes_in_cache:
            profile.avatar = bytes_in_cache
            return
        with concurrent.futures.ThreadPoolExecutor(max_workers=20) as executor:
            bytes = yield from loop.run_in_executor(executor, requests.get, url)
            file = tempfile.NamedTemporaryFile(delete=False)
            file.write(bytes.content)
            file.close()
            avatar = Image(file.name, width=35)
            self.store.cache.avatar[url] = avatar
            profile.avatar = avatar

    @asyncio.coroutine
    def start_real_time(self):
        self.store.slack.rtm_connect(auto_reconnect=True)

        def stop_typing(*args):
            # Prevent error while switching workspace
            if self.is_chatbox_rendered:
                self.chatbox.message_box.typing = None

        alarm = None

        while self.store.slack.server.connected is True:
            events = self.store.slack.rtm_read()

            for event in events:
                if event.get('type') == 'hello':
                    pass
                elif event.get('type') in ('channel_marked', 'group_marked', 'im_marked'):
                    unread = event.get('unread_count_display', 0)

                    if event.get('type') == 'channel_marked':
                        targets = self.sidebar.get_all_channels()
                    elif event.get('type') == 'group_marked':
                        targets = self.sidebar.get_all_groups()
                    else:
                        targets = self.sidebar.get_all_dms()

                    for target in targets:
                        if target.id == event['channel']:
                            target.set_unread(unread)

                elif event['type'] == 'message':
                    loop.create_task(
                        self.update_chat(event)
                    )

                    if event.get('channel') == self.store.state.channel['id']:
                        if not self.is_chatbox_rendered:
                            return

                        if event.get('subtype') == 'message_deleted':
                            for widget in self.chatbox.body.body:
                                if hasattr(widget, 'ts') and getattr(widget, 'ts') == event['deleted_ts']:
                                    self.chatbox.body.body.remove(widget)
                                    break
                        elif event.get('subtype') == 'message_changed':
                            for index, widget in enumerate(self.chatbox.body.body):
                                if hasattr(widget, 'ts') and getattr(widget, 'ts') == event['message']['ts']:
                                    self.chatbox.body.body[index] = self.render_message(event['message'])
                                    break
                        else:
                            self.chatbox.body.body.extend(self.render_messages([event]))
                            self.chatbox.body.scroll_to_bottom()
                    else:
                        pass
                elif event['type'] == 'user_typing':
                    if not self.is_chatbox_rendered:
                        return

                    if event.get('channel') == self.store.state.channel['id']:
                        user = self.store.find_user_by_id(event['user'])
                        name = user.get('display_name') or user.get('real_name') or user['name']
                        if alarm is not None:
                            self.urwid_loop.remove_alarm(alarm)
                        self.chatbox.message_box.typing = name
                        self.urwid_loop.set_alarm_in(3, stop_typing)
                    else:
                        pass
                        # print(json.dumps(event, indent=2))
                elif event.get('type') == 'dnd_updated' and 'dnd_status' in event:
                    self.store.is_snoozed = event['dnd_status']['snooze_enabled']
                    self.sidebar.profile.set_snooze(self.store.is_snoozed)
                elif event.get('ok', False):
                    if not self.is_chatbox_rendered:
                        return

                    # Message was sent, Slack confirmed it.
                    self.chatbox.body.body.extend(self.render_messages([{
                        'text': event['text'],
                        'ts': event['ts'],
                        'user': self.store.state.auth['user_id']
                    }]))
                    self.chatbox.body.scroll_to_bottom()
                    self.handle_mark_read(-1)
                else:
                    pass
                    # print(json.dumps(event, indent=2))
            yield from asyncio.sleep(0.5)

    def set_insert_mode(self):
        self.columns.focus_position = 1
        self.chatbox.focus_position = 'footer'
        self.message_box.focus_position = 1

    def set_edit_topic_mode(self):
        self.columns.focus_position = 1
        self.chatbox.focus_position = 'header'
        self.chatbox.header.go_to_end_of_topic()

    def go_to_chatbox(self):
        self.columns.focus_position = 1
        self.chatbox.focus_position = 'body'

    def leave_edit_mode(self):
        if self.store.state.editing_widget:
            self.store.state.editing_widget.unset_edit_mode()
            self.store.state.editing_widget = None
        self.chatbox.message_box.text = ''

    def go_to_sidebar(self):
        if len(self.columns.contents) > 2:
            self.columns.contents.pop()
        self.columns.focus_position = 0

        if self.store.state.editing_widget:
            self.leave_edit_mode()

        if self.quick_switcher:
            urwid.disconnect_signal(self.quick_switcher, 'go_to_channel', self.go_to_channel)
            self.urwid_loop.widget = self._body
            self.quick_switcher = None

    def submit_message(self, message):
        if self.store.state.editing_widget:
            channel = self.store.state.channel['id']
            ts = self.store.state.editing_widget.ts
            edit_result = self.store.edit_message(channel, ts, message)
            if edit_result['ok']:
                self.store.state.editing_widget.original_text = edit_result['text']
                self.store.state.editing_widget.set_text(MarkdownText(edit_result['text']))
            self.leave_edit_mode()
        else:
            channel = self.store.state.channel['id']
            if message.strip() != '':
                self.store.post_message(channel, message)
                self.leave_edit_mode()

    def go_to_last_message(self):
        self.go_to_chatbox()
        self.chatbox.body.go_to_last_message()

    @property
    def sidebar_width(self):
        return self.sidebar_column[1][1]

    def set_sidebar_width(self, newwidth):
        column, options = self.sidebar_column
        new_options = (options[0], newwidth, options[2])
        self.columns.contents[0] = (column, new_options)

    def hide_sidebar(self):
        self.set_sidebar_width(0)

    def show_sidebar(self):
        self.set_sidebar_width(self.config['sidebar']['width'])

    def toggle_sidebar(self):
        if self.sidebar_width > 0:
            self.hide_sidebar()
        else:
            self.show_sidebar()

    def unhandled_input(self, key):
        """
        Handle shortcut key press
        :param key:
        :return:
        """
        keymap = self.store.config['keymap']

        if key == keymap['go_to_chatbox'] or key == keymap['cursor_right'] and self.message_box:
            return self.go_to_chatbox()
        elif key == keymap['go_to_sidebar']:
            return self.go_to_sidebar()
        elif key == keymap['quit_application']:
            return self.quit_application()
        elif key == keymap['set_edit_topic_mode'] and self.message_box and not self.store.state.channel['id'][0] == 'D':
            return self.set_edit_topic_mode()
        elif key == keymap['set_insert_mode'] and self.message_box:
            return self.set_insert_mode()
        elif key == keymap['open_quick_switcher']:
            return self.open_quick_switcher()
        elif key in ('1', '2', '3', '4', '5', '6', '7', '8', '9') and len(self.workspaces) >= int(key):
            # Loading or only 1 workspace
            if self._loading or self.workspaces_line is None:
                return

            # Workspace is selected
            selected_workspace = int(key)
            if selected_workspace - 1 == self.workspaces_line.selected:
                return
            self.workspaces_line.select(selected_workspace)

            # Stop rtm to switch workspace
            self.real_time_task.cancel()
            return self.switch_to_workspace(selected_workspace)
        elif key == keymap['set_snooze']:
            return self.open_set_snooze()
        elif key == keymap['toggle_sidebar']:
            return self.toggle_sidebar()

    def open_quick_switcher(self):
        if not self.quick_switcher:
            self.quick_switcher = QuickSwitcher(self.urwid_loop.widget, self.urwid_loop)
            urwid.connect_signal(self.quick_switcher, 'go_to_channel', self.go_to_channel)
            self.urwid_loop.widget = self.quick_switcher

    def open_set_snooze(self):
        if not self.set_snooze_widget:
            self.set_snooze_widget = SetSnoozeWidget(self.urwid_loop.widget, self.urwid_loop)
            urwid.connect_signal(self.set_snooze_widget, 'set_snooze_time', self.handle_set_snooze_time)
            urwid.connect_signal(self.set_snooze_widget, 'close_set_snooze', self.handle_close_set_snooze)
            self.urwid_loop.widget = self.set_snooze_widget

    def configure_screen(self, screen):
        screen.set_terminal_properties(colors=self.store.config['colors'])
        screen.set_mouse_tracking()
        if self.workspaces_line is not None:
            urwid.connect_signal(self.workspaces_line, 'switch_workspace', self.switch_to_workspace)

    def quit_application(self):
        self.urwid_loop.stop()
        if hasattr(self, 'real_time_task'):
            self.real_time_task.cancel()
        sys.exit()
Exemple #4
0
    def mount_sidebar(self, executor):
        yield from asyncio.gather(
            loop.run_in_executor(executor, self.store.load_auth),
            loop.run_in_executor(executor, self.store.load_channels),
            loop.run_in_executor(executor, self.store.load_stars),
            loop.run_in_executor(executor, self.store.load_groups),
            loop.run_in_executor(executor, self.store.load_users),
            loop.run_in_executor(executor, self.store.load_user_dnd),
        )
        profile = Profile(name=self.store.state.auth['user'], is_snoozed=self.store.state.is_snoozed)

        channels = []
        dms = []
        stars = []
        star_user_tmp = []  # To contain user, channel should be on top of list
        stars_user_id = []  # To ignore item in DMs list
        stars_channel_id = []  # To ignore item in channels list
        max_users_sidebar = self.store.config['sidebar']['max_users']

        # Prepare list of Star users and channels
        for dm in self.store.state.stars:
            if is_dm(dm['channel']):
                detail = self.store.get_channel_info(dm['channel'])
                user = self.store.find_user_by_id(detail['user'])

                if user:
                    stars_user_id.append(user['id'])
                    star_user_tmp.append(Dm(
                        dm['channel'],
                        name=self.store.get_user_display_name(user),
                        user=user['id'],
                        you=False
                    ))
            elif is_channel(dm['channel']) or is_group(dm['channel']):
                channel = self.store.get_channel_info(dm['channel'])
                # Group chat (is_mpim) is not supported, prefer to https://github.com/haskellcamargo/sclack/issues/67
                if channel and not channel.get('is_archived', False) and not channel.get('is_mpim', False):
                    stars_channel_id.append(channel['id'])
                    stars.append(Channel(
                        id=channel['id'],
                        name=channel['name'],
                        is_private=channel.get('is_private', True)
                    ))
        stars.extend(star_user_tmp)

        # Prepare list of Channels
        for channel in self.store.state.channels:
            if channel['id'] in stars_channel_id:
                continue
            channels.append(Channel(
                id=channel['id'],
                name=channel['name'],
                is_private=channel['is_private']
            ))

        # Prepare list of DM
        dm_users = self.store.state.dms[:max_users_sidebar]
        for dm in dm_users:
            if dm['user'] in stars_user_id:
                continue
            user = self.store.find_user_by_id(dm['user'])
            if user:
                dms.append(Dm(
                    dm['id'],
                    name=self.store.get_user_display_name(user),
                    user=dm['user'],
                    you=user['id'] == self.store.state.auth['user_id']
                ))

        self.sidebar = SideBar(profile, channels, dms, stars=stars, title=self.store.state.auth['team'])
        urwid.connect_signal(self.sidebar, 'go_to_channel', self.go_to_channel)
        loop.create_task(self.get_channels_info(executor, self.sidebar.get_all_channels()))
        loop.create_task(self.get_presences(executor, self.sidebar.get_all_dms()))
        loop.create_task(self.get_dms_unread(executor, self.sidebar.get_all_dms()))
Exemple #5
0
class App:
    message_box = None

    def __init__(self, config):
        self.config = config
        self.store = Store(config['token'], self.config)
        Store.instance = self.store
        urwid.set_encoding('UTF-8')
        sidebar = LoadingSideBar()
        chatbox = LoadingChatBox('Everything is terrible!')
        palette = themes.get(config['theme'], themes['default'])
        self.columns = urwid.Columns([('fixed', config['sidebar']['width'],
                                       urwid.AttrWrap(sidebar, 'sidebar')),
                                      urwid.AttrWrap(chatbox, 'chatbox')])
        self.urwid_loop = urwid.MainLoop(
            urwid.Frame(self.columns),
            palette=palette,
            event_loop=urwid.AsyncioEventLoop(loop=loop),
            unhandled_input=self.unhandled_input)
        self.configure_screen(self.urwid_loop.screen)

    def start(self):
        self._loading = True
        loop.create_task(self.animate_loading())
        loop.create_task(self.component_did_mount())
        self.urwid_loop.run()

    @property
    def sidebar(self):
        return self.columns.contents[0][0].original_widget

    @sidebar.setter
    def sidebar(self, sidebar):
        self.columns.contents[0][0].original_widget = sidebar

    @property
    def chatbox(self):
        return self.columns.contents[1][0].original_widget

    @chatbox.setter
    def chatbox(self, chatbox):
        self.columns.contents[1][0].original_widget = chatbox

    @asyncio.coroutine
    def animate_loading(self):
        def update(*args):
            if self._loading:
                self.chatbox.circular_loading.next_frame()
                self.urwid_loop.set_alarm_in(0.2, update)

        update()

    @asyncio.coroutine
    def component_did_mount(self):
        with concurrent.futures.ThreadPoolExecutor(max_workers=20) as executor:
            yield from self.mount_sidebar(executor)
            yield from self.mount_chatbox(executor,
                                          self.store.state.channels[0]['id'])

    @asyncio.coroutine
    def mount_sidebar(self, executor):
        yield from asyncio.gather(
            loop.run_in_executor(executor, self.store.load_auth),
            loop.run_in_executor(executor, self.store.load_channels),
            loop.run_in_executor(executor, self.store.load_groups),
            loop.run_in_executor(executor, self.store.load_users))
        profile = Profile(name=self.store.state.auth['user'])
        channels = [
            Channel(id=channel['id'],
                    name=channel['name'],
                    is_private=channel['is_private'])
            for channel in self.store.state.channels
        ]
        dms = []
        dm_users = self.store.state.dms[:15]
        for dm in dm_users:
            user = self.store.find_user_by_id(dm['user'])
            if user:
                dms.append(
                    Dm(dm['id'],
                       name=user.get('real_name', user['name']),
                       user=dm['user'],
                       you=user['id'] == self.store.state.auth['user_id']))
        self.sidebar = SideBar(profile,
                               channels,
                               dms,
                               title=self.store.state.auth['team'])
        urwid.connect_signal(self.sidebar, 'go_to_channel', self.go_to_channel)
        loop.create_task(self.get_channels_info(executor, channels))
        loop.create_task(self.get_presences(executor, dms))

    @asyncio.coroutine
    def get_presences(self, executor, dm_widgets):
        def get_presence(dm_widget):
            # Compute and return presence because updating UI from another thread is unsafe
            presence = self.store.get_presence(dm_widget.user)
            return [dm_widget, presence]

        presences = yield from asyncio.gather(*[
            loop.run_in_executor(executor, get_presence, dm_widget)
            for dm_widget in dm_widgets
        ])
        for presence in presences:
            [widget, response] = presence
            if response['ok']:
                widget.set_presence(response['presence'])

    @asyncio.coroutine
    def get_channels_info(self, executor, channels):
        def get_info(channel):
            info = self.store.get_channel_info(channel.id)
            return [channel, info]

        channels_info = yield from asyncio.gather(*[
            loop.run_in_executor(executor, get_info, channel)
            for channel in channels
        ])
        for channel_info in channels_info:
            [widget, response] = channel_info
            widget.set_unread(response.get('unread_count_display', 0))

    @asyncio.coroutine
    def mount_chatbox(self, executor, channel):
        yield from asyncio.gather(
            loop.run_in_executor(executor, self.store.load_channel, channel),
            loop.run_in_executor(executor, self.store.load_messages, channel))
        messages = self.render_messages(self.store.state.messages)
        header = self.render_chatbox_header()
        self._loading = False
        self.sidebar.select_channel(channel)
        self.message_box = MessageBox(
            user=self.store.state.auth['user'],
            is_read_only=self.store.state.channel.get('is_read_only', False))
        self.chatbox = ChatBox(messages, header, self.message_box)
        urwid.connect_signal(self.chatbox, 'go_to_sidebar', self.go_to_sidebar)
        urwid.connect_signal(self.chatbox, 'quit_application',
                             self.quit_application)
        urwid.connect_signal(self.message_box.prompt_widget, 'submit_message',
                             self.submit_message)
        self.real_time_task = loop.create_task(self.start_real_time())

    def edit_message(self, widget, user_id, ts, original_text):
        is_logged_user = self.store.state.auth['user_id'] == user_id
        current_date = datetime.today()
        message_date = datetime.fromtimestamp(float(ts))
        # Only messages sent in the last 5 minutes can be edited
        if is_logged_user and (current_date -
                               message_date).total_seconds() < 60 * 5:
            self.store.state.editing_widget = widget
            self.set_insert_mode()
            self.chatbox.message_box.text = original_text
            widget.set_edit_mode()

    def delete_message(self, widget, user_id, ts):
        if self.store.state.auth['user_id'] == user_id:
            if self.store.delete_message(self.store.state.channel['id'],
                                         ts)['ok']:
                self.chatbox.body.body.remove(widget)

    def go_to_profile(self, user_id):
        if len(self.columns.contents) > 2:
            self.columns.contents.pop()
        if user_id == self.store.state.profile_user_id:
            self.store.state.profile_user_id = None
        else:
            user = self.store.find_user_by_id(user_id)
            if not user:
                return
            self.store.state.profile_user_id = user_id
            profile = ProfileSideBar(user.get('real_name', user['name']),
                                     user['profile'].get('status_text', None),
                                     user['profile'].get('tz_label', None),
                                     user['profile'].get('phone', None),
                                     user['profile'].get('email', None),
                                     user['profile'].get('skype', None))
            if self.config['features']['pictures']:
                loop.create_task(
                    self.load_profile_avatar(user['profile'].get('image_512'),
                                             profile))
            self.columns.contents.append((profile, ('given', 35, False)))

    def render_chatbox_header(self):

        if self.store.state.channel['id'][0] == 'D':
            user = self.store.find_user_by_id(self.store.state.channel['user'])
            header = ChannelHeader(name=user.get('real_name', user['name']),
                                   topic=user['profile']['status_text'],
                                   is_starred=self.store.state.channel.get(
                                       'is_starred', False),
                                   is_dm_workaround_please_remove_me=True)
        else:
            header = ChannelHeader(
                name=self.store.state.channel['name'],
                topic=self.store.state.channel['topic']['value'],
                num_members=len(self.store.state.channel['members']),
                pin_count=self.store.state.pin_count,
                is_private=self.store.state.channel.get('is_group', False),
                is_starred=self.store.state.channel.get('is_starred', False))
            urwid.connect_signal(header.topic_widget, 'done',
                                 self.on_change_topic)
        return header

    def on_change_topic(self, text):
        self.chatbox.header.original_topic = text
        self.store.set_topic(self.store.state.channel['id'], text)
        self.go_to_sidebar()

    def render_message(self, message):
        is_app = False
        subtype = message.get('subtype')
        if subtype == 'bot_message':
            bot = (self.store.find_user_by_id(message['bot_id'])
                   or self.store.find_or_load_bot(message['bot_id']))
            if bot:
                user_id = message['bot_id']
                user_name = bot.get('profile',
                                    {}).get('display_name') or bot.get('name')
                color = bot.get('color')
                is_app = 'app_id' in bot
            else:
                return None
        elif subtype == 'file_comment':
            user = self.store.find_user_by_id(message['comment']['user'])
            user_id = user['id']
            user_name = user['profile']['display_name'] or user.get('name')
            color = user.get('color')
            if message.get('file'):
                message['file'] = None
        else:
            user = self.store.find_user_by_id(message['user'])
            user_id = user['id']
            user_name = user['profile']['display_name'] or user.get('name')
            color = user.get('color')

        user = User(user_id, user_name, color, is_app)
        text = MarkdownText(message['text'])
        indicators = Indicators('edited' in message,
                                message.get('is_starred', False))
        reactions = [
            Reaction(reaction['name'], reaction['count'])
            for reaction in message.get('reactions', [])
        ]
        file = message.get('file')
        attachments = []
        for attachment in message.get('attachments', []):
            attachment_widget = Attachment(
                service_name=attachment.get('service_name'),
                title=attachment.get('title'),
                fields=attachment.get('fields'),
                color=attachment.get('color'),
                author_name=attachment.get('author_name'),
                pretext=attachment.get('pretext'),
                text=attachment.get('text'),
                footer=attachment.get('footer'))
            image_url = attachment.get('image_url')
            if image_url and self.config['features']['pictures']:
                loop.create_task(
                    self.load_picture_async(image_url,
                                            attachment.get('image_width', 500),
                                            attachment_widget,
                                            auth=False))
            attachments.append(attachment_widget)
        message = Message(message['ts'],
                          user,
                          text,
                          indicators,
                          attachments=attachments,
                          reactions=reactions)
        if (file and file.get('filetype')
                in ('bmp', 'gif', 'jpeg', 'jpg', 'png')
                and self.config['features']['pictures']):
            loop.create_task(
                self.load_picture_async(file['url_private'],
                                        file.get('original_w', 500), message))
        urwid.connect_signal(message, 'edit_message', self.edit_message)
        urwid.connect_signal(message, 'go_to_profile', self.go_to_profile)
        urwid.connect_signal(message, 'delete_message', self.delete_message)
        urwid.connect_signal(message, 'set_insert_mode', self.set_insert_mode)
        return message

    def render_messages(self, messages):
        _messages = []
        previous_date = self.store.state.last_date
        last_read_datetime = datetime.fromtimestamp(
            float(self.store.state.channel.get('last_read', '0')))
        today = datetime.today().date()
        for message in messages:
            message_datetime = datetime.fromtimestamp(float(message['ts']))
            message_date = message_datetime.date()
            date_text = None
            unread_text = None
            if not previous_date or previous_date != message_date:
                previous_date = message_date
                self.store.state.last_date = previous_date
                if message_date == today:
                    date_text = 'Today'
                else:
                    date_text = message_date.strftime('%A, %B %d')
            # New messages badge
            if (message_datetime > last_read_datetime
                    and not self.store.state.did_render_new_messages and
                (self.store.state.channel.get('unread_count_display', 0) > 0)):
                self.store.state.did_render_new_messages = True
                unread_text = 'new messages'
            if unread_text is not None:
                _messages.append(
                    NewMessagesDivider(unread_text, date=date_text))
            elif date_text is not None:
                _messages.append(
                    TextDivider(('history_date', date_text), 'center'))
            message = self.render_message(message)
            if message is not None:
                _messages.append(message)
        return _messages

    @asyncio.coroutine
    def _go_to_channel(self, channel_id):
        with concurrent.futures.ThreadPoolExecutor(max_workers=20) as executor:
            yield from asyncio.gather(
                loop.run_in_executor(executor, self.store.load_channel,
                                     channel_id),
                loop.run_in_executor(executor, self.store.load_messages,
                                     channel_id))
            self.store.state.last_date = None
            messages = self.render_messages(self.store.state.messages)
            header = self.render_chatbox_header()
            self.chatbox.body.body[:] = messages
            self.chatbox.header = header
            self.chatbox.message_box.is_read_only = self.store.state.channel.get(
                'is_read_only', False)
            self.sidebar.select_channel(channel_id)
            self.urwid_loop.set_alarm_in(
                0, lambda *args: self.chatbox.body.scroll_to_bottom())
            self.go_to_chatbox()

    def go_to_channel(self, channel_id):
        loop.create_task(self._go_to_channel(channel_id))

    @asyncio.coroutine
    def load_picture_async(self, url, width, message_widget, auth=True):
        width = min(width, 800)
        bytes_in_cache = self.store.cache.picture.get(url)
        if bytes_in_cache:
            message_widget.file = bytes_in_cache
            return
        with concurrent.futures.ThreadPoolExecutor(max_workers=20) as executor:
            headers = {}
            if auth:
                headers = {
                    'Authorization': 'Bearer {}'.format(self.store.slack_token)
                }
            bytes = yield from loop.run_in_executor(
                executor, functools.partial(requests.get, url,
                                            headers=headers))
            file = tempfile.NamedTemporaryFile(delete=False)
            file.write(bytes.content)
            file.close()
            picture = Image(file.name, width=(width / 10))
            message_widget.file = picture

    @asyncio.coroutine
    def load_profile_avatar(self, url, profile):
        bytes_in_cache = self.store.cache.avatar.get(url)
        if bytes_in_cache:
            profile.avatar = bytes_in_cache
            return
        with concurrent.futures.ThreadPoolExecutor(max_workers=20) as executor:
            bytes = yield from loop.run_in_executor(executor, requests.get,
                                                    url)
            file = tempfile.NamedTemporaryFile(delete=False)
            file.write(bytes.content)
            file.close()
            avatar = Image(file.name, width=35)
            self.store.cache.avatar[url] = avatar
            profile.avatar = avatar

    @asyncio.coroutine
    def start_real_time(self):
        self.store.slack.rtm_connect()

        def stop_typing(*args):
            self.chatbox.message_box.typing = None

        alarm = None
        while self.store.slack.server.connected is True:
            events = self.store.slack.rtm_read()
            for event in events:
                if event.get('type') == 'hello':
                    pass
                elif event.get('type') in ('channel_marked', 'group_marked'):
                    unread = event.get('unread_count_display', 0)
                    for channel in self.sidebar.channels:
                        if channel.id == event['channel']:
                            channel.set_unread(unread)
                elif event.get('channel') == self.store.state.channel['id']:
                    if event['type'] == 'message':
                        # Delete message
                        if event.get('subtype') == 'message_deleted':
                            for widget in self.chatbox.body.body:
                                if hasattr(widget, 'ts') and getattr(
                                        widget, 'ts') == event['deleted_ts']:
                                    self.chatbox.body.body.remove(widget)
                                    break
                        elif event.get('subtype') == 'message_changed':
                            for index, widget in enumerate(
                                    self.chatbox.body.body):
                                if hasattr(widget, 'ts') and getattr(
                                        widget,
                                        'ts') == event['message']['ts']:
                                    self.chatbox.body.body[
                                        index] = self.render_message(
                                            event['message'])
                                    break
                        else:
                            self.chatbox.body.body.extend(
                                self.render_messages([event]))
                            self.chatbox.body.scroll_to_bottom()
                    elif event['type'] == 'user_typing':
                        user = self.store.find_user_by_id(event['user'])
                        name = user.get('real_name', user['name'])
                        if alarm is not None:
                            self.urwid_loop.remove_alarm(alarm)
                        self.chatbox.message_box.typing = name
                        self.urwid_loop.set_alarm_in(3, stop_typing)
                    else:
                        pass
                        # print(json.dumps(event, indent=2))
                elif event.get('ok', False):
                    # Message was sent, Slack confirmed it.
                    self.chatbox.body.body.extend(
                        self.render_messages([{
                            'text':
                            event['text'],
                            'ts':
                            event['ts'],
                            'user':
                            self.store.state.auth['user_id']
                        }]))
                    self.chatbox.body.scroll_to_bottom()
                else:
                    pass
                    # print(json.dumps(event, indent=2))
            yield from asyncio.sleep(0.5)

    def set_insert_mode(self):
        self.columns.focus_position = 1
        self.chatbox.focus_position = 'footer'
        self.message_box.focus_position = 1

    def set_edit_topic_mode(self):
        self.columns.focus_position = 1
        self.chatbox.focus_position = 'header'
        self.chatbox.header.go_to_end_of_topic()

    def go_to_chatbox(self):
        self.columns.focus_position = 1
        self.chatbox.focus_position = 'body'

    def leave_edit_mode(self):
        if self.store.state.editing_widget:
            self.store.state.editing_widget.unset_edit_mode()
            self.store.state.editing_widget = None
        self.chatbox.message_box.text = ''

    def go_to_sidebar(self):
        if len(self.columns.contents) > 2:
            self.columns.contents.pop()
        self.columns.focus_position = 0
        if self.store.state.editing_widget:
            self.leave_edit_mode()

    def submit_message(self, message):
        if self.store.state.editing_widget:
            channel = self.store.state.channel['id']
            ts = self.store.state.editing_widget.ts
            edit_result = self.store.edit_message(channel, ts, message)
            if edit_result['ok']:
                self.store.state.editing_widget.original_text = edit_result[
                    'text']
                self.store.state.editing_widget.set_text(
                    MarkdownText(edit_result['text']))
            self.leave_edit_mode()
        else:
            channel = self.store.state.channel['id']
            if message.strip() != '':
                self.store.slack.rtm_send_message(channel, message)
                self.leave_edit_mode()

    def unhandled_input(self, key):
        keymap = self.store.config['keymap']
        if key == keymap['go_to_chatbox'] and self.message_box:
            return self.go_to_chatbox()
        elif key == keymap['go_to_sidebar']:
            return self.go_to_sidebar()
        elif key == keymap['quit_application']:
            return self.quit_application()
        elif key == keymap[
                'set_edit_topic_mode'] and self.message_box and not self.store.state.channel[
                    'id'][0] == 'D':
            return self.set_edit_topic_mode()
        elif key == keymap['set_insert_mode'] and self.message_box:
            return self.set_insert_mode()

    def configure_screen(self, screen):
        screen.set_terminal_properties(colors=self.store.config['colors'])
        screen.set_mouse_tracking()

    def quit_application(self):
        self.urwid_loop.stop()
        if hasattr(self, 'real_time_task'):
            self.real_time_task.cancel()
        sys.exit()