mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
@@ -0,0 +1,8 @@
|
||||
module.exports = {
|
||||
extends: ['react-app'],
|
||||
rules: {
|
||||
'no-unused-vars': 'off',
|
||||
'@typescript-eslint/no-unused-vars': 'warn'
|
||||
},
|
||||
ignorePatterns: ['android/**', 'build/**', 'node_modules/**', 'public/**', 'electron/**']
|
||||
};
|
||||
Generated
+6
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="CompilerConfiguration">
|
||||
<bytecodeTargetLevel target="21" />
|
||||
</component>
|
||||
</project>
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="deploymentTargetSelector">
|
||||
<selectionStates>
|
||||
<SelectionState runConfigName="app">
|
||||
<option name="selectionMode" value="DROPDOWN" />
|
||||
</SelectionState>
|
||||
</selectionStates>
|
||||
</component>
|
||||
</project>
|
||||
Generated
+4
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK" />
|
||||
</project>
|
||||
Generated
+13
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="RunConfigurationProducerService">
|
||||
<option name="ignoredProducers">
|
||||
<set>
|
||||
<option value="com.intellij.execution.junit.AbstractAllInDirectoryConfigurationProducer" />
|
||||
<option value="com.intellij.execution.junit.AllInPackageConfigurationProducer" />
|
||||
<option value="com.intellij.execution.junit.TestInClassConfigurationProducer" />
|
||||
<option value="com.intellij.execution.junit.UniqueIdConfigurationProducer" />
|
||||
</set>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
BIN
Binary file not shown.
+3
-1
@@ -1,5 +1,7 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
||||
Vendored
+182
-115
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env sh
|
||||
#!/bin/sh
|
||||
|
||||
#
|
||||
# Copyright 2015 the original author or authors.
|
||||
# Copyright © 2015-2021 the original authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -15,69 +15,104 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
##
|
||||
## Gradle start up script for UN*X
|
||||
##
|
||||
#
|
||||
# Gradle start up script for POSIX generated by Gradle.
|
||||
#
|
||||
# Important for running:
|
||||
#
|
||||
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
||||
# noncompliant, but you have some other compliant shell such as ksh or
|
||||
# bash, then to run this script, type that shell name before the whole
|
||||
# command line, like:
|
||||
#
|
||||
# ksh Gradle
|
||||
#
|
||||
# Busybox and similar reduced shells will NOT work, because this script
|
||||
# requires all of these POSIX shell features:
|
||||
# * functions;
|
||||
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
||||
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
||||
# * compound commands having a testable exit status, especially «case»;
|
||||
# * various built-in commands including «command», «set», and «ulimit».
|
||||
#
|
||||
# Important for patching:
|
||||
#
|
||||
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
||||
# by Bash, Ksh, etc; in particular arrays are avoided.
|
||||
#
|
||||
# The "traditional" practice of packing multiple parameters into a
|
||||
# space-separated string is a well documented source of bugs and security
|
||||
# problems, so this is (mostly) avoided, by progressively accumulating
|
||||
# options in "$@", and eventually passing that to Java.
|
||||
#
|
||||
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
||||
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
||||
# see the in-line comments for details.
|
||||
#
|
||||
# There are tweaks for specific operating systems such as AIX, CygWin,
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
|
||||
# Resolve links: $0 may be a link
|
||||
PRG="$0"
|
||||
# Need this for relative symlinks.
|
||||
while [ -h "$PRG" ] ; do
|
||||
ls=`ls -ld "$PRG"`
|
||||
link=`expr "$ls" : '.*-> \(.*\)$'`
|
||||
if expr "$link" : '/.*' > /dev/null; then
|
||||
PRG="$link"
|
||||
else
|
||||
PRG=`dirname "$PRG"`"/$link"
|
||||
fi
|
||||
app_path=$0
|
||||
|
||||
# Need this for daisy-chained symlinks.
|
||||
while
|
||||
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||
[ -h "$app_path" ]
|
||||
do
|
||||
ls=$( ls -ld "$app_path" )
|
||||
link=${ls#*' -> '}
|
||||
case $link in #(
|
||||
/*) app_path=$link ;; #(
|
||||
*) app_path=$APP_HOME$link ;;
|
||||
esac
|
||||
done
|
||||
SAVED="`pwd`"
|
||||
cd "`dirname \"$PRG\"`/" >/dev/null
|
||||
APP_HOME="`pwd -P`"
|
||||
cd "$SAVED" >/dev/null
|
||||
|
||||
APP_NAME="Gradle"
|
||||
APP_BASE_NAME=`basename "$0"`
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
# This is normally unused
|
||||
# shellcheck disable=SC2034
|
||||
APP_BASE_NAME=${0##*/}
|
||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
|
||||
' "$PWD" ) || exit
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD="maximum"
|
||||
MAX_FD=maximum
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
}
|
||||
} >&2
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
}
|
||||
} >&2
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "`uname`" in
|
||||
CYGWIN* )
|
||||
cygwin=true
|
||||
;;
|
||||
Darwin* )
|
||||
darwin=true
|
||||
;;
|
||||
MINGW* )
|
||||
msys=true
|
||||
;;
|
||||
NONSTOP* )
|
||||
nonstop=true
|
||||
;;
|
||||
case "$( uname )" in #(
|
||||
CYGWIN* ) cygwin=true ;; #(
|
||||
Darwin* ) darwin=true ;; #(
|
||||
MSYS* | MINGW* ) msys=true ;; #(
|
||||
NONSTOP* ) nonstop=true ;;
|
||||
esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
@@ -87,9 +122,9 @@ CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||
else
|
||||
JAVACMD="$JAVA_HOME/bin/java"
|
||||
JAVACMD=$JAVA_HOME/bin/java
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
@@ -98,88 +133,120 @@ Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD="java"
|
||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
JAVACMD=java
|
||||
if ! command -v java >/dev/null 2>&1
|
||||
then
|
||||
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
|
||||
MAX_FD_LIMIT=`ulimit -H -n`
|
||||
if [ $? -eq 0 ] ; then
|
||||
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
||||
MAX_FD="$MAX_FD_LIMIT"
|
||||
fi
|
||||
ulimit -n $MAX_FD
|
||||
if [ $? -ne 0 ] ; then
|
||||
warn "Could not set maximum file descriptor limit: $MAX_FD"
|
||||
fi
|
||||
else
|
||||
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
|
||||
fi
|
||||
fi
|
||||
|
||||
# For Darwin, add options to specify how the application appears in the dock
|
||||
if $darwin; then
|
||||
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
||||
fi
|
||||
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
|
||||
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||
|
||||
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||
|
||||
# We build the pattern for arguments to be converted via cygpath
|
||||
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
|
||||
SEP=""
|
||||
for dir in $ROOTDIRSRAW ; do
|
||||
ROOTDIRS="$ROOTDIRS$SEP$dir"
|
||||
SEP="|"
|
||||
done
|
||||
OURCYGPATTERN="(^($ROOTDIRS))"
|
||||
# Add a user-defined pattern to the cygpath arguments
|
||||
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
|
||||
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
|
||||
fi
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
i=0
|
||||
for arg in "$@" ; do
|
||||
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
|
||||
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
|
||||
|
||||
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
|
||||
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
|
||||
else
|
||||
eval `echo args$i`="\"$arg\""
|
||||
fi
|
||||
i=`expr $i + 1`
|
||||
done
|
||||
case $i in
|
||||
0) set -- ;;
|
||||
1) set -- "$args0" ;;
|
||||
2) set -- "$args0" "$args1" ;;
|
||||
3) set -- "$args0" "$args1" "$args2" ;;
|
||||
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
||||
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
||||
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
||||
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
||||
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
||||
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
||||
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
case $MAX_FD in #(
|
||||
max*)
|
||||
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
MAX_FD=$( ulimit -H -n ) ||
|
||||
warn "Could not query maximum file descriptor limit"
|
||||
esac
|
||||
case $MAX_FD in #(
|
||||
'' | soft) :;; #(
|
||||
*)
|
||||
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
ulimit -n "$MAX_FD" ||
|
||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||
esac
|
||||
fi
|
||||
|
||||
# Escape application args
|
||||
save () {
|
||||
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
||||
echo " "
|
||||
}
|
||||
APP_ARGS=`save "$@"`
|
||||
# Collect all arguments for the java command, stacking in reverse order:
|
||||
# * args from the command line
|
||||
# * the main class name
|
||||
# * -classpath
|
||||
# * -D...appname settings
|
||||
# * --module-path (only if needed)
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
||||
|
||||
# Collect all arguments for the java command, following the shell quoting and substitution rules
|
||||
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if "$cygwin" || "$msys" ; then
|
||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
||||
|
||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
for arg do
|
||||
if
|
||||
case $arg in #(
|
||||
-*) false ;; # don't mess with options #(
|
||||
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
||||
[ -e "$t" ] ;; #(
|
||||
*) false ;;
|
||||
esac
|
||||
then
|
||||
arg=$( cygpath --path --ignore --mixed "$arg" )
|
||||
fi
|
||||
# Roll the args list around exactly as many times as the number of
|
||||
# args, so each arg winds up back in the position where it started, but
|
||||
# possibly modified.
|
||||
#
|
||||
# NB: a `for` loop captures its iteration list before it begins, so
|
||||
# changing the positional parameters here affects neither the number of
|
||||
# iterations, nor the values presented in `arg`.
|
||||
shift # remove old arg
|
||||
set -- "$@" "$arg" # push replacement arg
|
||||
done
|
||||
fi
|
||||
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Collect all arguments for the java command:
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||
# and any embedded shellness will be escaped.
|
||||
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||
# treated as '${Hostname}' itself on the command line.
|
||||
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
-classpath "$CLASSPATH" \
|
||||
org.gradle.wrapper.GradleWrapperMain \
|
||||
"$@"
|
||||
|
||||
# Stop when "xargs" is not available.
|
||||
if ! command -v xargs >/dev/null 2>&1
|
||||
then
|
||||
die "xargs is not available"
|
||||
fi
|
||||
|
||||
# Use "xargs" to parse quoted args.
|
||||
#
|
||||
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||
#
|
||||
# In Bash we could simply go:
|
||||
#
|
||||
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
||||
# set -- "${ARGS[@]}" "$@"
|
||||
#
|
||||
# but POSIX shell has neither arrays nor command substitution, so instead we
|
||||
# post-process each arg (as a line of input to sed) to backslash-escape any
|
||||
# character that might be a shell metacharacter, then use eval to reverse
|
||||
# that process (while maintaining the separation between arguments), and wrap
|
||||
# the whole thing up as a single "set" statement.
|
||||
#
|
||||
# This will of course break if any of these variables contains a newline or
|
||||
# an unmatched quote.
|
||||
#
|
||||
|
||||
eval "set -- $(
|
||||
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||
xargs -n1 |
|
||||
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||
tr '\n' ' '
|
||||
)" '"$@"'
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
|
||||
Vendored
+21
-16
@@ -13,8 +13,10 @@
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
@rem SPDX-License-Identifier: Apache-2.0
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%" == "" @echo off
|
||||
@if "%DEBUG%"=="" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@@ -25,7 +27,8 @@
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%" == "" set DIRNAME=.
|
||||
if "%DIRNAME%"=="" set DIRNAME=.
|
||||
@rem This is normally unused
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@@ -40,13 +43,13 @@ if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if "%ERRORLEVEL%" == "0" goto execute
|
||||
if %ERRORLEVEL% equ 0 goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
@@ -56,11 +59,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
@@ -75,13 +78,15 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||
exit /b 1
|
||||
set EXIT_CODE=%ERRORLEVEL%
|
||||
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||
exit /b %EXIT_CODE%
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import path from 'path';
|
||||
|
||||
export default defineConfig({
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
rollupOptions: {
|
||||
input: {
|
||||
main: path.resolve(__dirname, 'main.js'),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="referrer" content="no-referrer" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta name="description" content="serverless, adminless, decentralized 4chan alternative" />
|
||||
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
|
||||
<title>plebchan</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/index.tsx"></script>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
|
||||
You can add webfonts, meta tags, or analytics to this file.
|
||||
The build step will place the bundled scripts into the <body> tag.
|
||||
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
</body>
|
||||
</html>
|
||||
+31
-17
@@ -22,6 +22,8 @@
|
||||
"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",
|
||||
"i18next-browser-languagedetector": "7.1.0",
|
||||
"i18next-http-backend": "2.2.2",
|
||||
@@ -29,14 +31,12 @@
|
||||
"lodash": "4.17.21",
|
||||
"memoizee": "0.4.15",
|
||||
"prettier": "3.0.3",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-draggable": "4.4.6",
|
||||
"react": "19.0.0",
|
||||
"react-dom": "19.0.0",
|
||||
"react-i18next": "13.2.2",
|
||||
"react-markdown": "8.0.6",
|
||||
"react-router-dom": "6.16.0",
|
||||
"react-router-hash-link": "2.4.3",
|
||||
"react-scripts": "5.0.1",
|
||||
"react-virtuoso": "4.12.3",
|
||||
"rehype-sanitize": "5.0.1",
|
||||
"remark-gfm": "3.0.1",
|
||||
@@ -46,12 +46,12 @@
|
||||
"zustand": "4.4.3"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "cross-env PUBLIC_URL=./ GENERATE_SOURCEMAP=false react-scripts build",
|
||||
"build-netlify": "cross-env PUBLIC_URL=./ GENERATE_SOURCEMAP=true REACT_APP_COMMIT_REF=$COMMIT_REF CI='' react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"analyze-bundle": "cross-env PUBLIC_URL=./ GENERATE_SOURCEMAP=true react-scripts build && source-map-explorer 'build/static/js/*.js'",
|
||||
"eject": "react-scripts eject",
|
||||
"start": "vite",
|
||||
"build": "cross-env PUBLIC_URL=./ GENERATE_SOURCEMAP=false vite build",
|
||||
"build-netlify": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" PUBLIC_URL=./ GENERATE_SOURCEMAP=true VITE_COMMIT_REF=$COMMIT_REF CI='' vite build",
|
||||
"test": "vitest",
|
||||
"preview": "vite preview",
|
||||
"analyze-bundle": "cross-env PUBLIC_URL=./ GENERATE_SOURCEMAP=true vite build && source-map-explorer 'build/assets/*.js'",
|
||||
"electron": "yarn electron:before && electron .",
|
||||
"electron:no-delete-data": "yarn electron:before:download-ipfs && electron .",
|
||||
"electron:start": "concurrently \"cross-env BROWSER=none yarn start\" \"wait-on http://localhost:3000 && yarn electron\"",
|
||||
@@ -64,13 +64,8 @@
|
||||
"electron:before:delete-data": "rimraf .plebbit",
|
||||
"android:build:icons": "cordova-res android --skip-config --copy --resources /tmp/plebbit-react-android-icons --icon-source ./android/icons/icon.png --splash-source ./android/icons/splash.png --icon-foreground-source ./android/icons/icon-foreground.png --icon-background-source '#ffffee'",
|
||||
"prettier": "prettier {src,electron}/**/*.{js,ts,tsx} --write",
|
||||
"changelog": "conventional-changelog -p angular -i CHANGELOG.md -s -r 0"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
]
|
||||
"changelog": "conventional-changelog -p angular -i CHANGELOG.md -s -r 0",
|
||||
"android:build": "yarn build && npx cap sync android && npx cap open android"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
@@ -91,16 +86,35 @@
|
||||
"@capacitor/cli": "5.0.0",
|
||||
"@capacitor/core": "5.0.0",
|
||||
"@types/memoizee": "0.4.9",
|
||||
"@typescript-eslint/eslint-plugin": "8.26.0",
|
||||
"@typescript-eslint/parser": "8.26.0",
|
||||
"@vitejs/plugin-react": "4.3.4",
|
||||
"assert": "2.1.0",
|
||||
"babel-plugin-react-compiler": "19.0.0-beta-40c6c23-20250301",
|
||||
"buffer": "6.0.3",
|
||||
"concurrently": "8.0.1",
|
||||
"conventional-changelog-cli": "4.1.0",
|
||||
"cordova-res": "0.15.4",
|
||||
"cross-env": "7.0.3",
|
||||
"crypto-browserify": "3.12.1",
|
||||
"cz-conventional-changelog": "3.3.0",
|
||||
"decompress": "4.2.1",
|
||||
"electron": "28.2.3",
|
||||
"electron-builder": "24.13.2",
|
||||
"electron-rebuild": "3.2.9",
|
||||
"eslint": "8.56.0",
|
||||
"eslint-config-react-app": "7.0.1",
|
||||
"eslint-plugin-react": "7.37.4",
|
||||
"eslint-plugin-react-compiler": "19.0.0-beta-40c6c23-20250301",
|
||||
"eslint-plugin-react-hooks": "5.2.0",
|
||||
"husky": "4.3.8",
|
||||
"isomorphic-fetch": "3.0.0",
|
||||
"lint-staged": "12.3.8",
|
||||
"stream-browserify": "3.0.0",
|
||||
"vite": "6.2.0",
|
||||
"vite-plugin-eslint": "1.8.1",
|
||||
"vite-plugin-node-polyfills": "0.23.0",
|
||||
"vite-plugin-pwa": "0.21.1",
|
||||
"wait-on": "7.0.1"
|
||||
},
|
||||
"main": "electron/main.js",
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="referrer" content="no-referrer" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta name="description" content="serverless, adminless, decentralized 4chan alternative" />
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/apple-touch-icon.png" />
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
|
||||
<!-- Preload assets -->
|
||||
<link rel="preload" href="%PUBLIC_URL%/assets/background-fade-blue.png" as="image" type="image/png" />
|
||||
<link rel="preload" href="%PUBLIC_URL%/assets/background-fade.png" as="image" type="image/png" />
|
||||
<link rel="preload" href="%PUBLIC_URL%/assets/buttons/button-fade-blue.png" as="image" type="image/png" />
|
||||
<link rel="preload" href="%PUBLIC_URL%/assets/buttons/button-fade-dark.png" as="image" type="image/png" />
|
||||
<link rel="preload" href="%PUBLIC_URL%/assets/buttons/button-fade.png" as="image" type="image/png" />
|
||||
<link rel="preload" href="%PUBLIC_URL%/assets/buttons/cross-blue.png" as="image" type="image/png" />
|
||||
<link rel="preload" href="%PUBLIC_URL%/assets/buttons/cross-dark.png" as="image" type="image/png" />
|
||||
<link rel="preload" href="%PUBLIC_URL%/assets/buttons/cross-photon.png" as="image" type="image/png" />
|
||||
<link rel="preload" href="%PUBLIC_URL%/assets/buttons/cross-red.png" as="image" type="image/png" />
|
||||
<link rel="preload" href="%PUBLIC_URL%/assets/buttons/minus-blue.png" as="image" type="image/png" />
|
||||
<link rel="preload" href="%PUBLIC_URL%/assets/buttons/minus-dark.png" as="image" type="image/png" />
|
||||
<link rel="preload" href="%PUBLIC_URL%/assets/buttons/minus-photon.png" as="image" type="image/png" />
|
||||
<link rel="preload" href="%PUBLIC_URL%/assets/buttons/minus-red.png" as="image" type="image/png" />
|
||||
<link rel="preload" href="%PUBLIC_URL%/assets/buttons/plus-blue.png" as="image" type="image/png" />
|
||||
<link rel="preload" href="%PUBLIC_URL%/assets/buttons/plus-dark.png" as="image" type="image/png" />
|
||||
<link rel="preload" href="%PUBLIC_URL%/assets/buttons/plus-photon.png" as="image" type="image/png" />
|
||||
<link rel="preload" href="%PUBLIC_URL%/assets/buttons/plus-red.png" as="image" type="image/png" />
|
||||
<link rel="preload" href="%PUBLIC_URL%/assets/buttons/question-blue.png" as="image" type="image/png" />
|
||||
<link rel="preload" href="%PUBLIC_URL%/assets/buttons/question-dark.png" as="image" type="image/png" />
|
||||
<link rel="preload" href="%PUBLIC_URL%/assets/buttons/question-photon.png" as="image" type="image/png" />
|
||||
<link rel="preload" href="%PUBLIC_URL%/assets/buttons/question-red.png" as="image" type="image/png" />
|
||||
<link rel="preload" href="%PUBLIC_URL%/assets/filedeleted-res.gif" as="image" type="image/gif" />
|
||||
<link rel="preload" href="%PUBLIC_URL%/assets/icons/closed.gif" as="image" type="image/gif" />
|
||||
<link rel="preload" href="%PUBLIC_URL%/assets/icons/offline-blue.png" as="image" type="image/png" />
|
||||
<link rel="preload" href="%PUBLIC_URL%/assets/icons/offline-red.png" as="image" type="image/png" />
|
||||
<link rel="preload" href="%PUBLIC_URL%/assets/icons/sticky.gif" as="image" type="image/gif" />
|
||||
<link rel="preload" href="%PUBLIC_URL%/assets/logo/logo-transparent.png" as="image" type="image/png" />
|
||||
<link rel="preload" href="%PUBLIC_URL%/assets/spoiler.png" as="image" type="image/png" />
|
||||
|
||||
<title>plebchan</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
|
||||
You can add webfonts, meta tags, or analytics to this file.
|
||||
The build step will place the bundled scripts into the <body> tag.
|
||||
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
</body>
|
||||
</html>
|
||||
@@ -290,5 +290,6 @@
|
||||
"this_thread_was_deleted": "تم حذف هذا الموضوع بواسطة مؤلفه",
|
||||
"this_thread_was_removed": "تم إزالة هذا الموضوع بواسطة مشرفي المنتدى",
|
||||
"this_reply_was_deleted": "تم حذف هذه الرد من قبل مؤلفه",
|
||||
"this_reply_was_removed": "تمت إزالة هذه الرد من قبل مشرفي المنتدى"
|
||||
"this_reply_was_removed": "تمت إزالة هذه الرد من قبل مشرفي المنتدى",
|
||||
"not_mod_of_any_board": "أنت لست مشرفًا في أي لوحة."
|
||||
}
|
||||
@@ -290,5 +290,6 @@
|
||||
"this_thread_was_deleted": "এই থ্রেডটি এর লেখক দ্বারা মুছে ফেলা হয়েছে",
|
||||
"this_thread_was_removed": "এই থ্রেডটি বোর্ড moderators দ্বারা মুছে ফেলা হয়েছে",
|
||||
"this_reply_was_deleted": "এই প্রতিক্রিয়া তার লেখক দ্বারা মুছে ফেলা হয়েছে",
|
||||
"this_reply_was_removed": "এই প্রতিক্রিয়া বোর্ড পরিচালকদের দ্বারা সরানো হয়েছে"
|
||||
"this_reply_was_removed": "এই প্রতিক্রিয়া বোর্ড পরিচালকদের দ্বারা সরানো হয়েছে",
|
||||
"not_mod_of_any_board": "আপনি কোনও বোর্ডের মড নন।"
|
||||
}
|
||||
@@ -290,5 +290,6 @@
|
||||
"this_thread_was_deleted": "Toto vlákno bylo smazáno jeho autorem",
|
||||
"this_thread_was_removed": "Toto vlákno bylo odstraněno moderátory fóra",
|
||||
"this_reply_was_deleted": "Tato odpověď byla smazána autorem",
|
||||
"this_reply_was_removed": "Tato odpověď byla odstraněna moderátory fóra"
|
||||
"this_reply_was_removed": "Tato odpověď byla odstraněna moderátory fóra",
|
||||
"not_mod_of_any_board": "Nejste moderátor žádné desky."
|
||||
}
|
||||
@@ -290,5 +290,6 @@
|
||||
"this_thread_was_deleted": "Denne tråd blev slettet af dens forfatter",
|
||||
"this_thread_was_removed": "Denne tråd blev fjernet af boardmoderatorer",
|
||||
"this_reply_was_deleted": "Dette svar blev slettet af forfatteren",
|
||||
"this_reply_was_removed": "Dette svar blev fjernet af forummoderatorer"
|
||||
"this_reply_was_removed": "Dette svar blev fjernet af forummoderatorer",
|
||||
"not_mod_of_any_board": "Du er ikke moderator for nogen tavler."
|
||||
}
|
||||
@@ -290,5 +290,6 @@
|
||||
"this_thread_was_deleted": "Dieser Thread wurde von seinem Autor gelöscht",
|
||||
"this_thread_was_removed": "Dieser Thread wurde von den Moderatoren des Boards entfernt",
|
||||
"this_reply_was_deleted": "Diese Antwort wurde vom Autor gelöscht",
|
||||
"this_reply_was_removed": "Diese Antwort wurde von den Forenmoderatoren entfernt"
|
||||
"this_reply_was_removed": "Diese Antwort wurde von den Forenmoderatoren entfernt",
|
||||
"not_mod_of_any_board": "Du bist kein Moderator eines Boards."
|
||||
}
|
||||
@@ -290,5 +290,6 @@
|
||||
"this_thread_was_deleted": "Αυτή η συζήτηση διαγράφηκε από τον συγγραφέα της",
|
||||
"this_thread_was_removed": "Αυτή η συζήτηση αφαιρέθηκε από τους διαχειριστές του φόρουμ",
|
||||
"this_reply_was_deleted": "Αυτή η απάντηση διαγράφηκε από τον συγγραφέα της",
|
||||
"this_reply_was_removed": "Η απάντηση αυτή αφαιρέθηκε από τους διαχειριστές του φόρουμ"
|
||||
"this_reply_was_removed": "Η απάντηση αυτή αφαιρέθηκε από τους διαχειριστές του φόρουμ",
|
||||
"not_mod_of_any_board": "Δεν είστε διαχειριστής σε καμία κοινότητα."
|
||||
}
|
||||
@@ -290,5 +290,6 @@
|
||||
"this_thread_was_deleted": "This thread was deleted by its author",
|
||||
"this_thread_was_removed": "This thread was removed by board moderators",
|
||||
"this_reply_was_deleted": "This reply was deleted by its author",
|
||||
"this_reply_was_removed": "This reply was removed by board moderators"
|
||||
"this_reply_was_removed": "This reply was removed by board moderators",
|
||||
"not_mod_of_any_board": "You are not a mod of any board."
|
||||
}
|
||||
@@ -290,5 +290,6 @@
|
||||
"this_thread_was_deleted": "Este hilo fue eliminado por su autor",
|
||||
"this_thread_was_removed": "Este hilo fue eliminado por los moderadores del foro",
|
||||
"this_reply_was_deleted": "Esta respuesta fue eliminada por su autor",
|
||||
"this_reply_was_removed": "Esta respuesta fue eliminada por los moderadores del foro"
|
||||
"this_reply_was_removed": "Esta respuesta fue eliminada por los moderadores del foro",
|
||||
"not_mod_of_any_board": "No eres moderador de ningún tablero."
|
||||
}
|
||||
@@ -290,5 +290,6 @@
|
||||
"this_thread_was_deleted": "این موضوع توسط نویسندهاش حذف شد",
|
||||
"this_thread_was_removed": "این تاپیک توسط مدیران انجمن حذف شد",
|
||||
"this_reply_was_deleted": "این پاسخ توسط نویسندهاش حذف شد",
|
||||
"this_reply_was_removed": "این پاسخ توسط مدیران انجمن حذف شد"
|
||||
"this_reply_was_removed": "این پاسخ توسط مدیران انجمن حذف شد",
|
||||
"not_mod_of_any_board": "شما هیچ تابلو مدیر نیستید."
|
||||
}
|
||||
@@ -290,5 +290,6 @@
|
||||
"this_thread_was_deleted": "Tämä ketju poistettiin sen kirjoittajan toimesta",
|
||||
"this_thread_was_removed": "Tämä ketju poistettiin foorumin moderaattoreiden toimesta",
|
||||
"this_reply_was_deleted": "Tämä vastaus poistettiin sen kirjoittajalta",
|
||||
"this_reply_was_removed": "Tämä vastaus poistettiin foorumin moderaattoreiden toimesta"
|
||||
"this_reply_was_removed": "Tämä vastaus poistettiin foorumin moderaattoreiden toimesta",
|
||||
"not_mod_of_any_board": "Et ole moderaattori missään taulussa."
|
||||
}
|
||||
@@ -290,5 +290,6 @@
|
||||
"this_thread_was_deleted": "Ang thread na ito ay tinanggal ng may-akda nito",
|
||||
"this_thread_was_removed": "Inalis ang thread na ito ng mga moderator ng board",
|
||||
"this_reply_was_deleted": "Ang tugon na ito ay tinanggal ng may-akda nito",
|
||||
"this_reply_was_removed": "Ang sagot na ito ay tinanggal ng mga moderator ng board"
|
||||
"this_reply_was_removed": "Ang sagot na ito ay tinanggal ng mga moderator ng board",
|
||||
"not_mod_of_any_board": "Hindi ka mod ng anumang board."
|
||||
}
|
||||
@@ -290,5 +290,6 @@
|
||||
"this_thread_was_deleted": "Ce sujet a été supprimé par son auteur",
|
||||
"this_thread_was_removed": "Ce sujet a été supprimé par les modérateurs du forum",
|
||||
"this_reply_was_deleted": "Cette réponse a été supprimée par son auteur",
|
||||
"this_reply_was_removed": "Cette réponse a été supprimée par les modérateurs du forum"
|
||||
"this_reply_was_removed": "Cette réponse a été supprimée par les modérateurs du forum",
|
||||
"not_mod_of_any_board": "Vous n'êtes pas modérateur de n'aucun forum."
|
||||
}
|
||||
@@ -290,5 +290,6 @@
|
||||
"this_thread_was_deleted": "השרשור הזה נמחק על ידי המחבר שלו",
|
||||
"this_thread_was_removed": "השרשור הזה הוסר על ידי מנהלי הפורום",
|
||||
"this_reply_was_deleted": "תשובה זו נמחקה על ידי המחבר שלה",
|
||||
"this_reply_was_removed": "תשובה זו הוסרה על ידי מנהלי הפורום"
|
||||
"this_reply_was_removed": "תשובה זו הוסרה על ידי מנהלי הפורום",
|
||||
"not_mod_of_any_board": "אתה לא מוד בכל לוח."
|
||||
}
|
||||
@@ -290,5 +290,6 @@
|
||||
"this_thread_was_deleted": "इस थ्रेड को इसके लेखक ने हटा दिया है",
|
||||
"this_thread_was_removed": "यह थ्रेड बोर्ड मॉडरेटर्स द्वारा हटा दिया गया था",
|
||||
"this_reply_was_deleted": "इस उत्तर को इसके लेखक द्वारा हटा दिया गया था",
|
||||
"this_reply_was_removed": "यह उत्तर बोर्ड मॉडरेटर द्वारा हटा दिया गया था"
|
||||
"this_reply_was_removed": "यह उत्तर बोर्ड मॉडरेटर द्वारा हटा दिया गया था",
|
||||
"not_mod_of_any_board": "आप किसी बोर्ड के मॉड नहीं हैं।"
|
||||
}
|
||||
@@ -290,5 +290,6 @@
|
||||
"this_thread_was_deleted": "Ez a szál törlésre került a szerzője által",
|
||||
"this_thread_was_removed": "Ezt a témát a fórum moderátorai eltávolították",
|
||||
"this_reply_was_deleted": "Ez a válasz törölve lett a szerzője által",
|
||||
"this_reply_was_removed": "Ez a válasz törlésre került a fórum moderátorai által"
|
||||
"this_reply_was_removed": "Ez a válasz törlésre került a fórum moderátorai által",
|
||||
"not_mod_of_any_board": "Nem vagy moderátor egyetlen táblán sem."
|
||||
}
|
||||
@@ -290,5 +290,6 @@
|
||||
"this_thread_was_deleted": "Thread ini dihapus oleh penulisnya",
|
||||
"this_thread_was_removed": "Thread ini dihapus oleh moderator papan",
|
||||
"this_reply_was_deleted": "Balasan ini dihapus oleh penulisnya",
|
||||
"this_reply_was_removed": "Balasan ini dihapus oleh moderator papan"
|
||||
"this_reply_was_removed": "Balasan ini dihapus oleh moderator papan",
|
||||
"not_mod_of_any_board": "Anda bukan mod di papan manapun."
|
||||
}
|
||||
@@ -290,5 +290,6 @@
|
||||
"this_thread_was_deleted": "Questo thread è stato eliminato dal suo autore",
|
||||
"this_thread_was_removed": "Questo thread è stato rimosso dai moderatori del board",
|
||||
"this_reply_was_deleted": "Questa risposta è stata eliminata dal suo autore",
|
||||
"this_reply_was_removed": "Questa risposta è stata rimossa dai moderatori del forum"
|
||||
"this_reply_was_removed": "Questa risposta è stata rimossa dai moderatori del forum",
|
||||
"not_mod_of_any_board": "Non sei un mod di nessuna board."
|
||||
}
|
||||
@@ -290,5 +290,6 @@
|
||||
"this_thread_was_deleted": "このスレッドは著者によって削除されました",
|
||||
"this_thread_was_removed": "このスレッドはボードのモデレーターによって削除されました",
|
||||
"this_reply_was_deleted": "この返信はその著者によって削除されました",
|
||||
"this_reply_was_removed": "この返信は掲示板のモデレーターによって削除されました"
|
||||
"this_reply_was_removed": "この返信は掲示板のモデレーターによって削除されました",
|
||||
"not_mod_of_any_board": "あなたはどのボードのモデレーターでもありません。"
|
||||
}
|
||||
@@ -290,5 +290,6 @@
|
||||
"this_thread_was_deleted": "이 스레드는 작성자에 의해 삭제되었습니다",
|
||||
"this_thread_was_removed": "이 스레드는 보드 관리자에 의해 제거되었습니다",
|
||||
"this_reply_was_deleted": "이 답글은 작성자에 의해 삭제되었습니다",
|
||||
"this_reply_was_removed": "이 답글은 게시판 관리자에 의해 삭제되었습니다"
|
||||
"this_reply_was_removed": "이 답글은 게시판 관리자에 의해 삭제되었습니다",
|
||||
"not_mod_of_any_board": "당신은 어떤 게시판의 관리자도 아닙니다."
|
||||
}
|
||||
@@ -290,5 +290,6 @@
|
||||
"this_thread_was_deleted": "हा धागा त्याच्या लेखकाने हटविला",
|
||||
"this_thread_was_removed": "हे थ्रेड बोर्ड मध्यमाने काढले गेले आहे",
|
||||
"this_reply_was_deleted": "हा प्रतिसाद त्याच्या लेखकाने हटवला आहे",
|
||||
"this_reply_was_removed": "या उत्तराला बोर्ड मॉडरेटरने काढले आहे"
|
||||
"this_reply_was_removed": "या उत्तराला बोर्ड मॉडरेटरने काढले आहे",
|
||||
"not_mod_of_any_board": "तुम्ही कोणत्याही बोर्डचे मॉड नाहीत."
|
||||
}
|
||||
@@ -290,5 +290,6 @@
|
||||
"this_thread_was_deleted": "Dit draadje is verwijderd door de auteur",
|
||||
"this_thread_was_removed": "Deze thread werd verwijderd door boardmoderators",
|
||||
"this_reply_was_deleted": "Dit antwoord werd verwijderd door de auteur",
|
||||
"this_reply_was_removed": "Dit antwoord is verwijderd door de forum moderators"
|
||||
"this_reply_was_removed": "Dit antwoord is verwijderd door de forum moderators",
|
||||
"not_mod_of_any_board": "Je bent geen mod van een bord."
|
||||
}
|
||||
@@ -290,5 +290,6 @@
|
||||
"this_thread_was_deleted": "Denne tråden ble slettet av forfatteren",
|
||||
"this_thread_was_removed": "Denne tråden ble fjernet av boardmoderatorer",
|
||||
"this_reply_was_deleted": "Dette svaret ble slettet av forfatteren",
|
||||
"this_reply_was_removed": "Dette svaret ble fjernet av forummoderatorer"
|
||||
"this_reply_was_removed": "Dette svaret ble fjernet av forummoderatorer",
|
||||
"not_mod_of_any_board": "Du er ikke moderator på noen brett."
|
||||
}
|
||||
@@ -290,5 +290,6 @@
|
||||
"this_thread_was_deleted": "Wątek ten został usunięty przez jego autora",
|
||||
"this_thread_was_removed": "Wątek został usunięty przez moderatorów forum",
|
||||
"this_reply_was_deleted": "Ta odpowiedź została usunięta przez autora",
|
||||
"this_reply_was_removed": "Ta odpowiedź została usunięta przez moderatorów forum"
|
||||
"this_reply_was_removed": "Ta odpowiedź została usunięta przez moderatorów forum",
|
||||
"not_mod_of_any_board": "Nie jesteś moderatorem żadnej tablicy."
|
||||
}
|
||||
@@ -290,5 +290,6 @@
|
||||
"this_thread_was_deleted": "Este tópico foi excluído pelo seu autor",
|
||||
"this_thread_was_removed": "Este tópico foi removido pelos moderadores do fórum",
|
||||
"this_reply_was_deleted": "Esta resposta foi excluída pelo autor",
|
||||
"this_reply_was_removed": "Esta resposta foi removida pelos moderadores do fórum"
|
||||
"this_reply_was_removed": "Esta resposta foi removida pelos moderadores do fórum",
|
||||
"not_mod_of_any_board": "Você não é um moderador de nenhum quadro."
|
||||
}
|
||||
@@ -290,5 +290,6 @@
|
||||
"this_thread_was_deleted": "Acest thread a fost șters de autorul său",
|
||||
"this_thread_was_removed": "Acest thread a fost eliminat de moderatorii board-ului",
|
||||
"this_reply_was_deleted": "Acest răspuns a fost șters de autorul său",
|
||||
"this_reply_was_removed": "Acest răspuns a fost eliminat de moderatorii forumului"
|
||||
"this_reply_was_removed": "Acest răspuns a fost eliminat de moderatorii forumului",
|
||||
"not_mod_of_any_board": "Nu ești moderator pe niciun tablou."
|
||||
}
|
||||
@@ -290,5 +290,6 @@
|
||||
"this_thread_was_deleted": "Эта тема была удалена её автором",
|
||||
"this_thread_was_removed": "Эта тема была удалена модераторами форума",
|
||||
"this_reply_was_deleted": "Этот ответ был удален автором",
|
||||
"this_reply_was_removed": "Этот ответ был удален модераторами форума"
|
||||
"this_reply_was_removed": "Этот ответ был удален модераторами форума",
|
||||
"not_mod_of_any_board": "Вы не модератор ни одной доски."
|
||||
}
|
||||
@@ -290,5 +290,6 @@
|
||||
"this_thread_was_deleted": "Ky thread u fshi nga autori i tij",
|
||||
"this_thread_was_removed": "Ky thread u hoq nga moderatorët e bordit",
|
||||
"this_reply_was_deleted": "Ky përgjigje u fshi nga autori i saj",
|
||||
"this_reply_was_removed": "Ky përgjigje u hoq nga moderatorët e forumit"
|
||||
"this_reply_was_removed": "Ky përgjigje u hoq nga moderatorët e forumit",
|
||||
"not_mod_of_any_board": "Nuk jeni mod në asnjë bord."
|
||||
}
|
||||
@@ -290,5 +290,6 @@
|
||||
"this_thread_was_deleted": "Denna tråd raderades av dess författare",
|
||||
"this_thread_was_removed": "Denna tråd togs bort av forumets moderatorer",
|
||||
"this_reply_was_deleted": "Detta svar togs bort av författaren",
|
||||
"this_reply_was_removed": "Detta svar togs bort av forumets moderatorer"
|
||||
"this_reply_was_removed": "Detta svar togs bort av forumets moderatorer",
|
||||
"not_mod_of_any_board": "Du är inte moderator på någon tavla."
|
||||
}
|
||||
@@ -290,5 +290,6 @@
|
||||
"this_thread_was_deleted": "ఈ థ్రెడ్ను దాని రచయిత తొలగించారు",
|
||||
"this_thread_was_removed": "ఈ థ్రెడ్ బోర్డ్ మోడరేటర్ల ద్వారా తీసివేయబడింది",
|
||||
"this_reply_was_deleted": "ఈ స్పందనను దాని రచయిత తొలగించారు",
|
||||
"this_reply_was_removed": "ఈ స్పందన బోర్డ్ మోడరేటర్లచే తొలగించబడింది"
|
||||
"this_reply_was_removed": "ఈ స్పందన బోర్డ్ మోడరేటర్లచే తొలగించబడింది",
|
||||
"not_mod_of_any_board": "మీరు ఎటువంటి బోర్డుకు మోడ్ కంటే లేరు."
|
||||
}
|
||||
@@ -290,5 +290,6 @@
|
||||
"this_thread_was_deleted": "กระทู้นี้ถูกลบโดยผู้แต่ง",
|
||||
"this_thread_was_removed": "กระทู้นี้ถูกลบโดยผู้ดูแลบอร์ด",
|
||||
"this_reply_was_deleted": "การตอบกลับนี้ถูกลบโดยผู้แต่ง",
|
||||
"this_reply_was_removed": "คำตอบนี้ถูกลบโดยผู้ดูแลบอร์ด"
|
||||
"this_reply_was_removed": "คำตอบนี้ถูกลบโดยผู้ดูแลบอร์ด",
|
||||
"not_mod_of_any_board": "คุณไม่ใช่ผู้ดูแลบอร์ดใดๆ"
|
||||
}
|
||||
@@ -290,5 +290,6 @@
|
||||
"this_thread_was_deleted": "Bu başlık, yazarının tarafından silindi",
|
||||
"this_thread_was_removed": "Bu konu, board moderatörleri tarafından kaldırıldı",
|
||||
"this_reply_was_deleted": "Bu yanıt, yazar tarafından silindi",
|
||||
"this_reply_was_removed": "Bu yanıt, forum moderatörleri tarafından kaldırıldı"
|
||||
"this_reply_was_removed": "Bu yanıt, forum moderatörleri tarafından kaldırıldı",
|
||||
"not_mod_of_any_board": "Hiçbir tahtanın moderatörü değilsiniz."
|
||||
}
|
||||
@@ -290,5 +290,6 @@
|
||||
"this_thread_was_deleted": "Цю тему видалив її автор",
|
||||
"this_thread_was_removed": "Цю тему було видалено модераторами форуму",
|
||||
"this_reply_was_deleted": "Цю відповідь було видалено її автором",
|
||||
"this_reply_was_removed": "Цю відповідь було видалено модераторами форуму"
|
||||
"this_reply_was_removed": "Цю відповідь було видалено модераторами форуму",
|
||||
"not_mod_of_any_board": "Ви не модератор жодної дошки."
|
||||
}
|
||||
@@ -290,5 +290,6 @@
|
||||
"this_thread_was_deleted": "اس تھریڈ کو اس کے مصنف نے حذف کیا",
|
||||
"this_thread_was_removed": "اس تھریڈ کو بورڈ کے موڈریٹرز نے ہٹا دیا ہے",
|
||||
"this_reply_was_deleted": "اس جواب کو اس کے مصنف نے حذف کر دیا تھا",
|
||||
"this_reply_was_removed": "اس جواب کو بورڈ کے منتظمین نے حذف کر دیا ہے"
|
||||
"this_reply_was_removed": "اس جواب کو بورڈ کے منتظمین نے حذف کر دیا ہے",
|
||||
"not_mod_of_any_board": "آپ کسی بورڈ کے موڈ نہیں ہیں۔"
|
||||
}
|
||||
@@ -290,5 +290,6 @@
|
||||
"this_thread_was_deleted": "Chủ đề này đã bị tác giả của nó xóa",
|
||||
"this_thread_was_removed": "Chủ đề này đã bị các quản trị viên diễn đàn xóa",
|
||||
"this_reply_was_deleted": "Câu trả lời này đã bị tác giả của nó xóa",
|
||||
"this_reply_was_removed": "Phản hồi này đã bị xóa bởi các quản trị viên diễn đàn"
|
||||
"this_reply_was_removed": "Phản hồi này đã bị xóa bởi các quản trị viên diễn đàn",
|
||||
"not_mod_of_any_board": "Bạn không phải là quản trị viên của bất kỳ bảng nào."
|
||||
}
|
||||
@@ -290,5 +290,6 @@
|
||||
"this_thread_was_deleted": "该线程已被其作者删除",
|
||||
"this_thread_was_removed": "此帖子已被论坛版主删除",
|
||||
"this_reply_was_deleted": "该回复已被其作者删除",
|
||||
"this_reply_was_removed": "该回复已被版主删除"
|
||||
"this_reply_was_removed": "该回复已被版主删除",
|
||||
"not_mod_of_any_board": "您不是任何板块的管理员。"
|
||||
}
|
||||
+25
-1
@@ -3,6 +3,8 @@ 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 useReplyModalStore from './stores/use-reply-modal-store';
|
||||
import useSpecialThemeStore from './stores/use-special-theme-store';
|
||||
import useIsMobile from './hooks/use-is-mobile';
|
||||
import useTheme from './hooks/use-theme';
|
||||
import styles from './app.module.css';
|
||||
@@ -16,10 +18,11 @@ import Post from './views/post';
|
||||
import { DesktopBoardButtons, MobileBoardButtons } from './components/board-buttons';
|
||||
import BoardHeader from './components/board-header';
|
||||
import ChallengeModal from './components/challenge-modal';
|
||||
import ReplyModal from './components/reply-modal';
|
||||
import PostForm from './components/post-form';
|
||||
import SubplebbitStats from './components/subplebbit-stats';
|
||||
import TopBar from './components/topbar';
|
||||
import useSpecialThemeStore from './stores/use-special-theme-store';
|
||||
import SettingsModal from './components/settings-modal';
|
||||
|
||||
const BoardLayout = () => {
|
||||
const { accountCommentIndex, subplebbitAddress } = useParams();
|
||||
@@ -87,9 +90,25 @@ const GlobalLayout = () => {
|
||||
}
|
||||
}, [theme]);
|
||||
|
||||
const { activeCid, threadCid, subplebbitAddress, closeModal, showReplyModal, scrollY } = useReplyModalStore();
|
||||
|
||||
const location = useLocation();
|
||||
const isInSettingsView = location.pathname.endsWith('/settings');
|
||||
|
||||
return (
|
||||
<>
|
||||
<ChallengeModal />
|
||||
{activeCid && threadCid && subplebbitAddress && (
|
||||
<ReplyModal
|
||||
closeModal={closeModal}
|
||||
parentCid={activeCid}
|
||||
postCid={threadCid}
|
||||
scrollY={scrollY}
|
||||
showReplyModal={showReplyModal}
|
||||
subplebbitAddress={subplebbitAddress}
|
||||
/>
|
||||
)}
|
||||
{isInSettingsView && <SettingsModal />}
|
||||
<Outlet />
|
||||
</>
|
||||
);
|
||||
@@ -128,6 +147,11 @@ const App = () => {
|
||||
<Route path='/p/subscriptions/catalog/:timeFilterName?' element={<Catalog />} />
|
||||
<Route path='/p/subscriptions/catalog/:timeFilterName?/settings' element={<Catalog />} />
|
||||
|
||||
<Route path='/p/mod/:timeFilterName?' element={<Board />} />
|
||||
<Route path='/p/mod/:timeFilterName?/settings' element={<Board />} />
|
||||
<Route path='/p/mod/catalog/:timeFilterName?' element={<Catalog />} />
|
||||
<Route path='/p/mod/catalog/:timeFilterName?/settings' element={<Catalog />} />
|
||||
|
||||
<Route path='/profile/:accountCommentIndex' element={<PendingPost />} />
|
||||
<Route path='/profile/:accountCommentIndex/settings' element={<PendingPost />} />
|
||||
</Route>
|
||||
|
||||
@@ -92,7 +92,7 @@ const UpdateButton = () => {
|
||||
const { t } = useTranslation();
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
const handleUpdate = () => {
|
||||
const handleManualUpdate = () => {
|
||||
window.alert('Manual updates are not available yet. Posts update automatically every ~2 minutes.');
|
||||
};
|
||||
|
||||
@@ -100,11 +100,11 @@ const UpdateButton = () => {
|
||||
<>
|
||||
{/* TODO: Implement update button once available in API */}
|
||||
{isMobile ? (
|
||||
<button className={`button ${styles.disabledButton}`} onClick={handleUpdate}>
|
||||
<button className={`button ${styles.disabledButton}`} onClick={handleManualUpdate}>
|
||||
{t('update')}
|
||||
</button>
|
||||
) : (
|
||||
<button className={`button ${styles.disabledButton}`} onClick={handleUpdate}>
|
||||
<button className={`button ${styles.disabledButton}`} onClick={handleManualUpdate}>
|
||||
{t('update')}
|
||||
</button>
|
||||
)}
|
||||
@@ -116,17 +116,21 @@ const AutoButton = () => {
|
||||
const { t } = useTranslation();
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
const handleManualUpdate = () => {
|
||||
window.alert('Manual updates are not available yet. Posts update automatically every ~2 minutes.');
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{isMobile ? (
|
||||
<button className='button'>
|
||||
<button className='button' onClick={handleManualUpdate}>
|
||||
<label>
|
||||
<input type='checkbox' className={styles.autoCheckbox} checked disabled />
|
||||
{t('Auto')}
|
||||
</label>
|
||||
</button>
|
||||
) : (
|
||||
<label>
|
||||
<label onClick={handleManualUpdate}>
|
||||
{' '}
|
||||
<input type='checkbox' className={styles.autoCheckbox} checked disabled /> {t('Auto')}
|
||||
</label>
|
||||
|
||||
@@ -69,7 +69,7 @@
|
||||
border-image-width: 40px 0px 0px 0px;
|
||||
border-image-outset: 0px 0px 0px 0px;
|
||||
border-image-repeat: repeat repeat;
|
||||
border-image-source: url('/public/assets/garland.png');
|
||||
border-image-source: url('/assets/garland.png');
|
||||
border-style: solid;
|
||||
padding-top: 50px;
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState } from 'react';
|
||||
import { useLocation, useParams } from 'react-router-dom';
|
||||
import { useAccountComment, useSubplebbit } from '@plebbit/plebbit-react-hooks';
|
||||
import { isAllView, isSubscriptionsView } from '../../lib/utils/view-utils';
|
||||
import { isAllView, isSubscriptionsView, isModView } from '../../lib/utils/view-utils';
|
||||
import styles from './board-header.module.css';
|
||||
import { useMultisubMetadata } from '../../hooks/use-default-subplebbits';
|
||||
import useIsMobile from '../../hooks/use-is-mobile';
|
||||
@@ -25,7 +25,7 @@ const BoardHeader = () => {
|
||||
const params = useParams();
|
||||
const isInAllView = isAllView(location.pathname);
|
||||
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;
|
||||
|
||||
@@ -33,8 +33,8 @@ const BoardHeader = () => {
|
||||
const { address, shortAddress } = subplebbit || {};
|
||||
|
||||
const multisubMetadata = useMultisubMetadata();
|
||||
const title = isInAllView ? multisubMetadata?.title || 'all' : isInSubscriptionsView ? 'Subscriptions' : subplebbit?.title;
|
||||
const subtitle = isInAllView ? 'p/all' : isInSubscriptionsView ? 'p/subscriptions' : `p/${address}`;
|
||||
const title = isInAllView ? multisubMetadata?.title || 'all' : isInSubscriptionsView ? 'Subscriptions' : isInModView ? 'Mod' : subplebbit?.title;
|
||||
const subtitle = isInAllView ? 'p/all' : isInSubscriptionsView ? 'p/subscriptions' : isInModView ? 'p/mod' : `p/${address}`;
|
||||
|
||||
const { isOffline, isOnlineStatusLoading, offlineIconClass, offlineTitle } = useIsSubplebbitOffline(subplebbit);
|
||||
|
||||
@@ -47,7 +47,7 @@ const BoardHeader = () => {
|
||||
)}
|
||||
<div className={styles.boardTitle}>
|
||||
{title || (shortAddress ? (shortAddress.endsWith('.eth') || shortAddress.endsWith('.sol') ? shortAddress.slice(0, -4) : shortAddress) : subplebbitAddress)}
|
||||
{(isOffline || isOnlineStatusLoading) && !isInAllView && !isInSubscriptionsView && (
|
||||
{(isOffline || isOnlineStatusLoading) && !isInAllView && !isInSubscriptionsView && !isInModView && (
|
||||
<span className={styles.offlineIconWrapper}>
|
||||
<Tooltip content={offlineTitle}>
|
||||
<span className={`${styles.offlineIcon} ${offlineIconClass}`} />
|
||||
|
||||
@@ -58,11 +58,11 @@
|
||||
}
|
||||
|
||||
.threadIcons .stickyIcon {
|
||||
background-image: url("/public/assets/icons/sticky.gif");
|
||||
background-image: url("/assets/icons/sticky.gif");
|
||||
}
|
||||
|
||||
.threadIcons .closedIcon {
|
||||
background-image: url("/public/assets/icons/closed.gif");
|
||||
background-image: url("/assets/icons/closed.gif");
|
||||
}
|
||||
|
||||
.mediaWrapper {
|
||||
@@ -72,6 +72,7 @@
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
clip-path: inset(-5px); /* direct media could change its dimensions after being published, so we need to clip the overflow. We can't use overflow: hidden because it will hide the box-shadow */
|
||||
}
|
||||
|
||||
.post img, .post video, .post audio {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useEffect, useRef, useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Link, useLocation, useParams } from 'react-router-dom';
|
||||
import { Comment, useComment } from '@plebbit/plebbit-react-hooks';
|
||||
import { Comment } from '@plebbit/plebbit-react-hooks';
|
||||
import { useFloating, offset, size, autoUpdate, Placement } from '@floating-ui/react';
|
||||
import { getHasThumbnail } from '../../lib/utils/media-utils';
|
||||
import { getFormattedTimeAgo } from '../../lib/utils/time-utils';
|
||||
@@ -20,6 +20,7 @@ import _ from 'lodash';
|
||||
import { ContentPreview } from '../../views/home/popular-threads-box';
|
||||
import { useCommentMediaInfo } from '../../hooks/use-comment-media-info';
|
||||
import { shouldShowSnow } from '../../lib/snow';
|
||||
import useReplies from '../../hooks/use-replies';
|
||||
|
||||
interface CatalogPostMediaProps {
|
||||
commentMediaInfo: any;
|
||||
@@ -104,7 +105,6 @@ const CatalogPost = ({ post }: { post: Comment }) => {
|
||||
content,
|
||||
isDescription,
|
||||
isRules,
|
||||
lastChildCid,
|
||||
link,
|
||||
linkHeight,
|
||||
linkWidth,
|
||||
@@ -115,10 +115,11 @@ const CatalogPost = ({ post }: { post: Comment }) => {
|
||||
subplebbitAddress,
|
||||
timestamp,
|
||||
title,
|
||||
thumbnailUrl,
|
||||
} = post || {};
|
||||
const linkCount = useCountLinksInReplies(post);
|
||||
|
||||
const commentMediaInfo = useCommentMediaInfo(post);
|
||||
const commentMediaInfo = useCommentMediaInfo(link, thumbnailUrl, linkWidth, linkHeight);
|
||||
const hasThumbnail = getHasThumbnail(commentMediaInfo, link);
|
||||
|
||||
const { hidden } = useHide({ cid });
|
||||
@@ -181,7 +182,8 @@ const CatalogPost = ({ post }: { post: Comment }) => {
|
||||
update();
|
||||
}, [update, windowWidth]);
|
||||
|
||||
const lastReply = useComment({ commentCid: lastChildCid });
|
||||
const replies = useReplies(post);
|
||||
const lastReply = replies.length > 0 ? replies[replies.length - 1] : null;
|
||||
|
||||
const { isCommentAuthorMod: isCatalogPostAuthorMod, commentAuthorRole: catalogPostAuthorRole } = useEditCommentPrivileges({
|
||||
commentAuthorAddress: author?.address,
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
.container {
|
||||
position: fixed;
|
||||
top: calc(50% - 150px);
|
||||
left: calc(50% - 150px);
|
||||
display: block;
|
||||
padding: 2px;
|
||||
font-size: 10pt;
|
||||
@@ -10,6 +8,9 @@
|
||||
z-index: 999;
|
||||
background-color: var(--challenge-modal-background-color);
|
||||
border: var(--challenge-modal-border);
|
||||
will-change: transform;
|
||||
backface-visibility: hidden;
|
||||
-webkit-backface-visibility: hidden;
|
||||
}
|
||||
|
||||
.title {
|
||||
@@ -24,6 +25,9 @@
|
||||
background-color: var(--challenge-modal-title-background-color);
|
||||
color: var(--challenge-modal-title-text-color);
|
||||
border: var(--challenge-modal-title-border);
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
.closeIcon {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { useRef, useState, useEffect } from 'react';
|
||||
import Draggable from 'react-draggable';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Challenge as ChallengeType } from '@plebbit/plebbit-react-hooks';
|
||||
import { getPublicationType } from '../../lib/utils/challenge-utils';
|
||||
@@ -7,6 +6,8 @@ import useIsMobile from '../../hooks/use-is-mobile';
|
||||
import useChallengesStore from '../../stores/use-challenges-store';
|
||||
import styles from './challenge-modal.module.css';
|
||||
import _ from 'lodash';
|
||||
import { useSpring, animated } from '@react-spring/web';
|
||||
import { useDrag } from '@use-gesture/react';
|
||||
|
||||
interface ChallengeProps {
|
||||
challenge: ChallengeType;
|
||||
@@ -63,13 +64,44 @@ const Challenge = ({ challenge, closeModal }: ChallengeProps) => {
|
||||
return () => document.removeEventListener('keydown', onEscapeKey);
|
||||
}, [closeModal]);
|
||||
|
||||
// react-draggable requires a ref to the modal node
|
||||
const nodeRef = useRef(null);
|
||||
const nodeRef = useRef<HTMLDivElement>(null);
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
const [{ x, y }, api] = useSpring(() => ({
|
||||
x: window.innerWidth / 2 - 150,
|
||||
y: window.innerHeight / 2 - 200,
|
||||
}));
|
||||
|
||||
const bind = useDrag(
|
||||
({ active, event, offset: [ox, oy] }) => {
|
||||
if (active) {
|
||||
event.preventDefault();
|
||||
document.body.style.userSelect = 'none';
|
||||
document.body.style.webkitUserSelect = 'none';
|
||||
} else {
|
||||
document.body.style.userSelect = '';
|
||||
document.body.style.webkitUserSelect = '';
|
||||
}
|
||||
api.start({ x: ox, y: oy, immediate: true });
|
||||
},
|
||||
{
|
||||
from: () => [x.get(), y.get()],
|
||||
filterTaps: true,
|
||||
bounds: undefined,
|
||||
},
|
||||
);
|
||||
|
||||
const modalContent = (
|
||||
<div className={styles.container} ref={nodeRef}>
|
||||
<div className={`challengeHandle ${styles.title}`}>
|
||||
<animated.div
|
||||
className={styles.container}
|
||||
ref={nodeRef}
|
||||
style={{
|
||||
x: isMobile ? 0 : x,
|
||||
y: isMobile ? 0 : y,
|
||||
touchAction: 'none',
|
||||
}}
|
||||
>
|
||||
<div className={`challengeHandle ${styles.title}`} {...(!isMobile ? bind() : {})}>
|
||||
Challenge for {publicationType}
|
||||
<button className={styles.closeIcon} onClick={closeModal} title='close' />
|
||||
</div>
|
||||
@@ -127,16 +159,10 @@ const Challenge = ({ challenge, closeModal }: ChallengeProps) => {
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</animated.div>
|
||||
);
|
||||
|
||||
return isMobile ? (
|
||||
modalContent
|
||||
) : (
|
||||
<Draggable handle='.challengeHandle' nodeRef={nodeRef}>
|
||||
{modalContent}
|
||||
</Draggable>
|
||||
);
|
||||
return modalContent;
|
||||
};
|
||||
|
||||
const ChallengeModal = () => {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useState } from 'react';
|
||||
import { Fragment, useState } from 'react';
|
||||
import { useLocation, useParams } from 'react-router-dom';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
import { Comment } from '@plebbit/plebbit-react-hooks';
|
||||
import { getFormattedDate, getFormattedTimeAgo } from '../../lib/utils/time-utils';
|
||||
import { isPostPageView } from '../../lib/utils/view-utils';
|
||||
import useIsMobile from '../../hooks/use-is-mobile';
|
||||
@@ -9,11 +10,10 @@ import LoadingEllipsis from '../../components/loading-ellipsis';
|
||||
import ReplyQuotePreview from '../../components/reply-quote-preview';
|
||||
import Markdown from '../../components/markdown';
|
||||
import Tooltip from '../../components/tooltip';
|
||||
import { Comment, useComment } from '@plebbit/plebbit-react-hooks';
|
||||
import styles from '../../views/post/post.module.css';
|
||||
import _ from 'lodash';
|
||||
|
||||
const CommentContent = ({ comment: post }: { comment: Comment }) => {
|
||||
const CommentContent = ({ comment: post, replies }: { comment: Comment; replies: Comment[] }) => {
|
||||
const { t } = useTranslation();
|
||||
const params = useParams();
|
||||
const location = useLocation();
|
||||
@@ -34,9 +34,10 @@ const CommentContent = ({ comment: post }: { comment: Comment }) => {
|
||||
? content.slice(0, 2000)
|
||||
: content);
|
||||
|
||||
const quotelinkReply = useComment({ commentCid: parentCid });
|
||||
const isReply = parentCid;
|
||||
const isReplyingToReply = (postCid && postCid !== parentCid) || quotelinkReply?.postCid !== parentCid;
|
||||
const quotelinkReply = parentCid && replies?.find((reply) => reply.cid === parentCid);
|
||||
|
||||
const isReply = !!parentCid;
|
||||
const isReplyingToReply = isReply && parentCid !== postCid;
|
||||
|
||||
const stateString = useStateString(post);
|
||||
|
||||
@@ -44,7 +45,9 @@ const CommentContent = ({ comment: post }: { comment: Comment }) => {
|
||||
|
||||
return (
|
||||
<blockquote className={`${styles.postMessage} ${!isReply && isMobile && styles.clampLines} ${isRules && styles.rulesMessage}`}>
|
||||
{isReply && state !== 'failed' && isReplyingToReply && !(deleted || removed) && <ReplyQuotePreview isQuotelinkReply={true} quotelinkReply={quotelinkReply} />}
|
||||
{isReply && state !== 'failed' && isReplyingToReply && !(deleted || removed) && (
|
||||
<ReplyQuotePreview isQuotelinkReply={true} quotelinkReply={quotelinkReply} replies={replies} />
|
||||
)}
|
||||
{removed ? (
|
||||
reason ? (
|
||||
<>
|
||||
@@ -84,7 +87,7 @@ const CommentContent = ({ comment: post }: { comment: Comment }) => {
|
||||
i18nKey={'comment_edited_at_timestamp'}
|
||||
values={{ timestamp: getFormattedDate(edit?.timestamp) }}
|
||||
shouldUnescape={true}
|
||||
components={{ 1: <Tooltip content={getFormattedTimeAgo(edit?.timestamp)} children={<></>} /> }}
|
||||
components={{ 1: <Tooltip content={getFormattedTimeAgo(edit?.timestamp)} children={<Fragment key={edit?.timestamp}></Fragment>} /> }}
|
||||
/>{' '}
|
||||
{reason && <>{t('reason_reason', { reason: reason, interpolation: { escapeValue: false } })} </>}
|
||||
{showOriginal ? (
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Comment } from '@plebbit/plebbit-react-hooks';
|
||||
import { CommentMediaInfo, getDisplayMediaInfoType, getHasThumbnail, getMediaDimensions } from '../../lib/utils/media-utils';
|
||||
import { getHostname } from '../../lib/utils/url-utils';
|
||||
import useExpandedMediaStore from '../../stores/use-expanded-media-store';
|
||||
@@ -14,12 +13,14 @@ interface MediaProps {
|
||||
deleted?: boolean;
|
||||
displayHeight?: string;
|
||||
displayWidth?: string;
|
||||
isDescription?: boolean;
|
||||
isRules?: boolean;
|
||||
isFloatingEmbed?: boolean;
|
||||
isOutOfFeed?: boolean;
|
||||
isReply?: boolean;
|
||||
linkHeight?: number;
|
||||
linkWidth?: number;
|
||||
post?: Comment;
|
||||
parentCid?: string;
|
||||
removed?: boolean;
|
||||
spoiler?: boolean;
|
||||
showThumbnail?: boolean;
|
||||
@@ -130,12 +131,12 @@ interface ImageProps {
|
||||
displayHeight: string;
|
||||
displayWidth: string;
|
||||
isOutOfFeed: boolean;
|
||||
post: Comment | undefined;
|
||||
parentCid?: string;
|
||||
spoiler?: boolean;
|
||||
}
|
||||
|
||||
const Image = ({ commentMediaInfo, displayHeight, displayWidth, isOutOfFeed, post }: ImageProps) => {
|
||||
const Image = ({ commentMediaInfo, displayHeight, displayWidth, isOutOfFeed, parentCid, spoiler }: ImageProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { parentCid, spoiler } = post || {};
|
||||
const { type, url } = commentMediaInfo || {};
|
||||
const isReply = parentCid;
|
||||
const isMobile = useIsMobile();
|
||||
@@ -181,7 +182,7 @@ const Image = ({ commentMediaInfo, displayHeight, displayWidth, isOutOfFeed, pos
|
||||
{mediaDimensions && `, ${mediaDimensions}`})
|
||||
</div>
|
||||
)}
|
||||
{type && !isImageExpanded && <div className={styles.fileInfo}>{`${post?.spoiler ? `${t('spoiler')} - ` : ''} ${getDisplayMediaInfoType(type, t)}`}</div>}
|
||||
{type && !isImageExpanded && <div className={styles.fileInfo}>{`${spoiler ? `${t('spoiler')} - ` : ''} ${getDisplayMediaInfoType(type, t)}`}</div>}
|
||||
</span>
|
||||
) : (
|
||||
<span
|
||||
@@ -197,8 +198,20 @@ const Image = ({ commentMediaInfo, displayHeight, displayWidth, isOutOfFeed, pos
|
||||
);
|
||||
};
|
||||
|
||||
const CommentMedia = ({ commentMediaInfo, isFloatingEmbed, post, showThumbnail, setShowThumbnail }: MediaProps) => {
|
||||
const { deleted, linkHeight, linkWidth, parentCid, removed, spoiler } = post || {};
|
||||
const CommentMedia = ({
|
||||
commentMediaInfo,
|
||||
deleted,
|
||||
isDescription,
|
||||
isFloatingEmbed,
|
||||
isRules,
|
||||
linkHeight,
|
||||
linkWidth,
|
||||
parentCid,
|
||||
removed,
|
||||
showThumbnail,
|
||||
setShowThumbnail,
|
||||
spoiler,
|
||||
}: MediaProps) => {
|
||||
const isReply = parentCid;
|
||||
const { t } = useTranslation();
|
||||
const isMobile = useIsMobile();
|
||||
@@ -235,14 +248,20 @@ const CommentMedia = ({ commentMediaInfo, isFloatingEmbed, post, showThumbnail,
|
||||
displayWidth = '100%';
|
||||
displayHeight = '100%';
|
||||
}
|
||||
const { isDescription, isRules } = post || {}; // custom properties, not from api
|
||||
const isOutOfFeed = isDescription || isRules || isFloatingEmbed || spoiler; // virtuoso wrapper unneeded
|
||||
const isOutOfFeed = isDescription || isRules || isFloatingEmbed || spoiler || false; // virtuoso wrapper unneeded
|
||||
|
||||
return (
|
||||
<span className={styles.content}>
|
||||
{commentMediaInfo?.type === 'image' ? (
|
||||
// images just enlarge when clicked, so they don't need two separate components
|
||||
<Image commentMediaInfo={commentMediaInfo} displayHeight={displayHeight} displayWidth={displayWidth} isOutOfFeed={isOutOfFeed} post={post} />
|
||||
<Image
|
||||
commentMediaInfo={commentMediaInfo}
|
||||
displayHeight={displayHeight}
|
||||
displayWidth={displayWidth}
|
||||
isOutOfFeed={isOutOfFeed}
|
||||
parentCid={parentCid}
|
||||
spoiler={spoiler}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<span className={`${showThumbnail ? styles.show : styles.hide} ${styles.thumbnail}`}>
|
||||
@@ -259,9 +278,9 @@ const CommentMedia = ({ commentMediaInfo, isFloatingEmbed, post, showThumbnail,
|
||||
setShowThumbnail={setShowThumbnail}
|
||||
/>
|
||||
)}
|
||||
{isMobile && type && <div className={styles.fileInfo}>{`${post?.spoiler ? `${t('spoiler')} - ` : ''} ${getDisplayMediaInfoType(type, t)}`}</div>}
|
||||
{isMobile && type && <div className={styles.fileInfo}>{`${spoiler ? `${t('spoiler')} - ` : ''} ${getDisplayMediaInfoType(type, t)}`}</div>}
|
||||
</span>
|
||||
{!showThumbnail && <Media commentMediaInfo={commentMediaInfo} isReply={post?.parentCid} setShowThumbnail={setShowThumbnail} />}
|
||||
{!showThumbnail && <Media commentMediaInfo={commentMediaInfo} isReply={!!parentCid} setShowThumbnail={setShowThumbnail} />}
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { useState } from 'react';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
import { Link, useLocation, useParams } from 'react-router-dom';
|
||||
import { Comment, useAuthorAvatar, useEditedComment, useSubplebbit } from '@plebbit/plebbit-react-hooks';
|
||||
import { Comment, useAuthorAvatar, useEditedComment } from '@plebbit/plebbit-react-hooks';
|
||||
import useSubplebbitsStore from '@plebbit/plebbit-react-hooks/dist/stores/subplebbits';
|
||||
import Plebbit from '@plebbit/plebbit-js/dist/browser/index.js';
|
||||
import styles from '../../views/post/post.module.css';
|
||||
import { getDisplayMediaInfoType, getHasThumbnail, getMediaDimensions } from '../../lib/utils/media-utils';
|
||||
import { CommentMediaInfo, getDisplayMediaInfoType, getHasThumbnail, getMediaDimensions } from '../../lib/utils/media-utils';
|
||||
import { hashStringToColor, getTextColorForBackground } from '../../lib/utils/post-utils';
|
||||
import { getFormattedDate, getFormattedTimeAgo } from '../../lib/utils/time-utils';
|
||||
import { isValidURL } from '../../lib/utils/url-utils';
|
||||
@@ -29,6 +30,7 @@ import { PostProps } from '../../views/post/post';
|
||||
import { create } from 'zustand';
|
||||
import _ from 'lodash';
|
||||
import { shouldShowSnow } from '../../lib/snow';
|
||||
import useReplyModalStore from '../../stores/use-reply-modal-store';
|
||||
|
||||
interface ShowOmittedRepliesState {
|
||||
showOmittedReplies: Record<string, boolean>;
|
||||
@@ -46,7 +48,7 @@ const useShowOmittedReplies = create<ShowOmittedRepliesState>((set) => ({
|
||||
})),
|
||||
}));
|
||||
|
||||
const PostInfo = ({ openReplyModal, post, postReplyCount = 0, roles, isHidden }: PostProps) => {
|
||||
const PostInfo = ({ post, postReplyCount = 0, roles, isHidden, replies: threadReplies }: PostProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { author, cid, deleted, locked, pinned, parentCid, postCid, reason, removed, shortCid, state, subplebbitAddress, timestamp } = post || {};
|
||||
const title = post?.title?.trim();
|
||||
@@ -76,6 +78,8 @@ const PostInfo = ({ openReplyModal, post, postReplyCount = 0, roles, isHidden }:
|
||||
|
||||
const { hidden } = useHide(post);
|
||||
|
||||
const { openReplyModal } = useReplyModalStore();
|
||||
|
||||
const onReplyModalClick = () => {
|
||||
deleted
|
||||
? isReply
|
||||
@@ -215,24 +219,28 @@ const PostInfo = ({ openReplyModal, post, postReplyCount = 0, roles, isHidden }:
|
||||
(reply: Comment, index: number) =>
|
||||
reply?.parentCid === cid &&
|
||||
reply?.cid &&
|
||||
!(reply?.deleted || reply?.removed) && <ReplyQuotePreview key={index} isBacklinkReply={true} backlinkReply={reply} />,
|
||||
!(reply?.deleted || reply?.removed) && <ReplyQuotePreview key={index} isBacklinkReply={true} backlinkReply={reply} replies={threadReplies} />,
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const PostMedia = ({ post, hasThumbnail }: PostProps) => {
|
||||
interface PostMediaProps {
|
||||
commentMediaInfo: CommentMediaInfo | undefined;
|
||||
hasThumbnail: boolean;
|
||||
isDescription: boolean;
|
||||
isRules: boolean;
|
||||
spoiler: boolean;
|
||||
deleted: boolean;
|
||||
removed: boolean;
|
||||
linkHeight: number;
|
||||
linkWidth: number;
|
||||
parentCid: string;
|
||||
}
|
||||
|
||||
const PostMedia = ({ commentMediaInfo, hasThumbnail, isDescription, isRules, spoiler, deleted, removed, linkHeight, linkWidth, parentCid }: PostMediaProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { spoiler, cid } = post || {};
|
||||
|
||||
// Reset state by remounting component when post changes
|
||||
return <PostMediaContent key={cid} post={post} hasThumbnail={hasThumbnail} spoiler={spoiler} t={t} />;
|
||||
};
|
||||
|
||||
const PostMediaContent = ({ post, hasThumbnail, spoiler, t }: { post: any; hasThumbnail: boolean | undefined; spoiler: boolean; t: any }) => {
|
||||
const { isDescription, isRules } = post || {}; // custom properties, not from api
|
||||
const commentMediaInfo = useCommentMediaInfo(post);
|
||||
const { url } = commentMediaInfo || {};
|
||||
let type = commentMediaInfo?.type;
|
||||
const gifFrameUrl = useFetchGifFirstFrame(url);
|
||||
@@ -268,8 +276,7 @@ const PostMediaContent = ({ post, hasThumbnail, spoiler, t }: { post: any; hasTh
|
||||
)}
|
||||
{showThumbnail && !hasThumbnail && embedUrl && canEmbed(embedUrl) && (
|
||||
<span>
|
||||
{' '}
|
||||
[
|
||||
-[
|
||||
<span className={styles.closeMedia} onClick={() => setShowThumbnail(false)}>
|
||||
{t('open')}
|
||||
</span>
|
||||
@@ -281,10 +288,17 @@ const PostMediaContent = ({ post, hasThumbnail, spoiler, t }: { post: any; hasTh
|
||||
<div className={styles.fileThumbnail}>
|
||||
<CommentMedia
|
||||
commentMediaInfo={commentMediaInfo}
|
||||
post={post}
|
||||
deleted={deleted}
|
||||
isDescription={isDescription}
|
||||
isRules={isRules}
|
||||
removed={removed}
|
||||
linkHeight={linkHeight}
|
||||
linkWidth={linkWidth}
|
||||
showThumbnail={showThumbnail}
|
||||
setShowThumbnail={setShowThumbnail}
|
||||
isOutOfFeed={isDescription || isRules}
|
||||
parentCid={parentCid}
|
||||
spoiler={spoiler}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@@ -292,7 +306,7 @@ const PostMediaContent = ({ post, hasThumbnail, spoiler, t }: { post: any; hasTh
|
||||
);
|
||||
};
|
||||
|
||||
const Reply = ({ openReplyModal, postReplyCount, reply, roles }: PostProps) => {
|
||||
const Reply = ({ postReplyCount, reply, roles, replies }: PostProps) => {
|
||||
let post = reply;
|
||||
// handle pending mod or author edit
|
||||
const { editedComment } = useEditedComment({ comment: reply });
|
||||
@@ -300,28 +314,43 @@ const Reply = ({ openReplyModal, postReplyCount, reply, roles }: PostProps) => {
|
||||
post = editedComment;
|
||||
}
|
||||
|
||||
const { author, cid, deleted, link, postCid, reason, removed, subplebbitAddress } = post || {};
|
||||
const { author, cid, deleted, link, linkHeight, linkWidth, postCid, reason, removed, spoiler, subplebbitAddress, thumbnailUrl, parentCid } = post || {};
|
||||
const { isDescription, isRules } = post || {}; // custom properties, not from api
|
||||
|
||||
const isRouteLinkToReply = useLocation().pathname.startsWith(`/p/${subplebbitAddress}/c/${cid}`);
|
||||
const { hidden } = useHide({ cid });
|
||||
|
||||
const commentMediaInfo = useCommentMediaInfo(post);
|
||||
const commentMediaInfo = useCommentMediaInfo(link, thumbnailUrl, linkWidth, linkHeight);
|
||||
const hasThumbnail = getHasThumbnail(commentMediaInfo, link);
|
||||
|
||||
return (
|
||||
<div className={styles.replyDesktop}>
|
||||
<div className={styles.sideArrows}>{'>>'}</div>
|
||||
<div className={`${styles.reply} ${isRouteLinkToReply && styles.highlight}`} data-cid={cid} data-author-address={author?.shortAddress} data-post-cid={postCid}>
|
||||
<PostInfo openReplyModal={openReplyModal} post={post} postReplyCount={postReplyCount} roles={roles} isHidden={hidden} />
|
||||
{link && !hidden && !(deleted || removed) && isValidURL(link) && <PostMedia post={post} hasThumbnail={hasThumbnail} />}
|
||||
{!hidden && (!(removed || deleted) || ((removed || deleted) && reason)) && <CommentContent comment={post} />}
|
||||
<PostInfo post={post} postReplyCount={postReplyCount} roles={roles} isHidden={hidden} replies={replies} />
|
||||
{link && !hidden && !(deleted || removed) && isValidURL(link) && (
|
||||
<PostMedia
|
||||
commentMediaInfo={commentMediaInfo}
|
||||
hasThumbnail={hasThumbnail}
|
||||
isDescription={isDescription}
|
||||
isRules={isRules}
|
||||
spoiler={spoiler}
|
||||
deleted={deleted}
|
||||
removed={removed}
|
||||
linkHeight={linkHeight}
|
||||
linkWidth={linkWidth}
|
||||
parentCid={parentCid}
|
||||
/>
|
||||
)}
|
||||
{!hidden && (!(removed || deleted) || ((removed || deleted) && reason)) && <CommentContent comment={post} replies={replies || []} />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const PostDesktop = ({ openReplyModal, post, roles, showAllReplies, showReplies = true }: PostProps) => {
|
||||
const PostDesktop = ({ post, roles, showAllReplies, showReplies = true, replies: threadReplies }: PostProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { author, cid, content, deleted, link, pinned, postCid, removed, state, subplebbitAddress } = post || {};
|
||||
const { author, cid, content, deleted, link, linkHeight, linkWidth, pinned, postCid, removed, spoiler, state, subplebbitAddress, thumbnailUrl, parentCid } = post || {};
|
||||
const { isDescription, isRules } = post || {}; // custom properties, not from api
|
||||
const params = useParams();
|
||||
const location = useLocation();
|
||||
@@ -331,7 +360,8 @@ const PostDesktop = ({ openReplyModal, post, roles, showAllReplies, showReplies
|
||||
const { hidden, unhide, hide } = useHide({ cid });
|
||||
const isHidden = hidden && !isInPostPageView;
|
||||
|
||||
const replies = useReplies(post);
|
||||
const commentReplies = useReplies(post);
|
||||
const replies = threadReplies || commentReplies;
|
||||
const visiblelinksCount = useCountLinksInReplies(post, 5);
|
||||
const totalLinksCount = useCountLinksInReplies(post);
|
||||
const replyCount = replies?.length;
|
||||
@@ -342,7 +372,7 @@ const PostDesktop = ({ openReplyModal, post, roles, showAllReplies, showReplies
|
||||
|
||||
const stateString = useStateString(post) || t('loading_board');
|
||||
|
||||
const subplebbit = useSubplebbit({ subplebbitAddress });
|
||||
const subplebbit = useSubplebbitsStore((state) => state.subplebbits[subplebbitAddress]);
|
||||
|
||||
const subplebbitRulesReply = {
|
||||
isRules: true,
|
||||
@@ -353,7 +383,7 @@ const PostDesktop = ({ openReplyModal, post, roles, showAllReplies, showReplies
|
||||
replyCount: 0,
|
||||
};
|
||||
|
||||
const commentMediaInfo = useCommentMediaInfo(post);
|
||||
const commentMediaInfo = useCommentMediaInfo(link, thumbnailUrl, linkWidth, linkHeight);
|
||||
const hasThumbnail = getHasThumbnail(commentMediaInfo, link);
|
||||
|
||||
return (
|
||||
@@ -373,10 +403,23 @@ const PostDesktop = ({ openReplyModal, post, roles, showAllReplies, showReplies
|
||||
)}
|
||||
<div data-cid={cid} data-author-address={author?.shortAddress} data-post-cid={postCid} className={shouldShowSnow() && hasThumbnail ? styles.xmasHatWrapper : ''}>
|
||||
{shouldShowSnow() && hasThumbnail && <img src={`${process.env.PUBLIC_URL}/assets/xmashat.gif`} className={styles.xmasHat} alt='' />}
|
||||
{link && !isHidden && !(deleted || removed) && isValidURL(link) && <PostMedia post={post} hasThumbnail={hasThumbnail} />}
|
||||
<PostInfo isHidden={hidden} openReplyModal={openReplyModal} post={post} postReplyCount={replyCount} roles={roles} />
|
||||
{link && !isHidden && !(deleted || removed) && isValidURL(link) && (
|
||||
<PostMedia
|
||||
commentMediaInfo={commentMediaInfo}
|
||||
hasThumbnail={hasThumbnail}
|
||||
isDescription={isDescription}
|
||||
isRules={isRules}
|
||||
spoiler={spoiler}
|
||||
deleted={deleted}
|
||||
removed={removed}
|
||||
linkHeight={linkHeight}
|
||||
linkWidth={linkWidth}
|
||||
parentCid={parentCid}
|
||||
/>
|
||||
)}
|
||||
<PostInfo isHidden={hidden} post={post} postReplyCount={replyCount} roles={roles} replies={replies} />
|
||||
{!isHidden && !content && !(deleted || removed) && <div className={styles.spacer} />}
|
||||
{!isHidden && <CommentContent comment={post} />}
|
||||
{!isHidden && <CommentContent comment={post} replies={replies} />}
|
||||
</div>
|
||||
{!isHidden && !isDescription && !isRules && !isInPendingPostView && (replyCount > 5 || (pinned && repliesCount > 0)) && !isInPostPageView && (
|
||||
<span className={styles.summary}>
|
||||
@@ -405,7 +448,7 @@ const PostDesktop = ({ openReplyModal, post, roles, showAllReplies, showReplies
|
||||
showReplies &&
|
||||
(showAllReplies || showOmittedReplies[cid] ? replies : replies.slice(-5)).map((reply, index) => (
|
||||
<div key={index} className={styles.replyContainer}>
|
||||
<Reply openReplyModal={openReplyModal} reply={reply} roles={roles} postReplyCount={replyCount} />
|
||||
<Reply reply={reply} roles={roles} postReplyCount={replyCount} replies={replies} />
|
||||
</div>
|
||||
))}
|
||||
{isDescription && subplebbit?.rules && subplebbit?.rules.length > 0 && (
|
||||
|
||||
@@ -132,8 +132,8 @@ const BlockBoardButton = ({ address }: { address: string }) => {
|
||||
|
||||
const PostMenuDesktop = ({ post }: { post: Comment }) => {
|
||||
const { t } = useTranslation();
|
||||
const { author, cid, isDescription, isRules, link, postCid, subplebbitAddress } = post || {};
|
||||
const commentMediaInfo = getCommentMediaInfo(post);
|
||||
const { author, cid, isDescription, isRules, link, thumbnailUrl, linkWidth, linkHeight, postCid, subplebbitAddress } = post || {};
|
||||
const commentMediaInfo = getCommentMediaInfo(link, thumbnailUrl, linkWidth, linkHeight);
|
||||
const { thumbnail, type, url } = commentMediaInfo || {};
|
||||
const [menuBtnRotated, setMenuBtnRotated] = useState(false);
|
||||
|
||||
|
||||
@@ -122,9 +122,9 @@ const BlockBoardButton = ({ address }: { address: string }) => {
|
||||
};
|
||||
|
||||
const PostMenuMobile = ({ post }: { post: Comment }) => {
|
||||
const { author, cid, deleted, isDescription, isRules, link, parentCid, postCid, removed, subplebbitAddress } = post || {};
|
||||
const { author, cid, deleted, isDescription, isRules, link, linkHeight, linkWidth, parentCid, postCid, removed, subplebbitAddress, thumbnailUrl } = post || {};
|
||||
const { isAccountMod, isAccountCommentAuthor } = useEditCommentPrivileges({ commentAuthorAddress: author?.address, subplebbitAddress });
|
||||
const commentMediaInfo = getCommentMediaInfo(post);
|
||||
const commentMediaInfo = getCommentMediaInfo(link, thumbnailUrl, linkWidth, linkHeight);
|
||||
const { thumbnail, type, url } = commentMediaInfo || {};
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||
const { refs, floatingStyles, context } = useFloating({
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Link, useLocation, useParams } from 'react-router-dom';
|
||||
import { Comment, useAuthorAvatar, useEditedComment, useSubplebbit } from '@plebbit/plebbit-react-hooks';
|
||||
import { Comment, useAuthorAvatar, useEditedComment } from '@plebbit/plebbit-react-hooks';
|
||||
import useSubplebbitsStore from '@plebbit/plebbit-react-hooks/dist/stores/subplebbits';
|
||||
import Plebbit from '@plebbit/plebbit-js/dist/browser/index.js';
|
||||
import styles from '../../views/post/post.module.css';
|
||||
import { shouldShowSnow } from '../../lib/snow';
|
||||
@@ -24,10 +25,29 @@ import ReplyQuotePreview from '../reply-quote-preview';
|
||||
import Tooltip from '../tooltip';
|
||||
import { PostProps } from '../../views/post/post';
|
||||
import _ from 'lodash';
|
||||
import useReplyModalStore from '../../stores/use-reply-modal-store';
|
||||
|
||||
const PostInfoAndMedia = ({ openReplyModal, post, postReplyCount = 0, roles }: PostProps) => {
|
||||
const PostInfoAndMedia = ({ post, postReplyCount = 0, roles }: PostProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { author, cid, deleted, link, locked, parentCid, pinned, postCid, reason, removed, shortCid, state, subplebbitAddress, timestamp } = post || {};
|
||||
const {
|
||||
author,
|
||||
cid,
|
||||
deleted,
|
||||
link,
|
||||
linkHeight,
|
||||
linkWidth,
|
||||
locked,
|
||||
parentCid,
|
||||
pinned,
|
||||
postCid,
|
||||
reason,
|
||||
removed,
|
||||
shortCid,
|
||||
state,
|
||||
subplebbitAddress,
|
||||
timestamp,
|
||||
thumbnailUrl,
|
||||
} = post || {};
|
||||
const isReply = parentCid;
|
||||
const title = post?.title?.trim();
|
||||
const { isDescription, isRules } = post || {}; // custom properties, not from api
|
||||
@@ -43,7 +63,7 @@ const PostInfoAndMedia = ({ openReplyModal, post, postReplyCount = 0, roles }: P
|
||||
const isInPostPageView = isPostPageView(location.pathname, params);
|
||||
const isInSubscriptionsView = isSubscriptionsView(location.pathname, params);
|
||||
|
||||
const commentMediaInfo = useCommentMediaInfo(post);
|
||||
const commentMediaInfo = useCommentMediaInfo(link, thumbnailUrl, linkWidth, linkHeight);
|
||||
const hasThumbnail = getHasThumbnail(commentMediaInfo, link);
|
||||
|
||||
const stateString = useStateString(post);
|
||||
@@ -57,6 +77,8 @@ const PostInfoAndMedia = ({ openReplyModal, post, postReplyCount = 0, roles }: P
|
||||
|
||||
const { hidden } = useHide(post);
|
||||
|
||||
const { openReplyModal } = useReplyModalStore();
|
||||
|
||||
const onReplyModalClick = () => {
|
||||
deleted
|
||||
? isReply
|
||||
@@ -179,31 +201,39 @@ const PostInfoAndMedia = ({ openReplyModal, post, postReplyCount = 0, roles }: P
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
{(hasThumbnail || link) && !(deleted || removed) && <PostMediaContent key={cid} post={post} link={link} t={t} />}
|
||||
{(hasThumbnail || link) && !(deleted || removed) && <PostMediaContent key={cid} post={post} link={link} />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const PostMediaContent = ({ post, link, t }: { post: any; link: string; t: any }) => {
|
||||
const PostMediaContent = ({ post, link }: { post: any; link: string }) => {
|
||||
const [showThumbnail, setShowThumbnail] = useState(true);
|
||||
const { isDescription, isRules } = post || {}; // custom properties, not from api
|
||||
const commentMediaInfo = useCommentMediaInfo(post);
|
||||
const { thumbnailUrl, linkWidth, linkHeight, spoiler, deleted, removed, parentCid } = post || {};
|
||||
const commentMediaInfo = useCommentMediaInfo(link, thumbnailUrl, linkWidth, linkHeight);
|
||||
const hasThumbnail = getHasThumbnail(commentMediaInfo, link);
|
||||
|
||||
return (
|
||||
hasThumbnail && (
|
||||
<CommentMedia
|
||||
commentMediaInfo={commentMediaInfo}
|
||||
post={post}
|
||||
deleted={deleted}
|
||||
isDescription={isDescription}
|
||||
isRules={isRules}
|
||||
removed={removed}
|
||||
linkHeight={linkHeight}
|
||||
linkWidth={linkWidth}
|
||||
showThumbnail={showThumbnail}
|
||||
setShowThumbnail={setShowThumbnail}
|
||||
isOutOfFeed={isDescription || isRules}
|
||||
parentCid={parentCid}
|
||||
spoiler={spoiler}
|
||||
/>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const ReplyBacklinks = ({ post }: PostProps) => {
|
||||
const ReplyBacklinks = ({ post, replies: threadReplies }: PostProps) => {
|
||||
const { cid, parentCid } = post || {};
|
||||
const replies = useReplies(post);
|
||||
|
||||
@@ -216,14 +246,14 @@ const ReplyBacklinks = ({ post }: PostProps) => {
|
||||
(reply: Comment, index: number) =>
|
||||
reply?.parentCid === cid &&
|
||||
reply?.cid &&
|
||||
!(reply?.deleted || reply?.removed) && <ReplyQuotePreview key={index} isBacklinkReply={true} backlinkReply={reply} />,
|
||||
!(reply?.deleted || reply?.removed) && <ReplyQuotePreview key={index} isBacklinkReply={true} backlinkReply={reply} replies={threadReplies} />,
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const Reply = ({ openReplyModal, postReplyCount, reply, roles }: PostProps) => {
|
||||
const Reply = ({ postReplyCount, reply, replies, roles }: PostProps) => {
|
||||
let post = reply;
|
||||
// handle pending mod or author edit
|
||||
const { editedComment } = useEditedComment({ comment: reply });
|
||||
@@ -243,8 +273,8 @@ const Reply = ({ openReplyModal, postReplyCount, reply, roles }: PostProps) => {
|
||||
data-author-address={author?.shortAddress}
|
||||
data-post-cid={postCid}
|
||||
>
|
||||
<PostInfoAndMedia openReplyModal={openReplyModal} post={post} postReplyCount={postReplyCount} roles={roles} />
|
||||
{!hidden && (!(removed || deleted) || ((removed || deleted) && reason)) && <CommentContent comment={post} />}
|
||||
<PostInfoAndMedia post={post} postReplyCount={postReplyCount} roles={roles} />
|
||||
{!hidden && (!(removed || deleted) || ((removed || deleted) && reason)) && <CommentContent comment={post} replies={replies || []} />}
|
||||
<ReplyBacklinks post={reply} />
|
||||
</div>
|
||||
</div>
|
||||
@@ -252,7 +282,7 @@ const Reply = ({ openReplyModal, postReplyCount, reply, roles }: PostProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
const PostMobile = ({ openReplyModal, post, roles, showAllReplies, showReplies = true }: PostProps) => {
|
||||
const PostMobile = ({ post, roles, showAllReplies, showReplies = true, replies: threadReplies }: PostProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { author, cid, pinned, postCid, replyCount, state, subplebbitAddress } = post || {};
|
||||
const { isDescription, isRules } = post || {}; // custom properties, not from api
|
||||
@@ -262,14 +292,15 @@ const PostMobile = ({ openReplyModal, post, roles, showAllReplies, showReplies =
|
||||
const isInPendingPostView = isPendingPostView(location.pathname, params);
|
||||
const isInPostView = isPostPageView(location.pathname, params);
|
||||
const linksCount = useCountLinksInReplies(post);
|
||||
const replies = useReplies(post);
|
||||
const commentReplies = useReplies(post);
|
||||
const replies = threadReplies || commentReplies;
|
||||
|
||||
const isInPostPageView = isPostPageView(location.pathname, params);
|
||||
const { hidden, unhide } = useHide({ cid });
|
||||
|
||||
const stateString = useStateString(post) || t('loading_post');
|
||||
|
||||
const subplebbit = useSubplebbit({ subplebbitAddress });
|
||||
const subplebbit = useSubplebbitsStore((state) => state.subplebbits[subplebbitAddress]);
|
||||
const showRules = isDescription && subplebbit?.rules && subplebbit?.rules.length > 0;
|
||||
const subplebbitRulesReply = {
|
||||
isRules: true,
|
||||
@@ -307,8 +338,8 @@ const PostMobile = ({ openReplyModal, post, roles, showAllReplies, showReplies =
|
||||
data-post-cid={postCid}
|
||||
>
|
||||
{shouldShowSnow() && <img src={`${process.env.PUBLIC_URL}/assets/xmashat.gif`} className={styles.xmasHat} alt='' />}
|
||||
<PostInfoAndMedia openReplyModal={openReplyModal} post={post} postReplyCount={replyCount} roles={roles} />
|
||||
<CommentContent comment={post} />
|
||||
<PostInfoAndMedia post={post} postReplyCount={replyCount} roles={roles} />
|
||||
<CommentContent comment={post} replies={replies} />
|
||||
</div>
|
||||
{!isInPostView && !isInPendingPostView && showReplies && (
|
||||
<div className={styles.postLink}>
|
||||
@@ -331,7 +362,7 @@ const PostMobile = ({ openReplyModal, post, roles, showAllReplies, showReplies =
|
||||
showReplies &&
|
||||
(showAllReplies ? replies : replies.slice(-5)).map((reply, index) => (
|
||||
<div key={index} className={styles.replyContainer}>
|
||||
<Reply openReplyModal={openReplyModal} postReplyCount={replyCount} reply={reply} roles={roles} />
|
||||
<Reply postReplyCount={replyCount} reply={reply} roles={roles} replies={replies} />
|
||||
</div>
|
||||
))}
|
||||
{showRules && (
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
font-size: 10pt;
|
||||
border-left: none;
|
||||
border-top: none;
|
||||
z-index: 3;
|
||||
z-index: 999;
|
||||
background-color: var(--challenge-modal-background-color);
|
||||
border: var(--challenge-modal-border);
|
||||
will-change: transform;
|
||||
|
||||
@@ -147,11 +147,19 @@ const ReplyModal = ({ closeModal, showReplyModal, parentCid, postCid, scrollY, s
|
||||
|
||||
const [{ x, y }, api] = useSpring(() => ({
|
||||
x: window.innerWidth / 2 - 150,
|
||||
y: window.innerHeight / 2 - 400,
|
||||
y: window.innerHeight / 2 - 200,
|
||||
}));
|
||||
|
||||
const bind = useDrag(
|
||||
({ offset: [ox, oy] }) => {
|
||||
({ active, event, offset: [ox, oy] }) => {
|
||||
if (active) {
|
||||
event.preventDefault();
|
||||
document.body.style.userSelect = 'none';
|
||||
document.body.style.webkitUserSelect = 'none';
|
||||
} else {
|
||||
document.body.style.userSelect = '';
|
||||
document.body.style.webkitUserSelect = '';
|
||||
}
|
||||
api.start({ x: ox, y: oy, immediate: true });
|
||||
},
|
||||
{
|
||||
|
||||
@@ -13,6 +13,7 @@ interface ReplyQuotePreviewProps {
|
||||
backlinkReply?: Comment;
|
||||
isQuotelinkReply?: boolean;
|
||||
quotelinkReply?: Comment;
|
||||
replies?: Comment[];
|
||||
}
|
||||
|
||||
const handleQuoteHover = (cid: string, onElementOutOfView: () => void) => {
|
||||
@@ -53,7 +54,7 @@ const handleQuoteHover = (cid: string, onElementOutOfView: () => void) => {
|
||||
}
|
||||
};
|
||||
|
||||
const DesktopQuotePreview = ({ backlinkReply, quotelinkReply, isBacklinkReply, isQuotelinkReply }: ReplyQuotePreviewProps) => {
|
||||
const DesktopQuotePreview = ({ backlinkReply, quotelinkReply, isBacklinkReply, isQuotelinkReply, replies }: ReplyQuotePreviewProps) => {
|
||||
const [hoveredCid, setHoveredCid] = useState<string | null>(null);
|
||||
const [outOfViewCid, setOutOfViewCid] = useState<string | null>(null);
|
||||
const placementRef = useRef<Placement>('right');
|
||||
@@ -144,7 +145,7 @@ const DesktopQuotePreview = ({ backlinkReply, quotelinkReply, isBacklinkReply, i
|
||||
outOfViewCid === backlinkReply?.cid &&
|
||||
createPortal(
|
||||
<div className={styles.replyQuotePreview} ref={refs.setFloating} style={floatingStyles}>
|
||||
<Post post={backlinkReply} showReplies={false} />
|
||||
<Post post={backlinkReply} showReplies={false} replies={replies} />
|
||||
</div>,
|
||||
document.body,
|
||||
)}
|
||||
@@ -173,7 +174,7 @@ const DesktopQuotePreview = ({ backlinkReply, quotelinkReply, isBacklinkReply, i
|
||||
outOfViewCid === quotelinkReply?.cid &&
|
||||
createPortal(
|
||||
<div className={styles.replyQuotePreview} ref={refs.setFloating} style={floatingStyles}>
|
||||
<Post post={quotelinkReply} showReplies={false} />
|
||||
<Post post={quotelinkReply} showReplies={false} replies={replies} />
|
||||
</div>,
|
||||
document.body,
|
||||
)}
|
||||
@@ -183,7 +184,7 @@ const DesktopQuotePreview = ({ backlinkReply, quotelinkReply, isBacklinkReply, i
|
||||
return isBacklinkReply ? replyBacklink : isQuotelinkReply && replyQuotelink;
|
||||
};
|
||||
|
||||
const MobileQuotePreview = ({ backlinkReply, quotelinkReply, isBacklinkReply, isQuotelinkReply }: ReplyQuotePreviewProps) => {
|
||||
const MobileQuotePreview = ({ backlinkReply, quotelinkReply, isBacklinkReply, isQuotelinkReply, replies }: ReplyQuotePreviewProps) => {
|
||||
const [hoveredCid, setHoveredCid] = useState<string | null>(null);
|
||||
const [outOfViewCid, setOutOfViewCid] = useState<string | null>(null);
|
||||
|
||||
@@ -256,7 +257,7 @@ const MobileQuotePreview = ({ backlinkReply, quotelinkReply, isBacklinkReply, is
|
||||
outOfViewCid === backlinkReply?.cid &&
|
||||
createPortal(
|
||||
<div className={styles.replyQuotePreview} ref={refs.setFloating} style={floatingStyles}>
|
||||
<Post post={backlinkReply} showReplies={false} />
|
||||
<Post post={backlinkReply} showReplies={false} replies={replies} />
|
||||
</div>,
|
||||
document.body,
|
||||
)}
|
||||
@@ -292,7 +293,7 @@ const MobileQuotePreview = ({ backlinkReply, quotelinkReply, isBacklinkReply, is
|
||||
outOfViewCid === quotelinkReply?.cid &&
|
||||
createPortal(
|
||||
<div className={styles.replyQuotePreview} ref={refs.setFloating} style={floatingStyles}>
|
||||
<Post post={quotelinkReply} showReplies={false} />
|
||||
<Post post={quotelinkReply} showReplies={false} replies={replies} />
|
||||
</div>,
|
||||
document.body,
|
||||
)}
|
||||
@@ -302,13 +303,25 @@ const MobileQuotePreview = ({ backlinkReply, quotelinkReply, isBacklinkReply, is
|
||||
return isBacklinkReply ? replyBacklink : isQuotelinkReply && replyQuotelink;
|
||||
};
|
||||
|
||||
const ReplyQuotePreview = ({ backlinkReply, quotelinkReply, isBacklinkReply, isQuotelinkReply }: ReplyQuotePreviewProps) => {
|
||||
const ReplyQuotePreview = ({ backlinkReply, quotelinkReply, isBacklinkReply, isQuotelinkReply, replies }: ReplyQuotePreviewProps) => {
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
return isMobile ? (
|
||||
<MobileQuotePreview backlinkReply={backlinkReply} quotelinkReply={quotelinkReply} isBacklinkReply={isBacklinkReply} isQuotelinkReply={isQuotelinkReply} />
|
||||
<MobileQuotePreview
|
||||
backlinkReply={backlinkReply}
|
||||
quotelinkReply={quotelinkReply}
|
||||
isBacklinkReply={isBacklinkReply}
|
||||
isQuotelinkReply={isQuotelinkReply}
|
||||
replies={replies}
|
||||
/>
|
||||
) : (
|
||||
<DesktopQuotePreview backlinkReply={backlinkReply} quotelinkReply={quotelinkReply} isBacklinkReply={isBacklinkReply} isQuotelinkReply={isQuotelinkReply} />
|
||||
<DesktopQuotePreview
|
||||
backlinkReply={backlinkReply}
|
||||
quotelinkReply={quotelinkReply}
|
||||
isBacklinkReply={isBacklinkReply}
|
||||
isQuotelinkReply={isQuotelinkReply}
|
||||
replies={replies}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -142,6 +142,7 @@ const AvatarSettings = () => {
|
||||
components={{
|
||||
1: (
|
||||
<Link
|
||||
key='token-address-link'
|
||||
to='https://github.com/plebbit/plebbit-react-hooks/blob/557cc3f40b5933a00553ed9c0bc310d2cd7a3b52/src/hooks/authors/author-avatars.ts#L133'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
@@ -177,9 +178,9 @@ const AvatarSettings = () => {
|
||||
i18nKey='copy_message_etherscan'
|
||||
values={{ copy: hasCopied ? t('copied') : t('copy') }}
|
||||
components={{
|
||||
1: <button onClick={copyMessageToSign} />,
|
||||
1: <button key='copy-message-button' onClick={copyMessageToSign} />,
|
||||
// eslint-disable-next-line
|
||||
2: <a href='https://etherscan.io/verifiedSignatures' target='_blank' rel='noopener noreferrer' />,
|
||||
2: <a key='etherscan-link' href='https://etherscan.io/verifiedSignatures' target='_blank' rel='noopener noreferrer' />,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
+5
-1
@@ -24,4 +24,8 @@
|
||||
.button:hover {
|
||||
color: var(--button-desktop-text-color-hover);
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.notBlocked {
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
+1
-1
@@ -50,7 +50,7 @@ const BlockedAddressesSetting = () => {
|
||||
</ul>
|
||||
</>
|
||||
) : (
|
||||
t('you_have_not_blocked')
|
||||
<div className={styles.notBlocked}>{t('you_have_not_blocked')}</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -12,7 +12,7 @@ import useSpecialThemeStore from '../../../stores/use-special-theme-store';
|
||||
import { isChristmas } from '../../../lib/utils/time-utils';
|
||||
import Version from '../../version';
|
||||
|
||||
const commitRef = process.env.REACT_APP_COMMIT_REF;
|
||||
const commitRef = process.env.VITE_COMMIT_REF;
|
||||
const isElectron = window.isElectron === true;
|
||||
|
||||
const CheckForUpdates = () => {
|
||||
|
||||
+5
-1
@@ -24,4 +24,8 @@
|
||||
.button:hover {
|
||||
color: var(--button-desktop-text-color-hover);
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.notSubscribed {
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ const SubscriptionsSetting = () => {
|
||||
</ul>
|
||||
</>
|
||||
) : (
|
||||
t('not_subscribed_to_any_board')
|
||||
<div className={styles.notSubscribed}>{t('not_subscribed_to_any_board')}</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -52,13 +52,13 @@ const SubplebbitStats = () => {
|
||||
<Trans
|
||||
i18nKey='board_stats_hour'
|
||||
values={{ userCount: stats.hourActiveUserCount ?? '?', postCount: stats.hourPostCount ?? '?' }}
|
||||
components={{ 1: <span className={styles.statValue} /> }}
|
||||
components={{ 1: <span key='hour-stat-value' className={styles.statValue} /> }}
|
||||
/>
|
||||
{' / '}
|
||||
<Trans
|
||||
i18nKey='board_stats_day'
|
||||
values={{ userCount: stats.dayActiveUserCount ?? '?', postCount: stats.dayPostCount ?? '?' }}
|
||||
components={{ 1: <span className={styles.statValue} /> }}
|
||||
components={{ 1: <span key='day-stat-value' className={styles.statValue} /> }}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -67,13 +67,13 @@ const SubplebbitStats = () => {
|
||||
<Trans
|
||||
i18nKey='board_stats_week'
|
||||
values={{ userCount: stats.weekActiveUserCount ?? '?', postCount: stats.weekPostCount ?? '?' }}
|
||||
components={{ 1: <span className={styles.statValue} /> }}
|
||||
components={{ 1: <span key='week-stat-value' className={styles.statValue} /> }}
|
||||
/>
|
||||
{' / '}
|
||||
<Trans
|
||||
i18nKey='board_stats_month'
|
||||
values={{ userCount: stats.monthActiveUserCount ?? '?', postCount: stats.monthPostCount ?? '?' }}
|
||||
components={{ 1: <span className={styles.statValue} /> }}
|
||||
components={{ 1: <span key='month-stat-value' className={styles.statValue} /> }}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -84,7 +84,7 @@ const SubplebbitStats = () => {
|
||||
<Trans
|
||||
i18nKey='board_stats_all'
|
||||
values={{ userCount: stats.allActiveUserCount ?? '?', postCount: stats.allPostCount ?? '?' }}
|
||||
components={{ 1: <span className={styles.statValue} /> }}
|
||||
components={{ 1: <span key='all-stat-value' className={styles.statValue} /> }}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { Link, useLocation, useNavigate, useParams } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Plebbit from '@plebbit/plebbit-js/dist/browser/index.js';
|
||||
import { useAccount, useAccountComment } from '@plebbit/plebbit-react-hooks';
|
||||
import { useAccount, useAccountComment, useAccountSubplebbits } from '@plebbit/plebbit-react-hooks';
|
||||
import { isAllView, isCatalogView, isSubscriptionsView } from '../../lib/utils/view-utils';
|
||||
import { useDefaultSubplebbitAddresses } from '../../hooks/use-default-subplebbits';
|
||||
import { useAutoSubscribe } from '../../hooks/use-auto-subscribe';
|
||||
@@ -79,10 +79,20 @@ const TopBarDesktop = () => {
|
||||
|
||||
const subscriptions = account?.subscriptions;
|
||||
|
||||
const { accountSubplebbits } = useAccountSubplebbits();
|
||||
const accountSubplebbitAddresses = Object.keys(accountSubplebbits);
|
||||
|
||||
return (
|
||||
<div className={styles.boardNavDesktop}>
|
||||
<span className={styles.boardList}>
|
||||
[<Link to='/p/all'>all</Link> / <Link to='/p/subscriptions'>subscriptions</Link>]{' '}
|
||||
[<Link to='/p/all'>all</Link> / <Link to='/p/subscriptions'>subscriptions</Link>
|
||||
{accountSubplebbitAddresses.length > 0 && (
|
||||
<>
|
||||
{' '}
|
||||
/ <Link to='/p/mod'>mod</Link>
|
||||
</>
|
||||
)}
|
||||
]{' '}
|
||||
{subscriptions?.length > 0 && (
|
||||
<>
|
||||
[
|
||||
@@ -137,11 +147,15 @@ const TopBarMobile = ({ subplebbitAddress }: { subplebbitAddress: string }) => {
|
||||
const isInSubscriptionsView = isSubscriptionsView(location.pathname, useParams());
|
||||
const selectValue = isInAllView ? 'all' : isInSubscriptionsView ? 'subscriptions' : subplebbitAddress;
|
||||
|
||||
const { accountSubplebbits } = useAccountSubplebbits();
|
||||
const accountSubplebbitAddresses = Object.keys(accountSubplebbits);
|
||||
|
||||
const boardSelect = (
|
||||
<select value={selectValue} onChange={(e) => navigate(`/p/${e.target.value}${isInCatalogView ? '/catalog' : ''}`)}>
|
||||
{!currentSubplebbitIsInList && subplebbitAddress && <option value={subplebbitAddress}>{displaySubplebbitAddress}</option>}
|
||||
<option value='all'>all</option>
|
||||
<option value='subscriptions'>subscriptions</option>
|
||||
{accountSubplebbitAddresses.length > 0 && <option value='mod'>mod</option>}
|
||||
{subplebbitAddresses.map((address: any, index: number) => {
|
||||
const subplebbitAddress = address?.includes('.') ? address : Plebbit.getShortAddress(address);
|
||||
return (
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import packageJson from '../../../package.json';
|
||||
|
||||
const { version } = packageJson;
|
||||
const commitRef = process.env.REACT_APP_COMMIT_REF;
|
||||
const commitRef = process.env.VITE_COMMIT_REF;
|
||||
const isElectron = window.isElectron === true;
|
||||
|
||||
const Version = () => {
|
||||
@@ -11,11 +11,11 @@ const Version = () => {
|
||||
return (
|
||||
<>
|
||||
<a
|
||||
href={commitRef ? `https://github.com/plebbit/seedit/commit/${commitRef}` : `https://github.com/plebbit/seedit/releases/tag/v${version}`}
|
||||
href={commitRef ? `https://github.com/plebbit/plebchan/commit/${commitRef}` : `https://github.com/plebbit/plebchan/releases/tag/v${version}`}
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
>
|
||||
v{commitRef ? '-dev' : version}
|
||||
plebchan v{commitRef ? `${version}-dev (#${commitRef.slice(0, 7)})` : version}
|
||||
</a>
|
||||
{isElectron && (
|
||||
<>
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
// plebchan shouldn't consider 'vulgar' or 'anti' as nsfw tags, unlike more sfw-oriented clients
|
||||
export const nsfwTags = ['adult', 'gore'];
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useAccount, useSubplebbit } from '@plebbit/plebbit-react-hooks';
|
||||
import { useAccount } from '@plebbit/plebbit-react-hooks';
|
||||
import useSubplebbitsStore from '@plebbit/plebbit-react-hooks/dist/stores/subplebbits';
|
||||
import useAnonModeStore from '../stores/use-anon-mode-store';
|
||||
import useAnonMode from './use-anon-mode';
|
||||
|
||||
@@ -12,7 +13,8 @@ interface AuthorPrivilegesProps {
|
||||
const useAuthorPrivileges = ({ commentAuthorAddress, subplebbitAddress, postCid }: AuthorPrivilegesProps) => {
|
||||
const account = useAccount();
|
||||
const accountAuthorAddress = account?.author?.address;
|
||||
const { roles } = useSubplebbit({ subplebbitAddress }) || {};
|
||||
const subplebbit = useSubplebbitsStore((state) => state.subplebbits[subplebbitAddress]);
|
||||
const { roles } = subplebbit || {};
|
||||
const { getAddressSigner, getThreadSigner } = useAnonModeStore();
|
||||
const { anonMode } = useAnonMode(postCid);
|
||||
|
||||
|
||||
@@ -33,8 +33,8 @@ const useCatalogFeedRows = (columnCount: number, feed: any, isFeedLoaded: boolea
|
||||
|
||||
// show account comments instantly in the feed once published (cid defined), instead of waiting for the feed to update
|
||||
const filteredComments = accountComments.filter((comment) => {
|
||||
const { cid, deleted, link, postCid, removed, state, subplebbitAddress, timestamp } = comment || {};
|
||||
const commentMediaInfo = getCommentMediaInfo(comment);
|
||||
const { cid, deleted, link, postCid, removed, state, subplebbitAddress, timestamp, thumbnailUrl, linkWidth, linkHeight } = comment || {};
|
||||
const commentMediaInfo = getCommentMediaInfo(link, thumbnailUrl, linkWidth, linkHeight);
|
||||
const isMediaShowed = getHasThumbnail(commentMediaInfo, link);
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { useLocation, useParams } from 'react-router-dom';
|
||||
import { Comment } from '@plebbit/plebbit-react-hooks';
|
||||
import { getCommentMediaInfo, fetchWebpageThumbnailIfNeeded } from '../lib/utils/media-utils';
|
||||
import { isPendingPostView, isPostPageView } from '../lib/utils/view-utils';
|
||||
|
||||
export const useCommentMediaInfo = (comment: Comment) => {
|
||||
export const useCommentMediaInfo = (link: string, thumbnailUrl: string, linkWidth: number, linkHeight: number) => {
|
||||
const location = useLocation();
|
||||
const params = useParams();
|
||||
const isInPostPageView = isPostPageView(location.pathname, params);
|
||||
@@ -12,7 +11,7 @@ export const useCommentMediaInfo = (comment: Comment) => {
|
||||
|
||||
// some sites have CORS access, so the thumbnail can be fetched client-side, which is helpful if subplebbit.settings.fetchThumbnailUrls is false
|
||||
const fetchThumbnail = useCallback(async () => {
|
||||
let commentMediaInfo = getCommentMediaInfo(comment);
|
||||
let commentMediaInfo = getCommentMediaInfo(link, thumbnailUrl, linkWidth, linkHeight);
|
||||
if (commentMediaInfo?.type === 'webpage' && !commentMediaInfo.thumbnail) {
|
||||
const newMediaInfo = await fetchWebpageThumbnailIfNeeded(commentMediaInfo);
|
||||
// Fetch the dimensions of the thumbnail
|
||||
@@ -30,7 +29,7 @@ export const useCommentMediaInfo = (comment: Comment) => {
|
||||
commentMediaInfo = newMediaInfo;
|
||||
}
|
||||
return commentMediaInfo;
|
||||
}, [comment]);
|
||||
}, [link, thumbnailUrl, linkWidth, linkHeight]);
|
||||
|
||||
useEffect(() => {
|
||||
// don't fetch in feed view, it displaces the posts
|
||||
@@ -39,5 +38,5 @@ export const useCommentMediaInfo = (comment: Comment) => {
|
||||
}
|
||||
}, [fetchThumbnail, isInPostPageView, isInPendingPostView]);
|
||||
|
||||
return getCommentMediaInfo(comment);
|
||||
return getCommentMediaInfo(link, thumbnailUrl, linkWidth, linkHeight);
|
||||
};
|
||||
|
||||
@@ -2,8 +2,8 @@ import { useMemo } from 'react';
|
||||
import { useLocation, useParams } from 'react-router-dom';
|
||||
import useThemeStore from '../stores/use-theme-store';
|
||||
import { useDefaultSubplebbits } from './use-default-subplebbits';
|
||||
import { isAllView, isHomeView, isNotFoundView, isPendingPostView, isSubscriptionsView } from '../lib/utils/view-utils';
|
||||
import { nsfwTags } from '../views/home/home';
|
||||
import { isAllView, isHomeView, isNotFoundView, isPendingPostView, isSubscriptionsView, isModView } from '../lib/utils/view-utils';
|
||||
import { nsfwTags } from '../constants/nsfwTags';
|
||||
import { useAccountComment } from '@plebbit/plebbit-react-hooks';
|
||||
|
||||
const useInitialTheme = (pendingPostSubplebbitAddress?: string) => {
|
||||
@@ -19,6 +19,7 @@ const useInitialTheme = (pendingPostSubplebbitAddress?: string) => {
|
||||
const isInNotFoundView = isNotFoundView(location.pathname, params);
|
||||
const isInAllView = isAllView(location.pathname);
|
||||
const isInSubscriptionsView = isSubscriptionsView(location.pathname, params);
|
||||
const isInModView = isModView(location.pathname);
|
||||
const isInPendingPostView = isPendingPostView(location.pathname, params);
|
||||
|
||||
const initialTheme = useMemo(() => {
|
||||
@@ -36,7 +37,7 @@ const useInitialTheme = (pendingPostSubplebbitAddress?: string) => {
|
||||
} else {
|
||||
theme = currentTheme || 'yotsuba';
|
||||
}
|
||||
} else if (isInAllView || isInSubscriptionsView) {
|
||||
} else if (isInAllView || isInSubscriptionsView || isInModView) {
|
||||
theme = getTheme('sfw', false) || 'yotsuba-b'; // Add 'false' parameter
|
||||
} else if (isInHomeView || isInNotFoundView) {
|
||||
theme = 'yotsuba';
|
||||
@@ -54,6 +55,7 @@ const useInitialTheme = (pendingPostSubplebbitAddress?: string) => {
|
||||
isInPendingPostView,
|
||||
isInAllView,
|
||||
isInSubscriptionsView,
|
||||
isInModView,
|
||||
isInHomeView,
|
||||
isInNotFoundView,
|
||||
paramsSubplebbitAddress,
|
||||
|
||||
@@ -22,10 +22,10 @@ const usePopularPosts = (subplebbits: Subplebbit[]) => {
|
||||
|
||||
if (subplebbit?.posts?.pages?.hot?.comments) {
|
||||
for (const post of Object.values(subplebbit.posts.pages.hot.comments as Comment)) {
|
||||
const { deleted, link, locked, pinned, removed, replyCount, timestamp } = post;
|
||||
const { deleted, link, linkHeight, linkWidth, locked, pinned, removed, replyCount, thumbnailUrl, timestamp } = post;
|
||||
|
||||
try {
|
||||
const commentMediaInfo = getCommentMediaInfo(post);
|
||||
const commentMediaInfo = getCommentMediaInfo(link, thumbnailUrl, linkWidth, linkHeight);
|
||||
const hasThumbnail = getHasThumbnail(commentMediaInfo, link);
|
||||
|
||||
if (
|
||||
|
||||
+10
-14
@@ -1,10 +1,10 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useLocation, useParams } from 'react-router-dom';
|
||||
import { isAllView, isSubscriptionsView } from '../lib/utils/view-utils';
|
||||
import { isAllView, isSubscriptionsView, isModView } from '../lib/utils/view-utils';
|
||||
import useThemeStore from '../stores/use-theme-store';
|
||||
import { useDefaultSubplebbits } from './use-default-subplebbits';
|
||||
import useInitialTheme from './use-initial-theme';
|
||||
import { nsfwTags } from '../views/home/home';
|
||||
import { nsfwTags } from '../constants/nsfwTags';
|
||||
import { useAccountComment } from '@plebbit/plebbit-react-hooks';
|
||||
import useSpecialThemeStore from '../stores/use-special-theme-store';
|
||||
import { isChristmas } from '../lib/utils/time-utils';
|
||||
@@ -37,26 +37,24 @@ const useTheme = (): [string, (theme: string) => void] => {
|
||||
|
||||
const isInAllView = isAllView(location.pathname);
|
||||
const isInSubscriptionsView = isSubscriptionsView(location.pathname, params);
|
||||
const isInModView = isModView(location.pathname);
|
||||
const subplebbitAddress = params?.subplebbitAddress || pendingPostSubplebbitAddress;
|
||||
|
||||
// Check for Christmas and initialize special theme if needed
|
||||
useEffect(() => {
|
||||
const isChristmasTime = isChristmas();
|
||||
const subplebbitAddress = params?.subplebbitAddress || pendingPostSubplebbitAddress;
|
||||
|
||||
if (isChristmasTime && isEnabled === null && subplebbitAddress && !isInAllView && !isInSubscriptionsView) {
|
||||
if (isChristmasTime && isEnabled === null && subplebbitAddress && !isInAllView && !isInSubscriptionsView && !isInModView) {
|
||||
setIsEnabled(true);
|
||||
setCurrentTheme('tomorrow');
|
||||
updateThemeClass('tomorrow');
|
||||
} else if (!isChristmasTime && isEnabled) {
|
||||
setIsEnabled(false);
|
||||
}
|
||||
}, [isEnabled, setIsEnabled, params, pendingPostSubplebbitAddress, location.pathname, isInAllView, isInSubscriptionsView]);
|
||||
}, [isEnabled, setIsEnabled, params, pendingPostSubplebbitAddress, location.pathname, isInAllView, isInSubscriptionsView, isInModView, subplebbitAddress]);
|
||||
|
||||
const getCurrentTheme = useCallback(() => {
|
||||
const { isEnabled } = useSpecialThemeStore.getState();
|
||||
const subplebbitAddress = params?.subplebbitAddress || pendingPostSubplebbitAddress;
|
||||
const isInAllView = isAllView(location.pathname);
|
||||
const isInSubscriptionsView = isSubscriptionsView(location.pathname, params);
|
||||
|
||||
// Always use yotsuba for home page
|
||||
if (location.pathname === '/') {
|
||||
@@ -69,7 +67,7 @@ const useTheme = (): [string, (theme: string) => void] => {
|
||||
}
|
||||
|
||||
let storedTheme = null;
|
||||
if (isInAllView || isInSubscriptionsView) {
|
||||
if (isInAllView || isInSubscriptionsView || isInModView) {
|
||||
storedTheme = getTheme('sfw', false);
|
||||
} else if (subplebbitAddress) {
|
||||
const subplebbit = subplebbits.find((s) => s.address === subplebbitAddress);
|
||||
@@ -81,6 +79,7 @@ const useTheme = (): [string, (theme: string) => void] => {
|
||||
}
|
||||
|
||||
return storedTheme || initialTheme;
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [location.pathname, params, getTheme, subplebbits, initialTheme, pendingPostSubplebbitAddress]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -97,11 +96,7 @@ const useTheme = (): [string, (theme: string) => void] => {
|
||||
|
||||
const setSubplebbitTheme = useCallback(
|
||||
async (newTheme: string) => {
|
||||
const subplebbitAddress = params?.subplebbitAddress || pendingPostSubplebbitAddress;
|
||||
const isInAllView = isAllView(location.pathname);
|
||||
const isInSubscriptionsView = isSubscriptionsView(location.pathname, params);
|
||||
|
||||
if (isInAllView || isInSubscriptionsView) {
|
||||
if (isInAllView || isInSubscriptionsView || isInModView) {
|
||||
await setThemeStore('sfw', newTheme);
|
||||
} else if (subplebbitAddress) {
|
||||
const subplebbit = subplebbits.find((s) => s.address === subplebbitAddress);
|
||||
@@ -115,6 +110,7 @@ const useTheme = (): [string, (theme: string) => void] => {
|
||||
setCurrentTheme(newTheme);
|
||||
updateThemeClass(newTheme);
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[location.pathname, params, setThemeStore, subplebbits, pendingPostSubplebbitAddress],
|
||||
);
|
||||
|
||||
|
||||
+2
-2
@@ -53,11 +53,11 @@ hr {
|
||||
}
|
||||
|
||||
.yellowOfflineIcon {
|
||||
background-image: url('/public/assets/icons/offline-blue.png');
|
||||
background-image: url('/assets/icons/offline-blue.png');
|
||||
}
|
||||
|
||||
.redOfflineIcon {
|
||||
background-image: url('/public/assets/icons/offline-red.png');
|
||||
background-image: url('/assets/icons/offline-red.png');
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import './polyfills.js';
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import App from './app';
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import localForageLru from '@plebbit/plebbit-react-hooks/dist/lib/localforage-lru/index.js';
|
||||
import { Comment } from '@plebbit/plebbit-react-hooks';
|
||||
import extName from 'ext-name';
|
||||
import { canEmbed } from '../../components/embed';
|
||||
import memoize from 'memoizee';
|
||||
@@ -13,7 +12,8 @@ export interface CommentMediaInfo {
|
||||
thumbnailWidth?: number;
|
||||
thumbnailHeight?: number;
|
||||
patternThumbnailUrl?: string;
|
||||
post?: Comment;
|
||||
linkWidth?: number;
|
||||
linkHeight?: number;
|
||||
}
|
||||
|
||||
export const getDisplayMediaInfoType = (type: string, t: any) => {
|
||||
@@ -35,19 +35,20 @@ export const getDisplayMediaInfoType = (type: string, t: any) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const getHasThumbnail = (commentMediaInfo: CommentMediaInfo | undefined, link: string | undefined): boolean => {
|
||||
const iframeThumbnail = commentMediaInfo?.patternThumbnailUrl || commentMediaInfo?.thumbnail;
|
||||
return link &&
|
||||
commentMediaInfo &&
|
||||
(commentMediaInfo.type === 'image' ||
|
||||
commentMediaInfo.type === 'video' ||
|
||||
commentMediaInfo.type === 'audio' ||
|
||||
commentMediaInfo.type === 'gif' ||
|
||||
(commentMediaInfo.type === 'webpage' && commentMediaInfo.thumbnail) ||
|
||||
(commentMediaInfo.type === 'iframe' && iframeThumbnail))
|
||||
? true
|
||||
: false;
|
||||
};
|
||||
export const getHasThumbnail = memoize(
|
||||
(commentMediaInfo: CommentMediaInfo | undefined, link: string | undefined): boolean => {
|
||||
if (!link || !commentMediaInfo) return false;
|
||||
|
||||
const { type, thumbnail, patternThumbnailUrl } = commentMediaInfo;
|
||||
|
||||
if (type === 'image' || type === 'video' || type === 'audio' || type === 'gif') return true;
|
||||
if (type === 'webpage' && thumbnail) return true;
|
||||
if (type === 'iframe' && (patternThumbnailUrl || thumbnail)) return true;
|
||||
|
||||
return false;
|
||||
},
|
||||
{ max: 1000 },
|
||||
);
|
||||
|
||||
const getYouTubeVideoId = (url: URL): string | null => {
|
||||
if (url.host.includes('youtu.be')) {
|
||||
@@ -182,55 +183,61 @@ const fetchWebpageThumbnail = async (url: string): Promise<string | undefined> =
|
||||
}
|
||||
};
|
||||
|
||||
export const getCommentMediaInfo = (comment: Comment): CommentMediaInfo | undefined => {
|
||||
if (!comment?.thumbnailUrl && !comment?.link) {
|
||||
export const getCommentMediaInfo = (link: string, thumbnailUrl: string, linkWidth: number, linkHeight: number): CommentMediaInfo | undefined => {
|
||||
if (!thumbnailUrl && !link) {
|
||||
return;
|
||||
}
|
||||
const linkInfo = comment.link ? getLinkMediaInfo(comment.link) : undefined;
|
||||
const linkInfo = link ? getLinkMediaInfo(link) : undefined;
|
||||
if (linkInfo) {
|
||||
return {
|
||||
...linkInfo,
|
||||
thumbnail: comment.thumbnailUrl || linkInfo.thumbnail,
|
||||
post: comment,
|
||||
thumbnail: thumbnailUrl || linkInfo.thumbnail,
|
||||
linkWidth,
|
||||
linkHeight,
|
||||
};
|
||||
}
|
||||
return;
|
||||
};
|
||||
|
||||
export const getMediaDimensions = (commentMediaInfo: CommentMediaInfo | undefined): string => {
|
||||
if (!commentMediaInfo) return '';
|
||||
const EMBED_DIMENSIONS = {
|
||||
'youtube.com': '800x450',
|
||||
'youtu.be': '800x450',
|
||||
'instagram.com': '360x420',
|
||||
'reddit.com': '500x520',
|
||||
'tiktok.com': '400x780',
|
||||
'x.com': '550x580',
|
||||
'twitter.com': '550x580',
|
||||
'soundcloud.com': '700x166',
|
||||
} as const;
|
||||
|
||||
const { type, url, post } = commentMediaInfo;
|
||||
export const getMediaDimensions = memoize(
|
||||
(commentMediaInfo: CommentMediaInfo | undefined): string => {
|
||||
if (!commentMediaInfo) return '';
|
||||
|
||||
if (type === 'iframe' && url) {
|
||||
const embedUrl = new URL(url);
|
||||
if (canEmbed(embedUrl)) {
|
||||
// hardcoded dimensions from embed.module.css
|
||||
if (embedUrl.hostname.includes('youtube.com') || embedUrl.hostname.includes('youtu.be')) {
|
||||
return '800x450';
|
||||
} else if (embedUrl.hostname.includes('instagram.com')) {
|
||||
return '360x420';
|
||||
} else if (embedUrl.hostname.includes('reddit.com')) {
|
||||
return '500x520';
|
||||
} else if (embedUrl.hostname.includes('tiktok.com')) {
|
||||
return '400x780';
|
||||
} else if (embedUrl.hostname.includes('x.com') || embedUrl.hostname.includes('twitter.com')) {
|
||||
return '550x580';
|
||||
} else if (embedUrl.hostname.includes('soundcloud.com')) {
|
||||
return '700x166';
|
||||
const { type, url, linkWidth, linkHeight } = commentMediaInfo;
|
||||
|
||||
if (type === 'iframe' && url) {
|
||||
const embedUrl = new URL(url);
|
||||
if (canEmbed(embedUrl)) {
|
||||
const hostname = embedUrl.hostname;
|
||||
for (const [site, dimensions] of Object.entries(EMBED_DIMENSIONS)) {
|
||||
if (hostname.includes(site)) {
|
||||
return dimensions;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (type === 'audio') {
|
||||
return '700x240';
|
||||
} else if (type === 'image' || type === 'video' || type === 'gif') {
|
||||
if (linkWidth && linkHeight) {
|
||||
return `${linkWidth}x${linkHeight}`;
|
||||
}
|
||||
}
|
||||
} else if (type === 'audio') {
|
||||
return '700x240'; // hardcoded dimensions from embed.module.css
|
||||
} else if (type === 'image' || type === 'video' || type === 'gif') {
|
||||
// media dimensions calculated by API
|
||||
if (post?.linkWidth && post?.linkHeight) {
|
||||
return `${post.linkWidth}x${post.linkHeight}`;
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
};
|
||||
return '';
|
||||
},
|
||||
{ max: 1000 },
|
||||
);
|
||||
|
||||
const thumbnailUrlsDb = localForageLru.createInstance({ name: 'plebchanThumbnailUrls', size: 500 });
|
||||
|
||||
|
||||
@@ -42,6 +42,10 @@ export const isHomeView = (pathname: string): boolean => {
|
||||
return pathname === '/';
|
||||
};
|
||||
|
||||
export const isModView = (pathname: string): boolean => {
|
||||
return pathname === `/p/mod` || pathname.startsWith(`/p/mod/`);
|
||||
};
|
||||
|
||||
export const isPendingPostView = (pathname: string, params: ParamsType): boolean => {
|
||||
return pathname === `/profile/${params.accountCommentIndex}` || pathname === `/profile/${params.accountCommentIndex}/settings`;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
// Polyfills for Node.js built-ins
|
||||
import { Buffer } from 'buffer';
|
||||
import process from 'process';
|
||||
import 'isomorphic-fetch';
|
||||
|
||||
window.Buffer = Buffer;
|
||||
window.process = process;
|
||||
window.global = window;
|
||||
|
||||
// For ethers.js
|
||||
window.process.version = ''; // Fake Node.js version
|
||||
window.process.env = window.process.env || {};
|
||||
|
||||
// Add any missing fetch polyfill
|
||||
if (!window.fetch) {
|
||||
console.warn('Fetch API is not available, using polyfill');
|
||||
}
|
||||
|
||||
// For crypto support
|
||||
if (typeof window.crypto === 'undefined') {
|
||||
window.crypto = {};
|
||||
}
|
||||
if (typeof window.crypto.getRandomValues === 'undefined') {
|
||||
window.crypto.getRandomValues = function (array) {
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
array[i] = Math.floor(Math.random() * 256);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -53,7 +53,9 @@ const useCatalogFiltersStore = create(
|
||||
// filterItems
|
||||
} = state;
|
||||
|
||||
const hasThumbnail = getHasThumbnail(getCommentMediaInfo(comment), comment?.link);
|
||||
const { link, linkHeight, linkWidth, thumbnailUrl } = comment || {};
|
||||
|
||||
const hasThumbnail = getHasThumbnail(getCommentMediaInfo(link, thumbnailUrl, linkWidth, linkHeight), link);
|
||||
// const title = comment?.title?.toLowerCase() || '';
|
||||
// const content = comment?.content?.toLowerCase() || '';
|
||||
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import { create } from 'zustand';
|
||||
import useSelectedTextStore from './use-selected-text-store';
|
||||
|
||||
interface ReplyModalState {
|
||||
showReplyModal: boolean;
|
||||
activeCid: string | null;
|
||||
threadCid: string | null;
|
||||
subplebbitAddress: string | null;
|
||||
scrollY: number;
|
||||
closeModal: () => void;
|
||||
openReplyModal: (parentCid: string, postCid: string, subplebbitAddress: string) => void;
|
||||
}
|
||||
|
||||
const useReplyModalStore = create<ReplyModalState>((set, get) => ({
|
||||
showReplyModal: false,
|
||||
activeCid: null,
|
||||
threadCid: null,
|
||||
subplebbitAddress: null,
|
||||
scrollY: 0,
|
||||
|
||||
closeModal: () => {
|
||||
// Reset selected text if you're using that store
|
||||
useSelectedTextStore.getState().resetSelectedText();
|
||||
set({
|
||||
showReplyModal: false,
|
||||
activeCid: null,
|
||||
});
|
||||
},
|
||||
|
||||
openReplyModal: (parentCid, postCid, subplebbitAddress) => {
|
||||
// Get selected text
|
||||
const text = document.getSelection()?.toString();
|
||||
if (text) {
|
||||
useSelectedTextStore.getState().setSelectedText(`>${text}\n`);
|
||||
}
|
||||
|
||||
// Handle mobile scrollY
|
||||
const isMobile = window.innerWidth <= 768; // Simple check, adjust as needed
|
||||
const scrollY = isMobile ? window.scrollY : 0;
|
||||
|
||||
// Don't update if already open with different parent
|
||||
if (get().activeCid && get().activeCid !== parentCid) {
|
||||
return;
|
||||
}
|
||||
|
||||
set({
|
||||
activeCid: parentCid,
|
||||
threadCid: postCid,
|
||||
showReplyModal: true,
|
||||
subplebbitAddress,
|
||||
scrollY,
|
||||
});
|
||||
},
|
||||
}));
|
||||
|
||||
export default useReplyModalStore;
|
||||
+43
-43
@@ -20,7 +20,7 @@
|
||||
|
||||
/* body */
|
||||
--body-background-base-color: #ffe;
|
||||
--body-background-pattern-image: url("/public/assets/background-fade.png");
|
||||
--body-background-pattern-image: url("/assets/background-fade.png");
|
||||
--body-font-color: maroon;
|
||||
--body-font-family: arial, helvetica, sans-serif;
|
||||
--body-font-size: 10pt;
|
||||
@@ -49,7 +49,7 @@
|
||||
/* desktop buttons */
|
||||
--button-desktop-text-color: #00e;
|
||||
--button-desktop-text-color-hover: red;
|
||||
--close-button-background-image: url("/public/assets/buttons/cross-red.png");
|
||||
--close-button-background-image: url("/assets/buttons/cross-red.png");
|
||||
|
||||
/* horizontal rule */
|
||||
--hr-border: none;
|
||||
@@ -90,7 +90,7 @@
|
||||
--button-font-size-mobile: 10pt;
|
||||
--button-font-weight-mobile: 700;
|
||||
--button-text-color-mobile: #800;
|
||||
--button-background-image-mobile: url("/public/assets/buttons/button-fade.png");
|
||||
--button-background-image-mobile: url("/assets/buttons/button-fade.png");
|
||||
--button-background-color-mobile: #f0e0d6;
|
||||
--button-border-mobile: 1px solid #c0a69d;
|
||||
|
||||
@@ -98,8 +98,8 @@
|
||||
--post-capcode-mod-text-color: purple;
|
||||
|
||||
/* post desktop */
|
||||
--post-hide-button-background-image: url("/public/assets/buttons/minus-red.png");
|
||||
--post-unhide-button-background-image: url("/public/assets/buttons/plus-red.png");
|
||||
--post-hide-button-background-image: url("/assets/buttons/minus-red.png");
|
||||
--post-unhide-button-background-image: url("/assets/buttons/plus-red.png");
|
||||
--post-subject-text-color: #cc1105;
|
||||
--post-subject-font-weight: 700;
|
||||
--post-name-text-color: #117743;
|
||||
@@ -190,9 +190,9 @@
|
||||
--settings-modal-border-right: 1px solid #d9bfb7;
|
||||
--settings-modal-border-bottom: 1px solid #d9bfb7;
|
||||
--settings-modal-border-left: none;
|
||||
--settings-modal-close-button-background-image: url("/public/assets/buttons/cross-red.png");
|
||||
--settings-modal-show-button-background-image: url("/public/assets/buttons/plus-red.png");
|
||||
--settings-modal-hide-button-background-image: url("/public/assets/buttons/minus-red.png");
|
||||
--settings-modal-close-button-background-image: url("/assets/buttons/cross-red.png");
|
||||
--settings-modal-show-button-background-image: url("/assets/buttons/plus-red.png");
|
||||
--settings-modal-hide-button-background-image: url("/assets/buttons/minus-red.png");
|
||||
|
||||
/* stats */
|
||||
--stats-font-size: 11px;
|
||||
@@ -234,7 +234,7 @@
|
||||
|
||||
/* body */
|
||||
--body-background-base-color: #eef2ff;
|
||||
--body-background-pattern-image: url("/public/assets/background-fade-blue.png");
|
||||
--body-background-pattern-image: url("/assets/background-fade-blue.png");
|
||||
--body-font-color: #000;
|
||||
--body-font-family: arial, helvetica, sans-serif;
|
||||
--body-font-size: 10pt;
|
||||
@@ -262,7 +262,7 @@
|
||||
/* desktop buttons */
|
||||
--button-desktop-text-color: #34345c;
|
||||
--button-desktop-text-color-hover: #d00;
|
||||
--close-button-background-image: url("/public/assets/buttons/cross-blue.png");
|
||||
--close-button-background-image: url("/assets/buttons/cross-blue.png");
|
||||
|
||||
/* footer */
|
||||
--footer-item-background-color-desktop: #eef2ff;
|
||||
@@ -302,7 +302,7 @@
|
||||
--button-font-size-mobile: 10pt;
|
||||
--button-font-weight-mobile: 700;
|
||||
--button-text-color-mobile: #34345c;
|
||||
--button-background-image-mobile: url("/public/assets/buttons/button-fade-blue.png");
|
||||
--button-background-image-mobile: url("/assets/buttons/button-fade-blue.png");
|
||||
--button-background-color-mobile: #d6daf0;
|
||||
--button-border-mobile: 1px solid #b7c5d9;
|
||||
|
||||
@@ -310,8 +310,8 @@
|
||||
--post-capcode-mod-text-color: purple;
|
||||
|
||||
/* post desktop */
|
||||
--post-hide-button-background-image: url("/public/assets/buttons/minus-blue.png");
|
||||
--post-unhide-button-background-image: url("/public/assets/buttons/plus-blue.png");
|
||||
--post-hide-button-background-image: url("/assets/buttons/minus-blue.png");
|
||||
--post-unhide-button-background-image: url("/assets/buttons/plus-blue.png");
|
||||
--post-subject-text-color: #0f0c5d;
|
||||
--post-subject-font-weight: 700;
|
||||
--post-name-text-color: #117743;
|
||||
@@ -402,9 +402,9 @@
|
||||
--settings-modal-border-bottom: 1px solid #b7c5d9;
|
||||
--settings-modal-border-left: none;
|
||||
--settings-modal-header-border-bottom: 1px solid #b7c5d9;
|
||||
--settings-modal-close-button-background-image: url("/public/assets/buttons/cross-blue.png");
|
||||
--settings-modal-show-button-background-image: url("/public/assets/buttons/plus-blue.png");
|
||||
--settings-modal-hide-button-background-image: url("/public/assets/buttons/minus-blue.png");
|
||||
--settings-modal-close-button-background-image: url("/assets/buttons/cross-blue.png");
|
||||
--settings-modal-show-button-background-image: url("/assets/buttons/plus-blue.png");
|
||||
--settings-modal-hide-button-background-image: url("/assets/buttons/minus-blue.png");
|
||||
|
||||
/* stats */
|
||||
--stats-font-size: 11px;
|
||||
@@ -478,7 +478,7 @@
|
||||
--button-desktop-text-color: #00e;
|
||||
--button-desktop-text-color-hover: red;
|
||||
--button-text-decoration: underline;
|
||||
--close-button-background-image: url("/public/assets/buttons/cross-red.png");
|
||||
--close-button-background-image: url("/assets/buttons/cross-red.png");
|
||||
|
||||
/* footer */
|
||||
--footer-item-background-color-desktop: #fed;
|
||||
@@ -498,7 +498,7 @@
|
||||
--button-font-size-mobile: 11pt;
|
||||
--button-font-weight-mobile: 700;
|
||||
--button-text-color-mobile: #800;
|
||||
--button-background-image-mobile: url("/public/assets/buttons/button-fade.png");
|
||||
--button-background-image-mobile: url("/assets/buttons/button-fade.png");
|
||||
--button-background-color-mobile: #f0e0d6;
|
||||
--button-border-mobile: 1px solid #c0a69d;
|
||||
|
||||
@@ -506,8 +506,8 @@
|
||||
--post-capcode-mod-text-color: purple;
|
||||
|
||||
/* post desktop */
|
||||
--post-hide-button-background-image: url("/public/assets/buttons/minus-red.png");
|
||||
--post-unhide-button-background-image: url("/public/assets/buttons/plus-red.png");
|
||||
--post-hide-button-background-image: url("/assets/buttons/minus-red.png");
|
||||
--post-unhide-button-background-image: url("/assets/buttons/plus-red.png");
|
||||
--post-subject-text-color: #cc1105;
|
||||
--post-subject-font-weight: 700;
|
||||
--post-name-text-color: #117743;
|
||||
@@ -583,9 +583,9 @@
|
||||
--settings-modal-border-bottom: 1px solid rgba(0, 0, 0, 0.20);
|
||||
--settings-modal-border-left: 1px solid rgba(0, 0, 0, 0.20);
|
||||
--settings-modal-header-border-bottom: 1px solid rgba(0,0,0,.2);
|
||||
--settings-modal-close-button-background-image: url("/public/assets/buttons/cross-red.png");
|
||||
--settings-modal-show-button-background-image: url("/public/assets/buttons/plus-red.png");
|
||||
--settings-modal-hide-button-background-image: url("/public/assets/buttons/minus-red.png");
|
||||
--settings-modal-close-button-background-image: url("/assets/buttons/cross-red.png");
|
||||
--settings-modal-show-button-background-image: url("/assets/buttons/plus-red.png");
|
||||
--settings-modal-hide-button-background-image: url("/assets/buttons/minus-red.png");
|
||||
|
||||
/* stats */
|
||||
--stats-font-size: 10pt;
|
||||
@@ -656,7 +656,7 @@
|
||||
--button-desktop-text-color: #34345c;
|
||||
--button-text-decoration: underline;
|
||||
--button-desktop-text-color-hover: #d00;
|
||||
--close-button-background-image: url("/public/assets/buttons/cross-blue.png");
|
||||
--close-button-background-image: url("/assets/buttons/cross-blue.png");
|
||||
|
||||
/* footer */
|
||||
--footer-item-background-color-desktop: #eef2ff;
|
||||
@@ -678,7 +678,7 @@
|
||||
--button-font-size-mobile: 11pt;
|
||||
--button-font-weight-mobile: 700;
|
||||
--button-text-color-mobile: #34345c;
|
||||
--button-background-image-mobile: url("/public/assets/buttons/button-fade-blue.png");
|
||||
--button-background-image-mobile: url("/assets/buttons/button-fade-blue.png");
|
||||
--button-background-color-mobile: #d6daf0;
|
||||
--button-border-mobile: 1px solid #b7c5d9;
|
||||
|
||||
@@ -694,8 +694,8 @@
|
||||
--post-capcode-mod-text-color: purple;
|
||||
|
||||
/* post desktop */
|
||||
--post-hide-button-background-image: url("/public/assets/buttons/minus-blue.png");
|
||||
--post-unhide-button-background-image: url("/public/assets/buttons/plus-blue.png");
|
||||
--post-hide-button-background-image: url("/assets/buttons/minus-blue.png");
|
||||
--post-unhide-button-background-image: url("/assets/buttons/plus-blue.png");
|
||||
--post-subject-text-color: #0f0c5d;
|
||||
--post-subject-font-weight: 700;
|
||||
--post-name-text-color: #117743;
|
||||
@@ -771,9 +771,9 @@
|
||||
--settings-modal-border-bottom: 1px solid rgba(0, 0, 0, 0.20);
|
||||
--settings-modal-border-left: 1px solid rgba(0, 0, 0, 0.20);
|
||||
--settings-modal-header-border-bottom: 1px solid rgba(0,0,0,.2);
|
||||
--settings-modal-close-button-background-image: url("/public/assets/buttons/cross-blue.png");
|
||||
--settings-modal-show-button-background-image: url("/public/assets/buttons/plus-blue.png");
|
||||
--settings-modal-hide-button-background-image: url("/public/assets/buttons/minus-blue.png");
|
||||
--settings-modal-close-button-background-image: url("/assets/buttons/cross-blue.png");
|
||||
--settings-modal-show-button-background-image: url("/assets/buttons/plus-blue.png");
|
||||
--settings-modal-hide-button-background-image: url("/assets/buttons/minus-blue.png");
|
||||
|
||||
/* stats */
|
||||
--stats-font-size: 10pt;
|
||||
@@ -847,7 +847,7 @@
|
||||
/* desktop buttons */
|
||||
--button-desktop-text-color: #81a2be;
|
||||
--button-desktop-text-color-hover: #5f89ac;
|
||||
--close-button-background-image: url("/public/assets/buttons/cross-dark.png");
|
||||
--close-button-background-image: url("/assets/buttons/cross-dark.png");
|
||||
|
||||
/* filter for inputs */
|
||||
--filter80: brightness(85%);
|
||||
@@ -882,7 +882,7 @@
|
||||
--button-font-size-mobile: 10pt;
|
||||
--button-font-weight-mobile: 700;
|
||||
--button-text-color-mobile: #707070;
|
||||
--button-background-image-mobile: url("/public/assets/buttons/button-fade-dark.png");
|
||||
--button-background-image-mobile: url("/assets/buttons/button-fade-dark.png");
|
||||
--button-background-color-mobile: #1b1c1e;
|
||||
--button-border-mobile: 1px solid #282a2e;
|
||||
|
||||
@@ -890,8 +890,8 @@
|
||||
--post-capcode-mod-text-color: #81a2be;
|
||||
|
||||
/* post desktop */
|
||||
--post-hide-button-background-image: url("/public/assets/buttons/minus-dark.png");
|
||||
--post-unhide-button-background-image: url("/public/assets/buttons/plus-dark.png");
|
||||
--post-hide-button-background-image: url("/assets/buttons/minus-dark.png");
|
||||
--post-unhide-button-background-image: url("/assets/buttons/plus-dark.png");
|
||||
--post-subject-text-color: #b294bb;
|
||||
--post-subject-font-weight: 700;
|
||||
--post-name-text-color: #c5c8c6;
|
||||
@@ -993,9 +993,9 @@
|
||||
--settings-modal-border-bottom: 1px solid #111;
|
||||
--settings-modal-border-left: 1px solid #111;
|
||||
--settings-modal-header-border-bottom: 1px solid #111;
|
||||
--settings-modal-close-button-background-image: url("/public/assets/buttons/cross-dark.png");
|
||||
--settings-modal-show-button-background-image: url("/public/assets/buttons/plus-dark.png");
|
||||
--settings-modal-hide-button-background-image: url("/public/assets/buttons/minus-dark.png");
|
||||
--settings-modal-close-button-background-image: url("/assets/buttons/cross-dark.png");
|
||||
--settings-modal-show-button-background-image: url("/assets/buttons/plus-dark.png");
|
||||
--settings-modal-hide-button-background-image: url("/assets/buttons/minus-dark.png");
|
||||
|
||||
/* stats */
|
||||
--stats-font-size: 11px;
|
||||
@@ -1066,7 +1066,7 @@
|
||||
/* desktop buttons */
|
||||
--button-desktop-text-color: #f60;
|
||||
--button-desktop-text-color-hover: #f30;
|
||||
--close-button-background-image: url("/public/assets/buttons/cross-photon.png");
|
||||
--close-button-background-image: url("/assets/buttons/cross-photon.png");
|
||||
|
||||
/* horizontal rule */
|
||||
--hr-border: none;
|
||||
@@ -1095,8 +1095,8 @@
|
||||
--post-capcode-mod-text-color: #f60;
|
||||
|
||||
/* post desktop */
|
||||
--post-hide-button-background-image: url("/public/assets/buttons/minus-photon.png");
|
||||
--post-unhide-button-background-image: url("/public/assets/buttons/plus-photon.png");
|
||||
--post-hide-button-background-image: url("/assets/buttons/minus-photon.png");
|
||||
--post-unhide-button-background-image: url("/assets/buttons/plus-photon.png");
|
||||
--post-subject-text-color: #111;
|
||||
--post-subject-font-weight: 700;
|
||||
--post-name-text-color: #004a99;
|
||||
@@ -1196,9 +1196,9 @@
|
||||
--settings-modal-border-bottom: 1px solid #ccc;
|
||||
--settings-modal-border-left: 1px solid #ccc;
|
||||
--settings-modal-header-border-bottom: 1px solid rgba(0, 0, 0, 0.20);
|
||||
--settings-modal-close-button-background-image: url("/public/assets/buttons/cross-photon.png");
|
||||
--settings-modal-show-button-background-image: url("/public/assets/buttons/plus-photon.png");
|
||||
--settings-modal-hide-button-background-image: url("/public/assets/buttons/minus-photon.png");
|
||||
--settings-modal-close-button-background-image: url("/assets/buttons/cross-photon.png");
|
||||
--settings-modal-show-button-background-image: url("/assets/buttons/plus-photon.png");
|
||||
--settings-modal-hide-button-background-image: url("/assets/buttons/minus-photon.png");
|
||||
|
||||
/* stats */
|
||||
--stats-font-size: 11px;
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
border-image-width: 40px 0px 0px 0px;
|
||||
border-image-outset: 0px 0px 0px 0px;
|
||||
border-image-repeat: repeat repeat;
|
||||
border-image-source: url('/public/assets/garland.png');
|
||||
border-image-source: url('/assets/garland.png');
|
||||
border-style: solid;
|
||||
padding-top: 50px;
|
||||
}
|
||||
+26
-32
@@ -1,30 +1,28 @@
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { Link, useLocation, useParams } from 'react-router-dom';
|
||||
import { Comment, useAccount, useAccountComments, useBlock, useFeed, useSubplebbit } from '@plebbit/plebbit-react-hooks';
|
||||
import { Comment, useAccount, useAccountComments, useAccountSubplebbits, useBlock, useFeed, useSubplebbit } from '@plebbit/plebbit-react-hooks';
|
||||
import { Virtuoso, VirtuosoHandle, StateSnapshot } from 'react-virtuoso';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
import styles from './board.module.css';
|
||||
import { shouldShowSnow } from '../../lib/snow';
|
||||
import { getCommentMediaInfo, getHasThumbnail } from '../../lib/utils/media-utils';
|
||||
import { isAllView, isSubscriptionsView } from '../../lib/utils/view-utils';
|
||||
import { isAllView, isSubscriptionsView, isModView } from '../../lib/utils/view-utils';
|
||||
import { useDefaultSubplebbitAddresses } from '../../hooks/use-default-subplebbits';
|
||||
import { useFeedStateString } from '../../hooks/use-state-string';
|
||||
import useReplyModal from '../../hooks/use-reply-modal';
|
||||
import useTimeFilter from '../../hooks/use-time-filter';
|
||||
import useInterfaceSettingsStore from '../../stores/use-interface-settings-store';
|
||||
import useFeedResetStore from '../../stores/use-feed-reset-store';
|
||||
import useSortingStore from '../../stores/use-sorting-store';
|
||||
import LoadingEllipsis from '../../components/loading-ellipsis';
|
||||
import { Post } from '../post';
|
||||
import ReplyModal from '../../components/reply-modal';
|
||||
import SettingsModal from '../../components/settings-modal';
|
||||
import SubplebbitDescription from '../../components/subplebbit-description';
|
||||
import SubplebbitRules from '../../components/subplebbit-rules';
|
||||
import { Post } from '../post';
|
||||
|
||||
const lastVirtuosoStates: { [key: string]: StateSnapshot } = {};
|
||||
|
||||
const threadsWithoutImagesFilter = (comment: Comment) => {
|
||||
if (!getHasThumbnail(getCommentMediaInfo(comment), comment?.link)) {
|
||||
const { link, linkHeight, linkWidth, thumbnailUrl } = comment || {};
|
||||
if (!getHasThumbnail(getCommentMediaInfo(link, thumbnailUrl, linkWidth, linkHeight), link)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
@@ -43,6 +41,10 @@ const Board = () => {
|
||||
const subscriptions = account?.subscriptions;
|
||||
const isInSubscriptionsView = isSubscriptionsView(location.pathname, useParams());
|
||||
|
||||
const isInModView = isModView(location.pathname);
|
||||
const { accountSubplebbits } = useAccountSubplebbits();
|
||||
const accountSubplebbitAddresses = Object.keys(accountSubplebbits);
|
||||
|
||||
const subplebbitAddresses = useMemo(() => {
|
||||
if (isInAllView) {
|
||||
return defaultSubplebbitAddresses;
|
||||
@@ -50,8 +52,11 @@ const Board = () => {
|
||||
if (isInSubscriptionsView) {
|
||||
return subscriptions || [];
|
||||
}
|
||||
if (isInModView) {
|
||||
return accountSubplebbitAddresses;
|
||||
}
|
||||
return [subplebbitAddress];
|
||||
}, [isInAllView, isInSubscriptionsView, subplebbitAddress, defaultSubplebbitAddresses, subscriptions]);
|
||||
}, [isInAllView, isInSubscriptionsView, isInModView, subplebbitAddress, defaultSubplebbitAddresses, subscriptions, accountSubplebbitAddresses]);
|
||||
|
||||
const { sortType } = useSortingStore();
|
||||
const { timeFilterSeconds, timeFilterName } = useTimeFilter();
|
||||
@@ -59,8 +64,8 @@ const Board = () => {
|
||||
const feedOptions = {
|
||||
subplebbitAddresses,
|
||||
sortType,
|
||||
postsPerPage: isInAllView || isInSubscriptionsView ? 5 : 25,
|
||||
...(isInAllView || isInSubscriptionsView ? { newerThan: timeFilterSeconds } : {}),
|
||||
postsPerPage: isInAllView || isInSubscriptionsView || isInModView ? 5 : 25,
|
||||
...(isInAllView || isInSubscriptionsView || isInModView ? { newerThan: timeFilterSeconds } : {}),
|
||||
filter: hideThreadsWithoutImages ? threadsWithoutImagesFilter : undefined,
|
||||
};
|
||||
|
||||
@@ -78,14 +83,14 @@ const Board = () => {
|
||||
const filteredComments = useMemo(
|
||||
() =>
|
||||
accountComments.filter((comment) => {
|
||||
const { cid, deleted, postCid, removed, state, timestamp } = comment || {};
|
||||
const { cid, deleted, link, linkHeight, linkWidth, postCid, removed, state, thumbnailUrl, timestamp } = comment || {};
|
||||
return (
|
||||
!deleted &&
|
||||
!removed &&
|
||||
timestamp > Date.now() / 1000 - 60 * 60 &&
|
||||
state === 'succeeded' &&
|
||||
cid &&
|
||||
(hideThreadsWithoutImages ? getHasThumbnail(getCommentMediaInfo(comment), comment?.link) : true) &&
|
||||
(hideThreadsWithoutImages ? getHasThumbnail(getCommentMediaInfo(link, thumbnailUrl, linkWidth, linkHeight), comment?.link) : true) &&
|
||||
cid === postCid &&
|
||||
comment?.subplebbitAddress === subplebbitAddress &&
|
||||
!feed.some((post) => post.cid === cid)
|
||||
@@ -113,9 +118,7 @@ const Board = () => {
|
||||
|
||||
const subplebbit = useSubplebbit({ subplebbitAddress });
|
||||
const { createdAt, description, error, rules, shortAddress, state, suggested } = subplebbit || {};
|
||||
const title = isInAllView ? t('all') : isInSubscriptionsView ? t('subscriptions') : subplebbit?.title;
|
||||
|
||||
const { activeCid, threadCid, closeModal, openReplyModal, showReplyModal, scrollY, subplebbitAddress: postSubplebbitAddress } = useReplyModal();
|
||||
const title = isInAllView ? t('all') : isInSubscriptionsView ? t('subscriptions') : isInModView ? t('mod') : subplebbit?.title;
|
||||
|
||||
const { blocked, unblock } = useBlock({ address: subplebbitAddress });
|
||||
|
||||
@@ -178,7 +181,7 @@ const Board = () => {
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
(isInAllView || isInSubscriptionsView) &&
|
||||
(isInAllView || isInSubscriptionsView || isInModView) &&
|
||||
showMorePostsSuggestion &&
|
||||
monthlyFeed.length > feed.length &&
|
||||
(weeklyFeed.length > feed.length ? (
|
||||
@@ -187,7 +190,7 @@ const Board = () => {
|
||||
i18nKey='more_threads_last_week'
|
||||
values={{ currentTimeFilterName, count: feed.length }}
|
||||
components={{
|
||||
1: <Link to={(isInAllView ? '/p/all' : isInSubscriptionsView ? '/p/subscriptions' : `/p/${subplebbitAddress}`) + '/1w'} />,
|
||||
1: <Link to={(isInAllView ? '/p/all' : isInSubscriptionsView ? '/p/subscriptions' : isInModView ? '/p/mod' : `/p/${subplebbitAddress}`) + '/1w'} />,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -197,7 +200,7 @@ const Board = () => {
|
||||
i18nKey='more_threads_last_month'
|
||||
values={{ currentTimeFilterName, count: feed.length }}
|
||||
components={{
|
||||
1: <Link to={(isInAllView ? '/p/all' : isInSubscriptionsView ? '/p/subscriptions' : `/p/${subplebbitAddress}`) + '/1m'} />,
|
||||
1: <Link to={(isInAllView ? '/p/all' : isInSubscriptionsView ? '/p/subscriptions' : isInModView ? '/p/mod' : `/p/${subplebbitAddress}`) + '/1m'} />,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -213,9 +216,11 @@ const Board = () => {
|
||||
{state === 'failed' ? (
|
||||
<span className='red'>{state}</span>
|
||||
) : isInSubscriptionsView && subscriptions?.length === 0 ? (
|
||||
t('not_subscribed_to_any_board')
|
||||
<span className='red'>{t('not_subscribed_to_any_board')}</span>
|
||||
) : isInModView && accountSubplebbitAddresses?.length === 0 ? (
|
||||
<span className='red'>{t('not_mod_of_any_board')}</span>
|
||||
) : blocked ? (
|
||||
t('you_have_blocked_this_board')
|
||||
<span className='red'>{t('you_have_blocked_this_board')}</span>
|
||||
) : (
|
||||
hasMore && <LoadingEllipsis string={loadingStateString} />
|
||||
)}
|
||||
@@ -270,17 +275,6 @@ const Board = () => {
|
||||
<>
|
||||
{shouldShowSnow() && <hr />}
|
||||
<div className={`${styles.content} ${shouldShowSnow() ? styles.garland : ''}`}>
|
||||
{location.pathname.endsWith('/settings') && <SettingsModal />}
|
||||
{activeCid && threadCid && postSubplebbitAddress && (
|
||||
<ReplyModal
|
||||
closeModal={closeModal}
|
||||
parentCid={activeCid}
|
||||
postCid={threadCid}
|
||||
scrollY={scrollY}
|
||||
showReplyModal={showReplyModal}
|
||||
subplebbitAddress={postSubplebbitAddress}
|
||||
/>
|
||||
)}
|
||||
{((description && description.length > 0) || isInAllView) && (
|
||||
<SubplebbitDescription
|
||||
avatarUrl={suggested?.avatarUrl}
|
||||
@@ -297,7 +291,7 @@ const Board = () => {
|
||||
increaseViewportBy={{ bottom: 1200, top: 1200 }}
|
||||
totalCount={combinedFeed.length}
|
||||
data={combinedFeed}
|
||||
itemContent={(index, post) => <Post index={index} post={post} openReplyModal={openReplyModal} />}
|
||||
itemContent={(index, post) => <Post index={index} post={post} />}
|
||||
useWindowScroll={true}
|
||||
components={{ Footer }}
|
||||
endReached={loadMore}
|
||||
|
||||
@@ -16,13 +16,13 @@ import useInterfaceSettingsStore from '../../stores/use-interface-settings-store
|
||||
import useSortingStore from '../../stores/use-sorting-store';
|
||||
import CatalogRow from '../../components/catalog-row';
|
||||
import LoadingEllipsis from '../../components/loading-ellipsis';
|
||||
import SettingsModal from '../../components/settings-modal';
|
||||
import styles from './catalog.module.css';
|
||||
|
||||
const lastVirtuosoStates: { [key: string]: StateSnapshot } = {};
|
||||
|
||||
const threadsWithoutImagesFilter = (comment: Comment) => {
|
||||
if (!getHasThumbnail(getCommentMediaInfo(comment), comment?.link)) {
|
||||
const { link, linkHeight, linkWidth, thumbnailUrl } = comment || {};
|
||||
if (!getHasThumbnail(getCommentMediaInfo(link, thumbnailUrl, linkWidth, linkHeight), link)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
@@ -98,14 +98,14 @@ const Catalog = () => {
|
||||
const filteredComments = useMemo(
|
||||
() =>
|
||||
accountComments.filter((comment) => {
|
||||
const { cid, deleted, postCid, removed, state, timestamp } = comment || {};
|
||||
const { cid, deleted, link, linkHeight, linkWidth, postCid, removed, state, thumbnailUrl, timestamp } = comment || {};
|
||||
return (
|
||||
!deleted &&
|
||||
!removed &&
|
||||
timestamp > Date.now() / 1000 - 60 * 60 &&
|
||||
state === 'succeeded' &&
|
||||
cid &&
|
||||
(hideThreadsWithoutImages ? getHasThumbnail(getCommentMediaInfo(comment), comment?.link) : true) &&
|
||||
(hideThreadsWithoutImages ? getHasThumbnail(getCommentMediaInfo(link, thumbnailUrl, linkWidth, linkHeight), comment?.link) : true) &&
|
||||
cid === postCid &&
|
||||
comment?.subplebbitAddress === subplebbitAddress &&
|
||||
!feed.some((post) => post.cid === cid)
|
||||
@@ -288,7 +288,6 @@ const Catalog = () => {
|
||||
|
||||
return (
|
||||
<div className={styles.content}>
|
||||
{location.pathname.endsWith('/settings') && <SettingsModal />}
|
||||
<hr />
|
||||
<div className={styles.catalog}>
|
||||
{combinedFeed.length !== 0 ? (
|
||||
|
||||
@@ -2,14 +2,15 @@ import { useState, useEffect } from 'react';
|
||||
import { Link, useLocation } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Plebbit from '@plebbit/plebbit-js/dist/browser/index.js';
|
||||
import { Subplebbit, useSubplebbit, useSubplebbitStats } from '@plebbit/plebbit-react-hooks';
|
||||
import { Subplebbit, useSubplebbitStats } from '@plebbit/plebbit-react-hooks';
|
||||
import useSubplebbitsStore from '@plebbit/plebbit-react-hooks/dist/stores/subplebbits';
|
||||
import { useDefaultSubplebbitsState, useDefaultSubplebbitTags } from '../../../hooks/use-default-subplebbits';
|
||||
import useIsMobile from '../../../hooks/use-is-mobile';
|
||||
import useIsSubplebbitOffline from '../../../hooks/use-is-subplebbit-offline';
|
||||
import LoadingEllipsis from '../../../components/loading-ellipsis';
|
||||
import Tooltip from '../../../components/tooltip';
|
||||
import styles from './boards-list.module.css';
|
||||
import { nsfwTags } from '../home';
|
||||
import { nsfwTags } from '../../../constants/nsfwTags';
|
||||
|
||||
const Board = ({ subplebbit, isMobile }: { subplebbit: Subplebbit; isMobile: boolean }) => {
|
||||
const { t } = useTranslation();
|
||||
@@ -18,7 +19,7 @@ const Board = ({ subplebbit, isMobile }: { subplebbit: Subplebbit; isMobile: boo
|
||||
|
||||
let stats = useSubplebbitStats({ subplebbitAddress: address });
|
||||
|
||||
const subplebbitData = useSubplebbit({ subplebbitAddress: address });
|
||||
const subplebbitData = useSubplebbitsStore((state) => state.subplebbits[address]);
|
||||
const { isOffline, isOnlineStatusLoading, offlineIconClass, offlineTitle } = useIsSubplebbitOffline(subplebbitData);
|
||||
|
||||
const displayAddress = address && Plebbit.getShortAddress(address);
|
||||
|
||||
@@ -11,8 +11,6 @@ import Version from '../../components/version';
|
||||
import _ from 'lodash';
|
||||
|
||||
// https://github.com/plebbit/temporary-default-subplebbits/blob/master/README.md
|
||||
// plebchan shouldn't consider 'vulgar' or 'anti' as nsfw tags, unlike more sfw-oriented clients
|
||||
export const nsfwTags = ['adult', 'gore'];
|
||||
|
||||
const SearchBar = () => {
|
||||
const searchInputRef = useRef<HTMLInputElement>(null);
|
||||
@@ -52,7 +50,13 @@ const InfoBox = () => {
|
||||
<h2>{t('what_is_plebchan')}</h2>
|
||||
</div>
|
||||
<div className={styles.boxContent}>
|
||||
<Trans i18nKey='plebchan_description' shouldUnescape={true} components={{ 1: <Link to='https://plebbit.com' target='_blank' rel='noopener noreferrer' /> }} />
|
||||
<Trans
|
||||
i18nKey='plebchan_description'
|
||||
shouldUnescape={true}
|
||||
components={{
|
||||
1: <Link key='plebbit-link' to='https://plebbit.com' target='_blank' rel='noopener noreferrer' />,
|
||||
}}
|
||||
/>
|
||||
<br />
|
||||
<br />
|
||||
{t('no_global_rules_info')}
|
||||
|
||||
@@ -1 +1 @@
|
||||
export { HomeLogo, Footer, nsfwTags, default } from './home';
|
||||
export { HomeLogo, Footer, default } from './home';
|
||||
|
||||
@@ -9,7 +9,7 @@ import { getCommentMediaInfo } from '../../../lib/utils/media-utils';
|
||||
import { CatalogPostMedia } from '../../../components/catalog-row';
|
||||
import LoadingEllipsis from '../../../components/loading-ellipsis';
|
||||
import BoxModal from '../box-modal';
|
||||
import { nsfwTags } from '../home';
|
||||
import { nsfwTags } from '../../../constants/nsfwTags';
|
||||
import { removeMarkdown } from '../../../lib/utils/post-utils';
|
||||
|
||||
interface PopularThreadProps {
|
||||
@@ -26,8 +26,8 @@ export const ContentPreview = ({ content, maxLength = 99 }: { content: string; m
|
||||
};
|
||||
|
||||
const PopularThreadCard = ({ post, boardTitle, boardShortAddress }: PopularThreadProps) => {
|
||||
const { cid, content, subplebbitAddress, title } = post || {};
|
||||
const commentMediaInfo = getCommentMediaInfo(post);
|
||||
const { cid, content, link, linkHeight, linkWidth, subplebbitAddress, thumbnailUrl, title } = post || {};
|
||||
const commentMediaInfo = getCommentMediaInfo(link, thumbnailUrl, linkWidth, linkHeight);
|
||||
|
||||
return (
|
||||
<div className={styles.popularThread} key={cid}>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useState } from 'react';
|
||||
import { Link, useLocation } from 'react-router-dom';
|
||||
import useSubplebbitsStore from '@plebbit/plebbit-react-hooks/dist/stores/subplebbits';
|
||||
import { HomeLogo } from '../home';
|
||||
import styles from './not-found.module.css';
|
||||
import { useSubplebbit } from '@plebbit/plebbit-react-hooks';
|
||||
|
||||
const totalNotFoundImages = 2;
|
||||
|
||||
@@ -18,7 +18,7 @@ const NotFoundImage = () => {
|
||||
const NotFound = () => {
|
||||
const location = useLocation();
|
||||
const subplebbitAddress = location.pathname.startsWith('/p/') ? location.pathname.split('/')[2] : '';
|
||||
const subplebbit = useSubplebbit({ subplebbitAddress });
|
||||
const subplebbit = useSubplebbitsStore((state) => state.subplebbits[subplebbitAddress]);
|
||||
const { address, shortAddress } = subplebbit || {};
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useLocation, useNavigate, useParams } from 'react-router-dom';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { useAccountComment, useAccountComments } from '@plebbit/plebbit-react-hooks';
|
||||
import { isSettingsView } from '../../lib/utils/view-utils';
|
||||
import { Post } from '../post';
|
||||
import SettingsModal from '../../components/settings-modal';
|
||||
|
||||
const PendingPost = () => {
|
||||
const { accountComments } = useAccountComments();
|
||||
@@ -11,9 +9,6 @@ const PendingPost = () => {
|
||||
const commentIndex = accountCommentIndex ? parseInt(accountCommentIndex) : undefined;
|
||||
const post = useAccountComment({ commentIndex });
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const params = useParams();
|
||||
const isInSettingsView = isSettingsView(location.pathname, params);
|
||||
|
||||
useEffect(() => window.scrollTo(0, 0), []);
|
||||
|
||||
@@ -36,12 +31,7 @@ const PendingPost = () => {
|
||||
}
|
||||
}, [post, navigate]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{isInSettingsView && <SettingsModal />}
|
||||
<Post post={post} />
|
||||
</>
|
||||
);
|
||||
return <Post post={post} />;
|
||||
};
|
||||
|
||||
export default PendingPost;
|
||||
|
||||
+8
-25
@@ -1,18 +1,15 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Comment, Role, useComment, useEditedComment, useSubplebbit } from '@plebbit/plebbit-react-hooks';
|
||||
import useSubplebbitsStore from '@plebbit/plebbit-react-hooks/dist/stores/subplebbits';
|
||||
import { useLocation, useParams } from 'react-router-dom';
|
||||
import { isAllView, isDescriptionView, isRulesView, isSettingsView } from '../../lib/utils/view-utils';
|
||||
import { isAllView, isDescriptionView, isRulesView } from '../../lib/utils/view-utils';
|
||||
import useIsMobile from '../../hooks/use-is-mobile';
|
||||
import useReplyModal from '../../hooks/use-reply-modal';
|
||||
import PostDesktop from '../../components/post-desktop';
|
||||
import PostMobile from '../../components/post-mobile';
|
||||
import ReplyModal from '../../components/reply-modal';
|
||||
import SettingsModal from '../../components/settings-modal';
|
||||
import SubplebbitDescription from '../../components/subplebbit-description';
|
||||
import SubplebbitRules from '../../components/subplebbit-rules';
|
||||
import styles from './post.module.css';
|
||||
|
||||
export interface PostProps {
|
||||
index?: number;
|
||||
isHidden?: boolean;
|
||||
@@ -23,11 +20,11 @@ export interface PostProps {
|
||||
roles?: Role[];
|
||||
showAllReplies?: boolean;
|
||||
showReplies?: boolean;
|
||||
openReplyModal?: (parentCid: string, postCid: string, subplebbitAddress: string) => void;
|
||||
replies?: Comment[];
|
||||
}
|
||||
|
||||
export const Post = ({ post, showAllReplies = false, showReplies = true, openReplyModal }: PostProps) => {
|
||||
const subplebbit = useSubplebbit({ subplebbitAddress: post?.subplebbitAddress });
|
||||
export const Post = ({ post, showAllReplies = false, showReplies = true, replies }: PostProps) => {
|
||||
const subplebbit = useSubplebbitsStore((state) => state.subplebbits[post?.subplebbitAddress]);
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
let comment = post;
|
||||
@@ -42,9 +39,9 @@ export const Post = ({ post, showAllReplies = false, showReplies = true, openRep
|
||||
<div className={styles.thread}>
|
||||
<div className={styles.postContainer}>
|
||||
{isMobile ? (
|
||||
<PostMobile post={comment} roles={subplebbit?.roles} showAllReplies={showAllReplies} showReplies={showReplies} openReplyModal={openReplyModal} />
|
||||
<PostMobile post={comment} roles={subplebbit?.roles} showAllReplies={showAllReplies} showReplies={showReplies} replies={replies} />
|
||||
) : (
|
||||
<PostDesktop post={comment} roles={subplebbit?.roles} showAllReplies={showAllReplies} showReplies={showReplies} openReplyModal={openReplyModal} />
|
||||
<PostDesktop post={comment} roles={subplebbit?.roles} showAllReplies={showAllReplies} showReplies={showReplies} replies={replies} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -57,15 +54,12 @@ const PostPage = () => {
|
||||
const location = useLocation();
|
||||
const { commentCid, subplebbitAddress } = params;
|
||||
const isInAllView = isAllView(location.pathname);
|
||||
const isInSettigsView = isSettingsView(location.pathname, params);
|
||||
const isInDescriptionView = isDescriptionView(location.pathname, params);
|
||||
const isInRulesView = isRulesView(location.pathname, params);
|
||||
|
||||
const subplebbit = useSubplebbit({ subplebbitAddress });
|
||||
const { createdAt, description, rules, shortAddress, suggested, title } = subplebbit;
|
||||
|
||||
const { activeCid, threadCid, closeModal, openReplyModal, showReplyModal, scrollY, subplebbitAddress: postSubplebbitAddress } = useReplyModal();
|
||||
|
||||
const comment = useComment({ commentCid });
|
||||
|
||||
// if the comment is a reply, return the post comment instead, then the reply will be highlighted in the thread
|
||||
@@ -92,17 +86,6 @@ const PostPage = () => {
|
||||
|
||||
return (
|
||||
<div className={styles.content}>
|
||||
{isInSettigsView && <SettingsModal />}
|
||||
{activeCid && threadCid && postSubplebbitAddress && (
|
||||
<ReplyModal
|
||||
closeModal={closeModal}
|
||||
parentCid={activeCid}
|
||||
postCid={threadCid}
|
||||
scrollY={scrollY}
|
||||
showReplyModal={showReplyModal}
|
||||
subplebbitAddress={postSubplebbitAddress}
|
||||
/>
|
||||
)}
|
||||
{/* TODO: remove this replyCount error once api supports scrolling replies pages */}
|
||||
{replyCount > 60 && <span className={styles.error}>Error: this thread has too many replies, some of them cannot be displayed right now.</span>}
|
||||
{error && <span className={styles.error}>Error: {error?.message || error?.toString?.()}</span>}
|
||||
@@ -119,7 +102,7 @@ const PostPage = () => {
|
||||
) : isInRulesView ? (
|
||||
<SubplebbitRules createdAt={createdAt} rules={rules} subplebbitAddress={subplebbitAddress} />
|
||||
) : (
|
||||
<Post post={post} showAllReplies={true} openReplyModal={openReplyModal} />
|
||||
<Post post={post} showAllReplies={true} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
import { resolve } from 'path';
|
||||
import { nodePolyfills } from 'vite-plugin-node-polyfills';
|
||||
import eslint from 'vite-plugin-eslint';
|
||||
|
||||
const isProduction = process.env.NODE_ENV === 'production';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
react({
|
||||
babel: {
|
||||
plugins: [
|
||||
['babel-plugin-react-compiler', {
|
||||
verbose: true
|
||||
}]
|
||||
]
|
||||
}
|
||||
}),
|
||||
!isProduction && eslint({
|
||||
lintOnStart: true,
|
||||
overrideConfigFile: './.eslintrc.cjs',
|
||||
failOnError: false,
|
||||
failOnWarning: false,
|
||||
cache: true,
|
||||
}),
|
||||
nodePolyfills({
|
||||
globals: {
|
||||
Buffer: true,
|
||||
global: true,
|
||||
process: true,
|
||||
},
|
||||
protocolImports: true,
|
||||
include: ['crypto', 'stream', 'util', 'buffer', 'events'],
|
||||
}),
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': resolve(__dirname, 'src'),
|
||||
'node-fetch': 'isomorphic-fetch',
|
||||
'assert': 'assert',
|
||||
'stream': 'stream-browserify',
|
||||
'crypto': 'crypto-browserify',
|
||||
'buffer': 'buffer',
|
||||
}
|
||||
},
|
||||
server: {
|
||||
port: 3000,
|
||||
open: true,
|
||||
watch: {
|
||||
usePolling: true,
|
||||
},
|
||||
hmr: {
|
||||
overlay: false
|
||||
}
|
||||
},
|
||||
build: {
|
||||
outDir: 'build',
|
||||
emptyOutDir: true,
|
||||
sourcemap: process.env.GENERATE_SOURCEMAP === 'true',
|
||||
target: process.env.ELECTRON ? 'electron-renderer' : 'modules',
|
||||
rollupOptions: {
|
||||
output: {
|
||||
manualChunks: {
|
||||
vendor: ['react', 'react-dom', 'react-router-dom']
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
base: process.env.PUBLIC_URL || '/',
|
||||
optimizeDeps: {
|
||||
esbuildOptions: {
|
||||
target: 'es2020',
|
||||
define: {
|
||||
global: 'globalThis',
|
||||
},
|
||||
},
|
||||
include: [
|
||||
'ethers',
|
||||
'assert',
|
||||
'buffer',
|
||||
'process',
|
||||
'stream-browserify',
|
||||
'isomorphic-fetch'
|
||||
],
|
||||
},
|
||||
esbuild: {
|
||||
target: 'es2020'
|
||||
},
|
||||
define: {
|
||||
'process.env.VITE_COMMIT_REF': JSON.stringify(process.env.COMMIT_REF),
|
||||
'global': 'globalThis',
|
||||
'__dirname': '""',
|
||||
}
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user