Skip to content
Merged
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
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ Lets moderators mark a member with a staff-only reason from a list the administr
- **ACP → Extensions → See Notes → Reasons**: administrators set up the list of reasons, each with a description, a color, and an on/off switch. Seven starter reasons (Moderation queue, Spam watch, Warez, Conduct, Possible returning member, Staff in contact, General) are added on first enable, ready to edit or remove. A switched-off reason can't be given any more, but members who have it keep it and its notes.
- **MCP → Moderator notes** and **ACP → Manage users → Moderator notes**: give a member a reason, choose which reason is current (or none), and add notes under each reason.
- **Notes use BBCode**, smilies and links, following the board's private message settings, so a PM sent to the member can be pasted into a quote for other staff.
- **Only staff see it.** The current reason shows as "SEE NOTES: Moderation queue (3)" on the member's profile, next to their posts, on the MCP post, report and approval queue pages, and in the MCP topic view. It links to the notes. A member with no current reason shows "SEE NOTES: NONE", which links straight to the form that gives one.
- **Only staff see it.** The current reason shows as "SEE NOTES: Moderation queue (3)" on the member's profile, next to their posts, on the MCP post, report and approval queue pages, and in the MCP topic view. It links to the notes, and shows only while the member has a current reason.
- **A notes button** next to every post (beside phpBB's Warn button) and a **Moderator notes** link on every profile take staff straight to a member's notes, or to the form that gives a reason when the member has none.
- The member's latest **phpBB user notes** (warnings, bans and so on) show read-only on the same page.
- Separate permissions for seeing, adding, editing notes, deleting notes and deleting a reason entry, with matching administrator permissions.
- Every change goes into the moderator log. Deleting a reason with all its entries is off by default, and needs the reason's name typed to confirm.
Expand All @@ -23,7 +24,7 @@ The full design is in [`docs/design.md`](docs/design.md).
<table>
<tr>
<td><a href="docs/images/screenshots/seenotes-mcp-notes.png"><img src="docs/images/screenshots/seenotes-mcp-notes.png" width="440" height="591" alt="MCP Moderator notes page for a member"></a></td>
<td><a href="docs/images/screenshots/seenotes-mcp-post.png"><img src="docs/images/screenshots/seenotes-mcp-post.png" width="440" height="300" alt="MCP post details with a SEE NOTES notice"></a><br><br><a href="docs/images/screenshots/seenotes-viewtopic.png"><img src="docs/images/screenshots/seenotes-viewtopic.png" width="440" height="58" alt="The reason in a post's mini-profile"></a><br><br><a href="docs/images/screenshots/seenotes-profile-none.png"><img src="docs/images/screenshots/seenotes-profile-none.png" width="440" height="113" alt="SEE NOTES: NONE on a profile"></a></td>
<td><a href="docs/images/screenshots/seenotes-mcp-post.png"><img src="docs/images/screenshots/seenotes-mcp-post.png" width="440" height="300" alt="MCP post details with a SEE NOTES notice"></a><br><br><a href="docs/images/screenshots/seenotes-viewtopic.png"><img src="docs/images/screenshots/seenotes-viewtopic.png" width="440" height="58" alt="The reason in a post's mini-profile"></a><br><br><a href="docs/images/screenshots/seenotes-post-button.png"><img src="docs/images/screenshots/seenotes-post-button.png" width="440" height="64" alt="The Moderator notes button next to a post by a member without a reason"></a></td>
</tr>
</table>

Expand Down
3 changes: 2 additions & 1 deletion docs/design.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,8 @@ To staff with the "see" permission only:
- **Mini-profile** next to posts (viewtopic): the same line next to the custom profile fields, in bold, in the reason's color (red by default).
- **MCP pages where moderators act on a post:** the post details, report details and approval queue details pages, as a notice above the post (where phpBB shows "This post was reported"), and the MCP topic view, after each poster's name. The queue and reports *lists* are left out: their templates have no events to add anything to, and every item in them links to its details page.
- All of these show the reason's description on hover (when it has one) and link to the member's Moderator notes tab in the MCP. The number in brackets is how many notes the current entry has.
- When a member has no current reason, the same places show "<label>: NONE" in the normal text color. NONE links straight to the "Give a reason" form on the member's notes page, so staff can add one in one click.
- When a member has no current reason, nothing shows in these places.
- Staff with the "see" permission also get a way into any member's notes, whether or not they have a reason: a **Moderator notes** button with the post buttons (next to phpBB's Warn button, template event `viewtopic_body_post_buttons_after`) and a **Moderator notes** link with the profile's user statistics (`memberlist_view_user_statistics_after`). Without a current reason, staff who may give one land on the "Give a reason" form. phpBB 4.0 draws post button icons with its `Icon()` Twig function, which phpBB 3.3 cannot compile, so the button has one template per branch.

