mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
@@ -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",
|
||||
|
||||
@@ -294,5 +294,6 @@
|
||||
"search_results_for": "نتائج البحث عن",
|
||||
"help": "مساعدة",
|
||||
"filter_and_highlights_help": "مساعدة الفلاتر والإبرازات",
|
||||
"filter_and_highlights": "التصفية والتسليط الضوء"
|
||||
"filter_and_highlights": "التصفية والتسليط الضوء",
|
||||
"boards_you_moderate": "المنتديات التي تقوم بالإشراف عليها"
|
||||
}
|
||||
@@ -294,5 +294,6 @@
|
||||
"search_results_for": "অনুসন্ধান ফলাফল",
|
||||
"help": "সাহায্য",
|
||||
"filter_and_highlights_help": "ফিল্টার এবং হাইলাইট সাহায্য",
|
||||
"filter_and_highlights": "ফিল্টার এবং হাইলাইট"
|
||||
"filter_and_highlights": "ফিল্টার এবং হাইলাইট",
|
||||
"boards_you_moderate": "আপনি যে বোর্ডগুলি পরিচালনা করেন"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -294,5 +294,6 @@
|
||||
"search_results_for": "Αποτελέσματα αναζήτησης για",
|
||||
"help": "Βοήθεια",
|
||||
"filter_and_highlights_help": "Βοήθεια φίλτρων και επισήμανσης",
|
||||
"filter_and_highlights": "Φίλτρα και Επισημάνσεις"
|
||||
"filter_and_highlights": "Φίλτρα και Επισημάνσεις",
|
||||
"boards_you_moderate": "Boards που διαχειρίζεστε"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -294,5 +294,6 @@
|
||||
"search_results_for": "نتایج جستجو برای",
|
||||
"help": "کمک",
|
||||
"filter_and_highlights_help": "کمک فیلترها و برجستهها",
|
||||
"filter_and_highlights": "فیلترها و برجستهها"
|
||||
"filter_and_highlights": "فیلترها و برجستهها",
|
||||
"boards_you_moderate": "تابلوهایی که مدیریت میکنید"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -294,5 +294,6 @@
|
||||
"search_results_for": "תוצאות חיפוש עבור",
|
||||
"help": "עזרה",
|
||||
"filter_and_highlights_help": "עזרה במסננים והדגשות",
|
||||
"filter_and_highlights": "מסננים והדגשות"
|
||||
"filter_and_highlights": "מסננים והדגשות",
|
||||
"boards_you_moderate": "לוחות שאתה מפקח עליהם"
|
||||
}
|
||||
@@ -294,5 +294,6 @@
|
||||
"search_results_for": "के लिए खोज परिणाम",
|
||||
"help": "मदद",
|
||||
"filter_and_highlights_help": "फ़िल्टर और हाइलाइट्स सहायता",
|
||||
"filter_and_highlights": "फ़िल्टर और हाइलाइट्स"
|
||||
"filter_and_highlights": "फ़िल्टर और हाइलाइट्स",
|
||||
"boards_you_moderate": "बोर्ड जो आप मॉडरेट करते हैं"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -294,5 +294,6 @@
|
||||
"search_results_for": "検索結果",
|
||||
"help": "ヘルプ",
|
||||
"filter_and_highlights_help": "フィルターとハイライトのヘルプ",
|
||||
"filter_and_highlights": "フィルターとハイライト"
|
||||
"filter_and_highlights": "フィルターとハイライト",
|
||||
"boards_you_moderate": "あなたが管理するボード"
|
||||
}
|
||||
@@ -294,5 +294,6 @@
|
||||
"search_results_for": "검색 결과",
|
||||
"help": "도움말",
|
||||
"filter_and_highlights_help": "필터 및 하이라이트 도움말",
|
||||
"filter_and_highlights": "필터 및 하이라이트"
|
||||
"filter_and_highlights": "필터 및 하이라이트",
|
||||
"boards_you_moderate": "당신이 관리하는 게시판"
|
||||
}
|
||||
@@ -294,5 +294,6 @@
|
||||
"search_results_for": "सर्च परिणाम",
|
||||
"help": "मदत",
|
||||
"filter_and_highlights_help": "फिल्टर आणि हायलाइट्स सहाय्य",
|
||||
"filter_and_highlights": "फिल्टर आणि हायलाइट्स"
|
||||
"filter_and_highlights": "फिल्टर आणि हायलाइट्स",
|
||||
"boards_you_moderate": "तुम्ही ज्यांचे नियंत्रण करता अशा बोर्ड्स"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -294,5 +294,6 @@
|
||||
"search_results_for": "Результаты поиска для",
|
||||
"help": "Помощь",
|
||||
"filter_and_highlights_help": "Помощь с фильтрами и выделениями",
|
||||
"filter_and_highlights": "Фильтры и выделения"
|
||||
"filter_and_highlights": "Фильтры и выделения",
|
||||
"boards_you_moderate": "boards, которые вы модерируете"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -294,5 +294,6 @@
|
||||
"search_results_for": "శోధన ఫలితాలు",
|
||||
"help": "సహాయం",
|
||||
"filter_and_highlights_help": "ఫిల్టర్లు మరియు హైలైట్ సహాయం",
|
||||
"filter_and_highlights": "ఫిల్టర్లు మరియు హైలైట్స్"
|
||||
"filter_and_highlights": "ఫిల్టర్లు మరియు హైలైట్స్",
|
||||
"boards_you_moderate": "మీరు మోడరేట్ చేసే బోర్డులు"
|
||||
}
|
||||
@@ -294,5 +294,6 @@
|
||||
"search_results_for": "ผลการค้นหาสำหรับ",
|
||||
"help": "ช่วยเหลือ",
|
||||
"filter_and_highlights_help": "ตัวกรองและการเน้นความช่วยเหลือ",
|
||||
"filter_and_highlights": "ตัวกรองและไฮไลท์"
|
||||
"filter_and_highlights": "ตัวกรองและไฮไลท์",
|
||||
"boards_you_moderate": "บอร์ดที่คุณดูแล"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -294,5 +294,6 @@
|
||||
"search_results_for": "Результати пошуку для",
|
||||
"help": "Допомога",
|
||||
"filter_and_highlights_help": "Допомога з фільтрами та підсвічуваннями",
|
||||
"filter_and_highlights": "Фільтри та виділення"
|
||||
"filter_and_highlights": "Фільтри та виділення",
|
||||
"boards_you_moderate": "boards, які ви модеруватимете"
|
||||
}
|
||||
@@ -294,5 +294,6 @@
|
||||
"search_results_for": "تلاش کے نتائج",
|
||||
"help": "مدد",
|
||||
"filter_and_highlights_help": "فلٹرز اور ہائی لائٹس کی مدد",
|
||||
"filter_and_highlights": "فلٹرز اور ہائلाइटس"
|
||||
"filter_and_highlights": "فلٹرز اور ہائلाइटس",
|
||||
"boards_you_moderate": "وہ بورڈز جن کی آپ نگرانی کرتے ہیں"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -294,5 +294,6 @@
|
||||
"search_results_for": "搜索结果",
|
||||
"help": "帮助",
|
||||
"filter_and_highlights_help": "过滤器和高亮帮助",
|
||||
"filter_and_highlights": "筛选器与高亮"
|
||||
"filter_and_highlights": "筛选器与高亮",
|
||||
"boards_you_moderate": "您管理的板块"
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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} />;
|
||||
};
|
||||
|
||||
@@ -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',
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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')}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
};
|
||||
|
||||
Vendored
-2
@@ -3,8 +3,6 @@ declare module '*.module.css' {
|
||||
export default classes;
|
||||
}
|
||||
|
||||
declare module 'ext-name';
|
||||
|
||||
declare module 'lodash';
|
||||
|
||||
declare module 'react-draggable';
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user