Compare commits

..

1 commit

Author SHA1 Message Date
Dessalines
b6e383791e Fix comment jumping. Fixes #529 2022-02-14 14:07:54 -05:00
315 changed files with 18875 additions and 122096 deletions

View file

@ -10,11 +10,11 @@
}
}
],
["@babel/typescript", { "isTSX": true, "allExtensions": true }]
["@babel/typescript", {"isTSX": true, "allExtensions": true}]
],
"plugins": [
"@babel/plugin-transform-runtime",
["babel-plugin-inferno", { "imports": true }],
["@babel/plugin-proposal-class-properties", { "loose": true }]
["babel-plugin-inferno", { "imports": true }],
["@babel/plugin-proposal-class-properties", { "loose": true }],
]
}

152
.drone.yml Normal file
View file

@ -0,0 +1,152 @@
---
kind: pipeline
name: amd64
platform:
os: linux
arch: amd64
steps:
- name: fetch git submodules
image: node:14-alpine
commands:
- apk add git
- git submodule init
- git submodule update --recursive --remote
- git fetch --tags
- name: yarn
image: node:14-alpine
commands:
- yarn
- name: yarn lint
image: node:14-alpine
commands:
- yarn lint
- name: yarn build:dev
image: node:14-alpine
commands:
- yarn build:dev
- name: publish release docker image
image: plugins/docker
settings:
dockerfile: Dockerfile
repo: dessalines/lemmy-ui
auto_tag: true
auto_tag_suffix: linux-amd64
username:
from_secret: docker_username
password:
from_secret: docker_password
when:
ref:
- refs/tags/*
- name: publish release docker manifest
image: plugins/manifest
settings:
username:
from_secret: docker_username
password:
from_secret: docker_password
target: "dessalines/lemmy-ui:${DRONE_TAG}"
template: "dessalines/lemmy-ui:${DRONE_TAG}-OS-ARCH"
platforms:
- linux/amd64
- linux/arm64
ignore_missing: true
when:
ref:
- refs/tags/*
- name: publish latest release docker manifest
image: plugins/manifest
settings:
username:
from_secret: docker_username
password:
from_secret: docker_password
target: "dessalines/lemmy-ui:latest"
template: "dessalines/lemmy-ui:${DRONE_TAG}-OS-ARCH"
platforms:
- linux/amd64
- linux/arm64
ignore_missing: true
when:
ref:
- refs/tags/*
---
kind: pipeline
name: arm64
platform:
os: linux
arch: arm64
steps:
- name: fetch git submodules
image: node:14-alpine
commands:
- apk add git
- git submodule init
- git submodule update --recursive --remote
- git fetch --tags
when:
ref:
- refs/heads/main
- refs/tags/*
- name: publish release docker image
image: plugins/docker
settings:
dockerfile: Dockerfile
repo: dessalines/lemmy-ui
auto_tag: true
auto_tag_suffix: linux-arm64
username:
from_secret: docker_username
password:
from_secret: docker_password
when:
ref:
- refs/tags/*
- name: publish release docker manifest
image: plugins/manifest
settings:
username:
from_secret: docker_username
password:
from_secret: docker_password
target: "dessalines/lemmy-ui:${DRONE_TAG}"
template: "dessalines/lemmy-ui:${DRONE_TAG}-OS-ARCH"
platforms:
- linux/amd64
- linux/arm64
ignore_missing: true
when:
ref:
- refs/tags/*
- name: publish latest release docker manifest
image: plugins/manifest
settings:
username:
from_secret: docker_username
password:
from_secret: docker_password
target: "dessalines/lemmy-ui:latest"
template: "dessalines/lemmy-ui:${DRONE_TAG}-OS-ARCH"
platforms:
- linux/amd64
- linux/arm64
ignore_missing: true
when:
ref:
- refs/tags/*

View file

@ -1,7 +1,3 @@
generate_translations.js
webpack.config.js
src/api_tests
**/*.png
**/*.css
**/*.scss
**/*.svg

View file

@ -3,12 +3,12 @@
"env": {
"browser": true
},
"plugins": ["@typescript-eslint", "jsx-a11y", "prettier"],
"plugins": [
"@typescript-eslint"
],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:inferno/recommended",
"plugin:jsx-a11y/recommended"
"plugin:@typescript-eslint/recommended"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
@ -19,7 +19,6 @@
"@typescript-eslint/ban-ts-comment": 0,
"@typescript-eslint/no-explicit-any": 0,
"@typescript-eslint/explicit-module-boundary-types": 0,
"@typescript-eslint/no-empty-function": 0,
"arrow-body-style": 0,
"curly": 0,
"eol-last": 0,
@ -39,9 +38,8 @@
"no-useless-constructor": 0,
"no-useless-escape": 0,
"no-var": 0,
"prefer-const": 1,
"prefer-const": 0,
"prefer-rest-params": 0,
"prettier/prettier": "error",
"quote-props": 0,
"unicorn/filename-case": 0
}

2
.github/CODEOWNERS vendored
View file

@ -1 +1 @@
* @dessalines @SleeplessOne1917 @alectrocute @jsit
* @dessalines

29
.github/ISSUE_TEMPLATE/BUG_REPORT.md vendored Normal file
View file

@ -0,0 +1,29 @@
---
name: "\U0001F41E Bug Report"
about: Create a report to help us improve Lemmy
title: ''
labels: bug
assignees: ''
---
Found a bug? Please fill out the sections below. 👍
For backend issues, use [lemmy](https://github.com/LemmyNet/lemmy)
### Issue Summary
A summary of the bug.
### Steps to Reproduce
1. (for example) I clicked login, and an endless spinner show up.
2. I tried to install lemmy via this guide, and I'm getting this error.
3. ...
### Technical details
* Please post your log: `sudo docker-compose logs > lemmy_log.out`.
* What OS are you trying to install lemmy on?
* Any browser console errors?

View file

@ -1,65 +0,0 @@
name: "\U0001F41E Bug report"
description: Report a bug to help us improve Lemmy-UI.
labels: ["bug", "triage"]
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to help improve Lemmy-UI by reporting a bug!
- type: checkboxes
attributes:
label: Requirements
description: Before you create a bug report, please carefully check the following
options:
- label: This is a bug report, and if not, please post to https://lemmy.ml/c/lemmy_support instead.
required: true
- label: Please [check](https://github.com/LemmyNet/lemmy-ui/issues) to see if this issue already exists.
required: true
- label: It's a single bug. Do not report multiple bugs in one issue.
required: true
- label: It's a frontend issue, not a backend issue; Otherwise please create an issue on the [backend repo](https://github.com/LemmyNet/lemmy) instead.
required: true
- type: textarea
id: summary
attributes:
label: Summary
description: Explain the bug and upload images, screenshots or videos if possible.
validations:
required: true
- type: textarea
id: reproduce
attributes:
label: Steps to Reproduce
description: |
In a numbered list, walk us through the steps needed to reproduce the bug.
The better your description is _(go 'here', click 'there'...)_, the quicker we can fix it.
value: |
1.
2.
3.
4.
validations:
required: true
- type: textarea
id: technical
attributes:
label: Technical Details
description: |
Describe your environment (OS, browser, model of smartphone, etc)
If relevant, also share any console errors and/or screenshots here.
validations:
required: true
- type: input
id: lemmy-ui-version
attributes:
label: Lemmy Instance Version
description: What's the version of the Lemmy instance where the bug can be reproduced?
placeholder: ex. 0.18-rc.6
validations:
required: true
- type: input
id: lemmy-instance
attributes:
label: Lemmy Instance URL
description: What's the URL of the Lemmy instance where the bug can be reproduced?
placeholder: https://lemmy.ml

View file

@ -0,0 +1,44 @@
---
name: "\U0001F680 Feature request"
about: Suggest an idea for improving Lemmy
title: ''
labels: enhancement
assignees: ''
---
For backend issues, use [lemmy](https://github.com/LemmyNet/lemmy)
### Is your proposal related to a problem?
<!--
Provide a clear and concise description of what the problem is.
For example, "I'm always frustrated when..."
-->
(Write your answer here.)
### Describe the solution you'd like
<!--
Provide a clear and concise description of what you want to happen.
-->
(Describe your proposed solution here.)
### Describe alternatives you've considered
<!--
Let us know about other solutions you've tried or researched.
-->
(Write your answer here.)
### Additional context
<!--
Is there anything else you can add about the proposal?
You might want to link to related issues here, if you haven't already.
-->
(Write your answer here.)

View file

@ -1,27 +0,0 @@
name: "\U0001F680 Feature request"
description: Suggest an idea for Lemmy-UI.
labels: ["enhancement"]
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to help improve Lemmy-UI by suggesting a feature!
- type: checkboxes
attributes:
label: Requirements
description: Before you create a feature request, please carefully check the following
options:
- label: This is a feature request and not a bug report. Otherwise, please create a new [bug report](https://github.com/LemmyNet/lemmy-ui/issues/new?assignees=&labels=bug%2Ctriage&projects=&template=BUG_REPORT.yml) instead.
required: true
- label: Please [check](https://github.com/LemmyNet/lemmy-ui/issues) to see if this request (or a similar one) already exists.
required: true
- label: It's a single feature. Please don't request multiple features in one issue.
required: true
- type: textarea
id: solution
attributes:
label: Describe the feature you'd like
description: |
Provide a clear and concise description of the feature. Explain why it's needed.
validations:
required: true

10
.github/ISSUE_TEMPLATE/QUESTION.md vendored Normal file
View file

@ -0,0 +1,10 @@
---
name: "? Question"
about: General questions about Lemmy
title: ''
labels: question
assignees: ''
---
What's the question you have about lemmy?

View file

@ -1,8 +0,0 @@
blank_issues_enabled: false
contact_links:
- name: Question
url: https://lemmy.ml/c/lemmy_support
about: Please ask and answer general questions here.
- name: Technical Discussion
url: https://github.com/LemmyNet/lemmy-ui/discussions
about: Please discuss technical topics with other contributors here.

10
.github/ISSUE_TEMPLATE/hexbear.md vendored Normal file
View file

@ -0,0 +1,10 @@
---
name: Hexbear
about: For hexbear issues
title: ''
labels: hexbear
assignees: ''
---
For hexbear-related issues

View file

@ -1,12 +0,0 @@
## Description
<!-- Please describe exactly what this PR changes, including URLs and issue
numbers. If it fixes an issue, add "Fixes #XXXX" -->
## Screenshots
<!-- Please include before and after screenshots if applicable -->
### Before
### After

2
.gitignore vendored
View file

@ -27,5 +27,3 @@ package-lock.json
src/shared/translations
stats.json

View file

@ -1,5 +0,0 @@
src/shared/translations
lemmy-translations
src/assets/css/themes/*.css
stats.json
dist

4
.prettierrc.js Normal file
View file

@ -0,0 +1,4 @@
module.exports = Object.assign(require("eslint-plugin-prettier"), {
arrowParens: "avoid",
semi: true,
});

View file

@ -1,4 +0,0 @@
{
"arrowParens": "avoid",
"semi": true
}

View file

@ -1,45 +0,0 @@
pipeline:
fetch_git_submodules:
image: node:alpine
commands:
- apk add git
- git submodule init
- git submodule update --recursive --remote
# - git fetch --tags
yarn:
image: node:alpine
commands:
- yarn
yarn_lint:
image: node:alpine
commands:
- yarn lint
yarn_build_dev:
image: node:alpine
commands:
- yarn build:dev
publish_release_docker:
image: woodpeckerci/plugin-docker-buildx
secrets: [docker_username, docker_password]
settings:
repo: dessalines/lemmy-ui
dockerfile: Dockerfile
platforms: linux/amd64
auto_tag: true
when:
event: tag
nightly_build:
image: woodpeckerci/plugin-docker-buildx
secrets: [docker_username, docker_password]
settings:
repo: dessalines/lemmy-ui
dockerfile: Dockerfile
platforms: linux/amd64
tag: dev
when:
event: cron

View file

@ -1,3 +1,3 @@
# Contributing
See [here](https://join-lemmy.org/docs/contributors/01-overview.html) for contributing Instructions.
See [here](https://join-lemmy.org/docs/en/contributing/contributing.html) for contributing Instructions.

View file

@ -1,16 +1,11 @@
FROM node:20.2-alpine as builder
RUN apk update && apk add curl yarn python3 build-base gcc wget git --no-cache
RUN curl -sf https://gobinaries.com/tj/node-prune | sh
FROM node:alpine as builder
RUN apk update && apk add yarn python3 build-base gcc wget git --no-cache
WORKDIR /usr/src/app
ENV npm_config_target_arch=x64
ENV npm_config_target_platform=linux
ENV npm_config_target_libc=musl
# Cache deps
COPY package.json yarn.lock ./
RUN yarn --production --prefer-offline --pure-lockfile
RUN yarn install --pure-lockfile
# Build
COPY generate_translations.js \
@ -26,17 +21,8 @@ COPY .git .git
# Set UI version
RUN echo "export const VERSION = '$(git describe --tag)';" > "src/shared/version.ts"
RUN yarn --production --prefer-offline
RUN NODE_OPTIONS="--max-old-space-size=8192" yarn build:prod
# Prune the image
RUN node-prune /usr/src/app/node_modules
RUN rm -rf ./node_modules/import-sort-parser-typescript
RUN rm -rf ./node_modules/typescript
RUN rm -rf ./node_modules/npm
RUN du -sh ./node_modules/* | sort -nr | grep '\dM.*'
RUN yarn
RUN yarn build:prod
FROM node:alpine as runner
COPY --from=builder /usr/src/app/dist /app/dist

View file

@ -1,20 +1,5 @@
# Lemmy-UI
The official web app for [Lemmy](https://github.com/LemmyNet/lemmy), written in inferno.
Based off of MrFoxPro's [inferno-isomorphic-template](https://github.com/MrFoxPro/inferno-isomorphic-template).
## Configuration
The following environment variables can be used to configure lemmy-ui:
| `ENV_VAR` | type | default | description |
| ------------------------------ | -------- | ---------------- | ----------------------------------------------------------------------------------- |
| `LEMMY_UI_HOST` | `string` | `0.0.0.0:1234` | The IP / port that the lemmy-ui isomorphic node server is hosted at. |
| `LEMMY_UI_LEMMY_INTERNAL_HOST` | `string` | `0.0.0.0:8536` | The internal IP / port that lemmy is hosted at. Often `lemmy:8536` if using docker. |
| `LEMMY_UI_LEMMY_EXTERNAL_HOST` | `string` | `0.0.0.0:8536` | The external IP / port that lemmy is hosted at. Often `DOMAIN.TLD`. |
| `LEMMY_UI_HTTPS` | `bool` | `false` | Whether to use https. |
| `LEMMY_UI_EXTRA_THEMES_FOLDER` | `string` | `./extra_themes` | A location for additional lemmy css themes. |
| `LEMMY_UI_DEBUG` | `bool` | `false` | Loads the [Eruda](https://github.com/liriliri/eruda) debugging utility. |
| `LEMMY_UI_DISABLE_CSP` | `bool` | `false` | Disables CSP security headers |
| `LEMMY_UI_CUSTOM_HTML_HEADER` | `string` | | Injects a custom script into `<head>`. |
# lemmy-ui
The official web app for [Lemmy](https://github.com/LemmyNet/lemmy), written in inferno.
Based off of MrFoxPro's [inferno-isomorphic-template](https://github.com/MrFoxPro/inferno-isomorphic-template).

View file

@ -4,8 +4,7 @@ set -e
new_tag="$1"
# Old deploy
# sudo docker build . --tag dessalines/lemmy-ui:$new_tag --platform=linux/amd64 --push
# sudo docker build . --tag dessalines/lemmy-ui:$new_tag --platform=linux/amd64
# sudo docker build . --tag dessalines/lemmy-ui:$new_tag
# sudo docker push dessalines/lemmy-ui:$new_tag
# Upgrade version

View file

@ -1,37 +0,0 @@
FROM node:20.2-alpine as builder
RUN apk update && apk add curl yarn python3 build-base gcc wget git --no-cache
WORKDIR /usr/src/app
ENV npm_config_target_arch=x64
ENV npm_config_target_platform=linux
ENV npm_config_target_libc=musl
# Cache deps
COPY package.json yarn.lock ./
RUN yarn --prefer-offline --pure-lockfile
# Build
COPY generate_translations.js \
tsconfig.json \
webpack.config.js \
.babelrc \
./
COPY lemmy-translations lemmy-translations
COPY src src
COPY .git .git
# Set UI version
RUN echo "export const VERSION = 'dev';" > "src/shared/version.ts"
RUN yarn --prefer-offline
RUN yarn build:dev
FROM node:alpine as runner
COPY --from=builder /usr/src/app/dist /app/dist
COPY --from=builder /usr/src/app/node_modules /app/node_modules
EXPOSE 1234
WORKDIR /app
CMD node dist/js/server.js

View file

@ -30,70 +30,30 @@ fs.readdir(translationDir, (_err, files) => {
const baseLanguage = "en";
fs.readFile(`${translationDir}${baseLanguage}.json`, "utf8", (_, fileStr) => {
const noOptionKeys = [];
const optionKeys = [];
const optionRegex = /\{\{(.+?)\}\}/g;
const optionMap = new Map();
for (const [key, val] of Object.entries(JSON.parse(fileStr))) {
const options = [];
for (
let match = optionRegex.exec(val);
match;
match = optionRegex.exec(val)
) {
options.push(match[1]);
}
if (options.length > 0) {
optionMap.set(key, options);
optionKeys.push(key);
} else {
noOptionKeys.push(key);
}
}
const indent = " ";
const keys = Object.keys(JSON.parse(fileStr));
const data = `import { i18n } from "i18next";
declare module "i18next" {
export type NoOptionI18nKeys =
${noOptionKeys.map(key => `${indent}| "${key}"`).join("\n")};
export type OptionI18nKeys =
${optionKeys.map(key => `${indent}| "${key}"`).join("\n")};
export type I18nKeys = NoOptionI18nKeys | OptionI18nKeys;
export type TTypedOptions<TKey extends OptionI18nKeys> =${Array.from(
optionMap.entries()
).reduce(
(acc, [key, options]) =>
`${acc} TKey extends \"${key}\" ? ${
options.reduce((acc, cur) => acc + `${cur}: string | number; `, "{ ") +
"}"
} :\n${indent}`,
""
)} (Record<string, unknown> | string);
export type I18nKeys =
${keys.map(key => ` | "${key}"`).join("\n")};
export interface TFunctionTyped {
// Translation requires options
<
TKey extends OptionI18nKeys | OptionI18nKeys[],
TResult extends TFunctionResult = string,
TInterpolationMap extends TTypedOptions<TKey> = StringMap
> (
key: TKey,
options: TOptions<TInterpolationMap> | string
): TResult;
// Translation does not require options
// basic usage
<
TResult extends TFunctionResult = string,
TInterpolationMap extends Record<string, unknown> = StringMap
> (
key: NoOptionI18nKeys | NoOptionI18nKeys[],
>(
key: I18nKeys | I18nKeys[],
options?: TOptions<TInterpolationMap> | string
): TResult;
// overloaded usage
<
TResult extends TFunctionResult = string,
TInterpolationMap extends Record<string, unknown> = StringMap
>(
key: I18nKeys | I18nKeys[],
defaultValue?: string,
options?: TOptions<TInterpolationMap> | string
): TResult;
}

@ -1 +1 @@
Subproject commit a241fe1255a6363c7ae1ec5a09520c066745e6ce
Subproject commit 0d63b5affe2273bb88b3593cdc13e4bb0f4d2d5d

View file

@ -1,140 +1,113 @@
{
"name": "lemmy-ui",
"version": "0.18.1-rc.9",
"description": "An isomorphic UI for lemmy",
"repository": "https://github.com/LemmyNet/lemmy-ui",
"license": "AGPL-3.0",
"version": "0.15.2",
"author": "Dessalines <tyhou13@gmx.com>",
"license": "AGPL-3.0",
"scripts": {
"analyze": "webpack --mode=none",
"prebuild:dev": "yarn clean && node generate_translations.js",
"build:dev": "webpack --env COMMIT_HASH=$(git rev-parse --short HEAD) --mode=development",
"prebuild:prod": "yarn clean && node generate_translations.js",
"build:prod": "webpack --env COMMIT_HASH=$(git rev-parse --short HEAD) --mode=production",
"build:dev": "webpack --mode=development",
"build:prod": "webpack --mode=production",
"clean": "yarn run rimraf dist",
"dev": "yarn build:dev --watch",
"lint": "yarn translations:generate && tsc --noEmit && eslint --report-unused-disable-directives --ext .js,.ts,.tsx \"src/**\" && prettier --check \"src/**/*.{ts,tsx,js,css,scss}\"",
"dev": "yarn start",
"lint": "node generate_translations.js && tsc --noEmit && eslint --report-unused-disable-directives --ext .js,.ts,.tsx src",
"prebuild:dev": "yarn clean && node generate_translations.js",
"prebuild:prod": "yarn clean && node generate_translations.js",
"prepare": "husky install",
"themes:build": "sass src/assets/css/themes/:src/assets/css/themes",
"themes:watch": "sass --watch src/assets/css/themes/:src/assets/css/themes",
"translations:generate": "node generate_translations.js",
"translations:init": "git submodule init && yarn translations:update",
"translations:update": "git submodule update --remote --recursive"
"start": "yarn build:dev --watch"
},
"repository": "https://github.com/LemmyNet/lemmy-ui",
"dependencies": {
"@typescript-eslint/parser": "^5.6.0",
"autosize": "^5.0.1",
"check-password-strength": "^2.0.3",
"choices.js": "^10.0.0",
"classnames": "^2.3.1",
"emoji-short-name": "^1.0.0",
"express": "~4.17.1",
"i18next": "^21.5.4",
"inferno": "^7.4.11",
"inferno-create-element": "^7.4.11",
"inferno-helmet": "^5.2.1",
"inferno-hydrate": "^7.4.11",
"inferno-i18next-dess": "^0.0.1",
"inferno-router": "^7.4.11",
"inferno-server": "^7.4.11",
"isomorphic-cookie": "^1.2.4",
"jwt-decode": "^3.1.2",
"markdown-it": "^12.1.0",
"markdown-it-container": "^3.0.0",
"markdown-it-footnote": "^3.0.3",
"markdown-it-html5-embed": "^1.0.0",
"markdown-it-sub": "^1.0.0",
"markdown-it-sup": "^1.0.0",
"moment": "^2.29.1",
"register-service-worker": "^1.7.2",
"rxjs": "^7.4.0",
"sass": "^1.47.0",
"serialize-javascript": "^6.0.0",
"tippy.js": "^6.3.7",
"toastify-js": "^1.11.2",
"tributejs": "^5.1.3",
"websocket-ts": "^1.1.1"
},
"devDependencies": {
"@babel/core": "^7.16.0",
"@babel/plugin-transform-runtime": "^7.16.4",
"@babel/plugin-transform-typescript": "^7.16.1",
"@babel/preset-env": "7.16.8",
"@babel/preset-typescript": "^7.16.0",
"@babel/runtime": "^7.16.3",
"@types/autosize": "^4.0.0",
"@types/express": "^4.17.13",
"@types/node": "^17.0.8",
"@types/node-fetch": "^2.5.11",
"@types/serialize-javascript": "^5.0.1",
"@typescript-eslint/eslint-plugin": "^5.6.0",
"babel-loader": "^8.2.3",
"babel-plugin-inferno": "^6.3.0",
"bootstrap": "^5.1.3",
"bootswatch": "^5.1.3",
"clean-webpack-plugin": "^4.0.0",
"copy-webpack-plugin": "^10.0.0",
"css-loader": "^6.5.1",
"eslint": "^8.4.0",
"eslint-plugin-prettier": "^4.0.0",
"husky": "^7.0.4",
"import-sort-style-module": "^6.0.0",
"iso-639-1": "^2.1.10",
"lemmy-js-client": "0.15.1-rc.1",
"lint-staged": "^12.1.2",
"mini-css-extract-plugin": "^2.4.5",
"node-fetch": "^2.6.1",
"prettier": "^2.5.1",
"prettier-plugin-import-sort": "^0.0.7",
"prettier-plugin-organize-imports": "^2.3.4",
"prettier-plugin-packagejson": "^2.2.15",
"rimraf": "^3.0.2",
"run-node-webpack-plugin": "^1.3.0",
"sass-loader": "^12.3.0",
"sortpack": "^2.2.0",
"style-loader": "^3.3.1",
"terser": "^5.10.0",
"typescript": "^4.5.2",
"webpack": "5.66.0",
"webpack-cli": "^4.9.1",
"webpack-dev-server": "4.7.3",
"webpack-node-externals": "^3.0.0"
},
"engines": {
"node": ">=8.9.0"
},
"engineStrict": true,
"lint-staged": {
"*.{ts,tsx,js}": [
"prettier --write",
"eslint --fix"
],
"*.{css, scss}": [
"prettier --write"
],
"package.json": [
"sortpack"
]
},
"dependencies": {
"@babel/plugin-proposal-decorators": "^7.21.0",
"@babel/plugin-transform-runtime": "^7.21.4",
"@babel/plugin-transform-typescript": "^7.21.3",
"@babel/preset-env": "7.21.5",
"@babel/preset-typescript": "^7.21.5",
"@babel/runtime": "^7.21.5",
"@emoji-mart/data": "^1.1.0",
"autosize": "^6.0.1",
"babel-loader": "^9.1.2",
"babel-plugin-inferno": "^6.6.0",
"bootstrap": "^5.2.3",
"check-password-strength": "^2.0.7",
"classnames": "^2.3.1",
"clean-webpack-plugin": "^4.0.0",
"cookie": "^0.5.0",
"copy-webpack-plugin": "^11.0.0",
"cross-fetch": "^3.1.5",
"css-loader": "^6.7.3",
"date-fns": "^2.30.0",
"emoji-mart": "^5.4.0",
"emoji-short-name": "^2.0.0",
"express": "~4.18.2",
"history": "^5.3.0",
"html-to-text": "^9.0.5",
"i18next": "^22.4.15",
"inferno": "^8.1.1",
"inferno-create-element": "^8.1.1",
"inferno-helmet": "^5.2.1",
"inferno-hydrate": "^8.1.1",
"inferno-i18next-dess": "0.0.2",
"inferno-router": "^8.1.1",
"inferno-server": "^8.1.1",
"jwt-decode": "^3.1.2",
"lemmy-js-client": "0.18.0-rc.2",
"lodash.isequal": "^4.5.0",
"lodash.merge": "^4.6.2",
"markdown-it": "^13.0.1",
"markdown-it-container": "^3.0.0",
"markdown-it-emoji": "^2.0.2",
"markdown-it-footnote": "^3.0.3",
"markdown-it-html5-embed": "^1.0.0",
"markdown-it-sub": "^1.0.0",
"markdown-it-sup": "^1.0.0",
"mini-css-extract-plugin": "^2.7.5",
"register-service-worker": "^1.7.2",
"run-node-webpack-plugin": "^1.3.0",
"sanitize-html": "^2.10.0",
"sass": "^1.62.1",
"sass-loader": "^13.2.2",
"serialize-javascript": "^6.0.1",
"service-worker-webpack": "^1.0.0",
"sharp": "^0.32.1",
"tippy.js": "^6.3.7",
"toastify-js": "^1.12.0",
"tributejs": "^5.1.3",
"webpack": "5.82.1",
"webpack-cli": "^5.1.1",
"webpack-node-externals": "^3.0.0"
},
"devDependencies": {
"@babel/core": "^7.21.8",
"@types/autosize": "^4.0.0",
"@types/bootstrap": "^5.2.6",
"@types/cookie": "^0.5.1",
"@types/express": "^4.17.17",
"@types/html-to-text": "^9.0.0",
"@types/lodash.isequal": "^4.5.6",
"@types/markdown-it": "^12.2.3",
"@types/markdown-it-container": "^2.0.5",
"@types/node": "^20.1.2",
"@types/path-browserify": "^1.0.0",
"@types/sanitize-html": "^2.9.0",
"@types/serialize-javascript": "^5.0.1",
"@types/toastify-js": "^1.11.1",
"@typescript-eslint/eslint-plugin": "^5.59.5",
"@typescript-eslint/parser": "^5.59.5",
"eslint": "^8.40.0",
"eslint-plugin-inferno": "^7.32.2",
"eslint-plugin-jsx-a11y": "^6.7.1",
"eslint-plugin-prettier": "^4.2.1",
"husky": "^8.0.3",
"import-sort-style-module": "^6.0.0",
"lint-staged": "^13.2.2",
"prettier": "^2.8.8",
"prettier-plugin-import-sort": "^0.0.7",
"prettier-plugin-organize-imports": "^3.2.2",
"prettier-plugin-packagejson": "^2.4.3",
"rimraf": "^5.0.0",
"sortpack": "^2.3.4",
"style-loader": "^3.3.2",
"terser": "^5.17.3",
"typescript": "^5.0.4",
"typescript-language-server": "^3.3.2",
"webpack-bundle-analyzer": "^4.9.0",
"webpack-dev-server": "4.15.0"
},
"packageManager": "yarn@1.22.19",
"engines": {
"node": ">=8.9.0"
},
"engineStrict": true,
"importSort": {
".js, .jsx, .ts, .tsx": {
"style": "module",

View file

@ -1,3 +1,7 @@
.navbar-toggler {
border: 0px;
}
.navbar-expand-lg .navbar-nav .nav-link {
padding-right: 0.75rem !important;
padding-left: 0.75rem !important;
@ -21,7 +25,7 @@
}
.upvote:hover {
color: var(--bs-info);
color: var(--info);
}
.upvote {
@ -29,14 +33,14 @@
}
.downvote:hover {
color: var(--bs-danger);
color: var(--danger);
}
.downvote {
margin-top: -10px;
}
.form-select {
.custom-select {
-moz-appearance: none;
}
@ -46,7 +50,7 @@
}
.md-div p:last-child {
margin-bottom: 0;
margin-bottom: 0px;
}
.md-div img {
@ -58,35 +62,53 @@
.md-div h1 {
font-size: 2rem;
}
.md-div h2 {
font-size: 1.8rem;
}
.md-div h3 {
font-size: 1.6rem;
}
.md-div h4 {
font-size: 1.4rem;
}
.md-div h5 {
font-size: 1.2rem;
}
.md-div pre {
white-space: pre;
overflow-x: auto;
.md-div table {
border-collapse: collapse;
width: 100%;
margin-bottom: 1rem;
border: 1px solid var(--dark);
}
.md-div table th,
.md-div table td {
padding: 0.3rem;
vertical-align: top;
border-top: 1px solid var(--dark);
border: 1px solid var(--dark);
}
.md-div table thead th {
vertical-align: bottom;
border-bottom: 2px solid var(--dark);
}
.md-div table tbody + tbody {
border-top: 2px solid var(--dark);
}
.vote-bar {
min-width: 5ch;
margin-top: -6.5px;
}
.post-title a:visited:not(:hover) {
color: var(--bs-gray) !important;
.post-title {
line-height: 1;
}
.post-title a:visited {
color: var(--gray) !important;
}
.icon {
@ -107,53 +129,10 @@
user-select: none;
}
.icon-emoji {
width: 4em;
height: auto;
max-height: inherit;
}
.icon-emoji-admin {
max-width: 24px;
max-height: 24px;
display: inline-block;
}
.icon-inline {
margin-bottom: 2px;
}
.emoji-picker-container {
position: absolute;
top: 0;
left: 50%;
z-index: 1000;
transform: translateX(-50%);
}
@media only screen and (max-width: 992px) {
.emoji-picker-container {
width: 100vw;
transform: translateX(0%);
position: fixed;
left: 0;
}
.emoji-picker-container > section {
width: 100% !important;
}
}
.click-away-container {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.3);
z-index: 999;
}
.spinner-large {
display: grid;
display: block;
@ -170,14 +149,21 @@
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(359deg);
}
}
.dropdown-content {
position: absolute;
background-color: var(--light);
min-width: 160px;
box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2);
z-index: 2000;
}
blockquote {
border-left: 2px solid var(--bs-secondary);
border-left: 2px solid var(--secondary);
margin: 0.5em 5px;
padding: 0.1em 5px;
}
@ -192,16 +178,11 @@ blockquote {
overflow-y: auto;
}
.comments {
list-style: none;
padding: 0;
}
.thumbnail {
object-fit: cover;
aspect-ratio: 1/1;
width: 5rem;
height: 5rem;
min-height: 60px;
max-height: 80px;
width: 100%;
}
.thumbnail svg {
@ -216,7 +197,7 @@ blockquote {
}
hr {
border-top: 1px solid var(--bs-light);
border-top: 1px solid var(--light);
}
.emoji {
@ -228,10 +209,6 @@ hr {
text-overflow: ellipsis;
}
.text-xs-center {
text-align: center;
}
.overflow-wrap-anywhere {
overflow-wrap: anywhere;
}
@ -264,16 +241,11 @@ hr {
-ms-transform: scale(1.2);
}
/**
* TODO: Fix this in markup rather than this overly specific selector:
* https://getbootstrap.com/docs/5.3/components/buttons/#block-buttons
*/
.btn.d-block + .btn.d-block {
margin-top: 0.5rem;
}
.mini-overlay {
display: block;
position: absolute;
top: 0;
right: 0;
padding: 2px;
height: 1.5em;
width: 1.5em;
background: rgba(0, 0, 0, 0.4);
@ -333,6 +305,16 @@ pre {
transition: width 0.2s ease-out 0s !important;
}
.show-input {
width: 13em !important;
}
.hide-input {
background: transparent !important;
background-color: transparent !important;
width: 0px !important;
padding: 0 !important;
}
br.big {
display: block;
content: "";
@ -346,12 +328,10 @@ br.big {
}
.avatar-overlay {
width: 20vw;
height: 20vw;
width: 20%;
height: 20%;
max-width: 120px;
max-height: 120px;
min-width: 80px;
min-height: 80px;
}
.avatar-pushup {
@ -359,9 +339,8 @@ br.big {
}
.img-icon {
width: calc(var(--bs-body-line-height) * 1em);
height: calc(var(--bs-body-line-height) * 1em);
border-radius: 0.25em;
width: 2rem;
height: 2rem;
}
.tribute-container ul {
@ -369,65 +348,29 @@ br.big {
margin-top: 2px;
padding: 0;
list-style: none;
background: var(--bs-light);
background: var(--light);
}
.tribute-container li {
padding: 5px;
padding: 5px 5px;
cursor: pointer;
}
.tribute-container li.highlight {
background: var(--bs-primary);
background: var(--primary);
}
.tribute-container li span {
font-weight: bold;
}
.tribute-container li.no-match {
cursor: default;
}
.tribute-container .menu-highlighted {
font-weight: bold;
}
.honeypot {
display: none !important;
display:none !important;
}
.slight-radius {
border-radius: 4px;
}
.modlog-choices-font-size {
font-size: 0.9375rem !important;
}
.preview-lines {
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
}
#emoji-picker {
width: 100%;
}
.skip-link {
top: -40px;
transition: top 0.3s ease;
}
@media (prefers-reduced-motion: reduce) {
.skip-link {
transition: none;
}
}
.skip-link:focus {
top: 0;
}

View file

@ -0,0 +1,865 @@
//
// Variables
// --------------------------------------------------
//== Colors
//
//## Gray and brand colors for use across Bootstrap.
//// colors from bs-2
// Grays
// -------------------------
$black: #000;
$grayDark: #555;
$gray: #bbb;
$grayLight: #bbb;
$white: #fff;
// Accent colors
// -------------------------
$blue: #5555ff;
$cyan: #55ffff;
$cyanDark: #00aaaa;
$blueDark: #000084;
$green: #55ff55;
$greenDark: #00aa00;
$magenta: #ff55ff;
$magentaDark: #aa00aa;
$red: #ff5555;
$redDark: #aa0000;
$yellow: #fefe54;
$brown: #aa5500;
$orange: #a85400;
$pink: #fe54fe;
$purple: #fe5454;
// end colors
$gray-base: $gray;
$gray-darker: $grayDark;
$gray-dark: $grayDark;
$gray-light: $grayLight;
$gray-lighter: $grayLight;
$brand-primary: $gray;
$brand-primary-bg: $cyanDark;
$brand-success: $greenDark;
$brand-info: $brown;
$brand-warning: $magentaDark;
$brand-danger: $redDark;
//== Scaffolding
//
//## Settings for some of the most global styles.
//** Background color for `<body>`.
$body-bg: $blueDark;
//** Global text color on `<body>`.
$text-color: $gray-light;
//** Global textual link color.
$link-color: $brand-primary;
//** Link hover color set via `darken()` function.
$link-hover-color: $white;
//** Link hover decoration.
$link-hover-decoration: none;
//== Typography
//
//## Font, line-height, and color for body text, headings, and more.
$font-family-sans-serif: DOS, Monaco, Menlo, Consolas, "Courier New", monospace;
$font-family-serif: DOS, Monaco, Menlo, Consolas, "Courier New", monospace;
//** Default monospace fonts for `<code>`, `<kbd>`, and `<pre>`.
$font-family-monospace: DOS, Monaco, Menlo, Consolas, "Courier New", monospace;
$font-family-base: $font-family-sans-serif;
$baseWidth: 10px;
$font-size-base: 18px;
$font-size-large: $font-size-base;
$font-size-small: $font-size-base;
$font-size-h1: $font-size-base;
$font-size-h2: $font-size-base;
$font-size-h3: $font-size-base;
$font-size-h4: $font-size-base;
$font-size-h5: $font-size-base;
$font-size-h6: $font-size-base;
//** Unit-less `line-height` for use in components like buttons.
$baseLineHeight: 19px;
$line-height-base: $baseLineHeight;
//** Computed "line-height" (`font-size` * `line-height`) for use with `margin`, `padding`, etc.
$line-height-computed: $line-height-base;
//** By default, this inherits from the `<body>`.
$headings-font-family: inherit;
$headings-font-weight: normal;
$headings-line-height: $line-height-base;
$headings-color: inherit;
$space: $baseWidth;
$halfbaseLineHeight: ($baseLineHeight / 2);
$borderWidth: 2px;
$baseLineWidth: ($baseLineHeight / 2);
$halfSpace: ($baseWidth / 2);
$lhsNB: ($baseWidth / 2 + 1);
$rhsNB: ($baseWidth / 2 - 1);
$lhs: ($lhsNB - ($borderWidth));
$rhs: ($rhsNB - ($borderWidth / 2));
$tsNB: ($baseLineHeight / 2);
$bsNB: $tsNB;
$ts: ($tsNB - ($borderWidth / 2));
$bs: $ts;
$tsMargin: 3px;
//== Iconography
//
//## Specify custom location and filename of the included Glyphicons icon font. Useful for those including Bootstrap via Bower.
//** Load fonts from this directory.
$icon-font-path: "../fonts/";
//** File name for all font files.
$icon-font-name: "glyphicons-halflings-regular";
//** Element ID within SVG icon file.
$icon-font-svg-id: "glyphicons_halflingsregular";
//== Components
//
//## Define common padding and border radius sizes and more. Values based on 14px text and 1.428 line-height (~20px to start).
$padding-base-vertical: 0px;
$padding-base-horizontal: 0px;
$padding-large-vertical: 0px;
$padding-large-horizontal: $halfSpace;
$padding-small-vertical: 0px;
$padding-small-horizontal: 0px;
$padding-xs-vertical: 0px;
$padding-xs-horizontal: 0px;
$line-height-large: $baseLineHeight;
$line-height-small: $baseLineHeight;
$border-radius-base: 0;
$border-radius-large: 0;
$border-radius-small: 0;
//** Global color for active items (e.g., navs or dropdowns).
$component-active-color: $white;
//** Global background color for active items (e.g., navs or dropdowns).
$component-active-bg: $black;
//** Width of the `border` for generating carets that indicator dropdowns.
$caret-width-base: 4px;
//** Carets increase slightly in size for larger components.
$caret-width-large: 5px;
//== Tables
//
//## Customizes the `.table` component with basic values, each used across all table variations.
//** Padding for `<th>`s and `<td>`s.
$table-cell-padding: $ts $rhs $bs $lhs;
//** Padding for cells in `.table-condensed`.
$table-condensed-cell-padding: $ts $rhs $bs $lhs;
//** Default background color used for all tables.
$table-bg: transparent;
//** Background color used for `.table-striped`.
$table-bg-accent: $black;
//** Background color used for `.table-hover`.
$table-bg-hover: #f5f5f5;
$table-bg-active: $table-bg-hover;
//** Border color for table and cell borders.
$table-border-color: $gray;
//== Buttons
//
//## For each of Bootstrap's buttons, define text, background and border color.
$btn-font-weight: normal;
$btn-default-color: $black;
$btn-default-bg: $grayLight;
$btn-default-border: $grayLight;
$btn-primary-color: $black;
$btn-primary-bg: $cyanDark;
$btn-primary-border: $grayLight;
$btn-success-color: #fff;
$btn-success-bg: $brand-success;
$btn-success-border: $btn-success-bg;
$btn-info-color: #fff;
$btn-info-bg: $brand-info;
$btn-info-border: $btn-info-bg;
$btn-warning-color: #fff;
$btn-warning-bg: $brand-warning;
$btn-warning-border: $btn-warning-bg;
$btn-danger-color: #fff;
$btn-danger-bg: $brand-danger;
$btn-danger-border: $btn-danger-bg;
$btn-link-disabled-color: $gray-light;
//== Forms
//
//##
//** `<input>` background color
$input-bg: $cyanDark;
//** `<input disabled>` background color
$input-bg-disabled: $gray-lighter;
//** Text color for `<input>`s
$input-color: $white;
//** `<input>` border color
$input-border: #ccc;
// TODO: Rename `$input-border-radius` to `$input-border-radius-base` in v4
//** Default `.form-control` border radius
// This has no effect on `<select>`s in some browsers, due to the limited stylability of `<select>`s in CSS.
$input-border-radius: $border-radius-base;
//** Large `.form-control` border radius
$input-border-radius-large: $border-radius-large;
//** Small `.form-control` border radius
$input-border-radius-small: $border-radius-small;
//** Border color for inputs on focus
$input-border-focus: $black;
//** Placeholder text color
$input-color-placeholder: $black;
//** Default `.form-control` height
$input-height-base: $line-height-computed;
//** Large `.form-control` height
$input-height-large: $input-height-base;
//** Small `.form-control` height
$input-height-small: $input-height-base;
$legend-color: $gray-dark;
$legend-border-color: #e5e5e5;
//** Background color for textual input addons
$input-group-addon-bg: $gray-lighter;
//** Border color for textual input addons
$input-group-addon-border-color: $input-border;
//** Disabled cursor for form controls and buttons.
$cursor-disabled: not-allowed;
//== Dropdowns
//
//## Dropdown menu container and contents.
//** Background for the dropdown menu.
$dropdown-bg: $gray;
//** Dropdown menu `border-color`.
$dropdown-border: rgb(0, 0, 0);
//** Dropdown menu `border-color` **for IE8**.
$dropdown-fallback-border: #ccc;
//** Divider color for between dropdown items.
$dropdown-divider-bg: $black;
//** Dropdown link text color.
$dropdown-link-color: $black;
//** Hover color for dropdown links.
$dropdown-link-hover-color: $gray;
//** Hover background for dropdown links.
$dropdown-link-hover-bg: $black;
//** Active dropdown menu item text color.
$dropdown-link-active-color: $component-active-color;
//** Active dropdown menu item background color.
$dropdown-link-active-bg: $component-active-bg;
//** Disabled dropdown menu item background color.
$dropdown-link-disabled-color: $gray-light;
//** Text color for headers within dropdown menus.
$dropdown-header-color: $black;
//** Deprecated `$dropdown-caret-color` as of v3.1.0
$dropdown-caret-color: #000;
//-- Z-index master list
//
// Warning: Avoid customizing these values. They're used for a bird's eye view
// of components dependent on the z-axis and are designed to all work together.
//
// Note: These variables are not generated into the Customizer.
$zindex-navbar: 1000;
$zindex-dropdown: 1000;
$zindex-popover: 1060;
$zindex-tooltip: 1070;
$zindex-navbar-fixed: 1030;
$zindex-modal: 1040;
//== Media queries breakpoints
//
//## Define the breakpoints at which your layout will change, adapting to different screen sizes.
// Extra small screen / phone
//** Deprecated `$screen-xs` as of v3.0.1
$screen-xs: 480px;
//** Deprecated `$screen-xs-min` as of v3.2.0
$screen-xs-min: $screen-xs;
//** Deprecated `$screen-phone` as of v3.0.1
$screen-phone: $screen-xs-min;
// Small screen / tablet
//** Deprecated `$screen-sm` as of v3.0.1
$screen-sm: 768px;
$screen-sm-min: $screen-sm;
//** Deprecated `$screen-tablet` as of v3.0.1
$screen-tablet: $screen-sm-min;
// Medium screen / desktop
//** Deprecated `$screen-md` as of v3.0.1
$screen-md: 992px;
$screen-md-min: $screen-md;
//** Deprecated `$screen-desktop` as of v3.0.1
$screen-desktop: $screen-md-min;
// Large screen / wide desktop
//** Deprecated `$screen-lg` as of v3.0.1
$screen-lg: 1200px;
$screen-lg-min: $screen-lg;
//** Deprecated `$screen-lg-desktop` as of v3.0.1
$screen-lg-desktop: $screen-lg-min;
// So media queries don't overlap when required, provide a maximum
$screen-xs-max: ($screen-sm-min - 1);
$screen-sm-max: ($screen-md-min - 1);
$screen-md-max: ($screen-lg-min - 1);
//== Grid system
//
//## Define your custom responsive grid.
//** Number of columns in the grid.
$grid-columns: 12;
//** Padding between columns. Gets divided in half for the left and right.
$grid-gutter-width: ($baseWidth * 2);
// Navbar collapse
//** Point at which the navbar becomes uncollapsed.
$grid-float-breakpoint: $screen-sm-min;
//** Point at which the navbar begins collapsing.
$grid-float-breakpoint-max: ($grid-float-breakpoint);
//== Container sizes
//
//## Define the maximum width of `.container` for different screen sizes.
// Small screen / tablet
$container-tablet: (720px + $grid-gutter-width);
//** For `$screen-sm-min` and up.
$container-sm: $container-tablet;
// Medium screen / desktop
$container-desktop: (940px + $grid-gutter-width);
//** For `$screen-md-min` and up.
$container-md: $container-desktop;
// Large screen / wide desktop
$container-large-desktop: (1140px + $grid-gutter-width);
//** For `$screen-lg-min` and up.
$container-lg: $container-large-desktop;
//== Navbar
//
//##
// Basics of a navbar
$navbar-height: 0px;
$navbar-margin-bottom: $line-height-computed;
$navbar-border-radius: $border-radius-base;
$navbar-padding-horizontal: ($baseWidth * 2);
$navbar-padding-vertical: 0;
$navbar-collapse-max-height: 340px;
$navbar-default-color: $black;
$navbar-default-bg: $grayLight;
$navbar-default-border: $navbar-default-bg;
// Navbar links
$navbar-default-link-color: $black;
$navbar-default-link-hover-color: $white;
$navbar-default-link-hover-bg: $black;
$navbar-default-link-active-color: $white;
$navbar-default-link-active-bg: $black;
$navbar-default-link-disabled-color: $gray;
$navbar-default-link-disabled-bg: transparent;
// Navbar brand label
$navbar-default-brand-color: $navbar-default-link-color;
$navbar-default-brand-hover-color: $navbar-default-brand-color;
$navbar-default-brand-hover-bg: transparent;
// Navbar toggle
$navbar-default-toggle-hover-bg: #ddd;
$navbar-default-toggle-icon-bar-bg: #888;
$navbar-default-toggle-border-color: #ddd;
// Inverted navbar
// Reset inverted navbar basics
$navbar-inverse-color: $gray;
$navbar-inverse-bg: $black;
$navbar-inverse-border: $navbar-inverse-bg;
// Inverted navbar links
$navbar-inverse-link-color: $gray-light;
$navbar-inverse-link-hover-color: $black;
$navbar-inverse-link-hover-bg: $grayLight;
$navbar-inverse-link-active-color: $white;
$navbar-inverse-link-active-bg: $grayDark;
$navbar-inverse-link-disabled-color: $gray;
$navbar-inverse-link-disabled-bg: transparent;
// Inverted navbar brand label
$navbar-inverse-brand-color: $navbar-inverse-link-color;
$navbar-inverse-brand-hover-color: #fff;
$navbar-inverse-brand-hover-bg: transparent;
// Inverted navbar toggle
$navbar-inverse-toggle-hover-bg: $grayLight;
$navbar-inverse-toggle-icon-bar-bg: #fff;
$navbar-inverse-toggle-border-color: #333;
//== Navs
//
//##
//=== Shared nav styles
$nav-link-padding: 0 $baseWidth;
$nav-link-hover-bg: $gray-lighter;
$nav-disabled-link-color: $gray-light;
$nav-disabled-link-hover-color: $gray-light;
//== Tabs
$nav-tabs-border-color: #ddd;
$nav-tabs-link-hover-border-color: $gray-lighter;
$nav-tabs-active-link-hover-bg: $black;
$nav-tabs-active-link-hover-color: $white;
$nav-tabs-justified-active-link-border-color: $body-bg;
//== Pills
$nav-pills-border-radius: $border-radius-base;
$nav-pills-active-link-hover-bg: $component-active-bg;
$nav-pills-active-link-hover-color: $component-active-color;
//== Pagination
//
//##
$pagination-color: $black;
$pagination-bg: $gray;
$pagination-border: #ddd;
$pagination-hover-color: $link-hover-color;
$pagination-hover-bg: $gray-lighter;
$pagination-hover-border: #ddd;
$pagination-active-color: #fff;
$pagination-active-bg: $brand-primary;
$pagination-active-border: $brand-primary;
$pagination-disabled-color: $gray-light;
$pagination-disabled-bg: #fff;
$pagination-disabled-border: #ddd;
//== Pager
//
//##
$pager-bg: $pagination-bg;
$pager-border: $pagination-border;
$pager-border-radius: 0;
$pager-hover-bg: $pagination-hover-bg;
$pager-active-bg: $pagination-active-bg;
$pager-active-color: $pagination-active-color;
$pager-disabled-color: $pagination-disabled-color;
//== Jumbotron
//
//##
$jumbotron-padding: ($ts) ($rhs + $baseWidth) ($bs) ($lhs + $baseWidth);
$jumbotron-color: $white;
$jumbotron-bg: transparent;
$jumbotron-heading-color: inherit;
$jumbotron-font-size: $font-size-base;
//== Form states and alerts
//
//## Define colors for form feedback states and, by default, alerts.
$state-success-text: $green;
$state-success-bg: $greenDark;
$state-success-border: $state-success-bg;
$state-info-text: $yellow;
$state-info-bg: $brown;
$state-info-border: $state-info-bg;
$state-warning-text: $magenta;
$state-warning-bg: $magentaDark;
$state-warning-border: $state-warning-bg;
$state-danger-text: $red;
$state-danger-bg: $black;
$state-danger-border: $state-danger-bg;
//== Tooltips
//
//##
//** Tooltip max width
$tooltip-max-width: ($baseWidth * 25);
//** Tooltip text color
$tooltip-color: $white;
//** Tooltip background color
$tooltip-bg: $grayDark;
$tooltip-opacity: 1;
//** Tooltip arrow width
$tooltip-arrow-width: 0px;
//** Tooltip arrow color
$tooltip-arrow-color: $tooltip-bg;
//== Popovers
//
//##
//** Popover body background color
$popover-bg: $gray;
//** Popover maximum width
$popover-max-width: ($baseWidth * 20);
//** Popover border color
$popover-border-color: rgb(0, 0, 0);
//** Popover fallback border color
$popover-fallback-border-color: #ccc;
//** Popover title background color
$popover-title-bg: $greenDark;
//** Popover arrow width
$popover-arrow-width: 10px;
//** Popover arrow color
$popover-arrow-color: $popover-bg;
//** Popover outer arrow width
$popover-arrow-outer-width: ($popover-arrow-width + 1);
//** Popover outer arrow color
$popover-arrow-outer-color: $popover-border-color;
//** Popover outer arrow fallback color
$popover-arrow-outer-fallback-color: $popover-fallback-border-color;
//== Labels
//
//##
//** Default label background color
$label-default-bg: $gray-light;
//** Primary label background color
$label-primary-bg: $brand-primary-bg;
//** Success label background color
$label-success-bg: $brand-success;
//** Info label background color
$label-info-bg: $brand-info;
//** Warning label background color
$label-warning-bg: $brand-warning;
//** Danger label background color
$label-danger-bg: $brand-danger;
//** Default label text color
$label-color: #fff;
//** Default text color of a linked label
$label-link-hover-color: #fff;
//== Modals
//
//##
//** Padding applied to the modal body
$modal-inner-padding: 0 $baseWidth;
//** Padding applied to the modal title
$modal-title-padding: 0 $baseWidth;
//** Modal title line-height
$modal-title-line-height: $line-height-base;
//** Background color of modal content area
$modal-content-bg: $gray;
//** Modal content border color
$modal-content-border-color: rgb(0, 0, 0);
//** Modal content border color **for IE8**
$modal-content-fallback-border-color: #999;
//** Modal backdrop background color
$modal-backdrop-bg: #000;
//** Modal backdrop opacity
// $modal-backdrop-opacity: @include 5;
//** Modal header border color
$modal-header-border-color: #e5e5e5;
//** Modal footer border color
$modal-footer-border-color: $modal-header-border-color;
$modal-lg: 900px;
$modal-md: 600px;
$modal-sm: 300px;
//== Alerts
//
//## Define alert colors, border radius, and padding.
$alert-padding: $line-height-base ($baseWidth * 2);
$alert-border-radius: $border-radius-base;
$alert-link-font-weight: normal;
$alert-success-bg: $state-success-bg;
$alert-success-text: $state-success-text;
$alert-success-border: $state-success-border;
$alert-info-bg: $state-info-bg;
$alert-info-text: $state-info-text;
$alert-info-border: $state-info-border;
$alert-warning-bg: $state-warning-bg;
$alert-warning-text: $state-warning-text;
$alert-warning-border: $state-warning-border;
$alert-danger-bg: $state-danger-bg;
$alert-danger-text: $state-danger-text;
$alert-danger-border: $state-danger-border;
//== Progress bars
//
//##
//** Background color of the whole progress component
$progress-bg: $black;
//** Progress bar text color
$progress-bar-color: $black;
//** Variable for setting rounded corners on progress bar.
$progress-border-radius: $border-radius-base;
//** Default progress bar color
$progress-bar-bg: $brand-primary;
//** Success progress bar color
$progress-bar-success-bg: $brand-success;
//** Warning progress bar color
$progress-bar-warning-bg: $brand-warning;
//** Danger progress bar color
$progress-bar-danger-bg: $brand-danger;
//** Info progress bar color
$progress-bar-info-bg: $brand-info;
//== List group
//
//##
//** Background color on `.list-group-item`
$list-group-bg: $gray;
//** `.list-group-item` border color
$list-group-border: #ddd;
//** List group border radius
$list-group-border-radius: $border-radius-base;
//** Background color of single list items on hover
$list-group-hover-bg: $black;
//** Text color of active list items
$list-group-active-color: $component-active-color;
//** Background color of active list items
$list-group-active-bg: $component-active-bg;
//** Border color of active list elements
$list-group-active-border: $list-group-active-bg;
//** Text color for content within active list items
$list-group-active-text-color: $component-active-color;
//** Text color of disabled list items
$list-group-disabled-color: $gray-dark;
//** Background color of disabled list items
$list-group-disabled-bg: $gray-lighter;
//** Text color for content within disabled list items
$list-group-disabled-text-color: $list-group-disabled-color;
$list-group-link-color: $black;
$list-group-link-hover-color: $list-group-link-color;
$list-group-link-heading-color: #333;
//== Panels
//
//##
$panel-bg: $gray;
$panel-body-padding: 0 $rhsNB 0 $lhsNB;
$panel-heading-padding: 0 $rhsNB 0 $lhsNB;
$panel-footer-padding: $panel-heading-padding;
$panel-border-radius: $border-radius-base;
//** Border color for elements within panels
$panel-inner-border: #ddd;
$panel-footer-bg: #f5f5f5;
$panel-default-text: $white;
$panel-default-border: #ddd;
$panel-default-heading-bg: $grayDark;
$panel-primary-text: $white;
$panel-primary-border: $brand-primary;
$panel-primary-heading-bg: $cyanDark;
$panel-success-text: $state-success-text;
$panel-success-border: $state-success-border;
$panel-success-heading-bg: $state-success-bg;
$panel-info-text: $state-info-text;
$panel-info-border: $state-info-border;
$panel-info-heading-bg: $state-info-bg;
$panel-warning-text: $state-warning-text;
$panel-warning-border: $state-warning-border;
$panel-warning-heading-bg: $state-warning-bg;
$panel-danger-text: $state-danger-text;
$panel-danger-border: $state-danger-border;
$panel-danger-heading-bg: $state-danger-bg;
//== Thumbnails
//
//##
//** Padding around the thumbnail image
$thumbnail-padding: 4px;
//** Thumbnail background color
$thumbnail-bg: $body-bg;
//** Thumbnail border color
$thumbnail-border: #ddd;
//** Thumbnail border radius
$thumbnail-border-radius: $border-radius-base;
//** Custom text color for thumbnail captions
$thumbnail-caption-color: $text-color;
//** Padding around the thumbnail caption
$thumbnail-caption-padding: 9px;
//== Wells
//
//##
$well-bg: $greenDark;
$well-border: $well-bg;
//== Badges
//
//##
$badge-color: $black;
//** Linked badge text color on hover
$badge-link-hover-color: #fff;
$badge-bg: $gray-light;
//** Badge text color in active nav link
$badge-active-color: $link-color;
//** Badge background color in active nav link
$badge-active-bg: $black;
$badge-font-weight: normal;
$badge-line-height: $line-height-base;
$badge-border-radius: 0;
//== Breadcrumbs
//
//##
$breadcrumb-padding-vertical: 8px;
$breadcrumb-padding-horizontal: 15px;
//** Breadcrumb background color
$breadcrumb-bg: #f5f5f5;
//** Breadcrumb text color
$breadcrumb-color: #ccc;
//** Text color of current page in the breadcrumb
$breadcrumb-active-color: $gray-light;
//** Textual separator for between breadcrumb elements
$breadcrumb-separator: "/";
//== Carousel
//
//##
$carousel-text-shadow: none;
$carousel-control-color: #fff;
$carousel-control-width: 15%;
$carousel-control-opacity: 1;
$carousel-control-font-size: $font-size-base;
$carousel-indicator-active-bg: #fff;
$carousel-indicator-border-color: #fff;
$carousel-caption-color: #fff;
//== Close
//
//##
$close-font-weight: normal;
$close-color: #000;
$close-text-shadow: none;
//== Code
//
//##
$code-color: #c7254e;
$code-bg: #f9f2f4;
$kbd-color: #fff;
$kbd-bg: #333;
$pre-bg: #f5f5f5;
$pre-color: $gray-dark;
$pre-border-color: #ccc;
$pre-scrollable-max-height: 340px;
//== Type
//
//##
//** Horizontal offset for forms and lists.
$component-offset-horizontal: 180px;
//** Text muted color
$text-muted: $gray-dark;
//** Abbreviations and acronyms border color
$abbr-border-color: $gray-light;
//** Headings small color
$headings-small-color: $gray-light;
//** Blockquote small color
$blockquote-small-color: $gray-light;
//** Blockquote font size
$blockquote-font-size: $font-size-base;
//** Blockquote border color
$blockquote-border-color: $gray-lighter;
//** Page header border color
$page-header-border-color: $gray-lighter;
//** Width of horizontal description list titles
$dl-horizontal-offset: $component-offset-horizontal;
//** Horizontal line color.
$hr-border: $black;

View file

@ -1 +0,0 @@
@import "variables.darkly";

View file

@ -1,117 +0,0 @@
@import "./variables";
// Colors
$white: #f3f3f3;
$gray-200: #ebebeb;
$gray-300: #dee2e6;
$gray-500: #adb5bd;
$gray-600: #666;
$gray-700: #333;
$gray-800: #202020;
$gray-900: #111;
$black: #000;
$blue: #375a7f;
$red: #e74c3c;
$yellow: #f39c12;
$green: #00bc8c;
$cyan: #3498db;
$primary: $green;
$secondary: $gray-700;
$success: $green;
$dark: $gray-300;
$body-color: $gray-200;
$body-bg: $black;
$link-color: $success;
$border-color: rgba($body-color, 0.25);
$mark-bg: $gray-900;
$text-muted: $gray-600;
$yiq-contrasted-threshold: 175;
$font-family-sans-serif: "Lato", -apple-system, BlinkMacSystemFont, "Segoe UI",
Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji",
"Segoe UI Emoji", "Segoe UI Symbol";
$font-size-base: 0.9375rem;
$h1-font-size: 3rem;
$h2-font-size: 2.5rem;
$h3-font-size: 2rem;
$card-cap-bg: $gray-900;
$card-bg: $gray-900;
$card-color: $gray-300;
$navbar-padding-y: 1rem;
$navbar-dark-color: rgba($white, 0.6);
$navbar-dark-hover-color: $white;
$navbar-light-color: rgba($white, 0.6);
$navbar-light-hover-color: $white;
$navbar-light-active-color: $white;
$navbar-light-toggler-border-color: rgba($gray-900, 0.1);
$navbar-light-brand-color: $white;
$navbar-light-brand-hover-color: $navbar-light-brand-color;
$nav-link-padding-x: 2rem;
$nav-link-disabled-color: $gray-500;
$nav-tabs-border-color: $gray-700;
$nav-tabs-link-hover-border-color: $nav-tabs-border-color $nav-tabs-border-color
transparent;
$nav-tabs-link-active-color: $white;
$nav-tabs-link-active-border-color: $nav-tabs-border-color
$nav-tabs-border-color transparent;
$input-bg: $gray-900;
$input-color: $white;
$input-disabled-bg: darken($gray-900, 20%);
$input-border-color: $gray-800;
$input-group-addon-color: $gray-800;
$input-group-addon-bg: $gray-800;
$hr-border-color: rgba($body-color, 0.25);
$table-border-color: $gray-700;
$custom-file-color: $gray-500;
$custom-file-border-color: $body-bg;
$dropdown-bg: $gray-900;
$dropdown-border-color: $gray-800;
$dropdown-divider-bg: $gray-700;
$dropdown-link-color: $white;
$dropdown-link-hover-color: $white;
$dropdown-link-hover-bg: $primary;
$pagination-color: $white;
$pagination-bg: $success;
$pagination-border-width: 0;
$pagination-border-color: transparent;
$pagination-hover-color: $white;
$pagination-hover-bg: lighten($success, 10%);
$pagination-hover-border-color: transparent;
$pagination-active-bg: $pagination-hover-bg;
$pagination-active-border-color: transparent;
$pagination-disabled-color: $white;
$pagination-disabled-bg: darken($success, 15%);
$pagination-disabled-border-color: transparent;
$jumbotron-bg: $gray-900;
$popover-bg: $gray-900;
$popover-header-bg: $gray-900;
$toast-background-color: $gray-800;
$toast-header-background-color: $gray-900;
$modal-content-bg: $gray-800;
$modal-content-border-color: $gray-700;
$modal-header-border-color: $gray-700;
$progress-bg: $gray-700;
$list-group-bg: $gray-800;
$list-group-border-color: $gray-700;
$list-group-hover-bg: $gray-700;
$breadcrumb-bg: $gray-700;
$close-color: $white;
$close-text-shadow: none;
$pre-color: inherit;
$custom-select-bg: $gray-700;
$custom-select-color: $white;
$light: $gray-900;

View file

@ -1,7 +0,0 @@
@import "variables.darkly";
$primary: $blue;
$secondary: #444;
$light: $gray-800;
$link-color: $red;

View file

@ -1,44 +1,60 @@
@import "./variables";
// Colors
$white: #fff;
$gray-100: #f8f9fa;
$gray-200: #ebebeb;
$gray-300: #dee2e6;
$gray-400: #ced4da;
$gray-500: #adb5bd;
$gray-600: #888;
$gray-700: #444;
$gray-800: #303030;
$gray-900: #222;
$black: #000;
$blue: #375a7f;
$indigo: #6610f2;
$purple: #6f42c1;
$pink: #e83e8c;
$red: #e74c3c;
$orange: #fd7e14;
$yellow: #f39c12;
$green: #00bc8c;
$teal: #20c997;
$cyan: #3498db;
$primary: $green;
$primary: $blue;
$secondary: $gray-700;
$success: $green;
$info: $cyan;
$warning: $yellow;
$danger: $red;
$dark: $gray-300;
$body-color: $gray-300;
$body-bg: $gray-900;
$link-color: $success;
$border-color: rgba($body-color, 0.25);
$mark-bg: #333;
$text-muted: $gray-600;
$yiq-contrasted-threshold: 175;
$font-family-sans-serif: "Lato", -apple-system, BlinkMacSystemFont, "Segoe UI",
Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji",
"Segoe UI Emoji", "Segoe UI Symbol";
$body-bg: $gray-900;
$body-color: $gray-300;
$link-color: $success;
$font-family-sans-serif: "Lato", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
$font-size-base: 0.9375rem;
$h1-font-size: 3rem;
$h2-font-size: 2.5rem;
$h3-font-size: 2rem;
$card-cap-bg: $gray-700;
$card-bg: $gray-800;
$text-muted: $gray-600;
$table-accent-bg: $gray-800;
$table-border-color: $gray-700;
$input-border-color: $body-bg;
$input-group-addon-color: $gray-500;
$input-group-addon-bg: $gray-700;
$custom-file-color: $gray-500;
$custom-file-border-color: $body-bg;
$dropdown-bg: $gray-900;
$dropdown-border-color: $gray-700;
$dropdown-divider-bg: $gray-700;
$dropdown-link-color: $white;
$dropdown-link-hover-color: $white;
$dropdown-link-hover-bg: $primary;
$nav-link-padding-x: 2rem;
$nav-link-disabled-color: $gray-500;
$nav-tabs-border-color: $gray-700;
$nav-tabs-link-hover-border-color: $nav-tabs-border-color $nav-tabs-border-color transparent;
$nav-tabs-link-active-color: $white;
$nav-tabs-link-active-border-color: $nav-tabs-border-color $nav-tabs-border-color transparent;
$navbar-padding-y: 1rem;
$navbar-dark-color: rgba($white, 0.6);
$navbar-dark-hover-color: $white;
@ -46,40 +62,6 @@ $navbar-light-color: rgba($white, 0.6);
$navbar-light-hover-color: $white;
$navbar-light-active-color: $white;
$navbar-light-toggler-border-color: rgba($gray-900, 0.1);
$navbar-light-brand-color: $white;
$navbar-light-brand-hover-color: $navbar-light-brand-color;
$nav-link-padding-x: 2rem;
$nav-link-disabled-color: $gray-500;
$nav-tabs-border-color: $gray-700;
$nav-tabs-link-hover-border-color: $nav-tabs-border-color $nav-tabs-border-color
transparent;
$nav-tabs-link-active-color: $white;
$nav-tabs-link-active-border-color: $nav-tabs-border-color
$nav-tabs-border-color transparent;
$input-bg: $gray-700;
$input-color: $white;
$input-disabled-bg: darken($gray-700, 10%);
$input-border-color: $body-bg;
$input-group-addon-color: $gray-500;
$input-group-addon-bg: $gray-700;
$hr-border-color: rgba($body-color, 0.25);
$table-border-color: $gray-700;
$custom-file-color: $gray-500;
$custom-file-border-color: $body-bg;
$dropdown-bg: $gray-900;
$dropdown-border-color: $gray-700;
$dropdown-divider-bg: $gray-700;
$dropdown-link-color: $white;
$dropdown-link-hover-color: $white;
$dropdown-link-hover-bg: $primary;
$pagination-color: $white;
$pagination-bg: $success;
$pagination-border-width: 0;
@ -92,8 +74,9 @@ $pagination-active-border-color: transparent;
$pagination-disabled-color: $white;
$pagination-disabled-bg: darken($success, 15%);
$pagination-disabled-border-color: transparent;
$jumbotron-bg: $gray-800;
$card-cap-bg: $gray-700;
$card-bg: $gray-800;
$popover-bg: $gray-800;
$popover-header-bg: $gray-700;
$toast-background-color: $gray-700;
@ -109,6 +92,12 @@ $breadcrumb-bg: $gray-700;
$close-color: $white;
$close-text-shadow: none;
$pre-color: inherit;
$custom-select-bg: $gray-700;
$mark-bg: #333;
$custom-select-bg: $secondary;
$custom-select-color: $white;
$input-bg: $secondary;
$input-color: $white;
$input-disabled-bg: darken($secondary, 10%);
$light: $gray-800;
$navbar-light-brand-color: $navbar-dark-active-color;
$navbar-light-brand-hover-color: $navbar-dark-active-color;

View file

@ -1,14 +1,3 @@
@import "./variables";
// Colors
$white: #fff;
$gray-100: #f8f9fa;
$gray-200: #ebebeb;
$gray-300: #bbb;
$gray-500: #adb5bd;
$gray-800: #303030;
$gray-900: #222;
$blue: #5555ff;
$cyan: #55ffff;
$green: #55ff55;
@ -18,41 +7,33 @@ $yellow: #fefe54;
$orange: #a85400;
$pink: #fe54fe;
$purple: #fe5454;
$primary: #fefe54;
$secondary: $gray-900;
$body-bg: #000084;
$gray-300: #bbb;
$body-color: $gray-300;
$link-hover-color: $white;
$font-family-sans-serif: DOS, Monaco, Menlo, Consolas, "Courier New", monospace;
$font-family-monospace: DOS, Monaco, Menlo, Consolas, "Courier New", monospace;
$navbar-dark-color: $gray-300;
$navbar-light-brand-color: $gray-300;
$success: #00aa00;
$danger: #aa0000;
$info: #00aaaa;
$warning: #aa00aa;
$light: $gray-800;
$dark: black;
$body-bg: #000084;
$body-color: $gray-300;
$link-hover-color: $white;
$font-family-sans-serif: DOS, Monaco, Menlo, Consolas, "Courier New", monospace;
$font-family-monospace: DOS, Monaco, Menlo, Consolas, "Courier New", monospace;
$navbar-dark-color: $gray-300;
$navbar-light-brand-color: $gray-300;
$navbar-dark-active-color: $gray-100;
$enable-rounded: false;
$input-color: $white;
$input-bg: rgb(102, 102, 102);
$input-disabled-bg: $gray-800;
$nav-tabs-link-active-color: $gray-100;
$navbar-dark-hover-color: rgba($gray-300, 0.75);
$light: $gray-800;
$navbar-light-disabled-color: $gray-800;
$navbar-light-active-color: $gray-100;
$navbar-light-hover-color: $gray-200;
$navbar-light-color: $gray-300;
$enable-rounded: false;
$input-color: $white;
$input-bg: rgb(102, 102, 102);
$input-placeholder-color: $gray-500;
$input-disabled-bg: $gray-800;
$card-bg: $gray-800;
$card-border-color: $white;
$input-placeholder-color: $gray-500;
$mark-bg: #463b00;
$secondary: $gray-900;

View file

@ -1 +0,0 @@
@import "variables.litely";

View file

@ -1,4 +0,0 @@
@import "variables.litely";
$secondary: #c80000;
$danger: darken($primary, 24%);

View file

@ -1,52 +1,32 @@
@import "./variables";
// Colors
$gray-100: #f8f9fa;
$gray-600: #6c757d;
$gray-700: #495057;
$gray-800: #343a40;
$gray-900: #212529;
$black: #222;
$blue: #007bff;
$indigo: #6610f2;
$red: #d8486a;
$white: #ffffff;
$orange: #f1641e;
$green: #00a846;
$cyan: #02bdc2;
$primary: $orange;
$green: #00c853;
$secondary: $green;
$success: $indigo;
$info: $blue;
$danger: darken($primary, 25%);
$body-color: $gray-700;
$body-bg: #fff;
$link-color: $primary;
$border-color: rgba($body-color, 0.25);
$mark-bg: rgb(255, 252, 239);
$headings-color: $gray-700;
$font-family-sans-serif: -apple-system, BlinkMacSystemFont, "Droid Sans",
"Segoe UI", "Helvetica", Arial, sans-serif;
$font-weight-bold: 600;
$card-color: $gray-700;
$card-cap-color: $gray-700;
$card-bg: $gray-100;
$navbar-dark-toggler-border-color: rgba($black, 0.1);
$navbar-light-color: $gray-600;
$navbar-light-hover-color: $gray-900;
$navbar-light-active-color: $gray-900;
$form-feedback-valid-color: $info;
$input-btn-focus-color: rgba($primary, 0.75);
$link-color: theme-color("primary");
$primary: $orange;
$red: #d8486a;
$border-radius: 0.5rem;
$border-radius-lg: 0.5rem;
$border-radius-sm: 1rem;
$font-family-sans-serif: -apple-system, BlinkMacSystemFont, "Droid Sans", "Segoe UI", "Helvetica", Arial, sans-serif;
$headings-color: $gray-700;
$input-btn-focus-color: rgba($component-active-bg, 0.75);
$form-feedback-valid-color: theme-color("info");
$navbar-light-color: $gray-600;
$black: #222222;
$navbar-dark-toggler-border-color: rgba($black, 0.1);
$navbar-light-active-color: $gray-900;
$card-color: $gray-700;
$card-cap-color: $gray-700;
$info: $blue;
$body-bg: #fff;
$success: $indigo;
$danger: darken($primary, 25%);
$navbar-light-hover-color: $gray-900;
$card-bg: $gray-100;
$border-color: $gray-700;
$mark-bg: rgb(255, 252, 239);
$font-weight-bold: 600;
$rounded-pill: 0.25rem;
$hr-border-color: rgba($body-color, 0.25);

View file

@ -1,3 +0,0 @@
$link-decoration: none;
$min-contrast-ratio: 3;
$font-size-base: 0.875rem;

View file

@ -0,0 +1,37 @@
$blue: #01cdfe;
$indigo: #b967ff;
$purple: #b967ff;
$pink: rgb(255, 64, 186);
$red: rgb(255, 95, 110);
$orange: rgb(255, 167, 93);
$yellow: #fffb96;
$green: #05ffa1;
$teal: #01cdfe;
$cyan: #01cdfe;
$enable-shadows: true;
$enable-gradients: true;
$enable-responsive-font-sizes: true;
$body-bg: $gray-900;
$body-color: $gray-200;
$border-radius: 1rem;
$border-radius-lg: 1rem;
$font-family-monospace: Arial, "Noto Sans", sans-serif;
$yiq-text-light: $gray-300;
$secondary: $blue;
$text-muted: $gray-500;
$primary: $pink;
$navbar-light-hover-color: rgba($primary, 0.7);
$light: darken($gray-100, 1.5);
$font-family-sans-serif: "Lucida Console", Monaco, monospace;
$card-bg: $body-bg;
$navbar-dark-color: rgba($body-bg, 0.5);
$navbar-light-active-color: rgba($gray-200, 0.9);
$navbar-light-disabled-color: rgba($gray-200, 0.3);
$navbar-light-color: rgba($white, 0.5);
$input-bg: $gray-700;
$input-color: $gray-200;
$input-disabled-bg: $gray-800;
$input-border-color: $gray-800;
$mark-bg: $gray-600;
$pre-color: $gray-200;
mark-bg: $gray-600;

View file

@ -0,0 +1,25 @@
$blue: #01cdfe;
$indigo: #b967ff;
$purple: #b967ff;
$pink: rgb(255, 64, 186);
$red: rgb(255, 95, 110);
$orange: rgb(255, 167, 93);
$yellow: #fffb96;
$green: #05ffa1;
$teal: #01cdfe;
$cyan: #01cdfe;
$enable-shadows: true;
$enable-gradients: true;
$enable-responsive-font-sizes: true;
$body-bg: $gray-100;
$body-color: $gray-700;
$border-radius: 1rem;
$border-radius-lg: 1rem;
$font-family-monospace: Arial, "Noto Sans", sans-serif;
$yiq-text-light: $gray-300;
$secondary: $blue;
$text-muted: $gray-500;
$primary: $pink;
$navbar-light-hover-color: rgba($primary, 0.7);
$light: darken($gray-100, 1.5);
$font-family-sans-serif: "Lucida Console", Monaco, monospace;

83
src/assets/css/themes/cyborg.min.css vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

View file

@ -1,59 +0,0 @@
@import "variables.darkly-compact";
/*
GENERAL
*/
// Desktop Breakpoint
$container-max-widths: (
lg: 1920px,
);
// Reduce hr height
hr.my-3 {
margin-top: 0.5rem !important;
margin-bottom: 0.5rem !important;
}
/*
POST-LISTING
*/
.post-listing {
line-height: 1;
.post-title h5 {
margin: 0;
}
.post-title + p {
padding-top: 0.125rem !important;
padding-bottom: 0.125rem !important;
}
.community-link {
padding-left: 0.125rem;
}
.person-listing {
padding-right: 0.125rem;
}
ul.list-inline {
&.mt-2 {
margin-top: 0.125rem !important;
}
&.mb-1 {
margin-bottom: 0.125rem !important;
}
}
.btn-sm {
--bs-btn-padding-y: 0;
}
.img-icon {
display: none;
}
}
@import "../../../../node_modules/bootstrap/scss/bootstrap";

File diff suppressed because it is too large Load diff

View file

@ -1,2 +0,0 @@
@import "variables.darkly-pureblack";
@import "../../../../node_modules/bootstrap/scss/bootstrap";

File diff suppressed because it is too large Load diff

View file

@ -1,2 +0,0 @@
@import "variables.darkly-red";
@import "../../../../node_modules/bootstrap/scss/bootstrap";

File diff suppressed because it is too large Load diff

1
src/assets/css/themes/darkly.min.css vendored Normal file

File diff suppressed because one or more lines are too long

View file

@ -1,2 +0,0 @@
@import "variables.darkly";
@import "../../../../node_modules/bootstrap/scss/bootstrap";

File diff suppressed because it is too large Load diff

1
src/assets/css/themes/i386.min.css vendored Normal file

File diff suppressed because one or more lines are too long

View file

@ -1,16 +0,0 @@
@import "variables.i386";
@import "../../../../node_modules/bootstrap/scss/bootstrap";
.btn-outline-secondary {
color: $gray-500;
}
.dropdown-item.active,
.dropdown-item:hover,
option:disabled {
color: $secondary;
}
.input-group-text {
background: $gray-500;
}

12
src/assets/css/themes/journal.min.css vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

View file

@ -1,59 +0,0 @@
@import "variables.litely-compact";
/*
GENERAL
*/
// Desktop Breakpoint
$container-max-widths: (
lg: 1920px,
);
// Reduce hr height
hr.my-3 {
margin-top: 0.5rem !important;
margin-bottom: 0.5rem !important;
}
/*
POST-LISTING
*/
.post-listing {
line-height: 1;
.post-title h5 {
margin: 0;
}
.post-title + p {
padding-top: 0.125rem !important;
padding-bottom: 0.125rem !important;
}
.community-link {
padding-left: 0.125rem;
}
.person-listing {
padding-right: 0.125rem;
}
ul.list-inline {
&.mt-2 {
margin-top: 0.125rem !important;
}
&.mb-1 {
margin-bottom: 0.125rem !important;
}
}
.btn-sm {
--bs-btn-padding-y: 0;
}
.img-icon {
display: none;
}
}
@import "../../../../node_modules/bootstrap/scss/bootstrap";

File diff suppressed because it is too large Load diff

View file

@ -1,2 +0,0 @@
@import "variables.litely-red";
@import "../../../../node_modules/bootstrap/scss/bootstrap";

File diff suppressed because it is too large Load diff

1
src/assets/css/themes/litely.min.css vendored Normal file

File diff suppressed because one or more lines are too long

View file

@ -1,2 +0,0 @@
@import "variables.litely";
@import "../../../../node_modules/bootstrap/scss/bootstrap";

12
src/assets/css/themes/litera.min.css vendored Normal file

File diff suppressed because one or more lines are too long

12
src/assets/css/themes/materia.min.css vendored Normal file

File diff suppressed because one or more lines are too long

12
src/assets/css/themes/minty.min.css vendored Normal file

File diff suppressed because one or more lines are too long

12
src/assets/css/themes/nord.min.css vendored Normal file

File diff suppressed because one or more lines are too long

12
src/assets/css/themes/sketchy.min.css vendored Normal file

File diff suppressed because one or more lines are too long

68
src/assets/css/themes/solar.min.css vendored Normal file

File diff suppressed because one or more lines are too long

12
src/assets/css/themes/united.min.css vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,49 @@
{
"name": "Lemmy",
"description": "A link aggregator for the fediverse",
"start_url": "/",
"display": "standalone",
"background_color": "#222222",
"icons": [
{
"src": "/static/assets/icons/icon-72x72.png",
"sizes": "72x72",
"type": "image/png"
},
{
"src": "/static/assets/icons/icon-96x96.png",
"sizes": "96x96",
"type": "image/png"
},
{
"src": "/static/assets/icons/icon-128x128.png",
"sizes": "128x128",
"type": "image/png"
},
{
"src": "/static/assets/icons/icon-144x144.png",
"sizes": "144x144",
"type": "image/png"
},
{
"src": "/static/assets/icons/icon-152x152.png",
"sizes": "152x152",
"type": "image/png"
},
{
"src": "/static/assets/icons/icon-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/static/assets/icons/icon-384x384.png",
"sizes": "384x384",
"type": "image/png"
},
{
"src": "/static/assets/icons/icon-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
]
}

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 53 KiB

View file

@ -1,28 +1,15 @@
import { initializeSite, setupDateFns } from "@utils/app";
import { hydrate } from "inferno-hydrate";
import { Router } from "inferno-router";
import { BrowserRouter } from "inferno-router";
import { App } from "../shared/components/app/app";
import { HistoryService } from "../shared/services";
import { initializeSite } from "../shared/utils";
import "bootstrap/js/dist/collapse";
import "bootstrap/js/dist/dropdown";
const site = window.isoData.site_res;
initializeSite(site);
async function startClient() {
initializeSite(window.isoData.site_res);
const wrapper = (
<BrowserRouter>
<App siteRes={window.isoData.site_res} />
</BrowserRouter>
);
await setupDateFns();
const wrapper = (
<Router history={HistoryService.history}>
<App />
</Router>
);
const root = document.getElementById("root");
if (root) {
hydrate(wrapper, root);
}
}
startClient();
hydrate(wrapper, document.getElementById("root"));

View file

@ -1,133 +0,0 @@
import { initializeSite, isAuthPath } from "@utils/app";
import { getHttpBaseInternal } from "@utils/env";
import { ErrorPageData } from "@utils/types";
import * as cookie from "cookie";
import fetch from "cross-fetch";
import type { Request, Response } from "express";
import { StaticRouter, matchPath } from "inferno-router";
import { renderToString } from "inferno-server";
import { GetSite, GetSiteResponse, LemmyHttp } from "lemmy-js-client";
import { App } from "../../shared/components/app/app";
import {
InitialFetchRequest,
IsoDataOptionalSite,
RouteData,
} from "../../shared/interfaces";
import { routes } from "../../shared/routes";
import {
FailedRequestState,
wrapClient,
} from "../../shared/services/HttpService";
import { createSsrHtml } from "../utils/create-ssr-html";
import { getErrorPageData } from "../utils/get-error-page-data";
import { setForwardedHeaders } from "../utils/set-forwarded-headers";
export default async (req: Request, res: Response) => {
try {
const activeRoute = routes.find(route => matchPath(req.path, route));
let auth = req.headers.cookie
? cookie.parse(req.headers.cookie).jwt
: undefined;
const getSiteForm: GetSite = { auth };
const headers = setForwardedHeaders(req.headers);
const client = wrapClient(
new LemmyHttp(getHttpBaseInternal(), { fetchFunction: fetch, headers })
);
const { path, url, query } = req;
// Get site data first
// This bypasses errors, so that the client can hit the error on its own,
// in order to remove the jwt on the browser. Necessary for wrong jwts
let site: GetSiteResponse | undefined = undefined;
let routeData: RouteData = {};
let errorPageData: ErrorPageData | undefined = undefined;
let try_site = await client.getSite(getSiteForm);
if (try_site.state === "failed" && try_site.msg == "not_logged_in") {
console.error(
"Incorrect JWT token, skipping auth so frontend can remove jwt cookie"
);
getSiteForm.auth = undefined;
auth = undefined;
try_site = await client.getSite(getSiteForm);
}
if (!auth && isAuthPath(path)) {
return res.redirect("/login");
}
if (try_site.state === "success") {
site = try_site.data;
initializeSite(site);
if (path !== "/setup" && !site.site_view.local_site.site_setup) {
return res.redirect("/setup");
}
if (site && activeRoute?.fetchInitialData) {
const initialFetchReq: InitialFetchRequest = {
client,
auth,
path,
query,
site,
};
routeData = await activeRoute.fetchInitialData(initialFetchReq);
}
if (!activeRoute) {
res.status(404);
}
} else if (try_site.state === "failed") {
res.status(500);
errorPageData = getErrorPageData(new Error(try_site.msg), site);
}
const error = Object.values(routeData).find(
res => res.state === "failed" && res.msg !== "couldnt_find_object" // TODO: find a better way of handling errors
) as FailedRequestState | undefined;
// Redirect to the 404 if there's an API error
if (error) {
console.error(error.msg);
if (error.msg === "instance_is_private") {
return res.redirect(`/signup`);
} else {
res.status(500);
errorPageData = getErrorPageData(new Error(error.msg), site);
}
}
const isoData: IsoDataOptionalSite = {
path,
site_res: site,
routeData,
errorPageData,
};
const wrapper = (
<StaticRouter location={url} context={isoData}>
<App />
</StaticRouter>
);
const root = renderToString(wrapper);
res.send(await createSsrHtml(root, isoData));
} catch (err) {
// If an error is caught here, the error page couldn't even be rendered
console.error(err);
res.statusCode = 500;
return res.send(
process.env.NODE_ENV === "development" ? err.message : "Server error"
);
}
};

View file

@ -1,31 +0,0 @@
import { getHttpBaseExternal, getHttpBaseInternal } from "@utils/env";
import fetch from "cross-fetch";
import type { Request, Response } from "express";
import { LemmyHttp } from "lemmy-js-client";
import { wrapClient } from "../../shared/services/HttpService";
import generateManifestJson from "../utils/generate-manifest-json";
import { setForwardedHeaders } from "../utils/set-forwarded-headers";
let manifest: Awaited<ReturnType<typeof generateManifestJson>> | undefined =
undefined;
export default async (req: Request, res: Response) => {
if (!manifest || manifest.start_url !== getHttpBaseExternal()) {
const headers = setForwardedHeaders(req.headers);
const client = wrapClient(
new LemmyHttp(getHttpBaseInternal(), { fetchFunction: fetch, headers })
);
const site = await client.getSite({});
if (site.state === "success") {
manifest = await generateManifestJson(site.data);
} else {
res.sendStatus(500);
return;
}
}
res.setHeader("content-type", "application/manifest+json");
res.send(manifest);
};

View file

@ -1,19 +0,0 @@
import type { Response } from "express";
export default async ({ res }: { res: Response }) => {
res.setHeader("content-type", "text/plain; charset=utf-8");
res.send(`User-Agent: *
Disallow: /login
Disallow: /login_reset
Disallow: /settings
Disallow: /create_community
Disallow: /create_post
Disallow: /create_private_message
Disallow: /inbox
Disallow: /setup
Disallow: /admin
Disallow: /password_change
Disallow: /search/
`);
};

View file

@ -1,17 +0,0 @@
import type { Response } from "express";
export default async ({ res }: { res: Response }) => {
res.setHeader("content-type", "text/plain; charset=utf-8");
res.send(
`Contact: mailto:security@lemmy.ml
Contact: mailto:admin@` +
process.env.LEMMY_UI_LEMMY_EXTERNAL_HOST +
`
Contact: mailto:security@` +
process.env.LEMMY_UI_LEMMY_EXTERNAL_HOST +
`
Expires: 2024-01-01T04:59:00.000Z
`
);
};

View file

@ -1,14 +0,0 @@
import type { Response } from "express";
import path from "path";
export default async ({ res }: { res: Response }) => {
res
.setHeader("Content-Type", "application/javascript")
.sendFile(
path.resolve(
`./dist/service-worker${
process.env.NODE_ENV === "development" ? "-development" : ""
}.js`
)
);
};

View file

@ -1,31 +0,0 @@
import type { Request, Response } from "express";
import { existsSync } from "fs";
import path from "path";
const extraThemesFolder =
process.env["LEMMY_UI_EXTRA_THEMES_FOLDER"] || "./extra_themes";
export default async (req: Request, res: Response) => {
res.contentType("text/css");
const theme = req.params.name;
if (!theme.endsWith(".css")) {
return res.status(400).send("Theme must be a css file");
}
const customTheme = path.resolve(extraThemesFolder, theme);
if (existsSync(customTheme)) {
return res.sendFile(customTheme);
} else {
const internalTheme = path.resolve(`./dist/assets/css/themes/${theme}`);
// If the theme doesn't exist, just send litely
if (existsSync(internalTheme)) {
return res.sendFile(internalTheme);
} else {
return res.sendFile(path.resolve("./dist/assets/css/themes/litely.css"));
}
}
};

View file

@ -1,6 +0,0 @@
import type { Response } from "express";
import { buildThemeList } from "../utils/build-themes-list";
export default async ({ res }: { res: Response }) => {
res.type("json").send(JSON.stringify(await buildThemeList()));
};

View file

@ -1,51 +1,209 @@
import { setupDateFns } from "@utils/app";
import { getStaticDir } from "@utils/env";
import express from "express";
import { IncomingHttpHeaders } from "http";
import { Helmet } from "inferno-helmet";
import { matchPath, StaticRouter } from "inferno-router";
import { renderToString } from "inferno-server";
import IsomorphicCookie from "isomorphic-cookie";
import { GetSite, GetSiteResponse, LemmyHttp } from "lemmy-js-client";
import path from "path";
import process from "process";
import CatchAllHandler from "./handlers/catch-all-handler";
import ManifestHandler from "./handlers/manifest-handler";
import RobotsHandler from "./handlers/robots-handler";
import SecurityHandler from "./handlers/security-handler";
import ServiceWorkerHandler from "./handlers/service-worker-handler";
import ThemeHandler from "./handlers/theme-handler";
import ThemesListHandler from "./handlers/themes-list-handler";
import { setCacheControl, setDefaultCsp } from "./middleware";
import serialize from "serialize-javascript";
import { App } from "../shared/components/app/app";
import { SYMBOLS } from "../shared/components/common/symbols";
import { httpBaseInternal } from "../shared/env";
import {
ILemmyConfig,
InitialFetchRequest,
IsoData,
} from "../shared/interfaces";
import { routes } from "../shared/routes";
import { initializeSite, setOptionalAuth } from "../shared/utils";
const server = express();
const [hostname, port] = process.env["LEMMY_UI_HOST"]
? process.env["LEMMY_UI_HOST"].split(":")
: ["0.0.0.0", "1234"];
server.use(express.json());
server.use(express.urlencoded({ extended: false }));
server.use(
getStaticDir(),
express.static(path.resolve("./dist"), {
maxAge: 24 * 60 * 60 * 1000, // 1 day
immutable: true,
})
);
server.use(setCacheControl);
server.use("/static", express.static(path.resolve("./dist")));
if (!process.env["LEMMY_UI_DISABLE_CSP"] && !process.env["LEMMY_UI_DEBUG"]) {
server.use(setDefaultCsp);
}
const robotstxt = `User-Agent: *
Disallow: /login
Disallow: /settings
Disallow: /create_community
Disallow: /create_post
Disallow: /create_private_message
Disallow: /inbox
Disallow: /setup
Disallow: /admin
Disallow: /password_change
Disallow: /search/
`;
server.get("/.well-known/security.txt", SecurityHandler);
server.get("/robots.txt", RobotsHandler);
server.get("/service-worker.js", ServiceWorkerHandler);
server.get("/manifest.webmanifest", ManifestHandler);
server.get("/css/themes/:name", ThemeHandler);
server.get("/css/themelist", ThemesListHandler);
server.get("/*", CatchAllHandler);
server.get("/robots.txt", async (_req, res) => {
res.setHeader("content-type", "text/plain; charset=utf-8");
res.send(robotstxt);
});
// server.use(cookieParser());
server.get("/*", async (req, res) => {
try {
const activeRoute = routes.find(route => matchPath(req.path, route)) || {};
const context = {} as any;
let auth: string = IsomorphicCookie.load("jwt", req);
let getSiteForm: GetSite = {};
setOptionalAuth(getSiteForm, auth);
let promises: Promise<any>[] = [];
let headers = setForwardedHeaders(req.headers);
let initialFetchReq: InitialFetchRequest = {
client: new LemmyHttp(httpBaseInternal, headers),
auth,
path: req.path,
};
// Get site data first
// This bypasses errors, so that the client can hit the error on its own,
// in order to remove the jwt on the browser. Necessary for wrong jwts
let try_site: any = await initialFetchReq.client.getSite(getSiteForm);
if (try_site.error == "not_logged_in") {
console.error(
"Incorrect JWT token, skipping auth so frontend can remove jwt cookie"
);
delete getSiteForm.auth;
delete initialFetchReq.auth;
try_site = await initialFetchReq.client.getSite(getSiteForm);
}
let site: GetSiteResponse = try_site;
initializeSite(site);
if (activeRoute.fetchInitialData) {
promises.push(...activeRoute.fetchInitialData(initialFetchReq));
}
let routeData = await Promise.all(promises);
// Redirect to the 404 if there's an API error
if (routeData[0] && routeData[0].error) {
let errCode = routeData[0].error;
console.error(errCode);
if (errCode == "instance_is_private") {
return res.redirect(`/signup`);
} else {
return res.redirect(`/404?err=${errCode}`);
}
}
let isoData: IsoData = {
path: req.path,
site_res: site,
routeData,
};
const wrapper = (
<StaticRouter location={req.url} context={isoData}>
<App siteRes={isoData.site_res} />
</StaticRouter>
);
if (context.url) {
return res.redirect(context.url);
}
const cspHtml = (
<meta
http-equiv="Content-Security-Policy"
content="default-src data: 'self'; connect-src * ws: wss:; frame-src *; img-src * data:; script-src 'self'; style-src 'self' 'unsafe-inline'; manifest-src 'self'"
/>
);
const root = renderToString(wrapper);
const symbols = renderToString(SYMBOLS);
const cspStr = process.env.LEMMY_EXTERNAL_HOST
? renderToString(cspHtml)
: "";
const helmet = Helmet.renderStatic();
const config: ILemmyConfig = { wsHost: process.env.LEMMY_WS_HOST };
res.send(`
<!DOCTYPE html>
<html ${helmet.htmlAttributes.toString()} lang="en">
<head>
<script>window.isoData = ${serialize(isoData)}</script>
<script>window.lemmyConfig = ${serialize(config)}</script>
<!-- A remote debugging utility for mobile
<script src="//cdn.jsdelivr.net/npm/eruda"></script>
<script>eruda.init();</script>
-->
${helmet.title.toString()}
${helmet.meta.toString()}
<!-- Required meta tags -->
<meta name="Description" content="Lemmy">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<!-- Content Security Policy -->
${cspStr}
<!-- Web app manifest -->
<link rel="manifest" href="/static/assets/manifest.webmanifest">
<!-- Styles -->
<link rel="stylesheet" type="text/css" href="/static/styles/styles.css" />
<!-- Current theme and more -->
${helmet.link.toString()}
<!-- Icons -->
${symbols}
</head>
<body ${helmet.bodyAttributes.toString()}>
<noscript>
<div class="alert alert-danger rounded-0" role="alert">
<b>Javascript is disabled. Actions will not work.</b>
</div>
</noscript>
<div id='root'>${root}</div>
<script defer src='/static/js/client.js'></script>
</body>
</html>
`);
} catch (err) {
console.error(err);
return res.redirect(`/404?err=${err}`);
}
});
server.listen(Number(port), hostname, () => {
setupDateFns();
console.log(`http://${hostname}:${port}`);
});
function setForwardedHeaders(headers: IncomingHttpHeaders): {
[key: string]: string;
} {
let out = {
host: headers.host,
};
if (headers["x-real-ip"]) {
out["x-real-ip"] = headers["x-real-ip"];
}
if (headers["x-forwarded-for"]) {
out["x-forwarded-for"] = headers["x-forwarded-for"];
}
return out;
}
process.on("SIGINT", () => {
console.info("Interrupted");
process.exit(0);

View file

@ -1,53 +0,0 @@
import type { NextFunction, Request, Response } from "express";
import { hasJwtCookie } from "./utils/has-jwt-cookie";
export function setDefaultCsp({
res,
next,
}: {
res: Response;
next: NextFunction;
}) {
res.setHeader(
"Content-Security-Policy",
`default-src 'self'; manifest-src *; connect-src *; img-src * data:; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; form-action 'self'; base-uri 'self'; frame-src *; media-src * data:`
);
next();
}
// Set cache-control headers. If user is logged in, set `private` to prevent storing data in
// shared caches (eg nginx) and leaking of private data. If user is not logged in, allow caching
// all responses for 5 seconds to reduce load on backend and database. The specific cache
// interval is rather arbitrary and could be set higher (less server load) or lower (fresher data).
//
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control
export function setCacheControl(
req: Request,
res: Response,
next: NextFunction
) {
if (process.env.NODE_ENV !== "production") {
return next();
}
let caching: string;
if (
req.path.match(/\.(js|css|txt|manifest\.webmanifest)\/?$/) ||
req.path.includes("/css/themelist")
) {
// Static content gets cached publicly for a day
caching = "public, max-age=86400";
} else {
if (hasJwtCookie(req)) {
caching = "private";
} else {
caching = "public, max-age=5";
}
}
res.setHeader("Cache-Control", caching);
next();
}

View file

@ -1,27 +0,0 @@
import { existsSync } from "fs";
import { readdir } from "fs/promises";
const extraThemesFolder =
process.env["LEMMY_UI_EXTRA_THEMES_FOLDER"] || "./extra_themes";
const themes: ReadonlyArray<string> = [
"darkly",
"darkly-red",
"darkly-compact",
"darkly-pureblack",
"litely",
"litely-red",
"litely-compact",
"i386",
];
export async function buildThemeList(): Promise<ReadonlyArray<string>> {
if (existsSync(extraThemesFolder)) {
const dirThemes = await readdir(extraThemesFolder);
const cssThemes = dirThemes
.filter(d => d.endsWith(".css"))
.map(d => d.replace(".css", ""));
return themes.concat(cssThemes);
}
return themes;
}

View file

@ -1,110 +0,0 @@
import { getStaticDir } from "@utils/env";
import { Helmet } from "inferno-helmet";
import { renderToString } from "inferno-server";
import serialize from "serialize-javascript";
import sharp from "sharp";
import { favIconPngUrl, favIconUrl } from "../../shared/config";
import { ILemmyConfig, IsoDataOptionalSite } from "../../shared/interfaces";
import { buildThemeList } from "./build-themes-list";
import { fetchIconPng } from "./fetch-icon-png";
const customHtmlHeader = process.env["LEMMY_UI_CUSTOM_HTML_HEADER"] || "";
let appleTouchIcon: string | undefined = undefined;
export async function createSsrHtml(
root: string,
isoData: IsoDataOptionalSite
) {
const site = isoData.site_res;
const fallbackTheme = `<link rel="stylesheet" type="text/css" href="/css/themes/${
(await buildThemeList())[0]
}.css" />`;
if (!appleTouchIcon) {
appleTouchIcon = site?.site_view.site.icon
? `data:image/png;base64,${await sharp(
await fetchIconPng(site.site_view.site.icon)
)
.resize(180, 180)
.extend({
bottom: 20,
top: 20,
left: 20,
right: 20,
background: "#222222",
})
.png()
.toBuffer()
.then(buf => buf.toString("base64"))}`
: favIconPngUrl;
}
const erudaStr =
process.env["LEMMY_UI_DEBUG"] === "true"
? renderToString(
<>
<script src="//cdn.jsdelivr.net/npm/eruda"></script>
<script>eruda.init();</script>
</>
)
: "";
const helmet = Helmet.renderStatic();
const config: ILemmyConfig = { wsHost: process.env.LEMMY_UI_LEMMY_WS_HOST };
return `
<!DOCTYPE html>
<html ${helmet.htmlAttributes.toString()}>
<head>
<script>window.isoData = ${serialize(isoData)}</script>
<script>window.lemmyConfig = ${serialize(config)}</script>
<!-- A remote debugging utility for mobile -->
${erudaStr}
<!-- Custom injected script -->
${customHtmlHeader}
${helmet.title.toString()}
${helmet.meta.toString()}
<!-- Required meta tags -->
<meta name="Description" content="Lemmy">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link
id="favicon"
rel="shortcut icon"
type="image/x-icon"
href=${site?.site_view.site.icon ?? favIconUrl}
/>
<!-- Web app manifest -->
<link rel="manifest" href="/manifest.webmanifest" />
<link rel="apple-touch-icon" href=${appleTouchIcon} />
<link rel="apple-touch-startup-image" href=${appleTouchIcon} />
<!-- Styles -->
<link rel="stylesheet" type="text/css" href="${getStaticDir()}/styles/styles.css" />
<!-- Current theme and more -->
${helmet.link.toString() || fallbackTheme}
</head>
<body ${helmet.bodyAttributes.toString()}>
<noscript>
<div class="alert alert-danger rounded-0" role="alert">
<b>Javascript is disabled. Actions will not work.</b>
</div>
</noscript>
<div id='root'>${root}</div>
<script defer src='${getStaticDir()}/js/client.js'></script>
</body>
</html>
`;
}

View file

@ -1,7 +0,0 @@
import fetch from "cross-fetch";
export async function fetchIconPng(iconUrl: string) {
return await fetch(iconUrl)
.then(res => res.blob())
.then(blob => blob.arrayBuffer());
}

View file

@ -1,98 +0,0 @@
import { getHttpBaseExternal } from "@utils/env";
import { readFile } from "fs/promises";
import { GetSiteResponse } from "lemmy-js-client";
import path from "path";
import sharp from "sharp";
import { fetchIconPng } from "./fetch-icon-png";
const iconSizes = [72, 96, 128, 144, 152, 192, 384, 512];
const defaultLogoPathDirectory = path.join(
process.cwd(),
"dist",
"assets",
"icons"
);
export default async function ({
my_user,
site_view: {
site,
local_site: { community_creation_admin_only },
},
}: GetSiteResponse) {
const url = getHttpBaseExternal();
const icon = site.icon ? await fetchIconPng(site.icon) : null;
return {
name: site.name,
description: site.description ?? "A link aggregator for the fediverse",
start_url: url,
scope: url,
display: "standalone",
id: "/",
background_color: "#222222",
theme_color: "#222222",
icons: await Promise.all(
iconSizes.map(async size => {
let src = await readFile(
path.join(defaultLogoPathDirectory, `icon-${size}x${size}.png`)
).then(buf => buf.toString("base64"));
if (icon) {
src = await sharp(icon)
.resize(size, size)
.png()
.toBuffer()
.then(buf => buf.toString("base64"));
}
return {
sizes: `${size}x${size}`,
type: "image/png",
src: `data:image/png;base64,${src}`,
purpose: "any maskable",
};
})
),
shortcuts: [
{
name: "Search",
short_name: "Search",
description: "Perform a search.",
url: "/search",
},
{
name: "Communities",
url: "/communities",
short_name: "Communities",
description: "Browse communities",
},
{
name: "Create Post",
url: "/create_post",
short_name: "Create Post",
description: "Create a post.",
},
].concat(
my_user?.local_user_view.person.admin || !community_creation_admin_only
? [
{
name: "Create Community",
url: "/create_community",
short_name: "Create Community",
description: "Create a community",
},
]
: []
),
related_applications: [
{
platform: "f-droid",
url: "https://f-droid.org/packages/com.jerboa/",
id: "com.jerboa",
},
],
};
}

View file

@ -1,20 +0,0 @@
import { ErrorPageData } from "@utils/types";
import { GetSiteResponse } from "lemmy-js-client";
export function getErrorPageData(error: Error, site?: GetSiteResponse) {
const errorPageData: ErrorPageData = {};
if (site) {
errorPageData.error = error.message;
}
const adminMatrixIds = site?.admins
.map(({ person: { matrix_user_id } }) => matrix_user_id)
.filter(id => id) as string[] | undefined;
if (adminMatrixIds && adminMatrixIds.length > 0) {
errorPageData.adminMatrixIds = adminMatrixIds;
}
return errorPageData;
}

View file

@ -1,6 +0,0 @@
import * as cookie from "cookie";
import type { Request } from "express";
export function hasJwtCookie(req: Request): boolean {
return Boolean(cookie.parse(req.headers.cookie ?? "").jwt?.length);
}

View file

@ -1,25 +0,0 @@
import { IncomingHttpHeaders } from "http";
export function setForwardedHeaders(headers: IncomingHttpHeaders): {
[key: string]: string;
} {
const out: { [key: string]: string } = {};
if (headers.host) {
out.host = headers.host;
}
const realIp = headers["x-real-ip"];
if (realIp) {
out["x-real-ip"] = realIp as string;
}
const forwardedFor = headers["x-forwarded-for"];
if (forwardedFor) {
out["x-forwarded-for"] = forwardedFor as string;
}
return out;
}

28
src/service-worker.ts Normal file
View file

@ -0,0 +1,28 @@
import { register } from "register-service-worker";
register("/service-worker.js", {
registrationOptions: { scope: "./" },
ready() {
console.log("Service worker is active.");
},
registered() {
console.log("Service worker has been registered.");
},
cached() {
console.log("Content has been cached for offline use.");
},
updatefound() {
console.log("New content is downloading.");
},
updated() {
console.log("New content is available; please refresh.");
},
offline() {
console.log(
"No internet connection found. App is running in offline mode."
);
},
error(error) {
console.error("Error during service worker registration:", error);
},
});

View file

@ -1,96 +1,65 @@
import { isAuthPath, setIsoData } from "@utils/app";
import { dataBsTheme } from "@utils/browser";
import { Component, RefObject, createRef, linkEvent } from "inferno";
import { Component } from "inferno";
import { Helmet } from "inferno-helmet";
import { Provider } from "inferno-i18next-dess";
import { Route, Switch } from "inferno-router";
import { IsoDataOptionalSite } from "../../interfaces";
import { GetSiteResponse } from "lemmy-js-client";
import { i18n } from "../../i18next";
import { routes } from "../../routes";
import { FirstLoadService, I18NextService, UserService } from "../../services";
import AuthGuard from "../common/auth-guard";
import ErrorGuard from "../common/error-guard";
import { ErrorPage } from "./error-page";
import { favIconPngUrl, favIconUrl } from "../../utils";
import { Footer } from "./footer";
import { Navbar } from "./navbar";
import { NoMatch } from "./no-match";
import "./styles.scss";
import { Theme } from "./theme";
export class App extends Component<any, any> {
private isoData: IsoDataOptionalSite = setIsoData(this.context);
private readonly mainContentRef: RefObject<HTMLElement>;
export interface AppProps {
siteRes: GetSiteResponse;
}
export class App extends Component<AppProps, any> {
constructor(props: any, context: any) {
super(props, context);
this.mainContentRef = createRef();
}
handleJumpToContent(event) {
event.preventDefault();
this.mainContentRef.current?.focus();
}
user = UserService.Instance.myUserInfo;
componentDidMount() {
this.setState({ bsTheme: dataBsTheme(this.user) });
}
render() {
const siteRes = this.isoData.site_res;
const siteView = siteRes?.site_view;
let siteRes = this.props.siteRes;
return (
<>
<Provider i18next={I18NextService.i18n}>
<div
id="app"
className="lemmy-site"
data-bs-theme={this.state?.bsTheme}
>
<button
type="button"
className="btn skip-link bg-light position-absolute start-0 z-3"
onClick={linkEvent(this, this.handleJumpToContent)}
>
{I18NextService.i18n.t("jump_to_content", "Jump to content")}
</button>
{siteView && (
<Theme defaultTheme={siteView.local_site.default_theme} />
)}
<Navbar siteRes={siteRes} />
<div className="mt-4 p-0 fl-1">
<Provider i18next={i18n}>
<div>
<Theme myUserInfo={siteRes.my_user} />
{siteRes &&
siteRes.site_view &&
this.props.siteRes.site_view.site.icon && (
<Helmet>
<link
id="favicon"
rel="shortcut icon"
type="image/x-icon"
href={this.props.siteRes.site_view.site.icon || favIconUrl}
/>
<link
rel="apple-touch-icon"
href={
this.props.siteRes.site_view.site.icon || favIconPngUrl
}
/>
</Helmet>
)}
<Navbar site_res={this.props.siteRes} />
<div class="mt-4 p-0 fl-1">
<Switch>
{routes.map(
({ path, component: RouteComponent, fetchInitialData }) => (
<Route
key={path}
path={path}
exact
component={routeProps => {
if (!fetchInitialData) {
FirstLoadService.falsify();
}
return (
<ErrorGuard>
<div tabIndex={-1}>
{RouteComponent &&
(isAuthPath(path ?? "") ? (
<AuthGuard>
<RouteComponent {...routeProps} />
</AuthGuard>
) : (
<RouteComponent {...routeProps} />
))}
</div>
</ErrorGuard>
);
}}
/>
)
)}
<Route component={ErrorPage} />
{routes.map(({ path, exact, component: C, ...rest }) => (
<Route
key={path}
path={path}
exact={exact}
render={props => <C {...props} {...rest} />}
/>
))}
<Route render={props => <NoMatch {...props} />} />
</Switch>
</div>
<Footer site={siteRes} />
<Footer site={this.props.siteRes} />
</div>
</Provider>
</>

View file

@ -1,70 +0,0 @@
import { setIsoData } from "@utils/app";
import { removeAuthParam } from "@utils/helpers";
import { Component } from "inferno";
import { T } from "inferno-i18next-dess";
import { Link } from "inferno-router";
import { IsoDataOptionalSite } from "../../interfaces";
import { I18NextService } from "../../services";
export class ErrorPage extends Component<any, any> {
private isoData: IsoDataOptionalSite = setIsoData(this.context);
constructor(props: any, context: any) {
super(props, context);
}
render() {
const { errorPageData } = this.isoData;
return (
<div className="error-page container-lg text-center">
<h1>
{errorPageData
? I18NextService.i18n.t("error_page_title")
: I18NextService.i18n.t("not_found_page_title")}
</h1>
{errorPageData ? (
<T i18nKey="error_page_paragraph" className="p-4" parent="p">
#<a href="https://lemmy.ml/c/lemmy_support">#</a>#
<a href="https://matrix.to/#/#lemmy-space:matrix.org">#</a>#
</T>
) : (
<p>{I18NextService.i18n.t("not_found_page_message")}</p>
)}
{!errorPageData && (
<Link to="/" replace>
{I18NextService.i18n.t("not_found_return_home_button")}
</Link>
)}
{errorPageData?.adminMatrixIds &&
errorPageData.adminMatrixIds.length > 0 && (
<>
<div>
{I18NextService.i18n.t("error_page_admin_matrix", {
instance:
this.isoData.site_res?.site_view.site.name ??
"this instance",
})}
</div>
<ul className="mx-auto mt-2" style={{ width: "fit-content" }}>
{errorPageData.adminMatrixIds.map(matrixId => (
<li key={matrixId} className="text-info">
{matrixId}
</li>
))}
</ul>
</>
)}
{errorPageData?.error && (
<T
i18nKey="error_code_message"
parent="p"
interpolation={{ error: removeAuthParam(errorPageData.error) }}
>
#<strong className="text-danger">#</strong>#
</T>
)}
</div>
);
}
}

View file

@ -1,12 +1,12 @@
import { Component } from "inferno";
import { NavLink } from "inferno-router";
import { GetSiteResponse } from "lemmy-js-client";
import { docsUrl, joinLemmyUrl, repoUrl } from "../../config";
import { I18NextService } from "../../services";
import { i18n } from "../../i18next";
import { docsUrl, joinLemmyUrl, repoUrl } from "../../utils";
import { VERSION } from "../../version";
interface FooterProps {
site?: GetSiteResponse;
site: GetSiteResponse;
}
export class Footer extends Component<FooterProps, any> {
@ -16,54 +16,47 @@ export class Footer extends Component<FooterProps, any> {
render() {
return (
<footer className="app-footer container-lg navbar navbar-expand-md navbar-light navbar-bg p-3">
<nav class="container navbar navbar-expand-md navbar-light navbar-bg p-3">
<div className="navbar-collapse">
<ul className="navbar-nav ms-auto">
{this.props.site?.version !== VERSION && (
<li className="nav-item">
<span className="nav-link">UI: {VERSION}</span>
<ul class="navbar-nav ml-auto">
{this.props.site.version !== VERSION && (
<li class="nav-item">
<span class="nav-link">UI: {VERSION}</span>
</li>
)}
<li className="nav-item">
<span className="nav-link">BE: {this.props.site?.version}</span>
<li class="nav-item">
<span class="nav-link">BE: {this.props.site.version}</span>
</li>
<li className="nav-item">
<NavLink className="nav-link" to="/modlog">
{I18NextService.i18n.t("modlog")}
{i18n.t("modlog")}
</NavLink>
</li>
{this.props.site?.site_view.local_site.legal_information && (
<li className="nav-item">
<NavLink className="nav-link" to="/legal">
{I18NextService.i18n.t("legal_information")}
</NavLink>
</li>
)}
{this.props.site?.site_view.local_site.federation_enabled && (
<li className="nav-item">
{this.props.site.federated_instances && (
<li class="nav-item">
<NavLink className="nav-link" to="/instances">
{I18NextService.i18n.t("instances")}
{i18n.t("instances")}
</NavLink>
</li>
)}
<li className="nav-item">
<li class="nav-item">
<a className="nav-link" href={docsUrl}>
{I18NextService.i18n.t("docs")}
{i18n.t("docs")}
</a>
</li>
<li className="nav-item">
<li class="nav-item">
<a className="nav-link" href={repoUrl}>
{I18NextService.i18n.t("code")}
{i18n.t("code")}
</a>
</li>
<li className="nav-item">
<li class="nav-item">
<a className="nav-link" href={joinLemmyUrl}>
{I18NextService.i18n.t("join_lemmy")}
{i18n.t("join_lemmy")}
</a>
</li>
</ul>
</div>
</footer>
</nav>
);
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,26 @@
import { I18nKeys } from "i18next";
import { Component } from "inferno";
import { i18n } from "../../i18next";
export class NoMatch extends Component<any, any> {
private errCode = new URLSearchParams(this.props.location.search).get(
"err"
) as I18nKeys;
constructor(props: any, context: any) {
super(props, context);
}
render() {
return (
<div class="container">
<h1>404</h1>
{this.errCode && (
<h3>
{i18n.t("code")}: {i18n.t(this.errCode)}
</h3>
)}
</div>
);
}
}

View file

@ -1,6 +1,6 @@
// Custom css
@import "../../../../node_modules/tributejs/dist/tribute.css";
@import "../../../../node_modules/toastify-js/src/toastify.css";
@import "../../../../node_modules/choices.js/src/styles/choices.scss";
@import "../../../../node_modules/tippy.js/dist/tippy.css";
@import "../../../../node_modules/bootstrap/dist/css/bootstrap-utilities.min.css";
@import "../../../assets/css/main.css";

View file

@ -1,55 +1,43 @@
import { Component } from "inferno";
import { Helmet } from "inferno-helmet";
import { UserService } from "../../services";
import { MyUserInfo } from "lemmy-js-client";
interface Props {
defaultTheme: string;
myUserInfo: MyUserInfo | undefined;
}
export class Theme extends Component<Props> {
render() {
const user = UserService.Instance.myUserInfo;
const hasTheme = user?.local_user_view.local_user.theme !== "browser";
let user = this.props.myUserInfo;
let hasTheme = user && user.local_user_view.local_user.theme !== "browser";
if (user && hasTheme) {
return (
<Helmet>
return (
<Helmet>
{hasTheme ? (
<link
rel="stylesheet"
type="text/css"
href={`/css/themes/${user.local_user_view.local_user.theme}.css`}
href={`/static/assets/css/themes/${user.local_user_view.local_user.theme}.min.css`}
/>
</Helmet>
);
} else if (this.props.defaultTheme != "browser") {
return (
<Helmet>
<link
rel="stylesheet"
type="text/css"
href={`/css/themes/${this.props.defaultTheme}.css`}
/>
</Helmet>
);
} else {
return (
<Helmet>
<link
rel="stylesheet"
type="text/css"
href="/css/themes/litely.css"
id="default-light"
media="(prefers-color-scheme: light)"
/>
<link
rel="stylesheet"
type="text/css"
href="/css/themes/darkly.css"
id="default-dark"
media="(prefers-color-scheme: no-preference), (prefers-color-scheme: dark)"
/>
</Helmet>
);
}
) : (
[
<link
rel="stylesheet"
type="text/css"
href="/static/assets/css/themes/litely.min.css"
id="default-light"
media="(prefers-color-scheme: light)"
/>,
<link
rel="stylesheet"
type="text/css"
href="/static/assets/css/themes/darkly.min.css"
id="default-dark"
media="(prefers-color-scheme: no-preference), (prefers-color-scheme: dark)"
/>,
]
)}
</Helmet>
);
}
}

View file

@ -1,69 +1,92 @@
import { myAuthRequired } from "@utils/app";
import { capitalizeFirstLetter } from "@utils/helpers";
import { Component } from "inferno";
import { T } from "inferno-i18next-dess";
import { Link } from "inferno-router";
import { CreateComment, EditComment, Language } from "lemmy-js-client";
import { CommentNodeI } from "../../interfaces";
import { I18NextService, UserService } from "../../services";
import {
CommentResponse,
CreateComment,
EditComment,
UserOperation,
} from "lemmy-js-client";
import { Subscription } from "rxjs";
import { i18n } from "../../i18next";
import { CommentNode as CommentNodeI } from "../../interfaces";
import { UserService, WebSocketService } from "../../services";
import {
authField,
capitalizeFirstLetter,
wsClient,
wsJsonToRes,
wsSubscribe,
wsUserOp,
} from "../../utils";
import { Icon } from "../common/icon";
import { MarkdownTextArea } from "../common/markdown-textarea";
interface CommentFormProps {
/**
* Can either be the parent, or the editable comment. The right side is a postId.
*/
node: CommentNodeI | number;
finished?: boolean;
postId?: number;
node?: CommentNodeI; // Can either be the parent, or the editable comment
edit?: boolean;
disabled?: boolean;
focus?: boolean;
onReplyCancel?(): void;
allLanguages: Language[];
siteLanguages: number[];
containerClass?: string;
onUpsertComment(form: EditComment | CreateComment): void;
onReplyCancel?(): any;
}
export class CommentForm extends Component<CommentFormProps, any> {
interface CommentFormState {
buttonTitle: string;
finished: boolean;
formId: string;
}
export class CommentForm extends Component<CommentFormProps, CommentFormState> {
private subscription: Subscription;
private emptyState: CommentFormState = {
buttonTitle: !this.props.node
? capitalizeFirstLetter(i18n.t("post"))
: this.props.edit
? capitalizeFirstLetter(i18n.t("save"))
: capitalizeFirstLetter(i18n.t("reply")),
finished: false,
formId: "empty_form",
};
constructor(props: any, context: any) {
super(props, context);
this.handleCommentSubmit = this.handleCommentSubmit.bind(this);
this.handleReplyCancel = this.handleReplyCancel.bind(this);
this.state = this.emptyState;
this.parseMessage = this.parseMessage.bind(this);
this.subscription = wsSubscribe(this.parseMessage);
}
componentWillUnmount() {
this.subscription.unsubscribe();
}
render() {
const initialContent =
typeof this.props.node !== "number"
? this.props.edit
? this.props.node.comment_view.comment.content
: undefined
: undefined;
return (
<div
className={["comment-form", "mb-3", this.props.containerClass].join(
" "
)}
>
<div class="mb-3">
{UserService.Instance.myUserInfo ? (
<MarkdownTextArea
initialContent={initialContent}
showLanguage
buttonTitle={this.buttonTitle}
finished={this.props.finished}
replyType={typeof this.props.node !== "number"}
initialContent={
this.props.edit
? this.props.node.comment_view.comment.content
: null
}
buttonTitle={this.state.buttonTitle}
finished={this.state.finished}
replyType={!!this.props.node}
focus={this.props.focus}
disabled={this.props.disabled}
onSubmit={this.handleCommentSubmit}
onReplyCancel={this.props.onReplyCancel}
placeholder={I18NextService.i18n.t("comment_here") ?? undefined}
allLanguages={this.props.allLanguages}
siteLanguages={this.props.siteLanguages}
onReplyCancel={this.handleReplyCancel}
placeholder={i18n.t("comment_here")}
/>
) : (
<div className="alert alert-warning" role="alert">
<Icon icon="alert-triangle" classes="icon-inline me-2" />
<div class="alert alert-warning" role="alert">
<Icon icon="alert-triangle" classes="icon-inline mr-2" />
<T i18nKey="must_login" class="d-inline">
#
<Link className="alert-link" to="/login">
@ -76,46 +99,56 @@ export class CommentForm extends Component<CommentFormProps, any> {
);
}
get buttonTitle(): string {
return typeof this.props.node === "number"
? capitalizeFirstLetter(I18NextService.i18n.t("post"))
: this.props.edit
? capitalizeFirstLetter(I18NextService.i18n.t("save"))
: capitalizeFirstLetter(I18NextService.i18n.t("reply"));
handleCommentSubmit(msg: { val: string; formId: string }) {
let content = msg.val;
this.state.formId = msg.formId;
let node = this.props.node;
if (this.props.edit) {
let form: EditComment = {
content,
form_id: this.state.formId,
comment_id: node.comment_view.comment.id,
auth: authField(),
};
WebSocketService.Instance.send(wsClient.editComment(form));
} else {
let form: CreateComment = {
content,
form_id: this.state.formId,
post_id: node ? node.comment_view.post.id : this.props.postId,
parent_id: node ? node.comment_view.comment.id : null,
auth: authField(),
};
WebSocketService.Instance.send(wsClient.createComment(form));
}
this.setState(this.state);
}
handleCommentSubmit(content: string, form_id: string, language_id?: number) {
const { node, onUpsertComment, edit } = this.props;
if (typeof node === "number") {
const post_id = node;
onUpsertComment({
content,
post_id,
language_id,
form_id,
auth: myAuthRequired(),
});
} else {
if (edit) {
const comment_id = node.comment_view.comment.id;
onUpsertComment({
content,
comment_id,
form_id,
language_id,
auth: myAuthRequired(),
});
} else {
const post_id = node.comment_view.post.id;
const parent_id = node.comment_view.comment.id;
this.props.onUpsertComment({
content,
parent_id,
post_id,
form_id,
language_id,
auth: myAuthRequired(),
});
handleReplyCancel() {
this.props.onReplyCancel();
}
parseMessage(msg: any) {
let op = wsUserOp(msg);
console.log(msg);
// Only do the showing and hiding if logged in
if (UserService.Instance.myUserInfo) {
if (
op == UserOperation.CreateComment ||
op == UserOperation.EditComment
) {
let data = wsJsonToRes<CommentResponse>(msg).data;
// This only finishes this form, if the randomly generated form_id matches the one received
if (this.state.formId == data.form_id) {
this.setState({ finished: true });
// Necessary because it broke tribute for some reason
this.setState({ finished: false });
}
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,39 +1,13 @@
import { colorList } from "@utils/app";
import classNames from "classnames";
import { Component } from "inferno";
import {
AddAdmin,
AddModToCommunity,
BanFromCommunity,
BanPerson,
BlockPerson,
CommentId,
CommunityModeratorView,
CreateComment,
CreateCommentLike,
CreateCommentReport,
DeleteComment,
DistinguishComment,
EditComment,
GetComments,
Language,
MarkCommentReplyAsRead,
MarkPersonMentionAsRead,
PersonView,
PurgeComment,
PurgePerson,
RemoveComment,
SaveComment,
TransferCommunity,
} from "lemmy-js-client";
import { CommentNodeI, CommentViewType } from "../../interfaces";
import { CommunityModeratorView, PersonViewSafe } from "lemmy-js-client";
import { CommentNode as CommentNodeI } from "../../interfaces";
import { CommentNode } from "./comment-node";
interface CommentNodesProps {
nodes: CommentNodeI[];
moderators?: CommunityModeratorView[];
admins?: PersonView[];
maxCommentsShown?: number;
admins?: PersonViewSafe[];
postCreatorId?: number;
noBorder?: boolean;
noIndent?: boolean;
viewOnly?: boolean;
@ -41,102 +15,40 @@ interface CommentNodesProps {
markable?: boolean;
showContext?: boolean;
showCommunity?: boolean;
enableDownvotes?: boolean;
viewType: CommentViewType;
allLanguages: Language[];
siteLanguages: number[];
hideImages?: boolean;
isChild?: boolean;
depth?: number;
finished: Map<CommentId, boolean | undefined>;
onSaveComment(form: SaveComment): void;
onCommentReplyRead(form: MarkCommentReplyAsRead): void;
onPersonMentionRead(form: MarkPersonMentionAsRead): void;
onCreateComment(form: EditComment | CreateComment): void;
onEditComment(form: EditComment | CreateComment): void;
onCommentVote(form: CreateCommentLike): void;
onBlockPerson(form: BlockPerson): void;
onDeleteComment(form: DeleteComment): void;
onRemoveComment(form: RemoveComment): void;
onDistinguishComment(form: DistinguishComment): void;
onAddModToCommunity(form: AddModToCommunity): void;
onAddAdmin(form: AddAdmin): void;
onBanPersonFromCommunity(form: BanFromCommunity): void;
onBanPerson(form: BanPerson): void;
onTransferCommunity(form: TransferCommunity): void;
onFetchChildren?(form: GetComments): void;
onCommentReport(form: CreateCommentReport): void;
onPurgePerson(form: PurgePerson): void;
onPurgeComment(form: PurgeComment): void;
maxCommentsShown?: number;
enableDownvotes: boolean;
}
export class CommentNodes extends Component<CommentNodesProps, any> {
constructor(props: CommentNodesProps, context: any) {
constructor(props: any, context: any) {
super(props, context);
}
render() {
const maxComments = this.props.maxCommentsShown ?? this.props.nodes.length;
const borderColor = this.props.depth
? colorList[(this.props.depth - 1) % colorList.length]
: colorList[0];
let maxComments = this.props.maxCommentsShown
? this.props.maxCommentsShown
: this.props.nodes.length;
return (
this.props.nodes.length > 0 && (
<ul
className={classNames("comments", {
"ms-1": !!this.props.isChild,
"border-top border-light": !this.props.noBorder,
})}
style={
this.props.isChild
? `border-left: 2px solid ${borderColor} !important;`
: undefined
}
>
{this.props.nodes.slice(0, maxComments).map(node => (
<CommentNode
key={node.comment_view.comment.id}
node={node}
noBorder={this.props.noBorder}
noIndent={this.props.noIndent}
viewOnly={this.props.viewOnly}
locked={this.props.locked}
moderators={this.props.moderators}
admins={this.props.admins}
markable={this.props.markable}
showContext={this.props.showContext}
showCommunity={this.props.showCommunity}
enableDownvotes={this.props.enableDownvotes}
viewType={this.props.viewType}
allLanguages={this.props.allLanguages}
siteLanguages={this.props.siteLanguages}
hideImages={this.props.hideImages}
onCommentReplyRead={this.props.onCommentReplyRead}
onPersonMentionRead={this.props.onPersonMentionRead}
finished={this.props.finished}
onCreateComment={this.props.onCreateComment}
onEditComment={this.props.onEditComment}
onCommentVote={this.props.onCommentVote}
onBlockPerson={this.props.onBlockPerson}
onSaveComment={this.props.onSaveComment}
onDeleteComment={this.props.onDeleteComment}
onRemoveComment={this.props.onRemoveComment}
onDistinguishComment={this.props.onDistinguishComment}
onAddModToCommunity={this.props.onAddModToCommunity}
onAddAdmin={this.props.onAddAdmin}
onBanPersonFromCommunity={this.props.onBanPersonFromCommunity}
onBanPerson={this.props.onBanPerson}
onTransferCommunity={this.props.onTransferCommunity}
onFetchChildren={this.props.onFetchChildren}
onCommentReport={this.props.onCommentReport}
onPurgePerson={this.props.onPurgePerson}
onPurgeComment={this.props.onPurgeComment}
/>
))}
</ul>
)
<div className="comments">
{this.props.nodes.slice(0, maxComments).map(node => (
<CommentNode
key={node.comment_view.comment.id}
node={node}
noBorder={this.props.noBorder}
noIndent={this.props.noIndent}
viewOnly={this.props.viewOnly}
locked={this.props.locked}
moderators={this.props.moderators}
admins={this.props.admins}
postCreatorId={this.props.postCreatorId}
markable={this.props.markable}
showContext={this.props.showContext}
showCommunity={this.props.showCommunity}
enableDownvotes={this.props.enableDownvotes}
/>
))}
</div>
);
}
}

View file

@ -1,112 +1,69 @@
import { myAuthRequired } from "@utils/app";
import { Component, InfernoNode, linkEvent } from "inferno";
import { Component, linkEvent } from "inferno";
import { T } from "inferno-i18next-dess";
import {
CommentReportView,
CommentView,
ResolveCommentReport,
} from "lemmy-js-client";
import { CommentNodeI, CommentViewType } from "../../interfaces";
import { I18NextService } from "../../services";
import { Icon, Spinner } from "../common/icon";
import { i18n } from "../../i18next";
import { CommentNode as CommentNodeI } from "../../interfaces";
import { WebSocketService } from "../../services";
import { authField, wsClient } from "../../utils";
import { Icon } from "../common/icon";
import { PersonListing } from "../person/person-listing";
import { CommentNode } from "./comment-node";
interface CommentReportProps {
report: CommentReportView;
onResolveReport(form: ResolveCommentReport): void;
}
interface CommentReportState {
loading: boolean;
}
export class CommentReport extends Component<
CommentReportProps,
CommentReportState
> {
state: CommentReportState = {
loading: false,
};
export class CommentReport extends Component<CommentReportProps, any> {
constructor(props: any, context: any) {
super(props, context);
}
componentWillReceiveProps(
nextProps: Readonly<{ children?: InfernoNode } & CommentReportProps>
): void {
if (this.props != nextProps) {
this.setState({ loading: false });
}
}
render() {
const r = this.props.report;
const comment = r.comment;
const tippyContent = I18NextService.i18n.t(
let r = this.props.report;
let comment = r.comment;
let tippyContent = i18n.t(
r.comment_report.resolved ? "unresolve_report" : "resolve_report"
);
// Set the original post data ( a troll could change it )
comment.content = r.comment_report.original_comment_text;
const comment_view: CommentView = {
let comment_view: CommentView = {
comment,
creator: r.comment_creator,
post: r.post,
community: r.community,
creator_banned_from_community: r.creator_banned_from_community,
counts: r.counts,
subscribed: "NotSubscribed",
subscribed: false,
saved: false,
creator_blocked: false,
my_vote: r.my_vote,
};
const node: CommentNodeI = {
let node: CommentNodeI = {
comment_view,
children: [],
depth: 0,
};
return (
<div className="comment-report">
<div>
<CommentNode
node={node}
viewType={CommentViewType.Flat}
moderators={[]}
admins={[]}
enableDownvotes={true}
viewOnly={true}
showCommunity={true}
allLanguages={[]}
siteLanguages={[]}
hideImages
// All of these are unused, since its viewonly
finished={new Map()}
onSaveComment={() => {}}
onBlockPerson={() => {}}
onDeleteComment={() => {}}
onRemoveComment={() => {}}
onCommentVote={() => {}}
onCommentReport={() => {}}
onDistinguishComment={() => {}}
onAddModToCommunity={() => {}}
onAddAdmin={() => {}}
onTransferCommunity={() => {}}
onPurgeComment={() => {}}
onPurgePerson={() => {}}
onCommentReplyRead={() => {}}
onPersonMentionRead={() => {}}
onBanPersonFromCommunity={() => {}}
onBanPerson={() => {}}
onCreateComment={() => Promise.resolve({ state: "empty" })}
onEditComment={() => Promise.resolve({ state: "empty" })}
/>
<div>
{I18NextService.i18n.t("reporter")}:{" "}
<PersonListing person={r.creator} />
{i18n.t("reporter")}: <PersonListing person={r.creator} />
</div>
<div>
{I18NextService.i18n.t("reason")}: {r.comment_report.reason}
{i18n.t("reason")}: {r.comment_report.reason}
</div>
{r.resolver && (
<div>
@ -129,27 +86,23 @@ export class CommentReport extends Component<
data-tippy-content={tippyContent}
aria-label={tippyContent}
>
{this.state.loading ? (
<Spinner />
) : (
<Icon
icon="check"
classes={`icon-inline ${
r.comment_report.resolved ? "text-success" : "text-danger"
}`}
/>
)}
<Icon
icon="check"
classes={`icon-inline ${
r.comment_report.resolved ? "text-success" : "text-danger"
}`}
/>
</button>
</div>
);
}
handleResolveReport(i: CommentReport) {
i.setState({ loading: true });
i.props.onResolveReport({
let form: ResolveCommentReport = {
report_id: i.props.report.comment_report.id,
resolved: !i.props.report.comment_report.resolved,
auth: myAuthRequired(),
});
auth: authField(),
};
WebSocketService.Instance.send(wsClient.resolveCommentReport(form));
}
}

Some files were not shown because too many files have changed in this diff Show more