Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 30 additions & 23 deletions app/controllers/discourse_mod_categories/messages_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -271,21 +271,37 @@ def notes_feed_seen
.update_all(read: true)

# Push the recalculated bell counts to every open tab so they refresh
# in lockstep with the shield tab being opened.
# in lockstep with the shield tab being opened. This also drops the
# in-dropdown shield-tab pip, which now derives from unread Notification
# rows (the same source the bell uses), so it stays in lockstep with
# the bell badge without a dedicated MessageBus channel.
current_user.publish_notifications_state if marked > 0

# Reset any other browser tabs/devices the staff member has open so
# their header pip + title prefix clears in lockstep, not on the next
# page load.
MessageBus.publish(
"/mod-note-unread-count/#{current_user.id}",
{ reset: true },
user_ids: [current_user.id],
)

render json: success_json
end

# Resolves a badge id to the current set of usernames who hold it.
# Used by the PM composer "Add badge group" button to splice badge
# holders into the standard `target_recipients` field — the PM is then
# sent through the normal PostCreator path with no further plugin code.
# Self is excluded (no point messaging yourself); the list is deduped.
def badge_members
guardian.ensure_can_send_private_messages!
badge = Badge.find_by(id: params[:badge_id])
raise Discourse::NotFound unless badge

usernames =
User
.joins(:user_badges)
.where(user_badges: { badge_id: badge.id })
.where(active: true)
.where.not(id: current_user.id)
.distinct
.pluck(:username)

render json: { usernames: usernames, badge: { id: badge.id, name: badge.display_name } }
end

# Adds a user to a topic's cumulative whisper conversation. From then on
# that user sees every whisper in the topic (both Guardian#can_see_post?
# and the topic-stream SQL filter grant visibility to participants).
Expand Down Expand Up @@ -387,22 +403,13 @@ def notify_staff_of_note(topic)
)

publish_note_alert(staff_user, topic, note, note_url)
publish_unread_count_bump(staff_user)
# The standard /notifications poll picks up the new unread row so
# both the bell dot and the in-dropdown shield-tab pip refresh
# together. No separate /mod-note-unread-count channel is needed.
staff_user.publish_notifications_state
end
end

# Publishes a small "+1" payload on a dedicated MessageBus channel the
# header pip / title-prefix subscriber listens on. A separate channel
# (independent of `/notification-alert/`) keeps the client-side reactivity
# focused on the moderator-notes counter rather than the bell badge.
def publish_unread_count_bump(staff_user)
MessageBus.publish(
"/mod-note-unread-count/#{staff_user.id}",
{ delta: 1 },
user_ids: [staff_user.id],
)
end

# Fires the small live notification pop-up for one staff member. The
# payload mirrors `PostAlerter.create_notification_alert`, but carries an
# explicit `translated_title` so the pop-up text clearly names a
Expand Down
178 changes: 0 additions & 178 deletions assets/javascripts/discourse/components/mod-note-header-pip.gjs

This file was deleted.

107 changes: 107 additions & 0 deletions assets/javascripts/discourse/components/mod-pm-badge-picker.gjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import Component from "@glimmer/component";
import { tracked } from "@glimmer/tracking";
import { hash } from "@ember/helper";
import { action } from "@ember/object";
import { service } from "@ember/service";
import DButton from "discourse/components/d-button";
import DModal from "discourse/components/d-modal";
import { ajax } from "discourse/lib/ajax";
import { popupAjaxError } from "discourse/lib/ajax-error";
import ComboBox from "select-kit/components/combo-box";
import { i18n } from "discourse-i18n";

// Picks a single badge, fetches the current holders' usernames, and
// splices them into the PM composer's targetRecipients string (deduped,
// comma-joined per Discourse convention). The PM is then sent through the
// normal PostCreator path with that union as recipients — badge-grant
// changes after send do NOT propagate, by design (a PM is a fixed-recipient
// conversation).
export default class ModPmBadgePicker extends Component {
@service store;

@tracked badgeChoices = [];
@tracked selectedBadgeId = null;
@tracked saving = false;

constructor() {
super(...arguments);
this.#loadBadges();
}

async #loadBadges() {
try {
const list = await this.store.findAll("badge");
this.badgeChoices = (list?.content || list || [])
.filter((b) => b?.enabled !== false)
.map((b) => ({ id: b.id, name: b.display_name || b.name }));
} catch (_e) {
this.badgeChoices = [];
}
}

@action
updateBadge(id) {
this.selectedBadgeId = id ? Number(id) : null;
}

@action
async confirm() {
const composer = this.args.model?.composer;
if (!composer || !this.selectedBadgeId) {
this.args.closeModal();
return;
}

this.saving = true;
try {
const data = await ajax(
`/discourse-mod-categories/badge-members/${this.selectedBadgeId}.json`
);
const newUsernames = Array.isArray(data?.usernames) ? data.usernames : [];

const current = (composer.targetRecipients || "")
.split(",")
.map((s) => s.trim())
.filter(Boolean);
const merged = [...new Set([...current, ...newUsernames])];
composer.set("targetRecipients", merged.join(","));

this.args.closeModal();
} catch (e) {
popupAjaxError(e);
} finally {
this.saving = false;
}
}

<template>
<DModal
@title={{i18n "discourse_mod_categories.pm_badge.modal_title"}}
@closeModal={{@closeModal}}
class="mod-pm-badge-picker-modal"
>
<:body>
<p>{{i18n "discourse_mod_categories.pm_badge.modal_instructions"}}</p>
<ComboBox
@value={{this.selectedBadgeId}}
@content={{this.badgeChoices}}
@nameProperty="name"
@valueProperty="id"
@onChange={{this.updateBadge}}
@options={{hash
filterPlaceholder="discourse_mod_categories.pm_badge.search_placeholder"
none="discourse_mod_categories.pm_badge.none"
}}
/>
</:body>
<:footer>
<DButton
@action={{this.confirm}}
@label="discourse_mod_categories.pm_badge.confirm"
@disabled={{this.saving}}
class="btn-primary"
/>
</:footer>
</DModal>
</template>
}
Loading