1 Commits

Author SHA1 Message Date
trashtemp
bba9689e30 fix(settings): small bug fixes 2021-12-24 14:26:07 +01:00
298 changed files with 11323 additions and 20805 deletions

View File

@@ -1 +0,0 @@
{ "plugins": [["@babel/plugin-proposal-decorators", { "legacy": true }]] }

View File

@@ -5,6 +5,3 @@ dist_injected
node_modules node_modules
.env .env
.env.local .env.local
Dockerfile
.dockerignore

2
.github/FUNDING.yml vendored Normal file
View File

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

View File

@@ -1,66 +0,0 @@
name: Bug report
description: File a bug report
title: "bug: "
labels: ["bug"]
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this bug report!
- type: textarea
id: what-happened
attributes:
label: What happened?
description: What did you expect to happen?
validations:
required: true
- type: dropdown
id: branch
attributes:
label: Branch
description: What branch of Revolt are you using?
options:
- Production (app.revolt.chat)
- Nightly (nightly.revolt.chat)
validations:
required: true
- type: textarea
id: commit-hash
attributes:
label: Commit hash
description: What is your commit hash? You can find this at the bottom of Settings, next to the branch name.
validations:
required: true
- type: dropdown
id: browsers
attributes:
label: What browsers are you seeing the problem on?
multiple: true
options:
- Firefox
- Chrome
- Safari
- Microsoft Edge
- Other (please specify in the "What happened" form)
- type: textarea
id: logs
attributes:
label: Relevant log output
description: Please copy and paste any relevant log output. (To get this, press `CTRL`- or `CMD`-`SHIFT`-`I` and navigate to the "Console" tab.)
render: shell
- type: checkboxes
id: desktop
attributes:
label: Desktop
description: Is this bug specific to [the desktop client](https://github.com/revoltchat/desktop)? (If not, leave this unchecked.)
options:
- label: Yes, this bug is specific to Revolt Desktop and is *not* an issue with Revolt Desktop itself.
required: false
- type: checkboxes
id: pwa
attributes:
label: PWA
description: Is this bug specific to the PWA (i.e. "installing" the web app on iOS or Android)? (If not, leave this unchecked.)
options:
- label: Yes, this bug is specific to the PWA.
required: false

View File

@@ -1,7 +0,0 @@
contact_links:
- name: Lounge Chat
url: https://rvlt.gg/Testers
about: Ask questions and discuss with others.
- name: Discussions
url: https://github.com/orgs/revoltchat/discussions
about: For larger feature requests and general question & answer.

View File

@@ -1,24 +0,0 @@
name: Feature request
description: Make a feature request
title: "feature request: "
labels: ["enhancement"]
body:
- type: markdown
attributes:
value: |
Before you start, a lot of bigger features may be better suited as [API issues](https://github.com/revoltchat/delta/issues/new) or [centralised discussions](https://github.com/revoltchat/revolt/discussions/new).
- type: textarea
id: your-idea
attributes:
label: What do you want to see?
description: Describe your idea in as much detail as possible - if applicable, screenshots/mockups are really useful.
validations:
required: true
- type: checkboxes
id: pwa
attributes:
label: PWA
description: Is this feature request specific to the PWA (i.e. "installing" the web app on iOS or Android)? (If not, leave this unchecked.)
options:
- label: Yes, this feature request is specific to the PWA.
required: false

24
.github/SECURITY.md vendored Normal file
View File

@@ -0,0 +1,24 @@
# 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

@@ -19,7 +19,7 @@ runs:
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v2 uses: actions/setup-node@v2
with: with:
node-version: 16 node-version: 15
cache: "yarn" cache: "yarn"
- name: Install Dependencies and Build - name: Install Dependencies and Build

View File

@@ -1,7 +0,0 @@
## Please make sure to check the following tasks before opening and submitting a PR
* [ ] I understand and have followed the [contribution guide](https://github.com/revoltchat/revolt/discussions/282)
* [ ] I have tested my changes locally and they are working as intended
* [ ] These changes do not have any notable side effects on other Revolt projects
* [ ] (optional) I have opened a pull request on [the translation repository](https://github.com/revoltchat/translations)
* [ ] I have included screenshots to demonstrate my changes

View File

@@ -100,7 +100,7 @@ jobs:
with: with:
context: . context: .
push: true push: true
platforms: linux/amd64 platforms: linux/amd64,linux/arm64,linux/arm/v7
tags: ${{ steps.meta.outputs.tags }} tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }} labels: ${{ steps.meta.outputs.labels }}
cache-from: type=local,src=/tmp/.buildx-cache/linux/amd64 cache-from: type=local,src=/tmp/.buildx-cache/linux/amd64

View File

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

5
.gitignore vendored
View File

@@ -7,11 +7,6 @@ dist-ssr
*.log *.log
/.idea /.idea
.yarn/cache
.yarn/install-state.gz
public/assets public/assets
public/assets_* public/assets_*
!public/assets_default !public/assets_default
.vscode/chrome_data

View File

@@ -1,4 +1,4 @@
image: node:16-buster image: node:14-buster
variables: variables:
GIT_SUBMODULE_STRATEGY: recursive GIT_SUBMODULE_STRATEGY: recursive

View File

@@ -4,12 +4,10 @@ module.exports = {
jsxBracketSameLine: true, jsxBracketSameLine: true,
importOrder: [ importOrder: [
"preact|classnames|.scss$", "preact|classnames|.scss$",
"^@revoltchat",
"/(lib)", "/(lib)",
"/(redux|mobx)", "/(redux|mobx)",
"/(context)", "/(context)",
"/(ui|common)$", "/(ui|common)|.svg|.webp|.png|.jpg$",
".svg|.webp|.png|.jpg$",
"^[./]", "^[./]",
], ],
importOrderSeparation: true, importOrderSeparation: true,

17
.vscode/launch.json vendored
View File

@@ -1,17 +0,0 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "pwa-chrome",
"request": "launch",
"name": "Launch Chrome against localhost",
"url": "http://local.revolt.chat:3000",
"webRoot": "${workspaceFolder}",
"runtimeExecutable": "/usr/bin/chromium",
"userDataDir": "${workspaceFolder}/.vscode/chrome_data"
}
]
}

View File

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

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

File diff suppressed because one or more lines are too long

View File

@@ -1,11 +0,0 @@
nodeLinker: node-modules
plugins:
- path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
spec: "@yarnpkg/plugin-interactive-tools"
- path: .yarn/plugins/@yarnpkg/plugin-typescript.cjs
spec: "@yarnpkg/plugin-typescript"
- path: .yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs
spec: "@yarnpkg/plugin-workspace-tools"
yarnPath: .yarn/releases/yarn-3.2.0.cjs

View File

@@ -1,15 +1,18 @@
FROM node:16-buster AS builder FROM node:16-buster AS builder
WORKDIR /usr/src/app WORKDIR /usr/src/app
COPY package*.json ./
RUN yarn --no-cache
COPY . . COPY . .
COPY .env.build .env COPY .env.build .env
RUN yarn add --dev @babel/plugin-proposal-decorators
RUN yarn install --frozen-lockfile
RUN yarn typecheck RUN yarn typecheck
RUN yarn build:highmem RUN yarn build
RUN yarn workspaces focus --production --all RUN npm prune --production
FROM node:16-alpine FROM node:16-buster
WORKDIR /usr/src/app WORKDIR /usr/src/app
COPY --from=builder /usr/src/app . COPY --from=builder /usr/src/app .

View File

@@ -38,8 +38,6 @@ yarn
yarn dev yarn dev
``` ```
You can now access the client at http://local.revolt.chat:3000.
## CLI Commands ## CLI Commands
| Command | Description | | Command | Description |

View File

@@ -1,6 +0,0 @@
<svg width="225" height="161" viewBox="0 0 225 161" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M69.6951 15.0002L20.9951 63.7002L69.6951 112.4L81.7601 100.325L45.1301 63.6952L81.7601 27.0652L69.6951 15.0002ZM132.955 112.39L181.655 63.6902L132.955 14.9902L120.89 27.0652L157.52 63.6952L120.89 100.325L132.955 112.39Z" fill="white"/>
<path d="M197 73H137C133.686 73 131 75.6863 131 79V139C131 142.314 133.686 145 137 145H197C200.314 145 203 142.314 203 139V79C203 75.6863 200.314 73 197 73Z" fill="#242424"/>
<path d="M191 79H143C139.686 79 137 81.6863 137 85V133C137 136.314 139.686 139 143 139H191C194.314 139 197 136.314 197 133V85C197 81.6863 194.314 79 191 79Z" fill="#D14F4F"/>
<path d="M181.5 95.5L153.5 123.5M153.5 95.5L181.5 123.5L153.5 95.5Z" stroke="white" stroke-width="5" stroke-linecap="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 828 B

2
external/lang vendored

View File

@@ -1,13 +1,10 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en" background="#191919"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<!--App Title-->
<title>Revolt</title> <title>Revolt</title>
<meta name="apple-mobile-web-app-title" content="Revolt" /> <meta name="apple-mobile-web-app-title" content="Revolt" />
<!--App Scaling-->
<meta <meta
name="viewport" name="viewport"
content="width=device-width, initial-scale=1.0, user-scalable=no" content="width=device-width, initial-scale=1.0, user-scalable=no"
@@ -72,72 +69,14 @@
media="(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2)" media="(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2)"
rel="apple-touch-startup-image" rel="apple-touch-startup-image"
/> />
<!--CSS for noscript screen-->
<style>
noscript {
background: #242424;
color: white;
position: fixed;
top: 0;
left: 0;
width: 100vw;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
user-select: none;
}
noscript > div {
padding: 12px;
display: flex;
font-family: "Open Sans", sans-serif;
flex-direction: column;
justify-content: center;
text-align: center;
}
noscript > div > h1 {
margin: 8px 0;
text-transform: uppercase;
font-size: 20px;
font-weight: 700;
}
noscript > div > p {
margin: 4px 0;
font-size: 14px;
}
noscript > div > a {
align-self: center;
margin-top: 20px;
padding: 8px 10px;
font-size: 14px;
width: 80px;
font-weight: 600;
background: #ed5151;
border-radius: 4px;
text-decoration: none;
color: white;
transition: background-color 0.2s;
}
noscript > div > a:hover {
background-color: #cf4848;
}
noscript > div > a:active {
background-color: #b64141;
}
</style>
</head> </head>
<body> <body onContextMenu="return false" ontouchstart="">
<div id="app"></div> <div id="app"></div>
<script type="module" src="/src/main.tsx"></script> <script type="module" src="/src/main.tsx"></script>
<noscript>
<div>
<img src="disabled-js.svg" />
<h1>Well, this is really awkward...</h1>
<p>Seems like your JavaScript is disabled.</p>
<p>You'll need to enable JavaScript to run this app.</p>
<a href="https://app.revolt.chat" target="_blank">Reload</a>
</div>
</noscript>
</body> </body>
<style>
html {
background-color: #191919;
}
</style>
</html> </html>

View File

@@ -4,9 +4,8 @@
"dev": "node scripts/setup_assets.js --check && vite", "dev": "node scripts/setup_assets.js --check && vite",
"pull": "node scripts/setup_assets.js", "pull": "node scripts/setup_assets.js",
"build": "rimraf build && node scripts/setup_assets.js --check && vite build", "build": "rimraf build && node scripts/setup_assets.js --check && vite build",
"build:highmem": "NODE_OPTIONS='--max-old-space-size=4096' yarn build",
"preview": "vite preview", "preview": "vite preview",
"lint": "eslint src/**/*.{js,jsx,ts,tsx}", "lint": "eslint 'src/**/*.{js,jsx,ts,tsx}'",
"fmt": "prettier --write 'src/**/*.{js,jsx,ts,tsx}'", "fmt": "prettier --write 'src/**/*.{js,jsx,ts,tsx}'",
"typecheck": "tsc --noEmit", "typecheck": "tsc --noEmit",
"start": "sirv dist --cors --single --host", "start": "sirv dist --cors --single --host",
@@ -38,47 +37,27 @@
{ {
"varsIgnorePattern": "^_" "varsIgnorePattern": "^_"
} }
],
"require-jsdoc": [
"error",
{
"require": {
"FunctionDeclaration": true,
"MethodDefinition": true,
"ClassDeclaration": true,
"ArrowFunctionExpression": false,
"FunctionExpression": false
},
"ignore": {
"MethodDefinition": [
"toJSON",
"hydrate"
]
}
}
] ]
} }
}, },
"dependencies": { "dependencies": {
"fs-extra": "^10.0.0", "fs-extra": "^10.0.0",
"klaw": "^3.0.0", "klaw": "^3.0.0",
"react-beautiful-dnd": "^13.1.0",
"sirv-cli": "^1.0.14", "sirv-cli": "^1.0.14",
"vite": "^2.6.14" "vite": "^2.6.14"
}, },
"devDependencies": { "devDependencies": {
"@babel/plugin-proposal-decorators": "^7.17.9",
"@fontsource/atkinson-hyperlegible": "^4.4.5", "@fontsource/atkinson-hyperlegible": "^4.4.5",
"@fontsource/bitter": "^4.5.7", "@fontsource/bree-serif": "^4.4.5",
"@fontsource/comic-neue": "^4.4.5", "@fontsource/comic-neue": "^4.4.5",
"@fontsource/fira-code": "^4.4.5", "@fontsource/fira-code": "^4.4.5",
"@fontsource/inter": "^4.4.5", "@fontsource/inter": "^4.4.5",
"@fontsource/jetbrains-mono": "^4.4.5", "@fontsource/jetbrains-mono": "^4.4.5",
"@fontsource/lato": "^4.4.5", "@fontsource/lato": "^4.4.5",
"@fontsource/lexend": "^4.5.2",
"@fontsource/montserrat": "^4.4.5", "@fontsource/montserrat": "^4.4.5",
"@fontsource/noto-sans": "^4.4.5", "@fontsource/noto-sans": "^4.4.5",
"@fontsource/open-sans": "^4.5.2", "@fontsource/open-sans": "^4.4.5",
"@fontsource/opendyslexic": "^4.5.2",
"@fontsource/poppins": "^4.4.5", "@fontsource/poppins": "^4.4.5",
"@fontsource/raleway": "^4.4.5", "@fontsource/raleway": "^4.4.5",
"@fontsource/roboto": "^4.4.5", "@fontsource/roboto": "^4.4.5",
@@ -88,15 +67,13 @@
"@fontsource/ubuntu": "^4.4.5", "@fontsource/ubuntu": "^4.4.5",
"@fontsource/ubuntu-mono": "^4.4.5", "@fontsource/ubuntu-mono": "^4.4.5",
"@hcaptcha/react-hcaptcha": "^0.3.6", "@hcaptcha/react-hcaptcha": "^0.3.6",
"@insertish/vite-plugin-babel-macros": "^1.0.5",
"@preact/preset-vite": "^2.0.0", "@preact/preset-vite": "^2.0.0",
"@revoltchat/ui": "1.0.33",
"@rollup/plugin-replace": "^2.4.2", "@rollup/plugin-replace": "^2.4.2",
"@styled-icons/boxicons-logos": "^10.38.0", "@styled-icons/boxicons-logos": "^10.34.0",
"@styled-icons/boxicons-regular": "^10.38.0", "@styled-icons/boxicons-regular": "^10.34.0",
"@styled-icons/boxicons-solid": "^10.38.0", "@styled-icons/boxicons-solid": "^10.37.0",
"@styled-icons/simple-icons": "^10.33.0", "@styled-icons/simple-icons": "^10.33.0",
"@tippyjs/react": "4.2.6", "@tippyjs/react": "^4.2.5",
"@traptitech/markdown-it-katex": "^3.4.3", "@traptitech/markdown-it-katex": "^3.4.3",
"@traptitech/markdown-it-spoiler": "^1.1.6", "@traptitech/markdown-it-spoiler": "^1.1.6",
"@trivago/prettier-plugin-sort-imports": "^2.0.2", "@trivago/prettier-plugin-sort-imports": "^2.0.2",
@@ -106,61 +83,63 @@
"@types/node": "^15.12.4", "@types/node": "^15.12.4",
"@types/preact-i18n": "^2.3.0", "@types/preact-i18n": "^2.3.0",
"@types/prismjs": "^1.16.5", "@types/prismjs": "^1.16.5",
"@types/react-beautiful-dnd": "^13", "@types/react-beautiful-dnd": "^13.1.2",
"@types/react-helmet": "^6.1.1", "@types/react-helmet": "^6.1.1",
"@types/react-router-dom": "^5.1.7", "@types/react-router-dom": "^5.1.7",
"@types/react-scroll": "^1.8.2", "@types/react-scroll": "^1.8.2",
"@types/react-virtualized-auto-sizer": "^1.0.1",
"@types/styled-components": "^5.1.10", "@types/styled-components": "^5.1.10",
"@types/twemoji": "^12.1.1", "@types/twemoji": "^12.1.1",
"@typescript-eslint/eslint-plugin": "^4.27.0", "@typescript-eslint/eslint-plugin": "^4.27.0",
"@typescript-eslint/parser": "^4.27.0", "@typescript-eslint/parser": "^4.27.0",
"@vitejs/plugin-legacy": "^1.7.1",
"classnames": "^2.3.1", "classnames": "^2.3.1",
"color-rgba": "^2.4.0",
"dayjs": "^1.10.6", "dayjs": "^1.10.6",
"detect-browser": "^5.2.0", "detect-browser": "^5.2.0",
"eslint": "^7.28.0", "eslint": "^7.28.0",
"eslint-config-preact": "^1.1.4", "eslint-config-preact": "^1.1.4",
"eventemitter3": "^4.0.7", "eventemitter3": "^4.0.7",
"json-stringify-deterministic": "^1.0.2", "highlight.js": "^11.0.1",
"localforage": "^1.9.0", "localforage": "^1.9.0",
"lodash.defaultsdeep": "^4.6.1", "lodash.defaultsdeep": "^4.6.1",
"lodash.isequal": "^4.5.0", "lodash.isequal": "^4.5.0",
"long": "^5.2.0",
"markdown-it": "^12.0.6", "markdown-it": "^12.0.6",
"markdown-it-emoji": "^2.0.0", "markdown-it-emoji": "^2.0.0",
"markdown-it-sub": "^1.0.0",
"markdown-it-sup": "^1.0.0",
"mediasoup-client": "npm:@insertish/mediasoup-client@3.6.36-esnext", "mediasoup-client": "npm:@insertish/mediasoup-client@3.6.36-esnext",
"mobx": "^6.6.0", "mobx": "^6.3.2",
"mobx-react-lite": "3.4.0", "mobx-react-lite": "^3.2.0",
"preact": "^10.5.14", "preact": "^10.5.14",
"preact-context-menu": "0.4.0-patch.0", "preact-context-menu": "^0.2.1",
"preact-i18n": "^2.4.0-preactx", "preact-i18n": "^2.4.0-preactx",
"prettier": "^2.3.1", "prettier": "^2.3.1",
"prismjs": "^1.23.0", "prismjs": "^1.23.0",
"react-beautiful-dnd": "^13.1.0", "react-device-detect": "^1.17.0",
"react-device-detect": "2.2.2",
"react-helmet": "^6.1.0", "react-helmet": "^6.1.0",
"react-hook-form": "6.3.0", "react-hook-form": "6.3.0",
"react-overlapping-panels": "1.2.2", "react-overlapping-panels": "1.2.2",
"react-redux": "^7.2.4",
"react-router-dom": "^5.2.0", "react-router-dom": "^5.2.0",
"react-scroll": "^1.8.2", "react-scroll": "^1.8.2",
"react-virtuoso": "^2.12.0", "react-virtualized-auto-sizer": "^1.0.5",
"revolt.js": "6.0.1", "react-virtuoso": "^1.10.4",
"redux": "^4.1.0",
"revolt-api": "0.5.3-alpha.10",
"revolt.js": "^5.1.0-alpha.10",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"sass": "^1.35.1", "sass": "^1.35.1",
"shade-blend-color": "^1.0.0", "shade-blend-color": "^1.0.0",
"stacktrace-js": "^2.0.2",
"styled-components": "^5.3.0", "styled-components": "^5.3.0",
"typescript": "^4.4.2", "typescript": "^4.4.2",
"ulid": "^2.3.0", "ulid": "^2.3.0",
"use-resize-observer": "^7.0.0", "use-resize-observer": "^7.0.0",
"vite-plugin-pwa": "^0.11.13", "vite-plugin-compression": "^0.3.6",
"vite-plugin-pwa": "^0.8.1",
"workbox-precaching": "^6.1.5" "workbox-precaching": "^6.1.5"
}, },
"name": "client", "name": "client",
"main": "index.js", "main": "index.js",
"repository": "https://github.com/revoltchat/revite.git", "repository": "https://github.com/revoltchat/revite.git",
"author": "Paul <paulmakles@gmail.com>", "author": "Paul <paulmakles@gmail.com>",
"license": "MIT", "license": "MIT"
"packageManager": "yarn@3.2.0"
} }

View File

@@ -1,22 +1,10 @@
[ [{
{ "relation": ["delegate_permission/common.handle_all_urls"],
"relation": ["delegate_permission/common.handle_all_urls"], "target": {
"target": { "namespace": "android_app",
"namespace": "android_app", "package_name": "chat.revolt.app.twa",
"package_name": "chat.revolt.app.twa", "sha256_cert_fingerprints": [
"sha256_cert_fingerprints": [ "6E:62:C1:BF:5A:2D:11:31:A3:22:91:8D:22:2B:2C:49:D3:70:F3:A1:45:DF:11:6A:97:DC:4C:A9:3B:C3:AA:FB"
"6E:62:C1:BF:5A:2D:11:31:A3:22:91:8D:22:2B:2C:49:D3:70:F3:A1:45:DF:11:6A:97:DC:4C:A9:3B:C3:AA:FB" ]
]
}
},
{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "chat.revolt.app.twa",
"sha256_cert_fingerprints": [
"2B:C7:89:87:BD:62:88:38:7B:C0:D7:5F:D1:10:F4:91:D5:24:A6:B3:25:3A:75:C2:3A:91:07:1B:63:C0:98:67"
]
}
} }
] }]

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

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,22 +0,0 @@
const { readdirSync } = require("fs");
console.log(
"var locale_keys = " +
JSON.stringify([
...readdirSync("node_modules/dayjs/locale")
.filter((x) => x.endsWith(".js"))
.map((x) => {
v = x.split(".");
v.pop();
return v.join(".");
}),
...readdirSync("external/lang")
.filter((x) => x.endsWith(".json"))
.map((x) => {
v = x.split(".");
v.pop();
return v.join(".");
}),
]) +
";",
);

View File

@@ -1,30 +1,7 @@
#!/bin/bash #!/bin/bash
# Build and publish release to production server version=$(cat VERSION)
# Remote Server
REMOTE=revolt-de-nrb-1
# Remote Directory
REMOTE_DIR=/root/revite
# Post-install script
POST_INSTALL="pm2 restart revite"
# Assets
export REVOLT_SAAS=https://github.com/revoltchat/assets
# 1. Build Revite
yarn
yarn build
# 2. Archive built files
tar -czvf build.tar.gz dist
# 3. Upload built files
scp build.tar.gz $REMOTE:$REMOTE_DIR/build.tar.gz
rm build.tar.gz
# 4. Apply changes
ssh $REMOTE "cd $REMOTE_DIR; tar -xvzf build.tar.gz; rm build.tar.gz; $POST_INSTALL"
docker build -t revoltchat/client:${version} . &&
docker tag revoltchat/client:${version} revoltchat/client:latest &&
docker push revoltchat/client:${version} &&
docker push revoltchat/client:latest

View File

@@ -3,8 +3,8 @@ const { copy, remove, access } = require("fs-extra");
const { exec: cexec } = require("child_process"); const { exec: cexec } = require("child_process");
const { resolve } = require("path"); const { resolve } = require("path");
let target = process.env.REVOLT_SAAS; let target = process.env.REVOLT_SASS;
let branch = process.env.REVOLT_SAAS_BRANCH; let branch = process.env.REVOLT_SASS_BRANCH;
let DEFAULT_DIRECTORY = "public/assets_default"; let DEFAULT_DIRECTORY = "public/assets_default";
let OUT_DIRECTORY = "public/assets"; let OUT_DIRECTORY = "public/assets";

View File