Events used, all present on both 3.3.x and master:

Expand Down
10 changes: 5 additions & 5 deletions docs/images/screenshots/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,15 @@ The current reason in the poster's mini-profile, in the reason's color, with the

## Profile

The reason with the other profile details.
The reason with the other profile details, and the Moderator notes link with the user statistics.

<a href="seenotes-profile.png"><img src="seenotes-profile.png" width="640" height="164" alt="A profile showing SEE NOTES: Moderation queue (2)"></a>
<a href="seenotes-profile.png"><img src="seenotes-profile.png" width="640" height="174" alt="A profile showing SEE NOTES: Moderation queue (2)"></a>

## No current reason
## The notes button

A member without a current reason shows NONE, which links straight to the form that gives one.
Every post gets a Moderator notes button for staff, next to phpBB's Warn button. Here the member has no current reason, so no SEE NOTES line shows, and the button opens the form that gives one.

<a href="seenotes-profile-none.png"><img src="seenotes-profile-none.png" width="640" height="164" alt="A profile showing SEE NOTES: NONE"></a>
<a href="seenotes-post-button.png"><img src="seenotes-post-button.png" width="640" height="93" alt="A post with the Moderator notes button, by a member without a reason"></a>

## MCP post details

Expand Down
Binary file modified docs/images/screenshots/seenotes-acp-users.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/images/screenshots/seenotes-mcp-notes.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/images/screenshots/seenotes-mcp-post.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/screenshots/seenotes-post-button.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed docs/images/screenshots/seenotes-profile-none.png
Binary file not shown.
Binary file modified docs/images/screenshots/seenotes-profile.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/images/screenshots/seenotes-viewtopic.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 10 additions & 0 deletions docs/update/2026-11-08/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# See Notes update screenshots — 11/08/2026

Staff get a Moderator notes button next to every post, beside phpBB's Warn button. This member has no current reason, so no SEE NOTES line shows, and the button opens the form that gives one.

[<img src="notes-button-next-to-post.png" width="640" height="93" alt="A post with the Moderator notes button, by a member without a reason" />](notes-button-next-to-post.png)

Profiles get a Moderator notes link with the user statistics. The SEE NOTES line still shows with the profile details while the member has a current reason.

[<img src="moderator-notes-link-on-profile.png" width="640" height="174" alt="A profile with the SEE NOTES line and the Moderator notes link" />](moderator-notes-link-on-profile.png)

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
90 changes: 64 additions & 26 deletions event/listener.php
Original file line number Diff line number Diff line change
Expand Up @@ -142,24 +142,31 @@ public function add_permissions(data $event)
}

/**
* Load the stylesheet for staff who may see reasons
* Load the stylesheet for staff who may see reasons, and tell the
* templates which phpBB branch draws the post buttons
*
* @return void
*/
public function load_stylesheet()
{
$this->template->assign_var('S_SEENOTES_CSS', $this->manager->can('view'));
$this->template->assign_vars([
'S_SEENOTES_CSS' => $this->manager->can('view'),
// phpBB 4.0 draws post button icons with its Icon() function
'S_SEENOTES_PHPBB4' => phpbb_version_compare(PHPBB_VERSION, '4.0.0-a1', '>='),
]);
}

