Skip to content

Commit 0934814

Browse files
committed
model/views: Add Visual desktop notification checkbox in Stream Info.
This commit adds a new checkbox 'Visual desktop notification' in the Stream settings section inside StreamInfoView. It uses the subscription field of initial_data's response to set the initial state of the checkbox, and henceforth events to sync its settings between client <-> server. The notify_user code has been updated to interact properly with the checkbox settings. Tests amended. Partially Fixes #887.
1 parent 0608209 commit 0934814

File tree

4 files changed

+167
-32
lines changed

4 files changed

+167
-32
lines changed

tests/model/test_model.py

Lines changed: 108 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ def model(self, mocker, initial_data, user_profile,
3636
return_value=[])
3737
mocker.patch('zulipterminal.model.Model.'
3838
'_stream_info_from_subscriptions',
39-
return_value=({}, set(), [], []))
39+
return_value=({}, set(), [], [], set()))
4040
# NOTE: PATCH WHERE USED NOT WHERE DEFINED
4141
self.classify_unread_counts = mocker.patch(
4242
'zulipterminal.model.classify_unread_counts',
@@ -134,7 +134,7 @@ def test_init_InvalidAPIKey_response(self, mocker, initial_data):
134134
return_value=[])
135135
mocker.patch('zulipterminal.model.Model.'
136136
'_stream_info_from_subscriptions',
137-
return_value=({}, set(), [], []))
137+
return_value=({}, set(), [], [], set()))
138138
self.classify_unread_counts = mocker.patch(
139139
'zulipterminal.model.classify_unread_counts',
140140
return_value=[])
@@ -156,7 +156,7 @@ def test_init_ZulipError_exception(self, mocker, initial_data,
156156
return_value=[])
157157
mocker.patch('zulipterminal.model.Model.'
158158
'_stream_info_from_subscriptions',
159-
return_value=({}, set(), [], []))
159+
return_value=({}, set(), [], [], set()))
160160
self.classify_unread_counts = mocker.patch(
161161
'zulipterminal.model.classify_unread_counts',
162162
return_value=[])
@@ -585,7 +585,7 @@ def test_success_get_messages(self, mocker, messages_successful_response,
585585
return_value=[])
586586
mocker.patch('zulipterminal.model.Model.'
587587
'_stream_info_from_subscriptions',
588-
return_value=({}, set(), [], []))
588+
return_value=({}, set(), [], [], set()))
589589
self.classify_unread_counts = mocker.patch(
590590
'zulipterminal.model.classify_unread_counts',
591591
return_value=[])
@@ -625,7 +625,7 @@ def test_get_message_false_first_anchor(
625625
return_value=[])
626626
mocker.patch('zulipterminal.model.Model.'
627627
'_stream_info_from_subscriptions',
628-
return_value=({}, set(), [], []))
628+
return_value=({}, set(), [], [], set()))
629629
self.classify_unread_counts = mocker.patch(
630630
'zulipterminal.model.classify_unread_counts',
631631
return_value=[])
@@ -659,7 +659,7 @@ def test_fail_get_messages(self, mocker, error_response,
659659
return_value=[])
660660
mocker.patch('zulipterminal.model.Model.'
661661
'_stream_info_from_subscriptions',
662-
return_value=({}, set(), [], []))
662+
return_value=({}, set(), [], [], set()))
663663
self.classify_unread_counts = mocker.patch(
664664
'zulipterminal.model.classify_unread_counts',
665665
return_value=[])
@@ -694,6 +694,29 @@ def test_toggle_stream_muted_status(self, mocker, model,
694694
self.display_error_if_present.assert_called_once_with(response,
695695
self.controller)
696696

697+
@pytest.mark.parametrize('initial_desktop_notified_streams, value', [
698+
({315}, True),
699+
({205, 315}, False),
700+
(set(), True),
701+
({205}, False),
702+
], ids=['desktop_notify_enable_205', 'desktop_notify_disable_205',
703+
'first_notify_enable_205', 'last_notify_disable_205'])
704+
def test_toggle_stream_desktop_notification(
705+
self, mocker, model, initial_desktop_notified_streams,
706+
value, response={'result': 'success'}):
707+
model.desktop_notifs_enabled_streams = initial_desktop_notified_streams
708+
model.client.update_subscription_settings.return_value = response
709+
model.toggle_stream_desktop_notification(205)
710+
request = [{
711+
'stream_id': 205,
712+
'property': 'desktop_notifications',
713+
'value': value
714+
}]
715+
(model.client.update_subscription_settings
716+
.assert_called_once_with(request))
717+
self.display_error_if_present.assert_called_once_with(response,
718+
self.controller)
719+
697720
@pytest.mark.parametrize('flags_before, expected_operator', [
698721
([], 'add'),
699722
(['starred'], 'remove'),
@@ -744,7 +767,7 @@ def test__update_initial_data_raises_exception(self, mocker, initial_data):
744767
return_value=[])
745768
mocker.patch('zulipterminal.model.Model.'
746769
'_stream_info_from_subscriptions',
747-
return_value=({}, set(), [], []))
770+
return_value=({}, set(), [], [], set()))
748771
self.classify_unread_counts = mocker.patch(
749772
'zulipterminal.model.classify_unread_counts',
750773
return_value=[])
@@ -778,26 +801,32 @@ def test_get_all_users(self, mocker, initial_data, user_list, user_dict,
778801
self.client.register.return_value = initial_data
779802
mocker.patch('zulipterminal.model.Model.'
780803
'_stream_info_from_subscriptions',
781-
return_value=({}, set(), [], []))
804+
return_value=({}, set(), [], [], set()))
782805
self.classify_unread_counts = mocker.patch(
783806
'zulipterminal.model.classify_unread_counts',
784807
return_value=[])
785808
model = Model(self.controller)
786809
assert model.user_dict == user_dict
787810
assert model.users == user_list
788811

789-
@pytest.mark.parametrize('muted', powerset([1, 2, 99, 1000]))
812+
@pytest.mark.parametrize('muted, desktop_notifs_enabled', list(
813+
zip(powerset([1, 2, 99, 1000]), powerset([1, 2, 99, 1000])))
814+
)
790815
def test__stream_info_from_subscriptions(self, initial_data, streams,
791-
muted):
792-
subs = [dict(entry, in_home_view=entry['stream_id'] not in muted)
816+
muted, desktop_notifs_enabled):
817+
subs = [dict(entry, in_home_view=entry['stream_id'] not in muted,
818+
desktop_notifications=entry['stream_id'] in
819+
desktop_notifs_enabled)
793820
for entry in initial_data['subscriptions']]
794-
by_id, muted_streams, pinned, unpinned = (
821+
(by_id, muted_streams, pinned, unpinned,
822+
desktop_notifs_enabled_streams) = (
795823
Model._stream_info_from_subscriptions(subs))
796824
assert len(by_id)
797825
assert all(msg_id == msg['stream_id'] for msg_id, msg in by_id.items())
798826
assert muted_streams == muted
799827
assert pinned == [] # FIXME generalize/parametrize
800828
assert unpinned == streams # FIXME generalize/parametrize
829+
assert desktop_notifs_enabled_streams == desktop_notifs_enabled
801830

802831
def test__handle_message_event_with_Falsey_log(self, mocker,
803832
model, message_fixture):
@@ -984,50 +1013,61 @@ def test__update_topic_index(self, topic_name, topic_order_initial,
9841013
assert model.index['topics'][86] == topic_order_final
9851014

9861015
# TODO: Ideally message_fixture would use standardized ids?
987-
@pytest.mark.parametrize(['user_id', 'vary_each_msg', 'stream_setting',
988-
'types_when_notify_called'], [
1016+
@pytest.mark.parametrize(['user_id', 'vary_each_msg',
1017+
'desktop_notification_status',
1018+
'types_when_notify_called',
1019+
'is_stream_muted'], [
9891020
(5140, {'flags': ['mentioned', 'wildcard_mentioned']}, True,
990-
[]), # message_fixture sender_id is 5140
1021+
[], False), # message_fixture sender_id is 5140
9911022
(5179, {'flags': ['mentioned']}, False,
992-
['stream', 'private']),
1023+
['stream', 'private'], False),
9931024
(5179, {'flags': ['wildcard_mentioned']}, False,
994-
['stream', 'private']),
1025+
['stream', 'private'], False),
9951026
(5179, {'flags': []}, True,
996-
['stream']),
1027+
['stream'], False),
9971028
(5179, {'flags': []}, False,
998-
['private']),
1029+
['private'], False),
1030+
(5140, {'flags': []}, True,
1031+
['stream'], True),
1032+
(5179, {'flags': ['mentioned']}, True,
1033+
['stream'], True)
9991034
], ids=[
10001035
'not_notified_since_self_message',
10011036
'notified_stream_and_private_since_directly_mentioned',
10021037
'notified_stream_and_private_since_wildcard_mentioned',
10031038
'notified_stream_since_stream_has_desktop_notifications',
10041039
'notified_private_since_private_message',
1040+
'not_notified_stream_since_muted_stream',
1041+
'notified_muted_stream_since_directly_mentioned'
10051042
])
10061043
def test_notify_users_calling_msg_type(self, mocker, model,
10071044
message_fixture,
10081045
user_id,
10091046
vary_each_msg,
1010-
stream_setting,
1047+
desktop_notification_status,
1048+
is_stream_muted,
10111049
types_when_notify_called):
10121050
message_fixture.update(vary_each_msg)
10131051
model.user_id = user_id
1014-
if 'stream_id' in message_fixture:
1015-
model.stream_dict.update(
1016-
{message_fixture['stream_id']:
1017-
{'desktop_notifications': stream_setting}}
1018-
)
1052+
mocker.patch('zulipterminal.model.Model.'
1053+
'is_desktop_notifications_enabled',
1054+
return_value=desktop_notification_status)
1055+
mocker.patch('zulipterminal.model.Model.'
1056+
'is_muted_stream', return_value=is_stream_muted)
10191057
notify = mocker.patch('zulipterminal.model.notify')
10201058

