diff --git a/zerver/lib/bugdown/__init__.py b/zerver/lib/bugdown/__init__.py index ee28c09c31..378780f175 100644 --- a/zerver/lib/bugdown/__init__.py +++ b/zerver/lib/bugdown/__init__.py @@ -507,6 +507,26 @@ class Emoji(markdown.inlinepatterns.Pattern): else: return None +class StreamSubscribeButton(markdown.inlinepatterns.Pattern): + # This markdown extension has required javascript in + # static/js/custom_markdown.js + def handleMatch(self, match): + stream_name = match.group('stream_name') + stream_name = stream_name.replace('\\)', ')').replace('\\\\', '\\') + + span = markdown.util.etree.Element('span') + span.set('class', 'inline-subscribe') + span.set('data-stream-name', stream_name) + + button = markdown.util.etree.SubElement(span, 'button') + button.text = 'Subscribe to ' + stream_name + button.set('class', 'inline-subscribe-button zulip-button') + + error = markdown.util.etree.SubElement(span, 'span') + error.set('class', 'inline-subscribe-error') + + return span + upload_re = re.compile(r"^(?:https://%s.s3.amazonaws.com|/user_uploads/\d+)/[^/]*/([^/]*)$" % (settings.S3_BUCKET,)) def url_filename(url): """Extract the filename if a URL is an uploaded file, or return the original URL""" @@ -791,6 +811,8 @@ class Bugdown(markdown.Extension): md.inlinePatterns.add('avatar', Avatar(r'!avatar\((?P[^)]*)\)'), '_begin') md.inlinePatterns.add('gravatar', Avatar(r'!gravatar\((?P[^)]*)\)'), '_begin') + md.inlinePatterns.add('stream_subscribe_button', StreamSubscribeButton(r'!_stream_subscribe_button\((?P(?:[^)\\]|\\\)|\\)*)\)'), '_begin') + md.inlinePatterns.add('usermention', UserMentionPattern(mention.find_mentions), '>backtick') md.inlinePatterns.add('emoji', Emoji(r'(?:[^:\s]+:)(?!\w)'), '_end') md.inlinePatterns.add('link', AtomicLinkPattern(markdown.inlinepatterns.LINK_RE, md), '>backtick') diff --git a/zerver/tests.py b/zerver/tests.py index 141cf1e97c..4e79a0af96 100644 --- a/zerver/tests.py +++ b/zerver/tests.py @@ -2043,6 +2043,61 @@ class SubscriptionAPITest(AuthedTestCase): add_streams, self.streams, self.test_email, self.streams + add_streams) self.assertEqual(len(events), 1) + def test_successful_subscriptions_notifies(self): + """ + Calling /json/subscriptions/add should notify when a new stream is created. + """ + invitee = "iago@zulip.com" + invitee_full_name = 'Iago' + + current_stream = self.get_streams(invitee)[0] + invite_streams = self.make_random_stream_names(current_stream)[:1] + result = self.common_subscribe_to_streams( + invitee, + invite_streams, + extra_post_data={ + 'announce': 'true', + 'principals': '["%s"]' % (self.user_profile.email,) + }, + ) + self.assert_json_success(result) + + msg = Message.objects.latest('id') + self.assertEqual(msg.sender_id, + get_user_profile_by_email('notification-bot@zulip.com').id) + expected_msg = "Hi there! %s just created a new stream '%s'. " \ + "!_stream_subscribe_button(%s)" % (invitee_full_name, + invite_streams[0], + invite_streams[0]) + self.assertEqual(msg.content, expected_msg) + + def test_successful_subscriptions_notifies_with_escaping(self): + """ + Calling /json/subscriptions/add should notify when a new stream is created. + """ + invitee = "iago@zulip.com" + invitee_full_name = 'Iago' + + invite_streams = ['strange ) \\ test'] + result = self.common_subscribe_to_streams( + invitee, + invite_streams, + extra_post_data={ + 'announce': 'true', + 'principals': '["%s"]' % (self.user_profile.email,) + }, + ) + self.assert_json_success(result) + + msg = Message.objects.latest('id') + self.assertEqual(msg.sender_id, + get_user_profile_by_email('notification-bot@zulip.com').id) + expected_msg = "Hi there! %s just created a new stream '%s'. " \ + "!_stream_subscribe_button(strange \\) \\\\ test)" % ( + invitee_full_name, + invite_streams[0]) + self.assertEqual(msg.content, expected_msg) + def test_non_ascii_stream_subscription(self): """ Subscribing to a stream name with non-ASCII characters succeeds. @@ -4825,6 +4880,58 @@ Content Cell | Content Cell self.assertEqual(converted, expected) + def test_stream_subscribe_button_simple(self): + msg = '!_stream_subscribe_button(simple)' + converted = bugdown_convert(msg) + self.assertEqual( + converted, + '

' + '' + '' + '' + '' + '

' + ) + + def test_stream_subscribe_button_in_name(self): + msg = '!_stream_subscribe_button(simple (not\\))' + converted = bugdown_convert(msg) + self.assertEqual( + converted, + '

' + '' + '' + '' + '' + '

' + ) + + def test_stream_subscribe_button_after_name(self): + msg = '!_stream_subscribe_button(simple) (not)' + converted = bugdown_convert(msg) + self.assertEqual( + converted, + '

' + '' + '' + '' + '' + ' (not)

' + ) + + def test_stream_subscribe_button_slash(self): + msg = '!_stream_subscribe_button(simple\\\\)' + converted = bugdown_convert(msg) + self.assertEqual( + converted, + '

' + '' + '' + '' + '' + '

' + ) + class UserPresenceTests(AuthedTestCase): def test_get_empty(self): self.login("hamlet@zulip.com") diff --git a/zerver/views/__init__.py b/zerver/views/__init__.py index 65a023b72e..0027f3f403 100644 --- a/zerver/views/__init__.py +++ b/zerver/views/__init__.py @@ -1145,6 +1145,11 @@ def stream_link(stream_name): "Escapes a stream name to make a #narrow/stream/stream_name link" return "#narrow/stream/%s" % (urllib.quote(stream_name.encode('utf-8')),) +def stream_button(stream_name): + stream_name = stream_name.replace('\\', '\\\\') + stream_name = stream_name.replace(')', '\\)') + return '!_stream_subscribe_button(%s)' % (stream_name,) + @has_request_variables def add_subscriptions_backend(request, user_profile, streams_raw = REQ("subscriptions", @@ -1236,16 +1241,17 @@ def add_subscriptions_backend(request, user_profile, (", ".join('`%s`' % (s.name,) for s in created_streams),) else: stream_msg = "a new stream `%s`" % (created_streams[0].name) - msg = ("%s just created %s. To join, visit your [Streams page](#subscriptions)." - % (user_profile.full_name, stream_msg)) + + stream_buttons = ' '.join(stream_button(s.name for s in created_streams)) + msg = ("%s just created %s. %s" % (user_profile.full_name, + stream_msg, stream_buttons)) notifications.append(internal_prep_message(settings.NOTIFICATION_BOT, "stream", notifications_stream.name, "Streams", msg, realm=notifications_stream.realm)) else: - msg = ("Hi there! %s just created a new stream '%s'. " - "To join, click the gear in the left-side streams list." - % (user_profile.full_name, created_streams[0].name)) + msg = ("Hi there! %s just created a new stream '%s'. %s" + % (user_profile.full_name, created_streams[0].name, stream_button(created_streams[0].name))) for realm_user_dict in get_active_user_dicts_in_realm(user_profile.realm): # Don't announce to yourself or to people you explicitly added # (who will get the notification above instead).