Skip to content

Commit ca6eddb

Browse files
Merge pull request #82 from duynguyenhoang/feature/support_mark_read
Add mark as read feature
2 parents bf4c344 + f5e1d05 commit ca6eddb

File tree

3 files changed

+188
-20
lines changed

3 files changed

+188
-20
lines changed

app.py

+71-7
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import os
77
import requests
88
import sys
9+
import time
910
import traceback
1011
import tempfile
1112
import urwid
@@ -24,6 +25,7 @@
2425
loop = asyncio.get_event_loop()
2526

2627
SCLACK_SUBTYPE = 'sclack_message'
28+
MARK_READ_ALARM_PERIOD = 3
2729

2830

2931
class SclackEventLoop(urwid.AsyncioEventLoop):
@@ -81,6 +83,7 @@ def __init__(self, config):
8183
unhandled_input=self.unhandled_input
8284
)
8385
self.configure_screen(self.urwid_loop.screen)
86+
self.last_keypress = (0, None)
8487

8588
def start(self):
8689
self._loading = True
@@ -246,11 +249,14 @@ def mount_chatbox(self, executor, channel):
246249
user=self.store.state.auth['user'],
247250
is_read_only=self.store.state.channel.get('is_read_only', False)
248251
)
249-
self.chatbox = ChatBox(messages, header, self.message_box)
252+
self.chatbox = ChatBox(messages, header, self.message_box, self.urwid_loop)
250253
urwid.connect_signal(self.chatbox, 'set_insert_mode', self.set_insert_mode)
254+
urwid.connect_signal(self.chatbox, 'mark_read', self.handle_mark_read)
251255
urwid.connect_signal(self.chatbox, 'open_quick_switcher', self.open_quick_switcher)
256+
252257
urwid.connect_signal(self.message_box.prompt_widget, 'submit_message', self.submit_message)
253258
urwid.connect_signal(self.message_box.prompt_widget, 'go_to_last_message', self.go_to_last_message)
259+
254260
self.real_time_task = loop.create_task(self.start_real_time())
255261

256262
def edit_message(self, widget, user_id, ts, original_text):
@@ -318,20 +324,22 @@ def on_change_topic(self, text):
318324
self.store.set_topic(self.store.state.channel['id'], text)
319325
self.go_to_sidebar()
320326

321-
def render_message(self, message):
327+
def render_message(self, message, channel_id=None):
322328
is_app = False
323329
subtype = message.get('subtype')
324330

325331
if subtype == SCLACK_SUBTYPE:
326332
message = Message(
327333
message['ts'],
334+
'',
328335
User('1', 'sclack'),
329336
MarkdownText(message['text']),
330337
Indicators(False, False)
331338
)
332339
urwid.connect_signal(message, 'go_to_sidebar', self.go_to_sidebar)
333340
urwid.connect_signal(message, 'quit_application', self.quit_application)
334341
urwid.connect_signal(message, 'set_insert_mode', self.set_insert_mode)
342+
urwid.connect_signal(message, 'mark_read', self.handle_mark_read)
335343

336344
return message
337345

@@ -417,8 +425,11 @@ def render_message(self, message):
417425
if file:
418426
files.append(file)
419427

428+
message_channel = channel_id if channel_id is not None else message.get('channel')
429+
420430
message = Message(
421431
message['ts'],
432+
message_channel,
422433
user,
423434
text,
424435
indicators,
@@ -434,6 +445,7 @@ def render_message(self, message):
434445
urwid.connect_signal(message, 'delete_message', self.delete_message)
435446
urwid.connect_signal(message, 'quit_application', self.quit_application)
436447
urwid.connect_signal(message, 'set_insert_mode', self.set_insert_mode)
448+
urwid.connect_signal(message, 'mark_read', self.handle_mark_read)
437449

438450
return message
439451

@@ -458,7 +470,7 @@ def lazy_load_images(self, files, widget):
458470
not file.get('is_external', True)
459471
))
460472