@@ -673,7 +673,6 @@ export const emojiDictionary = {
mandarin: "🍊", mandarin: "🍊",
lemon: "🍋", lemon: "🍋",
banana: "🍌", banana: "🍌",
nanner: "🍌",
pineapple: "🍍", pineapple: "🍍",
mango: "🥭", mango: "🥭",
apple: "🍎", apple: "🍎",
@@ -877,7 +876,6 @@ export const emojiDictionary = {
train: "🚋", train: "🚋",
bus: "🚌", bus: "🚌",
oncoming_bus: "🚍", oncoming_bus: "🚍",
trolley: "🚎",
trolleybus: "🚎", trolleybus: "🚎",
minibus: "🚐", minibus: "🚐",
ambulance: "🚑", ambulance: "🚑",
@@ -1849,108 +1847,4 @@ export const emojiDictionary = {
england: "🏴󠁧󠁢󠁥󠁮󠁧󠁿", england: "🏴󠁧󠁢󠁥󠁮󠁧󠁿",
scotland: "🏴󠁧󠁢󠁳󠁣󠁴󠁿", scotland: "🏴󠁧󠁢󠁳󠁣󠁴󠁿",
wales: "🏴󠁧󠁢󠁷󠁬󠁳󠁿", wales: "🏴󠁧󠁢󠁷󠁬󠁳󠁿",
...{
"1984": "custom:1984.gif",
KekW: "custom:KekW.png",
amogus: "custom:amogus.gif",
awaa: "custom:awaa.png",
boohoo: "custom:boohoo.png",
boohoo_goes_hard: "custom:boohoo_goes_hard.png",
boohoo_shaken: "custom:boohoo_shaken.png",
cat_arrival: "custom:cat_arrival.gif",
cat_awson: "custom:cat_awson.png",
cat_blob: "custom:cat_blob.png",
cat_bonk: "custom:cat_bonk.png",
cat_concern: "custom:cat_concern.png",
cat_fast: "custom:cat_fast.gif",
cat_kitty: "custom:cat_kitty.png",
cat_lick: "custom:cat_lick.gif",
cat_not_like: "custom:cat_not_like.png",
cat_put: "custom:cat_put.gif",
cat_pwease: "custom:cat_pwease.png",
cat_rage: "custom:cat_rage.png",
cat_sad: "custom:cat_sad.png",
cat_snuff: "custom:cat_snuff.gif",
cat_spin: "custom:cat_spin.gif",
cat_squish: "custom:cat_squish.gif",
cat_stare: "custom:cat_stare.gif",
cat_steal: "custom:cat_steal.gif",
cat_sussy: "custom:cat_sussy.gif",
clueless: "custom:clueless.png",
death: "custom:death.gif",
developers: "custom:developers.gif",
fastwawa: "custom:fastwawa.gif",
ferris: "custom:ferris.png",
ferris_bongo: "custom:ferris_bongo.gif",
ferris_nom: "custom:ferris_nom.png",
ferris_pensive: "custom:ferris_pensive.png",
ferris_unsafe: "custom:ferris_unsafe.png",
flesh: "custom:flesh.png",
flooshed: "custom:flooshed.png",
flosh: "custom:flosh.png",
flushee: "custom:flushee.png",
forgor: "custom:forgor.png",
hollow: "custom:hollow.png",
john: "custom:john.png",
lightspeed: "custom:lightspeed.png",
little_guy: "custom:little_guy.png",
lmaoooo: "custom:lmaoooo.gif",
lol: "custom:lol.png",
looking: "custom:looking.gif",
marie: "custom:marie.png",
marie_furret: "custom:marie_furret.gif",
marie_smug: "custom:marie_smug.png",
megumin: "custom:megumin.png",
michi_above: "custom:michi_above.png",
michi_awww: "custom:michi_awww.gif",
michi_drag: "custom:michi_drag.gif",
michi_flustered: "custom:michi_flustered.png",
michi_glare: "custom:michi_glare.png",
michi_sus: "custom:michi_sus.png",
monkaS: "custom:monkaS.png",
monkaStare: "custom:monkaStare.png",
monkey_grr: "custom:monkey_grr.png",
monkey_pensive: "custom:monkey_pensive.png",
monkey_zany: "custom:monkey_zany.png",
nazu_sit: "custom:nazu_sit.png",
nazu_sus: "custom:nazu_sus.png",
ok_and: "custom:ok_and.gif",
owo: "custom:owo.png",
pat: "custom:pat.png",
pointThink: "custom:pointThink.png",
rainbowHype: "custom:rainbowHype.gif",
rawr: "custom:rawr.png",
rember: "custom:rember.png",
revolt: "custom:revolt.png",
sickly: "custom:sickly.png",
stare: "custom:stare.png",
tfyoulookingat: "custom:tfyoulookingat.png",
thanks: "custom:thanks.png",
thonk: "custom:thonk.png",
trol: "custom:trol.png",
troll_smile: "custom:troll_smile.gif",
uber: "custom:uber.png",
ubertroll: "custom:ubertroll.png",
verycool: "custom:verycool.png",
verygood: "custom:verygood.png",
wawafast: "custom:wawafast.gif",
wawastance: "custom:wawastance.png",
yeahokayyy: "custom:yeahokayyy.png",
yed: "custom:yed.png",
yems: "custom:yems.png",
michael: "custom:michael.gif",
charle: "custom:charle.gif",
sadge: "custom:sadge.webp",
sus: "custom:sus.webp",
chade: "custom:chade.gif",
gigachad: "custom:gigachad.webp",
sippy: "custom:sippy.webp",
ayame_heart: "custom:ayame_heart.png",
catgirl_peek: "custom:catgirl_peek.png",
girl_happy: "custom:girl_happy.png",
hug_plushie: "custom:hug_plushie.png",
huggies: "custom:huggies.png",
noted: "custom:noted.gif",
waving: "custom:waving.png",
},
}; };

View File

@@ -0,0 +1,29 @@
import call_join from "./call_join.mp3";
import call_leave from "./call_leave.mp3";
import message from "./message.mp3";
import outbound from "./outbound.mp3";
const SoundMap: { [key in Sounds]: string } = {
message,
outbound,
call_join,
call_leave,
};
export type Sounds = "message" | "outbound" | "call_join" | "call_leave";
export const SOUNDS_ARRAY: Sounds[] = [
"message",
"outbound",
"call_join",
"call_leave",
];
export function playSound(sound: Sounds) {
const file = SoundMap[sound];
const el = new Audio(file);
try {
el.play();
} catch (err) {
console.error("Failed to play audio file", file, err);
}
}

View File

@@ -1,16 +1,14 @@
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { useHistory } from "react-router-dom"; import { useHistory } from "react-router-dom";
import { Channel } from "revolt.js"; import { Channel } from "revolt.js/dist/maps/Channels";
import styled from "styled-components/macro"; import styled from "styled-components";
import { Text } from "preact-i18n"; import { Text } from "preact-i18n";
import { useState } from "preact/hooks"; import { useState } from "preact/hooks";
import { Button } from "@revoltchat/ui"; import { dispatch, getState } from "../../redux";
import { useApplicationState } from "../../mobx/State";
import { SECTION_NSFW } from "../../mobx/stores/Layout";
import Button from "../ui/Button";
import Checkbox from "../ui/Checkbox"; import Checkbox from "../ui/Checkbox";
import { Children } from "../../types/Preact"; import { Children } from "../../types/Preact";
@@ -51,7 +49,9 @@ type Props = {
export default observer((props: Props) => { export default observer((props: Props) => {
const history = useHistory(); const history = useHistory();
const layout = useApplicationState().layout; const [consent, setConsent] = useState(
getState().sectionToggle["nsfw"] ?? false,
);
const [ageGate, setAgeGate] = useState(false); const [ageGate, setAgeGate] = useState(false);
if (ageGate || !props.gated) { if (ageGate || !props.gated) {
@@ -81,19 +81,26 @@ export default observer((props: Props) => {
</span> </span>
<Checkbox <Checkbox
checked={layout.getSectionState(SECTION_NSFW, false)} checked={consent}
onChange={() => layout.toggleSectionState(SECTION_NSFW, false)}> onChange={(v) => {
setConsent(v);
if (v) {
dispatch({
type: "SECTION_TOGGLE_SET",
id: "nsfw",
state: true,
});
} else {
dispatch({ type: "SECTION_TOGGLE_UNSET", id: "nsfw" });
}
}}>
<Text id="app.main.channel.nsfw.confirm" /> <Text id="app.main.channel.nsfw.confirm" />
</Checkbox> </Checkbox>
<div className="actions"> <div className="actions">
<Button palette="secondary" onClick={() => history.goBack()}> <Button contrast onClick={() => history.goBack()}>
<Text id="app.special.modals.actions.back" /> <Text id="app.special.modals.actions.back" />
</Button> </Button>
<Button <Button contrast onClick={() => consent && setAgeGate(true)}>
palette="secondary"
onClick={() =>
layout.getSectionState(SECTION_NSFW) && setAgeGate(true)
}>
<Text id={`app.main.channel.nsfw.${props.type}.confirm`} /> <Text id={`app.main.channel.nsfw.${props.type}.confirm`} />
</Button> </Button>
</div> </div>

View File

@@ -1,5 +1,6 @@
import { Channel, User } from "revolt.js"; import { Channel } from "revolt.js/dist/maps/Channels";
import styled, { css } from "styled-components/macro"; import { User } from "revolt.js/dist/maps/Users";
import styled, { css } from "styled-components";
import { StateUpdater, useState } from "preact/hooks"; import { StateUpdater, useState } from "preact/hooks";
@@ -13,19 +14,19 @@ import UserIcon from "./user/UserIcon";
export type AutoCompleteState = export type AutoCompleteState =
| { type: "none" } | { type: "none" }
| ({ selected: number; within: boolean } & ( | ({ selected: number; within: boolean } & (
| { | {
type: "emoji"; type: "emoji";
matches: string[]; matches: string[];
} }
| { | {
type: "user"; type: "user";
matches: User[]; matches: User[];
} }
| { | {
type: "channel"; type: "channel";
matches: Channel[]; matches: Channel[];
} }
)); ));
export type SearchClues = { export type SearchClues = {
users?: { type: "channel"; id: string } | { type: "all" }; users?: { type: "channel"; id: string } | { type: "all" };
@@ -78,15 +79,15 @@ export function useAutoComplete(
if (current === ":" || current === "@" || current === "#") { if (current === ":" || current === "@" || current === "#") {
const search = content.slice(j + 1, content.length); const search = content.slice(j + 1, content.length);
const minLen = current === ":" ? 2 : 1; const minLen = current === ":" ? 2 : 1
if (search.length >= minLen) { if (search.length >= minLen) {
return [ return [
current === "#" current === "#"
? "channel" ? "channel"
: current === ":" : current === ":"
? "emoji" ? "emoji"
: "user", : "user",
search.toLowerCase(), search.toLowerCase(),
j + 1, j + 1,
]; ];
@@ -166,8 +167,8 @@ export function useAutoComplete(
const matches = ( const matches = (
search.length > 0 search.length > 0
? users.filter((user) => ? users.filter((user) =>
user.username.toLowerCase().match(regex), user.username.toLowerCase().match(regex),
) )
: users : users
) )
.splice(0, 5) .splice(0, 5)
@@ -198,8 +199,8 @@ export function useAutoComplete(
const matches = ( const matches = (
search.length > 0 search.length > 0
? channels.filter((channel) => ? channels.filter((channel) =>
channel.name!.toLowerCase().match(regex), channel.name!.toLowerCase().match(regex),
) )
: channels : channels
) )
.splice(0, 5) .splice(0, 5)
@@ -416,7 +417,7 @@ export default function AutoComplete({
<Emoji <Emoji
emoji={ emoji={
(emojiDictionary as Record<string, string>)[ (emojiDictionary as Record<string, string>)[
match match
] ]
} }
size={20} size={20}

View File

@@ -1,14 +1,13 @@
import { Hash, VolumeFull } from "@styled-icons/boxicons-regular"; import { Hash, VolumeFull } from "@styled-icons/boxicons-regular";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { Channel } from "revolt.js"; import { Channel } from "revolt.js/dist/maps/Channels";
import { useContext } from "preact/hooks"; import { useContext } from "preact/hooks";
import { AppContext } from "../../context/revoltjs/RevoltClient"; import { AppContext } from "../../context/revoltjs/RevoltClient";
import fallback from "./assets/group.png";
import { ImageIconBase, IconBaseProps } from "./IconBase"; import { ImageIconBase, IconBaseProps } from "./IconBase";
import fallback from "./assets/group.png";
interface Props extends IconBaseProps<Channel> { interface Props extends IconBaseProps<Channel> {
isServerChannel?: boolean; isServerChannel?: boolean;
@@ -33,7 +32,7 @@ export default observer(
...imgProps ...imgProps
} = props; } = props;
const iconURL = client.generateFileURL( const iconURL = client.generateFileURL(
target?.icon ?? attachment ?? undefined, target?.icon ?? attachment,
{ max_side: 256 }, { max_side: 256 },
animate, animate,
); );

View File

@@ -1,6 +1,7 @@
import { ChevronDown } from "@styled-icons/boxicons-regular"; import { ChevronDown } from "@styled-icons/boxicons-regular";
import { useApplicationState } from "../../mobx/State"; import { State, store } from "../../redux";
import { Action } from "../../redux/reducers";
import Details from "../ui/Details"; import Details from "../ui/Details";
@@ -24,14 +25,27 @@ export default function CollapsibleSection({
children, children,
...detailsProps ...detailsProps
}: Props) { }: Props) {
const layout = useApplicationState().layout; const state: State = store.getState();
function setState(state: boolean) {
if (state === defaultValue) {
store.dispatch({
type: "SECTION_TOGGLE_UNSET",
id,
} as Action);
} else {
store.dispatch({
type: "SECTION_TOGGLE_SET",
id,
state,
} as Action);
}
}
return ( return (
<Details <Details
open={layout.getSectionState(id, defaultValue)} open={state.sectionToggle[id] ?? defaultValue}
onToggle={(e) => onToggle={(e) => setState(e.currentTarget.open)}
layout.setSectionState(id, e.currentTarget.open, defaultValue)
}
{...detailsProps}> {...detailsProps}>
<summary> <summary>
<div class="padding"> <div class="padding">

View File

@@ -1,9 +1,9 @@
export type EmojiPack = "mutant" | "twemoji" | "noto" | "openmoji"; import { EmojiPacks } from "../../redux/reducers/settings";
let EMOJI_PACK: EmojiPack = "mutant"; let EMOJI_PACK = "mutant";
const REVISION = 3; const REVISION = 3;
export function setGlobalEmojiPack(pack: EmojiPack) { export function setEmojiPack(pack: EmojiPacks) {
EMOJI_PACK = pack; EMOJI_PACK = pack;
} }
@@ -41,12 +41,6 @@ function toCodePoint(rune: string) {
} }
function parseEmoji(emoji: string) { function parseEmoji(emoji: string) {
if (emoji.startsWith("custom:")) {
return `https://dl.insrt.uk/projects/revolt/emotes/${emoji.substring(
7,
)}`;
}
const codepoint = toCodePoint(emoji); const codepoint = toCodePoint(emoji);
return `https://static.revolt.chat/emoji/${EMOJI_PACK}/${codepoint}.svg?rev=${REVISION}`; return `https://static.revolt.chat/emoji/${EMOJI_PACK}/${codepoint}.svg?rev=${REVISION}`;
} }

View File

@@ -1,19 +1,14 @@
import { API } from "revolt.js"; import { Attachment } from "revolt-api/types/Autumn";
import { Nullable } from "revolt.js"; import styled, { css } from "styled-components";
import styled, { css } from "styled-components/macro";
import { Ref } from "preact";
export interface IconBaseProps<T> { export interface IconBaseProps<T> {
target?: T; target?: T;
url?: string; url?: string;
attachment?: Nullable<API.File>; attachment?: Attachment;
size: number; size: number;
hover?: boolean; hover?: boolean;
animate?: boolean; animate?: boolean;
innerRef?: Ref<any>;
} }
interface IconModifiers { interface IconModifiers {
@@ -26,7 +21,7 @@ interface IconModifiers {
export default styled.svg<IconModifiers>` export default styled.svg<IconModifiers>`
flex-shrink: 0; flex-shrink: 0;
cursor: pointer;
img { img {
width: 100%; width: 100%;
height: 100%; height: 100%;

View File

@@ -1,21 +1,23 @@
import { useApplicationState } from "../../mobx/State"; import { dispatch } from "../../redux";
import { connectState } from "../../redux/connector";
import { Language, Languages } from "../../context/Locale";
import ComboBox from "../ui/ComboBox"; import ComboBox from "../ui/ComboBox";
import { Language, Languages } from "../../../external/lang/Languages"; type Props = {
locale: string;
/** };
* Component providing a language selector combobox.
* Note: this is not an observer but this is fine as we are just using a combobox.
*/
export default function LocaleSelector() {
const locale = useApplicationState().locale;
export function LocaleSelector(props: Props) {
return ( return (
<ComboBox <ComboBox
value={locale.getLanguage()} value={props.locale}
onChange={(e) => onChange={(e) =>
locale.setLanguage(e.currentTarget.value as Language) dispatch({
type: "SET_LOCALE",
locale: e.currentTarget.value as Language,
})
}> }>
{Object.keys(Languages).map((x) => { {Object.keys(Languages).map((x) => {
const l = Languages[x as keyof typeof Languages]; const l = Languages[x as keyof typeof Languages];
@@ -28,3 +30,9 @@ export default function LocaleSelector() {
</ComboBox> </ComboBox>
); );
} }
export default connectState(LocaleSelector, (state) => {
return {
locale: state.locale,
};
});

View File

@@ -2,134 +2,86 @@ import { Check } from "@styled-icons/boxicons-regular";
import { Cog } from "@styled-icons/boxicons-solid"; import { Cog } from "@styled-icons/boxicons-solid";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { Server } from "revolt.js"; import { ServerPermission } from "revolt.js/dist/api/permissions";
import styled, { css } from "styled-components/macro"; import { Server } from "revolt.js/dist/maps/Servers";
import styled from "styled-components";
import { Text } from "preact-i18n"; import { Text } from "preact-i18n";
import Header from "../ui/Header";
import IconButton from "../ui/IconButton"; import IconButton from "../ui/IconButton";
import Tooltip from "./Tooltip"; import Tooltip from "./Tooltip";
interface Props { interface Props {
server: Server; server: Server;
background?: boolean;
} }
const ServerBanner = styled.div<Omit<Props, "server">>` const ServerName = styled.div`
flex-shrink: 0; flex-grow: 1;
display: flex;
flex-direction: column;
justify-content: flex-end;
background-size: cover;
background-repeat: norepeat;
background-position: center center;
${(props) =>
props.background
? css`
height: 120px;
.container {
background: linear-gradient(
0deg,
var(--secondary-background),
transparent
);
}
`
: css`
background-color: var(--secondary-header);
`}
.container {
height: var(--header-height);
display: flex;
align-items: center;
padding: 0 14px;
font-weight: 600;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
gap: 8px;
.title {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex-grow: 1;
}
}
`; `;
export default observer(({ server }: Props) => { export default observer(({ server }: Props) => {
const bannerURL = server.generateBannerURL({ width: 480 }); const bannerURL = server.generateBannerURL({ width: 480 });
return ( return (
<ServerBanner <Header
borders
placement="secondary"
background={typeof bannerURL !== "undefined"} background={typeof bannerURL !== "undefined"}
style={{ style={{
backgroundImage: bannerURL ? `url('${bannerURL}')` : undefined, background: bannerURL ? `url('${bannerURL}')` : undefined,
}}> }}>
<div className="container"> {server.flags && server.flags & 1 ? (
{server.flags && server.flags & 1 ? ( <Tooltip
<Tooltip content={<Text id="app.special.server-badges.official" />}
content={ placement={"bottom-start"}>
<Text id="app.special.server-badges.official" /> <svg width="20" height="20">
} <image
placement={"bottom-start"}> xlinkHref="/assets/badges/verified.svg"
<svg width="20" height="20"> height="20"
<image width="20"
xlinkHref="/assets/badges/verified.svg" />
height="20" <image
width="20" xlinkHref="/assets/badges/revolt_r.svg"
/> height="15"
<image width="15"
xlinkHref="/assets/badges/revolt_r.svg" x="2"
height="15" y="3"
width="15" style={
x="2" "justify-content: center; align-items: center; filter: brightness(0);"
y="3" }
style={ />
"justify-content: center; align-items: center; filter: brightness(0);" </svg>
} </Tooltip>
/> ) : undefined}
</svg> {server.flags && server.flags & 2 ? (
</Tooltip> <Tooltip
) : undefined} content={<Text id="app.special.server-badges.verified" />}
{server.flags && server.flags & 2 ? ( placement={"bottom-start"}>
<Tooltip <svg width="20" height="20">
content={ <image
<Text id="app.special.server-badges.verified" /> xlinkHref="/assets/badges/verified.svg"
} height="20"
placement={"bottom-start"}> width="20"
<svg width="20" height="20"> />
<image <foreignObject x="2" y="2" width="15" height="15">
xlinkHref="/assets/badges/verified.svg" <Check size={15} color="black" strokeWidth={8} />
height="20" </foreignObject>
width="20" </svg>
/> </Tooltip>
<foreignObject x="2" y="2" width="15" height="15"> ) : undefined}
<Check
size={15} <ServerName>{server.name}</ServerName>
color="black" {(server.permission & ServerPermission.ManageServer) > 0 && (
strokeWidth={8} <div className="actions">
/>
</foreignObject>
</svg>
</Tooltip>
) : undefined}
<div className="title">{server.name}</div>
{server.havePermission("ManageServer") && (
<Link to={`/server/${server._id}/settings`}> <Link to={`/server/${server._id}/settings`}>
<IconButton> <IconButton>
<Cog size={20} /> <Cog size={24} />
</IconButton> </IconButton>
</Link> </Link>
)} </div>
</div> )}
</ServerBanner> </Header>
); );
}); });

View File

@@ -1,6 +1,6 @@
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { Server } from "revolt.js"; import { Server } from "revolt.js/dist/maps/Servers";
import styled from "styled-components/macro"; import styled from "styled-components";
import { useContext } from "preact/hooks"; import { useContext } from "preact/hooks";
@@ -13,13 +13,10 @@ interface Props extends IconBaseProps<Server> {
} }
const ServerText = styled.div` const ServerText = styled.div`
display: flex; display: grid;
align-items: center;
justify-content: center;
padding: 0.2em; padding: 0.2em;
font-size: 0.75rem;
font-weight: 600;
overflow: hidden; overflow: hidden;
place-items: center;
color: var(--foreground); color: var(--foreground);
background: var(--primary-background); background: var(--primary-background);
border-radius: var(--border-radius-half); border-radius: var(--border-radius-half);
@@ -39,7 +36,7 @@ export default observer(
const { target, attachment, size, animate, server_name, ...imgProps } = const { target, attachment, size, animate, server_name, ...imgProps } =
props; props;
const iconURL = client.generateFileURL( const iconURL = client.generateFileURL(
target?.icon ?? attachment ?? undefined, target?.icon ?? attachment,
{ max_side: 256 }, { max_side: 256 },
animate, animate,
); );
@@ -52,9 +49,7 @@ export default observer(
{name {name
.split(" ") .split(" ")
.map((x) => x[0]) .map((x) => x[0])
.filter((x) => typeof x !== "undefined") .filter((x) => typeof x !== "undefined")}
.join("")
.substring(0, 3)}
</ServerText> </ServerText>
); );
} }

View File

@@ -1,5 +1,5 @@
import Tippy, { TippyProps } from "@tippyjs/react"; import Tippy, { TippyProps } from "@tippyjs/react";
import styled from "styled-components/macro"; import styled from "styled-components";
import { Text } from "preact-i18n"; import { Text } from "preact-i18n";
@@ -14,7 +14,7 @@ export default function Tooltip(props: Props) {
const { children, content, ...tippyProps } = props; const { children, content, ...tippyProps } = props;
return ( return (
<Tippy content={content} animation="shift-away" {...tippyProps}> <Tippy content={content} {...tippyProps}>
{/* {/*
// @ts-expect-error Type mis-match. */} // @ts-expect-error Type mis-match. */}
<div style={`display: flex;`}>{children}</div> <div style={`display: flex;`}>{children}</div>

View File

@@ -1,11 +1,11 @@
/* eslint-disable react-hooks/rules-of-hooks */ /* eslint-disable react-hooks/rules-of-hooks */
import { Download, CloudDownload } from "@styled-icons/boxicons-regular"; import { Download, CloudDownload } from "@styled-icons/boxicons-regular";
import { useEffect, useState } from "preact/hooks"; import { useContext, useEffect, useState } from "preact/hooks";
import { internalSubscribe } from "../../lib/eventEmitter"; import { internalSubscribe } from "../../lib/eventEmitter";
import { useApplicationState } from "../../mobx/State"; import { ThemeContext } from "../../context/Theme";
import IconButton from "../ui/IconButton"; import IconButton from "../ui/IconButton";
@@ -27,7 +27,7 @@ export default function UpdateIndicator({ style }: Props) {
}); });
if (!pending) return null; if (!pending) return null;
const theme = useApplicationState().settings.theme; const theme = useContext(ThemeContext);
if (style === "titlebar") { if (style === "titlebar") {
return ( return (
@@ -36,10 +36,7 @@ export default function UpdateIndicator({ style }: Props) {
content="A new update is available!" content="A new update is available!"
placement="bottom"> placement="bottom">
<div onClick={() => updateSW(true)}> <div onClick={() => updateSW(true)}>
<CloudDownload <CloudDownload size={22} color={theme.success} />
size={22}
color={theme.getVariable("success")}
/>
</div> </div>
</Tooltip> </Tooltip>
</div> </div>
@@ -50,7 +47,7 @@ export default function UpdateIndicator({ style }: Props) {
return ( return (
<IconButton onClick={() => updateSW(true)}> <IconButton onClick={() => updateSW(true)}>
<Download size={22} color={theme.getVariable("success")} /> <Download size={22} color={theme.success} />
</IconButton> </IconButton>
); );
} }

View File

@@ -1,14 +1,13 @@
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { Message as MessageObject } from "revolt.js"; import { Message as MessageObject } from "revolt.js/dist/maps/Messages";
import { useTriggerEvents } from "preact-context-menu"; import { attachContextMenu } from "preact-context-menu";
import { memo } from "preact/compat"; import { memo } from "preact/compat";
import { useEffect, useState } from "preact/hooks"; import { useState } from "preact/hooks";
import { internalEmit } from "../../../lib/eventEmitter"; import { internalEmit } from "../../../lib/eventEmitter";
import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice";
import { QueuedMessage } from "../../../mobx/stores/MessageQueue"; import { QueuedMessage } from "../../../redux/reducers/queue";
import { useIntermediate } from "../../../context/intermediate/Intermediate"; import { useIntermediate } from "../../../context/intermediate/Intermediate";
import { useClient } from "../../../context/revoltjs/RevoltClient"; import { useClient } from "../../../context/revoltjs/RevoltClient";
@@ -26,7 +25,6 @@ import MessageBase, {
} from "./MessageBase"; } from "./MessageBase";
import Attachment from "./attachments/Attachment"; import Attachment from "./attachments/Attachment";
import { MessageReply } from "./attachments/MessageReply"; import { MessageReply } from "./attachments/MessageReply";
import { MessageOverlayBar } from "./bars/MessageOverlayBar";
import Embed from "./embed/Embed"; import Embed from "./embed/Embed";
import InviteList from "./embed/EmbedInvite"; import InviteList from "./embed/EmbedInvite";
@@ -57,16 +55,18 @@ const Message = observer(
const { openScreen } = useIntermediate(); const { openScreen } = useIntermediate();
const content = message.content; const content = message.content as string;
const head = const head =
preferHead || (message.reply_ids && message.reply_ids.length > 0); preferHead || (message.reply_ids && message.reply_ids.length > 0);
// ! TODO: tell fatal to make this type generic
// bree: Fatal please...
const userContext = attachContext const userContext = attachContext
? useTriggerEvents("Menu", { ? (attachContextMenu("Menu", {
user: message.author_id, user: message.author_id,
contextualChannel: message.channel_id, contextualChannel: message.channel_id,
// eslint-disable-next-line // eslint-disable-next-line
}) }) as any)
: undefined; : undefined;
const openProfile = () => const openProfile = () =>
@@ -86,8 +86,7 @@ const Message = observer(
}; };
// ! FIXME(?): animate on hover // ! FIXME(?): animate on hover
const [mouseHovering, setAnimate] = useState(false); const [animate, setAnimate] = useState(false);
useEffect(() => setAnimate(false), [replacement]);
return ( return (
<div id={message._id}> <div id={message._id}>
@@ -97,7 +96,7 @@ const Message = observer(
key={message_id} key={message_id}
index={index} index={index}
id={message_id} id={message_id}
channel={message.channel} channel={message.channel!}
parent_mentions={message.mention_ids ?? []} parent_mentions={message.mention_ids ?? []}
/> />
))} ))}
@@ -117,25 +116,26 @@ const Message = observer(
sending={typeof queued !== "undefined"} sending={typeof queued !== "undefined"}
mention={message.mention_ids?.includes(client.user!._id)} mention={message.mention_ids?.includes(client.user!._id)}
failed={typeof queued?.error !== "undefined"} failed={typeof queued?.error !== "undefined"}
{...(attachContext onContextMenu={
? useTriggerEvents("Menu", { attachContext
message, ? attachContextMenu("Menu", {
contextualChannel: message.channel_id, message,
queued, contextualChannel: message.channel_id,
}) queued,
: undefined)} })
: undefined
}
onMouseEnter={() => setAnimate(true)} onMouseEnter={() => setAnimate(true)}
onMouseLeave={() => setAnimate(false)}> onMouseLeave={() => setAnimate(false)}>
<MessageInfo click={typeof head !== "undefined"}> <MessageInfo>
{head ? ( {head ? (
<UserIcon <UserIcon
className="avatar"
url={message.generateMasqAvatarURL()} url={message.generateMasqAvatarURL()}
target={user} target={user}
size={36} size={36}
onContextMenu={userContext}
onClick={handleUserClick} onClick={handleUserClick}
animate={mouseHovering} animate={animate}
{...(userContext as any)}
showServerIdentity showServerIdentity
/> />
) : ( ) : (
@@ -150,8 +150,8 @@ const Message = observer(
className="author" className="author"
showServerIdentity showServerIdentity
onClick={handleUserClick} onClick={handleUserClick}
onContextMenu={userContext}
masquerade={message.masquerade!} masquerade={message.masquerade!}
{...userContext}
/> />
<MessageDetail <MessageDetail
message={message} message={message}
@@ -168,23 +168,12 @@ const Message = observer(
<Attachment <Attachment
key={index} key={index}
attachment={attachment} attachment={attachment}
hasContent={ hasContent={index > 0 || content.length > 0}
index > 0 ||
(content ? content.length > 0 : false)
}
/> />
))} ))}
{message.embeds?.map((embed, index) => ( {message.embeds?.map((embed, index) => (
<Embed key={index} embed={embed} /> <Embed key={index} embed={embed} />
))} ))}
{mouseHovering &&
!replacement &&
!isTouchscreenDevice && (
<MessageOverlayBar
message={message}
queued={queued}
/>
)}
</MessageContent> </MessageContent>
</MessageBase> </MessageBase>
</div> </div>

View File

@@ -1,6 +1,6 @@
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { Message } from "revolt.js"; import { Message } from "revolt.js/dist/maps/Messages";
import styled, { css, keyframes } from "styled-components/macro"; import styled, { css, keyframes } from "styled-components";
import { decodeTime } from "ulid"; import { decodeTime } from "ulid";
import { Text } from "preact-i18n"; import { Text } from "preact-i18n";
@@ -134,7 +134,7 @@ export default styled.div<BaseMessageProps>`
} }
`; `;
export const MessageInfo = styled.div<{ click: boolean }>` export const MessageInfo = styled.div`
width: 62px; width: 62px;
display: flex; display: flex;
flex-shrink: 0; flex-shrink: 0;
@@ -142,14 +142,6 @@ export const MessageInfo = styled.div<{ click: boolean }>`
flex-direction: row; flex-direction: row;
justify-content: center; justify-content: center;
.avatar {
user-select: none;
cursor: pointer;
&:active {
transform: translateY(1px);
}
}
.copyBracket { .copyBracket {
opacity: 0; opacity: 0;
position: absolute; position: absolute;
@@ -160,6 +152,15 @@ export const MessageInfo = styled.div<{ click: boolean }>`
position: absolute; position: absolute;
} }
svg {
user-select: none;
cursor: pointer;
&:active {
transform: translateY(1px);
}
}
time { time {
opacity: 0; opacity: 0;
} }
@@ -191,19 +192,9 @@ export const MessageInfo = styled.div<{ click: boolean }>`
margin-right: 0.5em; margin-right: 0.5em;
color: var(--tertiary-foreground); color: var(--tertiary-foreground);
} }
/*${(props) =>
props.click &&
css`
cursor: pointer;
`}*/
`; `;
export const MessageContent = styled.div` export const MessageContent = styled.div`
// Position relatively so we can put
// the overlay in the right place.
position: relative;
min-width: 0; min-width: 0;
flex-grow: 1; flex-grow: 1;
display: flex; display: flex;

View File

@@ -1,17 +1,9 @@
import { Send, ShieldX } from "@styled-icons/boxicons-solid"; import { Send, ShieldX } from "@styled-icons/boxicons-solid";
import Axios, { CancelTokenSource } from "axios"; import Axios, { CancelTokenSource } from "axios";
import Long from "long";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { import { ChannelPermission } from "revolt.js/dist/api/permissions";
Channel, import { Channel } from "revolt.js/dist/maps/Channels";
DEFAULT_PERMISSION_DIRECT_MESSAGE, import styled, { css } from "styled-components";
DEFAULT_PERMISSION_VIEW_ONLY,
Permission,
Server,
U32_MAX,
UserPermission,
} from "revolt.js";
import styled, { css } from "styled-components/macro";
import { ulid } from "ulid"; import { ulid } from "ulid";
import { Text } from "preact-i18n"; import { Text } from "preact-i18n";
@@ -28,9 +20,10 @@ import {
SMOOTH_SCROLL_ON_RECEIVE, SMOOTH_SCROLL_ON_RECEIVE,
} from "../../../lib/renderer/Singleton"; } from "../../../lib/renderer/Singleton";
import { useApplicationState } from "../../../mobx/State"; import { dispatch, getState } from "../../../redux";
import { Reply } from "../../../mobx/stores/MessageQueue"; import { Reply } from "../../../redux/reducers/queue";
import { SoundContext } from "../../../context/Settings";
import { useIntermediate } from "../../../context/intermediate/Intermediate"; import { useIntermediate } from "../../../context/intermediate/Intermediate";
import { import {
FileUploader, FileUploader,
@@ -64,7 +57,6 @@ export type UploadState =
| { type: "failed"; files: File[]; error: string }; | { type: "failed"; files: File[]; error: string };
const Base = styled.div` const Base = styled.div`
z-index: 1;
display: flex; display: flex;
align-items: flex-start; align-items: flex-start;
background: var(--message-box); background: var(--message-box);
@@ -87,15 +79,9 @@ const Blocked = styled.div`
user-select: none; user-select: none;
font-size: var(--text-size); font-size: var(--text-size);
color: var(--tertiary-foreground); color: var(--tertiary-foreground);
flex-grow: 1;
cursor: not-allowed;
.text { .text {
padding: var(--message-box-padding); padding: 14px 14px 14px 0;
}
> div > div {
cursor: default;
} }
svg { svg {
@@ -104,17 +90,13 @@ const Blocked = styled.div`
`; `;
const Action = styled.div` const Action = styled.div`
display: flex;
place-items: center;
> div { > div {
height: 48px; height: 48px;
width: 48px; width: 48px;
display: flex; padding: 12px;
align-items: center;
justify-content: center;
/*padding: 14px 0 14px 14px;*/
}
.mobile {
width: 62px;
} }
${() => ${() =>
@@ -126,44 +108,28 @@ const Action = styled.div`
`} `}
`; `;
const FileAction = styled.div`
> div {
height: 48px;
width: 62px;
display: flex;
align-items: center;
justify-content: center;
}
`;
const ThisCodeWillBeReplacedAnywaysSoIMightAsWellJustDoItThisWay__Padding = styled.div`
width: 16px;
`;
// For sed replacement // For sed replacement
const RE_SED = new RegExp("^s/([^])*/([^])*$"); const RE_SED = new RegExp("^s/([^])*/([^])*$");
// Tests for code block delimiters (``` at start of line)
const RE_CODE_DELIMITER = new RegExp("^```", "gm");
// ! FIXME: add to app config and load from app config // ! FIXME: add to app config and load from app config
export const CAN_UPLOAD_AT_ONCE = 5; export const CAN_UPLOAD_AT_ONCE = 4;
export default observer(({ channel }: Props) => { export default observer(({ channel }: Props) => {
const state = useApplicationState(); const [draft, setDraft] = useState(getState().drafts[channel._id] ?? "");
const [uploadState, setUploadState] = useState<UploadState>({ const [uploadState, setUploadState] = useState<UploadState>({
type: "none", type: "none",
}); });
const [typing, setTyping] = useState<boolean | number>(false); const [typing, setTyping] = useState<boolean | number>(false);
const [replies, setReplies] = useState<Reply[]>([]); const [replies, setReplies] = useState<Reply[]>([]);
const playSound = useContext(SoundContext);
const { openScreen } = useIntermediate(); const { openScreen } = useIntermediate();
const client = useContext(AppContext); const client = useContext(AppContext);
const translate = useTranslation(); const translate = useTranslation();
const renderer = getRenderer(channel); const renderer = getRenderer(channel);
if (!channel.havePermission("SendMessage")) { if (!(channel.permission & ChannelPermission.SendMessage)) {
return ( return (
<Base> <Base>
<Blocked> <Blocked>
@@ -182,18 +148,27 @@ export default observer(({ channel }: Props) => {
); );
} }
// Push message content to draft.
const setMessage = useCallback( const setMessage = useCallback(
(content?: string) => state.draft.set(channel._id, content), (content?: string) => {
[state.draft, channel._id], setDraft(content ?? "");
if (content) {
dispatch({
type: "SET_DRAFT",
channel: channel._id,
content,
});
} else {
dispatch({
type: "CLEAR_DRAFT",
channel: channel._id,
});
}
},
[channel._id],
); );
useEffect(() => { useEffect(() => {
/**
*
* @param content
* @param action
*/
function append(content: string, action: "quote" | "mention") { function append(content: string, action: "quote" | "mention") {
const text = const text =
action === "quote" action === "quote"
@@ -203,10 +178,10 @@ export default observer(({ channel }: Props) => {
.join("\n")}\n\n` .join("\n")}\n\n`
: `${content} `; : `${content} `;
if (!state.draft.has(channel._id)) { if (!draft || draft.length === 0) {
setMessage(text); setMessage(text);
} else { } else {
setMessage(`${state.draft.get(channel._id)}\n${text}`); setMessage(`${draft}\n${text}`);
} }
} }
@@ -215,20 +190,16 @@ export default observer(({ channel }: Props) => {
"append", "append",
append as (...args: unknown[]) => void, append as (...args: unknown[]) => void,
); );
}, [state.draft, channel._id, setMessage]); }, [draft, setMessage]);
/**
* Trigger send message.
*/
async function send() { async function send() {
if (uploadState.type === "uploading" || uploadState.type === "sending") if (uploadState.type === "uploading" || uploadState.type === "sending")
return; return;
const content = state.draft.get(channel._id)?.trim() ?? ""; const content = draft?.trim() ?? "";
if (uploadState.type === "attached") return sendFile(content); if (uploadState.type === "attached") return sendFile(content);
if (content.length === 0) return; if (content.length === 0) return;
internalEmit("NewMessages", "hide");
stopTyping(); stopTyping();
setMessage(); setMessage();
setReplies([]); setReplies([]);
@@ -244,7 +215,7 @@ export default observer(({ channel }: Props) => {
); );
renderer.messages.reverse(); renderer.messages.reverse();
if (msg?.content) { if (msg) {
// eslint-disable-next-line prefer-const // eslint-disable-next-line prefer-const
let [_, toReplace, newText, flags] = content.split(/\//); let [_, toReplace, newText, flags] = content.split(/\//);
@@ -276,15 +247,20 @@ export default observer(({ channel }: Props) => {
} }
} }
} else { } else {
state.settings.sounds.playSound("outbound"); playSound("outbound");
state.queue.add(nonce, channel._id, { dispatch({
_id: nonce, type: "QUEUE_ADD",
nonce,
channel: channel._id, channel: channel._id,
author: client.user!._id, message: {
_id: nonce,
channel: channel._id,
author: client.user!._id,
content, content,
replies, replies,
},
}); });
defer(() => renderer.jumpToBottom(SMOOTH_SCROLL_ON_RECEIVE)); defer(() => renderer.jumpToBottom(SMOOTH_SCROLL_ON_RECEIVE));
@@ -296,16 +272,15 @@ export default observer(({ channel }: Props) => {
replies, replies,
}); });
} catch (error) { } catch (error) {
state.queue.fail(nonce, takeError(error)); dispatch({
type: "QUEUE_FAIL",
error: takeError(error),
nonce,
});
} }
} }
} }
/**
*
* @param content
* @returns
*/
async function sendFile(content: string) { async function sendFile(content: string) {
if (uploadState.type !== "attached") return; if (uploadState.type !== "attached") return;
const attachments: string[] = []; const attachments: string[] = [];
@@ -385,7 +360,7 @@ export default observer(({ channel }: Props) => {
setMessage(); setMessage();
setReplies([]); setReplies([]);
state.settings.sounds.playSound("outbound"); playSound("outbound");
if (files.length > CAN_UPLOAD_AT_ONCE) { if (files.length > CAN_UPLOAD_AT_ONCE) {
setUploadState({ setUploadState({
@@ -397,10 +372,6 @@ export default observer(({ channel }: Props) => {
} }
} }
/**
*
* @returns
*/
function startTyping() { function startTyping() {
if (typeof typing === "number" && +new Date() < typing) return; if (typeof typing === "number" && +new Date() < typing) return;
@@ -414,10 +385,6 @@ export default observer(({ channel }: Props) => {
} }
} }
/**
*
* @param force
*/
function stopTyping(force?: boolean) { function stopTyping(force?: boolean) {
if (force || typing) { if (force || typing) {
const ws = client.websocket; const ws = client.websocket;
@@ -431,21 +398,6 @@ export default observer(({ channel }: Props) => {
} }
} }
function isInCodeBlock(cursor: number): boolean {
const content = state.draft.get(channel._id) || "";
const contentBeforeCursor = content.substring(0, cursor);
let delimiterCount = 0;
for (const delimiter of contentBeforeCursor.matchAll(
RE_CODE_DELIMITER,
)) {
delimiterCount++;
}
// Odd number of ``` delimiters before cursor => we are in code block
return delimiterCount % 2 === 1;
}
// TODO: change to useDebounceCallback // TODO: change to useDebounceCallback
// eslint-disable-next-line // eslint-disable-next-line
const debouncedStopTyping = useCallback( const debouncedStopTyping = useCallback(
@@ -506,8 +458,8 @@ export default observer(({ channel }: Props) => {
setReplies={setReplies} setReplies={setReplies}
/> />
<Base> <Base>
{channel.havePermission("UploadFiles") ? ( {channel.permission & ChannelPermission.UploadFiles ? (
<FileAction> <Action>
<FileUploader <FileUploader
size={24} size={24}
behaviour="multi" behaviour="multi"
@@ -542,10 +494,8 @@ export default observer(({ channel }: Props) => {
} }
}} }}
/> />
</FileAction> </Action>
) : ( ) : undefined}
<ThisCodeWillBeReplacedAnywaysSoIMightAsWellJustDoItThisWay__Padding />
)}
<TextAreaAutoSize <TextAreaAutoSize
autoFocus autoFocus
hideBorder hideBorder
@@ -553,7 +503,7 @@ export default observer(({ channel }: Props) => {
id="message" id="message"
maxLength={2000} maxLength={2000}
onKeyUp={onKeyUp} onKeyUp={onKeyUp}
value={state.draft.get(channel._id) ?? ""} value={draft ?? ""}
padding="var(--message-box-padding)" padding="var(--message-box-padding)"
onKeyDown={(e) => { onKeyDown={(e) => {
if (e.ctrlKey && e.key === "Enter") { if (e.ctrlKey && e.key === "Enter") {
@@ -565,7 +515,7 @@ export default observer(({ channel }: Props) => {
if ( if (
e.key === "ArrowUp" && e.key === "ArrowUp" &&
!state.draft.has(channel._id) (!draft || draft.length === 0)
) { ) {
e.preventDefault(); e.preventDefault();
internalEmit("MessageRenderer", "edit_last"); internalEmit("MessageRenderer", "edit_last");
@@ -576,8 +526,7 @@ export default observer(({ channel }: Props) => {
!e.shiftKey && !e.shiftKey &&
!e.isComposing && !e.isComposing &&
e.key === "Enter" && e.key === "Enter" &&
!isTouchscreenDevice && !isTouchscreenDevice
!isInCodeBlock(e.currentTarget.selectionStart)
) { ) {
e.preventDefault(); e.preventDefault();
return send(); return send();
@@ -625,23 +574,12 @@ export default observer(({ channel }: Props) => {
onFocus={onFocus} onFocus={onFocus}
onBlur={onBlur} onBlur={onBlur}
/> />
{/*<Action>
<IconButton>
<Box size={24} />
</IconButton>
</Action>
<Action>
<IconButton>
<HappyBeaming size={24} />
</IconButton>
</Action>*/}
<Action> <Action>
{/*<IconButton onClick={emojiPicker}>
<HappyAlt size={20} />
</IconButton>*/}
<IconButton <IconButton
className={ className="mobile"
state.settings.get("appearance:show_send_button")
? ""
: "mobile"
}
onClick={send} onClick={send}
onMouseDown={(e) => e.preventDefault()}> onMouseDown={(e) => e.preventDefault()}>
<Send size={20} /> <Send size={20} />

View File

@@ -11,10 +11,11 @@ import {
MessageSquareEdit, MessageSquareEdit,
} from "@styled-icons/boxicons-solid"; } from "@styled-icons/boxicons-solid";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { Message, API } from "revolt.js"; import { SystemMessage as SystemMessageI } from "revolt-api/types/Channels";
import styled from "styled-components/macro"; import { Message } from "revolt.js/dist/maps/Messages";
import styled from "styled-components";
import { useTriggerEvents } from "preact-context-menu"; import { attachContextMenu } from "preact-context-menu";
import { TextReact } from "../../../lib/i18n"; import { TextReact } from "../../../lib/i18n";
@@ -28,26 +29,6 @@ const SystemContent = styled.div`
flex-wrap: wrap; flex-wrap: wrap;
align-items: center; align-items: center;
flex-direction: row; flex-direction: row;
font-size: 14px;
color: var(--secondary-foreground);
span {
font-weight: 600;
color: var(--foreground);
}
svg {
margin-inline-end: 4px;
}
svg,
span {
cursor: pointer;
}
span:hover {
text-decoration: underline;
}
`; `;
interface Props { interface Props {
@@ -74,11 +55,13 @@ export const SystemMessage = observer(
({ attachContext, message, highlight, hideInfo }: Props) => { ({ attachContext, message, highlight, hideInfo }: Props) => {
const data = message.asSystemMessage; const data = message.asSystemMessage;
const SystemMessageIcon = const SystemMessageIcon =
iconDictionary[data.type as API.SystemMessage["type"]] ?? iconDictionary[data.type as SystemMessageI["type"]] ?? InfoCircle;
InfoCircle;
let children = null; let children;
switch (data.type) { switch (data.type) {
case "text":
children = <span>{data.content}</span>;
break;
case "user_added": case "user_added":
case "user_remove": case "user_remove":
children = ( children = (
@@ -135,14 +118,16 @@ export const SystemMessage = observer(
return ( return (
<MessageBase <MessageBase
highlight={highlight} highlight={highlight}
{...(attachContext onContextMenu={
? useTriggerEvents("Menu", { attachContext
message, ? attachContextMenu("Menu", {
contextualChannel: message.channel, message,
}) contextualChannel: message.channel,
: undefined)}> })
: undefined
}>
{!hideInfo && ( {!hideInfo && (
<MessageInfo click={false}> <MessageInfo>
<MessageDetail message={message} position="left" /> <MessageDetail message={message} position="left" />
<SystemMessageIcon className="systemIcon" /> <SystemMessageIcon className="systemIcon" />
</MessageInfo> </MessageInfo>

View File

@@ -1,8 +1,8 @@
import { API } from "revolt.js"; import { Attachment as AttachmentI } from "revolt-api/types/Autumn";
import styles from "./Attachment.module.scss"; import styles from "./Attachment.module.scss";
import classNames from "classnames"; import classNames from "classnames";
import { useTriggerEvents } from "preact-context-menu"; import { attachContextMenu } from "preact-context-menu";
import { useContext, useState } from "preact/hooks"; import { useContext, useState } from "preact/hooks";
import { AppContext } from "../../../../context/revoltjs/RevoltClient"; import { AppContext } from "../../../../context/revoltjs/RevoltClient";
@@ -14,8 +14,8 @@ import Spoiler from "./Spoiler";
import TextFile from "./TextFile"; import TextFile from "./TextFile";
interface Props { interface Props {
attachment: API.File; attachment: AttachmentI;
hasContent?: boolean; hasContent: boolean;
} }
const MAX_ATTACHMENT_WIDTH = 480; const MAX_ATTACHMENT_WIDTH = 480;
@@ -37,7 +37,7 @@ export default function Attachment({ attachment, hasContent }: Props) {
<SizedGrid <SizedGrid
width={metadata.width} width={metadata.width}
height={metadata.height} height={metadata.height}
{...useTriggerEvents("Menu", { onContextMenu={attachContextMenu("Menu", {
attachment, attachment,
})} })}
className={classNames({ className={classNames({

View File

@@ -1,11 +1,11 @@
import { import {
LinkExternal,
Headphone,
Download, Download,
LinkExternal,
File,
Headphone,
Video,
} from "@styled-icons/boxicons-regular"; } from "@styled-icons/boxicons-regular";
import { File, Video } from "@styled-icons/boxicons-solid"; import { Attachment } from "revolt-api/types/Autumn";
import { isFirefox } from "react-device-detect";
import { API } from "revolt.js";
import styles from "./AttachmentActions.module.scss"; import styles from "./AttachmentActions.module.scss";
import classNames from "classnames"; import classNames from "classnames";
@@ -18,16 +18,16 @@ import { AppContext } from "../../../../context/revoltjs/RevoltClient";
import IconButton from "../../../ui/IconButton"; import IconButton from "../../../ui/IconButton";
interface Props { interface Props {
attachment: API.File; attachment: Attachment;
} }
export default function AttachmentActions({ attachment }: Props) { export default function AttachmentActions({ attachment }: Props) {
const client = useContext(AppContext); const client = useContext(AppContext);
const { filename, metadata, size } = attachment; const { filename, metadata, size } = attachment;
const url = client.generateFileURL(attachment); const url = client.generateFileURL(attachment)!;
const open_url = `${url}/${filename}`; const open_url = `${url}/${filename}`;
const download_url = url?.replace("attachments", "attachments/download"); const download_url = url.replace("attachments", "attachments/download");
const filesize = determineFileSize(size); const filesize = determineFileSize(size);
@@ -52,7 +52,7 @@ export default function AttachmentActions({ attachment }: Props) {
href={download_url} href={download_url}
className={styles.downloadIcon} className={styles.downloadIcon}
download download
target={isFirefox || window.native ? "_blank" : "_self"} target="_blank"
rel="noreferrer"> rel="noreferrer">
<IconButton> <IconButton>
<Download size={24} /> <Download size={24} />
@@ -70,7 +70,7 @@ export default function AttachmentActions({ attachment }: Props) {
href={download_url} href={download_url}
className={styles.downloadIcon} className={styles.downloadIcon}
download download
target={isFirefox || window.native ? "_blank" : "_self"} target="_blank"
rel="noreferrer"> rel="noreferrer">
<IconButton> <IconButton>
<Download size={24} /> <Download size={24} />
@@ -90,7 +90,7 @@ export default function AttachmentActions({ attachment }: Props) {
href={download_url} href={download_url}
className={styles.downloadIcon} className={styles.downloadIcon}
download download
target={isFirefox || window.native ? "_blank" : "_self"} target="_blank"
rel="noreferrer"> rel="noreferrer">
<IconButton> <IconButton>
<Download size={24} /> <Download size={24} />
@@ -119,7 +119,7 @@ export default function AttachmentActions({ attachment }: Props) {
href={download_url} href={download_url}
className={styles.downloadIcon} className={styles.downloadIcon}
download download
target={isFirefox || window.native ? "_blank" : "_self"} target="_blank"
rel="noreferrer"> rel="noreferrer">
<IconButton> <IconButton>
<Download size={24} /> <Download size={24} />

View File

@@ -1,15 +1,12 @@
import styled from "styled-components/macro"; import styled from "styled-components";
import { Ref } from "preact";
import { Children } from "../../../../types/Preact"; import { Children } from "../../../../types/Preact";
const Grid = styled.div<{ width: number; height: number }>` const Grid = styled.div<{ width: number; height: number }>`
--width: ${(props) => props.width}px; --width: ${props => props.width}px;
--height: ${(props) => props.height}px; --height: ${props => props.height}px;
display: grid; display: grid;
overflow: hidden;
aspect-ratio: ${(props) => props.width} / ${(props) => props.height}; aspect-ratio: ${(props) => props.width} / ${(props) => props.height};
max-width: min(var(--width), var(--attachment-max-width)); max-width: min(var(--width), var(--attachment-max-width));
@@ -45,7 +42,7 @@ const Grid = styled.div<{ width: number; height: number }>`
overflow: hidden; overflow: hidden;
object-fit: contain; object-fit: contain;
// It's something // It's something
object-position: left; object-position: left;
} }
@@ -74,14 +71,13 @@ type Props = Omit<
children?: Children; children?: Children;
width: number; width: number;
height: number; height: number;
innerRef?: Ref<any>;
}; };
export function SizedGrid(props: Props) { export function SizedGrid(props: Props) {
const { width, height, children, innerRef, ...divProps } = props; const { width, height, children, ...divProps } = props;
return ( return (
<Grid {...divProps} width={width} height={height} ref={innerRef}> <Grid {...divProps} width={width} height={height}>
{children} {children}
</Grid> </Grid>
); );

View File

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

View File

@@ -2,8 +2,10 @@ import { Reply } from "@styled-icons/boxicons-regular";
import { File } from "@styled-icons/boxicons-solid"; import { File } from "@styled-icons/boxicons-solid";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { useHistory } from "react-router-dom"; import { useHistory } from "react-router-dom";
import { Channel, Message, API } from "revolt.js"; import { RelationshipStatus } from "revolt-api/types/Users";
import styled, { css } from "styled-components/macro"; import { Channel } from "revolt.js/dist/maps/Channels";
import { Message } from "revolt.js/dist/maps/Messages";
import styled, { css } from "styled-components";
import { Text } from "preact-i18n"; import { Text } from "preact-i18n";
import { useLayoutEffect, useState } from "preact/hooks"; import { useLayoutEffect, useState } from "preact/hooks";
@@ -16,7 +18,7 @@ import { SystemMessage } from "../SystemMessage";
interface Props { interface Props {
parent_mentions: string[]; parent_mentions: string[];
channel?: Channel; channel: Channel;
index: number; index: number;
id: string; id: string;
} }
@@ -26,25 +28,30 @@ export const ReplyBase = styled.div<{
fail?: boolean; fail?: boolean;
preview?: boolean; preview?: boolean;
}>` }>`
gap: 8px; gap: 4px;
min-width: 0; min-width: 0;
display: flex; display: flex;
margin-inline-start: 30px; margin-inline-start: 30px;
margin-inline-end: 12px; margin-inline-end: 12px;
font-size: 0.8em; font-size: 0.8em;
user-select: none; user-select: none;
align-items: end; align-items: center;
color: var(--secondary-foreground); color: var(--secondary-foreground);
/* nizune's Discord replies,
does not scale properly with messages,
reverted temporarily
&::before { &::before {
content: ""; content: "";
flex-shrink: 0;
width: 22px;
height: 10px; height: 10px;
border-inline-start: 2px solid var(--message-box); width: 28px;
border-top: 2px solid var(--message-box); margin-inline-end: 2px;
align-self: flex-end; 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; overflow: hidden;
@@ -53,7 +60,6 @@ export const ReplyBase = styled.div<{
} }
.user { .user {
//margin-inline-start: 12px;
gap: 6px; gap: 6px;
display: flex; display: flex;
flex-shrink: 0; flex-shrink: 0;
@@ -68,6 +74,13 @@ export const ReplyBase = styled.div<{
text-decoration: underline; text-decoration: underline;
} }
} }
/*&::before {
position:relative;
width: 50px;
height: 2px;
background: red;
}*/
} }
.content { .content {
@@ -85,10 +98,6 @@ export const ReplyBase = styled.div<{
transition: transform ease-in-out 0.1s; transition: transform ease-in-out 0.1s;
filter: brightness(1); filter: brightness(1);
> svg {
flex-shrink: 0;
}
> span > p { > span > p {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -106,6 +115,10 @@ export const ReplyBase = styled.div<{
> * { > * {
pointer-events: none; pointer-events: none;
} }
/*> span > p {
display: flex;
}*/
} }
> svg:first-child { > svg:first-child {
@@ -124,10 +137,6 @@ export const ReplyBase = styled.div<{
props.head && props.head &&
css` css`
margin-top: 12px; margin-top: 12px;
&::before {
border-start-start-radius: 4px;
}
`} `}
${(props) => ${(props) =>
@@ -139,8 +148,6 @@ export const ReplyBase = styled.div<{
export const MessageReply = observer( export const MessageReply = observer(
({ index, channel, id, parent_mentions }: Props) => { ({ index, channel, id, parent_mentions }: Props) => {
if (!channel) return null;
const view = getRenderer(channel); const view = getRenderer(channel);
if (view.state !== "RENDER") return null; if (view.state !== "RENDER") return null;
@@ -170,9 +177,8 @@ export const MessageReply = observer(
return ( return (
<ReplyBase head={index === 0}> <ReplyBase head={index === 0}>
{/*<Reply size={16} />*/} <Reply size={16} />
{message.author?.relationship === RelationshipStatus.Blocked ? (
{message.author?.relationship === "Blocked" ? (
<Text id="app.main.channel.misc.blocked_user" /> <Text id="app.main.channel.misc.blocked_user" />
) : ( ) : (
<> <>
@@ -182,7 +188,7 @@ export const MessageReply = observer(
<> <>
<div className="user"> <div className="user">
<UserShort <UserShort
size={14} size={16}
showServerIdentity showServerIdentity
user={message.author} user={message.author}
masquerade={message.masquerade!} masquerade={message.masquerade!}
@@ -223,10 +229,9 @@ export const MessageReply = observer(
)} )}
<Markdown <Markdown
disallowBigEmoji disallowBigEmoji
content={message.content?.replace( content={(
/\n/g, message.content as string
" ", ).replace(/\n/g, " ")}
)}
/> />
</div> </div>
</> </>

View File

@@ -1,4 +1,4 @@
import styled from "styled-components/macro"; import styled from "styled-components";
import { Text } from "preact-i18n"; import { Text } from "preact-i18n";

View File

@@ -1,8 +1,7 @@
import axios from "axios"; import axios from "axios";
import { API } from "revolt.js"; import { Attachment } from "revolt-api/types/Autumn";
import styles from "./Attachment.module.scss"; import styles from "./Attachment.module.scss";
import { Text } from "preact-i18n";
import { useContext, useEffect, useState } from "preact/hooks"; import { useContext, useEffect, useState } from "preact/hooks";
import RequiresOnline from "../../../../context/revoltjs/RequiresOnline"; import RequiresOnline from "../../../../context/revoltjs/RequiresOnline";
@@ -12,16 +11,14 @@ import {
} from "../../../../context/revoltjs/RevoltClient"; } from "../../../../context/revoltjs/RevoltClient";
import Preloader from "../../../ui/Preloader"; import Preloader from "../../../ui/Preloader";
import { Button } from "@revoltchat/ui";
interface Props { interface Props {
attachment: API.File; attachment: Attachment;
} }
const fileCache: { [key: string]: string } = {}; const fileCache: { [key: string]: string } = {};
export default function TextFile({ attachment }: Props) { export default function TextFile({ attachment }: Props) {
const [gated, setGated] = useState(attachment.size > 100_000);
const [content, setContent] = useState<undefined | string>(undefined); const [content, setContent] = useState<undefined | string>(undefined);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const status = useContext(StatusContext); const status = useContext(StatusContext);
@@ -32,7 +29,13 @@ export default function TextFile({ attachment }: Props) {
useEffect(() => { useEffect(() => {
if (typeof content !== "undefined") return; if (typeof content !== "undefined") return;
if (loading) return; if (loading) return;
if (gated) return;
if (attachment.size > 100_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",
);
return;
}
setLoading(true); setLoading(true);
@@ -57,17 +60,13 @@ export default function TextFile({ attachment }: Props) {
setLoading(false); setLoading(false);
}); });
} }
}, [content, loading, gated, status, attachment._id, attachment.size, url]); }, [content, loading, status, attachment._id, attachment.size, url]);
return ( return (
<div <div
className={styles.textContent} className={styles.textContent}
data-loading={typeof content === "undefined"}> data-loading={typeof content === "undefined"}>
{gated ? ( {content ? (
<Button palette="accent" onClick={() => setGated(false)}>
<Text id="app.main.channel.misc.load_file" />
</Button>
) : content ? (
<pre> <pre>
<code>{content}</code> <code>{content}</code>
</pre> </pre>

View File

@@ -1,6 +1,6 @@
/* eslint-disable react-hooks/rules-of-hooks */ /* eslint-disable react-hooks/rules-of-hooks */
import { XCircle, Plus, Share, X, File } from "@styled-icons/boxicons-regular"; import { XCircle, Plus, Share, X, File } from "@styled-icons/boxicons-regular";
import styled from "styled-components/macro"; import styled from "styled-components";
import { Text } from "preact-i18n"; import { Text } from "preact-i18n";
import { useEffect, useState } from "preact/hooks"; import { useEffect, useState } from "preact/hooks";

View File

@@ -1,121 +1,38 @@
import { DownArrowAlt } from "@styled-icons/boxicons-regular"; import { DownArrowAlt } from "@styled-icons/boxicons-regular";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { Channel } from "revolt.js"; import { Channel } from "revolt.js/dist/maps/Channels";
import styled, { css } from "styled-components/macro"; import styled, { css } from "styled-components";
import { Text } from "preact-i18n"; import { Text } from "preact-i18n";
import { internalEmit } from "../../../../lib/eventEmitter";
import { isTouchscreenDevice } from "../../../../lib/isTouchscreenDevice"; import { isTouchscreenDevice } from "../../../../lib/isTouchscreenDevice";
import { getRenderer } from "../../../../lib/renderer/Singleton"; import { getRenderer } from "../../../../lib/renderer/Singleton";
export const Bar = styled.div<{ position: "top" | "bottom"; accent?: boolean }>` const Bar = styled.div`
z-index: 1; z-index: 10;
position: relative; position: relative;
@keyframes bottomBounce {
0% {
transform: translateY(33px);
}
100% {
transform: translateY(0px);
}
}
@keyframes topBounce {
0% {
transform: translateY(-33px);
}
100% {
transform: translateY(0px);
}
}
${(props) =>
props.position === "top" &&
css`
top: 0;
animation: topBounce 340ms cubic-bezier(0.2, 0.9, 0.5, 1.16)
forwards;
`}
${(props) =>
props.position === "bottom" &&
css`
top: -28px;
animation: bottomBounce 340ms cubic-bezier(0.2, 0.9, 0.5, 1.16)
forwards;
${() =>
isTouchscreenDevice &&
css`
top: -90px;
`}
`}
> div { > div {
top: -26px;
height: 28px; height: 28px;
width: 100%; width: 100%;
position: absolute; position: absolute;
display: flex; display: flex;
align-items: center; align-items: center;
cursor: pointer; cursor: pointer;
font-size: 12px; font-size: 13px;
font-weight: 600;
padding: 0 8px; padding: 0 8px;
user-select: none; user-select: none;
justify-content: space-between; justify-content: space-between;
color: var(--secondary-foreground);
transition: color ease-in-out 0.08s; transition: color ease-in-out 0.08s;
background: var(--secondary-background);
white-space: nowrap; border-radius: var(--border-radius) var(--border-radius) 0 0;
overflow: hidden;
text-overflow: ellipsis;
${(props) =>
props.accent
? css`
color: var(--accent-contrast);
background-color: rgba(
var(--accent-rgb),
max(var(--min-opacity), 0.9)
);
backdrop-filter: blur(20px);
`
: css`
color: var(--secondary-foreground);
background-color: rgba(
var(--secondary-background-rgb),
max(var(--min-opacity), 0.9)
);
backdrop-filter: blur(20px);
`}
${(props) =>
props.position === "top"
? css`
top: 48px;
border-radius: 0 0 var(--border-radius)
var(--border-radius);
`
: css`
border-radius: var(--border-radius) var(--border-radius) 0
0;
`}
${() =>
isTouchscreenDevice &&
css`
top: 56px;
`}
> div { > div {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 6px; gap: 6px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
} }
&:hover { &:hover {
@@ -130,15 +47,10 @@ export const Bar = styled.div<{ position: "top" | "bottom"; accent?: boolean }>`
isTouchscreenDevice && isTouchscreenDevice &&
css` css`
height: 34px; height: 34px;
top: -32px;
padding: 0 12px; padding: 0 12px;
`} `}
} }
@media only screen and (max-width: 800px) {
.right > span {
display: none;
}
}
`; `;
export default observer(({ channel }: { channel: Channel }) => { export default observer(({ channel }: { channel: Channel }) => {
@@ -146,20 +58,14 @@ export default observer(({ channel }: { channel: Channel }) => {
if (renderer.state !== "RENDER" || renderer.atBottom) return null; if (renderer.state !== "RENDER" || renderer.atBottom) return null;
return ( return (
<Bar position="bottom"> <Bar>
<div <div onClick={() => renderer.jumpToBottom(true)}>
onClick={() => {
renderer.jumpToBottom(true);
internalEmit("NewMessages", "hide");
}}>
<div> <div>
<Text id="app.main.channel.misc.viewing_old" /> <Text id="app.main.channel.misc.viewing_old" />
</div> </div>
<div className="right"> <div>
<span> <Text id="app.main.channel.misc.jump_present" />{" "}
<Text id="app.main.channel.misc.jump_present" /> <DownArrowAlt size={20} />
</span>
<DownArrowAlt size={18} />
</div> </div>
</div> </div>
</Bar> </Bar>

View File

@@ -1,213 +0,0 @@
import { DotsVerticalRounded, LinkAlt } from "@styled-icons/boxicons-regular";
import {
Pencil,
Trash,
Share,
InfoSquare,
Notification,
} from "@styled-icons/boxicons-solid";
import { observer } from "mobx-react-lite";
import { Permission } from "revolt.js";
import { Message as MessageObject } from "revolt.js";
import styled from "styled-components";
import { openContextMenu } from "preact-context-menu";
import { useEffect, useState } from "preact/hooks";
import { internalEmit } from "../../../../lib/eventEmitter";
import { shiftKeyPressed } from "../../../../lib/modifiers";
import { getRenderer } from "../../../../lib/renderer/Singleton";
import { QueuedMessage } from "../../../../mobx/stores/MessageQueue";
import {
Screen,
useIntermediate,
} from "../../../../context/intermediate/Intermediate";
import { useClient } from "../../../../context/revoltjs/RevoltClient";
import Tooltip from "../../../common/Tooltip";
interface Props {
message: MessageObject;
queued?: QueuedMessage;
}
const OverlayBar = styled.div`
display: flex;
position: absolute;
justify-self: end;
align-self: end;
align-content: center;
justify-content: center;
right: 0;
top: -18px;
z-index: 0;
transition: box-shadow 0.1s ease-out;
border-radius: 5px;
background: var(--primary-header);
border: 1px solid var(--background);
&:hover {
box-shadow: rgb(0 0 0 / 20%) 0px 2px 10px;
}
`;
const Entry = styled.div`
height: 32px;
width: 32px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
cursor: pointer;
transition: 0.2s ease background-color;
color: var(--secondary-foreground);
&:hover {
background-color: var(--secondary-header);
color: var(--foreground);
}
&:focus {
border-radius: var(--border-radius);
box-shadow: 0 0 0 2.5pt var(--accent);
}
&:active {
svg {
transform: translateY(1px);
}
}
`;
const Divider = styled.div`
margin: 6px 4px;
width: 0.5px;
background: var(--tertiary-background);
`;
export const MessageOverlayBar = observer(({ message, queued }: Props) => {
const client = useClient();
const { openScreen, writeClipboard } = useIntermediate();
const isAuthor = message.author_id === client.user!._id;
const [copied, setCopied] = useState<"link" | "id">(null!);
const [extraActions, setExtra] = useState(shiftKeyPressed);
useEffect(() => {
const handler = (ev: KeyboardEvent) => setExtra(ev.shiftKey);
document.addEventListener("keyup", handler);
document.addEventListener("keydown", handler);
return () => {
document.removeEventListener("keyup", handler);
document.removeEventListener("keydown", handler);
};
});
return (
<OverlayBar>
<Tooltip content="Reply">
<Entry onClick={() => internalEmit("ReplyBar", "add", message)}>
<Share size={18} />
</Entry>
</Tooltip>
{isAuthor && (
<Tooltip content="Edit">
<Entry
onClick={() =>
internalEmit(
"MessageRenderer",
"edit_message",
message._id,
)
}>
<Pencil size={18} />
</Entry>
</Tooltip>
)}
{isAuthor ||
(message.channel &&
message.channel.havePermission("ManageMessages")) ? (
<Tooltip content="Delete">
<Entry
onClick={(e) =>
e.shiftKey
? message.delete()
: openScreen({
id: "special_prompt",
type: "delete_message",
target: message,
} as unknown as Screen)
}>
<Trash size={18} color={"var(--error)"} />
</Entry>
</Tooltip>
) : undefined}
<Tooltip content="More">
<Entry
onClick={() =>
openContextMenu("Menu", {
message,
contextualChannel: message.channel_id,
queued,
})
}>
<DotsVerticalRounded size={18} />
</Entry>
</Tooltip>
{extraActions && (
<>
<Divider />
<Tooltip content="Mark as Unread">
<Entry
onClick={() => {
// ! FIXME: deduplicate this code with ctx menu
const messages = getRenderer(
message.channel!,
).messages;
const index = messages.findIndex(
(x) => x._id === message._id,
);
let unread_id = message._id;
if (index > 0) {
unread_id = messages[index - 1]._id;
}
internalEmit("NewMessages", "mark", unread_id);
message.channel?.ack(unread_id, true);
}}>
<Notification size={18} />
</Entry>
</Tooltip>
<Tooltip
content={copied === "link" ? "Copied!" : "Copy Link"}
hideOnClick={false}>
<Entry
onClick={() => {
setCopied("link");
writeClipboard(message.url);
}}>
<LinkAlt size={18} />
</Entry>
</Tooltip>
<Tooltip
content={copied === "id" ? "Copied!" : "Copy ID"}
hideOnClick={false}>
<Entry
onClick={() => {
setCopied("id");
writeClipboard(message._id);
}}>
<InfoSquare size={18} />
</Entry>
</Tooltip>
</>
)}
</OverlayBar>
);
});

View File

@@ -1,69 +0,0 @@
import { UpArrowAlt } from "@styled-icons/boxicons-regular";
import { observer } from "mobx-react-lite";
import { useHistory } from "react-router-dom";
import { Channel } from "revolt.js";
import { decodeTime } from "ulid";
import { Text } from "preact-i18n";
import { useEffect, useState } from "preact/hooks";
import { internalSubscribe } from "../../../../lib/eventEmitter";
import { getRenderer } from "../../../../lib/renderer/Singleton";
import { dayjs } from "../../../../context/Locale";
import { Bar } from "./JumpToBottom";
export default observer(
({ channel, last_id }: { channel: Channel; last_id?: string }) => {
const [hidden, setHidden] = useState(false);
const hide = () => setHidden(true);
useEffect(() => setHidden(false), [last_id]);
useEffect(() => internalSubscribe("NewMessages", "hide", hide), []);
useEffect(() => {
const onKeyDown = (e: KeyboardEvent) =>
e.key === "Escape" && hide();
document.addEventListener("keydown", onKeyDown);
return () => document.removeEventListener("keydown", onKeyDown);
}, []);
const renderer = getRenderer(channel);
const history = useHistory();
if (renderer.state !== "RENDER") return null;
if (!last_id) return null;
if (hidden) return null;
return (
<Bar position="top" accent>
<div
onClick={() => {
setHidden(true);
if (channel.channel_type === "TextChannel") {
history.push(
`/server/${channel.server_id}/channel/${channel._id}/${last_id}`,
);
} else {
history.push(`/channel/${channel._id}/${last_id}`);
}
}}>
<div>
<Text
id="app.main.channel.misc.new_messages"
fields={{
time_ago: dayjs(decodeTime(last_id)).fromNow(),
}}
/>
</div>
<div className="right">
<span>
<Text id="app.main.channel.misc.jump_beginning" />
</span>
<UpArrowAlt size={20} />
</div>
</div>
</Bar>
);
},
);

View File

@@ -1,19 +1,18 @@
import { At } from "@styled-icons/boxicons-regular"; import { At, Reply as ReplyIcon } from "@styled-icons/boxicons-regular";
import { File, XCircle } from "@styled-icons/boxicons-solid"; import { File, XCircle } from "@styled-icons/boxicons-solid";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { Channel, Message } from "revolt.js"; import { Channel } from "revolt.js/dist/maps/Channels";
import styled from "styled-components/macro"; import { Message } from "revolt.js/dist/maps/Messages";
import styled from "styled-components";
import { Text } from "preact-i18n"; import { Text } from "preact-i18n";
import { StateUpdater, useEffect } from "preact/hooks"; import { StateUpdater, useEffect } from "preact/hooks";
import { internalSubscribe } from "../../../../lib/eventEmitter"; import { internalSubscribe } from "../../../../lib/eventEmitter";
import { useApplicationState } from "../../../../mobx/State"; import { dispatch, getState } from "../../../../redux";
import { SECTION_MENTION } from "../../../../mobx/stores/Layout"; import { Reply } from "../../../../redux/reducers/queue";
import { Reply } from "../../../../mobx/stores/MessageQueue";
import Tooltip from "../../../common/Tooltip";
import IconButton from "../../../ui/IconButton"; import IconButton from "../../../ui/IconButton";
import Markdown from "../../../markdown/Markdown"; import Markdown from "../../../markdown/Markdown";
@@ -28,22 +27,12 @@ interface Props {
} }
const Base = styled.div` const Base = styled.div`
@keyframes bottomBounce {
0% {
transform: translateY(33px);
}
100% {
transform: translateY(0px);
}
}
display: flex; display: flex;
height: 30px; height: 30px;
padding: 0 20px; padding: 0 12px;
user-select: none; user-select: none;
align-items: center; align-items: center;
background: var(--secondary-background); background: var(--message-box);
animation: bottomBounce 340ms cubic-bezier(0.2, 0.9, 0.5, 1.16) forwards;
> div { > div {
flex-grow: 1; flex-grow: 1;
@@ -59,34 +48,21 @@ const Base = styled.div`
display: flex; display: flex;
font-size: 12px; font-size: 12px;
align-items: center; align-items: center;
font-weight: 800; font-weight: 600;
text-transform: uppercase; text-transform: uppercase;
min-width: 6ch; min-width: 6ch;
} }
.replyto { .username {
align-self: center; display: flex;
font-weight: 500; align-items: center;
flex-shrink: 0; gap: 6px;
font-weight: 600;
} }
.content { .message {
display: flex; display: flex;
pointer-events: none; max-height: 26px;
.username {
display: flex;
align-items: center;
gap: 6px;
font-weight: 600;
flex-shrink: 0;
}
.message {
display: flex;
max-height: 26px;
gap: 4px;
}
} }
.actions { .actions {
@@ -105,7 +81,6 @@ const Base = styled.div`
const MAX_REPLIES = 5; const MAX_REPLIES = 5;
export default observer(({ channel, replies, setReplies }: Props) => { export default observer(({ channel, replies, setReplies }: Props) => {
const client = channel.client; const client = channel.client;
const layout = useApplicationState().layout;
// Event listener for adding new messages to reply bar. // Event listener for adding new messages to reply bar.
useEffect(() => { useEffect(() => {
@@ -124,7 +99,7 @@ export default observer(({ channel, replies, setReplies }: Props) => {
mention: mention:
message.author_id === client.user!._id message.author_id === client.user!._id
? false ? false
: layout.getSectionState(SECTION_MENTION, false), : getState().sectionToggle.mention ?? false,
}, },
]); ]);
}); });
@@ -152,48 +127,39 @@ export default observer(({ channel, replies, setReplies }: Props) => {
return ( return (
<Base key={reply.id}> <Base key={reply.id}>
<ReplyBase preview> <ReplyBase preview>
<div class="replyto"> <ReplyIcon size={22} />
<Text id="app.main.channel.reply.replying" /> <div class="username">
<UserShort
size={16}
showServerIdentity
user={message.author}
masquerade={message.masquerade!}
/>
</div> </div>
<div class="content"> <div class="message">
<div class="username"> {message.attachments && (
<UserShort <>
size={16} <File size={16} />
showServerIdentity <em>
user={message.author} {message.attachments.length > 1 ? (
masquerade={message.masquerade!} <Text id="app.main.channel.misc.sent_multiple_files" />
/> ) : (
</div> <Text id="app.main.channel.misc.sent_file" />
<div class="message">
{message.attachments && (
<>
<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>
</>
)}
{message.author_id ===
"00000000000000000000000000" ? (
<SystemMessage
message={message}
hideInfo
/>
) : (
<Markdown
disallowBigEmoji
content={message.content?.replace(
/\n/g,
" ",
)} )}
/> </em>
)} </>
</div> )}
{message.author_id ===
"00000000000000000000000000" ? (
<SystemMessage message={message} hideInfo />
) : (
<Markdown
disallowBigEmoji
content={(
message.content as string
).replace(/\n/g, " ")}
/>
)}
</div> </div>
</ReplyBase> </ReplyBase>
<span class="actions"> <span class="actions">
@@ -215,27 +181,22 @@ export default observer(({ channel, replies, setReplies }: Props) => {
}), }),
); );
layout.setSectionState( dispatch({
SECTION_MENTION, type: "SECTION_TOGGLE_SET",
id: "mention",
state, state,
false, });
);
}}> }}>
<Tooltip <span class="toggle">
content={ <At size={15} />
<Text id="app.main.channel.reply.toggle" /> <Text
}> id={
<span class="toggle"> reply.mention
<At size={15} /> ? "general.on"
<Text : "general.off"
id={ }
reply.mention />
? "general.on" </span>
: "general.off"
}
/>
</span>
</Tooltip>
</IconButton> </IconButton>
)} )}
<IconButton <IconButton

View File

@@ -1,6 +1,7 @@
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { Channel } from "revolt.js"; import { RelationshipStatus } from "revolt-api/types/Users";
import styled from "styled-components/macro"; import { Channel } from "revolt.js/dist/maps/Channels";
import styled from "styled-components";
import { Text } from "preact-i18n"; import { Text } from "preact-i18n";
@@ -12,10 +13,10 @@ const Base = styled.div`
position: relative; position: relative;
> div { > div {
height: 26px; height: 24px;
top: -26px; margin-top: -24px;
position: absolute; position: absolute;
font-size: 13px;
gap: 8px; gap: 8px;
display: flex; display: flex;
padding: 0 10px; padding: 0 10px;
@@ -24,11 +25,7 @@ const Base = styled.div`
flex-direction: row; flex-direction: row;
width: calc(100% - var(--scrollbar-thickness)); width: calc(100% - var(--scrollbar-thickness));
color: var(--secondary-foreground); color: var(--secondary-foreground);
background-color: rgba( background: var(--secondary-background);
var(--secondary-background-rgb),
max(var(--min-opacity), 0.75)
);
backdrop-filter: blur(10px);
} }
.avatars { .avatars {
@@ -39,12 +36,9 @@ const Base = styled.div`
height: 16px; height: 16px;
object-fit: cover; object-fit: cover;
border-radius: var(--border-radius-half); border-radius: var(--border-radius-half);
background: var(--secondary-background);
//background-clip: border-box;
border: 2px solid var(--secondary-background);
&:not(:first-child) { &:not(:first-child) {
margin-left: -6px; margin-left: -4px;
} }
} }
} }
@@ -55,7 +49,6 @@ const Base = styled.div`
overflow: hidden; overflow: hidden;
white-space: nowrap; white-space: nowrap;
text-overflow: ellipsis; text-overflow: ellipsis;
//font-weight: 600;
} }
`; `;
@@ -64,7 +57,7 @@ export default observer(({ channel }: Props) => {
(x) => (x) =>
typeof x !== "undefined" && typeof x !== "undefined" &&
x._id !== x.client.user!._id && x._id !== x.client.user!._id &&
x.relationship !== "Blocked", x.relationship !== RelationshipStatus.Blocked,
); );
if (users.length > 0) { if (users.length > 0) {

View File

@@ -4,7 +4,6 @@
iframe { iframe {
border: none; border: none;
border-radius: var(--border-radius); border-radius: var(--border-radius);
width: 100%;
} }
&.image { &.image {

View File

@@ -1,4 +1,4 @@
import { API } from "revolt.js"; import { Embed as EmbedI } from "revolt-api/types/January";
import styles from "./Embed.module.scss"; import styles from "./Embed.module.scss";
import classNames from "classnames"; import classNames from "classnames";
@@ -8,12 +8,10 @@ import { useIntermediate } from "../../../../context/intermediate/Intermediate";
import { useClient } from "../../../../context/revoltjs/RevoltClient"; import { useClient } from "../../../../context/revoltjs/RevoltClient";
import { MessageAreaWidthContext } from "../../../../pages/channels/messaging/MessageArea"; import { MessageAreaWidthContext } from "../../../../pages/channels/messaging/MessageArea";
import Markdown from "../../../markdown/Markdown";
import Attachment from "../attachments/Attachment";
import EmbedMedia from "./EmbedMedia"; import EmbedMedia from "./EmbedMedia";
interface Props { interface Props {
embed: API.Embed; embed: EmbedI;
} }
const MAX_EMBED_WIDTH = 480; const MAX_EMBED_WIDTH = 480;
@@ -47,63 +45,39 @@ export default function Embed({ embed }: Props) {
} }
switch (embed.type) { switch (embed.type) {
case "Text":
case "Website": { case "Website": {
// Determine special embed size. // Determine special embed size.
let mw, mh; let mw, mh;
const largeMedia = const largeMedia =
embed.type === "Text" (embed.special && embed.special.type !== "None") ||
? typeof embed.media !== "undefined" embed.image?.size === "Large";
: (embed.special && embed.special.type !== "None") || switch (embed.special?.type) {
embed.image?.size === "Large"; case "YouTube":
case "Bandcamp": {
if (embed.type === "Text") { mw = embed.video?.width ?? 1280;
mw = MAX_EMBED_WIDTH; mh = embed.video?.height ?? 720;
mh = 1; break;
} else { }
switch (embed.special?.type) { case "Twitch": {
case "YouTube": mw = 1280;
case "Bandcamp": { mh = 720;
mw = embed.video?.width ?? 1280; break;
mh = embed.video?.height ?? 720; }
break; default: {
} if (embed.image?.size === "Preview") {
case "Twitch": mw = MAX_EMBED_WIDTH;
case "Lightspeed": { mh = Math.min(
mw = 1280; embed.image.height ?? 0,
mh = 720; MAX_PREVIEW_SIZE,
break; );
} } else {
default: { mw = embed.image?.width ?? MAX_EMBED_WIDTH;
if (embed.image?.size === "Preview") { mh = embed.image?.height ?? 0;
mw = MAX_EMBED_WIDTH;
mh = Math.min(
embed.image.height ?? 0,
MAX_PREVIEW_SIZE,
);
} else {
mw = embed.image?.width ?? MAX_EMBED_WIDTH;
mh = embed.image?.height ?? 0;
}
} }
} }
} }
const { width, height } = calculateSize(mw, mh); const { width, height } = calculateSize(mw, mh);
if (embed.type === "Website" && embed.special?.type === "GIF") {
return (
<EmbedMedia
embed={embed}
width={
height *
((embed.image?.width ?? 0) /
(embed.image?.height ?? 0))
}
height={height}
/>
);
}
return ( return (
<div <div
className={classNames(styles.embed, styles.website)} className={classNames(styles.embed, styles.website)}
@@ -113,9 +87,7 @@ export default function Embed({ embed }: Props) {
width: width + CONTAINER_PADDING, width: width + CONTAINER_PADDING,
}}> }}>
<div> <div>
{(embed.type === "Text" {embed.site_name && (
? embed.title
: embed.site_name) && (
<div className={styles.siteinfo}> <div className={styles.siteinfo}>
{embed.icon_url && ( {embed.icon_url && (
<img <img
@@ -130,43 +102,35 @@ export default function Embed({ embed }: Props) {
/> />
)} )}
<div className={styles.site}> <div className={styles.site}>
{embed.type === "Text" {embed.site_name}{" "}
? embed.title
: embed.site_name}{" "}
</div> </div>
</div> </div>
)} )}
{/*<span><a href={embed.url} target={"_blank"} className={styles.author}>Author</a></span>*/} {/*<span><a href={embed.url} target={"_blank"} className={styles.author}>Author</a></span>*/}
{embed.type === "Website" && embed.title && ( {embed.title && (
<span> <span>
<a <a
onMouseDown={(ev) => onMouseDown={(ev) =>
(ev.button === 0 || ev.button === 1) && (ev.button === 0 || ev.button === 1) &&
openLink(embed.url!) openLink(embed.url)
} }
className={styles.title}> className={styles.title}>
{embed.title} {embed.title}
</a> </a>
</span> </span>
)} )}
{embed.description && {embed.description && (
(embed.type === "Text" ? ( <div className={styles.description}>
<Markdown content={embed.description} /> {embed.description}
) : ( </div>
<div className={styles.description}> )}
{embed.description}
</div>
))}
{largeMedia && {largeMedia && (
(embed.type === "Text" ? ( <EmbedMedia embed={embed} height={height} />
<Attachment attachment={embed.media!} /> )}
) : (
<EmbedMedia embed={embed} height={height} />
))}
</div> </div>
{!largeMedia && embed.type === "Website" && ( {!largeMedia && (
<div> <div>
<EmbedMedia <EmbedMedia
embed={embed} embed={embed}
@@ -196,18 +160,6 @@ export default function Embed({ embed }: Props) {
/> />
); );
} }
case "Video": {
return (
<video
className={classNames(styles.embed, styles.image)}
style={calculateSize(embed.width, embed.height)}
src={client.proxyFile(embed.url)}
frameBorder="0"
loading="lazy"
controls
/>
);
}
default: default:
return null; return null;
} }

View File

@@ -1,16 +1,17 @@
import { Group } from "@styled-icons/boxicons-solid"; import { autorun } from "mobx";
import { reaction } from "mobx";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { useHistory } from "react-router-dom"; import { useHistory } from "react-router-dom";
import { Message, API } from "revolt.js"; import { RetrievedInvite } from "revolt-api/types/Invites";
import styled, { css } from "styled-components/macro"; import { Message } from "revolt.js/dist/maps/Messages";
import styled, { css } from "styled-components";
import { useContext, useEffect, useState } from "preact/hooks"; import { useContext, useEffect, useState } from "preact/hooks";
import { Button } from "@revoltchat/ui"; import { defer } from "../../../../lib/defer";
import { isTouchscreenDevice } from "../../../../lib/isTouchscreenDevice"; import { isTouchscreenDevice } from "../../../../lib/isTouchscreenDevice";
import { dispatch } from "../../../../redux";
import { import {
AppContext, AppContext,
ClientStatus, ClientStatus,
@@ -19,6 +20,7 @@ import {
import { takeError } from "../../../../context/revoltjs/util"; import { takeError } from "../../../../context/revoltjs/util";
import ServerIcon from "../../../../components/common/ServerIcon"; import ServerIcon from "../../../../components/common/ServerIcon";
import Button from "../../../../components/ui/Button";
import Overline from "../../../ui/Overline"; import Overline from "../../../ui/Overline";
import Preloader from "../../../ui/Preloader"; import Preloader from "../../../ui/Preloader";
@@ -31,7 +33,7 @@ const EmbedInviteBase = styled.div`
align-items: center; align-items: center;
padding: 0 12px; padding: 0 12px;
margin-top: 2px; margin-top: 2px;
${() => ${() =>
isTouchscreenDevice && isTouchscreenDevice &&
css` css`
flex-wrap: wrap; flex-wrap: wrap;
@@ -42,17 +44,19 @@ const EmbedInviteBase = styled.div`
> button { > button {
width: 100%; width: 100%;
} }
`} `
}
`; `;
const EmbedInviteDetails = styled.div` const EmbedInviteDetails = styled.div`
flex-grow: 1; flex-grow: 1;
padding-inline-start: 12px; padding-left: 12px;
${() => ${() =>
isTouchscreenDevice && isTouchscreenDevice &&
css` css`
width: calc(100% - 55px); width: calc(100% - 55px);
`} `
}
`; `;
const EmbedInviteName = styled.div` const EmbedInviteName = styled.div`
@@ -63,30 +67,24 @@ const EmbedInviteName = styled.div`
`; `;
const EmbedInviteMemberCount = styled.div` const EmbedInviteMemberCount = styled.div`
display: flex;
align-items: center;
gap: 2px;
font-size: 0.8em; font-size: 0.8em;
> svg {
color: var(--secondary-foreground);
}
`; `;
type Props = { type Props = {
code: string; code: string;
}; };
export function EmbedInvite({ code }: Props) { export function EmbedInvite(props: Props) {
const history = useHistory(); const history = useHistory();
const client = useContext(AppContext); const client = useContext(AppContext);
const status = useContext(StatusContext); const status = useContext(StatusContext);
const code = props.code;
const [processing, setProcessing] = useState(false); const [processing, setProcessing] = useState(false);
const [error, setError] = useState<string | undefined>(undefined); const [error, setError] = useState<string | undefined>(undefined);
const [joinError, setJoinError] = useState<string | undefined>(undefined); const [joinError, setJoinError] = useState<string | undefined>(undefined);
const [invite, setInvite] = useState< const [invite, setInvite] = useState<RetrievedInvite | undefined>(
(API.InviteResponse & { type: "Server" }) | undefined undefined,
>(undefined); );
useEffect(() => { useEffect(() => {
if ( if (
@@ -95,9 +93,7 @@ export function EmbedInvite({ code }: Props) {
) { ) {
client client
.fetchInvite(code) .fetchInvite(code)
.then((data) => .then((data) => setInvite(data))
setInvite(data as API.InviteResponse & { type: "Server" }),
)
.catch((err) => setError(takeError(err))); .catch((err) => setError(takeError(err)));
} }
}, [client, code, invite, status]); }, [client, code, invite, status]);
@@ -128,9 +124,7 @@ export function EmbedInvite({ code }: Props) {
<EmbedInviteDetails> <EmbedInviteDetails>
<EmbedInviteName>{invite.server_name}</EmbedInviteName> <EmbedInviteName>{invite.server_name}</EmbedInviteName>
<EmbedInviteMemberCount> <EmbedInviteMemberCount>
<Group size={12} /> {invite.member_count.toLocaleString()} {invite.member_count === 1 ? "member" : "members"}
{invite.member_count.toLocaleString()}{" "}
{invite.member_count === 1 ? "member" : "members"}
</EmbedInviteMemberCount> </EmbedInviteMemberCount>
</EmbedInviteDetails> </EmbedInviteDetails>
{processing ? ( {processing ? (
@@ -140,23 +134,45 @@ export function EmbedInvite({ code }: Props) {
) : ( ) : (
<Button <Button
onClick={async () => { onClick={async () => {
setProcessing(true);
try { try {
await client.joinInvite(invite); setProcessing(true);
history.push( if (invite.type === "Server") {
`/server/${invite.server_id}/channel/${invite.channel_id}`, if (client.servers.get(invite.server_id)) {
); history.push(
`/server/${invite.server_id}/channel/${invite.channel_id}`,
);
}
const dispose = autorun(() => {
const server = client.servers.get(
invite.server_id,
);
defer(() => {
if (server) {
dispatch({
type: "UNREADS_MARK_MULTIPLE_READ",
channels: server.channel_ids,
});
history.push(
`/server/${server._id}/channel/${invite.channel_id}`,
);
}
});
dispose();
});
}
await client.joinInvite(code);
} catch (err) { } catch (err) {
setJoinError(takeError(err)); setJoinError(takeError(err));
} finally {
setProcessing(false); setProcessing(false);
} }
}}> }}>
{client.servers.get(invite.server_id) {client.servers.get(invite.server_id) ? "Joined" : "Join"}
? "Joined"
: "Join"}
</Button> </Button>
)} )}
</EmbedInviteBase> </EmbedInviteBase>
@@ -191,12 +207,9 @@ export default observer(({ message }: { message: Message }) => {
return ( return (
<> <>
{entries.map( {entries.map((entry) => (
(entry) => <EmbedInvite key={entry} code={entry} />
entry !== "discover" && ( ))}
<EmbedInvite key={entry} code={entry} />
),
)}
</> </>
); );
} }

View File

@@ -1,5 +1,5 @@
/* eslint-disable react-hooks/rules-of-hooks */ /* eslint-disable react-hooks/rules-of-hooks */
import { API } from "revolt.js"; import { Embed } from "revolt-api/types/January";
import styles from "./Embed.module.scss"; import styles from "./Embed.module.scss";
@@ -7,7 +7,7 @@ import { useIntermediate } from "../../../../context/intermediate/Intermediate";
import { useClient } from "../../../../context/revoltjs/RevoltClient"; import { useClient } from "../../../../context/revoltjs/RevoltClient";
interface Props { interface Props {
embed: API.Embed; embed: Embed;
width?: number; width?: number;
height: number; height: number;
} }
@@ -47,17 +47,6 @@ export default function EmbedMedia({ embed, width, height }: Props) {
style={{ height }} style={{ height }}
/> />
); );
case "Lightspeed":
return (
<iframe
src={`https://next.lightspeed.tv/embed/${embed.special.id}`}
frameBorder="0"
allowFullScreen
scrolling="no"
loading="lazy"
style={{ height }}
/>
);
case "Spotify": case "Spotify":
return ( return (
<iframe <iframe
@@ -94,21 +83,7 @@ export default function EmbedMedia({ embed, width, height }: Props) {
); );
} }
default: { default: {
if (embed.video) { if (embed.image) {
const url = embed.video.url;
return (
<video
loading="lazy"
className={styles.image}
style={{ width, height }}
src={client.proxyFile(url)}
loop={embed.special?.type === "GIF"}
controls={embed.special?.type !== "GIF"}
autoPlay={embed.special?.type === "GIF"}
muted={embed.special?.type === "GIF" ? true : undefined}
/>
);
} else if (embed.image) {
const url = embed.image.url; const url = embed.image.url;
return ( return (
<img <img
@@ -119,7 +94,7 @@ export default function EmbedMedia({ embed, width, height }: Props) {
onClick={() => onClick={() =>
openScreen({ openScreen({
id: "image_viewer", id: "image_viewer",
embed: embed.image!, embed: embed.image,
}) })
} }
onMouseDown={(ev) => onMouseDown={(ev) =>

View File

@@ -1,12 +1,12 @@
import { LinkExternal } from "@styled-icons/boxicons-regular"; import { LinkExternal } from "@styled-icons/boxicons-regular";
import { API } from "revolt.js"; import { EmbedImage } from "revolt-api/types/January";
import styles from "./Embed.module.scss"; import styles from "./Embed.module.scss";
import IconButton from "../../../ui/IconButton"; import IconButton from "../../../ui/IconButton";
interface Props { interface Props {
embed: API.Image; embed: EmbedImage;
} }
export default function EmbedMediaActions({ embed }: Props) { export default function EmbedMediaActions({ embed }: Props) {

View File

@@ -1,23 +1,11 @@
import { Shield } from "@styled-icons/boxicons-regular"; import { Shield } from "@styled-icons/boxicons-regular";
import styled from "styled-components/macro"; import { Badges } from "revolt-api/types/Users";
import styled from "styled-components";
import { Localizer, Text } from "preact-i18n"; import { Localizer, Text } from "preact-i18n";
import Tooltip from "../Tooltip"; import Tooltip from "../Tooltip";
enum Badges {
Developer = 1,
Translator = 2,
Supporter = 4,
ResponsibleDisclosure = 8,
Founder = 16,
PlatformModeration = 32,
ActiveSupporter = 64,
Paw = 128,
EarlyAdopter = 256,
ReservedRelevantJokeBadge1 = 512,
}
const BadgesBase = styled.div` const BadgesBase = styled.div`
gap: 8px; gap: 8px;
display: flex; display: flex;

View File

@@ -1,4 +1,4 @@
import { User } from "revolt.js"; import { User } from "revolt.js/dist/maps/Users";
import Checkbox, { CheckboxProps } from "../../ui/Checkbox"; import Checkbox, { CheckboxProps } from "../../ui/Checkbox";

View File

@@ -1,8 +1,8 @@
import { Cog } from "@styled-icons/boxicons-solid"; import { Cog } from "@styled-icons/boxicons-solid";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { User } from "revolt.js"; import { User } from "revolt.js/dist/maps/Users";
import styled from "styled-components/macro"; import styled from "styled-components";
import { openContextMenu } from "preact-context-menu"; import { openContextMenu } from "preact-context-menu";
import { Text, Localizer } from "preact-i18n"; import { Text, Localizer } from "preact-i18n";
@@ -52,7 +52,7 @@ export default observer(({ user }: Props) => {
const { writeClipboard } = useIntermediate(); const { writeClipboard } = useIntermediate();
return ( return (
<Header topBorder placement="secondary"> <Header borders placement="secondary">
<HeaderBase> <HeaderBase>
<Localizer> <Localizer>
<Tooltip content={<Text id="app.special.copy_username" />}> <Tooltip content={<Text id="app.special.copy_username" />}>

View File

@@ -1,5 +1,5 @@
import { User } from "revolt.js"; import { User } from "revolt.js/dist/maps/Users";
import styled from "styled-components/macro"; import styled from "styled-components";
import { Children } from "../../../types/Preact"; import { Children } from "../../../types/Preact";
import Tooltip from "../Tooltip"; import Tooltip from "../Tooltip";
@@ -42,7 +42,10 @@ export default function UserHover({ user, children }: Props) {
placement="right-end" placement="right-end"
content={ content={
<Base> <Base>
<Username className="username" user={user} /> <Username
className="username"
user={user}
/>
<span className="status"> <span className="status">
<UserStatus user={user} /> <UserStatus user={user} />
</span> </span>

View File

@@ -1,11 +1,16 @@
import { VolumeMute, MicrophoneOff } from "@styled-icons/boxicons-solid"; import { MicrophoneOff } from "@styled-icons/boxicons-regular";
import { VolumeMute } from "@styled-icons/boxicons-solid";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
import { User, API } from "revolt.js"; import { Masquerade } from "revolt-api/types/Channels";
import styled, { css } from "styled-components/macro"; import { Presence } from "revolt-api/types/Users";
import { User } from "revolt.js/dist/maps/Users";
import { Nullable } from "revolt.js/dist/util/null";
import styled, { css } from "styled-components";
import { useApplicationState } from "../../../mobx/State"; import { useContext } from "preact/hooks";
import { ThemeContext } from "../../../context/Theme";
import { useClient } from "../../../context/revoltjs/RevoltClient"; import { useClient } from "../../../context/revoltjs/RevoltClient";
import fallback from "../assets/user.png"; import fallback from "../assets/user.png";
@@ -16,20 +21,20 @@ type VoiceStatus = "muted" | "deaf";
interface Props extends IconBaseProps<User> { interface Props extends IconBaseProps<User> {
status?: boolean; status?: boolean;
voice?: VoiceStatus; voice?: VoiceStatus;
masquerade?: API.Masquerade; masquerade?: Masquerade;
showServerIdentity?: boolean; showServerIdentity?: boolean;
} }
export function useStatusColour(user?: User) { export function useStatusColour(user?: User) {
const theme = useApplicationState().settings.theme; const theme = useContext(ThemeContext);
return user?.online && user?.status?.presence !== "Invisible" return user?.online && user?.status?.presence !== Presence.Invisible
? user?.status?.presence === "Idle" ? user?.status?.presence === Presence.Idle
? theme.getVariable("status-away") ? theme["status-away"]
: user?.status?.presence === "Busy" : user?.status?.presence === Presence.Busy
? theme.getVariable("status-busy") ? theme["status-busy"]
: theme.getVariable("status-online") : theme["status-online"]
: theme.getVariable("status-invisible"); : theme["status-invisible"];
} }
const VoiceIndicator = styled.div<{ status: VoiceStatus }>` const VoiceIndicator = styled.div<{ status: VoiceStatus }>`
@@ -41,6 +46,10 @@ const VoiceIndicator = styled.div<{ status: VoiceStatus }>`
align-items: center; align-items: center;
justify-content: center; justify-content: center;
svg {
stroke: white;
}
${(props) => ${(props) =>
(props.status === "muted" || props.status === "deaf") && (props.status === "muted" || props.status === "deaf") &&
css` css`
@@ -68,13 +77,12 @@ export default observer(
hover, hover,
showServerIdentity, showServerIdentity,
masquerade, masquerade,
innerRef,
...svgProps ...svgProps
} = props; } = props;
let { url } = props; let { url } = props;
if (masquerade?.avatar) { if (masquerade?.avatar) {
url = client.proxyFile(masquerade.avatar); url = masquerade.avatar;
} else if (!url) { } else if (!url) {
let override; let override;
if (target && showServerIdentity) { if (target && showServerIdentity) {
@@ -93,7 +101,7 @@ export default observer(
url = url =
client.generateFileURL( client.generateFileURL(
override ?? target?.avatar ?? attachment ?? undefined, override ?? target?.avatar ?? attachment,
{ max_side: 256 }, { max_side: 256 },
animate, animate,
) ?? (target ? target.defaultAvatarURL : fallback); ) ?? (target ? target.defaultAvatarURL : fallback);
@@ -102,7 +110,6 @@ export default observer(
return ( return (
<IconBase <IconBase
{...svgProps} {...svgProps}
ref={innerRef}
width={size} width={size}
height={size} height={size}
hover={hover} hover={hover}

View File

@@ -1,9 +1,10 @@
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
import { User, API } from "revolt.js"; import { Masquerade } from "revolt-api/types/Channels";
import styled from "styled-components/macro"; import { User } from "revolt.js/dist/maps/Users";
import { Nullable } from "revolt.js/dist/util/null";
import styled from "styled-components";
import { Ref } from "preact";
import { Text } from "preact-i18n"; import { Text } from "preact-i18n";
import { internalEmit } from "../../../lib/eventEmitter"; import { internalEmit } from "../../../lib/eventEmitter";
@@ -20,9 +21,10 @@ const BotBadge = styled.div`
padding: 0 4px; padding: 0 4px;
font-size: 0.6em; font-size: 0.6em;
user-select: none; user-select: none;
margin-inline-start: 4px; margin-inline-start: 2px;
text-transform: uppercase; text-transform: uppercase;
color: var(--accent-contrast);
color: var(--foreground);
background: var(--accent); background: var(--accent);
border-radius: calc(var(--border-radius) / 2); border-radius: calc(var(--border-radius) / 2);
`; `;
@@ -30,10 +32,8 @@ const BotBadge = styled.div`
type UsernameProps = JSX.HTMLAttributes<HTMLElement> & { type UsernameProps = JSX.HTMLAttributes<HTMLElement> & {
user?: User; user?: User;
prefixAt?: boolean; prefixAt?: boolean;
masquerade?: API.Masquerade; masquerade?: Masquerade;
showServerIdentity?: boolean | "both"; showServerIdentity?: boolean | "both";
innerRef?: Ref<any>;
}; };
export const Username = observer( export const Username = observer(
@@ -42,7 +42,6 @@ export const Username = observer(
prefixAt, prefixAt,
masquerade, masquerade,
showServerIdentity, showServerIdentity,
innerRef,
...otherProps ...otherProps
}: UsernameProps) => { }: UsernameProps) => {
let username = user?.username; let username = user?.username;
@@ -70,7 +69,7 @@ export const Username = observer(
const srv = client.servers.get(member._id.server); const srv = client.servers.get(member._id.server);
if (srv?.roles) { if (srv?.roles) {
for (const role of member.roles) { for (const role of member.roles) {
const c = srv.roles[role]?.colour; const c = srv.roles[role].colour;
if (c) { if (c) {
color = c; color = c;
continue; continue;
@@ -85,24 +84,20 @@ export const Username = observer(
if (user?.bot) { if (user?.bot) {
return ( return (
<> <>
<span {...otherProps} ref={innerRef} style={{ color }}> <span {...otherProps} style={{ color }}>
{masquerade?.name ?? username ?? ( {masquerade?.name ?? username ?? (
<Text id="app.main.channel.unknown_user" /> <Text id="app.main.channel.unknown_user" />
)} )}
</span> </span>
<BotBadge> <BotBadge>
{masquerade ? ( <Text id="app.main.channel.bot" />
<Text id="app.main.channel.bridge" />
) : (
<Text id="app.main.channel.bot" />
)}
</BotBadge> </BotBadge>
</> </>
); );
} }
return ( return (
<span {...otherProps} ref={innerRef} style={{ color }}> <span {...otherProps} style={{ color }}>
{prefixAt ? "@" : undefined} {prefixAt ? "@" : undefined}
{masquerade?.name ?? username ?? ( {masquerade?.name ?? username ?? (
<Text id="app.main.channel.unknown_user" /> <Text id="app.main.channel.unknown_user" />
@@ -122,7 +117,7 @@ export default function UserShort({
user?: User; user?: User;
size?: number; size?: number;
prefixAt?: boolean; prefixAt?: boolean;
masquerade?: API.Masquerade; masquerade?: Masquerade;
showServerIdentity?: boolean; showServerIdentity?: boolean;
}) { }) {
const { openScreen } = useIntermediate(); const { openScreen } = useIntermediate();

View File

@@ -1,5 +1,6 @@
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { User, API } from "revolt.js"; import { Presence } from "revolt-api/types/Users";
import { User } from "revolt.js/dist/maps/Users";
import { Text } from "preact-i18n"; import { Text } from "preact-i18n";
@@ -24,15 +25,15 @@ export default observer(({ user, tooltip }: Props) => {
return <>{user.status.text}</>; return <>{user.status.text}</>;
} }
if (user.status?.presence === "Busy") { if (user.status?.presence === Presence.Busy) {
return <Text id="app.status.busy" />; return <Text id="app.status.busy" />;
} }
if (user.status?.presence === "Idle") { if (user.status?.presence === Presence.Idle) {
return <Text id="app.status.idle" />; return <Text id="app.status.idle" />;
} }
if (user.status?.presence === "Invisible") { if (user.status?.presence === Presence.Invisible) {
return <Text id="app.status.offline" />; return <Text id="app.status.offline" />;
} }

View File

@@ -1,7 +1,5 @@
.markdown { .markdown {
:global(.emoji) { :global(.emoji) {
object-fit: contain;
height: 1.25em; height: 1.25em;
width: 1.25em; width: 1.25em;
margin: 0 0.05em 0 0.1em; margin: 0 0.05em 0 0.1em;
@@ -27,7 +25,6 @@
&[data-type="mention"] { &[data-type="mention"] {
padding: 0 6px; padding: 0 6px;
flex-shrink: 0;
font-weight: 600; font-weight: 600;
display: inline-block; display: inline-block;
background: var(--secondary-background); background: var(--secondary-background);
@@ -93,7 +90,6 @@
p > code { p > code {
padding: 1px 4px; padding: 1px 4px;
flex-shrink: 0;
} }
code { code {

View File

@@ -3,7 +3,7 @@ import { Suspense, lazy } from "preact/compat";
const Renderer = lazy(() => import("./Renderer")); const Renderer = lazy(() => import("./Renderer"));
export interface MarkdownProps { export interface MarkdownProps {
content?: string | null; content?: string;
disallowBigEmoji?: boolean; disallowBigEmoji?: boolean;
} }

View File

@@ -5,6 +5,10 @@ import "katex/dist/katex.min.css";
import MarkdownIt from "markdown-it"; import MarkdownIt from "markdown-it";
// @ts-expect-error No typings. // @ts-expect-error No typings.
import MarkdownEmoji from "markdown-it-emoji/dist/markdown-it-emoji-bare"; import MarkdownEmoji from "markdown-it-emoji/dist/markdown-it-emoji-bare";
// @ts-expect-error No typings.
import MarkdownSub from "markdown-it-sub";
// @ts-expect-error No typings.
import MarkdownSup from "markdown-it-sup";
import { RE_MENTIONS } from "revolt.js"; import { RE_MENTIONS } from "revolt.js";
import styles from "./Markdown.module.scss"; import styles from "./Markdown.module.scss";
@@ -13,7 +17,6 @@ import { useCallback, useContext } from "preact/hooks";
import { internalEmit } from "../../lib/eventEmitter"; import { internalEmit } from "../../lib/eventEmitter";
import { determineLink } from "../../lib/links"; import { determineLink } from "../../lib/links";
import { dayjs } from "../../context/Locale";
import { useIntermediate } from "../../context/intermediate/Intermediate"; import { useIntermediate } from "../../context/intermediate/Intermediate";
import { AppContext } from "../../context/revoltjs/RevoltClient"; import { AppContext } from "../../context/revoltjs/RevoltClient";
@@ -60,6 +63,8 @@ export const md: MarkdownIt = MarkdownIt({
.disable("image") .disable("image")
.use(MarkdownEmoji, { defs: emojiDictionary }) .use(MarkdownEmoji, { defs: emojiDictionary })
.use(MarkdownSpoilers) .use(MarkdownSpoilers)
.use(MarkdownSup)
.use(MarkdownSub)
.use(MarkdownKatex, { .use(MarkdownKatex, {
throwOnError: false, throwOnError: false,
maxExpand: 0, maxExpand: 0,
@@ -116,45 +121,16 @@ const RE_TWEMOJI = /:(\w+):/g;
// ! FIXME: Move to library // ! FIXME: Move to library
const RE_CHANNELS = /<#([A-z0-9]{26})>/g; const RE_CHANNELS = /<#([A-z0-9]{26})>/g;
const RE_TIME = /<t:([0-9]+):(\w)>/g;
export default function Renderer({ content, disallowBigEmoji }: MarkdownProps) { export default function Renderer({ content, disallowBigEmoji }: MarkdownProps) {
const client = useContext(AppContext); const client = useContext(AppContext);
const { openLink } = useIntermediate(); const { openLink } = useIntermediate();
if (typeof content === "undefined") return null; if (typeof content === "undefined") return null;
if (!content || content.length === 0) return null; if (content.length === 0) return null;
// We replace the message with the mention at the time of render. // We replace the message with the mention at the time of render.
// We don't care if the mention changes. // We don't care if the mention changes.
const newContent = content const newContent = content
.replace(RE_TIME, (sub: string, ...args: unknown[]) => {
if (isNaN(args[0] as number)) return sub;
const date = dayjs.unix(args[0] as number);
const format = args[1] as string;
let final = "";
switch (format) {
case "t":
final = date.format("hh:mm");
break;
case "T":
final = date.format("hh:mm:ss");
break;
case "R":
final = date.fromNow();
break;
case "D":
final = date.format("DD MMMM YYYY");
break;
case "F":
final = date.format("dddd, DD MMMM YYYY hh:mm");
break;
default:
final = date.format("DD MMMM YYYY hh:mm");
break;
}
return `\`${final}\``;
})
.replace(RE_MENTIONS, (sub: string, ...args: unknown[]) => { .replace(RE_MENTIONS, (sub: string, ...args: unknown[]) => {
const id = args[0] as string, const id = args[0] as string,
user = client.users.get(id); user = client.users.get(id);

View File

@@ -1,25 +1,14 @@
import { Wrench } from "@styled-icons/boxicons-solid"; import { Wrench } from "@styled-icons/boxicons-solid";
import styled, { css } from "styled-components/macro"; import styled from "styled-components";
import UpdateIndicator from "../common/UpdateIndicator"; import UpdateIndicator from "../common/UpdateIndicator";
interface Props { const TitlebarBase = styled.div`
overlay?: boolean;
}
const TitlebarBase = styled.div<Props>`
height: var(--titlebar-height); height: var(--titlebar-height);
display: flex; display: flex;
user-select: none; user-select: none;
align-items: center; align-items: center;
${(props) =>
props.overlay &&
css`
position: fixed;
width: 100%;
`}
.drag { .drag {
flex-grow: 1; flex-grow: 1;
-webkit-app-region: drag; -webkit-app-region: drag;
@@ -95,9 +84,9 @@ const TitlebarBase = styled.div<Props>`
} }
`; `;
export function Titlebar(props: Props) { export function Titlebar() {
return ( return (
<TitlebarBase {...props}> <TitlebarBase>
<div class="title"> <div class="title">
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"

View File

@@ -1,11 +1,12 @@
import { Message, Group, Compass } from "@styled-icons/boxicons-solid"; import { Message, Group } from "@styled-icons/boxicons-solid";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { useHistory, useLocation } from "react-router"; import { useHistory, useLocation } from "react-router";
import styled, { css } from "styled-components/macro"; import styled, { css } from "styled-components";
import ConditionalLink from "../../lib/ConditionalLink"; import ConditionalLink from "../../lib/ConditionalLink";
import { useApplicationState } from "../../mobx/State"; import { connectState } from "../../redux/connector";
import { LastOpened } from "../../redux/reducers/last_opened";
import { useClient } from "../../context/revoltjs/RevoltClient"; import { useClient } from "../../context/revoltjs/RevoltClient";
@@ -17,10 +18,10 @@ const Base = styled.div`
`; `;
const Navbar = styled.div` const Navbar = styled.div`
z-index: 500; z-index: 100;
display: flex;
margin: 0 auto;
max-width: 500px; max-width: 500px;
margin: 0 auto;
display: flex;
height: var(--bottom-navigation-height); height: var(--bottom-navigation-height);
`; `;
@@ -46,18 +47,22 @@ const Button = styled.a<{ active: boolean }>`
`} `}
`; `;
export default observer(() => { interface Props {
lastOpened: LastOpened;
}
export const BottomNavigation = observer(({ lastOpened }: Props) => {
const client = useClient(); const client = useClient();
const layout = useApplicationState().layout;
const user = client.users.get(client.user!._id); const user = client.users.get(client.user!._id);
const history = useHistory(); const history = useHistory();
const path = useLocation().pathname; const path = useLocation().pathname;
const channel_id = lastOpened["home"];
const friendsActive = path.startsWith("/friends"); const friendsActive = path.startsWith("/friends");
const settingsActive = path.startsWith("/settings"); const settingsActive = path.startsWith("/settings");
const discoverActive = path.startsWith("/discover"); const homeActive = !(friendsActive || settingsActive);
const homeActive = !(friendsActive || settingsActive || discoverActive);
return ( return (
<Base> <Base>
@@ -67,16 +72,14 @@ export default observer(() => {
onClick={() => { onClick={() => {
if (settingsActive) { if (settingsActive) {
if (history.length > 0) { if (history.length > 0) {
history.replace(layout.getLastPath()); history.goBack();
return;
} }
} }
const path = layout.getLastHomePath(); if (channel_id) {
if (path.startsWith("/friends")) { history.push(`/channel/${channel_id}`);
history.push("/");
} else { } else {
history.push(path); history.push("/");
} }
}}> }}>
<Message size={24} /> <Message size={24} />
@@ -103,15 +106,6 @@ export default observer(() => {
</IconButton> </IconButton>
</ConditionalLink> </ConditionalLink>
</Button>*/} </Button>*/}
<Button active={discoverActive}>
<ConditionalLink
active={discoverActive}
to="/discover/servers">
<IconButton>
<Compass size={24} />
</IconButton>
</ConditionalLink>
</Button>
<Button active={settingsActive}> <Button active={settingsActive}>
<ConditionalLink active={settingsActive} to="/settings"> <ConditionalLink active={settingsActive} to="/settings">
<IconButton> <IconButton>
@@ -123,3 +117,9 @@ export default observer(() => {
</Base> </Base>
); );
}); });
export default connectState(BottomNavigation, (state) => {
return {
lastOpened: state.lastOpened,
};
});

View File

@@ -1,23 +1,14 @@
import { observer } from "mobx-react-lite";
import { Route, Switch } from "react-router"; import { Route, Switch } from "react-router";
import { useLocation } from "react-router-dom";
import { isTouchscreenDevice } from "../../lib/isTouchscreenDevice";
import { useApplicationState } from "../../mobx/State";
import { SIDEBAR_CHANNELS } from "../../mobx/stores/Layout";
import SidebarBase from "./SidebarBase"; import SidebarBase from "./SidebarBase";
import HomeSidebar from "./left/HomeSidebar"; import HomeSidebar from "./left/HomeSidebar";
import ServerListSidebar from "./left/ServerListSidebar"; import ServerListSidebar from "./left/ServerListSidebar";
import ServerSidebar from "./left/ServerSidebar"; import ServerSidebar from "./left/ServerSidebar";
import { useSelector } from "react-redux";
import { State } from "../../redux";
export default observer(() => { export default function LeftSidebar() {
const layout = useApplicationState().layout; const isOpen = useSelector((state: State) => state.sectionToggle['sidebar_channels'] ?? true)
const { pathname } = useLocation();
const isOpen =
!pathname.startsWith("/discover") &&
(isTouchscreenDevice || layout.getSectionState(SIDEBAR_CHANNELS, true));
return ( return (
<SidebarBase> <SidebarBase>
@@ -42,4 +33,4 @@ export default observer(() => {
</Switch> </Switch>
</SidebarBase> </SidebarBase>
); );
}); }

View File

@@ -1,4 +1,4 @@
import styled, { css } from "styled-components/macro"; import styled, { css } from "styled-components";
import { isTouchscreenDevice } from "../../lib/isTouchscreenDevice"; import { isTouchscreenDevice } from "../../lib/isTouchscreenDevice";
@@ -8,13 +8,6 @@ export default styled.div`
user-select: none; user-select: none;
flex-direction: row; flex-direction: row;
align-items: stretch; align-items: stretch;
/*background: var(--background);*/
background-color: rgba(
var(--background-rgb),
max(var(--min-opacity), 0.75)
);
backdrop-filter: blur(20px);
`; `;
export const GenericSidebarBase = styled.div<{ export const GenericSidebarBase = styled.div<{
@@ -28,15 +21,10 @@ export const GenericSidebarBase = styled.div<{
/*border-end-start-radius: 8px;*/ /*border-end-start-radius: 8px;*/
background: var(--secondary-background); background: var(--secondary-background);
/*> :nth-child(1) { > :nth-child(1) {
//border-end-start-radius: 8px; border-end-start-radius: 8px;
} }
> :nth-child(2) {
margin-top: 48px;
background: red;
}*/
${(props) => ${(props) =>
props.mobilePadding && props.mobilePadding &&
isTouchscreenDevice && isTouchscreenDevice &&

View File

@@ -1,12 +1,12 @@
import { X } from "@styled-icons/boxicons-regular"; import { X, Crown } from "@styled-icons/boxicons-regular";
import { Crown } from "@styled-icons/boxicons-solid";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { User, Channel } from "revolt.js"; import { Presence } from "revolt-api/types/Users";
import { Channel } from "revolt.js/dist/maps/Channels";
import { User } from "revolt.js/dist/maps/Users";
import styles from "./Item.module.scss"; import styles from "./Item.module.scss";
import classNames from "classnames"; import classNames from "classnames";
import { Ref } from "preact"; import { attachContextMenu } from "preact-context-menu";
import { useTriggerEvents } from "preact-context-menu";
import { Localizer, Text } from "preact-i18n"; import { Localizer, Text } from "preact-i18n";
import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice"; import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice";
@@ -63,9 +63,9 @@ export const UserButton = observer((props: UserProps) => {
data-alert={typeof alert === "string"} data-alert={typeof alert === "string"}
data-online={ data-online={
typeof channel !== "undefined" || typeof channel !== "undefined" ||
(user.online && user.status?.presence !== "Invisible") (user.online && user.status?.presence !== Presence.Invisible)
} }
{...useTriggerEvents("Menu", { onContextMenu={attachContextMenu("Menu", {
user: user._id, user: user._id,
channel: channel?._id, channel: channel?._id,
unread: alert, unread: alert,
@@ -88,7 +88,7 @@ export const UserButton = observer((props: UserProps) => {
alert ? ( alert ? (
channel.last_message.content.slice(0, 32) channel.last_message.content.slice(0, 32)
) : ( ) : (
<UserStatus user={user} tooltip /> <UserStatus user={user} />
)} )}
</div> </div>
} }
@@ -152,17 +152,16 @@ export const ChannelButton = observer((props: ChannelProps) => {
} }
const { openScreen } = useIntermediate(); const { openScreen } = useIntermediate();
const alerting = alert && !muted && !active;
return ( return (
<div <div
{...divProps} {...divProps}
data-active={active} data-active={active}
data-alert={alerting} data-alert={typeof alert === "string" && !muted}
data-muted={muted} data-muted={muted}
aria-label={channel.name} aria-label={channel.name}
className={classNames(styles.item, { [styles.compact]: compact })} className={classNames(styles.item, { [styles.compact]: compact })}
{...useTriggerEvents("Menu", { onContextMenu={attachContextMenu("Menu", {
channel: channel._id, channel: channel._id,
unread: !!alert, unread: !!alert,
})}> })}>
@@ -190,7 +189,7 @@ export const ChannelButton = observer((props: ChannelProps) => {
)} )}
</div> </div>
<div className={styles.button}> <div className={styles.button}>
{alerting && ( {alert && !muted && (
<div className={styles.alert} data-style={alert}> <div className={styles.alert} data-style={alert}>
{alertCount} {alertCount}
</div> </div>

View File

@@ -4,14 +4,12 @@ import { useContext } from "preact/hooks";
import { import {
ClientStatus, ClientStatus,
StatusContext, StatusContext,
useClient,
} from "../../../context/revoltjs/RevoltClient"; } from "../../../context/revoltjs/RevoltClient";
import Banner from "../../ui/Banner"; import Banner from "../../ui/Banner";
export default function ConnectionStatus() { export default function ConnectionStatus() {
const status = useContext(StatusContext); const status = useContext(StatusContext);
const client = useClient();
if (status === ClientStatus.OFFLINE) { if (status === ClientStatus.OFFLINE) {
return ( return (
@@ -22,10 +20,7 @@ export default function ConnectionStatus() {
} else if (status === ClientStatus.DISCONNECTED) { } else if (status === ClientStatus.DISCONNECTED) {
return ( return (
<Banner> <Banner>
<Text id="app.special.status.disconnected" /> <br /> <Text id="app.special.status.disconnected" />
<a onClick={() => client.websocket.connect()}>
<Text id="app.special.status.reconnect" />
</a>
</Banner> </Banner>
); );
} else if (status === ClientStatus.CONNECTING) { } else if (status === ClientStatus.CONNECTING) {

View File

@@ -117,7 +117,7 @@
} }
&[data-muted="true"] { &[data-muted="true"] {
opacity: 0.4; color: var(--tertiary-foreground);
} }
&[data-alert="true"], &[data-alert="true"],
@@ -158,10 +158,10 @@
@media (pointer: coarse) { @media (pointer: coarse) {
.item { .item {
height: 50px; height: 40px;
&.compact { &.compact {
height: 50px; height: var(--bottom-navigation-height);
> div { > div {
gap: 20px; gap: 20px;

View File

@@ -5,8 +5,8 @@ import {
Notepad, Notepad,
} from "@styled-icons/boxicons-solid"; } from "@styled-icons/boxicons-solid";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { Link, useLocation, useParams } from "react-router-dom"; import { Link, Redirect, useLocation, useParams } from "react-router-dom";
import styled, { css } from "styled-components/macro"; import { RelationshipStatus } from "revolt-api/types/Users";
import { Text } from "preact-i18n"; import { Text } from "preact-i18n";
import { useContext, useEffect } from "preact/hooks"; import { useContext, useEffect } from "preact/hooks";
@@ -15,66 +15,57 @@ import ConditionalLink from "../../../lib/ConditionalLink";
import PaintCounter from "../../../lib/PaintCounter"; import PaintCounter from "../../../lib/PaintCounter";
import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice"; import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice";
import { useApplicationState } from "../../../mobx/State"; import { dispatch } from "../../../redux";
import { connectState } from "../../../redux/connector";
import { Unreads } from "../../../redux/reducers/unreads";
import { useIntermediate } from "../../../context/intermediate/Intermediate"; import { useIntermediate } from "../../../context/intermediate/Intermediate";
import { AppContext } from "../../../context/revoltjs/RevoltClient"; import { AppContext } from "../../../context/revoltjs/RevoltClient";
import Category from "../../ui/Category"; import Category from "../../ui/Category";
import placeholderSVG from "../items/placeholder.svg"; import placeholderSVG from "../items/placeholder.svg";
import { mapChannelWithUnread, useUnreads } from "./common";
import { GenericSidebarBase, GenericSidebarList } from "../SidebarBase"; import { GenericSidebarBase, GenericSidebarList } from "../SidebarBase";
import ButtonItem, { ChannelButton } from "../items/ButtonItem"; import ButtonItem, { ChannelButton } from "../items/ButtonItem";
import ConnectionStatus from "../items/ConnectionStatus"; import ConnectionStatus from "../items/ConnectionStatus";
const Navbar = styled.div` type Props = {
display: flex; unreads: Unreads;
align-items: center; };
padding: 0 14px;
font-weight: 600;
flex-shrink: 0;
height: 48px;
${() => const HomeSidebar = observer((props: Props) => {
isTouchscreenDevice &&
css`
height: 56px;
`}
`;
export default observer(() => {
const { pathname } = useLocation(); const { pathname } = useLocation();
const client = useContext(AppContext); const client = useContext(AppContext);
const state = useApplicationState(); const { channel } = useParams<{ channel: string }>();
const { channel: channel_id } = useParams<{ channel: string }>();
const { openScreen } = useIntermediate(); const { openScreen } = useIntermediate();
const channels = [...client.channels.values()].filter( const channels = [...client.channels.values()]
(x) => .filter(
(x.channel_type === "DirectMessage" && x.active) || (x) =>
x.channel_type === "Group", x.channel_type === "DirectMessage" ||
); x.channel_type === "Group",
)
.map((x) => mapChannelWithUnread(x, props.unreads));
const channel = client.channels.get(channel_id); const obj = client.channels.get(channel);
if (channel && !obj) return <Redirect to="/" />;
if (obj) useUnreads({ ...props, channel: obj });
// ! FIXME: move this globally useEffect(() => {
// Track what page the user was last on (in home page). if (!channel) return;
useEffect(() => state.layout.setLastHomePath(pathname), [pathname]);
channels.sort((b, a) => dispatch({
a.last_message_id_or_past.localeCompare(b.last_message_id_or_past), type: "LAST_OPENED_SET",
); parent: "home",
child: channel,
});
}, [channel]);
// ! FIXME: must be a better way channels.sort((b, a) => a.timestamp.localeCompare(b.timestamp));
const incoming = [...client.users.values()].filter(
(user) => user?.relationship === "Incoming",
);
return ( return (
<GenericSidebarBase mobilePadding> <GenericSidebarBase mobilePadding>
<Navbar>
<Text id="app.home.directs" />
</Navbar>
<ConnectionStatus /> <ConnectionStatus />
<GenericSidebarList> <GenericSidebarList>
<ConditionalLink active={pathname === "/"} to="/"> <ConditionalLink active={pathname === "/"} to="/">
@@ -93,9 +84,14 @@ export default observer(() => {
<ButtonItem <ButtonItem
active={pathname === "/friends"} active={pathname === "/friends"}
alert={ alert={
incoming.length > 0 ? "mention" : undefined typeof [...client.users.values()].find(
} (user) =>
alertCount={incoming.length}> user?.relationship ===
RelationshipStatus.Incoming,
) !== "undefined"
? "unread"
: undefined
}>
<UserDetail size={20} /> <UserDetail size={20} />
<span> <span>
<Text id="app.navigation.tabs.friends" /> <Text id="app.navigation.tabs.friends" />
@@ -105,10 +101,9 @@ export default observer(() => {
</> </>
)} )}
<ConditionalLink <ConditionalLink
active={channel?.channel_type === "SavedMessages"} active={obj?.channel_type === "SavedMessages"}
to="/open/saved"> to="/open/saved">
<ButtonItem <ButtonItem active={obj?.channel_type === "SavedMessages"}>
active={channel?.channel_type === "SavedMessages"}>
<Notepad size={20} /> <Notepad size={20} />
<span> <span>
<Text id="app.navigation.tabs.saved" /> <Text id="app.navigation.tabs.saved" />
@@ -137,37 +132,31 @@ export default observer(() => {
{channels.length === 0 && ( {channels.length === 0 && (
<img src={placeholderSVG} loading="eager" /> <img src={placeholderSVG} loading="eager" />
)} )}
{channels.map((channel) => { {channels.map((x) => {
let user; let user;
if (channel.channel_type === "DirectMessage") { if (x.channel.channel_type === "DirectMessage") {
if (!channel.active) return null; if (!x.channel.active) return null;
user = channel.recipient; user = x.channel.recipient;
if (!user) return null; if (!user) {
console.warn(
`Skipped DM ${x.channel._id} because user was missing.`,
);
return null;
}
} }
const isUnread = channel.isUnread(state.notifications);
const mentionCount = channel.getMentions(
state.notifications,
).length;
return ( return (
<ConditionalLink <ConditionalLink
key={channel._id} key={x.channel._id}
active={channel._id === channel_id} active={x.channel._id === channel}
to={`/channel/${channel._id}`}> to={`/channel/${x.channel._id}`}>
<ChannelButton <ChannelButton
user={user} user={user}
channel={channel} channel={x.channel}
alert={ alert={x.unread}
mentionCount > 0 alertCount={x.alertCount}
? "mention" active={x.channel._id === channel}
: isUnread
? "unread"
: undefined
}
alertCount={mentionCount}
active={channel._id === channel_id}
/> />
</ConditionalLink> </ConditionalLink>
); );
@@ -177,3 +166,13 @@ export default observer(() => {
</GenericSidebarBase> </GenericSidebarBase>
); );
}); });
export default connectState(
HomeSidebar,
(state) => {
return {
unreads: state.unreads,
};
},
true,
);

View File

@@ -1,42 +1,366 @@
import { Plus } from "@styled-icons/boxicons-regular";
import { Cog } from "@styled-icons/boxicons-solid";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { useParams } from "react-router-dom"; import { Link, useHistory, useLocation, useParams } from "react-router-dom";
import { RelationshipStatus } from "revolt-api/types/Users";
import styled, { css } from "styled-components";
import { useCallback } from "preact/hooks"; import { attachContextMenu } from "preact-context-menu";
import { Text } from "preact-i18n";
import { ServerList } from "@revoltchat/ui"; import ConditionalLink from "../../../lib/ConditionalLink";
import PaintCounter from "../../../lib/PaintCounter";
import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice";
import { useApplicationState } from "../../../mobx/State"; import { connectState } from "../../../redux/connector";
import { LastOpened } from "../../../redux/reducers/last_opened";
import { Unreads } from "../../../redux/reducers/unreads";
import { useIntermediate } from "../../../context/intermediate/Intermediate"; import { useIntermediate } from "../../../context/intermediate/Intermediate";
import { useClient } from "../../../context/revoltjs/RevoltClient"; import { useClient } from "../../../context/revoltjs/RevoltClient";
/** import ServerIcon from "../../common/ServerIcon";
* Server list sidebar shim component import Tooltip from "../../common/Tooltip";
*/ import UserHover from "../../common/user/UserHover";
export default observer(() => { import UserIcon from "../../common/user/UserIcon";
const client = useClient(); import IconButton from "../../ui/IconButton";
const state = useApplicationState(); import LineDivider from "../../ui/LineDivider";
const { openScreen } = useIntermediate(); import { mapChannelWithUnread } from "./common";
const { server: server_id } = useParams<{ server?: string }>();
const createServer = useCallback( import { Children } from "../../../types/Preact";
() =>
openScreen({ function Icon({
id: "special_input", children,
type: "create_server", unread,
}), count,
[], size,
}: {
children: Children;
unread?: "mention" | "unread";
count: number | 0;
size: number;
}) {
return (
<svg width={size} height={size} aria-hidden="true" viewBox="0 0 32 32">
<use href="#serverIndicator" />
<foreignObject
x="0"
y="0"
width="32"
height="32"
mask={unread ? "url(#server)" : undefined}>
{children}
</foreignObject>
{unread === "unread" && (
<circle cx="27" cy="5" r="5" fill={"white"} />
)}
{unread === "mention" && (
<>
<circle cx="27" cy="5" r="5" fill={"var(--error)"} />
<text
x="27"
y="5"
r="5"
fill={"white"}
style={"text-align:center;"}
text-anchor="middle"
fontSize={"7.5"}
alignmentBaseline={"middle"}
dominant-baseline={"middle"}>
{count < 10 ? count : "9+"}
</text>
</>
)}
</svg>
); );
}
const ServersBase = styled.div`
width: 56px;
height: 100%;
padding-left: 2px;
display: flex;
flex-shrink: 0;
flex-direction: column;
${isTouchscreenDevice &&
css`
padding-bottom: 50px;
`}
`;
const ServerList = styled.div`
flex-grow: 1;
display: flex;
overflow-y: scroll;
padding-bottom: 20px;
flex-direction: column;
scrollbar-width: none;
> :first-child > svg {
margin: 6px 0 6px 4px;
}
&::-webkit-scrollbar {
width: 0px;
}
`;
const ServerEntry = styled.div<{ active: boolean; home?: boolean }>`
height: 58px;
display: flex;
align-items: center;
:focus {
outline: 3px solid blue;
}
> div {
height: 42px;
padding-inline-start: 6px;
display: grid;
place-items: center;
border-start-start-radius: 50%;
border-end-start-radius: 50%;
&:active {
transform: translateY(1px);
}
${(props) =>
props.active &&
css`
&:active {
transform: none;
}
`}
}
> span {
width: 0;
display: relative;
${(props) =>
!props.active &&
css`
display: none;
`}
svg {
margin-top: 5px;
pointer-events: none;
}
}
${(props) =>
(!props.active || props.home) &&
css`
cursor: pointer;
`}
`;
const SettingsButton = styled.div`
width: 50px;
height: 56px;
display: grid;
place-items: center;
`;
function Swoosh() {
return (
<span>
<svg
width="54"
height="106"
viewBox="0 0 54 106"
xmlns="http://www.w3.org/2000/svg">
<path
d="M54 53C54 67.9117 41.9117 80 27 80C12.0883 80 0 67.9117 0 53C0 38.0883 12.0883 26 27 26C41.9117 26 54 38.0883 54 53Z"
fill="var(--sidebar-active)"
/>
<path
d="M27 80C4.5 80 54 53 54 53L54.0001 106C54.0001 106 49.5 80 27 80Z"
fill="var(--sidebar-active)"
/>
<path
d="M27 26C4.5 26 54 53 54 53L53.9999 0C53.9999 0 49.5 26 27 26Z"
fill="var(--sidebar-active)"
/>
</svg>
</span>
);
}
interface Props {
unreads: Unreads;
lastOpened: LastOpened;
}
export const ServerListSidebar = observer(({ unreads, lastOpened }: Props) => {
const client = useClient();
const { server: server_id } = useParams<{ server?: string }>();
const server = server_id ? client.servers.get(server_id) : undefined;
const activeServers = [...client.servers.values()];
const channels = [...client.channels.values()].map((x) =>
mapChannelWithUnread(x, unreads),
);
const unreadChannels = channels
.filter((x) => x.unread)
.map((x) => x.channel?._id);
const servers = activeServers.map((server) => {
let alertCount = 0;
for (const id of server.channel_ids) {
const channel = channels.find((x) => x.channel?._id === id);
if (channel?.alertCount) {
alertCount += channel.alertCount;
}
}
return {
server,
unread: (typeof server.channel_ids.find((x) =>
unreadChannels.includes(x),
) !== "undefined"
? alertCount > 0
? "mention"
: "unread"
: undefined) as "mention" | "unread" | undefined,
alertCount,
};
});
const history = useHistory();
const path = useLocation().pathname;
const { openScreen } = useIntermediate();
let homeUnread: "mention" | "unread" | undefined;
let alertCount = 0;
for (const x of channels) {
if (x.channel?.channel_type === "Group" && x.unread) {
homeUnread = "unread";
alertCount += x.alertCount ?? 0;
}
if (
x.channel?.channel_type === "DirectMessage" &&
x.channel.active &&
x.unread
) {
alertCount++;
}
}
alertCount += [...client.users.values()].filter(
(x) => x.relationship === RelationshipStatus.Incoming,
).length;
if (alertCount > 0) homeUnread = "mention";
const homeActive =
typeof server === "undefined" && !path.startsWith("/invite");
return ( return (
<ServerList <ServersBase>
client={client} <ServerList>
active={server_id} <ConditionalLink
createServer={createServer} active={homeActive}
permit={state.notifications} to={lastOpened.home ? `/channel/${lastOpened.home}` : "/"}>
home={state.layout.getLastHomePath} <ServerEntry home active={homeActive}>
servers={state.ordering.orderedServers} <Swoosh />
reorder={state.ordering.reorderServer} <div
/> onContextMenu={attachContextMenu("Status")}
onClick={() =>
homeActive && history.push("/settings")
}>
<UserHover user={client.user}>
<Icon
size={42}
unread={homeUnread}
count={alertCount}>
<UserIcon
target={client.user}
size={32}
status
hover
/>
</Icon>
</UserHover>
</div>
</ServerEntry>
</ConditionalLink>
<LineDivider />
{servers.map((entry) => {
const active = entry.server._id === server?._id;
const id = lastOpened[entry.server._id];
return (
<ConditionalLink
key={entry.server._id}
active={active}
to={`/server/${entry.server._id}${
id ? `/channel/${id}` : ""
}`}>
<ServerEntry
active={active}
onContextMenu={attachContextMenu("Menu", {
server: entry.server._id,
unread: entry.unread,
})}>
<Swoosh />
<Tooltip
content={entry.server.name}
placement="right">
<Icon
size={42}
unread={entry.unread}
count={entry.alertCount}>
<ServerIcon
size={32}
target={entry.server}
/>
</Icon>
</Tooltip>
</ServerEntry>
</ConditionalLink>
);
})}
<IconButton
onClick={() =>
openScreen({
id: "special_input",
type: "create_server",
})
}>
<Plus size={36} />
</IconButton>
<PaintCounter small />
</ServerList>
{!isTouchscreenDevice && (
<SettingsButton>
<Link to="/settings">
<Tooltip
content={<Text id="app.settings.title" />}
placement="right">
<IconButton>
<Cog size={32} strokeWidth="0.5" />
</IconButton>
</Tooltip>
</Link>
</SettingsButton>
)}
</ServersBase>
); );
}); });
export default connectState(ServerListSidebar, (state) => {
return {
unreads: state.unreads,
lastOpened: state.lastOpened,
};
});

View File

@@ -1,10 +1,8 @@
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { Redirect, useParams } from "react-router"; import { Redirect, useParams } from "react-router";
import { Server } from "revolt.js"; import styled, { css } from "styled-components";
import styled, { css } from "styled-components/macro";
import { Ref } from "preact"; import { attachContextMenu } from "preact-context-menu";
import { useTriggerEvents } from "preact-context-menu";
import { useEffect } from "preact/hooks"; import { useEffect } from "preact/hooks";
import ConditionalLink from "../../../lib/ConditionalLink"; import ConditionalLink from "../../../lib/ConditionalLink";
@@ -12,17 +10,26 @@ import PaintCounter from "../../../lib/PaintCounter";
import { internalEmit } from "../../../lib/eventEmitter"; import { internalEmit } from "../../../lib/eventEmitter";
import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice"; import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice";
import { useApplicationState } from "../../../mobx/State"; import { dispatch } from "../../../redux";
import { connectState } from "../../../redux/connector";
import { Notifications } from "../../../redux/reducers/notifications";
import { Unreads } from "../../../redux/reducers/unreads";
import { useClient } from "../../../context/revoltjs/RevoltClient"; import { useClient } from "../../../context/revoltjs/RevoltClient";
import CollapsibleSection from "../../common/CollapsibleSection"; import CollapsibleSection from "../../common/CollapsibleSection";
import ServerHeader from "../../common/ServerHeader"; import ServerHeader from "../../common/ServerHeader";
import Category from "../../ui/Category"; import Category from "../../ui/Category";
import { mapChannelWithUnread, useUnreads } from "./common";
import { ChannelButton } from "../items/ButtonItem"; import { ChannelButton } from "../items/ButtonItem";
import ConnectionStatus from "../items/ConnectionStatus"; import ConnectionStatus from "../items/ConnectionStatus";
interface Props {
unreads: Unreads;
notifications: Notifications;
}
const ServerBase = styled.div` const ServerBase = styled.div`
height: 100%; height: 100%;
width: 232px; width: 232px;
@@ -50,9 +57,8 @@ const ServerList = styled.div`
} }
`; `;
export default observer(() => { const ServerSidebar = observer((props: Props) => {
const client = useClient(); const client = useClient();
const state = useApplicationState();
const { server: server_id, channel: channel_id } = const { server: server_id, channel: channel_id } =
useParams<{ server: string; channel?: string }>(); useParams<{ server: string; channel?: string }>();
@@ -61,13 +67,25 @@ export default observer(() => {
const channel = channel_id ? client.channels.get(channel_id) : undefined; const channel = channel_id ? client.channels.get(channel_id) : undefined;
// ! FIXME: move this globally // The user selected no channel, let's see if there's a channel available
// Track which channel the user was last on. if (!channel && server.channel_ids.length > 0)
return (
<Redirect
to={`/server/${server_id}/channel/${server.channel_ids[0]}`}
/>
);
if (channel_id && !channel) return <Redirect to={`/server/${server_id}`} />;
if (channel) useUnreads({ ...props, channel });
useEffect(() => { useEffect(() => {
if (!channel_id) return; if (!channel_id) return;
if (!server_id) return;
state.layout.setLastOpened(server_id, channel_id); dispatch({
type: "LAST_OPENED_SET",
parent: server_id!,
child: channel_id!,
});
}, [channel_id, server_id]); }, [channel_id, server_id]);
const uncategorised = new Set(server.channel_ids); const uncategorised = new Set(server.channel_ids);
@@ -78,8 +96,7 @@ export default observer(() => {
if (!entry) return; if (!entry) return;
const active = channel?._id === entry._id; const active = channel?._id === entry._id;
const isUnread = entry.isUnread(state.notifications); const muted = props.notifications[id] === "none";
const mentionCount = entry.getMentions(state.notifications);
return ( return (
<ConditionalLink <ConditionalLink
@@ -100,15 +117,10 @@ export default observer(() => {
<ChannelButton <ChannelButton
channel={entry} channel={entry}
active={active} active={active}
alert={ // ! FIXME: pull it out directly
mentionCount.length > 0 alert={mapChannelWithUnread(entry, props.unreads).unread}
? "mention"
: isUnread
? "unread"
: undefined
}
compact compact
muted={state.notifications.isMuted(entry)} muted={muted}
/> />
</ConditionalLink> </ConditionalLink>
); );
@@ -142,7 +154,7 @@ export default observer(() => {
<ServerHeader server={server} /> <ServerHeader server={server} />
<ConnectionStatus /> <ConnectionStatus />
<ServerList <ServerList
{...useTriggerEvents("Menu", { onContextMenu={attachContextMenu("Menu", {
server_list: server._id, server_list: server._id,
})}> })}>
{elements} {elements}
@@ -151,3 +163,10 @@ export default observer(() => {
</ServerBase> </ServerBase>
); );
}); });
export default connectState(ServerSidebar, (state) => {
return {
unreads: state.unreads,
notifications: state.notifications,
};
});

View File

@@ -0,0 +1,79 @@
import { reaction } from "mobx";
import { Channel } from "revolt.js/dist/maps/Channels";
import { useLayoutEffect, useRef } from "preact/hooks";
import { dispatch } from "../../../redux";
import { Unreads } from "../../../redux/reducers/unreads";
type UnreadProps = {
channel: Channel;
unreads: Unreads;
};
export function useUnreads({ channel, unreads }: UnreadProps) {
// const firstLoad = useRef(true);
useLayoutEffect(() => {
function checkUnread(target: Channel) {
if (!target) return;
if (target._id !== channel._id) return;
if (
target.channel_type === "SavedMessages" ||
target.channel_type === "VoiceChannel"
)
return;
const unread = unreads[channel._id]?.last_id;
if (target.last_message_id) {
if (
!unread ||
(unread && target.last_message_id.localeCompare(unread) > 0)
) {
dispatch({
type: "UNREADS_MARK_READ",
channel: channel._id,
message: target.last_message_id,
});
channel.ack(target.last_message_id);
}
}
}
checkUnread(channel);
return reaction(
() => channel.last_message,
() => checkUnread(channel),
);
}, [channel, unreads]);
}
export function mapChannelWithUnread(channel: Channel, unreads: Unreads) {
const last_message_id = channel.last_message_id;
let unread: "mention" | "unread" | undefined;
let alertCount: undefined | number;
if (last_message_id && unreads) {
const u = unreads[channel._id];
if (u) {
if (u.mentions && u.mentions.length > 0) {
alertCount = u.mentions.length;
unread = "mention";
} else if (
u.last_id &&
(last_message_id as string).localeCompare(u.last_id) > 0
) {
unread = "unread";
}
} else {
unread = "unread";
}
}
return {
channel,
timestamp: last_message_id ?? channel._id,
unread,
alertCount,
};
}

View File

@@ -1,6 +1,6 @@
/* eslint-disable react-hooks/rules-of-hooks */ /* eslint-disable react-hooks/rules-of-hooks */
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { Channel } from "revolt.js"; import { Channel } from "revolt.js/dist/maps/Channels";
import { getRenderer } from "../../../lib/renderer/Singleton"; import { getRenderer } from "../../../lib/renderer/Singleton";

View File

@@ -1,22 +1,21 @@
import { Link } from "react-router-dom";
import { GroupedVirtuoso } from "react-virtuoso"; import { GroupedVirtuoso } from "react-virtuoso";
import { Channel, User } from "revolt.js"; import { Channel } from "revolt.js/dist/maps/Channels";
import styled, { css } from "styled-components/macro"; import { User } from "revolt.js/dist/maps/Users";
import styled, { css } from "styled-components";
import { Text } from "preact-i18n"; import { Text } from "preact-i18n";
import { memo } from "preact/compat"; import { memo } from "preact/compat";
import { internalEmit } from "../../../lib/eventEmitter";
import { import {
Screen, Screen,
useIntermediate, useIntermediate,
} from "../../../context/intermediate/Intermediate"; } from "../../../context/intermediate/Intermediate";
import { UserButton } from "../items/ButtonItem"; import { UserButton } from "../items/ButtonItem";
import { internalEmit } from "../../../lib/eventEmitter";
export type MemberListGroup = { export type MemberListGroup = {
type: "online" | "offline" | "role" | "no_offline"; type: "online" | "offline" | "role";
name?: string; name?: string;
users: User[]; users: User[];
}; };
@@ -40,20 +39,6 @@ const ListCategory = styled.div<{ first?: boolean }>`
`} `}
`; `;
// ! FIXME: temporary performance fix
const NoOomfie = styled.div`
padding: 4px;
padding-bottom: 12px;
font-size: 0.8em;
text-align: center;
color: var(--secondary-foreground);
flex-direction: column;
display: flex;
gap: 4px;
`;
const ItemContent = memo( const ItemContent = memo(
({ ({
item, item,
@@ -69,7 +54,7 @@ const ItemContent = memo(
user={item} user={item}
margin margin
context={context} context={context}
onClick={(e) => { onClick={e => {
if (e.shiftKey) { if (e.shiftKey) {
internalEmit( internalEmit(
"MessageBox", "MessageBox",
@@ -77,13 +62,12 @@ const ItemContent = memo(
`<@${item._id}>`, `<@${item._id}>`,
"mention", "mention",
); );
} else } else[
[ openScreen({
openScreen({ id: "profile",
id: "profile", user_id: item._id,
user_id: item._id, })
}), ]
];
}} }}
/> />
), ),
@@ -102,22 +86,18 @@ export default function MemberList({
<GroupedVirtuoso <GroupedVirtuoso
groupCounts={entries.map((x) => x.users.length)} groupCounts={entries.map((x) => x.users.length)}
groupContent={(index) => { groupContent={(index) => {
const entry = entries[index]; const type = entries[index].type;
return ( return (
<ListCategory first={index === 0}> <ListCategory first={index === 0}>
{entry.type === "role" ? ( {type === "role" ? (
<>{entry.name}</> <>{entries[index].name}</>
) : entry.type === "online" ? ( ) : type === "online" ? (
<Text id="app.status.online" /> <Text id="app.status.online" />
) : ( ) : (
<Text id="app.status.offline" /> <Text id="app.status.offline" />
)} )}
{entry.type !== "no_offline" && ( {" - "}
<> {entries[index].users.length}
{" - "}
{entry.users.length}
</>
)}
</ListCategory> </ListCategory>
); );
}} }}
@@ -128,32 +108,7 @@ export default function MemberList({
.slice(0, groupIndex) .slice(0, groupIndex)
.reduce((a, b) => a + b.users.length, 0); .reduce((a, b) => a + b.users.length, 0);
const entry = entries[groupIndex]; const item = entries[groupIndex].users[relativeIndex];
if (entry.type === "no_offline") {
return (
<NoOomfie>
<div>
Offline users temporarily disabled for this
server, see issue{" "}
<a
href="https://github.com/revoltchat/delta/issues/128"
target="_blank">
#128
</a>{" "}
for when this will be resolved.
</div>
<div>
You may re-enable them in{" "}
<Link to="/settings/experiments">
<a>experiments</a>
</Link>
.
</div>
</NoOomfie>
);
}
const item = entry.users[relativeIndex];
if (!item) return null; if (!item) return null;
return ( return (

View File

@@ -1,10 +1,13 @@
/* eslint-disable react-hooks/rules-of-hooks */ /* eslint-disable react-hooks/rules-of-hooks */
import { autorun } from "mobx";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
import { Channel, Server, User, API } from "revolt.js"; import { Role } from "revolt-api/types/Servers";
import { Presence } from "revolt-api/types/Users";
import { Channel } from "revolt.js/dist/maps/Channels";
import { Server } from "revolt.js/dist/maps/Servers";
import { User } from "revolt.js/dist/maps/Users";
import { useContext, useEffect, useState } from "preact/hooks"; import { useContext, useEffect, useMemo } from "preact/hooks";
import { import {
ClientStatus, ClientStatus,
@@ -30,15 +33,9 @@ export default function MemberSidebar() {
} }
} }
function useEntries( function useEntries(channel: Channel, keys: string[], isServer?: boolean) {
channel: Channel,
generateKeys: () => string[],
isServer?: boolean,
) {
const client = channel.client; const client = channel.client;
const [entries, setEntries] = useState<MemberListGroup[]>([]); return useMemo(() => {
function sort(keys: string[]) {
const categories: { [key: string]: [User, string][] } = { const categories: { [key: string]: [User, string][] } = {
online: [], online: [],
offline: [], offline: [],
@@ -58,7 +55,7 @@ function useEntries(
.map((id) => { .map((id) => {
return [id, roles![id], roles![id].rank ?? 0] as [ return [id, roles![id], roles![id].rank ?? 0] as [
string, string,
API.Role, Role,
number, number,
]; ];
}) })
@@ -92,7 +89,7 @@ function useEntries(
const sort = member?.nickname ?? u.username; const sort = member?.nickname ?? u.username;
const entry = [u, sort] as [User, string]; const entry = [u, sort] as [User, string];
if (!u.online || u.status?.presence === "Invisible") { if (!u.online || u.status?.presence === Presence.Invisible) {
categories.offline.push(entry); categories.offline.push(entry);
} else { } else {
if (isServer) { if (isServer) {
@@ -141,94 +138,48 @@ function useEntries(
}); });
} }
// ! FIXME: Temporary performance fix if (categories.offline.length > 0) {
if (shouldSkipOffline(channel.server_id!)) {
entries.push({
type: "no_offline",
users: [null!],
});
} else if (categories.offline.length > 0) {
entries.push({ entries.push({
type: "offline", type: "offline",
users: categories.offline.map((x) => x[0]), users: categories.offline.map((x) => x[0]),
}); });
} }
setEntries(entries); return entries;
}
useEffect(() => {
return autorun(() => sort(generateKeys()));
// eslint-disable-next-line // eslint-disable-next-line
}, [channel]); }, [keys]);
return entries;
} }
export const GroupMemberSidebar = observer( export const GroupMemberSidebar = observer(
({ channel }: { channel: Channel }) => { ({ channel }: { channel: Channel }) => {
const entries = useEntries(channel, () => channel.recipient_ids!); const keys = [...channel.recipient_ids!];
const entries = useEntries(channel, keys);
return ( return (
<GenericSidebarBase data-scroll-offset="with-padding"> <GenericSidebarBase>
{/*<Container>
{isTouchscreenDevice && <div>Group settings go here</div>}
</Container>*/}
<MemberList entries={entries} context={channel} /> <MemberList entries={entries} context={channel} />
</GenericSidebarBase> </GenericSidebarBase>
); );
}, },
); );
// ! FIXME: this is temporary code until we get lazy guilds like subscriptions
const FETCHED: Set<String> = new Set();
export function resetMemberSidebarFetched() {
FETCHED.clear();
}
const SKIP_OFFLINE = new Set(["01F7ZSBSFHQ8TA81725KQCSDDP"]);
let SKIP_ENABLED = true;
export function setOfflineSkipEnabled(value: boolean) {
SKIP_ENABLED = value;
}
function shouldSkipOffline(id: string) {
if (SKIP_ENABLED) {
return SKIP_OFFLINE.has(id);
}
return false;
}
export const ServerMemberSidebar = observer( export const ServerMemberSidebar = observer(
({ channel }: { channel: Channel }) => { ({ channel }: { channel: Channel }) => {
const client = useClient(); const client = useClient();
const status = useContext(StatusContext); const status = useContext(StatusContext);
useEffect(() => { useEffect(() => {
const server_id = channel.server_id!; if (status === ClientStatus.ONLINE) {
if (status === ClientStatus.ONLINE && !FETCHED.has(server_id)) { channel.server!.fetchMembers();
FETCHED.add(server_id);
channel
.server!.syncMembers(shouldSkipOffline(server_id))
.catch(() => FETCHED.delete(server_id));
} }
}, [status, channel]); // eslint-disable-next-line
}, [status, channel.server_id]);
const entries = useEntries( const keys = [...client.members.keys()];
channel, const entries = useEntries(channel, keys, true);
() => [...client.members.keys()],
true,
);
return ( return (
<GenericSidebarBase data-scroll-offset="with-padding"> <GenericSidebarBase>
{/*<Container>
{isTouchscreenDevice && <div>Server settings go here</div>}
</Container>*/}
<MemberList entries={entries} context={channel} /> <MemberList entries={entries} context={channel} />
</GenericSidebarBase> </GenericSidebarBase>
); );

View File

@@ -1,15 +1,14 @@
import { Link, useParams } from "react-router-dom"; import { Link, useParams } from "react-router-dom";
import { Message as MessageI } from "revolt.js"; import { Message as MessageI } from "revolt.js/dist/maps/Messages";
import styled from "styled-components/macro"; import styled from "styled-components";
import { Text } from "preact-i18n"; import { Text } from "preact-i18n";
import { useEffect, useState } from "preact/hooks"; import { useEffect, useState } from "preact/hooks";
import { Button } from "@revoltchat/ui";
import { useClient } from "../../../context/revoltjs/RevoltClient"; import { useClient } from "../../../context/revoltjs/RevoltClient";
import Message from "../../common/messaging/Message"; import Message from "../../common/messaging/Message";
import Button from "../../ui/Button";
import InputBox from "../../ui/InputBox"; import InputBox from "../../ui/InputBox";
import Overline from "../../ui/Overline"; import Overline from "../../ui/Overline";
import Preloader from "../../ui/Preloader"; import Preloader from "../../ui/Preloader";
@@ -100,7 +99,7 @@ export function SearchSidebar({ close }: Props) {
}, [sort]); }, [sort]);
return ( return (
<GenericSidebarBase data-scroll-offset="with-padding"> <GenericSidebarBase>
<GenericSidebarList> <GenericSidebarList>
<SearchBase> <SearchBase>
<Overline type="accent" block hover> <Overline type="accent" block hover>
@@ -119,7 +118,7 @@ export function SearchSidebar({ close }: Props) {
<Button <Button
key={key} key={key}
compact compact
palette={sort === key ? "accent" : "primary"} accent={sort === key}
onClick={() => setSort(key as Sort)}> onClick={() => setSort(key as Sort)}>
<Text <Text
id={`app.main.channel.search.sort.${key.toLowerCase()}`} id={`app.main.channel.search.sort.${key.toLowerCase()}`}

View File

@@ -1,279 +0,0 @@
import { Brush } from "@styled-icons/boxicons-solid";
import { observer } from "mobx-react-lite";
import { Link } from "react-router-dom";
// @ts-expect-error shade-blend-color does not have typings.
import pSBC from "shade-blend-color";
import { Text } from "preact-i18n";
import TextAreaAutoSize from "../../lib/TextAreaAutoSize";
import { useApplicationState } from "../../mobx/State";
import {
Fonts,
FONTS,
FONT_KEYS,
MonospaceFonts,
MONOSPACE_FONTS,
MONOSPACE_FONT_KEYS,
} from "../../context/Theme";
import Checkbox from "../ui/Checkbox";
import ColourSwatches from "../ui/ColourSwatches";
import ComboBox from "../ui/ComboBox";
import Radio from "../ui/Radio";
import CategoryButton from "../ui/fluent/CategoryButton";
import { EmojiSelector } from "./appearance/EmojiSelector";
import { ThemeBaseSelector } from "./appearance/ThemeBaseSelector";
/**
* Component providing a way to switch the base theme being used.
*/
export const ThemeBaseSelectorShim = observer(() => {
const theme = useApplicationState().settings.theme;
return (
<ThemeBaseSelector
value={theme.isModified() ? undefined : theme.getBase()}
setValue={(base) => {
theme.setBase(base);
theme.reset();
}}
/>
);
});
/**
* Component providing a link to the theme shop.
* Only appears if experiment is enabled.
* TODO: stabilise
*/
export const ThemeShopShim = () => {
return (
<Link to="/discover/themes" replace>
<CategoryButton
icon={<Brush size={24} />}
action="chevron"
description={
<Text id="app.settings.pages.appearance.discover.description" />
}
hover>
<Text id="app.settings.pages.appearance.discover.title" />
</CategoryButton>
</Link>
);
};
/**
* Component providing a way to change current accent colour.
*/
export const ThemeAccentShim = observer(() => {
const theme = useApplicationState().settings.theme;
return (
<>
<h3>
<Text id="app.settings.pages.appearance.accent_selector" />
</h3>
<ColourSwatches
value={theme.getVariable("accent")}
onChange={(colour) => {
theme.setVariable("accent", colour as string);
theme.setVariable("scrollbar-thumb", pSBC(-0.2, colour));
}}
/>
</>
);
});
/**
* Component providing a way to edit custom CSS.
*/
export const ThemeCustomCSSShim = observer(() => {
const theme = useApplicationState().settings.theme;
return (
<>
<h3>
<Text id="app.settings.pages.appearance.custom_css" />
</h3>
<TextAreaAutoSize
maxRows={20}
minHeight={480}
code
value={theme.getCSS() ?? ""}
onChange={(ev) => theme.setCSS(ev.currentTarget.value)}
/>
</>
);
});
/**
* Component providing a way to switch between compact and normal message view.
*/
export const DisplayCompactShim = () => {
// TODO: WIP feature
return (
<>
<h3>
<Text id="app.settings.pages.appearance.message_display" />
</h3>
<div /* className={styles.display} */>
<Radio
description={
<Text id="app.settings.pages.appearance.display.default_description" />
}
checked>
<Text id="app.settings.pages.appearance.display.default" />
</Radio>
<Radio
description={
<Text id="app.settings.pages.appearance.display.compact_description" />
}
disabled>
<Text id="app.settings.pages.appearance.display.compact" />
</Radio>
</div>
</>
);
};
/**
* Component providing a way to change primary text font.
*/
export const DisplayFontShim = observer(() => {
const theme = useApplicationState().settings.theme;
return (
<>
<h3>
<Text id="app.settings.pages.appearance.font" />
</h3>
<ComboBox
value={theme.getFont()}
onChange={(e) => theme.setFont(e.currentTarget.value as Fonts)}>
{FONT_KEYS.map((key) => (
<option value={key} key={key}>
{FONTS[key as keyof typeof FONTS].name}
</option>
))}
</ComboBox>
</>
);
});
/**
* Component providing a way to change secondary, monospace text font.
*/
export const DisplayMonospaceFontShim = observer(() => {
const theme = useApplicationState().settings.theme;
return (
<>
<h3>
<Text id="app.settings.pages.appearance.mono_font" />
</h3>
<ComboBox
value={theme.getMonospaceFont()}
onChange={(e) =>
theme.setMonospaceFont(
e.currentTarget.value as MonospaceFonts,
)
}>
{MONOSPACE_FONT_KEYS.map((key) => (
<option value={key} key={key}>
{
MONOSPACE_FONTS[key as keyof typeof MONOSPACE_FONTS]
.name
}
</option>
))}
</ComboBox>
</>
);
});
/**
* Component providing a way to toggle font ligatures.
*/
export const DisplayLigaturesShim = observer(() => {
const settings = useApplicationState().settings;
if (settings.theme.getFont() !== "Inter") return null;
return (
<>
<Checkbox
checked={settings.get("appearance:ligatures") ?? false}
onChange={(v) => settings.set("appearance:ligatures", v)}
description={
<Text id="app.settings.pages.appearance.ligatures_desc" />
}>
<Text id="app.settings.pages.appearance.ligatures" />
</Checkbox>
</>
);
});
/**
* Component providing a way to toggle showing the send button on desktop.
*/
export const ShowSendButtonShim = observer(() => {
const settings = useApplicationState().settings;
return (
<Checkbox
checked={settings.get("appearance:show_send_button") ?? false}
onChange={(v) => settings.set("appearance:show_send_button", v)}
description={
<Text id="app.settings.pages.appearance.appearance_options.show_send_desc" />
}>
<Text id="app.settings.pages.appearance.appearance_options.show_send" />
</Checkbox>
);
});
/**
* Component providing a way to toggle seasonal themes.
*/
export const DisplaySeasonalShim = observer(() => {
const settings = useApplicationState().settings;
return (
<Checkbox
checked={settings.get("appearance:seasonal") ?? true}
onChange={(v) => settings.set("appearance:seasonal", v)}
description={
<Text id="app.settings.pages.appearance.theme_options.seasonal_desc" />
}>
<Text id="app.settings.pages.appearance.theme_options.seasonal" />
</Checkbox>
);
});
/**
* Component providing a way to toggle transparency effects.
*/
export const DisplayTransparencyShim = observer(() => {
const settings = useApplicationState().settings;
return (
<Checkbox
checked={settings.get("appearance:transparency") ?? true}
onChange={(v) => settings.set("appearance:transparency", v)}
description={
<Text id="app.settings.pages.appearance.theme_options.transparency_desc" />
}>
<Text id="app.settings.pages.appearance.theme_options.transparency" />
</Checkbox>
);
});
/**
* Component providing a way to change emoji pack.
*/
export const DisplayEmojiShim = observer(() => {
const settings = useApplicationState().settings;
return (
<EmojiSelector
value={settings.get("appearance:emoji")}
setValue={(v) => settings.set("appearance:emoji", v)}
/>
);
});

View File

@@ -1,161 +0,0 @@
import styled from "styled-components/macro";
import { Text } from "preact-i18n";
import { EmojiPack } from "../../common/Emoji";
import mutantSVG from "./mutant_emoji.svg";
import notoSVG from "./noto_emoji.svg";
import openmojiSVG from "./openmoji_emoji.svg";
import twemojiSVG from "./twemoji_emoji.svg";
const Container = styled.div`
gap: 12px;
display: flex;
flex-direction: column;
.row {
gap: 12px;
display: flex;
> div {
flex: 1;
display: flex;
flex-direction: column;
}
}
.button {
padding: 2rem 1.2rem;
display: grid;
place-items: center;
cursor: pointer;
transition: border 0.3s;
background: var(--hover);
border: 3px solid transparent;
border-radius: var(--border-radius);
img {
max-width: 100%;
}
&[data-active="true"] {
cursor: default;
background: var(--secondary-background);
border: 3px solid var(--accent);
&:hover {
border: 3px solid var(--accent);
}
}
&:hover {
background: var(--secondary-background);
border: 3px solid var(--tertiary-background);
}
}
h4 {
text-transform: unset !important;
a {
opacity: 0.7;
color: var(--accent);
font-weight: 600;
&:hover {
text-decoration: underline;
}
}
@media only screen and (max-width: 800px) {
a {
display: block;
}
}
}
`;
interface Props {
value?: EmojiPack;
setValue: (pack: EmojiPack) => void;
}
export function EmojiSelector({ value, setValue }: Props) {
return (
<>
<h3>
<Text id="app.settings.pages.appearance.emoji_pack" />
</h3>
<Container>
<div class="row">
<div>
<div
class="button"
onClick={() => setValue("mutant")}
data-active={!value || value === "mutant"}>
<img
loading="eager"
src={mutantSVG}
draggable={false}
onContextMenu={(e) => e.preventDefault()}
/>
</div>
<h4>
Mutant Remix{" "}
<a
href="https://mutant.revolt.chat"
target="_blank"
rel="noreferrer">
(by Revolt)
</a>
</h4>
</div>
<div>
<div
class="button"
onClick={() => setValue("twemoji")}
data-active={value === "twemoji"}>
<img
loading="eager"
src={twemojiSVG}
draggable={false}
onContextMenu={(e) => e.preventDefault()}
/>
</div>
<h4>Twemoji</h4>
</div>
</div>
<div class="row">
<div>
<div
class="button"
onClick={() => setValue("openmoji")}
data-active={value === "openmoji"}>
<img
loading="eager"
src={openmojiSVG}
draggable={false}
onContextMenu={(e) => e.preventDefault()}
/>
</div>
<h4>Openmoji</h4>
</div>
<div>
<div
class="button"
onClick={() => setValue("noto")}
data-active={value === "noto"}>
<img
loading="eager"
src={notoSVG}
draggable={false}
onContextMenu={(e) => e.preventDefault()}
/>
</div>
<h4>Noto Emoji</h4>
</div>
</div>
</Container>
</>
);
}

View File

@@ -1,83 +0,0 @@
import styled from "styled-components/macro";
import { Text } from "preact-i18n";
import darkSVG from "./dark.svg";
import lightSVG from "./light.svg";
const List = styled.div`
gap: 8px;
width: 100%;
display: flex;
margin-bottom: 15px;
> div {
min-width: 0;
display: flex;
flex-direction: column;
}
img {
cursor: pointer;
border-radius: var(--border-radius);
transition: border 0.3s;
border: 3px solid transparent;
width: 100%;
&[data-active="true"] {
cursor: default;
border: 3px solid var(--accent);
&:hover {
border: 3px solid var(--accent);
}
}
&:hover {
border: 3px solid var(--tertiary-background);
}
}
`;
interface Props {
value?: "light" | "dark";
setValue: (base: "light" | "dark") => void;
}
export function ThemeBaseSelector({ value, setValue }: Props) {
return (
<>
<h3>
<Text id="app.settings.pages.appearance.theme" />
</h3>
<List>
<div>
<img
loading="eager"
src={lightSVG}
draggable={false}
data-active={value === "light"}
onClick={() => setValue("light")}
onContextMenu={(e) => e.preventDefault()}
/>
<h4>
<Text id="app.settings.pages.appearance.color.light" />
</h4>
</div>
<div>
<img
loading="eager"
src={darkSVG}
draggable={false}
data-active={value === "dark"}
onClick={() => setValue("dark")}
onContextMenu={(e) => e.preventDefault()}
/>
<h4>
<Text id="app.settings.pages.appearance.color.dark" />
</h4>
</div>
</List>
</>
);
}

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