Skip to content

Commit 966d88a

Browse files
showelltimabbott
authored andcommitted
stream colors: Fix stream color assignment.
The bug here probably didn't come up too much in practice, but if we were adding a user to multiple streams when they already had used all N available colors, all the new streams would be assigned the same color, since the size of used_colors would stay at N, thwarting our little modulo-len hackery. It's not a terrible bug, since users can obviously customize their stream colors as they see fit. Usually when we are adding a user to multiple streams, the users are fairly new, and thus don't have many existing streams, so I have never heard this bug reported in the field. Anyway, assigning the colors in bulk seems to make more sense, and I added some tests. For the situations where all the colors have already been used, I didn't put a ton of thought into exactly which repeated colors we want to choose; instead, I just ensure they're different modulo 24. It's possible that we should just have more than 24 canned colors, or we should just assign the same default color every time and let users change it themselves (once they've gone beyond the 24, to be clear). Or maybe we can just do something smarter here. I don't have enough time for a deep dive on this issue.
1 parent fe3295d commit 966d88a

File tree

2 files changed

+88
-14
lines changed

2 files changed

+88
-14
lines changed

zerver/lib/actions.py

Lines changed: 35 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3596,14 +3596,32 @@ def internal_send_huddle_message(
35963596
return message_ids[0]
35973597

35983598

3599-
def pick_color(used_colors: Set[str]) -> str:
3600-
# These colors are shared with the palette in stream_settings_ui.js.
3599+
def pick_colors(
3600+
used_colors: Set[str], color_map: Dict[int, str], recipient_ids: List[int]
3601+
) -> Dict[int, str]:
3602+
used_colors = set(used_colors)
3603+
recipient_ids = sorted(recipient_ids)
3604+
result = {}
3605+
3606+
other_recipient_ids = []
3607+
for recipient_id in recipient_ids:
3608+
if recipient_id in color_map:
3609+
color = color_map[recipient_id]
3610+
result[recipient_id] = color
3611+
used_colors.add(color)
3612+
else:
3613+
other_recipient_ids.append(recipient_id)
3614+
36013615
available_colors = [s for s in STREAM_ASSIGNMENT_COLORS if s not in used_colors]
36023616

3603-
if available_colors:
3604-
return available_colors[0]
3605-
else:
3606-
return STREAM_ASSIGNMENT_COLORS[len(used_colors) % len(STREAM_ASSIGNMENT_COLORS)]
3617+
for i, recipient_id in enumerate(other_recipient_ids):
3618+
if i < len(available_colors):
3619+
color = available_colors[i]
3620+
else:
3621+
color = STREAM_ASSIGNMENT_COLORS[i % len(STREAM_ASSIGNMENT_COLORS)]
3622+
result[recipient_id] = color
3623+
3624+
return result
36073625

36083626

36093627
def validate_user_access_to_subscribers(
@@ -3858,6 +3876,12 @@ def bulk_add_subscriptions(
38583876
recipient_ids = [stream.recipient_id for stream in streams]
38593877
recipient_id_to_stream = {stream.recipient_id: stream for stream in streams}
38603878

3879+
recipient_color_map = {}
3880+
for stream in streams:
3881+
color: Optional[str] = color_map.get(stream.name, None)
3882+
if color is not None:
3883+
recipient_color_map[stream.recipient_id] = color
3884+
38613885
used_colors_for_user_ids: Dict[int, Set[str]] = get_used_colors_for_user_ids(user_ids)
38623886

38633887
existing_subs = Subscription.objects.filter(
@@ -3875,12 +3899,11 @@ def bulk_add_subscriptions(
38753899
subs_to_add: List[SubInfo] = []
38763900
for user_profile in users:
38773901
my_subs = subs_by_user[user_profile.id]
3878-
used_colors = used_colors_for_user_ids.get(user_profile.id, set())
38793902

38803903
# Make a fresh set of all new recipient ids, and then we will
38813904
# remove any for which our user already has a subscription
38823905
# (and we'll re-activate any subscriptions as needed).
3883-
new_recipient_ids = {stream.recipient_id for stream in streams}
3906+
new_recipient_ids: Set[int] = {stream.recipient_id for stream in streams}
38843907

38853908
for sub in my_subs:
38863909
if sub.recipient_id in new_recipient_ids:
@@ -3892,14 +3915,12 @@ def bulk_add_subscriptions(
38923915
else:
38933916
subs_to_activate.append(sub_info)
38943917

3918+
used_colors = used_colors_for_user_ids.get(user_profile.id, set())
3919+
user_color_map = pick_colors(used_colors, recipient_color_map, list(new_recipient_ids))
3920+
38953921
for recipient_id in new_recipient_ids:
38963922
stream = recipient_id_to_stream[recipient_id]
3897-
3898-
if stream.name in color_map:
3899-
color = color_map[stream.name]
3900-
else:
3901-
color = pick_color(used_colors)
3902-
used_colors.add(color)
3923+
color = user_color_map[recipient_id]
39033924

39043925
sub = Subscription(
39053926
user_profile=user_profile,

zerver/tests/test_subs.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from django.utils.timezone import now as timezone_now
1212

1313
from zerver.lib.actions import (
14+
STREAM_ASSIGNMENT_COLORS,
1415
bulk_add_subscriptions,
1516
bulk_get_subscriber_user_ids,
1617
bulk_remove_subscriptions,
@@ -38,6 +39,7 @@
3839
get_default_streams_for_realm,
3940
get_topic_messages,
4041
lookup_default_stream_groups,
42+
pick_colors,
4143
round_to_2_significant_digits,
4244
validate_user_access_to_subscribers_helper,
4345
)
@@ -97,6 +99,57 @@ def test_test_helper(self) -> None:
9799
self.assertIn("* Verona", s)
98100
self.assertNotIn("* Denmark", s)
99101

102+
def test_pick_colors(self) -> None:
103+
used_colors: Set[str] = set()
104+
color_map: Dict[int, str] = {}
105+
recipient_ids = list(range(30))
106+
user_color_map = pick_colors(used_colors, color_map, recipient_ids)
107+
self.assertEqual(
108+
user_color_map,
109+
{
110+
0: "#76ce90",
111+
1: "#fae589",
112+
2: "#a6c7e5",
113+
3: "#e79ab5",
114+
4: "#bfd56f",
115+
5: "#f4ae55",
116+
6: "#b0a5fd",
117+
7: "#addfe5",
118+
8: "#f5ce6e",
119+
9: "#c2726a",
120+
10: "#94c849",
121+
11: "#bd86e5",
122+
12: "#ee7e4a",
123+
13: "#a6dcbf",
124+
14: "#95a5fd",
125+
15: "#53a063",
126+
16: "#9987e1",
127+
17: "#e4523d",
128+
18: "#c2c2c2",
129+
19: "#4f8de4",
130+
20: "#c6a8ad",
131+
21: "#e7cc4d",
132+
22: "#c8bebf",
133+
23: "#a47462",
134+
# start repeating
135+
24: "#76ce90",
136+
25: "#fae589",
137+
26: "#a6c7e5",
138+
27: "#e79ab5",
139+
28: "#bfd56f",
140+
29: "#f4ae55",
141+
},
142+
)
143+
144+
color_map = {98: "color98", 99: "color99"}
145+
used_colors = set(STREAM_ASSIGNMENT_COLORS) - {"#c6a8ad", "#9987e1"}
146+
recipient_ids = [99, 98, 1, 2, 3, 4]
147+
user_color_map = pick_colors(used_colors, color_map, recipient_ids)
148+
self.assertEqual(
149+
user_color_map,
150+
{98: "color98", 99: "color99", 1: "#9987e1", 2: "#c6a8ad", 3: "#a6c7e5", 4: "#e79ab5"},
151+
)
152+
100153
def test_empty_results(self) -> None:
101154
# These are essentially just tests to ensure line
102155
# coverage for codepaths that won't ever really be

0 commit comments

Comments
 (0)