2021-03-16 23:38:59 +01:00
|
|
|
import * as blueslip from "./blueslip";
|
2021-02-28 00:53:59 +01:00
|
|
|
import * as color_data from "./color_data";
|
|
|
|
import {FoldDict} from "./fold_dict";
|
2021-03-25 22:35:45 +01:00
|
|
|
import {page_params} from "./page_params";
|
2021-02-28 00:53:59 +01:00
|
|
|
import * as peer_data from "./peer_data";
|
|
|
|
import * as people from "./people";
|
|
|
|
import * as settings_config from "./settings_config";
|
2023-05-08 09:01:27 +02:00
|
|
|
import * as settings_data from "./settings_data";
|
2024-02-13 02:08:24 +01:00
|
|
|
import {current_user, realm} from "./state_data";
|
2021-04-15 16:35:14 +02:00
|
|
|
import * as sub_store from "./sub_store";
|
2023-07-25 19:22:22 +02:00
|
|
|
import type {
|
|
|
|
ApiStreamSubscription,
|
|
|
|
NeverSubscribedStream,
|
|
|
|
Stream,
|
2023-10-04 22:48:28 +02:00
|
|
|
StreamPostPolicy,
|
2023-07-25 19:22:22 +02:00
|
|
|
StreamSpecificNotificationSettings,
|
|
|
|
StreamSubscription,
|
|
|
|
} from "./sub_store";
|
2022-12-29 17:49:38 +01:00
|
|
|
import * as user_groups from "./user_groups";
|
2021-07-28 16:00:58 +02:00
|
|
|
import {user_settings} from "./user_settings";
|
2021-02-28 00:53:59 +01:00
|
|
|
import * as util from "./util";
|
2019-02-08 11:56:33 +01:00
|
|
|
|
2023-07-25 19:22:22 +02:00
|
|
|
type StreamInitParams = {
|
|
|
|
subscriptions: ApiStreamSubscription[];
|
|
|
|
unsubscribed: ApiStreamSubscription[];
|
|
|
|
never_subscribed: NeverSubscribedStream[];
|
|
|
|
realm_default_streams: Stream[];
|
|
|
|
};
|
|
|
|
|
|
|
|
// Type for the parameter of `create_sub_from_server_data` function.
|
2023-08-12 10:47:19 +02:00
|
|
|
type ApiGenericStreamSubscription =
|
|
|
|
| NeverSubscribedStream
|
|
|
|
| ApiStreamSubscription
|
|
|
|
| (Stream & {stream_weekly_traffic: number | null; subscribers: number[]});
|
2023-07-25 19:22:22 +02:00
|
|
|
|
2023-09-22 10:32:06 +02:00
|
|
|
export type InviteStreamData = {
|
2023-07-25 19:22:22 +02:00
|
|
|
name: string;
|
|
|
|
stream_id: number;
|
|
|
|
invite_only: boolean;
|
|
|
|
is_web_public: boolean;
|
|
|
|
default_stream: boolean;
|
|
|
|
};
|
|
|
|
|
2023-12-23 00:17:04 +01:00
|
|
|
export const DEFAULT_COLOR = "#c2c2c2";
|
2021-03-28 16:51:24 +02:00
|
|
|
|
2021-01-12 21:38:01 +01:00
|
|
|
// Expose get_subscriber_count for our automated puppeteer tests.
|
2021-02-28 00:53:59 +01:00
|
|
|
export const get_subscriber_count = peer_data.get_subscriber_count;
|
2021-01-12 21:38:01 +01:00
|
|
|
|
2023-07-25 19:22:22 +02:00
|
|
|
class BinaryDict<T> {
|
2019-12-27 15:56:46 +01:00
|
|
|
/*
|
|
|
|
A dictionary that keeps track of which objects had the predicate
|
|
|
|
return true or false for efficient lookups and iteration.
|
|
|
|
|
|
|
|
This class is an optimization for managing subscriptions.
|
|
|
|
Typically you only subscribe to a small minority of streams, and
|
|
|
|
most common operations want to efficiently iterate through only
|
|
|
|
streams where the current user is subscribed:
|
|
|
|
|
|
|
|
- search bar search
|
|
|
|
- build left sidebar
|
|
|
|
- autocomplete #stream_links
|
|
|
|
- autocomplete stream in compose
|
|
|
|
*/
|
|
|
|
|
2023-07-25 19:22:22 +02:00
|
|
|
trues = new Map<number, T>();
|
|
|
|
falses = new Map<number, T>();
|
|
|
|
pred: (v: T) => boolean;
|
2019-12-27 15:56:46 +01:00
|
|
|
|
2023-07-25 19:22:22 +02:00
|
|
|
constructor(pred: (v: T) => boolean) {
|
2020-07-23 02:42:29 +02:00
|
|
|
this.pred = pred;
|
|
|
|
}
|
2019-12-27 15:56:46 +01:00
|
|
|
|
2023-07-25 19:22:22 +02:00
|
|
|
true_values(): IterableIterator<T> {
|
2020-07-23 02:42:29 +02:00
|
|
|
return this.trues.values();
|
|
|
|
}
|
2019-12-27 15:56:46 +01:00
|
|
|
|
2023-07-25 19:22:22 +02:00
|
|
|
num_true_items(): number {
|
2020-07-23 02:42:29 +02:00
|
|
|
return this.trues.size;
|
|
|
|
}
|
2019-12-27 15:56:46 +01:00
|
|
|
|
2023-07-25 19:22:22 +02:00
|
|
|
false_values(): IterableIterator<T> {
|
2020-07-23 02:42:29 +02:00
|
|
|
return this.falses.values();
|
|
|
|
}
|
|
|
|
|
2023-07-25 19:22:22 +02:00
|
|
|
*values(): IterableIterator<T> {
|
2023-08-03 02:57:28 +02:00
|
|
|
yield* this.trues.values();
|
|
|
|
yield* this.falses.values();
|
2020-07-23 02:42:29 +02:00
|
|
|
}
|
2019-12-27 15:56:46 +01:00
|
|
|
|
2023-07-25 19:22:22 +02:00
|
|
|
get(k: number): T {
|
2020-07-23 02:42:29 +02:00
|
|
|
const res = this.trues.get(k);
|
2019-12-27 15:56:46 +01:00
|
|
|
|
|
|
|
if (res !== undefined) {
|
|
|
|
return res;
|
|
|
|
}
|
|
|
|
|
2023-07-25 19:22:22 +02:00
|
|
|
return this.falses.get(k)!;
|
2020-07-23 02:42:29 +02:00
|
|
|
}
|
2019-12-27 15:56:46 +01:00
|
|
|
|
2023-07-25 19:22:22 +02:00
|
|
|
set(k: number, v: T): void {
|
2020-07-23 02:42:29 +02:00
|
|
|
if (this.pred(v)) {
|
|
|
|
this.set_true(k, v);
|
2019-12-27 15:56:46 +01:00
|
|
|
} else {
|
2020-07-23 02:42:29 +02:00
|
|
|
this.set_false(k, v);
|
2019-12-27 15:56:46 +01:00
|
|
|
}
|
2020-07-23 02:42:29 +02:00
|
|
|
}
|
|
|
|
|
2023-07-25 19:22:22 +02:00
|
|
|
set_true(k: number, v: T): void {
|
2020-07-23 02:42:29 +02:00
|
|
|
this.falses.delete(k);
|
|
|
|
this.trues.set(k, v);
|
|
|
|
}
|
|
|
|
|
2023-07-25 19:22:22 +02:00
|
|
|
set_false(k: number, v: T): void {
|
2020-07-23 02:42:29 +02:00
|
|
|
this.trues.delete(k);
|
|
|
|
this.falses.set(k, v);
|
|
|
|
}
|
|
|
|
|
2023-07-25 19:22:22 +02:00
|
|
|
delete(k: number): void {
|
2020-07-23 02:42:29 +02:00
|
|
|
this.trues.delete(k);
|
|
|
|
this.falses.delete(k);
|
|
|
|
}
|
|
|
|
}
|
2013-09-16 23:47:05 +02:00
|
|
|
|
2023-05-26 02:14:06 +02:00
|
|
|
// The stream_info variable maps stream ids to stream properties objects
|
2013-09-16 23:47:05 +02:00
|
|
|
// Call clear_subscriptions() to initialize it.
|
2023-07-25 19:22:22 +02:00
|
|
|
let stream_info: BinaryDict<StreamSubscription>;
|
2013-09-16 23:47:05 +02:00
|
|
|
|
2023-07-25 19:22:22 +02:00
|
|
|
const stream_ids_by_name = new FoldDict<number>();
|
|
|
|
const stream_ids_by_old_names = new FoldDict<number>();
|
|
|
|
const default_stream_ids = new Set<number>();
|
2017-05-11 21:49:38 +02:00
|
|
|
|
2023-07-25 19:22:22 +02:00
|
|
|
export function clear_subscriptions(): void {
|
2021-01-12 16:05:24 +01:00
|
|
|
// This function is only used once at page load, and then
|
|
|
|
// it should only be used in tests.
|
2020-07-02 01:45:54 +02:00
|
|
|
stream_info = new BinaryDict((sub) => sub.subscribed);
|
2021-04-15 16:35:14 +02:00
|
|
|
sub_store.clear();
|
2021-02-28 00:53:59 +01:00
|
|
|
}
|
2016-06-04 22:40:25 +02:00
|
|
|
|
2021-02-28 00:53:59 +01:00
|
|
|
clear_subscriptions();
|
2013-08-15 21:11:07 +02:00
|
|
|
|
2023-07-25 19:22:22 +02:00
|
|
|
export function rename_sub(sub: StreamSubscription, new_name: string): void {
|
2019-11-02 00:06:25 +01:00
|
|
|
const old_name = sub.name;
|
2023-05-26 02:14:06 +02:00
|
|
|
stream_ids_by_old_names.set(old_name, sub.stream_id);
|
2017-05-11 21:49:38 +02:00
|
|
|
|
2016-10-30 17:33:23 +01:00
|
|
|
sub.name = new_name;
|
2023-05-26 02:14:06 +02:00
|
|
|
stream_info.set(sub.stream_id, sub);
|
|
|
|
stream_ids_by_name.delete(old_name);
|
|
|
|
stream_ids_by_name.set(new_name, sub.stream_id);
|
2021-02-28 00:53:59 +01:00
|
|
|
}
|
2016-10-30 17:33:23 +01:00
|
|
|
|
2023-07-25 19:22:22 +02:00
|
|
|
export function subscribe_myself(sub: StreamSubscription): void {
|
2019-11-02 00:06:25 +01:00
|
|
|
const user_id = people.my_current_user_id();
|
2021-01-12 21:38:01 +01:00
|
|
|
peer_data.add_subscriber(sub.stream_id, user_id);
|
2017-01-20 23:04:40 +01:00
|
|
|
sub.subscribed = true;
|
2017-04-28 15:59:30 +02:00
|
|
|
sub.newly_subscribed = true;
|
2023-05-26 02:14:06 +02:00
|
|
|
stream_info.set_true(sub.stream_id, sub);
|
2021-02-28 00:53:59 +01:00
|
|
|
}
|
2017-01-20 23:04:40 +01:00
|
|
|
|
2023-07-25 19:22:22 +02:00
|
|
|
export function unsubscribe_myself(sub: StreamSubscription): void {
|
2016-11-09 16:26:35 +01:00
|
|
|
// Remove user from subscriber's list
|
2019-11-02 00:06:25 +01:00
|
|
|
const user_id = people.my_current_user_id();
|
2021-01-12 21:38:01 +01:00
|
|
|
peer_data.remove_subscriber(sub.stream_id, user_id);
|
2016-11-09 16:26:35 +01:00
|
|
|
sub.subscribed = false;
|
2017-04-28 15:59:30 +02:00
|
|
|
sub.newly_subscribed = false;
|
2023-05-26 02:14:06 +02:00
|
|
|
stream_info.set_false(sub.stream_id, sub);
|
2021-02-28 00:53:59 +01:00
|
|
|
}
|
2016-11-09 16:26:35 +01:00
|
|
|
|
2023-07-25 19:22:22 +02:00
|
|
|
export function add_sub(sub: StreamSubscription): void {
|
2021-01-12 20:58:11 +01:00
|
|
|
// This function is currently used only by tests.
|
|
|
|
// We use create_sub_from_server_data at page load.
|
|
|
|
// We use create_streams for new streams in live-update events.
|
2023-05-26 02:14:06 +02:00
|
|
|
stream_info.set(sub.stream_id, sub);
|
|
|
|
stream_ids_by_name.set(sub.name, sub.stream_id);
|
2021-04-15 16:35:14 +02:00
|
|
|
sub_store.add_hydrated_sub(sub.stream_id, sub);
|
2021-02-28 00:53:59 +01:00
|
|
|
}
|
2013-08-15 21:11:07 +02:00
|
|
|
|
2023-07-25 19:22:22 +02:00
|
|
|
export function get_sub(stream_name: string): StreamSubscription | undefined {
|
2023-05-26 02:14:06 +02:00
|
|
|
const stream_id = stream_ids_by_name.get(stream_name);
|
|
|
|
if (stream_id) {
|
|
|
|
return stream_info.get(stream_id);
|
|
|
|
}
|
|
|
|
return undefined;
|
|
|
|
}
|
|
|
|
|
2023-07-25 19:22:22 +02:00
|
|
|
export function get_sub_by_id(stream_id: number): StreamSubscription | undefined {
|
2023-05-26 02:14:06 +02:00
|
|
|
if (!stream_id) {
|
|
|
|
return undefined;
|
|
|
|
}
|
|
|
|
return stream_info.get(stream_id);
|
2021-02-28 00:53:59 +01:00
|
|
|
}
|
2013-08-15 21:11:07 +02:00
|
|
|
|
2023-07-25 19:22:22 +02:00
|
|
|
export function get_stream_id(name: string): number | undefined {
|
2017-05-11 21:49:38 +02:00
|
|
|
// Note: Only use this function for situations where
|
|
|
|
// you are comfortable with a user dealing with an
|
|
|
|
// old name of a stream (from prior to a rename).
|
2023-05-26 02:14:06 +02:00
|
|
|
let stream_id = stream_ids_by_name.get(name);
|
|
|
|
if (!stream_id) {
|
|
|
|
stream_id = stream_ids_by_old_names.get(name);
|
2017-05-11 21:49:38 +02:00
|
|
|
}
|
|
|
|
return stream_id;
|
2021-02-28 00:53:59 +01:00
|
|
|
}
|
2017-05-11 21:49:38 +02:00
|
|
|
|
2023-07-26 22:07:21 +02:00
|
|
|
export function get_stream_name_from_id(stream_id: number): string {
|
|
|
|
return get_sub_by_id(stream_id)?.name ?? "";
|
|
|
|
}
|
|
|
|
|
2023-07-25 19:22:22 +02:00
|
|
|
export function get_sub_by_name(name: string): StreamSubscription | undefined {
|
2017-05-11 21:49:38 +02:00
|
|
|
// Note: Only use this function for situations where
|
|
|
|
// you are comfortable with a user dealing with an
|
|
|
|
// old name of a stream (from prior to a rename).
|
2023-05-26 02:14:06 +02:00
|
|
|
let stream_id = stream_ids_by_name.get(name);
|
|
|
|
if (!stream_id) {
|
|
|
|
stream_id = stream_ids_by_old_names.get(name);
|
2017-05-11 21:49:38 +02:00
|
|
|
}
|
|
|
|
if (!stream_id) {
|
2020-09-24 07:50:36 +02:00
|
|
|
return undefined;
|
2017-05-11 21:49:38 +02:00
|
|
|
}
|
|
|
|
|
2021-04-15 16:35:14 +02:00
|
|
|
return sub_store.get(stream_id);
|
2021-02-28 00:53:59 +01:00
|
|
|
}
|
2017-05-11 21:49:38 +02:00
|
|
|
|
2023-07-25 19:22:22 +02:00
|
|
|
export function name_to_slug(name: string): string {
|
2021-02-28 00:53:59 +01:00
|
|
|
const stream_id = get_stream_id(name);
|
2018-02-15 21:02:47 +01:00
|
|
|
|
|
|
|
if (!stream_id) {
|
|
|
|
return name;
|
|
|
|
}
|
|
|
|
|
|
|
|
// The name part of the URL doesn't really matter, so we try to
|
|
|
|
// make it pretty.
|
2022-02-28 20:54:06 +01:00
|
|
|
name = name.replaceAll(" ", "-");
|
2018-02-15 21:02:47 +01:00
|
|
|
|
2023-07-25 19:22:22 +02:00
|
|
|
return `${stream_id}-${name}`;
|
2021-02-28 00:53:59 +01:00
|
|
|
}
|
2018-02-15 21:02:47 +01:00
|
|
|
|
2023-07-25 19:22:22 +02:00
|
|
|
export function slug_to_name(slug: string): string {
|
2020-07-03 16:37:16 +02:00
|
|
|
/*
|
|
|
|
Modern stream slugs look like this, where 42
|
|
|
|
is a stream id:
|
|
|
|
|
|
|
|
42
|
|
|
|
42-stream-name
|
|
|
|
|
|
|
|
We have legacy slugs that are just the name
|
|
|
|
of the stream:
|
|
|
|
|
|
|
|
stream-name
|
|
|
|
|
|
|
|
And it's plausible that old stream slugs will have
|
|
|
|
be based on stream names that collide with modern
|
|
|
|
slugs:
|
|
|
|
|
|
|
|
4-horseman
|
|
|
|
411
|
|
|
|
2016-election
|
|
|
|
|
|
|
|
If there is any ambiguity about whether a stream slug
|
|
|
|
is old or modern, we prefer modern, as long as the integer
|
|
|
|
prefix matches a real stream id. Eventually we will
|
|
|
|
stop supporting the legacy slugs, which only matter now
|
|
|
|
because people have linked to Zulip threads in things like
|
|
|
|
GitHub conversations. We migrated to modern slugs in
|
|
|
|
early 2018.
|
|
|
|
*/
|
2021-02-10 14:31:52 +01:00
|
|
|
const m = /^(\d+)(-.*)?$/.exec(slug);
|
2018-02-15 21:02:47 +01:00
|
|
|
if (m) {
|
2020-10-07 09:17:30 +02:00
|
|
|
const stream_id = Number.parseInt(m[1], 10);
|
2021-04-15 16:35:14 +02:00
|
|
|
const sub = sub_store.get(stream_id);
|
2018-02-15 21:02:47 +01:00
|
|
|
if (sub) {
|
|
|
|
return sub.name;
|
|
|
|
}
|
|
|
|
// if nothing was found above, we try to match on the stream
|
|
|
|
// name in the somewhat unlikely event they had a historical
|
|
|
|
// link to a stream like 4-horsemen
|
|
|
|
}
|
|
|
|
|
2020-07-03 16:37:16 +02:00
|
|
|
/*
|
|
|
|
We are dealing with a pre-2018 slug that doesn't have the
|
|
|
|
stream id as a prefix.
|
|
|
|
*/
|
2018-02-15 21:02:47 +01:00
|
|
|
return slug;
|
2021-02-28 00:53:59 +01:00
|
|
|
}
|
2018-02-15 21:02:47 +01:00
|
|
|
|
2023-07-25 19:22:22 +02:00
|
|
|
export function delete_sub(stream_id: number): void {
|
2023-05-26 02:14:06 +02:00
|
|
|
if (!stream_info.get(stream_id)) {
|
2023-07-25 19:22:22 +02:00
|
|
|
blueslip.warn("Failed to archive stream " + stream_id.toString());
|
2017-02-16 03:47:08 +01:00
|
|
|
return;
|
|
|
|
}
|
2021-04-15 16:35:14 +02:00
|
|
|
sub_store.delete_sub(stream_id);
|
2023-05-26 02:14:06 +02:00
|
|
|
stream_info.delete(stream_id);
|
2021-02-28 00:53:59 +01:00
|
|
|
}
|
2013-08-21 23:21:31 +02:00
|
|
|
|
2023-07-25 19:22:22 +02:00
|
|
|
export function get_non_default_stream_names(): {name: string; unique_id: string}[] {
|
2023-03-02 01:58:25 +01:00
|
|
|
let subs = [...stream_info.values()];
|
2021-02-28 00:53:59 +01:00
|
|
|
subs = subs.filter((sub) => !is_default_stream_id(sub.stream_id) && !sub.invite_only);
|
2022-04-01 19:22:41 +02:00
|
|
|
const names = subs.map((sub) => ({
|
|
|
|
name: sub.name,
|
|
|
|
unique_id: sub.stream_id.toString(),
|
|
|
|
}));
|
2017-08-22 20:00:09 +02:00
|
|
|
return names;
|
2021-02-28 00:53:59 +01:00
|
|
|
}
|
2017-08-22 20:00:09 +02:00
|
|
|
|
2023-07-25 19:22:22 +02:00
|
|
|
export function get_unsorted_subs(): StreamSubscription[] {
|
2023-03-02 01:58:25 +01:00
|
|
|
return [...stream_info.values()];
|
2021-02-28 00:53:59 +01:00
|
|
|
}
|
2018-07-30 15:27:18 +02:00
|
|
|
|
2023-07-25 19:22:22 +02:00
|
|
|
export function num_subscribed_subs(): number {
|
2019-12-27 15:56:46 +01:00
|
|
|
return stream_info.num_true_items();
|
2021-02-28 00:53:59 +01:00
|
|
|
}
|
2019-12-27 15:56:46 +01:00
|
|
|
|
2023-07-25 19:22:22 +02:00
|
|
|
export function subscribed_subs(): StreamSubscription[] {
|
2023-03-02 01:58:25 +01:00
|
|
|
return [...stream_info.true_values()];
|
2021-02-28 00:53:59 +01:00
|
|
|
}
|
2013-08-15 21:11:07 +02:00
|
|
|
|
2023-07-25 19:22:22 +02:00
|
|
|
export function unsubscribed_subs(): StreamSubscription[] {
|
2023-03-02 01:58:25 +01:00
|
|
|
return [...stream_info.false_values()];
|
2021-02-28 00:53:59 +01:00
|
|
|
}
|
2016-10-25 21:45:19 +02:00
|
|
|
|
2023-07-25 19:22:22 +02:00
|
|
|
export function subscribed_streams(): string[] {
|
2021-02-28 00:53:59 +01:00
|
|
|
return subscribed_subs().map((sub) => sub.name);
|
|
|
|
}
|
2013-08-15 21:11:07 +02:00
|
|
|
|
2023-07-25 19:22:22 +02:00
|
|
|
export function subscribed_stream_ids(): number[] {
|
2021-02-28 00:53:59 +01:00
|
|
|
return subscribed_subs().map((sub) => sub.stream_id);
|
|
|
|
}
|
2020-07-25 18:20:54 +02:00
|
|
|
|
2023-07-25 19:22:22 +02:00
|
|
|
export function muted_stream_ids(): number[] {
|
2021-11-20 11:51:35 +01:00
|
|
|
return subscribed_subs()
|
|
|
|
.filter((sub) => sub.is_muted)
|
|
|
|
.map((sub) => sub.stream_id);
|
|
|
|
}
|
|
|
|
|
2023-07-29 00:49:51 +02:00
|
|
|
export function get_streams_for_user(user_id: number): {
|
|
|
|
subscribed: StreamSubscription[];
|
|
|
|
can_subscribe: StreamSubscription[];
|
|
|
|
} {
|
2021-06-17 22:26:17 +02:00
|
|
|
// Note that we only have access to subscribers of some streams
|
|
|
|
// depending on our role.
|
|
|
|
const all_subs = get_unsorted_subs();
|
|
|
|
const subscribed_subs = [];
|
2023-07-29 00:49:51 +02:00
|
|
|
const can_subscribe_subs = [];
|
2021-06-17 22:26:17 +02:00
|
|
|
for (const sub of all_subs) {
|
|
|
|
if (!can_view_subscribers(sub)) {
|
|
|
|
// Private streams that we have been removed from appear
|
|
|
|
// in get_unsorted_subs; we don't attempt to check their
|
|
|
|
// subscribers (which would trigger a warning).
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
if (is_user_subscribed(sub.stream_id, user_id)) {
|
|
|
|
subscribed_subs.push(sub);
|
2023-10-18 10:04:05 +02:00
|
|
|
} else if (can_subscribe_user(sub, user_id)) {
|
2023-07-29 00:49:51 +02:00
|
|
|
can_subscribe_subs.push(sub);
|
2021-06-17 22:26:17 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-07-29 00:49:51 +02:00
|
|
|
return {
|
|
|
|
subscribed: subscribed_subs,
|
|
|
|
can_subscribe: can_subscribe_subs,
|
|
|
|
};
|
2021-06-17 22:26:17 +02:00
|
|
|
}
|
|
|
|
|
2023-07-25 19:22:22 +02:00
|
|
|
export function get_invite_stream_data(): InviteStreamData[] {
|
|
|
|
function get_data(sub: StreamSubscription): InviteStreamData {
|
2019-01-10 17:57:35 +01:00
|
|
|
return {
|
|
|
|
name: sub.name,
|
|
|
|
stream_id: sub.stream_id,
|
|
|
|
invite_only: sub.invite_only,
|
2022-12-15 06:03:19 +01:00
|
|
|
is_web_public: sub.is_web_public,
|
2020-03-22 17:04:47 +01:00
|
|
|
default_stream: default_stream_ids.has(sub.stream_id),
|
2019-01-10 17:57:35 +01:00
|
|
|
};
|
2020-03-22 17:31:47 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
const streams = [];
|
|
|
|
|
|
|
|
// Invite users to all default streams...
|
|
|
|
for (const stream_id of default_stream_ids) {
|
2023-07-25 19:22:22 +02:00
|
|
|
const sub = sub_store.get(stream_id)!;
|
2020-03-22 17:31:47 +01:00
|
|
|
streams.push(get_data(sub));
|
|
|
|
}
|
|
|
|
|
|
|
|
// ...plus all your subscribed streams (avoiding repeats).
|
2021-02-28 00:53:59 +01:00
|
|
|
for (const sub of subscribed_subs()) {
|
2020-03-22 17:31:47 +01:00
|
|
|
if (!default_stream_ids.has(sub.stream_id)) {
|
|
|
|
streams.push(get_data(sub));
|
2019-01-10 17:57:35 +01:00
|
|
|
}
|
2020-03-22 17:31:47 +01:00
|
|
|
}
|
|
|
|
|
2019-01-10 17:57:35 +01:00
|
|
|
return streams;
|
2021-02-28 00:53:59 +01:00
|
|
|
}
|
2019-01-10 17:57:35 +01:00
|
|
|
|
2023-07-25 19:22:22 +02:00
|
|
|
export function get_colors(): string[] {
|
2021-02-28 00:53:59 +01:00
|
|
|
return subscribed_subs().map((sub) => sub.color);
|
|
|
|
}
|
2013-08-15 21:11:07 +02:00
|
|
|
|
2023-07-25 19:22:22 +02:00
|
|
|
export function update_stream_email_address(sub: StreamSubscription, email: string): void {
|
2018-04-05 19:58:27 +02:00
|
|
|
sub.email_address = email;
|
2021-02-28 00:53:59 +01:00
|
|
|
}
|
2018-04-05 19:58:27 +02:00
|
|
|
|
2023-07-25 19:22:22 +02:00
|
|
|
export function update_stream_post_policy(
|
|
|
|
sub: StreamSubscription,
|
|
|
|
stream_post_policy: StreamPostPolicy,
|
|
|
|
): void {
|
2020-02-04 21:50:55 +01:00
|
|
|
sub.stream_post_policy = stream_post_policy;
|
2021-02-28 00:53:59 +01:00
|
|
|
}
|
2019-05-07 07:12:14 +02:00
|
|
|
|
2023-07-25 19:22:22 +02:00
|
|
|
export function update_stream_privacy(
|
|
|
|
sub: StreamSubscription,
|
|
|
|
values: {
|
|
|
|
invite_only: boolean;
|
|
|
|
history_public_to_subscribers: boolean;
|
|
|
|
is_web_public: boolean;
|
|
|
|
},
|
|
|
|
): void {
|
2019-05-07 07:12:14 +02:00
|
|
|
sub.invite_only = values.invite_only;
|
|
|
|
sub.history_public_to_subscribers = values.history_public_to_subscribers;
|
2020-11-10 15:57:14 +01:00
|
|
|
sub.is_web_public = values.is_web_public;
|
2021-02-28 00:53:59 +01:00
|
|
|
}
|
2019-05-07 07:12:14 +02:00
|
|
|
|
2023-07-25 19:22:22 +02:00
|
|
|
export function update_message_retention_setting(
|
|
|
|
sub: StreamSubscription,
|
|
|
|
message_retention_days: number | null,
|
|
|
|
): void {
|
2020-06-15 17:00:00 +02:00
|
|
|
sub.message_retention_days = message_retention_days;
|
2021-02-28 00:53:59 +01:00
|
|
|
}
|
2020-06-15 17:00:00 +02:00
|
|
|
|
2023-07-25 19:22:22 +02:00
|
|
|
export function update_can_remove_subscribers_group_id(
|
|
|
|
sub: StreamSubscription,
|
|
|
|
can_remove_subscribers_group_id: number,
|
|
|
|
): void {
|
2023-07-12 12:57:57 +02:00
|
|
|
sub.can_remove_subscribers_group = can_remove_subscribers_group_id;
|
2022-12-29 18:36:06 +01:00
|
|
|
}
|
|
|
|
|
2023-07-25 19:22:22 +02:00
|
|
|
export function receives_notifications(
|
|
|
|
stream_id: number,
|
|
|
|
notification_name: keyof StreamSpecificNotificationSettings,
|
|
|
|
): boolean {
|
2021-04-15 16:35:14 +02:00
|
|
|
const sub = sub_store.get(stream_id);
|
2019-06-14 19:41:26 +02:00
|
|
|
if (sub === undefined) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
if (sub[notification_name] !== null) {
|
2023-07-25 19:22:22 +02:00
|
|
|
return sub[notification_name]!;
|
2019-06-14 19:41:26 +02:00
|
|
|
}
|
2022-08-25 21:09:32 +02:00
|
|
|
return user_settings[settings_config.generalize_stream_notification_setting[notification_name]];
|
2021-02-28 00:53:59 +01:00
|
|
|
}
|
2019-06-14 19:41:26 +02:00
|
|
|
|
2023-07-25 19:22:22 +02:00
|
|
|
export function all_subscribed_streams_are_in_home_view(): boolean {
|
2021-02-28 00:53:59 +01:00
|
|
|
return subscribed_subs().every((sub) => !sub.is_muted);
|
|
|
|
}
|
2013-08-15 21:11:07 +02:00
|
|
|
|
2023-07-25 19:22:22 +02:00
|
|
|
export function home_view_stream_names(): string[] {
|
2021-02-28 00:53:59 +01:00
|
|
|
const home_view_subs = subscribed_subs().filter((sub) => !sub.is_muted);
|
2020-07-02 01:39:34 +02:00
|
|
|
return home_view_subs.map((sub) => sub.name);
|
2021-02-28 00:53:59 +01:00
|
|
|
}
|
2014-01-15 20:59:31 +01:00
|
|
|
|
2023-07-25 19:22:22 +02:00
|
|
|
export function canonicalized_name(stream_name: string): string {
|
2018-05-06 21:43:17 +02:00
|
|
|
return stream_name.toString().toLowerCase();
|
2021-02-28 00:53:59 +01:00
|
|
|
}
|
2013-08-15 21:11:07 +02:00
|
|
|
|
2023-07-25 19:22:22 +02:00
|
|
|
export function get_color(stream_id: number): string {
|
2023-06-27 01:40:25 +02:00
|
|
|
const sub = get_sub_by_id(stream_id);
|
2013-08-15 21:11:07 +02:00
|
|
|
if (sub === undefined) {
|
2021-03-28 16:51:24 +02:00
|
|
|
return DEFAULT_COLOR;
|
2013-08-15 21:11:07 +02:00
|
|
|
}
|
|
|
|
return sub.color;
|
2021-02-28 00:53:59 +01:00
|
|
|
}
|
2013-08-15 21:11:07 +02:00
|
|
|
|
2023-07-25 19:22:22 +02:00
|
|
|
export function is_muted(stream_id: number): boolean {
|
2021-04-15 16:35:14 +02:00
|
|
|
const sub = sub_store.get(stream_id);
|
2019-05-21 09:33:21 +02:00
|
|
|
// Return true for undefined streams
|
|
|
|
if (sub === undefined) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
return sub.is_muted;
|
2021-02-28 00:53:59 +01:00
|
|
|
}
|
2017-05-13 20:54:53 +02:00
|
|
|
|
2023-07-25 19:22:22 +02:00
|
|
|
export function is_stream_muted_by_name(stream_name: string): boolean {
|
2021-02-28 00:53:59 +01:00
|
|
|
const sub = get_sub(stream_name);
|
2019-05-21 09:33:21 +02:00
|
|
|
// Return true for undefined streams
|
|
|
|
if (sub === undefined) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
return sub.is_muted;
|
2021-02-28 00:53:59 +01:00
|
|
|
}
|
2013-08-15 21:11:07 +02:00
|
|
|
|
2024-02-07 12:13:02 +01:00
|
|
|
export function is_new_stream_announcements_stream_muted(): boolean {
|
|
|
|
return is_muted(realm.realm_new_stream_announcements_stream_id);
|
2021-02-28 00:53:59 +01:00
|
|
|
}
|
2017-05-13 20:54:53 +02:00
|
|
|
|
2023-07-25 19:22:22 +02:00
|
|
|
export function can_toggle_subscription(sub: StreamSubscription): boolean {
|
2021-11-25 02:43:43 +01:00
|
|
|
// You can always remove your subscription if you're subscribed.
|
|
|
|
//
|
|
|
|
// One can only join a stream if it is public (!invite_only) and
|
|
|
|
// your role is Member or above (!is_guest).
|
2021-11-16 07:05:04 +01:00
|
|
|
// Spectators cannot subscribe to any streams.
|
2021-11-25 02:43:43 +01:00
|
|
|
//
|
|
|
|
// Note that the correctness of this logic relies on the fact that
|
|
|
|
// one cannot be subscribed to a deactivated stream, and
|
|
|
|
// deactivated streams are automatically made private during the
|
|
|
|
// archive stream process.
|
2021-11-16 07:05:04 +01:00
|
|
|
return (
|
2024-02-13 02:08:16 +01:00
|
|
|
(sub.subscribed || (!current_user.is_guest && !sub.invite_only)) &&
|
|
|
|
!page_params.is_spectator
|
2021-11-16 07:05:04 +01:00
|
|
|
);
|
2021-04-04 17:23:40 +02:00
|
|
|
}
|
|
|
|
|
2023-09-29 19:54:03 +02:00
|
|
|
export function can_access_stream_email(sub: StreamSubscription): boolean {
|
|
|
|
return (
|
2024-02-13 02:08:16 +01:00
|
|
|
(sub.subscribed || sub.is_web_public || (!current_user.is_guest && !sub.invite_only)) &&
|
2023-09-29 19:54:03 +02:00
|
|
|
!page_params.is_spectator
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2023-07-25 19:22:22 +02:00
|
|
|
export function can_access_topic_history(sub: StreamSubscription): boolean {
|
2022-10-18 19:01:58 +02:00
|
|
|
// Anyone can access topic history for web-public streams and
|
|
|
|
// subscriptions; additionally, members can access history for
|
|
|
|
// public streams.
|
|
|
|
return sub.is_web_public || can_toggle_subscription(sub);
|
|
|
|
}
|
|
|
|
|
2023-08-03 09:07:15 +02:00
|
|
|
export function can_preview(sub: StreamSubscription): boolean {
|
2023-12-22 01:07:57 +01:00
|
|
|
return sub.subscribed || !sub.invite_only || sub.previously_subscribed;
|
2021-04-04 17:36:38 +02:00
|
|
|
}
|
|
|
|
|
2023-07-25 19:22:22 +02:00
|
|
|
export function can_change_permissions(sub: StreamSubscription): boolean {
|
2024-02-13 02:08:16 +01:00
|
|
|
return current_user.is_admin && (!sub.invite_only || sub.subscribed);
|
2021-04-04 17:40:48 +02:00
|
|
|
}
|
|
|
|
|
2023-07-25 19:22:22 +02:00
|
|
|
export function can_view_subscribers(sub: StreamSubscription): boolean {
|
2021-04-04 17:52:09 +02:00
|
|
|
// Guest users can't access subscribers of any(public or private) non-subscribed streams.
|
2024-02-13 02:08:16 +01:00
|
|
|
return current_user.is_admin || sub.subscribed || (!current_user.is_guest && !sub.invite_only);
|
2021-04-04 17:52:09 +02:00
|
|
|
}
|
|
|
|
|
2023-07-25 19:22:22 +02:00
|
|
|
export function can_subscribe_others(sub: StreamSubscription): boolean {
|
2023-05-08 09:01:27 +02:00
|
|
|
// User can add other users to stream if stream is public or user is subscribed to stream
|
|
|
|
// and realm level setting allows user to add subscribers.
|
|
|
|
return (
|
2024-02-13 02:08:16 +01:00
|
|
|
!current_user.is_guest &&
|
2023-05-08 09:01:27 +02:00
|
|
|
(!sub.invite_only || sub.subscribed) &&
|
|
|
|
settings_data.user_can_subscribe_other_users()
|
|
|
|
);
|
2021-04-04 18:15:17 +02:00
|
|
|
}
|
|
|
|
|
2023-10-18 10:04:05 +02:00
|
|
|
export function can_subscribe_user(sub: StreamSubscription, user_id: number): boolean {
|
|
|
|
if (people.is_my_user_id(user_id)) {
|
|
|
|
return can_toggle_subscription(sub);
|
|
|
|
}
|
|
|
|
|
|
|
|
return can_subscribe_others(sub);
|
|
|
|
}
|
|
|
|
|
2023-07-25 19:22:22 +02:00
|
|
|
export function can_unsubscribe_others(sub: StreamSubscription): boolean {
|
2022-12-29 17:49:38 +01:00
|
|
|
// Whether the current user has permission to remove other users
|
|
|
|
// from the stream. Organization administrators can remove users
|
|
|
|
// from any stream; additionally, users who can access the stream
|
|
|
|
// and are in the stream's can_remove_subscribers_group can do so
|
|
|
|
// as well.
|
|
|
|
//
|
|
|
|
// TODO: The API allows the current user to remove bots that it
|
|
|
|
// administers from streams; so we might need to refactor this
|
|
|
|
// logic to accept a target_user_id parameter in order to support
|
|
|
|
// that in the UI.
|
|
|
|
|
|
|
|
// A user must be able to view subscribers in a stream in order to
|
|
|
|
// remove them. This check may never fire in practice, since the
|
|
|
|
// UI for removing subscribers generally is a list of the stream's
|
|
|
|
// subscribers.
|
|
|
|
if (!can_view_subscribers(sub)) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2024-02-13 02:08:16 +01:00
|
|
|
if (current_user.is_admin) {
|
2022-12-29 17:49:38 +01:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
return user_groups.is_user_in_group(
|
2023-07-12 12:57:57 +02:00
|
|
|
sub.can_remove_subscribers_group,
|
2022-12-29 17:49:38 +01:00
|
|
|
people.my_current_user_id(),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2023-07-25 19:22:22 +02:00
|
|
|
export function can_post_messages_in_stream(stream: StreamSubscription): boolean {
|
2022-11-10 01:45:15 +01:00
|
|
|
if (page_params.is_spectator) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2024-02-13 02:08:16 +01:00
|
|
|
if (current_user.is_admin) {
|
2021-12-27 10:27:03 +01:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2023-10-04 22:48:28 +02:00
|
|
|
if (stream.stream_post_policy === settings_config.stream_post_policy_values.admins.code) {
|
2021-12-27 10:27:03 +01:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2024-02-13 02:08:16 +01:00
|
|
|
if (current_user.is_moderator) {
|
2021-12-27 10:27:03 +01:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2023-10-04 22:48:28 +02:00
|
|
|
if (stream.stream_post_policy === settings_config.stream_post_policy_values.moderators.code) {
|
2021-12-27 10:27:03 +01:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (
|
2024-02-13 02:08:16 +01:00
|
|
|
current_user.is_guest &&
|
2023-10-04 22:48:28 +02:00
|
|
|
stream.stream_post_policy !== settings_config.stream_post_policy_values.everyone.code
|
2021-12-27 10:27:03 +01:00
|
|
|
) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
const person = people.get_by_user_id(people.my_current_user_id());
|
2023-07-25 19:22:22 +02:00
|
|
|
const current_datetime = Date.now();
|
|
|
|
const person_date_joined = new Date(person.date_joined).getTime();
|
2021-12-27 10:27:03 +01:00
|
|
|
const days = (current_datetime - person_date_joined) / 1000 / 86400;
|
|
|
|
if (
|
2023-10-04 22:48:28 +02:00
|
|
|
stream.stream_post_policy ===
|
|
|
|
settings_config.stream_post_policy_values.non_new_members.code &&
|
2024-02-13 02:08:24 +01:00
|
|
|
days < realm.realm_waiting_period_threshold
|
2021-12-27 10:27:03 +01:00
|
|
|
) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2023-08-03 09:07:15 +02:00
|
|
|
export function is_subscribed_by_name(stream_name: string): boolean {
|
2021-02-28 00:53:59 +01:00
|
|
|
const sub = get_sub(stream_name);
|
2023-08-03 09:07:15 +02:00
|
|
|
return sub ? sub.subscribed : false;
|
2021-02-28 00:53:59 +01:00
|
|
|
}
|
2013-08-15 21:11:07 +02:00
|
|
|
|
2023-08-03 09:07:15 +02:00
|
|
|
export function is_subscribed(stream_id: number): boolean {
|
2021-04-15 16:35:14 +02:00
|
|
|
const sub = sub_store.get(stream_id);
|
2023-08-03 09:07:15 +02:00
|
|
|
return sub ? sub.subscribed : false;
|
2021-02-28 00:53:59 +01:00
|
|
|
}
|
2017-05-14 16:32:18 +02:00
|
|
|
|
2023-07-25 19:22:22 +02:00
|
|
|
export function get_stream_privacy_policy(stream_id: number): string {
|
|
|
|
const sub = sub_store.get(stream_id)!;
|
2020-07-10 14:25:21 +02:00
|
|
|
|
2021-05-20 13:02:00 +02:00
|
|
|
if (sub.is_web_public) {
|
2023-10-04 22:48:28 +02:00
|
|
|
return settings_config.stream_privacy_policy_values.web_public.code;
|
2021-05-20 13:02:00 +02:00
|
|
|
}
|
2020-07-10 14:25:21 +02:00
|
|
|
if (!sub.invite_only) {
|
2023-10-04 22:48:28 +02:00
|
|
|
return settings_config.stream_privacy_policy_values.public.code;
|
2020-07-10 14:25:21 +02:00
|
|
|
}
|
|
|
|
if (sub.invite_only && !sub.history_public_to_subscribers) {
|
2023-10-04 22:48:28 +02:00
|
|
|
return settings_config.stream_privacy_policy_values.private.code;
|
2020-07-10 14:25:21 +02:00
|
|
|
}
|
2023-10-04 22:48:28 +02:00
|
|
|
return settings_config.stream_privacy_policy_values.private_with_public_history.code;
|
2021-02-28 00:53:59 +01:00
|
|
|
}
|
2020-07-10 14:25:21 +02:00
|
|
|
|
2023-08-03 09:07:15 +02:00
|
|
|
export function is_web_public(stream_id: number): boolean {
|
2021-05-20 13:02:00 +02:00
|
|
|
const sub = sub_store.get(stream_id);
|
2023-08-03 09:07:15 +02:00
|
|
|
return sub ? sub.is_web_public : false;
|
2021-05-20 13:02:00 +02:00
|
|
|
}
|
|
|
|
|
2023-08-06 00:00:41 +02:00
|
|
|
export function is_invite_only_by_stream_id(stream_id: number): boolean {
|
|
|
|
const sub = get_sub_by_id(stream_id);
|
2013-08-15 21:11:07 +02:00
|
|
|
if (sub === undefined) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
return sub.invite_only;
|
2021-02-28 00:53:59 +01:00
|
|
|
}
|
2013-08-15 21:11:07 +02:00
|
|
|
|
2023-07-25 19:22:22 +02:00
|
|
|
export function is_web_public_by_stream_name(stream_name: string): boolean {
|
2021-11-20 04:20:21 +01:00
|
|
|
const sub = get_sub(stream_name);
|
|
|
|
if (sub === undefined) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
return sub.is_web_public;
|
|
|
|
}
|
|
|
|
|
2023-07-25 19:22:22 +02:00
|
|
|
export function set_realm_default_streams(realm_default_streams: Stream[]): void {
|
2017-08-22 18:20:00 +02:00
|
|
|
default_stream_ids.clear();
|
|
|
|
|
2021-01-22 22:29:08 +01:00
|
|
|
for (const stream of realm_default_streams) {
|
2019-12-30 13:20:45 +01:00
|
|
|
default_stream_ids.add(stream.stream_id);
|
2021-01-22 22:29:08 +01:00
|
|
|
}
|
2021-02-28 00:53:59 +01:00
|
|
|
}
|
2017-08-22 18:20:00 +02:00
|
|
|
|
2023-07-25 19:22:22 +02:00
|
|
|
export function get_default_stream_ids(): number[] {
|
2023-03-02 01:58:25 +01:00
|
|
|
return [...default_stream_ids];
|
2021-02-28 00:53:59 +01:00
|
|
|
}
|
2018-07-22 11:30:38 +02:00
|
|
|
|
2023-07-25 19:22:22 +02:00
|
|
|
export function is_default_stream_id(stream_id: number): boolean {
|
2017-08-22 18:20:00 +02:00
|
|
|
return default_stream_ids.has(stream_id);
|
2021-02-28 00:53:59 +01:00
|
|
|
}
|
2017-03-21 21:10:20 +01:00
|
|
|
|
2023-07-25 19:22:22 +02:00
|
|
|
export function get_name(stream_name: string): string {
|
2013-08-19 19:25:44 +02:00
|
|
|
// This returns the actual name of a stream if we are subscribed to
|
2023-10-09 21:28:43 +02:00
|
|
|
// it (e.g. "Denmark" vs. "denmark"), while falling thru to
|
2013-08-19 19:25:44 +02:00
|
|
|
// stream_name if we don't have a subscription. (Stream names
|
|
|
|
// are case-insensitive, but we try to display the actual name
|
|
|
|
// when we know it.)
|
2017-05-11 23:25:42 +02:00
|
|
|
//
|
|
|
|
// This function will also do the right thing if we have
|
|
|
|
// an old stream name in memory for a recently renamed stream.
|
2021-02-28 00:53:59 +01:00
|
|
|
const sub = get_sub_by_name(stream_name);
|
2013-08-19 19:25:44 +02:00
|
|
|
if (sub === undefined) {
|
|
|
|
return stream_name;
|
|
|
|
}
|
|
|
|
return sub.name;
|
2021-02-28 00:53:59 +01:00
|
|
|
}
|
2013-08-19 19:25:44 +02:00
|
|
|
|
2023-12-22 01:37:24 +01:00
|
|
|
export function is_user_subscribed(stream_id: number, user_id: number): boolean {
|
2021-04-15 16:35:14 +02:00
|
|
|
const sub = sub_store.get(stream_id);
|
2021-04-04 17:52:09 +02:00
|
|
|
if (sub === undefined || !can_view_subscribers(sub)) {
|
2018-04-03 00:36:31 +02:00
|
|
|
// If we don't know about the stream, or we ourselves cannot access subscriber list,
|
2023-12-22 01:37:24 +01:00
|
|
|
// so we return false.
|
2020-07-15 00:34:28 +02:00
|
|
|
blueslip.warn(
|
|
|
|
"We got a is_user_subscribed call for a non-existent or inaccessible stream.",
|
|
|
|
);
|
2023-12-22 01:37:24 +01:00
|
|
|
return false;
|
2013-09-07 02:48:44 +02:00
|
|
|
}
|
2021-03-24 20:14:12 +01:00
|
|
|
if (user_id === undefined) {
|
2018-04-06 05:22:07 +02:00
|
|
|
blueslip.warn("Undefined user_id passed to function is_user_subscribed");
|
2023-12-22 01:37:24 +01:00
|
|
|
return false;
|
2016-10-30 15:47:20 +01:00
|
|
|
}
|
|
|
|
|
2021-01-12 21:38:01 +01:00
|
|
|
return peer_data.is_user_subscribed(stream_id, user_id);
|
2021-02-28 00:53:59 +01:00
|
|
|
}
|
2013-09-07 02:48:44 +02:00
|
|
|
|
2023-07-25 19:22:22 +02:00
|
|
|
export function create_streams(streams: Stream[]): void {
|
js: Automatically convert _.each to for…of.
This commit was automatically generated by the following script,
followed by lint --fix and a few small manual lint-related cleanups.
import * as babelParser from "recast/parsers/babel";
import * as recast from "recast";
import * as tsParser from "recast/parsers/typescript";
import { builders as b, namedTypes as n } from "ast-types";
import { Context } from "ast-types/lib/path-visitor";
import K from "ast-types/gen/kinds";
import { NodePath } from "ast-types/lib/node-path";
import assert from "assert";
import fs from "fs";
import path from "path";
import process from "process";
const checkExpression = (node: n.Node): node is K.ExpressionKind =>
n.Expression.check(node);
const checkStatement = (node: n.Node): node is K.StatementKind =>
n.Statement.check(node);
for (const file of process.argv.slice(2)) {
console.log("Parsing", file);
const ast = recast.parse(fs.readFileSync(file, { encoding: "utf8" }), {
parser: path.extname(file) === ".ts" ? tsParser : babelParser,
});
let changed = false;
let inLoop = false;
let replaceReturn = false;
const visitLoop = (...args: string[]) =>
function(this: Context, path: NodePath) {
for (const arg of args) {
this.visit(path.get(arg));
}
const old = { inLoop };
inLoop = true;
this.visit(path.get("body"));
inLoop = old.inLoop;
return false;
};
recast.visit(ast, {
visitDoWhileStatement: visitLoop("test"),
visitExpressionStatement(path) {
const { expression, comments } = path.node;
let valueOnly;
if (
n.CallExpression.check(expression) &&
n.MemberExpression.check(expression.callee) &&
!expression.callee.computed &&
n.Identifier.check(expression.callee.object) &&
expression.callee.object.name === "_" &&
n.Identifier.check(expression.callee.property) &&
["each", "forEach"].includes(expression.callee.property.name) &&
[2, 3].includes(expression.arguments.length) &&
checkExpression(expression.arguments[0]) &&
(n.FunctionExpression.check(expression.arguments[1]) ||
n.ArrowFunctionExpression.check(expression.arguments[1])) &&
[1, 2].includes(expression.arguments[1].params.length) &&
n.Identifier.check(expression.arguments[1].params[0]) &&
((valueOnly = expression.arguments[1].params[1] === undefined) ||
n.Identifier.check(expression.arguments[1].params[1])) &&
(expression.arguments[2] === undefined ||
n.ThisExpression.check(expression.arguments[2]))
) {
const old = { inLoop, replaceReturn };
inLoop = false;
replaceReturn = true;
this.visit(
path
.get("expression")
.get("arguments")
.get(1)
.get("body")
);
inLoop = old.inLoop;
replaceReturn = old.replaceReturn;
const [right, { body, params }] = expression.arguments;
const loop = b.forOfStatement(
b.variableDeclaration("let", [
b.variableDeclarator(
valueOnly ? params[0] : b.arrayPattern([params[1], params[0]])
),
]),
valueOnly
? right
: b.callExpression(
b.memberExpression(right, b.identifier("entries")),
[]
),
checkStatement(body) ? body : b.expressionStatement(body)
);
loop.comments = comments;
path.replace(loop);
changed = true;
}
this.traverse(path);
},
visitForStatement: visitLoop("init", "test", "update"),
visitForInStatement: visitLoop("left", "right"),
visitForOfStatement: visitLoop("left", "right"),
visitFunction(path) {
this.visit(path.get("params"));
const old = { replaceReturn };
replaceReturn = false;
this.visit(path.get("body"));
replaceReturn = old.replaceReturn;
return false;
},
visitReturnStatement(path) {
if (replaceReturn) {
assert(!inLoop); // could use labeled continue if this ever fires
const { argument, comments } = path.node;
if (argument === null) {
const s = b.continueStatement();
s.comments = comments;
path.replace(s);
} else {
const s = b.expressionStatement(argument);
s.comments = comments;
path.replace(s, b.continueStatement());
}
return false;
}
this.traverse(path);
},
visitWhileStatement: visitLoop("test"),
});
if (changed) {
console.log("Writing", file);
fs.writeFileSync(file, recast.print(ast).code, { encoding: "utf8" });
}
}
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-02-06 06:19:47 +01:00
|
|
|
for (const stream of streams) {
|
2016-10-30 15:47:20 +01:00
|
|
|
// We handle subscriber stuff in other events.
|
2020-06-21 15:23:43 +02:00
|
|
|
|
2020-02-09 04:21:30 +01:00
|
|
|
const attrs = {
|
2023-03-22 10:10:58 +01:00
|
|
|
stream_weekly_traffic: null,
|
2016-10-30 15:47:20 +01:00
|
|
|
subscribers: [],
|
2020-02-09 04:21:30 +01:00
|
|
|
...stream,
|
|
|
|
};
|
2023-08-12 10:47:19 +02:00
|
|
|
create_sub_from_server_data(attrs, false, false);
|
js: Automatically convert _.each to for…of.
This commit was automatically generated by the following script,
followed by lint --fix and a few small manual lint-related cleanups.
import * as babelParser from "recast/parsers/babel";
import * as recast from "recast";
import * as tsParser from "recast/parsers/typescript";
import { builders as b, namedTypes as n } from "ast-types";
import { Context } from "ast-types/lib/path-visitor";
import K from "ast-types/gen/kinds";
import { NodePath } from "ast-types/lib/node-path";
import assert from "assert";
import fs from "fs";
import path from "path";
import process from "process";
const checkExpression = (node: n.Node): node is K.ExpressionKind =>
n.Expression.check(node);
const checkStatement = (node: n.Node): node is K.StatementKind =>
n.Statement.check(node);
for (const file of process.argv.slice(2)) {
console.log("Parsing", file);
const ast = recast.parse(fs.readFileSync(file, { encoding: "utf8" }), {
parser: path.extname(file) === ".ts" ? tsParser : babelParser,
});
let changed = false;
let inLoop = false;
let replaceReturn = false;
const visitLoop = (...args: string[]) =>
function(this: Context, path: NodePath) {
for (const arg of args) {
this.visit(path.get(arg));
}
const old = { inLoop };
inLoop = true;
this.visit(path.get("body"));
inLoop = old.inLoop;
return false;
};
recast.visit(ast, {
visitDoWhileStatement: visitLoop("test"),
visitExpressionStatement(path) {
const { expression, comments } = path.node;
let valueOnly;
if (
n.CallExpression.check(expression) &&
n.MemberExpression.check(expression.callee) &&
!expression.callee.computed &&
n.Identifier.check(expression.callee.object) &&
expression.callee.object.name === "_" &&
n.Identifier.check(expression.callee.property) &&
["each", "forEach"].includes(expression.callee.property.name) &&
[2, 3].includes(expression.arguments.length) &&
checkExpression(expression.arguments[0]) &&
(n.FunctionExpression.check(expression.arguments[1]) ||
n.ArrowFunctionExpression.check(expression.arguments[1])) &&
[1, 2].includes(expression.arguments[1].params.length) &&
n.Identifier.check(expression.arguments[1].params[0]) &&
((valueOnly = expression.arguments[1].params[1] === undefined) ||
n.Identifier.check(expression.arguments[1].params[1])) &&
(expression.arguments[2] === undefined ||
n.ThisExpression.check(expression.arguments[2]))
) {
const old = { inLoop, replaceReturn };
inLoop = false;
replaceReturn = true;
this.visit(
path
.get("expression")
.get("arguments")
.get(1)
.get("body")
);
inLoop = old.inLoop;
replaceReturn = old.replaceReturn;
const [right, { body, params }] = expression.arguments;
const loop = b.forOfStatement(
b.variableDeclaration("let", [
b.variableDeclarator(
valueOnly ? params[0] : b.arrayPattern([params[1], params[0]])
),
]),
valueOnly
? right
: b.callExpression(
b.memberExpression(right, b.identifier("entries")),
[]
),
checkStatement(body) ? body : b.expressionStatement(body)
);
loop.comments = comments;
path.replace(loop);
changed = true;
}
this.traverse(path);
},
visitForStatement: visitLoop("init", "test", "update"),
visitForInStatement: visitLoop("left", "right"),
visitForOfStatement: visitLoop("left", "right"),
visitFunction(path) {
this.visit(path.get("params"));
const old = { replaceReturn };
replaceReturn = false;
this.visit(path.get("body"));
replaceReturn = old.replaceReturn;
return false;
},
visitReturnStatement(path) {
if (replaceReturn) {
assert(!inLoop); // could use labeled continue if this ever fires
const { argument, comments } = path.node;
if (argument === null) {
const s = b.continueStatement();
s.comments = comments;
path.replace(s);
} else {
const s = b.expressionStatement(argument);
s.comments = comments;
path.replace(s, b.continueStatement());
}
return false;
}
this.traverse(path);
},
visitWhileStatement: visitLoop("test"),
});
if (changed) {
console.log("Writing", file);
fs.writeFileSync(file, recast.print(ast).code, { encoding: "utf8" });
}
}
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-02-06 06:19:47 +01:00
|
|
|
}
|
2021-02-28 00:53:59 +01:00
|
|
|
}
|
2016-10-15 21:10:10 +02:00
|
|
|
|
2023-07-25 19:22:22 +02:00
|
|
|
export function clean_up_description(sub: StreamSubscription): void {
|
2021-04-04 19:07:25 +02:00
|
|
|
if (sub.rendered_description !== undefined) {
|
|
|
|
sub.rendered_description = sub.rendered_description.replace("<p>", "").replace("</p>", "");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-07-25 19:22:22 +02:00
|
|
|
export function create_sub_from_server_data(
|
|
|
|
attrs: ApiGenericStreamSubscription,
|
2023-08-12 10:47:19 +02:00
|
|
|
subscribed: boolean,
|
|
|
|
previously_subscribed: boolean,
|
2023-07-25 19:22:22 +02:00
|
|
|
): StreamSubscription {
|
2016-10-15 20:17:32 +02:00
|
|
|
if (!attrs.stream_id) {
|
2020-09-24 07:56:29 +02:00
|
|
|
// fail fast
|
|
|
|
throw new Error("We cannot create a sub without a stream_id");
|
2016-10-15 20:17:32 +02:00
|
|
|
}
|
|
|
|
|
2021-04-15 16:35:14 +02:00
|
|
|
let sub = sub_store.get(attrs.stream_id);
|
2020-06-21 15:23:43 +02:00
|
|
|
if (sub !== undefined) {
|
|
|
|
// We've already created this subscription, no need to continue.
|
|
|
|
return sub;
|
|
|
|
}
|
|
|
|
|
2016-10-15 20:17:32 +02:00
|
|
|
// Our internal data structure for subscriptions is mostly plain dictionaries,
|
|
|
|
// so we just reuse the attrs that are passed in to us, but we encapsulate how
|
2018-11-29 21:50:20 +01:00
|
|
|
// we handle subscribers. We defensively remove the `subscribers` field from
|
|
|
|
// the original `attrs` object, which will get thrown away. (We used to make
|
|
|
|
// a copy of the object with `_.omit(attrs, 'subscribers')`, but `_.omit` is
|
|
|
|
// slow enough to show up in timings when you have 1000s of streams.
|
|
|
|
|
2019-11-02 00:06:25 +01:00
|
|
|
const subscriber_user_ids = attrs.subscribers;
|
2016-10-15 20:17:32 +02:00
|
|
|
|
2018-11-29 21:50:20 +01:00
|
|
|
delete attrs.subscribers;
|
|
|
|
|
2020-02-09 04:21:30 +01:00
|
|
|
sub = {
|
2024-02-13 02:08:24 +01:00
|
|
|
render_subscribers: !realm.realm_is_zephyr_mirror_realm || attrs.invite_only,
|
2017-04-28 15:59:30 +02:00
|
|
|
newly_subscribed: false,
|
2019-05-15 08:54:25 +02:00
|
|
|
is_muted: false,
|
2021-10-08 13:51:55 +02:00
|
|
|
desktop_notifications: null,
|
|
|
|
audible_notifications: null,
|
|
|
|
push_notifications: null,
|
|
|
|
email_notifications: null,
|
|
|
|
wildcard_mentions_notify: null,
|
2023-07-25 19:22:22 +02:00
|
|
|
color: "color" in attrs ? attrs.color : color_data.pick_color(),
|
2023-08-12 10:47:19 +02:00
|
|
|
subscribed,
|
|
|
|
previously_subscribed,
|
2020-02-09 04:21:30 +01:00
|
|
|
...attrs,
|
|
|
|
};
|
2016-10-15 20:17:32 +02:00
|
|
|
|
2023-07-25 19:22:22 +02:00
|
|
|
peer_data.set_subscribers(sub.stream_id, subscriber_user_ids ?? []);
|
2016-10-15 20:17:32 +02:00
|
|
|
|
2021-04-04 19:07:25 +02:00
|
|
|
clean_up_description(sub);
|
2018-03-20 22:28:57 +01:00
|
|
|
|
2023-05-26 02:14:06 +02:00
|
|
|
stream_info.set(sub.stream_id, sub);
|
|
|
|
stream_ids_by_name.set(sub.name, sub.stream_id);
|
2021-04-15 16:35:14 +02:00
|
|
|
sub_store.add_hydrated_sub(sub.stream_id, sub);
|
2016-10-15 20:17:32 +02:00
|
|
|
|
|
|
|
return sub;
|
2021-02-28 00:53:59 +01:00
|
|
|
}
|
2016-10-15 20:17:32 +02:00
|
|
|
|
2023-07-25 19:22:22 +02:00
|
|
|
export function get_streams_for_admin(): StreamSubscription[] {
|
2017-08-22 20:51:41 +02:00
|
|
|
// Sort and combine all our streams.
|
2023-07-25 19:22:22 +02:00
|
|
|
function by_name(a: StreamSubscription, b: StreamSubscription): number {
|
2017-08-22 20:51:41 +02:00
|
|
|
return util.strcmp(a.name, b.name);
|
|
|
|
}
|
|
|
|
|
2023-03-02 01:58:25 +01:00
|
|
|
const subs = [...stream_info.values()];
|
2017-08-22 20:51:41 +02:00
|
|
|
|
|
|
|
subs.sort(by_name);
|
|
|
|
|
|
|
|
return subs;
|
2021-02-28 00:53:59 +01:00
|
|
|
}
|
2017-08-22 20:51:41 +02:00
|
|
|
|
2020-04-15 18:29:26 +02:00
|
|
|
/*
|
|
|
|
This module provides a common helper for finding the notification
|
2024-02-13 02:08:24 +01:00
|
|
|
stream, but we don't own the data. The `realm` structure
|
2020-04-15 18:29:26 +02:00
|
|
|
is the authoritative source of this data, and it will be updated by
|
|
|
|
server_events_dispatch in case of changes.
|
|
|
|
*/
|
2024-02-07 12:13:02 +01:00
|
|
|
export function realm_has_new_stream_announcements_stream(): boolean {
|
|
|
|
return realm.realm_new_stream_announcements_stream_id !== -1;
|
2021-02-28 00:53:59 +01:00
|
|
|
}
|
2020-04-15 18:29:26 +02:00
|
|
|
|
2024-02-07 12:13:02 +01:00
|
|
|
export function get_new_stream_announcements_stream(): string {
|
|
|
|
const stream_id = realm.realm_new_stream_announcements_stream_id;
|
2020-04-14 12:55:18 +02:00
|
|
|
if (stream_id !== -1) {
|
2021-04-15 16:35:14 +02:00
|
|
|
const stream_obj = sub_store.get(stream_id);
|
2020-04-14 12:55:18 +02:00
|
|
|
if (stream_obj) {
|
|
|
|
return stream_obj.name;
|
|
|
|
}
|
|
|
|
// We reach here when the notifications stream is a private
|
|
|
|
// stream the current user is not subscribed to.
|
|
|
|
}
|
2020-07-15 01:29:15 +02:00
|
|
|
return "";
|
2021-02-28 00:53:59 +01:00
|
|
|
}
|
2020-04-14 12:55:18 +02:00
|
|
|
|
2023-07-25 19:22:22 +02:00
|
|
|
export function initialize(params: StreamInitParams): void {
|
2020-02-25 12:16:26 +01:00
|
|
|
/*
|
|
|
|
We get `params` data, which is data that we "own"
|
2024-02-13 02:56:26 +01:00
|
|
|
and which has already been removed from `state_data`.
|
2020-02-25 12:16:26 +01:00
|
|
|
We only use it in this function to populate other
|
|
|
|
data structures.
|
|
|
|
*/
|
|
|
|
|
|
|
|
const subscriptions = params.subscriptions;
|
|
|
|
const unsubscribed = params.unsubscribed;
|
|
|
|
const never_subscribed = params.never_subscribed;
|
2020-03-22 17:32:31 +01:00
|
|
|
const realm_default_streams = params.realm_default_streams;
|
2020-02-25 12:16:26 +01:00
|
|
|
|
|
|
|
/*
|
2024-02-13 02:08:24 +01:00
|
|
|
We also consume some data directly from `realm`.
|
2020-02-25 12:16:26 +01:00
|
|
|
This data can be accessed by any other module,
|
|
|
|
and we consider the authoritative source to be
|
2024-02-13 02:08:24 +01:00
|
|
|
`realm`. Some of this data should eventually
|
2020-03-22 17:32:31 +01:00
|
|
|
be fully handled by stream_data.
|
2020-02-25 12:16:26 +01:00
|
|
|
*/
|
|
|
|
|
|
|
|
color_data.claim_colors(subscriptions);
|
2018-11-28 23:12:40 +01:00
|
|
|
|
2023-07-25 19:22:22 +02:00
|
|
|
function populate_subscriptions(
|
|
|
|
subs: ApiStreamSubscription[] | NeverSubscribedStream[],
|
|
|
|
subscribed: boolean,
|
|
|
|
previously_subscribed: boolean,
|
|
|
|
): void {
|
2021-01-22 22:29:08 +01:00
|
|
|
for (const sub of subs) {
|
2023-08-12 10:47:19 +02:00
|
|
|
create_sub_from_server_data(sub, subscribed, previously_subscribed);
|
2021-01-22 22:29:08 +01:00
|
|
|
}
|
Clean up startup code for streams.
The startup code in subs.js used to intermingle data
stuff and UI stuff in a loop inside a called function,
which made the code hard to reason about.
Now there is a clear separation of concerns, with these methods
being called in succession:
stream_data.initialize_from_page_params();
stream_list.create_initial_sidebar_rows();
The first method was mostly extracted from subs.js, but I simplified
some things, like not needing to make a copy of the hashes
we were passed in, plus I now garbage collect email_dict. Also,
the code path that initialize_from_page_params() mostly replaces
used to call create_sub(), which fired a trigger, but now it
just does data stuff.
Once the data structure is built up, it's a very simple matter
to build the initial sidebar rows, and that's what the second
method does.
2016-10-17 19:34:58 +02:00
|
|
|
}
|
|
|
|
|
2021-02-28 00:53:59 +01:00
|
|
|
set_realm_default_streams(realm_default_streams);
|
2017-03-21 21:10:20 +01:00
|
|
|
|
2020-02-25 12:16:26 +01:00
|
|
|
populate_subscriptions(subscriptions, true, true);
|
|
|
|
populate_subscriptions(unsubscribed, false, true);
|
|
|
|
populate_subscriptions(never_subscribed, false, false);
|
2021-02-28 00:53:59 +01:00
|
|
|
}
|
Clean up startup code for streams.
The startup code in subs.js used to intermingle data
stuff and UI stuff in a loop inside a called function,
which made the code hard to reason about.
Now there is a clear separation of concerns, with these methods
being called in succession:
stream_data.initialize_from_page_params();
stream_list.create_initial_sidebar_rows();
The first method was mostly extracted from subs.js, but I simplified
some things, like not needing to make a copy of the hashes
we were passed in, plus I now garbage collect email_dict. Also,
the code path that initialize_from_page_params() mostly replaces
used to call create_sub(), which fired a trigger, but now it
just does data stuff.
Once the data structure is built up, it's a very simple matter
to build the initial sidebar rows, and that's what the second
method does.
2016-10-17 19:34:58 +02:00
|
|
|
|
2023-07-25 19:22:22 +02:00
|
|
|
export function remove_default_stream(stream_id: number): void {
|
2019-12-30 13:20:45 +01:00
|
|
|
default_stream_ids.delete(stream_id);
|
2021-02-28 00:53:59 +01:00
|
|
|
}
|
2023-06-20 11:30:30 +02:00
|
|
|
|
2023-07-25 19:22:22 +02:00
|
|
|
export function get_options_for_dropdown_widget(): {
|
|
|
|
name: string;
|
|
|
|
unique_id: number;
|
|
|
|
stream: StreamSubscription;
|
|
|
|
}[] {
|
2023-07-03 08:42:43 +02:00
|
|
|
return subscribed_subs()
|
|
|
|
.map((stream) => ({
|
|
|
|
name: stream.name,
|
|
|
|
unique_id: stream.stream_id,
|
|
|
|
stream,
|
|
|
|
}))
|
|
|
|
.sort((a, b) => util.strcmp(a.name.toLowerCase(), b.name.toLowerCase()));
|
2023-06-20 11:30:30 +02:00
|
|
|
}
|