diff --git a/web/src/add_group_members_pill.ts b/web/src/add_group_members_pill.ts index 348798500c..bf767ef061 100644 --- a/web/src/add_group_members_pill.ts +++ b/web/src/add_group_members_pill.ts @@ -1,8 +1,10 @@ +import $ from "jquery"; import assert from "minimalistic-assert"; import * as add_subscribers_pill from "./add_subscribers_pill.ts"; import * as input_pill from "./input_pill.ts"; import * as keydown_util from "./keydown_util.ts"; +import * as loading from "./loading.ts"; import * as people from "./people.ts"; import type {User} from "./people.ts"; import * as stream_pill from "./stream_pill.ts"; @@ -13,9 +15,9 @@ import * as user_groups from "./user_groups.ts"; import type {UserGroup} from "./user_groups.ts"; import * as user_pill from "./user_pill.ts"; -function get_pill_user_ids(pill_widget: CombinedPillContainer): number[] { +async function get_pill_user_ids(pill_widget: CombinedPillContainer): Promise { const user_ids = user_pill.get_user_ids(pill_widget); - const stream_user_ids = stream_pill.get_user_ids(pill_widget); + const stream_user_ids = await stream_pill.get_user_ids(pill_widget); return [...user_ids, ...stream_user_ids]; } @@ -108,14 +110,22 @@ export function create({ if (onPillCreateAction) { pill_widget.onPillCreate(() => { - onPillCreateAction(get_pill_user_ids(pill_widget), get_pill_group_ids(pill_widget)); + void (async () => { + loading.make_indicator($(".add-group-member-loading-spinner"), { + height: 56, // 4em at 14px / 1em + }); + const user_ids = await get_pill_user_ids(pill_widget); + onPillCreateAction(user_ids, get_pill_group_ids(pill_widget)); + loading.destroy_indicator($(".add-group-member-loading-spinner")); + })(); }); } if (onPillRemoveAction) { - pill_widget.onPillRemove(() => { - onPillRemoveAction(get_pill_user_ids(pill_widget), get_pill_group_ids(pill_widget)); - }); + void (async () => { + const user_ids = await get_pill_user_ids(pill_widget); + onPillRemoveAction(user_ids, get_pill_group_ids(pill_widget)); + })(); } function get_users(): User[] { @@ -175,9 +185,23 @@ export function set_up_handlers({ */ function callback(): void { const pill_widget = get_pill_widget(); - const pill_user_ids = get_pill_user_ids(pill_widget); - const pill_group_ids = get_pill_group_ids(pill_widget); - action({pill_user_ids, pill_group_ids}); + void (async () => { + loading.make_indicator($(".add-group-member-loading-spinner"), { + height: 56, // 4em at 14px / 1em + }); + const pill_user_ids = await get_pill_user_ids(pill_widget); + // If we're no longer in the same view after fetching + // subscriber data, don't update the UI. We don't need + // to destroy the loading spinner because the tab re-renders + // every time it opens, and also there might be a new tab + // with a current loading spinner. + if (get_pill_widget() !== pill_widget) { + return; + } + loading.destroy_indicator($(".add-group-member-loading-spinner")); + const pill_group_ids = get_pill_group_ids(pill_widget); + action({pill_user_ids, pill_group_ids}); + })(); } $parent_container.on("keyup", pill_selector, (e) => { diff --git a/web/src/add_subscribers_pill.ts b/web/src/add_subscribers_pill.ts index 7d1dc676e5..98fd94fb7e 100644 --- a/web/src/add_subscribers_pill.ts +++ b/web/src/add_subscribers_pill.ts @@ -1,8 +1,10 @@ +import $ from "jquery"; import assert from "minimalistic-assert"; import * as blueslip from "./blueslip.ts"; import * as input_pill from "./input_pill.ts"; import * as keydown_util from "./keydown_util.ts"; +import * as loading from "./loading.ts"; import type {User} from "./people.ts"; import * as pill_typeahead from "./pill_typeahead.ts"; import * as stream_pill from "./stream_pill.ts"; @@ -151,13 +153,23 @@ export function create({ if (onPillCreateAction) { pill_widget.onPillCreate(() => { - onPillCreateAction(get_pill_user_ids(pill_widget)); + void (async () => { + loading.make_indicator($(".add-subscriber-loading-spinner"), { + height: 56, // 4em at 14px / 1em + }); + const user_ids = await get_pill_user_ids(pill_widget); + onPillCreateAction(user_ids); + loading.destroy_indicator($(".add-subscriber-loading-spinner")); + })(); }); } if (onPillRemoveAction) { pill_widget.onPillRemove(() => { - onPillRemoveAction(get_pill_user_ids(pill_widget)); + void (async () => { + const user_ids = await get_pill_user_ids(pill_widget); + onPillRemoveAction(user_ids); + })(); }); } @@ -202,9 +214,9 @@ export function append_user_group_from_name( user_group_pill.append_user_group(user_group, pill_widget); } -function get_pill_user_ids(pill_widget: CombinedPillContainer): number[] { +async function get_pill_user_ids(pill_widget: CombinedPillContainer): Promise { const user_ids = user_pill.get_user_ids(pill_widget); - const stream_user_ids = stream_pill.get_user_ids(pill_widget); + const stream_user_ids = await stream_pill.get_user_ids(pill_widget); const group_user_ids = user_group_pill.get_user_ids(pill_widget); return [...user_ids, ...stream_user_ids, ...group_user_ids]; } @@ -251,8 +263,22 @@ export function set_up_handlers({ */ function callback(): void { const pill_widget = get_pill_widget(); - const pill_user_ids = get_pill_user_ids(pill_widget); - action({pill_user_ids}); + void (async () => { + loading.make_indicator($(".add-subscriber-loading-spinner"), { + height: 56, // 4em at 14px / 1em + }); + const pill_user_ids = await get_pill_user_ids(pill_widget); + // If we're no longer in the same view after fetching + // subscriber data, don't update the UI. We don't need + // to destroy the loading spinner because the tab re-renders + // every time it opens, and also there might be a new tab + // with a current loading spinner. + if (get_pill_widget() !== pill_widget) { + return; + } + loading.destroy_indicator($(".add-subscriber-loading-spinner")); + action({pill_user_ids}); + })(); } $parent_container.on("keyup", pill_selector, (e) => { diff --git a/web/src/stream_pill.ts b/web/src/stream_pill.ts index 71e50da502..92bda51e36 100644 --- a/web/src/stream_pill.ts +++ b/web/src/stream_pill.ts @@ -60,12 +60,25 @@ export function get_stream_name_from_item(item: StreamPill): string { return stream.name; } -export function get_user_ids(pill_widget: StreamPillWidget | CombinedPillContainer): number[] { - let user_ids = pill_widget - .items() - .flatMap((item) => - item.type === "stream" ? peer_data.get_subscribers(item.stream_id) : [], - ); +export async function get_user_ids( + pill_widget: StreamPillWidget | CombinedPillContainer, +): Promise { + const stream_ids = get_stream_ids(pill_widget); + const results = await Promise.all( + stream_ids.map(async (stream_id) => peer_data.get_all_subscribers(stream_id, true)), + ); + + const current_stream_ids_in_widget = get_stream_ids(pill_widget); + let user_ids: number[] = []; + for (const [index, stream_id] of stream_ids.entries()) { + const subscribers = results[index]!; + // Double check if the stream pill has been removed from the pill + // widget while we were doing fetches. + if (current_stream_ids_in_widget.includes(stream_id)) { + user_ids = [...user_ids, ...subscribers]; + } + } + user_ids = [...new Set(user_ids)]; user_ids.sort((a, b) => a - b); return user_ids; diff --git a/web/styles/subscriptions.css b/web/styles/subscriptions.css index 43e92d9d10..ecdcfe8f03 100644 --- a/web/styles/subscriptions.css +++ b/web/styles/subscriptions.css @@ -147,6 +147,19 @@ h4.user_group_setting_subsection_title { margin-top: 100px; } +#people_to_add_in_group, +#people_to_add, +.member_list_settings_container, +.subscriber_list_settings_container { + display: flex; + flex-direction: column; +} + +.add-group-member-loading-spinner, +.add-subscriber-loading-spinner { + align-self: center; +} + .member-list-box, .subscriber-list-box { text-align: center; diff --git a/web/templates/stream_settings/new_stream_users.hbs b/web/templates/stream_settings/new_stream_users.hbs index 26e68d1981..fa47794b10 100644 --- a/web/templates/stream_settings/new_stream_users.hbs +++ b/web/templates/stream_settings/new_stream_users.hbs @@ -11,6 +11,8 @@ autocomplete="off" placeholder="{{t 'Filter' }}" /> +
+
diff --git a/web/templates/stream_settings/stream_members.hbs b/web/templates/stream_settings/stream_members.hbs index 461c7286f0..f5274323d9 100644 --- a/web/templates/stream_settings/stream_members.hbs +++ b/web/templates/stream_settings/stream_members.hbs @@ -16,6 +16,7 @@ +
{{> stream_members_table .}}
diff --git a/web/templates/user_group_settings/new_user_group_users.hbs b/web/templates/user_group_settings/new_user_group_users.hbs index 0a9e79ed5b..43143504c5 100644 --- a/web/templates/user_group_settings/new_user_group_users.hbs +++ b/web/templates/user_group_settings/new_user_group_users.hbs @@ -9,6 +9,8 @@ autocomplete="off" placeholder="{{t 'Filter' }}" /> +
+
diff --git a/web/templates/user_group_settings/user_group_members.hbs b/web/templates/user_group_settings/user_group_members.hbs index 0caaf5f844..ec43f37f13 100644 --- a/web/templates/user_group_settings/user_group_members.hbs +++ b/web/templates/user_group_settings/user_group_members.hbs @@ -18,6 +18,7 @@ +
{{> user_group_members_table .}}
diff --git a/web/tests/stream_pill.test.cjs b/web/tests/stream_pill.test.cjs index 0aefea55a8..877f0278b1 100644 --- a/web/tests/stream_pill.test.cjs +++ b/web/tests/stream_pill.test.cjs @@ -132,11 +132,11 @@ run_test("get_stream_id", () => { assert.equal(stream_pill.get_stream_name_from_item(denmark_pill), denmark.name); }); -run_test("get_user_ids", () => { +run_test("get_user_ids", async () => { const items = [denmark_pill, sweden_pill]; const widget = {items: () => items}; - const user_ids = stream_pill.get_user_ids(widget); + const user_ids = await stream_pill.get_user_ids(widget); assert.deepEqual(user_ids, [1, 2, 3, 4, 5, 77]); });