1 Commits

Author SHA1 Message Date
Paul
745ae35c6b Simple plugin proof of concept. 2021-07-11 09:51:34 +01:00
291 changed files with 6733 additions and 14107 deletions

View File

@@ -1,7 +0,0 @@
.github
.vscode
dist
dist_injected
node_modules
.env
.env.local

2
.env
View File

@@ -1,2 +1,2 @@
VITE_API_URL=https://api.revolt.chat
VITE_THEMES_URL=https://themes.revolt.chat
VITE_THEMES_URL=https://static.revolt.chat/themes

View File

@@ -1,2 +0,0 @@
VITE_API_URL=__API_URL__
VITE_THEMES_URL=https://themes.revolt.chat

1
.github/FUNDING.yml vendored
View File

@@ -1,2 +1 @@
ko_fi: insertish
custom: https://insrt.uk/donate

24
.github/SECURITY.md vendored
View File

@@ -1,24 +0,0 @@
# Security
## Reporting a Vulnerability
If you would like to report a security vulnerability,
please email **[security@revolt.chat](mailto:security@revolt.chat)**,
this will open a new ticket in ticket system, you should receive a response
within the next couple of days, potentially within a few minutes if someone
is currently active.
To help us best triage the issue, please provide:
- The type of issue at hand
- The name of the relevant project affected
- Reproduction steps
- Reference to any relevant source file(s) that you may suspect are causing the issue
- Any extra information about your configuration.
- Description of potential ways this can be exploited, if you can list any
For revoltchat/revite in particular:
- Please include the commit hash of the client, it is visible in settings under the log out button.
Thank you for helping Revolt.

View File

@@ -1,33 +0,0 @@
name: Build
description: Builds a project instance, assuming all the correct project files are in the build folder
inputs:
base:
name: Base path
description: The path to use as a base for linking
required: true
default: /
folder:
name: Build Folder
description: The folder to try to build from
required: true
default: .
runs:
using: composite
steps:
- name: Setup Node
uses: actions/setup-node@v2
with:
node-version: 15
cache: "yarn"
- name: Install Dependencies and Build
shell: bash -l {0}
env:
BUILD_FOLDER: ${{ inputs.folder }}
BASE: ${{ inputs.base }}
run: |
cd "$BUILD_FOLDER"
yarn install
yarn build --base "$BASE"

View File

@@ -1,111 +0,0 @@
name: Docker
on:
push:
branches:
- "master"
tags:
- "v*"
paths-ignore:
- ".github/**"
- "!.github/workflows/docker.yml"
- "!.github/workflows/preview_*.yml"
- ".vscode/**"
- ".gitignore"
- ".gitlab-ci.yml"
- "LICENSE"
- "README"
pull_request:
branches:
- "master"
paths-ignore:
- ".github/**"
- "!.github/workflows/docker.yml"
- "!.github/workflows/preview_*.yml"
- ".vscode/**"
- ".gitignore"
- ".gitlab-ci.yml"
- "LICENSE"
- "README"
workflow_dispatch:
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
architecture: [linux/amd64]
steps:
- name: Checkout
uses: actions/checkout@v2
with:
submodules: "recursive"
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Cache Docker layers
uses: actions/cache@v2
with:
path: /tmp/.buildx-cache/${{ matrix.architecture }}
key: ${{ runner.os }}-buildx-${{ matrix.architecture }}-${{ github.sha }}
- name: Build
uses: docker/build-push-action@v2
with:
context: .
platforms: ${{ matrix.architecture }}
cache-from: type=local,src=/tmp/.buildx-cache/${{ matrix.architecture }}
cache-to: type=local,dest=/tmp/.buildx-cache-new/${{ matrix.architecture }},mode=max
- name: Move cache
run: |
rm -rf /tmp/.buildx-cache/${{ matrix.architecture }}
mv /tmp/.buildx-cache-new/${{ matrix.architecture }} /tmp/.buildx-cache/${{ matrix.architecture }}
publish:
needs: [test]
runs-on: ubuntu-latest
if: github.event_name != 'pull_request'
steps:
- name: Checkout
uses: actions/checkout@v2
with:
submodules: "recursive"
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Cache amd64 Docker layers
uses: actions/cache@v2
with:
path: /tmp/.buildx-cache/linux/amd64
key: ${{ runner.os }}-buildx-linux/amd64-${{ github.sha }}
- name: Docker meta
id: meta
uses: docker/metadata-action@v3
with:
images: revoltchat/client, ghcr.io/revoltchat/client
- name: Login to DockerHub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to Github Container Registry
uses: docker/login-action@v1
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and publish
uses: docker/build-push-action@v2
with:
context: .
push: true
platforms: linux/amd64,linux/arm64,linux/arm/v7
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=local,src=/tmp/.buildx-cache/linux/amd64
cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max
- name: Move cache
run: |
rm -rf /tmp/.buildx-cache
mv /tmp/.buildx-cache-new /tmp/.buildx-cache

View File

@@ -1,17 +0,0 @@
name: Mirroring
on:
push:
branches:
- "master"
- "production"
jobs:
to_gitlab:
runs-on: ubuntu-18.04
steps:
- uses: actions/checkout@v1
- uses: pixta-dev/repository-mirroring-action@v1
with:
target_repo_url: git@gitlab.com:insert/revolt-vite.git
ssh_private_key: ${{ secrets.GITLAB_SSH_PRIVATE_KEY }}

View File

@@ -1,49 +0,0 @@
name: Add Issue to Board
on:
issues:
types: [opened]
jobs:
track_issue:
runs-on: ubuntu-latest
steps:
- name: Get project data
env:
GITHUB_TOKEN: ${{ secrets.PAT }}
run: |
gh api graphql -f query='
query {
organization(login: "revoltchat"){
projectNext(number: 3) {
id
fields(first:20) {
nodes {
id
name
settings
}
}
}
}
}' > project_data.json
echo 'PROJECT_ID='$(jq '.data.organization.projectNext.id' project_data.json) >> $GITHUB_ENV
echo 'STATUS_FIELD_ID='$(jq '.data.organization.projectNext.fields.nodes[] | select(.name== "Status") | .id' project_data.json) >> $GITHUB_ENV
echo 'TODO_OPTION_ID='$(jq '.data.organization.projectNext.fields.nodes[] | select(.name== "Status") |.settings | fromjson.options[] | select(.name=="Todo") |.id' project_data.json) >> $GITHUB_ENV
- name: Add issue to project
env:
GITHUB_TOKEN: ${{ secrets.PAT }}
ISSUE_ID: ${{ github.event.issue.node_id }}
run: |
item_id="$( gh api graphql -f query='
mutation($project:ID!, $issue:ID!) {
addProjectNextItem(input: {projectId: $project, contentId: $issue}) {
projectNextItem {
id
}
}
}' -f project=$PROJECT_ID -f issue=$ISSUE_ID --jq '.data.addProjectNextItem.projectNextItem.id')"
echo 'ITEM_ID='$item_id >> $GITHUB_ENV

View File

@@ -1,72 +0,0 @@
name: Add PR to Board
on:
pull_request_target:
types: [opened, synchronize, ready_for_review, review_requested]
jobs:
track_pr:
runs-on: ubuntu-latest
steps:
- name: Get project data
env:
GITHUB_TOKEN: ${{ secrets.PAT }}
run: |
gh api graphql -f query='
query {
organization(login: "revoltchat"){
projectNext(number: 3) {
id
fields(first:20) {
nodes {
id
name
settings
}
}
}
}
}' > project_data.json
echo 'PROJECT_ID='$(jq '.data.organization.projectNext.id' project_data.json) >> $GITHUB_ENV
echo 'STATUS_FIELD_ID='$(jq '.data.organization.projectNext.fields.nodes[] | select(.name== "Status") | .id' project_data.json) >> $GITHUB_ENV
echo 'INCOMING_OPTION_ID='$(jq '.data.organization.projectNext.fields.nodes[] | select(.name== "Status") |.settings | fromjson.options[] | select(.name=="Incoming PRs") |.id' project_data.json) >> $GITHUB_ENV
- name: Add PR to project
env:
GITHUB_TOKEN: ${{ secrets.PAT }}
PR_ID: ${{ github.event.pull_request.node_id }}
run: |
item_id="$( gh api graphql -f query='
mutation($project:ID!, $pr:ID!) {
addProjectNextItem(input: {projectId: $project, contentId: $pr}) {
projectNextItem {
id
}
}
}' -f project=$PROJECT_ID -f pr=$PR_ID --jq '.data.addProjectNextItem.projectNextItem.id')"
echo 'ITEM_ID='$item_id >> $GITHUB_ENV
- name: Set fields
env:
GITHUB_TOKEN: ${{ secrets.PAT }}
run: |
gh api graphql -f query='
mutation (
$project: ID!
$item: ID!
$status_field: ID!
$status_value: String!
) {
set_status: updateProjectNextItemField(input: {
projectId: $project
itemId: $item
fieldId: $status_field
value: $status_value
}) {
projectNextItem {
id
}
}
}' -f project=$PROJECT_ID -f item=$ITEM_ID -f status_field=$STATUS_FIELD_ID -f status_value=${{ env.INCOMING_OPTION_ID }} --silent

7
.gitignore vendored
View File

@@ -1,12 +1,5 @@
node_modules
.DS_Store
dist
dist_injected
dist-ssr
*.local
*.log
/.idea
public/assets
public/assets_*
!public/assets_default

View File

@@ -1,40 +0,0 @@
image: node:14-buster
variables:
GIT_SUBMODULE_STRATEGY: recursive
cache:
paths:
- node_modules
# Fetch dependencies and setup project for compilation.
install:
stage: prepare
script:
- yarn
# Type check the project
typecheck:
stage: test
needs:
- install
dependencies:
- install
script:
- yarn typecheck
# Lint the project and check prettier output.
lint:
stage: test
allow_failure: true
needs:
- install
dependencies:
- install
script:
- yarn lint
- yarn --check 'src/**/*.{js,jsx,ts,tsx}'
stages:
- prepare
- test

2
.gitmodules vendored
View File

@@ -1,3 +1,3 @@
[submodule "external/lang"]
path = external/lang
url = https://github.com/revoltchat/translations
url = https://gitlab.insrt.uk/revolt/translations

View File

@@ -1 +0,0 @@
src/components/markdown/prism.ts

View File

@@ -1,14 +1,8 @@
module.exports = {
tabWidth: 4,
trailingComma: "all",
jsxBracketSameLine: true,
importOrder: [
"preact|classnames|.scss$",
"/(lib)",
"/(redux|mobx)",
"/(context)",
"/(ui|common)|.svg|.webp|.png|.jpg$",
"^[./]",
],
importOrderSeparation: true,
};
"tabWidth": 4,
"trailingComma": "all",
"jsxBracketSameLine": true,
"importOrder": ["preact|classnames|.scss$", "/(lib)", "/(redux)", "/(context)", "/(ui|common)|.svg$", "^[./]"],
"importOrderSeparation": true,
}

View File

@@ -1,7 +1,3 @@
{
"recommendations": [
"esbenp.prettier-vscode",
"dbaeumer.vscode-eslint",
"kol.commit-lint"
]
"recommendations": ["esbenp.prettier-vscode"]
}

View File

@@ -1,5 +1,4 @@
{
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"compile-hero.disable-compile-files-on-did-save-code": true
"editor.formatOnSave": true
}

View File

@@ -1,20 +0,0 @@
FROM node:16-buster AS builder
WORKDIR /usr/src/app
COPY package*.json ./
RUN yarn --no-cache
COPY . .
COPY .env.build .env
RUN yarn add --dev @babel/plugin-proposal-decorators
RUN yarn typecheck
RUN yarn build
RUN npm prune --production
FROM node:16-buster
WORKDIR /usr/src/app
COPY --from=builder /usr/src/app .
EXPOSE 5000
CMD [ "yarn", "start:inject" ]

