user_profile: Add the subscribe widget to user profile.

This commit adds a subscription widget to the user profile,
including the logic to prevent non-admin users from seeing the
subscription widget of other users. Additionally, as it is not
possible to subscribe generic bots to streams, and the user should
not be a deactivated user, we check for these conditions before
displaying the subscription widget.

To ensure that the alert for both subscribing and unsubscribing
appears on top of the subscribe widget, changed the location of
the alert to be displayed at the top.

Additionally, considering that no stream will be initially selected,
we have made the decision to disable the subscribe button. Once the
user selects a stream, we will enable the subscribe button
accordingly.

Changed the add_user_ids_to_stream function inside subscriber_api.js
to support self subscribe also, so that we don't have to duplicate the
logic in user_profile.js

Created a separate file for the subscribe widget called
user_profile_subscribe_widget.hbs.

Fixes: #18883
This commit is contained in:
palashb01 2023-09-08 23:07:58 +05:30 committed by Tim Abbott
parent d494f5bdaa
commit 4e786293ae
5 changed files with 130 additions and 1 deletions

View File

@ -1,4 +1,5 @@
import * as channel from "./channel";
import * as people from "./people";
/*
This module simply encapsulates our legacy API for subscribing
@ -10,6 +11,16 @@ import * as channel from "./channel";
export function add_user_ids_to_stream(user_ids, sub, success, failure) {
// TODO: use stream_id when backend supports it
const stream_name = sub.name;
if (user_ids.length === 1 && people.is_my_user_id(Number(user_ids[0]))) {
// Self subscribe
const color = sub.color;
return channel.post({
url: "/json/users/me/subscriptions",
data: {subscriptions: JSON.stringify([{name: stream_name, color}])},
success,
error: failure,
});
}
return channel.post({
url: "/json/users/me/subscriptions",
data: {

View File

@ -9,6 +9,7 @@ import * as browser_history from "./browser_history";
import * as buddy_data from "./buddy_data";
import * as channel from "./channel";
import * as components from "./components";
import * as dropdown_widget from "./dropdown_widget";
import * as hash_util from "./hash_util";
import {$t, $t_html} from "./i18n";
import * as ListWidget from "./list_widget";
@ -29,6 +30,7 @@ import * as user_pill from "./user_pill";
import * as util from "./util";
let user_streams_list_widget;
let user_profile_subscribe_widget;
function compare_by_name(a, b) {
return util.strcmp(a.name, b.name);
@ -72,6 +74,50 @@ function initialize_bot_owner(element_id, bot_id) {
return user_pills;
}
function render_user_profile_subscribe_widget() {
const opts = {
widget_name: "user_profile_subscribe",
get_options: get_user_unsub_streams,
item_click_callback: change_state_of_subscribe_button,
$events_container: $("#user-profile-modal"),
tippy_props: {
placement: "bottom-start",
},
};
user_profile_subscribe_widget = new dropdown_widget.DropdownWidget(opts);
user_profile_subscribe_widget.setup();
}
function change_state_of_subscribe_button(event, dropdown) {
dropdown.hide();
event.preventDefault();
event.stopPropagation();
user_profile_subscribe_widget.render();
const $subscribe_button = $("#user-profile-modal .add-subscription-button");
$subscribe_button.prop("disabled", false);
}
export function get_user_unsub_streams() {
const target_user_id = Number.parseInt($("#user-profile-modal").attr("data-user-id"), 10);
return stream_data
.get_streams_for_user(target_user_id)
.can_subscribe.filter((stream) => stream_data.can_subscribe_others(stream))
.map((stream) => ({
name: stream.name,
unique_id: stream.stream_id.toString(),
stream,
}))
.sort((a, b) => {
if (a.name.toLowerCase() < b.name.toLowerCase()) {
return -1;
}
if (a.name.toLowerCase() > b.name.toLowerCase()) {
return 1;
}
return 0;
});
}
function format_user_stream_list_item(stream, user) {
const show_unsubscribe_button =
people.can_admin_user(user) || stream_data.can_unsubscribe_others(stream);
@ -210,6 +256,14 @@ export function show_user_profile(user, default_tab_key = "profile-tab") {
.map((f) => get_custom_profile_field_data(user, f, field_types))
.filter((f) => f.name !== undefined);
const user_streams = stream_data.get_streams_for_user(user.user_id).subscribed;
// We only show the subscribe widget if the user is an admin, the user has opened their own profile,
// or if the user profile belongs to a bot whose owner has opened the user profile. However, we don't
// want to show the subscribe widget for generic bots since they are system bots and for deactivated users.
// Therefore, we also check for that condition.
const show_user_subscribe_widget =
(people.can_admin_user(user) || page_params.is_admin) &&
!user.is_system_bot &&
people.is_person_active(user.user_id);
const groups_of_user = user_groups.get_user_groups_of_user(user.user_id);
const args = {
user_id: user.user_id,
@ -228,6 +282,7 @@ export function show_user_profile(user, default_tab_key = "profile-tab") {
user_time: people.get_user_time(user.user_id),
user_type: people.get_user_type(user.user_id),
user_is_guest: user.is_guest,
show_user_subscribe_widget,
};
if (user.is_bot) {
@ -272,6 +327,9 @@ export function show_user_profile(user, default_tab_key = "profile-tab") {
render_user_group_list(groups_of_user, user);
break;
case "user-profile-streams-tab":
if (show_user_subscribe_widget) {
render_user_profile_subscribe_widget();
}
render_user_stream_list(user_streams, user);
break;
}
@ -281,6 +339,9 @@ export function show_user_profile(user, default_tab_key = "profile-tab") {
const $elem = components.toggle(opts).get();
$elem.addClass("large allow-overflow");
$("#tab-toggle").append($elem);
if (show_user_subscribe_widget) {
$("#user-profile-modal .add-subscription-button").prop("disabled", true);
}
}
function handle_remove_stream_subscription(target_user_id, sub, success, failure) {
@ -306,6 +367,38 @@ export function register_click_handlers() {
e.preventDefault();
});
$("body").on("click", "#user-profile-modal .add-subscription-button", (e) => {
e.preventDefault();
const stream_id = Number.parseInt(user_profile_subscribe_widget.value(), 10);
const sub = sub_store.get(stream_id);
const target_user_id = Number.parseInt($("#user-profile-modal").attr("data-user-id"), 10);
const $alert_box = $("#user-profile-streams-tab .stream_list_info");
function addition_success(data) {
if (Object.keys(data.subscribed).length > 0) {
ui_report.success(
$t_html({defaultMessage: "Subscribed successfully!"}),
$alert_box,
1200,
);
} else {
ui_report.client_error(
$t_html({defaultMessage: "Already subscribed."}),
$alert_box,
1200,
);
}
}
function addition_failure(xhr) {
ui_report.error("", xhr, $alert_box, 1200);
}
subscriber_api.add_user_ids_to_stream(
[target_user_id],
sub,
addition_success,
addition_failure,
);
});
$("body").on("click", "#user-profile-modal .remove-subscription-button", (e) => {
e.preventDefault();
const $stream_row = $(e.currentTarget).closest("[data-stream-id]");

View File

@ -526,6 +526,19 @@ ul {
.filter-icon i {
padding-right: 3px;
}
.subscribe-widget-header {
margin: 0;
display: inline-block;
font-weight: inherit;
}
.user_profile_subscribe_widget {
display: flex;
justify-content: space-between;
align-items: baseline;
padding-top: 2px;
}
}
.stream-list-top-section {

View File

@ -90,6 +90,13 @@
</div>
<div class="tabcontent" id="user-profile-streams-tab">
<div class="alert stream_list_info"></div>
{{#if show_user_subscribe_widget}}
<div class="header-section">
<h3 class="subscribe-widget-header">{{t 'Subscribe {full_name} to streams'}}</h3>
</div>
{{> user_profile_subscribe_widget}}
{{/if}}
<div class="stream-list-top-section">
<div class="header-section">
<h3 class="stream-list-header">{{t 'Subscribed streams' }}</h3>
@ -99,7 +106,6 @@
<i class="fa fa-remove" aria-hidden="true"></i>
</button>
</div>
<div class="alert stream_list_info"></div>
<div class="subscription-stream-list">
<table class="user-stream-list" data-empty="{{t 'No stream subscriptions.'}}"></table>
</div>

View File

@ -0,0 +1,6 @@
<div class="user_profile_subscribe_widget">
{{> dropdown_widget widget_name="user_profile_subscribe" default_text=(t 'Select a stream')}}
<button type="button" name="subscribe" class="add-subscription-button button small rounded btn-success">
{{t 'Subscribe' }}
</button>
</div>