10211059
model.notify_user(message_fixture)
10221060

1061+
target = None
10231062
if message_fixture['type'] in types_when_notify_called:
10241063
who = message_fixture['type']
1025-
if who == 'stream':
1064+
if who == 'stream' and not is_stream_muted:
10261065
target = 'PTEST -> Test'
10271066
elif who == 'private':
10281067
target = 'you'
10291068
if len(message_fixture['display_recipient']) > 2:
10301069
target += ', Bar Bar'
1070+
if target is not None:
10311071
title = 'Test Organization Name:\nFoo Foo (to {})'.format(target)
10321072
# TODO: Test message content too?
10331073
notify.assert_called_once_with(title, mocker.ANY)
@@ -1822,6 +1862,34 @@ def set_from_list_of_dict(data):
18221862
update_left_panel.assert_called_once_with()
18231863
model.controller.update_screen.assert_called_once_with()
18241864

1865+
@pytest.mark.parametrize('event, final_desktop_notified_streams, ', [
1866+
(
1867+
{'property': 'desktop_notifications',
1868+
'op': 'update',
1869+
'stream_id': 19,
1870+
'value': False},
1871+
{15}
1872+
),
1873+
(
1874+
{'property': 'desktop_notifications',
1875+
'op': 'update',
1876+
'stream_id': 30,
1877+
'value': True},
1878+
{15, 19, 30}
1879+
)
1880+
], ids=[
1881+
'remove_19', 'add_30'
1882+
])
1883+
def test__handle_subscription_event_desktop_notified_streams(
1884+
self, model, mocker, stream_button, event,
1885+
final_desktop_notified_streams):
1886+
model.desktop_notifs_enabled_streams = {15, 19}
1887+
model._handle_subscription_event(event)
1888+
1889+
assert (model.desktop_notifs_enabled_streams
1890+
== final_desktop_notified_streams)
1891+
model.controller.update_screen.assert_called_once_with()
1892+
18251893
@pytest.mark.parametrize(['event', 'feature_level',
18261894
'stream_id', 'expected_subscribers'], [
18271895
({'op': 'peer_add', 'stream_id': 99, 'user_id': 12}, None,
@@ -1979,6 +2047,20 @@ def test_is_muted_stream(self, muted_streams, stream_id, is_muted,
19792047
model.muted_streams = muted_streams
19802048
assert model.is_muted_stream(stream_id) == is_muted
19812049

2050+
@pytest.mark.parametrize(['desktop_notified_streams',
2051+
'stream_id', 'is_enabled'], [
2052+
({1}, 1, True),
2053+
({1, 3, 7}, 2, False),
2054+
(set(), 1, False),
2055+
], ids=['single_stream', 'multiple_streams', 'no_stream'])
2056+
def test_is_desktop_notifications_enabled(self, desktop_notified_streams,
2057+
stream_id, is_enabled,
2058+
stream_dict, model):
2059+
model.stream_dict = stream_dict
2060+
model.desktop_notifs_enabled_streams = desktop_notified_streams
2061+
assert (model.is_desktop_notifications_enabled(stream_id)
2062+
== is_enabled)
2063+
19822064
@pytest.mark.parametrize('topic, is_muted', [
19832065
((1, 'stream muted & unmuted topic'), False),
19842066
((2, 'muted topic'), True),

tests/ui_tools/test_popups.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -572,6 +572,8 @@ def mock_external_classes(self, mocker, monkeypatch):
572572
return_value=(64, 64))
573573
self.controller.model.is_muted_stream.return_value = False
574574
self.controller.model.is_pinned_stream.return_value = False
575+
(self.controller.model.is_desktop_notifications_enabled.
576+
return_value) = False
575577
mocker.patch(VIEWS + ".urwid.SimpleFocusListWalker", return_value=[])
576578
self.stream_id = 10
577579
self.controller.model.stream_dict = {
@@ -669,7 +671,7 @@ def test_keypress_navigation(self, mocker, widget_size,
669671

670672
@pytest.mark.parametrize('key', (*keys_for_command('ENTER'), ' '))
671673
def test_checkbox_toggle_mute_stream(self, mocker, key, widget_size):
672-
mute_checkbox = self.stream_info_view.widgets[7]
674+
mute_checkbox = self.stream_info_view.widgets[-3]
673675
toggle_mute_status = self.controller.model.toggle_stream_muted_status
674676
stream_id = self.stream_info_view.stream_id
675677
size = widget_size(mute_checkbox)
@@ -680,7 +682,7 @@ def test_checkbox_toggle_mute_stream(self, mocker, key, widget_size):
680682

681683
@pytest.mark.parametrize('key', (*keys_for_command('ENTER'), ' '))
682684
def test_checkbox_toggle_pin_stream(self, mocker, key, widget_size):
683-
pin_checkbox = self.stream_info_view.widgets[8]
685+
pin_checkbox = self.stream_info_view.widgets[-2]
684686
toggle_pin_status = self.controller.model.toggle_stream_pinned_status
685687
stream_id = self.stream_info_view.stream_id
686688
size = widget_size(pin_checkbox)
@@ -689,6 +691,19 @@ def test_checkbox_toggle_pin_stream(self, mocker, key, widget_size):
689691

690692
toggle_pin_status.assert_called_once_with(stream_id)
691693

694+
@pytest.mark.parametrize('key', (*keys_for_command('ENTER'), ' '))
695+
def test_checkbox_toggle_desktop_notification(self, mocker,
696+
key, widget_size):
697+
desktop_notify_checkbox = self.stream_info_view.widgets[-1]
698+
toggle_desktop_notify_status = (
699+
self.controller.model.toggle_stream_desktop_notification)
700+
stream_id = self.stream_info_view.stream_id
701+
size = widget_size(desktop_notify_checkbox)
702+
703+
desktop_notify_checkbox.keypress(size, key)
704+
705+
toggle_desktop_notify_status.assert_called_once_with(stream_id)
706+
692707

693708
class TestStreamMembersView:
694709
@pytest.fixture(autouse=True)

zulipterminal/model.py

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,8 @@ def __init__(self, controller: Any) -> None:
117117
subscriptions = self.initial_data['subscriptions']
118118
stream_data = Model._stream_info_from_subscriptions(subscriptions)
119119
(self.stream_dict, self.muted_streams,
120-
self.pinned_streams, self.unpinned_streams) = stream_data
120+
self.pinned_streams, self.unpinned_streams,
121+
self.desktop_notifs_enabled_streams) = stream_data
121122

122123
# NOTE: The expected response has been upgraded from
123124
# [stream_name, topic] to [stream_name, topic, date_muted] in
@@ -548,6 +549,9 @@ def exception_safe_result(future: 'Future[str]') -> str:
548549
def is_muted_stream(self, stream_id: int) -> bool:
549550
return stream_id in self.muted_streams
550551

552+
def is_desktop_notifications_enabled(self, stream_id: int) -> bool:
553+
return stream_id in self.desktop_notifs_enabled_streams
554+
551555
def is_muted_topic(self, stream_id: int, topic: str) -> bool:
552556
"""
553557
Returns True if topic is muted via muted_topics.
@@ -729,7 +733,8 @@ def user_name_from_id(self, user_id: int) -> str:
729733
@staticmethod
730734
def _stream_info_from_subscriptions(
731735
subscriptions: List[Dict[str, Any]]
732-
) -> Tuple[Dict[int, Any], Set[int], List[StreamData], List[StreamData]]:
736+
) -> Tuple[Dict[int, Any], Set[int], List[StreamData],
737+
List[StreamData], Set[int]]:
733738

734739
def make_reduced_stream_data(stream: Dict[str, Any]) -> StreamData:
735740
# stream_id has been changed to id.
@@ -759,6 +764,8 @@ def make_reduced_stream_data(stream: Dict[str, Any]) -> StreamData:
759764
if stream['in_home_view'] is False},
760765
pinned_streams,
761766
unpinned_streams,
767+
{stream['stream_id'] for stream in subscriptions
768+
if stream['desktop_notifications'] is True},
762769
)
763770

764771
def _group_info_from_realm_user_groups(self,
@@ -808,6 +815,15 @@ def toggle_stream_pinned_status(self, stream_id: int) -> bool:
808815
def is_user_subscribed_to_stream(self, stream_id: int) -> bool:
809816
return stream_id in self.stream_dict
810817

818+
def toggle_stream_desktop_notification(self, stream_id: int) -> None:
819+
request = [{
820+
'stream_id': stream_id,
821+
'property': 'desktop_notifications',
822+
'value': not self.is_desktop_notifications_enabled(stream_id)
823+
}]
824+
response = self.client.update_subscription_settings(request)
825+
display_error_if_present(response, self.controller)
826+
811827
def _handle_subscription_event(self, event: Event) -> None:
812828
"""
813829
Handle changes in subscription (eg. muting/unmuting,
@@ -865,6 +881,14 @@ def get_stream_by_id(streams: List[StreamData], stream_id: int
865881
sort_streams(self.pinned_streams)
866882
self.controller.view.left_panel.update_stream_view()
867883
self.controller.update_screen()
884+
elif event.get('property', None) == 'desktop_notifications':
885+
stream_id = event['stream_id']
886+
887+
if event['value']:
888+
self.desktop_notifs_enabled_streams.add(stream_id)
889+
else:
890+
self.desktop_notifs_enabled_streams.remove(stream_id)
891+
self.controller.update_screen()
868892
elif event['op'] in ('peer_add', 'peer_remove'):
869893
# NOTE: ZFL 35 commit was not atomic with API change
870894
# (ZFL >=35 can use new plural style)
@@ -943,7 +967,9 @@ def notify_user(self, message: Message) -> str:
943967
{'mentioned', 'wildcard_mentioned'}.intersection(
944968
set(message['flags'])
945969
)
946-
or self.stream_dict[message['stream_id']]['desktop_notifications']
970+
or (not self.is_muted_stream(message['stream_id'])
971+
and self.is_desktop_notifications_enabled(
972+
message['stream_id']))
947973
):
948974
recipient = '{display_recipient} -> {subject}'.format(**message)
949975

0 commit comments

Comments
 (0)