Compare commits

...

84 Commits

Author SHA1 Message Date
Greg Price
d583fcec59 shared: Bump version to 0.0.9. 2022-03-11 17:27:08 -08:00
Austin Riba
4301148bee shared: Add first Flow types for typeahead module
[greg: simplified to just the function we need right now,
 leaving the rest for later]
2022-03-11 17:23:16 -08:00
Kartik Srivastava
eefaa9120f user_topic: Rename topic_mutes.py to user_topics.py. 2022-03-11 14:26:55 -08:00
Kartik Srivastava
ce38eda54d test_events: Fix 'normalize' assuming subscription data is present.
This avoids a crash in normalize for tests that don't include these in
fetch_event_types.
2022-03-11 14:26:14 -08:00
Aman Agrawal
82837304ec api: Send full message in GET /messages/{message_id} response.
Previously, this URL just returned the `raw_content` field. It seems
cleanest to just make it a single-message variant of GET /messages,
deprecating the only format.
2022-03-11 10:25:22 -08:00
Anders Kaseorg
b4675d978f icons: Clean up globe icon.
The previous icon had a slight asymmetry, some not-quite-straight
lines, and curves with an excessive number of nodes resulting from
some kind of vector → raster → vector workflow.  Rebuild it from
scratch.  This will be visually equivalent but render more
efficiently.

Signed-off-by: Anders Kaseorg <anders@zulip.com>
2022-03-11 10:10:12 -08:00
Alex Vandiver
6f5ae8d13d puppet: wal-g backups are required for replication.
Previously, it was possible to configure `wal-g` backups without
replication enabled; this resulted in only daily backups, not
streaming backups.  It was also possible to enable replication without
configuring the `wal-g` backups bucket; this simply failed to work.

Make `wal-g` backups always streaming, and warn loudly if replication
is enabled but `wal-g` is not configured.
2022-03-11 10:09:35 -08:00
Alex Vandiver
6496d43148 puppet: Only s3_backups_bucket is required for backups.
`s3_backups_key` / `s3_backups_secret_key` are optional, as the
permissions could come from the EC2 instance's role.
2022-03-11 10:09:35 -08:00
Alex Vandiver
19beed2709 puppet: Default s3_region to the current ec2 region. 2022-03-11 10:09:35 -08:00
Alex Vandiver
bfdc547b00 docs: Document s3_region setting. 2022-03-11 10:09:35 -08:00
Tim Abbott
20368a936c settings: Add web-public streams beta subdomain list.
This will make it convenient to add a handful of organizations to the
beta of this feature during its first few weeks to try to catch bugs,
before we open it to everyone in Zulip Cloud.
2022-03-10 18:37:01 -08:00
Sahil Batra
57f01e0727 actions: Use transaction.atomic for do_remove_realm_domain. 2022-03-10 17:48:02 -08:00
Sahil Batra
5999dcd316 actions: Use transaction.atomic for do_change_realm_domain. 2022-03-10 17:48:02 -08:00
Sahil Batra
9b9931df7f actions: Use transaction.atomic for do_add_realm_domain. 2022-03-10 17:48:02 -08:00
Sahil Batra
07352271b9 realm: Create RealmAuditLog entry for removing a realm domain.
Fixes a part of #21268.
2022-03-10 17:48:02 -08:00
Sahil Batra
ab5567e8c5 realm: Create RealmAuditLog entry while changing an allowed domain.
This commit also adds 'acting_user' argument for do_change_realm_domain
function.

Fixes a part of #21268.
2022-03-10 17:48:02 -08:00
Sahil Batra
5ef8da40a9 realm: Create RealmAuditLog entry while adding a new allowed domain.
This commit also adds 'acting_user' argument for do_add_realm_domain
function.

Fixes a part of #21268.
2022-03-10 17:48:02 -08:00
Sahil Batra
ee11a68f7a models: Fix return type of get_realm_domains.
The correct return type of get_realm_domains should
be List[Dict[str, Union[bool, str]]] instead of
List[Dict[str, str]] because allowed_subdomains is
a bool field not str.
2022-03-10 17:48:02 -08:00
Alya Abbott
a80e470c9e portico: Create use cases index page. 2022-03-10 16:34:34 -08:00
somesh202
68a1912f89 compose: Disable 'c' and 'x' hotkeys while compose is open.
The most plausible situations through which one would press these
hotkeys with the compose box still open are accidents (basically,
you're trying to type but the browser focus is unexpectedly not in the
compose textarea).

So we disable these keyboard shortcuts when the compose box is open,
regardless of where keyboard focus lies.

Fixes #21128.
2022-03-10 16:23:50 -08:00
Tim Abbott
ed4ff268a8 compose: Save drafts when starting compose might destroy content.
Starting composing a message to a new recipient will clear the compose
box. Previously, we were saving drafts before doing so only in the
compose_actions.respond_to_message code path (i.e. when starting a
reply). Logically, this behavior should apply regardless of why we're
initiating a new message, so it belongs in compose_actions.start.

Fixes #21128.
Fixes #21171.
2022-03-10 16:17:51 -08:00
jai2201
ea65da462e stream-settings: Use CSS nesting at media breakpoints.
Avoid writing repeated class name of `#subscription_overlay` and `subscription_settings`
for CSS at media breakpoints.
2022-03-10 16:00:21 -08:00
Tim Abbott
e374d7ef7d css: Deduplicate a subscriptions.css heading. 2022-03-10 15:59:56 -08:00
jai2201
f68961533a stream settings: Remove dead preview-stream CSS.
Remove the CSS written for class 'preview-stream', which stopped
existing in the application in 368b585980.
2022-03-10 15:54:46 -08:00
Tim Abbott
6b6a1d53f7 stream settings: Remove Bootstrap collapse class.
This class was leftover from a very old version of this design, and
had the side effect of settings `overflow: hidden` on the panel.

This, in turn, resulted in the focus outlines for clicking on
checkboxes looking broken.
2022-03-10 15:49:18 -08:00
Tim Abbott
89afe55076 stream settings: Fix inconsistent headings for subscribers panel.
Previously, these two headers were inconsistent with the rest of the
application, and with "Edit subscribers". We make them the same as
"Edit subscribers".
2022-03-10 15:27:52 -08:00
Tim Abbott
f9539617ee stream settings: Remove doubled 10px margin after heading. 2022-03-10 15:21:48 -08:00
NerdyLucifer
10e6fd04e7 stream_settings: Fix fonts & margins in create/edit stream form.
In "stream_types.hbs"
For "Who can access the stream?" and "Who can post to the stream?" replace
"h4" with "label" to make the for smaller and to remove boldness.
For "Message retention for stream" replace the "h4" with "label"
and add class="stream-title".

In "subscriptions.css":
Add "margin:25px auto" to "#announce-new-stream" to ensure equal
gaps above and below it.
Reduce margin and paddings for ".radio-input-parent".
For "select" set "width: fit-content" and
"height: fit-content" to ensure that the text in the
dropdown is clearly visible.

Fixes: #21322
2022-03-10 15:20:20 -08:00
NerdyLucifer
5f472b1607 stream_settings: Implement dropdown widget for "stream post policy".
In stream edit and stream create replace the existing checkbox
format for choosing "stream post policy" with dropdown widget.

In "stream_types.hbs" implement the dropdown menu and remove
the checkbox format for selecting "stream post policy".

In "stream_create.js" and "stream_edit.js" edit the code for
"stream_post_policy" to extract the "stream post policy" value
from the dropdown menu after submitting the form.
2022-03-10 15:11:35 -08:00
NerdyLucifer
1eb1aa70ed stream_settings: Improve stream_post_policy_values order, description.
In "stream_data.js/stream_post_policy_values", change, the object to match
the following order and description of these policies:

1. Everyone [Default]
2. Admins, moderators and full members
3. Admins and moderators
4. Admins only

This sorts from least to most restrictive.
2022-03-10 15:11:06 -08:00
Sayam Samal
681414caf1 user_profile_modal: Interchange posititons of "Joined" and "Role" fields.
This commit swaps the posititon of "Joined" and "Role" fields in the
User Profile Modal to make it consistent with settings/profile.
2022-03-10 15:10:24 -08:00
Sayam Samal
4227f74638 user_info_popover: Interchange positions of 'Role' and 'Local time'. 2022-03-10 15:10:24 -08:00
Sayam Samal
2f606ffbd9 settings: Fix text alignment issues in profile picture overlay.
Fixes #21342
2022-03-10 15:10:24 -08:00
Sayam Samal
9b378b0718 settings: Add responsiveness to textarea fields.
This commit adds responsiveness to textarea fields to improve the
responsive flow of the page.
2022-03-10 15:10:24 -08:00
Sayam Samal
cccb3b1b32 settings: Move user details to the right side panel in profile section.
This commit moves the "Role" and "Joined" attributes to the right
side panel of settings/profile to maintain continuity between the
mutable fields.
2022-03-10 15:10:24 -08:00
Sayam Samal
2bf63c1e49 settings: Remove profile picture header in the profile section.
This commit removes the profile picture header and adds an overlay to
handle disabled avatar changes in an organization.
2022-03-10 15:10:24 -08:00
Sayam Samal
d5821858dc settings: Fix hidden delete button in profile picture section.
This commit fixes the issue where the delete (x) button on the
top right corner of the profile picture section remains hidden
even when a hover action is performed on the profile photo.
2022-03-10 15:10:24 -08:00
Steve Howell
8e05a9fcf7 unread: Replace sender_id with other_user_id.
Note that we still send sender_id for legacy mobile
clients.
2022-03-10 13:33:21 -08:00
Steve Howell
822c232e37 message flags: Extract create_historical_user_messages.
A user can subscribe to a stream and sometimes (depending
on stream permissions) see messages from the stream
that were sent before they subscribed, and that user
won't have a UserMessage row for that message.

In order to do things like star a message, we need
to create UserMessage records on the fly.

In the past we wisely constrained this logic to the
specific use cases. But I think we can generalize
the logic now.  For example, we are now building a
feature to mark messages as unread, and it motivates
the same need to auto-create UserMessage rows.

So now we handle this in a more generalized fashion.
2022-03-10 13:21:02 -08:00
Lauryn Menard
05a548f5a3 api_docs: Refactor of MessagesBase schema.
Refactors and cleans up the shared `MessagesBase` schema in
the OpenAPI so that it accurately reflects the general base
for message objects for endpoints that use it as a reference.

A follow-up to adding `edit_history` as a property of message
objects. And a prepartory commit for `GET /messages/{msg_id}`
to return not only the raw content of the message but also
the message object.
2022-03-10 13:10:14 -08:00
Tim Abbott
a8b0762699 help: Document web public streams are not indexed by Google. 2022-03-10 12:42:03 -08:00
Alya Abbott
757cdeefb1 help center: Change web-public streams status to beta.
Also adds references to web-public streams where appropriate.
2022-03-10 12:31:01 -08:00
Alex Vandiver
2fc7054a09 droplet: Fix printed instructions to have the right username/hostname. 2022-03-10 12:25:05 -08:00
Alex Vandiver
72b10937fc droplet: Factor out common droplet_domain_name. 2022-03-10 12:25:05 -08:00
Alex Vandiver
5086241361 droplet: Set a secure erlang cookie at startup. 2022-03-10 12:25:05 -08:00
Alex Vandiver
aa9039d83e droplet: Switch to a new Debian 10 template snapshot. 2022-03-10 12:25:05 -08:00
Alex Vandiver
d8c77eafb4 droplet: Always create with the "dev" tag.
This allows the firewall to be enforced on new hosts.
2022-03-10 12:25:05 -08:00
Alex Vandiver
25d753889b droplet: Allow overriding the subdomain. 2022-03-10 12:25:05 -08:00
Tim Abbott
84bdf86246 message_edit: Don't display resolved topics as MOVED.
We loop through edit history entries and see if any of them
are more interesting than a (un)resolve topic edit, extending
the existing loop we had.

We also update the associated node tests.

Fixes #19919.

Co-authored by: Lauryn Menard <lauryn@zulip.com>
2022-03-10 12:00:53 -08:00
Lauryn Menard
93f3021dfb edit_history: Update simulated edit history entries in frontend.
Updates the simulated edit history entries that are saved when
`update_messages` events are received for the modern data
structure on the server for `message.edit_history` entries.

Also cleans up a misnamed field in said entries, `edited_by`.
2022-03-10 11:50:48 -08:00
Steve Howell
a90d9ef536 unread: Remove unused client parameter. 2022-03-10 10:04:51 -05:00
Alex Vandiver
c2f2863d37 push_notifications: Remove access control on "remove" notifications.
When removing notifications, we skip the access control on if the user
still can read them -- they should not have a notification of them,
both because they currently cannot read the message, as well as
because they have already done so.
2022-03-09 16:33:51 -08:00
Alex Vandiver
19dfd8e6a7 push_notifications: Ensure notifications are on for the remove codepath.
This causes it to mirror the handle_push_notification codepath.
2022-03-09 16:33:51 -08:00
Lauryn Menard
c9c980d7b0 url_preview: Fix 'Edited' notification for url preview events.
Uses the `rendering_only` field in the `update_message` event
to filter the addition of `last_edit_timestamp` to the message,
which is what triggers the addition of an `Edited` notification
when a message is rerendered in the web app.

Also, removes the deletion of `msg.last_edit_timestr` since this is
regenerated every time the message is rendered, and so it did nothing
beyond confusing the code.
2022-03-09 15:49:50 -08:00
Sahil Batra
cbac466658 compose: Show topic required error if topic is "(no topic)".
We already show the error if topic input is empty and it is
not allowed to send messages without topic in the organization,
and this commit also shows error when topic is "(no topic)".

The topic is set to "(no topic)" when someone sends a message
with empty topic input box and when it is allowed to send message
without topics in the organization.

This is not ideal behavior as we may want to treat "(no topic)"
differently from empty topic, but we can fix this in future and
this commit can be a short term fix.

Fixes #21344.
2022-03-09 15:29:13 -08:00
Tim Abbott
d693a6717c i18n: Remove quote syntax from stream description notification.
Translators found it confusing, since it's not at all obvious that the
word "quote" should not be translated.

I'm not altogether happy with the code formatting for this.

While we're changing this, also standardize on the "```` quote" style
of quote blocks to ensure code/quote blocks in stream descriptions are
unlikely to conflict with this syntax.
2022-03-09 15:22:57 -08:00
Alex Vandiver
7c4293a7d3 restart-server: Check if service is running before restart, vs start.
In some instances (e.g. during upgrades) we run `restart-server` and
not `start-server`, even though we expect the server to most likely
already be stopped.  `supervisorctl restart servicename` if the
service is stopped produces the perhaps-alarming message:

```
restart-server: Restarting servicename
servicename: ERROR (not running)
servicename: started
```

This may cause operators to worry that something is broken, when it is
not.

Check if the service is already running, and switch from "restart" to
"start" in cases where it is not.

The race condition here is safe -- if the service transitions from
stopped to started between the check and the `start` call, it will
merely output:
```
servicename: ERROR (already started)
```
...and continue, as that has exit status 0.

If the service transitions from started to stopped between the check
and the `restart` call, we are merely back in the current case, where
it outputs:
```
servicename: ERROR (not running)
servicename: started
```

In none of these cases does a call to "restart" fail to result in the
service being stopped and then started.
2022-03-09 14:42:15 -08:00
Sahil Batra
cb15e0265d settings_users: Do not allow sorting by email if emails are hidden. 2022-03-09 13:56:57 -08:00
prashantpaidi
05af6fd8b4 recent topics: Disable browser autocomplete filter widget.
The browser autocomplete is annoying/unhelpful here.

Fixes #21349.
2022-03-09 13:56:14 -08:00
Anders Kaseorg
cbca80c846 styles: Fix some invalid CSS.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2022-03-09 09:37:31 -08:00
Greg Price
a71fad9d6b shared: Bump version to 0.0.8. 2022-03-08 20:35:10 -08:00
Alex Vandiver
3264361f63 push_notifications: Set a timeout on FCM requests.
In steady-state, requests to FCM take about a second; however, in
cases where the remote FCM server is unstable, the request has been
observed to block for more than a minute.

As noted in the previous commit, pushes must complete within 30s;
fail fast, and let the retries and exponential backoff handle errors.

The worst-case total time taken with timeouts and errors for an FCM
notification is 19.5s.  Unfortunately, `aioapns` does not appear to
have any timeouts, and thus this commit cannot guarantee a total of
fewer than 30s.
2022-03-08 12:52:58 -08:00
Alex Vandiver
f531f3a27f push_notifications: Drop FCM retries to 2, not 10.
This reverts bc15085098 (which provided
not justification for its change) and moves further, down to 2 retries
from the default of 5.

10 retries, with exponential backoff, is equivalent to sleeping 2^11
seconds, or just about 34 minutes (though the code uses a jitter which
may make this up to 51 minutes).  This is an unreasonable amount of
time to spend in this codepath -- as only one worker is used, and it
is single-threaded, this could effectively block all missed message
notifications for half an hour or longer.

This is also necessary because messages sent through the push bouncer
are sent synchronously; the sending server uses a 30-second timeout,
set in PushBouncerSession.  Having retries which linger longer than
this can cause duplicate messages; the sending server will time out
and re-queue the message in RabbitMQ, while the push bouncer's request
will continue, and may succeed.

