Compare commits

...

59 Commits

Author SHA1 Message Date
Greg Price
8bc2ac4b8d shared: Bump version to 0.0.11. 2022-04-04 15:39:23 -07:00
Greg Price
d1c326a6cd poll_data: Write down types for Flow, for mobile.
These are based on my reading of the poll_data.js implementation.
2022-04-04 15:37:42 -07:00
Alex Vandiver
5c8086bf90 docs: Fix typo.
We don't suggest self-hosing, unless via a sprinkler in warm weather.
2022-04-04 14:52:04 -07:00
Steve Howell
17b60efdc7 markdown: Inject function for emoticon translations.
We want our parser to be as re-entrant as possible.
2022-04-04 14:07:18 -07:00
Steve Howell
03c15c8c14 markdown: Rename r to renderer. 2022-04-04 14:07:18 -07:00
Steve Howell
214ec099bb markdown: Eliminate setup() call.
It has always been pretty arbitrary what we did inside
of setup() vs. parse(), and we want to avoid unpredictable
results from other platforms neglecting to call setup().

On my machine you can parse a simple message in about
25 microseconds, based on a trial of a million messages
with the content of "**bold**".  Whatever portion of
that time is related to setup-related things like
compiling regexes should be negligible from the user's
perspective, since we never run parse() in a loop.
2022-04-04 14:07:18 -07:00
Steve Howell
093eba077a markdown: Avoid needless code duplication.
We only need to loop through the preprocessors
once, and we should use the options passed
in to the parser, not the default options
from the original setOptions call.

The first loop here was doing nothing.
2022-04-04 14:07:18 -07:00
Steve Howell
a77bf90601 markdown: Narrow stream/user_group types for mobile.
Our sub (i.e stream) and user_group objects have a bunch
of fields that aren't relevant to markdown parsing, so
we create narrow types that make it easier for us to
share code with mobile in the future.

I considered working purely in id space, but the problem
there is that user-entered stream names and user group
names need to be canonicalized.
2022-04-04 14:07:18 -07:00
Steve Howell
2bfdbbe7dc markdown: Extract get_topic_links. 2022-04-04 14:07:18 -07:00
Steve Howell
326dbfb934 markdown: Use options, not rules, for linkifier regexes.
This avoids the need to set a global from linkifiers.js.
2022-04-04 14:07:18 -07:00
Steve Howell
029b3e79a9 markdown: Add abstract_map() concept.
The abstract_map() helper clarifies that our code
doesn't need a concrete Map object from JS. This
change is possibly premature until we get a little
bit closer on integrating with mobile, since it
solves kinda the same problem that we might handle
more elegantly with TypeScript or Flow.

OTOH I find it to be pretty non-intrusive for the
webapp.
2022-04-04 14:07:18 -07:00
Steve Howell
3884710033 markdown: Inject linkifiers helper. 2022-04-04 14:07:18 -07:00
Steve Howell
1156001840 markdown: Inject emoji helpers.
Note that we try to avoid the helpers global, but we
still need a future commit to further clean things up.
2022-04-04 14:07:18 -07:00
Steve Howell
94f1fe6891 markdown: Avoid helpers global in some places.
These are the low-hanging-fruit places where we
can avoid using the helpers global.

The long term goal here is to make the markdown
code truly re-entrant, but some challenges still
remain.
2022-04-04 14:07:18 -07:00
Steve Howell
cc51e89730 markdown: Set handlers at parse time.
This will help us make the code a bit more
re-entrant in future commits.
2022-04-04 14:07:18 -07:00
Steve Howell
38a3d89a13 zcommands: Fully parse messages from the server.
Before this change, we would use **some** options relating
to parsing messages, but not all of them.  The reason for
this was completely unintentional.

It's mostly a moot point, since the server sends back pretty
generic messages when you do something like invoke the
"/dark" command, and the message would parse the same way
whether or not the parser was looking for things like user
mentions or stream links.

In order to make this code predictable, I had to decide
whether we do a completely vanilla parse or a full message
parse. My decision now is mostly tactical. It's a trivial
one-line change to just use all the options for message
parsing, whereas it requires a major overhaul to allow a
vanilla parse.

I also predict that we will eventually want to parse these
server responses as if they were messages. I doubt the
zcommand responses would ever take advantage of it, but I
could imagine things like nag messages wanting to use user
mentions.

Even if my predictions are wrong, my decisions here are
pretty easy to reverse once we learn more.

For the particular case of zcommands, it is puzzling to me
why the server doesn't just send back HTML, but I don't want
to open that can of worms yet, as that would technically be
an API change.

For now I am happy with the one-line fix.
2022-04-04 14:07:18 -07:00
Steve Howell
06ba05b44d markdown: Extract parse_non_message().
The zcommand code was calling directly into the "marked"
library, which was extremely misleading, since you don't
get a vanilla parse of the markdown due to the fact
that markdown.js calls setOptions at initialize time.

This commit shifts the responsibility to markdown.js
as well as adding a bit of test coverage, but it is
otherwise just a pure code-move refactoring.

The next commit will tweak things further.
2022-04-04 14:07:18 -07:00
Steve Howell
cf1149539e emojis: Swap the loops to build the emojis map.
This is mostly done for correctness reasons--it is
easiest from a logical standpoint to set the realm
emojis at the end of the function, since we do not want
them to be overwritten by normal emojis. The code
worked before this change, but it involved a clunky
check to map.has().

There is also probably a very minor performance
improvement insofar as N (the number of normal
emojis) is typically greater than R (the number
of realm emojis), and we eliminate N calls to
map.has in return for R calls to map.set.  Even
if R is quite large, the readability advantages
probably far outweigh any performance considerations,
since we are using native map calls.

Thanks to Austin Riba for this suggestion.
2022-04-04 13:28:49 -07:00
Steve Howell
3f6d0939fc emojis: Extract build_default_emoji_aliases(). 2022-04-04 13:28:49 -07:00
Steve Howell
e7738981b7 emojis: Make build_emojis_by_name() a pure function. 2022-04-04 13:28:49 -07:00
Steve Howell
d10e7ef85c emojis: Make build_emoticon_translations() a pure function. 2022-04-04 13:28:49 -07:00
Steve Howell
c943447c6e emojis: Un-share the emoji.js module.
The mobile app was never able to use the shared
version of emoji.js, because, among other problems
with our code organization, the emoji.js module
is strongly based on a mutate-the-data paradigm
that doesn't play nice with React.  The way
that we mutate data and violate encapsuation
here is something that we would mostly want to fix
without even trying to shared code with mobile, so
subsequent commits will try to extract some pure
functions into a shared module.
2022-04-04 13:28:49 -07:00
Heidi Ahlberg
cf1f70e3ef docs: Add translation guidelines for Finnish. 2022-04-04 13:24:22 -07:00
Archisman Das
491b1513eb settings: Fix length of custom profile field URL input.
The backend validates that URL inputs are RFC valid URLs (with no
specific length limit), but the frontend appears to have a maximum
length of 50 specified, likely because of a copy-paste error.

Increase the HTML maxlength for this input to 2048, which is a length
supported for URLs by all major browsers.

Fixes #21633
2022-04-04 12:10:05 -07:00
Tim Abbott
2ad60b0cda version: Update link to blog post for 5.0 release. 2022-04-04 11:58:10 -07:00
Sahil Batra
c3efd6b6a4 i18n: Translate the whole text in stream deactivation modal.
This commit fixes the template of stream deactivation modal
to tag all the text for translation. This commit also removes
the unnecessary span element.
2022-04-04 11:57:19 -07:00
Aman Agrawal
dcdf071751 message_edit: Fix false sub/unsub bookend on using a near link.
We were not setting the `historical` flag correctly for
messages fetched via `json_fetch_raw_message` when used didn't
have any UserMessage.

Extended relevant tests to fetch check message flags too.
2022-04-04 11:51:12 -07:00
Lauryn Menard
0b03275329 message_send: Move sender parameter to REQ.
Instead of using request.POST to get any potential `sender`
parameters for `send_message_backend`, moves it to the REQ
framework parameters as `req_sender`.

Also, updates `create_mirrored_message_users` to take specific
parameters instead of an HTTP request parameter, which then
accessed parameters via request.POST. And updates existing tests
for those changes.
2022-04-04 11:47:52 -07:00
Alex Vandiver
104e11c4fd version: Update version and changelog after 5.1 release. 2022-04-01 23:17:11 -07:00
Alex Vandiver
35e27aef4a migrations: Remove the possibly-duplicated emoji re-uploading.
In 85e531e377, we duplicated this block
of migration code to fix a bug, but moving it (aka deleting the
original copy) is a cleaner solution.
2022-04-01 17:51:00 -07:00
Alex Vandiver
eb31681934 check-database-compatibility: Ignore squashed and renamed migrations.
Fixes: #21596.
2022-04-01 16:15:41 -07:00
Tim Abbott
85e531e377 migrations: Repeat part of migration 0376.
The blockquote explains the motivation for this change in detail.

Fixes #21608.
2022-04-01 15:20:43 -07:00
NerdyLucifer
c79849dab6 settings: Replace "Delete bot" with "Deactivate bot".
The feature deactivates the bot user; Zulip has no "delete bot"
feature. So fix the label to match what it does.

We also change the icon to match the one we use for deactivating users
in the "Manage users" UI.
2022-04-01 15:03:23 -07:00
Sahil Batra
3a3ed78fd9 stream_settings: Live update the banner on changing subscription. 2022-04-01 14:52:06 -07:00
Palash
4d44698805 stream_settings: Remove pencil icon from 'General' tab in stream settings.
For user who is not an administrator.
Also implemented a banner that notifies the user if they can edit
the following settings (name/description and stream permission).
Also increased padding-top of stream header by 10px. This change is done
to increase vertical spacing between the banner
and the stream header.

Fixes #20001.
2022-04-01 14:52:06 -07:00
Alex Vandiver
0af00a3233 upgrade: Mark puppet as having started the server.
We previously used restart-server if puppet was run, as a nod to the
fact that `supervisor reread && supervisor update` will _start_
service groups that were modified, even if they were previously
stopped; this is because they are marked as `autostart=true`, which is
honored on service change.

However, upgrades want to run while there are no services running.  If
puppet is run, explicitly set the server as potentially being "up", so
that a `shutdown_server()` before migrations, if they exist, will stop
services.
2022-03-31 17:21:39 -07:00
Alex Vandiver
e9596637e7 upgrade: Move the shutdown_server calls to where they are relevant.
shutdown_server is a noop if the server is already stopped; placing
these in each block makes the logic more apparent.
2022-03-31 17:21:39 -07:00
Aman Agrawal
2df4ace441 navbar_alerts: Adjust height of recent topics when alert is visible.
Fixes #21619

We need to adjust height of recent topics along with the app
otherwise the container becomes separately scrollable due to
it overflowing the app height.
2022-03-31 11:25:09 -07:00
Alex Vandiver
2e50ead9d1 data_import: Fix bot email address de-duplication.
4815f6e28b tried to de-duplicate bot
email addresses, but instead caused duplicates to crash:

```
Traceback (most recent call last):
  File "./manage.py", line 157, in <module>
    execute_from_command_line(sys.argv)
  File "./manage.py", line 122, in execute_from_command_line
    utility.execute()
  File "/srv/zulip-venv-cache/56ac6adf406011a100282dd526d03537be84d23e/zulip-py3-venv/lib/python3.8/site-packages/django/core/management/__init__.py", line 413, in execute
    self.fetch_command(subcommand).run_from_argv(self.argv)
  File "/srv/zulip-venv-cache/56ac6adf406011a100282dd526d03537be84d23e/zulip-py3-venv/lib/python3.8/site-packages/django/core/management/base.py", line 354, in run_from_argv
    self.execute(*args, **cmd_options)
  File "/srv/zulip-venv-cache/56ac6adf406011a100282dd526d03537be84d23e/zulip-py3-venv/lib/python3.8/site-packages/django/core/management/base.py", line 398, in execute
    output = self.handle(*args, **options)
  File "/home/zulip/deployments/2022-03-16-22-25-42/zerver/management/commands/convert_slack_data.py", line 59, in handle
    do_convert_data(path, output_dir, token, threads=num_threads)
  File "/home/zulip/deployments/2022-03-16-22-25-42/zerver/data_import/slack.py", line 1320, in do_convert_data
    ) = slack_workspace_to_realm(
  File "/home/zulip/deployments/2022-03-16-22-25-42/zerver/data_import/slack.py", line 141, in slack_workspace_to_realm
    ) = users_to_zerver_userprofile(slack_data_dir, user_list, realm_id, int(NOW), domain_name)
  File "/home/zulip/deployments/2022-03-16-22-25-42/zerver/data_import/slack.py", line 248, in users_to_zerver_userprofile
    email = get_user_email(user, domain_name)
  File "/home/zulip/deployments/2022-03-16-22-25-42/zerver/data_import/slack.py", line 406, in get_user_email
    return SlackBotEmail.get_email(user["profile"], domain_name)
  File "/home/zulip/deployments/2022-03-16-22-25-42/zerver/data_import/slack.py", line 85, in get_email
    email_prefix += cls.duplicate_email_count[email]
TypeError: can only concatenate str (not "int") to str
```

Fix the stringification, make it case-insensitive, append with a dash
for readability, and add tests for all of the above.
2022-03-31 11:10:18 -07:00
Alex Vandiver
65e19c4fbd supervisor: 'foo:*' also matches 'foo'.
7c4293a7d3 switched to checking if the
service was already running, and use `supervisorctl start` if it was
not.

Unfortunately, `list_supervisor_processes("zulip-tornado:*")` did not
include `zulip-tornado`, and as such a non-sharded process was always
considered to _not_ be running, and was thus started, not restarted.
Starting an already-started service is a no-op, and thus non-sharded
tornado processes were never restarted.

The observed behaviour is that requests to the tornado process attempt
to load the user from the cache, with a different prefix from Django,
and immediately invalidate the session and eject the user back to the
login page.

