From 2372aa0e6b27ca22826f0f5561e3a13aba239fa1 Mon Sep 17 00:00:00 2001 From: sahil839 Date: Mon, 17 Feb 2020 02:50:18 +0530 Subject: [PATCH] stream settings: Stream_post_policy field for restricting reactions. Option to stream_post_policy is added for restricting posting and reacting to admins only. Fixes #12835 --- frontend_tests/node_tests/stream_data.js | 37 ++++++++++++++++++++-- frontend_tests/node_tests/stream_events.js | 2 ++ static/generated/pygments_data.js | 12 +++++++ static/js/click_handlers.js | 11 +++++-- static/js/compose.js | 3 +- static/js/emoji_picker.js | 12 +++++-- static/js/message_list_view.js | 2 ++ static/js/popovers.js | 12 ++++++- static/js/stream_data.js | 13 ++++++++ static/js/stream_events.js | 1 + static/js/stream_ui_updates.js | 9 +++++- static/styles/reactions.scss | 20 +++++++++--- static/styles/zulip.scss | 2 +- static/templates/message_body.hbs | 2 +- static/templates/message_controls.hbs | 5 +-- static/templates/message_reaction.hbs | 3 +- static/templates/message_reactions.hbs | 2 +- static/templates/subscription_type.hbs | 2 ++ zerver/lib/actions.py | 14 +++++--- zerver/lib/streams.py | 6 ++-- zerver/models.py | 5 ++- zerver/tests/test_reactions.py | 29 ++++++++++++++++- zerver/tests/test_subs.py | 28 ++++++++++++++++ zerver/views/reactions.py | 9 +++++- 24 files changed, 213 insertions(+), 28 deletions(-) create mode 100644 static/generated/pygments_data.js diff --git a/frontend_tests/node_tests/stream_data.js b/frontend_tests/node_tests/stream_data.js index 04f2ba5a246823..c3cb04b29af199 100644 --- a/frontend_tests/node_tests/stream_data.js +++ b/frontend_tests/node_tests/stream_data.js @@ -477,8 +477,8 @@ run_test('stream_settings', () => { assert.equal(sub_rows[2].invite_only, false); assert.equal(sub_rows[0].history_public_to_subscribers, true); - assert.equal(sub_rows[0].stream_post_policy === - stream_data.stream_post_policy_values.admins.code, true); + assert.equal(sub_rows[0].stream_post_policy, + stream_data.stream_post_policy_values.admins.code); const sub = stream_data.get_sub('a'); stream_data.update_stream_privacy(sub, { @@ -991,3 +991,36 @@ run_test('all_topics_in_cache', () => { sub.first_message_id = 2; assert.equal(stream_data.all_topics_in_cache(sub), true); }); + +run_test('get_restrict_emoji_reaction', () => { + const general = { + name: 'general', + stream_id: 1, + stream_post_policy: stream_data.stream_post_policy_values.everyone.code, + }; + + const test = { + name: 'test', + stream_id: 1, + stream_post_policy: stream_data.stream_post_policy_values.admins_can_post_and_react.code, + }; + + stream_data.add_sub(general); + stream_data.add_sub(test); + + page_params.is_admin = false; + + let restrict_emoji_reaction = stream_data.get_restrict_emoji_reaction('general'); + assert.equal(restrict_emoji_reaction, false); + + restrict_emoji_reaction = stream_data.get_restrict_emoji_reaction('test'); + assert.equal(restrict_emoji_reaction, true); + + page_params.is_admin = true; + + restrict_emoji_reaction = stream_data.get_restrict_emoji_reaction('general'); + assert.equal(restrict_emoji_reaction, false); + + restrict_emoji_reaction = stream_data.get_restrict_emoji_reaction('test'); + assert.equal(restrict_emoji_reaction, false); +}); diff --git a/frontend_tests/node_tests/stream_events.js b/frontend_tests/node_tests/stream_events.js index df64af8cb5bcdb..8492cef900b38f 100644 --- a/frontend_tests/node_tests/stream_events.js +++ b/frontend_tests/node_tests/stream_events.js @@ -3,6 +3,7 @@ const return_true = function () { return true; }; set_global('$', global.make_zjquery()); set_global('document', 'document-stub'); set_global('i18n', global.stub_i18n); +set_global('current_msg_list', {rerender: noop}); set_global('colorspace', { sRGB_to_linear: noop, @@ -14,6 +15,7 @@ set_global('colorspace', { zrequire('people'); zrequire('stream_data'); zrequire('stream_events'); + const with_overrides = global.with_overrides; const george = { diff --git a/static/generated/pygments_data.js b/static/generated/pygments_data.js new file mode 100644 index 00000000000000..1c22119b401a45 --- /dev/null +++ b/static/generated/pygments_data.js @@ -0,0 +1,12 @@ +var pygments_data = (function () { + +var exports = {}; + +exports.langs = {"abap": 27, "ada": 25, "awk": 1, "bash": 7, "c": 49, "c#": 47, "c++": 48, "cobol": 26, "cpp": 48, "csharp": 47, "css": 48, "d": 29, "dart": 28, "delphi": 42, "erlang": 10, "fsharp": 19, "go": 34, "groovy": 13, "haskell": 15, "html": 30, "java": 50, "javascript": 51, "js": 43, "julia": 4, "latex": 40, "lisp": 18, "lua": 22, "mask": 2, "math": 50, "matlab": 33, "mql": 9, "mql4": 9, "objective-c": 35, "objectivec": 35, "objectpascal": 42, "pascal": 42, "perl": 40, "php": 44, "pl": 40, "prolog": 16, "python": 46, "quote": 50, "r": 37, "rb": 39, "ruby": 39, "rust": 8, "sas": 30, "scala": 21, "scheme": 14, "sql": 32, "swift": 41, "tex": 40, "vb.net": 45, "vbnet": 45, "xml": 1, "apl": 0, "abnf": 0, "as3": 0, "actionscript3": 0, "as": 0, "actionscript": 0, "ada95": 0, "ada2005": 0, "adl": 0, "agda": 0, "aheui": 0, "alloy": 0, "at": 0, "ambienttalk": 0, "ambienttalk/2": 0, "ampl": 0, "html+ng2": 0, "ng2": 0, "antlr-as": 0, "antlr-actionscript": 0, "antlr-csharp": 0, "antlr-c#": 0, "antlr-cpp": 0, "antlr-java": 0, "antlr": 0, "antlr-objc": 0, "antlr-perl": 0, "antlr-python": 0, "antlr-ruby": 0, "antlr-rb": 0, "apacheconf": 0, "aconf": 0, "apache": 0, "applescript": 0, "arduino": 0, "aspectj": 0, "asy": 0, "asymptote": 0, "augeas": 0, "autoit": 0, "ahk": 0, "autohotkey": 0, "gawk": 0, "mawk": 0, "nawk": 0, "bbcbasic": 0, "bbcode": 0, "bc": 0, "bst": 0, "bst-pybtex": 0, "basemake": 0, "sh": 0, "ksh": 0, "zsh": 0, "shell": 0, "console": 0, "shell-session": 0, "bat": 0, "batch": 0, "dosbatch": 0, "winbatch": 0, "befunge": 0, "bib": 0, "bibtex": 0, "blitzbasic": 0, "b3d": 0, "bplus": 0, "blitzmax": 0, "bmax": 0, "bnf": 0, "boa": 0, "boo": 0, "boogie": 0, "brainfuck": 0, "bf": 0, "bugs": 0, "winbugs": 0, "openbugs": 0, "camkes": 0, "idl4": 0, "cmake": 0, "c-objdump": 0, "cpsa": 0, "aspx-cs": 0, "ca65": 0, "cadl": 0, "capdl": 0, "capnp": 0, "cbmbas": 0, "ceylon": 0, "cfengine3": 0, "cf3": 0, "chai": 0, "chaiscript": 0, "chapel": 0, "chpl": 0, "charmci": 0, "html+cheetah": 0, "html+spitfire": 0, "htmlcheetah": 0, "js+cheetah": 0, "javascript+cheetah": 0, "js+spitfire": 0, "javascript+spitfire": 0, "cheetah": 0, "spitfire": 0, "xml+cheetah": 0, "xml+spitfire": 0, "cirru": 0, "clay": 0, "clean": 0, "clojure": 0, "clj": 0, "clojurescript": 0, "cljs": 0, "cobolfree": 0, "coffee-script": 0, "coffeescript": 0, "coffee": 0, "cfc": 0, "cfm": 0, "cfs": 0, "common-lisp": 0, "cl": 0, "componentpascal": 0, "cp": 0, "coq": 0, "cpp-objdump": 0, "c++-objdumb": 0, "cxx-objdump": 0, "crmsh": 0, "pcmk": 0, "croc": 0, "cryptol": 0, "cry": 0, "cr": 0, "crystal": 0, "csound-document": 0, "csound-csd": 0, "csound": 0, "csound-orc": 0, "csound-score": 0, "csound-sco": 0, "css+django": 0, "css+jinja": 0, "css+erb": 0, "css+ruby": 0, "css+genshitext": 0, "css+genshi": 0, "css+php": 0, "css+smarty": 0, "cuda": 0, "cu": 0, "cypher": 0, "cython": 0, "pyx": 0, "pyrex": 0, "d-objdump": 0, "dpatch": 0, "dasm16": 0, "control": 0, "debcontrol": 0, "pas": 0, "dg": 0, "diff": 0, "udiff": 0, "django": 0, "jinja": 0, "docker": 0, "dockerfile": 0, "dtd": 0, "duel": 0, "jbst": 0, "jsonml+bst": 0, "dylan-console": 0, "dylan-repl": 0, "dylan": 0, "dylan-lid": 0, "lid": 0, "ecl": 0, "ec": 0, "earl-grey": 0, "earlgrey": 0, "eg": 0, "easytrieve": 0, "ebnf": 0, "eiffel": 0, "iex": 0, "elixir": 0, "ex": 0, "exs": 0, "elm": 0, "emacs": 0, "elisp": 0, "emacs-lisp": 0, "email": 0, "eml": 0, "erb": 0, "erl": 0, "html+evoque": 0, "evoque": 0, "xml+evoque": 0, "ezhil": 0, "f#": 0, "factor": 0, "fancy": 0, "fy": 0, "fan": 0, "felix": 0, "flx": 0, "fennel": 0, "fnl": 0, "fish": 0, "fishshell": 0, "flatline": 0, "floscript": 0, "flo": 0, "forth": 0, "fortranfixed": 0, "fortran": 0, "foxpro": 0, "vfp": 0, "clipper": 0, "xbase": 0, "freefem": 0, "gap": 0, "glsl": 0, "gas": 0, "asm": 0, "genshi": 0, "kid": 0, "xml+genshi": 0, "xml+kid": 0, "genshitext": 0, "pot": 0, "po": 0, "cucumber": 0, "gherkin": 0, "gnuplot": 0, "golo": 0, "gooddata-cl": 0, "gosu": 0, "gst": 0, "groff": 0, "nroff": 0, "man": 0, "hlsl": 0, "haml": 0, "html+handlebars": 0, "handlebars": 0, "hs": 0, "hx": 0, "haxe": 0, "hxsl": 0, "hexdump": 0, "hsail": 0, "hsa": 0, "hspec": 0, "html+django": 0, "html+jinja": 0, "htmldjango": 0, "html+genshi": 0, "html+kid": 0, "html+php": 0, "html+smarty": 0, "http": 0, "haxeml": 0, "hxml": 0, "hylang": 0, "hybris": 0, "hy": 0, "idl": 0, "icon": 0, "idris": 0, "idr": 0, "igor": 0, "igorpro": 0, "inform6": 0, "i6": 0, "i6t": 0, "inform7": 0, "i7": 0, "ini": 0, "cfg": 0, "dosini": 0, "io": 0, "ioke": 0, "ik": 0, "irc": 0, "isabelle": 0, "j": 0, "jags": 0, "jasmin": 0, "jasminxt": 0, "js+django": 0, "javascript+django": 0, "js+jinja": 0, "javascript+jinja": 0, "js+erb": 0, "javascript+erb": 0, "js+ruby": 0, "javascript+ruby": 0, "js+genshitext": 0, "js+genshi": 0, "javascript+genshitext": 0, "javascript+genshi": 0, "js+php": 0, "javascript+php": 0, "js+smarty": 0, "javascript+smarty": 0, "jcl": 0, "jsgf": 0, "json-object": 0, "jsonld": 0, "json-ld": 0, "json": 0, "jsp": 0, "jlcon": 0, "jl": 0, "juttle": 0, "kal": 0, "kconfig": 0, "menuconfig": 0, "linux-config": 0, "kernel-config": 0, "koka": 0, "kotlin": 0, "lsl": 0, "css+lasso": 0, "html+lasso": 0, "js+lasso": 0, "javascript+lasso": 0, "lasso": 0, "lassoscript": 0, "xml+lasso": 0, "lean": 0, "less": 0, "lighty": 0, "lighttpd": 0, "limbo": 0, "liquid": 0, "lagda": 0, "literate-agda": 0, "lcry": 0, "literate-cryptol": 0, "lcryptol": 0, "lhs": 0, "literate-haskell": 0, "lhaskell": 0, "lidr": 0, "literate-idris": 0, "lidris": 0, "live-script": 0, "livescript": 0, "llvm": 0, "logos": 0, "logtalk": 0, "mime": 0, "moocode": 0, "moo": 0, "doscon": 0, "make": 0, "makefile": 0, "mf": 0, "bsdmake": 0, "css+mako": 0, "html+mako": 0, "js+mako": 0, "javascript+mako": 0, "mako": 0, "xml+mako": 0, "maql": 0, "md": 0, "mason": 0, "mathematica": 0, "mma": 0, "nb": 0, "matlabsession": 0, "minid": 0, "modelica": 0, "modula2": 0, "m2": 0, "trac-wiki": 0, "moin": 0, "monkey": 0, "monte": 0, "moon": 0, "moonscript": 0, "css+mozpreproc": 0, "mozhashpreproc": 0, "javascript+mozpreproc": 0, "mozpercentpreproc": 0, "xul+mozpreproc": 0, "mq4": 0, "mq5": 0, "mql5": 0, "mscgen": 0, "msc": 0, "mupad": 0, "mxml": 0, "mysql": 0, "css+myghty": 0, "html+myghty": 0, "js+myghty": 0, "javascript+myghty": 0, "myghty": 0, "xml+myghty": 0, "ncl": 0, "nsis": 0, "nsi": 0, "nsh": 0, "nasm": 0, "objdump-nasm": 0, "nemerle": 0, "nesc": 0, "newlisp": 0, "newspeak": 0, "nginx": 0, "nim": 0, "nimrod": 0, "nit": 0, "nixos": 0, "nix": 0, "notmuch": 0, "nusmv": 0, "numpy": 0, "objdump": 0, "obj-c": 0, "objc": 0, "objective-c++": 0, "objectivec++": 0, "obj-c++": 0, "objc++": 0, "objective-j": 0, "objectivej": 0, "obj-j": 0, "objj": 0, "ocaml": 0, "octave": 0, "odin": 0, "ooc": 0, "opa": 0, "openedge": 0, "abl": 0, "progress": 0, "pacmanconf": 0, "pan": 0, "parasail": 0, "pawn": 0, "perl6": 0, "pl6": 0, "php3": 0, "php4": 0, "php5": 0, "pig": 0, "pike": 0, "pkgconfig": 0, "plpgsql": 0, "pony": 0, "postscript": 0, "postscr": 0, "psql": 0, "postgresql-console": 0, "postgres-console": 0, "postgresql": 0, "postgres": 0, "pov": 0, "powershell": 0, "posh": 0, "ps1": 0, "psm1": 0, "ps1con": 0, "praat": 0, "properties": 0, "jproperties": 0, "protobuf": 0, "proto": 0, "pug": 0, "jade": 0, "puppet": 0, "pypylog": 0, "pypy": 0, "python2": 0, "py2": 0, "py2tb": 0, "pycon": 0, "py": 0, "sage": 0, "python3": 0, "py3": 0, "pytb": 0, "py3tb": 0, "qbasic": 0, "basic": 0, "qvto": 0, "qvt": 0, "qml": 0, "qbs": 0, "rconsole": 0, "rout": 0, "rnc": 0, "rng-compact": 0, "spec": 0, "racket": 0, "rkt": 0, "ragel-c": 0, "ragel-cpp": 0, "ragel-d": 0, "ragel-em": 0, "ragel-java": 0, "ragel": 0, "ragel-objc": 0, "ragel-ruby": 0, "ragel-rb": 0, "raw": 0, "rd": 0, "rebol": 0, "red": 0, "red/system": 0, "redcode": 0, "registry": 0, "resource": 0, "resourcebundle": 0, "rexx": 0, "arexx": 0, "rhtml": 0, "html+erb": 0, "html+ruby": 0, "roboconf-graph": 0, "roboconf-instances": 0, "robotframework": 0, "rql": 0, "rsl": 0, "rst": 0, "rest": 0, "restructuredtext": 0, "rts": 0, "trafficscript": 0, "rbcon": 0, "irb": 0, "duby": 0, "rs": 0, "splus": 0, "s": 0, "sml": 0, "sarl": 0, "sass": 0, "scaml": 0, "scdoc": 0, "scd": 0, "scm": 0, "scilab": 0, "scss": 0, "shexc": 0, "shex": 0, "shen": 0, "silver": 0, "slash": 0, "slim": 0, "slurm": 0, "sbatch": 0, "smali": 0, "smalltalk": 0, "squeak": 0, "st": 0, "sgf": 0, "smarty": 0, "snobol": 0, "snowball": 0, "solidity": 0, "sp": 0, "sourceslist": 0, "sources.list": 0, "debsources": 0, "sparql": 0, "sqlite3": 0, "squidconf": 0, "squid.conf": 0, "squid": 0, "ssp": 0, "stan": 0, "stata": 0, "do": 0, "sc": 0, "supercollider": 0, "swig": 0, "systemverilog": 0, "sv": 0, "tap": 0, "toml": 0, "tads3": 0, "tasm": 0, "tcl": 0, "tcsh": 0, "csh": 0, "tcshcon": 0, "tea": 0, "ttl": 0, "teraterm": 0, "teratermmacro": 0, "termcap": 0, "terminfo": 0, "terraform": 0, "tf": 0, "text": 0, "thrift": 0, "todotxt": 0, "tsql": 0, "t-sql": 0, "treetop": 0, "turtle": 0, "html+twig": 0, "twig": 0, "ts": 0, "typescript": 0, "typoscriptcssdata": 0, "typoscripthtmldata": 0, "typoscript": 0, "ucode": 0, "unicon": 0, "urbiscript": 0, "vbscript": 0, "vcl": 0, "vclsnippets": 0, "vclsnippet": 0, "vctreestatus": 0, "vgl": 0, "vala": 0, "vapi": 0, "aspx-vb": 0, "html+velocity": 0, "velocity": 0, "xml+velocity": 0, "verilog": 0, "v": 0, "vhdl": 0, "vim": 0, "wdiff": 0, "whiley": 0, "x10": 0, "xten": 0, "xquery": 0, "xqy": 0, "xq": 0, "xql": 0, "xqm": 0, "xml+django": 0, "xml+jinja": 0, "xml+erb": 0, "xml+ruby": 0, "xml+php": 0, "xml+smarty": 0, "xorg.conf": 0, "xslt": 0, "xtend": 0, "extempore": 0, "yaml+jinja": 0, "salt": 0, "sls": 0, "yaml": 0, "zeek": 0, "bro": 0, "zephir": 0, "zig": 0, "ipython2": 0, "ipython": 0, "ipython3": 0, "ipythonconsole": 0}; + +return exports; + +}()); +if (typeof module !== 'undefined') { + module.exports = pygments_data; +} \ No newline at end of file diff --git a/static/js/click_handlers.js b/static/js/click_handlers.js index c923255beb49de..75730892ac3821 100644 --- a/static/js/click_handlers.js +++ b/static/js/click_handlers.js @@ -191,7 +191,10 @@ exports.initialize = function () { e.stopPropagation(); const local_id = $(this).attr('data-reaction-id'); const message_id = rows.get_message_id(this); - reactions.process_reaction_click(message_id, local_id); + const message = current_msg_list.get(message_id); + if (!message.is_stream || !stream_data.get_restrict_emoji_reaction(message.stream)) { + reactions.process_reaction_click(message_id, local_id); + } $(".tooltip").remove(); }); @@ -230,7 +233,10 @@ exports.initialize = function () { const local_id = elem.attr('data-reaction-id'); const message_id = rows.get_message_id(e.currentTarget); const title = reactions.get_reaction_title_data(message_id, local_id); - + const message = current_msg_list.get(message_id); + if (message.is_stream && stream_data.get_restrict_emoji_reaction(message.stream)) { + $(this).closest(".message_reaction").find(".disable-reaction-button").show(); + } elem.tooltip({ title: title, trigger: 'hover', @@ -246,6 +252,7 @@ exports.initialize = function () { $('#main_div').on('mouseleave', '.message_reaction', function (e) { e.stopPropagation(); $(e.currentTarget).tooltip('destroy'); + $(this).closest(".message_reaction").find(".disable-reaction-button").hide(); }); // DESTROY PERSISTING TOOLTIPS ON HOVER diff --git a/static/js/compose.js b/static/js/compose.js index ef0127708a38d2..6ae0cc14a38257 100644 --- a/static/js/compose.js +++ b/static/js/compose.js @@ -493,7 +493,8 @@ function validate_stream_message_post_policy(stream_name) { const stream_post_permission_type = stream_data.stream_post_policy_values; const stream_post_policy = stream_data.get_stream_post_policy(stream_name); - if (stream_post_policy === stream_post_permission_type.admins.code) { + if (stream_post_policy === stream_post_permission_type.admins.code || + stream_post_policy === stream_post_permission_type.admins_can_post_and_react.code) { compose_error(i18n.t("Only organization admins are allowed to post to this stream.")); return false; } diff --git a/static/js/emoji_picker.js b/static/js/emoji_picker.js index 42bea26392ff1d..562436a3068352 100644 --- a/static/js/emoji_picker.js +++ b/static/js/emoji_picker.js @@ -684,14 +684,21 @@ exports.register_click_handlers = function () { $("#main_div").on("click", ".reaction_button", function (e) { e.stopPropagation(); - const message_id = rows.get_message_id(this); - exports.toggle_emoji_popover(this, message_id); + const message = current_msg_list.get(message_id); + if (!message.is_stream || !stream_data.get_restrict_emoji_reaction(message.stream)) { + exports.toggle_emoji_popover(this, message_id); + } }); $("#main_div").on("mouseenter", ".reaction_button", function (e) { e.stopPropagation(); + const message_id = rows.get_message_id(this); + const message = current_msg_list.get(message_id); + if (message.is_stream && stream_data.get_restrict_emoji_reaction(message.stream)) { + $(this).find(".disable-emoji-icon").show(); + } const elem = $(e.currentTarget); const title = i18n.t("Add emoji reaction"); elem.tooltip({ @@ -706,6 +713,7 @@ exports.register_click_handlers = function () { $('#main_div').on('mouseleave', '.reaction_button', function (e) { e.stopPropagation(); + $(this).find(".disable-emoji-icon").hide(); $(e.currentTarget).tooltip('hide'); }); diff --git a/static/js/message_list_view.js b/static/js/message_list_view.js index 0f7a54967867b2..30d5ae2ce792f4 100644 --- a/static/js/message_list_view.js +++ b/static/js/message_list_view.js @@ -352,6 +352,8 @@ MessageListView.prototype = { if (message_container.msg.stream) { message_container.background_color = stream_data.get_color(message_container.msg.stream); + message_container.restrict_emoji_reaction = + stream_data.get_restrict_emoji_reaction(message_container.msg.stream); } message_container.contains_mention = message_container.msg.mentioned; diff --git a/static/js/popovers.js b/static/js/popovers.js index f52be5e4dc6d93..19d377d7a3d255 100644 --- a/static/js/popovers.js +++ b/static/js/popovers.js @@ -101,6 +101,16 @@ function calculate_info_popover_placement(size, elt) { } } +function show_add_reaction_option(message) { + if (!message.sent_by_me) { + return false; + } + if (message.stream && stream_data.get_restrict_emoji_reaction(message.stream)) { + return false; + } + return true; +} + function get_custom_profile_field_data(user, field, field_types, dateFormat) { const field_value = people.get_custom_profile_data(user.user_id, field.id); const field_type = field.type; @@ -478,7 +488,7 @@ exports.toggle_actions_popover = function (element, id) { can_unmute_topic: can_unmute_topic, should_display_collapse: should_display_collapse, should_display_uncollapse: should_display_uncollapse, - should_display_add_reaction_option: message.sent_by_me, + should_display_add_reaction_option: show_add_reaction_option(message), should_display_edit_history_option: should_display_edit_history_option, conversation_time_uri: conversation_time_uri, narrowed: narrow_state.active(), diff --git a/static/js/stream_data.js b/static/js/stream_data.js index 8e0475186d2fbe..921455afbeba92 100644 --- a/static/js/stream_data.js +++ b/static/js/stream_data.js @@ -103,6 +103,10 @@ exports.stream_post_policy_values = { code: 3, description: i18n.t("Only organization full members can post"), }, + admins_can_post_and_react: { + code: 4, + description: i18n.t("Only organization administrators can post and react"), + }, }; exports.clear_subscriptions = function () { @@ -519,6 +523,15 @@ exports.get_stream_post_policy = function (stream_name) { return sub.stream_post_policy; }; +exports.get_restrict_emoji_reaction = function (stream_name) { + const stream_post_policy = exports.get_stream_post_policy(stream_name); + if (stream_post_policy === exports.stream_post_policy_values.admins_can_post_and_react.code + && !page_params.is_admin) { + return true; + } + return false; +}; + exports.all_topics_in_cache = function (sub) { // Checks whether this browser's cache of contiguous messages // (used to locally render narrows) in message_list.all has all diff --git a/static/js/stream_events.js b/static/js/stream_events.js index cca170fb521c3c..8ec77408f48698 100644 --- a/static/js/stream_events.js +++ b/static/js/stream_events.js @@ -55,6 +55,7 @@ exports.update_property = function (stream_id, property, value, other_values) { break; case 'stream_post_policy': subs.update_stream_post_policy(sub, value); + current_msg_list.rerender(); break; default: blueslip.warn("Unexpected subscription property type", {property: property, diff --git a/static/js/stream_ui_updates.js b/static/js/stream_ui_updates.js index e28c8bab8d9d3e..f6cd40bdfa7005 100644 --- a/static/js/stream_ui_updates.js +++ b/static/js/stream_ui_updates.js @@ -143,7 +143,14 @@ exports.update_stream_privacy_type_icon = function (sub) { exports.update_stream_subscription_type_text = function (sub) { const stream_settings = stream_edit.settings_for_sub(sub); - const html = render_subscription_type(sub); + const template_data = { + invite_only: sub.invite_only, + history_public_to_subscribers: sub.history_public_to_subscribers, + is_web_public: sub.is_web_public, + stream_post_policy: sub.stream_post_policy, + stream_post_policy_values: stream_data.stream_post_policy_values, + }; + const html = render_subscription_type(template_data); if (stream_edit.is_sub_settings_active(sub)) { stream_settings.find('.subscription-type-text').expectOne().html(html); } diff --git a/static/styles/reactions.scss b/static/styles/reactions.scss index 9755f6dbfe7f2f..9f43472a48e31a 100644 --- a/static/styles/reactions.scss +++ b/static/styles/reactions.scss @@ -13,12 +13,13 @@ background-color: hsl(0, 0%, 100%); border: 1px solid hsl(194, 37%, 84%); border-radius: 4px; + position: relative; &.reacted { background-color: hsl(195, 50%, 95%); } - &:hover { + &:not(.disabled):hover { border: 1px solid hsl(200, 100%, 40%); } @@ -64,7 +65,7 @@ color: hsl(0, 0%, 33%); } - &:hover .message_reaction + .reaction_button { + &:not(.disabled):hover .message_reaction + .reaction_button { visibility: visible; pointer-events: all; background-color: hsl(0, 0%, 98%); @@ -76,7 +77,7 @@ margin-right: 3px; } - &:hover i { + &:not(.disabled):hover i { color: hsl(200, 100%, 40%); } @@ -84,7 +85,7 @@ display: none; } - &:hover { + &:not(.disabled):hover { border: 1px solid hsl(200, 100%, 40%); background-color: hsl(195, 50%, 95%); cursor: pointer; @@ -296,3 +297,14 @@ .typeahead .emoji { top: 2px; } + +.disable-emoji-icon, +.disable-reaction-button { + display: none; + position: absolute; + width: 100%; + height: 2px; + background-color: hsl(0, 100%, 0%); + top: 8px; + left: 0px; +} diff --git a/static/styles/zulip.scss b/static/styles/zulip.scss index 04ed4c44de4827..6eb4dca4762bde 100644 --- a/static/styles/zulip.scss +++ b/static/styles/zulip.scss @@ -647,7 +647,7 @@ td.pointer { display: inline-block; position: relative; color: hsl(0, 0%, 73%); - &:hover { + &:not(.disabled):hover { color: hsl(200, 100%, 40%); } } diff --git a/static/templates/message_body.hbs b/static/templates/message_body.hbs index c79f2a746ac2d1..27d5c50eeff2d2 100644 --- a/static/templates/message_body.hbs +++ b/static/templates/message_body.hbs @@ -48,4 +48,4 @@
{{t "[More...]" }}
{{t "[Condense message]" }}
-
{{> message_reactions }}
+
{{> message_reactions}}
diff --git a/static/templates/message_controls.hbs b/static/templates/message_controls.hbs index d52e502b8fae2e..cb49b51ba58832 100644 --- a/static/templates/message_controls.hbs +++ b/static/templates/message_controls.hbs @@ -4,8 +4,9 @@ {{/if}} {{#unless msg/sent_by_me}} -
- +
+ +
{{/unless}} diff --git a/static/templates/message_reaction.hbs b/static/templates/message_reaction.hbs index 2e0c5c8dbc4a2c..cb5b75e1b4dd7c 100644 --- a/static/templates/message_reaction.hbs +++ b/static/templates/message_reaction.hbs @@ -1,4 +1,4 @@ -
+
{{#if this.emoji_alt_code}}
 :{{this.emoji_name}}:
{{else}} @@ -8,5 +8,6 @@
{{/if}} {{/if}} +
{{this.count}}
diff --git a/static/templates/message_reactions.hbs b/static/templates/message_reactions.hbs index 995516fd80057a..d31d63666249cd 100644 --- a/static/templates/message_reactions.hbs +++ b/static/templates/message_reactions.hbs @@ -1,5 +1,5 @@ {{#each this/msg/message_reactions}} -{{> message_reaction}} +{{> message_reaction restrict_emoji_reaction = ../restrict_emoji_reaction}} {{/each}}
diff --git a/static/templates/subscription_type.hbs b/static/templates/subscription_type.hbs index a453813a7b24f3..8b395e2b492bc3 100644 --- a/static/templates/subscription_type.hbs +++ b/static/templates/subscription_type.hbs @@ -10,6 +10,8 @@ {{/if}} {{#if (eq stream_post_policy stream_post_policy_values.admins.code)}} {{t 'Only organization administrators can post.'}} +{{else if (eq stream_post_policy stream_post_policy_values.admins_can_post_and_react.code)}} +{{t 'Only orgainzation administrators can post and react'}} {{else if (eq stream_post_policy stream_post_policy_values.non_new_members.code)}} {{t 'Only organization full members can post.'}} {{else}} diff --git a/zerver/lib/actions.py b/zerver/lib/actions.py index 2b05f12ac325e4..7e7b226880b9b1 100644 --- a/zerver/lib/actions.py +++ b/zerver/lib/actions.py @@ -2274,7 +2274,8 @@ def validate_sender_can_write_to_stream(sender: UserProfile, elif sender.is_bot and (sender.bot_owner is not None and sender.bot_owner.is_realm_admin): pass - elif stream.stream_post_policy == Stream.STREAM_POST_POLICY_ADMINS: + elif (stream.stream_post_policy == Stream.STREAM_POST_POLICY_ADMINS or + stream.stream_post_policy == Stream.STREAM_POST_POLICY_ADMINS_CAN_POST_AND_REACT): raise JsonableError(_("Only organization administrators can send to this stream.")) elif stream.stream_post_policy == Stream.STREAM_POST_POLICY_RESTRICT_NEW_MEMBERS: if sender.is_bot and (sender.bot_owner is not None and @@ -3653,11 +3654,14 @@ def do_change_stream_post_policy(stream: Stream, stream_post_policy: int) -> Non # is_announcement_only property in early 2020, but we send a # duplicate event for legacy mobile clients that might want the # data. + is_announcement_only_value = (stream.stream_post_policy == Stream.STREAM_POST_POLICY_ADMINS or + stream.stream_post_policy == + Stream.STREAM_POST_POLICY_ADMINS_CAN_POST_AND_REACT) event = dict( op="update", type="stream", property="is_announcement_only", - value=stream.stream_post_policy == Stream.STREAM_POST_POLICY_ADMINS, + value=is_announcement_only_value, stream_id=stream.id, name=stream.name, ) @@ -4865,7 +4869,8 @@ def gather_subscriptions_helper(user_profile: UserProfile, # updated for the is_announcement_only -> stream_post_policy # migration. stream_dict['is_announcement_only'] = \ - stream['stream_post_policy'] == Stream.STREAM_POST_POLICY_ADMINS + stream['stream_post_policy'] == Stream.STREAM_POST_POLICY_ADMINS or \ + stream['stream_post_policy'] == Stream.STREAM_POST_POLICY_ADMINS_CAN_POST_AND_REACT # Add a few computed fields not directly from the data models. stream_dict['is_old_stream'] = is_old_stream(stream["date_created"]) @@ -4916,7 +4921,8 @@ def gather_subscriptions_helper(user_profile: UserProfile, stream["id"], stream["date_created"], recent_traffic) # Backwards-compatibility addition of removed field. stream_dict['is_announcement_only'] = \ - stream['stream_post_policy'] == Stream.STREAM_POST_POLICY_ADMINS + stream['stream_post_policy'] == Stream.STREAM_POST_POLICY_ADMINS or \ + stream['stream_post_policy'] == Stream.STREAM_POST_POLICY_ADMINS_CAN_POST_AND_REACT if is_public or user_profile.is_realm_admin: subscribers = subscriber_map[stream["id"]] diff --git a/zerver/lib/streams.py b/zerver/lib/streams.py index 65c774d743312b..45509ce72c0c68 100644 --- a/zerver/lib/streams.py +++ b/zerver/lib/streams.py @@ -262,8 +262,10 @@ def list_to_streams(streams_raw: Iterable[Mapping[str, Any]], stream = existing_stream_map.get(stream_name.lower()) if stream is None: # Non admins cannot create STREAM_POST_POLICY_ADMINS streams. - if ((stream_dict.get("stream_post_policy", False) == - Stream.STREAM_POST_POLICY_ADMINS) and not user_profile.is_realm_admin): + if (((stream_dict.get("stream_post_policy", False) == + Stream.STREAM_POST_POLICY_ADMINS) or (stream_dict.get("stream_post_policy", False) == + Stream.STREAM_POST_POLICY_ADMINS_CAN_POST_AND_REACT)) + and not user_profile.is_realm_admin): member_creating_announcement_only_stream = True # New members cannot create STREAM_POST_POLICY_RESTRICT_NEW_MEMBERS streams, # unless they are admins who are also new members of the organization. diff --git a/zerver/models.py b/zerver/models.py index b38f05e640f78c..30cd204014d9a6 100644 --- a/zerver/models.py +++ b/zerver/models.py @@ -1376,6 +1376,7 @@ class Stream(models.Model): STREAM_POST_POLICY_EVERYONE = 1 STREAM_POST_POLICY_ADMINS = 2 STREAM_POST_POLICY_RESTRICT_NEW_MEMBERS = 3 + STREAM_POST_POLICY_ADMINS_CAN_POST_AND_REACT = 4 # TODO: Implement policy to restrict posting to a user group or admins. # Who in the organization has permission to send messages to this stream. @@ -1384,6 +1385,7 @@ class Stream(models.Model): STREAM_POST_POLICY_EVERYONE, STREAM_POST_POLICY_ADMINS, STREAM_POST_POLICY_RESTRICT_NEW_MEMBERS, + STREAM_POST_POLICY_ADMINS_CAN_POST_AND_REACT, ] # The unique thing about Zephyr public streams is that we never list their @@ -1457,7 +1459,8 @@ def to_dict(self) -> Dict[str, Any]: result['stream_id'] = self.id continue result[field_name] = getattr(self, field_name) - result['is_announcement_only'] = self.stream_post_policy == Stream.STREAM_POST_POLICY_ADMINS + result['is_announcement_only'] = self.stream_post_policy == Stream.STREAM_POST_POLICY_ADMINS or \ + self.stream_post_policy == Stream.STREAM_POST_POLICY_ADMINS_CAN_POST_AND_REACT return result post_save.connect(flush_stream, sender=Stream) diff --git a/zerver/tests/test_reactions.py b/zerver/tests/test_reactions.py index ed66560a0f4093..41b1ca411e6e12 100644 --- a/zerver/tests/test_reactions.py +++ b/zerver/tests/test_reactions.py @@ -9,7 +9,7 @@ from zerver.lib.request import JsonableError from zerver.lib.test_helpers import tornado_redirected_to_list from zerver.lib.test_classes import ZulipTestCase -from zerver.models import get_realm, Message, Reaction, RealmEmoji, UserMessage +from zerver.models import get_realm, Message, Reaction, RealmEmoji, Stream, UserMessage, UserProfile class ReactionEmojiTest(ZulipTestCase): def test_missing_emoji(self) -> None: @@ -339,6 +339,33 @@ def test_remove_existing_reaction_with_deactivated_realm_emoji(self) -> None: result = self.api_delete(sender, '/api/v1/messages/1/reactions', reaction_info) self.assert_json_success(result) + def test_add_reaction_in_admins_can_post_and_react_streams(self) -> None: + realm = get_realm('zulip') + sender = self.example_email('iago') + reaction_sender = self.example_user("hamlet") + reaction_sender.role = UserProfile.ROLE_MEMBER + reaction_sender.save() + emoji_code, reaction_type = emoji_name_to_emoji_code(realm, 'smile') + stream = self.make_stream("example1") + stream.save() + msg_id = self.send_stream_message(sender, stream.name, + topic_name="test", content="test") + reaction_info = { + 'emoji_name': 'smile', + 'emoji_code': emoji_code, + 'reaction_type': reaction_type + } + + stream.stream_post_policy = Stream.STREAM_POST_POLICY_ADMINS_CAN_POST_AND_REACT + stream.save() + result = self.api_post(reaction_sender.email, '/api/v1/messages/%s/reactions' % (msg_id,), reaction_info) + self.assert_json_error(result, "Only admins can react.") + + stream.stream_post_policy = Stream.STREAM_POST_POLICY_ADMINS + stream.save() + result = self.api_post(reaction_sender.email, '/api/v1/messages/%s/reactions' % (msg_id,), reaction_info) + self.assert_json_success(result) + class ReactionEventTest(ZulipTestCase): def test_add_event(self) -> None: """ diff --git a/zerver/tests/test_subs.py b/zerver/tests/test_subs.py index d0fd955d22d72e..31f40a28b9165d 100644 --- a/zerver/tests/test_subs.py +++ b/zerver/tests/test_subs.py @@ -816,6 +816,12 @@ def test_change_stream_post_policy_requires_realm_admin(self) -> None: Stream.STREAM_POST_POLICY_RESTRICT_NEW_MEMBERS)}) self.assert_json_error(result, 'Must be an organization administrator') + stream_id = get_stream('stream_name1', user_profile.realm).id + result = self.client_patch('/json/streams/%d' % (stream_id,), + {'stream_post_policy': ujson.dumps( + Stream.STREAM_POST_POLICY_ADMINS_CAN_POST_AND_REACT)}) + self.assert_json_error(result, 'Must be an organization administrator') + def set_up_stream_for_deletion(self, stream_name: str, invite_only: bool=False, subscribed: bool=True) -> Stream: """ @@ -2653,6 +2659,28 @@ def test_subscribe_to_stream_post_policy_restrict_new_members_stream(self) -> No self.assertEqual(result[1][0].name, 'newer_stream') self.assertTrue(result[1][0].stream_post_policy == Stream.STREAM_POST_POLICY_RESTRICT_NEW_MEMBERS) + def test_subscribe_to_stream_post_policy_admins_can_post_and_react_stream(self) -> None: + """Members can subscribe to streams where only admins can post and react + but not create those streams, only realm admins can""" + member = self.example_user("AARON") + result = self.common_subscribe_to_streams(member.email, ["general"]) + self.assert_json_success(result) + + streams_raw = [{ + 'name': 'new_stream', + 'stream_post_policy': Stream.STREAM_POST_POLICY_ADMINS_CAN_POST_AND_REACT, + }] + with self.assertRaisesRegex( + JsonableError, "User cannot create a stream with these settings."): + list_to_streams(streams_raw, member, autocreate=True) + + admin = self.example_user("iago") + result = list_to_streams(streams_raw, admin, autocreate=True) + self.assert_length(result[0], 0) + self.assert_length(result[1], 1) + self.assertEqual(result[1][0].name, 'new_stream') + self.assertTrue(result[1][0].stream_post_policy == Stream.STREAM_POST_POLICY_ADMINS_CAN_POST_AND_REACT) + def test_guest_user_subscribe(self) -> None: """Guest users cannot subscribe themselves to anything""" guest_user = self.example_user("polonius") diff --git a/zerver/views/reactions.py b/zerver/views/reactions.py index b7fb7ec00e315a..6a33adbd4e1c2b 100644 --- a/zerver/views/reactions.py +++ b/zerver/views/reactions.py @@ -8,7 +8,8 @@ from zerver.lib.message import access_message from zerver.lib.request import JsonableError from zerver.lib.response import json_success -from zerver.models import Message, Reaction, UserMessage, UserProfile +from zerver.lib.streams import access_stream_by_id +from zerver.models import Message, Reaction, Recipient, Stream, UserMessage, UserProfile from typing import Optional @@ -37,6 +38,12 @@ def add_reaction(request: HttpRequest, user_profile: UserProfile, message_id: in emoji_code = emoji_name_to_emoji_code(message.sender.realm, emoji_name)[0] + if message.recipient.type == Recipient.STREAM: + stream, recipient, sub = access_stream_by_id(user_profile, message.recipient.type_id) + if stream.stream_post_policy == Stream.STREAM_POST_POLICY_ADMINS_CAN_POST_AND_REACT: + if not user_profile.is_realm_admin: + raise JsonableError(_("Only admins can react.")) + if Reaction.objects.filter(user_profile=user_profile, message=message, emoji_code=emoji_code,