View File

@@ -1,57 +1,21 @@
# Revite
## Description
This is the web client for Revolt, which is also available live at [app.revolt.chat](https://app.revolt.chat).
## Stack
You can track progress on the client on [our Wekan board](https://wekan.insrt.uk/b/jj3x5C6nbYzM6ERQD/revolt).
- [Preact](https://preactjs.com/)
- [Vite](https://vitejs.dev/)
## Submodule Hint
This project contains submodules. Run `git submodule init` after you clone this repository to initialize the submodules.
It is also recommended to run `git submodule update` after you pull from upstream.
## Resources
### Revite
- [Revite Issue Board](https://github.com/revoltchat/revite/issues)
- [Google Drive Folder with Screenshots](https://drive.google.com/drive/folders/1Ckhl7_9OTTaKzyisrWHzZw1hHj55JwhD)
### Revolt
- [Revolt Project Board](https://github.com/revoltchat/revolt/discussions) (Submit feature requests here)
- [Revolt Testers Server](https://app.revolt.chat/invite/Testers)
- [Contribution Guide](https://developers.revolt.chat/contributing)
## Quick Start
Get revite up and running locally.
```
git clone --recursive https://github.com/revoltchat/revite
cd revite
yarn
yarn dev
```
Official screenshots of the client are available in [this Google Drive folder](https://drive.google.com/drive/folders/1Ckhl7_9OTTaKzyisrWHzZw1hHj55JwhD).
## CLI Commands
| Command | Description |
| ------------------- | -------------------------------------------- |
| `yarn pull` | Setup assets required for Revite. |
| `yarn dev` | Start the Revolt client in development mode. |
| `yarn build` | Build the Revolt client. |
| `yarn preview` | Start a local server with the built client. |
| `yarn lint` | Run ESLint on the client. |
| `yarn fmt` | Run Prettier on the client. |
| `yarn typecheck` | Run TypeScript type checking on the client. |
| `yarn start` | Start a local sirv server with built client. |
| `yarn start:inject` | Inject a given API URL and start server. |
* `yarn dev`: Runs a development server.
## License
* `yarn build`: Creates a production build of the client.
Revite is licensed under the [GNU Affero General Public License v3.0](https://github.com/revoltchat/revite/blob/master/LICENSE).
* `yarn preview`: Starts a local server with the production build.
* `yarn lint`: Runs ESLint to check project.
* `yarn fmt`: Runs prettier on source code.
* `yarn typecheck`: Runs Typescript compiler in noEmit mode.

View File

@@ -1 +1 @@
0.5.3-1
1.0.0-vite

2
external/lang vendored

View File

@@ -3,74 +3,29 @@
<head>
<meta charset="UTF-8" />
<title>Revolt</title>
<meta name="apple-mobile-web-app-title" content="Revolt" />
<meta name="apple-mobile-web-app-title" content="Revolt">
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, user-scalable=no"
/>
<meta name="apple-mobile-web-app-capable" content="yes" />
<!--<link rel="icon" type="image/svg+xml" href="/src/favicon.svg" />-->
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no" />
<meta name="apple-mobile-web-app-capable" content="yes">
<!--App Icons-->
<link
rel="apple-touch-icon"
href="public/assets/icons/apple-touch.png"
/>
<link rel="icon" type="image/png" href="/assets/logo_round.png" />
<link rel="apple-touch-icon" href="public/assets/icons/apple-touch.png">
<link rel="icon" type="image/png" href="/src/assets/logo_round.png" />
<!--Splash Screens for iOS Devices-->
<link
href="public/assets/splashscreens/iphone5_splash.png"
media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2)"
rel="apple-touch-startup-image"
/>
<link
href="public/assets/splashscreens/iphone6_splash.png"
media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2)"
rel="apple-touch-startup-image"
/>
<link
href="public/assets/splashscreens/iphoneplus_splash.png"
media="(device-width: 621px) and (device-height: 1104px) and (-webkit-device-pixel-ratio: 3)"
rel="apple-touch-startup-image"
/>
<link
href="public/assets/splashscreens/iphonex_splash.png"
media="(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3)"
rel="apple-touch-startup-image"
/>
<link
href="public/assets/splashscreens/iphonexr_splash.png"
media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2)"
rel="apple-touch-startup-image"
/>
<link
href="public/assets/splashscreens/iphonexsmax_splash.png"
media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3)"
rel="apple-touch-startup-image"
/>
<link
href="public/assets/splashscreens/ipad_splash.png"
media="(device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2)"
rel="apple-touch-startup-image"
/>
<link
href="public/assets/splashscreens/ipadpro1_splash.png"
media="(device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2)"
rel="apple-touch-startup-image"
/>
<link
href="public/assets/splashscreens/ipadpro3_splash.png"
media="(device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2)"
rel="apple-touch-startup-image"
/>
<link
href="public/assets/splashscreens/ipadpro2_splash.png"
media="(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2)"
rel="apple-touch-startup-image"
/>
<link href="public/assets/splashscreens/iphone5_splash.png" media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2)" rel="apple-touch-startup-image" />
<link href="public/assets/splashscreens/iphone6_splash.png" media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2)" rel="apple-touch-startup-image" />
<link href="public/assets/splashscreens/iphoneplus_splash.png" media="(device-width: 621px) and (device-height: 1104px) and (-webkit-device-pixel-ratio: 3)" rel="apple-touch-startup-image" />
<link href="public/assets/splashscreens/iphonex_splash.png" media="(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3)" rel="apple-touch-startup-image" />
<link href="public/assets/splashscreens/iphonexr_splash.png" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2)" rel="apple-touch-startup-image" />
<link href="public/assets/splashscreens/iphonexsmax_splash.png" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3)" rel="apple-touch-startup-image" />
<link href="public/assets/splashscreens/ipad_splash.png" media="(device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2)" rel="apple-touch-startup-image" />
<link href="public/assets/splashscreens/ipadpro1_splash.png" media="(device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2)" rel="apple-touch-startup-image" />
<link href="public/assets/splashscreens/ipadpro3_splash.png" media="(device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2)" rel="apple-touch-startup-image" />
<link href="public/assets/splashscreens/ipadpro2_splash.png" media="(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2)" rel="apple-touch-startup-image" />
</head>
<body onContextMenu="return false" ontouchstart="">
<body>
<div id="app"></div>
<script type="module" src="/src/main.tsx"></script>
</body>

View File

@@ -1,15 +1,12 @@
{
"version": "0.0.0",
"scripts": {
"dev": "node scripts/setup_assets.js --check && vite",
"pull": "node scripts/setup_assets.js",
"build": "rimraf build && node scripts/setup_assets.js --check && vite build",
"dev": "vite",
"build": "rimraf build && vite build",
"preview": "vite preview",
"lint": "eslint 'src/**/*.{js,jsx,ts,tsx}'",
"fmt": "prettier --write 'src/**/*.{js,jsx,ts,tsx}'",
"typecheck": "tsc --noEmit",
"start": "sirv dist --cors --single --host",
"start:inject": "node scripts/inject.js && sirv dist_injected --cors --single --host"
"typecheck": "tsc --noEmit"
},
"eslintConfig": {
"parser": "@typescript-eslint/parser",
@@ -21,31 +18,11 @@
"build/"
],
"rules": {
"radix": "off",
"no-spaced-func": "off",
"react/no-danger": "off",
"@typescript-eslint/explicit-module-boundary-types": "off",
"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/no-unused-vars": [
"warn",
{
"varsIgnorePattern": "^_"
}
],
"no-unused-vars": [
"warn",
{
"varsIgnorePattern": "^_"
}
]
"@typescript-eslint/explicit-module-boundary-types": "off"
}
},
"dependencies": {
"fs-extra": "^10.0.0",
"klaw": "^3.0.0",
"react-beautiful-dnd": "^13.1.0",
"sirv-cli": "^1.0.14",
"vite": "^2.6.14"
"preact": "^10.5.13"
},
"devDependencies": {
"@fontsource/atkinson-hyperlegible": "^4.4.5",
@@ -53,7 +30,6 @@
"@fontsource/comic-neue": "^4.4.5",
"@fontsource/fira-code": "^4.4.5",
"@fontsource/inter": "^4.4.5",
"@fontsource/jetbrains-mono": "^4.4.5",
"@fontsource/lato": "^4.4.5",
"@fontsource/montserrat": "^4.4.5",
"@fontsource/noto-sans": "^4.4.5",
@@ -71,7 +47,7 @@
"@rollup/plugin-replace": "^2.4.2",
"@styled-icons/boxicons-logos": "^10.34.0",
"@styled-icons/boxicons-regular": "^10.34.0",
"@styled-icons/boxicons-solid": "^10.37.0",
"@styled-icons/boxicons-solid": "^10.34.0",
"@styled-icons/simple-icons": "^10.33.0",
"@tippyjs/react": "^4.2.5",
"@traptitech/markdown-it-katex": "^3.4.3",
@@ -83,11 +59,9 @@
"@types/node": "^15.12.4",
"@types/preact-i18n": "^2.3.0",
"@types/prismjs": "^1.16.5",
"@types/react-beautiful-dnd": "^13.1.2",
"@types/react-helmet": "^6.1.1",
"@types/react-router-dom": "^5.1.7",
"@types/react-scroll": "^1.8.2",
"@types/react-virtualized-auto-sizer": "^1.0.1",
"@types/styled-components": "^5.1.10",
"@types/twemoji": "^12.1.1",
"@typescript-eslint/eslint-plugin": "^4.27.0",
@@ -99,6 +73,7 @@
"eslint-config-preact": "^1.1.4",
"eventemitter3": "^4.0.7",
"highlight.js": "^11.0.1",
"idb": "^6.1.2",
"localforage": "^1.9.0",
"lodash.defaultsdeep": "^4.6.1",
"lodash.isequal": "^4.5.0",
@@ -107,10 +82,7 @@
"markdown-it-sub": "^1.0.0",
"markdown-it-sup": "^1.0.0",
"mediasoup-client": "npm:@insertish/mediasoup-client@3.6.36-esnext",
"mobx": "^6.3.2",
"mobx-react-lite": "^3.2.0",
"preact": "^10.5.14",
"preact-context-menu": "^0.2.1",
"preact-context-menu": "^0.1.5",
"preact-i18n": "^2.4.0-preactx",
"prettier": "^2.3.1",
"prismjs": "^1.23.0",
@@ -121,25 +93,17 @@
"react-redux": "^7.2.4",
"react-router-dom": "^5.2.0",
"react-scroll": "^1.8.2",
"react-virtualized-auto-sizer": "^1.0.5",
"react-virtuoso": "^1.10.4",
"redux": "^4.1.0",
"revolt-api": "0.5.3-alpha.10",
"revolt.js": "^5.1.0-alpha.10",
"revolt.js": "4.3.3-alpha.14",
"rimraf": "^3.0.2",
"sass": "^1.35.1",
"shade-blend-color": "^1.0.0",
"styled-components": "^5.3.0",
"typescript": "^4.4.2",
"typescript": "^4.3.2",
"ulid": "^2.3.0",
"use-resize-observer": "^7.0.0",
"vite-plugin-compression": "^0.3.6",
"vite": "npm:@insertish/vite@2.4.0-beta.3-dynamic-import-css-3c1466b",
"vite-plugin-pwa": "^0.8.1",
"workbox-precaching": "^6.1.5"
},
"name": "client",
"main": "index.js",
"repository": "https://github.com/revoltchat/revite.git",
"author": "Paul <paulmakles@gmail.com>",
"license": "MIT"
}
}

View File

@@ -0,0 +1,4 @@
<svg width="343" height="343" viewBox="0 0 343 343" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M163.994 101.423C163.994 118.65 154.621 128.991 134.617 128.991H101.497V74.4793H134.623C154.621 74.4793 163.994 85.1293 163.994 101.423ZM16 30L48.0653 74.5844V249.881H101.503V166.887H114.317L159.948 249.899H220.262L169.636 162.814C183.771 159.366 196.307 151.183 205.17 139.62C214.033 128.057 218.692 113.807 218.375 99.2285C218.375 61.0106 191.502 30 137.749 30H48.0653H16Z" fill="#EFAB44" stroke="#EFAB44" stroke-width="0.97733"/>
<path d="M323.215 148.038L280.59 190.653L241.854 151.906L284.469 109.292C258.674 98.6844 227.937 103.835 206.997 124.797C186.057 145.737 180.895 176.485 191.503 202.28L122.731 271.04C118.447 275.324 118.447 282.25 122.731 286.534L145.984 309.787C150.268 314.071 157.194 314.071 161.478 309.787L230.238 241.015C256.033 251.622 286.781 246.461 307.721 225.521C328.661 204.57 333.812 173.822 323.215 148.038Z" fill="#99AAB5"/>
</svg>

After

Width:  |  Height:  |  Size: 973 B

View File

@@ -0,0 +1,4 @@
<svg width="343" height="343" viewBox="0 0 343 343" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M252.571 30L333.429 171.5L252.571 313H90.8571L10 171.5L90.8571 30H252.571Z" fill="#509AF0"/>
<path d="M164.486 82.3018C182.31 82.3018 196.421 86.5928 206.818 95.1748C217.298 103.757 222.538 115.722 222.538 131.071C222.538 139.323 220.475 146.791 216.349 153.475C212.223 160.159 206.406 165.523 198.896 169.566C209.046 173.032 216.803 178.561 222.167 186.153C227.613 193.744 230.336 202.945 230.336 213.755C230.336 230.672 225.427 243.875 215.607 253.365C205.787 262.772 192.295 267.476 175.131 267.476C162.093 267.476 150.499 264.34 140.349 258.068V313H104.577V137.012C104.577 126.697 107.176 117.373 112.375 109.038C117.573 100.621 124.794 94.0608 134.036 89.3572C143.278 84.6536 153.428 82.3018 164.486 82.3018ZM186.766 133.794C186.766 127.027 184.703 121.581 180.577 117.455C176.534 113.329 171.17 111.266 164.486 111.266C157.472 111.266 151.695 113.618 147.157 118.322C142.618 122.943 140.349 129.338 140.349 137.507V231.456C147.115 236.242 156.028 238.635 167.085 238.635C175.502 238.635 182.186 236.283 187.137 231.58C192.089 226.793 194.564 220.687 194.564 213.26C194.564 204.348 192.295 197.416 187.756 192.465C183.3 187.432 176.699 184.915 167.952 184.915H155.945V158.797H165.6C179.711 158.302 186.766 149.968 186.766 133.794Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1,15 @@
<svg width="343" height="343" viewBox="0 0 343 343" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0)">
<rect x="141.421" y="31" width="200" height="200" transform="rotate(45 141.421 31)" fill="#01BE6E"/>
<path d="M137.926 182.464C128.751 173.507 120.668 165.642 116.08 147.51H148.194V133.965H116.298V116.051H102.535V134.184H70.4214V147.728H103.191C103.191 147.728 102.972 150.35 102.535 152.316C97.9475 170.23 92.486 181.59 70.4214 192.731L75.0091 206.276C95.9814 195.134 106.904 181.153 111.711 165.642C116.298 177.439 124.163 187.051 133.12 195.79L137.926 182.464Z" fill="white"/>
<path d="M180.307 138.551H161.956L129.842 228.775H143.605L152.781 201.686H189.482L198.658 228.775H212.421L180.307 138.551ZM157.368 188.142L171.132 152.095L184.895 188.36L157.368 188.142Z" fill="white"/>
<path d="M305.473 170.182L208.174 267.48C206.834 268.82 205.148 269.773 203.306 270.234L148.421 281.018L159.206 226.123C159.666 224.291 160.619 222.605 161.959 221.265L259.258 123.967L274.421 108.803L320.421 103.921V155.233L305.473 170.182Z" fill="#99AAB5"/>
<path d="M208.174 267.478L305.473 170.18L259.258 123.965L161.959 221.263C160.619 222.603 159.666 224.289 159.206 226.121L148.421 281.016L203.306 270.232C205.148 269.771 206.834 268.818 208.174 267.478ZM336.883 138.769C345.06 130.592 345.06 117.337 336.883 109.16L320.278 92.5542C312.1 84.3771 298.845 84.3771 290.668 92.5542L274.063 109.16L320.278 155.375L336.883 138.769Z" fill="#EA596E"/>
<path d="M305.473 170.182L208.174 267.48C206.834 268.82 205.148 269.773 203.306 270.234L148.421 281.018L159.206 226.123C159.666 224.291 160.619 222.605 161.959 221.265L259.258 123.967L305.473 170.182Z" fill="#FFCC4D"/>
</g>
<defs>
<clipPath id="clip0">
<rect width="343" height="343" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