/**
* Show the member's current reason with the profile details
* Show the member's current reason with the profile details, and the
* link to their notes with the user statistics
*
* @param data $event
* @return void
*/
public function show_on_profile(data $event)
{
$this->template->assign_vars($this->get_display_vars((int) $event['member']['user_id']));
$user_id = (int) $event['member']['user_id'];
$this->template->assign_vars(array_merge($this->get_display_vars($user_id), $this->get_button_vars($user_id)));
}

/**
Expand All @@ -178,14 +185,16 @@ public function load_viewtopic_posters(data $event)
}

/**
* Show the poster's current reason in their mini-profile
* Show the poster's current reason in their mini-profile, and the notes
* button with the post buttons
*
* @param data $event
* @return void
*/
public function show_on_post(data $event)
{
$vars = $this->get_display_vars((int) $event['poster_id']);
$poster_id = (int) $event['poster_id'];
$vars = array_merge($this->get_display_vars($poster_id), $this->get_button_vars($poster_id));
if ($vars)
{
$event['post_row'] = array_merge($event['post_row'], $vars);
Expand Down Expand Up @@ -281,53 +290,82 @@ public function update_username(data $event)
}

/**
* Template variables for a member's current reason, or "NONE" when they
* have none, for viewers who may see it
* Template variables for a member's current reason, for viewers who may
* see it
*
* @param int $user_id
* @return array Empty when the viewer may not see reasons
* @return array Empty when there is nothing to show
*/
protected function get_display_vars($user_id)
{
if (!$user_id || $user_id == ANONYMOUS || !$this->manager->can('view'))
if (!$this->can_view_member($user_id))
{
return [];
}

$current = $this->manager->get_current_for_users([$user_id]);
$u_notes = $this->get_notes_url($user_id);

// No current reason: "NONE", linking to the form that gives one
if (!isset($current[$user_id]))
{
$can_manage = $this->manager->can('manage');

return [
'S_SEENOTES' => true,
'S_SEENOTES_NONE' => true,
'SEENOTES_LABEL' => $this->manager->get_label(),
'SEENOTES_REASON' => $this->language->lang('SEENOTES_NONE'),
'SEENOTES_LINK_TEXT' => $this->language->lang($can_manage ? 'SEENOTES_NONE_GIVE' : 'SEENOTES_NONE_EXPLAIN'),
'U_SEENOTES' => ($u_notes && $can_manage) ? $u_notes . '#seenotes-add-entry' : $u_notes,
];
return [];
}

$entry = $current[$user_id];

return [
'S_SEENOTES' => true,
'S_SEENOTES_NONE' => false,
'SEENOTES_LABEL' => $this->manager->get_label(),
'SEENOTES_REASON' => $entry['reason_title'],
'SEENOTES_DESCRIPTION' => $entry['reason_description'],
'SEENOTES_COLOR' => $this->manager->clean_color($entry['reason_color']),
'SEENOTES_COUNT' => (int) $entry['note_count'],
'SEENOTES_COUNT_TEXT' => $this->language->lang('SEENOTES_NOTE_COUNT', (int) $entry['note_count']),
'SEENOTES_LINK_TEXT' => $this->language->lang('SEENOTES_OPEN_NOTES'),
'U_SEENOTES' => $u_notes,
'U_SEENOTES' => $this->get_notes_url($user_id),
];
}

/**
* Template variables for the notes button next to posts and the link on
* the profile, shown to staff whether or not the member has a reason.
* Without a current reason, staff who may give one land on that form.
*
* @param int $user_id
* @return array Empty when the viewer may not see notes or reach them
*/
protected function get_button_vars($user_id)
{
if (!$this->can_view_member($user_id))
{
return [];
}

$u_notes = $this->get_notes_url($user_id);
if ($u_notes === '')
{
return [];
}

$current = $this->manager->get_current_for_users([$user_id]);
if (!isset($current[$user_id]) && $this->manager->can('manage'))
{
$u_notes .= '#seenotes-add-entry';
}

return [
'U_SEENOTES_NOTES' => $u_notes,
];
}

/**
* Whether the viewer may see See Notes for this member
*
* @param int $user_id
* @return bool
*/
protected function can_view_member($user_id)
{
return $user_id && $user_id != ANONYMOUS && $this->manager->can('view');
}

/**
* Where the viewer can read the member's notes: the MCP for moderators,
* ACP > Manage users for administrators without moderator access
Expand Down
5 changes: 2 additions & 3 deletions language/en/common.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,8 @@
2 => '%d notes',
],
'SEENOTES_OPEN_NOTES' => 'Read the moderator notes about this member',
'SEENOTES_NONE' => 'NONE',
'SEENOTES_NONE_GIVE' => 'This member has no current reason. Give them one',
'SEENOTES_NONE_EXPLAIN' => 'This member has no current reason. Read the moderator notes about them',
'SEENOTES_NOTES_BUTTON' => 'Moderator notes',
'SEENOTES_VIEW_NOTES' => 'View or add moderator notes',

'LOG_SEENOTES_ENTRY_ADDED' => '<strong>Gave a member a See Notes reason</strong><br>» %1$s, to %2$s',
'LOG_SEENOTES_CURRENT_SET' => '<strong>Changed a member’s current See Notes reason</strong><br>» %1$s, for %2$s',
Expand Down
2 changes: 1 addition & 1 deletion styles/all/template/event/mcp_post_text_before.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{% if S_SEENOTES %}
<p class="post-notice seenotes-notice{% if S_SEENOTES_NONE %} seenotes-notice-none{% endif %}">
<p class="post-notice seenotes-notice">
<i class="icon fa-sticky-note fa-fw" aria-hidden="true"></i>
{% include '@phpbbmodders_seenotes/seenotes_reason.html' with {'sn': _context} only %}
</p>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{% if U_SEENOTES_NOTES %}
<dt>{{ lang('SEENOTES_NOTES_BUTTON') }}{{ lang('COLON') }}</dt>
<dd><a href="{{ U_SEENOTES_NOTES }}">{{ lang('SEENOTES_VIEW_NOTES') }}</a></dd>
{% endif %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{% if postrow.U_SEENOTES_NOTES %}
{# phpBB 4.0 draws post button icons with Icon(), which phpBB 3.3 cannot compile, so each branch has its own button #}
{% include S_SEENOTES_PHPBB4 ? '@phpbbmodders_seenotes/seenotes_post_button_40.html' : '@phpbbmodders_seenotes/seenotes_post_button_33.html' %}
{% endif %}
5 changes: 5 additions & 0 deletions styles/all/template/seenotes_post_button_33.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<li>
<a href="{{ postrow.U_SEENOTES_NOTES }}" title="{{ lang('SEENOTES_NOTES_BUTTON')|e('html_attr') }}" class="button button-icon-only">
<i class="icon fa-sticky-note fa-fw" aria-hidden="true"></i><span class="sr-only">{{ lang('SEENOTES_NOTES_BUTTON') }}</span>
</a>
</li>
5 changes: 5 additions & 0 deletions styles/all/template/seenotes_post_button_40.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<li>
<a href="{{ postrow.U_SEENOTES_NOTES }}" title="{{ lang('SEENOTES_NOTES_BUTTON')|e('html_attr') }}" class="button button-icon-only">
{{ Icon('font', 'note-sticky', lang('SEENOTES_NOTES_BUTTON'), post_buttons_hide_text, 'fas c-button-icon') }}
</a>
</li>
2 changes: 1 addition & 1 deletion styles/all/template/seenotes_reason.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{# The reason line. Pass the row with the SEENOTES_* values as "sn". #}
<span class="seenotes-line{% if sn.S_SEENOTES_NONE %} seenotes-line-none{% endif %}"{% if not sn.S_SEENOTES_NONE %} style="color: #{{ sn.SEENOTES_COLOR }};"{% endif %}>
<span class="seenotes-line" style="color: #{{ sn.SEENOTES_COLOR }};">
<strong>{{ sn.SEENOTES_LABEL }}{{ lang('COLON') }}</strong>
<strong>{% include '@phpbbmodders_seenotes/seenotes_reason_link.html' with {'sn': sn} only %}</strong>
</span>
4 changes: 2 additions & 2 deletions styles/all/template/seenotes_reason_link.html
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
{# The reason, or NONE, as a link to the notes. Pass the row with the SEENOTES_* values as "sn". #}
{% if sn.U_SEENOTES %}<a href="{{ sn.U_SEENOTES }}"{% else %}<span{% endif %} class="seenotes-reason{% if sn.S_SEENOTES_NONE %} seenotes-none{% endif %}"{% if not sn.S_SEENOTES_NONE %} style="color: #{{ sn.SEENOTES_COLOR }};"{% endif %}{% if sn.SEENOTES_DESCRIPTION %} title="{{ sn.SEENOTES_DESCRIPTION }}"{% endif %}>{{ sn.SEENOTES_REASON }}{% if not sn.S_SEENOTES_NONE %} <span aria-hidden="true">({{ sn.SEENOTES_COUNT }})</span>{% endif %}<span class="sr-only">{% if not sn.S_SEENOTES_NONE %}, {{ sn.SEENOTES_COUNT_TEXT }}{% if sn.SEENOTES_DESCRIPTION %}: {{ sn.SEENOTES_DESCRIPTION }}{% endif %}{% endif %}{% if sn.U_SEENOTES %}. {{ sn.SEENOTES_LINK_TEXT }}{% endif %}</span>{% if sn.U_SEENOTES %}</a>{% else %}</span>{% endif %}
{# The reason as a link to the notes. Pass the row with the SEENOTES_* values as "sn". #}
{% if sn.U_SEENOTES %}<a href="{{ sn.U_SEENOTES }}"{% else %}<span{% endif %} class="seenotes-reason" style="color: #{{ sn.SEENOTES_COLOR }};"{% if sn.SEENOTES_DESCRIPTION %} title="{{ sn.SEENOTES_DESCRIPTION }}"{% endif %}>{{ sn.SEENOTES_REASON }} <span aria-hidden="true">({{ sn.SEENOTES_COUNT }})</span><span class="sr-only">, {{ sn.SEENOTES_COUNT_TEXT }}{% if sn.SEENOTES_DESCRIPTION %}: {{ sn.SEENOTES_DESCRIPTION }}{% endif %}{% if sn.U_SEENOTES %}. {{ lang('SEENOTES_OPEN_NOTES') }}{% endif %}</span>{% if sn.U_SEENOTES %}</a>{% else %}</span>{% endif %}
5 changes: 0 additions & 5 deletions styles/all/theme/seenotes.css
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,3 @@
min-height: 0;
overflow-x: auto;
}

/* NONE: no current reason, so a quieter line in the normal text color */
.seenotes-notice-none {
border-left-color: rgba(128, 128, 128, 0.5);
}
8 changes: 4 additions & 4 deletions tests/event/listener_test.php
Original file line number Diff line number Diff line change
Expand Up @@ -127,9 +127,10 @@ public function test_post_row_gets_the_current_reason()
$this->assertSame('Moderation queue', $row['SEENOTES_REASON']);
$this->assertSame(2, $row['SEENOTES_COUNT']);
$this->assertStringContainsString('mode=notes&amp;u=3', $row['U_SEENOTES']);
$this->assertStringEndsWith('u=3', $row['U_SEENOTES_NOTES']);
}

public function test_post_row_shows_none_without_a_current_reason()
public function test_post_row_without_a_current_reason_gets_only_the_button()
{
$this->manager->method('can')->willReturn(true);
$this->manager->method('get_current_for_users')->willReturn([]);
Expand All @@ -143,9 +144,8 @@ public function test_post_row_shows_none_without_a_current_reason()
$this->get_listener()->show_on_post($event);

$row = $event['post_row'];
$this->assertTrue($row['S_SEENOTES_NONE']);
$this->assertSame('SEENOTES_NONE', $row['SEENOTES_REASON']);
$this->assertStringEndsWith('u=3#seenotes-add-entry', $row['U_SEENOTES']);
$this->assertArrayNotHasKey('S_SEENOTES', $row);
$this->assertStringEndsWith('u=3#seenotes-add-entry', $row['U_SEENOTES_NOTES']);
}

public function test_other_acp_users_modes_are_ignored()
Expand Down
Loading