461-
def render_messages(self, messages):
473+
def render_messages(self, messages, channel_id=None):
462474
_messages = []
463475
previous_date = self.store.state.last_date
464476
last_read_datetime = datetime.fromtimestamp(float(self.store.state.channel.get('last_read', '0')))
@@ -485,11 +497,59 @@ def render_messages(self, messages):
485497
_messages.append(NewMessagesDivider(unread_text, date=date_text))
486498
elif date_text is not None:
487499
_messages.append(TextDivider(('history_date', date_text), 'center'))
488-
message = self.render_message(message)
500+
501+
message = self.render_message(message, channel_id)
502+
489503
if message is not None:
490504
_messages.append(message)
505+
491506
return _messages
492507

508+
def handle_mark_read(self, data):
509+
"""
510+
Mark as read to bottom
511+
:return:
512+
"""
513+
row_index = data if data is not None else -1
514+
515+
def read(*kwargs):
516+
loop.create_task(
517+
self.mark_read_slack(row_index)
518+
)
519+
520+
now = time.time()
521+
if now - self.last_keypress[0] < MARK_READ_ALARM_PERIOD and self.last_keypress[1] is not None:
522+
self.urwid_loop.remove_alarm(self.last_keypress[1])
523+
524+
self.last_keypress = (now, self.urwid_loop.set_alarm_in(MARK_READ_ALARM_PERIOD, read))
525+
526+
def scroll_messages(self, *args):
527+
index = self.chatbox.body.scroll_to_new_messages()
528+
loop.create_task(
529+
self.mark_read_slack(index)
530+
)
531+
532+
@asyncio.coroutine
533+
def mark_read_slack(self, index):
534+
if not self.chatbox.body or not self.chatbox.body.body:
535+
return
536+
537+
if index is None or index == -1:
538+
index = len(self.chatbox.body.body) - 1
539+
540+
if self.chatbox.body.body and self.chatbox.body.body and len(self.chatbox.body.body) > index:
541+
message = self.chatbox.body.body[index]
542+
543+
# Only apply for message
544+
if not hasattr(message, 'channel_id'):
545+
if len(self.chatbox.body.body) > index + 1:
546+
message = self.chatbox.body.body[index + 1]
547+
else:
548+
message = self.chatbox.body.body[index - 1]
549+
550+
if message.channel_id:
551+
self.store.mark_read(message.channel_id, message.ts)
552+
493553
@asyncio.coroutine
494554
def _go_to_channel(self, channel_id):
495555
with concurrent.futures.ThreadPoolExecutor(max_workers=20) as executor:
@@ -506,14 +566,14 @@ def _go_to_channel(self, channel_id):
506566
'subtype': SCLACK_SUBTYPE,
507567
}])
508568
else:
509-
messages = self.render_messages(self.store.state.messages)
569+
messages = self.render_messages(self.store.state.messages, channel_id=channel_id)
510570

511571
header = self.render_chatbox_header()
512572
self.chatbox.body.body[:] = messages
513573
self.chatbox.header = header
514574
self.chatbox.message_box.is_read_only = self.store.state.channel.get('is_read_only', False)
515575
self.sidebar.select_channel(channel_id)
516-
self.urwid_loop.set_alarm_in(0, lambda *args: self.chatbox.body.scroll_to_new_messages())
576+
self.urwid_loop.set_alarm_in(0, self.scroll_messages)
517577

