mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-30 09:22:24 +00:00
Add shared hashtag normalization helper
This commit is contained in:
1 parent
cedc2a91d0
commit
0ac64867da
10 files changed
+145
-77
No files matched your search
@@ -27,6 +27,7 @@ import { attachHealthBadges } from "./gridHealth.js";
|
||||
import { attachUrlHealthBadges } from "./urlHealthObserver.js";
|
||||
import { updateVideoCardSourceVisibility } from "./utils/cardSourceVisibility.js";
|
||||
import { collectVideoTags } from "./utils/videoTags.js";
|
||||
import { normalizeHashtag } from "./utils/hashtagNormalization.js";
|
||||
import { sanitizeProfileMediaUrl } from "./utils/profileMedia.js";
|
||||
import { ADMIN_INITIAL_EVENT_BLACKLIST } from "./lists.js";
|
||||
import { userBlocks } from "./userBlocks.js";
|
||||
@@ -2514,16 +2515,7 @@ class Application {
|
||||
}
|
||||
|
||||
normalizeTagPreferenceCandidate(tag) {
|
||||
if (typeof tag !== "string") {
|
||||
return "";
|
||||
}
|
||||
|
||||
const trimmed = tag.trim().replace(/^#+/, "");
|
||||
if (!trimmed) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return trimmed.toLowerCase();
|
||||
return normalizeHashtag(tag);
|
||||
}
|
||||
|
||||
getTagPreferenceState(tag) {
|
||||
|
||||
@@ -8,6 +8,7 @@ export {
|
||||
export {
|
||||
createDedupeByRootStage,
|
||||
createBlacklistFilterStage,
|
||||
createTagPreferenceFilterStage,
|
||||
createWatchHistorySuppressionStage,
|
||||
createModerationStage,
|
||||
createResolvePostedAtStage,
|
||||
|
||||
+53
-1
@@ -10,10 +10,25 @@ import nostrService from "../services/nostrService.js";
|
||||
import moderationService from "../services/moderationService.js";
|
||||
import logger from "../utils/logger.js";
|
||||
import { dedupeToNewestByRoot } from "../utils/videoDeduper.js";
|
||||
import { normalizeHashtag } from "../utils/hashtagNormalization.js";
|
||||
import { isPlainObject, toSet, hasDisinterestedTag } from "./utils.js";
|
||||
|
||||
const FEED_HIDE_BYPASS_NAMES = new Set(["home", "recent"]);
|
||||
|
||||
function normalizeTagSet(values) {
|
||||
const normalized = new Set();
|
||||
const source = toSet(values);
|
||||
|
||||
for (const value of source) {
|
||||
const tag = normalizeHashtag(value);
|
||||
if (tag) {
|
||||
normalized.add(tag);
|
||||
}
|
||||
}
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function resolveDedupeFunction(customDedupe) {
|
||||
if (typeof customDedupe === "function") {
|
||||
return customDedupe;
|
||||
@@ -258,6 +273,43 @@ export function createResolvePostedAtStage({
|
||||
};
|
||||
}
|
||||
|
||||
export function createTagPreferenceFilterStage({
|
||||
stageName = "tag-preference-filter",
|
||||
} = {}) {
|
||||
return async function tagPreferenceFilterStage(items = [], context = {}) {
|
||||
const tagPreferences = context?.runtime?.tagPreferences;
|
||||
const disinterests = normalizeTagSet(tagPreferences?.disinterests);
|
||||
if (!disinterests.size) {
|
||||
return items;
|
||||
}
|
||||
|
||||
const results = [];
|
||||
|
||||
for (const item of items) {
|
||||
const video = item?.video;
|
||||
if (!video || typeof video !== "object") {
|
||||
results.push(item);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (hasDisinterestedTag(video, disinterests)) {
|
||||
context?.addWhy?.({
|
||||
stage: stageName,
|
||||
type: "filter",
|
||||
reason: "disinterested-tag",
|
||||
videoId: typeof video.id === "string" ? video.id : null,
|
||||
pubkey: typeof video.pubkey === "string" ? video.pubkey : null,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
results.push(item);
|
||||
}
|
||||
|
||||
return results;
|
||||
};
|
||||
}
|
||||
|
||||
export function createBlacklistFilterStage({
|
||||
stageName = "blacklist-filter",
|
||||
shouldIncludeVideo,
|
||||
@@ -277,7 +329,7 @@ export function createBlacklistFilterStage({
|
||||
const options = { blacklistedEventIds: blacklist, isAuthorBlocked };
|
||||
|
||||
const tagPreferences = context?.runtime?.tagPreferences;
|
||||
const disinterests = toSet(tagPreferences?.disinterests);
|
||||
const disinterests = normalizeTagSet(tagPreferences?.disinterests);
|
||||
const hasTagPreferences = disinterests.size > 0;
|
||||
|
||||
const results = [];
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
// js/feedEngine/utils.js
|
||||
|
||||
import { normalizeHashtag } from "../utils/hashtagNormalization.js";
|
||||
|
||||
export function isPlainObject(value) {
|
||||
if (value === null || typeof value !== "object") {
|
||||
return false;
|
||||
@@ -33,11 +35,16 @@ export function hasDisinterestedTag(video, disinterestsSet) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const matchesDisinterest = (tag) => {
|
||||
const normalized = normalizeHashtag(tag);
|
||||
return Boolean(normalized && disinterestsSet.has(normalized));
|
||||
};
|
||||
|
||||
// Check raw tags
|
||||
if (Array.isArray(video.tags)) {
|
||||
for (const tag of video.tags) {
|
||||
if (Array.isArray(tag) && tag[0] === "t" && typeof tag[1] === "string") {
|
||||
if (disinterestsSet.has(tag[1].toLowerCase())) {
|
||||
if (matchesDisinterest(tag[1])) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -47,7 +54,7 @@ export function hasDisinterestedTag(video, disinterestsSet) {
|
||||
// Check NIP-71 metadata hashtags if available
|
||||
if (video.nip71 && Array.isArray(video.nip71.hashtags)) {
|
||||
for (const tag of video.nip71.hashtags) {
|
||||
if (typeof tag === "string" && disinterestsSet.has(tag.toLowerCase())) {
|
||||
if (typeof tag === "string" && matchesDisinterest(tag)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
assertAnyRelayAccepted,
|
||||
} from "../nostrPublish.js";
|
||||
import { userLogger, devLogger } from "../utils/logger.js";
|
||||
import { normalizeHashtag } from "../utils/hashtagNormalization.js";
|
||||
import { profileCache } from "../state/profileCache.js";
|
||||
|
||||
const LOG_PREFIX = "[HashtagPreferences]";
|
||||
@@ -95,19 +96,6 @@ function sanitizeRelayList(candidate) {
|
||||
: [];
|
||||
}
|
||||
|
||||
function normalizeTag(input) {
|
||||
if (typeof input !== "string") {
|
||||
return "";
|
||||
}
|
||||
|
||||
const trimmed = input.trim().replace(/^#+/, "");
|
||||
if (!trimmed) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return trimmed.toLowerCase();
|
||||
}
|
||||
|
||||
function normalizeEncryptionToken(value) {
|
||||
if (typeof value !== "string") {
|
||||
return "";
|
||||
@@ -133,7 +121,7 @@ function normalizePreferencesPayload(payload) {
|
||||
|
||||
const interests = new Set();
|
||||
for (const tag of rawInterests) {
|
||||
const normalizedTag = normalizeTag(tag);
|
||||
const normalizedTag = normalizeHashtag(tag);
|
||||
if (normalizedTag) {
|
||||
interests.add(normalizedTag);
|
||||
}
|
||||
@@ -141,7 +129,7 @@ function normalizePreferencesPayload(payload) {
|
||||
|
||||
const disinterests = new Set();
|
||||
for (const tag of rawDisinterests) {
|
||||
const normalizedTag = normalizeTag(tag);
|
||||
const normalizedTag = normalizeHashtag(tag);
|
||||
if (!normalizedTag) {
|
||||
continue;
|
||||
}
|
||||
@@ -342,7 +330,7 @@ class HashtagPreferencesService {
|
||||
}
|
||||
|
||||
addInterest(tag) {
|
||||
const normalized = normalizeTag(tag);
|
||||
const normalized = normalizeHashtag(tag);
|
||||
if (!normalized) {
|
||||
return false;
|
||||
}
|
||||
@@ -360,7 +348,7 @@ class HashtagPreferencesService {
|
||||
}
|
||||
|
||||
removeInterest(tag) {
|
||||
const normalized = normalizeTag(tag);
|
||||
const normalized = normalizeHashtag(tag);
|
||||
if (!normalized || !this.interests.has(normalized)) {
|
||||
return false;
|
||||
}
|
||||
@@ -371,7 +359,7 @@ class HashtagPreferencesService {
|
||||
}
|
||||
|
||||
addDisinterest(tag) {
|
||||
const normalized = normalizeTag(tag);
|
||||
const normalized = normalizeHashtag(tag);
|
||||
if (!normalized) {
|
||||
return false;
|
||||
}
|
||||
@@ -389,7 +377,7 @@ class HashtagPreferencesService {
|
||||
}
|
||||
|
||||
removeDisinterest(tag) {
|
||||
const normalized = normalizeTag(tag);
|
||||
const normalized = normalizeHashtag(tag);
|
||||
if (!normalized || !this.disinterests.has(normalized)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ import {
|
||||
formatShortNpub as defaultFormatShortNpub,
|
||||
} from "../../utils/formatters.js";
|
||||
import { sanitizeProfileMediaUrl } from "../../utils/profileMedia.js";
|
||||
import { normalizeHashtag } from "../../utils/hashtagNormalization.js";
|
||||
import { getBreakpointLg } from "../../designSystem/metrics.js";
|
||||
|
||||
const HEX64_REGEX = /^[0-9a-f]{64}$/i;
|
||||
@@ -2917,7 +2918,7 @@ export class VideoModal {
|
||||
}
|
||||
|
||||
const raw = typeof entry === "string" ? entry : String(entry ?? "");
|
||||
const trimmed = raw.trim().replace(/^#+/, "");
|
||||
const trimmed = normalizeHashtag(raw);
|
||||
|
||||
if (!trimmed) {
|
||||
continue;
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { userLogger } from "../../utils/logger.js";
|
||||
import {
|
||||
normalizeHashtag,
|
||||
formatHashtag,
|
||||
} from "../../utils/hashtagNormalization.js";
|
||||
// NOTE: Keep the Upload, Edit, and Revert modals in lockstep when updating NIP-71 form features.
|
||||
|
||||
const REPEATER_KEYS = ["imeta", "text-track", "segment", "t", "p", "r"];
|
||||
@@ -468,22 +472,11 @@ export class Nip71FormManager {
|
||||
value = `${value ?? ""}`;
|
||||
}
|
||||
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const withoutHash = trimmed.replace(/^#+/, "").trim();
|
||||
if (!withoutHash) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return withoutHash.toLowerCase();
|
||||
return normalizeHashtag(value);
|
||||
}
|
||||
|
||||
renderHashtagValue(value) {
|
||||
const sanitized = this.sanitizeHashtagValue(value);
|
||||
return sanitized ? `#${sanitized}` : "";
|
||||
return formatHashtag(value);
|
||||
}
|
||||
|
||||
collectNestedValues(entry, nestedKey) {
|
||||
@@ -804,4 +797,3 @@ export class Nip71FormManager {
|
||||
return this.sections.get(key) || null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { normalizeDesignSystemContext } from "../../designSystem.js";
|
||||
import {
|
||||
normalizeHashtag,
|
||||
formatHashtag,
|
||||
} from "../../utils/hashtagNormalization.js";
|
||||
|
||||
export const TAG_PREFERENCE_ACTIONS = {
|
||||
ADD_INTEREST: "add-interest",
|
||||
@@ -83,19 +87,9 @@ function appendMenuAction(doc, list, { text, action, dataset = {}, disabled = fa
|
||||
}
|
||||
|
||||
function normalizeTag(tag) {
|
||||
if (typeof tag !== "string") {
|
||||
return { label: "", normalized: "" };
|
||||
}
|
||||
|
||||
const trimmed = tag.trim();
|
||||
if (!trimmed) {
|
||||
return { label: "", normalized: "" };
|
||||
}
|
||||
|
||||
const withoutHash = trimmed.replace(/^#+/, "");
|
||||
const normalized = withoutHash.toLowerCase();
|
||||
const normalized = normalizeHashtag(tag);
|
||||
return {
|
||||
label: withoutHash,
|
||||
label: normalized,
|
||||
normalized,
|
||||
};
|
||||
}
|
||||
@@ -172,7 +166,7 @@ export function createTagPreferenceMenu({
|
||||
normalizeDesignSystemContext(designSystem);
|
||||
|
||||
const { label, normalized } = normalizeTag(tag);
|
||||
const headingLabel = label ? `#${label}` : "Tag preferences";
|
||||
const headingLabel = label ? formatHashtag(label) : "Tag preferences";
|
||||
|
||||
const { panel, list } = ensureMenuContainer(doc, ["w-56", "p-0"]);
|
||||
panel.dataset.menu = "tag-preference";
|
||||
@@ -191,7 +185,7 @@ export function createTagPreferenceMenu({
|
||||
}
|
||||
|
||||
const buttons = {};
|
||||
const actionDataset = { tag: label || normalized };
|
||||
const actionDataset = { tag: normalized };
|
||||
|
||||
buttons.addInterest = appendMenuAction(doc, list, {
|
||||
text: "Add to interests",
|
||||
@@ -225,7 +219,7 @@ export function createTagPreferenceMenu({
|
||||
}
|
||||
try {
|
||||
onAction(action, {
|
||||
tag: label || normalized,
|
||||
tag: normalized,
|
||||
normalizedTag: normalized,
|
||||
event,
|
||||
button,
|
||||
|
||||
@@ -16,6 +16,10 @@ import {
|
||||
import { getBreakpointLg } from "../designSystem/metrics.js";
|
||||
import { getProviderMetadata } from "../services/authProviders/index.js";
|
||||
import { devLogger, userLogger } from "../utils/logger.js";
|
||||
import {
|
||||
normalizeHashtag,
|
||||
formatHashtag,
|
||||
} from "../utils/hashtagNormalization.js";
|
||||
import { getActiveSigner } from "../nostr/client.js";
|
||||
|
||||
const noop = () => {};
|
||||
@@ -4465,24 +4469,11 @@ export class ProfileModalController {
|
||||
}
|
||||
|
||||
normalizeHashtagTag(value) {
|
||||
if (typeof value !== "string") {
|
||||
return "";
|
||||
}
|
||||
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return trimmed.replace(/^#+/, "").toLowerCase();
|
||||
return normalizeHashtag(value);
|
||||
}
|
||||
|
||||
formatHashtagTag(value) {
|
||||
const normalized = this.normalizeHashtagTag(value);
|
||||
if (!normalized) {
|
||||
return "";
|
||||
}
|
||||
return `#${normalized}`;
|
||||
return formatHashtag(value);
|
||||
}
|
||||
|
||||
sanitizeHashtagList(list) {
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
// js/utils/hashtagNormalization.js
|
||||
|
||||
const HASH_PREFIX_PATTERN = /^#+/;
|
||||
|
||||
/**
|
||||
* Normalize hashtags consistently across storage, matching, and UI validation.
|
||||
*
|
||||
* Normalization rules:
|
||||
* 1) Trim surrounding whitespace.
|
||||
* 2) Strip leading "#" characters.
|
||||
* 3) Lowercase the remaining value.
|
||||
*
|
||||
* Note: We do not strip punctuation today. Only add punctuation stripping if
|
||||
* product explicitly decides to change the rules.
|
||||
*
|
||||
* @param {string} value
|
||||
* @returns {string} normalized hashtag or empty string when invalid.
|
||||
*/
|
||||
export function normalizeHashtag(value) {
|
||||
if (typeof value !== "string") {
|
||||
return "";
|
||||
}
|
||||
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const withoutHashes = trimmed.replace(HASH_PREFIX_PATTERN, "").trim();
|
||||
if (!withoutHashes) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return withoutHashes.toLowerCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a hashtag for display, ensuring a single leading hash.
|
||||
*
|
||||
* @param {string} value
|
||||
* @returns {string}
|
||||
*/
|
||||
export function formatHashtag(value) {
|
||||
const normalized = normalizeHashtag(value);
|
||||
if (!normalized) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return `#${normalized}`;
|
||||
}
|
||||
Reference in new issue
Block a user