View File

@@ -0,0 +1,3 @@
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M30.9299 18.0767C30.9299 20.7484 29.4776 22.3521 26.3783 22.3521H21.2468V13.8981H26.3792C29.4776 13.8981 30.9299 15.5498 30.9299 18.0767ZM8 7L12.9681 13.9144V41.1006H21.2477V28.2293H23.2331L30.3031 41.1035H39.648L31.8041 27.5976C33.9941 27.0629 35.9365 25.7938 37.3097 24.0006C38.683 22.2073 39.4048 19.9973 39.3556 17.7364C39.3556 11.8093 35.192 7 26.8636 7H8Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 490 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

BIN
public/assets/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

View File

@@ -1 +0,0 @@
<svg xmlns:xlink="http://www.w3.org/1999/xlink" width="64.00001046823172" xmlns="http://www.w3.org/2000/svg" id="screenshot" version="1.1" viewBox="-0.000008942940667111543 -0.0000033745862566547657 64.00001046823172 64.00000545874563" height="64.00000545874563" style="-webkit-print-color-adjust: exact;"><g id="shape-d9b11490-3403-11ec-bc16-7b519797d558"><rect rx="0" ry="0" x="0" y="0" transform="matrix(1.0000000000000007,-8.726646259971662e-8,-1.5707963280665485e-7,1.0000000000000124,0.000005026548230091521,0.0000027925264056705146)" width="64" height="64" style="fill: rgb(255, 255, 255); fill-opacity: 1;"/></g></svg>

Before

Width:  |  Height:  |  Size: 626 B

View File

@@ -1 +0,0 @@
<svg xmlns:xlink="http://www.w3.org/1999/xlink" width="64.00001046823172" xmlns="http://www.w3.org/2000/svg" id="screenshot" version="1.1" viewBox="-0.000008942940667111543 -0.0000033745862566547657 64.00001046823172 64.00000545874563" height="64.00000545874563" style="-webkit-print-color-adjust: exact;"><g id="shape-d9b11490-3403-11ec-bc16-7b519797d558"><rect rx="0" ry="0" x="0" y="0" transform="matrix(1.0000000000000007,-8.726646259971662e-8,-1.5707963280665485e-7,1.0000000000000124,0.000005026548230091521,0.0000027925264056705146)" width="64" height="64" style="fill: rgb(255, 255, 255); fill-opacity: 1;"/></g></svg>

Before

Width:  |  Height:  |  Size: 626 B

View File

@@ -1 +0,0 @@
<svg xmlns:xlink="http://www.w3.org/1999/xlink" width="64.00001046823172" xmlns="http://www.w3.org/2000/svg" id="screenshot" version="1.1" viewBox="-0.000008942940667111543 -0.0000033745862566547657 64.00001046823172 64.00000545874563" height="64.00000545874563" style="-webkit-print-color-adjust: exact;"><g id="shape-d9b11490-3403-11ec-bc16-7b519797d558"><rect rx="0" ry="0" x="0" y="0" transform="matrix(1.0000000000000007,-8.726646259971662e-8,-1.5707963280665485e-7,1.0000000000000124,0.000005026548230091521,0.0000027925264056705146)" width="64" height="64" style="fill: rgb(255, 255, 255); fill-opacity: 1;"/></g></svg>

Before

Width:  |  Height:  |  Size: 626 B

View File

@@ -1 +0,0 @@
<svg xmlns:xlink="http://www.w3.org/1999/xlink" width="64.00001046823172" xmlns="http://www.w3.org/2000/svg" id="screenshot" version="1.1" viewBox="-0.000008942940667111543 -0.0000033745862566547657 64.00001046823172 64.00000545874563" height="64.00000545874563" style="-webkit-print-color-adjust: exact;"><g id="shape-d9b11490-3403-11ec-bc16-7b519797d558"><rect rx="0" ry="0" x="0" y="0" transform="matrix(1.0000000000000007,-8.726646259971662e-8,-1.5707963280665485e-7,1.0000000000000124,0.000005026548230091521,0.0000027925264056705146)" width="64" height="64" style="fill: rgb(255, 255, 255); fill-opacity: 1;"/></g></svg>

Before

Width:  |  Height:  |  Size: 626 B

View File

@@ -1 +0,0 @@
<svg xmlns:xlink="http://www.w3.org/1999/xlink" width="64.00001046823172" xmlns="http://www.w3.org/2000/svg" id="screenshot" version="1.1" viewBox="-0.000008942940667111543 -0.0000033745862566547657 64.00001046823172 64.00000545874563" height="64.00000545874563" style="-webkit-print-color-adjust: exact;"><g id="shape-d9b11490-3403-11ec-bc16-7b519797d558"><rect rx="0" ry="0" x="0" y="0" transform="matrix(1.0000000000000007,-8.726646259971662e-8,-1.5707963280665485e-7,1.0000000000000124,0.000005026548230091521,0.0000027925264056705146)" width="64" height="64" style="fill: rgb(255, 255, 255); fill-opacity: 1;"/></g></svg>

Before

Width:  |  Height:  |  Size: 626 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

View File

@@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-chat" viewBox="0 0 16 16">
<path d="M2.678 11.894a1 1 0 0 1 .287.801 10.97 10.97 0 0 1-.398 2c1.395-.323 2.247-.697 2.634-.893a1 1 0 0 1 .71-.074A8.06 8.06 0 0 0 8 14c3.996 0 7-2.807 7-6 0-3.192-3.004-6-7-6S1 4.808 1 8c0 1.468.617 2.83 1.678 3.894zm-.493 3.905a21.682 21.682 0 0 1-.713.129c-.2.032-.352-.176-.273-.362a9.68 9.68 0 0 0 .244-.637l.003-.01c.248-.72.45-1.548.524-2.319C.743 11.37 0 9.76 0 8c0-3.866 3.582-7 8-7s8 3.134 8 7-3.582 7-8 7a9.06 9.06 0 0 1-2.347-.306c-.52.263-1.639.742-3.468 1.105z"/>
</svg>

Before

Width:  |  Height:  |  Size: 613 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

View File

