Merge pull request #723 from plebbit/development

Development
This commit is contained in:
plebeius
2025-06-04 16:40:51 +02:00
committed by GitHub
50 changed files with 448 additions and 102 deletions
-1
View File
@@ -23,7 +23,6 @@
"cross-env": "7.0.3",
"electron-context-menu": "3.3.0",
"electron-is-dev": "2.0.0",
"ext-name": "5.0.0",
"fs-extra": "11.3.0",
"http-proxy": "1.18.1",
"i18next": "23.5.1",
+2 -1
View File
@@ -294,5 +294,6 @@
"search_results_for": "نتائج البحث عن",
"help": "مساعدة",
"filter_and_highlights_help": "مساعدة الفلاتر والإبرازات",
"filter_and_highlights": "التصفية والتسليط الضوء"
"filter_and_highlights": "التصفية والتسليط الضوء",
"boards_you_moderate": "المنتديات التي تقوم بالإشراف عليها"
}
+2 -1
View File
@@ -294,5 +294,6 @@
"search_results_for": "অনুসন্ধান ফলাফল",
"help": "সাহায্য",
"filter_and_highlights_help": "ফিল্টার এবং হাইলাইট সাহায্য",
"filter_and_highlights": "ফিল্টার এবং হাইলাইট"
"filter_and_highlights": "ফিল্টার এবং হাইলাইট",
"boards_you_moderate": "আপনি যে বোর্ডগুলি পরিচালনা করেন"
}
+2 -1
View File
@@ -294,5 +294,6 @@
"search_results_for": "Výsledky hledání pro",
"help": "Pomoc",
"filter_and_highlights_help": "Pomoc s filtry a zvýrazněním",
"filter_and_highlights": "Filtry a zvýraznění"
"filter_and_highlights": "Filtry a zvýraznění",
"boards_you_moderate": "Fóra, která moderujete"
}
+2 -1
View File
@@ -294,5 +294,6 @@
"search_results_for": "Søgeresultater for",
"help": "Hjælp",
"filter_and_highlights_help": "Filtre og fremhævelser Hjælp",
"filter_and_highlights": "Filtre og fremhævninger"
"filter_and_highlights": "Filtre og fremhævninger",
"boards_you_moderate": "Boards du modererer"
}
+2 -1
View File
@@ -294,5 +294,6 @@
"search_results_for": "Suchergebnisse für",
"help": "Hilfe",
"filter_and_highlights_help": "Filter- und Markierungs-Hilfe",
"filter_and_highlights": "Filter und Hervorhebungen"
"filter_and_highlights": "Filter und Hervorhebungen",
"boards_you_moderate": "Boards, die Sie moderieren"
}
+2 -1
View File
@@ -294,5 +294,6 @@
"search_results_for": "Αποτελέσματα αναζήτησης για",
"help": "Βοήθεια",
"filter_and_highlights_help": "Βοήθεια φίλτρων και επισήμανσης",
"filter_and_highlights": "Φίλτρα και Επισημάνσεις"
"filter_and_highlights": "Φίλτρα και Επισημάνσεις",
"boards_you_moderate": "Boards που διαχειρίζεστε"
}
+3 -1
View File
@@ -141,6 +141,7 @@
"user_deleted_this_post": "User deleted this post.",
"reason": "reason",
"search": "search",
"search_ops_placeholder": "Search OPs...",
"ban_user_for": "Ban user for <1></1> days",
"optional": "optional",
"close_thread": "close thread",
@@ -294,5 +295,6 @@
"search_results_for": "Search results for",
"help": "Help",
"filter_and_highlights_help": "Filters & Highlights Help",
"filter_and_highlights": "Filters & Highlights"
"filter_and_highlights": "Filters & Highlights",
"boards_you_moderate": "boards you moderate"
}
+2 -1
View File
@@ -294,5 +294,6 @@
"search_results_for": "Resultados de búsqueda para",
"help": "Ayuda",
"filter_and_highlights_help": "Ayuda de filtros y resaltes",
"filter_and_highlights": "Filtros y resaltados"
"filter_and_highlights": "Filtros y resaltados",
"boards_you_moderate": "tableros que moderas"
}
+2 -1
View File
@@ -294,5 +294,6 @@
"search_results_for": "نتایج جستجو برای",
"help": "کمک",
"filter_and_highlights_help": "کمک فیلترها و برجسته‌ها",
"filter_and_highlights": "فیلترها و برجسته‌ها"
"filter_and_highlights": "فیلترها و برجسته‌ها",
"boards_you_moderate": "تابلوهایی که مدیریت می‌کنید"
}
+2 -1
View File
@@ -294,5 +294,6 @@
"search_results_for": "Hakutulokset hakusanalle",
"help": "Apua",
"filter_and_highlights_help": "Suodattimet ja korostukset -apu",
"filter_and_highlights": "Suodattimet ja korostukset"
"filter_and_highlights": "Suodattimet ja korostukset",
"boards_you_moderate": "lautakunnat, joita valvot"
}
+2 -1
View File
@@ -294,5 +294,6 @@
"search_results_for": "Mga resulta ng paghahanap para sa",
"help": "Tulong",
"filter_and_highlights_help": "Tulong sa mga filter at highlight",
"filter_and_highlights": "Mga filter at highlight"
"filter_and_highlights": "Mga filter at highlight",
"boards_you_moderate": "mga board na iyong minomoderate"
}
+2 -1
View File
@@ -294,5 +294,6 @@
"search_results_for": "Résultats de recherche pour",
"help": "Aide",
"filter_and_highlights_help": "Aide sur les filtres et surlignages",
"filter_and_highlights": "Filtres et surlignages"
"filter_and_highlights": "Filtres et surlignages",
"boards_you_moderate": "boards que vous modérez"
}
+2 -1
View File
@@ -294,5 +294,6 @@
"search_results_for": "תוצאות חיפוש עבור",
"help": "עזרה",
"filter_and_highlights_help": "עזרה במסננים והדגשות",
"filter_and_highlights": "מסננים והדגשות"
"filter_and_highlights": "מסננים והדגשות",
"boards_you_moderate": "לוחות שאתה מפקח עליהם"
}
+2 -1
View File
@@ -294,5 +294,6 @@
"search_results_for": "के लिए खोज परिणाम",
"help": "मदद",
"filter_and_highlights_help": "फ़िल्टर और हाइलाइट्स सहायता",
"filter_and_highlights": "फ़िल्टर और हाइलाइट्स"
"filter_and_highlights": "फ़िल्टर और हाइलाइट्स",
"boards_you_moderate": "बोर्ड जो आप मॉडरेट करते हैं"
}
+2 -1
View File
@@ -294,5 +294,6 @@
"search_results_for": "Keresési eredmények",
"help": "Segítség",
"filter_and_highlights_help": "Szűrők és kiemelések súgó",
"filter_and_highlights": "Szűrők és kiemelések"
"filter_and_highlights": "Szűrők és kiemelések",
"boards_you_moderate": "boardok, amelyeket moderálsz"
}
+2 -1
View File
@@ -294,5 +294,6 @@
"search_results_for": "Hasil pencarian untuk",
"help": "Bantuan",
"filter_and_highlights_help": "Bantuan Filter dan Sorotan",
"filter_and_highlights": "Filter dan Sorotan"
"filter_and_highlights": "Filter dan Sorotan",
"boards_you_moderate": "board yang Anda moderasi"
}
+2 -1
View File
@@ -294,5 +294,6 @@
"search_results_for": "Risultati di ricerca per",
"help": "Aiuto",
"filter_and_highlights_help": "Aiuto su Filtri e Evidenziazioni",
"filter_and_highlights": "Filtri e Evidenziazioni"
"filter_and_highlights": "Filtri e Evidenziazioni",
"boards_you_moderate": "board che moderi"
}
+2 -1
View File
@@ -294,5 +294,6 @@
"search_results_for": "検索結果",
"help": "ヘルプ",
"filter_and_highlights_help": "フィルターとハイライトのヘルプ",
"filter_and_highlights": "フィルターとハイライト"
"filter_and_highlights": "フィルターとハイライト",
"boards_you_moderate": "あなたが管理するボード"
}
+2 -1
View File
@@ -294,5 +294,6 @@
"search_results_for": "검색 결과",
"help": "도움말",
"filter_and_highlights_help": "필터 및 하이라이트 도움말",
"filter_and_highlights": "필터 및 하이라이트"
"filter_and_highlights": "필터 및 하이라이트",
"boards_you_moderate": "당신이 관리하는 게시판"
}
+2 -1
View File
@@ -294,5 +294,6 @@
"search_results_for": "सर्च परिणाम",
"help": "मदत",
"filter_and_highlights_help": "फिल्टर आणि हायलाइट्स सहाय्य",
"filter_and_highlights": "फिल्टर आणि हायलाइट्स"
"filter_and_highlights": "फिल्टर आणि हायलाइट्स",
"boards_you_moderate": "तुम्ही ज्यांचे नियंत्रण करता अशा बोर्ड्स"
}
+2 -1
View File
@@ -294,5 +294,6 @@
"search_results_for": "Zoekresultaten voor",
"help": "Help",
"filter_and_highlights_help": "Filters en Markeringen Help",
"filter_and_highlights": "Filters en markeringen"
"filter_and_highlights": "Filters en markeringen",
"boards_you_moderate": "boards die je beheert"
}
+2 -1
View File
@@ -294,5 +294,6 @@
"search_results_for": "Søkeresultater for",
"help": "Hjelp",
"filter_and_highlights_help": "Filtre og fremhevinger Hjelp",
"filter_and_highlights": "Filtre og uthevinger"
"filter_and_highlights": "Filtre og uthevinger",
"boards_you_moderate": "boards du modererer"
}
+2 -1
View File
@@ -294,5 +294,6 @@
"search_results_for": "Wyniki wyszukiwania dla",
"help": "Pomoc",
"filter_and_highlights_help": "Pomoc dotycząca filtrów i wyróżnień",
"filter_and_highlights": "Filtry i wyróżnienia"
"filter_and_highlights": "Filtry i wyróżnienia",
"boards_you_moderate": "boards, które moderujesz"
}
+2 -1
View File
@@ -294,5 +294,6 @@
"search_results_for": "Resultados de pesquisa para",
"help": "Ajuda",
"filter_and_highlights_help": "Ajuda de Filtros e Realces",
"filter_and_highlights": "Filtros e Destaques"
"filter_and_highlights": "Filtros e Destaques",
"boards_you_moderate": "boards que você modera"
}
+2 -1
View File
@@ -294,5 +294,6 @@
"search_results_for": "Rezultatele căutării pentru",
"help": "Ajutor",
"filter_and_highlights_help": "Ajutor pentru filtre și evidențieri",
"filter_and_highlights": "Filtre și evidențieri"
"filter_and_highlights": "Filtre și evidențieri",
"boards_you_moderate": "boards pe care le moderezi"
}
+2 -1
View File
@@ -294,5 +294,6 @@
"search_results_for": "Результаты поиска для",
"help": "Помощь",
"filter_and_highlights_help": "Помощь с фильтрами и выделениями",
"filter_and_highlights": "Фильтры и выделения"
"filter_and_highlights": "Фильтры и выделения",
"boards_you_moderate": "boards, которые вы модерируете"
}
+2 -1
View File
@@ -294,5 +294,6 @@
"search_results_for": "Rezultatet e kërkimit për",
"help": "Ndihmë",
"filter_and_highlights_help": "Ndihma për Filtro & Thekso",
"filter_and_highlights": "Filtra dhe thekson"
"filter_and_highlights": "Filtra dhe thekson",
"boards_you_moderate": "boards që i moderoni"
}
+2 -1
View File
@@ -294,5 +294,6 @@
"search_results_for": "Sökresultat för",
"help": "Hjälp",
"filter_and_highlights_help": "Filter och markeringar Hjälp",
"filter_and_highlights": "Filter och markeringar"
"filter_and_highlights": "Filter och markeringar",
"boards_you_moderate": "boards du modererar"
}
+2 -1
View File
@@ -294,5 +294,6 @@
"search_results_for": "శోధన ఫలితాలు",
"help": "సహాయం",
"filter_and_highlights_help": "ఫిల్టర్లు మరియు హైలైట్ సహాయం",
"filter_and_highlights": "ఫిల్టర్లు మరియు హైలైట్స్"
"filter_and_highlights": "ఫిల్టర్లు మరియు హైలైట్స్",
"boards_you_moderate": "మీరు మోడరేట్ చేసే బోర్డులు"
}
+2 -1
View File
@@ -294,5 +294,6 @@
"search_results_for": "ผลการค้นหาสำหรับ",
"help": "ช่วยเหลือ",
"filter_and_highlights_help": "ตัวกรองและการเน้นความช่วยเหลือ",
"filter_and_highlights": "ตัวกรองและไฮไลท์"
"filter_and_highlights": "ตัวกรองและไฮไลท์",
"boards_you_moderate": "บอร์ดที่คุณดูแล"
}
+2 -1
View File
@@ -294,5 +294,6 @@
"search_results_for": "Arama sonuçları için",
"help": "Yardım",
"filter_and_highlights_help": "Filtreler ve Vurgular Yardımı",
"filter_and_highlights": "Filtreler ve Vurgular"
"filter_and_highlights": "Filtreler ve Vurgular",
"boards_you_moderate": "modere ettiğiniz panolar"
}
+2 -1
View File
@@ -294,5 +294,6 @@
"search_results_for": "Результати пошуку для",
"help": "Допомога",
"filter_and_highlights_help": "Допомога з фільтрами та підсвічуваннями",
"filter_and_highlights": "Фільтри та виділення"
"filter_and_highlights": "Фільтри та виділення",
"boards_you_moderate": "boards, які ви модеруватимете"
}
+2 -1
View File
@@ -294,5 +294,6 @@
"search_results_for": "تلاش کے نتائج",
"help": "مدد",
"filter_and_highlights_help": "فلٹرز اور ہائی لائٹس کی مدد",
"filter_and_highlights": "فلٹرز اور ہائلाइटس"
"filter_and_highlights": "فلٹرز اور ہائلाइटس",
"boards_you_moderate": "وہ بورڈز جن کی آپ نگرانی کرتے ہیں"
}
+2 -1
View File
@@ -294,5 +294,6 @@
"search_results_for": "Kết quả tìm kiếm cho",
"help": "Trợ giúp",
"filter_and_highlights_help": "Trợ giúp bộ lọc và đánh dấu",
"filter_and_highlights": "Bộ lọc và Nổi bật"
"filter_and_highlights": "Bộ lọc và Nổi bật",
"boards_you_moderate": "boards bạn điều hành"
}
+2 -1
View File
@@ -294,5 +294,6 @@
"search_results_for": "搜索结果",
"help": "帮助",
"filter_and_highlights_help": "过滤器和高亮帮助",
"filter_and_highlights": "筛选器与高亮"
"filter_and_highlights": "筛选器与高亮",
"boards_you_moderate": "您管理的板块"
}
+1 -1
View File
@@ -17,7 +17,7 @@ let releaseChangelog =
// format
releaseChangelog = releaseChangelog.trim().replace(/\n\n+/g, '\n\n')
const releaseBody = `This version fixes a bug where the android app would launch in fullscreen mode.
const releaseBody = `This version adds minor features and fixes some bugs.
- Web app: https://plebchan.app
- Decentralized web app: https://plebchan.eth (only works on [Brave Browser](https://brave.com/) or via [IPFS Companion](https://docs.ipfs.tech/install/ipfs-companion/#prerequisites))
+5 -4
View File
@@ -2,7 +2,7 @@ import { useEffect, useState } from 'react';
import { Outlet, Route, Routes, useLocation, useParams } from 'react-router-dom';
import { useAccountComment } from '@plebbit/plebbit-react-hooks';
import { initSnow, removeSnow } from './lib/snow';
import { isAllView, isSubscriptionsView } from './lib/utils/view-utils';
import { isAllView, isModView, isSubscriptionsView } from './lib/utils/view-utils';
import useReplyModalStore from './stores/use-reply-modal-store';
import useSpecialThemeStore from './stores/use-special-theme-store';
import useIsMobile from './hooks/use-is-mobile';
@@ -30,6 +30,7 @@ const BoardLayout = () => {
const isMobile = useIsMobile();
const isInAllView = isAllView(location.pathname);
const isInSubscriptionsView = isSubscriptionsView(location.pathname, useParams());
const isInModView = isModView(location.pathname);
const pendingPost = useAccountComment({ commentIndex: accountCommentIndex ? parseInt(accountCommentIndex) : undefined });
// Christmas theme
@@ -53,17 +54,17 @@ const BoardLayout = () => {
<TopBar />
<BoardHeader />
{isMobile
? (subplebbitAddress || isInAllView || isInSubscriptionsView || pendingPost?.subplebbitAddress) && (
? (subplebbitAddress || isInAllView || isInModView || isInSubscriptionsView || pendingPost?.subplebbitAddress) && (
<>
<PostForm key={key} />
<hr />
<MobileBoardButtons />
</>
)
: (subplebbitAddress || isInAllView || isInSubscriptionsView || pendingPost?.subplebbitAddress) && (
: (subplebbitAddress || isInAllView || isInModView || isInSubscriptionsView || pendingPost?.subplebbitAddress) && (
<>
<PostForm key={key} />
{!(isInAllView || isInSubscriptionsView) && <SubplebbitStats />}
{!(isInAllView || isInSubscriptionsView || isInModView) && <SubplebbitStats />}
<DesktopBoardButtons />
</>
)}
@@ -88,6 +88,27 @@
margin-bottom: 15px;
}
.searchOPsInput {
height: 16px;
line-height: 16px;
margin-left: 2px;
padding: 0 2px;
width: 120px;
margin: 0px;
margin-right: 4px;
padding: 0 2px;
border: var(--post-form-field-input-border, revert);
background-color: var(--post-form-field-input-background-color, revert);
color: var(--post-form-field-input-color);
outline: none;
font-family: arial, helvetica, sans-serif;
font-size: 10pt;
}
.searchOPsInput:focus {
border: var(--post-form-field-input-focus-border, revert);
}
@media (max-width: 640px) {
.desktopBoardButtons {
display: none;
+69 -16
View File
@@ -3,7 +3,7 @@ import { Link, useLocation, useNavigate, useParams } from 'react-router-dom';
import { useAccountComment, useSubscribe } from '@plebbit/plebbit-react-hooks';
import useSubplebbitsStore from '@plebbit/plebbit-react-hooks/dist/stores/subplebbits';
import useSubplebbitsPagesStore from '@plebbit/plebbit-react-hooks/dist/stores/subplebbits-pages';
import { isAllView, isCatalogView, isDescriptionView, isPendingPostView, isPostPageView, isSubscriptionsView } from '../../lib/utils/view-utils';
import { isAllView, isCatalogView, isDescriptionView, isModView, isPendingPostView, isPostPageView, isSubscriptionsView } from '../../lib/utils/view-utils';
import useCatalogFiltersStore from '../../stores/use-catalog-filters-store';
import useCatalogStyleStore from '../../stores/use-catalog-style-store';
import useFeedResetStore from '../../stores/use-feed-reset-store';
@@ -22,10 +22,11 @@ interface BoardButtonsProps {
isInAllView?: boolean;
isInCatalogView?: boolean;
isInSubscriptionsView?: boolean;
isInModView?: boolean;
isTopbar?: boolean;
}
const CatalogButton = ({ address, isInAllView, isInSubscriptionsView }: BoardButtonsProps) => {
const CatalogButton = ({ address, isInAllView, isInSubscriptionsView, isInModView }: BoardButtonsProps) => {
const { t } = useTranslation();
const params = useParams();
@@ -36,6 +37,9 @@ const CatalogButton = ({ address, isInAllView, isInSubscriptionsView }: BoardBut
} else if (isInSubscriptionsView) {
if (params?.timeFilterName) return `/p/subscriptions/catalog/${params.timeFilterName}`;
return `/p/subscriptions/catalog`;
} else if (isInModView) {
if (params?.timeFilterName) return `/p/mod/catalog/${params.timeFilterName}`;
return `/p/mod/catalog`;
}
return `/p/${address}/catalog`;
};
@@ -58,7 +62,7 @@ const SubscribeButton = ({ address }: BoardButtonsProps) => {
);
};
const ReturnButton = ({ address, isInAllView, isInSubscriptionsView }: BoardButtonsProps) => {
const ReturnButton = ({ address, isInAllView, isInSubscriptionsView, isInModView }: BoardButtonsProps) => {
const { t } = useTranslation();
const params = useParams();
@@ -69,6 +73,9 @@ const ReturnButton = ({ address, isInAllView, isInSubscriptionsView }: BoardButt
} else if (isInSubscriptionsView) {
if (params?.timeFilterName) return `/p/subscriptions/${params.timeFilterName}`;
return `/p/subscriptions`;
} else if (isInModView) {
if (params?.timeFilterName) return `/p/mod/${params.timeFilterName}`;
return `/p/mod`;
}
return `/p/${address}`;
};
@@ -190,7 +197,7 @@ const ShowOPCommentOption = () => {
);
};
export const TimeFilter = ({ isInAllView, isInCatalogView, isInSubscriptionsView, isTopbar = false }: BoardButtonsProps) => {
export const TimeFilter = ({ isInAllView, isInCatalogView, isInSubscriptionsView, isInModView, isTopbar = false }: BoardButtonsProps) => {
const { t } = useTranslation();
const navigate = useNavigate();
const { timeFilterName, timeFilterNames } = useTimeFilter();
@@ -205,6 +212,10 @@ export const TimeFilter = ({ isInAllView, isInCatalogView, isInSubscriptionsView
? isInCatalogView
? `/p/subscriptions/catalog/${timeFilterName}`
: `/p/subscriptions/${timeFilterName}`
: isInModView
? isInCatalogView
? `/p/mod/catalog/${timeFilterName}`
: `/p/mod/${timeFilterName}`
: null;
link && navigate(link);
};
@@ -242,6 +253,7 @@ export const MobileBoardButtons = () => {
const isInPendingPostPage = isPendingPostView(location.pathname, params);
const isInPostView = isPostPageView(location.pathname, params);
const isInSubscriptionsView = isSubscriptionsView(location.pathname, useParams());
const isInModView = isModView(location.pathname);
const accountComment = useAccountComment({ commentIndex: params?.accountCommentIndex as any });
const subplebbitAddress = params?.subplebbitAddress || accountComment?.subplebbitAddress;
@@ -252,8 +264,8 @@ export const MobileBoardButtons = () => {
<div className={`${styles.mobileBoardButtons} ${!isInCatalogView ? styles.addMargin : ''}`}>
{isInPostView || isInPendingPostPage ? (
<>
<ReturnButton address={subplebbitAddress} isInAllView={isInAllView} isInSubscriptionsView={isInSubscriptionsView} />
<CatalogButton address={subplebbitAddress} isInAllView={isInAllView} isInSubscriptionsView={isInSubscriptionsView} />
<ReturnButton address={subplebbitAddress} isInAllView={isInAllView} isInSubscriptionsView={isInSubscriptionsView} isInModView={isInModView} />
<CatalogButton address={subplebbitAddress} isInAllView={isInAllView} isInSubscriptionsView={isInSubscriptionsView} isInModView={isInModView} />
<SubscribeButton address={subplebbitAddress} />
<div className={styles.secondRow}>
<UpdateButton />
@@ -263,11 +275,11 @@ export const MobileBoardButtons = () => {
) : (
<>
{isInCatalogView ? (
<ReturnButton address={subplebbitAddress} isInAllView={isInAllView} isInSubscriptionsView={isInSubscriptionsView} />
<ReturnButton address={subplebbitAddress} isInAllView={isInAllView} isInSubscriptionsView={isInSubscriptionsView} isInModView={isInModView} />
) : (
<CatalogButton address={subplebbitAddress} isInAllView={isInAllView} isInSubscriptionsView={isInSubscriptionsView} />
<CatalogButton address={subplebbitAddress} isInAllView={isInAllView} isInSubscriptionsView={isInSubscriptionsView} isInModView={isInModView} />
)}
{!(isInAllView || isInSubscriptionsView) && <SubscribeButton address={subplebbitAddress} />}
{!(isInAllView || isInSubscriptionsView || isInModView) && <SubscribeButton address={subplebbitAddress} />}
<RefreshButton />
{isInCatalogView && searchText ? (
<span className={styles.filteredThreadsCount}>
@@ -338,6 +350,7 @@ export const DesktopBoardButtons = () => {
const isInPendingPostPage = isPendingPostView(location.pathname, params);
const isInPostView = isPostPageView(location.pathname, params);
const isInSubscriptionsView = isSubscriptionsView(location.pathname, useParams());
const isInModView = isModView(location.pathname);
const { filteredCount, searchText } = useCatalogFiltersStore();
@@ -348,9 +361,9 @@ export const DesktopBoardButtons = () => {
{isInPostView || isInPendingPostPage ? (
<>
[
<ReturnButton address={subplebbitAddress} isInAllView={isInAllView} isInSubscriptionsView={isInSubscriptionsView} />
<ReturnButton address={subplebbitAddress} isInAllView={isInAllView} isInSubscriptionsView={isInSubscriptionsView} isInModView={isInModView} />
] [
<CatalogButton address={subplebbitAddress} isInAllView={isInAllView} isInSubscriptionsView={isInSubscriptionsView} />] [
<CatalogButton address={subplebbitAddress} isInAllView={isInAllView} isInSubscriptionsView={isInSubscriptionsView} isInModView={isInModView} />] [
<UpdateButton />
] [
<AutoButton />]
@@ -363,12 +376,13 @@ export const DesktopBoardButtons = () => {
{isInCatalogView ? (
<>
[
<ReturnButton address={subplebbitAddress} isInAllView={isInAllView} isInSubscriptionsView={isInSubscriptionsView} />]{' '}
<ReturnButton address={subplebbitAddress} isInAllView={isInAllView} isInSubscriptionsView={isInSubscriptionsView} isInModView={isInModView} />]{' '}
</>
) : (
<>
<SearchOPsBar />
[
<CatalogButton address={subplebbitAddress} isInAllView={isInAllView} isInSubscriptionsView={isInSubscriptionsView} />]{' '}
<CatalogButton address={subplebbitAddress} isInAllView={isInAllView} isInSubscriptionsView={isInSubscriptionsView} isInModView={isInModView} />]{' '}
</>
)}
[<RefreshButton />]
@@ -394,10 +408,10 @@ export const DesktopBoardButtons = () => {
<ShowOPCommentOption />
</>
)}
{(isInAllView || isInSubscriptionsView) && (
<TimeFilter isInAllView={isInAllView} isInCatalogView={isInCatalogView} isInSubscriptionsView={isInSubscriptionsView} />
{(isInAllView || isInSubscriptionsView || isInModView) && (
<TimeFilter isInAllView={isInAllView} isInCatalogView={isInCatalogView} isInSubscriptionsView={isInSubscriptionsView} isInModView={isInModView} />
)}
{!(isInAllView || isInSubscriptionsView) && (
{!(isInAllView || isInSubscriptionsView || isInModView) && (
<>
[
<SubscribeButton address={subplebbitAddress} />]
@@ -415,3 +429,42 @@ export const DesktopBoardButtons = () => {
</>
);
};
const SearchOPsBar = () => {
const { t } = useTranslation();
const navigate = useNavigate();
const params = useParams();
const location = useLocation();
const isInAllView = isAllView(location.pathname);
const isInSubscriptionsView = isSubscriptionsView(location.pathname, useParams());
const isInModView = isModView(location.pathname);
const handleSearch = (event: React.KeyboardEvent<HTMLInputElement>) => {
if (event.key === 'Enter') {
const searchQuery = (event.target as HTMLInputElement).value.trim();
if (searchQuery) {
let catalogUrl = '';
if (isInAllView) {
catalogUrl = params?.timeFilterName
? `/p/all/catalog/${params.timeFilterName}?q=${encodeURIComponent(searchQuery)}`
: `/p/all/catalog?q=${encodeURIComponent(searchQuery)}`;
} else if (isInSubscriptionsView) {
catalogUrl = params?.timeFilterName
? `/p/subscriptions/catalog/${params.timeFilterName}?q=${encodeURIComponent(searchQuery)}`
: `/p/subscriptions/catalog?q=${encodeURIComponent(searchQuery)}`;
} else if (isInModView) {
catalogUrl = params?.timeFilterName
? `/p/mod/catalog/${params.timeFilterName}?q=${encodeURIComponent(searchQuery)}`
: `/p/mod/catalog?q=${encodeURIComponent(searchQuery)}`;
} else {
catalogUrl = `/p/${params?.subplebbitAddress}/catalog?q=${encodeURIComponent(searchQuery)}`;
}
navigate(catalogUrl);
}
}
};
return <input type='text' placeholder={t('search_ops_placeholder', 'Search OPs...')} onKeyDown={handleSearch} className={styles.searchOPsInput} />;
};
+17 -2
View File
@@ -1,6 +1,8 @@
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useLocation, useParams } from 'react-router-dom';
import { useAccountComment } from '@plebbit/plebbit-react-hooks';
import Plebbit from '@plebbit/plebbit-js';
import useSubplebbitsStore from '@plebbit/plebbit-react-hooks/dist/stores/subplebbits';
import { isAllView, isSubscriptionsView, isModView } from '../../lib/utils/view-utils';
import styles from './board-header.module.css';
@@ -9,6 +11,7 @@ import useIsMobile from '../../hooks/use-is-mobile';
import useIsSubplebbitOffline from '../../hooks/use-is-subplebbit-offline';
import { shouldShowSnow } from '../../lib/snow';
import Tooltip from '../tooltip';
import _ from 'lodash';
const totalBanners = 63;
@@ -22,6 +25,7 @@ const ImageBanner = () => {
};
const BoardHeader = () => {
const { t } = useTranslation();
const location = useLocation();
const params = useParams();
const isInAllView = isAllView(location.pathname);
@@ -35,7 +39,13 @@ const BoardHeader = () => {
const { address, shortAddress } = subplebbit || {};
const multisubMetadata = useMultisubMetadata();
const title = isInAllView ? multisubMetadata?.title || 'all' : isInSubscriptionsView ? 'Subscriptions' : isInModView ? 'Mod' : subplebbit?.title;
const title = isInAllView
? multisubMetadata?.title || 'all'
: isInSubscriptionsView
? 'Subscriptions'
: isInModView
? _.startCase(t('boards_you_moderate'))
: subplebbit?.title;
const subtitle = isInAllView ? 'p/all' : isInSubscriptionsView ? 'p/subscriptions' : isInModView ? 'p/mod' : `p/${address}`;
const { isOffline, isOnlineStatusLoading, offlineIconClass, offlineTitle } = useIsSubplebbitOffline(subplebbit);
@@ -48,7 +58,12 @@ const BoardHeader = () => {
</div>
)}
<div className={styles.boardTitle}>
{title || (shortAddress ? (shortAddress.endsWith('.eth') || shortAddress.endsWith('.sol') ? shortAddress.slice(0, -4) : shortAddress) : subplebbitAddress)}
{title ||
(shortAddress
? shortAddress.endsWith('.eth') || shortAddress.endsWith('.sol')
? shortAddress.slice(0, -4)
: shortAddress
: Plebbit.getShortAddress(subplebbitAddress))}
{(isOffline || isOnlineStatusLoading) && !isInAllView && !isInSubscriptionsView && !isInModView && (
<span className={styles.offlineIconWrapper}>
<Tooltip content={offlineTitle}>
@@ -1,11 +1,11 @@
.button {
text-transform: capitalize;
color: var(--button-desktop-text-color);
text-decoration: var(--button-text-decoration);
color: var(--button-desktop-text-color, revert);
text-decoration: var(--button-text-decoration, revert);
}
.button:hover {
color: var(--button-desktop-text-color-hover);
color: var(--button-desktop-text-color-hover, revert);
cursor: pointer;
}
@@ -22,23 +22,23 @@
margin: 0px 5px;
padding: 1px;
outline: none;
border: 1px solid #AAA;
border: var(--post-form-field-input-border, revert);
font-size: 11px;
}
.searchContainer input:focus {
border: var(--post-form-field-input-focus-border);
border: var(--post-form-field-input-focus-border, revert);
}
.closeSearch {
cursor: pointer;
border: none;
white-space: nowrap;
color: var(--button-desktop-text-color);
color: var(--button-desktop-text-color, revert);
}
.closeSearch:hover {
color: var(--button-desktop-text-color-hover);
color: var(--button-desktop-text-color-hover, revert);
}
@media (max-width: 640px) {
@@ -1,5 +1,6 @@
import { useEffect, useState, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useLocation, useNavigate } from 'react-router-dom';
import styles from './catalog-search.module.css';
import useIsMobile from '../../hooks/use-is-mobile';
import useCatalogFiltersStore from '../../stores/use-catalog-filters-store';
@@ -7,21 +8,52 @@ import _ from 'lodash';
const CatalogSearch = () => {
const { t } = useTranslation();
const location = useLocation();
const navigate = useNavigate();
const [openSearch, setOpenSearch] = useState(false);
const [inputValue, setInputValue] = useState('');
const { setSearchFilter, clearSearchFilter } = useCatalogFiltersStore();
// Create a debounced version of setSearchFilter
// Extract query parameter from URL
useEffect(() => {
const urlParams = new URLSearchParams(location.search);
const queryParam = urlParams.get('q');
if (queryParam) {
setInputValue(queryParam);
setSearchFilter(queryParam);
setOpenSearch(true);
}
}, [location.search, setSearchFilter]);
// Update URL when search changes
const updateURL = useCallback(
(searchText: string) => {
const urlParams = new URLSearchParams(location.search);
if (searchText.trim()) {
urlParams.set('q', searchText);
} else {
urlParams.delete('q');
}
const newSearch = urlParams.toString();
const newPath = location.pathname + (newSearch ? `?${newSearch}` : '');
navigate(newPath, { replace: true });
},
[location.pathname, location.search, navigate],
);
// Create a debounced version of setSearchFilter and URL update
// eslint-disable-next-line react-hooks/exhaustive-deps
const debouncedSetSearchFilter = useCallback(
_.debounce((text: string) => {
if (text.trim()) {
setSearchFilter(text);
updateURL(text);
} else {
clearSearchFilter();
updateURL('');
}
}, 300),
[setSearchFilter, clearSearchFilter],
[setSearchFilter, clearSearchFilter, updateURL],
);
const handleToggleSearch = useCallback(() => {
@@ -30,14 +62,16 @@ const CatalogSearch = () => {
if (openSearch) {
setInputValue('');
clearSearchFilter();
updateURL('');
}
}, [openSearch, clearSearchFilter]);
}, [openSearch, clearSearchFilter, updateURL]);
const handleCloseSearch = useCallback(() => {
setOpenSearch(false);
setInputValue('');
clearSearchFilter();
}, [clearSearchFilter]);
updateURL('');
}, [clearSearchFilter, updateURL]);
useEffect(() => {
if (openSearch) {
@@ -96,8 +96,8 @@ const Challenge = ({ challenge, closeModal }: ChallengeProps) => {
className={styles.container}
ref={nodeRef}
style={{
x: isMobile ? 0 : x,
y: isMobile ? 0 : y,
x: isMobile ? window.innerWidth / 2 - 150 : x,
y: isMobile ? window.innerHeight / 2 - 200 : y,
touchAction: 'none',
}}
>
+55 -7
View File
@@ -11,8 +11,9 @@ import { isCatalogView } from '../../lib/utils/view-utils';
import useIsMobile from '../../hooks/use-is-mobile';
import CommentMedia from '../comment-media';
import styles from './markdown.module.css';
import { useLocation, useParams } from 'react-router-dom';
import { Link, useLocation, useParams } from 'react-router-dom';
import { canEmbed } from '../embed';
import { isPlebchanLink, transformPlebchanLinkToInternal, preprocessPlebchanPatterns } from '../../lib/utils/url-utils';
interface ContentLinkEmbedProps {
children: any;
@@ -153,6 +154,52 @@ interface MarkdownProps {
title?: string;
}
const renderAnchorLink = (children: React.ReactNode, href: string) => {
if (!href) {
return <span>{children}</span>;
}
// Check if this is a valid plebchan link that should be handled internally
if (isPlebchanLink(href)) {
const internalPath = transformPlebchanLinkToInternal(href);
if (internalPath) {
// Check if the link text should be replaced with the internal path
let shouldReplaceText = false;
if (typeof children === 'string') {
shouldReplaceText = children === href || children.trim() === href.trim();
} else if (Array.isArray(children) && children.length === 1 && typeof children[0] === 'string') {
shouldReplaceText = children[0] === href || children[0].trim() === href.trim();
}
// For display purposes, remove leading slash from paths like "/p/something"
let displayText: React.ReactNode = children;
if (shouldReplaceText && internalPath.startsWith('/p/')) {
displayText = internalPath.substring(1); // Remove leading slash
} else if (shouldReplaceText) {
displayText = internalPath;
}
return <Link to={internalPath}>{displayText}</Link>;
} else {
console.warn('Failed to transform plebchan link to internal path:', href);
return <Link to={href}>{children}</Link>;
}
}
// Handle hash routes and internal patterns (including routes that start with /#/)
if (href.startsWith('#/') || href.startsWith('/#/') || href.startsWith('/p/') || href.match(/^\/p\/[^/]+(\/c\/[^/]+)?$/)) {
return <Link to={href}>{children}</Link>;
}
// External links
return (
<a href={href} target='_blank' rel='noopener noreferrer'>
{children}
</a>
);
};
const Markdown = ({ content, title }: MarkdownProps) => {
const remarkPlugins: any[] = [[supersub]];
@@ -179,6 +226,9 @@ const Markdown = ({ content, title }: MarkdownProps) => {
const isInCatalogView = isCatalogView(useLocation().pathname, useParams());
// Preprocess content to convert plain text plebchan patterns to markdown links
const processedContent = preprocessPlebchanPatterns(content || '');
return (
<span className={styles.markdown}>
{isInCatalogView && title && (
@@ -188,7 +238,7 @@ const Markdown = ({ content, title }: MarkdownProps) => {
</span>
)}
<ReactMarkdown
children={content}
children={processedContent}
remarkPlugins={remarkPlugins}
rehypePlugins={[[rehypeRaw as any], [rehypeSanitize, customSchema]]}
components={
@@ -220,12 +270,10 @@ const Markdown = ({ content, title }: MarkdownProps) => {
console.debug('Invalid URL:', href);
}
return (
<a href={href} target='_blank' rel='noopener noreferrer'>
{children}
</a>
);
return renderAnchorLink(children, href);
}
return renderAnchorLink(children, href || '');
},
} as ExtendedComponents
}
+23 -7
View File
@@ -1,13 +1,14 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useLocation, useNavigate, useParams } from 'react-router-dom';
import { Comment, setAccount, useAccount, useAccountComment, useEditedComment } from '@plebbit/plebbit-react-hooks';
import { Comment, setAccount, useAccount, useAccountComment, useAccountSubplebbits, useEditedComment } from '@plebbit/plebbit-react-hooks';
import Plebbit from '@plebbit/plebbit-js';
import useSubplebbitsStore from '@plebbit/plebbit-react-hooks/dist/stores/subplebbits';
import useSubplebbitsPagesStore from '@plebbit/plebbit-react-hooks/dist/stores/subplebbits-pages';
import { getHasThumbnail, getLinkMediaInfo } from '../../lib/utils/media-utils';
import { formatMarkdown } from '../../lib/utils/post-utils';
import { isValidURL } from '../../lib/utils/url-utils';
import { isAllView, isDescriptionView, isPostPageView, isRulesView, isSubscriptionsView } from '../../lib/utils/view-utils';
import { isAllView, isDescriptionView, isModView, isPostPageView, isRulesView, isSubscriptionsView } from '../../lib/utils/view-utils';
import useAnonMode from '../../hooks/use-anon-mode';
import { useDefaultSubplebbitAddresses } from '../../hooks/use-default-subplebbits';
import useFetchGifFirstFrame from '../../hooks/use-fetch-gif-first-frame';
@@ -53,10 +54,14 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
const location = useLocation();
const isInAllView = isAllView(location.pathname);
const isInModView = isModView(location.pathname);
const isInSubscriptionsView = isSubscriptionsView(location.pathname, useParams());
const subscriptions = account?.subscriptions || [];
const defaultSubplebbitAddresses = useDefaultSubplebbitAddresses();
const { accountSubplebbits } = useAccountSubplebbits();
const accountSubplebbitAddresses = Object.keys(accountSubplebbits);
const { anonMode, getNewSigner, getExistingSigner } = useAnonMode(postCid);
const comment = useSubplebbitsPagesStore((state) => state.comments[postCid]);
const address = comment?.author?.address;
@@ -129,7 +134,7 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
return;
}
if ((isInAllView || isInSubscriptionsView) && !publishPostOptions.subplebbitAddress) {
if ((isInAllView || isInSubscriptionsView || isInModView) && !publishPostOptions.subplebbitAddress) {
alert(t('no_board_selected_warning'));
return;
}
@@ -394,7 +399,7 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
]
</td>
</tr>
{(isInAllView || isInSubscriptionsView) && (
{(isInAllView || isInSubscriptionsView || isInModView) && (
<tr>
<td>{t('board')}</td>
<td>
@@ -403,7 +408,13 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
{isInAllView &&
defaultSubplebbitAddresses.map((address: string) => (
<option key={address} value={address}>
{address}
{address && Plebbit.getShortAddress(address)}
</option>
))}
{isInModView &&
accountSubplebbitAddresses.map((address: string) => (
<option key={address} value={address}>
{address && Plebbit.getShortAddress(address)}
</option>
))}
{isInSubscriptionsView &&
@@ -429,6 +440,7 @@ const PostForm = () => {
const isInPostView = isPostPageView(location.pathname, params);
const isInRulesView = isRulesView(location.pathname, params);
const isInAllView = isAllView(location.pathname);
const isInModView = isModView(location.pathname);
const isInSubscriptionsView = isSubscriptionsView(location.pathname, params);
const commentCid = params?.commentCid;
@@ -453,7 +465,9 @@ const PostForm = () => {
return (
<>
<div className={styles.postFormDesktop}>
{!(isInAllView || isInSubscriptionsView) && showForm && (isOffline || isOnlineStatusLoading) && <div className={styles.offlineBoard}>{offlineTitle}</div>}
{!(isInAllView || isInSubscriptionsView || isInModView) && showForm && (isOffline || isOnlineStatusLoading) && (
<div className={styles.offlineBoard}>{offlineTitle}</div>
)}
{isThreadClosed ? (
<div className={styles.closed}>
{t('thread_closed')}
@@ -473,7 +487,9 @@ const PostForm = () => {
)}
</div>
<div className={styles.postFormMobile}>
{!(isInAllView || isInSubscriptionsView) && showForm && (isOffline || isOnlineStatusLoading) && <div className={styles.offlineBoard}>{offlineTitle}</div>}
{!(isInAllView || isInSubscriptionsView || isInModView) && showForm && (isOffline || isOnlineStatusLoading) && (
<div className={styles.offlineBoard}>{offlineTitle}</div>
)}
{isThreadClosed ? (
<div className={styles.closed}>
{t('thread_closed')}
+17 -11
View File
@@ -1,5 +1,4 @@
import localForageLru from '@plebbit/plebbit-react-hooks/dist/lib/localforage-lru/index.js';
import extName from 'ext-name';
import { canEmbed } from '../../components/embed';
import memoize from 'memoizee';
import { isValidURL } from './url-utils';
@@ -72,6 +71,11 @@ const getPatternThumbnailUrl = (url: URL): string | undefined => {
}
};
// Known media file extensions - only these will be classified as media files
const KNOWN_IMAGE_EXTENSIONS = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'bmp', 'ico', 'tiff'];
const KNOWN_VIDEO_EXTENSIONS = ['mp4', 'webm', 'mov', 'avi', 'mkv', 'flv', 'wmv', 'm4v'];
const KNOWN_AUDIO_EXTENSIONS = ['mp3', 'wav', 'ogg', 'flac', 'aac', 'm4a', 'wma'];
export const getLinkMediaInfo = memoize(
(link: string): CommentMediaInfo | undefined => {
if (!isValidURL(link)) {
@@ -80,7 +84,6 @@ export const getLinkMediaInfo = memoize(
const url = new URL(link);
let patternThumbnailUrl: string | undefined;
let type: string = 'webpage';
let mime: string | undefined;
if (url.pathname === '/_next/image' && url.search.startsWith('?url=')) {
return { url: link, type: 'image' };
@@ -93,16 +96,19 @@ export const getLinkMediaInfo = memoize(
}
try {
mime = extName(url.pathname.toLowerCase().replace('/', ''))[0]?.mime;
if (mime) {
if (mime.startsWith('image')) {
type = mime === 'image/gif' ? 'gif' : 'image';
} else if (mime.startsWith('video')) {
type = 'video';
} else if (mime.startsWith('audio')) {
type = 'audio';
}
// Extract file extension
const pathParts = url.pathname.toLowerCase().split('.');
const extension = pathParts.length > 1 ? pathParts[pathParts.length - 1] : '';
// Only classify as media if we explicitly know the extension
if (KNOWN_IMAGE_EXTENSIONS.includes(extension)) {
type = extension === 'gif' ? 'gif' : 'image';
} else if (KNOWN_VIDEO_EXTENSIONS.includes(extension)) {
type = 'video';
} else if (KNOWN_AUDIO_EXTENSIONS.includes(extension)) {
type = 'audio';
}
// Unknown extensions remain as 'webpage'
if (!url.pathname.includes('.')) {
type = 'webpage';
+117
View File
@@ -21,3 +21,120 @@ export const copyShareLinkToClipboard = async (subplebbitAddress: string, cid: s
const shareLink = `https://pleb.bz/p/${subplebbitAddress}/c/${cid}?redirect=plebchan.app`;
await copyToClipboard(shareLink);
};
const PLEBCHAN_HOSTNAMES = ['pleb.bz', 'plebchan.app', 'plebchan.eth.limo', 'plebchan.eth.link', 'plebchan.eth.sucks', 'plebchan.netlify.app'];
// Check if a URL is a valid plebchan link that should be handled internally
export const isPlebchanLink = (url: string): boolean => {
try {
const parsedUrl = new URL(url);
const hostname = parsedUrl.hostname.replace(/^www\./, '');
if (!PLEBCHAN_HOSTNAMES.includes(hostname)) {
return false;
}
// Check both pathname and hash for the route pattern
let routePath = parsedUrl.pathname;
// If there's a hash that starts with #/, use that as the route path
if (parsedUrl.hash && parsedUrl.hash.startsWith('#/')) {
routePath = parsedUrl.hash.substring(1); // Remove the # to get the path
}
// For pleb.bz, only support the exact sharelink format
if (hostname === 'pleb.bz') {
// Must match exactly: /p/{subplebbitAddress}/c/{cid}
// Allow redirect parameter since these are still valid internal links
return /^\/p\/[^/]+\/c\/[^/]+$/.test(routePath);
}
// For other plebchan hostnames, support:
// - /p/{subplebbitAddress}
// - /p/{subplebbitAddress}/c/{commentCid}
return /^\/p\/[^/]+(\/c\/[^/]+)?$/.test(routePath);
} catch {
return false;
}
};
// Transform a valid plebchan URL to an internal route
export const transformPlebchanLinkToInternal = (url: string): string | null => {
if (!isPlebchanLink(url)) {
return null;
}
try {
const parsedUrl = new URL(url);
// Check if this is a hash-based route
if (parsedUrl.hash && parsedUrl.hash.startsWith('#/')) {
// Extract the route from the hash, preserving any query params within the hash
const hashPath = parsedUrl.hash.substring(1); // Remove the #
return hashPath;
}
// For regular pathname-based routes, remove redirect parameter from query string
const searchParams = new URLSearchParams(parsedUrl.search);
searchParams.delete('redirect'); // Remove redirect parameter for cleaner internal links
const cleanSearch = searchParams.toString();
const searchString = cleanSearch ? `?${cleanSearch}` : '';
return parsedUrl.pathname + searchString + parsedUrl.hash;
} catch {
return null;
}
};
// Check if a string is a valid IPNS public key (52 chars starting with 12D3KooW)
const isValidIPNSKey = (str: string): boolean => {
return str.length === 52 && str.startsWith('12D3KooW');
};
// Check if a string is a valid domain (contains a dot)
const isValidDomain = (str: string): boolean => {
return str.includes('.') && str.split('.').length >= 2 && str.split('.').every((part) => part.length > 0);
};
// Check if a plain text pattern is a valid plebchan subplebbit reference
export const isValidSubplebbitPattern = (pattern: string): boolean => {
// Must start with "p/"
if (!pattern.startsWith('p/')) {
return false;
}
const pathPart = pattern.substring(2); // Remove "p/"
// Check if it's a post pattern: subplebbitAddress/c/cid
const postMatch = pathPart.match(/^([^/]+)\/c\/([^/]+)$/);
if (postMatch) {
const [, subplebbitAddress, cid] = postMatch;
// CID should be at least 10 characters (minimum reasonable CID length)
return (isValidDomain(subplebbitAddress) || isValidIPNSKey(subplebbitAddress)) && cid.length >= 10;
}
// Check if it's just a subplebbit pattern: subplebbitAddress
return isValidDomain(pathPart) || isValidIPNSKey(pathPart);
};
// Preprocess content to convert plain text plebchan patterns to markdown links
export const preprocessPlebchanPatterns = (content: string): string => {
// Pattern to match "p/something" or "p/something/c/something"
// Negative lookbehind prevents matching patterns that are already part of URLs
const pattern = /(?<!https?:\/\/[^\s]*)\bp\/([a-zA-Z0-9\-.]+(?:\/c\/[a-zA-Z0-9]{10,100})?)[.,:;!?]*/g;
return content.replace(pattern, (match, capturedPath) => {
// Remove any trailing punctuation from the captured path
const cleanPath = capturedPath.replace(/[.,:;!?]+$/, '');
const fullPattern = `p/${cleanPath}`;
if (isValidSubplebbitPattern(fullPattern)) {
// Preserve trailing punctuation outside the link
const trailingPunctuation = match.slice(fullPattern.length);
return `[${fullPattern}](/${fullPattern})${trailingPunctuation}`;
}
return match;
});
};
-2
View File
@@ -3,8 +3,6 @@ declare module '*.module.css' {
export default classes;
}
declare module 'ext-name';
declare module 'lodash';
declare module 'react-draggable';
+5 -3
View File
@@ -148,15 +148,17 @@ const Catalog = () => {
}
return true;
})
.map((subplebbit) => subplebbit.address);
.map((subplebbit) => subplebbit.address)
.filter(Boolean); // Filter out any undefined/null values
if (isInAllView) {
return filteredDefaultSubplebbits;
}
if (isInSubscriptionsView) {
return subscriptions || [];
return (subscriptions || []).filter(Boolean); // Filter out any undefined/null values
}
return [subplebbitAddress];
// Only include subplebbitAddress if it's defined
return subplebbitAddress ? [subplebbitAddress] : [];
}, [isInAllView, isInSubscriptionsView, subplebbitAddress, defaultSubplebbits, subscriptions, hideAdultBoards, hideGoreBoards]);
const { imageSize } = useCatalogStyleStore();