Merge branch 'zulip:main' into add-script-color

This commit is contained in:
Rohan Sen 2024-11-06 16:17:25 +05:30 committed by GitHub
commit 653835e95a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
399 changed files with 10680 additions and 7618 deletions

View File

@ -20,6 +20,7 @@ on:
- web/webpack.config.ts
- zerver/worker/queue_processors.py
- zerver/lib/push_notifications.py
- zerver/lib/storage.py
- zerver/decorator.py
- zproject/**
workflow_dispatch:

View File

@ -217,7 +217,7 @@ jobs:
- name: Test locked requirements
if: ${{ matrix.os == 'jammy' }}
run: |
. /srv/zulip-py3-venv/bin/activate && \
source tools/ci/activate-venv
./tools/test-locked-requirements
- name: Upload coverage reports

View File

@ -134,9 +134,14 @@ repository](https://github.com/zulip/zulip/issues?q=is%3Aopen+is%3Aissue+label%3
alone.
You can look through issues tagged with the "help wanted" label, which is used
to indicate the issues that are ready for contributions. Some repositories also
use the "good first issue" label to tag issues that are especially approachable
for new contributors.
to indicate the issues that are open for contributions. You'll be able to claim
unassigned issues, which you can find using the `no:assignee` filter in GitHub.
You can also pick up issues that are assigned but are no longer being worked on.
Some repositories use the "good first issue" label to tag issues that are
especially approachable for new contributors.
Here are some handy links for issues to look through:
- [Server and web app](https://github.com/zulip/zulip/issues?q=is%3Aopen+is%3Aissue+label%3A%22help+wanted%22)
- Mobile apps: no "help wanted" label, but see the
@ -155,21 +160,29 @@ changes to tests).
We recommend the following process for finding an issue to work on:
1. Read the description of an issue tagged with the "help wanted" label and make
sure you understand it.
2. If it seems promising, poke around the product
1. Find an issue tagged with the "help wanted" label that is either unassigned,
or looks to be abandoned.
1. Read the description of the issue and make sure you understand it.
1. If it seems promising, poke around the product
(on [chat.zulip.org](https://chat.zulip.org) or in the development
environment) until you know how the piece being
described fits into the bigger picture. If after some exploration the
description seems confusing or ambiguous, post a question on the GitHub
issue, as others may benefit from the clarification as well.
3. When you find an issue you like, try to get started working on it. See if you
1. When you find an issue you like, try to get started working on it. See if you
can find the part of the code you'll need to modify (`git grep` is your
friend!) and get some idea of how you'll approach the problem.
4. If you feel lost, that's OK! Go through these steps again with another issue.
1. If you feel lost, that's OK! Go through these steps again with another issue.
There's plenty to work on, and the exploration you do will help you learn
more about the project.
An assigned issue can be considered abandoned if:
- There is no recent contributor activity.
- There are no open PRs, or an open PR needs work in order to be ready for
review. For example, a PR may need to be updated to address reviewer feedback
or to pass tests.
Note that you are _not_ claiming an issue while you are iterating through steps
1-4. _Before you claim an issue_, you should be confident that you will be able to
tackle it effectively.
@ -204,9 +217,10 @@ are set up with a GitHub workflow bot called
requests in order to create a better workflow for Zulip contributors.
To claim an issue in these repositories, simply post a comment that says
`@zulipbot claim` to the issue thread. If the issue is tagged with a [help
wanted](https://github.com/zulip/zulip/issues?q=is%3Aopen+is%3Aissue+label%3A%22help+wanted%22)
label, Zulipbot will immediately assign the issue to you.
`@zulipbot claim` to the issue thread. If the issue is [tagged with a help
wanted label and is not assigned to someone
else](https://github.com/zulip/zulip/issues?q=is%3Aopen+is%3Aissue+label%3A%22help+wanted%22+no%3Aassignee),
Zulipbot will immediately assign the issue to you.
Note that new contributors can only claim one issue until their first pull request is
merged. This is to encourage folks to finish ongoing work before starting
@ -270,7 +284,7 @@ labels.
### Common questions
- **What if somebody is already working on the issue I want do claim?** There
- **What if somebody is already working on the issue I want to claim?** There
are lots of issue to work on! If somebody else is actively working on the
issue, you can find a different one, or help with
reviewing their work.

View File

@ -20,6 +20,81 @@ format used by the Zulip server that they are interacting with.
## Changes in Zulip 10.0
**Feature level 319**
* [Markdown message
formatting](/api/message-formatting#links-to-channels-topics-and-messages): Added
new `message-link` format for special direct links to messages.
**Feature level 318**
* [`POST /register`](/api/register-queue): Updated
`realm_incoming_webhook_bots` with a new `config_options` key,
defining which options should be offered when creating URLs for this
integration.
**Feature level 317**
* [`POST /user_groups/create`](/api/create-user-group):
Added `group_id` to the success response of the user group creation
endpoint, enabling clients to easily access the unique identifier
of the newly created user group.
**Feature level 316**
* `PATCH /realm`, [`GET /events`](/api/get-events),
[`POST /register`](/api/register-queue):
Added `can_move_messages_between_topics_group` realm setting which is a
[group-setting value](/api/group-setting-values) describing the set of users
with permission to move messages from one topic to another within a channel
in the organization.
* `PATCH /realm`, [`GET /events`](/api/get-events): Removed
`edit_topic_policy` property, as the permission to move messages between
topics in the organization is now controlled by
`can_move_messages_between_topics_group` setting.
**Feature level 315**
* [POST /register](/api/register-queue), [`GET
/streams/{stream_id}`](/api/get-stream-by-id), [`GET
/events`](/api/get-events), [GET
/users/me/subscriptions](/api/get-subscriptions): The `is_archived`
property has been added to channel and subscription objects.
* [`GET /streams`](/api/get-streams): The new parameter
`exclude_archived` controls whether archived channels should be
returned.
* [`POST /register`](/api/register-queue): The new `archived_channels`
[client
capability](/api/register-queue#parameter-client_capabilities)
allows the client to specify whether it supports archived channels
being present in the response.
**Feature level 314**
* `PATCH /realm`, [`POST /register`](/api/register-queue),
[`GET /events`](/api/get-events): Anonymous groups are now accepted
by `create_multiuse_invite_group` realm setting, which is a now a
[group-setting value](/api/group-setting-values) instead of an
integer ID of the group.
* `PATCH /realm`, [`POST /register`](/api/register-queue),
[`GET /events`](/api/get-events): Anonymous groups are now accepted
by `can_access_all_users_group` realm setting, which is a now a
[group-setting value](/api/group-setting-values) instead of an
integer ID of the group.
**Feature level 313**
* [`PATCH /users/{user_id}`](/api/update-user): Added `new_email` field to
allow updating the email address of the target user. The requester must be
an organization administrator and have the `can_change_user_emails` special
permission.
* [`PATCH /users/{email}`](/api/update-user-by-email): Added new endpoint,
which is a copy of [`PATCH /users/{user_id}`](/api/update-user), but the user
is specified by their email address, following the same rules as [`GET
/users/{email}`](/api/get-user-by-email).
**Feature level 312**
* [`GET /events`](/api/get-events): Added `realm_export_consent` event

View File

@ -66,6 +66,7 @@
* [Get all users](/api/get-users)
* [Create a user](/api/create-user)
* [Update a user](/api/update-user)
* [Update a user by email](/api/update-user-by-email)
* [Deactivate a user](/api/deactivate-user)
* [Deactivate own user](/api/deactivate-own-user)
* [Reactivate a user](/api/reactivate-user)

View File

@ -23,6 +23,42 @@ for syntax highlighting. This field is used in the
mentions][help-global-time] to supported Markdown message formatting
features.
## Links to channels, topics, and messages
Zulip's markup supports special readable Markdown syntax for [linking
to channels, topics, and messages](/help/link-to-a-message-or-conversation).
Sample HTML formats are as follows:
``` html
<!-- Syntax: #**announce** -->
<a class="stream" data-stream-id="9"
href="/#narrow/channel/9-announce">
#announce
</a>
<!-- Syntax: #**announce>Zulip updates** -->
<a class="stream-topic" data-stream-id="9"
href="/#narrow/channel/9-announce/topic/Zulip.20updates">
#announce &gt; Zulip updates
</a>
<!-- Syntax: #**announce>Zulip updates@214** -->
<a class="message-link"
href="/#narrow/channel/9-announce/topic/Zulip.20updates/near/214">
#announce &gt; Zulip updates @ 💬
</a>
```
The older stream/topic elements include a `data-stream-id`, which
historically was used in order to display the current channel name if
the channel had been renamed. That field is **deprecated**, because
displaying an updated value for the most common forms of this syntax
requires parsing the URL to get the topic to use anyway.
**Changes**: In Zulip 10.0 (feature level 319), added Markdown syntax
for linking to a specific message in a conversation. Declared the
`data-stream-id` field to be deprecated as detailed above.
## Image previews
When a Zulip message is sent linking to an uploaded image, Zulip will

View File

@ -54,9 +54,10 @@ from zerver.lib.send_email import (
from zerver.lib.timestamp import datetime_to_timestamp, timestamp_to_datetime
from zerver.lib.url_encoding import append_url_query_string
from zerver.lib.utils import assert_is_not_none
from zerver.models import Realm, RealmAuditLog, UserProfile
from zerver.models import Realm, RealmAuditLog, Stream, UserProfile
from zerver.models.realm_audit_logs import AuditLogEventType
from zerver.models.realms import get_org_type_display_name, get_realm
from zerver.models.streams import get_stream
from zerver.models.users import get_system_bot
from zilencer.lib.remote_counts import MissingDataError
from zilencer.models import (
@ -1093,7 +1094,7 @@ class BillingSession(ABC):
metadata=stripe_customer_data.metadata,
)
event_time = timestamp_to_datetime(stripe_customer.created)
with transaction.atomic():
with transaction.atomic(durable=True):
self.write_to_audit_log(BillingSessionEventType.STRIPE_CUSTOMER_CREATED, event_time)
customer = self.update_or_create_customer(stripe_customer.id)
return customer
@ -1792,7 +1793,7 @@ class BillingSession(ABC):
# TODO: The correctness of this relies on user creation, deactivation, etc being
# in a transaction.atomic() with the relevant RealmAuditLog entries
with transaction.atomic():
with transaction.atomic(durable=True):
# We get the current license count here in case the number of billable
# licenses has changed since the upgrade process began.
current_licenses_count = self.get_billable_licenses_for_customer(
@ -2197,7 +2198,7 @@ class BillingSession(ABC):
# event_time should roughly be timezone_now(). Not designed to handle
# event_times in the past or future
@transaction.atomic
@transaction.atomic(savepoint=False)
def make_end_of_cycle_updates_if_needed(
self, plan: CustomerPlan, event_time: datetime
) -> tuple[CustomerPlan | None, LicenseLedger | None]:
@ -2911,7 +2912,7 @@ class BillingSession(ABC):
if status is not None:
if status == CustomerPlan.ACTIVE:
assert plan.status < CustomerPlan.LIVE_STATUS_THRESHOLD
with transaction.atomic():
with transaction.atomic(durable=True):
# Switch to a different plan was cancelled. We end the next plan
# and set the current one as active.
if plan.status == CustomerPlan.SWITCH_PLAN_TIER_AT_PLAN_END:
@ -3411,7 +3412,7 @@ class BillingSession(ABC):
raise BillingError("Form validation error", message=message)
request_context = self.get_sponsorship_request_session_specific_context()
with transaction.atomic():
with transaction.atomic(durable=True):
# Ensures customer is created first before updating sponsorship status.
self.update_customer_sponsorship_status(True)
sponsorship_request = ZulipSponsorshipRequest(
@ -3843,6 +3844,31 @@ class BillingSession(ABC):
return last_ledger
def send_support_admin_realm_internal_message(
self, channel_name: str, topic: str, message: str
) -> None:
from zerver.actions.message_send import (
internal_send_private_message,
internal_send_stream_message,
)
admin_realm = get_realm(settings.SYSTEM_BOT_REALM)
sender = get_system_bot(settings.NOTIFICATION_BOT, admin_realm.id)
try:
channel = get_stream(channel_name, admin_realm)
internal_send_stream_message(
sender,
channel,
topic,
message,
)
except Stream.DoesNotExist: # nocoverage
direct_message = (
f":red_circle: Channel named '{channel_name}' doesn't exist.\n\n{topic}:\n{message}"
)
for user in admin_realm.get_human_admin_users():
internal_send_private_message(sender, user, direct_message)
class RealmBillingSession(BillingSession):
def __init__(
@ -4006,6 +4032,7 @@ class RealmBillingSession(BillingSession):
return customer
@override
@transaction.atomic(savepoint=False)
def do_change_plan_type(
self, *, tier: int | None, is_sponsored: bool = False, background_update: bool = False
) -> None:
@ -4211,6 +4238,14 @@ class RealmBillingSession(BillingSession):
# anything weird.
pass
def send_realm_created_internal_admin_message(self) -> None:
channel = "signups"
topic = "new organizations"
support_url = self.support_url()
organization_type = get_org_type_display_name(self.realm.org_type)
message = f"[{self.realm.name}]({support_url}) ([{self.realm.display_subdomain}]({self.realm.url})). Organization type: {organization_type}"
self.send_support_admin_realm_internal_message(channel, topic, message)
class RemoteRealmBillingSession(BillingSession):
def __init__(
@ -4404,7 +4439,7 @@ class RemoteRealmBillingSession(BillingSession):
return customer
@override
@transaction.atomic
@transaction.atomic(savepoint=False)
def do_change_plan_type(
self, *, tier: int | None, is_sponsored: bool = False, background_update: bool = False
) -> None: # nocoverage
@ -4505,7 +4540,7 @@ class RemoteRealmBillingSession(BillingSession):
def process_downgrade(
self, plan: CustomerPlan, background_update: bool = False
) -> None: # nocoverage
with transaction.atomic():
with transaction.atomic(savepoint=False):
old_plan_type = self.remote_realm.plan_type
new_plan_type = RemoteRealm.PLAN_TYPE_SELF_MANAGED
self.remote_realm.plan_type = new_plan_type
@ -4841,7 +4876,7 @@ class RemoteServerBillingSession(BillingSession):
return customer
@override
@transaction.atomic
@transaction.atomic(savepoint=False)
def do_change_plan_type(
self, *, tier: int | None, is_sponsored: bool = False, background_update: bool = False
) -> None:
@ -4932,7 +4967,7 @@ class RemoteServerBillingSession(BillingSession):
def process_downgrade(
self, plan: CustomerPlan, background_update: bool = False
) -> None: # nocoverage
with transaction.atomic():
with transaction.atomic(savepoint=False):
old_plan_type = self.remote_server.plan_type
new_plan_type = RemoteZulipServer.PLAN_TYPE_SELF_MANAGED
self.remote_server.plan_type = new_plan_type
@ -5231,7 +5266,7 @@ def ensure_customer_does_not_have_active_plan(customer: Customer) -> None:
raise UpgradeWithExistingPlanError
@transaction.atomic
@transaction.atomic(durable=True)
def do_reactivate_remote_server(remote_server: RemoteZulipServer) -> None:
"""
Utility function for reactivating deactivated registrations.
@ -5253,7 +5288,7 @@ def do_reactivate_remote_server(remote_server: RemoteZulipServer) -> None:
)
@transaction.atomic
@transaction.atomic(durable=True)
def do_deactivate_remote_server(
remote_server: RemoteZulipServer, billing_session: RemoteServerBillingSession
) -> None:

View File

@ -1,11 +1,8 @@
from dataclasses import dataclass
from datetime import datetime, timedelta
from typing import Optional, TypedDict, Union
from urllib.parse import urlencode, urljoin, urlunsplit
from django.conf import settings
from django.db.models import Sum
from django.urls import reverse
from django.utils.timezone import now as timezone_now
from corporate.lib.stripe import (
@ -30,7 +27,7 @@ from corporate.models import (
)
from zerver.models import Realm
from zerver.models.realm_audit_logs import AuditLogEventType
from zerver.models.realms import get_org_type_display_name, get_realm
from zerver.models.realms import get_org_type_display_name
from zilencer.lib.remote_counts import MissingDataError
from zilencer.models import (
RemoteCustomerUserCount,
@ -130,15 +127,6 @@ def get_stripe_customer_url(stripe_id: str) -> str:
return f"https://dashboard.stripe.com/customers/{stripe_id}" # nocoverage
def get_realm_support_url(realm: Realm) -> str:
support_realm_url = get_realm(settings.STAFF_SUBDOMAIN).url
support_url = urljoin(
support_realm_url,
urlunsplit(("", "", reverse("support"), urlencode({"q": realm.string_id}), "")),
)
return support_url
def get_realm_user_data(realm: Realm) -> UserData:
non_guests = get_non_guest_user_count(realm)
guests = get_guest_user_count(realm)

View File

@ -29,20 +29,20 @@ def get_remote_server_activity(request: HttpRequest) -> HttpResponse:
query = SQL(
"""
with mobile_push_forwarded_count as (
with remote_server_push_forwarded_count as (
select
server_id,
sum(coalesce(value, 0)) as push_forwarded_count
sum(coalesce(value, 0)) as server_push_forwarded_count
from zilencer_remoteinstallationcount
where
property = 'mobile_pushes_forwarded::day'
and end_time >= current_timestamp(0) - interval '7 days'
group by server_id
),
remote_push_devices as (
remote_server_push_devices as (
select
server_id,
count(distinct(user_id, user_uuid)) as push_user_count
count(distinct(user_id, user_uuid)) as server_push_user_count
from zilencer_remotepushdevicetoken
group by server_id
),
@ -68,6 +68,23 @@ def get_remote_server_activity(request: HttpRequest) -> HttpResponse:
is_system_bot_realm = False
and realm_deactivated = False
group by server_id, id, name, org_type
),
remote_realm_push_devices as (
select
remote_realm_id,
count(distinct(user_id, user_uuid)) as realm_push_user_count
from zilencer_remotepushdevicetoken
group by remote_realm_id
),
remote_realm_push_forwarded_count as (
select
remote_realm_id,
sum(coalesce(value, 0)) as realm_push_forwarded_count
from zilencer_remoterealmcount
where
property = 'mobile_pushes_forwarded::day'
and end_time >= current_timestamp(0) - interval '7 days'
group by remote_realm_id
)
select
rserver.id,
@ -80,16 +97,20 @@ def get_remote_server_activity(request: HttpRequest) -> HttpResponse:
rserver.contact_email,
rserver.last_version,
rserver.last_audit_log_update,
push_user_count,
push_forwarded_count,
server_push_user_count,
realm_push_user_count,
server_push_forwarded_count,
realm_push_forwarded_count,
realm_type
from zilencer_remotezulipserver rserver
left join mobile_push_forwarded_count on mobile_push_forwarded_count.server_id = rserver.id
left join remote_push_devices on remote_push_devices.server_id = rserver.id
left join remote_server_push_forwarded_count on remote_server_push_forwarded_count.server_id = rserver.id
left join remote_server_push_devices on remote_server_push_devices.server_id = rserver.id
left join remote_realms on remote_realms.server_id = rserver.id
left join remote_server_audit_log on remote_server_audit_log.server_id = rserver.id
left join remote_realm_push_devices on remote_realm_push_devices.remote_realm_id = realm_id
left join remote_realm_push_forwarded_count on remote_realm_push_forwarded_count.remote_realm_id = realm_id
where not deactivated
order by push_user_count DESC NULLS LAST
order by server_push_user_count DESC NULLS LAST
"""
)
@ -102,8 +123,8 @@ def get_remote_server_activity(request: HttpRequest) -> HttpResponse:
"Server contact email",
"Server Zulip version",
"Server last audit log update (UTC)",
"Server mobile users",
"Server mobile pushes",
"Mobile users",
"Mobile pushes",
"Realm organization type",
"Plan name",
"Plan status",
@ -120,6 +141,10 @@ def get_remote_server_activity(request: HttpRequest) -> HttpResponse:
REALM_CREATED = 2
SERVER_HOST = 3
REALM_HOST = 4
SERVER_PUSH_USER_COUNT = 9
REALM_PUSH_USER_COUNT = 10
SERVER_PUSH_FORWARDED = 11
REALM_PUSH_FORWARDED = 12
# Column constants:
DATE_CREATED = 2
@ -153,12 +178,16 @@ def get_remote_server_activity(request: HttpRequest) -> HttpResponse:
ids_string = f"{server_id}"
row.insert(SERVER_AND_REALM_IDS, ids_string)
# Set date created data
# For remote realm row, remove server created value;
# for remote server row, remove realm created value
# Remove extra mobile user/push data and set created date
# For remote realm row, remove server push data and created date;
# for remote server row, remove realm push data and created date.
if realm_id is not None:
row.pop(SERVER_PUSH_FORWARDED)
row.pop(SERVER_PUSH_USER_COUNT)
row.pop(SERVER_CREATED)
else:
row.pop(REALM_PUSH_FORWARDED)
row.pop(REALM_PUSH_USER_COUNT)
row.pop(REALM_CREATED)
# Get server_host for support link

View File

@ -98,7 +98,7 @@ class DemoRequestForm(forms.Form):
@zulip_login_required
@typed_endpoint_without_parameters
def support_request(request: HttpRequest) -> HttpResponse:
from corporate.lib.support import get_realm_support_url
from corporate.lib.stripe import build_support_url
user = request.user
assert user.is_authenticated
@ -119,7 +119,7 @@ def support_request(request: HttpRequest) -> HttpResponse:
"realm_string_id": user.realm.string_id,
"request_subject": form.cleaned_data["request_subject"],
"request_message": form.cleaned_data["request_message"],
"support_url": get_realm_support_url(user.realm),
"support_url": build_support_url("support", user.realm.string_id),
"user_role": user.get_role_name(),
}
# Sent to the server's support team, so this email is not user-facing.

View File

@ -1,9 +1,9 @@
# Continuing unfinished work
Sometimes, work on an issue is started, but not brought to completion. This may
happen for a variety of reasons — the contributor working on the project gets
busy, maintainers cannot prioritize reviewing the work, a contributor doesn't
have the skills required to complete the project, there is
Sometimes, work is started on an issue or PR, but not brought to completion.
This may happen for a variety of reasons — the contributor working on the
project gets busy, maintainers cannot prioritize reviewing the work, a
contributor doesn't have the skills required to complete the project, there is
an unexpected technical challenge or blocker, etc.
Completing work that someone else has started is a great way to contribute! Here
@ -12,18 +12,21 @@ are the steps required:
1. [Find work to be completed.](#find-work-to-be-completed)
1. [Review existing work and feedback.](#review-existing-work-and-feedback)
1. [Decide how to use prior work.](#decide-how-to-use-prior-work)
1. [Credit prior work in your commit history.](#credit-prior-work-in-your-commit-history)
1. [Present your pull request.](#present-your-pull-request)
## Find work to be completed
In Zulip's server and web app [repository](https://github.com/zulip/zulip), pull
requests that have significant work towards something valuable are often tagged
with a [completion candidate label][completion-candidate] label. You can review
this label for issues that you find interesting and have the skills to complete.
with a [completion candidate][completion-candidate] label. You can review
this label for unfinished work that you find interesting and have the skills to
complete.
In general, it's common to see one or more pull request linked to an issue
Note that it's common to see one or more pull requests linked to an issue
you're interested in. The guidelines below apply regardless of whether you
intentionally set out to find work to complete.
intentionally set out to find work to complete or simply find yourself
building on someone else's work.
## Review existing work and feedback
@ -45,6 +48,7 @@ If prior work looks like a good start:
1. Carefully address any open feedback from reviewers.
1. Make any other changes you think are needed, including completing any parts
of the work that had not been finished.
1. Make sure the work of others is [properly credited](#credit-prior-work-in-your-commit-history).
1. [Self-review](../contributing/code-reviewing.md), test, and revise the work,
including potentially [splitting out](../contributing/commit-discipline.md)
preparatory commits to make it easier to read. You should be proud of the
@ -57,6 +61,61 @@ Otherwise, you can:
1. Go through reviewer feedback on prior work. Would any of it apply to the
changes you're proposing? Be sure to address it if so.
## Credit prior work in your commit history
When you use or build upon someone else's unmerged work, it is both
professionally and ethically necessary to [properly
credit][coauthor-git-guide] their contributions in the commit history
of work that you submit. Git, used properly, does a good job of
preserving the original authorship of commits.
However, it's normal to find yourself making changes to commits
originally authored by other contributors, whether resolving merge
conflicts when doing `git rebase` or fixing bugs to create an
atomically correct commit compliant with Zulip's [commit
guidelines](../contributing/commit-discipline.md).
When you do that, it's your responsibility to ensure the resulting
commit series correctly credits the work of everyone who materially
contributed to it. The most direct way to credit the work of someone
beyond the commit's author maintained in the Git metadata is
`Co-authored-by:` line after a blank line at the end of your commit
message:
Co-authored-by: Greg Price <greg@zulip.com>
Be careful to type it precisely, because software parses these commit
message records in generating statistics. You should add such a line
in two scenarios:
- If your own work was squashed into a commit originally authored by
another contributor, add such a line crediting yourself.
- If you used another contributor's work in generating your own
commit, add such a line crediting the other contributor(s).
Sometimes, you make a mistake when rebasing and accidentally squash
commits in a way that messes up Git's authorship records. Often,
undoing the rebase change via `git reflog` is the best way to correct
such mistakes, but there are two other Git commands that can be used
to correct Git's primary authorship information after the fact:
- `git commit --amend --reset-author` will replace the Git commit
metadata (date, author, etc.) of the currently checked out commit
with yourself. This is useful to correct a commit that incorrectly
shows someone else as the author of your work.
- `git commit --amend -C <commit_id>` will replace the commit metadata
(date, author, etc.) on a commit with that of the provided commit
ID. This is useful if you accidentally made someone else's commit
show yourself as the author, or lost a useful commit message via
accidental squashing. (You can usually find the right commit ID to
use with `git reflog` or from GitHub).
As an aside, maintainers who modify commits before merging them are
credited via Git's "Committer" records (visible with `git show
--pretty=fuller`, for example). As a result, they may not bother with
adding a separate `Co-authored-by` record on commits that they revise
as part of merging a pull request.
## Present your pull request
In addition to the usual [guidance](../contributing/reviewable-prs.md) for
@ -71,12 +130,9 @@ putting together your pull request, there are a few key points to keep in mind.
- Test the work carefully, even if others have tested it before. There may be
problems that the reviewers missed, or that were introduced by rebasing across other changes.
- **Give credit where credit is due.** In the commit message for any commits
that use somebody else's work, [credit][coauthor-git-guide] co-authors by
adding a `Co-authored-by:` line after a blank line at the end of your commit
message:
Co-authored-by: Greg Price <greg@zulip.com>
- **Give credit where credit is due.** Reviewers should be able to examine your
commit history and see that you have [properly credited](#credit-prior-work-in-your-commit-history)
the work of others.
- **Explain the relationship between your PR and prior work** in the description
for your pull request.

View File

@ -21,6 +21,10 @@ _Unreleased_
synchronizing role, and otherwise functions like the old one, except Zulip
custom profile fields are referred to with the prefix `custom__`. See the updated
comment documentation in `/etc/zulip/settings.py` for details.
- PostgreSQL 12 is no longer supported; if you are currently using it,
you will need to [upgrade
PostgreSQL](../production/upgrade.md#upgrading-postgresql) before
upgrading Zulip.
## Zulip Server 9.x series

View File

@ -68,7 +68,7 @@ as well as those mentioned in the
[install](install.md#installer-options) documentation:
- `--postgresql-version`: Sets the version of PostgreSQL that will be
installed. We currently support PostgreSQL 12, 13, 14, 15, and 16, with 16
installed. We currently support PostgreSQL 13, 14, 15, and 16, with 16
being the default.
- `--postgresql-database-name=exampledbname`: With this option, you

View File

@ -75,7 +75,7 @@ $ sudo service puppet stop
### PostgreSQL
Zulip expects to install PostgreSQL 12, and find that listening on
Zulip expects to install PostgreSQL 16, and find that listening on
port 5432; any other version of PostgreSQL that is detected at install
time will cause the install to abort. If you already have PostgreSQL
installed, you can pass `--postgresql-version=` to the installer to

View File

@ -7,3 +7,4 @@
| 7.x | 12, 13, 14, 15 |
| 8.x | 12, 13, 14, 15, 16 |
| 9.x | 12, 13, 14, 15, 16 |
| 10.x (unreleased) | 13, 14, 15, 16 |

View File

@ -3,3 +3,37 @@
pointer-events: none;
cursor: default;
}
.sl-markdown-content {
img {
vertical-align: top;
box-shadow: 0 0 4px hsl(0deg 0% 0% / 5%);
border: 1px solid hsl(0deg 0% 87%);
border-radius: 4px;
margin-top: 0;
&.emoji-small {
display: inline-block;
width: 1.25em;
box-shadow: none;
border: none;
vertical-align: text-top;
}
&.emoji-big {
display: inline-block;
width: 1.5em;
box-shadow: none;
border: none;
vertical-align: text-top;
}
&.help-center-icon {
display: inline-block;
width: 1.25em;
box-shadow: none;
border: none;
vertical-align: text-top;
}
}
}

View File

@ -2,19 +2,29 @@
{!admin-only.md!}
Archiving a channel will immediately unsubscribe all users from the channel,
remove the channel from search and other typeaheads, and remove the channel's
messages from **Combined feed**.
You can archive channels you no longer plan to use. Archiving a channel:
Archiving a channel does not delete a channel's messages. Users will still be
able to find any given message by searching for it. However, links to
messages and topics in the channel may or may not continue to work.
- Removes it from the left sidebar for all users.
- Prevents new messages from being sent to the channel.
- Prevents messages in the channel from being edited, deleted, or moved.
In most cases, we recommend [renaming channels](/help/rename-a-channel) rather
than archiving them.
Archiving a channel does not remove subscribers, or change who can access it.
Messages in archived channels still appear in [search
results](/help/search-for-messages), the [combined feed](/help/combined-feed),
and [recent conversations](/help/recent-conversations).
To hide the content in a channel, you can make it
[private](/help/change-the-privacy-of-a-channel) and [unsubscribe
users](/help/manage-user-channel-subscriptions) from it prior to archiving.
## Archive a channel
!!! warn ""
Channels can be [unarchived](#unarchiving-archived-channels) only by
[contacting support](/help/contact-support) for organizations hosted
on Zulip Cloud, or by your self-hosted server's administrator.
{start_tabs}
{tab|desktop-web}
@ -23,8 +33,8 @@ than archiving them.
1. Select a channel.
1. Click the **trash** <i class="fa fa-trash-o"></i> icon near the top right
corner of the channel settings panel.
1. Click the **archive** (<i class="zulip-icon zulip-icon-archive"></i>) icon
in the upper right corner of the channel settings panel.
1. Approve by clicking **Confirm**.
@ -32,22 +42,19 @@ than archiving them.
You can also hover over a channel in the left sidebar, click on the
**ellipsis** (<i class="zulip-icon zulip-icon-more-vertical"></i>), and
select **Channel settings** to access the **trash**
<i class="fa fa-trash-o"></i> icon.
select **Channel settings** to access settings for the channel.
{end_tabs}
!!! warn ""
Archiving a channel is currently irreversible via the UI.
## Unarchiving archived channels
Zulip Cloud organizations that need to unarchive a channel can [contact Zulip
support](/help/contact-support).
If you are self-hosting, you can unarchive an archived channel using the
`unarchive_channel` [management command][management-command]. This will restore
it as a private channel with shared history, and subscribe all organization
owners to it. If you are using Zulip Cloud, you can [contact us](/help/contact-support)
for help.
owners to it.
[management-command]:
https://zulip.readthedocs.io/en/latest/production/management-commands.html#other-useful-manage-py-commands
@ -59,4 +66,3 @@ https://zulip.readthedocs.io/en/latest/production/management-commands.html#other
* [Delete a topic](/help/delete-a-topic)
* [Message retention policy](/help/message-retention-policy)
* [Channel permissions](/help/channel-permissions)
* [Zulip Cloud or self-hosting?](/help/zulip-cloud-or-self-hosting)

View File

@ -1,19 +1,7 @@
# Configure emoticon translations
You can configure whether emoticons like `:)` or `:(` will be automatically
translated into emoji equivalents like
<img
src="/static/generated/emoji/images-google-64/1f642.png"
alt="smile"
class="emoji-small"
/>
or
<img
src="/static/generated/emoji/images-google-64/1f641.png"
alt="slight_frown"
class="emoji-small"
/>
by Zulip.
translated into emoji equivalents like 🙂 or 🙁 by Zulip.
## Configure emoticon translations

View File

@ -13,8 +13,8 @@ on how to use these views.
You can configure which view is set as your home view, and whether
the <kbd>Esc</kbd> key navigates to the home view. Also, you can
always reach the home view by using the <kbd>Ctrl</kbd> + <kbd>[</kbd>
shortcut.
always reach the home view by using the
<kbd data-mac-key="Ctrl">Ctrl</kbd> + <kbd>[</kbd> shortcut.
## Change home view
@ -44,7 +44,8 @@ organization settings:
1. To see your changes in action, open a new Zulip tab, or use a keyboard
shortcut twice to exit the settings and navigate to your home view
(<kbd>Ctrl</kbd> + <kbd>[</kbd> or <kbd>Esc</kbd> if enabled).
(<kbd data-mac-key="Ctrl">Ctrl</kbd> + <kbd>[</kbd> or <kbd>Esc</kbd>
if enabled).
!!! tip ""
@ -62,8 +63,8 @@ designed to enhance the user experience in the app.
By default, the <kbd>Esc</kbd> key shortcut will ultimately navigate to
your home view. You can disable this key binding if you would prefer.
This will not disable other <kbd>Esc</kbd> key shortcuts used in Zulip,
and will not affect the behavior of the <kbd>Ctrl</kbd> + <kbd>[</kbd>
shortcut.
and will not affect the behavior of the
<kbd data-mac-key="Ctrl">Ctrl</kbd> + <kbd>[</kbd> shortcut.
### Toggle whether <kbd>Esc</kbd> navigates to the home view

View File

@ -2,6 +2,13 @@
{!user-groups-intro.md!}
Many organizations find it helpful to create groups for:
- Each team, e.g., “mobile”, “design”, or “IT”.
- Leadership roles, e.g., “managers”, “engineering-leads”.
{!user-groups-applications.md!}
## How to create a user group
{!how-to-create-a-user-group.md!}

View File

@ -47,13 +47,7 @@
### Use an emoticon
You can configure Zulip to convert emoticons into emoji, so that, e.g., `:)`
will be displayed as
<img
src="/static/generated/emoji/images-google-64/1f642.png"
alt="smile"
class="emoji-small"
/>
.
will be displayed as 🙂 .
{start_tabs}

View File

@ -32,9 +32,7 @@ message.
!!! keyboard_tip ""
You can react to the selected message with <img alt=":thumbs_up:"
class="emoji" src="/static/generated/emoji/images/emoji/unicode/1f44d.png"
title="thumbs up"/> by using the <kbd>+</kbd> shortcut.
You can react to the selected message with 👍 by using the <kbd>+</kbd> shortcut.
{tab|mobile}

View File

@ -1,30 +1,36 @@
{start_tabs}
{tab|desktop-web}
{tab|via-left-sidebar}
1. Click the **Start new conversation** button at the bottom of the app, or
use the <kbd>C</kbd> keyboard shortcut.
1. Click the **plus** (<i class="zulip-icon zulip-icon-square-plus"></i>) button next
to the name of the channel where you'd like to start a conversation.
1. _(optional)_ You can change the destination channel for your message using
the dropdown in the top left of the compose box. You can start typing to
filter channels.
1. Enter a topic name. Auto-complete will provide suggestions for previously
used topics.
1. Enter a topic name. Think about finishing the sentence: “Hey, can we chat about… ?”
{!compose-and-send-message.md!}
!!! tip ""
!!! keyboard_tip ""
You can click on the
**Clear topic** (<i class="zulip-icon zulip-icon-close"></i>) icon
near the upper right of the compose box to erase the topic name.
You can also use the <kbd>C</kbd> keyboard shortcut to start a new topic in
the channel you're viewing.
!!! warn ""
{tab|via-compose-box}
In Zulip, you can compose a message to a different place than the one you
are viewing. In this situation, the message feed will fade to indicate
what's going on.
1. Click the **Start new conversation** button at the bottom of the app.
1. _(optional)_ You can change the destination channel for your message using
the dropdown in the top left of the compose box. Start typing to filter
channels.
1. Enter a topic name. Think about finishing the sentence: “Hey, can we chat
about… ?”
{!compose-and-send-message.md!}
!!! keyboard_tip ""
You can also use the <kbd>C</kbd> keyboard shortcut to start a new topic in
the channel you're viewing.
{tab|mobile}

View File

@ -4,7 +4,8 @@
Named link: [Zulip homepage](zulip.com)
A URL (links automatically): zulip.com
Channel link: #**channel name**
Channel and topic link: #**channel name>topic name**
Topic link: #**channel name>topic name**
Message link: #**channel name>topic name@123**
Custom linkifier: For example, #2468 can automatically link to an issue in your tracker.
```

View File

@ -2,7 +2,6 @@ In Zulip, you can insert a named link using Markdown formatting. In addition, Zu
automatically creates links for you when you enter:
- A URL
- An appropriately formatted channel name, or a channel name followed by a topic
(see also [Link to a message or
conversation](/help/link-to-a-message-or-conversation))
- A reference to a channel, topic, or specific message (see also [Link to a
message or conversation](/help/link-to-a-message-or-conversation))
- Text that matches a [custom linkifier](/help/add-a-custom-linkifier) set up by your organization

View File

@ -1,5 +1,7 @@
Review the settings for your organization to set everything up how you
want it to be.
Review the settings for your organization to set everything up how you want it
to be.
{!user-groups-intro.md!}
{start_tabs}

View File

@ -2,8 +2,9 @@
{tab|desktop-web}
1. Click the **New direct message** button at the bottom of the app, or the
**Start new conversation** button if you are in a direct message view.
1. Click the **plus** (<i class="zulip-icon zulip-icon-square-plus"></i>) button next
to **DIRECT MESSAGES** in the left sidebar, or the **New direct message**
button at the bottom of the app.
1. Start typing the name of the person or [group](/help/user-groups) you want to
message, and select their name from the list of suggestions. You can continue

View File

@ -23,11 +23,11 @@
## Setting up your organization
* [Migrating from other chat tools](/help/migrating-from-other-chat-tools)
* [Create your organization profile](/help/create-your-organization-profile)
* [Create user groups](/help/create-user-groups)
* [Customize organization settings](/help/customize-organization-settings)
* [Create channels](/help/create-channels)
* [Customize settings for new users](/help/customize-settings-for-new-users)
* [Invite users to join](/help/invite-users-to-join)
* [Create user groups](/help/create-user-groups)
* [Set up integrations](/help/set-up-integrations)
## Account basics
@ -168,7 +168,6 @@
# Zulip administration
## Organization basics
* [Review your organization's settings](/help/review-your-organization-settings)
* [Organization type](/help/organization-type)
* [Communities directory](/help/communities-directory)
* [Import from Mattermost](/help/import-from-mattermost)

View File

@ -0,0 +1,9 @@
Groups provide an easy way to refer to multiple users at once. You can:
- [Mention](/help/mention-a-user-or-group) a group of users,
[notifying](/help/dm-mention-alert-notifications) everyone in the group as if
they were personally mentioned.
- Compose a [direct message](/help/direct-messages) to a user group. This
automatically puts all the users in the group into the addressee field.
- Subscribe a user group to a channel. This individually subscribes all the users
in the group.

View File

@ -1,14 +1,4 @@
User groups make it easier to manage any organization with multiple teams or job
functions. You can:
- Assign permissions to user groups.
- Add a user group to another user group.
- Subscribe a user group to a channel. This individually subscribes all the users
in the group.
- [Mention](/help/mention-a-user-or-group) a group of users,
[notifying](/help/dm-mention-alert-notifications) everyone in the group as if
they were personally mentioned.
- Compose a [direct message](/help/direct-messages) to a user group. This
automatically puts all the users in the group into the addressee field.
You may want to create a user group for each team in your organization.
User groups offer a flexible way to manage permissions in your organization.
Most permissions in Zulip can be granted to any combination of
[roles](/help/roles-and-permissions), [groups](/help/user-groups), and
individual [users](/help/manage-a-user).

View File

@ -8,6 +8,7 @@ Topic names should be brief but specific, for example:
* **Not so good topic names:** "question", "hi", "help", "this topic is about
a question I have about topics"
!!! tip ""
Don't overthink naming your topic. The first 2-3 words that come to mind
are probably fine!
Don't stress about making it perfect! The first 2-3 words that
come to mind are probably fine, and you can always [change it
later](/help/rename-a-topic).

View File

@ -8,6 +8,8 @@
## How to start a new topic
Zulip lets you start a new conversation in any channel, no matter where you are.
{!how-to-start-a-new-topic.md!}
## Further reading

View File

@ -32,9 +32,9 @@ in the Zulip app to add more to your repertoire as needed.
<kbd>Ctrl</kbd> + <kbd>V</kbd>, and press <kbd>Ctrl</kbd> + <kbd>Z</kbd> to
remove formatting.
* **Cancel compose and save draft**: <kbd>Esc</kbd> or <kbd>Ctrl</kbd> +
<kbd>[</kbd> — Close the compose box and save the unsent message as a
draft.
* **Cancel compose and save draft**: <kbd>Esc</kbd> or
<kbd data-mac-key="Ctrl">Ctrl</kbd> + <kbd>[</kbd> — Close the compose box
and save the unsent message as a draft.
* **View drafts**: <kbd>D</kbd> — Use the arrow keys and <kbd>Enter</kbd>
to restore a draft. Press <kbd>D</kbd> again to close.
@ -54,8 +54,8 @@ in the Zulip app to add more to your repertoire as needed.
* **Toggle keyboard shortcuts view**: <kbd>?</kbd>
* **Go to your home view**: <kbd>Ctrl</kbd> + <kbd>[</kbd> (or
<kbd>Esc</kbd>, [if enabled][disable-escape])
* **Go to your home view**: <kbd data-mac-key="Ctrl">Ctrl</kbd> + <kbd>[</kbd>
(or <kbd>Esc</kbd>, [if enabled][disable-escape])
until you are in your [home view](/help/configure-home-view).
[disable-escape]: /help/configure-home-view#configure-whether-esc-navigates-to-the-home-view
@ -65,7 +65,7 @@ in the Zulip app to add more to your repertoire as needed.
* **Filter channels**: <kbd>Q</kbd>
* **Search people**: <kbd>W</kbd>
* **Filter users**: <kbd>W</kbd>
## Scrolling
@ -158,8 +158,9 @@ in the Zulip app to add more to your repertoire as needed.
* **Toggle preview mode**: <kbd>Alt</kbd> + <kbd>P</kbd>
* **Cancel compose and save draft**: <kbd>Esc</kbd> or <kbd>Ctrl</kbd> +
<kbd>[</kbd> — Close the compose box and save the unsent message as a draft.
* **Cancel compose and save draft**: <kbd>Esc</kbd> or
<kbd data-mac-key="Ctrl">Ctrl</kbd> + <kbd>[</kbd> — Close the compose box
and save the unsent message as a draft.
## Message actions
@ -185,9 +186,7 @@ in the Zulip app to add more to your repertoire as needed.
* **Star message**: <kbd>Ctrl</kbd> + <kbd>S</kbd>
* **React with <img alt=":thumbs_up:" class="emoji"
src="/static/generated/emoji/images/emoji/unicode/1f44d.png"
title="thumbs up"/>**: <kbd>+</kbd>
* **React with 👍**: <kbd>+</kbd>
* **Toggle first emoji reaction**: <kbd>=</kbd>

View File

@ -50,13 +50,21 @@ This copies to your clipboard a permanent link to the message,
displayed in its thread (i.e. topic view for messages in a channel).
Viewing a topic via a message link will never mark messages as read.
These links will still work even when the message is
[moved to another topic](/help/move-content-to-another-topic)
or [channel](/help/move-content-to-another-channel) or
if its [topic is resolved](/help/resolve-a-topic).
These links will still work even when the message is [moved to another
topic](/help/move-content-to-another-topic) or
[channel](/help/move-content-to-another-channel), or if its [topic is
resolved](/help/resolve-a-topic). Zulip uses the same permanent link syntax when
[quoting a message](/help/quote-and-reply).
Zulip uses the same permanent link syntax when [quoting a
message](/help/quote-and-reply).
When you paste a message link into the compose box, it gets automatically
formatted to be easy to read:
```
#**channel name>topic name@message ID**
```
When you send your message, the link will appear as **#channel name>topic
name@💬**.
{start_tabs}
@ -71,6 +79,12 @@ message](/help/quote-and-reply).
If using Zulip in a browser, you can also click on the timestamp
of a message, and copy the URL from your browser's address bar.
!!! tip ""
When you paste a message link into Zulip, it is automatically
formatted for you. You can paste as plain text if you prefer with
<kbd data-mac-following-key="⌥">Ctrl</kbd> + <kbd>Shift</kbd> + <kbd>V</kbd>.
{end_tabs}
### Get a link to a specific topic

View File

@ -2,6 +2,8 @@
{!user-groups-intro.md!}
{!user-groups-applications.md!}
## Create a user group
!!! tip ""
@ -34,10 +36,14 @@
!!! warn ""
Guests can never manage user groups, add anyone else to a group, or remove
Guests can never administer user groups, add anyone else to a group, or remove
anyone else from a group, even if they belong to a group that has permissions
to do so.
!!! tip ""
Users who can add members to a group can always join the group.
{start_tabs}
{tab|desktop-web}
@ -48,7 +54,7 @@
1. Select the **General** tab on the right.
1. Under **Group permissions**, configure **Who can manage this group**, **Who
1. Under **Group permissions**, configure **Who can administer this group**, **Who
can mention this group**, **Who can add members to this group**, **Who can join
this group**, and **Who can leave this group**.
@ -58,6 +64,10 @@
## Add groups and users to a group
You can add users to a group, or add a group to any other group. Nesting groups
makes them easier to maintain. For example, moving a user from one team group to
another can automatically update what department group they belong to.
{start_tabs}
{tab|desktop-web}
@ -131,16 +141,17 @@ so.
{end_tabs}
## Configure who can manage user groups
## Configure who can administer all user groups
{!admin-only.md!}
You can configure who can manage groups in your organization. Guests can never
manage user groups, even if they belong to a group that has permissions to do
so.
You can configure who can administer all user groups in your
organization. Guests can never administer user groups, even if they
belong to a group that has permissions to do so.
In addition, you can [give users permission](#configure-group-permissions) to
manage a specific group.
In addition, you can [give users
permission](#configure-group-permissions) to administer a specific
group.
{start_tabs}
@ -148,7 +159,7 @@ manage a specific group.
{settings_tab|organization-permissions}
1. Under **Other permissions**, configure **Who can manage user groups**.
1. Under **Other permissions**, configure **Who can administer all user groups**.
{!save-changes.md!}

View File

@ -116,6 +116,9 @@ you will need to upgrade your plan.
1. [Create your organization profile](/help/create-your-organization-profile),
which is displayed on your organization's registration and login pages.
1. [Create user groups](/help/create-user-groups), which offer a flexible way to
manage permissions.
1. Review [organization permissions](/help/roles-and-permissions), such as who
can invite users, create channels, etc.

View File

@ -1,19 +0,0 @@
# Review your organization settings
It's good to periodically review your organization's settings. Zulip is in
active development and is constantly adding new features, and as your
organization grows, features that may not have been applicable at a small
scale may be applicable now.
Note that most organization settings are visible to all users, though
generally only organization administrators can interact with them.
### Review your organization settings
{start_tabs}
{relative|gear|organization-settings}
1. Click on each tab on the left.
{end_tabs}

View File

@ -1,12 +1,12 @@
# Roles and permissions
## User roles
User roles make it convenient to configure different permissions for different
users in your organization. You can decide what role a user will have when you
[send them an invitation](/help/invite-new-users), and later [change a user's
role](/help/roles-and-permissions#change-a-users-role) if needed.
{!user-groups-intro.md!}
!!! tip ""
Learn about [channel permissions](/help/channel-permissions), including
@ -65,6 +65,7 @@ role](/help/roles-and-permissions#change-a-users-role) if needed.
## Related articles
* [Change a user's role](/help/change-a-users-role)
* [User groups](/help/user-groups)
* [Channel permissions](/help/channel-permissions)
* [Inviting new users](/help/invite-new-users)
* [Zulip Cloud billing](/help/zulip-cloud-billing)

View File

@ -41,9 +41,7 @@ Some details to keep in mind:
- Zulip search ignores very common words like `a`, `the`, and about 100 others.
- [Emoji](/help/emoji-and-emoticons) in messages (but not [emoji
reactions](/help/emoji-reactions)) are included in searches, so if you search
for `octopus`, your results will include messages with the `:octopus:` emoji (
<img src="/static/generated/emoji/images-google-64/1f419.png" alt="octopus"
class="emoji-small"/>).
for `thumbs_up`, your results will include messages with the `:thumbs_up:` emoji (👍).
## Search filters
@ -184,6 +182,6 @@ A summary of the search filters above is available in the Zulip app.
## Related articles
* [Configure multi-language search](/help/configure-multi-language-search)
* [Search people](/help/user-list#search-people)
* [Filter users](/help/user-list#filter-users)
* [Link to a message or
conversation](/help/link-to-a-message-or-conversation#link-to-zulip-from-anywhere)

View File

@ -2,6 +2,8 @@
{!user-groups-intro.md!}
{!user-groups-applications.md!}
## Browse and join user groups
{start_tabs}

View File

@ -1,33 +1,32 @@
# User list
In the Zulip web and desktop app, the right sidebar shows a list of users in
your organization. It shows
[non-deactivated](/help/deactivate-or-reactivate-a-user) users, and does not
include [bots](/help/bots-overview).
your organization. The user list has up to three section:
In organizations with up to 600 users, everyone is shown. In larger organizations,
only users who have been active in the last two weeks are listed. All users are
included when you search for people.
- **In this conversation**: Recent participants in the conversation you're viewing.
- **In this channel** or **Others in this channel**: Subscribers to the channel you're viewing.
- **Others**: Everyone else.
When you view a channel or a topic within a channel, subscribers to that channel
are shown separately from other members of your organization. To avoid
distraction, you can hide the user list any time.
In organizations with up to 600 users, everyone is shown. In larger
organizations, only users who have been active in the last two weeks are shown,
but everyone is included when you [search](#filter-users).
[Deactivated users](/help/deactivate-or-reactivate-a-user) and
[bots](/help/bots-overview) are not listed.
## Search people
To avoid distraction, you can [hide](#show-or-hide-the-user-list) the user list
any time.
## Filter users
{start_tabs}
{tab|desktop-web}
1. If the user list in the right sidebar is hidden, click the **user list** (<i
class="zulip-icon zulip-icon-triple-users"></i> ) icon in the upper right to
show it.
1. If the user list is hidden, click the **user list** (<i class="zulip-icon
zulip-icon-triple-users"></i> ) icon in the upper right to show it.
1. Click the **search** (<i class="search_icon zulip-icon
zulip-icon-search"></i>) icon at the top of the right sidebar to open the
search box.
1. Type the name of the user you are looking for.
1. Type the name of the user you are looking for in the **Filter users** box at
the top of the right sidebar.
!!! keyboard_tip ""
@ -47,7 +46,7 @@ show it.
!!! keyboard_tip ""
The <kbd>W</kbd> keyboard shortcut to search people reveals the user list if
The <kbd>W</kbd> keyboard shortcut to filter users reveals the user list if
it is hidden.
{end_tabs}

View File

@ -1,6 +1,6 @@
{
"private": true,
"packageManager": "pnpm@9.12.0+sha512.4abf725084d7bcbafbd728bfc7bee61f2f791f977fd87542b3579dcb23504d170d46337945e4c66485cd12d588a0c0e570ed9c477e7ccdd8507cf05f3f92eaca",
"packageManager": "pnpm@9.12.3+sha512.cce0f9de9c5a7c95bef944169cc5dfe8741abfb145078c0d508b868056848a87c81e626246cb60967cbd7fd29a6c062ef73ff840d96b3c86c40ac92cf4a813ee",
"dependencies": {
"@babel/core": "^7.5.5",
"@babel/preset-env": "^7.5.5",
@ -79,6 +79,7 @@
"stackframe": "^1.3.4",
"stacktrace-gps": "^3.0.4",
"style-loader": "^4.0.0",
"stylelint-high-performance-animation": "^1.10.0",
"text-field-edit": "^4.0.0",
"textarea-caret": "^3.1.0",
"tippy.js": "^6.3.7",
@ -108,7 +109,7 @@
"@types/lodash": "^4.14.172",
"@types/micromodal": "^0.3.3",
"@types/minimalistic-assert": "^1.0.1",
"@types/node": "^20.11.20",
"@types/node": "^22.9.0",
"@types/plotly.js": "^2.12.20",
"@types/sortablejs": "^1.15.1",
"@types/spectrum": "^1.8.4",
@ -151,7 +152,7 @@
"swagger-parser": "^10.0.0",
"ts-node": "^10.0.0",
"typescript": "^5.0.2",
"vnu-jar": "^23.4.11",
"vnu-jar": "^24.10.17",
"webpack-dev-server": "^5.0.2",
"xvfb": "^0.4.0",
"yaml": "^2.0.0-8",
@ -163,7 +164,6 @@
"source-map@^0.6": "npm:source-map-js@^1.2.1"
},
"patchedDependencies": {
"puppeteer-core": "patches/puppeteer-core.patch",
"source-sans@3.46.0": "patches/source-sans@3.46.0.patch",
"tippy.js@6.3.7": "patches/tippy.js@6.3.7.patch"
}

View File

@ -1,56 +0,0 @@
diff --git a/lib/cjs/puppeteer/common/QueryHandler.js b/lib/cjs/puppeteer/common/QueryHandler.js
index db0ff2ee001ac452b6de5804ec8810e566992299..13437d905fe4fca94ad69e4e2fd95ba6b2cce89a 100644
--- a/lib/cjs/puppeteer/common/QueryHandler.js
+++ b/lib/cjs/puppeteer/common/QueryHandler.js
@@ -163,8 +163,7 @@ class QueryHandler {
return await frame.isolatedRealm().adoptHandle(elementOrFrame);
})(), false);
const { visible = false, hidden = false, timeout, signal } = options;
- const polling = options.polling ??
- (visible || hidden ? "raf" /* PollingOptions.RAF */ : "mutation" /* PollingOptions.MUTATION */);
+ const polling = visible || hidden ? "raf" /* PollingOptions.RAF */ : options.polling;
try {
const env_4 = { stack: [], error: void 0, hasError: false };
try {
diff --git a/lib/es5-iife/puppeteer-core-browser.js b/lib/es5-iife/puppeteer-core-browser.js
index 3262ea9f5fed2d0e3d7a0ece16ad31c7e5bc4cf6..f73deab795a92d80c913adafb6c3eff7eda5e5e0 100644
--- a/lib/es5-iife/puppeteer-core-browser.js
+++ b/lib/es5-iife/puppeteer-core-browser.js
@@ -4594,7 +4594,7 @@ var Puppeteer = function (exports, _PuppeteerURL, _LazyArg, _ARIAQueryHandler, _
timeout,
signal
} = options;
- const polling = options.polling ?? (visible || hidden ? "raf" /* PollingOptions.RAF */ : "mutation" /* PollingOptions.MUTATION */);
+ const polling = visible || hidden ? "raf" /* PollingOptions.RAF */ : options.polling;
try {
const env_4 = {
stack: [],
diff --git a/lib/esm/puppeteer/common/QueryHandler.js b/lib/esm/puppeteer/common/QueryHandler.js
index b07ddf54fb1830c8ddc42dc6d0452b288696caa4..cb0203c7c1f98f03ea6658b33c8da3710ab71b29 100644
--- a/lib/esm/puppeteer/common/QueryHandler.js
+++ b/lib/esm/puppeteer/common/QueryHandler.js
@@ -160,8 +160,7 @@ export class QueryHandler {
return await frame.isolatedRealm().adoptHandle(elementOrFrame);
})(), false);
const { visible = false, hidden = false, timeout, signal } = options;
- const polling = options.polling ??
- (visible || hidden ? "raf" /* PollingOptions.RAF */ : "mutation" /* PollingOptions.MUTATION */);
+ const polling = visible || hidden ? "raf" /* PollingOptions.RAF */ : options.polling;
try {
const env_4 = { stack: [], error: void 0, hasError: false };
try {
diff --git a/src/common/QueryHandler.ts b/src/common/QueryHandler.ts
index f377771f22a193164760e703c2f3af123588ab16..db10e1cf94ada080c710f33d4dda6f4a116400fa 100644
--- a/src/common/QueryHandler.ts
+++ b/src/common/QueryHandler.ts
@@ -162,9 +162,7 @@ export class QueryHandler {
})();
const {visible = false, hidden = false, timeout, signal} = options;
- const polling =
- options.polling ??
- (visible || hidden ? PollingOptions.RAF : PollingOptions.MUTATION);
+ const polling = visible || hidden ? PollingOptions.RAF : options.polling;
try {
signal?.throwIfAborted();

File diff suppressed because it is too large Load Diff

View File

@ -30,7 +30,7 @@ class zulip::profile::postgresql {
group => 'postgres',
}
if $version in ['12','13','14'] {
if $version in ['13','14'] {
$postgresql_conf_file = "${zulip::postgresql_base::postgresql_confdir}/postgresql.conf"
file { $postgresql_conf_file:
ensure => file,

View File

@ -1,812 +0,0 @@
# -----------------------------
# PostgreSQL configuration file
# -----------------------------
#
# This file consists of lines of the form:
#
# name = value
#
# (The "=" is optional.) Whitespace may be used. Comments are introduced with
# "#" anywhere on a line. The complete list of parameter names and allowed
# values can be found in the PostgreSQL documentation.
#
# The commented-out settings shown in this file represent the default values.
# Re-commenting a setting is NOT sufficient to revert it to the default value;
# you need to reload the server.
#
# This file is read on server startup and when the server receives a SIGHUP
# signal. If you edit the file on a running system, you have to SIGHUP the
# server for the changes to take effect, run "pg_ctl reload", or execute
# "SELECT pg_reload_conf()". Some parameters, which are marked below,
# require a server shutdown and restart to take effect.
#
# Any parameter can also be given as a command-line option to the server, e.g.,
# "postgres -c log_connections=on". Some parameters can be changed at run time
# with the "SET" SQL command.
#
# Memory units: kB = kilobytes Time units: ms = milliseconds
# MB = megabytes s = seconds
# GB = gigabytes min = minutes
# TB = terabytes h = hours
# d = days
#------------------------------------------------------------------------------
# FILE LOCATIONS
#------------------------------------------------------------------------------
# The default values of these variables are driven from the -D command-line
# option or PGDATA environment variable, represented here as ConfigDir.
data_directory = '<%= scope["zulip::postgresql_base::postgresql_datadir"] %>' # use data in another directory
# (change requires restart)
hba_file = '<%= scope["zulip::postgresql_base::postgresql_confdir"] %>/pg_hba.conf' # host-based authentication file
# (change requires restart)
ident_file = '<%= scope["zulip::postgresql_base::postgresql_confdir"] %>/pg_ident.conf' # ident configuration file
# (change requires restart)
# If external_pid_file is not explicitly set, no extra PID file is written.
external_pid_file = '/var/run/postgresql/<%= scope["zulip::postgresql_common::version"] %>-main.pid' # write an extra PID file
# (change requires restart)
#------------------------------------------------------------------------------
# CONNECTIONS AND AUTHENTICATION
#------------------------------------------------------------------------------
# - Connection Settings -
#listen_addresses = 'localhost' # what IP address(es) to listen on;
# comma-separated list of addresses;
# defaults to 'localhost'; use '*' for all
# (change requires restart)
port = 5432 # (change requires restart)
max_connections = 100 # (change requires restart)
#superuser_reserved_connections = 3 # (change requires restart)
unix_socket_directories = '/var/run/postgresql' # comma-separated list of directories
# (change requires restart)
#unix_socket_group = '' # (change requires restart)
#unix_socket_permissions = 0777 # begin with 0 to use octal notation
# (change requires restart)
#bonjour = off # advertise server via Bonjour
# (change requires restart)
#bonjour_name = '' # defaults to the computer name
# (change requires restart)
# - TCP settings -
# see "man 7 tcp" for details
#tcp_keepalives_idle = 0 # TCP_KEEPIDLE, in seconds;
# 0 selects the system default
#tcp_keepalives_interval = 0 # TCP_KEEPINTVL, in seconds;
# 0 selects the system default
#tcp_keepalives_count = 0 # TCP_KEEPCNT;
# 0 selects the system default
#tcp_user_timeout = 0 # TCP_USER_TIMEOUT, in milliseconds;
# 0 selects the system default
# - Authentication -
#authentication_timeout = 1min # 1s-600s
#password_encryption = md5 # md5 or scram-sha-256
#db_user_namespace = off
# GSSAPI using Kerberos
#krb_server_keyfile = ''
#krb_caseins_users = off
# - SSL -
ssl = on
#ssl_ca_file = ''
ssl_cert_file = '/etc/ssl/certs/ssl-cert-snakeoil.pem'
#ssl_crl_file = ''
ssl_key_file = '/etc/ssl/private/ssl-cert-snakeoil.key'
#ssl_ciphers = 'HIGH:MEDIUM:+3DES:!aNULL' # allowed SSL ciphers
#ssl_prefer_server_ciphers = on
#ssl_ecdh_curve = 'prime256v1'
#ssl_min_protocol_version = 'TLSv1'
#ssl_max_protocol_version = ''
#ssl_dh_params_file = ''
#ssl_passphrase_command = ''
#ssl_passphrase_command_supports_reload = off
#------------------------------------------------------------------------------
# RESOURCE USAGE (except WAL)
#------------------------------------------------------------------------------
# - Memory -
shared_buffers = 128MB # min 128kB
# (change requires restart)
#huge_pages = try # on, off, or try
# (change requires restart)
#temp_buffers = 8MB # min 800kB
#max_prepared_transactions = 0 # zero disables the feature
# (change requires restart)
# Caution: it is not advisable to set max_prepared_transactions nonzero unless
# you actively intend to use prepared transactions.
#work_mem = 4MB # min 64kB
#maintenance_work_mem = 64MB # min 1MB
#autovacuum_work_mem = -1 # min 1MB, or -1 to use maintenance_work_mem
#max_stack_depth = 2MB # min 100kB
#shared_memory_type = mmap # the default is the first option
# supported by the operating system:
# mmap
# sysv
# windows
# (change requires restart)
dynamic_shared_memory_type = posix # the default is the first option
# supported by the operating system:
# posix
# sysv
# windows
# mmap
# (change requires restart)
# - Disk -
#temp_file_limit = -1 # limits per-process temp file space
# in kB, or -1 for no limit
# - Kernel Resources -
#max_files_per_process = 1000 # min 25
# (change requires restart)
# - Cost-Based Vacuum Delay -
#vacuum_cost_delay = 0 # 0-100 milliseconds (0 disables)
#vacuum_cost_page_hit = 1 # 0-10000 credits
#vacuum_cost_page_miss = 10 # 0-10000 credits
#vacuum_cost_page_dirty = 20 # 0-10000 credits
#vacuum_cost_limit = 200 # 1-10000 credits
# - Background Writer -
#bgwriter_delay = 200ms # 10-10000ms between rounds
#bgwriter_lru_maxpages = 100 # max buffers written/round, 0 disables
#bgwriter_lru_multiplier = 2.0 # 0-10.0 multiplier on buffers scanned/round
#bgwriter_flush_after = 512kB # measured in pages, 0 disables
# - Asynchronous Behavior -
#effective_io_concurrency = 1 # 1-1000; 0 disables prefetching
#max_worker_processes = 8 # (change requires restart)
#max_parallel_maintenance_workers = 2 # taken from max_parallel_workers
#max_parallel_workers_per_gather = 2 # taken from max_parallel_workers
#parallel_leader_participation = on
#max_parallel_workers = 8 # maximum number of max_worker_processes that
# can be used in parallel operations
#old_snapshot_threshold = -1 # 1min-60d; -1 disables; 0 is immediate
# (change requires restart)
#backend_flush_after = 0 # measured in pages, 0 disables
#------------------------------------------------------------------------------
# WRITE-AHEAD LOG
#------------------------------------------------------------------------------
# - Settings -
#wal_level = replica # minimal, replica, or logical
# (change requires restart)
#fsync = on # flush data to disk for crash safety
# (turning this off can cause
# unrecoverable data corruption)
#synchronous_commit = on # synchronization level;
# off, local, remote_write, remote_apply, or on
#wal_sync_method = fsync # the default is the first option
# supported by the operating system:
# open_datasync
# fdatasync (default on Linux)
# fsync
# fsync_writethrough
# open_sync
#full_page_writes = on # recover from partial page writes
#wal_compression = off # enable compression of full-page writes
#wal_log_hints = off # also do full page writes of non-critical updates
# (change requires restart)
#wal_init_zero = on # zero-fill new WAL files
#wal_recycle = on # recycle WAL files
#wal_buffers = -1 # min 32kB, -1 sets based on shared_buffers
# (change requires restart)
#wal_writer_delay = 200ms # 1-10000 milliseconds
#wal_writer_flush_after = 1MB # measured in pages, 0 disables
#commit_delay = 0 # range 0-100000, in microseconds
#commit_siblings = 5 # range 1-1000
# - Checkpoints -
#checkpoint_timeout = 5min # range 30s-1d
max_wal_size = 1GB
min_wal_size = 80MB
#checkpoint_completion_target = 0.5 # checkpoint target duration, 0.0 - 1.0
#checkpoint_flush_after = 256kB # measured in pages, 0 disables
#checkpoint_warning = 30s # 0 disables
# - Archiving -
#archive_mode = off # enables archiving; off, on, or always
# (change requires restart)
#archive_command = '' # command to use to archive a logfile segment
# placeholders: %p = path of file to archive
# %f = file name only
# e.g. 'test ! -f /mnt/server/archivedir/%f && cp %p /mnt/server/archivedir/%f'
#archive_timeout = 0 # force a logfile segment switch after this
# number of seconds; 0 disables
# - Archive Recovery -
# These are only used in recovery mode.
#restore_command = '' # command to use to restore an archived logfile segment
# placeholders: %p = path of file to restore
# %f = file name only
# e.g. 'cp /mnt/server/archivedir/%f %p'
# (change requires restart)
#archive_cleanup_command = '' # command to execute at every restartpoint
#recovery_end_command = '' # command to execute at completion of recovery
# - Recovery Target -
# Set these only when performing a targeted recovery.
#recovery_target = '' # 'immediate' to end recovery as soon as a
# consistent state is reached
# (change requires restart)
#recovery_target_name = '' # the named restore point to which recovery will proceed
# (change requires restart)
#recovery_target_time = '' # the time stamp up to which recovery will proceed
# (change requires restart)
#recovery_target_xid = '' # the transaction ID up to which recovery will proceed
# (change requires restart)
#recovery_target_lsn = '' # the WAL LSN up to which recovery will proceed
# (change requires restart)
#recovery_target_inclusive = on # Specifies whether to stop:
# just after the specified recovery target (on)
# just before the recovery target (off)
# (change requires restart)
#recovery_target_timeline = 'latest' # 'current', 'latest', or timeline ID
# (change requires restart)
#recovery_target_action = 'pause' # 'pause', 'promote', 'shutdown'
# (change requires restart)
#------------------------------------------------------------------------------
# REPLICATION
#------------------------------------------------------------------------------
# - Sending Servers -
# Set these on the master and on any standby that will send replication data.
#max_wal_senders = 10 # max number of walsender processes
# (change requires restart)
#wal_keep_segments = 0 # in logfile segments; 0 disables
#wal_sender_timeout = 60s # in milliseconds; 0 disables
#max_replication_slots = 10 # max number of replication slots
# (change requires restart)
#track_commit_timestamp = off # collect timestamp of transaction commit
# (change requires restart)
# - Master Server -
# These settings are ignored on a standby server.
#synchronous_standby_names = '' # standby servers that provide sync rep
# method to choose sync standbys, number of sync standbys,
# and comma-separated list of application_name
# from standby(s); '*' = all
#vacuum_defer_cleanup_age = 0 # number of xacts by which cleanup is delayed
# - Standby Servers -
# These settings are ignored on a master server.
#primary_conninfo = '' # connection string to sending server
# (change requires restart)
#primary_slot_name = '' # replication slot on sending server
# (change requires restart)
#promote_trigger_file = '' # file name whose presence ends recovery
#hot_standby = on # "off" disallows queries during recovery
# (change requires restart)
#max_standby_archive_delay = 30s # max delay before canceling queries
# when reading WAL from archive;
# -1 allows indefinite delay
#max_standby_streaming_delay = 30s # max delay before canceling queries
# when reading streaming WAL;
# -1 allows indefinite delay
#wal_receiver_status_interval = 10s # send replies at least this often
# 0 disables
#hot_standby_feedback = off # send info from standby to prevent
# query conflicts
#wal_receiver_timeout = 60s # time that receiver waits for
# communication from master
# in milliseconds; 0 disables
#wal_retrieve_retry_interval = 5s # time to wait before retrying to
# retrieve WAL after a failed attempt
#recovery_min_apply_delay = 0 # minimum delay for applying changes during recovery
# - Subscribers -
# These settings are ignored on a publisher.
#max_logical_replication_workers = 4 # taken from max_worker_processes
# (change requires restart)
#max_sync_workers_per_subscription = 2 # taken from max_logical_replication_workers
#------------------------------------------------------------------------------
# QUERY TUNING
#------------------------------------------------------------------------------
# - Planner Method Configuration -
#enable_bitmapscan = on
#enable_hashagg = on
#enable_hashjoin = on
#enable_indexscan = on
#enable_indexonlyscan = on
#enable_material = on
#enable_mergejoin = on
#enable_nestloop = on
#enable_parallel_append = on
#enable_seqscan = on
#enable_sort = on
#enable_tidscan = on
#enable_partitionwise_join = off
#enable_partitionwise_aggregate = off
#enable_parallel_hash = on
#enable_partition_pruning = on
# - Planner Cost Constants -
#seq_page_cost = 1.0 # measured on an arbitrary scale
#random_page_cost = 4.0 # same scale as above
#cpu_tuple_cost = 0.01 # same scale as above
#cpu_index_tuple_cost = 0.005 # same scale as above
#cpu_operator_cost = 0.0025 # same scale as above
#parallel_tuple_cost = 0.1 # same scale as above
#parallel_setup_cost = 1000.0 # same scale as above
#jit_above_cost = 100000 # perform JIT compilation if available
# and query more expensive than this;
# -1 disables
#jit_inline_above_cost = 500000 # inline small functions if query is
# more expensive than this; -1 disables
#jit_optimize_above_cost = 500000 # use expensive JIT optimizations if
# query is more expensive than this;
# -1 disables
#min_parallel_table_scan_size = 8MB
#min_parallel_index_scan_size = 512kB
#effective_cache_size = 4GB
# - Genetic Query Optimizer -
#geqo = on
#geqo_threshold = 12
#geqo_effort = 5 # range 1-10
#geqo_pool_size = 0 # selects default based on effort
#geqo_generations = 0 # selects default based on effort
#geqo_selection_bias = 2.0 # range 1.5-2.0
#geqo_seed = 0.0 # range 0.0-1.0
# - Other Planner Options -
#default_statistics_target = 100 # range 1-10000
#constraint_exclusion = partition # on, off, or partition
#cursor_tuple_fraction = 0.1 # range 0.0-1.0
#from_collapse_limit = 8
#join_collapse_limit = 8 # 1 disables collapsing of explicit
# JOIN clauses
#force_parallel_mode = off
#jit = on # allow JIT compilation
#plan_cache_mode = auto # auto, force_generic_plan or
# force_custom_plan
#------------------------------------------------------------------------------
# REPORTING AND LOGGING
#------------------------------------------------------------------------------
# - Where to Log -
#log_destination = 'stderr' # Valid values are combinations of
# stderr, csvlog, syslog, and eventlog,
# depending on platform. csvlog
# requires logging_collector to be on.
# This is used when logging to stderr:
#logging_collector = off # Enable capturing of stderr and csvlog
# into log files. Required to be on for
# csvlogs.
# (change requires restart)
# These are only used if logging_collector is on:
#log_directory = 'log' # directory where log files are written,
# can be absolute or relative to PGDATA
#log_filename = 'postgresql-%Y-%m-%d_%H%M%S.log' # log file name pattern,
# can include strftime() escapes
#log_file_mode = 0600 # creation mode for log files,
# begin with 0 to use octal notation
#log_truncate_on_rotation = off # If on, an existing log file with the
# same name as the new log file will be
# truncated rather than appended to.
# But such truncation only occurs on
# time-driven rotation, not on restarts
# or size-driven rotation. Default is
# off, meaning append to existing files
# in all cases.
#log_rotation_age = 1d # Automatic rotation of logfiles will
# happen after that time. 0 disables.
#log_rotation_size = 10MB # Automatic rotation of logfiles will
# happen after that much log output.
# 0 disables.
# These are relevant when logging to syslog:
#syslog_facility = 'LOCAL0'
#syslog_ident = 'postgres'
#syslog_sequence_numbers = on
#syslog_split_messages = on
# This is only relevant when logging to eventlog (win32):
# (change requires restart)
#event_source = 'PostgreSQL'
# - When to Log -
#log_min_messages = warning # values in order of decreasing detail:
# debug5
# debug4
# debug3
# debug2
# debug1
# info
# notice
# warning
# error
# log
# fatal
# panic
#log_min_error_statement = error # values in order of decreasing detail:
# debug5
# debug4
# debug3
# debug2
# debug1
# info
# notice
# warning
# error
# log
# fatal
# panic (effectively off)
#log_min_duration_statement = -1 # -1 is disabled, 0 logs all statements
# and their durations, > 0 logs only
# statements running at least this number
# of milliseconds
#log_transaction_sample_rate = 0.0 # Fraction of transactions whose statements
# are logged regardless of their duration. 1.0 logs all
# statements from all transactions, 0.0 never logs.
# - What to Log -
#debug_print_parse = off
#debug_print_rewritten = off
#debug_print_plan = off
#debug_pretty_print = on
#log_checkpoints = off
#log_connections = off
#log_disconnections = off
#log_duration = off
#log_error_verbosity = default # terse, default, or verbose messages
#log_hostname = off
log_line_prefix = '%m [%p] %q%u@%d ' # special values:
# %a = application name
# %u = user name
# %d = database name
# %r = remote host and port
# %h = remote host
# %p = process ID
# %t = timestamp without milliseconds
# %m = timestamp with milliseconds
# %n = timestamp with milliseconds (as a Unix epoch)
# %i = command tag
# %e = SQL state
# %c = session ID
# %l = session line number
# %s = session start timestamp
# %v = virtual transaction ID
# %x = transaction ID (0 if none)
# %q = stop here in non-session
# processes
# %% = '%'
# e.g. '<%%u%%%d> '
#log_lock_waits = off # log lock waits >= deadlock_timeout
#log_statement = 'none' # none, ddl, mod, all
#log_replication_commands = off
#log_temp_files = -1 # log temporary files equal or larger
# than the specified size in kilobytes;
# -1 disables, 0 logs all temp files
log_timezone = 'UTC'
#------------------------------------------------------------------------------
# PROCESS TITLE
#------------------------------------------------------------------------------
cluster_name = '<%= scope["zulip::postgresql_common::version"] %>/main' # added to process titles if nonempty
# (change requires restart)
#update_process_title = on
#------------------------------------------------------------------------------
# STATISTICS
#------------------------------------------------------------------------------
# - Query and Index Statistics Collector -
#track_activities = on
#track_counts = on
#track_io_timing = off
#track_functions = none # none, pl, all
#track_activity_query_size = 1024 # (change requires restart)
stats_temp_directory = '/var/run/postgresql/<%= scope["zulip::postgresql_common::version"] %>-main.pg_stat_tmp'
# - Monitoring -
#log_parser_stats = off
#log_planner_stats = off
#log_executor_stats = off
#log_statement_stats = off
#------------------------------------------------------------------------------
# AUTOVACUUM
#------------------------------------------------------------------------------
#autovacuum = on # Enable autovacuum subprocess? 'on'
# requires track_counts to also be on.
#log_autovacuum_min_duration = -1 # -1 disables, 0 logs all actions and
# their durations, > 0 logs only
# actions running at least this number
# of milliseconds.
#autovacuum_max_workers = 3 # max number of autovacuum subprocesses
# (change requires restart)
#autovacuum_naptime = 1min # time between autovacuum runs
#autovacuum_vacuum_threshold = 50 # min number of row updates before
# vacuum
#autovacuum_analyze_threshold = 50 # min number of row updates before
# analyze
#autovacuum_vacuum_scale_factor = 0.2 # fraction of table size before vacuum
#autovacuum_analyze_scale_factor = 0.1 # fraction of table size before analyze
#autovacuum_freeze_max_age = 200000000 # maximum XID age before forced vacuum
# (change requires restart)
#autovacuum_multixact_freeze_max_age = 400000000 # maximum multixact age
# before forced vacuum
# (change requires restart)
#autovacuum_vacuum_cost_delay = 2ms # default vacuum cost delay for
# autovacuum, in milliseconds;
# -1 means use vacuum_cost_delay
#autovacuum_vacuum_cost_limit = -1 # default vacuum cost limit for
# autovacuum, -1 means use
# vacuum_cost_limit
#------------------------------------------------------------------------------
# CLIENT CONNECTION DEFAULTS
#------------------------------------------------------------------------------
# - Statement Behavior -
#client_min_messages = notice # values in order of decreasing detail:
# debug5
# debug4
# debug3
# debug2
# debug1
# log
# notice
# warning
# error
#search_path = '"$user", public' # schema names
#row_security = on
#default_tablespace = '' # a tablespace name, '' uses the default
#temp_tablespaces = '' # a list of tablespace names, '' uses
# only default tablespace
#default_table_access_method = 'heap'
#check_function_bodies = on
#default_transaction_isolation = 'read committed'
#default_transaction_read_only = off
#default_transaction_deferrable = off
#session_replication_role = 'origin'
#statement_timeout = 0 # in milliseconds, 0 is disabled
#lock_timeout = 0 # in milliseconds, 0 is disabled
#idle_in_transaction_session_timeout = 0 # in milliseconds, 0 is disabled
#vacuum_freeze_min_age = 50000000
#vacuum_freeze_table_age = 150000000
#vacuum_multixact_freeze_min_age = 5000000
#vacuum_multixact_freeze_table_age = 150000000
#vacuum_cleanup_index_scale_factor = 0.1 # fraction of total number of tuples
# before index cleanup, 0 always performs
# index cleanup
#bytea_output = 'hex' # hex, escape
#xmlbinary = 'base64'
#xmloption = 'content'
#gin_fuzzy_search_limit = 0
#gin_pending_list_limit = 4MB
# - Locale and Formatting -
datestyle = 'iso, mdy'
#intervalstyle = 'postgres'
timezone = 'UTC'
#timezone_abbreviations = 'Default' # Select the set of available time zone
# abbreviations. Currently, there are
# Default
# Australia (historical usage)
# India
# You can create your own file in
# share/timezonesets/.
#extra_float_digits = 1 # min -15, max 3; any value >0 actually
# selects precise output mode
#client_encoding = sql_ascii # actually, defaults to database
# encoding
# These settings are initialized by initdb, but they can be changed.
lc_messages = 'C.UTF-8' # locale for system error message
# strings
lc_monetary = 'C.UTF-8' # locale for monetary formatting
lc_numeric = 'C.UTF-8' # locale for number formatting
lc_time = 'C.UTF-8' # locale for time formatting
# default configuration for text search
default_text_search_config = 'pg_catalog.english'
# - Shared Library Preloading -
#shared_preload_libraries = '' # (change requires restart)
#local_preload_libraries = ''
#session_preload_libraries = ''
#jit_provider = 'llvmjit' # JIT library to use
# - Other Defaults -
#dynamic_library_path = '$libdir'
#------------------------------------------------------------------------------
# LOCK MANAGEMENT
#------------------------------------------------------------------------------
#deadlock_timeout = 1s
#max_locks_per_transaction = 64 # min 10
# (change requires restart)
#max_pred_locks_per_transaction = 64 # min 10
# (change requires restart)
#max_pred_locks_per_relation = -2 # negative values mean
# (max_pred_locks_per_transaction
# / -max_pred_locks_per_relation) - 1
#max_pred_locks_per_page = 2 # min 0
#------------------------------------------------------------------------------
# VERSION AND PLATFORM COMPATIBILITY
#------------------------------------------------------------------------------
# - Previous PostgreSQL Versions -
#array_nulls = on
#backslash_quote = safe_encoding # on, off, or safe_encoding
#escape_string_warning = on
#lo_compat_privileges = off
#operator_precedence_warning = off
#quote_all_identifiers = off
#standard_conforming_strings = on
#synchronize_seqscans = on
# - Other Platforms and Clients -
#transform_null_equals = off
#------------------------------------------------------------------------------
# ERROR HANDLING
#------------------------------------------------------------------------------
#exit_on_error = off # terminate session on any error?
#restart_after_crash = on # reinitialize after backend crash?
#data_sync_retry = off # retry or panic on failure to fsync
# data?
# (change requires restart)
#------------------------------------------------------------------------------
# CONFIG FILE INCLUDES
#------------------------------------------------------------------------------
# These options allow settings to be loaded from files other than the
# default postgresql.conf. Note that these are directives, not variable
# assignments, so they can usefully be given more than once.
include_dir = 'conf.d' # include files ending in '.conf' from
# a directory, e.g., 'conf.d'
#include_if_exists = '...' # include file only if it exists
#include = '...' # include file
#------------------------------------------------------------------------------
# CUSTOMIZED OPTIONS
#------------------------------------------------------------------------------
# Add settings for extensions here
timezone = 'UTC'
default_text_search_config = 'zulip.english_us_search'
log_destination = 'stderr'
logging_collector = on
log_directory = '/var/log/postgresql'
log_filename = 'postgresql-%Y-%m-%d_%H%M%S.log'
log_rotation_age = 7d
log_rotation_size = 100MB
log_min_duration_statement = 500
log_line_prefix = '%m [%c]: [%l-1] '
log_checkpoints = on
log_lock_waits = on
log_temp_files = 0
log_autovacuum_min_duration = 100
autovacuum_freeze_max_age = 2000000000
vacuum_freeze_min_age = 1000000000
vacuum_freeze_table_age = 1800000000
# Performance settings
max_connections = 1000
maintenance_work_mem = <%= scope["zulip::profile::postgresql::maintenance_work_mem"] %>MB
effective_cache_size = <%= scope["zulip::profile::postgresql::effective_cache_size"] %>MB
work_mem = <%= scope["zulip::profile::postgresql::work_mem"] %>MB
shared_buffers = <%= scope["zulip::profile::postgresql::shared_buffers"] %>MB
wal_buffers = 4MB
checkpoint_completion_target = 0.7
<% unless @random_page_cost.nil? -%>
random_page_cost = <%= @random_page_cost %>
<% end -%>
<% unless @effective_io_concurrency.nil? -%>
effective_io_concurrency = <%= @effective_io_concurrency %>
<% end -%>
<% unless @listen_addresses.nil? -%>
listen_addresses = <%= @listen_addresses %>
<% end -%>
<% if @s3_backups_bucket != '' -%>
# WAL backups to S3 (may also be used for replication)
archive_mode = on
archive_command = '/usr/bin/timeout 10m /usr/local/bin/env-wal-g wal-push %p'
restore_command = '/usr/local/bin/env-wal-g wal-fetch "%f" "%p"'
<% end -%>
<% unless @replication_primary.nil? || @replication_user.nil? -%>
# Streaming replication
primary_conninfo = 'host=<%= @replication_primary %> user=<%= @replication_user -%>
<% if @replication_password != '' %> password=<%= @replication_password %><% end -%>
<% unless @ssl_mode.nil? %> sslmode=<%= @ssl_mode %><% end -%>
'
<% end -%>
<% unless @ssl_cert_file.nil? -%>
ssl_cert_file = '<%= @ssl_cert_file %>' # (change requires restart)
<% end -%>
<% unless @ssl_key_file.nil? -%>
ssl_key_file = '<%= @ssl_key_file %>' # (change requires restart)
<% end -%>
<% unless @ssl_ca_file.nil? -%>
ssl_ca_file = '<%= @ssl_ca_file %>' # (change requires restart)
<% end -%>

View File

@ -189,7 +189,7 @@ django-scim2
circuitbreaker
# Runtime monkeypatching of django-stubs generics
https://github.com/zulip/django-stubs/archive/a006b9993130cc2c1e80f2eb55d959422337554a.zip#egg=django-stubs-ext==5.1.0+git&subdirectory=ext # https://github.com/typeddjango/django-stubs/pull/2408, https://github.com/typeddjango/django-stubs/pull/2411
django-stubs-ext
# Structured data representation with parsing.
pydantic

View File

@ -51,7 +51,7 @@ python-digitalocean
pip-tools<6.3.0 # https://github.com/jazzband/pip-tools/pull/1455 breaks our hack for installing specific commits from Git
# zulip's linting framework - zulint
https://github.com/zulip/zulint/archive/a070f3a349bc0c7ce09ad097b1d3e419b42a6126.zip#egg=zulint==1.0.0+git
https://github.com/zulip/zulint/archive/9be0a32bf75a9d8738b005f0b880567fff64e943.zip#egg=zulint==1.0.0+git
-r mypy.in

File diff suppressed because it is too large Load Diff

View File

@ -1,10 +1,10 @@
# After editing this file, you MUST afterward run
# /tools/update-locked-requirements to update requirements/dev.txt.
# See requirements/README.md for more detail.
mypy
mypy[faster-cache]
boto3-stubs[s3,ses,sns,sqs]
https://github.com/zulip/django-stubs/archive/a006b9993130cc2c1e80f2eb55d959422337554a.zip#egg=django-stubs==5.1.0+git # https://github.com/typeddjango/django-stubs/pull/2408, https://github.com/typeddjango/django-stubs/pull/2411
django-stubs
lxml-stubs
SQLAlchemy[mypy]
types-beautifulsoup4

View File

@ -1,3 +1,3 @@
https://github.com/zulip/pip/archive/39e8b5ecfcd8d19872f103d680713bc8e9de9d00.zip#egg=pip==20.3.4+git # Our hack for installing specific commits from Git requires --use-deprecated=legacy-resolver: https://github.com/pypa/pip/issues/5780
setuptools
https://github.com/zulip/pip/archive/50e61dcc78d0da8a041e4fecc566f40b2b0604df.zip#egg=pip==20.3.4+git # Our hack for installing specific commits from Git requires --use-deprecated=legacy-resolver: https://github.com/pypa/pip/issues/5780
setuptools<71.0.2 # Newer setuptools fools old pip into thinking typing_extensions is already installed
wheel

View File

@ -13,8 +13,8 @@ wheel==0.44.0 \
# via -r requirements/pip.in
# The following packages are considered to be unsafe in a requirements file:
https://github.com/zulip/pip/archive/39e8b5ecfcd8d19872f103d680713bc8e9de9d00.zip#egg=pip==20.3.4+git \
--hash=sha256:2c8ae0783fea3e2b98b422d443b19c443779f0881882f905e0728be7b4043d1b
https://github.com/zulip/pip/archive/50e61dcc78d0da8a041e4fecc566f40b2b0604df.zip#egg=pip==20.3.4+git \
--hash=sha256:35e47938b7e2a91523359b54a53c62fc2aeed5adaaba24437a33cbd9a4641574
# via -r requirements/pip.in
setuptools==71.0.0 \
--hash=sha256:98da3b8aca443b9848a209ae4165e2edede62633219afa493a58fbba57f72e2e \

File diff suppressed because it is too large Load Diff

View File

@ -62,10 +62,10 @@ if os.path.exists("/etc/init.d/postgresql") and os.path.exists("/etc/zulip/zulip
)
sys.exit(1)
if django_pg_version < 12:
if django_pg_version < 13:
logging.critical("Unsupported PostgreSQL version: %d", postgresql_version)
logging.info(
"Please upgrade to PostgreSQL 12 or newer first.\n"
"Please upgrade to PostgreSQL 13 or newer first.\n"
"See https://zulip.readthedocs.io/en/stable/production/"
"upgrade.html#upgrading-postgresql"
)

View File

@ -199,8 +199,8 @@ if [ "$EXTERNAL_HOST" = zulip.example.com ] \
fi
case "$POSTGRESQL_VERSION" in
[0-9] | [0-9].* | 1[01] | 1[01].*)
echo "error: PostgreSQL 12 or newer is required." >&2
[0-9] | [0-9].* | 1[0-2] | 1[0-2].*)
echo "error: PostgreSQL 13 or newer is required." >&2
exit 1
;;
esac

View File

@ -1,18 +1,18 @@
#!/usr/bin/env bash
set -euo pipefail
version=20.18.0
version=22.11.0
arch="$(uname -m)"
case $arch in
x86_64)
tarball="node-v$version-linux-x64.tar.xz"
sha256=4543670b589593f8fa5f106111fd5139081da42bb165a9239f05195e405f240a
sha256=83bf07dd343002a26211cf1fcd46a9d9534219aad42ee02847816940bf610a72
;;
aarch64)
tarball="node-v$version-linux-arm64.tar.xz"
sha256=a9ce85675ba33f00527f6234d90000946c0936fb4fca605f1891bb5f4fe6fb0a
sha256=6031d04b98f59ff0f7cb98566f65b115ecd893d3b7870821171708cdbaf7ae6e
;;
esac

View File

@ -48,12 +48,6 @@ pg_args["connect_timeout"] = "600"
conn = psycopg2.connect(connection_factory=None, **pg_args)
conn.autocommit = True
pg_server_version = conn.server_version
can_concurrently = pg_server_version >= 110000 # Version 11.0.0
if options.concurrently and not can_concurrently:
raise RuntimeError("Only PostgreSQL 11 and above can REINDEX CONCURRENTLY.")
cursor = conn.cursor()
cursor.execute(
"""

Binary file not shown.

Before

Width:  |  Height:  |  Size: 157 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

View File

@ -2,6 +2,7 @@
module.exports = {
extends: ["stylelint-config-standard"],
plugins: ["stylelint-high-performance-animation"],
rules: {
// Add some exceptions for recommended rules
"at-rule-no-unknown": [true, {ignoreAtRules: ["extend"]}],
@ -37,6 +38,7 @@ module.exports = {
// We use hsl instead of rgb
"rgb",
],
"plugin/no-low-performance-animation-properties": [true, {ignore: "paint-properties"}],
// Zulip CSS should have no dependencies on external resources
"function-url-no-scheme-relative": true,

View File

@ -88,6 +88,8 @@
<li><code>./manage.py mark_all_messages_unread</code>: Useful for testing reading messages.</li>
<li><code>./manage.py create_realm</code>: Add a new realm. Useful for testing onboarding.</li>
<li><code>./manage.py create_user</code>: Add a new user. Useful for testing onboarding.</li>
<li><code>./manage.py send_zulip_update_announcements</code>: Send <a href="https://zulip.com/help/configure-automated-notices#zulip-update-announcements">Zulip
update notices</a> drafted in `zerver/lib/zulip_update_announcements.py`.</li>
<li><code>./manage.py add_mock_conversation</code>: Add test messages, streams, images, emoji, etc.
into the dev environment. First edit zilencer/management/commands/add_mock_conversation.py
to add the data you're testing.

View File

@ -38,12 +38,12 @@
{% else %}
<div class="find-account-form">
<p>
{% trans %}
Enter your email address to receive an email with the URLs
for all the Zulip Cloud organizations in which you have
active accounts. If you have also forgotten your password,
you can <a href="/help/change-your-password">reset it</a>.
{% endtrans %}
{% if corporate_enabled %}
{% trans %}Enter your email address to receive an email with the URLs for all the Zulip Cloud organizations in which you have active accounts.{% endtrans %}
{% else %}
{% trans %}Enter your email address to receive an email with the URLs for all the Zulip organizations on this server in which you have active accounts.{% endtrans %}
{% endif %}
{% trans %}If you have also forgotten your password, you can <a href="/help/change-your-password">reset it</a>.{% endtrans %}
</p>
<form class="form-inline" id="find_account" name="email_form"
action="{{ current_url }}" method="post">
@ -55,7 +55,7 @@
</div>
<button type="submit">{{ _('Find accounts') }}</button>
</div>
<div><i>{{ form.emails.help_text }}</i></div>
<div class="find-account-form-tip"><i>{{ form.emails.help_text }}</i></div>
</form>
<div id="errors"></div>
{% if form.emails.errors %}

View File

@ -12,14 +12,15 @@
<h1 class="get-started">{{ _('No organization found') }}</h1>
</div>
<div class="app-main white-box">
<div class="app-main find-account-page-container white-box">
<p>
{% trans %}There is no Zulip organization at <b>{{ current_url }}</b>.{% endtrans %}
<br />
</p>
<p>
{% if corporate_enabled %}
{% trans %}Please try a different URL, or <a href="mailto:{{ support_email }}">contact Zulip support</a>.{% endtrans %}
{% trans %}Please try a different URL, <a href="{{ root_domain_url }}/accounts/find/">get a list of your Zulip Cloud accounts</a>, or <a href="mailto:{{ support_email }}">contact Zulip support</a>.{% endtrans %}
{% else %}
{% trans %}Please try a different URL, or <a href="mailto:{{ support_email }}">contact this Zulip server's administrators</a>.{% endtrans %}
{% trans %}Please try a different URL, <a href="{{ root_domain_url }}/accounts/find/">get a list of your accounts</a> on this server, or <a href="mailto:{{ support_email }}">contact this Zulip server's administrators</a>.{% endtrans %}
{% endif %}
</p>
</div>

View File

@ -44,7 +44,9 @@
</div>
<div class="bottom-text">
{{ _("Need to get your group started on Zulip?") }} <a target="_blank" rel="noopener noreferrer" href="/new/">{{ _("Create a new organization.") }}</a>
{% trans org_creation_link="/new/" %}
<a target="_blank" rel="noopener noreferrer" href="{{ org_creation_link }}">Create a new organization</a> if you don't have one yet.
{% endtrans %}
</div>
</div>

View File

@ -1,4 +1,6 @@
#!/usr/bin/env bash
ZULIP_PATH="$(dirname "${BASH_SOURCE[0]}")/../.."
source /srv/zulip-py3-venv/bin/activate
source "$ZULIP_PATH"/tools/python-warnings.bash
echo "Using $VIRTUAL_ENV"

View File

@ -2,6 +2,9 @@
set -e
set -x
ZULIP_PATH="$(dirname "${BASH_SOURCE[0]}")/../.."
. "$ZULIP_PATH/tools/python-warnings.bash"
# This is just a thin wrapper around provision.
# Provisioning may fail due to many issues but most of the times a network
# connection issue is the reason. So we are going to retry entire provisioning

View File

@ -18,6 +18,21 @@ os.environ["DJANGO_SETTINGS_MODULE"] = "zproject.settings"
django.setup()
def replace_image_path(markdown_string: str) -> str:
"""
We will point to the existing image folder till
the cutover. After that, we will copy the images
to src folder for help-beta in order to take
advantage of Astro's image optimization.
See https://chat.zulip.org/#narrow/stream/6-frontend/topic/Handling.20images.20in.20help.20center.20starlight.20migration.2E/near/1915130
"""
# We do not replace /static/images directly since there are a few
# instances in the documentation where zulip.com links are
# referenced with that blurb as a part of the url.
result = markdown_string.replace("(/static/images/help-beta", "(../../../../static/images/help")
return result.replace('="/static/images/help-beta', '="../../../../static/images/help')
def escape_curly_braces(markdown_string: str) -> str:
"""
MDX will treat curly braces as a JS expression,
@ -54,6 +69,7 @@ def insert_frontmatter(markdown_string: str) -> str:
def convert_string_to_mdx(markdown_string: str) -> str:
result = escape_curly_braces(markdown_string)
result = fix_relative_path(result)
result = replace_image_path(result)
return insert_frontmatter(result)

View File

@ -1,4 +1,5 @@
import re
from io import StringIO
from re import Match
from bs4 import BeautifulSoup
@ -163,6 +164,8 @@ IGNORED_PHRASES = [
r"does not apply to users who can delete any message",
# Used as indicator with names for guest users.
r"guest",
# Used as indicator with names for archived streams.
r"archived",
# Used in pills for deactivated users.
r"deactivated",
# This is a reference to a setting/secret and should be lowercase.
@ -182,7 +185,8 @@ IGNORED_PHRASES.sort(key=len, reverse=True)
# text using BeautifulSoup and then removes extra whitespaces from
# it. This step enables us to add HTML in our regexes directly.
COMPILED_IGNORED_PHRASES = [
re.compile(r" ".join(BeautifulSoup(regex, "lxml").text.split())) for regex in IGNORED_PHRASES
re.compile(r" ".join(BeautifulSoup(StringIO(regex), "lxml").text.split()))
for regex in IGNORED_PHRASES
]
SPLIT_BOUNDARY = r"?.!" # Used to split string into sentences.
@ -241,7 +245,7 @@ def get_safe_text(text: str) -> str:
This returns text which is rendered by BeautifulSoup and is in the
form that can be split easily and has all IGNORED_PHRASES processed.
"""
soup = BeautifulSoup(text, "lxml")
soup = BeautifulSoup(StringIO(text), "lxml")
text = " ".join(soup.text.split()) # Remove extra whitespaces.
for phrase_regex in COMPILED_IGNORED_PHRASES:
text = phrase_regex.sub(replace_with_safe_phrase, text)

View File

@ -0,0 +1,83 @@
# shellcheck shell=bash
export PYTHONWARNINGS=error
PYTHONWARNINGS+=',ignore::ResourceWarning'
# https://github.com/python/cpython/pull/96629
PYTHONWARNINGS+=',default:check_home argument is deprecated and ignored.:DeprecationWarning:distutils.command.install'
# https://github.com/disqus/django-bitfield/pull/135
PYTHONWARNINGS+=',default:Attribute s is deprecated and will be removed in Python 3.14; use value instead:DeprecationWarning:__main__'
# https://github.com/jaysonsantos/python-binary-memcached/pull/257
PYTHONWARNINGS+=',ignore:urllib.parse.splitport() is deprecated as of 3.8:DeprecationWarning:bmemcached.protocol'
# https://github.com/boto/botocore/pull/3239
PYTHONWARNINGS+=',ignore:datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version.:DeprecationWarning:botocore.auth'
# https://bugs.launchpad.net/beautifulsoup/+bug/2076897
PYTHONWARNINGS+=',ignore:The '\''strip_cdata'\'' option of HTMLParser() has never done anything and will eventually be removed.:DeprecationWarning:bs4.builder._lxml'
# https://github.com/fabfuel/circuitbreaker/pull/63
PYTHONWARNINGS+=',ignore:datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version.:DeprecationWarning:circuitbreaker'
# This gets triggered due to our do_patch_activate_script
PYTHONWARNINGS+=',default:Attempting to work in a virtualenv.:UserWarning:IPython.core.interactiveshell'
# https://github.com/SAML-Toolkits/python3-saml/pull/420
PYTHONWARNINGS+=',ignore:datetime.datetime.utcfromtimestamp() is deprecated and scheduled for removal in a future version.:DeprecationWarning:onelogin.saml2.utils'
PYTHONWARNINGS+=',ignore:datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version.:DeprecationWarning:onelogin.saml2.utils'
# Probably due to ancient pip
PYTHONWARNINGS+=',default:DEPRECATION::pip._internal.models.link'
PYTHONWARNINGS+=',default:Unimplemented abstract methods:DeprecationWarning:pip._internal.metadata.importlib._dists'
PYTHONWARNINGS+=',default:module '\''sre_constants'\'' is deprecated:DeprecationWarning:pip._vendor.pyparsing'
PYTHONWARNINGS+=',default:Creating a LegacyVersion has been deprecated and will be removed in the next major release:DeprecationWarning:pip._vendor.packaging.version'
PYTHONWARNINGS+=',default:path is deprecated.:DeprecationWarning:pip._vendor.certifi.core'
PYTHONWARNINGS+=',default:ssl.PROTOCOL_TLS is deprecated:DeprecationWarning:pip._vendor.urllib3.util.ssl_'
PYTHONWARNINGS+=',default:Creating a LegacyVersion has been deprecated and will be removed in the next major release:DeprecationWarning:pip._vendor.packaging.specifiers'
PYTHONWARNINGS+=',default:path is deprecated.:DeprecationWarning:pip._vendor.pep517.wrappers'
PYTHONWARNINGS+=',default:The distutils package is deprecated and slated for removal in Python 3.12.:DeprecationWarning:pip._internal.locations'
PYTHONWARNINGS+=',default:The distutils.sysconfig module is deprecated:DeprecationWarning:pip._internal.locations'
PYTHONWARNINGS+=',default:ssl.match_hostname() is deprecated:DeprecationWarning:pip._vendor.urllib3.connection'
PYTHONWARNINGS+=',default:The distutils package is deprecated and slated for removal in Python 3.12.:DeprecationWarning:pip._internal.locations._distutils'
PYTHONWARNINGS+=',default:The distutils.sysconfig module is deprecated:DeprecationWarning:distutils.command.install'
PYTHONWARNINGS+=',default:The distutils package is deprecated and slated for removal in Python 3.12.:DeprecationWarning:pip._internal.cli.cmdoptions'
# https://github.com/python-openapi/openapi-core/issues/931
PYTHONWARNINGS+=',ignore::DeprecationWarning:openapi_core.validation.request.validators'
# pkg_resources deprecation
PYTHONWARNINGS+=',default:pkg_resources is deprecated as an API.:DeprecationWarning'
PYTHONWARNINGS+=',default:Deprecated call to `pkg_resources.declare_namespace(:DeprecationWarning:pkg_resources'
# https://github.com/seb-m/pyinotify/issues/204
PYTHONWARNINGS+=',ignore:The asyncore module is deprecated and will be removed in Python 3.12.:DeprecationWarning:pyinotify'
# Semgrep still supports Python 3.8
PYTHONWARNINGS+=',ignore:path is deprecated.:DeprecationWarning:semgrep.semgrep_core'
# Various warnings from setuptools
PYTHONWARNINGS+=',default:bdist_wheel.universal is deprecated:UserWarning'
PYTHONWARNINGS+=',ignore:setup.py install is deprecated.:UserWarning'
PYTHONWARNINGS+=',default:Unknown distribution option:UserWarning'
PYTHONWARNINGS+=',default:setuptools.installer and fetch_build_eggs are deprecated.:UserWarning'
PYTHONWARNINGS+=',default:The '\''wheel'\'' package is no longer the canonical location of the '\''bdist_wheel'\'' command:DeprecationWarning:wheel.bdist_wheel'
PYTHONWARNINGS+=',default:Package '\''integrations.:UserWarning'
PYTHONWARNINGS+=',default:Package '\''zulip.:UserWarning'
PYTHONWARNINGS+=',default:Could not find libsqlite3:UserWarning'
# https://github.com/scrapy/scrapy/issues/3288
PYTHONWARNINGS+=',ignore:Passing method to twisted.internet.ssl.CertificateOptions was deprecated in Twisted 17.1.0.:DeprecationWarning:scrapy.core.downloader.contextfactory'
# https://github.com/scrapy/scrapy/issues/6450
PYTHONWARNINGS+=',ignore:twisted.web.http.HTTPClient was deprecated in Twisted 24.7.0:DeprecationWarning:scrapy.core.downloader.webclient'
# https://github.com/adamchainz/time-machine/pull/486
PYTHONWARNINGS+=',ignore:datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version.:DeprecationWarning:time_machine'
# https://github.com/zulip/python-zulip-api/pull/833
PYTHONWARNINGS+=',default:distro.linux_distribution() is deprecated.:DeprecationWarning:zulip'
export SQLALCHEMY_WARN_20=1

View File

@ -1,18 +1,18 @@
#!/usr/bin/env bash
set -eu
version=3.9.0
version=3.10.0
arch="$(uname -m)"
case $arch in
"x86_64")
binary="shfmt_v${version}_linux_amd64"
sha256=d99b06506aee2ac9113daec3049922e70dc8cffb84658e3ae512c6a6cbe101b6
sha256=1f57a384d59542f8fac5f503da1f3ea44242f46dff969569e80b524d64b71dbc
;;
"aarch64")
binary="shfmt_v${version}_linux_arm64"
sha256=5e511463068f3d27ae1b087fb597fb9e8ad865be2ac501964a222a834fc1c463
sha256=9d23013d56640e228732fd2a04a9ede0ab46bc2d764bf22a4a35fb1b14d707a8
;;
esac

View File

@ -59,7 +59,8 @@ with test_server_running(
# Prepare the admin client
email = "iago@zulip.com" # Iago is an admin
realm = get_realm("zulip")
user = get_user(email, realm)
iago = get_user(email, realm)
user = iago
# Iago needs permission to manage all user groups.
admins_group = NamedUserGroup.objects.get(
@ -69,9 +70,10 @@ with test_server_running(
realm, "can_manage_all_groups", admins_group, acting_user=None
)
# Required to test can_create_users endpoints.
# Required to test can_create_users and can_change_user_emails endpoints.
user.can_create_users = True
user.save(update_fields=["can_create_users"])
user.can_change_user_emails = True
user.save(update_fields=["can_create_users", "can_change_user_emails"])
api_key = get_api_key(user)
site = "http://zulip.zulipdev.com:9981"
@ -85,6 +87,7 @@ with test_server_running(
email = "desdemona@zulip.com" # desdemona is an owner
realm = get_realm("zulip")
user = get_user(email, realm)
api_key = get_api_key(user)
site = "http://zulip.zulipdev.com:9981"
owner_client = Client(

View File

@ -94,7 +94,7 @@ EXEMPT_FILES = make_set(
"web/src/css_variables.js",
"web/src/custom_profile_fields_ui.ts",
"web/src/debug.ts",
"web/src/demo_organizations_ui.js",
"web/src/demo_organizations_ui.ts",
"web/src/deprecated_feature_notice.ts",
"web/src/desktop_integration.js",
"web/src/desktop_notifications.ts",
@ -199,7 +199,7 @@ EXEMPT_FILES = make_set(
"web/src/saved_snippets_ui.ts",
"web/src/scheduled_messages.ts",
"web/src/scheduled_messages_feed_ui.ts",
"web/src/scheduled_messages_overlay_ui.js",
"web/src/scheduled_messages_overlay_ui.ts",
"web/src/scheduled_messages_ui.ts",
"web/src/scroll_bar.ts",
"web/src/scroll_util.ts",
@ -210,7 +210,7 @@ EXEMPT_FILES = make_set(
"web/src/sentry.ts",
"web/src/server_events.js",
"web/src/settings.js",
"web/src/settings_account.js",
"web/src/settings_account.ts",
"web/src/settings_bots.ts",
"web/src/settings_components.ts",
"web/src/settings_emoji.ts",
@ -231,7 +231,7 @@ EXEMPT_FILES = make_set(
"web/src/settings_toggle.js",
"web/src/settings_ui.ts",
"web/src/settings_user_topics.ts",
"web/src/settings_users.js",
"web/src/settings_users.ts",
"web/src/setup.ts",
"web/src/sidebar_ui.ts",
"web/src/spectators.ts",
@ -243,7 +243,7 @@ EXEMPT_FILES = make_set(
"web/src/stream_color_events.ts",
"web/src/stream_create.ts",
"web/src/stream_create_subscribers.ts",
"web/src/stream_edit.js",
"web/src/stream_edit.ts",
"web/src/stream_edit_subscribers.ts",
"web/src/stream_edit_toggler.ts",
"web/src/stream_list.ts",
@ -292,7 +292,7 @@ EXEMPT_FILES = make_set(
"web/src/user_sort.ts",
"web/src/user_status.ts",
"web/src/user_status_ui.ts",
"web/src/user_topic_popover.js",
"web/src/user_topic_popover.ts",
"web/src/user_topics.ts",
"web/src/user_topics_ui.ts",
"web/src/views_util.ts",

View File

@ -34,7 +34,7 @@ DESKTOP_WARNING_VERSION = "5.9.3"
# new level means in api_docs/changelog.md, as well as "**Changes**"
# entries in the endpoint's documentation in `zulip.yaml`.
API_FEATURE_LEVEL = 312 # Last bumped for adding 'realm_export_consent' event type.
API_FEATURE_LEVEL = 319 # Last bumped for message-link class
# Bump the minor PROVISION_VERSION to indicate that folks should provision
# only when going from an old version of the code to a newer version. Bump
@ -49,4 +49,4 @@ API_FEATURE_LEVEL = 312 # Last bumped for adding 'realm_export_consent' event t
# historical commits sharing the same major version, in which case a
# minor version bump suffices.
PROVISION_VERSION = (295, 0) # bumped 2024-10-20 to upgrade Python requirements
PROVISION_VERSION = (298, 1) # bumped 2024-11-05 to upgrade shfmt

View File

@ -14,7 +14,7 @@ module.exports = {
[
"@babel/preset-env",
{
corejs: "3.37",
corejs: "3.39",
include: ["transform-optional-chaining"],
shippedProposals: true,
useBuiltIns: "usage",

View File

@ -404,21 +404,21 @@ async function test_stream_search_filters_stream_list(page: Page): Promise<void>
async function test_users_search(page: Page): Promise<void> {
console.log("Search users using right sidebar");
async function assert_in_list(page: Page, name: string): Promise<void> {
await page.waitForSelector(`#buddy-list-other-users li [data-name="${CSS.escape(name)}"]`, {
await page.waitForSelector(`#buddy-list-other-users li[data-name="${CSS.escape(name)}"]`, {
visible: true,
});
}
async function assert_selected(page: Page, name: string): Promise<void> {
await page.waitForSelector(
`#buddy-list-other-users li.highlighted_user [data-name="${CSS.escape(name)}"]`,
`#buddy-list-other-users li.highlighted_user[data-name="${CSS.escape(name)}"]`,
{visible: true},
);
}
async function assert_not_selected(page: Page, name: string): Promise<void> {
await page.waitForSelector(
`#buddy-list-other-users li.highlighted_user [data-name="${CSS.escape(name)}"]`,
`#buddy-list-other-users li.highlighted_user[data-name="${CSS.escape(name)}"]`,
{hidden: true},
);
}
@ -429,7 +429,7 @@ async function test_users_search(page: Page): Promise<void> {
await assert_in_list(page, "aaron");
// Enter the search box and test selected suggestion navigation
await page.click("#user_filter_icon");
await page.click(".user-list-filter");
await page.waitForSelector("#buddy-list-other-users .highlighted_user", {visible: true});
await assert_selected(page, "Desdemona");
await assert_not_selected(page, "Cordelia, Lear's daughter");
@ -452,7 +452,7 @@ async function test_users_search(page: Page): Promise<void> {
await arrow(page, "Down");
// Now Iago must be highlighted
await page.waitForSelector('#buddy-list-other-users li.highlighted_user [data-name="Iago"]', {
await page.waitForSelector('#buddy-list-other-users li.highlighted_user[data-name="Iago"]', {
visible: true,
});
await assert_not_selected(page, "King Hamlet");

Binary file not shown.

After

Width:  |  Height:  |  Size: 566 B

View File

@ -212,7 +212,7 @@ export function narrow_for_user_id(opts: {user_id: number}): void {
assert(narrow_by_email);
narrow_by_email(email);
assert(user_filter !== undefined);
user_filter.clear_and_hide_search();
user_filter.clear_search();
}
function keydown_enter_key(): void {
@ -274,9 +274,9 @@ export function initiate_search(): void {
}
}
export function escape_search(): void {
export function clear_search(): void {
if (user_filter) {
user_filter.clear_and_hide_search();
user_filter.clear_search();
}
}

View File

@ -1,11 +1,16 @@
import assert from "minimalistic-assert";
import * as add_subscribers_pill from "./add_subscribers_pill";
import * as input_pill from "./input_pill";
import * as keydown_util from "./keydown_util";
import type {User} from "./people";
import * as stream_pill from "./stream_pill";
import type {CombinedPill, CombinedPillContainer} from "./typeahead_helper";
import * as user_group_components from "./user_group_components";
import * as user_group_create_members_data from "./user_group_create_members_data";
import * as user_group_pill from "./user_group_pill";
import * as user_groups from "./user_groups";
import type {UserGroup} from "./user_groups";
import * as user_pill from "./user_pill";
function get_pill_user_ids(pill_widget: CombinedPillContainer): number[] {
@ -19,6 +24,80 @@ function get_pill_group_ids(pill_widget: CombinedPillContainer): number[] {
return group_user_ids;
}
export function create_item_from_text(
text: string,
current_items: CombinedPill[],
): CombinedPill | undefined {
const funcs = [
stream_pill.create_item_from_stream_name,
user_group_pill.create_item_from_group_name,
user_pill.create_item_from_email,
];
const stream_item = stream_pill.create_item_from_stream_name(text, current_items);
if (stream_item) {
return stream_item;
}
const group_item = user_group_pill.create_item_from_group_name(text, current_items);
if (group_item) {
const subgroup = user_groups.get_user_group_from_id(group_item.group_id);
const current_group_id = user_group_components.active_group_id;
assert(current_group_id !== undefined);
const current_group = user_groups.get_user_group_from_id(current_group_id);
if (user_groups.check_group_can_be_subgroup(subgroup, current_group)) {
return group_item;
}
return undefined;
}
for (const func of funcs) {
const item = func(text, current_items);
if (item) {
return item;
}
}
return undefined;
}
export function create({
$pill_container,
get_potential_members,
get_potential_groups,
}: {
$pill_container: JQuery;
get_potential_members: () => User[];
get_potential_groups: () => UserGroup[];
}): CombinedPillContainer {
const pill_widget = input_pill.create<CombinedPill>({
$container: $pill_container,
create_item_from_text,
get_text_from_item: add_subscribers_pill.get_text_from_item,
get_display_value_from_item: add_subscribers_pill.get_display_value_from_item,
generate_pill_html: add_subscribers_pill.generate_pill_html,
});
function get_users(): User[] {
const potential_members = get_potential_members();
return user_pill.filter_taken_users(potential_members, pill_widget);
}
function get_user_groups(): UserGroup[] {
const potential_groups = get_potential_groups();
return user_group_pill.filter_taken_groups(potential_groups, pill_widget);
}
add_subscribers_pill.set_up_pill_typeahead({
pill_widget,
$pill_container,
get_users,
get_user_groups,
});
add_subscribers_pill.set_up_handlers_for_add_button_state(pill_widget, $pill_container);
return pill_widget;
}
export function create_without_add_button({
$pill_container,
onPillCreateAction,

View File

@ -31,6 +31,8 @@ export function initialize(): void {
instance.setContent(parse_html(render_left_sidebar_stream_setting_popover()));
popover_menus.on_show_prep(instance);
$("#streams_header").addClass("showing-streams-popover");
// When showing the popover menu, we want the
// "Add channels" and the "Filter channels" tooltip
// to appear below the "Add channels" icon.
@ -55,6 +57,9 @@ export function initialize(): void {
onHidden(instance) {
instance.destroy();
popover_menus.popover_instances.stream_settings = null;
$("#streams_header").removeClass("showing-streams-popover");
// After the popover menu is closed, we want the
// "Add channels" and the "Filter channels" tooltip
// to appear at it's original position that is

View File

@ -11,6 +11,7 @@ import * as stream_pill from "./stream_pill";
import type {CombinedPill, CombinedPillContainer} from "./typeahead_helper";
import * as user_group_pill from "./user_group_pill";
import * as user_groups from "./user_groups";
import type {UserGroup} from "./user_groups";
import * as user_pill from "./user_pill";
export function create_item_from_text(
@ -51,17 +52,28 @@ export function set_up_pill_typeahead({
pill_widget,
$pill_container,
get_users,
get_user_groups,
}: {
pill_widget: CombinedPillContainer;
$pill_container: JQuery;
get_users: () => User[];
get_user_groups?: () => UserGroup[];
}): void {
const opts = {
const opts: {
user_source: () => User[];
stream: boolean;
user_group: boolean;
user: boolean;
user_group_source?: () => UserGroup[];
} = {
user_source: get_users,
stream: true,
user_group: true,
user: true,
};
if (get_user_groups !== undefined) {
opts.user_group_source = get_user_groups;
}
pill_typeahead.set_up_combined($pill_container.find(".input"), pill_widget, opts);
}
@ -88,6 +100,30 @@ export function generate_pill_html(item: CombinedPill): string {
return stream_pill.generate_pill_html(item);
}
export function set_up_handlers_for_add_button_state(
pill_widget: CombinedPillContainer,
$pill_container: JQuery,
): void {
const $pill_widget_input = $pill_container.find(".input");
const $pill_widget_button = $pill_container.parent().find(".add-users-button");
// Disable the add button first time the pill container is created.
$pill_widget_button.prop("disabled", true);
// If all the pills are removed, disable the add button.
pill_widget.onPillRemove(() =>
$pill_widget_button.prop("disabled", pill_widget.items().length === 0),
);
// Disable the add button when there is no pending text that can be converted
// into a pill and the number of existing pills is zero.
$pill_widget_input.on("input", () =>
$pill_widget_button.prop(
"disabled",
!pill_widget.is_pending() && pill_widget.items().length === 0,
),
);
}
export function create({
$pill_container,
get_potential_subscribers,
@ -109,23 +145,7 @@ export function create({
set_up_pill_typeahead({pill_widget, $pill_container, get_users});
const $pill_widget_input = $pill_container.find(".input");
const $pill_widget_button = $pill_container.parent().find(".add-users-button");
// Disable the add button first time the pill container is created.
$pill_widget_button.prop("disabled", true);
// If all the pills are removed, disable the add button.
pill_widget.onPillRemove(() =>
$pill_widget_button.prop("disabled", pill_widget.items().length === 0),
);
// Disable the add button when there is no pending text that can be converted
// into a pill and the number of existing pills is zero.
$pill_widget_input.on("input", () =>
$pill_widget_button.prop(
"disabled",
!pill_widget.is_pending() && pill_widget.items().length === 0,
),
);
set_up_handlers_for_add_button_state(pill_widget, $pill_container);
return pill_widget;
}

View File

@ -8,6 +8,7 @@ import * as bot_data from "./bot_data";
import * as demo_organizations_ui from "./demo_organizations_ui";
import {$t, get_language_name, language_list} from "./i18n";
import {page_params} from "./page_params";
import * as people from "./people";
import {realm_user_settings_defaults} from "./realm_user_settings_defaults";
import * as settings from "./settings";
import * as settings_bots from "./settings_bots";
@ -21,7 +22,6 @@ import * as settings_sections from "./settings_sections";
import * as settings_toggle from "./settings_toggle";
import * as settings_users from "./settings_users";
import {current_user, realm} from "./state_data";
import * as user_groups from "./user_groups";
const admin_settings_label = {
// Organization profile
@ -110,6 +110,7 @@ export function build_page() {
const options = {
custom_profile_field_types: realm.custom_profile_field_types,
full_name: current_user.full_name,
profile_picture: people.small_avatar_url_for_person(current_user),
realm_name: realm.realm_name,
realm_org_type: realm.realm_org_type,
realm_available_video_chat_providers: realm.realm_available_video_chat_providers,
@ -142,8 +143,6 @@ export function build_page() {
language_list,
realm_default_language_name: get_language_name(realm.realm_default_language),
realm_default_language_code: realm.realm_default_language,
realm_direct_message_initiator_group_id: realm.realm_direct_message_initiator_group,
realm_direct_message_permission_group_id: realm.realm_direct_message_permission_group,
realm_waiting_period_threshold: realm.realm_waiting_period_threshold,
realm_new_stream_announcements_stream_id: realm.realm_new_stream_announcements_stream_id,
realm_signup_announcements_stream_id: realm.realm_signup_announcements_stream_id,
@ -184,12 +183,6 @@ export function build_page() {
realm_invite_required: realm.realm_invite_required,
can_create_user_groups: settings_data.user_can_create_user_groups(),
policy_values: settings_config.common_policy_values,
realm_can_delete_any_message_group: realm.realm_can_delete_any_message_group,
realm_can_delete_own_message_group: realm.realm_can_delete_own_message_group,
realm_can_add_custom_emoji_group: realm.realm_can_add_custom_emoji_group,
realm_can_add_custom_emoji_group_name: user_groups.get_user_group_from_id(
realm.realm_can_add_custom_emoji_group,
).name,
...settings_org.get_organization_settings_options(),
demote_inactive_streams_values: settings_config.demote_inactive_streams_values,
web_mark_read_on_scroll_policy_values:
@ -282,10 +275,6 @@ export function build_page() {
tippy.default($("#realm_can_access_all_users_group_widget_container")[0], opts);
}
settings_org.check_disable_direct_message_initiator_group_dropdown(
realm.realm_direct_message_permission_group,
);
}
export function launch(section, user_settings_tab) {

View File

@ -92,7 +92,6 @@ function delete_attachments(attachment: string, file_name: string): void {
html_heading: $t_html({defaultMessage: "Delete file?"}),
html_body,
html_submit_button: $t_html({defaultMessage: "Delete"}),
id: "confirm_delete_file_modal",
focus_submit_on_open: true,
on_click() {
dialog_widget.submit_api_request(channel.del, "/json/attachments/" + attachment, {});

View File

@ -172,11 +172,13 @@ export type BuddyUserInfo = {
href: string;
name: string;
user_id: number;
profile_picture: string;
status_emoji_info: user_status.UserStatusEmojiInfo | undefined;
is_current_user: boolean;
num_unread: number;
user_circle_class: string;
status_text: string | undefined;
has_status_text: boolean;
user_list_style: {
COMPACT: boolean;
WITH_STATUS: boolean;
@ -204,10 +206,12 @@ export function info_for(user_id: number): BuddyUserInfo {
name: person.full_name,
user_id,
status_emoji_info,
profile_picture: people.small_avatar_url_for_person(person),
is_current_user: people.is_my_user_id(user_id),
num_unread: get_num_unread(user_id),
user_circle_class,
status_text,
has_status_text: Boolean(status_text),
user_list_style,
should_add_guest_user_indicator: people.should_add_guest_user_indicator(user_id),
};
@ -370,7 +374,7 @@ function filter_user_ids(user_filter_text: string, user_ids: number[]): number[]
return user_ids;
}
// If a query is present in "Search people", we return matches.
// If a query is present in "Filter users", we return matches.
const persons = user_ids.map((user_id) => people.get_by_user_id(user_id));
return [...people.filter_people_by_search_terms(persons, user_filter_text)];
}
@ -379,8 +383,7 @@ function get_filtered_user_id_list(
user_filter_text: string,
conversation_participants: Set<number>,
): number[] {
// We always want to show conversation participants even if they're inactive.
let base_user_id_list = [...conversation_participants];
let base_user_id_list = [];
if (user_filter_text) {
// If there's a filter, select from all users, not just those
@ -418,7 +421,9 @@ function get_filtered_user_id_list(
}
const user_ids = filter_user_ids(user_filter_text, base_user_id_list);
return user_ids;
// Make sure all the participants are in the list, even if they're inactive.
const user_ids_set = new Set([...user_ids, ...conversation_participants]);
return [...user_ids_set];
}
export function get_conversation_participants(): Set<number> {

View File

@ -77,9 +77,8 @@ type BuddyListRenderData = {
pm_ids_set: Set<number>;
total_human_subscribers_count: number;
other_users_count: number;
total_human_users: number;
hide_headers: boolean;
participant_ids_set: Set<number>;
all_participant_ids: Set<number>;
};
function get_render_data(): BuddyListRenderData {
@ -87,19 +86,17 @@ function get_render_data(): BuddyListRenderData {
const pm_ids_set = narrow_state.pm_ids_set();
const total_human_subscribers_count = get_total_human_subscriber_count(current_sub, pm_ids_set);
const total_human_users = people.get_active_human_count();
const other_users_count = total_human_users - total_human_subscribers_count;
const other_users_count = people.get_active_human_count() - total_human_subscribers_count;
const hide_headers = should_hide_headers(current_sub, pm_ids_set);
const participant_ids_set = buddy_data.get_conversation_participants();
const all_participant_ids = buddy_data.get_conversation_participants();
return {
current_sub,
pm_ids_set,
total_human_subscribers_count,
other_users_count,
total_human_users,
hide_headers,
participant_ids_set,
all_participant_ids,
};
}
@ -247,9 +244,8 @@ export class BuddyList extends BuddyListConf {
);
}
} else {
const total_human_users = people.get_active_human_count();
const other_users_count =
total_human_users - total_human_subscribers_count;
people.get_active_human_count() - total_human_subscribers_count;
tooltip_text = $t(
{
defaultMessage:
@ -285,26 +281,38 @@ export class BuddyList extends BuddyListConf {
// in already-sorted order.
this.all_user_ids = opts.all_user_ids;
this.fill_screen_with_content();
$("#buddy-list-users-matching-view-container .view-all-subscribers-link").remove();
$("#buddy-list-other-users-container .view-all-users-link").remove();
if (!buddy_data.get_is_searching_users()) {
this.render_view_user_list_links();
}
this.render_section_headers();
if (this.render_data.hide_headers) {
// Ensure the section isn't collapsed, because we're hiding its header
// so there's no way to collapse or uncollapse the list in this view.
$("#buddy-list-other-users-container").toggleClass("collapsed", false);
if (buddy_data.get_is_searching_users()) {
// Show all sections when searching users
this.set_section_collapse(".buddy-list-section-container", false);
} else {
$("#buddy-list-other-users-container").toggleClass(
"collapsed",
this.render_view_user_list_links();
this.set_section_collapse(
"#buddy-list-participants-container",
this.participants_is_collapsed,
);
this.set_section_collapse(
"#buddy-list-users-matching-view-container",
this.users_matching_view_is_collapsed,
);
this.set_section_collapse(
"#buddy-list-other-users-container",
this.other_users_is_collapsed,
);
this.update_empty_list_placeholders();
}
// Ensure the "other" section is visible when headers are collapsed,
// because we're hiding its header so there's no way to collapse or
// uncollapse the list in this view. Ensure we're showing/hiding as
// the user specified otherwise.
this.set_section_collapse(
"#buddy-list-other-users-container",
this.render_data.hide_headers ? false : this.other_users_is_collapsed,
);
this.update_empty_list_placeholders();
this.fill_screen_with_content();
}
update_empty_list_placeholders(): void {
@ -315,10 +323,13 @@ export class BuddyList extends BuddyListConf {
let matching_view_empty_list_message;
let other_users_empty_list_message;
// Only visible when searching, since we usually hide an empty participants section
let participants_empty_list_message;
if (buddy_data.get_is_searching_users()) {
matching_view_empty_list_message = $t({defaultMessage: "No matching users."});
other_users_empty_list_message = $t({defaultMessage: "No matching users."});
participants_empty_list_message = $t({defaultMessage: "No matching users."});
} else {
if (has_inactive_users_matching_view) {
matching_view_empty_list_message = $t({defaultMessage: "No active users."});
@ -333,36 +344,43 @@ export class BuddyList extends BuddyListConf {
}
}
$("#buddy-list-users-matching-view").attr(
"data-search-results-empty",
matching_view_empty_list_message,
);
if ($("#buddy-list-users-matching-view .empty-list-message").length) {
const empty_list_widget_html = render_empty_list_widget_for_list({
empty_list_message: matching_view_empty_list_message,
});
$("#buddy-list-users-matching-view").html(empty_list_widget_html);
function add_or_update_empty_list_placeholder(selector: string, message: string): void {
if (
$(selector).children().length === 0 ||
$(`${selector} .empty-list-message`).length
) {
const empty_list_widget_html = render_empty_list_widget_for_list({
empty_list_message: message,
});
$(selector).html(empty_list_widget_html);
}
}
$("#buddy-list-other-users").attr(
"data-search-results-empty",
add_or_update_empty_list_placeholder(
"#buddy-list-users-matching-view",
matching_view_empty_list_message,
);
add_or_update_empty_list_placeholder(
"#buddy-list-other-users",
other_users_empty_list_message,
);
if ($("#buddy-list-other-users .empty-list-message").length) {
const empty_list_widget_html = render_empty_list_widget_for_list({
empty_list_message: other_users_empty_list_message,
});
$("#buddy-list-other-users").html(empty_list_widget_html);
if (participants_empty_list_message) {
add_or_update_empty_list_placeholder(
"#buddy-list-participants",
participants_empty_list_message,
);
}
}
update_section_header_counts(): void {
const {total_human_subscribers_count, other_users_count, participant_ids_set} =
const {total_human_subscribers_count, other_users_count, all_participant_ids} =
this.render_data;
const subscriber_section_user_count =
total_human_subscribers_count - this.participant_user_ids.length;
total_human_subscribers_count - all_participant_ids.size;
const formatted_participants_count = get_formatted_sub_count(participant_ids_set.size);
const formatted_participants_count = get_formatted_sub_count(all_participant_ids.size);
const formatted_sub_users_count = get_formatted_sub_count(subscriber_section_user_count);
const formatted_other_users_count = get_formatted_sub_count(other_users_count);
@ -378,7 +396,7 @@ export class BuddyList extends BuddyListConf {
$("#buddy-list-participants-section-heading").attr(
"data-user-count",
participant_ids_set.size,
all_participant_ids.size,
);
$("#buddy-list-users-matching-view-section-heading").attr(
"data-user-count",
@ -391,11 +409,11 @@ export class BuddyList extends BuddyListConf {
}
render_section_headers(): void {
const {hide_headers, participant_ids_set} = this.render_data;
const {hide_headers, all_participant_ids} = this.render_data;
// We only show the participants list if it has members, so even if we're not
// changing filters and only updating user counts for the current filter, that
// can affect if we show/hide this section.
const show_participants_list = !hide_headers && participant_ids_set.size;
const show_participants_list = !hide_headers && all_participant_ids.size;
$("#buddy-list-participants-container").toggleClass("no-display", !show_participants_list);
// If we're not changing filters, this just means some users were added or
@ -407,8 +425,7 @@ export class BuddyList extends BuddyListConf {
}
this.current_filter = narrow_state.filter();
const {current_sub, total_human_subscribers_count, other_users_count, total_human_users} =
this.render_data;
const {current_sub, total_human_subscribers_count, other_users_count} = this.render_data;
$(".buddy-list-subsection-header").empty();
// If we're in the mode of hiding headers, that means we're only showing the "other users"
@ -424,20 +441,13 @@ export class BuddyList extends BuddyListConf {
$("#buddy-list-users-matching-view-container").toggleClass("no-display", true);
}
// Usually we show the user counts in the headers, but if we're hiding
// those headers then we show the total user count in the main title.
const default_userlist_title = $t({defaultMessage: "USERS"});
if (hide_headers) {
const formatted_count = get_formatted_sub_count(total_human_users);
const userlist_title = `${default_userlist_title} (${formatted_count})`;
$("#userlist-title").text(userlist_title);
return;
}
$("#userlist-title").text(default_userlist_title);
let header_text;
if (current_sub) {
if (participant_ids_set.size) {
if (all_participant_ids.size) {
header_text = $t({defaultMessage: "Others in this channel"});
} else {
header_text = $t({defaultMessage: "In this channel"});
@ -451,8 +461,7 @@ export class BuddyList extends BuddyListConf {
render_section_header({
id: "buddy-list-participants-section-heading",
header_text: $t({defaultMessage: "In this conversation"}),
user_count: get_formatted_sub_count(this.participant_user_ids.length),
toggle_class: "toggle-participants",
user_count: get_formatted_sub_count(all_participant_ids.size),
is_collapsed: this.participants_is_collapsed,
}),
),
@ -464,9 +473,8 @@ export class BuddyList extends BuddyListConf {
id: "buddy-list-users-matching-view-section-heading",
header_text,
user_count: get_formatted_sub_count(
total_human_subscribers_count - this.participant_user_ids.length,
total_human_subscribers_count - all_participant_ids.size,
),
toggle_class: "toggle-users-matching-view",
is_collapsed: this.users_matching_view_is_collapsed,
}),
),
@ -478,25 +486,28 @@ export class BuddyList extends BuddyListConf {
id: "buddy-list-other-users-section-heading",
header_text: $t({defaultMessage: "Others"}),
user_count: get_formatted_sub_count(other_users_count),
toggle_class: "toggle-other-users",
is_collapsed: this.other_users_is_collapsed,
}),
),
);
}
set_section_collapse(container_selector: string, is_collapsed: boolean): void {
$(container_selector).toggleClass("collapsed", is_collapsed);
$(`${container_selector} .buddy-list-section-toggle`).toggleClass(
"rotate-icon-down",
!is_collapsed,
);
$(`${container_selector} .buddy-list-section-toggle`).toggleClass(
"rotate-icon-right",
is_collapsed,
);
}
toggle_participants_section(): void {
this.participants_is_collapsed = !this.participants_is_collapsed;
$("#buddy-list-participants-container").toggleClass(
"collapsed",
this.participants_is_collapsed,
);
$("#buddy-list-participants-container .toggle-participants").toggleClass(
"rotate-icon-down",
!this.participants_is_collapsed,
);
$("#buddy-list-participants-container .toggle-participants").toggleClass(
"rotate-icon-right",
this.set_section_collapse(
"#buddy-list-participants-container",
this.participants_is_collapsed,
);
@ -507,16 +518,8 @@ export class BuddyList extends BuddyListConf {
toggle_users_matching_view_section(): void {
this.users_matching_view_is_collapsed = !this.users_matching_view_is_collapsed;
$("#buddy-list-users-matching-view-container").toggleClass(
"collapsed",
this.users_matching_view_is_collapsed,
);
$("#buddy-list-users-matching-view-container .toggle-users-matching-view").toggleClass(
"rotate-icon-down",
!this.users_matching_view_is_collapsed,
);
$("#buddy-list-users-matching-view-container .toggle-users-matching-view").toggleClass(
"rotate-icon-right",
this.set_section_collapse(
"#buddy-list-users-matching-view-container",
this.users_matching_view_is_collapsed,
);
@ -527,16 +530,8 @@ export class BuddyList extends BuddyListConf {
toggle_other_users_section(): void {
this.other_users_is_collapsed = !this.other_users_is_collapsed;
$("#buddy-list-other-users-container").toggleClass(
"collapsed",
this.other_users_is_collapsed,
);
$("#buddy-list-other-users-container .toggle-other-users").toggleClass(
"rotate-icon-down",
!this.other_users_is_collapsed,
);
$("#buddy-list-other-users-container .toggle-other-users").toggleClass(
"rotate-icon-right",
this.set_section_collapse(
"#buddy-list-other-users-container",
this.other_users_is_collapsed,
);
@ -566,7 +561,7 @@ export class BuddyList extends BuddyListConf {
for (const item of items) {
if (buddy_data.user_matches_narrow(item.user_id, pm_ids_set, current_sub?.stream_id)) {
if (this.render_data.participant_ids_set.has(item.user_id)) {
if (this.render_data.all_participant_ids.has(item.user_id)) {
participants.push(item);
this.participant_user_ids.push(item.user_id);
} else {
@ -581,6 +576,10 @@ export class BuddyList extends BuddyListConf {
this.$participants_list = $(this.participants_list_selector);
if (participants.length) {
// Remove the empty list message before adding users
if ($(`${this.participants_list_selector} .empty-list-message`).length > 0) {
this.$participants_list.empty();
}
const participants_html = this.items_to_html({
items: participants,
});
@ -826,7 +825,7 @@ export class BuddyList extends BuddyListConf {
list_user_id,
current_sub,
pm_ids_set,
this.render_data.participant_ids_set,
this.render_data.all_participant_ids,
) < 0,
);
return i === -1 ? user_id_list.length : i;
@ -938,7 +937,7 @@ export class BuddyList extends BuddyListConf {
current_sub?.stream_id,
);
let user_id_list;
if (this.render_data.participant_ids_set.has(user_id)) {
if (this.render_data.all_participant_ids.has(user_id)) {
user_id_list = this.participant_user_ids;
} else if (is_subscribed_user) {
user_id_list = this.users_matching_view_ids;
@ -963,7 +962,7 @@ export class BuddyList extends BuddyListConf {
html,
new_user_id,
is_subscribed_user,
is_participant_user: this.render_data.participant_ids_set.has(user_id),
is_participant_user: this.render_data.all_participant_ids.has(user_id),
});
}

View File

@ -440,6 +440,9 @@ export function initialize() {
if (e.metaKey || e.ctrlKey || e.shiftKey) {
return;
}
if ($(e.target).parents(".user-profile-picture").length === 1) {
return;
}
const $li = $(e.target).parents("li");
@ -515,8 +518,9 @@ export function initialize() {
// BUDDY LIST TOOLTIPS (not displayed on touch devices)
$(".buddy-list-section").on("mouseenter", ".selectable_sidebar_block", (e) => {
e.stopPropagation();
const $elem = $(e.currentTarget).closest(".user_sidebar_entry").find(".user-presence-link");
const user_id_string = $elem.attr("data-user-id");
const user_id_string = $(e.currentTarget)
.closest(".user_sidebar_entry")
.attr("data-user-id");
const title_data = buddy_data.get_title_data(user_id_string, false);
// `target_node` is the `ul` element since it stays in DOM even after updates.
@ -531,28 +535,26 @@ export function initialize() {
);
}
do_render_buddy_list_tooltip(
$elem.parent(),
title_data,
get_target_node,
check_reference_removed,
);
const $elem = $(e.currentTarget)
.closest(".user_sidebar_entry")
.find(".selectable_sidebar_block");
do_render_buddy_list_tooltip($elem, title_data, get_target_node, check_reference_removed);
/*
The following implements a little tooltip giving the name for status emoji
when hovering them in the right sidebar. This requires special logic, to avoid
conflicting with the main tooltip or showing duplicate tooltips.
*/
$(".user-presence-link .status-emoji-name").off("mouseenter").off("mouseleave");
$(".user-presence-link .status-emoji-name").on("mouseenter", () => {
const instance = $elem.parent()[0]._tippy;
$(".user_sidebar_entry .status-emoji-name").off("mouseenter").off("mouseleave");
$(".user_sidebar_entry .status-emoji-name").on("mouseenter", () => {
const instance = $elem[0]._tippy;
if (instance && instance.state.isVisible) {
instance.destroy();
}
});
$(".user-presence-link .status-emoji-name").on("mouseleave", () => {
$(".user_sidebar_entry .status-emoji-name").on("mouseleave", () => {
do_render_buddy_list_tooltip(
$elem.parent(),
$elem,
title_data,
get_target_node,
check_reference_removed,

View File

@ -34,10 +34,14 @@ export function adjust_mac_kbd_tags(kbd_elem_class: string): void {
let key_text = $(this).text();
// We use data-mac-key attribute to override the default key in case
// of exceptions. Currently, there are 2 shortcuts (for navigating back
// and forth in browser history) which need `Cmd` instead of the expected
// mapping (`Opt`) for the `Alt` key, so we use this attribute to override
// `Opt` with `Cmd`.
// of exceptions:
// - There are 2 shortcuts (for navigating back and forth in browser
// history) which need "⌘" instead of the expected mapping ("Opt")
// for the "Alt" key, so we use this attribute to override "Opt"
// with "⌘".
// - The "Ctrl" + "[" shortcuts (which match the Vim keybinding behavior
// of mapping to "Esc") need to display "Ctrl" for all users, so we
// use this attribute to override "⌘" with "Ctrl".
const replace_key = $(this).attr("data-mac-key") ?? keys_map.get(key_text);
if (replace_key !== undefined) {
key_text = replace_key;

View File

@ -259,7 +259,7 @@ function get_options_for_recipient_widget(): Option[] {
name: $t({defaultMessage: "Direct message"}),
};
if (!user_groups.is_empty_group(realm.realm_direct_message_permission_group)) {
if (!user_groups.is_setting_group_empty(realm.realm_direct_message_permission_group)) {
options.unshift(direct_messages_option);
} else {
options.push(direct_messages_option);

View File

@ -114,7 +114,7 @@ export function needs_subscribe_warning(user_id: number, stream_id: number): boo
export function check_dm_permissions_and_get_error_string(user_ids_string: string): string {
if (!people.user_can_direct_message(user_ids_string)) {
if (user_groups.is_empty_group(realm.realm_direct_message_permission_group)) {
if (user_groups.is_setting_group_empty(realm.realm_direct_message_permission_group)) {
return $t({
defaultMessage: "Direct messages are disabled in this organization.",
});
@ -569,6 +569,10 @@ export function validate_stream_message_mentions(opts: StreamWildcardOptions): b
}
export function validate_stream_message_address_info(sub: StreamSubscription): boolean {
if (sub.is_archived) {
compose_banner.show_stream_does_not_exist_error(sub.name);
return false;
}
if (sub.subscribed) {
return true;
}

View File

@ -666,24 +666,40 @@ export function try_stream_topic_syntax_text(text: string): string | null {
return null;
}
// Now we're sure that the URL is a valid stream topic URL.
// But the produced #**stream>topic** syntax could be broken.
const stream = stream_data.get_sub_by_id(stream_topic.stream_id);
assert(stream !== undefined);
const stream_name = stream.name;
if (topic_link_util.will_produce_broken_stream_topic_link(stream_name)) {
return null;
return topic_link_util.get_fallback_markdown_link(
stream_name,
stream_topic.topic_name,
stream_topic.message_id,
);
}
if (
stream_topic.topic_name !== undefined &&
topic_link_util.will_produce_broken_stream_topic_link(stream_topic.topic_name)
) {
return null;
return topic_link_util.get_fallback_markdown_link(
stream_name,
stream_topic.topic_name,
stream_topic.message_id,
);
}
let syntax_text = "#**" + stream_name;
if (stream_topic.topic_name) {
syntax_text += ">" + stream_topic.topic_name;
}
if (stream_topic.message_id !== undefined) {
syntax_text += "@" + stream_topic.message_id;
}
syntax_text += "**";
return syntax_text;
}

View File

@ -77,23 +77,22 @@ export function append_custom_profile_fields(element_id: string, user_id: number
}
}
export type PillUpdateField = {
type: number;
field_data: string;
hint: string;
id: number;
name: string;
order: number;
required: boolean;
display_in_profile_summary?: boolean | undefined;
};
export function initialize_custom_user_type_fields(
element_id: string,
user_id: number,
is_target_element_editable: boolean,
pill_update_handler?: (
field: {
type: number;
field_data: string;
hint: string;
id: number;
name: string;
order: number;
required: boolean;
display_in_profile_summary?: boolean | undefined;
},
pills: UserPillWidget,
) => void,
pill_update_handler?: (field: PillUpdateField, pills: UserPillWidget) => void,
): Map<number, UserPillWidget> {
const field_types = realm.custom_profile_field_types;
const user_pills = new Map<number, UserPillWidget>();

View File

@ -1,4 +1,5 @@
import $ from "jquery";
import {z} from "zod";
import render_convert_demo_organization_form from "../templates/settings/convert_demo_organization_form.hbs";
import render_demo_organization_warning from "../templates/settings/demo_organization_warning.hbs";
@ -11,9 +12,11 @@ import {get_demo_organization_deadline_days_remaining} from "./navbar_alerts";
import * as settings_config from "./settings_config";
import * as settings_data from "./settings_data";
import * as settings_org from "./settings_org";
import type {RequestOpts} from "./settings_ui";
import {current_user, realm} from "./state_data";
import type {HTMLSelectOneElement} from "./types";
export function insert_demo_organization_warning() {
export function insert_demo_organization_warning(): void {
const days_remaining = get_demo_organization_deadline_days_remaining();
const rendered_demo_organization_warning = render_demo_organization_warning({
is_demo_organization: realm.demo_organization_scheduled_deletion_date,
@ -23,7 +26,7 @@ export function insert_demo_organization_warning() {
$(".organization-box").find(".settings-section").prepend($(rendered_demo_organization_warning));
}
export function handle_demo_organization_conversion() {
export function handle_demo_organization_conversion(): void {
$(".convert-demo-organization-button").on("click", () => {
if (!current_user.is_owner) {
return;
@ -39,7 +42,7 @@ export function handle_demo_organization_conversion() {
realm_org_type_values: settings_org.get_org_type_dropdown_options(),
});
function demo_organization_conversion_post_render() {
function demo_organization_conversion_post_render(): void {
const $convert_submit_button = $(
"#demo-organization-conversion-modal .dialog_submit_button",
);
@ -53,8 +56,10 @@ export function handle_demo_organization_conversion() {
} else {
// Disable submit button if either form field blank.
$("#convert-demo-organization-form").on("input change", () => {
const string_id = $("#new_subdomain").val().trim();
const org_type = $("#add_organization_type").val();
const string_id = $<HTMLInputElement>("input#new_subdomain").val()!.trim();
const org_type = $<HTMLSelectOneElement>(
"select:not([multiple])#add_organization_type",
).val()!;
$convert_submit_button.prop(
"disabled",
string_id === "" ||
@ -65,15 +70,16 @@ export function handle_demo_organization_conversion() {
}
}
function submit_subdomain() {
function submit_subdomain(): void {
const $string_id = $("#new_subdomain");
const $organization_type = $("#add_organization_type");
const data = {
string_id: $string_id.val(),
org_type: $organization_type.val(),
};
const opts = {
success_continuation(data) {
const opts: RequestOpts = {
success_continuation(raw_data) {
const data = z.object({realm_url: z.string()}).parse(raw_data);
window.location.href = data.realm_url;
},
};

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