@@ -1,21 +0,0 @@
<svg xmlns:xlink="http://www.w3.org/1999/xlink" width="1266.1200469886633" xmlns="http://www.w3.org/2000/svg" id="screenshot" version="1.1" viewBox="-0.00002945408027699159 -0.0001327683537510893 1266.1200469886633 240.00014843559939" height="240.00014843559939" style="-webkit-print-color-adjust: exact;"><g id="shape-0d86b240-33f8-11ec-bc16-7b519797d558" width="1268" height="242" fill="none"><g id="shape-939dc8a0-33f8-11ec-bc16-7b519797d558"><g><path d="M14795 8655 c-472 -46 -855 -158 -1277 -372 -384 -195 -700 -454
-973 -798 -370 -468 -598 -1019 -691 -1675 -42 -299 -58 -819 -34 -1131 50
-637 207 -1176 477 -1634 415 -702 1027 -1168 1838 -1398 530 -151 1212 -178
1790 -72 782 144 1452 566 1915 1205 263 362 451 843 495 1261 l7 66 -953 5
c-644 4 -956 2 -961 -5 -3 -6 -9 -31 -13 -55 -17 -113 -95 -302 -174 -422 -56
-86 -187 -216 -277 -276 -199 -132 -398 -190 -694 -201 -277 -10 -504 36 -725
146 -461 230 -726 723 -786 1466 -15 190 -6 638 16 794 63 454 203 797 424
1046 212 238 488 376 831 416 143 16 386 7 513 -20 197 -42 402 -144 539 -269
123 -114 224 -276 281 -452 31 -94 41 -139 62 -272 l6 -38 956 0 955 0 -6 58
c-50 437 -133 735 -296 1067 -258 524 -661 930 -1195 1202 -365 186 -721 291
-1175 349 -175 22 -690 27 -875 9z M19190 5090 l0 -3490 945 0 945 0 0 1365 0
1365 1260 0 1260 0 0 -1365 0 -1365 940 0 940 0 0 3490 0 3490 -940 0 -940 0
0 -1365 0 -1365 -1260 0 -1260 0 0 1365 0 1365 -945 0 -945 0 0 -3490z M27365
5253 c-604 -1831 -1122 -3401 -1151 -3491 l-52 -162 1017 0 1017 0 179 592
c98 326 189 628 202 671 l24 77 1154 0 1154 0 24 -77 c13 -43 104 -345 202
-671 l179 -592 1017 0 1018 0 -14 43 c-19 57 -854 2589 -1639 4969 -355 1077
-646 1960 -646 1963 0 3 -582 5 -1293 5 l-1293 0 -1099 -3327z m2429 1390
c225 -747 677 -2250 682 -2268 l6 -25 -727 0 -727 0 6 23 c3 12 156 524 341
1137 185 613 338 1123 341 1133 4 12 16 17 39 17 23 0 35 -5 39 -17z M32960
7820 l0 -760 1050 0 1050 0 0 -2730 0 -2730 930 0 930 0 0 2730 0 2730 1050 0
1050 0 0 760 0 760 -3030 0 -3030 0 0 -760z" transform="translate(0, 0) scale(0.25, 0.25) translate(0.000000,960.000000) scale(0.100000,-0.100000)" fill="#ffffff" stroke="none"/></g></g></g></svg>

Before

Width:  |  Height:  |  Size: 2.1 KiB

0
scripts/publish.sh → publish.sh Executable file → Normal file
View File

View File

@@ -1,49 +0,0 @@
/* eslint-disable */
const { copy, remove, access, readFile, writeFile } = require("fs-extra");
const klaw = require("klaw");
let target = /__API_URL__/g;
let replacement = process.env.REVOLT_PUBLIC_URL;
let BUILD_DIRECTORY = "dist";
let OUT_DIRECTORY = "dist_injected";
if (typeof replacement === "undefined") {
console.error("No REVOLT_PUBLIC_URL specified in environment variables.");
process.exit(1);
}
(async () => {
console.log("Ensuring project has been built at least once.");
try {
await access(BUILD_DIRECTORY);
} catch (err) {
console.error("Build project at least once!");
return process.exit(1);
}
console.log("Determining if injected build already exists...");
try {
await access(OUT_DIRECTORY);
console.log("Deleting existing build...");
await remove(OUT_DIRECTORY);
} catch (err) {}
await copy(BUILD_DIRECTORY, OUT_DIRECTORY);
console.log("Processing bundles...");
for await (const file of klaw(OUT_DIRECTORY)) {
let path = file.path;
if (path.endsWith(".js")) {
let data = await readFile(path);
if (target.test(data)) {
console.log("Matched file", path);
let processed = data.toString().replace(target, replacement);
await writeFile(path, processed);
}
}
}
console.log("Complete.");
})();

View File

@@ -1,39 +0,0 @@
/* eslint-disable */
const { copy, remove, access } = require("fs-extra");
const { exec: cexec } = require("child_process");
const { resolve } = require("path");
let target = process.env.REVOLT_SASS;
let branch = process.env.REVOLT_SASS_BRANCH;
let DEFAULT_DIRECTORY = "public/assets_default";
let OUT_DIRECTORY = "public/assets";
function exec(command) {
return new Promise((fulfil, reject) => {
cexec(command, (err, stdout, stderr) => {
if (err) {
reject(err);
return;
}
fulfil({ stdout, stderr });
});
});
}
(async () => {
try {
await access(OUT_DIRECTORY);
if (process.argv[2] === "--check") return;
await remove(OUT_DIRECTORY);
} catch (err) {}
if (target) {
let arg = branch ? `-b ${branch} ` : "";
await exec(`git clone ${arg}${target} ${OUT_DIRECTORY}`);
await exec(`rm -rf ${resolve(OUT_DIRECTORY, ".git")}`);
} else {
await copy(DEFAULT_DIRECTORY, OUT_DIRECTORY);
}
})();

3
src/assets/logo.svg Normal file
View File

@@ -0,0 +1,3 @@
<svg width="500" height="500" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M290.947 209.226C290.947 227.612 280.944 238.648 259.595 238.648H224.247V180.471H259.601C280.944 180.471 290.947 191.837 290.947 209.226ZM133 133L167.222 180.583V367.669H224.254V279.094H237.93L286.63 367.689H351L296.969 274.746C312.054 271.066 325.434 262.333 334.893 249.993C344.353 237.652 349.325 222.444 348.986 206.885C348.986 166.096 320.306 133 262.938 133H167.222H133Z" fill="#FF4654" stroke="#FF4654" stroke-width="1.04306"/>
</svg>

After

Width:  |  Height:  |  Size: 551 B

BIN
src/assets/logo_round.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

4
src/assets/wide.svg Normal file
View File

@@ -0,0 +1,4 @@
<?xml version="1.0"?>
<svg xmlns="http://www.w3.org/2000/svg" width="193.733" height="37.438" viewBox="0 0 193.733 37.438">
<path d="M23.393,1.382c0,2.787-1.52,4.46-4.764,4.46H13.258V-2.977H18.63C21.873-2.977,23.393-1.254,23.393,1.382Zm-24-11.555,5.2,7.213V25.4h8.666V11.973h2.078l7.4,13.43h9.781l-8.21-14.089A10.355,10.355,0,0,0,32.212,1.027c0-6.183-4.358-11.2-13.075-11.2Zm60.035,0H37.634V25.4H59.426V18.46H46.3v-7.8H57.906V3.966H46.3V-2.969H59.426Zm20.981,26.86-8.818-26.86H62.365L74.984,25.4H85.83L98.449-10.173H89.276Zm56.659-9.173c0-10.693-8.058-18.194-18.194-18.194-10.085,0-18.3,7.5-18.3,18.194a17.9,17.9,0,0,0,18.3,18.244A17.815,17.815,0,0,0,137.066,7.514Zm-27.62,0c0-6.335,3.649-10.338,9.426-10.338,5.676,0,9.376,4,9.376,10.338,0,6.233-3.7,10.338-9.376,10.338C113.095,17.852,109.446,13.747,109.446,7.514ZM141.88-10.173V25.4H161.9v-6.95H150.545V-10.173Zm22.248,7.2h9.426V25.4h8.666V-2.975h9.426v-7.2H164.128Z" transform="translate(1.586 11.18)" fill="#fff" stroke="#fff" stroke-width="1"/>
</svg>

After

Width:  |  Height:  |  Size: 1008 B

View File