Fix the `list_supervisor_processes` logic to match without the
trailing `:*`.
2022-03-31 10:41:41 -07:00
Heidi Ahlberg
9e6836d0af i18n: Fix missing translation tags in stream creation view. 2022-03-31 10:33:34 -07:00
Greg Price
3ff2bcf62a shared: Bump version to 0.0.10. 2022-03-30 21:06:37 -07:00
Anders Kaseorg
7de1e7c477 changelog: Remove broken link.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2022-03-30 20:37:31 -07:00
Steve Howell
b7f670f5a0 markdown: Extract set_linkifier_regexes.
This is definitely better than having linkifiers
reach directly into marked.js, but there is
probably further improvement we can do here
to clean up how these regexes get set.

This introduces a circular dependency between
markdown.js and linkifiers.js, but we will
soon break it in the other direction.
2022-03-30 14:31:00 -07:00
Steve Howell
2a240d3e19 markdown: Move handleLinkifier.
All the other handleFoo functions followed this
convention.
2022-03-30 14:31:00 -07:00
Steve Howell
efdf2c8fe3 linkifiers: Avoid parallel data structure.
We can pretty easily work with a map in the
two places that we ever relied on an array.
2022-03-30 14:31:00 -07:00
Steve Howell
71c12e313c markdown: Fix overly loose regex for previews.
Fortunately, the only impact of this bug was that
we would unnecessarily wait for the server to render
the markdown if we got false matches.
2022-03-30 14:31:00 -07:00
Steve Howell
da6d687215 markdown: Extract contains_preview_link.
I also clean up some variable names, comments, and
idioms.
2022-03-30 14:31:00 -07:00
Steve Howell
58799c6ca1 markdown: Extract contains_problematic_linkifier.
Note we now avoid linkifier checks for the case that a message
contains more obvious backend-only syntax such as attachments.

The next commit will eliminate the ugly early-return.
2022-03-30 14:31:00 -07:00
Steve Howell
8d9e6d6b87 markdown: Clean up API for future reuse.
This gets us closer to having an API that can
be used my mobile.

The parse() function becomes a subset of
apply_markdown() that is no longer coupled
to the shape of a webapp object, and it can
be supplied with a new helper_config for each
invocation. Mobile will likely call this directly.

The setup() function becomes a subset of
initialize() that allows you to set up the
parser **before** having to build any kind of
message-specific helpers. Mobile will likely
call this directly.

The webapp continues to call these functions,
which are now thin wrappers:

    * apply_markdown (wrapping parse)
    * initialize (wrapping setup)

Note we still have several other problems to
solve before mobile can use this code, but we
introduce this now so that we can get a head
start on prototyping and unit testing.