518578
if len(self.store.state.messages) == 0:
519579
self.go_to_sidebar()
@@ -602,7 +662,10 @@ def stop_typing(*args):
602662
else:
603663
self.chatbox.body.body.extend(self.render_messages([event]))
604664
self.chatbox.body.scroll_to_bottom()
605-
elif event['type'] == 'user_typing':
665+
else:
666+
pass
667+
elif event['type'] == 'user_typing':
668+
if event.get('channel') == self.store.state.channel['id']:
606669
user = self.store.find_user_by_id(event['user'])
607670
name = user.get('display_name') or user.get('real_name') or user['name']
608671
if alarm is not None:
@@ -623,6 +686,7 @@ def stop_typing(*args):
623686
'user': self.store.state.auth['user_id']
624687
}]))
625688
self.chatbox.body.scroll_to_bottom()
689+
self.handle_mark_read(-1)
626690
else:
627691
pass
628692
# print(json.dumps(event, indent=2))

sclack/components.py

+65-13
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@
1212
from .store import Store
1313

1414

15+
MARK_READ_ALARM_PERIOD = 3
16+
17+
1518
def get_icon(name):
1619
return Store.instance.config['icons'][name]
1720

@@ -25,9 +28,18 @@ def __init__(self, widget, color):
2528
tline='', trcorner='', rline='', bline='', brcorner='')
2629
super(Box, self).__init__(body, urwid.AttrSpec(color, 'h235'))
2730

31+
2832
class Attachment(Box):
29-
def __init__(self, color=None, service_name=None, title=None, title_link=None,
30-
author_name=None, pretext=None, text=None, fields=None, footer=None):
33+
def __init__(self,
34+
color=None,
35+
service_name=None,
36+
title=None,
37+
title_link=None,
38+
author_name=None,
39+
pretext=None,
40+
text=None,
41+
fields=None,
42+
footer=None):
3143
body = []
3244
if not color:
3345
color = 'CCCCCC'
@@ -64,7 +76,7 @@ def file(self, image):
6476

6577

6678
class BreadCrumbs(urwid.Text):
67-
def __init__(self, elements=[]):
79+
def __init__(self, elements=()):
6880
separator = ('separator', ' {} '.format(get_icon('divider')))
6981
body = []
7082
for element in elements:
@@ -206,20 +218,24 @@ def keypress(self, size, key):
206218

207219
class ChatBox(urwid.Frame):
208220
__metaclass__ = urwid.MetaSignals
209-
signals = ['go_to_sidebar', 'open_quick_switcher', 'set_insert_mode']
221+
signals = ['go_to_sidebar', 'open_quick_switcher', 'set_insert_mode', 'mark_read']
210222

211-
def __init__(self, messages, header, message_box):
223+
def __init__(self, messages, header, message_box, event_loop):
212224
self._header = header
213225
self.message_box = message_box
214-
self.body = ChatBoxMessages(messages=messages)
226+
self.body = ChatBoxMessages(messages=messages, event_loop=event_loop)
215227
self.body.scroll_to_bottom()
216228
urwid.connect_signal(self.body, 'set_date', self._header.on_set_date)
217229
urwid.connect_signal(self.body, 'set_insert_mode', self.set_insert_mode)
230+
urwid.connect_signal(self.body, 'mark_read', self.mark_as_read)
218231
super(ChatBox, self).__init__(self.body, header=header, footer=self.message_box)
219232

220233
def set_insert_mode(self):
221234
urwid.emit_signal(self, 'set_insert_mode')
222235

236+
def mark_as_read(self, data):
237+
urwid.emit_signal(self, 'mark_read', data)
238+
223239
def keypress(self, size, key):
224240
keymap = Store.instance.config['keymap']
225241
if key == keymap['open_quick_switcher']:
@@ -241,12 +257,14 @@ def header(self, header):
241257

242258
class ChatBoxMessages(urwid.ListBox):
243259
__metaclass__ = urwid.MetaSignals
244-
signals = ['set_auto_scroll', 'set_date', 'set_insert_mode']
260+
signals = ['set_auto_scroll', 'set_date', 'set_insert_mode', 'mark_read']
245261

246-
def __init__(self, messages=[]):
262+
def __init__(self, messages=(), event_loop=None):
247263
self.body = urwid.SimpleFocusListWalker(messages)
248264
super(ChatBoxMessages, self).__init__(self.body)
249265
self.auto_scroll = True
266+
self.last_keypress = (0, None, 0)
267+
self.event_loop = event_loop
250268