Limit to 2 retries (APNS currently uses 3), and results expected max
of 4 seconds of sleep, potentially up to 6.  If this fails, there
exists another retry loop above it, at the RabbitMQ layer (either
locally, or via the remote server's queue), which will result in up to
3 additional retries -- all told, the request will me made to FCM up
to 12 times.
2022-03-08 12:52:58 -08:00
byshen-dev
73bc5480f3 models: Add unique constraint on RealmUserDefault.realm.
This model is by designed intended to exist on a 1:1 relationship with
Realms, and we attempt to ensure that with application code, but we
should have a unique constraint too, since a database with duplicate
such entries would be corrupted.

We do this via the standard Django OneToOneField.
2022-03-07 21:43:07 -08:00
Greg Price
760cfcc603 resolved_topic: Implement resolve, unresolve, and display.
We have two different frontend implementations of computing the
un-resolved form of a topic name, and they have a subtle -- but
intentional -- difference in behavior.

Factor them both out into the resolve_topic module, along with
their inverse, and with comments and tests.
2022-03-07 21:35:00 -08:00
Greg Price
7852d8e015 topic_list_data: Move "if resolved" conditionals to a single place.
These two conditionals are each relying on the other to trigger
on the same condition, and to do complementary things.  Move them
together to a single place so that that relationship is easy to see,
and to refactor.
2022-03-07 21:35:00 -08:00
Greg Price
7bf0fd3fa3 resolved_topic: Add and use predicate is_resolved.
We leave in place a couple of sites where the `startsWith` is
entangled with other string manipulation.  We'll handle those next.
2022-03-07 21:35:00 -08:00
Greg Price
624cdb0a14 resolved_topic: Start module, in shared package.
For the moment, just move the RESOLVED_TOPIC_PREFIX constant.
More next.
2022-03-07 21:35:00 -08:00
Greg Price
dd1091c59a shared: Add a gitignore, for node_modules. 2022-03-07 21:35:00 -08:00
Steve Howell
c43d48b22f stream create: Overhaul create-stream add-subscribers UI.
The most notable change here is that when you are adding
subscribers to a stream as part of creating the stream,
you can now use the same essential pill-based UI for
adding users as we do when you edit subscribers for an
existing stream.

We don't try to exactly mimic the edit-stream UI or
implementation, since when you are adding subscribers
during create-stream, we are just updating a list in
memory, whereas in the edit-stream UI, we immediately
send info to the server.

Fixes #20499
2022-03-07 16:58:58 -08:00
Anders Kaseorg
43ee1f7b93 tests: Avoid use of Python internal __unittest_skip__ flag.
It was there to work around https://bugs.python.org/issue17519.  This
workaround with del seems like a partial improvement.

Signed-off-by: Anders Kaseorg <anders@zulip.com>
2022-03-07 16:26:37 -08:00
Steve Howell
df47e4312b unread: Refactor aggregated message info.
We want TypedDicts that have actual teeth.

In order to make type checks meaningful, we want
to avoid Any, object, or crazy Union types when
we aggregate each type of message, so we replaced
a generic function with three concrete functions.
2022-03-07 15:34:56 -08:00
Mateusz Mandera
dec092751c tests: Mark test_migrations tests to be automatically skipped.
This fixes an issue where these tests will fail when adding new
migrations on top of them.
2022-03-07 15:33:29 -08:00
Dinesh
9f8d60cd5a actions: Mention that stream can also be changed with do_update_message(). 2022-03-07 12:44:36 -08:00
Dinesh
57b8e43dbb dialog_widget: Mention post_render in optional parameters for launch(). 2022-03-07 12:44:36 -08:00
Tim Abbott
5d136887b5 policies: Fix effective date on most recent DPA update.
The effective date on the DPA should have been February 7, because we
didn't actually update the zulip.com website until that day.

(This commit was added to the internal zulip.com branch during
deployment of the last DPA update, so users always saw the correct
information).
2022-03-07 12:01:25 -08:00
Tim Abbott
92cc771392 test-all: Pass --skip-external-links to documentation tests.
This is what we already do in CI.  The external links often cause
these tests to fail, and it's not helpful for test-all to not match
CI.
2022-03-07 11:54:01 -08:00
NerdyLucifer
4b9770e270 stream_settings: Show stream privacy & description in stream events.
Provide stream privacy and description in stream notification events
when stream is created.
In function "send_messages_for_new_subscribers" for when stream is
created, put policy name and description of the stream.

Fixes #21004
2022-03-07 11:53:49 -08:00
Anders Kaseorg
646e466341 install: Desupport Ubuntu 22.04 for now.
Ubuntu 22.04 pushed a post-feature-freeze update to Python 3.10,
breaking virtual environments in a Debian patch
(https://bugs.launchpad.net/ubuntu/+source/python3.10/+bug/1962791).
Also, our antique version of Tornado doesn’t work in 3.10, and we’ll
need to do some work to upgrade that.

Signed-off-by: Anders Kaseorg <anders@zulip.com>
2022-03-07 11:46:07 -08:00
Tim Abbott
5ee5a7e635 specators: Fix persistent recent topics loading indicator.
This unfortunately requires somewhat ugly duplicated code, but I think
it's the best option for now.

I expect we will somewhat soon work on the transition to no longer
have two duplicate fetches here, and doing so will let us remove this
secondary code path.

Fixes #21304.
2022-03-04 15:51:09 -08:00
Steve Howell
2644fa9645 settings: Make list header sections consistent.
I made the header sections above all our settings
panel lists more consistent.

Before this change:

    * some lists had titles, others didn't
    * the placement of the filter box was random
    * alerts strangely went between the filter box
      and the list
    * filter boxes were too large
    * CSS was haphazard
    * forms were squished against tables

Now all the settings with list have consistent
HTML, CSS, and look-and-feel in the area directly above
their list of items.

With the exception of Custom Profile Fields, all the
lists with headers above them happen to be based on
ListWidget, but the header styling is not coupled
to ListWidget, because we want consistent headers
even if Custom Profile Fields has a non-ListWidget
list (due to its drag&drop features).
2022-03-04 14:37:28 -08:00
Alya Abbott
ba1f804518 docs: Update README and installation guide.
This is a general cleanup that also aims to link to the new
self-hosting page to provide added context.
2022-03-04 13:59:17 -08:00
Tim Abbott
0c015c7bf3 css: Deduplicate CSS for .always_visible_topic_edit. 2022-03-04 13:25:30 -08:00
Junchen Liu
e3237ae7e1 css: Fix hover color for "Edit topic" icon in recipient bars.
Previously, this had different hover behavior from the adjacent
elements, which seems like a bug.

The CSS for this component is shared with Recent Topics; we migrate
the styling for on_hover_topic_read for consistency.

Fixes #21273.
2022-03-04 13:25:30 -08:00
137 changed files with 2056 additions and 1297 deletions

View File

@@ -12,6 +12,8 @@ world, with 74+ people who have each contributed 100+ commits. With
over 1000 contributors merging over 500 commits a month, Zulip is the
largest and fastest growing open source team chat project.
Come find us on the [development community chat](https://zulip.com/development-community/)!
[![GitHub Actions build status](https://github.com/zulip/zulip/actions/workflows/zulip-ci.yml/badge.svg)](https://github.com/zulip/zulip/actions/workflows/zulip-ci.yml?query=branch%3Amain)
[![coverage status](https://img.shields.io/codecov/c/github/zulip/zulip/main.svg)](https://codecov.io/gh/zulip/zulip)
[![Mypy coverage](https://img.shields.io/badge/mypy-100%25-green.svg)][mypy-coverage]
@@ -30,58 +32,50 @@ largest and fastest growing open source team chat project.
## Getting started
Click on the appropriate link below. If nothing seems to apply,
join us on the
[Zulip community server](https://zulip.com/development-community/)
and tell us what's up!
- **Contributing code**. Check out our [guide for new
contributors](https://zulip.readthedocs.io/en/latest/overview/contributing.html)
to get started. We have invested into making Zulips code uniquely readable,
well tested, and easy to modify. Beyond that, we have written an extraordinary
150K words of documentation on how to contribute to Zulip.
You might be interested in:
- **Contributing non-code**. [Report an
issue](https://zulip.readthedocs.io/en/latest/overview/contributing.html#reporting-issues),
[translate](https://zulip.readthedocs.io/en/latest/translating/translating.html)
Zulip into your language, or [give us
feedback](https://zulip.readthedocs.io/en/latest/overview/contributing.html#user-feedback).
We'd love to hear from you, whether you've been using Zulip for years, or are just
trying it out for the first time.
- **Contributing code**. Check out our
[guide for new contributors](https://zulip.readthedocs.io/en/latest/overview/contributing.html)
to get started. Zulip prides itself on maintaining a clean and
well-tested codebase, and a stock of hundreds of
[beginner-friendly issues][beginner-friendly].
- **Checking Zulip out**. The best way to see Zulip in action is to drop by the
[Zulip community server](https://zulip.com/development-community/). We also
recommend reading about Zulip's [unique
approach](https://zulip.com/why-zulip/) to organizing conversations.
- **Contributing non-code**.
[Report an issue](https://zulip.readthedocs.io/en/latest/overview/contributing.html#reporting-issues),
[translate](https://zulip.readthedocs.io/en/latest/translating/translating.html) Zulip
into your language,
[write](https://zulip.readthedocs.io/en/latest/overview/contributing.html#zulip-outreach)
for the Zulip blog, or
[give us feedback](https://zulip.readthedocs.io/en/latest/overview/contributing.html#user-feedback). We
would love to hear from you, even if you're just trying the product out.
- **Running a Zulip server**. Self host Zulip directly on Ubuntu or Debian
Linux, in [Docker](https://github.com/zulip/docker-zulip), or with prebuilt
images for [Digital Ocean](https://marketplace.digitalocean.com/apps/zulip) and
[Render](https://render.com/docs/deploy-zulip).
Learn more about [self-hosting Zulip](https://zulip.com/self-hosting/).
- **Supporting Zulip**. Advocate for your organization to use Zulip, become a [sponsor](https://github.com/sponsors/zulip), write a
review in the mobile app stores, or
[upvote Zulip](https://zulip.readthedocs.io/en/latest/overview/contributing.html#zulip-outreach) on
product comparison sites.
- **Checking Zulip out**. The best way to see Zulip in action is to drop by
the
[Zulip community server](https://zulip.com/development-community/). We
also recommend reading Zulip for
[open source](https://zulip.com/for/open-source/), Zulip for
[companies](https://zulip.com/for/companies/), or Zulip for
[communities](https://zulip.com/for/working-groups-and-communities/).
- **Running a Zulip server**. Use a preconfigured [DigitalOcean droplet](https://marketplace.digitalocean.com/apps/zulip),
[install Zulip](https://zulip.readthedocs.io/en/stable/production/install.html)
directly, or use Zulip's
experimental [Docker image](https://zulip.readthedocs.io/en/latest/production/deployment.html#zulip-in-docker).
Commercial support is available; see <https://zulip.com/plans> for details.
- **Using Zulip without setting up a server**. <https://zulip.com>
offers free and commercial hosting, including providing our paid
plan for free to fellow open source projects.
- **Using Zulip without setting up a server**. Learn about [Zulip
Cloud](https://zulip.com/plans/) hosting options. Zulip sponsors free [Zulip
Cloud Standard](https://zulip.com/plans/) for hundreds of worthy
organizations, including [fellow open-source
projects](https://zulip.com/for/open-source/).
- **Participating in [outreach
programs](https://zulip.readthedocs.io/en/latest/overview/contributing.html#outreach-programs)**
like Google Summer of Code.
like [Google Summer of Code](https://developers.google.com/open-source/gsoc/)
and [Outreachy](https://www.outreachy.org/).
- **Supporting Zulip**. Advocate for your organization to use Zulip, become a
[sponsor](https://github.com/sponsors/zulip), write a review in the mobile app
stores, or [help others find
Zulip](https://zulip.readthedocs.io/en/latest/overview/contributing.html#help-others-find-zulip).
You may also be interested in reading our [blog](https://blog.zulip.org/), and
following us on [Twitter](https://twitter.com/zulip) and
[LinkedIn](https://www.linkedin.com/company/zulip-project/).
You may also be interested in reading our [blog](https://blog.zulip.org/) or
following us on [Twitter](https://twitter.com/zulip).
Zulip is distributed under the
[Apache 2.0](https://github.com/zulip/zulip/blob/main/LICENSE) license.
[beginner-friendly]: https://github.com/zulip/zulip/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22

View File

@@ -1431,8 +1431,7 @@ class TestLoggingCountStats(AnalyticsTestCase):
self.subscribe(user2, stream.name)
self.send_personal_message(user1, user2)
client = get_client("website")
do_mark_all_as_read(user2, client)
do_mark_all_as_read(user2)
self.assertEqual(
1,
UserCount.objects.filter(property=read_count_property).aggregate(Sum("value"))[
@@ -1463,7 +1462,7 @@ class TestLoggingCountStats(AnalyticsTestCase):
)
message = self.send_stream_message(user2, stream.name)
do_update_message_flags(user1, client, "add", "read", [message])
do_update_message_flags(user1, "add", "read", [message])
self.assertEqual(
4,
UserCount.objects.filter(property=read_count_property).aggregate(Sum("value"))[

View File

@@ -12,7 +12,7 @@ Contents:
If you'd like to install a Zulip development environment on a computer
that's running one of:
- Ubuntu 20.04 Focal, 22.04 Jammy (beta)
- Ubuntu 20.04 Focal
- Debian 10 Buster, 11 Bullseye
- CentOS 7 (beta)
- Fedora 33 and 34 (beta)

View File

@@ -49,7 +49,7 @@ a proxy to access the internet.)
- **All**: 2GB available RAM, Active broadband internet connection, [GitHub account][set-up-git].
- **macOS**: macOS (10.11 El Capitan or newer recommended)
- **Ubuntu LTS**: 20.04 or 22.04
- **Ubuntu LTS**: 20.04
- or **Debian**: 10 "buster" or 11 "bullseye"
- **Windows**: Windows 64-bit (Win 10 recommended), hardware
virtualization enabled (VT-x or AMD-V), administrator access.

View File

@@ -508,23 +508,15 @@ things you need to be careful about when configuring it:
Zulip's configuration allows for [warm standby database
replicas][warm-standby] as a disaster recovery solution; see the
linked PostgreSQL documentation for details on this type of
deployment. Zulip's configuration leverages `wal-g`, our [database
backup solution][wal-g], and thus requires that it be configured for
the primary and all secondary warm standby replicas.
deployment. Zulip's configuration builds on top of `wal-g`, our
[database backup solution][wal-g], and thus requires that it be
configured for the primary and all secondary warm standby replicas.
The primary should have log-shipping enabled, with:
Warm spare replicas should also have `wal-g` backups configured, and
their primary replica and replication username set:
```ini
[postgresql]
replication = yes
```
Warm spare replicas should have log-shipping enabled, and their
primary replica and replication username configured:
```ini
[postgresql]
replication = yes
replication_user = replicator
replication_primary = hostname-of-primary.example.com
```
@@ -688,14 +680,6 @@ setting](https://www.postgresql.org/docs/current/runtime-config-connection.html#
Override PostgreSQL's [`random_page_cost`
setting](https://www.postgresql.org/docs/current/runtime-config-query.html#GUC-RANDOM-PAGE-COST)
#### `replication`
Set to true to enable replication to enable [log shipping replication
between PostgreSQL servers](#postgresql-warm-standby). This should be
enabled on the primary, as well as any replicas, and further requires
configuration of
[wal-g](export-and-import.md#backup-details).
#### `replication_primary`
On the [warm standby replicas](#postgresql-warm-standby), set to the

View File

@@ -169,9 +169,10 @@ data includes:
PostgreSQL server to add:
```ini
s3_backups_key = # aws public key
s3_backups_secret_key = # aws secret key
s3_backups_bucket = # name of S3 backup
s3_region = # region to write to S3; defaults to EC2 host's region
s3_backups_key = # aws public key; optional, if access not through role
s3_backups_secret_key = # aws secret key; optional, if access not through role
s3_backups_bucket = # name of S3 backup bucket
```
After adding the secrets, run

View File

@@ -6,7 +6,7 @@ maxdepth: 3
---
requirements
Installing a production server <install>
install
troubleshooting
management-commands
settings

View File

@@ -1,16 +1,27 @@
# Production installation
# Install a Zulip server
You'll need an Ubuntu or Debian system that satisfies
## Before you begin
To install a Zulip server, you'll need an Ubuntu or Debian system that satisfies
[the installation requirements](requirements.md). Alternatively,
you can use a preconfigured
[DigitalOcean droplet](https://marketplace.digitalocean.com/apps/zulip?refcode=3ee45da8ee26), or
Zulip's
[experimental Docker image](deployment.md#zulip-in-docker).
Note that if you're developing for Zulip, you should install Zulip's
[development environment](../development/overview.md) instead. If
you're just looking to play around with Zulip and see what it looks like,
you can create a test organization at <https://zulip.com/new>.
### Should I follow this installation guide?
- If you are just looking to play around with Zulip and see what it looks like,
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
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.
- If you're developing for Zulip, you should follow the instructions
to install Zulip's [development environment](../development/overview.md).
## Step 1: Download the latest release

View File

@@ -5,7 +5,6 @@ To run a Zulip server, you will need:
- A dedicated machine or VM
- A supported OS:
- Ubuntu 20.04 Focal
- Ubuntu 22.04 Jammy
- Debian 11 Bullseye
- Debian 10 Buster
- At least 2GB RAM, and 10GB disk space
@@ -34,7 +33,7 @@ on issues you'll encounter](install-existing-server.md).
#### Operating system
Ubuntu 20.04 Focal, Ubuntu 22.04 Jammy, Debian 11 Bullseye, and Debian 10
Ubuntu 20.04 Focal, Debian 11 Bullseye, and Debian 10
Buster are supported for running Zulip in production. You can also
run Zulip on other platforms that support Docker using
[docker-zulip][docker-zulip-homepage].

View File

@@ -16,9 +16,9 @@ const ui_util = mock_esm("../../static/js/ui_util");
const compose_pm_pill = zrequire("compose_pm_pill");
const compose_validate = zrequire("compose_validate");
const message_edit = zrequire("message_edit");
const peer_data = zrequire("peer_data");
const people = zrequire("people");
const resolved_topic = zrequire("../shared/js/resolved_topic");
const settings_config = zrequire("settings_config");
const settings_data = mock_esm("../../static/js/settings_data");
const stream_data = zrequire("stream_data");
@@ -247,6 +247,13 @@ test_ui("validate", ({override, mock_template}) => {
$("#compose-error-msg").html(),
$t_html({defaultMessage: "Topics are required in this organization"}),
);
compose_state.topic("(no topic)");
assert.ok(!compose_validate.validate());
assert.equal(
$("#compose-error-msg").html(),
$t_html({defaultMessage: "Topics are required in this organization"}),
);
});
test_ui("get_invalid_recipient_emails", ({override_rewire}) => {
@@ -770,7 +777,7 @@ test_ui("test warn_if_topic_resolved", ({override, mock_template}) => {
mock_template("compose_resolved_topic.hbs", false, (context) => {
assert.ok(context.can_move_topic);
assert.ok(context.topic_name.startsWith(message_edit.RESOLVED_TOPIC_PREFIX));
assert.ok(resolved_topic.is_resolved(context.topic_name));
return "fake-compose_resolved_topic";
});
@@ -786,7 +793,7 @@ test_ui("test warn_if_topic_resolved", ({override, mock_template}) => {
compose_state.set_message_type("stream");
compose_state.stream_name("Do not exist");
compose_state.topic(message_edit.RESOLVED_TOPIC_PREFIX + "hello");
compose_state.topic(resolved_topic.resolve_name("hello"));
// Do not show a warning if stream name does not exist
compose_validate.warn_if_topic_resolved();

View File

@@ -7,9 +7,9 @@ const {run_test} = require("../zjsunit/test");
const $ = require("../zjsunit/zjquery");
const {page_params} = require("../zjsunit/zpage_params");
const message_edit = mock_esm("../../static/js/message_edit");
const message_store = mock_esm("../../static/js/message_store");
const resolved_topic = zrequire("../shared/js/resolved_topic");
const stream_data = zrequire("stream_data");
const people = zrequire("people");
const {Filter} = zrequire("../js/filter");
@@ -685,7 +685,7 @@ test("predicate_basics", () => {
assert.ok(predicate({}));
predicate = get_predicate([["is", "resolved"]]);
const resolved_topic_name = message_edit.RESOLVED_TOPIC_PREFIX + "foo";
const resolved_topic_name = resolved_topic.resolve_name("foo");
assert.ok(predicate({type: "stream", topic: resolved_topic_name}));
assert.ok(!predicate({topic: resolved_topic_name}));
assert.ok(!predicate({type: "stream", topic: "foo"}));

View File

@@ -73,10 +73,16 @@ test("msg_moved_var", () => {
message_context = {
...message_context,
};
message_context.msg = {
last_edit_timestamp: (next_timestamp += 1),
...message,
};
if ("edit_history" in message) {
message_context.msg = {
last_edit_timestamp: (next_timestamp += 1),
...message,
};
} else {
message_context.msg = {
...message,
};
}
return message_context;
}
@@ -96,50 +102,80 @@ test("msg_moved_var", () => {
function assert_moved_false(message_container) {
assert.equal(message_container.moved, false);
}
function assert_moved_undefined(message_container) {
assert.equal(message_container.moved, undefined);
}
(function test_msg_moved_var() {
const messages = [
// no edits: Not moved.
build_message_context(),
// stream changed: Move
// no edit history: NO LABEL
build_message_context({}),
// stream changed: MOVED
build_message_context({
edit_history: [{prev_stream: "test_stream", timestamp: 1000, user_id: 1}],
edit_history: [{prev_stream: 1, timestamp: 1000, user_id: 1}],
}),
// topic changed: Move
// topic changed (not resolved/unresolved): MOVED
build_message_context({
edit_history: [{prev_topic: "test_topic", timestamp: 1000, user_id: 1}],
edit_history: [
{prev_topic: "test_topic", topic: "new_topic", timestamp: 1000, user_id: 1},
],
}),
// content edited: Edit
// content edited: EDITED
build_message_context({
edit_history: [{prev_content: "test_content", timestamp: 1000, user_id: 1}],
}),
// stream and topic edited: Move
// stream and topic edited: MOVED
build_message_context({
edit_history: [
{prev_stream: "test_stream", timestamp: 1000, user_id: 1},
{prev_topic: "test_topic", timestamp: 1000, user_id: 1},
{
prev_stream: 1,
prev_topic: "test_topic",
topic: "new_topic",
timestamp: 1000,
user_id: 1,
},
],
}),
// topic and content changed: Edit
// topic and content changed: EDITED
build_message_context({
edit_history: [
{prev_topic: "test_topic", timestamp: 1000, user_id: 1},
{prev_content: "test_content", timestamp: 1001, user_id: 1},
{
prev_topic: "test_topic",
topic: "new_topic",
prev_content: "test_content",
timestamp: 1000,
user_id: 1,
},
],
}),
// stream and content changed: Edit
// only topic resolved: NO LABEL
build_message_context({
edit_history: [
{prev_content: "test_content", timestamp: 1000, user_id: 1},
{prev_stream: "test_stream", timestamp: 1001, user_id: 1},
{prev_topic: "test_topic", topic: "✔ test_topic", timestamp: 1000, user_id: 1},
],
}),
// topic, stream, and content changed: Edit
// only topic unresolved: NO LABEL
build_message_context({
edit_history: [
{prev_topic: "test_topic", timestamp: 1000, user_id: 1},
{prev_stream: "test_stream", timestamp: 1001, user_id: 1},
{prev_topic: "✔ test_topic", topic: "test_topic", timestamp: 1000, user_id: 1},
],
}),
// multiple edit history logs, with at least one content edit: EDITED
build_message_context({
edit_history: [
{prev_stream: 1, timestamp: 1000, user_id: 1},
{prev_topic: "old_topic", topic: "test_topic", timestamp: 1001, user_id: 1},
{prev_content: "test_content", timestamp: 1002, user_id: 1},
{prev_topic: "test_topic", topic: "✔ test_topic", timestamp: 1003, user_id: 1},
],
}),
// multiple edit history logs with no content edit: MOVED
build_message_context({
edit_history: [
{prev_stream: 1, timestamp: 1000, user_id: 1},
{prev_topic: "old_topic", topic: "test_topic", timestamp: 1001, user_id: 1},
{prev_topic: "test_topic", topic: "✔ test_topic", timestamp: 1002, user_id: 1},
{prev_topic: "✔ test_topic", topic: "test_topic", timestamp: 1003, user_id: 1},
],
}),
];
@@ -154,8 +190,8 @@ test("msg_moved_var", () => {
const result = list._message_groups[0].message_containers;
// no edits: false
assert_moved_false(result[0]);
// no edit history: undefined
assert_moved_undefined(result[0]);
// stream changed: true
assert_moved_true(result[1]);
// topic changed: true
@@ -166,10 +202,14 @@ test("msg_moved_var", () => {
assert_moved_true(result[4]);
// topic and content changed: false
assert_moved_false(result[5]);
// stream and content changed: false
assert_moved_false(result[6]);
// topic, stream, and content changed: false
assert_moved_false(result[7]);
// only topic resolved: undefined
assert_moved_undefined(result[6]);
// only topic unresolved: undefined
assert_moved_undefined(result[7]);
// multiple edits with content edit: false
assert_moved_false(result[8]);
// multiple edits without content edit: true
assert_moved_true(result[9]);
})();
});
@@ -189,6 +229,7 @@ test("msg_edited_vars", () => {
message_context.msg = {
is_me_message: false,
last_edit_timestamp: (next_timestamp += 1),
edit_history: [{prev_content: "test_content", timestamp: 1000, user_id: 1}],
...message,
};
return message_context;

View File

@@ -6,12 +6,12 @@ const {mock_esm, zrequire} = require("../zjsunit/namespace");
const {run_test} = require("../zjsunit/test");
const all_messages_data = mock_esm("../../static/js/all_messages_data");
const message_edit = mock_esm("../../static/js/message_edit");
const {Filter} = zrequire("../js/filter");
const {MessageListData} = zrequire("../js/message_list_data");
const narrow_state = zrequire("narrow_state");
const narrow = zrequire("narrow");
const resolved_topic = zrequire("../shared/js/resolved_topic");
function test_with(fixture) {
const filter = new Filter(fixture.filter_terms);
@@ -302,7 +302,7 @@ run_test("is:alerted with no unreads and one match", () => {
});
run_test("is:resolved with one unread", () => {
const resolved_topic_name = message_edit.RESOLVED_TOPIC_PREFIX + "foo";
const resolved_topic_name = resolved_topic.resolve_name("foo");
const fixture = {
filter_terms: [{operator: "is", operand: "resolved"}],
unread_info: {
@@ -327,7 +327,7 @@ run_test("is:resolved with one unread", () => {
});
run_test("is:resolved with no unreads", () => {
const resolved_topic_name = message_edit.RESOLVED_TOPIC_PREFIX + "foo";
const resolved_topic_name = resolved_topic.resolve_name("foo");
const fixture = {
filter_terms: [{operator: "is", operand: "resolved"}],
unread_info: {

View File

@@ -163,13 +163,6 @@ const bob = {
full_name: "Bob van Roberts",
};
const alice2 = {
email: "alice2@example.com",
delivery_email: "alice2-delivery@example.com",
user_id: 204,
full_name: "Alice",
};
const charles = {
email: "charles@example.com",
user_id: 301,
@@ -582,77 +575,6 @@ test_people("set_custom_profile_field_data", () => {
assert.equal(person.profile_data[field.id].rendered_value, "<p>Field value</p>");
});
test_people("get_people_for_stream_create", () => {
people.add_active_user(alice1);
people.add_active_user(bob);
people.add_active_user(alice2);
assert.equal(people.get_active_human_count(), 4);
page_params.is_admin = true;
page_params.realm_email_address_visibility = admins_only;
let others = people.get_people_for_stream_create();
let expected = [
{
email: "alice1-delivery@example.com",
user_id: alice1.user_id,
full_name: "Alice",
checked: false,
disabled: false,
show_email: true,
},
{
email: "alice2-delivery@example.com",
user_id: alice2.user_id,
full_name: "Alice",
checked: false,
disabled: false,
show_email: true,
},
{
email: "bob-delivery@example.com",
user_id: bob.user_id,
full_name: "Bob van Roberts",
checked: false,
disabled: false,
show_email: true,
},
];
assert.deepEqual(others, expected);
page_params.is_admin = false;
alice1.delivery_email = undefined;
alice2.delivery_email = undefined;
bob.delivery_email = undefined;
others = people.get_people_for_stream_create();
expected = [
{
email: "alice1@example.com",
user_id: alice1.user_id,
full_name: "Alice",
checked: false,
disabled: false,
show_email: false,
},
{
email: "alice2@example.com",
user_id: alice2.user_id,
full_name: "Alice",
checked: false,
disabled: false,
show_email: false,
},
{
email: "bob@example.com",
user_id: bob.user_id,
full_name: "Bob van Roberts",
checked: false,
disabled: false,
show_email: false,
},
];
assert.deepEqual(others, expected);
});
test_people("recipient_counts", () => {
const user_id = 99;
assert.equal(people.get_recipient_count({user_id}), 0);
@@ -672,7 +594,7 @@ test_people("filtered_users", () => {
people.add_active_user(plain_noah);
const search_term = "a";
const users = people.get_people_for_stream_create();
const users = people.get_realm_users();
let filtered_people = people.filter_people_by_search_terms(users, [search_term]);
assert.equal(filtered_people.size, 2);
assert.ok(filtered_people.has(ashton.user_id));

View File

@@ -0,0 +1,56 @@
"use strict";
const {strict: assert} = require("assert");
const {zrequire} = require("../zjsunit/namespace");
const {run_test} = require("../zjsunit/test");
const resolved_topic = zrequire("../shared/js/resolved_topic");
const topic_name = "asdf";
const resolved_name = "✔ " + topic_name;
const overresolved_name = "✔ ✔✔ " + topic_name;
const pseudoresolved_name = "✔" + topic_name; // check mark, but no space
const names = [topic_name, resolved_name, overresolved_name, pseudoresolved_name];
run_test("is_resolved", () => {
assert.ok(!resolved_topic.is_resolved(topic_name));
assert.ok(resolved_topic.is_resolved(resolved_name));
assert.ok(resolved_topic.is_resolved(overresolved_name));
assert.ok(!resolved_topic.is_resolved(pseudoresolved_name));
});
run_test("resolve_name", () => {
assert.equal(resolved_topic.resolve_name(topic_name), resolved_name);
for (const name of names) {
assert.notEqual(resolved_topic.resolve_name(name), name);
}
});
run_test("unresolve_name", () => {
assert.equal(resolved_topic.unresolve_name(topic_name), topic_name);
assert.equal(resolved_topic.unresolve_name(resolved_name), topic_name);
assert.equal(resolved_topic.unresolve_name(overresolved_name), topic_name);
assert.equal(resolved_topic.unresolve_name(pseudoresolved_name), pseudoresolved_name);
});
run_test("display_parts", () => {
const results = [];
for (const name of names) {
const [prefix, display_name] = resolved_topic.display_parts(name);
// The parts always partition the input name.
assert.equal(prefix + display_name, name);
// The prefix is always the canonical prefix, or empty…
assert.ok(prefix === "" || prefix === resolved_topic.RESOLVED_TOPIC_PREFIX);
// … and which one is determined by is_resolved.
assert.equal(Boolean(prefix), resolved_topic.is_resolved(name));
// The parts, together, differ from those of any other input.
// (Yes, this is quadratic. Keep the list of test data nice and short.)
assert.ok(!results.some(([p, d]) => p === prefix && d === display_name));
results.push([prefix, display_name]);
}
});

View File

@@ -0,0 +1,80 @@
"use strict";
const {strict: assert} = require("assert");
const {zrequire} = require("../zjsunit/namespace");
const {run_test} = require("../zjsunit/test");
const {page_params} = require("../zjsunit/zpage_params");
const people = zrequire("people");
const stream_create_subscribers_data = zrequire("stream_create_subscribers_data");
const me = {
email: "me@zulip.com",
full_name: "Zed", // Zed will sort to the top by virtue of being the current user.
user_id: 400,
};
const test_user101 = {
email: "test101@zulip.com",
full_name: "Test User 101",
user_id: 101,
};
const test_user102 = {
email: "test102@zulip.com",
full_name: "Test User 102",
user_id: 102,
};
const test_user103 = {
email: "test102@zulip.com",
full_name: "Test User 103",
user_id: 103,
};
function test(label, f) {
run_test(label, ({override, override_rewire}) => {
page_params.is_admin = false;
people.init();
people.add_active_user(me);
people.add_active_user(test_user101);
people.add_active_user(test_user102);
people.add_active_user(test_user103);
page_params.user_id = me.user_id;
people.initialize_current_user(me.user_id);
f({override, override_rewire});
});
}
test("basics", () => {
stream_create_subscribers_data.initialize_with_current_user();
assert.deepEqual(stream_create_subscribers_data.sorted_user_ids(), [me.user_id]);
assert.deepEqual(stream_create_subscribers_data.get_principals(), [me.user_id]);
const all_user_ids = stream_create_subscribers_data.get_all_user_ids();
assert.deepEqual(all_user_ids, [101, 102, 103, 400]);
stream_create_subscribers_data.add_user_ids(all_user_ids);
assert.deepEqual(stream_create_subscribers_data.sorted_user_ids(), [400, 101, 102, 103]);
stream_create_subscribers_data.remove_user_ids([101, 103]);
assert.deepEqual(stream_create_subscribers_data.sorted_user_ids(), [400, 102]);
assert.deepEqual(stream_create_subscribers_data.get_potential_subscribers(), [
test_user101,
test_user103,
]);
assert.ok(stream_create_subscribers_data.must_be_subscribed(me.user_id));
assert.ok(!stream_create_subscribers_data.must_be_subscribed(test_user101.user_id));
});
test("must_be_subscribed", () => {
page_params.is_admin = false;
assert.ok(stream_create_subscribers_data.must_be_subscribed(me.user_id));
assert.ok(!stream_create_subscribers_data.must_be_subscribed(test_user101.user_id));
page_params.is_admin = true;
assert.ok(!stream_create_subscribers_data.must_be_subscribed(me.user_id));
assert.ok(!stream_create_subscribers_data.must_be_subscribed(test_user101.user_id));
});

View File

@@ -83,10 +83,9 @@ test("get_list_info w/real stream_topic_history", ({override}) => {
is_active_topic: true,
is_muted: false,
is_zero: true,
resolved: false,
resolved_topic_prefix: "✔ ",
topic_display_name: "topic 6",
topic_name: "topic 6",
topic_resolved_prefix: "",
unread: 0,
url: "#narrow/stream/556-general/topic/topic.206",
});
@@ -95,10 +94,9 @@ test("get_list_info w/real stream_topic_history", ({override}) => {
is_active_topic: false,
is_muted: false,
is_zero: true,
resolved: true,
resolved_topic_prefix: "✔ ",
topic_display_name: "topic 5",
topic_name: "✔ topic 5",
topic_resolved_prefix: "✔ ",
unread: 0,
url: "#narrow/stream/556-general/topic/.E2.9C.94.20topic.205",
});

View File

@@ -625,6 +625,8 @@ test("server_counts", () => {
page_params.unread_msgs = {
pms: [
{
other_user_id: 101,
// sender_id is deprecated.
sender_id: 101,
unread_message_ids: [31, 32, 60, 61, 62, 63],
},

View File

@@ -4,33 +4,29 @@ import type {ElementHandle, Page} from "puppeteer";
import common from "../puppeteer_lib/common";
async function user_checkbox(page: Page, name: string): Promise<string> {
async function user_row_selector(page: Page, name: string): Promise<string> {
const user_id = await common.get_user_id_from_name(page, name);
return `#user_checkbox_${CSS.escape(user_id.toString())}`;
const selector = `.remove_potential_subscriber[data-user-id="${user_id}"]`;
return selector;
}
async function user_span(page: Page, name: string): Promise<string> {
return (await user_checkbox(page, name)) + " span";
async function await_user_visible(page: Page, name: string): Promise<void> {
const selector = await user_row_selector(page, name);
await page.waitForSelector(selector, {visible: true});
}
async function stream_checkbox(page: Page, stream_name: string): Promise<string> {
const stream_id = await common.get_stream_id(page, stream_name);
return `#stream-checkboxes [data-stream-id="${CSS.escape(stream_id.toString())}"]`;
async function await_user_hidden(page: Page, name: string): Promise<void> {
const selector = await user_row_selector(page, name);
await page.waitForSelector(selector, {hidden: true});
}
async function stream_span(page: Page, stream_name: string): Promise<string> {
return (await stream_checkbox(page, stream_name)) + " input ~ span";
}
async function wait_for_checked(page: Page, user_name: string, is_checked: boolean): Promise<void> {
const selector = await user_checkbox(page, user_name);
await page.waitForFunction(
(selector: string, is_checked: boolean) =>
$(selector).find("input").prop("checked") === is_checked,
{},
selector,
is_checked,
async function add_user_to_stream(page: Page, name: string): Promise<void> {
const user_id = await common.get_user_id_from_name(page, name);
await page.evaluate(
(user_id: Number) => zulip_test.add_user_id_to_new_stream(user_id),
user_id,
);
await await_user_visible(page, name);
}
async function stream_name_error(page: Page): Promise<string> {
@@ -83,29 +79,9 @@ async function test_subscription_button(page: Page): Promise<void> {
button = await subscribed();
}
async function click_create_new_stream(
page: Page,
cordelia_checkbox: string,
othello_checkbox: string,
): Promise<void> {
async function click_create_new_stream(page: Page): Promise<void> {
await page.click("#add_new_subscription .create_stream_button");
await page.waitForSelector(cordelia_checkbox, {visible: true});
await page.waitForSelector(othello_checkbox, {visible: true});
}
async function open_copy_from_stream_dropdown(
page: Page,
scotland_checkbox: string,
rome_checkbox: string,
): Promise<void> {
await page.click("#copy-from-stream-expand-collapse .control-label");
await page.waitForSelector(scotland_checkbox, {visible: true});
await page.waitForSelector(rome_checkbox, {visible: true});
}
async function verify_check_all_only_affects_visible_users(page: Page): Promise<void> {
await wait_for_checked(page, "cordelia", false);
await wait_for_checked(page, "othello", true);
await await_user_visible(page, "desdemona");
}
async function clear_ot_filter_with_backspace(page: Page): Promise<void> {
@@ -114,52 +90,33 @@ async function clear_ot_filter_with_backspace(page: Page): Promise<void> {
await page.keyboard.press("Backspace");
}
async function verify_filtered_users_are_visible_again(
page: Page,
cordelia_checkbox: string,
othello_checkbox: string,
): Promise<void> {
await page.waitForSelector(cordelia_checkbox, {visible: true});
await page.waitForSelector(othello_checkbox, {visible: true});
}
async function test_user_filter_ui(
page: Page,
cordelia_checkbox: string,
othello_checkbox: string,
scotland_checkbox: string,
rome_checkbox: string,
): Promise<void> {
async function test_user_filter_ui(page: Page): Promise<void> {
await page.waitForSelector("form#stream_creation_form", {visible: true});
// Desdemona should be checked by default
await wait_for_checked(page, "desdemona", true);
// Desdemona should be there by default
await await_user_visible(page, "desdemona");
await add_user_to_stream(page, "cordelia");
await add_user_to_stream(page, "othello");
await page.type(`form#stream_creation_form [name="user_list_filter"]`, "ot", {delay: 100});
await page.waitForSelector("#user-checkboxes", {visible: true});
await page.waitForSelector("#create_stream_subscribers", {visible: true});
// Wait until filtering is completed.
await page.waitForFunction(
() => document.querySelectorAll("#user-checkboxes label").length === 1,
() =>
document.querySelectorAll("#create_stream_subscribers .remove_potential_subscriber")
.length === 1,
);
await page.waitForSelector(cordelia_checkbox, {hidden: true});
await page.waitForSelector(othello_checkbox, {visible: true});
await await_user_hidden(page, "cordelia");
await await_user_hidden(page, "desdemona");
await await_user_visible(page, "othello");
// Filter shouldn't affect streams.
await page.waitForSelector(scotland_checkbox, {visible: true});
await page.waitForSelector(rome_checkbox, {visible: true});
// Test check all
await page.click(".subs_set_all_users");
await wait_for_checked(page, "othello", true);
// Clear the filter.
await clear_ot_filter_with_backspace(page);
await verify_filtered_users_are_visible_again(page, cordelia_checkbox, othello_checkbox);
await verify_check_all_only_affects_visible_users(page);
// Test unset all
await page.click(".subs_unset_all_users");
await verify_filtered_users_are_visible_again(page, cordelia_checkbox, othello_checkbox);
await wait_for_checked(page, "cordelia", false);
await wait_for_checked(page, "othello", false);
await await_user_visible(page, "cordelia");
await await_user_visible(page, "desdemona");
await await_user_visible(page, "othello");
}
async function create_stream(page: Page): Promise<void> {
@@ -168,13 +125,6 @@ async function create_stream(page: Page): Promise<void> {
stream_name: "Puppeteer",
stream_description: "Everything Puppeteer",
});
await page.click(await stream_span(page, "Scotland")); // Subscribes all users from Scotland
await page.click(await user_span(page, "cordelia")); // Add cordelia.
await page.click(await user_span(page, "desdemona")); // Add cordelia.
await page.click(await user_span(page, "othello")); // Remove othello who was selected from Scotland.
await wait_for_checked(page, "cordelia", true);
await wait_for_checked(page, "desdemona", true); // Add desdemona back as we did unset all in last test.
await wait_for_checked(page, "othello", false);
await page.click("form#stream_creation_form .finalize_create_stream");
await page.waitForFunction(() => $(".stream-name").is(':contains("Puppeteer")'));
const stream_name = await common.get_text_from_selector(
@@ -189,9 +139,9 @@ async function create_stream(page: Page): Promise<void> {
assert.strictEqual(stream_name, "Puppeteer");
assert.strictEqual(stream_description, "Everything Puppeteer");
// Assert subscriber count becomes 6(scotland(+5), cordelia(+1), othello(-1), Desdemona(+1)).
// Assert subscriber count becomes 3 (cordelia, desdemona, othello)
await page.waitForFunction(
(subscriber_count_selector: string) => $(subscriber_count_selector).text().trim() === "6",
(subscriber_count_selector: string) => $(subscriber_count_selector).text().trim() === "3",
{},
subscriber_count_selector,
);
@@ -201,13 +151,13 @@ async function test_streams_with_empty_names_cannot_be_created(page: Page): Prom
await page.click("#add_new_subscription .create_stream_button");
await page.waitForSelector("form#stream_creation_form", {visible: true});
await common.fill_form(page, "form#stream_creation_form", {stream_name: " "});
await page.click("form#stream_creation_form button.button.sea-green");
await page.click("form#stream_creation_form button.finalize_create_stream");
assert.strictEqual(await stream_name_error(page), "A stream needs to have a name");
}
async function test_streams_with_duplicate_names_cannot_be_created(page: Page): Promise<void> {
await common.fill_form(page, "form#stream_creation_form", {stream_name: "Puppeteer"});
await page.click("form#stream_creation_form button.button.sea-green");
await page.click("form#stream_creation_form button.finalize_create_stream");
assert.strictEqual(await stream_name_error(page), "A stream with this name already exists");
const cancel_button_selector = "form#stream_creation_form button.button.white";
@@ -215,20 +165,8 @@ async function test_streams_with_duplicate_names_cannot_be_created(page: Page):
}
async function test_stream_creation(page: Page): Promise<void> {
const cordelia_checkbox = await user_checkbox(page, "cordelia");
const othello_checkbox = await user_checkbox(page, "othello");
const scotland_checkbox = await stream_checkbox(page, "Scotland");
const rome_checkbox = await stream_checkbox(page, "Rome");
await click_create_new_stream(page, cordelia_checkbox, othello_checkbox);
await open_copy_from_stream_dropdown(page, scotland_checkbox, rome_checkbox);
await test_user_filter_ui(
page,
cordelia_checkbox,
othello_checkbox,
scotland_checkbox,
rome_checkbox,
);
await click_create_new_stream(page);
await test_user_filter_ui(page);
await create_stream(page);
await test_streams_with_empty_names_cannot_be_created(page);
await test_streams_with_duplicate_names_cannot_be_created(page);

View File

@@ -4,11 +4,18 @@ if [ -z "$ZULIP_SECRETS_CONF" ]; then
fi
export PGHOST=/var/run/postgresql/
AWS_REGION=$(crudini --get "$ZULIP_SECRETS_CONF" secrets s3_region)
AWS_REGION=$(crudini --get "$ZULIP_SECRETS_CONF" secrets s3_region 2>/dev/null)
if [ "$AWS_REGION" = "" ]; then
# Fall back to the current region, if possible
AZ=$(ec2metadata --availability-zone || true)
if [ -n "$AZ" ] && [ "$AZ" != "unavailable" ]; then
AWS_REGION=$(echo "$AZ" | sed 's/.$//')
fi
fi
export AWS_REGION
AWS_ACCESS_KEY_ID=$(crudini --get "$ZULIP_SECRETS_CONF" secrets s3_backups_key)
AWS_ACCESS_KEY_ID=$(crudini --get "$ZULIP_SECRETS_CONF" secrets s3_backups_key 2>/dev/null)
export AWS_ACCESS_KEY_ID
AWS_SECRET_ACCESS_KEY=$(crudini --get "$ZULIP_SECRETS_CONF" secrets s3_backups_secret_key)
AWS_SECRET_ACCESS_KEY=$(crudini --get "$ZULIP_SECRETS_CONF" secrets s3_backups_secret_key 2>/dev/null)
export AWS_SECRET_ACCESS_KEY
if ! s3_backups_bucket=$(crudini --get "$ZULIP_SECRETS_CONF" secrets s3_backups_bucket 2>&1); then
echo "Could not determine which s3 bucket to use:" "$s3_backups_bucket"

View File

@@ -108,10 +108,8 @@ class zulip::postgresql_base {
}
}
$s3_backups_key = zulipsecret('secrets', 's3_backups_key', '')
$s3_backups_secret_key = zulipsecret('secrets', 's3_backups_secret_key', '')
$s3_backups_bucket = zulipsecret('secrets', 's3_backups_bucket', '')
if $s3_backups_key != '' and $s3_backups_secret_key != '' and $s3_backups_bucket != '' {
$s3_backups_bucket = zulipsecret('secrets', 's3_backups_bucket', '')
if $s3_backups_bucket != '' {
include zulip::postgresql_backups
}
}

View File

@@ -13,7 +13,7 @@ class zulip::profile::postgresql {
$listen_addresses = zulipconf('postgresql', 'listen_addresses', undef)
$replication = zulipconf('postgresql', 'replication', undef)
$s3_backups_bucket = zulipsecret('secrets', 's3_backups_bucket', '')
$replication_primary = zulipconf('postgresql', 'replication_primary', undef)
$replication_user = zulipconf('postgresql', 'replication_user', undef)
@@ -38,6 +38,13 @@ class zulip::profile::postgresql {
}
if $replication_primary != '' and $replication_user != '' {
if $s3_backups_bucket == '' {
$message = @(EOT/L)
Replication is enabled, but s3_backups_bucket is not set in zulip-secrets.conf! \
Streaming replication requires wal-g backups be configured.
|-EOT
warning($message)
}
if $zulip::postgresql_common::version in ['10', '11'] {
# PostgreSQL 11 and below used a recovery.conf file for replication
file { "${zulip::postgresql_base::postgresql_confdir}/recovery.conf":

View File

@@ -787,8 +787,8 @@ effective_io_concurrency = <%= @effective_io_concurrency %>
listen_addresses = <%= @listen_addresses %>
<% end -%>
<% if @replication != '' || (@replication_primary != '' && @replication_user != '') -%>
# Replication
<% if @s3_backups_bucket != '' -%>
# Streaming backups and replication
max_wal_senders = 5
archive_mode = on
archive_command = '/usr/bin/timeout 10m /usr/local/bin/env-wal-g wal-push %p'

View File

@@ -818,8 +818,8 @@ effective_io_concurrency = <%= @effective_io_concurrency %>
listen_addresses = <%= @listen_addresses %>
<% end -%>
<% if @replication != '' || (@replication_primary != '' && @replication_user != '') -%>
# Replication
<% if @s3_backups_bucket != '' -%>
# Streaming backups and replication
max_wal_senders = 5
archive_mode = on
archive_command = '/usr/bin/timeout 10m /usr/local/bin/env-wal-g wal-push %p'

View File

@@ -839,8 +839,8 @@ effective_io_concurrency = <%= @effective_io_concurrency %>
listen_addresses = <%= @listen_addresses %>
<% end -%>
<% if @replication != '' || (@replication_primary != '' && @replication_user != '') -%>
# Replication
<% if @s3_backups_bucket != '' -%>
# Streaming backups and replication
max_wal_senders = 5
archive_mode = on
archive_command = '/usr/bin/timeout 10m /usr/local/bin/env-wal-g wal-push %p'

View File

@@ -212,7 +212,7 @@ if [ -f /etc/os-release ]; then
fi
case "$os_id $os_version_id" in
'debian 10' | 'debian 11' | 'ubuntu 20.04' | 'ubuntu 22.04') ;;
'debian 10' | 'debian 11' | 'ubuntu 20.04') ;;
*)
set +x
cat <<EOF
@@ -223,7 +223,6 @@ Zulip in production is supported only on:
- Debian 10 "buster"
- Debian 11 "bullseye"
- Ubuntu 20.04 LTS "focal"
- Ubuntu 22.04 LTS "jammy"
For more information, see:
https://zulip.readthedocs.io/en/latest/production/requirements.html

View File

@@ -98,6 +98,14 @@ aux_services = list_supervisor_processes(["go-camo", "smokescreen"], only_runnin
if aux_services:
subprocess.check_call(["supervisorctl", "start", *aux_services])
def restart_or_start(service: str) -> None:
our_verb = action
if our_verb == "restart" and len(list_supervisor_processes([service], only_running=True)) == 0:
our_verb = "start"
subprocess.check_call(["supervisorctl", our_verb, service])
if action == "restart" and len(workers) > 0:
if args.less_graceful:
# The less graceful form stops every worker now; we start them
@@ -111,7 +119,7 @@ if action == "restart" and len(workers) > 0:
# requires multiple `supervisorctl restart` calls.
for worker in workers:
logging.info("Restarting %s", worker)
subprocess.check_call(["supervisorctl", "restart", worker])
restart_or_start(worker)
if has_application_server():
# Next, we restart the Tornado processes sequentially, in order to
@@ -130,12 +138,10 @@ if has_application_server():
# supervisord group where if any individual process is slow to
# stop, the whole bundle stays stopped for an extended time.
logging.info("%s Tornado process on port %s", verbing, p)
subprocess.check_call(
["supervisorctl", action, f"zulip-tornado:zulip-tornado-port-{p}"]
)
restart_or_start(f"zulip-tornado:zulip-tornado-port-{p}")
else:
logging.info("%s Tornado process", verbing)
subprocess.check_call(["supervisorctl", action, "zulip-tornado:*"])
restart_or_start("zulip-tornado:*")
# Finally, restart the Django uWSGI processes.
if (
@@ -160,7 +166,7 @@ if has_application_server():
subprocess.check_call(["supervisorctl", "start", "zulip-django"])
else:
logging.info("%s django server", verbing)
subprocess.check_call(["supervisorctl", action, "zulip-django"])
restart_or_start("zulip-django")
using_sso = subprocess.check_output(["./scripts/get-django-setting", "USING_APACHE_SSO"])
if using_sso.strip() == b"True":

View File

@@ -1,38 +1 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="512.000000pt" height="512.000000pt" viewBox="0 0 512.000000 512.000000"
preserveAspectRatio="xMidYMid meet">
<g transform="translate(0.000000,512.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M2380 5114 c-19 -2 -78 -9 -130 -14 -330 -36 -695 -160 -990 -336
-375 -224 -680 -529 -904 -904 -175 -292 -291 -632 -338 -990 -16 -123 -16
-497 0 -620 82 -623 356 -1150 820 -1581 256 -239 575 -425 922 -539 274 -91
491 -124 800 -124 228 0 329 9 530 50 689 141 1304 583 1674 1204 175 292 291
632 338 990 16 123 16 497 0 620 -47 358 -163 698 -338 990 -224 375 -529 680
-904 904 -289 173 -634 291 -980 336 -88 12 -438 21 -500 14z m281 -320 c109
-55 219 -193 320 -399 81 -167 125 -292 184 -517 l43 -168 -648 0 -648 0 43
168 c59 225 103 350 184 517 114 233 237 376 360 416 38 12 123 3 162 -17z
m-695 -86 c-143 -232 -277 -596 -346 -940 l-11 -58 -495 0 c-272 0 -494 3
-494 6 0 12 95 152 159 234 271 349 667 627 1086 760 55 17 105 33 110 35 6 2
12 4 14 4 1 1 -9 -18 -23 -41z m1254 14 c428 -128 845 -415 1121 -772 64 -82
159 -222 159 -234 0 -3 -222 -6 -494 -6 l-495 0 -11 58 c-69 344 -203 708
-346 940 -16 25 -21 40 -13 38 8 -3 43 -14 79 -24z m-1664 -1334 c-7 -31 -23
-174 -38 -338 -7 -79 -12 -283 -12 -495 0 -342 5 -428 40 -762 l7 -63 -550 0
-550 0 -36 113 c-73 224 -104 401 -114 637 -11 296 33 570 142 883 l17 47 549
0 549 0 -4 -22z m1704 15 c0 -5 5 -37 10 -73 59 -391 63 -993 11 -1450 l-16
-145 -705 0 -705 0 -16 145 c-52 457 -48 1059 11 1450 5 36 10 68 10 73 0 4
315 7 700 7 385 0 700 -3 700 -7z m1415 -40 c183 -524 192 -1014 28 -1520
l-36 -113 -550 0 -550 0 7 63 c35 334 40 420 40 762 0 212 -5 416 -12 495 -15
164 -31 307 -38 338 l-4 22 549 0 549 0 17 -47z m-3043 -2063 c77 -344 198
-667 334 -888 16 -25 21 -40 13 -38 -8 3 -43 14 -79 24 -411 123 -813 393
-1085 727 -74 91 -205 280 -205 296 0 5 216 8 497 7 l497 -3 28 -125z m1578
123 c0 -24 -94 -364 -125 -453 -84 -243 -230 -494 -341 -586 -76 -64 -110 -78
-184 -78 -74 0 -108 14 -184 78 -111 92 -257 343 -341 586 -31 89 -125 429
-125 453 0 4 293 7 650 7 358 0 650 -3 650 -7z m1300 -2 c0 -4 -30 -53 -66
-107 -302 -453 -738 -773 -1262 -928 l-54 -15 26 42 c134 217 258 545 330 872
15 66 29 126 31 133 3 9 112 12 500 12 274 0 495 -4 495 -9z"/>
</g>
</svg>
<svg viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><path d="M0 256a256 256 0 0 0 512 0 256 256 0 0 0-512 0m451 114a226 226 0 0 1-138 105c17-26 30-63 38-105m-190 0c8 42 21 79 38 105A226 226 0 0 1 61 370m0-228A226 226 0 0 1 199 37c-17 26-30 63-38 105m190 0c-8-42-21-79-38-105a226 226 0 0 1 138 105m-95 198c8-54 8-114 0-168h110a226 226 0 0 1 0 168M156 172c-8 54-8 114 0 168H46a226 226 0 0 1 0-168m275 198c-34 149-96 149-130 0m0-228c34-149 96-149 130 0m5 30c8 54 8 114 0 168H186c-8-54-8-114 0-168"/></svg>

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 512 B

View File

@@ -160,6 +160,7 @@ export function build_page() {
create_web_public_stream_policy_values:
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(),
};
if (options.realm_logo_source !== "D" && options.realm_night_logo_source === "D") {

View File

@@ -222,6 +222,10 @@ export function start(msg_type, opts) {
return;
}
// We may be able to clear it to change the recipient, so save any
// existing content as a draft.
drafts.update_draft();
autosize_message_content();
if (reload_state.is_in_progress()) {
@@ -328,10 +332,6 @@ export function cancel() {
}
export function respond_to_message(opts) {
// Before initiating a reply to a message, if there's an
// in-progress composition, snapshot it.
drafts.update_draft();
let message;
let msg_type;
if (recent_topics_util.is_visible()) {

View File

@@ -1,5 +1,6 @@
import $ from "jquery";
import * as resolved_topic from "../shared/js/resolved_topic";
import render_compose_all_everyone from "../templates/compose_all_everyone.hbs";
import render_compose_announce from "../templates/compose_announce.hbs";
import render_compose_invite_users from "../templates/compose_invite_users.hbs";
@@ -13,7 +14,6 @@ import * as compose_pm_pill from "./compose_pm_pill";
import * as compose_state from "./compose_state";
import * as compose_ui from "./compose_ui";
import {$t_html} from "./i18n";
import * as message_edit from "./message_edit";
import {page_params} from "./page_params";
import * as peer_data from "./peer_data";
import * as people from "./people";
@@ -178,7 +178,7 @@ export function warn_if_topic_resolved() {
const sub = stream_data.get_sub(stream_name);
if (sub && topic_name.startsWith(message_edit.RESOLVED_TOPIC_PREFIX)) {
if (sub && resolved_topic.is_resolved(topic_name)) {
const error_area = $("#compose_resolved_topic");
if (error_area.html()) {
@@ -524,7 +524,9 @@ function validate_stream_message() {
if (page_params.realm_mandatory_topics) {
const topic = compose_state.topic();
if (topic === "") {
// TODO: We plan to migrate the empty topic to only using the
// `""` representation for i18n reasons, but have not yet done so.
if (topic === "" || topic === "(no topic)") {
compose_error.show(
$t_html({defaultMessage: "Topics are required in this organization"}),
$("#stream_message_recipient_topic"),

View File

@@ -86,6 +86,7 @@ export function launch(conf) {
// * on_shown: Callback to run when the modal is shown.
// * on_hide: Callback to run when the modal is triggered to hide.
// * on_hidden: Callback to run when the modal is hidden.
// * post_render: Callback to run after the modal body is added to DOM.
for (const f of mandatory_fields) {
if (conf[f] === undefined) {

View File

@@ -1,9 +1,10 @@
import Handlebars from "handlebars/runtime";
import _ from "lodash";
import * as resolved_topic from "../shared/js/resolved_topic";
import * as hash_util from "./hash_util";
import {$t} from "./i18n";
import * as message_edit from "./message_edit";
import * as message_parser from "./message_parser";
import * as message_store from "./message_store";
import {page_params} from "./page_params";
@@ -95,10 +96,7 @@ function message_matches_search_term(message, operator, operand) {
case "unread":
return unread.message_unread(message);
case "resolved":
return (
message.type === "stream" &&
message.topic.startsWith(message_edit.RESOLVED_TOPIC_PREFIX)
);
return message.type === "stream" && resolved_topic.is_resolved(message.topic);
default:
return false; // is:whatever returns false
}

View File

@@ -806,10 +806,14 @@ export function process_hotkey(e, hotkey) {
compose_actions.respond_to_message({trigger: "hotkey"});
return true;
case "compose": // 'c': compose
compose_actions.start("stream", {trigger: "compose_hotkey"});
if (!compose_state.composing()) {
compose_actions.start("stream", {trigger: "compose_hotkey"});
}
return true;
case "compose_private_message":
compose_actions.start("private", {trigger: "compose_hotkey"});
if (!compose_state.composing()) {
compose_actions.start("private", {trigger: "compose_hotkey"});
}
return true;
case "open_drafts":
browser_history.go_to_location("drafts");

View File

@@ -1,7 +1,7 @@
import ClipboardJS from "clipboard";
import $ from "jquery";
import _ from "lodash";
import * as resolved_topic from "../shared/js/resolved_topic";
import render_delete_message_modal from "../templates/confirm_dialog/confirm_delete_message.hbs";
import render_message_edit_form from "../templates/message_edit_form.hbs";
import render_topic_edit_form from "../templates/topic_edit_form.hbs";
@@ -38,7 +38,6 @@ const currently_editing_messages = new Map();
let currently_deleting_messages = [];
let currently_topic_editing_messages = [];
const currently_echoing_messages = new Map();
export const RESOLVED_TOPIC_PREFIX = "✔ ";
// These variables are designed to preserve the user's most recent
// choices when editing a group of messages, to make it convenient to
@@ -641,10 +640,10 @@ export function start(row, edit_box_open_callback) {
export function toggle_resolve_topic(message_id, old_topic_name) {
let new_topic_name;
if (old_topic_name.startsWith(RESOLVED_TOPIC_PREFIX)) {
new_topic_name = _.trimStart(old_topic_name, RESOLVED_TOPIC_PREFIX);
if (resolved_topic.is_resolved(old_topic_name)) {
new_topic_name = resolved_topic.unresolve_name(old_topic_name);
} else {
new_topic_name = RESOLVED_TOPIC_PREFIX + old_topic_name;
new_topic_name = resolved_topic.resolve_name(old_topic_name);
}
const request = {

View File

@@ -184,7 +184,37 @@ export function update_messages(events) {
const old_stream = sub_store.get(event.stream_id);
// A topic edit may affect multiple messages, listed in
// Save the content edit to the front end msg.edit_history
// before topic edits to ensure that combined topic / content
// edits have edit_history logged for both before any
// potential narrowing as part of the topic edit loop.
if (event.orig_content !== undefined) {
if (page_params.realm_allow_edit_history) {
// Note that we do this for topic edits separately, below.
// If an event changed both content and topic, we'll generate
// two client-side events, which is probably good for display.
const edit_history_entry = {
user_id: event.user_id,
prev_content: event.orig_content,
prev_rendered_content: event.orig_rendered_content,
prev_rendered_content_version: event.prev_rendered_content_version,
timestamp: event.edit_timestamp,
};
// Add message's edit_history in message dict
// For messages that are edited, edit_history needs to
// be added to message in frontend.
if (msg.edit_history === undefined) {
msg.edit_history = [];
}
msg.edit_history = [edit_history_entry].concat(msg.edit_history);
}
message_content_edited = true;
// Update raw_content, so that editing a few times in a row is fast.
msg.raw_content = event.content;
}
// A topic or stream edit may affect multiple messages, listed in
// event.message_ids. event.message_id is still the first message
// where the user initiated the edit.
topic_edited = new_topic !== undefined;
@@ -234,18 +264,23 @@ export function update_messages(events) {
* messages that were moved are displayed as such
* without a browser reload. */
const edit_history_entry = {
edited_by: event.edited_by,
prev_topic: orig_topic,
prev_stream: event.stream_id,
user_id: event.user_id,
timestamp: event.edit_timestamp,
};
if (stream_changed) {
edit_history_entry.stream = event.new_stream_id;
edit_history_entry.prev_stream = event.stream_id;
}
if (topic_edited) {
edit_history_entry.topic = new_topic;
edit_history_entry.prev_topic = orig_topic;
}
if (msg.edit_history === undefined) {
msg.edit_history = [];
}
msg.edit_history = [edit_history_entry].concat(msg.edit_history);
}
msg.last_edit_timestamp = event.edit_timestamp;
delete msg.last_edit_timestr;
// Remove the recent topics entry for the old topics;
// must be called before we call set_message_topic.
@@ -389,35 +424,14 @@ export function update_messages(events) {
}
}
if (event.orig_content !== undefined) {
if (page_params.realm_allow_edit_history) {
// Note that we do this for topic edits separately, above.
// If an event changed both content and topic, we'll generate
// two client-side events, which is probably good for display.
const edit_history_entry = {
edited_by: event.edited_by,
prev_content: event.orig_content,
prev_rendered_content: event.orig_rendered_content,
prev_rendered_content_version: event.prev_rendered_content_version,
timestamp: event.edit_timestamp,
};
// Add message's edit_history in message dict
// For messages that are edited, edit_history needs to
// be added to message in frontend.
if (msg.edit_history === undefined) {
msg.edit_history = [];
}
msg.edit_history = [edit_history_entry].concat(msg.edit_history);
}
message_content_edited = true;
// Update raw_content, so that editing a few times in a row is fast.
msg.raw_content = event.content;
// Mark the message as edited for the UI. The rendering_only
// flag is used to indicated update_message events that are
// triggered by server latency optimizations, not user
// interactions; these should not generate edit history updates.
if (!event.rendering_only) {
msg.last_edit_timestamp = event.edit_timestamp;
}
msg.last_edit_timestamp = event.edit_timestamp;
delete msg.last_edit_timestr;
notifications.received_messages([msg]);
alert_words.process_message(msg);

View File

@@ -431,6 +431,12 @@ export function initialize(home_view_loaded) {
}
if (data.found_newest) {
if (page_params.is_spectator) {
// Since for spectators, this is the main fetch, we
// hide the Recent Topics loading indicator here.
recent_topics_ui.hide_loading_indicator();
}
// See server_events.js for this callback.
home_view_loaded();
start_backfilling_messages();
@@ -472,6 +478,10 @@ export function initialize(home_view_loaded) {
if (page_params.is_spectator) {
// Since spectators never have old unreads, we can skip the
// hacky fetch below for them (which would just waste resources).
// This optimization requires a bit of duplicated loading
// indicator code, here and hiding logic in hide_more.
recent_topics_ui.show_loading_indicator();
return;
}

View File

@@ -2,6 +2,7 @@ import {isSameDay} from "date-fns";
import $ from "jquery";
import _ from "lodash";
import * as resolved_topic from "../shared/js/resolved_topic";
import render_bookend from "../templates/bookend.hbs";
import render_message_group from "../templates/message_group.hbs";
import render_recipient_row from "../templates/recipient_row.hbs";
@@ -56,22 +57,50 @@ function same_recipient(a, b) {
return util.same_recipient(a.msg, b.msg);
}
function message_was_only_moved(message) {
// Returns true if the message has had its stream/topic edited
// (i.e. the message was moved), but its content has not been
// edited.
function analyze_edit_history(message) {
// Returns a dict of booleans that describe the message's history:
// * edited: if the message has had its content edited
// * moved: if the message has had its stream/topic edited
// * resolve_toggled: if the message has had a topic resolve/unresolve edit
let edited = false;
let moved = false;
let resolve_toggled = false;
if (message.edit_history !== undefined) {
for (const edit_history_event of message.edit_history) {
if (edit_history_event.prev_content) {
return false;
edited = true;
}
if (edit_history_event.prev_topic || edit_history_event.prev_stream) {
if (edit_history_event.prev_stream) {
moved = true;
}
if (edit_history_event.prev_topic) {
// We know it has a topic edit. Now we need to determine if
// it was a true move or a resolve/unresolve.
if (
resolved_topic.is_resolved(edit_history_event.topic) &&
edit_history_event.topic.slice(2) === edit_history_event.prev_topic
) {
// Resolved.
resolve_toggled = true;
continue;
}
if (
resolved_topic.is_resolved(edit_history_event.prev_topic) &&
edit_history_event.prev_topic.slice(2) === edit_history_event.topic
) {
// Unresolved.
resolve_toggled = true;
continue;
}
// Otherwise, it is a real topic rename/move.
moved = true;
}
}
}
return moved;
return {edited, moved, resolve_toggled};
}
function render_group_display_date(group, message_container) {
@@ -181,7 +210,7 @@ function populate_group_from_message_container(group, message_container) {
} else {
group.stream_id = sub.stream_id;
}
group.topic_is_resolved = group.topic.startsWith(message_edit.RESOLVED_TOPIC_PREFIX);
group.topic_is_resolved = resolved_topic.is_resolved(group.topic);
group.topic_muted = muted_topics.is_topic_muted(group.stream_id, group.topic);
} else if (group.is_private) {
group.pm_with_url = message_container.pm_with_url;
@@ -237,8 +266,11 @@ export class MessageListView {
}
_add_msg_edited_vars(message_container) {
// This adds variables to message_container object which calculate bools for
// checking position of "EDITED" label as well as the edited timestring
// This function computes data on whether the message was edited
// and in what ways, as well as where the "EDITED" or "MOVED"
// label should be located, and adds it to the message_container
// object.
//
// The bools can be defined only when the message is edited
// (or when the `last_edit_timestr` is defined). The bools are:
// * `edited_in_left_col` -- when label appears in left column.
@@ -248,18 +280,29 @@ export class MessageListView {
const include_sender = message_container.include_sender;
const is_hidden = message_container.is_hidden;
const status_message = Boolean(message_container.status_message);
if (last_edit_timestr !== undefined) {
message_container.last_edit_timestr = last_edit_timestr;
message_container.edited_in_left_col = !include_sender && !is_hidden;
message_container.edited_alongside_sender = include_sender && !status_message;
message_container.edited_status_msg = include_sender && status_message;
message_container.moved = message_was_only_moved(message_container.msg);
} else {
const edit_history_details = analyze_edit_history(message_container.msg);
if (
last_edit_timestr === undefined ||
!(edit_history_details.moved || edit_history_details.edited)
) {
// For messages whose edit history at most includes
// resolving topics, we don't display an EDITED/MOVED
// notice at all. (The message actions popover will still
// display an edit history option, so you can see when it
// was marked as resolved if you need to).
delete message_container.last_edit_timestr;
message_container.edited_in_left_col = false;
message_container.edited_alongside_sender = false;
message_container.edited_status_msg = false;
return;
}
message_container.last_edit_timestr = last_edit_timestr;
message_container.edited_in_left_col = !include_sender && !is_hidden;
message_container.edited_alongside_sender = include_sender && !status_message;
message_container.edited_status_msg = include_sender && status_message;
message_container.moved = edit_history_details.moved && !edit_history_details.edited;
}
set_calculated_message_container_variables(message_container, is_revealed) {

View File

@@ -1067,44 +1067,6 @@ export function get_user_id_from_name(full_name) {
return person.user_id;
}
function people_cmp(person1, person2) {
const name_cmp = util.strcmp(person1.full_name, person2.full_name);
if (name_cmp < 0) {
return -1;
} else if (name_cmp > 0) {
return 1;
}
return util.strcmp(person1.email, person2.email);
}
export function get_people_for_stream_create() {
/*
If you are thinking of reusing this function,
a better option in most cases is to just
call `get_realm_users()` and then filter out
the "me" user yourself as part of any other
filtering that you are doing.
In particular, this function does a sort
that is kinda expensive and may not apply
to your use case.
*/
const people_minus_you = [];
for (const person of active_user_dict.values()) {
if (!is_my_user_id(person.user_id)) {
people_minus_you.push({
email: get_visible_email(person),
show_email: settings_data.show_email(),
user_id: person.user_id,
full_name: person.full_name,
checked: false,
disabled: false,
});
}
}
return people_minus_you.sort(people_cmp);
}
export function track_duplicate_full_name(full_name, user_id, to_remove) {
let ids;
if (duplicate_full_name_data.has(full_name)) {

View File

@@ -2,26 +2,19 @@ import $ from "jquery";
import render_announce_stream_docs from "../templates/announce_stream_docs.hbs";
import render_subscription_invites_warning_modal from "../templates/confirm_dialog/confirm_subscription_invites_warning.hbs";
import render_new_stream_user from "../templates/new_stream_user.hbs";
import render_new_stream_users from "../templates/stream_settings/new_stream_users.hbs";
import * as channel from "./channel";
import * as confirm_dialog from "./confirm_dialog";
import {$t, $t_html} from "./i18n";
import * as ListWidget from "./list_widget";
import * as loading from "./loading";
import {page_params} from "./page_params";
import * as peer_data from "./peer_data";
import * as people from "./people";
import * as settings_data from "./settings_data";
import * as stream_create_subscribers from "./stream_create_subscribers";
import * as stream_data from "./stream_data";
import * as stream_settings_data from "./stream_settings_data";
import * as stream_settings_ui from "./stream_settings_ui";
import * as ui_report from "./ui_report";
let created_stream;
let all_users;
let all_users_list_widget;
export function reset_created_stream() {
created_stream = undefined;
@@ -146,11 +139,6 @@ function update_announce_stream_state() {
$("#announce-new-stream").show();
}
function get_principals() {
// Return list of user ids which were selected by user.
return all_users.filter((user) => user.checked === true).map((user) => user.user_id);
}
function create_stream() {
const data = {};
const stream_name = $("#create_stream_name").val().trim();
@@ -208,7 +196,7 @@ function create_stream() {
data.history_public_to_subscribers = JSON.stringify(history_public_to_subscribers);
const stream_post_policy = Number.parseInt(
$("#stream_creation_form input[name=stream-post-policy]:checked").val(),
$("#stream_creation_form select[name=stream-post-policy]").val(),
10,
);
@@ -233,7 +221,7 @@ function create_stream() {
// TODO: We can eliminate the user_ids -> principals conversion
// once we upgrade the backend to accept user_ids.
const user_ids = get_principals();
const user_ids = stream_create_subscribers.get_principals();
data.principals = JSON.stringify(user_ids);
loading.make_indicator($("#stream_creating_indicator"), {
@@ -303,40 +291,7 @@ export function show_new_stream_modal() {
$(".right .settings").hide();
stream_settings_ui.hide_or_disable_stream_privacy_options_if_required($("#stream-creation"));
const add_people_container = $("#people_to_add");
add_people_container.html(
render_new_stream_users({
streams: stream_settings_data.get_streams_for_settings_page(),
}),
);
all_users = people.get_people_for_stream_create();
// Add current user on top of list
const current_user = people.get_by_user_id(page_params.user_id);
all_users.unshift({
show_email: settings_data.show_email(),
email: people.get_visible_email(current_user),
user_id: current_user.user_id,
full_name: current_user.full_name,
checked: true,
disabled: !page_params.is_admin,
});
all_users_list_widget = ListWidget.create($("#user-checkboxes"), all_users, {
name: "new_stream_add_users",
parent_container: add_people_container,
modifier(item) {
return render_new_stream_user(item);
},
filter: {
element: $("#people_to_add .add-user-list-filter"),
predicate(user, search_term) {
return people.build_person_matcher(search_term)(user);
},
},
simplebar_container: $("#user-checkboxes-simplebar-wrapper"),
html_selector: (user) => $(`#${CSS.escape("user_checkbox_" + user.user_id)}`),
});
stream_create_subscribers.build_widgets();
// Select the first visible and enabled choice for stream privacy.
$("#make-invite-only input:visible:not([disabled]):first").prop("checked", true);
@@ -365,82 +320,9 @@ export function show_new_stream_modal() {
clear_error_display();
}
function create_handlers_for_users(container) {
// container should be $('#people_to_add')...see caller to verify
function update_checked_state_for_users(value, users) {
// Update the all_users backing data structure for
// which users will be submitted should the user click save,
// and also ensure that any visible checkboxes reflect
// the state of that data structure.
// If we have to rerender a very large number of users, it's
// eventually faster to just do a full redraw rather than
// many hundreds of single-item rerenders.
const full_redraw = !users || users.length > 250;
for (const user of all_users) {
// We don't want to uncheck the user creating the stream if it is not admin.
if (user.user_id === page_params.user_id && value === false && !page_params.is_admin) {
continue;
}
// We update for all users if `users` parameter is empty.
if (users === undefined || users.includes(user.user_id)) {
user.checked = value;
if (!full_redraw) {
all_users_list_widget.render_item(user);
}
}
}
if (full_redraw) {
all_users_list_widget.hard_redraw();
}
}
container.on("change", "#user-checkboxes input", (e) => {
const elem = $(e.target);
const user_id = Number.parseInt(elem.attr("data-user-id"), 10);
const checked = elem.prop("checked");
update_checked_state_for_users(checked, [user_id]);
});
// 'Check all' and 'Uncheck all' visible users
container.on("click", ".subs_set_all_users, .subs_unset_all_users", (e) => {
e.preventDefault();
// Only `check / uncheck` users who are displayed.
const mark_checked = e.target.classList.contains("subs_set_all_users");
const users_displayed = all_users_list_widget.get_current_list();
if (all_users.length !== users_displayed.length) {
update_checked_state_for_users(
mark_checked,
users_displayed.map((user) => user.user_id),
);
} else {
update_checked_state_for_users(mark_checked);
}
});
container.on("click", "#copy-from-stream-expand-collapse", (e) => {
e.preventDefault();
$("#stream-checkboxes").toggle();
$("#copy-from-stream-expand-collapse .toggle").toggleClass("fa-caret-right fa-caret-down");
});
container.on("change", "#stream-checkboxes label.checkbox", (e) => {
e.preventDefault();
const elem = $(e.target).closest("[data-stream-id]");
const stream_id = Number.parseInt(elem.attr("data-stream-id"), 10);
const checked = elem.find("input").prop("checked");
const subscriber_ids = peer_data.get_subscribers(stream_id);
update_checked_state_for_users(checked, subscriber_ids);
});
}
export function set_up_handlers() {
// Sets up all the event handlers concerning the `People to add`
// section in Create stream UI.
const people_to_add_holder = $("#people_to_add").expectOne();
create_handlers_for_users(people_to_add_holder);
stream_create_subscribers.create_handlers(people_to_add_holder);
const container = $("#stream-creation").expectOne();
@@ -457,7 +339,7 @@ export function set_up_handlers() {
return;
}
const principals = get_principals();
const principals = stream_create_subscribers.get_principals();
if (principals.length === 0) {
stream_subscription_error.report_no_subs_to_stream();
return;

View File

@@ -0,0 +1,118 @@
import $ from "jquery";
import render_new_stream_user from "../templates/stream_settings/new_stream_user.hbs";
import render_new_stream_users from "../templates/stream_settings/new_stream_users.hbs";
import * as add_subscribers_pill from "./add_subscribers_pill";
import * as ListWidget from "./list_widget";
import {page_params} from "./page_params";
import * as people from "./people";
import * as settings_data from "./settings_data";
import * as stream_create_subscribers_data from "./stream_create_subscribers_data";
let pill_widget;
let all_users_list_widget;
export function get_principals() {
return stream_create_subscribers_data.get_principals();
}
function redraw_subscriber_list() {
all_users_list_widget.replace_list_data(stream_create_subscribers_data.sorted_user_ids());
}
function add_user_ids(user_ids) {
stream_create_subscribers_data.add_user_ids(user_ids);
redraw_subscriber_list();
}
function add_all_users() {
const user_ids = stream_create_subscribers_data.get_all_user_ids();
add_user_ids(user_ids);
}
function remove_user_ids(user_ids) {
stream_create_subscribers_data.remove_user_ids(user_ids);
redraw_subscriber_list();
}
function build_pill_widget({parent_container}) {
const pill_container = parent_container.find(".pill-container");
const get_potential_subscribers = stream_create_subscribers_data.get_potential_subscribers;
pill_widget = add_subscribers_pill.create({pill_container, get_potential_subscribers});
}
export function create_handlers(container) {
container.on("click", ".add_all_users_to_stream", (e) => {
e.preventDefault();
add_all_users();
$(".add-user-list-filter").focus();
});
container.on("click", ".remove_potential_subscriber", (e) => {
e.preventDefault();
const elem = $(e.target);
const user_id = Number.parseInt(elem.attr("data-user-id"), 10);
remove_user_ids([user_id]);
});
function add_users({pill_user_ids}) {
add_user_ids(pill_user_ids);
pill_widget.clear();
}
add_subscribers_pill.set_up_handlers({
get_pill_widget: () => pill_widget,
parent_container: container,
pill_selector: ".add_subscribers_container .input",
button_selector: ".add_subscribers_container button.add-subscriber-button",
action: add_users,
});
}
export function build_widgets() {
const add_people_container = $("#people_to_add");
add_people_container.html(render_new_stream_users({}));
const simplebar_container = add_people_container.find(".subscriber_list_container");
build_pill_widget({parent_container: add_people_container});
stream_create_subscribers_data.initialize_with_current_user();
const current_user_id = page_params.user_id;
all_users_list_widget = ListWidget.create($("#create_stream_subscribers"), [current_user_id], {
name: "new_stream_add_users",
parent_container: add_people_container,
modifier(user_id) {
const user = people.get_by_user_id(user_id);
const item = {
show_email: settings_data.show_email(),
email: people.get_visible_email(user),
user_id,
full_name: user.full_name,
is_current_user: user_id === current_user_id,
disabled: stream_create_subscribers_data.must_be_subscribed(user_id),
};
return render_new_stream_user(item);
},
filter: {
element: $("#people_to_add .add-user-list-filter"),
predicate(user_id, search_term) {
const user = people.get_by_user_id(user_id);
return people.build_person_matcher(search_term)(user);
},
},
simplebar_container,
html_selector: (user_id) => {
const user = people.get_by_user_id(user_id);
return $(`#${CSS.escape("user_checkbox_" + user.user_id)}`);
},
});
}
export function add_user_id_to_new_stream(user_id) {
// This is only used by puppeteer tests.
add_user_ids([user_id]);
}

View File

@@ -0,0 +1,55 @@
import {page_params} from "./page_params";
import * as people from "./people";
let user_id_set;
export function initialize_with_current_user() {
const current_user_id = page_params.user_id;
user_id_set = new Set();
user_id_set.add(current_user_id);
}
export function sorted_user_ids() {
const users = people.get_users_from_ids(Array.from(user_id_set));
people.sort_but_pin_current_user_on_top(users);
return users.map((user) => user.user_id);
}
export function get_all_user_ids() {
const potential_subscribers = people.get_realm_users();
const user_ids = potential_subscribers.map((user) => user.user_id);
// sort for determinism
user_ids.sort((a, b) => a - b);
return user_ids;
}
export function get_principals() {
// Return list of user ids which were selected by user.
return Array.from(user_id_set);
}
export function get_potential_subscribers() {
const potential_subscribers = people.get_realm_users();
return potential_subscribers.filter((user) => !user_id_set.has(user.user_id));
}
export function must_be_subscribed(user_id) {
return !page_params.is_admin && user_id === page_params.user_id;
}
export function add_user_ids(user_ids) {
for (const user_id of user_ids) {
if (!user_id_set.has(user_id)) {
const user = people.get_by_user_id(user_id);
if (user) {
user_id_set.add(user_id);
}
}
}
}
export function remove_user_ids(user_ids) {
for (const user_id of user_ids) {
user_id_set.delete(user_id);
}
}

View File

@@ -142,21 +142,21 @@ export const stream_post_policy_values = {
// Stream.POST_POLICIES object in zerver/models.py.
everyone: {
code: 1,
description: $t({defaultMessage: "All stream members can post"}),
description: $t({defaultMessage: "Everyone"}),
},
admins: {
code: 2,
description: $t({defaultMessage: "Only organization administrators can post"}),
non_new_members: {
code: 3,
description: $t({defaultMessage: "Admins, moderators and full members"}),
},
moderators: {
code: 4,
description: $t({
defaultMessage: "Only organization administrators and moderators can post",
defaultMessage: "Admins and moderators",
}),
},
non_new_members: {
code: 3,
description: $t({defaultMessage: "Only organization full members can post"}),
admins: {
code: 2,
description: $t({defaultMessage: "Admins only"}),
},
};

View File

@@ -379,7 +379,7 @@ function change_stream_privacy(e) {
const privacy_setting = $("#stream_privacy_modal input[name=privacy]:checked").val();
const stream_post_policy = Number.parseInt(
$("#stream_privacy_modal input[name=stream-post-policy]:checked").val(),
$("#stream_privacy_modal select[name=stream-post-policy]").val(),
10,
);

View File

@@ -1,6 +1,7 @@
import ClipboardJS from "clipboard";
import $ from "jquery";
import * as resolved_topic from "../shared/js/resolved_topic";
import render_all_messages_sidebar_actions from "../templates/all_messages_sidebar_actions.hbs";
import render_delete_topic_modal from "../templates/confirm_dialog/confirm_delete_topic.hbs";
import render_drafts_sidebar_actions from "../templates/drafts_sidebar_action.hbs";
@@ -290,7 +291,7 @@ function build_topic_popover(opts) {
topic_muted,
can_move_topic,
is_realm_admin: page_params.is_admin,
topic_is_resolved: topic_name.startsWith(message_edit.RESOLVED_TOPIC_PREFIX),
topic_is_resolved: resolved_topic.is_resolved(topic_name),
color: sub.color,
has_starred_messages,
});

View File

@@ -1,5 +1,6 @@
import * as resolved_topic from "../shared/js/resolved_topic";
import * as hash_util from "./hash_util";
import * as message_edit from "./message_edit";
import * as muted_topics from "./muted_topics";
import * as narrow_state from "./narrow_state";
import * as stream_topic_history from "./stream_topic_history";
@@ -32,14 +33,8 @@ export function get_list_info(stream_id, zoomed) {
const num_unread = unread.num_unread_for_topic(stream_id, topic_name);
const is_active_topic = active_topic === topic_name.toLowerCase();
const is_topic_muted = muted_topics.is_topic_muted(stream_id, topic_name);
const resolved = topic_name.startsWith(message_edit.RESOLVED_TOPIC_PREFIX);
let topic_display_name = topic_name;
if (resolved) {
topic_display_name = topic_display_name.slice(
message_edit.RESOLVED_TOPIC_PREFIX.length,
);
}
const [topic_resolved_prefix, topic_display_name] =
resolved_topic.display_parts(topic_name);
if (!zoomed) {
function should_show_topic(topics_selected) {
@@ -100,14 +95,13 @@ export function get_list_info(stream_id, zoomed) {
const topic_info = {
topic_name,
topic_resolved_prefix,
topic_display_name,
unread: num_unread,
is_zero: num_unread === 0,
is_muted: is_topic_muted,
is_active_topic,
url: hash_util.by_stream_topic_url(stream_id, topic_name),
resolved,
resolved_topic_prefix: message_edit.RESOLVED_TOPIC_PREFIX,
};
items.push(topic_info);

View File

@@ -98,7 +98,7 @@ class UnreadPMCounter {
set_pms(pms) {
for (const obj of pms) {
const user_ids_string = obj.sender_id.toString();
const user_ids_string = obj.other_user_id.toString();
this.set_message_ids(user_ids_string, obj.unread_message_ids);
}
}

View File

@@ -14,3 +14,4 @@ export {last_visible as last_visible_row, id as row_id} from "./rows";
export {cancel as cancel_compose} from "./compose_actions";
export {page_params, page_params_parse_time} from "./page_params";
export {initiate as initiate_reload} from "./reload";
export {add_user_id_to_new_stream} from "./stream_create_subscribers";

4
static/shared/.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
# This /node_modules doesn't ordinarily appear when developing on web.
# But it does appear when developing this package in tandem with
# mobile, using `yarn link`.
/node_modules

View File

@@ -0,0 +1,45 @@
/** The canonical form of the resolved-topic prefix. */
export const RESOLVED_TOPIC_PREFIX = "✔ ";
/**
* Pattern for an arbitrary resolved-topic prefix.
*
* These always begin with the canonical prefix, but can go on longer.
*/
// The class has the same characters as RESOLVED_TOPIC_PREFIX.
// It's designed to remove a weird "✔ ✔✔ " prefix, if present.
// Compare maybe_send_resolve_topic_notifications in zerver/lib/actions.py.
const RESOLVED_TOPIC_PREFIX_RE = /^✔ [ ✔]*/;
export function is_resolved(topic_name) {
return topic_name.startsWith(RESOLVED_TOPIC_PREFIX);
}
export function resolve_name(topic_name) {
return RESOLVED_TOPIC_PREFIX + topic_name;
}
/**
* The un-resolved form of this topic name.
*
* If the topic is already not a resolved topic, this is the identity.
*/
export function unresolve_name(topic_name) {
return topic_name.replace(RESOLVED_TOPIC_PREFIX_RE, "");
}
/**
* Split the topic name for display, into a "resolved" prefix and remainder.
*
* The prefix is always the canonical resolved-topic prefix, or empty.
*
* This function is injective: different topics never produce the same
* result, even when `unresolve_name` would give the same result. That's a
* property we want when listing topics in the UI, so that we don't end up
* showing what look like several identical topics.
*/
export function display_parts(topic_name) {
return is_resolved(topic_name)
? [RESOLVED_TOPIC_PREFIX, topic_name.slice(RESOLVED_TOPIC_PREFIX.length)]
: ["", topic_name];
}

View File

@@ -0,0 +1,11 @@
// @flow strict
declare export var RESOLVED_TOPIC_PREFIX: string;
declare export function is_resolved(topic_name: string): boolean;
declare export function resolve_name(topic_name: string): string;
declare export function unresolve_name(topic_name: string): string;
declare export function display_parts(topic_name: string): [string, string];

View File

@@ -0,0 +1,15 @@
// @flow strict
// declare export var popular_emojis
declare export function remove_diacritics(s: string): string;
// declare export function query_matches_source_attrs
// declare export function clean_query_lowercase
// declare export function get_emoji_matcher
// declare export function triage
// declare export function sort_emojis

View File

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

View File

@@ -959,7 +959,7 @@ body.dark-theme {
.alert-box {
.alert.alert-error::before {
color: 1px solid hsl(0, 75%, 65%);
color: hsl(0, 75%, 65%);
}
.stacktrace {
@@ -1108,7 +1108,7 @@ body.dark-theme {
}
#out-of-view-notification {
border: 1px solid 1px solid hsl(144, 45%, 62%);
border: 1px solid hsl(144, 45%, 62%);
}
#bots_lists_navbar .active a {

View File

@@ -41,7 +41,6 @@
.image-delete-button:focus,
.image-delete-button:hover {
opacity: 1;
color: hsl(0, 0%, 100%);
}
@@ -97,6 +96,10 @@
visibility: visible;
}
.image-delete-button {
opacity: 1;
}
.image-upload-background {
display: block;
}
@@ -110,7 +113,7 @@
position: relative;
.inline-block {
margin: 5px 20px 0 0;
margin-top: 15px;
vertical-align: top;
border-radius: 4px;
}
@@ -119,23 +122,37 @@
/* CSS related to settings page user avatar upload widget only */
#user-avatar-upload-widget {
.image_upload_button {
position: absolute;
width: 100%;
height: 100%;
display: flex;
align-items: center;
text-align: center;
justify-content: center;
border-radius: 5px;
box-shadow: 0 0 10px hsla(0, 0%, 0%, 0.1);
z-index: 99;
}
.image-disabled-text {
color: hsl(0, 0%, 85%);
cursor: not-allowed;
position: absolute;
text-align: center;
visibility: hidden;
z-index: 99;
}
.image-delete-button {
font-size: 3rem;
}
.image-delete-text {
top: 90px;
right: 40px;
}
.image-upload-text {
top: 90px;
right: 24px;
.image-delete-text,
.image-upload-text,
.image-disabled-text {
box-sizing: border-box;
width: 100%;
padding: 0 10px;
}
.image_upload_spinner {
@@ -152,16 +169,18 @@
height: 200px;
top: 0;
}
&:hover {
.image-disabled-text {
visibility: visible;
}
}
}
#user-avatar-source {
font-size: 1em;
z-index: 99;
a {
position: relative;
top: 10px;
}
margin-top: 10px;
}
/* CSS related to settings page realm icon upload widget only */

View File

@@ -331,7 +331,7 @@ ul {
margin-bottom: 5px;
}
#user-type {
#date-joined {
margin-bottom: 15px;
}
}

View File

@@ -113,9 +113,22 @@ h3 .fa-question-circle-o {
flex-wrap: wrap-reverse;
}
.user-avatar-section {
float: right;
.profile-main-panel {
margin-right: 20px;
}
.profile-side-panel {
margin-right: 10px;
}
.user-details-title {
display: inline-block;
min-width: 80px;
font-weight: 600;
padding-right: 5px;
}
.user-avatar-section {
.avatar-controls {
margin-top: 20px;
box-shadow: none;
@@ -338,13 +351,6 @@ td .button {
}
}
input[type="text"].search {
float: right;
margin: 2px 5px 2px 0;
padding: 2px 5px;
font-size: 0.9em;
}
#admin_page_users_loading_indicator,
#attachments_loading_indicator,
#admin_page_deactivated_users_loading_indicator,
@@ -720,6 +726,7 @@ input[type="checkbox"] {
input[type="text"] {
padding: 6px;
}
margin-bottom: 15px;
}
.add-new-emoji-box #emoji-file-name {
@@ -745,6 +752,7 @@ input[type="checkbox"] {
button {
margin-left: calc(10em + 20px) !important;
}
margin-bottom: 15px;
}
.grey-box .wrapper {
@@ -757,7 +765,7 @@ input[type="checkbox"] {
cursor: move;
.fa-ellipsis-v {
color: hsl(0, 0, 75%);
color: hsl(0, 0%, 75%);
position: relative;
top: 1px;
@@ -1317,11 +1325,6 @@ input[type="checkbox"] {
line-height: 1.2;
}
input.search {
font-size: 0.9rem;
margin: 10px 0 20px;
}
.form-sidebar {
position: absolute;
top: 45px;
@@ -1587,7 +1590,8 @@ input[type="checkbox"] {
}
textarea {
width: 320px;
width: max(206px, 25vw);
max-width: 320px;
height: 80px;
}
@@ -1886,14 +1890,12 @@ input[type="checkbox"] {
}
.admin_exports_table {
margin-top: 20px;
margin-bottom: 20px;
}
@media (width < $lg_min) {
.user-avatar-section,
.realm-icon-section {
float: none;
display: block;
}
@@ -2040,3 +2042,18 @@ input[type="checkbox"] {
margin-top: 10px;
}
}
.settings_panel_list_header {
position: relative;
h3 {
display: inline-block;
}
input.search {
float: right;
font-size: 1em;
max-width: 160px;
margin-top: 12px;
}
}

View File

@@ -141,22 +141,6 @@
margin-right: 10px;
}
.preview-stream {
display: none;
float: right;
padding: 3px 10px;
border: 1px solid hsl(0, 0%, 80%);
margin: 9px 10px 0 0;
color: hsl(0, 0%, 20%);
}
.preview-stream:hover {
color: hsl(0, 0%, 20%);
text-decoration: none;
background-color: hsl(0, 0%, 92%);
border: 1px solid hsl(0, 0%, 68%);
}
.create_stream_plus_button {
font-size: 24px;
font-weight: 500;
@@ -313,7 +297,7 @@ h4.stream_setting_subsection_title {
.subscriber_list_add {
width: 100%;
margin: 10px auto;
margin: 0 0 10px;
.stream_subscription_request_result {
a {
@@ -608,7 +592,7 @@ h4.stream_setting_subsection_title {
}
#announce-new-stream {
margin-top: 10px;
margin: 25px auto;
div[class^="fa"] {
margin-left: 3px;
@@ -757,10 +741,6 @@ h4.stream_setting_subsection_title {
color: hsl(0, 0%, 67%);
}
&:hover .preview-stream {
display: inline-block;
}
&:hover .check:not(.checked) svg,
&.active:hover .check:not(.checked) svg {
fill: hsl(0, 0%, 87%);
@@ -805,8 +785,22 @@ h4.stream_setting_subsection_title {
margin: 8px 0;
}
.add_all_users_to_stream {
margin-left: 10px;
}
.create_stream_subscriber_list_header {
margin-top: 10px;
margin-bottom: 3px;
h5 {
display: inline-block;
}
}
.add-user-list-filter {
width: calc(100% - 10px);
width: 140px;
float: right;
}
#stream_creation_form {
@@ -1008,10 +1002,8 @@ h4.stream_setting_subsection_title {
font-weight: 400;
line-height: 20px;
}
}
#subscription_overlay .subsection-parent {
.input-group {
.subsection-parent .input-group {
input[type="checkbox"] {
margin-top: 0;
}
@@ -1041,8 +1033,8 @@ h4.stream_setting_subsection_title {
.radio-input-parent {
border-bottom: 1px solid hsl(0, 0%, 87%);
margin: 5px 0 5px 5px;
padding: 5px 0;
margin: 2px 0 2px 5px;
padding: 2px 0;
&:last-of-type {
border-bottom: none;
@@ -1062,11 +1054,16 @@ h4.stream_setting_subsection_title {
}
select {
width: auto;
margin-bottom: 0;
}
}
}
select {
/* Match .setting_desktop_icon_count_display */
width: 325px;
height: fit-content;
}
}
.stream-creation-body input[type="radio"] {
@@ -1199,42 +1196,44 @@ h4.stream_setting_subsection_title {
.stream-header .button-group {
margin-top: -5px;
}
}
#subscription_overlay .subscription_settings .stream_change_property_info {
/* For small widths where there is not enough space
to show alert beside stream name we set its display
to block so it is shown in new line. But to avoid
it covering whole screen width we set max-width
so that it does not losses inline-block appearance. */
.stream_change_property_info {
/* For small widths where there is not enough space
to show alert beside stream name we set its display
to block so it is shown in new line. But to avoid
it covering whole screen width we set max-width
so that it does not losses inline-block appearance. */
/* TODO: This will probably be not required once
we have tabbed navigation as button group width
will be smaller. */
display: block;
max-width: max-content;
white-space: nowrap;
/* TODO: This will probably be not required once
we have tabbed navigation as button group width
will be smaller. */
display: block;
max-width: max-content;
white-space: nowrap;
}
}
}
@media (width <= 500px) {
#subscription_overlay .stream_settings_header {
display: block;
text-align: center;
margin-left: 0;
#subscription_overlay {
.stream_settings_header {
display: block;
text-align: center;
margin-left: 0;
.tab-container {
.ind-tab {
width: 85px;
.tab-container {
.ind-tab {
width: 85px;
}
}
}
}
#subscription_overlay .stream_setting_subsection_header {
display: block;
.stream_setting_subsection_header {
display: block;
.stream_permission_change_info {
margin: 12px auto 0 3px;
.stream_permission_change_info {
margin: 12px auto 0 3px;
}
}
}
}

View File

@@ -27,7 +27,7 @@ $idle_color: hsl(29, 84%, 51%);
}
.user_circle_empty {
background-color: none;
background-color: transparent;
border-color: hsl(0, 0%, 50%);
}

View File

@@ -86,7 +86,7 @@ pre {
font-family: "Source Sans 3", sans-serif !important;
/* Affects all tippy tooltips not using any theme. */
.tippy-box:not[data-theme] {
.tippy-box:not([data-theme]) {
background-color: hsl(0, 0%, 12%);
&[data-placement^="top"] {
@@ -734,7 +734,7 @@ strong {
li.actual-dropdown-menu > a:focus {
color: hsl(0, 0%, 100%);
text-decoration: none;
background-color: none;
background-color: transparent;
background-image: none;
filter: none;
outline: 0;
@@ -1372,15 +1372,7 @@ td.pointer {
color: hsl(200, 100%, 40%);
}
.on_hover_topic_edit,
.on_hover_topic_read {
opacity: 0.2;
}
.always_visible_topic_edit {
opacity: 0.7;
}
.always_visible_topic_edit,
.on_hover_topic_unmute {
opacity: 0.7;
@@ -1390,6 +1382,8 @@ td.pointer {
}
}
.on_hover_topic_edit,
.on_hover_topic_read,
.on_hover_topic_unresolve,
.on_hover_topic_resolve,
.on_hover_topic_mute {
@@ -1401,15 +1395,6 @@ td.pointer {
}
}
.on_hover_topic_edit,
.always_visible_topic_edit,
.on_hover_topic_read {
&:hover {
cursor: pointer;
opacity: 1;
}
}
.has_actions_popover .info {
opacity: 1;
visibility: visible;
@@ -2313,12 +2298,7 @@ div.floating_recipient {
color: hsl(0, 0%, 100%);
}
#user-checkboxes-simplebar-wrapper {
max-height: 500px;
overflow-y: auto;
}
#user-checkboxes {
#create_stream_subscribers {
margin-top: 10px;
.checkbox {
@@ -2331,24 +2311,6 @@ div.floating_recipient {
}
}
#stream-checkboxes {
margin-top: 10px;
display: none;
.checkbox {
display: block;
}
input[type="checkbox"] {
margin: 5px 0;
float: none;
}
}
#copy-from-stream-expand-collapse {
cursor: pointer;
}
.sub_button_row {
text-align: center;
}
@@ -3062,7 +3024,7 @@ select.inline_select_topic_edit {
.include-sender .message_controls {
background: none !important;
padding: none !important;
padding: 0 !important;
border: none !important;
}

View File

@@ -1,5 +0,0 @@
<label class="checkbox add-user-label" id="user_checkbox_{{user_id}}">
<input type="checkbox" name="user" {{#if checked}}checked="checked"{{#if disabled}} disabled="disabled"{{/if}}{{/if}} data-user-id="{{user_id}}"/>
<span></span>
{{full_name}} {{#if show_email}}({{email}}){{else}}({{#tr}}User ID: {user_id}; <em>email hidden</em>{{/tr}}){{/if}}
</label>

View File

@@ -3,7 +3,7 @@
{{> recent_topics_filters}}
</div>
<div class="search_group" role="group">
<input type="text" id="recent_topics_search" value="{{ search_val }}" placeholder="{{t 'Filter topics (t)' }}" />
<input type="text" id="recent_topics_search" value="{{ search_val }}" autocomplete="off" placeholder="{{t 'Filter topics (t)' }}" />
<button type="button" class="btn clear_search_button" id="recent_topics_search_clear">
<i class="fa fa-remove" aria-hidden="true"></i>
</button>

View File

@@ -27,6 +27,10 @@
</div>
</div>
</form>
<div class="settings_panel_list_header">
<h3>{{t "Alert words"}}</h3>
</div>
<div class="progressive-table-wrapper" data-simplebar>
<table class="table table-condensed table-striped wrapped-table">
<thead>

View File

@@ -1,6 +1,9 @@
<div id="attachments-settings" class="settings-section" data-name="uploaded-files">
<div id="attachment-stats-holder"></div>
<input id="upload_file_search" class="search" type="text" placeholder="{{t 'Filter uploads' }}" aria-label="{{t 'Filter uploads' }}"/>
<div class="settings_panel_list_header">
<h3>{{t "Uploaded files"}}</h3>
<input id="upload_file_search" class="search" type="text" placeholder="{{t 'Filter uploaded files' }}" aria-label="{{t 'Filter uploads' }}"/>
</div>
<div class="clear-float"></div>
<div class="alert" id="delete-upload-status"></div>
<div class="progressive-table-wrapper" data-simplebar>

View File

@@ -1,9 +1,13 @@
<div id="admin-bot-list" class="settings-section" data-name="bot-list-admin">
<div class="tip bot-settings-tip"></div>
<h3 class="inline-block">{{t "Bots" }}</h3>
<input type="text" class="search" placeholder="{{t 'Filter bots' }}" aria-label="{{t 'Filter bots' }}"/>
<div class="alert-notification" id="bot-field-status"></div>
<div class="clear-float"></div>
<div class="settings_panel_list_header">
<h3>{{t "Bots"}}</h3>
<input type="text" class="search" placeholder="{{t 'Filter bots' }}" aria-label="{{t 'Filter bots' }}"/>
</div>
<div class="progressive-table-wrapper" data-simplebar>
<table class="table table-condensed table-striped wrapped-table">
<thead class="table-sticky-headers">

View File

@@ -1,5 +1,5 @@
<div id="data-exports" class="settings-section" data-name="data-exports-admin">
<h3>{{t "Data exports" }}
<h3>{{t "Export organization" }}
{{> ../help_link_widget link="/help/export-your-organization" }}
</h3>
<p>
@@ -29,8 +29,13 @@
</div>
</form>
{{/if}}
<input type="hidden" class="search" placeholder="{{t 'Filter exports' }}"
aria-label="{{t 'Filter exports' }}"/>
<div class="settings_panel_list_header">
<h3>{{t "Data exports"}}</h3>
<input type="hidden" class="search" placeholder="{{t 'Filter exports' }}"
aria-label="{{t 'Filter exports' }}"/>
</div>
<div class="progressive-table-wrapper" data-simplebar>
<table class="table table-condensed table-striped wrapped-table admin_exports_table">
<thead class="table-sticky-headers">

View File

@@ -1,16 +1,19 @@
<div id="admin-deactivated-users-list" class="settings-section" data-name="deactivated-users-admin">
<h3 class="inline-block">{{t "Deactivated users" }}
{{> ../help_link_widget link="/help/deactivate-or-reactivate-a-user" }}
</h3>
<input type="text" class="search" placeholder="{{t 'Filter deactivated users' }}" aria-label="{{t 'Filter deactivated users' }}"/>
<div class="alert-notification" id="deactivated-user-field-status"></div>
<div class="clear-float"></div>
<div class="settings_panel_list_header">
<h3>{{t "Deactivated users" }}
{{> ../help_link_widget link="/help/deactivate-or-reactivate-a-user" }}
</h3>
<input type="text" class="search" placeholder="{{t 'Filter deactivated users' }}" aria-label="{{t 'Filter deactivated users' }}"/>
</div>
<div class="progressive-table-wrapper" data-simplebar>
<table class="table table-condensed table-striped wrapped-table">
<thead class="table-sticky-headers">
<th class="active" data-sort="alphabetic" data-sort-prop="full_name">{{t "Name" }}</th>
<th data-sort="email">{{t "Email" }}</th>
<th {{#if can_sort_by_email}}data-sort="email"{{/if}}>{{t "Email" }}</th>
<th class="user_id" data-sort="id">{{t "User ID" }}</th>
<th class="user_role" data-sort="role">{{t "Role" }}</th>
{{#if is_admin}}

View File

@@ -21,8 +21,10 @@
</form>
{{/if}}
<input type="text" class="search" placeholder="{{t 'Filter streams' }}" aria-label="{{t 'Filter streams' }}"/>
<div class="clear-float"></div>
<div class="settings_panel_list_header">
<h3>{{t "Default streams"}}</h3>
<input type="text" class="search" placeholder="{{t 'Filter default streams' }}" aria-label="{{t 'Filter streams' }}"/>
</div>
<div class="progressive-table-wrapper" data-simplebar>
<table class="table table-condensed table-striped wrapped-table">

View File

@@ -32,8 +32,11 @@
</div>
</form>
<input type="text" class="search" placeholder="{{t 'Filter emojis' }}"
aria-label="{{t 'Filter emojis' }}"/>
<div class="settings_panel_list_header">
<h3>{{t "Custom emoji"}}</h3>
<input type="text" class="search" placeholder="{{t 'Filter emoji' }}"
aria-label="{{t 'Filter emoji' }}"/>
</div>
<div class="progressive-table-wrapper" data-simplebar>
<table class="table table-condensed table-striped wrapped-table admin_emoji_table">
<thead class="table-sticky-headers">

View File

@@ -1,4 +1,10 @@
<div id ="{{widget}}-upload-widget" class="inline-block image_upload_widget">
<div id="{{widget}}-upload-widget" class="inline-block image_upload_widget">
<div class="image-disabled {{#if is_editable_by_current_user}}hide{{/if}}">
<div class="image-upload-background"></div>
<span class="image-disabled-text flex" aria-label="{{ disabled_text }}" role="button" tabindex="0">
{{ disabled_text }}
</span>
</div>
<div class="image_upload_button {{#unless is_editable_by_current_user}}hide{{/unless}}">
<div class="image-upload-background"></div>
<button class="image-delete-button" aria-label="{{ delete_text }}" role="button" tabindex="0">

View File

@@ -5,12 +5,12 @@
{{#if can_invite_others_to_realm}}
<a class="invite-user-link" href="#invite"><i class="fa fa-user-plus" aria-hidden="true"></i>{{t "Invite more users" }}</a>
{{/if}}
<div>
<h3 class="inline-block">{{t "Invites" }}</h3>
<div class="alert-notification" id="invites-field-status"></div>
<div class="settings_panel_list_header">
<h3>{{t "Invites"}}</h3>
<input type="text" class="search" placeholder="{{t 'Filter invites' }}" aria-label="{{t 'Filter invites' }}"/>
<div class="alert-notification" id="invites-field-status"></div>
</div>
<div class="clear-float"></div>
<div class="progressive-table-wrapper" data-simplebar>
<table class="table table-condensed table-striped">

View File

@@ -65,8 +65,13 @@
</form>
{{/if}}
<input type="text" class="search" placeholder="{{t 'Filter linkifiers' }}" aria-label="{{t 'Filter linkifiers' }}"/>
<div class="alert-notification edit-linkifier-status" id="linkifier-field-status"></div>
<div class="settings_panel_list_header">
<h3>{{t "Linkifiers"}}</h3>
<input type="text" class="search" placeholder="{{t 'Filter linkifiers' }}" aria-label="{{t 'Filter linkifiers' }}"/>
</div>
<div class="progressive-table-wrapper" data-simplebar>
<table class="table table-condensed table-striped wrapped-table admin_linkifiers_table">
<thead class="table-sticky-headers">

View File

@@ -1,5 +1,8 @@
<div id="muted-topic-settings" class="settings-section" data-name="muted-topics">
<input id="muted_topics_search" class="search" type="text" placeholder="{{t 'Filter muted topics' }}" aria-label="{{t 'Filter muted topics' }}"/>
<div class="settings_panel_list_header">
<h3>{{t "Muted topics"}}</h3>
<input id="muted_topics_search" class="search" type="text" placeholder="{{t 'Filter muted topics' }}" aria-label="{{t 'Filter muted topics' }}"/>
</div>
<div class="progressive-table-wrapper" data-simplebar>
<table class="table table-condensed table-striped wrapped-table">
<thead class="table-sticky-headers">

View File

@@ -1,5 +1,8 @@
<div id="muted-user-settings" class="settings-section" data-name="muted-users">
<input id="muted_users_search" class="search" type="text" placeholder="{{t 'Filter muted users' }}" aria-label="{{t 'Filter muted users' }}"/>
<div class="settings_panel_list_header">
<h3>{{t "Muted users"}}</h3>
<input id="muted_users_search" class="search" type="text" placeholder="{{t 'Filter muted users' }}" aria-label="{{t 'Filter muted users' }}"/>
</div>
<div class="progressive-table-wrapper" data-simplebar>
<table class="table table-condensed table-striped wrapped-table">
<thead class="table-sticky-headers">

View File

@@ -50,7 +50,11 @@
</form>
{{/if}}
<input type="text" class="search" placeholder="{{t 'Filter code playgrounds' }}" aria-label="{{t 'Filter code playgrounds' }}"/>
<div class="settings_panel_list_header">
<h3>{{t "Code playgrounds"}}</h3>
<input type="text" class="search" placeholder="{{t 'Filter code playgrounds' }}" aria-label="{{t 'Filter code playgrounds' }}"/>
</div>
<div class="progressive-table-wrapper" data-simplebar>
<table class="table table-condensed table-striped wrapped-table admin_playgrounds_table">
<thead>

View File

@@ -1,6 +1,8 @@
<div id="profile-field-settings" class="settings-section" data-name="profile-field-settings">
<h3 class="inline-block">{{t "Custom profile fields" }}</h3>
<div class="alert-notification" id="admin-profile-field-status"></div>
<div class="settings_panel_list_header">
<h3>{{t "Custom profile fields"}}</h3>
</div>
<div class="admin-table-wrapper">
<table class="table table-condensed table-striped admin_profile_fields_table">
<tbody id="admin_profile_fields_table">

View File

@@ -1,6 +1,6 @@
<div id="profile-settings" class="settings-section show" data-name="profile">
<div class="profile-settings-form">
<div class="inline-block">
<div class="profile-main-panel inline-block">
<h3 class="inline-block hide" id="user-profile-header">{{t "Profile" }}</h3>
<div id="user_details_section">
<form class="form-horizontal full-name-change-form">
@@ -19,14 +19,6 @@
</div>
</form>
<div class="input-group grid">
<span class="title">{{t "Role" }}: {{user_role_text}}</span>
</div>
<div class="input-group grid">
<span class="title">{{t "Joined" }}: {{date_joined_text}}</span>
</div>
<form class="form-horizontal timezone-setting-form">
<div class="input-group grid">
<label for="timezone" class="dropdown-title inline-block">{{t "Time zone" }}</label>
@@ -52,22 +44,30 @@
</button>
</div>
</div>
<div class="inline-block user-avatar-section">
<h3>
{{t "Profile picture" }}
<i class="fa fa-question-circle change_name_tooltip tippy-zulip-tooltip settings-info-icon"
{{#if user_can_change_avatar}}style="display:none"{{/if}}
data-tippy-content="{{t 'Avatar changes are disabled in this organization.' }}">
</i>
</h3>
{{> image_upload_widget
widget = "user-avatar"
upload_text = (t "Upload new profile picture")
delete_text = (t "Delete profile picture")
is_editable_by_current_user = user_can_change_avatar
image = page_params.avatar_url_medium}}
<div id="user-avatar-source">
<a href="https://en.gravatar.com/" target="_blank" rel="noopener noreferrer">{{t "Avatar from Gravatar" }}</a>
<div class="profile-side-panel">
<div class="inline-block user-avatar-section">
{{> image_upload_widget
widget = "user-avatar"
upload_text = (t "Upload new profile picture")
delete_text = (t "Delete profile picture")
disabled_text = (t "Avatar changes are disabled in this organization")
is_editable_by_current_user = user_can_change_avatar
image = page_params.avatar_url_medium}}
<div id="user-avatar-source">
<a href="https://en.gravatar.com/" target="_blank" rel="noopener noreferrer">{{t "Avatar from Gravatar" }}</a>
</div>
</div>
<div class="user-details">
<div class="input-group">
<span class="user-details-title">{{t "Role" }}:</span>
<span class="user-details-desc">{{user_role_text}}</span>
</div>
<div class="input-group">
<span class="user-details-title">{{t "Joined" }}: </span>
<span class="user-details-desc">{{date_joined_text}}</span>
</div>
</div>
</div>
</div>

View File

@@ -1,15 +1,18 @@
<div id="admin-user-list" class="settings-section" data-name="user-list-admin">
<h3 class="inline-block">{{t "Users" }}</h3>
<input type="text" class="search" placeholder="{{t 'Filter users' }}" aria-label="{{t 'Filter users' }}"/>
<div class="alert-notification" id="user-field-status"></div>
<div class="clear-float"></div>
<div class="settings_panel_list_header">
<h3>{{t "Users"}}</h3>
<input type="text" class="search" placeholder="{{t 'Filter users' }}" aria-label="{{t 'Filter users' }}"/>
</div>
<div class="progressive-table-wrapper" data-simplebar>
<table class="table table-condensed table-striped wrapped-table">
<thead class="table-sticky-headers">
<th class="active" data-sort="alphabetic" data-sort-prop="full_name">{{t "Name" }}</th>
<th data-sort="email">{{t "Email" }}</th>
<th {{#if can_sort_by_email}}data-sort="email"{{/if}}>{{t "Email" }}</th>
<th class="user_id" data-sort="id">{{t "User ID" }}</th>
<th class="user_role" data-sort="role">{{t "Role" }}</th>
<th class="last_active" data-sort="last_active">{{t "Last active" }}</th>

View File

@@ -6,7 +6,7 @@
</div>
</div>
<div class="add_subscriber_btn_wrapper inline-block">
<button type="submit" name="add_subscriber" class="button add-subscriber-button small rounded" tabindex="0">
<button type="submit" name="add_subscriber" class="button add-subscriber-button small rounded sea-green" tabindex="0">
{{t 'Add' }}
</button>
</div>

View File

@@ -0,0 +1,14 @@
<tr>
<td>
{{full_name}}{{#if is_current_user}} <span class="my_user_status">{{t "(you)"}}</span>{{/if}}
</td>
{{#if show_email}}
<td class="subscriber-email">{{email}}</td>
{{else}}
<td class="hidden-subscriber-email">{{t "(hidden)"}}</td>
{{/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>
</td>
</tr>

View File

@@ -1,32 +1,27 @@
{{! Client-side Mustache template for rendering users in the stream creation modal.}}
<div id="copy-from-stream-expand-collapse" class="add-user-label">
<i class="toggle fa fa-caret-right" aria-hidden="true"></i>
<span class="control-label">
{{t "Copy from stream" }}
</span>
<div class="subscriber_list_add float-left">
{{> add_subscribers_form}}
</div>
<div id="stream-checkboxes">
{{#each streams}}
<label class="checkbox add-user-label" data-stream-id="{{this.stream_id}}">
<input type="checkbox" name="stream" />
<span></span>
{{this.name}} ( <i class="fa fa-user" aria-hidden="true"></i> {{this.subscriber_count}})
</label>
{{/each}}
</div>
<br />
<input class="add-user-list-filter" name="user_list_filter" type="text"
autocomplete="off" placeholder="{{t 'Filter' }}" />
{{t "Do you want to add everyone?"}}
<button class="add_all_users_to_stream small button rounded sea-green">{{t 'Add all users'}}</button>
<div>
<a draggable="false" class="subs_set_all_users" tabindex="0">{{t "Check all" }}</a> |
<a draggable="false" class="subs_unset_all_users" tabindex="0">{{t "Uncheck all" }}</a>
<div class="create_stream_subscriber_list_header">
<h4 class="stream_setting_subsection_title">Subscribers</h4>
<input class="add-user-list-filter" name="user_list_filter" type="text"
autocomplete="off" placeholder="{{t 'Filter subscribers' }}" />
</div>
<div id="user-checkboxes-simplebar-wrapper" data-simplebar>
<div id="user-checkboxes"></div>
<div class="subscriber-list-box">
<div class="subscriber_list_container" data-simplebar>
<table class="subscriber-list table table-striped">
<thead class="table-sticky-headers">
<th>{{t "Name" }}</th>
<th>{{t "Email" }}</th>
<th>{{t "User ID" }}</th>
<th>{{t "Action" }}</th>
</thead>
<tbody id="create_stream_subscribers" class="subscriber_table"></tbody>
</table>
</div>
</div>

View File

@@ -27,8 +27,8 @@
</div>
</section>
<section class="block">
<label class="stream-title" for="people_to_add">
{{t "People to add" }}
<label for="people_to_add">
<h4 class="stream_setting_subsection_title">{{t "Choose subscribers" }}</h4>
</label>
<div id="stream_subscription_error" class="stream_creation_error"></div>
<div class="controls" id="people_to_add"></div>

View File

@@ -72,7 +72,7 @@
</div>
</div>
<div id="personal-stream-settings" class="personal_settings stream_section collapse {{#sub.subscribed}}in{{/sub.subscribed}}">
<div id="personal-stream-settings" class="personal_settings stream_section">
<div class="subsection-header">
<h3 class="stream_setting_subsection_title inline-block">{{t "Personal settings" }}</h3>
<div id="stream_change_property_status{{sub.stream_id}}" class="stream_change_property_status alert-notification"></div>

View File

@@ -1,8 +1,8 @@
<div class="input-group stream-privacy-values">
<div class="alert stream-privacy-status"></div>
<h4>{{t 'Who can access the stream?'}}
<label>{{t 'Who can access the stream?'}}
{{> ../help_link_widget link="/help/stream-permissions" }}
</h4>
</label>
{{#each stream_privacy_policy_values}}
<div class="radio-input-parent">
<label class="radio">
@@ -25,24 +25,23 @@
{{/if}}
<div class="input-group">
<h4>{{t 'Who can post to the stream?'}}
<label class="dropdown-title">{{t 'Who can post to the stream?'}}
{{> ../help_link_widget link="/help/stream-sending-policy" }}
</h4>
{{#each stream_post_policy_values}}
<div class="radio-input-parent">
<label class="radio">
<input type="radio" name="stream-post-policy" value="{{ this.code }}" {{#if (eq this.code ../stream_post_policy) }}checked{{/if}} />
{{ this.description }}
</label>
</div>
{{/each}}
</label>
<select name="stream-post-policy" class="stream_post_policy_setting prop-element">
{{#each stream_post_policy_values}}
<option value="{{this.code}}" {{#if (eq this.code ../stream_post_policy) }}selected{{/if}}>
{{ this.description}}
</option>
{{/each}}
</select>
</div>
{{#if (or is_owner is_stream_edit)}}
<div>
<h4>{{t "Message retention for stream" }}
<label class="stream-title">{{t "Message retention for stream" }}
{{> ../help_link_widget link="/help/message-retention-policy" }}
</h4>
</label>
{{> ../settings/upgrade_tip_widget}}
@@ -51,7 +50,7 @@
<i class="fa fa-info-circle settings-info-icon stream_message_retention_tooltip tippy-zulip-tooltip" aria-hidden="true" data-tippy-content="{{t 'Only owners can change stream message retention policy.' }}"></i>
</div>
<select name="stream_message_retention_setting"
class="stream_message_retention_setting" class="prop-element"
class="stream_message_retention_setting prop-element"
{{#if disable_message_retention_setting}}disabled{{/if}}>
<option value="realm_default">{{#tr}}Use organization level settings {org_level_message_retention_setting}{{/tr}}</option>
<option value="unlimited">{{t 'Retain forever' }}</option>

View File

@@ -1,9 +1,7 @@
<li class='bottom_left_row {{#if is_active_topic}}active-sub-filter{{/if}} {{#if is_zero}}zero-topic-unreads{{/if}} {{#if is_muted}}muted_topic{{/if}} topic-list-item' data-topic-name='{{topic_name}}'>
<span class='topic-box'>
<span class="sidebar-topic-check">
{{#if resolved}}
{{resolved_topic_prefix}}
{{/if}}
{{topic_resolved_prefix}}
</span>
<a href='{{url}}' class="topic-name" title="{{topic_name}}">
{{topic_display_name}}

View File

@@ -28,10 +28,6 @@
{{/if}}
{{#if user_time}}
<li class="hidden-for-spectators">{{ user_time }} {{#tr}}Local time{{/tr}}</li>
{{/if}}
{{#if is_bot}}
{{#if bot_owner}}
<li>{{#tr}}Owner{{/tr}}:
@@ -49,6 +45,11 @@
{{else}}
<li>{{ user_type }}</li>
{{/if}}
{{#if user_time}}
<li class="hidden-for-spectators">{{ user_time }} {{#tr}}Local time{{/tr}}</li>
{{/if}}
<li class="only-visible-for-spectators">Joined {{date_joined}}</li>
</div>

View File

@@ -24,14 +24,14 @@
<span class="value">{{email}}</span>
</div>
{{/if}}
<div id="date-joined" class="default-field">
<span class="name">{{#tr}}Joined{{/tr}}</span>
<span class="value">{{date_joined}}</span>
</div>
<div id="user-type" class="default-field">
<span class="name">{{#tr}}Role{{/tr}}</span>
<span class="value">{{user_type}}</span>
</div>
<div id="date-joined" class="default-field">
<span class="name">{{#tr}}Joined{{/tr}}</span>
<span class="value">{{date_joined}}</span>
</div>
<span class="value">{{last_seen}}</span>
{{#if user_time}}
<div class="default-field">

View File

@@ -20,6 +20,20 @@ format used by the Zulip server that they are interacting with.
## Changes in Zulip 5.0
**Feature level 120**
* [`GET /messages/{message_id}`](/api/get-message): This endpoint
now sends the full message details. Previously, it only returned
the message's raw Markdown content.
**Feature level 119**
* [`POST /register`](/api/register-queue): The `unread_msgs` section
of the response now prefers `other_user_id` over the poorly named
`sender_id` field in the `pms` dictionaries. This change is
motivated by the possibility that a message you yourself sent to
another user could be marked as unread.
**Feature level 118**
* [`GET /messages`](/api/get-messages), [`GET

View File

@@ -0,0 +1,16 @@
## Use cases
* [Business](/for/business/)
* [Education](/for/education/)
* [Research](/for/research/)
* [Events and conferences](/for/events/)
* [Open source projects](/for/open-source/)
* [Communities](/for/communities/)
## Customer stories
* [iDrift AS company](/case-studies/idrift/)
* [Technical University of Munich](/case-studies/tum/)
* [University of California San Diego](/case-studies/ucsd/)
* [Lean theorem prover community](/case-studies/lean/)
* [Rust language community](/case-studies/rust/)

View File

@@ -9,7 +9,7 @@
* [Add an emoji reaction](/api/add-reaction)
* [Remove an emoji reaction](/api/remove-reaction)
* [Render a message](/api/render-message)
* [Get a message's raw Markdown](/api/get-raw-message)
* [Fetch a single message](/api/get-message)
* [Check if messages match narrow](/api/check-messages-match-narrow)
* [Get a message's edit history](/api/get-message-history)
* [Update personal message flags](/api/update-message-flags)

View File

@@ -158,6 +158,7 @@
## Stream management
* [Stream permissions](/help/stream-permissions)
* [Web-public streams](/help/web-public-streams)
* [Stream posting policy](/help/stream-sending-policy)
* [Restrict stream creation](/help/configure-who-can-create-streams)
* [Restrict stream invitation](/help/configure-who-can-invite-to-streams)

View File

@@ -0,0 +1,12 @@
Administrators may enable the option to create **web-public streams**.
Web-public streams can be viewed by anyone on the Internet without
creating an account in your organization.
For example, you can [link to a Zulip
topic](/help/link-to-a-message-or-conversation) in a web-public stream
from a GitHub issue, a social media post, or a forum thread, and
anyone will be able to click the link and view the discussion in the
Zulip web application without needing to create an account.
Users who wish to post content will need to create an account in order
to do so.

View File

@@ -53,13 +53,11 @@ organization's policy choices.
* [Deactivate bots](/help/deactivate-or-reactivate-a-bot) or
[delete custom emoji](/help/custom-emoji#delete-custom-emoji).
## In the works
## Web-public streams
* **Delete spammer**. This will wipe the user from your Zulip, by deleting
all their messages and reactions, banning them, etc.
* **New users join as guests**. This will allow users joining via open
registration to have extremely limited permissions by default, but still
enough permissions to ask the core team a question or to get a feel for your
community.
* **Public archive**. This will give a read-only view of selected streams,
removing the need in some organizations for having open registration.
{!web-public-streams-intro.md!}
## Related articles
* [Setting up your organization](/help/getting-your-organization-started-with-zulip)
* [Web-public streams](/help/web-public-streams)

View File

@@ -4,7 +4,7 @@ Streams are similar to chat rooms, IRC channels, or email lists in that they
determine who receives a message. Zulip supports a few types of streams:
* **Public** (**#**): Members can join and view the complete message history.
Public streams are visible to Guest users only if they are
Public streams are visible to guest users only if they are
subscribed (exactly like private streams with shared history).
* **Private** (<i class="fa fa-lock"></i>): New subscribers must be
@@ -16,6 +16,11 @@ determine who receives a message. Zulip supports a few types of streams:
* In **private streams with protected history**, new subscribers
can only see messages sent after they join.
* [**Web-public**](/help/web-public-streams) (<i class="zulip-icon
zulip-icon-globe"></i>): Members can join (guests must be invited by a
subscriber). Anyone on the Internet can view complete message history without
creating an account.
## Privacy model for private streams
At a high level:
@@ -74,8 +79,8 @@ administrator can access private stream messages:
<span class="legend_symbol">&#9726;</span><span class="legend_label">If subscribed to the stream</span>
<span class="legend_symbol">&#10038;</span><span class="legend_label">[Configurable](/help/stream-sending-policy). Owners,
Administrators, and Members can, by default, post to any public
stream, and Guests can only post to public streams if they are
administrators, and members can, by default, post to any public
stream, and guests can only post to public streams if they are
subscribed.</span>
### Private streams
@@ -108,3 +113,4 @@ must be subscribed to the stream.</span>
* [Roles and permissions](/help/roles-and-permissions)
* [Stream sending policy](/help/stream-sending-policy)
* [Web-public streams](/help/web-public-streams)

View File

@@ -2,20 +2,10 @@
!!! warn ""
This feature is under development, and is not yet available on Zulip Cloud.
This feature is in beta. Contact [support@zulip.com](mailto:support@zulip.com) to
enable it for your Zulip Cloud organization.
Administrators may enable the option to create **web-public streams**.
Web-public streams can be viewed by anyone on the Internet without
creating an account in your organization.
For example, you can [link to a Zulip
topic](/help/link-to-a-message-or-conversation) in a web-public stream
from a GitHub issue, a social media post, or a forum thread, and
anyone will be able to click the link and view the discussion in the
Zulip web application without needing to create an account.
Users who wish to post content will need to create an account in order
to do so.
{!web-public-streams-intro.md!}
Web-public streams are indicated with a globe (<i class="zulip-icon zulip-icon-globe"></i>) icon.
@@ -176,9 +166,14 @@ with Zulip's Rules of Use.
## Caveats
The web-public visitors feature is not yet integrated with Zulip's
live-update system. As a result, a visitor will not see messages that are sent
while Zulip is open until they reload the browser window.
* Web-public streams do not yet support search engine indexing. You
can use [zulip-archive](https://github.com/zulip/zulip-archive) to
create an archive of a Zulip organization that can be indexed by
search engines.
* The web-public view is not yet integrated with Zulip's live-update
system. As a result, a visitor will not see new messages that are
sent to a topic they are currently viewing without reloading the
browser window.
## Related articles

View File

@@ -0,0 +1,43 @@
{% extends "zerver/portico.html" %}
{% set entrypoint = "landing-page" %}
{% block title %}
<title>Use cases and customer stories</title>
{% endblock %}
{% block customhead %}
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
{% endblock %}
{% block portico_content %}
{% include 'zerver/landing_nav.html' %}
<div class="portico-landing plans why-page no-slide solutions-page for-companies">
<div class="hero bg-companies">
<div class="bg-dimmer"></div>
<h1 class="center">Use cases and customer stories</h1>
<p>Learn how our customers are using Zulip.</p>
<div class="hero-buttons center">
<a href="/new/" class="button">
{{ _('Create organization') }}
</a>
<a href="/plans/" class="button">
{{ _('View pricing') }}
</a>
<a href="/self-hosting/" class="button">
{{ _('Self-host Zulip') }}
</a>
</div>
</div>
<div class="main">
<div class="padded-content">
<div class="inner-content markdown">
{{ render_markdown_path('zerver/for/use-cases.md') }}
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -15,6 +15,7 @@ import argparse
import configparser
import json
import os
import secrets
import sys
import time
import urllib.error
@@ -30,6 +31,7 @@ parser.add_argument(
)
parser.add_argument("--tags", nargs="+", default=[])
parser.add_argument("-f", "--recreate", action="store_true")
parser.add_argument("-s", "--subdomain")
parser.add_argument("-p", "--production", action="store_true")
@@ -109,7 +111,9 @@ def get_ssh_keys_string_from_github_ssh_key_dicts(userkey_dicts: List[Dict[str,
return "\n".join([userkey_dict["key"] for userkey_dict in userkey_dicts])
def generate_dev_droplet_user_data(username: str, userkey_dicts: List[Dict[str, Any]]) -> str:
def generate_dev_droplet_user_data(
username: str, subdomain: str, userkey_dicts: List[Dict[str, Any]]
) -> str:
ssh_keys_string = get_ssh_keys_string_from_github_ssh_key_dicts(userkey_dicts)
setup_root_ssh_keys = f"printf '{ssh_keys_string}' > /root/.ssh/authorized_keys"
setup_zulipdev_ssh_keys = f"printf '{ssh_keys_string}' > /home/zulipdev/.ssh/authorized_keys"
@@ -117,7 +121,7 @@ def generate_dev_droplet_user_data(username: str, userkey_dicts: List[Dict[str,
# We pass the hostname as username.zulipdev.org to the DigitalOcean API.
# But some droplets (eg on 18.04) are created with with hostname set to just username.
# So we fix the hostname using cloud-init.
hostname_setup = f"hostnamectl set-hostname {username}.zulipdev.org"
hostname_setup = f"hostnamectl set-hostname {subdomain}.zulipdev.org"
setup_repo = (
"cd /home/zulipdev/{1} && "
@@ -129,11 +133,19 @@ def generate_dev_droplet_user_data(username: str, userkey_dicts: List[Dict[str,
server_repo_setup = setup_repo.format(username, "zulip")
python_api_repo_setup = setup_repo.format(username, "python-zulip-api")
erlang_cookie = secrets.token_hex(16)
setup_erlang_cookie = (
f"echo '{erlang_cookie}' > /var/lib/rabbitmq/.erlang.cookie && "
"chown rabbitmq:rabbitmq /var/lib/rabbitmq/.erlang.cookie && "
"service rabbitmq-server restart"
)
cloudconf = f"""\
#!/bin/bash
{setup_zulipdev_ssh_keys}
{setup_root_ssh_keys}
{setup_erlang_cookie}
sed -i "s/PasswordAuthentication yes/PasswordAuthentication no/g" /etc/ssh/sshd_config
service ssh restart
{hostname_setup}
@@ -221,10 +233,10 @@ def create_dns_record(my_token: str, record_name: str, ip_address: str) -> None:
domain.create_new_domain_record(type="A", name=wildcard_name, data=ip_address)
def print_dev_droplet_instructions(droplet_domain_name: str) -> None:
def print_dev_droplet_instructions(username: str, droplet_domain_name: str) -> None:
print(
"""
COMPLETE! Droplet for GitHub user {0} is available at {0}.zulipdev.org.
COMPLETE! Droplet for GitHub user {0} is available at {1}.
Instructions for use are below. (copy and paste to the user)
@@ -232,15 +244,15 @@ Instructions for use are below. (copy and paste to the user)
Your remote Zulip dev server has been created!
- Connect to your server by running
`ssh zulipdev@{0}` on the command line
`ssh zulipdev@{1}` on the command line
(Terminal for macOS and Linux, Bash for Git on Windows).
- There is no password; your account is configured to use your SSH keys.
- Once you log in, you should see `(zulip-py3-venv) ~$`.
- To start the dev server, `cd zulip` and then run `./tools/run-dev.py`.
- While the dev server is running, you can see the Zulip server in your browser at
http://{0}:9991.
http://{1}:9991.
""".format(
droplet_domain_name
username, droplet_domain_name
)
)
@@ -291,6 +303,12 @@ def get_zulip_oneclick_app_slug(api_token: str) -> str:
if __name__ == "__main__":
args = parser.parse_args()
username = args.username.lower()
if args.subdomain:
subdomain = args.subdomain.lower()
elif args.production:
subdomain = "{username}-prod"
else:
subdomain = username
if args.production:
print(f"Creating production droplet for GitHub user {username}...")
@@ -303,26 +321,24 @@ if __name__ == "__main__":
assert_github_user_exists(github_username=username)
public_keys = get_ssh_public_keys_from_github(github_username=username)
droplet_domain_name = f"{subdomain}.zulipdev.org"
if args.production:
subdomain = f"{username}-prod"
droplet_domain_name = f"{subdomain}.zulipdev.org"
template_id = get_zulip_oneclick_app_slug(api_token)
user_data = generate_prod_droplet_user_data(username=username, userkey_dicts=public_keys)
else:
assert_user_forked_zulip_server_repo(username=username)
subdomain = username
droplet_domain_name = f"{subdomain}.zulipdev.org"
user_data = generate_dev_droplet_user_data(username=username, userkey_dicts=public_keys)
user_data = generate_dev_droplet_user_data(
username=username, subdomain=subdomain, userkey_dicts=public_keys
)
# define id of image to create new droplets from
# You can get this with something like the following. You may need to try other pages.
# Broken in two to satisfy linter (line too long)
# curl -X GET -H "Content-Type: application/json" -u <API_KEY>: "https://api.digitaloc
# ean.com/v2/images?page=5" | grep --color=always base.zulipdev.org
template_id = "63219191"
template_id = "103231841"
assert_droplet_does_not_exist(
my_token=api_token, droplet_name=droplet_domain_name, recreate=args.recreate
@@ -332,7 +348,7 @@ if __name__ == "__main__":
my_token=api_token,
template_id=template_id,
name=droplet_domain_name,
tags=args.tags,
tags=args.tags + ["dev"],
user_data=user_data,
)
@@ -341,6 +357,6 @@ if __name__ == "__main__":
if args.production:
print_production_droplet_instructions(droplet_domain_name=droplet_domain_name)
else:
print_dev_droplet_instructions(droplet_domain_name=droplet_domain_name)
print_dev_droplet_instructions(username=username, droplet_domain_name=droplet_domain_name)
sys.exit(1)

View File

@@ -57,8 +57,8 @@ run ./tools/test-migrations
# Not running missing bot avatar detection since it's low churn
# ./tools/setup/generate_integration_bots_avatars.py --check-missing
# Not running documentation tests since it takes 20s and only tests documentation
# run ./tools/test-documentation
run ./tools/test-help-documentation $FORCEARG
# run ./tools/test-documentation --skip-external-links
run ./tools/test-help-documentation --skip-external-links $FORCEARG
run ./tools/test-api
# Not running requirements check locally, because slow and low-churn
# run ./tools/test-locked-requirements

View File

@@ -167,6 +167,7 @@ EXEMPT_FILES = make_set(
"static/js/stream_bar.js",
"static/js/stream_color.js",
"static/js/stream_create.js",
"static/js/stream_create_subscribers.js",
"static/js/stream_edit.js",
"static/js/stream_edit_subscribers.js",
"static/js/stream_list.js",

Some files were not shown because too many files have changed in this diff Show More