@@ -1,6 +1,5 @@
import { observer } from "mobx-react-lite";
import { useHistory } from "react-router-dom";
import { Channel } from "revolt.js/dist/maps/Channels";
import { Channel } from "revolt.js";
import styled from "styled-components";
import { Text } from "preact-i18n";
@@ -47,7 +46,7 @@ type Props = {
channel: Channel;
};
export default observer((props: Props) => {
export default function AgeGate(props: Props) {
const history = useHistory();
const [consent, setConsent] = useState(
getState().sectionToggle["nsfw"] ?? false,
@@ -68,7 +67,6 @@ export default observer((props: Props) => {
return (
<Base>
<img
loading="eager"
src={"https://static.revolt.chat/emoji/mutant/26a0.svg"}
draggable={false}
/>
@@ -106,4 +104,4 @@ export default observer((props: Props) => {
</div>
</Base>
);
});
}

View File

@@ -1,10 +1,10 @@
import { Channel } from "revolt.js/dist/maps/Channels";
import { User } from "revolt.js/dist/maps/Users";
import { SYSTEM_USER_ID, User } from "revolt.js";
import { Channels } from "revolt.js/dist/api/objects";
import styled, { css } from "styled-components";
import { StateUpdater, useState } from "preact/hooks";
import { StateUpdater, useContext, useState } from "preact/hooks";
import { useClient } from "../../context/revoltjs/RevoltClient";
import { AppContext } from "../../context/revoltjs/RevoltClient";
import { emojiDictionary } from "../../assets/emojis";
import ChannelIcon from "./ChannelIcon";
@@ -24,7 +24,7 @@ export type AutoCompleteState =
}
| {
type: "channel";
matches: Channel[];
matches: Channels.TextChannel[];
}
));
@@ -52,7 +52,7 @@ export function useAutoComplete(
): AutoCompleteProps {
const [state, setState] = useState<AutoCompleteState>({ type: "none" });
const [focused, setFocused] = useState(false);
const client = useClient();
const client = useContext(AppContext);
function findSearchString(
el: HTMLTextAreaElement,
@@ -79,9 +79,7 @@ export function useAutoComplete(
if (current === ":" || current === "@" || current === "#") {
const search = content.slice(j + 1, content.length);
const minLen = current === ":" ? 2 : 1
if (search.length >= minLen) {
if (search.length > 0) {
return [
current === "#"
? "channel"
@@ -105,7 +103,7 @@ export function useAutoComplete(
const regex = new RegExp(search, "i");
if (type === "emoji") {
// ! TODO: we should convert it to a Binary Search Tree and use that
// ! FIXME: we should convert it to a Binary Search Tree and use that
const matches = Object.keys(emojiDictionary)
.filter((emoji: string) => emoji.match(regex))
.splice(0, 5);
@@ -129,7 +127,7 @@ export function useAutoComplete(
let users: User[] = [];
switch (searchClues.users.type) {
case "all":
users = [...client.users.values()];
users = client.users.toArray();
break;
case "channel": {
const channel = client.channels.get(
@@ -138,21 +136,25 @@ export function useAutoComplete(
switch (channel?.channel_type) {
case "Group":
case "DirectMessage":
users = channel.recipients!.filter(
users = client.users
.mapKeys(channel.recipients)
.filter(
(x) => typeof x !== "undefined",
) as User[];
break;
case "TextChannel":
{
const server = channel.server_id;
users = [...client.members.keys()]
.map((x) => JSON.parse(x))
.filter((x) => x.server === server)
.map((x) => client.users.get(x.user))
const server = channel.server;
users = client.servers.members
.toArray()
.filter(
(x) => x._id.substr(0, 26) === server,
)
.map((x) =>
client.users.get(x._id.substr(26)),
)
.filter(
(x) => typeof x !== "undefined",
) as User[];
}
break;
default:
return;
@@ -160,9 +162,7 @@ export function useAutoComplete(
}
}
users = users.filter(
(x) => x._id !== "00000000000000000000000000",
);
users = users.filter((x) => x._id !== SYSTEM_USER_ID);
const matches = (
search.length > 0
@@ -192,14 +192,15 @@ export function useAutoComplete(
if (type === "channel" && searchClues?.channels) {
const channels = client.servers
.get(searchClues.channels.server)
?.channels.filter(
?.channels.map((x) => client.channels.get(x))
.filter(
(x) => typeof x !== "undefined",
) as Channel[];
) as Channels.TextChannel[];
const matches = (
search.length > 0
? channels.filter((channel) =>
channel.name!.toLowerCase().match(regex),
channel.name.toLowerCase().match(regex),
)
: channels
)
@@ -267,7 +268,6 @@ export function useAutoComplete(
function onClick(ev: JSX.TargetedMouseEvent<HTMLButtonElement>) {
ev.preventDefault();
selectCurrent(document.querySelector("#message")!);
setFocused(false);
}
function onKeyDown(e: KeyboardEvent) {
@@ -309,7 +309,7 @@ export function useAutoComplete(
function onKeyUp(e: KeyboardEvent) {
if (e.currentTarget !== null) {
// @ts-expect-error Type mis-match.
// @ts-expect-error
onChange(e);
}
}
@@ -396,7 +396,6 @@ export default function AutoComplete({
{state.type === "emoji" &&
state.matches.map((match, i) => (
<button
key={match}
className={i === state.selected ? "active" : ""}
onMouseEnter={() =>
(i !== state.selected || !state.within) &&
@@ -428,7 +427,6 @@ export default function AutoComplete({
{state.type === "user" &&
state.matches.map((match, i) => (
<button
key={match}
className={i === state.selected ? "active" : ""}
onMouseEnter={() =>
(i !== state.selected || !state.within) &&
@@ -453,7 +451,6 @@ export default function AutoComplete({
{state.type === "channel" &&
state.matches.map((match, i) => (
<button
key={match}
className={i === state.selected ? "active" : ""}
onMouseEnter={() =>
(i !== state.selected || !state.within) &&

View File

@@ -1,6 +1,5 @@
import { Hash, VolumeFull } from "@styled-icons/boxicons-regular";
import { observer } from "mobx-react-lite";
import { Channel } from "revolt.js/dist/maps/Channels";
import { Channels } from "revolt.js/dist/api/objects";
import { useContext } from "preact/hooks";
@@ -9,18 +8,16 @@ import { AppContext } from "../../context/revoltjs/RevoltClient";
import { ImageIconBase, IconBaseProps } from "./IconBase";
import fallback from "./assets/group.png";
interface Props extends IconBaseProps<Channel> {
interface Props
extends IconBaseProps<
Channels.GroupChannel | Channels.TextChannel | Channels.VoiceChannel
> {
isServerChannel?: boolean;
}
export default observer(
(
props: Props &
Omit<
JSX.HTMLAttributes<HTMLImageElement>,
keyof Props | "children" | "as"
>,
) => {
export default function ChannelIcon(
props: Props & Omit<JSX.HTMLAttributes<HTMLImageElement>, keyof Props>,
) {
const client = useContext(AppContext);
const {
@@ -29,6 +26,8 @@ export default observer(
attachment,
isServerChannel: server,
animate,
children,
as,
...imgProps
} = props;
const iconURL = client.generateFileURL(
@@ -51,23 +50,16 @@ export default observer(
}
}
// The border radius of the channel icon, if it's a server-channel it should be square (undefined).
let borderRadius: string | undefined = "--border-radius-channel-icon";
if (isServerChannel) {
borderRadius = undefined;
}
return (
// ! TODO: replace fallback with <picture /> + <source />
// ! fixme: replace fallback with <picture /> + <source />
<ImageIconBase
{...imgProps}
width={size}
height={size}
loading="lazy"
aria-hidden="true"
borderRadius={borderRadius}
square={isServerChannel}
src={iconURL ?? fallback}
/>
);
},
);
}

View File

@@ -55,7 +55,6 @@ export default function Emoji({
return (
<img
alt={emoji}
loading="lazy"
className="emoji"
draggable={false}
src={parseEmoji(emoji)}
@@ -67,7 +66,7 @@ export default function Emoji({
}
export function generateEmoji(emoji: string) {
return `<img loading="lazy" class="emoji" draggable="false" alt="${emoji}" src="${parseEmoji(
return `<img class="emoji" draggable="false" alt="${emoji}" src="${parseEmoji(
emoji,
)}" />`;
}

View File

@@ -1,22 +1,16 @@
import { Attachment } from "revolt-api/types/Autumn";
import { Attachment } from "revolt.js/dist/api/objects";
import styled, { css } from "styled-components";
export interface IconBaseProps<T> {
target?: T;
url?: string;
attachment?: Attachment;
size: number;
hover?: boolean;
animate?: boolean;
}
interface IconModifiers {
/**
* If this is undefined or null then the icon defaults to square, else uses the CSS variable given.
*/
borderRadius?: string;
hover?: boolean;
square?: boolean;
}
export default styled.svg<IconModifiers>`
@@ -28,19 +22,11 @@ export default styled.svg<IconModifiers>`
object-fit: cover;
${(props) =>
props.borderRadius &&
!props.square &&
css`
border-radius: var(${props.borderRadius});
border-radius: 50%;
`}
}
${(props) =>
props.hover &&
css`
&:hover .icon {
filter: brightness(0.8);
}
`}
`;
export const ImageIconBase = styled.img<IconModifiers>`
@@ -48,16 +34,8 @@ export const ImageIconBase = styled.img<IconModifiers>`
object-fit: cover;
${(props) =>
props.borderRadius &&
!props.square &&
css`
border-radius: var(${props.borderRadius});
`}
${(props) =>
props.hover &&
css`
&:hover img {
filter: brightness(0.8);
}
border-radius: 50%;
`}
`;

View File

@@ -22,7 +22,7 @@ export function LocaleSelector(props: Props) {
{Object.keys(Languages).map((x) => {
const l = Languages[x as keyof typeof Languages];
return (
<option value={x} key={x}>
<option value={x}>
{l.emoji} {l.display}
</option>
);

View File

@@ -1,28 +1,30 @@
import { Check } from "@styled-icons/boxicons-regular";
import { Cog } from "@styled-icons/boxicons-solid";
import { observer } from "mobx-react-lite";
import { Link } from "react-router-dom";
import { Server } from "revolt.js/dist/api/objects";
import { ServerPermission } from "revolt.js/dist/api/permissions";
import { Server } from "revolt.js/dist/maps/Servers";
import styled from "styled-components";
import { Text } from "preact-i18n";
import { HookContext, useServerPermission } from "../../context/revoltjs/hooks";
import Header from "../ui/Header";
import IconButton from "../ui/IconButton";
import Tooltip from "./Tooltip";
interface Props {
server: Server;
ctx: HookContext;
}
const ServerName = styled.div`
flex-grow: 1;
`;
export default observer(({ server }: Props) => {
const bannerURL = server.generateBannerURL({ width: 480 });
export default function ServerHeader({ server, ctx }: Props) {
const permissions = useServerPermission(server._id, ctx);
const bannerURL = ctx.client.servers.getBannerURL(
server._id,
{ width: 480 },
true,
);
return (
<Header
@@ -32,48 +34,8 @@ export default observer(({ server }: Props) => {
style={{
background: bannerURL ? `url('${bannerURL}')` : undefined,
}}>
{server.flags && server.flags & 1 ? (
<Tooltip
content={<Text id="app.special.server-badges.official" />}
placement={"bottom-start"}>
<svg width="20" height="20">
<image
xlinkHref="/assets/badges/verified.svg"
height="20"
width="20"
/>
<image
xlinkHref="/assets/badges/revolt_r.svg"
height="15"
width="15"
x="2"
y="3"
style={
"justify-content: center; align-items: center; filter: brightness(0);"
}
/>
</svg>
</Tooltip>
) : undefined}
{server.flags && server.flags & 2 ? (
<Tooltip
content={<Text id="app.special.server-badges.verified" />}
placement={"bottom-start"}>
<svg width="20" height="20">
<image
xlinkHref="/assets/badges/verified.svg"
height="20"
width="20"
/>
<foreignObject x="2" y="2" width="15" height="15">
<Check size={15} color="black" strokeWidth={8} />
</foreignObject>
</svg>
</Tooltip>
) : undefined}
<ServerName>{server.name}</ServerName>
{(server.permission & ServerPermission.ManageServer) > 0 && (
{(permissions & ServerPermission.ManageServer) > 0 && (
<div className="actions">
<Link to={`/server/${server._id}/settings`}>
<IconButton>
@@ -84,4 +46,4 @@ export default observer(({ server }: Props) => {
)}
</Header>
);
});
}

View File

@@ -1,5 +1,4 @@
import { observer } from "mobx-react-lite";
import { Server } from "revolt.js/dist/maps/Servers";
import { Server } from "revolt.js/dist/api/objects";
import styled from "styled-components";
import { useContext } from "preact/hooks";
@@ -16,25 +15,28 @@ const ServerText = styled.div`
display: grid;
padding: 0.2em;
overflow: hidden;
border-radius: 50%;
place-items: center;
color: var(--foreground);
background: var(--primary-background);
border-radius: var(--border-radius-half);
`;
// const fallback = "/assets/group.png";
export default observer(
(
props: Props &
Omit<
JSX.HTMLAttributes<HTMLImageElement>,
keyof Props | "children" | "as"
>,
) => {
const fallback = "/assets/group.png";
export default function ServerIcon(
props: Props & Omit<JSX.HTMLAttributes<HTMLImageElement>, keyof Props>,
) {
const client = useContext(AppContext);
const { target, attachment, size, animate, server_name, ...imgProps } =
props;
const {
target,
attachment,
size,
animate,
server_name,
children,
as,
...imgProps
} = props;
const iconURL = client.generateFileURL(
target?.icon ?? attachment,
{ max_side: 256 },
@@ -59,11 +61,9 @@ export default observer(
{...imgProps}
width={size}
height={size}
borderRadius="--border-radius-server-icon"
src={iconURL}
loading="lazy"
aria-hidden="true"
/>
);
},
);
}

View File

@@ -16,8 +16,8 @@ export default function Tooltip(props: Props) {
return (
<Tippy content={content} {...tippyProps}>
{/*
// @ts-expect-error Type mis-match. */}
<div style={`display: flex;`}>{children}</div>
// @ts-expect-error */}
<div>{children}</div>
</Tippy>
);
}
@@ -28,14 +28,14 @@ const PermissionTooltipBase = styled.div`
flex-direction: column;
span {
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
color: var(--secondary-foreground);
font-size: 11px;
}
code {
font-family: var(--monospace-font);
font-family: var(--monoscape-font);
}
`;

View File

@@ -1,5 +1,4 @@
/* eslint-disable react-hooks/rules-of-hooks */
import { Download, CloudDownload } from "@styled-icons/boxicons-regular";
import { Download } from "@styled-icons/boxicons-regular";
import { useContext, useEffect, useState } from "preact/hooks";
@@ -10,16 +9,11 @@ import { ThemeContext } from "../../context/Theme";
import IconButton from "../ui/IconButton";
import { updateSW } from "../../main";
import Tooltip from "./Tooltip";
let pendingUpdate = false;
internalSubscribe("PWA", "update", () => (pendingUpdate = true));
interface Props {
style: "titlebar" | "channel";
}
export default function UpdateIndicator({ style }: Props) {
export default function UpdateIndicator() {
const [pending, setPending] = useState(pendingUpdate);
useEffect(() => {
@@ -29,22 +23,6 @@ export default function UpdateIndicator({ style }: Props) {
if (!pending) return null;
const theme = useContext(ThemeContext);
if (style === "titlebar") {
return (
<div class="actions">
<Tooltip
content="A new update is available!"
placement="bottom">
<div onClick={() => updateSW(true)}>
<CloudDownload size={22} color={theme.success} />
</div>
</Tooltip>
</div>
);
}
if (window.isNative) return null;
return (
<IconButton onClick={() => updateSW(true)}>
<Download size={22} color={theme.success} />

View File

@@ -1,16 +1,13 @@
import { observer } from "mobx-react-lite";
import { Message as MessageObject } from "revolt.js/dist/maps/Messages";
import { attachContextMenu } from "preact-context-menu";
import { memo } from "preact/compat";
import { useState } from "preact/hooks";
import { internalEmit } from "../../../lib/eventEmitter";
import { useContext } from "preact/hooks";
import { QueuedMessage } from "../../../redux/reducers/queue";
import { useIntermediate } from "../../../context/intermediate/Intermediate";
import { useClient } from "../../../context/revoltjs/RevoltClient";
import { AppContext } from "../../../context/revoltjs/RevoltClient";
import { useUser } from "../../../context/revoltjs/hooks";
import { MessageObject } from "../../../context/revoltjs/util";
import Overline from "../../ui/Overline";
@@ -26,7 +23,6 @@ import MessageBase, {
import Attachment from "./attachments/Attachment";
import { MessageReply } from "./attachments/MessageReply";
import Embed from "./embed/Embed";
import InviteList from "./embed/EmbedInvite";
interface Props {
attachContext?: boolean;
@@ -36,11 +32,9 @@ interface Props {
contrast?: boolean;
content?: Children;
head?: boolean;
hideReply?: boolean;
}
const Message = observer(
({
function Message({
highlight,
attachContext,
message,
@@ -48,95 +42,60 @@ const Message = observer(
content: replacement,
head: preferHead,
queued,
hideReply,
}: Props) => {
const client = useClient();
const user = message.author;
}: Props) {
// TODO: Can improve re-renders here by providing a list
// TODO: of dependencies. We only need to update on u/avatar.
const user = useUser(message.author);
const client = useContext(AppContext);
const { openScreen } = useIntermediate();
const content = message.content as string;
const head =
preferHead || (message.reply_ids && message.reply_ids.length > 0);
const head = preferHead || (message.replies && message.replies.length > 0);
// ! TODO: tell fatal to make this type generic
// ! FIXME: tell fatal to make this type generic
// bree: Fatal please...
const userContext = attachContext
? (attachContextMenu("Menu", {
user: message.author_id,
contextualChannel: message.channel_id,
// eslint-disable-next-line
user: message.author,
contextualChannel: message.channel,
}) as any)
: undefined;
const openProfile = () =>
openScreen({ id: "profile", user_id: message.author_id });
const handleUserClick = (e: MouseEvent) => {
if (e.shiftKey && user?._id) {
internalEmit(
"MessageBox",
"append",
`<@${user._id}>`,
"mention",
);
} else {
openProfile();
}
};
// ! FIXME(?): animate on hover
const [animate, setAnimate] = useState(false);
openScreen({ id: "profile", user_id: message.author });
return (
<div id={message._id}>
{!hideReply &&
message.reply_ids?.map((message_id, index) => (
{message.replies?.map((message_id, index) => (
<MessageReply
key={message_id}
index={index}
id={message_id}
channel={message.channel!}
parent_mentions={message.mention_ids ?? []}
channel={message.channel}
/>
))}
<MessageBase
highlight={highlight}
head={
hideReply
? false
: (head &&
!(
message.reply_ids &&
message.reply_ids.length > 0
)) ??
false
}
head={head && !(message.replies && message.replies.length > 0)}
contrast={contrast}
sending={typeof queued !== "undefined"}
mention={message.mention_ids?.includes(client.user!._id)}
mention={message.mentions?.includes(client.user!._id)}
failed={typeof queued?.error !== "undefined"}
onContextMenu={
attachContext
? attachContextMenu("Menu", {
message,
contextualChannel: message.channel_id,
contextualChannel: message.channel,
queued,
})
: undefined
}
onMouseEnter={() => setAnimate(true)}
onMouseLeave={() => setAnimate(false)}>
}>
<MessageInfo>
{head ? (
<UserIcon
url={message.generateMasqAvatarURL()}
target={user}
size={36}
onContextMenu={userContext}
onClick={handleUserClick}
animate={animate}
showServerIdentity
onClick={openProfile}
/>
) : (
<MessageDetail message={message} position="left" />
@@ -146,21 +105,15 @@ const Message = observer(
{head && (
<span className="detail">
<Username
user={user}
className="author"
showServerIdentity
onClick={handleUserClick}
user={user}
onContextMenu={userContext}
masquerade={message.masquerade!}
/>
<MessageDetail
message={message}
position="top"
onClick={openProfile}
/>
<MessageDetail message={message} position="top" />
</span>
)}
{replacement ?? <Markdown content={content} />}
{!queued && <InviteList message={message} />}
{queued?.error && (
<Overline type="error" error={queued.error} />
)}
@@ -178,7 +131,6 @@ const Message = observer(
</MessageBase>
</div>
);
},
);
}
export default memo(Message);

View File

@@ -1,14 +1,12 @@
import { observer } from "mobx-react-lite";
import { Message } from "revolt.js/dist/maps/Messages";
import styled, { css, keyframes } from "styled-components";
import { decodeTime } from "ulid";
import { Text } from "preact-i18n";
import { useDictionary } from "../../../lib/i18n";
import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice";
import { dayjs } from "../../../context/Locale";
import { MessageObject } from "../../../context/revoltjs/util";
import Tooltip from "../Tooltip";
@@ -33,13 +31,7 @@ export default styled.div<BaseMessageProps>`
overflow: none;
padding: 0.125rem;
flex-direction: row;
padding-inline-end: 16px;
${() =>
isTouchscreenDevice &&
css`
user-select: none;
`}
padding-right: 16px;
${(props) =>
props.contrast &&
@@ -97,20 +89,12 @@ export default styled.div<BaseMessageProps>`
gap: 8px;
display: flex;
align-items: center;
flex-shrink: 0;
}
.author {
overflow: hidden;
cursor: pointer;
font-weight: 600 !important;
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
text-overflow: ellipsis;
white-space: normal;
&:hover {
text-decoration: underline;
}
@@ -127,10 +111,6 @@ export default styled.div<BaseMessageProps>`
time {
opacity: 1;
}
.system-message-icon {
display: none;
}
}
`;
@@ -185,13 +165,6 @@ export const MessageInfo = styled.div`
.header {
cursor: pointer;
}
.systemIcon {
height: 1.33em;
width: 1.33em;
margin-right: 0.5em;
color: var(--tertiary-foreground);
}
`;
export const MessageContent = styled.div`
@@ -199,13 +172,12 @@ export const MessageContent = styled.div`
flex-grow: 1;
display: flex;
// overflow: hidden;
font-size: var(--text-size);
flex-direction: column;
justify-content: center;
font-size: var(--text-size);
`;
export const DetailBase = styled.div`
flex-shrink: 0;
gap: 4px;
font-size: 10px;
display: inline-flex;
@@ -220,8 +192,13 @@ export const DetailBase = styled.div`
}
`;
export const MessageDetail = observer(
({ message, position }: { message: Message; position: "left" | "top" }) => {
export function MessageDetail({
message,
position,
}: {
message: MessageObject;
position: "left" | "top";
}) {
const dict = useDictionary();
if (position === "left") {
@@ -231,13 +208,12 @@ export const MessageDetail = observer(
<time className="copyTime">
<i className="copyBracket">[</i>
{dayjs(decodeTime(message._id)).format(
dict.dayjs?.timeFormat,
dict.dayjs.timeFormat,
)}
<i className="copyBracket">]</i>
</time>
<span className="edited">
<Tooltip
content={dayjs(message.edited).format("LLLL")}>
<Tooltip content={dayjs(message.edited).format("LLLL")}>
<Text id="app.main.channel.edited" />
</Tooltip>
</span>
@@ -249,7 +225,7 @@ export const MessageDetail = observer(
<time>
<i className="copyBracket">[</i>
{dayjs(decodeTime(message._id)).format(
dict.dayjs?.timeFormat,
dict.dayjs.timeFormat,
)}
<i className="copyBracket">]</i>
</time>
@@ -269,5 +245,4 @@ export const MessageDetail = observer(
)}
</DetailBase>
);
},
);
}

View File

@@ -1,9 +1,9 @@
import { Send, ShieldX } from "@styled-icons/boxicons-solid";
import { Send, HappyAlt, ShieldX } from "@styled-icons/boxicons-solid";
import { Styleshare } from "@styled-icons/simple-icons";
import Axios, { CancelTokenSource } from "axios";
import { observer } from "mobx-react-lite";
import { Channel } from "revolt.js";
import { ChannelPermission } from "revolt.js/dist/api/permissions";
import { Channel } from "revolt.js/dist/maps/Channels";
import styled, { css } from "styled-components";
import styled from "styled-components";
import { ulid } from "ulid";
import { Text } from "preact-i18n";
@@ -16,7 +16,7 @@ import { internalEmit, internalSubscribe } from "../../../lib/eventEmitter";
import { useTranslation } from "../../../lib/i18n";
import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice";
import {
getRenderer,
SingletonMessageRenderer,
SMOOTH_SCROLL_ON_RECEIVE,
} from "../../../lib/renderer/Singleton";
@@ -31,10 +31,12 @@ import {
uploadFile,
} from "../../../context/revoltjs/FileUploads";
import { AppContext } from "../../../context/revoltjs/RevoltClient";
import { useChannelPermission } from "../../../context/revoltjs/hooks";
import { takeError } from "../../../context/revoltjs/util";
import IconButton from "../../ui/IconButton";
import { PluginSingleton } from "../../../plugins";
import AutoComplete, { useAutoComplete } from "../AutoComplete";
import { PermissionTooltip } from "../Tooltip";
import FilePreview from "./bars/FilePreview";
@@ -99,22 +101,17 @@ const Action = styled.div`
padding: 12px;
}
${() =>
!isTouchscreenDevice &&
css`
.mobile {
@media (pointer: fine) {
display: none;
}
`}
}
`;
// For sed replacement
const RE_SED = new RegExp("^s/([^])*/([^])*$");
// ! FIXME: add to app config and load from app config
export const CAN_UPLOAD_AT_ONCE = 4;
export default observer(({ channel }: Props) => {
export default function MessageBox({ channel }: Props) {
const [draft, setDraft] = useState(getState().drafts[channel._id] ?? "");
const [uploadState, setUploadState] = useState<UploadState>({
@@ -127,9 +124,8 @@ export default observer(({ channel }: Props) => {
const client = useContext(AppContext);
const translate = useTranslation();
const renderer = getRenderer(channel);
if (!(channel.permission & ChannelPermission.SendMessage)) {
const permissions = useChannelPermission(channel._id);
if (!(permissions & ChannelPermission.SendMessage)) {
return (
<Base>
<Blocked>
@@ -148,8 +144,7 @@ export default observer(({ channel }: Props) => {
);
}
const setMessage = useCallback(
(content?: string) => {
function setMessage(content?: string) {
setDraft(content ?? "");
if (content) {
@@ -164,9 +159,7 @@ export default observer(({ channel }: Props) => {
channel: channel._id,
});
}
},
[channel._id],
);
}
useEffect(() => {
function append(content: string, action: "quote" | "mention") {
@@ -185,70 +178,26 @@ export default observer(({ channel }: Props) => {
}
}
return internalSubscribe(
"MessageBox",
"append",
append as (...args: unknown[]) => void,
);
}, [draft, setMessage]);
return internalSubscribe("MessageBox", "append", append);
}, [draft]);
async function send() {
if (uploadState.type === "uploading" || uploadState.type === "sending")
return;
const content = draft?.trim() ?? "";
if (uploadState.type === "attached") return sendFile(content);
if (content.length === 0) return;
const target = { content };
if (PluginSingleton.emit("Message:Send", target)) return;
if (uploadState.type === "attached") return sendFile(target.content);
if (target.content.length === 0) return;
stopTyping();
setMessage();
setReplies([]);
const nonce = ulid();
// sed style message editing.
// If the user types for example `s/abc/def`, the string "abc"
// will be replaced with "def" in their last sent message.
if (RE_SED.test(content)) {
renderer.messages.reverse();
const msg = renderer.messages.find(
(msg) => msg.author_id === client.user!._id,
);
renderer.messages.reverse();
if (msg) {
// eslint-disable-next-line prefer-const
let [_, toReplace, newText, flags] = content.split(/\//);
if (toReplace == "*") toReplace = msg.content.toString();
const newContent =
toReplace == ""
? msg.content.toString() + newText
: msg.content
.toString()
.replace(new RegExp(toReplace, flags), newText);
if (newContent != msg.content) {
if (newContent.length == 0) {
msg.delete().catch(console.error);
} else {
msg.edit({
content: newContent.substr(0, 2000),
})
.then(() =>
defer(() =>
renderer.jumpToBottom(
SMOOTH_SCROLL_ON_RECEIVE,
),
),
)
.catch(console.error);
}
}
}
} else {
playSound("outbound");
const nonce = ulid();
dispatch({
type: "QUEUE_ADD",
nonce,
@@ -258,16 +207,21 @@ export default observer(({ channel }: Props) => {
channel: channel._id,
author: client.user!._id,
content,
content: target.content,
replies,
},
});
defer(() => renderer.jumpToBottom(SMOOTH_SCROLL_ON_RECEIVE));
defer(() =>
SingletonMessageRenderer.jumpToBottom(
channel._id,
SMOOTH_SCROLL_ON_RECEIVE,
),
);
try {
await channel.sendMessage({
content,
await client.channels.sendMessage(channel._id, {
content: target.content,
nonce,
replies,
});
@@ -279,7 +233,6 @@ export default observer(({ channel }: Props) => {
});
}
}
}
async function sendFile(content: string) {
if (uploadState.type !== "attached") return;
@@ -318,8 +271,7 @@ export default observer(({ channel }: Props) => {
);
}
} catch (err) {
// eslint-disable-next-line
if ((err as any)?.message === "cancel") {
if (err?.message === "cancel") {
setUploadState({
type: "attached",
files,
@@ -342,7 +294,7 @@ export default observer(({ channel }: Props) => {
const nonce = ulid();
try {
await channel.sendMessage({
await client.channels.sendMessage(channel._id, {
content,
nonce,
replies,
@@ -377,7 +329,7 @@ export default observer(({ channel }: Props) => {
const ws = client.websocket;
if (ws.connected) {
setTyping(+new Date() + 2500);
setTyping(+new Date() + 4000);
ws.send({
type: "BeginTyping",
channel: channel._id,
@@ -398,12 +350,9 @@ export default observer(({ channel }: Props) => {
}
}
// TODO: change to useDebounceCallback
// eslint-disable-next-line
const debouncedStopTyping = useCallback(
debounce(stopTyping as (...args: unknown[]) => void, 1000),
[channel._id],
);
const debouncedStopTyping = useCallback(debounce(stopTyping, 1000), [
channel._id,
]);
const {
onChange,
onKeyUp,
@@ -415,7 +364,7 @@ export default observer(({ channel }: Props) => {
users: { type: "channel", id: channel._id },
channels:
channel.channel_type === "TextChannel"
? { server: channel.server_id! }
? { server: channel.server }
: undefined,
});
@@ -453,12 +402,12 @@ export default observer(({ channel }: Props) => {
}}
/>
<ReplyBar
channel={channel}
channel={channel._id}
replies={replies}
setReplies={setReplies}
/>
<Base>
{channel.permission & ChannelPermission.UploadFiles ? (
{permissions & ChannelPermission.UploadFiles ? (
<Action>
<FileUploader
size={24}
@@ -501,16 +450,10 @@ export default observer(({ channel }: Props) => {
hideBorder
maxRows={20}
id="message"
maxLength={2000}
onKeyUp={onKeyUp}
value={draft ?? ""}
padding="var(--message-box-padding)"
onKeyDown={(e) => {
if (e.ctrlKey && e.key === "Enter") {
e.preventDefault();
return send();
}
if (onKeyDown(e)) return;
if (
@@ -524,7 +467,6 @@ export default observer(({ channel }: Props) => {
if (
!e.shiftKey &&
!e.isComposing &&
e.key === "Enter" &&
!isTouchscreenDevice
) {
@@ -532,34 +474,19 @@ export default observer(({ channel }: Props) => {
return send();
}
if (e.key === "Escape") {
if (replies.length > 0) {
setReplies(replies.slice(0, -1));
} else if (
uploadState.type === "attached" &&
uploadState.files.length > 0
) {
setUploadState({
type:
uploadState.files.length > 1
? "attached"
: "none",
files: uploadState.files.slice(0, -1),
});
}
}
debouncedStopTyping(true);
}}
placeholder={
channel.channel_type === "DirectMessage"
? translate("app.main.channel.message_who", {
person: channel.recipient?.username,
person: client.users.get(
client.channels.getRecipient(channel._id),
)?.username,
})
: channel.channel_type === "SavedMessages"
? translate("app.main.channel.message_saved")
: translate("app.main.channel.message_where", {
channel_name: channel.name ?? undefined,
channel_name: channel.name,
})
}
disabled={
@@ -588,4 +515,4 @@ export default observer(({ channel }: Props) => {
</Base>
</>
);
});
}

View File

@@ -1,24 +1,13 @@
import {
InfoCircle,
UserPlus,
UserMinus,
ArrowToRight,
ArrowToLeft,
UserX,
ShieldX,
EditAlt,
Edit,
MessageSquareEdit,
} from "@styled-icons/boxicons-solid";
import { observer } from "mobx-react-lite";
import { SystemMessage as SystemMessageI } from "revolt-api/types/Channels";
import { Message } from "revolt.js/dist/maps/Messages";
import { User } from "revolt.js";
import styled from "styled-components";
import { attachContextMenu } from "preact-context-menu";
import { TextReact } from "../../../lib/i18n";
import { useForceUpdate, useUser } from "../../../context/revoltjs/hooks";
import { MessageObject } from "../../../context/revoltjs/util";
import UserShort from "../user/UserShort";
import MessageBase, { MessageDetail, MessageInfo } from "./MessageBase";
@@ -31,31 +20,77 @@ const SystemContent = styled.div`
flex-direction: row;
`;
type SystemMessageParsed =
| { type: "text"; content: string }
| { type: "user_added"; user: User; by: User }
| { type: "user_remove"; user: User; by: User }
| { type: "user_joined"; user: User }
| { type: "user_left"; user: User }
| { type: "user_kicked"; user: User }
| { type: "user_banned"; user: User }
| { type: "channel_renamed"; name: string; by: User }
| { type: "channel_description_changed"; by: User }
| { type: "channel_icon_changed"; by: User };
interface Props {
attachContext?: boolean;
message: Message;
message: MessageObject;
highlight?: boolean;
hideInfo?: boolean;
}
const iconDictionary = {
user_added: UserPlus,
user_remove: UserMinus,
user_joined: ArrowToRight,
user_left: ArrowToLeft,
user_kicked: UserX,
user_banned: ShieldX,
channel_renamed: EditAlt,
channel_description_changed: Edit,
channel_icon_changed: MessageSquareEdit,
text: InfoCircle,
};
export function SystemMessage({
attachContext,
message,
highlight,
hideInfo,
}: Props) {
const ctx = useForceUpdate();
export const SystemMessage = observer(
({ attachContext, message, highlight, hideInfo }: Props) => {
const data = message.asSystemMessage;
const SystemMessageIcon =
iconDictionary[data.type as SystemMessageI["type"]] ?? InfoCircle;
let data: SystemMessageParsed;
const content = message.content;
if (typeof content === "object") {
switch (content.type) {
case "text":
data = content;
break;
case "user_added":
case "user_remove":
data = {
type: content.type,
user: useUser(content.id, ctx) as User,
by: useUser(content.by, ctx) as User,
};
break;
case "user_joined":
case "user_left":
case "user_kicked":
case "user_banned":
data = {
type: content.type,
user: useUser(content.id, ctx) as User,
};
break;
case "channel_renamed":
data = {
type: "channel_renamed",
name: content.name,
by: useUser(content.by, ctx) as User,
};
break;
case "channel_description_changed":
case "channel_icon_changed":
data = {
type: content.type,
by: useUser(content.by, ctx) as User,
};
break;
default:
data = { type: "text", content: JSON.stringify(content) };
}
} else {
data = { type: "text", content };
}
let children;
switch (data.type) {
@@ -67,9 +102,7 @@ export const SystemMessage = observer(
children = (
<TextReact
id={`app.main.channel.system.${
data.type === "user_added"
? "added_by"
: "removed_by"
data.type === "user_added" ? "added_by" : "removed_by"
}`}
fields={{
user: <UserShort user={data.user} />,
@@ -129,11 +162,9 @@ export const SystemMessage = observer(
{!hideInfo && (
<MessageInfo>
<MessageDetail message={message} position="left" />
<SystemMessageIcon className="systemIcon" />
</MessageInfo>
)}
<SystemContent>{children}</SystemContent>
</MessageBase>
);
},
);
}

View File

@@ -3,7 +3,7 @@
grid-auto-flow: row dense;
grid-auto-columns: min(100%, var(--attachment-max-width));
margin: 0.125rem 0 0.125rem;
margin: .125rem 0 .125rem;
width: max-content;
max-width: 100%;
@@ -56,7 +56,7 @@
}
pre code {
font-family: var(--monospace-font), sans-serif;
font-family: var(--monoscape-font), sans-serif;
}
&[data-loading="true"] {
@@ -84,18 +84,6 @@
}
}
.container,
.attachment,
.image {
.container, .attachment, .image {
border-radius: var(--border-radius);
}
.image {
cursor: pointer;
width: 100%;
height: 100%;
&.loading {
background: var(--background);
}
}

View File

@@ -1,20 +1,19 @@
import { Attachment as AttachmentI } from "revolt-api/types/Autumn";
import { Attachment as AttachmentRJS } from "revolt.js/dist/api/objects";
import styles from "./Attachment.module.scss";
import classNames from "classnames";
import { attachContextMenu } from "preact-context-menu";
import { useContext, useState } from "preact/hooks";
import { useIntermediate } from "../../../../context/intermediate/Intermediate";
import { AppContext } from "../../../../context/revoltjs/RevoltClient";
import AttachmentActions from "./AttachmentActions";
import { SizedGrid } from "./Grid";
import ImageFile from "./ImageFile";
import Spoiler from "./Spoiler";
import TextFile from "./TextFile";
interface Props {
attachment: AttachmentI;
attachment: AttachmentRJS;
hasContent: boolean;
}
@@ -22,6 +21,7 @@ const MAX_ATTACHMENT_WIDTH = 480;
export default function Attachment({ attachment, hasContent }: Props) {
const client = useContext(AppContext);
const { openScreen } = useIntermediate();
const { filename, metadata } = attachment;
const [spoiler, setSpoiler] = useState(filename.startsWith("SPOILER_"));
@@ -37,17 +37,21 @@ export default function Attachment({ attachment, hasContent }: Props) {
<SizedGrid
width={metadata.width}
height={metadata.height}
onContextMenu={attachContextMenu("Menu", {
attachment,
})}
className={classNames({
[styles.margin]: hasContent,
spoiler,
})}>
<ImageFile
attachment={attachment}
width={metadata.width}
height={metadata.height}
<img
src={url}
alt={filename}
className={styles.image}
loading="lazy"
onClick={() =>
openScreen({ id: "image_viewer", attachment })
}
onMouseDown={(ev) =>
ev.button === 1 && window.open(url, "_blank")
}
/>
{spoiler && <Spoiler set={setSpoiler} />}
</SizedGrid>

View File

@@ -5,7 +5,7 @@ import {
Headphone,
Video,
} from "@styled-icons/boxicons-regular";
import { Attachment } from "revolt-api/types/Autumn";
import { Attachment } from "revolt.js/dist/api/objects";
import styles from "./AttachmentActions.module.scss";
import classNames from "classnames";

View File

@@ -2,54 +2,26 @@ import styled from "styled-components";
import { Children } from "../../../../types/Preact";
const Grid = styled.div<{ width: number; height: number }>`
--width: ${props => props.width}px;
--height: ${props => props.height}px;
const Grid = styled.div`
display: grid;
aspect-ratio: ${(props) => props.width} / ${(props) => props.height};
overflow: hidden;
max-width: min(var(--width), var(--attachment-max-width));
max-height: min(var(--height), var(--attachment-max-height));
// This is a hack for browsers not supporting aspect-ratio.
// Stolen from https://codepen.io/una/pen/BazyaOM.
@supports not (
aspect-ratio: ${(props) => props.width} / ${(props) => props.height}
) {
div::before {
float: left;
padding-top: ${(props) => (props.height / props.width) * 100}%;
content: "";
}
div::after {
display: block;
content: "";
clear: both;
}
}
max-width: min(var(--attachment-max-width), 100%, var(--width));
max-height: min(var(--attachment-max-height), var(--height));
aspect-ratio: var(--aspect-ratio);
img,
video {
grid-area: 1 / 1;
min-width: 100%;
min-height: 100%;
display: block;
width: auto;
height: auto;
max-width: 100%;
max-height: 100%;
overflow: hidden;
object-fit: contain;
// It's something
object-position: left;
}
video {
width: 100%;
height: 100%;
grid-area: 1 / 1;
}
&.spoiler {
@@ -68,16 +40,24 @@ type Props = Omit<
JSX.HTMLAttributes<HTMLDivElement>,
"children" | "as" | "style"
> & {
style?: JSX.CSSProperties;
children?: Children;
width: number;
height: number;
};
export function SizedGrid(props: Props) {
const { width, height, children, ...divProps } = props;
const { width, height, children, style, ...divProps } = props;
return (
<Grid {...divProps} width={width} height={height}>
<Grid
{...divProps}
style={{
...style,
"--width": `${width}px`,
"--height": `${height}px`,
"--aspect-ratio": width / height,
}}>
{children}
</Grid>
);

View File

@@ -1,47 +0,0 @@
import { Attachment } from "revolt-api/types/Autumn";
import styles from "./Attachment.module.scss";
import classNames from "classnames";
import { useContext, useState } from "preact/hooks";
import { useIntermediate } from "../../../../context/intermediate/Intermediate";
import { AppContext } from "../../../../context/revoltjs/RevoltClient";
enum ImageLoadingState {
Loading,
Loaded,
Error
}
type Props = JSX.HTMLAttributes<HTMLImageElement> & {
attachment: Attachment;
}
export default function ImageFile({ attachment, ...props }: Props) {
const [loading, setLoading] = useState(ImageLoadingState.Loading);
const client = useContext(AppContext);
const { openScreen } = useIntermediate();
const url = client.generateFileURL(attachment)!;
return <img
{...props}
src={url}
alt={attachment.filename}
loading="lazy"
className={classNames(styles.image, {
[styles.loading]: loading !== ImageLoadingState.Loaded
})}
onClick={() =>
openScreen({ id: "image_viewer", attachment })
}
onMouseDown={(ev) =>
ev.button === 1 && window.open(url, "_blank")
}
onLoad={() =>
setLoading(ImageLoadingState.Loaded)
}
onError={() =>
setLoading(ImageLoadingState.Error)
}
/>
}

View File

@@ -1,24 +1,24 @@
import { Reply } from "@styled-icons/boxicons-regular";
import { File } from "@styled-icons/boxicons-solid";
import { observer } from "mobx-react-lite";
import { useHistory } from "react-router-dom";
import { RelationshipStatus } from "revolt-api/types/Users";
import { Channel } from "revolt.js/dist/maps/Channels";
import { Message } from "revolt.js/dist/maps/Messages";
import { SYSTEM_USER_ID } from "revolt.js";
import { Users } from "revolt.js/dist/api/objects";
import styled, { css } from "styled-components";
import { Text } from "preact-i18n";
import { useLayoutEffect, useState } from "preact/hooks";
import { getRenderer } from "../../../../lib/renderer/Singleton";
import { useRenderState } from "../../../../lib/renderer/Singleton";
import { useForceUpdate, useUser } from "../../../../context/revoltjs/hooks";
import { mapMessage, MessageObject } from "../../../../context/revoltjs/util";
import Markdown from "../../../markdown/Markdown";
import UserShort from "../../user/UserShort";
import { SystemMessage } from "../SystemMessage";
interface Props {
parent_mentions: string[];
channel: Channel;
channel: string;
index: number;
id: string;
}
@@ -29,30 +29,15 @@ export const ReplyBase = styled.div<{
preview?: boolean;
}>`
gap: 4px;
min-width: 0;
display: flex;
margin-inline-start: 30px;
margin-inline-end: 12px;
margin-bottom: 4px;
font-size: 0.8em;
user-select: none;
align-items: center;
color: var(--secondary-foreground);
/* nizune's Discord replies,
does not scale properly with messages,
reverted temporarily
&::before {
content: "";
height: 10px;
width: 28px;
margin-inline-end: 2px;
align-self: flex-end;
display: flex;
border-top: 2.2px solid var(--tertiary-foreground);
border-inline-start: 2.2px solid var(--tertiary-foreground);
border-start-start-radius: 6px;
}*/
* {
overflow: hidden;
white-space: nowrap;
@@ -60,13 +45,11 @@ export const ReplyBase = styled.div<{
}
.user {
gap: 6px;
display: flex;
gap: 4px;
flex-shrink: 0;
font-weight: 600;
overflow: visible;
align-items: center;
padding: 2px 0;
span {
cursor: pointer;
@@ -84,26 +67,15 @@ export const ReplyBase = styled.div<{
}
.content {
max-height: 32px;
gap: 4px;
display: flex;
padding: 2px 0;
cursor: pointer;
overflow: hidden;
align-items: center;
flex-direction: row;
transition: filter 1s ease-in-out;
transition: transform ease-in-out 0.1s;
filter: brightness(1);
> span > p {
display: flex;
align-items: center;
gap: 4px;
}
&:hover {
filter: brightness(2);
}
@@ -116,9 +88,9 @@ export const ReplyBase = styled.div<{
pointer-events: none;
}
/*> span > p {
> span {
display: flex;
}*/
}
}
> svg:first-child {
@@ -146,21 +118,26 @@ export const ReplyBase = styled.div<{
`}
`;
export const MessageReply = observer(
({ index, channel, id, parent_mentions }: Props) => {
const view = getRenderer(channel);
if (view.state !== "RENDER") return null;
const [message, setMessage] = useState<Message | undefined>(undefined);
export function MessageReply({ index, channel, id }: Props) {
const ctx = useForceUpdate();
const view = useRenderState(channel);
if (view?.type !== "RENDER") return null;
const [message, setMessage] = useState<MessageObject | undefined>(
undefined,
);
useLayoutEffect(() => {
const message = channel.client.messages.get(id);
if (message) {
setMessage(message);
// ! FIXME: We should do this through the message renderer, so it can fetch it from cache if applicable.
const m = view.messages.find((x) => x._id === id);
if (m) {
setMessage(m);
} else {
channel.fetchMessage(id).then(setMessage);
ctx.client.channels
.fetchMessage(channel, id)
.then((m) => setMessage(mapMessage(m)));
}
}, [id, channel, view.messages]);
}, [view.messages]);
if (!message) {
return (
@@ -173,59 +150,43 @@ export const MessageReply = observer(
);
}
const user = useUser(message.author, ctx);
const history = useHistory();
return (
<ReplyBase head={index === 0}>
<Reply size={16} />
{message.author?.relationship === RelationshipStatus.Blocked ? (
{user?.relationship === Users.Relationship.Blocked ? (
<>
<Text id="app.main.channel.misc.blocked_user" />
</>
) : (
<>
{message.author_id === "00000000000000000000000000" ? (
{message.author === SYSTEM_USER_ID ? (
<SystemMessage message={message} hideInfo />
) : (
<>
<div className="user">
<UserShort
size={16}
showServerIdentity
user={message.author}
masquerade={message.masquerade!}
prefixAt={parent_mentions.includes(
message.author_id,
)}
/>
<UserShort user={user} size={16} />
</div>
<div
className="content"
onClick={() => {
const channel = message.channel!;
if (
channel.channel_type ===
"TextChannel"
) {
const obj =
ctx.client.channels.get(channel);
if (obj?.channel_type === "TextChannel") {
history.push(
`/server/${channel.server_id}/channel/${channel._id}/${message._id}`,
`/server/${obj.server}/channel/${obj._id}/${message._id}`,
);
} else {
history.push(
`/channel/${channel._id}/${message._id}`,
`/channel/${channel}/${message._id}`,
);
}
}}>
{message.attachments && (
<>
{message.attachments &&
message.attachments.length > 0 && (
<File size={16} />
<em>
{message.attachments.length >
1 ? (
<Text id="app.main.channel.misc.sent_multiple_files" />
) : (
<Text id="app.main.channel.misc.sent_file" />
)}
</em>
</>
)}
<Markdown
disallowBigEmoji
@@ -240,5 +201,4 @@ export const MessageReply = observer(
)}
</ReplyBase>
);
},
);
}

View File

@@ -1,5 +1,5 @@
import axios from "axios";
import { Attachment } from "revolt-api/types/Autumn";
import { Attachment } from "revolt.js/dist/api/objects";
import styles from "./Attachment.module.scss";
import { useContext, useEffect, useState } from "preact/hooks";
@@ -30,9 +30,9 @@ export default function TextFile({ attachment }: Props) {
if (typeof content !== "undefined") return;
if (loading) return;
if (attachment.size > 100_000) {
if (attachment.size > 20_000) {
setContent(
"This file is > 100 KB, for your sake I did not load it.\nSee tracking issue here for previews: https://github.com/revoltchat/revite/issues/35",
"This file is > 20 KB, for your sake I did not load it.\nSee tracking issue here for previews: https://gitlab.insrt.uk/revolt/revite/-/issues/2",
);
return;
}
@@ -60,7 +60,7 @@ export default function TextFile({ attachment }: Props) {
setLoading(false);
});
}
}, [content, loading, status, attachment._id, attachment.size, url]);
}, [content, loading, status]);
return (
<div

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