diff --git a/src/components/catalog-filters/filters-protip/filters-protip.tsx b/src/components/catalog-filters/filters-protip/filters-protip.tsx
index 9060a242..5ae12370 100644
--- a/src/components/catalog-filters/filters-protip/filters-protip.tsx
+++ b/src/components/catalog-filters/filters-protip/filters-protip.tsx
@@ -55,6 +55,18 @@ const FiltersProtip = () => {
idolm*ster — this can match "idolmaster" or "idolm@ster", etc…
+
+ Filtering by display name or user ID
+ -
+ Prefix the pattern with
# to search by user ID: #GPPr4CQSRCt8
+
+ -
+ Prefix the pattern with
## to search by name: ##Anonymous
+
+ -
+ To filter by role:
#!#owner, #!#admin, #!#moderator (or #!#mod)
+
+
It is also possible to filter by regular expression:
-
@@ -75,6 +87,9 @@ const FiltersProtip = () => {
-
On — enables or disables the filter.
+ -
+ Color — highlights matched threads with the specified color.
+
-
Hide — hides matched threads.
diff --git a/src/lib/utils/pattern-utils.ts b/src/lib/utils/pattern-utils.ts
index 81484791..b260d162 100644
--- a/src/lib/utils/pattern-utils.ts
+++ b/src/lib/utils/pattern-utils.ts
@@ -1,4 +1,5 @@
import { Comment } from '@plebbit/plebbit-react-hooks';
+import useSubplebbitsStore from '@plebbit/plebbit-react-hooks/dist/stores/subplebbits';
/**
* Checks if a text matches a pattern according to various pattern matching rules:
@@ -93,7 +94,110 @@ export const matchesPattern = (text: string, pattern: string): boolean => {
};
/**
- * Checks if a comment matches a pattern in its title or content
+ * Checks if a user ID matches a pattern
+ *
+ * @param comment The comment to check
+ * @param pattern The pattern to match (without the # prefix)
+ * @returns True if the user ID matches the pattern, false otherwise
+ */
+export const userIdMatchesPattern = (comment: Comment, pattern: string): boolean => {
+ if (!pattern || !comment?.author) return false;
+
+ // Check both address and shortAddress if available
+ const address = comment.author.address || '';
+ const shortAddress = comment.author.shortAddress || '';
+
+ return address.includes(pattern) || shortAddress.includes(pattern);
+};
+
+/**
+ * Checks if a display name matches a pattern
+ *
+ * @param comment The comment to check
+ * @param pattern The pattern to match (without the ## prefix)
+ * @returns True if the display name matches the pattern, false otherwise
+ */
+export const displayNameMatchesPattern = (comment: Comment, pattern: string): boolean => {
+ if (!pattern) return false;
+
+ // Special case for "Anonymous" to match undefined display names
+ if (pattern.toLowerCase() === 'anonymous') {
+ return comment?.author?.displayName === undefined;
+ }
+
+ if (!comment?.author?.displayName) return false;
+
+ return comment.author.displayName.toLowerCase().includes(pattern.toLowerCase());
+};
+
+/**
+ * Checks if a user has a specific role
+ *
+ * @param comment The comment to check
+ * @param role The role to check for (without the #!# prefix)
+ * @returns True if the user has the specified role, false otherwise
+ */
+export const userHasRole = (comment: Comment, role: string): boolean => {
+ if (!role || !comment?.author?.address || !comment?.subplebbitAddress) {
+ return false;
+ }
+
+ const subplebbits = useSubplebbitsStore.getState().subplebbits;
+ const subplebbit = subplebbits[comment.subplebbitAddress];
+
+ if (!subplebbit?.roles) {
+ return false;
+ }
+
+ const userRole = subplebbit.roles[comment.author.address]?.role;
+
+ // Handle different role names (moderator/mod)
+ if ((role.toLowerCase() === 'moderator' || role.toLowerCase() === 'mod') && userRole === 'moderator') {
+ return true;
+ }
+
+ return userRole?.toLowerCase() === role.toLowerCase();
+};
+
+/**
+ * Parses a pattern that may contain special filters and content filters
+ *
+ * @param pattern The pattern to parse
+ * @returns An object containing the parsed special filters and content filters
+ */
+export const parsePattern = (
+ pattern: string,
+): {
+ specialFilters: { type: 'userId' | 'displayName' | 'role'; value: string }[];
+ contentFilter: string;
+} => {
+ if (!pattern) return { specialFilters: [], contentFilter: '' };
+
+ const parts = pattern.split(' ');
+ const specialFilters: { type: 'userId' | 'displayName' | 'role'; value: string }[] = [];
+ const contentParts: string[] = [];
+
+ parts.forEach((part) => {
+ if (part.startsWith('#') && !part.startsWith('##') && !part.startsWith('#!#')) {
+ specialFilters.push({ type: 'userId', value: part.substring(1) });
+ } else if (part.startsWith('##')) {
+ specialFilters.push({ type: 'displayName', value: part.substring(2) });
+ } else if (part.startsWith('#!#')) {
+ specialFilters.push({ type: 'role', value: part.substring(3) });
+ } else {
+ contentParts.push(part);
+ }
+ });
+
+ return {
+ specialFilters,
+ contentFilter: contentParts.join(' '),
+ };
+};
+
+/**
+ * Checks if a comment matches a pattern in its title or content,
+ * or if it matches special filters for user ID, display name, or role
*
* @param comment The comment to check
* @param pattern The pattern to match
@@ -102,6 +206,53 @@ export const matchesPattern = (text: string, pattern: string): boolean => {
export const commentMatchesPattern = (comment: Comment, pattern: string): boolean => {
if (!pattern || !comment) return false;
+ // Check if the pattern contains spaces, which might indicate combined filters
+ if (pattern.includes(' ') && (pattern.includes('#') || pattern.includes('##') || pattern.includes('#!#'))) {
+ // Parse the pattern into special filters and content filter
+ const { specialFilters, contentFilter } = parsePattern(pattern);
+
+ // If there are special filters, check if the comment matches all of them
+ if (specialFilters.length > 0) {
+ const allSpecialFiltersMatch = specialFilters.every((filter) => {
+ if (filter.type === 'userId') {
+ return userIdMatchesPattern(comment, filter.value);
+ } else if (filter.type === 'displayName') {
+ return displayNameMatchesPattern(comment, filter.value);
+ } else if (filter.type === 'role') {
+ return userHasRole(comment, filter.value);
+ }
+ return false;
+ });
+
+ // If there's also a content filter, check if the comment matches it as well
+ if (contentFilter) {
+ return allSpecialFiltersMatch && matchesPattern((comment?.title || '') + ' ' + (comment?.content || ''), contentFilter);
+ }
+
+ return allSpecialFiltersMatch;
+ }
+ }
+
+ // Simple cases for single filters
+ // Check for user ID filter (starts with #)
+ if (pattern.startsWith('#') && !pattern.startsWith('##') && !pattern.startsWith('#!#')) {
+ const userIdPattern = pattern.substring(1);
+ return userIdMatchesPattern(comment, userIdPattern);
+ }
+
+ // Check for display name filter (starts with ##)
+ if (pattern.startsWith('##')) {
+ const displayNamePattern = pattern.substring(2);
+ return displayNameMatchesPattern(comment, displayNamePattern);
+ }
+
+ // Check for role filter (starts with #!#)
+ if (pattern.startsWith('#!#')) {
+ const rolePattern = pattern.substring(3);
+ return userHasRole(comment, rolePattern);
+ }
+
+ // Regular content matching
const titleLower = comment?.title?.toLowerCase() || '';
const contentLower = comment?.content?.toLowerCase() || '';
const textToMatch = titleLower + ' ' + contentLower;