Also, this commit does not address the fact
that contains_backend_only_syntax() is still
bound to the webapp config.
2022-03-30 14:31:00 -07:00
Anders Kaseorg
935cb605a5 puppet: Do not ensure Chrony is running.
Commit f6d27562fa (#21564) tried to
ensure Chrony is running, which fails in containers where Chrony
doesn’t have permission to update the host clock.

The Debian package should still attempt to start it, and Puppet should
still restart it when chrony.conf is modified.

Signed-off-by: Anders Kaseorg <anders@zulip.com>
2022-03-30 11:37:54 -07:00
Sahil Batra
ca38b33346 settings: Fix push notifications tooltip being incorrectly shown.
We were showing the push notifications tooltip in user default
settings section even if the push notifications were configured
on the server.

The bug was because the setting value was undefined in the template
used for user default settings section, so this commit fixes the bug
by correctly passing the setting value to relevant template file.

Fixes #21602.
2022-03-30 11:31:29 -07:00
Lauryn Menard
0008a76703 tests: Remove ignored stream_name parameter from test.
Removes unnecessary `stream_name` parameter from
`test_stream_permission_changes_updates_updates_attachments`.
2022-03-30 11:30:31 -07:00
Tim Abbott
d9bf8baca1 tools: Add per-repository commit counts in contributions tool.
This makes the output nice enough to include in the blog post.
2022-03-29 14:13:17 -07:00
Tim Abbott
46b19fe8bd mailmap: Add entries to deduplicate more contributors. 2022-03-29 12:13:21 -07:00
Tim Abbott
c1103e4c7b i18n: Fix missing translation tag in footer. 2022-03-29 10:04:35 -07:00
Tim Abbott
c8dba33408 docs: Fix broken link in changelog. 2022-03-29 09:52:07 -07:00
Tim Abbott
6ea9947991 docs: Run prettier on changelog. 2022-03-29 09:24:06 -07:00
Tim Abbott
12e8f0f5ea version: Update version following 5.0 release. 2022-03-29 08:36:41 -07:00
68 changed files with 1117 additions and 306 deletions

View File

@@ -12,6 +12,8 @@
# # shows raw names/emails, filtered by mapped name:
# $ git log --format='%an %ae' --author=$NAME | uniq -c
Adam Benesh <Adam.Benesh@gmail.com> <Adam-Daniel.Benesh@t-systems.com>
Adam Benesh <Adam.Benesh@gmail.com>
Alex Vandiver <alexmv@zulip.com> <alex@chmrr.net>
Alex Vandiver <alexmv@zulip.com> <github@chmrr.net>
Allen Rabinovich <allenrabinovich@yahoo.com> <allenr@humbughq.com>
@@ -20,6 +22,9 @@ Alya Abbott <alya@zulip.com> <2090066+alya@users.noreply.github.com>
Aman Agrawal <amanagr@zulip.com> <f2016561@pilani.bits-pilani.ac.in>
Anders Kaseorg <anders@zulip.com> <anders@zulipchat.com>
Anders Kaseorg <anders@zulip.com> <andersk@mit.edu>
Aryan Shridhar <aryanshridhar7@gmail.com> <53977614+aryanshridhar@users.noreply.github.com>
Aryan Shridhar <aryanshridhar7@gmail.com>
Ashwat Kumar Singh <ashwat.kumarsingh.met20@itbhu.ac.in>
Austin Riba <austin@zulip.com> <austin@m51.io>
BIKI DAS <bikid475@gmail.com>
Brock Whittaker <brock@zulipchat.com> <bjwhitta@asu.edu>
@@ -37,6 +42,7 @@ Jeff Arnold <jbarnold@gmail.com> <jbarnold@humbughq.com>
Jeff Arnold <jbarnold@gmail.com> <jbarnold@zulip.com>
Jessica McKellar <jesstess@mit.edu> <jesstess@humbughq.com>
Jessica McKellar <jesstess@mit.edu> <jesstess@zulip.com>
Julia Bichler <julia.bichler@tum.de> <74348920+juliaBichler01@users.noreply.github.com>
Kevin Mehall <km@kevinmehall.net> <kevin@humbughq.com>
Kevin Mehall <km@kevinmehall.net> <kevin@zulip.com>
Kevin Scott <kevin.scott.98@gmail.com>
@@ -45,11 +51,13 @@ Mateusz Mandera <mateusz.mandera@zulip.com> <mateusz.mandera@protonmail.com>
m-e-l-u-h-a-n <purushottam.tiwari.cd.cse19@itbhu.ac.in>
Palash Raghuwanshi <singhpalash0@gmail.com>
Parth <mittalparth22@gmail.com>
Priyam Seth <sethpriyam1@gmail.com> <b19188@students.iitmandi.ac.in>
Ray Kraesig <rkraesig@zulip.com> <rkraesig@zulipchat.com>
Reid Barton <rwbarton@gmail.com> <rwbarton@humbughq.com>
Rishi Gupta <rishig@zulipchat.com> <rishig+git@mit.edu>
Rishi Gupta <rishig@zulipchat.com> <rishig@kandralabs.com>
Rishi Gupta <rishig@zulipchat.com> <rishig@users.noreply.github.com>
Reid Barton <rwbarton@gmail.com> <rwbarton@humbughq.com>
Rishabh Maheshwari <b20063@students.iitmandi.ac.in>
Sayam Samal <samal.sayam@gmail.com>
Scott Feeney <scott@oceanbase.org> <scott@humbughq.com>
Scott Feeney <scott@oceanbase.org> <scott@zulip.com>
@@ -71,3 +79,5 @@ Sahil Batra <sahil@zulip.com> <sahilbatra839@gmail.com>
Yash RE <33805964+YashRE42@users.noreply.github.com> <YashRE42@github.com>
Yash RE <33805964+YashRE42@users.noreply.github.com>
Yogesh Sirsat <yogeshsirsat56@gmail.com>
Zeeshan Equbal <equbalzeeshan@gmail.com> <54993043+zee-bit@users.noreply.github.com>
Zeeshan Equbal <equbalzeeshan@gmail.com>

View File

@@ -1,18 +1,44 @@
# Version history
This page contains the release history for the Zulip 5.x stable
release series. See the [current Zulip changelog][latest-changelog]
for newer release series, or the [commit log][commit-log] for an
up-to-date list of raw changes.
This page the release history for the Zulip server. See also the
[Zulip release lifecycle](../overview/release-lifecycle.md).
## Zulip 5.x series
## Zulip 6.x series
### 5.0 -- 2022-03-29
### 6.0 -- unreleased
This section is an incomplete draft of the release notes for the next
major release, and is only updated occasionally. See the [commit
log][commit-log] for an up-to-date list of raw changes.
#### Upgrade notes for 6.0
- None yet.
## Zulip 5.x series
### 5.1 -- 2022-04-01
- Fixed upgrade bug where preexisting animated emoji would still
always animate in statuses.
- Improved check that prevents servers from accidentally downgrading,
to not block upgrading servers that originally installed Zulip
Server prior to mid-2017.
- Fixed email address de-duplication in Slack imports.
- Prevented an extraneous scrollbar when a notification banner was
present across the top.
- Fixed installation in LXC containers, which failed due to `chrony`
not being runnable there.
- Prevented a "push notifications not configured" warning from
appearing in the new user default settings panel even when push
notifications were configured.
- Fixed a bug which, in uncommon configurations, would prevent Tornado
from being restarted during upgrades; users would be able to log in,
but would immediately be logged out.
- Updated translations.
### 5.0 -- 2022-03-29
#### Highlights
- New [resolve topic](https://zulip.com/help/resolve-a-topic) feature
@@ -1566,7 +1592,7 @@ Zulip installations; it has minimal changes for existing servers.
disruption by running this migration first, before beginning the
user-facing downtime. However, if you'd like to watch the downtime
phase of the upgrade closely, we recommend
[running them first manually](https://zulip.readthedocs.io/en/1.9.0/production/expensive-migrations.html)
running them first manually
as well as the usual trick of doing an apt upgrade first.
#### Full feature changelog
@@ -1955,7 +1981,7 @@ running a version from before 1.7 should upgrade directly to 1.7.1.
minimizes disruption by running these first, before beginning the
user-facing downtime. However, if you'd like to watch the downtime
phase of the upgrade closely, we recommend
[running them first manually](https://zulip.readthedocs.io/en/1.9.0/production/expensive-migrations.html)
running them first manually
as well as the usual trick of doing an apt upgrade first.
- We've removed support for an uncommon legacy deployment model where
@@ -2580,15 +2606,17 @@ running a version from before 1.7 should upgrade directly to 1.7.1.
This section links to the upgrade notes from past releases, so you can
easily read them all when upgrading across multiple releases.
- [Upgrade notes for 5.0](#upgrade-notes-for-50)
- [Upgrade notes for 4.0](#upgrade-notes-for-40)
- [Upgrade notes for 3.0](#upgrade-notes-for-30)
- [Upgrade notes for 2.1.5](#upgrade-notes-for-215)
- [Upgrade notes for 2.1.0](#upgrade-notes-for-210)
- [Upgrade notes for 2.0.0](#upgrade-notes-for-200)
- [Upgrade notes for 1.9.0](#upgrade-notes-for-190)
- [Upgrade notes for 1.8.0](#upgrade-notes-for-180)
- [Upgrade notes for 1.7.0](#upgrade-notes-for-170)
- [Draft upgrade notes for 6.0](#upgrade-notes-for-60)
* [Upgrade notes for 5.0](#upgrade-notes-for-50)
* [Upgrade notes for 4.0](#upgrade-notes-for-40)
* [Upgrade notes for 3.0](#upgrade-notes-for-30)
* [Upgrade notes for 2.1.5](#upgrade-notes-for-215)
* [Upgrade notes for 2.1.0](#upgrade-notes-for-210)
* [Upgrade notes for 2.0.0](#upgrade-notes-for-200)
* [Upgrade notes for 1.9.0](#upgrade-notes-for-190)
* [Upgrade notes for 1.8.0](#upgrade-notes-for-180)
* [Upgrade notes for 1.7.0](#upgrade-notes-for-170)
[docker-zulip]: https://github.com/zulip/docker-zulip
[commit-log]: https://github.com/zulip/zulip/commits/main

View File

@@ -15,7 +15,7 @@ Zulip's
you can create a test Zulip Cloud organization at <https://zulip.com/new>.
- If you are deciding between self-hosting Zulip and signing up for [Zulip Cloud](https://zulip.com/plans/),
our [self-hosing overview](https://zulip.com/self-hosting/) and [guide to
our [self-hosting overview](https://zulip.com/self-hosting/) and [guide to
choosing between Zulip Cloud and
self-hosting](https://zulip.com/help/getting-your-organization-started-with-zulip#choosing-between-zulip-cloud-and-self-hosting)
are great places to start.

140
docs/translating/finnish.md Normal file
View File

@@ -0,0 +1,140 @@
# Finnish translation style guide
## Guidelines
Tervetuloa!
Before you start, take a look these instructions we have gathered here
for you to help on your translation journey.
### Word order
Consider translating the same thing with the easiest Finnish possible.
It's not mandatory to follow the English text word by word, as long as
the message is clear.
Eg.
- Click the button below to create the organization and register your
account. -> Luo organisaatio ja rekisteröi tilisi napsauttamalla
alla olevaa painiketta.
- Sent! Your message is outside your current narrow. -> Lähetetty!
Viesti on nykyisen näkymäsi ulkopuolella.
### Grammatical case (Sijamuodot)
Translate using the UI to be sure what is the correct grammatical
case. Basic form of a word might not always be suitable for the
purpose.
Eg.
- Topics marked as resolved -> Ratkaistut aiheet (versus Aiheet, jotka on merkitty ratkaistuiksi)
- View Shortcuts -> Katsel**un** pikanäppäimet
### Loan word (Lainasanat)
Even though it's common to use words formed directly from English, try
to consider also users without IT background, people that don't speak
English and accessibility. User interface should contain these with
minimum amount, but for technical error messages could be preferred.
There are some amount of software related words that are widely used
as loan words.
See section [Terms](#terms) for more details.
### **_Please_**, in error messages
As it might appeal to use correspondence _Ole hyvä ja_, it's not
commonly used in Finnish. We are strict and used to more direct
messaging. Let's not translate _please_ and use instruction format
only. No Finn is going to be offended by this.
Eg.
- Please enter at most 10 emails. -> Lisää korkeintaan 10 sähköpostia.
But
- Yes, please! -> Kyllä, kiitos!
### Zulip word inflection
- in/from Zulip - **Zulipissa** / **Zulipista** / **Zulipin**
- Zulip organization - **Zulip-organisaatio**
- Zulip app - **Zulip-sovellus**
### Your -expression
Finnish language has _form of ownership_ so there shouldn't be need to
thanslate _your_ as _sinun_ but rather use _si_ ending. It might be
considered to leave out as well.
Eg.
- Your organization -> Organisaatio**si**
- Your account -> Tili**si**
But
- You do have active accounts in the following organization(s). ->
Sinulla ei ole aktiivista tiliä seuraavissa organisaatio(i)ssa.
### Comma
Use commas in whole sentences where it is required. You can use these instructions as help.
[Kotimaisten kielten keskus - pilkku.](http://www.kielitoimistonohjepankki.fi/haku/pilkku/ohje/86)
## Terms
- Administrator - **Järjestelmänvalvoja**
- App - **Sovellus**
- Authorization - **Valtuus**
- Avatar - **Avatar**
- Beta - **Beta**
- Change - **Muuta**
- Cheat sheet - **Lunttilappu**
- Click - **Napsauta**
- Configure - **Määritä**
- Deactivate - **Poista käytöstä**
- Domain - **Verkkotunnus**
- Export - **Poiminta**
- Filter - **Suodata**
- Full member - **Täysivaltainen jäsen**
- Host - **Isäntä**
- Help Center - **Tukikeskus**
- ID - **Tunnus**
- Integraatio - **Integraatio**
- Interactive - **Interaktiivinen**
- Invalid - **Virheellinen**
- Moderator - **Moderaattori**
- Mute - **Mykistää**
- Narrow - **Rajaa hakua**
- Notification - **Ilmoitus**
- Topic - **Aihe**
- Organization - **Organisaatio**
- Permission - **Lupa**
- Pin - **Kiinnitä**
- Picker - **Valitsin**
- Plan - **Tilaus**
- PM (private messages) - **YV (yksityisviesti)** - Short version is needed in mobile.
- Reset - **Nollata**
- Save - **Tallenna**
- Stream - **Kanava**
- Subscriber - **Tilaaja**
- Subscription - **Tilaus**
- Subscribe a stream - **Tilaa kanava**
- Subdomain - **Aliverkkotunnus**
- Shortcuts - **Pikanäppäimet**
- Unsubscripe - **Peru tilaus**
- Unsupported - **Ei-tuettu**
- Unresolve - **Merkitse ratkaisemattomaksi**
- Webhook - **Webhook**
- Whoops - **Hupsista**
- Widget - **Widgetti**
## Other
Some translations can be tricky, so please don't hesitate to ask the
community or to contribute to this guide! Thanks for your effort!

View File

@@ -8,6 +8,7 @@ maxdepth: 3
translating
internationalization
chinese
finnish
french
german
hindi

View File

@@ -165,6 +165,7 @@ translate words like "stream" to), with reasoning, so that future
translators can understand and preserve those decisions:
- [Chinese](chinese.md)
- [Finnish](finnish.md)
- [French](french.md)
- [German](german.md)
- [Hindi](hindi.md)

View File

@@ -39,10 +39,10 @@ set_global("setTimeout", (f, time) => {
});
set_global("document", "document-stub");
const emoji = zrequire("../shared/js/emoji");
const typeahead = zrequire("../shared/js/typeahead");
const compose_state = zrequire("compose_state");
const compose_validate = zrequire("compose_validate");
const emoji = zrequire("emoji");
const typeahead_helper = zrequire("typeahead_helper");
const muted_users = zrequire("muted_users");
const people = zrequire("people");

View File

@@ -87,6 +87,7 @@ page_params.realm_description = "already set description";
// For data-oriented modules, just use them, don't stub them.
const alert_words = zrequire("alert_words");
const emoji = zrequire("emoji");
const stream_topic_history = zrequire("stream_topic_history");
const stream_list = zrequire("stream_list");
const message_helper = zrequire("message_helper");
@@ -96,8 +97,6 @@ const starred_messages = zrequire("starred_messages");
const user_status = zrequire("user_status");
const compose_pm_pill = zrequire("compose_pm_pill");
const emoji = zrequire("../shared/js/emoji");
const server_events_dispatch = zrequire("server_events_dispatch");
function dispatch(ev) {

View File

@@ -9,7 +9,7 @@ const emoji_codes = zrequire("../generated/emoji/emoji_codes.json");
const events = require("./lib/events");
const emoji = zrequire("../shared/js/emoji");
const emoji = zrequire("emoji");
const realm_emoji = events.test_realm_emojis;

View File

@@ -7,7 +7,7 @@ const _ = require("lodash");
const {zrequire} = require("../zjsunit/namespace");
const {run_test} = require("../zjsunit/test");
const emoji = zrequire("../shared/js/emoji");
const emoji = zrequire("emoji");
const emoji_picker = zrequire("emoji_picker");
const emoji_codes = zrequire("../generated/emoji/emoji_codes.json");

View File

@@ -110,9 +110,9 @@ message_lists.current = {
},
};
const emoji_codes = zrequire("../generated/emoji/emoji_codes.json");
const emoji = zrequire("../shared/js/emoji");
const activity = zrequire("activity");
const emoji = zrequire("emoji");
const emoji_codes = zrequire("../generated/emoji/emoji_codes.json");
const hotkey = zrequire("hotkey");
emoji.initialize({

View File

@@ -7,9 +7,11 @@ const {run_test} = require("../zjsunit/test");
const blueslip = require("../zjsunit/zblueslip");
const linkifiers = zrequire("linkifiers");
const marked = zrequire("../third/marked/lib/marked");
const markdown = zrequire("markdown");
const markdown_config = zrequire("markdown_config");
linkifiers.initialize([]);
markdown.initialize(markdown_config.get_helpers());
run_test("python_to_js_linkifier", () => {
// The only way to reach python_to_js_linkifier is indirectly, hence the call
@@ -26,7 +28,7 @@ run_test("python_to_js_linkifier", () => {
id: 20,
},
]);
let actual_value = marked.InlineLexer.rules.zulip.linkifiers;
let actual_value = markdown.get_linkifier_regexes();
let expected_value = [/\/aa\/g(?!\w)/gim, /\/aa\/g(?!\w)/g];
assert.deepEqual(actual_value, expected_value);
// Test case with multiple replacements.
@@ -37,7 +39,7 @@ run_test("python_to_js_linkifier", () => {
id: 30,
},
]);
actual_value = marked.InlineLexer.rules.zulip.linkifiers;
actual_value = markdown.get_linkifier_regexes();
expected_value = [/#cf(\d+)([A-Z][\dA-Z]*)(?!\w)/g];
assert.deepEqual(actual_value, expected_value);
// Test incorrect syntax.
@@ -52,7 +54,7 @@ run_test("python_to_js_linkifier", () => {
id: 40,
},
]);
actual_value = marked.InlineLexer.rules.zulip.linkifiers;
actual_value = markdown.get_linkifier_regexes();
expected_value = [];
assert.deepEqual(actual_value, expected_value);
});

View File

@@ -35,7 +35,7 @@ set_global("Image", Image);
set_global("document", {compatMode: "CSS1Compat"});
const emoji = zrequire("../shared/js/emoji");
const emoji = zrequire("emoji");
const emoji_codes = zrequire("../generated/emoji/emoji_codes.json");
const linkifiers = zrequire("linkifiers");
const pygments_data = zrequire("../generated/pygments_data.json");
@@ -771,10 +771,16 @@ test("backend_only_linkifiers", () => {
});
test("translate_emoticons_to_names", () => {
const get_emoticon_translations = emoji.get_emoticon_translations;
function translate_emoticons_to_names(src) {
return markdown.translate_emoticons_to_names({src, get_emoticon_translations});
}
// Simple test
const test_text = "Testing :)";
const expected = "Testing :smile:";
const result = markdown.translate_emoticons_to_names(test_text);
const result = translate_emoticons_to_names(test_text);
assert.equal(result, expected);
// Extensive tests.
@@ -813,12 +819,16 @@ test("translate_emoticons_to_names", () => {
expected: `Hello ${full_name}!`,
},
]) {
const result = markdown.translate_emoticons_to_names(original);
const result = translate_emoticons_to_names(original);
assert.equal(result, expected);
}
}
});
test("parse_non_message", () => {
assert.equal(markdown.parse_non_message("type `/day`"), "<p>type <code>/day</code></p>");
});
test("missing unicode emojis", ({override_rewire}) => {
const message = {raw_content: "\u{1F6B2}"};
@@ -833,6 +843,8 @@ test("missing unicode emojis", ({override_rewire}) => {
assert.equal(codepoint, "1f6b2");
// return undefined
});
markdown.initialize(markdown_config.get_helpers());
markdown.apply_markdown(message);
assert.equal(message.content, "<p>\u{1F6B2}</p>");
});

View File

@@ -0,0 +1,230 @@
"use strict";
const {strict: assert} = require("assert");
const {zrequire} = require("../zjsunit/namespace");
const {run_test} = require("../zjsunit/test");
const markdown = zrequire("markdown");
const my_id = 101;
const user_map = new Map();
user_map.set(my_id, "Me Myself");
user_map.set(105, "greg");
function get_actual_name_from_user_id(user_id) {
return user_map.get(user_id);
}
function get_user_id_from_name(name) {
for (const [user_id, _name] of user_map.entries()) {
if (name === _name) {
return user_id;
}
}
return undefined;
}
function is_valid_full_name_and_user_id(name, user_id) {
return user_map.has(user_id) && user_map.get(user_id) === name;
}
function my_user_id() {
return my_id;
}
function is_valid_user_id(user_id) {
return user_map.has(user_id);
}
const staff_group = {
id: 201,
name: "Staff",
};
const user_group_map = new Map();
user_group_map.set(staff_group.name, staff_group);
function get_user_group_from_name(name) {
return user_group_map.get(name);
}
function is_member_of_user_group(user_group_id, user_id) {
assert.equal(user_group_id, staff_group.id);
assert.equal(user_id, my_id);
return true;
}
const social = {
stream_id: 301,
name: "social",
};
const sub_map = new Map();
sub_map.set(social.name, social);
function get_stream_by_name(name) {
return sub_map.get(name);
}
function stream_hash(stream_id) {
return `stream-${stream_id}`;
}
function stream_topic_hash(stream_id, topic) {
return `stream-${stream_id}-topic-${topic}`;
}
function get_emoticon_translations() {
return [
{regex: /(:\))/g, replacement_text: ":smile:"},
{regex: /(<3)/g, replacement_text: ":heart:"},
];
}
const emoji_map = new Map();
emoji_map.set("smile", "1f642");
emoji_map.set("alien", "1f47d");
function get_emoji_codepoint(emoji_name) {
return emoji_map.get(emoji_name);
}
function get_emoji_name(codepoint) {
for (const [emoji_name, _codepoint] of emoji_map.entries()) {
if (codepoint === _codepoint) {
return emoji_name;
}
}
return undefined;
}
const realm_emoji_map = new Map();
realm_emoji_map.set("heart", "/images/emoji/heart.bmp");
function get_realm_emoji_url(emoji_name) {
return realm_emoji_map.get(emoji_name);
}
const regex = /#foo(\d+)(?!\w)/g;
const linkifier_map = new Map();
linkifier_map.set(regex, "http://foo.com/\\1");
function get_linkifier_map() {
return linkifier_map;
}
const helper_config = {
// user stuff
get_actual_name_from_user_id,
get_user_id_from_name,
is_valid_full_name_and_user_id,
is_valid_user_id,
my_user_id,
// user groups
get_user_group_from_name,
is_member_of_user_group,
// stream hashes
get_stream_by_name,
stream_hash,
stream_topic_hash,
// settings
should_translate_emoticons: () => true,
// emojis
get_emoji_codepoint,
get_emoji_name,
get_emoticon_translations,
get_realm_emoji_url,
// linkifiers
get_linkifier_map,
};
function assert_parse(raw_content, expected_content) {
const {content} = markdown.parse({raw_content, helper_config});
assert.equal(content, expected_content);
}
run_test("basics", () => {
assert_parse("boring", "<p>boring</p>");
assert_parse("**bold**", "<p><strong>bold</strong></p>");
});
run_test("user mentions", () => {
assert_parse("@**greg**", '<p><span class="user-mention" data-user-id="105">@greg</span></p>');
assert_parse("@**|105**", '<p><span class="user-mention" data-user-id="105">@greg</span></p>');
assert_parse(
"@**greg|105**",
'<p><span class="user-mention" data-user-id="105">@greg</span></p>',
);
assert_parse(
"@**Me Myself|101**",
'<p><span class="user-mention" data-user-id="101">@Me Myself</span></p>',
);
});
run_test("user group mentions", () => {
assert_parse(
"@*Staff*",
'<p><span class="user-group-mention" data-user-group-id="201">@Staff</span></p>',
);
});
run_test("stream links", () => {
assert_parse(
"#**social**",
'<p><a class="stream" data-stream-id="301" href="/stream-301">#social</a></p>',
);
assert_parse(
"#**social>lunch**",
'<p><a class="stream-topic" data-stream-id="301" href="/stream-301-topic-lunch">#social &gt; lunch</a></p>',
);
});
run_test("emojis", () => {
assert_parse(
"yup :)",
'<p>yup <span aria-label="smile" class="emoji emoji-1f642" role="img" title="smile">:smile:</span></p>',
);
assert_parse(
"I <3 JavaScript",
'<p>I <img alt=":heart:" class="emoji" src="/images/emoji/heart.bmp" title="heart"> JavaScript</p>',
);
assert_parse(
"Mars Attacks! \uD83D\uDC7D",
'<p>Mars Attacks! <span aria-label="alien" class="emoji emoji-1f47d" role="img" title="alien">:alien:</span></p>',
);
});
run_test("linkifiers", () => {
assert_parse(
"see #foo12345 for details",
'<p>see <a href="http://foo.com/12345" title="http://foo.com/12345">#foo12345</a> for details</p>',
);
});
run_test("topic links", () => {
const topic = "progress on #foo101 and #foo102";
const topic_links = markdown.get_topic_links({topic, get_linkifier_map});
assert.deepEqual(topic_links, [
{
text: "#foo101",
url: "http://foo.com/101",
},
{
text: "#foo102",
url: "http://foo.com/102",
},
]);
});

View File

@@ -61,8 +61,8 @@ message_lists.current = {
};
set_global("document", "document-stub");
const emoji = zrequire("emoji");
const emoji_codes = zrequire("../generated/emoji/emoji_codes.json");
const emoji = zrequire("../shared/js/emoji");
const people = zrequire("people");
const reactions = zrequire("reactions");

View File

@@ -14,7 +14,7 @@ const peer_data = zrequire("peer_data");
const people = zrequire("people");
const stream_data = zrequire("stream_data");
const emoji = zrequire("../shared/js/emoji");
const emoji = zrequire("emoji");
const pygments_data = zrequire("../generated/pygments_data.json");
const actual_pygments_data = {...pygments_data};
const ct = zrequire("composebox_typeahead");

View File

@@ -10,7 +10,7 @@ const channel = mock_esm("../../static/js/channel");
const user_status = zrequire("user_status");
const emoji_codes = zrequire("../generated/emoji/emoji_codes.json");
const emoji = zrequire("../shared/js/emoji");
const emoji = zrequire("emoji");
const emoji_params = {
realm_emoji: {

View File

@@ -61,7 +61,7 @@ class zulip::profile::base {
}
}
package { 'ntp': ensure => 'purged', before => Package['chrony'] }
service { 'chrony': ensure => 'running', require => Package['chrony'] }
service { 'chrony': require => Package['chrony'] }
package { $base_packages: ensure => 'installed' }
group { 'zulip':

View File

@@ -28,6 +28,45 @@ missing.discard(("guardian", "0001_initial"))
missing.discard(("sites", "0001_initial"))
missing.discard(("sites", "0002_alter_domain_unique"))
# These migrations were squashed into 0001, in 6fbddf578a6e through
# a21f2d771553, 1.7.0~3135.
missing.difference_update(
[
("zerver", "0002_django_1_8"),
("zerver", "0003_custom_indexes"),
("zerver", "0004_userprofile_left_side_userlist"),
("zerver", "0005_auto_20150920_1340"),
("zerver", "0006_zerver_userprofile_email_upper_idx"),
("zerver", "0007_userprofile_is_bot_active_indexes"),
("zerver", "0008_preregistrationuser_upper_email_idx"),
("zerver", "0009_add_missing_migrations"),
("zerver", "0010_delete_streamcolor"),
("zerver", "0011_remove_guardian"),
("zerver", "0012_remove_appledevicetoken"),
("zerver", "0013_realmemoji"),
("zerver", "0014_realm_emoji_url_length"),
("zerver", "0015_attachment"),
("zerver", "0016_realm_create_stream_by_admins_only"),
("zerver", "0017_userprofile_bot_type"),
("zerver", "0018_realm_emoji_message"),
("zerver", "0019_preregistrationuser_realm_creation"),
("zerver", "0020_add_tracking_attachment"),
("zerver", "0021_migrate_attachment_data"),
("zerver", "0022_subscription_pin_to_top"),
("zerver", "0023_userprofile_default_language"),
("zerver", "0024_realm_allow_message_editing"),
("zerver", "0025_realm_message_content_edit_limit"),
("zerver", "0026_delete_mituser"),
("zerver", "0027_realm_default_language"),
("zerver", "0028_userprofile_tos_version"),
]
)
# This migration was in python-social-auth, and was mistakenly removed
# from its `replaces` in
# https://github.com/python-social-auth/social-app-django/pull/25
missing.discard(("default", "0005_auto_20160727_2333"))
for key, migration in loader.disk_migrations.items():
missing.discard(key)
missing.difference_update(migration.replaces)

View File

@@ -42,7 +42,10 @@ def list_supervisor_processes(
if filter_names:
match = False
for filter_name in filter_names:
if filter_name.endswith(":*") and name.startswith(filter_name[:-1]):
# zulip-tornado:* matches zulip-tornado:9800 and zulip-tornado
if filter_name.endswith(":*") and (
name.startswith(filter_name[:-1]) or name == filter_name[:-2]
):
match = True
break
if name == filter_name:

View File

@@ -309,11 +309,10 @@ if not args.skip_migrations:
if line_str.startswith("[ ]"):
migrations_needed = True
if (not args.skip_puppet or migrations_needed) and IS_SERVER_UP:
# By default, we shut down the service to apply migrations and
# Puppet changes, to minimize risk of issues due to inconsistent
# state.
shutdown_server()
# NOTE: Here begins the most likely critical period, where we may be
# shutting down the server; we should strive to minimize the number of
# steps that happen between here and the "Restarting Zulip" line
# below.
if rabbitmq_dist_listen:
shutdown_server()
@@ -327,6 +326,7 @@ if cookie_size is not None and cookie_size == 20:
# characters long by a good randomizer, it would be 96 bits and
# more than sufficient. We generate, using good randomness, a
# 255-character cookie, the max allowed length.
shutdown_server()
logging.info("Generating a secure erlang cookie...")
subprocess.check_call(["./scripts/setup/generate-rabbitmq-cookie"])
@@ -357,19 +357,25 @@ if classes != new_classes:
if not args.skip_puppet:
# Puppet may adjust random services; to minimize risk of issues
# due to inconsistent state, we shut down the server first.
shutdown_server()
logging.info("Applying Puppet changes...")
subprocess.check_call(["./scripts/zulip-puppet-apply", "--force"])
subprocess.check_call(["apt-get", "-y", "--allow-downgrades", "upgrade"])
# Puppet may have reloaded supervisor, and in so doing started
# services; mark as potentially needing to stop the server.
IS_SERVER_UP = True
if migrations_needed:
# Database migrations assume that they run on a database in
# quiesced state.
shutdown_server()
logging.info("Applying database migrations...")
subprocess.check_call(["./manage.py", "migrate", "--noinput"], preexec_fn=su_to_zulip)
logging.info("Restarting Zulip...")
if IS_SERVER_UP or not args.skip_puppet:
# Even if the server wasn't up previously, puppet might have
# started it if there were supervisord configuration changes, so
# we need to use restart-server if puppet ran.
if IS_SERVER_UP:
restart_args = ["--fill-cache"]
if args.skip_tornado:
restart_args.append("--skip-tornado")

View File

@@ -161,6 +161,7 @@ export function build_page() {
settings_config.create_web_public_stream_policy_values,
disable_enable_spectator_access_setting: !page_params.server_web_public_streams_enabled,
can_sort_by_email: settings_data.show_email(),
realm_push_notifications_enabled: page_params.realm_push_notifications_enabled,
};
if (options.realm_logo_source !== "D" && options.realm_night_logo_source === "D") {

View File

@@ -2,7 +2,6 @@ import $ from "jquery";
import _ from "lodash";
import pygments_data from "../generated/pygments_data.json";
import * as emoji from "../shared/js/emoji";
import * as typeahead from "../shared/js/typeahead";
import * as compose from "./compose";
@@ -10,6 +9,7 @@ import * as compose_pm_pill from "./compose_pm_pill";
import * as compose_state from "./compose_state";
import * as compose_ui from "./compose_ui";
import * as compose_validate from "./compose_validate";
import * as emoji from "./emoji";
import * as flatpickr from "./flatpickr";
import {$t} from "./i18n";
import * as message_store from "./message_store";

View File

@@ -6,13 +6,13 @@ let emoji_codes = {};
// `emojis_by_name` is the central data source that is supposed to be
// used by every widget in the web app for gathering data for displaying
// emojis. Emoji picker uses this data to derive data for its own use.
export const emojis_by_name = new Map();
export let emojis_by_name = new Map();
export const all_realm_emojis = new Map();
export const active_realm_emojis = new Map();
export const deactivated_emoji_name_to_code = new Map();
const default_emoji_aliases = new Map();
let default_emoji_aliases = new Map();
// For legacy reasons we track server_realm_emoji_data,
// since our settings code builds off that format. We
@@ -28,9 +28,12 @@ export function get_server_realm_emoji_data() {
let emoticon_translations = [];
function build_emoticon_translations() {
function build_emoticon_translations({emoticon_conversions}) {
/*
Please keep this as a pure function so that we can
eventually share this code with the mobile codebase.
Build a data structure that looks like something
like this:
@@ -54,7 +57,7 @@ function build_emoticon_translations() {
*/
const translations = [];
for (const [emoticon, replacement_text] of Object.entries(emoji_codes.emoticon_conversions)) {
for (const [emoticon, replacement_text] of Object.entries(emoticon_conversions)) {
const regex = new RegExp("(" + _.escapeRegExp(emoticon) + ")", "g");
translations.push({
@@ -63,7 +66,7 @@ function build_emoticon_translations() {
});
}
emoticon_translations = translations;
return translations;
}
const zulip_emoji = {
@@ -110,8 +113,34 @@ export function get_realm_emoji_url(emoji_name) {
return data.emoji_url;
}
export function build_emoji_data(realm_emojis) {
emojis_by_name.clear();
function build_emojis_by_name({
realm_emojis,
emoji_catalog,
get_emoji_name,
default_emoji_aliases,
}) {
// Please keep this as a pure function so that we can
// eventually share this code with the mobile codebase.
const map = new Map();
for (const codepoints of Object.values(emoji_catalog)) {
for (const codepoint of codepoints) {
const emoji_name = get_emoji_name(codepoint);
if (emoji_name !== undefined) {
const emoji_dict = {
name: emoji_name,
display_name: emoji_name,
aliases: default_emoji_aliases.get(codepoint),
is_realm_emoji: false,
emoji_code: codepoint,
has_reacted: false,
};
// We may later get overridden by a realm emoji.
map.set(emoji_name, emoji_dict);
}
}
}
for (const [realm_emoji_name, realm_emoji] of realm_emojis) {
const emoji_dict = {
name: realm_emoji_name,
@@ -121,25 +150,12 @@ export function build_emoji_data(realm_emojis) {
url: realm_emoji.emoji_url,
has_reacted: false,
};
emojis_by_name.set(realm_emoji_name, emoji_dict);
// We want the realm emoji to overwrite any existing entry in this map.
map.set(realm_emoji_name, emoji_dict);
}
for (const codepoints of Object.values(emoji_codes.emoji_catalog)) {
for (const codepoint of codepoints) {
const emoji_name = get_emoji_name(codepoint);
if (emoji_name !== undefined && !emojis_by_name.has(emoji_name)) {
const emoji_dict = {
name: emoji_name,
display_name: emoji_name,
aliases: default_emoji_aliases.get(codepoint),
is_realm_emoji: false,
emoji_code: codepoint,
has_reacted: false,
};
emojis_by_name.set(emoji_name, emoji_dict);
}
}
}
return map;
}
export function update_emojis(realm_emojis) {
@@ -182,7 +198,12 @@ export function update_emojis(realm_emojis) {
// here "zulip" is an emoji name, which is fine.
active_realm_emojis.set("zulip", zulip_emoji);
build_emoji_data(active_realm_emojis);
emojis_by_name = build_emojis_by_name({
realm_emojis: active_realm_emojis,
emoji_catalog: emoji_codes.emoji_catalog,
get_emoji_name,
default_emoji_aliases,
});
}
// This function will provide required parameters that would
@@ -244,20 +265,37 @@ export function get_emoji_details_for_rendering(opts) {
};
}
function build_default_emoji_aliases({names, get_emoji_codepoint}) {
// Please keep this as a pure function so that we can
// eventually share this code with the mobile codebase.
// Create a map of codepoint -> names
const map = new Map();
for (const name of names) {
const base_name = get_emoji_codepoint(name);
if (map.has(base_name)) {
map.get(base_name).push(name);
} else {
map.set(base_name, [name]);
}
}
return map;
}
export function initialize(params) {
emoji_codes = params.emoji_codes;
build_emoticon_translations();
emoticon_translations = build_emoticon_translations({
emoticon_conversions: emoji_codes.emoticon_conversions,
});
for (const value of emoji_codes.names) {
const base_name = get_emoji_codepoint(value);
if (default_emoji_aliases.has(base_name)) {
default_emoji_aliases.get(base_name).push(value);
} else {
default_emoji_aliases.set(base_name, [value]);
}
}
default_emoji_aliases = build_default_emoji_aliases({
names: emoji_codes.names,
get_emoji_codepoint,
});
update_emojis(params.realm_emoji);
}

View File

@@ -1,7 +1,6 @@
import $ from "jquery";
import emoji_codes from "../generated/emoji/emoji_codes.json";
import * as emoji from "../shared/js/emoji";
import * as typeahead from "../shared/js/typeahead";
import render_emoji_popover from "../templates/emoji_popover.hbs";
import render_emoji_popover_content from "../templates/emoji_popover_content.hbs";
@@ -10,6 +9,7 @@ import render_emoji_showcase from "../templates/emoji_showcase.hbs";
import * as blueslip from "./blueslip";
import * as compose_ui from "./compose_ui";
import * as emoji from "./emoji";
import * as message_lists from "./message_lists";
import * as message_store from "./message_store";
import * as popovers from "./popovers";

View File

@@ -1,7 +1,5 @@
import $ from "jquery";
import * as emoji from "../shared/js/emoji";
import * as activity from "./activity";
import * as browser_history from "./browser_history";
import * as common from "./common";
@@ -12,6 +10,7 @@ import * as condense from "./condense";
import * as copy_and_paste from "./copy_and_paste";
import * as deprecated_feature_notice from "./deprecated_feature_notice";
import * as drafts from "./drafts";
import * as emoji from "./emoji";
import * as emoji_picker from "./emoji_picker";
import * as feedback_widget from "./feedback_widget";
import * as gear_menu from "./gear_menu";

View File

@@ -1,22 +1,9 @@
import marked from "../third/marked/lib/marked";
import * as blueslip from "./blueslip";
const linkifier_map = new Map();
export let linkifier_list = [];
const linkifier_map = new Map(); // regex -> url
function handleLinkifier(pattern, matches) {
let url = linkifier_map.get(pattern);
let current_group = 1;
for (const match of matches) {
const back_ref = "\\" + current_group;
url = url.replace(back_ref, match);
current_group += 1;
}
return url;
export function get_linkifier_map() {
return linkifier_map;
}
function python_to_js_linkifier(pattern, url) {
@@ -78,11 +65,7 @@ function python_to_js_linkifier(pattern, url) {
}
export function update_linkifier_rules(linkifiers) {
// Update the marked parser with our particular set of linkifiers
linkifier_map.clear();
linkifier_list = [];
const marked_rules = [];
for (const linkifier of linkifiers) {
const [regex, final_url] = python_to_js_linkifier(linkifier.pattern, linkifier.url_format);
@@ -92,18 +75,9 @@ export function update_linkifier_rules(linkifiers) {
}
linkifier_map.set(regex, final_url);
linkifier_list.push({
pattern: regex,
url_format: final_url,
});
marked_rules.push(regex);
}
marked.InlineLexer.rules.zulip.linkifiers = marked_rules;
}
export function initialize(linkifiers) {
update_linkifier_rules(linkifiers);
marked.setOptions({linkifierHandler: handleLinkifier});
}

View File

@@ -2,12 +2,10 @@ import {isValid} from "date-fns";
import katex from "katex"; // eslint-disable-line import/no-unresolved
import _ from "lodash";
import * as emoji from "../shared/js/emoji";
import * as fenced_code from "../shared/js/fenced_code";
import marked from "../third/marked/lib/marked";
import * as blueslip from "./blueslip";
import * as linkifiers from "./linkifiers";
// This contains zulip's frontend Markdown implementation; see
// docs/subsystems/markdown.md for docs on our Markdown syntax. The other
@@ -23,8 +21,9 @@ import * as linkifiers from "./linkifiers";
// for example usage.
let helpers;
// Regexes that match some of our common backend-only Markdown syntax
const backend_only_markdown_re = [
// If we see preview-related syntax in our content, we will need the
// backend to render it.
const preview_regexes = [
// Inline image previews, check for contiguous chars ending in image suffix
// To keep the below regexes simple, split them out for the end-of-message case
@@ -33,12 +32,16 @@ const backend_only_markdown_re = [
// Twitter and youtube links are given previews
/\S*(?:twitter|youtube).com\/\S*/,
/\S*(?:twitter|youtube)\.com\/\S*/,
];
export function translate_emoticons_to_names(text) {
function contains_preview_link(content) {
return preview_regexes.some((re) => re.test(content));
}
export function translate_emoticons_to_names({src, get_emoticon_translations}) {
// Translates emoticons in a string to their colon syntax.
let translated = text;
let translated = src;
let replacement_text;
const terminal_symbols = ",.;?!()[] \"'\n\t"; // From composebox_typeahead
const symbols_except_space = terminal_symbols.replace(" ", "");
@@ -64,7 +67,7 @@ export function translate_emoticons_to_names(text) {
return match;
};
for (const translation of emoji.get_emoticon_translations()) {
for (const translation of get_emoticon_translations()) {
// We can't pass replacement_text directly into
// emoticon_replacer, because emoticon_replacer is
// a callback for `replace()`. Instead we just mutate
@@ -76,30 +79,43 @@ export function translate_emoticons_to_names(text) {
return translated;
}
function contains_problematic_linkifier(content) {
// If a linkifier doesn't start with some specified characters
// then don't render it locally. It is workaround for the fact that
// javascript regex doesn't support lookbehind.
for (const re of helpers.get_linkifier_map().keys()) {
const pattern = /[^\s"'(,:<]/.source + re.source + /(?!\w)/.source;
const regex = new RegExp(pattern);
if (regex.test(content)) {
return true;
}
}
return false;
}
export function contains_backend_only_syntax(content) {
// Try to guess whether or not a message contains syntax that only the
// backend Markdown processor can correctly handle.
// If it doesn't, we can immediately render it client-side for local echo.
const markedup = backend_only_markdown_re.find((re) => re.test(content));
// If a linkifier doesn't start with some specified characters
// then don't render it locally. It is workaround for the fact that
// javascript regex doesn't support lookbehind.
const linkifier_list = linkifiers.linkifier_list;
const false_linkifier_match = linkifier_list.find((re) => {
const pattern = /[^\s"'(,:<]/.source + re.pattern.source + /(?!\w)/.source;
const regex = new RegExp(pattern);
return regex.test(content);
});
return markedup !== undefined || false_linkifier_match !== undefined;
return contains_preview_link(content) || contains_problematic_linkifier(content);
}
export function apply_markdown(message) {
function parse_with_options({raw_content, helper_config, options}) {
// Given the raw markdown content of a message (raw_content)
// we return the HTML content (content) and flags.
// Our caller passes a helper_config object that has several
// helper functions for getting info about users, streams, etc.
// And it also passes in options for the marked processor.
helpers = helper_config;
let mentioned = false;
let mentioned_group = false;
let mentioned_wildcard = false;
const options = {
const marked_options = {
...options,
userMentionHandler(mention, silently) {
if (mention === "all" || mention === "everyone" || mention === "stream") {
let classes;
@@ -144,15 +160,15 @@ export function apply_markdown(message) {
if (full_name === undefined) {
// For @**|id** syntax
if (!helpers.is_valid_user_id(user_id)) {
if (!helper_config.is_valid_user_id(user_id)) {
// silently ignore invalid user id.
user_id = undefined;
} else {
full_name = helpers.get_actual_name_from_user_id(user_id);
full_name = helper_config.get_actual_name_from_user_id(user_id);
}
} else {
// For @**user|id** syntax
if (!helpers.is_valid_full_name_and_user_id(full_name, user_id)) {
if (!helper_config.is_valid_full_name_and_user_id(full_name, user_id)) {
user_id = undefined;
full_name = undefined;
}
@@ -162,7 +178,7 @@ export function apply_markdown(message) {
if (user_id === undefined) {
// Handle normal syntax
full_name = mention;
user_id = helpers.get_user_id_from_name(full_name);
user_id = helper_config.get_user_id_from_name(full_name);
}
if (user_id === undefined) {
@@ -179,12 +195,12 @@ export function apply_markdown(message) {
// If I mention "@aLiCe sMITH", I still want "Alice Smith" to
// show in the pill.
let display_text = helpers.get_actual_name_from_user_id(user_id);
let display_text = helper_config.get_actual_name_from_user_id(user_id);
let classes;
if (silently) {
classes = "user-mention silent";
} else {
if (helpers.my_user_id() === user_id) {
if (helper_config.my_user_id() === user_id) {
// Personal mention of current user.
mentioned = true;
}
@@ -197,7 +213,7 @@ export function apply_markdown(message) {
)}</span>`;
},
groupMentionHandler(name, silently) {
const group = helpers.get_user_group_from_name(name);
const group = helper_config.get_user_group_from_name(name);
if (group !== undefined) {
let display_text;
let classes;
@@ -207,7 +223,9 @@ export function apply_markdown(message) {
} else {
display_text = "@" + group.name;
classes = "user-group-mention";
if (helpers.is_member_of_user_group(group.id, helpers.my_user_id())) {
if (
helper_config.is_member_of_user_group(group.id, helper_config.my_user_id())
) {
// Mentioned the current user's group.
mentioned_group = true;
}
@@ -249,34 +267,28 @@ export function apply_markdown(message) {
};
// Our Python-Markdown processor appends two \n\n to input
message.content = marked(message.raw_content + "\n\n", options).trim();
const content = marked(raw_content + "\n\n", marked_options).trim();
// Simulate message flags for our locally rendered
// message. Messages the user themselves sent via the browser are
// always marked as read.
message.flags = ["read"];
const flags = ["read"];
if (mentioned || mentioned_group) {
message.flags.push("mentioned");
flags.push("mentioned");
}
if (mentioned_wildcard) {
message.flags.push("wildcard_mentioned");
flags.push("wildcard_mentioned");
}
message.is_me_message = is_status_message(message.raw_content);
return {content, flags};
}
export function add_topic_links(message) {
if (message.type !== "stream") {
message.topic_links = [];
return;
}
const topic = message.topic;
export function get_topic_links({topic, get_linkifier_map}) {
// We export this for testing purposes, and mobile may want to
// use this as well in the future.
const links = [];
const linkifier_list = linkifiers.linkifier_list;
for (const linkifier of linkifier_list) {
const pattern = linkifier.pattern;
const url = linkifier.url_format;
for (const [pattern, url] of get_linkifier_map().entries()) {
let match;
while ((match = pattern.exec(topic)) !== null) {
let link_url = url;
@@ -307,7 +319,8 @@ export function add_topic_links(message) {
for (const match of links) {
delete match.index;
}
message.topic_links = links;
return links;
}
export function is_status_message(raw_content) {
@@ -320,9 +333,9 @@ function make_emoji_span(codepoint, title, alt_text) {
)}" role="img" title="${_.escape(title)}">${_.escape(alt_text)}</span>`;
}
function handleUnicodeEmoji(unicode_emoji) {
function handleUnicodeEmoji({unicode_emoji, get_emoji_name}) {
const codepoint = unicode_emoji.codePointAt(0).toString(16);
const emoji_name = emoji.get_emoji_name(codepoint);
const emoji_name = get_emoji_name(codepoint);
if (emoji_name) {
const alt_text = ":" + emoji_name + ":";
@@ -333,7 +346,7 @@ function handleUnicodeEmoji(unicode_emoji) {
return unicode_emoji;
}
function handleEmoji(emoji_name) {
function handleEmoji({emoji_name, get_realm_emoji_url, get_emoji_codepoint}) {
const alt_text = ":" + emoji_name + ":";
const title = emoji_name.replace(/_/g, " ");
@@ -344,7 +357,7 @@ function handleEmoji(emoji_name) {
// Otherwise we'll look at Unicode emoji to render with an emoji
// span using the spritesheet; and if it isn't one of those
// either, we pass through the plain text syntax unmodified.
const emoji_url = emoji.get_realm_emoji_url(emoji_name);
const emoji_url = get_realm_emoji_url(emoji_name);
if (emoji_url) {
return `<img alt="${_.escape(alt_text)}" class="emoji" src="${_.escape(
@@ -352,7 +365,7 @@ function handleEmoji(emoji_name) {
)}" title="${_.escape(title)}">`;
}
const codepoint = emoji.get_emoji_codepoint(emoji_name);
const codepoint = get_emoji_codepoint(emoji_name);
if (codepoint) {
return make_emoji_span(codepoint, title, alt_text);
}
@@ -360,6 +373,20 @@ function handleEmoji(emoji_name) {
return alt_text;
}
function handleLinkifier({pattern, matches, get_linkifier_map}) {
let url = get_linkifier_map().get(pattern);
let current_group = 1;
for (const match of matches) {
const back_ref = "\\" + current_group;
url = url.replace(back_ref, match);
current_group += 1;
}
return url;
}
function handleTimestamp(time) {
let timeobject;
if (Number.isNaN(Number(time))) {
@@ -386,23 +413,23 @@ function handleTimestamp(time) {
return `<time datetime="${escaped_isotime}">${escaped_time}</time>`;
}
function handleStream(stream_name) {
const stream = helpers.get_stream_by_name(stream_name);
function handleStream({stream_name, get_stream_by_name, stream_hash}) {
const stream = get_stream_by_name(stream_name);
if (stream === undefined) {
return undefined;
}
const href = helpers.stream_hash(stream.stream_id);
const href = stream_hash(stream.stream_id);
return `<a class="stream" data-stream-id="${_.escape(stream.stream_id)}" href="/${_.escape(
href,
)}">#${_.escape(stream.name)}</a>`;
}
function handleStreamTopic(stream_name, topic) {
const stream = helpers.get_stream_by_name(stream_name);
function handleStreamTopic({stream_name, topic, get_stream_by_name, stream_topic_hash}) {
const stream = get_stream_by_name(stream_name);
if (stream === undefined || !topic) {
return undefined;
}
const href = helpers.stream_topic_hash(stream.stream_id, topic);
const href = stream_topic_hash(stream.stream_id, topic);
const text = `#${stream.name} > ${topic}`;
return `<a class="stream-topic" data-stream-id="${_.escape(
stream.stream_id,
@@ -422,9 +449,11 @@ function handleTex(tex, fullmatch) {
}
}
export function initialize(helper_config) {
helpers = helper_config;
export function get_linkifier_regexes() {
return Array.from(helpers.get_linkifier_map().keys());
}
export function parse({raw_content, helper_config}) {
function disable_markdown_regex(rules, name) {
rules[name] = {
exec() {
@@ -434,18 +463,19 @@ export function initialize(helper_config) {
}
// Configure the marked Markdown parser for our usage
const r = new marked.Renderer();
const renderer = new marked.Renderer();
// No <code> around our code blocks instead a codehilite <div> and disable
// class-specific highlighting.
r.code = (code) => fenced_code.wrap_code(code) + "\n\n";
renderer.code = (code) => fenced_code.wrap_code(code) + "\n\n";
// Prohibit empty links for some reason.
const old_link = r.link;
r.link = (href, title, text) => old_link.call(r, href, title, text.trim() ? text : href);
const old_link = renderer.link;
renderer.link = (href, title, text) =>
old_link.call(renderer, href, title, text.trim() ? text : href);
// Put a newline after a <br> in the generated HTML to match Markdown
r.br = function () {
renderer.br = function () {
return "<br>\n";
};
@@ -454,13 +484,16 @@ export function initialize(helper_config) {
}
function preprocess_translate_emoticons(src) {
if (!helpers.should_translate_emoticons()) {
if (!helper_config.should_translate_emoticons()) {
return src;
}
// In this scenario, the message has to be from the user, so the only
// requirement should be that they have the setting on.
return translate_emoticons_to_names(src);
return translate_emoticons_to_names({
src,
get_emoticon_translations: helper_config.get_emoticon_translations,
});
}
// Disable headings
@@ -485,7 +518,55 @@ export function initialize(helper_config) {
// HTML into the output. This generated HTML is safe to not escape
fenced_code.set_stash_func((html) => marked.stashHtml(html, true));
marked.setOptions({
function streamHandler(stream_name) {
return handleStream({
stream_name,
get_stream_by_name: helper_config.get_stream_by_name,
stream_hash: helper_config.stream_hash,
});
}
function streamTopicHandler(stream_name, topic) {
return handleStreamTopic({
stream_name,
topic,
get_stream_by_name: helper_config.get_stream_by_name,
stream_topic_hash: helper_config.stream_topic_hash,
});
}
function emojiHandler(emoji_name) {
return handleEmoji({
emoji_name,
get_realm_emoji_url: helper_config.get_realm_emoji_url,
get_emoji_codepoint: helper_config.get_emoji_codepoint,
});
}
function unicodeEmojiHandler(unicode_emoji) {
return handleUnicodeEmoji({
unicode_emoji,
get_emoji_name: helper_config.get_emoji_name,
});
}
function linkifierHandler(pattern, matches) {
return handleLinkifier({
pattern,
matches,
get_linkifier_map: helper_config.get_linkifier_map,
});
}
const options = {
get_linkifier_regexes,
linkifierHandler,
emojiHandler,
unicodeEmojiHandler,
streamHandler,
streamTopicHandler,
texHandler: handleTex,
timestampHandler: handleTimestamp,
gfm: true,
tables: true,
breaks: true,
@@ -494,13 +575,52 @@ export function initialize(helper_config) {
smartLists: true,
smartypants: false,
zulip: true,
emojiHandler: handleEmoji,
unicodeEmojiHandler: handleUnicodeEmoji,
streamHandler: handleStream,
streamTopicHandler: handleStreamTopic,
texHandler: handleTex,
timestampHandler: handleTimestamp,
renderer: r,
renderer,
preprocessors: [preprocess_code_blocks, preprocess_translate_emoticons],
};
return parse_with_options({raw_content, helper_config, options});
}
// NOTE: Everything below this line is likely to be webapp-specific
// and won't be used by future platforms such as mobile.
// We may eventually move this code to a new file, but we want
// to wait till the dust settles a bit on some other changes first.
let webapp_helpers;
export function initialize(helper_config) {
// This is generally only intended to be called by the webapp. Most
// other platforms should call setup().
webapp_helpers = helper_config;
helpers = helper_config;
}
export function apply_markdown(message) {
// This is generally only intended to be called by the webapp. Most
// other platforms should call parse().
const raw_content = message.raw_content;
const {content, flags} = parse({raw_content, helper_config: webapp_helpers});
message.content = content;
message.flags = flags;
message.is_me_message = is_status_message(raw_content);
}
export function add_topic_links(message) {
if (message.type !== "stream") {
message.topic_links = [];
return;
}
message.topic_links = get_topic_links({
topic: message.topic,
get_linkifier_map: webapp_helpers.get_linkifier_map,
});
}
export function parse_non_message(raw_content) {
// Occasionally we get markdown from the server that is not technically
// a message, but we want to convert it to HTML. Note that we parse
// raw_content exactly as if it were a Zulip message, so we will
// handle things like mentions, stream links, and linkifiers.
return parse({raw_content, helper_config: webapp_helpers}).content;
}

View File

@@ -1,4 +1,6 @@
import * as emoji from "./emoji";
import * as hash_util from "./hash_util";
import * as linkifiers from "./linkifiers";
import * as people from "./people";
import * as stream_data from "./stream_data";
import * as user_groups from "./user_groups";
@@ -26,6 +28,36 @@ import {user_settings} from "./user_settings";
when the lookups fail.
*/
function abstract_map(map) {
return {
keys: () => map.keys(),
entries: () => map.entries(),
get: (k) => map.get(k),
};
}
function stream(obj) {
if (obj === undefined) {
return undefined;
}
return {
stream_id: obj.stream_id,
name: obj.name,
};
}
function user_group(obj) {
if (obj === undefined) {
return undefined;
}
return {
id: obj.id,
name: obj.name,
};
}
export const get_helpers = () => ({
// user stuff
get_actual_name_from_user_id: people.get_actual_name_from_user_id,
@@ -35,14 +67,23 @@ export const get_helpers = () => ({
is_valid_user_id: people.is_known_user_id,
// user groups
get_user_group_from_name: user_groups.get_user_group_from_name,
get_user_group_from_name: (name) => user_group(user_groups.get_user_group_from_name(name)),
is_member_of_user_group: user_groups.is_member_of,
// stream hashes
get_stream_by_name: stream_data.get_sub,
get_stream_by_name: (name) => stream(stream_data.get_sub(name)),
stream_hash: hash_util.by_stream_url,
stream_topic_hash: hash_util.by_stream_topic_url,
// settings
should_translate_emoticons: () => user_settings.translate_emoticons,
// emojis
get_emoji_name: emoji.get_emoji_name,
get_emoji_codepoint: emoji.get_emoji_codepoint,
get_emoticon_translations: emoji.get_emoticon_translations,
get_realm_emoji_url: emoji.get_realm_emoji_url,
// linkifiers
get_linkifier_map: () => abstract_map(linkifiers.get_linkifier_map()),
});

View File

@@ -23,6 +23,7 @@ import * as util from "./util";
export function resize_app() {
const navbar_alerts_wrapper_height = $("#navbar_alerts_wrapper").height();
$("body > .app").height("calc(100% - " + navbar_alerts_wrapper_height + "px)");
$(".recent_topics_container").height("calc(100vh - " + navbar_alerts_wrapper_height + "px)");
// the floating recipient bar is usually positioned right below
// the `.header` element (including padding).

View File

@@ -1,10 +1,10 @@
import $ from "jquery";
import * as emoji from "../shared/js/emoji";
import render_message_reaction from "../templates/message_reaction.hbs";
import * as blueslip from "./blueslip";
import * as channel from "./channel";
import * as emoji from "./emoji";
import * as emoji_picker from "./emoji_picker";
import {$t} from "./i18n";
import * as message_lists from "./message_lists";

View File

@@ -1,7 +1,5 @@
import $ from "jquery";
import * as emoji from "../shared/js/emoji";
import * as activity from "./activity";
import * as alert_words from "./alert_words";
import * as alert_words_ui from "./alert_words_ui";
@@ -15,6 +13,7 @@ import * as compose_fade from "./compose_fade";
import * as compose_pm_pill from "./compose_pm_pill";
import * as composebox_typeahead from "./composebox_typeahead";
import * as dark_theme from "./dark_theme";
import * as emoji from "./emoji";
import * as emoji_picker from "./emoji_picker";
import * as giphy from "./giphy";
import * as hotspots from "./hotspots";

View File

@@ -98,6 +98,7 @@ export function build_page() {
user_can_change_avatar: settings_data.user_can_change_avatar(),
user_role_text: people.get_user_type(page_params.user_id),
default_language_name: settings_display.user_default_language_name,
realm_push_notifications_enabled: page_params.realm_push_notifications_enabled,
settings_object: user_settings,
});

View File

@@ -170,6 +170,7 @@ export function append_custom_profile_fields(element_id, user_id) {
is_long_text_field: field.type === all_field_types.LONG_TEXT.id,
is_user_field: field.type === all_field_types.USER.id,
is_date_field: field.type === all_field_types.DATE.id,
is_url_field: field.type === all_field_types.URL.id,
is_select_field,
field_choices,
});

View File

@@ -362,7 +362,7 @@ export function set_up() {
$(`[name*='${CSS.escape(selected_bot)}']`).show();
});
$("#active_bots_list").on("click", "button.delete_bot", (e) => {
$("#active_bots_list").on("click", "button.deactivate_bot", (e) => {
const bot_id = Number.parseInt($(e.currentTarget).attr("data-user-id"), 10);
channel.del({

View File

@@ -1,13 +1,13 @@
import $ from "jquery";
import emoji_codes from "../generated/emoji/emoji_codes.json";
import * as emoji from "../shared/js/emoji";
import emoji_settings_warning_modal from "../templates/confirm_dialog/confirm_emoji_settings_warning.hbs";
import render_admin_emoji_list from "../templates/settings/admin_emoji_list.hbs";
import render_settings_emoji_settings_tip from "../templates/settings/emoji_settings_tip.hbs";
import * as channel from "./channel";
import * as confirm_dialog from "./confirm_dialog";
import * as emoji from "./emoji";
import {$t_html} from "./i18n";
import * as ListWidget from "./list_widget";
import * as loading from "./loading";

View File

@@ -318,6 +318,7 @@ export function update_settings_for_subscribed(slim_sub) {
// Display the swatch and subscription stream_settings
stream_ui_updates.update_regular_sub_settings(sub);
stream_ui_updates.update_permissions_banner(sub);
}
export function show_active_stream_in_left_panel() {
@@ -350,6 +351,7 @@ export function update_settings_for_unsubscribed(slim_sub) {
// Remove private streams from subscribed streams list.
stream_ui_updates.update_stream_row_in_settings_tab(sub);
stream_ui_updates.update_permissions_banner(sub);
}
function triage_stream(left_panel_params, sub) {

View File

@@ -1,6 +1,7 @@
import $ from "jquery";
import render_stream_permission_description from "../templates/stream_settings/stream_permission_description.hbs";
import render_stream_settings_tip from "../templates/stream_settings/stream_settings_tip.hbs";
import * as hash_util from "./hash_util";
import {$t} from "./i18n";
@@ -122,6 +123,13 @@ export function update_change_stream_privacy_settings(sub) {
}
}
export function update_permissions_banner(sub) {
const $settings = $(`.subscription_settings[data-stream-id='${CSS.escape(sub.stream_id)}']`);
const rendered_tip = render_stream_settings_tip(sub);
$settings.find(".stream-settings-tip-container").html(rendered_tip);
}
export function update_notification_setting_checkbox(notification_name) {
// This is in the right panel (Personal settings).
const $stream_row = $("#manage_streams_container .stream-row.active");

View File

@@ -3,7 +3,6 @@ import _ from "lodash";
import generated_emoji_codes from "../generated/emoji/emoji_codes.json";
import generated_pygments_data from "../generated/pygments_data.json";
import * as emoji from "../shared/js/emoji";
import * as fenced_code from "../shared/js/fenced_code";
import render_compose from "../templates/compose.hbs";
import render_edit_content_button from "../templates/edit_content_button.hbs";
@@ -28,6 +27,7 @@ import * as copy_and_paste from "./copy_and_paste";
import * as dark_theme from "./dark_theme";
import * as drafts from "./drafts";
import * as echo from "./echo";
import * as emoji from "./emoji";
import * as emoji_picker from "./emoji_picker";
import * as emojisets from "./emojisets";
import * as gear_menu from "./gear_menu";

View File

@@ -1,7 +1,6 @@
import * as emoji from "../shared/js/emoji";
import * as blueslip from "./blueslip";
import * as channel from "./channel";
import * as emoji from "./emoji";
import {user_settings} from "./user_settings";
const away_user_ids = new Set();

View File

@@ -1,10 +1,10 @@
import $ from "jquery";
import * as emoji from "../shared/js/emoji";
import render_set_status_overlay from "../templates/set_status_overlay.hbs";
import render_status_emoji_selector from "../templates/status_emoji_selector.hbs";
import * as dialog_widget from "./dialog_widget";
import * as emoji from "./emoji";
import {$t, $t_html} from "./i18n";
import * as people from "./people";
import * as user_status from "./user_status";

View File

@@ -1,12 +1,11 @@
import $ from "jquery";
import marked from "../third/marked/lib/marked";
import * as channel from "./channel";
import * as common from "./common";
import * as dark_theme from "./dark_theme";
import * as feedback_widget from "./feedback_widget";
import {$t} from "./i18n";
import * as markdown from "./markdown";
import * as scroll_bar from "./scroll_bar";
/*
@@ -66,7 +65,7 @@ export function switch_to_light_theme() {
dark_theme.disable();
feedback_widget.show({
populate($container) {
const rendered_msg = marked(data.msg).trim();
const rendered_msg = markdown.parse_non_message(data.msg);
$container.html(rendered_msg);
},
on_undo() {
@@ -88,7 +87,7 @@ export function switch_to_dark_theme() {
dark_theme.enable();
feedback_widget.show({
populate($container) {
const rendered_msg = marked(data.msg).trim();
const rendered_msg = markdown.parse_non_message(data.msg);
$container.html(rendered_msg);
},
on_undo() {
@@ -110,7 +109,7 @@ export function enter_fluid_mode() {
scroll_bar.set_layout_width();
feedback_widget.show({
populate($container) {
const rendered_msg = marked(data.msg).trim();
const rendered_msg = markdown.parse_non_message(data.msg);
$container.html(rendered_msg);
},
on_undo() {
@@ -132,7 +131,7 @@ export function enter_fixed_mode() {
scroll_bar.set_layout_width();
feedback_widget.show({
populate($container) {
const rendered_msg = marked(data.msg).trim();
const rendered_msg = markdown.parse_non_message(data.msg);
$container.html(rendered_msg);
},
on_undo() {

View File

@@ -0,0 +1,46 @@
// @flow strict
/**
* The data encoded in a submessage that acts on a poll widget.
*
* In reality these are more specific than this type. But they're currently
* completely undocumented in the API:
* https://chat.zulip.org/#narrow/stream/412-api-documentation/topic/.60.2Esubmessages.60.20on.20message.20objects/near/1358493
* so we don't attempt to go any further here.
*/
type PollEvent = {...};
declare export class PollData {
constructor({
message_sender_id: number,
current_user_id: number,
is_my_poll: boolean,
question: string,
options: interface {entries(): Iterable<[number, string]>},
comma_separated_names: (user_ids: number[]) => string,
report_error_function: (msg: string) => void,
}): void;
set_question(question: string): void;
get_question(): string;
set_input_mode(): void;
clear_input_mode(): void;
get_input_mode(): boolean;
get_widget_data(): {
question: string,
options: Array<{
option: string,
names: string,
count: number,
key: string,
current_user_vote: boolean,
}>,
};
handle_event(sender_id: number, data: PollEvent): void;
}

View File

@@ -1,6 +1,6 @@
{
"name": "@zulip/shared",
"version": "0.0.9",
"version": "0.0.11",
"license": "Apache-2.0",
"dependencies": {
"katex": "^0.15.3",

View File

@@ -857,6 +857,7 @@ h4.stream_setting_subsection_title {
.stream-header {
white-space: nowrap;
padding-top: 10px;
.stream-name {
display: inline-block;

View File

@@ -1,2 +1,5 @@
Archiving stream <strong>{{stream_name}}</strong> <span>will immediately unsubscribe everyone. This action cannot be undone.</span>
{{#tr}}
Archiving stream <z-stream></z-stream> will immediately unsubscribe everyone. This action cannot be undone.
{{#*inline "z-stream"}}<strong>{{stream_name}}</strong>{{/inline}}
{{/tr}}
<p><strong>{{t "Are you sure you want to archive this stream?" }}</strong></p>

View File

@@ -14,8 +14,8 @@
<button type="submit" id="copy_zuliprc" class="btn copy_zuliprc" title="{{t 'Copy zuliprc' }}">
<i class="fa fa-clipboard copy-gold"></i>
</button>
<button type="submit" class="btn delete_bot" title="{{t 'Delete bot' }}" data-user-id="{{user_id}}">
<i class="fa fa-trash-o danger-red" aria-hidden="true"></i>
<button type="submit" class="btn deactivate_bot danger-red" title="{{t 'Deactivate bot' }}" data-user-id="{{user_id}}">
<i class="fa fa-user-times" aria-hidden="true"></i>
</button>
</div>
{{/if}}

View File

@@ -20,6 +20,8 @@
<input class="custom_user_field_value datepicker" data-field-id="{{ field.id }}" type="text"
value="{{ field_value.value }}" />
<span class="remove_date"><i class="fa fa-close"></i></span>
{{else if is_url_field }}
<input class="custom_user_field_value" type="{{ field_type }}" value="{{ field_value.value }}" maxlength="2048" />
{{else}}
<input class="custom_user_field_value" type="{{ field_type }}" value="{{ field_value.value }}" maxlength="50" />
{{/if}}

View File

@@ -12,7 +12,7 @@
<th colspan="2" width="28%">{{t "Desktop"}}</th>
<th rowspan="2" width="14%" class="{{#if show_push_notifications_tooltip.push_notifications}}control-label-disabled{{/if}}">
{{t "Mobile"}}
{{#if (not page_params.realm_push_notifications_enabled) }}
{{#if (not realm_push_notifications_enabled) }}
<i class="fa fa-question-circle settings-info-icon tippy-zulip-tooltip" data-tippy-content="{{t 'Mobile push notifications are not configured on this server.' }}"></i>
{{/if}}
</th>

View File

@@ -9,6 +9,6 @@
{{/if}}
<td>{{user_id}} </td>
<td>
<button {{#if disabled}} disabled="disabled"{{/if}} data-user-id="{{user_id}}" class="remove_potential_subscriber button small rounded btn-danger">Remove</button>
<button {{#if disabled}} disabled="disabled"{{/if}} data-user-id="{{user_id}}" class="remove_potential_subscriber button small rounded btn-danger">{{t 'Remove' }}</button>
</td>
</tr>

View File

@@ -7,7 +7,7 @@
<button class="add_all_users_to_stream small button rounded sea-green">{{t 'Add all users'}}</button>
<div class="create_stream_subscriber_list_header">
<h4 class="stream_setting_subsection_title">Subscribers</h4>
<h4 class="stream_setting_subsection_title">{{t 'Subscribers' }}</h4>
<input class="add-user-list-filter" name="user_list_filter" type="text"
autocomplete="off" placeholder="{{t 'Filter subscribers' }}" />
</div>

View File

@@ -23,6 +23,9 @@
<div class="general_settings stream_section">
{{#with sub}}
<div class="stream-settings-tip-container">
{{> stream_settings_tip}}
</div>
<div class="stream-header">
{{> stream_privacy_icon
invite_only=invite_only
@@ -31,7 +34,7 @@
<span class="sub-stream-name" title="{{name}}">{{name}}</span>
</div>
<div class="stream_change_property_info alert-notification"></div>
<div class="button-group">
<div class="button-group" {{#unless can_change_name_description}}style="display:none"{{/unless}}>
<button id="open_stream_info_modal" class="button rounded small btn-warning" title="{{t 'Change stream info' }}">
<i class="fa fa-pencil" aria-hidden="true"></i>
</button>

View File

@@ -0,0 +1,7 @@
{{#unless can_change_stream_permissions}}
{{#if can_change_name_description}}
<div class="tip">{{t "Only subscribers to this stream can edit stream permissions."}}</div>
{{else}}
<div class="tip">{{t "Only organization administrators can edit these settings."}}</div>
{{/if}}
{{/unless}}

View File

@@ -484,7 +484,6 @@ var inline = {
stream: noop,
tex: noop,
timestamp: noop,
linkifiers: [],
text: /^[\s\S]+?(?=[\\<!\[_*`$]| {2,}\n|$)/
};
@@ -550,7 +549,6 @@ inline.zulip = merge({}, inline.breaks, {
stream: /^#\*\*([^\*]+)\*\*/,
tex: /^(\$\$([^\n_$](\\\$|[^\n$])*)\$\$(?!\$))\B/,
timestamp: /^<time:([^>]+)>/,
linkifiers: [],
text: replace(inline.breaks.text)
('|', '|(\ud83c[\udd00-\udfff]|\ud83d[\udc00-\ude4f]|' +
'\ud83d[\ude80-\udeff]|\ud83e[\udd00-\uddff]|' +
@@ -647,8 +645,10 @@ InlineLexer.prototype.output = function(src) {
// linkifier (Zulip)
var self = this;
this.rules.linkifiers.forEach(function (linkifier) {
var ret = self.inlineReplacement(linkifier, src, function(regex, groups, match) {
const regexes = this.options.get_linkifier_regexes ? this.options.get_linkifier_regexes() : [];
regexes.forEach(function (regex) {
var ret = self.inlineReplacement(regex, src, function(regex, groups, match) {
// Insert the created URL
href = self.linkifier(regex, groups, match);
if (href !== undefined) {
@@ -1416,9 +1416,6 @@ function marked(src, opt, callback) {
htmlStashCounter = 0;
htmlStash = [];
for (var k = 0; k < opt.preprocessors.length; k++) {
src = opt.preprocessors[k](src);
}
try {
tokens = Lexer.lex(src, opt)
@@ -1480,8 +1477,8 @@ function marked(src, opt, callback) {
if (opt) opt = merge({}, marked.defaults, opt);
htmlStashCounter = 0;
htmlStash = [];
for (var i = 0; i < marked.defaults.preprocessors.length; i++) {
src = marked.defaults.preprocessors[i](src);
for (var i = 0; i < opt.preprocessors.length; i++) {
src = opt.preprocessors[i](src);
}
return Parser.parse(Lexer.lex(src, opt), opt);
} catch (e) {

View File

@@ -18,6 +18,11 @@ clients should check the `zulip_feature_level` field, present in the
/register`](/api/register-queue) responses, to determine the API
format used by the Zulip server that they are interacting with.
## Changes in Zulip 6.0
Feature levels 123-124 are reserved for future use in 5.x maintenance
releases.
## Changes in Zulip 5.0
**Feature level 122**

View File

@@ -143,7 +143,7 @@
<li><a href="/team/">{{ _("Team") }}</a> &amp; <a href="/history/">{{ _("History") }}</a></li>
<li><a href="https://twitter.com/zulip/">Twitter</a></li>
<li><a href="/jobs/">{{ _("Jobs") }}</a></li>
<li><a href="/attribution">Website attributions</a></li>
<li><a href="/attribution">{{ _("Website attributions") }}</a></li>
<li><a href="https://github.com/sponsors/zulip">{{ _("Sponsor Zulip") }}</a></li>
</ul>
</div>

View File

@@ -39,7 +39,7 @@ rules:
- zerver/migrations/0206_stream_rendered_description.py
- zerver/migrations/0209_user_profile_no_empty_password.py
- zerver/migrations/0260_missed_message_addresses_from_redis_to_db.py
- zerver/migrations/0376_set_realmemoji_author_and_reupload_realmemoji.py
- zerver/migrations/0387_reupload_realmemoji_again.py
- pgroonga/migrations/0002_html_escape_subject.py
- id: logging-format

View File

@@ -7,6 +7,8 @@ import sys
from collections import defaultdict
from typing import Dict, List
bot_commits = 0
def add_log(committer_dict: Dict[str, int], input: List[str]) -> None:
for dataset in input:
@@ -15,6 +17,8 @@ def add_log(committer_dict: Dict[str, int], input: List[str]) -> None:
if committer_name.endswith("[bot]"):
# Exclude dependabot[bot] and other GitHub bots.
global bot_commits
bot_commits += commit_count
continue
committer_dict[committer_name] += commit_count
@@ -131,11 +135,21 @@ print(
f"Commit range {lower_zulip_version}..{upper_zulip_version} corresponds to {lower_time} to {upper_time}"
)
repository_dict: Dict[str, int] = defaultdict(int)
out_dict: Dict[str, int] = defaultdict(int)
subprocess.check_call(["git", "fetch"], cwd=find_path("zulip"))
zulip = retrieve_log("zulip", lower_zulip_version, upper_zulip_version)
print(f"Commit range for zulip/zulip: {lower_zulip_version[0:12]}..{upper_zulip_version[0:12]}")
add_log(out_dict, zulip)
commit_count = len(
subprocess.check_output(
["git", "log", "--pretty=oneline", f"{lower_zulip_version}..{upper_zulip_version}"],
cwd=find_path("zulip"),
text=True,
).splitlines()
)
repo_log = retrieve_log("zulip", lower_zulip_version, upper_zulip_version)
print(
f"{commit_count} commits from zulip/zulip: {lower_zulip_version[0:12]}..{upper_zulip_version[0:12]}"
)
add_log(out_dict, repo_log)
# TODO: We should migrate the last couple repositories to use the
# `main` default branch name and then simplify this.
@@ -163,9 +177,16 @@ for (full_repository, branch) in [
subprocess.check_call(["git", "fetch"], cwd=find_path(repository))
lower_repo_version = find_last_commit_before_time(repository, branch, lower_time)
upper_repo_version = find_last_commit_before_time(repository, branch, upper_time)
commit_count = len(
subprocess.check_output(
["git", "log", "--pretty=oneline", f"{lower_repo_version}..{upper_repo_version}"],
cwd=find_path(repository),
text=True,
).splitlines()
)
repo_log = retrieve_log(repository, lower_repo_version, upper_repo_version)
print(
f"Commit range for {full_repository}: {lower_repo_version[0:12]}..{upper_repo_version[0:12]}"
f"{commit_count} commits from {full_repository}: {lower_repo_version[0:12]}..{upper_repo_version[0:12]}"
)
add_log(out_dict, repo_log)
@@ -177,7 +198,8 @@ for committer_name, commit_count in sorted(
print(str(commit_count) + "\t" + committer_name)
grand_total += commit_count
print(f"Excluded {bot_commits} commits authored by bots.")
print(
f"{grand_total} total commits by {len(out_dict)} contributors between "
f"{lower_zulip_version} and {upper_repo_version}."
f"{lower_zulip_version} and {upper_zulip_version}."
)

View File

@@ -1,6 +1,6 @@
import os
ZULIP_VERSION = "5.0"
ZULIP_VERSION = "6.0-dev+git"
# Add information on number of commits and commit hash to version, if available
zulip_git_version_file = os.path.join(
@@ -14,8 +14,8 @@ ZULIP_VERSION = lines.pop(0).strip()
ZULIP_MERGE_BASE = lines.pop(0).strip()
LATEST_MAJOR_VERSION = "5.0"
LATEST_RELEASE_VERSION = "5.0"
LATEST_RELEASE_ANNOUNCEMENT = "https://blog.zulip.com/2021/05/13/zulip-4-0-released/"
LATEST_RELEASE_VERSION = "5.1"
LATEST_RELEASE_ANNOUNCEMENT = "https://blog.zulip.com/2022/03/29/zulip-5-0-released/"
# Versions of the desktop app below DESKTOP_MINIMUM_VERSION will be
# prevented from connecting to the Zulip server. Versions above

View File

@@ -78,14 +78,13 @@ class SlackBotEmail:
else:
raise AssertionError("Could not identify bot type")
email = slack_bot_name.replace("Bot", "").replace(" ", "") + f"-bot@{domain_name}"
email = slack_bot_name.replace("Bot", "").replace(" ", "").lower() + f"-bot@{domain_name}"
if email in cls.duplicate_email_count:
email_prefix, email_suffix = email.split("@")
email_prefix += cls.duplicate_email_count[email]
email = "@".join([email_prefix, email_suffix])
# Increment the duplicate email count
cls.duplicate_email_count[email] += 1
email_prefix, email_suffix = email.split("@")
email_prefix += "-" + str(cls.duplicate_email_count[email])
email = "@".join([email_prefix, email_suffix])
else:
cls.duplicate_email_count[email] = 1

View File

@@ -1,10 +1,7 @@
from django.conf import settings
from django.db import migrations
from django.db.backends.postgresql.schema import DatabaseSchemaEditor
from django.db.migrations.state import StateApps
from zerver.lib.queue import queue_json_publish
def set_emoji_author(apps: StateApps, schema_editor: DatabaseSchemaEditor) -> None:
"""
@@ -13,7 +10,6 @@ def set_emoji_author(apps: StateApps, schema_editor: DatabaseSchemaEditor) -> No
"""
RealmEmoji = apps.get_model("zerver", "RealmEmoji")
Realm = apps.get_model("zerver", "Realm")
UserProfile = apps.get_model("zerver", "UserProfile")
ROLE_REALM_OWNER = 100
@@ -32,18 +28,12 @@ def set_emoji_author(apps: StateApps, schema_editor: DatabaseSchemaEditor) -> No
RealmEmoji.objects.bulk_update(realm_emoji_to_update, ["author_id"])
if settings.TEST_SUITE:
# There are no custom emoji in the test suite data set, and
# the below code won't work because RabbitMQ isn't enabled for
# the test suite.
return
for realm_id in Realm.objects.order_by("id").values_list("id", flat=True):
event = {
"type": "reupload_realm_emoji",
"realm_id": realm_id,
}
queue_json_publish("deferred_work", event)
# Previously, this also pushed `reupload_realm_emoji` events onto
# the `deferred_work` queue; however,
# https://github.com/zulip/zulip/issues/21608 made those possibly
# run too early, and that work was repeated in migration 0387 to
# ensure it ran. As such, the work has been removed from this
# migration, so it does not unnecessarily run twice.
class Migration(migrations.Migration):

View File

@@ -0,0 +1,48 @@
from django.conf import settings
from django.db import migrations
from django.db.backends.postgresql.schema import DatabaseSchemaEditor
from django.db.migrations.state import StateApps
from zerver.lib.queue import queue_json_publish
def reupload_realm_emoji(apps: StateApps, schema_editor: DatabaseSchemaEditor) -> None:
"""As detailed in https://github.com/zulip/zulip/issues/21608, it is
possible for the deferred_work queue from Zulip 4.x to have been
started up by puppet during the deployment before migrations were
run on Zulip 5.0.
This means that the deferred_work events originally produced by
migration 0376 might have been processed and discarded without
effect.
That code has been removed from the 0376 migration, and we run it
here, after the upgrade code has been fixed; servers which already
processed that migration might at worst do this work twice, which
is harmless aside from being a small waste of resources.
"""
Realm = apps.get_model("zerver", "Realm")
if settings.TEST_SUITE:
# There are no custom emoji in the test suite data set, and
# the below code won't work because RabbitMQ isn't enabled for
# the test suite.
return
for realm_id in Realm.objects.order_by("id").values_list("id", flat=True):
event = {
"type": "reupload_realm_emoji",
"realm_id": realm_id,
}
queue_json_publish("deferred_work", event)
class Migration(migrations.Migration):
dependencies = [
("zerver", "0386_fix_attachment_caches"),
]
operations = [
migrations.RunPython(reupload_realm_emoji, reverse_code=migrations.RunPython.noop),
]

View File

@@ -322,6 +322,7 @@ class EditMessageTest(EditMessageTestCase):
self.assert_json_success(result)
self.assertEqual(result.json()["raw_content"], "Personal message")
self.assertEqual(result.json()["message"]["id"], msg_id)
self.assertEqual(result.json()["message"]["flags"], [])
# Send message to web public stream where hamlet is not subscribed.
# This will test case of user having no `UserMessage` but having access
@@ -335,6 +336,7 @@ class EditMessageTest(EditMessageTestCase):
self.assert_json_success(result)
self.assertEqual(result.json()["raw_content"], "web-public message")
self.assertEqual(result.json()["message"]["id"], web_public_stream_msg_id)
self.assertEqual(result.json()["message"]["flags"], ["read", "historical"])
# Spectator should be able to fetch message in web public stream.
self.logout()
@@ -416,6 +418,7 @@ class EditMessageTest(EditMessageTestCase):
result = self.client_get("/json/messages/" + str(web_public_stream_msg_id))
self.assert_json_success(result)
self.assertEqual(result.json()["raw_content"], "web-public message")
self.assertEqual(result.json()["message"]["flags"], ["read"])
# Verify LIMITED plan type does not allow web-public access.
do_change_realm_plan_type(user_profile.realm, Realm.PLAN_TYPE_LIMITED, acting_user=None)

View File

@@ -1,4 +1,3 @@
from collections import namedtuple
from typing import Any, List
from unittest import mock
@@ -8,33 +7,23 @@ from django.utils.timezone import now as timezone_now
from zerver.lib.actions import create_mirror_user_if_needed
from zerver.lib.create_user import create_user_profile
from zerver.lib.test_classes import ZulipTestCase
from zerver.lib.test_helpers import HostRequestMock, reset_emails_in_zulip_realm
from zerver.models import UserProfile, get_realm, get_user
from zerver.lib.test_helpers import reset_emails_in_zulip_realm
from zerver.models import UserProfile, get_client, get_realm, get_user
from zerver.views.message_send import InvalidMirrorInput, create_mirrored_message_users
class MirroredMessageUsersTest(ZulipTestCase):
def test_invalid_sender(self) -> None:
user = self.example_user("hamlet")
recipients: List[str] = []
Request = namedtuple("Request", ["POST"])
request = Request(POST={}) # no sender
with self.assertRaises(InvalidMirrorInput):
create_mirrored_message_users(request, user, recipients)
def test_invalid_client(self) -> None:
user = self.example_user("hamlet")
sender = user
recipients: List[str] = []
post_data = dict(sender=sender.email, type="private")
request = HostRequestMock(post_data=post_data, client_name="banned_mirror")
message_type = "private"
client = get_client("banned_mirror")
with self.assertRaises(InvalidMirrorInput):
create_mirrored_message_users(request, user, recipients)
create_mirrored_message_users(client, user, recipients, sender.email, message_type)
def test_invalid_email(self) -> None:
invalid_email = "alice AT example.com"
@@ -44,13 +33,13 @@ class MirroredMessageUsersTest(ZulipTestCase):
user = self.mit_user("starnine")
sender = user
post_data = dict(sender=sender.email, type="private")
message_type = "private"
for client_name in ["zephyr_mirror", "irc_mirror", "jabber_mirror"]:
request = HostRequestMock(post_data=post_data, client_name=client_name)
client = get_client(client_name)
with self.assertRaises(InvalidMirrorInput):
create_mirrored_message_users(request, user, recipients)
create_mirrored_message_users(client, user, recipients, sender.email, message_type)
@mock.patch(
"DNS.dnslookup",
@@ -65,11 +54,12 @@ class MirroredMessageUsersTest(ZulipTestCase):
recipients = [user.email, new_user_email]
# Now make the request.
post_data = dict(sender=sender.email, type="private")
request = HostRequestMock(post_data=post_data, client_name="zephyr_mirror")
message_type = "private"
client = get_client("zephyr_mirror")
mirror_sender = create_mirrored_message_users(request, user, recipients)
mirror_sender = create_mirrored_message_users(
client, user, recipients, sender.email, message_type
)
self.assertEqual(mirror_sender, sender)
@@ -92,11 +82,12 @@ class MirroredMessageUsersTest(ZulipTestCase):
recipients = ["stream_name"]
# Now make the request.
post_data = dict(sender=sender_email, type="stream")
request = HostRequestMock(post_data=post_data, client_name="zephyr_mirror")
message_type = "stream"
client = get_client("zephyr_mirror")
mirror_sender = create_mirrored_message_users(request, user, recipients)
mirror_sender = create_mirrored_message_users(
client, user, recipients, sender_email, message_type
)
assert mirror_sender is not None
self.assertEqual(mirror_sender.email, sender_email)
@@ -105,7 +96,8 @@ class MirroredMessageUsersTest(ZulipTestCase):
def test_irc_mirror(self) -> None:
reset_emails_in_zulip_realm()
sender = self.example_user("hamlet")
user = self.example_user("hamlet")
sender = user
recipients = [
self.nonreg_email("alice"),
@@ -113,11 +105,12 @@ class MirroredMessageUsersTest(ZulipTestCase):
self.nonreg_email("cordelia"),
]
# Now make the request.
post_data = dict(sender=sender.email, type="private")
request = HostRequestMock(post_data=post_data, client_name="irc_mirror")
message_type = "private"
client = get_client("irc_mirror")
mirror_sender = create_mirrored_message_users(request, sender, recipients)
mirror_sender = create_mirrored_message_users(
client, user, recipients, sender.email, message_type
)
self.assertEqual(mirror_sender, sender)
@@ -132,8 +125,8 @@ class MirroredMessageUsersTest(ZulipTestCase):
def test_jabber_mirror(self) -> None:
reset_emails_in_zulip_realm()
sender = self.example_user("hamlet")
user = sender
user = self.example_user("hamlet")
sender = user
recipients = [
self.nonreg_email("alice"),
@@ -141,11 +134,12 @@ class MirroredMessageUsersTest(ZulipTestCase):
self.nonreg_email("cordelia"),
]
# Now make the request.
post_data = dict(sender=sender.email, type="private")
request = HostRequestMock(post_data=post_data, client_name="jabber_mirror")
message_type = "private"
client = get_client("jabber_mirror")
mirror_sender = create_mirrored_message_users(request, user, recipients)
mirror_sender = create_mirrored_message_users(
client, user, recipients, sender.email, message_type
)
self.assertEqual(mirror_sender, sender)

View File

@@ -25,6 +25,7 @@ from zerver.data_import.slack import (
AddedChannelsT,
AddedMPIMsT,
DMMembersT,
SlackBotEmail,
channel_message_to_zerver_message,
channels_to_zerver_stream,
convert_slack_workspace_messages,
@@ -1213,3 +1214,38 @@ class SlackImporter(ZulipTestCase):
self.assertEqual(uploads_list[0]["s3_path"], image_path)
self.assertEqual(uploads_list[0]["realm_id"], realm_id)
self.assertEqual(uploads_list[0]["user_profile_email"], "alice@example.com")
def test_bot_duplicates(self) -> None:
self.assertEqual(
SlackBotEmail.get_email(
{"real_name_normalized": "Real Bot", "bot_id": "foo"}, "example.com"
),
"real-bot@example.com",
)
# SlackBotEmail keeps state -- doing it again appends a "2", "3", etc
self.assertEqual(
SlackBotEmail.get_email(
{"real_name_normalized": "Real Bot", "bot_id": "bar"}, "example.com"
),
"real-bot-2@example.com",
)
self.assertEqual(
SlackBotEmail.get_email(
{"real_name_normalized": "Real Bot", "bot_id": "baz"}, "example.com"
),
"real-bot-3@example.com",
)
# But caches based on the bot_id
self.assertEqual(
SlackBotEmail.get_email(
{"real_name_normalized": "Real Bot", "bot_id": "foo"}, "example.com"
),
"real-bot@example.com",
)
self.assertEqual(
SlackBotEmail.get_email({"first_name": "Other Name", "bot_id": "other"}, "example.com"),
"othername-bot@example.com",
)

View File

@@ -978,7 +978,6 @@ class StreamAdminTest(ZulipTestCase):
self.assertTrue(attachment.is_realm_public)
params = {
"stream_name": orjson.dumps("test_stream").decode(),
"is_private": orjson.dumps(True).decode(),
"history_public_to_subscribers": orjson.dumps(True).decode(),
}
@@ -1000,7 +999,6 @@ class StreamAdminTest(ZulipTestCase):
self.assertFalse(validate_attachment_request_for_spectator_access(realm, attachment))
params = {
"stream_name": orjson.dumps("test_stream").decode(),
"is_private": orjson.dumps(False).decode(),
"is_web_public": orjson.dumps(True).decode(),
"history_public_to_subscribers": orjson.dumps(True).decode(),
@@ -1025,7 +1023,6 @@ class StreamAdminTest(ZulipTestCase):
self.assertTrue(attachment.is_realm_public)
params = {
"stream_name": orjson.dumps("test_stream").decode(),
"is_private": orjson.dumps(False).decode(),
"is_web_public": orjson.dumps(False).decode(),
"history_public_to_subscribers": orjson.dumps(True).decode(),

View File

@@ -205,6 +205,8 @@ def json_fetch_raw_message(
else:
if user_message:
flags = user_message.flags_list()
else:
flags = ["read", "historical"]
allow_edit_history = maybe_user_profile.realm.allow_edit_history
# Security note: It's important that we call this only with a

View File

@@ -42,20 +42,19 @@ class InvalidMirrorInput(Exception):
def create_mirrored_message_users(
request: HttpRequest, user_profile: UserProfile, recipients: Iterable[str]
client: Client,
user_profile: UserProfile,
recipients: Iterable[str],
sender: str,
message_type: str,
) -> UserProfile:
if "sender" not in request.POST:
raise InvalidMirrorInput("No sender")
sender_email = request.POST["sender"].strip().lower()
sender_email = sender.strip().lower()
referenced_users = {sender_email}
if request.POST["type"] == "private":
if message_type == "private":
for email in recipients:
referenced_users.add(email.lower())
client = RequestNotes.get_notes(request).client
assert client is not None
if client.name == "zephyr_mirror":
user_check = same_realm_zephyr_user
fullname_function = compute_mit_user_fullname
@@ -188,6 +187,7 @@ def send_message_backend(
user_profile: UserProfile,
message_type_name: str = REQ("type"),
req_to: Optional[str] = REQ("to", default=None),
req_sender: Optional[str] = REQ("sender", default=None, documentation_pending=True),
forged_str: Optional[str] = REQ("forged", default=None, documentation_pending=True),
topic_name: Optional[str] = REQ_topic(),
message_content: str = REQ("content"),
@@ -252,7 +252,7 @@ def send_message_backend(
# The most important security checks are in
# `create_mirrored_message_users` below, which checks the
# same-realm constraint.
if "sender" not in request.POST:
if req_sender is None:
raise JsonableError(_("Missing sender"))
if message_type_name != "private" and not can_forge_sender:
raise JsonableError(_("User not authorized for this query"))
@@ -268,7 +268,9 @@ def send_message_backend(
message_to = cast(Sequence[str], message_to)
try:
mirror_sender = create_mirrored_message_users(request, user_profile, message_to)
mirror_sender = create_mirrored_message_users(
client, user_profile, message_to, req_sender, message_type_name
)
except InvalidMirrorInput:
raise JsonableError(_("Invalid mirrored message"))
@@ -276,7 +278,7 @@ def send_message_backend(
raise JsonableError(_("Zephyr mirroring is not allowed in this organization"))
sender = mirror_sender
else:
if "sender" in request.POST:
if req_sender is not None:
raise JsonableError(_("Invalid mirrored message"))
sender = user_profile