mirror of
https://github.com/zulip/zulip.git
synced 2025-10-24 00:23:49 +00:00
Compare commits
84 Commits
shared-0.0
...
shared-0.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d583fcec59 | ||
|
|
4301148bee | ||
|
|
eefaa9120f | ||
|
|
ce38eda54d | ||
|
|
82837304ec | ||
|
|
b4675d978f | ||
|
|
6f5ae8d13d | ||
|
|
6496d43148 | ||
|
|
19beed2709 | ||
|
|
bfdc547b00 | ||
|
|
20368a936c | ||
|
|
57f01e0727 | ||
|
|
5999dcd316 | ||
|
|
9b9931df7f | ||
|
|
07352271b9 | ||
|
|
ab5567e8c5 | ||
|
|
5ef8da40a9 | ||
|
|
ee11a68f7a | ||
|
|
a80e470c9e | ||
|
|
68a1912f89 | ||
|
|
ed4ff268a8 | ||
|
|
ea65da462e | ||
|
|
e374d7ef7d | ||
|
|
f68961533a | ||
|
|
6b6a1d53f7 | ||
|
|
89afe55076 | ||
|
|
f9539617ee | ||
|
|
10e6fd04e7 | ||
|
|
5f472b1607 | ||
|
|
1eb1aa70ed | ||
|
|
681414caf1 | ||
|
|
4227f74638 | ||
|
|
2f606ffbd9 | ||
|
|
9b378b0718 | ||
|
|
cccb3b1b32 | ||
|
|
2bf63c1e49 | ||
|
|
d5821858dc | ||
|
|
8e05a9fcf7 | ||
|
|
822c232e37 | ||
|
|
05a548f5a3 | ||
|
|
a8b0762699 | ||
|
|
757cdeefb1 | ||
|
|
2fc7054a09 | ||
|
|
72b10937fc | ||
|
|
5086241361 | ||
|
|
aa9039d83e | ||
|
|
d8c77eafb4 | ||
|
|
25d753889b | ||
|
|
84bdf86246 | ||
|
|
93f3021dfb | ||
|
|
a90d9ef536 | ||
|
|
c2f2863d37 | ||
|
|
19dfd8e6a7 | ||
|
|
c9c980d7b0 | ||
|
|
cbac466658 | ||
|
|
d693a6717c | ||
|
|
7c4293a7d3 | ||
|
|
cb15e0265d | ||
|
|
05af6fd8b4 | ||
|
|
cbca80c846 | ||
|
|
a71fad9d6b | ||
|
|
3264361f63 | ||
|
|
f531f3a27f | ||
|
|
73bc5480f3 | ||
|
|
760cfcc603 | ||
|
|
7852d8e015 | ||
|
|
7bf0fd3fa3 | ||
|
|
624cdb0a14 | ||
|
|
dd1091c59a | ||
|
|
c43d48b22f | ||
|
|
43ee1f7b93 | ||
|
|
df47e4312b | ||
|
|
dec092751c | ||
|
|
9f8d60cd5a | ||
|
|
57b8e43dbb | ||
|
|
5d136887b5 | ||
|
|
92cc771392 | ||
|
|
4b9770e270 | ||
|
|
646e466341 | ||
|
|
5ee5a7e635 | ||
|
|
2644fa9645 | ||
|
|
ba1f804518 | ||
|
|
0c015c7bf3 | ||
|
|
e3237ae7e1 |
84
README.md
84
README.md
@@ -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/)!
|
||||
|
||||
[](https://github.com/zulip/zulip/actions/workflows/zulip-ci.yml?query=branch%3Amain)
|
||||
[](https://codecov.io/gh/zulip/zulip)
|
||||
[][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 Zulip’s 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
|
||||
|
||||
@@ -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"))[
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -6,7 +6,7 @@ maxdepth: 3
|
||||
---
|
||||
|
||||
requirements
|
||||
Installing a production server <install>
|
||||
install
|
||||
troubleshooting
|
||||
management-commands
|
||||
settings
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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].
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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"}));
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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));
|
||||
|
||||
56
frontend_tests/node_tests/resolved_topic.js
Normal file
56
frontend_tests/node_tests/resolved_topic.js
Normal 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]);
|
||||
}
|
||||
});
|
||||
80
frontend_tests/node_tests/stream_create_subscribers_data.js
Normal file
80
frontend_tests/node_tests/stream_create_subscribers_data.js
Normal 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));
|
||||
});
|
||||
@@ -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",
|
||||
});
|
||||
|
||||
@@ -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],
|
||||
},
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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 |
Binary file not shown.
@@ -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") {
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
118
static/js/stream_create_subscribers.js
Normal file
118
static/js/stream_create_subscribers.js
Normal 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]);
|
||||
}
|
||||
55
static/js/stream_create_subscribers_data.js
Normal file
55
static/js/stream_create_subscribers_data.js
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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"}),
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
4
static/shared/.gitignore
vendored
Normal 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
|
||||
45
static/shared/js/resolved_topic.js
Normal file
45
static/shared/js/resolved_topic.js
Normal 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];
|
||||
}
|
||||
11
static/shared/js/resolved_topic.js.flow
Normal file
11
static/shared/js/resolved_topic.js.flow
Normal 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];
|
||||
15
static/shared/js/typeahead.js.flow
Normal file
15
static/shared/js/typeahead.js.flow
Normal 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
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@zulip/shared",
|
||||
"version": "0.0.7",
|
||||
"version": "0.0.9",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"katex": "^0.12.0",
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -331,7 +331,7 @@ ul {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
#user-type {
|
||||
#date-joined {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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%);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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}}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
14
static/templates/stream_settings/new_stream_user.hbs
Normal file
14
static/templates/stream_settings/new_stream_user.hbs
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}}
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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
|
||||
|
||||
16
templates/zerver/for/use-cases.md
Normal file
16
templates/zerver/for/use-cases.md
Normal 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/)
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
12
templates/zerver/help/include/web-public-streams-intro.md
Normal file
12
templates/zerver/help/include/web-public-streams-intro.md
Normal 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.
|
||||
@@ -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)
|
||||
|
||||
@@ -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">◾</span><span class="legend_label">If subscribed to the stream</span>
|
||||
|
||||
<span class="legend_symbol">✶</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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
43
templates/zerver/use-cases.html
Normal file
43
templates/zerver/use-cases.html
Normal 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 %}
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user