251269
@property
252270
def auto_scroll(self):
@@ -256,23 +274,47 @@ def auto_scroll(self):
256274
def auto_scroll(self, switch):
257275
if type(switch) != bool:
258276
return
277+
259278
self._auto_scroll = switch
260279
urwid.emit_signal(self, 'set_auto_scroll', switch)
261280

281+
def mark_read_emit(self, loop, data):
282+
urwid.emit_signal(self, 'mark_read', data)
283+
262284
def keypress(self, size, key):
263285
keymap = Store.instance.config['keymap']
264286
self.handle_floating_date(size)
287+
288+
if key in (keymap['cursor_up'], keymap['cursor_down'], 'up', 'down', ):
289+
now = time.time()
290+
max_focus = self.get_focus()[1]
291+
292+
if now - self.last_keypress[0] < MARK_READ_ALARM_PERIOD and self.last_keypress[1] is not None:
293+
if max_focus < self.last_keypress[2]:
294+
max_focus = self.last_keypress[2]
295+
296+
self.event_loop.remove_alarm(self.last_keypress[1])
297+
298+
self.last_keypress = (
299+
now,
300+
self.event_loop.set_alarm_in(MARK_READ_ALARM_PERIOD, self.mark_read_emit, max_focus),
301+
max_focus
302+
)
303+
265304
# Go to insert mode
266305
if key == 'down' and self.get_focus()[1] == len(self.body) - 1:
267306
urwid.emit_signal(self, 'set_insert_mode')
268307
return True
308+
269309
super(ChatBoxMessages, self).keypress(size, key)
310+
270311
if key in ('page up', 'page down'):
271312
self.auto_scroll = self.get_focus()[1] == len(self.body) - 1
313+
272314
if key == keymap['cursor_up']:
273315
self.keypress(size, 'up')
274316
if key == keymap['cursor_down']:
275-
self.keypress(size,'down')
317+
self.keypress(size, 'down')
276318

277319
def mouse_event(self, size, event, button, col, row, focus):
278320
self.handle_floating_date(size)
@@ -290,6 +332,7 @@ def scroll_to_new_messages(self):
290332
for index, widget in enumerate(self.body):
291333
if isinstance(widget, NewMessagesDivider):
292334
return self.set_focus(index)
335+
293336
return self.scroll_to_bottom()
294337

295338
def scroll_to_bottom(self):
@@ -440,10 +483,19 @@ def __init__(self, is_edited=False, is_starred=False):
440483

441484
class Message(urwid.AttrMap):
442485
__metaclass__ = urwid.MetaSignals
443-
signals = ['delete_message', 'edit_message', 'go_to_profile', 'go_to_sidebar', 'quit_application', 'set_insert_mode']
444-
445-
def __init__(self, ts, user, text, indicators, reactions=[], attachments=[]):
486+
signals = [
487+
'delete_message',
488+
'edit_message',
489+
'go_to_profile',
490+
'go_to_sidebar',
491+
'quit_application',
492+
'set_insert_mode',
493+
'mark_read',
494+
]
495+
496+
def __init__(self, ts, channel_id, user, text, indicators, reactions=(), attachments=()):
446497
self.ts = ts
498+
self.channel_id = channel_id
447499
self.user_id = user.id
448500
self.markdown_text = text
449501
self.original_text = text.original_text
@@ -837,7 +889,7 @@ def __init__(self, id, name, color=None, is_app=False):
837889
self.id = id
838890
if not color:
839891
color = '333333'
840-
color='#{}'.format(shorten_hex(color))
892+
color = '#{}'.format(shorten_hex(color))
841893
markup = [
842894
(urwid.AttrSpec(color, 'h235'), '{} '.format(name))
843895
]

0 commit comments

Comments
 (0)