Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add basic thread support #126

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,11 +144,12 @@ Focus on message and press <kbd>r</kbd> (or your custom shortcut) to get permali
"go_to_sidebar": "esc",
"open_quick_switcher": "ctrl k",
"quit_application": "q",
"set_edit_topic_mode": "t",
"set_edit_topic_mode": "ctrl t",
"set_insert_mode": "i",
"yank_message": "y",
"get_permalink": "r",
"set_snooze": "ctrl d"
"set_snooze": "ctrl d",
"toggle_thread": "t"
}
}
```
Expand Down
82 changes: 78 additions & 4 deletions app.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ def __init__(self, config):
self.set_snooze_widget = None
self.workspaces = list(config['workspaces'].items())
self.store = Store(self.workspaces, self.config)
self.showing_thread = False
Store.instance = self.store
urwid.set_encoding('UTF-8')
sidebar = LoadingSideBar()
Expand Down Expand Up @@ -95,7 +96,6 @@ def __init__(self, config):
def sidebar_column(self):
return self.columns.contents[0]


def start(self):
self._loading = True
loop.create_task(self.animate_loading())
Expand Down Expand Up @@ -294,7 +294,7 @@ def mount_chatbox(self, executor, channel):
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)
messages = self.render_messages(self.store.state.messages, channel_id=channel)
header = self.render_chatbox_header()
self._loading = False
self.sidebar.select_channel(channel)
Expand Down Expand Up @@ -362,7 +362,6 @@ def go_to_profile(self, user_id):
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(
Expand Down Expand Up @@ -467,6 +466,8 @@ def render_message(self, message, channel_id=None):
for reaction in message.get('reactions', [])
]

responses = message.get('replies', [])

attachments = []
for attachment in message.get('attachments', []):
attachment_widget = Attachment(
Expand Down Expand Up @@ -506,7 +507,8 @@ def render_message(self, message, channel_id=None):
text,
indicators,
attachments=attachments,
reactions=reactions
reactions=reactions,
responses=responses
)

self.lazy_load_images(files, message)
Expand All @@ -519,6 +521,7 @@ def render_message(self, message, channel_id=None):
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)
urwid.connect_signal(message, 'toggle_thread', self.toggle_thread)

return message

Expand Down Expand Up @@ -548,6 +551,16 @@ def render_messages(self, messages, channel_id=None):
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()

# If we are viewing a thread, add a dummy 'message' to indicate this
# to the user.
if self.showing_thread:
_messages.append(self.render_message({
'text': "VIEWING THREAD",
'ts': '0',
'subtype': SCLACK_SUBTYPE,
}))

for message in messages:
message_datetime = datetime.fromtimestamp(float(message['ts']))
message_date = message_datetime.date()
Expand Down Expand Up @@ -659,8 +672,58 @@ def go_to_channel(self, channel_id):
urwid.disconnect_signal(self.quick_switcher, 'go_to_channel', self.go_to_channel)
self.urwid_loop.widget = self._body
self.quick_switcher = None

# We are not showing a thread - this needs to be reset as this method might be
# triggered from the sidebar while a thread is being shown.
self.showing_thread = False

# Show the channel in the chatbox
loop.create_task(self._go_to_channel(channel_id))

@asyncio.coroutine
def _show_thread(self, channel_id, parent_ts):
"""
Display the requested thread in the chatbox
"""
with concurrent.futures.ThreadPoolExecutor(max_workers=20) as executor:
yield from asyncio.gather(
loop.run_in_executor(executor, self.store.load_thread_messages, channel_id, parent_ts)
)
self.store.state.last_date = None

if len(self.store.state.thread_messages) == 0:
messages = self.render_messages([{
'text': "There was an error showing this thread :(",
'ts': '0',
'subtype': SCLACK_SUBTYPE,
}])
else:
messages = self.render_messages(self.store.state.thread_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 toggle_thread(self, channel_id, parent_ts):
if self.showing_thread:
# Currently showing a thread, return to the main channel
self.showing_thread = False
loop.create_task(self._go_to_channel(channel_id))
else:
# Show the chosen thread
self.showing_thread = True
self.store.state.thread_parent = parent_ts
loop.create_task(self._show_thread(channel_id, parent_ts))

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

Expand Down Expand Up @@ -846,12 +909,23 @@ def submit_message(self, message):
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()
if self.showing_thread:
channel = self.store.state.channel['id']
if message.strip() != '':
self.store.post_thread_message(channel, self.store.state.thread_parent, message)
self.leave_edit_mode()

# Refresh the thread to make sure the new message immediately shows up
loop.create_task(self._show_thread(channel, self.store.state.thread_parent))
else:
channel = self.store.state.channel['id']
if message.strip() != '':
self.store.post_message(channel, message)
self.leave_edit_mode()

# Refresh the channel to make sure the new message shows up
loop.create_task(self._go_to_channel(channel))

def go_to_last_message(self):
self.go_to_chatbox()
self.chatbox.body.go_to_last_message()
Expand Down
5 changes: 3 additions & 2 deletions config.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,13 @@
"go_to_sidebar": "esc",
"open_quick_switcher": "ctrl k",
"quit_application": "q",
"set_edit_topic_mode": "t",
"set_edit_topic_mode": "ctrl t",
"set_insert_mode": "i",
"toggle_sidebar": "s",
"yank_message": "y",
"get_permalink": "r",
"set_snooze": "ctrl d"
"set_snooze": "ctrl d",
"toggle_thread": "t"
},
"sidebar": {
"width": 25,
Expand Down
12 changes: 11 additions & 1 deletion sclack/component/message.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import pyperclip
import webbrowser
from sclack.store import Store
from sclack.components import ThreadText
from sclack.component.time import Time


Expand All @@ -18,9 +19,10 @@ class Message(urwid.AttrMap):
'quit_application',
'set_insert_mode',
'mark_read',
'toggle_thread',
]

def __init__(self, ts, channel_id, user, text, indicators, reactions=(), attachments=()):
def __init__(self, ts, channel_id, user, text, indicators, reactions=(), attachments=(), responses=()):
self.ts = ts
self.channel_id = channel_id
self.user_id = user.id
Expand All @@ -30,10 +32,15 @@ def __init__(self, ts, channel_id, user, text, indicators, reactions=(), attachm
main_column = [urwid.Columns([('pack', user), self.text_widget])]
main_column.extend(attachments)
self._file_index = len(main_column)

if reactions:
main_column.append(urwid.Columns([
('pack', reaction) for reaction in reactions
]))

if responses:
main_column.append(ThreadText(len(responses)))

self.main_column = urwid.Pile(main_column)
columns = [
('fixed', 7, Time(ts)),
Expand Down Expand Up @@ -76,6 +83,9 @@ def keypress(self, size, key):
elif key == keymap['get_permalink']:
# FIXME
urwid.emit_signal(self, 'get_permalink', self, self.channel_id, self.ts)
elif key == keymap['toggle_thread']:
urwid.emit_signal(self, 'toggle_thread', self.channel_id, self.ts)
return True
elif key == 'enter':
browser_name = Store.instance.config['features']['browser']

Expand Down
12 changes: 12 additions & 0 deletions sclack/components.py
Original file line number Diff line number Diff line change
Expand Up @@ -894,6 +894,18 @@ def __init__(self, id, name, color=None, is_app=False):
super(User, self).__init__(markup)


class ThreadText(urwid.Text):
"""
A text element used to indicate the number of messages in a thread
"""
def __init__(self, num_replies):
color = "#" + shorten_hex('146BF7')
markup = [
(urwid.AttrSpec(color, 'h235'), 'Thread ({})'.format(num_replies))
]
super(ThreadText, self).__init__(markup)


class Workspace(urwid.AttrMap):
__metaclass__ = urwid.MetaSignals
signals = ['select_workspace']
Expand Down
25 changes: 25 additions & 0 deletions sclack/store.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ def __init__(self):
self.groups = []
self.stars = []
self.messages = []
self.thread_messages = []
self.thread_parent = None
self.users = []
self.pin_count = 0
self.has_more = False
Expand Down Expand Up @@ -81,6 +83,19 @@ def load_messages(self, channel_id):
self.state.pin_count = history['pin_count']
self.state.messages.reverse()

def load_thread_messages(self, channel_id, parent_ts):
"""
Load all of the messages sent in reply to the message with the given timestamp.
"""
replies = self.slack.api_call(
"conversations.replies",
channel=channel_id,
ts=parent_ts,
)

self.state.thread_messages = replies['messages']
self.state.has_more = replies.get('has_more', False)

def is_valid_channel_id(self, channel_id):
"""
Check whether channel_id is valid
Expand Down Expand Up @@ -219,6 +234,16 @@ def post_message(self, channel_id, message):
text=message
)

def post_thread_message(self, channel_id, parent_ts, message):
return self.slack.api_call(
'chat.postMessage',
channel=channel_id,
as_user=True,
link_name=True,
text=message,
thread_ts=parent_ts
)

def get_presence(self, user_id):
response = self.slack.api_call('users.getPresence', user=user_id)

Expand Down