Compare commits
1 Commits
1.0.1
...
ui/setting
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bba9689e30 |
1
.babelrc
@@ -1 +0,0 @@
|
||||
{ "plugins": [["@babel/plugin-proposal-decorators", { "legacy": true }]] }
|
||||
@@ -5,6 +5,3 @@ dist_injected
|
||||
node_modules
|
||||
.env
|
||||
.env.local
|
||||
|
||||
Dockerfile
|
||||
.dockerignore
|
||||
|
||||
2
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
ko_fi: insertish
|
||||
custom: https://insrt.uk/donate
|
||||
66
.github/ISSUE_TEMPLATE/bug.yml
vendored
@@ -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
|
||||
7
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -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.
|
||||
24
.github/ISSUE_TEMPLATE/feature.yml
vendored
@@ -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
@@ -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.
|
||||
2
.github/actions/build/action.yml
vendored
@@ -19,7 +19,7 @@ runs:
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 16
|
||||
node-version: 15
|
||||
cache: "yarn"
|
||||
|
||||
- name: Install Dependencies and Build
|
||||
|
||||
7
.github/pull_request_template.md
vendored
@@ -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
|
||||
4
.github/workflows/docker.yml
vendored
@@ -5,7 +5,7 @@ on:
|
||||
branches:
|
||||
- "master"
|
||||
tags:
|
||||
- "*"
|
||||
- "v*"
|
||||
paths-ignore:
|
||||
- ".github/**"
|
||||
- "!.github/workflows/docker.yml"
|
||||
@@ -100,7 +100,7 @@ jobs:
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
platforms: linux/amd64
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=local,src=/tmp/.buildx-cache/linux/amd64
|
||||
|
||||
1
.github/workflows/mirroring.yml
vendored
@@ -4,6 +4,7 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- "master"
|
||||
- "production"
|
||||
|
||||
jobs:
|
||||
to_gitlab:
|
||||
|
||||
128
.github/workflows/triage_pr.yml
vendored
@@ -1,72 +1,72 @@
|
||||
name: Add PR to Board
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, synchronize, ready_for_review, review_requested]
|
||||
pull_request_target:
|
||||
types: [opened, synchronize, ready_for_review, review_requested]
|
||||
|
||||
jobs:
|
||||
track_pr:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Get project data
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.PAT }}
|
||||
run: |
|
||||
gh api graphql -f query='
|
||||
query {
|
||||
organization(login: "revoltchat"){
|
||||
projectNext(number: 3) {
|
||||
id
|
||||
fields(first:20) {
|
||||
nodes {
|
||||
id
|
||||
name
|
||||
settings
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}' > project_data.json
|
||||
track_pr:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Get project data
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.PAT }}
|
||||
run: |
|
||||
gh api graphql -f query='
|
||||
query {
|
||||
organization(login: "revoltchat"){
|
||||
projectNext(number: 3) {
|
||||
id
|
||||
fields(first:20) {
|
||||
nodes {
|
||||
id
|
||||
name
|
||||
settings
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}' > project_data.json
|
||||
|
||||
echo 'PROJECT_ID='$(jq '.data.organization.projectNext.id' project_data.json) >> $GITHUB_ENV
|
||||
echo 'STATUS_FIELD_ID='$(jq '.data.organization.projectNext.fields.nodes[] | select(.name== "Status") | .id' project_data.json) >> $GITHUB_ENV
|
||||
echo 'INCOMING_OPTION_ID='$(jq '.data.organization.projectNext.fields.nodes[] | select(.name== "Status") |.settings | fromjson.options[] | select(.name=="Incoming PRs") |.id' project_data.json) >> $GITHUB_ENV
|
||||
echo 'PROJECT_ID='$(jq '.data.organization.projectNext.id' project_data.json) >> $GITHUB_ENV
|
||||
echo 'STATUS_FIELD_ID='$(jq '.data.organization.projectNext.fields.nodes[] | select(.name== "Status") | .id' project_data.json) >> $GITHUB_ENV
|
||||
echo 'INCOMING_OPTION_ID='$(jq '.data.organization.projectNext.fields.nodes[] | select(.name== "Status") |.settings | fromjson.options[] | select(.name=="Incoming PRs") |.id' project_data.json) >> $GITHUB_ENV
|
||||
|
||||
- name: Add PR to project
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.PAT }}
|
||||
PR_ID: ${{ github.event.pull_request.node_id }}
|
||||
run: |
|
||||
item_id="$( gh api graphql -f query='
|
||||
mutation($project:ID!, $pr:ID!) {
|
||||
addProjectNextItem(input: {projectId: $project, contentId: $pr}) {
|
||||
projectNextItem {
|
||||
id
|
||||
}
|
||||
}
|
||||
}' -f project=$PROJECT_ID -f pr=$PR_ID --jq '.data.addProjectNextItem.projectNextItem.id')"
|
||||
|
||||
echo 'ITEM_ID='$item_id >> $GITHUB_ENV
|
||||
- name: Add PR to project
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.PAT }}
|
||||
PR_ID: ${{ github.event.pull_request.node_id }}
|
||||
run: |
|
||||
item_id="$( gh api graphql -f query='
|
||||
mutation($project:ID!, $pr:ID!) {
|
||||
addProjectNextItem(input: {projectId: $project, contentId: $pr}) {
|
||||
projectNextItem {
|
||||
id
|
||||
}
|
||||
}
|
||||
}' -f project=$PROJECT_ID -f pr=$PR_ID --jq '.data.addProjectNextItem.projectNextItem.id')"
|
||||
|
||||
- name: Set fields
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.PAT }}
|
||||
run: |
|
||||
gh api graphql -f query='
|
||||
mutation (
|
||||
$project: ID!
|
||||
$item: ID!
|
||||
$status_field: ID!
|
||||
$status_value: String!
|
||||
) {
|
||||
set_status: updateProjectNextItemField(input: {
|
||||
projectId: $project
|
||||
itemId: $item
|
||||
fieldId: $status_field
|
||||
value: $status_value
|
||||
}) {
|
||||
projectNextItem {
|
||||
id
|
||||
}
|
||||
}
|
||||
}' -f project=$PROJECT_ID -f item=$ITEM_ID -f status_field=$STATUS_FIELD_ID -f status_value=${{ env.INCOMING_OPTION_ID }} --silent
|
||||
echo 'ITEM_ID='$item_id >> $GITHUB_ENV
|
||||
|
||||
- name: Set fields
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.PAT }}
|
||||
run: |
|
||||
gh api graphql -f query='
|
||||
mutation (
|
||||
$project: ID!
|
||||
$item: ID!
|
||||
$status_field: ID!
|
||||
$status_value: String!
|
||||
) {
|
||||
set_status: updateProjectNextItemField(input: {
|
||||
projectId: $project
|
||||
itemId: $item
|
||||
fieldId: $status_field
|
||||
value: $status_value
|
||||
}) {
|
||||
projectNextItem {
|
||||
id
|
||||
}
|
||||
}
|
||||
}' -f project=$PROJECT_ID -f item=$ITEM_ID -f status_field=$STATUS_FIELD_ID -f status_value=${{ env.INCOMING_OPTION_ID }} --silent
|
||||
|
||||
5
.gitignore
vendored
@@ -7,11 +7,6 @@ dist-ssr
|
||||
*.log
|
||||
/.idea
|
||||
|
||||
.yarn/cache
|
||||
.yarn/install-state.gz
|
||||
|
||||
public/assets
|
||||
public/assets_*
|
||||
!public/assets_default
|
||||
|
||||
.vscode/chrome_data
|
||||
|
||||
40
.gitlab-ci.yml
Normal file
@@ -0,0 +1,40 @@
|
||||
image: node:14-buster
|
||||
|
||||
variables:
|
||||
GIT_SUBMODULE_STRATEGY: recursive
|
||||
|
||||
cache:
|
||||
paths:
|
||||
- node_modules
|
||||
|
||||
# Fetch dependencies and setup project for compilation.
|
||||
install:
|
||||
stage: prepare
|
||||
script:
|
||||
- yarn
|
||||
|
||||
# Type check the project
|
||||
typecheck:
|
||||
stage: test
|
||||
needs:
|
||||
- install
|
||||
dependencies:
|
||||
- install
|
||||
script:
|
||||
- yarn typecheck
|
||||
|
||||
# Lint the project and check prettier output.
|
||||
lint:
|
||||
stage: test
|
||||
allow_failure: true
|
||||
needs:
|
||||
- install
|
||||
dependencies:
|
||||
- install
|
||||
script:
|
||||
- yarn lint
|
||||
- yarn --check 'src/**/*.{js,jsx,ts,tsx}'
|
||||
|
||||
stages:
|
||||
- prepare
|
||||
- test
|
||||
@@ -4,12 +4,10 @@ module.exports = {
|
||||
jsxBracketSameLine: true,
|
||||
importOrder: [
|
||||
"preact|classnames|.scss$",
|
||||
"^@revoltchat",
|
||||
"/(lib)",
|
||||
"/(redux|mobx)",
|
||||
"/(context)",
|
||||
"/(ui|common)$",
|
||||
".svg|.webp|.png|.jpg$",
|
||||
"/(ui|common)|.svg|.webp|.png|.jpg$",
|
||||
"^[./]",
|
||||
],
|
||||
importOrderSeparation: true,
|
||||
|
||||
17
.vscode/launch.json
vendored
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
3
.vscode/settings.json
vendored
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"editor.formatOnSave": true
|
||||
"editor.formatOnSave": true,
|
||||
"compile-hero.disable-compile-files-on-did-save-code": true
|
||||
}
|
||||
|
||||
546
.yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
vendored
9
.yarn/plugins/@yarnpkg/plugin-typescript.cjs
vendored
785
.yarn/releases/yarn-3.2.0.cjs
vendored
11
.yarnrc.yml
@@ -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
|
||||
13
Dockerfile
@@ -1,15 +1,18 @@
|
||||
FROM node:16-buster AS builder
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
COPY package*.json ./
|
||||
|
||||
RUN yarn --no-cache
|
||||
|
||||
COPY . .
|
||||
COPY .env.build .env
|
||||
|
||||
RUN yarn install --frozen-lockfile
|
||||
RUN yarn add --dev @babel/plugin-proposal-decorators
|
||||
RUN yarn typecheck
|
||||
RUN yarn build:highmem
|
||||
RUN yarn workspaces focus --production --all
|
||||
RUN yarn build
|
||||
RUN npm prune --production
|
||||
|
||||
FROM node:16-alpine
|
||||
FROM node:16-buster
|
||||
WORKDIR /usr/src/app
|
||||
COPY --from=builder /usr/src/app .
|
||||
|
||||
|
||||
35
README.md
@@ -4,16 +4,6 @@
|
||||
|
||||
This is the web client for Revolt, which is also available live at [app.revolt.chat](https://app.revolt.chat).
|
||||
|
||||
## Pending Rewrite
|
||||
|
||||
The following code is pending a partial or full rewrite:
|
||||
|
||||
- `src/components`: components are being migrated to [revoltchat/components](https://github.com/revoltchat/components)
|
||||
- `src/styles`: needs to be migrated to [revoltchat/components](https://github.com/revoltchat/components)
|
||||
- `src/context/intermediate`: modal system is being rewritten from scratch
|
||||
- `src/context/revoltjs`: client state management needs to be rewritten and include support for concurrent clients
|
||||
- `src/lib`: this needs to be organised
|
||||
|
||||
## Stack
|
||||
|
||||
- [Preact](https://preactjs.com/)
|
||||
@@ -48,22 +38,19 @@ yarn
|
||||
yarn dev
|
||||
```
|
||||
|
||||
You can now access the client at http://local.revolt.chat:3000.
|
||||
|
||||
## CLI Commands
|
||||
|
||||
| Command | Description |
|
||||
| --------------------------------------- | -------------------------------------------- |
|
||||
| `yarn pull` | Setup assets required for Revite. |
|
||||
| `yarn dev` | Start the Revolt client in development mode. |
|
||||
| `yarn build` | Build the Revolt client. |
|
||||
| `yarn preview` | Start a local server with the built client. |
|
||||
| `yarn lint` | Run ESLint on the client. |
|
||||
| `yarn fmt` | Run Prettier on the client. |
|
||||
| `yarn typecheck` | Run TypeScript type checking on the client. |
|
||||
| `yarn start` | Start a local sirv server with built client. |
|
||||
| `yarn start:inject` | Inject a given API URL and start server. |
|
||||
| `yarn lint \| egrep "no-literals" -B 1` | Scan for untranslated strings. |
|
||||
| Command | Description |
|
||||
| ------------------- | -------------------------------------------- |
|
||||
| `yarn pull` | Setup assets required for Revite. |
|
||||
| `yarn dev` | Start the Revolt client in development mode. |
|
||||
| `yarn build` | Build the Revolt client. |
|
||||
| `yarn preview` | Start a local server with the built client. |
|
||||
| `yarn lint` | Run ESLint on the client. |
|
||||
| `yarn fmt` | Run Prettier on the client. |
|
||||
| `yarn typecheck` | Run TypeScript type checking on the client. |
|
||||
| `yarn start` | Start a local sirv server with built client. |
|
||||
| `yarn start:inject` | Inject a given API URL and start server. |
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -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
75
index.html
@@ -1,13 +1,10 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" background="#191919">
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
|
||||
<!--App Title-->
|
||||
<title>Revolt</title>
|
||||
<meta name="apple-mobile-web-app-title" content="Revolt" />
|
||||
|
||||
<!--App Scaling-->
|
||||
<meta
|
||||
name="viewport"
|
||||
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)"
|
||||
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>
|
||||
<body>
|
||||
<body onContextMenu="return false" ontouchstart="">
|
||||
<div id="app"></div>
|
||||
<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>
|
||||
<style>
|
||||
html {
|
||||
background-color: #191919;
|
||||
}
|
||||
</style>
|
||||
</html>
|
||||
|
||||
65
package.json
@@ -1,12 +1,11 @@
|
||||
{
|
||||
"version": "1.0.1",
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"dev": "node scripts/setup_assets.js --check && vite",
|
||||
"pull": "node scripts/setup_assets.js",
|
||||
"build": "rimraf build && node scripts/setup_assets.js --check && vite build",
|
||||
"build:highmem": "NODE_OPTIONS='--max-old-space-size=4096' yarn build",
|
||||
"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}'",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"start": "sirv dist --cors --single --host",
|
||||
@@ -38,30 +37,27 @@
|
||||
{
|
||||
"varsIgnorePattern": "^_"
|
||||
}
|
||||
],
|
||||
"react/jsx-no-literals": "warn"
|
||||
]
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"fs-extra": "^10.0.0",
|
||||
"klaw": "^3.0.0",
|
||||
"react-beautiful-dnd": "^13.1.0",
|
||||
"sirv-cli": "^1.0.14",
|
||||
"vite": "^2.6.14"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/plugin-proposal-decorators": "^7.17.9",
|
||||
"@fontsource/atkinson-hyperlegible": "^4.4.5",
|
||||
"@fontsource/bitter": "^4.5.7",
|
||||
"@fontsource/bree-serif": "^4.4.5",
|
||||
"@fontsource/comic-neue": "^4.4.5",
|
||||
"@fontsource/fira-code": "^4.4.5",
|
||||
"@fontsource/inter": "^4.4.5",
|
||||
"@fontsource/jetbrains-mono": "^4.4.5",
|
||||
"@fontsource/lato": "^4.4.5",
|
||||
"@fontsource/lexend": "^4.5.2",
|
||||
"@fontsource/montserrat": "^4.4.5",
|
||||
"@fontsource/noto-sans": "^4.4.5",
|
||||
"@fontsource/open-sans": "^4.5.2",
|
||||
"@fontsource/opendyslexic": "^4.5.2",
|
||||
"@fontsource/open-sans": "^4.4.5",
|
||||
"@fontsource/poppins": "^4.4.5",
|
||||
"@fontsource/raleway": "^4.4.5",
|
||||
"@fontsource/roboto": "^4.4.5",
|
||||
@@ -71,15 +67,13 @@
|
||||
"@fontsource/ubuntu": "^4.4.5",
|
||||
"@fontsource/ubuntu-mono": "^4.4.5",
|
||||
"@hcaptcha/react-hcaptcha": "^0.3.6",
|
||||
"@insertish/vite-plugin-babel-macros": "^1.0.5",
|
||||
"@preact/preset-vite": "^2.0.0",
|
||||
"@revoltchat/ui": "1.0.46",
|
||||
"@rollup/plugin-replace": "^2.4.2",
|
||||
"@styled-icons/boxicons-logos": "^10.38.0",
|
||||
"@styled-icons/boxicons-regular": "^10.38.0",
|
||||
"@styled-icons/boxicons-solid": "^10.38.0",
|
||||
"@styled-icons/boxicons-logos": "^10.34.0",
|
||||
"@styled-icons/boxicons-regular": "^10.34.0",
|
||||
"@styled-icons/boxicons-solid": "^10.37.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-spoiler": "^1.1.6",
|
||||
"@trivago/prettier-plugin-sort-imports": "^2.0.2",
|
||||
@@ -89,66 +83,63 @@
|
||||
"@types/node": "^15.12.4",
|
||||
"@types/preact-i18n": "^2.3.0",
|
||||
"@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-router-dom": "^5.1.7",
|
||||
"@types/react-scroll": "^1.8.2",
|
||||
"@types/semver": "^7",
|
||||
"@types/react-virtualized-auto-sizer": "^1.0.1",
|
||||
"@types/styled-components": "^5.1.10",
|
||||
"@types/twemoji": "^12.1.1",
|
||||
"@typescript-eslint/eslint-plugin": "^4.27.0",
|
||||
"@typescript-eslint/parser": "^4.27.0",
|
||||
"@vitejs/plugin-legacy": "^1.7.1",
|
||||
"classnames": "^2.3.1",
|
||||
"color-rgba": "^2.4.0",
|
||||
"dayjs": "^1.10.6",
|
||||
"detect-browser": "^5.2.0",
|
||||
"eslint": "^7.28.0",
|
||||
"eslint-config-preact": "^1.1.4",
|
||||
"eslint-plugin-jsdoc": "^39.3.2",
|
||||
"eventemitter3": "^4.0.7",
|
||||
"history": "4",
|
||||
"json-stringify-deterministic": "^1.0.2",
|
||||
"highlight.js": "^11.0.1",
|
||||
"localforage": "^1.9.0",
|
||||
"lodash.defaultsdeep": "^4.6.1",
|
||||
"lodash.isequal": "^4.5.0",
|
||||
"long": "^5.2.0",
|
||||
"markdown-it": "^12.0.6",
|
||||
"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",
|
||||
"mobx": "^6.6.0",
|
||||
"mobx-react-lite": "3.4.0",
|
||||
"mobx": "^6.3.2",
|
||||
"mobx-react-lite": "^3.2.0",
|
||||
"preact": "^10.5.14",
|
||||
"preact-context-menu": "0.4.1",
|
||||
"preact-context-menu": "^0.2.1",
|
||||
"preact-i18n": "^2.4.0-preactx",
|
||||
"prettier": "^2.3.1",
|
||||
"prismjs": "^1.23.0",
|
||||
"qrcode.react": "^3.0.2",
|
||||
"react-beautiful-dnd": "^13.1.0",
|
||||
"react-device-detect": "2.2.2",
|
||||
"react-device-detect": "^1.17.0",
|
||||
"react-helmet": "^6.1.0",
|
||||
"react-hook-form": "6.3.0",
|
||||
"react-overlapping-panels": "1.2.2",
|
||||
"react-redux": "^7.2.4",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"react-scroll": "^1.8.2",
|
||||
"react-virtuoso": "^2.12.0",
|
||||
"revolt.js": "6.0.3",
|
||||
"react-virtualized-auto-sizer": "^1.0.5",
|
||||
"react-virtuoso": "^1.10.4",
|
||||
"redux": "^4.1.0",
|
||||
"revolt-api": "0.5.3-alpha.10",
|
||||
"revolt.js": "^5.1.0-alpha.10",
|
||||
"rimraf": "^3.0.2",
|
||||
"sass": "^1.35.1",
|
||||
"semver": "^7.3.7",
|
||||
"shade-blend-color": "^1.0.0",
|
||||
"stacktrace-js": "^2.0.2",
|
||||
"styled-components": "^5.3.0",
|
||||
"typescript": "^4.4.2",
|
||||
"ulid": "^2.3.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"
|
||||
},
|
||||
"name": "client",
|
||||
"main": "index.js",
|
||||
"repository": "https://github.com/revoltchat/revite.git",
|
||||
"author": "Paul <paulmakles@gmail.com>",
|
||||
"license": "MIT",
|
||||
"packageManager": "yarn@3.2.0"
|
||||
"license": "MIT"
|
||||
}
|
||||
|
||||
@@ -1,22 +1,10 @@
|
||||
[
|
||||
{
|
||||
"relation": ["delegate_permission/common.handle_all_urls"],
|
||||
"target": {
|
||||
"namespace": "android_app",
|
||||
"package_name": "chat.revolt.app.twa",
|
||||
"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"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"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"
|
||||
]
|
||||
}
|
||||
[{
|
||||
"relation": ["delegate_permission/common.handle_all_urls"],
|
||||
"target": {
|
||||
"namespace": "android_app",
|
||||
"package_name": "chat.revolt.app.twa",
|
||||
"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"
|
||||
]
|
||||
}
|
||||
]
|
||||
}]
|
||||
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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(".");
|
||||
}),
|
||||
]) +
|
||||
";",
|
||||
);
|
||||
@@ -1,30 +1,7 @@
|
||||
#!/bin/bash
|
||||
# Build and publish release to production server
|
||||
|
||||
# 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"
|
||||
version=$(cat VERSION)
|
||||
|
||||
docker build -t revoltchat/client:${version} . &&
|
||||
docker tag revoltchat/client:${version} revoltchat/client:latest &&
|
||||
docker push revoltchat/client:${version} &&
|
||||
docker push revoltchat/client:latest
|
||||
|
||||
@@ -3,8 +3,8 @@ const { copy, remove, access } = require("fs-extra");
|
||||
const { exec: cexec } = require("child_process");
|
||||
const { resolve } = require("path");
|
||||
|
||||
let target = process.env.REVOLT_SAAS;
|
||||
let branch = process.env.REVOLT_SAAS_BRANCH;
|
||||
let target = process.env.REVOLT_SASS;
|
||||
let branch = process.env.REVOLT_SASS_BRANCH;
|
||||
let DEFAULT_DIRECTORY = "public/assets_default";
|
||||
let OUT_DIRECTORY = "public/assets";
|
||||
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
type Element =
|
||||
| string
|
||||
| {
|
||||
type: "image";
|
||||
src: string;
|
||||
};
|
||||
|
||||
export interface ChangelogPost {
|
||||
date: Date;
|
||||
title: string;
|
||||
content: Element[];
|
||||
}
|
||||
|
||||
export const changelogEntries: Record<number, ChangelogPost> = {
|
||||
1: {
|
||||
date: new Date("2022-06-12T20:39:16.674Z"),
|
||||
title: "Secure your account with 2FA",
|
||||
content: [
|
||||
"Two-factor authentication is now available to all users, you can now head over to settings to enable recovery codes and an authenticator app.",
|
||||
{
|
||||
type: "image",
|
||||
src: "https://autumn.revolt.chat/attachments/E21kwmuJGcASgkVLiSIW0wV3ggcaOWjW0TQF7cdFNY/image.png",
|
||||
},
|
||||
"Once enabled, you will be prompted on login.",
|
||||
{
|
||||
type: "image",
|
||||
src: "https://autumn.revolt.chat/attachments/LWRYoKR2tE1ggW_Lzm547P1pnrkNgmBaoCAfWvHE74/image.png",
|
||||
},
|
||||
"Other authentication methods coming later, stay tuned!",
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const changelogEntryArray = Object.keys(changelogEntries).map(
|
||||
(index) => changelogEntries[index as unknown as number],
|
||||
);
|
||||
|
||||
export const latestChangelog = changelogEntryArray.length;
|
||||
@@ -673,7 +673,6 @@ export const emojiDictionary = {
|
||||
mandarin: "🍊",
|
||||
lemon: "🍋",
|
||||
banana: "🍌",
|
||||
nanner: "🍌",
|
||||
pineapple: "🍍",
|
||||
mango: "🥭",
|
||||
apple: "🍎",
|
||||
@@ -877,7 +876,6 @@ export const emojiDictionary = {
|
||||
train: "🚋",
|
||||
bus: "🚌",
|
||||
oncoming_bus: "🚍",
|
||||
trolley: "🚎",
|
||||
trolleybus: "🚎",
|
||||
minibus: "🚐",
|
||||
ambulance: "🚑",
|
||||
@@ -1849,108 +1847,4 @@ export const emojiDictionary = {
|
||||
england: "🏴",
|
||||
scotland: "🏴",
|
||||
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",
|
||||
},
|
||||
};
|
||||
|
||||
29
src/assets/sounds/Audio.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
The following folders should not be added to or modified:
|
||||
|
||||
- `common`
|
||||
- `markdown`
|
||||
- `native`
|
||||
- `ui`
|
||||
|
||||
The following are part-legacy, will remain in place and will be rewritten to some degree still:
|
||||
|
||||
- `navigation`
|
||||
|
||||
The following are mostly good to go:
|
||||
|
||||
- `settings`
|
||||
@@ -1,15 +1,17 @@
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import { Channel } from "revolt.js";
|
||||
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 { useState } from "preact/hooks";
|
||||
|
||||
import { Button, Checkbox } 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 { Children } from "../../types/Preact";
|
||||
|
||||
const Base = styled.div`
|
||||
display: flex;
|
||||
@@ -47,7 +49,9 @@ type Props = {
|
||||
|
||||
export default observer((props: Props) => {
|
||||
const history = useHistory();
|
||||
const layout = useApplicationState().layout;
|
||||
const [consent, setConsent] = useState(
|
||||
getState().sectionToggle["nsfw"] ?? false,
|
||||
);
|
||||
const [ageGate, setAgeGate] = useState(false);
|
||||
|
||||
if (ageGate || !props.gated) {
|
||||
@@ -77,19 +81,26 @@ export default observer((props: Props) => {
|
||||
</span>
|
||||
|
||||
<Checkbox
|
||||
title={<Text id="app.main.channel.nsfw.confirm" />}
|
||||
value={layout.getSectionState(SECTION_NSFW, false)}
|
||||
onChange={() => layout.toggleSectionState(SECTION_NSFW, false)}
|
||||
/>
|
||||
checked={consent}
|
||||
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" />
|
||||
</Checkbox>
|
||||
<div className="actions">
|
||||
<Button palette="secondary" onClick={() => history.goBack()}>
|
||||
<Button contrast onClick={() => history.goBack()}>
|
||||
<Text id="app.special.modals.actions.back" />
|
||||
</Button>
|
||||
<Button
|
||||
palette="secondary"
|
||||
onClick={() =>
|
||||
layout.getSectionState(SECTION_NSFW) && setAgeGate(true)
|
||||
}>
|
||||
<Button contrast onClick={() => consent && setAgeGate(true)}>
|
||||
<Text id={`app.main.channel.nsfw.${props.type}.confirm`} />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Channel, User } from "revolt.js";
|
||||
import styled, { css } from "styled-components/macro";
|
||||
import { Channel } from "revolt.js/dist/maps/Channels";
|
||||
import { User } from "revolt.js/dist/maps/Users";
|
||||
import styled, { css } from "styled-components";
|
||||
|
||||
import { StateUpdater, useState } from "preact/hooks";
|
||||
|
||||
@@ -13,19 +14,19 @@ import UserIcon from "./user/UserIcon";
|
||||
export type AutoCompleteState =
|
||||
| { type: "none" }
|
||||
| ({ selected: number; within: boolean } & (
|
||||
| {
|
||||
type: "emoji";
|
||||
matches: string[];
|
||||
}
|
||||
| {
|
||||
type: "user";
|
||||
matches: User[];
|
||||
}
|
||||
| {
|
||||
type: "channel";
|
||||
matches: Channel[];
|
||||
}
|
||||
));
|
||||
| {
|
||||
type: "emoji";
|
||||
matches: string[];
|
||||
}
|
||||
| {
|
||||
type: "user";
|
||||
matches: User[];
|
||||
}
|
||||
| {
|
||||
type: "channel";
|
||||
matches: Channel[];
|
||||
}
|
||||
));
|
||||
|
||||
export type SearchClues = {
|
||||
users?: { type: "channel"; id: string } | { type: "all" };
|
||||
@@ -78,17 +79,17 @@ export function useAutoComplete(
|
||||
|
||||
if (current === ":" || current === "@" || current === "#") {
|
||||
const search = content.slice(j + 1, content.length);
|
||||
const minLen = current === ":" ? 2 : 1;
|
||||
const minLen = current === ":" ? 2 : 1
|
||||
|
||||
if (search.length >= minLen) {
|
||||
return [
|
||||
current === "#"
|
||||
? "channel"
|
||||
: current === ":"
|
||||
? "emoji"
|
||||
: "user",
|
||||
? "emoji"
|
||||
: "user",
|
||||
search.toLowerCase(),
|
||||
current === ":" ? j + 1 : j,
|
||||
j + 1,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -166,8 +167,8 @@ export function useAutoComplete(
|
||||
const matches = (
|
||||
search.length > 0
|
||||
? users.filter((user) =>
|
||||
user.username.toLowerCase().match(regex),
|
||||
)
|
||||
user.username.toLowerCase().match(regex),
|
||||
)
|
||||
: users
|
||||
)
|
||||
.splice(0, 5)
|
||||
@@ -198,8 +199,8 @@ export function useAutoComplete(
|
||||
const matches = (
|
||||
search.length > 0
|
||||
? channels.filter((channel) =>
|
||||
channel.name!.toLowerCase().match(regex),
|
||||
)
|
||||
channel.name!.toLowerCase().match(regex),
|
||||
)
|
||||
: channels
|
||||
)
|
||||
.splice(0, 5)
|
||||
@@ -242,7 +243,7 @@ export function useAutoComplete(
|
||||
);
|
||||
} else if (state.type === "user") {
|
||||
content.splice(
|
||||
index,
|
||||
index - 1,
|
||||
search.length + 1,
|
||||
"<@",
|
||||
state.matches[state.selected]._id,
|
||||
@@ -250,7 +251,7 @@ export function useAutoComplete(
|
||||
);
|
||||
} else {
|
||||
content.splice(
|
||||
index,
|
||||
index - 1,
|
||||
search.length + 1,
|
||||
"<#",
|
||||
state.matches[state.selected]._id,
|
||||
@@ -416,7 +417,7 @@ export default function AutoComplete({
|
||||
<Emoji
|
||||
emoji={
|
||||
(emojiDictionary as Record<string, string>)[
|
||||
match
|
||||
match
|
||||
]
|
||||
}
|
||||
size={20}
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import { Hash, VolumeFull } from "@styled-icons/boxicons-regular";
|
||||
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 { AppContext } from "../../context/revoltjs/RevoltClient";
|
||||
|
||||
import fallback from "./assets/group.png";
|
||||
|
||||
import { ImageIconBase, IconBaseProps } from "./IconBase";
|
||||
import fallback from "./assets/group.png";
|
||||
|
||||
interface Props extends IconBaseProps<Channel> {
|
||||
isServerChannel?: boolean;
|
||||
@@ -33,7 +32,7 @@ export default observer(
|
||||
...imgProps
|
||||
} = props;
|
||||
const iconURL = client.generateFileURL(
|
||||
target?.icon ?? attachment ?? undefined,
|
||||
target?.icon ?? attachment,
|
||||
{ max_side: 256 },
|
||||
animate,
|
||||
);
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { ChevronDown } from "@styled-icons/boxicons-regular";
|
||||
|
||||
import { Details } from "@revoltchat/ui";
|
||||
import { State, store } from "../../redux";
|
||||
import { Action } from "../../redux/reducers";
|
||||
|
||||
import { useApplicationState } from "../../mobx/State";
|
||||
import Details from "../ui/Details";
|
||||
|
||||
import { Children } from "../../types/Preact";
|
||||
|
||||
interface Props {
|
||||
id: string;
|
||||
@@ -22,17 +25,30 @@ export default function CollapsibleSection({
|
||||
children,
|
||||
...detailsProps
|
||||
}: 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 (
|
||||
<Details
|
||||
open={layout.getSectionState(id, defaultValue)}
|
||||
onToggle={(e) =>
|
||||
layout.setSectionState(id, e.currentTarget.open, defaultValue)
|
||||
}
|
||||
open={state.sectionToggle[id] ?? defaultValue}
|
||||
onToggle={(e) => setState(e.currentTarget.open)}
|
||||
{...detailsProps}>
|
||||
<summary>
|
||||
<div className="padding">
|
||||
<div class="padding">
|
||||
<ChevronDown size={20} />
|
||||
{summary}
|
||||
</div>
|
||||
|
||||
@@ -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;
|
||||
|
||||
export function setGlobalEmojiPack(pack: EmojiPack) {
|
||||
export function setEmojiPack(pack: EmojiPacks) {
|
||||
EMOJI_PACK = pack;
|
||||
}
|
||||
|
||||
@@ -41,12 +41,6 @@ function toCodePoint(rune: string) {
|
||||
}
|
||||
|
||||
function parseEmoji(emoji: string) {
|
||||
if (emoji.startsWith("custom:")) {
|
||||
return `https://dl.insrt.uk/projects/revolt/emotes/${emoji.substring(
|
||||
7,
|
||||
)}`;
|
||||
}
|
||||
|
||||
const codepoint = toCodePoint(emoji);
|
||||
return `https://static.revolt.chat/emoji/${EMOJI_PACK}/${codepoint}.svg?rev=${REVISION}`;
|
||||
}
|
||||
|
||||
@@ -1,19 +1,14 @@
|
||||
import { API } from "revolt.js";
|
||||
import { Nullable } from "revolt.js";
|
||||
import styled, { css } from "styled-components/macro";
|
||||
|
||||
import { Ref } from "preact";
|
||||
import { Attachment } from "revolt-api/types/Autumn";
|
||||
import styled, { css } from "styled-components";
|
||||
|
||||
export interface IconBaseProps<T> {
|
||||
target?: T;
|
||||
url?: string;
|
||||
attachment?: Nullable<API.File>;
|
||||
attachment?: Attachment;
|
||||
|
||||
size: number;
|
||||
hover?: boolean;
|
||||
animate?: boolean;
|
||||
|
||||
innerRef?: Ref<any>;
|
||||
}
|
||||
|
||||
interface IconModifiers {
|
||||
@@ -26,7 +21,7 @@ interface IconModifiers {
|
||||
|
||||
export default styled.svg<IconModifiers>`
|
||||
flex-shrink: 0;
|
||||
cursor: pointer;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
@@ -1,21 +1,23 @@
|
||||
import { ComboBox } from "@revoltchat/ui";
|
||||
import { dispatch } from "../../redux";
|
||||
import { connectState } from "../../redux/connector";
|
||||
|
||||
import { useApplicationState } from "../../mobx/State";
|
||||
import { Language, Languages } from "../../context/Locale";
|
||||
|
||||
import { Language, Languages } from "../../../external/lang/Languages";
|
||||
import ComboBox from "../ui/ComboBox";
|
||||
|
||||
/**
|
||||
* 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;
|
||||
type Props = {
|
||||
locale: string;
|
||||
};
|
||||
|
||||
export function LocaleSelector(props: Props) {
|
||||
return (
|
||||
<ComboBox
|
||||
value={locale.getLanguage()}
|
||||
value={props.locale}
|
||||
onChange={(e) =>
|
||||
locale.setLanguage(e.currentTarget.value as Language)
|
||||
dispatch({
|
||||
type: "SET_LOCALE",
|
||||
locale: e.currentTarget.value as Language,
|
||||
})
|
||||
}>
|
||||
{Object.keys(Languages).map((x) => {
|
||||
const l = Languages[x as keyof typeof Languages];
|
||||
@@ -28,3 +30,9 @@ export default function LocaleSelector() {
|
||||
</ComboBox>
|
||||
);
|
||||
}
|
||||
|
||||
export default connectState(LocaleSelector, (state) => {
|
||||
return {
|
||||
locale: state.locale,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -2,134 +2,86 @@ import { Check } from "@styled-icons/boxicons-regular";
|
||||
import { Cog } from "@styled-icons/boxicons-solid";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { Link } from "react-router-dom";
|
||||
import { Server } from "revolt.js";
|
||||
import styled, { css } from "styled-components/macro";
|
||||
import { ServerPermission } from "revolt.js/dist/api/permissions";
|
||||
import { Server } from "revolt.js/dist/maps/Servers";
|
||||
import styled from "styled-components";
|
||||
|
||||
import { Text } from "preact-i18n";
|
||||
|
||||
import { IconButton } from "@revoltchat/ui";
|
||||
import Header from "../ui/Header";
|
||||
import IconButton from "../ui/IconButton";
|
||||
|
||||
import Tooltip from "./Tooltip";
|
||||
|
||||
interface Props {
|
||||
server: Server;
|
||||
background?: boolean;
|
||||
}
|
||||
|
||||
const ServerBanner = styled.div<Omit<Props, "server">>`
|
||||
flex-shrink: 0;
|
||||
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;
|
||||
}
|
||||
}
|
||||
const ServerName = styled.div`
|
||||
flex-grow: 1;
|
||||
`;
|
||||
|
||||
export default observer(({ server }: Props) => {
|
||||
const bannerURL = server.generateBannerURL({ width: 480 });
|
||||
|
||||
return (
|
||||
<ServerBanner
|
||||
<Header
|
||||
borders
|
||||
placement="secondary"
|
||||
background={typeof bannerURL !== "undefined"}
|
||||
style={{
|
||||
backgroundImage: bannerURL ? `url('${bannerURL}')` : undefined,
|
||||
background: bannerURL ? `url('${bannerURL}')` : undefined,
|
||||
}}>
|
||||
<div className="container">
|
||||
{server.flags && server.flags & 1 ? (
|
||||
<Tooltip
|
||||
content={
|
||||
<Text id="app.special.server-badges.official" />
|
||||
}
|
||||
placement={"bottom-start"}>
|
||||
<svg width="20" height="20">
|
||||
<image
|
||||
xlinkHref="/assets/badges/verified.svg"
|
||||
height="20"
|
||||
width="20"
|
||||
/>
|
||||
<image
|
||||
xlinkHref="/assets/badges/revolt_r.svg"
|
||||
height="15"
|
||||
width="15"
|
||||
x="2"
|
||||
y="3"
|
||||
style={
|
||||
"justify-content: center; align-items: center; filter: brightness(0);"
|
||||
}
|
||||
/>
|
||||
</svg>
|
||||
</Tooltip>
|
||||
) : undefined}
|
||||
{server.flags && server.flags & 2 ? (
|
||||
<Tooltip
|
||||
content={
|
||||
<Text id="app.special.server-badges.verified" />
|
||||
}
|
||||
placement={"bottom-start"}>
|
||||
<svg width="20" height="20">
|
||||
<image
|
||||
xlinkHref="/assets/badges/verified.svg"
|
||||
height="20"
|
||||
width="20"
|
||||
/>
|
||||
<foreignObject x="2" y="2" width="15" height="15">
|
||||
<Check
|
||||
size={15}
|
||||
color="black"
|
||||
strokeWidth={8}
|
||||
/>
|
||||
</foreignObject>
|
||||
</svg>
|
||||
</Tooltip>
|
||||
) : undefined}
|
||||
<div className="title">{server.name}</div>
|
||||
{server.havePermission("ManageServer") && (
|
||||
{server.flags && server.flags & 1 ? (
|
||||
<Tooltip
|
||||
content={<Text id="app.special.server-badges.official" />}
|
||||
placement={"bottom-start"}>
|
||||
<svg width="20" height="20">
|
||||
<image
|
||||
xlinkHref="/assets/badges/verified.svg"
|
||||
height="20"
|
||||
width="20"
|
||||
/>
|
||||
<image
|
||||
xlinkHref="/assets/badges/revolt_r.svg"
|
||||
height="15"
|
||||
width="15"
|
||||
x="2"
|
||||
y="3"
|
||||
style={
|
||||
"justify-content: center; align-items: center; filter: brightness(0);"
|
||||
}
|
||||
/>
|
||||
</svg>
|
||||
</Tooltip>
|
||||
) : undefined}
|
||||
{server.flags && server.flags & 2 ? (
|
||||
<Tooltip
|
||||
content={<Text id="app.special.server-badges.verified" />}
|
||||
placement={"bottom-start"}>
|
||||
<svg width="20" height="20">
|
||||
<image
|
||||
xlinkHref="/assets/badges/verified.svg"
|
||||
height="20"
|
||||
width="20"
|
||||
/>
|
||||
<foreignObject x="2" y="2" width="15" height="15">
|
||||
<Check size={15} color="black" strokeWidth={8} />
|
||||
</foreignObject>
|
||||
</svg>
|
||||
</Tooltip>
|
||||
) : undefined}
|
||||
|
||||
<ServerName>{server.name}</ServerName>
|
||||
{(server.permission & ServerPermission.ManageServer) > 0 && (
|
||||
<div className="actions">
|
||||
<Link to={`/server/${server._id}/settings`}>
|
||||
<IconButton>
|
||||
<Cog size={20} />
|
||||
<Cog size={24} />
|
||||
</IconButton>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</ServerBanner>
|
||||
</div>
|
||||
)}
|
||||
</Header>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { Server } from "revolt.js";
|
||||
import styled from "styled-components/macro";
|
||||
import { Server } from "revolt.js/dist/maps/Servers";
|
||||
import styled from "styled-components";
|
||||
|
||||
import { useContext } from "preact/hooks";
|
||||
|
||||
@@ -13,13 +13,10 @@ interface Props extends IconBaseProps<Server> {
|
||||
}
|
||||
|
||||
const ServerText = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
display: grid;
|
||||
padding: 0.2em;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
overflow: hidden;
|
||||
place-items: center;
|
||||
color: var(--foreground);
|
||||
background: var(--primary-background);
|
||||
border-radius: var(--border-radius-half);
|
||||
@@ -39,7 +36,7 @@ export default observer(
|
||||
const { target, attachment, size, animate, server_name, ...imgProps } =
|
||||
props;
|
||||
const iconURL = client.generateFileURL(
|
||||
target?.icon ?? attachment ?? undefined,
|
||||
target?.icon ?? attachment,
|
||||
{ max_side: 256 },
|
||||
animate,
|
||||
);
|
||||
@@ -52,9 +49,7 @@ export default observer(
|
||||
{name
|
||||
.split(" ")
|
||||
.map((x) => x[0])
|
||||
.filter((x) => typeof x !== "undefined")
|
||||
.join("")
|
||||
.substring(0, 3)}
|
||||
.filter((x) => typeof x !== "undefined")}
|
||||
</ServerText>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import Tippy, { TippyProps } from "@tippyjs/react";
|
||||
import styled from "styled-components/macro";
|
||||
import styled from "styled-components";
|
||||
|
||||
import { Text } from "preact-i18n";
|
||||
|
||||
import { Children } from "../../types/Preact";
|
||||
|
||||
type Props = Omit<TippyProps, "children"> & {
|
||||
children: Children;
|
||||
content: Children;
|
||||
@@ -12,7 +14,7 @@ export default function Tooltip(props: Props) {
|
||||
const { children, content, ...tippyProps } = props;
|
||||
|
||||
return (
|
||||
<Tippy content={content} animation="shift-away" {...tippyProps}>
|
||||
<Tippy content={content} {...tippyProps}>
|
||||
{/*
|
||||
// @ts-expect-error Type mis-match. */}
|
||||
<div style={`display: flex;`}>{children}</div>
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
/* eslint-disable react-hooks/rules-of-hooks */
|
||||
import { Download, CloudDownload } from "@styled-icons/boxicons-regular";
|
||||
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
|
||||
import { IconButton } from "@revoltchat/ui";
|
||||
import { useContext, useEffect, useState } from "preact/hooks";
|
||||
|
||||
import { internalSubscribe } from "../../lib/eventEmitter";
|
||||
|
||||
import { useApplicationState } from "../../mobx/State";
|
||||
import { ThemeContext } from "../../context/Theme";
|
||||
|
||||
import { updateSW } from "../../updateWorker";
|
||||
import IconButton from "../ui/IconButton";
|
||||
|
||||
import { updateSW } from "../../main";
|
||||
import Tooltip from "./Tooltip";
|
||||
|
||||
let pendingUpdate = false;
|
||||
@@ -27,30 +27,27 @@ export default function UpdateIndicator({ style }: Props) {
|
||||
});
|
||||
|
||||
if (!pending) return null;
|
||||
const theme = useApplicationState().settings.theme;
|
||||
const theme = useContext(ThemeContext);
|
||||
|
||||
if (style === "titlebar") {
|
||||
return (
|
||||
<div className="actions">
|
||||
<div class="actions">
|
||||
<Tooltip
|
||||
content="A new update is available!"
|
||||
placement="bottom">
|
||||
<div onClick={() => updateSW(true)}>
|
||||
<CloudDownload
|
||||
size={22}
|
||||
color={theme.getVariable("success")}
|
||||
/>
|
||||
<CloudDownload size={22} color={theme.success} />
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (window.isNative && window.native.getConfig().frame) return null;
|
||||
if (window.isNative) return null;
|
||||
|
||||
return (
|
||||
<IconButton onClick={() => updateSW(true)}>
|
||||
<Download size={22} color={theme.getVariable("success")} />
|
||||
<Download size={22} color={theme.success} />
|
||||
</IconButton>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,21 +1,20 @@
|
||||
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 { useEffect, useState } from "preact/hooks";
|
||||
|
||||
import { Category } from "@revoltchat/ui";
|
||||
import { useState } from "preact/hooks";
|
||||
|
||||
import { internalEmit } from "../../../lib/eventEmitter";
|
||||
import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice";
|
||||
|
||||
import { QueuedMessage } from "../../../mobx/stores/MessageQueue";
|
||||
import { QueuedMessage } from "../../../redux/reducers/queue";
|
||||
|
||||
import { I18nError } from "../../../context/Locale";
|
||||
import { useIntermediate } from "../../../context/intermediate/Intermediate";
|
||||
import { useClient } from "../../../context/revoltjs/RevoltClient";
|
||||
|
||||
import Overline from "../../ui/Overline";
|
||||
|
||||
import { Children } from "../../../types/Preact";
|
||||
import Markdown from "../../markdown/Markdown";
|
||||
import UserIcon from "../user/UserIcon";
|
||||
import { Username } from "../user/UserShort";
|
||||
@@ -26,7 +25,6 @@ import MessageBase, {
|
||||
} from "./MessageBase";
|
||||
import Attachment from "./attachments/Attachment";
|
||||
import { MessageReply } from "./attachments/MessageReply";
|
||||
import { MessageOverlayBar } from "./bars/MessageOverlayBar";
|
||||
import Embed from "./embed/Embed";
|
||||
import InviteList from "./embed/EmbedInvite";
|
||||
|
||||
@@ -57,16 +55,18 @@ const Message = observer(
|
||||
|
||||
const { openScreen } = useIntermediate();
|
||||
|
||||
const content = message.content;
|
||||
const content = message.content as string;
|
||||
const head =
|
||||
preferHead || (message.reply_ids && message.reply_ids.length > 0);
|
||||
|
||||
// ! TODO: tell fatal to make this type generic
|
||||
// bree: Fatal please...
|
||||
const userContext = attachContext
|
||||
? useTriggerEvents("Menu", {
|
||||
? (attachContextMenu("Menu", {
|
||||
user: message.author_id,
|
||||
contextualChannel: message.channel_id,
|
||||
// eslint-disable-next-line
|
||||
})
|
||||
}) as any)
|
||||
: undefined;
|
||||
|
||||
const openProfile = () =>
|
||||
@@ -86,8 +86,7 @@ const Message = observer(
|
||||
};
|
||||
|
||||
// ! FIXME(?): animate on hover
|
||||
const [mouseHovering, setAnimate] = useState(false);
|
||||
useEffect(() => setAnimate(false), [replacement]);
|
||||
const [animate, setAnimate] = useState(false);
|
||||
|
||||
return (
|
||||
<div id={message._id}>
|
||||
@@ -97,7 +96,7 @@ const Message = observer(
|
||||
key={message_id}
|
||||
index={index}
|
||||
id={message_id}
|
||||
channel={message.channel}
|
||||
channel={message.channel!}
|
||||
parent_mentions={message.mention_ids ?? []}
|
||||
/>
|
||||
))}
|
||||
@@ -117,25 +116,26 @@ const Message = observer(
|
||||
sending={typeof queued !== "undefined"}
|
||||
mention={message.mention_ids?.includes(client.user!._id)}
|
||||
failed={typeof queued?.error !== "undefined"}
|
||||
{...(attachContext
|
||||
? useTriggerEvents("Menu", {
|
||||
message,
|
||||
contextualChannel: message.channel_id,
|
||||
queued,
|
||||
})
|
||||
: undefined)}
|
||||
onContextMenu={
|
||||
attachContext
|
||||
? attachContextMenu("Menu", {
|
||||
message,
|
||||
contextualChannel: message.channel_id,
|
||||
queued,
|
||||
})
|
||||
: undefined
|
||||
}
|
||||
onMouseEnter={() => setAnimate(true)}
|
||||
onMouseLeave={() => setAnimate(false)}>
|
||||
<MessageInfo click={typeof head !== "undefined"}>
|
||||
<MessageInfo>
|
||||
{head ? (
|
||||
<UserIcon
|
||||
className="avatar"
|
||||
url={message.generateMasqAvatarURL()}
|
||||
target={user}
|
||||
size={36}
|
||||
onContextMenu={userContext}
|
||||
onClick={handleUserClick}
|
||||
animate={mouseHovering}
|
||||
{...(userContext as any)}
|
||||
animate={animate}
|
||||
showServerIdentity
|
||||
/>
|
||||
) : (
|
||||
@@ -150,8 +150,8 @@ const Message = observer(
|
||||
className="author"
|
||||
showServerIdentity
|
||||
onClick={handleUserClick}
|
||||
onContextMenu={userContext}
|
||||
masquerade={message.masquerade!}
|
||||
{...userContext}
|
||||
/>
|
||||
<MessageDetail
|
||||
message={message}
|
||||
@@ -162,31 +162,18 @@ const Message = observer(
|
||||
{replacement ?? <Markdown content={content} />}
|
||||
{!queued && <InviteList message={message} />}
|
||||
{queued?.error && (
|
||||
<Category>
|
||||
<I18nError error={queued.error} />
|
||||
</Category>
|
||||
<Overline type="error" error={queued.error} />
|
||||
)}
|
||||
{message.attachments?.map((attachment, index) => (
|
||||
<Attachment
|
||||
key={index}
|
||||
attachment={attachment}
|
||||
hasContent={
|
||||
index > 0 ||
|
||||
(content ? content.length > 0 : false)
|
||||
}
|
||||
hasContent={index > 0 || content.length > 0}
|
||||
/>
|
||||
))}
|
||||
{message.embeds?.map((embed, index) => (
|
||||
<Embed key={index} embed={embed} />
|
||||
))}
|
||||
{mouseHovering &&
|
||||
!replacement &&
|
||||
!isTouchscreenDevice && (
|
||||
<MessageOverlayBar
|
||||
message={message}
|
||||
queued={queued}
|
||||
/>
|
||||
)}
|
||||
</MessageContent>
|
||||
</MessageBase>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { Message } from "revolt.js";
|
||||
import styled, { css, keyframes } from "styled-components/macro";
|
||||
import { Message } from "revolt.js/dist/maps/Messages";
|
||||
import styled, { css, keyframes } from "styled-components";
|
||||
import { decodeTime } from "ulid";
|
||||
|
||||
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;
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
@@ -142,14 +142,6 @@ export const MessageInfo = styled.div<{ click: boolean }>`
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
|
||||
.avatar {
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
&:active {
|
||||
transform: translateY(1px);
|
||||
}
|
||||
}
|
||||
|
||||
.copyBracket {
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
@@ -160,6 +152,15 @@ export const MessageInfo = styled.div<{ click: boolean }>`
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
svg {
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
|
||||
&:active {
|
||||
transform: translateY(1px);
|
||||
}
|
||||
}
|
||||
|
||||
time {
|
||||
opacity: 0;
|
||||
}
|
||||
@@ -191,19 +192,9 @@ export const MessageInfo = styled.div<{ click: boolean }>`
|
||||
margin-right: 0.5em;
|
||||
color: var(--tertiary-foreground);
|
||||
}
|
||||
|
||||
/*${(props) =>
|
||||
props.click &&
|
||||
css`
|
||||
cursor: pointer;
|
||||
`}*/
|
||||
`;
|
||||
|
||||
export const MessageContent = styled.div`
|
||||
// Position relatively so we can put
|
||||
// the overlay in the right place.
|
||||
position: relative;
|
||||
|
||||
min-width: 0;
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
import { Send, ShieldX } from "@styled-icons/boxicons-solid";
|
||||
import Axios, { CancelTokenSource } from "axios";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { Channel } from "revolt.js";
|
||||
import styled, { css } from "styled-components/macro";
|
||||
import { ChannelPermission } from "revolt.js/dist/api/permissions";
|
||||
import { Channel } from "revolt.js/dist/maps/Channels";
|
||||
import styled, { css } from "styled-components";
|
||||
import { ulid } from "ulid";
|
||||
|
||||
import { Text } from "preact-i18n";
|
||||
import { useCallback, useContext, useEffect, useState } from "preact/hooks";
|
||||
|
||||
import { IconButton } from "@revoltchat/ui";
|
||||
|
||||
import TextAreaAutoSize from "../../../lib/TextAreaAutoSize";
|
||||
import { debounce } from "../../../lib/debounce";
|
||||
import { defer } from "../../../lib/defer";
|
||||
@@ -21,10 +20,11 @@ import {
|
||||
SMOOTH_SCROLL_ON_RECEIVE,
|
||||
} from "../../../lib/renderer/Singleton";
|
||||
|
||||
import { useApplicationState } from "../../../mobx/State";
|
||||
import { Reply } from "../../../mobx/stores/MessageQueue";
|
||||
import { dispatch, getState } from "../../../redux";
|
||||
import { Reply } from "../../../redux/reducers/queue";
|
||||
|
||||
import { modalController } from "../../../context/modals";
|
||||
import { SoundContext } from "../../../context/Settings";
|
||||
import { useIntermediate } from "../../../context/intermediate/Intermediate";
|
||||
import {
|
||||
FileUploader,
|
||||
grabFiles,
|
||||
@@ -33,6 +33,8 @@ import {
|
||||
import { AppContext } from "../../../context/revoltjs/RevoltClient";
|
||||
import { takeError } from "../../../context/revoltjs/util";
|
||||
|
||||
import IconButton from "../../ui/IconButton";
|
||||
|
||||
import AutoComplete, { useAutoComplete } from "../AutoComplete";
|
||||
import { PermissionTooltip } from "../Tooltip";
|
||||
import FilePreview from "./bars/FilePreview";
|
||||
@@ -55,7 +57,6 @@ export type UploadState =
|
||||
| { type: "failed"; files: File[]; error: string };
|
||||
|
||||
const Base = styled.div`
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
background: var(--message-box);
|
||||
@@ -78,15 +79,9 @@ const Blocked = styled.div`
|
||||
user-select: none;
|
||||
font-size: var(--text-size);
|
||||
color: var(--tertiary-foreground);
|
||||
flex-grow: 1;
|
||||
cursor: not-allowed;
|
||||
|
||||
.text {
|
||||
padding: var(--message-box-padding);
|
||||
}
|
||||
|
||||
> div > div {
|
||||
cursor: default;
|
||||
padding: 14px 14px 14px 0;
|
||||
}
|
||||
|
||||
svg {
|
||||
@@ -95,17 +90,13 @@ const Blocked = styled.div`
|
||||
`;
|
||||
|
||||
const Action = styled.div`
|
||||
> a {
|
||||
display: flex;
|
||||
place-items: center;
|
||||
|
||||
> div {
|
||||
height: 48px;
|
||||
width: 48px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
/*padding: 14px 0 14px 14px;*/
|
||||
}
|
||||
|
||||
.mobile {
|
||||
width: 62px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
${() =>
|
||||
@@ -117,43 +108,28 @@ const Action = styled.div`
|
||||
`}
|
||||
`;
|
||||
|
||||
const FileAction = styled.div`
|
||||
> a {
|
||||
height: 48px;
|
||||
width: 62px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
`;
|
||||
|
||||
const ThisCodeWillBeReplacedAnywaysSoIMightAsWellJustDoItThisWay__Padding = styled.div`
|
||||
width: 16px;
|
||||
`;
|
||||
|
||||
// For sed replacement
|
||||
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
|
||||
export const CAN_UPLOAD_AT_ONCE = 5;
|
||||
export const CAN_UPLOAD_AT_ONCE = 4;
|
||||
|
||||
export default observer(({ channel }: Props) => {
|
||||
const state = useApplicationState();
|
||||
const [draft, setDraft] = useState(getState().drafts[channel._id] ?? "");
|
||||
|
||||
const [uploadState, setUploadState] = useState<UploadState>({
|
||||
type: "none",
|
||||
});
|
||||
const [typing, setTyping] = useState<boolean | number>(false);
|
||||
const [replies, setReplies] = useState<Reply[]>([]);
|
||||
const playSound = useContext(SoundContext);
|
||||
const { openScreen } = useIntermediate();
|
||||
const client = useContext(AppContext);
|
||||
const translate = useTranslation();
|
||||
|
||||
const renderer = getRenderer(channel);
|
||||
|
||||
if (!channel.havePermission("SendMessage")) {
|
||||
if (!(channel.permission & ChannelPermission.SendMessage)) {
|
||||
return (
|
||||
<Base>
|
||||
<Blocked>
|
||||
@@ -172,18 +148,27 @@ export default observer(({ channel }: Props) => {
|
||||
);
|
||||
}
|
||||
|
||||
// Push message content to draft.
|
||||
const setMessage = useCallback(
|
||||
(content?: string) => state.draft.set(channel._id, content),
|
||||
[state.draft, channel._id],
|
||||
(content?: string) => {
|
||||
setDraft(content ?? "");
|
||||
|
||||
if (content) {
|
||||
dispatch({
|
||||
type: "SET_DRAFT",
|
||||
channel: channel._id,
|
||||
content,
|
||||
});
|
||||
} else {
|
||||
dispatch({
|
||||
type: "CLEAR_DRAFT",
|
||||
channel: channel._id,
|
||||
});
|
||||
}
|
||||
},
|
||||
[channel._id],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
/**
|
||||
*
|
||||
* @param content
|
||||
* @param action
|
||||
*/
|
||||
function append(content: string, action: "quote" | "mention") {
|
||||
const text =
|
||||
action === "quote"
|
||||
@@ -193,10 +178,10 @@ export default observer(({ channel }: Props) => {
|
||||
.join("\n")}\n\n`
|
||||
: `${content} `;
|
||||
|
||||
if (!state.draft.has(channel._id)) {
|
||||
if (!draft || draft.length === 0) {
|
||||
setMessage(text);
|
||||
} else {
|
||||
setMessage(`${state.draft.get(channel._id)}\n${text}`);
|
||||
setMessage(`${draft}\n${text}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -205,20 +190,16 @@ export default observer(({ channel }: Props) => {
|
||||
"append",
|
||||
append as (...args: unknown[]) => void,
|
||||
);
|
||||
}, [state.draft, channel._id, setMessage]);
|
||||
}, [draft, setMessage]);
|
||||
|
||||
/**
|
||||
* Trigger send message.
|
||||
*/
|
||||
async function send() {
|
||||
if (uploadState.type === "uploading" || uploadState.type === "sending")
|
||||
return;
|
||||
|
||||
const content = state.draft.get(channel._id)?.trim() ?? "";
|
||||
const content = draft?.trim() ?? "";
|
||||
if (uploadState.type === "attached") return sendFile(content);
|
||||
if (content.length === 0) return;
|
||||
|
||||
internalEmit("NewMessages", "hide");
|
||||
stopTyping();
|
||||
setMessage();
|
||||
setReplies([]);
|
||||
@@ -234,7 +215,7 @@ export default observer(({ channel }: Props) => {
|
||||
);
|
||||
renderer.messages.reverse();
|
||||
|
||||
if (msg?.content) {
|
||||
if (msg) {
|
||||
// eslint-disable-next-line prefer-const
|
||||
let [_, toReplace, newText, flags] = content.split(/\//);
|
||||
|
||||
@@ -266,15 +247,20 @@ export default observer(({ channel }: Props) => {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
state.settings.sounds.playSound("outbound");
|
||||
playSound("outbound");
|
||||
|
||||
state.queue.add(nonce, channel._id, {
|
||||
_id: nonce,
|
||||
dispatch({
|
||||
type: "QUEUE_ADD",
|
||||
nonce,
|
||||
channel: channel._id,
|
||||
author: client.user!._id,
|
||||
message: {
|
||||
_id: nonce,
|
||||
channel: channel._id,
|
||||
author: client.user!._id,
|
||||
|
||||
content,
|
||||
replies,
|
||||
content,
|
||||
replies,
|
||||
},
|
||||
});
|
||||
|
||||
defer(() => renderer.jumpToBottom(SMOOTH_SCROLL_ON_RECEIVE));
|
||||
@@ -286,16 +272,15 @@ export default observer(({ channel }: Props) => {
|
||||
replies,
|
||||
});
|
||||
} catch (error) {
|
||||
state.queue.fail(nonce, takeError(error));
|
||||
dispatch({
|
||||
type: "QUEUE_FAIL",
|
||||
error: takeError(error),
|
||||
nonce,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param content
|
||||
* @returns
|
||||
*/
|
||||
async function sendFile(content: string) {
|
||||
if (uploadState.type !== "attached") return;
|
||||
const attachments: string[] = [];
|
||||
@@ -375,7 +360,7 @@ export default observer(({ channel }: Props) => {
|
||||
|
||||
setMessage();
|
||||
setReplies([]);
|
||||
state.settings.sounds.playSound("outbound");
|
||||
playSound("outbound");
|
||||
|
||||
if (files.length > CAN_UPLOAD_AT_ONCE) {
|
||||
setUploadState({
|
||||
@@ -387,10 +372,6 @@ export default observer(({ channel }: Props) => {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns
|
||||
*/
|
||||
function startTyping() {
|
||||
if (typeof typing === "number" && +new Date() < typing) return;
|
||||
|
||||
@@ -404,10 +385,6 @@ export default observer(({ channel }: Props) => {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param force
|
||||
*/
|
||||
function stopTyping(force?: boolean) {
|
||||
if (force || typing) {
|
||||
const ws = client.websocket;
|
||||
@@ -421,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
|
||||
// eslint-disable-next-line
|
||||
const debouncedStopTyping = useCallback(
|
||||
@@ -472,10 +434,7 @@ export default observer(({ channel }: Props) => {
|
||||
files: [...uploadState.files, ...files],
|
||||
}),
|
||||
() =>
|
||||
modalController.push({
|
||||
type: "error",
|
||||
error: "FileTooLarge",
|
||||
}),
|
||||
openScreen({ id: "error", error: "FileTooLarge" }),
|
||||
true,
|
||||
)
|
||||
}
|
||||
@@ -499,8 +458,8 @@ export default observer(({ channel }: Props) => {
|
||||
setReplies={setReplies}
|
||||
/>
|
||||
<Base>
|
||||
{channel.havePermission("UploadFiles") ? (
|
||||
<FileAction>
|
||||
{channel.permission & ChannelPermission.UploadFiles ? (
|
||||
<Action>
|
||||
<FileUploader
|
||||
size={24}
|
||||
behaviour="multi"
|
||||
@@ -535,10 +494,8 @@ export default observer(({ channel }: Props) => {
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</FileAction>
|
||||
) : (
|
||||
<ThisCodeWillBeReplacedAnywaysSoIMightAsWellJustDoItThisWay__Padding />
|
||||
)}
|
||||
</Action>
|
||||
) : undefined}
|
||||
<TextAreaAutoSize
|
||||
autoFocus
|
||||
hideBorder
|
||||
@@ -546,7 +503,7 @@ export default observer(({ channel }: Props) => {
|
||||
id="message"
|
||||
maxLength={2000}
|
||||
onKeyUp={onKeyUp}
|
||||
value={state.draft.get(channel._id) ?? ""}
|
||||
value={draft ?? ""}
|
||||
padding="var(--message-box-padding)"
|
||||
onKeyDown={(e) => {
|
||||
if (e.ctrlKey && e.key === "Enter") {
|
||||
@@ -558,7 +515,7 @@ export default observer(({ channel }: Props) => {
|
||||
|
||||
if (
|
||||
e.key === "ArrowUp" &&
|
||||
!state.draft.has(channel._id)
|
||||
(!draft || draft.length === 0)
|
||||
) {
|
||||
e.preventDefault();
|
||||
internalEmit("MessageRenderer", "edit_last");
|
||||
@@ -569,8 +526,7 @@ export default observer(({ channel }: Props) => {
|
||||
!e.shiftKey &&
|
||||
!e.isComposing &&
|
||||
e.key === "Enter" &&
|
||||
!isTouchscreenDevice &&
|
||||
!isInCodeBlock(e.currentTarget.selectionStart)
|
||||
!isTouchscreenDevice
|
||||
) {
|
||||
e.preventDefault();
|
||||
return send();
|
||||
@@ -618,23 +574,12 @@ export default observer(({ channel }: Props) => {
|
||||
onFocus={onFocus}
|
||||
onBlur={onBlur}
|
||||
/>
|
||||
{/*<Action>
|
||||
<IconButton>
|
||||
<Box size={24} />
|
||||
</IconButton>
|
||||
</Action>
|
||||
<Action>
|
||||
<IconButton>
|
||||
<HappyBeaming size={24} />
|
||||
</IconButton>
|
||||
</Action>*/}
|
||||
<Action>
|
||||
{/*<IconButton onClick={emojiPicker}>
|
||||
<HappyAlt size={20} />
|
||||
</IconButton>*/}
|
||||
<IconButton
|
||||
className={
|
||||
state.settings.get("appearance:show_send_button")
|
||||
? ""
|
||||
: "mobile"
|
||||
}
|
||||
className="mobile"
|
||||
onClick={send}
|
||||
onMouseDown={(e) => e.preventDefault()}>
|
||||
<Send size={20} />
|
||||
|
||||
@@ -11,10 +11,11 @@ import {
|
||||
MessageSquareEdit,
|
||||
} from "@styled-icons/boxicons-solid";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { Message, API } from "revolt.js";
|
||||
import styled from "styled-components/macro";
|
||||
import { SystemMessage as SystemMessageI } from "revolt-api/types/Channels";
|
||||
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";
|
||||
|
||||
@@ -28,26 +29,6 @@ const SystemContent = styled.div`
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
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 {
|
||||
@@ -74,11 +55,13 @@ export const SystemMessage = observer(
|
||||
({ attachContext, message, highlight, hideInfo }: Props) => {
|
||||
const data = message.asSystemMessage;
|
||||
const SystemMessageIcon =
|
||||
iconDictionary[data.type as API.SystemMessage["type"]] ??
|
||||
InfoCircle;
|
||||
iconDictionary[data.type as SystemMessageI["type"]] ?? InfoCircle;
|
||||
|
||||
let children = null;
|
||||
let children;
|
||||
switch (data.type) {
|
||||
case "text":
|
||||
children = <span>{data.content}</span>;
|
||||
break;
|
||||
case "user_added":
|
||||
case "user_remove":
|
||||
children = (
|
||||
@@ -135,14 +118,16 @@ export const SystemMessage = observer(
|
||||
return (
|
||||
<MessageBase
|
||||
highlight={highlight}
|
||||
{...(attachContext
|
||||
? useTriggerEvents("Menu", {
|
||||
message,
|
||||
contextualChannel: message.channel,
|
||||
})
|
||||
: undefined)}>
|
||||
onContextMenu={
|
||||
attachContext
|
||||
? attachContextMenu("Menu", {
|
||||
message,
|
||||
contextualChannel: message.channel,
|
||||
})
|
||||
: undefined
|
||||
}>
|
||||
{!hideInfo && (
|
||||
<MessageInfo click={false}>
|
||||
<MessageInfo>
|
||||
<MessageDetail message={message} position="left" />
|
||||
<SystemMessageIcon className="systemIcon" />
|
||||
</MessageInfo>
|
||||
|
||||
@@ -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 classNames from "classnames";
|
||||
import { useTriggerEvents } from "preact-context-menu";
|
||||
import { attachContextMenu } from "preact-context-menu";
|
||||
import { useContext, useState } from "preact/hooks";
|
||||
|
||||
import { AppContext } from "../../../../context/revoltjs/RevoltClient";
|
||||
@@ -14,8 +14,8 @@ import Spoiler from "./Spoiler";
|
||||
import TextFile from "./TextFile";
|
||||
|
||||
interface Props {
|
||||
attachment: API.File;
|
||||
hasContent?: boolean;
|
||||
attachment: AttachmentI;
|
||||
hasContent: boolean;
|
||||
}
|
||||
|
||||
const MAX_ATTACHMENT_WIDTH = 480;
|
||||
@@ -37,7 +37,7 @@ export default function Attachment({ attachment, hasContent }: Props) {
|
||||
<SizedGrid
|
||||
width={metadata.width}
|
||||
height={metadata.height}
|
||||
{...useTriggerEvents("Menu", {
|
||||
onContextMenu={attachContextMenu("Menu", {
|
||||
attachment,
|
||||
})}
|
||||
className={classNames({
|
||||
|
||||
@@ -1,33 +1,33 @@
|
||||
import {
|
||||
LinkExternal,
|
||||
Headphone,
|
||||
Download,
|
||||
LinkExternal,
|
||||
File,
|
||||
Headphone,
|
||||
Video,
|
||||
} from "@styled-icons/boxicons-regular";
|
||||
import { File, Video } from "@styled-icons/boxicons-solid";
|
||||
import { isFirefox } from "react-device-detect";
|
||||
import { API } from "revolt.js";
|
||||
import { Attachment } from "revolt-api/types/Autumn";
|
||||
|
||||
import styles from "./AttachmentActions.module.scss";
|
||||
import classNames from "classnames";
|
||||
import { useContext } from "preact/hooks";
|
||||
|
||||
import { IconButton } from "@revoltchat/ui";
|
||||
|
||||
import { determineFileSize } from "../../../../lib/fileSize";
|
||||
|
||||
import { AppContext } from "../../../../context/revoltjs/RevoltClient";
|
||||
|
||||
import IconButton from "../../../ui/IconButton";
|
||||
|
||||
interface Props {
|
||||
attachment: API.File;
|
||||
attachment: Attachment;
|
||||
}
|
||||
|
||||
export default function AttachmentActions({ attachment }: Props) {
|
||||
const client = useContext(AppContext);
|
||||
const { filename, metadata, size } = attachment;
|
||||
|
||||
const url = client.generateFileURL(attachment);
|
||||
const url = client.generateFileURL(attachment)!;
|
||||
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);
|
||||
|
||||
@@ -52,7 +52,7 @@ export default function AttachmentActions({ attachment }: Props) {
|
||||
href={download_url}
|
||||
className={styles.downloadIcon}
|
||||
download
|
||||
target={isFirefox || window.native ? "_blank" : "_self"}
|
||||
target="_blank"
|
||||
rel="noreferrer">
|
||||
<IconButton>
|
||||
<Download size={24} />
|
||||
@@ -70,7 +70,7 @@ export default function AttachmentActions({ attachment }: Props) {
|
||||
href={download_url}
|
||||
className={styles.downloadIcon}
|
||||
download
|
||||
target={isFirefox || window.native ? "_blank" : "_self"}
|
||||
target="_blank"
|
||||
rel="noreferrer">
|
||||
<IconButton>
|
||||
<Download size={24} />
|
||||
@@ -90,7 +90,7 @@ export default function AttachmentActions({ attachment }: Props) {
|
||||
href={download_url}
|
||||
className={styles.downloadIcon}
|
||||
download
|
||||
target={isFirefox || window.native ? "_blank" : "_self"}
|
||||
target="_blank"
|
||||
rel="noreferrer">
|
||||
<IconButton>
|
||||
<Download size={24} />
|
||||
@@ -119,7 +119,7 @@ export default function AttachmentActions({ attachment }: Props) {
|
||||
href={download_url}
|
||||
className={styles.downloadIcon}
|
||||
download
|
||||
target={isFirefox || window.native ? "_blank" : "_self"}
|
||||
target="_blank"
|
||||
rel="noreferrer">
|
||||
<IconButton>
|
||||
<Download size={24} />
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import styled from "styled-components/macro";
|
||||
import styled from "styled-components";
|
||||
|
||||
import { Ref } from "preact";
|
||||
import { Children } from "../../../../types/Preact";
|
||||
|
||||
const Grid = styled.div<{ width: number; height: number }>`
|
||||
--width: ${(props) => props.width}px;
|
||||
--height: ${(props) => props.height}px;
|
||||
--width: ${props => props.width}px;
|
||||
--height: ${props => props.height}px;
|
||||
|
||||
display: grid;
|
||||
overflow: hidden;
|
||||
aspect-ratio: ${(props) => props.width} / ${(props) => props.height};
|
||||
|
||||
max-width: min(var(--width), var(--attachment-max-width));
|
||||
@@ -43,7 +42,7 @@ const Grid = styled.div<{ width: number; height: number }>`
|
||||
overflow: hidden;
|
||||
|
||||
object-fit: contain;
|
||||
|
||||
|
||||
// It's something
|
||||
object-position: left;
|
||||
}
|
||||
@@ -72,14 +71,13 @@ type Props = Omit<
|
||||
children?: Children;
|
||||
width: number;
|
||||
height: number;
|
||||
innerRef?: Ref<any>;
|
||||
};
|
||||
|
||||
export function SizedGrid(props: Props) {
|
||||
const { width, height, children, innerRef, ...divProps } = props;
|
||||
const { width, height, children, ...divProps } = props;
|
||||
|
||||
return (
|
||||
<Grid {...divProps} width={width} height={height} ref={innerRef}>
|
||||
<Grid {...divProps} width={width} height={height}>
|
||||
{children}
|
||||
</Grid>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { API } from "revolt.js";
|
||||
import { Attachment } from "revolt-api/types/Autumn";
|
||||
|
||||
import styles from "./Attachment.module.scss";
|
||||
import classNames from "classnames";
|
||||
@@ -10,12 +10,12 @@ import { AppContext } from "../../../../context/revoltjs/RevoltClient";
|
||||
enum ImageLoadingState {
|
||||
Loading,
|
||||
Loaded,
|
||||
Error,
|
||||
Error
|
||||
}
|
||||
|
||||
type Props = JSX.HTMLAttributes<HTMLImageElement> & {
|
||||
attachment: API.File;
|
||||
};
|
||||
attachment: Attachment;
|
||||
}
|
||||
|
||||
export default function ImageFile({ attachment, ...props }: Props) {
|
||||
const [loading, setLoading] = useState(ImageLoadingState.Loading);
|
||||
@@ -23,19 +23,25 @@ export default function ImageFile({ attachment, ...props }: Props) {
|
||||
const { openScreen } = useIntermediate();
|
||||
const url = client.generateFileURL(attachment)!;
|
||||
|
||||
return (
|
||||
<img
|
||||
{...props}
|
||||
src={url}
|
||||
alt={attachment.filename}
|
||||
loading="lazy"
|
||||
className={classNames(styles.image, {
|
||||
[styles.loading]: loading !== ImageLoadingState.Loaded,
|
||||
})}
|
||||
onClick={() => openScreen({ id: "image_viewer", attachment })}
|
||||
onMouseDown={(ev) => ev.button === 1 && window.open(url, "_blank")}
|
||||
onLoad={() => setLoading(ImageLoadingState.Loaded)}
|
||||
onError={() => setLoading(ImageLoadingState.Error)}
|
||||
/>
|
||||
);
|
||||
return <img
|
||||
{...props}
|
||||
src={url}
|
||||
alt={attachment.filename}
|
||||
loading="lazy"
|
||||
className={classNames(styles.image, {
|
||||
[styles.loading]: loading !== ImageLoadingState.Loaded
|
||||
})}
|
||||
onClick={() =>
|
||||
openScreen({ id: "image_viewer", attachment })
|
||||
}
|
||||
onMouseDown={(ev) =>
|
||||
ev.button === 1 && window.open(url, "_blank")
|
||||
}
|
||||
onLoad={() =>
|
||||
setLoading(ImageLoadingState.Loaded)
|
||||
}
|
||||
onError={() =>
|
||||
setLoading(ImageLoadingState.Error)
|
||||
}
|
||||
/>
|
||||
}
|
||||
|
||||
@@ -2,8 +2,10 @@ import { Reply } from "@styled-icons/boxicons-regular";
|
||||
import { File } from "@styled-icons/boxicons-solid";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import { Channel, Message, API } from "revolt.js";
|
||||
import styled, { css } from "styled-components/macro";
|
||||
import { RelationshipStatus } from "revolt-api/types/Users";
|
||||
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 { useLayoutEffect, useState } from "preact/hooks";
|
||||
@@ -16,7 +18,7 @@ import { SystemMessage } from "../SystemMessage";
|
||||
|
||||
interface Props {
|
||||
parent_mentions: string[];
|
||||
channel?: Channel;
|
||||
channel: Channel;
|
||||
index: number;
|
||||
id: string;
|
||||
}
|
||||
@@ -26,25 +28,30 @@ export const ReplyBase = styled.div<{
|
||||
fail?: boolean;
|
||||
preview?: boolean;
|
||||
}>`
|
||||
gap: 8px;
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
margin-inline-start: 30px;
|
||||
margin-inline-end: 12px;
|
||||
font-size: 0.8em;
|
||||
user-select: none;
|
||||
align-items: end;
|
||||
align-items: center;
|
||||
color: var(--secondary-foreground);
|
||||
|
||||
/* nizune's Discord replies,
|
||||
does not scale properly with messages,
|
||||
reverted temporarily
|
||||
&::before {
|
||||
content: "";
|
||||
flex-shrink: 0;
|
||||
width: 22px;
|
||||
height: 10px;
|
||||
border-inline-start: 2px solid var(--message-box);
|
||||
border-top: 2px solid var(--message-box);
|
||||
width: 28px;
|
||||
margin-inline-end: 2px;
|
||||
align-self: flex-end;
|
||||
}
|
||||
display: flex;
|
||||
border-top: 2.2px solid var(--tertiary-foreground);
|
||||
border-inline-start: 2.2px solid var(--tertiary-foreground);
|
||||
border-start-start-radius: 6px;
|
||||
}*/
|
||||
|
||||
* {
|
||||
overflow: hidden;
|
||||
@@ -53,7 +60,6 @@ export const ReplyBase = styled.div<{
|
||||
}
|
||||
|
||||
.user {
|
||||
//margin-inline-start: 12px;
|
||||
gap: 6px;
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
@@ -68,6 +74,13 @@ export const ReplyBase = styled.div<{
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
/*&::before {
|
||||
position:relative;
|
||||
width: 50px;
|
||||
height: 2px;
|
||||
background: red;
|
||||
}*/
|
||||
}
|
||||
|
||||
.content {
|
||||
@@ -85,10 +98,6 @@ export const ReplyBase = styled.div<{
|
||||
transition: transform ease-in-out 0.1s;
|
||||
filter: brightness(1);
|
||||
|
||||
> svg {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
> span > p {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -106,6 +115,10 @@ export const ReplyBase = styled.div<{
|
||||
> * {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/*> span > p {
|
||||
display: flex;
|
||||
}*/
|
||||
}
|
||||
|
||||
> svg:first-child {
|
||||
@@ -124,10 +137,6 @@ export const ReplyBase = styled.div<{
|
||||
props.head &&
|
||||
css`
|
||||
margin-top: 12px;
|
||||
|
||||
&::before {
|
||||
border-start-start-radius: 4px;
|
||||
}
|
||||
`}
|
||||
|
||||
${(props) =>
|
||||
@@ -139,8 +148,6 @@ export const ReplyBase = styled.div<{
|
||||
|
||||
export const MessageReply = observer(
|
||||
({ index, channel, id, parent_mentions }: Props) => {
|
||||
if (!channel) return null;
|
||||
|
||||
const view = getRenderer(channel);
|
||||
if (view.state !== "RENDER") return null;
|
||||
|
||||
@@ -170,9 +177,8 @@ export const MessageReply = observer(
|
||||
|
||||
return (
|
||||
<ReplyBase head={index === 0}>
|
||||
{/*<Reply size={16} />*/}
|
||||
|
||||
{message.author?.relationship === "Blocked" ? (
|
||||
<Reply size={16} />
|
||||
{message.author?.relationship === RelationshipStatus.Blocked ? (
|
||||
<Text id="app.main.channel.misc.blocked_user" />
|
||||
) : (
|
||||
<>
|
||||
@@ -182,7 +188,7 @@ export const MessageReply = observer(
|
||||
<>
|
||||
<div className="user">
|
||||
<UserShort
|
||||
size={14}
|
||||
size={16}
|
||||
showServerIdentity
|
||||
user={message.author}
|
||||
masquerade={message.masquerade!}
|
||||
@@ -223,10 +229,9 @@ export const MessageReply = observer(
|
||||
)}
|
||||
<Markdown
|
||||
disallowBigEmoji
|
||||
content={message.content?.replace(
|
||||
/\n/g,
|
||||
" ",
|
||||
)}
|
||||
content={(
|
||||
message.content as string
|
||||
).replace(/\n/g, " ")}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import styled from "styled-components/macro";
|
||||
import styled from "styled-components";
|
||||
|
||||
import { Text } from "preact-i18n";
|
||||
|
||||
|
||||
@@ -1,26 +1,24 @@
|
||||
import axios from "axios";
|
||||
import { API } from "revolt.js";
|
||||
import { Attachment } from "revolt-api/types/Autumn";
|
||||
|
||||
import styles from "./Attachment.module.scss";
|
||||
import { Text } from "preact-i18n";
|
||||
import { useContext, useEffect, useState } from "preact/hooks";
|
||||
|
||||
import { Button, Preloader } from "@revoltchat/ui";
|
||||
|
||||
import RequiresOnline from "../../../../context/revoltjs/RequiresOnline";
|
||||
import {
|
||||
AppContext,
|
||||
StatusContext,
|
||||
} from "../../../../context/revoltjs/RevoltClient";
|
||||
|
||||
import Preloader from "../../../ui/Preloader";
|
||||
|
||||
interface Props {
|
||||
attachment: API.File;
|
||||
attachment: Attachment;
|
||||
}
|
||||
|
||||
const fileCache: { [key: string]: string } = {};
|
||||
|
||||
export default function TextFile({ attachment }: Props) {
|
||||
const [gated, setGated] = useState(attachment.size > 100_000);
|
||||
const [content, setContent] = useState<undefined | string>(undefined);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const status = useContext(StatusContext);
|
||||
@@ -31,7 +29,13 @@ export default function TextFile({ attachment }: Props) {
|
||||
useEffect(() => {
|
||||
if (typeof content !== "undefined") 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);
|
||||
|
||||
@@ -56,17 +60,13 @@ export default function TextFile({ attachment }: Props) {
|
||||
setLoading(false);
|
||||
});
|
||||
}
|
||||
}, [content, loading, gated, status, attachment._id, attachment.size, url]);
|
||||
}, [content, loading, status, attachment._id, attachment.size, url]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={styles.textContent}
|
||||
data-loading={typeof content === "undefined"}>
|
||||
{gated ? (
|
||||
<Button palette="accent" onClick={() => setGated(false)}>
|
||||
<Text id="app.main.channel.misc.load_file" />
|
||||
</Button>
|
||||
) : content ? (
|
||||
{content ? (
|
||||
<pre>
|
||||
<code>{content}</code>
|
||||
</pre>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* eslint-disable react-hooks/rules-of-hooks */
|
||||
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 { useEffect, useState } from "preact/hooks";
|
||||
@@ -149,12 +149,12 @@ function FileEntry({
|
||||
<EmptyEntry className="icon">
|
||||
<File size={36} />
|
||||
</EmptyEntry>
|
||||
<div className="overlay">
|
||||
<div class="overlay">
|
||||
<XCircle size={36} />
|
||||
</div>
|
||||
</PreviewBox>
|
||||
<span className="fn">{file.name}</span>
|
||||
<span className="size">{determineFileSize(file.size)}</span>
|
||||
<span class="fn">{file.name}</span>
|
||||
<span class="size">{determineFileSize(file.size)}</span>
|
||||
</Entry>
|
||||
);
|
||||
|
||||
@@ -169,18 +169,13 @@ function FileEntry({
|
||||
return (
|
||||
<Entry className={index >= CAN_UPLOAD_AT_ONCE ? "fade" : ""}>
|
||||
<PreviewBox onClick={remove}>
|
||||
<img
|
||||
className="icon"
|
||||
src={url}
|
||||
alt={file.name}
|
||||
loading="eager"
|
||||
/>
|
||||
<div className="overlay">
|
||||
<img class="icon" src={url} alt={file.name} loading="eager" />
|
||||
<div class="overlay">
|
||||
<XCircle size={36} />
|
||||
</div>
|
||||
</PreviewBox>
|
||||
<span className="fn">{file.name}</span>
|
||||
<span className="size">{determineFileSize(file.size)}</span>
|
||||
<span class="fn">{file.name}</span>
|
||||
<span class="size">{determineFileSize(file.size)}</span>
|
||||
</Entry>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,121 +1,38 @@
|
||||
import { DownArrowAlt } from "@styled-icons/boxicons-regular";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { Channel } from "revolt.js";
|
||||
import styled, { css } from "styled-components/macro";
|
||||
import { Channel } from "revolt.js/dist/maps/Channels";
|
||||
import styled, { css } from "styled-components";
|
||||
|
||||
import { Text } from "preact-i18n";
|
||||
|
||||
import { internalEmit } from "../../../../lib/eventEmitter";
|
||||
import { isTouchscreenDevice } from "../../../../lib/isTouchscreenDevice";
|
||||
import { getRenderer } from "../../../../lib/renderer/Singleton";
|
||||
|
||||
export const Bar = styled.div<{ position: "top" | "bottom"; accent?: boolean }>`
|
||||
z-index: 1;
|
||||
const Bar = styled.div`
|
||||
z-index: 10;
|
||||
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 {
|
||||
top: -26px;
|
||||
height: 28px;
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
padding: 0 8px;
|
||||
user-select: none;
|
||||
justify-content: space-between;
|
||||
color: var(--secondary-foreground);
|
||||
transition: color ease-in-out 0.08s;
|
||||
|
||||
white-space: nowrap;
|
||||
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;
|
||||
`}
|
||||
background: var(--secondary-background);
|
||||
border-radius: var(--border-radius) var(--border-radius) 0 0;
|
||||
|
||||
> div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
@@ -130,15 +47,10 @@ export const Bar = styled.div<{ position: "top" | "bottom"; accent?: boolean }>`
|
||||
isTouchscreenDevice &&
|
||||
css`
|
||||
height: 34px;
|
||||
top: -32px;
|
||||
padding: 0 12px;
|
||||
`}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 800px) {
|
||||
.right > span {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default observer(({ channel }: { channel: Channel }) => {
|
||||
@@ -146,20 +58,14 @@ export default observer(({ channel }: { channel: Channel }) => {
|
||||
if (renderer.state !== "RENDER" || renderer.atBottom) return null;
|
||||
|
||||
return (
|
||||
<Bar position="bottom">
|
||||
<div
|
||||
onClick={() => {
|
||||
renderer.jumpToBottom(true);
|
||||
internalEmit("NewMessages", "hide");
|
||||
}}>
|
||||
<Bar>
|
||||
<div onClick={() => renderer.jumpToBottom(true)}>
|
||||
<div>
|
||||
<Text id="app.main.channel.misc.viewing_old" />
|
||||
</div>
|
||||
<div className="right">
|
||||
<span>
|
||||
<Text id="app.main.channel.misc.jump_present" />
|
||||
</span>
|
||||
<DownArrowAlt size={18} />
|
||||
<div>
|
||||
<Text id="app.main.channel.misc.jump_present" />{" "}
|
||||
<DownArrowAlt size={20} />
|
||||
</div>
|
||||
</div>
|
||||
</Bar>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
@@ -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>
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -1,21 +1,20 @@
|
||||
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 { observer } from "mobx-react-lite";
|
||||
import { Channel, Message } from "revolt.js";
|
||||
import styled from "styled-components/macro";
|
||||
import { Channel } from "revolt.js/dist/maps/Channels";
|
||||
import { Message } from "revolt.js/dist/maps/Messages";
|
||||
import styled from "styled-components";
|
||||
|
||||
import { Text } from "preact-i18n";
|
||||
import { StateUpdater, useEffect } from "preact/hooks";
|
||||
|
||||
import { IconButton } from "@revoltchat/ui";
|
||||
|
||||
import { internalSubscribe } from "../../../../lib/eventEmitter";
|
||||
|
||||
import { useApplicationState } from "../../../../mobx/State";
|
||||
import { SECTION_MENTION } from "../../../../mobx/stores/Layout";
|
||||
import { Reply } from "../../../../mobx/stores/MessageQueue";
|
||||
import { dispatch, getState } from "../../../../redux";
|
||||
import { Reply } from "../../../../redux/reducers/queue";
|
||||
|
||||
import IconButton from "../../../ui/IconButton";
|
||||
|
||||
import Tooltip from "../../../common/Tooltip";
|
||||
import Markdown from "../../../markdown/Markdown";
|
||||
import UserShort from "../../user/UserShort";
|
||||
import { SystemMessage } from "../SystemMessage";
|
||||
@@ -28,22 +27,12 @@ interface Props {
|
||||
}
|
||||
|
||||
const Base = styled.div`
|
||||
@keyframes bottomBounce {
|
||||
0% {
|
||||
transform: translateY(33px);
|
||||
}
|
||||
100% {
|
||||
transform: translateY(0px);
|
||||
}
|
||||
}
|
||||
|
||||
display: flex;
|
||||
height: 30px;
|
||||
padding: 0 20px;
|
||||
padding: 0 12px;
|
||||
user-select: none;
|
||||
align-items: center;
|
||||
background: var(--secondary-background);
|
||||
animation: bottomBounce 340ms cubic-bezier(0.2, 0.9, 0.5, 1.16) forwards;
|
||||
background: var(--message-box);
|
||||
|
||||
> div {
|
||||
flex-grow: 1;
|
||||
@@ -59,34 +48,21 @@ const Base = styled.div`
|
||||
display: flex;
|
||||
font-size: 12px;
|
||||
align-items: center;
|
||||
font-weight: 800;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
min-width: 6ch;
|
||||
}
|
||||
|
||||
.replyto {
|
||||
align-self: center;
|
||||
font-weight: 500;
|
||||
flex-shrink: 0;
|
||||
.username {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.content {
|
||||
.message {
|
||||
display: flex;
|
||||
pointer-events: none;
|
||||
|
||||
.username {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-weight: 600;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.message {
|
||||
display: flex;
|
||||
max-height: 26px;
|
||||
gap: 4px;
|
||||
}
|
||||
max-height: 26px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
@@ -105,7 +81,6 @@ const Base = styled.div`
|
||||
const MAX_REPLIES = 5;
|
||||
export default observer(({ channel, replies, setReplies }: Props) => {
|
||||
const client = channel.client;
|
||||
const layout = useApplicationState().layout;
|
||||
|
||||
// Event listener for adding new messages to reply bar.
|
||||
useEffect(() => {
|
||||
@@ -124,7 +99,7 @@ export default observer(({ channel, replies, setReplies }: Props) => {
|
||||
mention:
|
||||
message.author_id === client.user!._id
|
||||
? false
|
||||
: layout.getSectionState(SECTION_MENTION, false),
|
||||
: getState().sectionToggle.mention ?? false,
|
||||
},
|
||||
]);
|
||||
});
|
||||
@@ -152,51 +127,42 @@ export default observer(({ channel, replies, setReplies }: Props) => {
|
||||
return (
|
||||
<Base key={reply.id}>
|
||||
<ReplyBase preview>
|
||||
<div className="replyto">
|
||||
<Text id="app.main.channel.reply.replying" />
|
||||
<ReplyIcon size={22} />
|
||||
<div class="username">
|
||||
<UserShort
|
||||
size={16}
|
||||
showServerIdentity
|
||||
user={message.author}
|
||||
masquerade={message.masquerade!}
|
||||
/>
|
||||
</div>
|
||||
<div className="content">
|
||||
<div className="username">
|
||||
<UserShort
|
||||
size={16}
|
||||
showServerIdentity
|
||||
user={message.author}
|
||||
masquerade={message.masquerade!}
|
||||
/>
|
||||
</div>
|
||||
<div className="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,
|
||||
" ",
|
||||
<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" />
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</em>
|
||||
</>
|
||||
)}
|
||||
{message.author_id ===
|
||||
"00000000000000000000000000" ? (
|
||||
<SystemMessage message={message} hideInfo />
|
||||
) : (
|
||||
<Markdown
|
||||
disallowBigEmoji
|
||||
content={(
|
||||
message.content as string
|
||||
).replace(/\n/g, " ")}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</ReplyBase>
|
||||
<span className="actions">
|
||||
<span class="actions">
|
||||
{message.author_id !== client.user!._id && (
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
@@ -215,27 +181,22 @@ export default observer(({ channel, replies, setReplies }: Props) => {
|
||||
}),
|
||||
);
|
||||
|
||||
layout.setSectionState(
|
||||
SECTION_MENTION,
|
||||
dispatch({
|
||||
type: "SECTION_TOGGLE_SET",
|
||||
id: "mention",
|
||||
state,
|
||||
false,
|
||||
);
|
||||
});
|
||||
}}>
|
||||
<Tooltip
|
||||
content={
|
||||
<Text id="app.main.channel.reply.toggle" />
|
||||
}>
|
||||
<span className="toggle">
|
||||
<At size={15} />
|
||||
<Text
|
||||
id={
|
||||
reply.mention
|
||||
? "general.on"
|
||||
: "general.off"
|
||||
}
|
||||
/>
|
||||
</span>
|
||||
</Tooltip>
|
||||
<span class="toggle">
|
||||
<At size={15} />
|
||||
<Text
|
||||
id={
|
||||
reply.mention
|
||||
? "general.on"
|
||||
: "general.off"
|
||||
}
|
||||
/>
|
||||
</span>
|
||||
</IconButton>
|
||||
)}
|
||||
<IconButton
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { Channel } from "revolt.js";
|
||||
import styled from "styled-components/macro";
|
||||
import { RelationshipStatus } from "revolt-api/types/Users";
|
||||
import { Channel } from "revolt.js/dist/maps/Channels";
|
||||
import styled from "styled-components";
|
||||
|
||||
import { Text } from "preact-i18n";
|
||||
|
||||
@@ -12,10 +13,10 @@ const Base = styled.div`
|
||||
position: relative;
|
||||
|
||||
> div {
|
||||
height: 26px;
|
||||
top: -26px;
|
||||
height: 24px;
|
||||
margin-top: -24px;
|
||||
position: absolute;
|
||||
font-size: 13px;
|
||||
|
||||
gap: 8px;
|
||||
display: flex;
|
||||
padding: 0 10px;
|
||||
@@ -24,11 +25,7 @@ const Base = styled.div`
|
||||
flex-direction: row;
|
||||
width: calc(100% - var(--scrollbar-thickness));
|
||||
color: var(--secondary-foreground);
|
||||
background-color: rgba(
|
||||
var(--secondary-background-rgb),
|
||||
max(var(--min-opacity), 0.75)
|
||||
);
|
||||
backdrop-filter: blur(10px);
|
||||
background: var(--secondary-background);
|
||||
}
|
||||
|
||||
.avatars {
|
||||
@@ -39,12 +36,9 @@ const Base = styled.div`
|
||||
height: 16px;
|
||||
object-fit: cover;
|
||||
border-radius: var(--border-radius-half);
|
||||
background: var(--secondary-background);
|
||||
//background-clip: border-box;
|
||||
border: 2px solid var(--secondary-background);
|
||||
|
||||
&:not(:first-child) {
|
||||
margin-left: -6px;
|
||||
margin-left: -4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -55,7 +49,6 @@ const Base = styled.div`
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
//font-weight: 600;
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -64,7 +57,7 @@ export default observer(({ channel }: Props) => {
|
||||
(x) =>
|
||||
typeof x !== "undefined" &&
|
||||
x._id !== x.client.user!._id &&
|
||||
x.relationship !== "Blocked",
|
||||
x.relationship !== RelationshipStatus.Blocked,
|
||||
);
|
||||
|
||||
if (users.length > 0) {
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
iframe {
|
||||
border: none;
|
||||
border-radius: var(--border-radius);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&.image {
|
||||
|
||||
@@ -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 classNames from "classnames";
|
||||
@@ -8,12 +8,10 @@ import { useIntermediate } from "../../../../context/intermediate/Intermediate";
|
||||
import { useClient } from "../../../../context/revoltjs/RevoltClient";
|
||||
|
||||
import { MessageAreaWidthContext } from "../../../../pages/channels/messaging/MessageArea";
|
||||
import Markdown from "../../../markdown/Markdown";
|
||||
import Attachment from "../attachments/Attachment";
|
||||
import EmbedMedia from "./EmbedMedia";
|
||||
|
||||
interface Props {
|
||||
embed: API.Embed;
|
||||
embed: EmbedI;
|
||||
}
|
||||
|
||||
const MAX_EMBED_WIDTH = 480;
|
||||
@@ -47,63 +45,39 @@ export default function Embed({ embed }: Props) {
|
||||
}
|
||||
|
||||
switch (embed.type) {
|
||||
case "Text":
|
||||
case "Website": {
|
||||
// Determine special embed size.
|
||||
let mw, mh;
|
||||
const largeMedia =
|
||||
embed.type === "Text"
|
||||
? typeof embed.media !== "undefined"
|
||||
: (embed.special && embed.special.type !== "None") ||
|
||||
embed.image?.size === "Large";
|
||||
|
||||
if (embed.type === "Text") {
|
||||
mw = MAX_EMBED_WIDTH;
|
||||
mh = 1;
|
||||
} else {
|
||||
switch (embed.special?.type) {
|
||||
case "YouTube":
|
||||
case "Bandcamp": {
|
||||
mw = embed.video?.width ?? 1280;
|
||||
mh = embed.video?.height ?? 720;
|
||||
break;
|
||||
}
|
||||
case "Twitch":
|
||||
case "Lightspeed": {
|
||||
mw = 1280;
|
||||
mh = 720;
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
if (embed.image?.size === "Preview") {
|
||||
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;
|
||||
}
|
||||
(embed.special && embed.special.type !== "None") ||
|
||||
embed.image?.size === "Large";
|
||||
switch (embed.special?.type) {
|
||||
case "YouTube":
|
||||
case "Bandcamp": {
|
||||
mw = embed.video?.width ?? 1280;
|
||||
mh = embed.video?.height ?? 720;
|
||||
break;
|
||||
}
|
||||
case "Twitch": {
|
||||
mw = 1280;
|
||||
mh = 720;
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
if (embed.image?.size === "Preview") {
|
||||
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);
|
||||
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 (
|
||||
<div
|
||||
className={classNames(styles.embed, styles.website)}
|
||||
@@ -113,9 +87,7 @@ export default function Embed({ embed }: Props) {
|
||||
width: width + CONTAINER_PADDING,
|
||||
}}>
|
||||
<div>
|
||||
{(embed.type === "Text"
|
||||
? embed.title
|
||||
: embed.site_name) && (
|
||||
{embed.site_name && (
|
||||
<div className={styles.siteinfo}>
|
||||
{embed.icon_url && (
|
||||
<img
|
||||
@@ -130,43 +102,35 @@ export default function Embed({ embed }: Props) {
|
||||
/>
|
||||
)}
|
||||
<div className={styles.site}>
|
||||
{embed.type === "Text"
|
||||
? embed.title
|
||||
: embed.site_name}{" "}
|
||||
{embed.site_name}{" "}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/*<span><a href={embed.url} target={"_blank"} className={styles.author}>Author</a></span>*/}
|
||||
{embed.type === "Website" && embed.title && (
|
||||
{embed.title && (
|
||||
<span>
|
||||
<a
|
||||
onMouseDown={(ev) =>
|
||||
(ev.button === 0 || ev.button === 1) &&
|
||||
openLink(embed.url!)
|
||||
openLink(embed.url)
|
||||
}
|
||||
className={styles.title}>
|
||||
{embed.title}
|
||||
</a>
|
||||
</span>
|
||||
)}
|
||||
{embed.description &&
|
||||
(embed.type === "Text" ? (
|
||||
<Markdown content={embed.description} />
|
||||
) : (
|
||||
<div className={styles.description}>
|
||||
{embed.description}
|
||||
</div>
|
||||
))}
|
||||
{embed.description && (
|
||||
<div className={styles.description}>
|
||||
{embed.description}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{largeMedia &&
|
||||
(embed.type === "Text" ? (
|
||||
<Attachment attachment={embed.media!} />
|
||||
) : (
|
||||
<EmbedMedia embed={embed} height={height} />
|
||||
))}
|
||||
{largeMedia && (
|
||||
<EmbedMedia embed={embed} height={height} />
|
||||
)}
|
||||
</div>
|
||||
{!largeMedia && embed.type === "Website" && (
|
||||
{!largeMedia && (
|
||||
<div>
|
||||
<EmbedMedia
|
||||
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:
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import { Group } from "@styled-icons/boxicons-solid";
|
||||
import { reaction } from "mobx";
|
||||
import { autorun } from "mobx";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import { Message, API } from "revolt.js";
|
||||
import styled, { css } from "styled-components/macro";
|
||||
import { RetrievedInvite } from "revolt-api/types/Invites";
|
||||
import { Message } from "revolt.js/dist/maps/Messages";
|
||||
import styled, { css } from "styled-components";
|
||||
|
||||
import { useContext, useEffect, useState } from "preact/hooks";
|
||||
|
||||
import { Button, Category, Preloader } from "@revoltchat/ui";
|
||||
|
||||
import { defer } from "../../../../lib/defer";
|
||||
import { isTouchscreenDevice } from "../../../../lib/isTouchscreenDevice";
|
||||
|
||||
import { I18nError } from "../../../../context/Locale";
|
||||
import { dispatch } from "../../../../redux";
|
||||
|
||||
import {
|
||||
AppContext,
|
||||
ClientStatus,
|
||||
@@ -20,6 +20,9 @@ import {
|
||||
import { takeError } from "../../../../context/revoltjs/util";
|
||||
|
||||
import ServerIcon from "../../../../components/common/ServerIcon";
|
||||
import Button from "../../../../components/ui/Button";
|
||||
import Overline from "../../../ui/Overline";
|
||||
import Preloader from "../../../ui/Preloader";
|
||||
|
||||
const EmbedInviteBase = styled.div`
|
||||
width: 400px;
|
||||
@@ -30,7 +33,7 @@ const EmbedInviteBase = styled.div`
|
||||
align-items: center;
|
||||
padding: 0 12px;
|
||||
margin-top: 2px;
|
||||
${() =>
|
||||
${() =>
|
||||
isTouchscreenDevice &&
|
||||
css`
|
||||
flex-wrap: wrap;
|
||||
@@ -41,17 +44,19 @@ const EmbedInviteBase = styled.div`
|
||||
> button {
|
||||
width: 100%;
|
||||
}
|
||||
`}
|
||||
`
|
||||
}
|
||||
`;
|
||||
|
||||
const EmbedInviteDetails = styled.div`
|
||||
flex-grow: 1;
|
||||
padding-inline-start: 12px;
|
||||
${() =>
|
||||
padding-left: 12px;
|
||||
${() =>
|
||||
isTouchscreenDevice &&
|
||||
css`
|
||||
width: calc(100% - 55px);
|
||||
`}
|
||||
`
|
||||
}
|
||||
`;
|
||||
|
||||
const EmbedInviteName = styled.div`
|
||||
@@ -62,30 +67,24 @@ const EmbedInviteName = styled.div`
|
||||
`;
|
||||
|
||||
const EmbedInviteMemberCount = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
font-size: 0.8em;
|
||||
|
||||
> svg {
|
||||
color: var(--secondary-foreground);
|
||||
}
|
||||
`;
|
||||
|
||||
type Props = {
|
||||
code: string;
|
||||
};
|
||||
|
||||
export function EmbedInvite({ code }: Props) {
|
||||
export function EmbedInvite(props: Props) {
|
||||
const history = useHistory();
|
||||
const client = useContext(AppContext);
|
||||
const status = useContext(StatusContext);
|
||||
const code = props.code;
|
||||
const [processing, setProcessing] = useState(false);
|
||||
const [error, setError] = useState<string | undefined>(undefined);
|
||||
const [joinError, setJoinError] = useState<string | undefined>(undefined);
|
||||
const [invite, setInvite] = useState<
|
||||
(API.InviteResponse & { type: "Server" }) | undefined
|
||||
>(undefined);
|
||||
const [invite, setInvite] = useState<RetrievedInvite | undefined>(
|
||||
undefined,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
@@ -94,9 +93,7 @@ export function EmbedInvite({ code }: Props) {
|
||||
) {
|
||||
client
|
||||
.fetchInvite(code)
|
||||
.then((data) =>
|
||||
setInvite(data as API.InviteResponse & { type: "Server" }),
|
||||
)
|
||||
.then((data) => setInvite(data))
|
||||
.catch((err) => setError(takeError(err)));
|
||||
}
|
||||
}, [client, code, invite, status]);
|
||||
@@ -127,9 +124,7 @@ export function EmbedInvite({ code }: Props) {
|
||||
<EmbedInviteDetails>
|
||||
<EmbedInviteName>{invite.server_name}</EmbedInviteName>
|
||||
<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>
|
||||
</EmbedInviteDetails>
|
||||
{processing ? (
|
||||
@@ -139,31 +134,49 @@ export function EmbedInvite({ code }: Props) {
|
||||
) : (
|
||||
<Button
|
||||
onClick={async () => {
|
||||
setProcessing(true);
|
||||
|
||||
try {
|
||||
await client.joinInvite(invite);
|
||||
setProcessing(true);
|
||||
|
||||
history.push(
|
||||
`/server/${invite.server_id}/channel/${invite.channel_id}`,
|
||||
);
|
||||
if (invite.type === "Server") {
|
||||
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) {
|
||||
setJoinError(takeError(err));
|
||||
} finally {
|
||||
setProcessing(false);
|
||||
}
|
||||
}}>
|
||||
{client.servers.get(invite.server_id)
|
||||
? "Joined"
|
||||
: "Join"}
|
||||
{client.servers.get(invite.server_id) ? "Joined" : "Join"}
|
||||
</Button>
|
||||
)}
|
||||
</EmbedInviteBase>
|
||||
{joinError && (
|
||||
<Category>
|
||||
<I18nError error={joinError} />
|
||||
</Category>
|
||||
)}
|
||||
{joinError && <Overline type="error" error={joinError} />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -194,12 +207,9 @@ export default observer(({ message }: { message: Message }) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
{entries.map(
|
||||
(entry) =>
|
||||
entry !== "discover" && (
|
||||
<EmbedInvite key={entry} code={entry} />
|
||||
),
|
||||
)}
|
||||
{entries.map((entry) => (
|
||||
<EmbedInvite key={entry} code={entry} />
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/* 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";
|
||||
|
||||
@@ -7,7 +7,7 @@ import { useIntermediate } from "../../../../context/intermediate/Intermediate";
|
||||
import { useClient } from "../../../../context/revoltjs/RevoltClient";
|
||||
|
||||
interface Props {
|
||||
embed: API.Embed;
|
||||
embed: Embed;
|
||||
width?: number;
|
||||
height: number;
|
||||
}
|
||||
@@ -47,17 +47,6 @@ export default function EmbedMedia({ embed, width, height }: Props) {
|
||||
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":
|
||||
return (
|
||||
<iframe
|
||||
@@ -94,21 +83,7 @@ export default function EmbedMedia({ embed, width, height }: Props) {
|
||||
);
|
||||
}
|
||||
default: {
|
||||
if (embed.video) {
|
||||
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) {
|
||||
if (embed.image) {
|
||||
const url = embed.image.url;
|
||||
return (
|
||||
<img
|
||||
@@ -119,7 +94,7 @@ export default function EmbedMedia({ embed, width, height }: Props) {
|
||||
onClick={() =>
|
||||
openScreen({
|
||||
id: "image_viewer",
|
||||
embed: embed.image!,
|
||||
embed: embed.image,
|
||||
})
|
||||
}
|
||||
onMouseDown={(ev) =>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
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 { IconButton } from "@revoltchat/ui";
|
||||
import IconButton from "../../../ui/IconButton";
|
||||
|
||||
interface Props {
|
||||
embed: API.Image;
|
||||
embed: EmbedImage;
|
||||
}
|
||||
|
||||
export default function EmbedMediaActions({ embed }: Props) {
|
||||
@@ -20,7 +20,7 @@ export default function EmbedMediaActions({ embed }: Props) {
|
||||
</span>
|
||||
<a
|
||||
href={embed.url}
|
||||
className={styles.openIcon}
|
||||
class={styles.openIcon}
|
||||
target="_blank"
|
||||
rel="noreferrer">
|
||||
<IconButton>
|
||||
|
||||
@@ -1,23 +1,11 @@
|
||||
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 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`
|
||||
gap: 8px;
|
||||
display: flex;
|
||||
|
||||
@@ -1,24 +1,17 @@
|
||||
import { User } from "revolt.js";
|
||||
import { User } from "revolt.js/dist/maps/Users";
|
||||
|
||||
import { Checkbox, Row, Column } from "@revoltchat/ui";
|
||||
import Checkbox, { CheckboxProps } from "../../ui/Checkbox";
|
||||
|
||||
import UserIcon from "./UserIcon";
|
||||
import { Username } from "./UserShort";
|
||||
|
||||
type UserProps = { value: boolean; onChange: (v: boolean) => void; user: User };
|
||||
type UserProps = Omit<CheckboxProps, "children"> & { user: User };
|
||||
|
||||
export default function UserCheckbox({ user, ...props }: UserProps) {
|
||||
return (
|
||||
<Checkbox
|
||||
{...props}
|
||||
title={
|
||||
<Row centred>
|
||||
<UserIcon target={user} size={32} />
|
||||
<Column centred>
|
||||
<Username user={user} />
|
||||
</Column>
|
||||
</Row>
|
||||
}
|
||||
/>
|
||||
<Checkbox {...props}>
|
||||
<UserIcon target={user} size={32} />
|
||||
<Username user={user} />
|
||||
</Checkbox>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
import { Cog } from "@styled-icons/boxicons-solid";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { Link } from "react-router-dom";
|
||||
import { User } from "revolt.js";
|
||||
import styled from "styled-components/macro";
|
||||
import { User } from "revolt.js/dist/maps/Users";
|
||||
import styled from "styled-components";
|
||||
|
||||
import { openContextMenu } from "preact-context-menu";
|
||||
import { Text, Localizer } from "preact-i18n";
|
||||
|
||||
import { Header, IconButton } from "@revoltchat/ui";
|
||||
|
||||
import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice";
|
||||
|
||||
import { useIntermediate } from "../../../context/intermediate/Intermediate";
|
||||
|
||||
import Header from "../../ui/Header";
|
||||
import IconButton from "../../ui/IconButton";
|
||||
|
||||
import Tooltip from "../Tooltip";
|
||||
import UserStatus from "./UserStatus";
|
||||
|
||||
@@ -51,7 +52,7 @@ export default observer(({ user }: Props) => {
|
||||
const { writeClipboard } = useIntermediate();
|
||||
|
||||
return (
|
||||
<Header topBorder palette="secondary">
|
||||
<Header borders placement="secondary">
|
||||
<HeaderBase>
|
||||
<Localizer>
|
||||
<Tooltip content={<Text id="app.special.copy_username" />}>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { User } from "revolt.js";
|
||||
import styled from "styled-components/macro";
|
||||
import { User } from "revolt.js/dist/maps/Users";
|
||||
import styled from "styled-components";
|
||||
|
||||
import { Children } from "../../../types/Preact";
|
||||
import Tooltip from "../Tooltip";
|
||||
import { Username } from "./UserShort";
|
||||
import UserStatus from "./UserStatus";
|
||||
@@ -41,7 +42,10 @@ export default function UserHover({ user, children }: Props) {
|
||||
placement="right-end"
|
||||
content={
|
||||
<Base>
|
||||
<Username className="username" user={user} />
|
||||
<Username
|
||||
className="username"
|
||||
user={user}
|
||||
/>
|
||||
<span className="status">
|
||||
<UserStatus user={user} />
|
||||
</span>
|
||||
|
||||
@@ -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 { useParams } from "react-router-dom";
|
||||
import { User, API } from "revolt.js";
|
||||
import styled, { css } from "styled-components/macro";
|
||||
import { Masquerade } from "revolt-api/types/Channels";
|
||||
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 fallback from "../assets/user.png";
|
||||
@@ -16,20 +21,20 @@ type VoiceStatus = "muted" | "deaf";
|
||||
interface Props extends IconBaseProps<User> {
|
||||
status?: boolean;
|
||||
voice?: VoiceStatus;
|
||||
masquerade?: API.Masquerade;
|
||||
masquerade?: Masquerade;
|
||||
showServerIdentity?: boolean;
|
||||
}
|
||||
|
||||
export function useStatusColour(user?: User) {
|
||||
const theme = useApplicationState().settings.theme;
|
||||
const theme = useContext(ThemeContext);
|
||||
|
||||
return user?.online && user?.status?.presence !== "Invisible"
|
||||
? user?.status?.presence === "Idle"
|
||||
? theme.getVariable("status-away")
|
||||
: user?.status?.presence === "Busy"
|
||||
? theme.getVariable("status-busy")
|
||||
: theme.getVariable("status-online")
|
||||
: theme.getVariable("status-invisible");
|
||||
return user?.online && user?.status?.presence !== Presence.Invisible
|
||||
? user?.status?.presence === Presence.Idle
|
||||
? theme["status-away"]
|
||||
: user?.status?.presence === Presence.Busy
|
||||
? theme["status-busy"]
|
||||
: theme["status-online"]
|
||||
: theme["status-invisible"];
|
||||
}
|
||||
|
||||
const VoiceIndicator = styled.div<{ status: VoiceStatus }>`
|
||||
@@ -41,6 +46,10 @@ const VoiceIndicator = styled.div<{ status: VoiceStatus }>`
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
svg {
|
||||
stroke: white;
|
||||
}
|
||||
|
||||
${(props) =>
|
||||
(props.status === "muted" || props.status === "deaf") &&
|
||||
css`
|
||||
@@ -56,7 +65,7 @@ export default observer(
|
||||
keyof Props | "children" | "as"
|
||||
>,
|
||||
) => {
|
||||
const client = useApplicationState().client!;
|
||||
const client = useClient();
|
||||
|
||||
const {
|
||||
target,
|
||||
@@ -68,13 +77,12 @@ export default observer(
|
||||
hover,
|
||||
showServerIdentity,
|
||||
masquerade,
|
||||
innerRef,
|
||||
...svgProps
|
||||
} = props;
|
||||
|
||||
let { url } = props;
|
||||
if (masquerade?.avatar) {
|
||||
url = client.proxyFile(masquerade.avatar);
|
||||
url = masquerade.avatar;
|
||||
} else if (!url) {
|
||||
let override;
|
||||
if (target && showServerIdentity) {
|
||||
@@ -93,7 +101,7 @@ export default observer(
|
||||
|
||||
url =
|
||||
client.generateFileURL(
|
||||
override ?? target?.avatar ?? attachment ?? undefined,
|
||||
override ?? target?.avatar ?? attachment,
|
||||
{ max_side: 256 },
|
||||
animate,
|
||||
) ?? (target ? target.defaultAvatarURL : fallback);
|
||||
@@ -102,7 +110,6 @@ export default observer(
|
||||
return (
|
||||
<IconBase
|
||||
{...svgProps}
|
||||
ref={innerRef}
|
||||
width={size}
|
||||
height={size}
|
||||
hover={hover}
|
||||
@@ -114,7 +121,7 @@ export default observer(
|
||||
y="0"
|
||||
width="32"
|
||||
height="32"
|
||||
className="icon"
|
||||
class="icon"
|
||||
mask={mask ?? (status ? "url(#user)" : undefined)}>
|
||||
{<img src={url} draggable={false} loading="lazy" />}
|
||||
</foreignObject>
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { User, API } from "revolt.js";
|
||||
import styled from "styled-components/macro";
|
||||
import { Masquerade } from "revolt-api/types/Channels";
|
||||
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 { internalEmit } from "../../../lib/eventEmitter";
|
||||
@@ -20,9 +21,10 @@ const BotBadge = styled.div`
|
||||
padding: 0 4px;
|
||||
font-size: 0.6em;
|
||||
user-select: none;
|
||||
margin-inline-start: 4px;
|
||||
margin-inline-start: 2px;
|
||||
text-transform: uppercase;
|
||||
color: var(--accent-contrast);
|
||||
|
||||
color: var(--foreground);
|
||||
background: var(--accent);
|
||||
border-radius: calc(var(--border-radius) / 2);
|
||||
`;
|
||||
@@ -30,10 +32,8 @@ const BotBadge = styled.div`
|
||||
type UsernameProps = JSX.HTMLAttributes<HTMLElement> & {
|
||||
user?: User;
|
||||
prefixAt?: boolean;
|
||||
masquerade?: API.Masquerade;
|
||||
masquerade?: Masquerade;
|
||||
showServerIdentity?: boolean | "both";
|
||||
|
||||
innerRef?: Ref<any>;
|
||||
};
|
||||
|
||||
export const Username = observer(
|
||||
@@ -42,7 +42,6 @@ export const Username = observer(
|
||||
prefixAt,
|
||||
masquerade,
|
||||
showServerIdentity,
|
||||
innerRef,
|
||||
...otherProps
|
||||
}: UsernameProps) => {
|
||||
let username = user?.username;
|
||||
@@ -70,7 +69,7 @@ export const Username = observer(
|
||||
const srv = client.servers.get(member._id.server);
|
||||
if (srv?.roles) {
|
||||
for (const role of member.roles) {
|
||||
const c = srv.roles[role]?.colour;
|
||||
const c = srv.roles[role].colour;
|
||||
if (c) {
|
||||
color = c;
|
||||
continue;
|
||||
@@ -85,24 +84,20 @@ export const Username = observer(
|
||||
if (user?.bot) {
|
||||
return (
|
||||
<>
|
||||
<span {...otherProps} ref={innerRef} style={{ color }}>
|
||||
<span {...otherProps} style={{ color }}>
|
||||
{masquerade?.name ?? username ?? (
|
||||
<Text id="app.main.channel.unknown_user" />
|
||||
)}
|
||||
</span>
|
||||
<BotBadge>
|
||||
{masquerade ? (
|
||||
<Text id="app.main.channel.bridge" />
|
||||
) : (
|
||||
<Text id="app.main.channel.bot" />
|
||||
)}
|
||||
<Text id="app.main.channel.bot" />
|
||||
</BotBadge>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<span {...otherProps} ref={innerRef} style={{ color }}>
|
||||
<span {...otherProps} style={{ color }}>
|
||||
{prefixAt ? "@" : undefined}
|
||||
{masquerade?.name ?? username ?? (
|
||||
<Text id="app.main.channel.unknown_user" />
|
||||
@@ -122,7 +117,7 @@ export default function UserShort({
|
||||
user?: User;
|
||||
size?: number;
|
||||
prefixAt?: boolean;
|
||||
masquerade?: API.Masquerade;
|
||||
masquerade?: Masquerade;
|
||||
showServerIdentity?: boolean;
|
||||
}) {
|
||||
const { openScreen } = useIntermediate();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
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";
|
||||
|
||||
@@ -24,15 +25,15 @@ export default observer(({ user, tooltip }: Props) => {
|
||||
return <>{user.status.text}</>;
|
||||
}
|
||||
|
||||
if (user.status?.presence === "Busy") {
|
||||
if (user.status?.presence === Presence.Busy) {
|
||||
return <Text id="app.status.busy" />;
|
||||
}
|
||||
|
||||
if (user.status?.presence === "Idle") {
|
||||
if (user.status?.presence === Presence.Idle) {
|
||||
return <Text id="app.status.idle" />;
|
||||
}
|
||||
|
||||
if (user.status?.presence === "Invisible") {
|
||||
if (user.status?.presence === Presence.Invisible) {
|
||||
return <Text id="app.status.offline" />;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
.markdown {
|
||||
:global(.emoji) {
|
||||
object-fit: contain;
|
||||
|
||||
height: 1.25em;
|
||||
width: 1.25em;
|
||||
margin: 0 0.05em 0 0.1em;
|
||||
@@ -27,7 +25,6 @@
|
||||
|
||||
&[data-type="mention"] {
|
||||
padding: 0 6px;
|
||||
flex-shrink: 0;
|
||||
font-weight: 600;
|
||||
display: inline-block;
|
||||
background: var(--secondary-background);
|
||||
@@ -93,7 +90,6 @@
|
||||
|
||||
p > code {
|
||||
padding: 1px 4px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
code {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Suspense, lazy } from "preact/compat";
|
||||
const Renderer = lazy(() => import("./Renderer"));
|
||||
|
||||
export interface MarkdownProps {
|
||||
content?: string | null;
|
||||
content?: string;
|
||||
disallowBigEmoji?: boolean;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,10 @@ import "katex/dist/katex.min.css";
|
||||
import MarkdownIt from "markdown-it";
|
||||
// @ts-expect-error No typings.
|
||||
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 styles from "./Markdown.module.scss";
|
||||
@@ -13,12 +17,12 @@ import { useCallback, useContext } from "preact/hooks";
|
||||
import { internalEmit } from "../../lib/eventEmitter";
|
||||
import { determineLink } from "../../lib/links";
|
||||
|
||||
import { dayjs } from "../../context/Locale";
|
||||
import { useIntermediate } from "../../context/intermediate/Intermediate";
|
||||
import { AppContext } from "../../context/revoltjs/RevoltClient";
|
||||
|
||||
import { emojiDictionary } from "../../assets/emojis";
|
||||
import { generateEmoji } from "../common/Emoji";
|
||||
|
||||
import { emojiDictionary } from "../../assets/emojis";
|
||||
import { MarkdownProps } from "./Markdown";
|
||||
import Prism from "./prism";
|
||||
|
||||
@@ -59,6 +63,8 @@ export const md: MarkdownIt = MarkdownIt({
|
||||
.disable("image")
|
||||
.use(MarkdownEmoji, { defs: emojiDictionary })
|
||||
.use(MarkdownSpoilers)
|
||||
.use(MarkdownSup)
|
||||
.use(MarkdownSub)
|
||||
.use(MarkdownKatex, {
|
||||
throwOnError: false,
|
||||
maxExpand: 0,
|
||||
@@ -115,45 +121,16 @@ const RE_TWEMOJI = /:(\w+):/g;
|
||||
// ! FIXME: Move to library
|
||||
const RE_CHANNELS = /<#([A-z0-9]{26})>/g;
|
||||
|
||||
const RE_TIME = /<t:([0-9]+):(\w)>/g;
|
||||
|
||||
export default function Renderer({ content, disallowBigEmoji }: MarkdownProps) {
|
||||
const client = useContext(AppContext);
|
||||
const { openLink } = useIntermediate();
|
||||
|
||||
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 don't care if the mention changes.
|
||||
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[]) => {
|
||||
const id = args[0] as string,
|
||||
user = client.users.get(id);
|
||||
@@ -168,10 +145,7 @@ export default function Renderer({ content, disallowBigEmoji }: MarkdownProps) {
|
||||
const id = args[0] as string,
|
||||
channel = client.channels.get(id);
|
||||
|
||||
if (
|
||||
channel?.channel_type === "TextChannel" ||
|
||||
channel?.channel_type === "VoiceChannel"
|
||||
) {
|
||||
if (channel?.channel_type === "TextChannel") {
|
||||
return `[#${channel.name}](/server/${channel.server_id}/channel/${id})`;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,25 +1,14 @@
|
||||
import { Wrench } from "@styled-icons/boxicons-solid";
|
||||
import styled, { css } from "styled-components/macro";
|
||||
import styled from "styled-components";
|
||||
|
||||
import UpdateIndicator from "../common/UpdateIndicator";
|
||||
|
||||
interface Props {
|
||||
overlay?: boolean;
|
||||
}
|
||||
|
||||
const TitlebarBase = styled.div<Props>`
|
||||
const TitlebarBase = styled.div`
|
||||
height: var(--titlebar-height);
|
||||
display: flex;
|
||||
user-select: none;
|
||||
align-items: center;
|
||||
|
||||
${(props) =>
|
||||
props.overlay &&
|
||||
css`
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
`}
|
||||
|
||||
.drag {
|
||||
flex-grow: 1;
|
||||
-webkit-app-region: drag;
|
||||
@@ -95,10 +84,10 @@ const TitlebarBase = styled.div<Props>`
|
||||
}
|
||||
`;
|
||||
|
||||
export function Titlebar(props: Props) {
|
||||
export function Titlebar() {
|
||||
return (
|
||||
<TitlebarBase {...props}>
|
||||
<div className="title">
|
||||
<TitlebarBase>
|
||||
<div class="title">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 193.733 37.438">
|
||||
@@ -114,7 +103,7 @@ export function Titlebar(props: Props) {
|
||||
<Wrench size="12.5" />
|
||||
)}
|
||||
</div>
|
||||
{/*<div className="actions quick">
|
||||
{/*<div class="actions quick">
|
||||
<Tooltip
|
||||
content="Mute"
|
||||
placement="bottom">
|
||||
@@ -130,9 +119,9 @@ export function Titlebar(props: Props) {
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>*/}
|
||||
<div className="drag" />
|
||||
<div class="drag" />
|
||||
<UpdateIndicator style="titlebar" />
|
||||
<div className="actions">
|
||||
<div class="actions">
|
||||
<div onClick={window.native.min}>
|
||||
<svg
|
||||
aria-hidden="false"
|
||||
@@ -164,7 +153,7 @@ export function Titlebar(props: Props) {
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div onClick={window.native.close} className="error">
|
||||
<div onClick={window.native.close} class="error">
|
||||
<svg
|
||||
aria-hidden="false"
|
||||
width="12"
|
||||
|
||||
@@ -1,43 +1,32 @@
|
||||
import { Message, Group, Compass } from "@styled-icons/boxicons-solid";
|
||||
import { Message, Group } from "@styled-icons/boxicons-solid";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useHistory, useLocation } from "react-router";
|
||||
import styled, { css } from "styled-components/macro";
|
||||
|
||||
import { Centred, IconButton } from "@revoltchat/ui";
|
||||
import styled, { css } from "styled-components";
|
||||
|
||||
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 UserIcon from "../common/user/UserIcon";
|
||||
import IconButton from "../ui/IconButton";
|
||||
|
||||
const Base = styled.div`
|
||||
background: var(--secondary-background);
|
||||
`;
|
||||
|
||||
const Navbar = styled.div`
|
||||
z-index: 500;
|
||||
display: flex;
|
||||
margin: 0 auto;
|
||||
z-index: 100;
|
||||
max-width: 500px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
height: var(--bottom-navigation-height);
|
||||
`;
|
||||
|
||||
/**
|
||||
* I've decided that this whole component
|
||||
* needs to be re-written 👍👍👍👍👍👍
|
||||
*/
|
||||
|
||||
const Button = styled.a<{ active: boolean }>`
|
||||
flex: 1;
|
||||
color: var(--foreground);
|
||||
|
||||
// ok
|
||||
* {
|
||||
color: var(--foreground) !important;
|
||||
}
|
||||
|
||||
> a,
|
||||
> div,
|
||||
@@ -58,48 +47,50 @@ const Button = styled.a<{ active: boolean }>`
|
||||
`}
|
||||
`;
|
||||
|
||||
export default observer(() => {
|
||||
interface Props {
|
||||
lastOpened: LastOpened;
|
||||
}
|
||||
|
||||
export const BottomNavigation = observer(({ lastOpened }: Props) => {
|
||||
const client = useClient();
|
||||
const layout = useApplicationState().layout;
|
||||
const user = client.users.get(client.user!._id);
|
||||
|
||||
const history = useHistory();
|
||||
const path = useLocation().pathname;
|
||||
|
||||
const channel_id = lastOpened["home"];
|
||||
|
||||
const friendsActive = path.startsWith("/friends");
|
||||
const settingsActive = path.startsWith("/settings");
|
||||
const discoverActive = path.startsWith("/discover");
|
||||
const homeActive = !(friendsActive || settingsActive || discoverActive);
|
||||
const homeActive = !(friendsActive || settingsActive);
|
||||
|
||||
return (
|
||||
<Base>
|
||||
<Navbar>
|
||||
<Button active={homeActive}>
|
||||
<Centred
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
if (settingsActive) {
|
||||
if (history.length > 0) {
|
||||
history.replace(layout.getLastPath());
|
||||
return;
|
||||
history.goBack();
|
||||
}
|
||||
}
|
||||
|
||||
const path = layout.getLastHomePath();
|
||||
if (path.startsWith("/friends")) {
|
||||
history.push("/");
|
||||
if (channel_id) {
|
||||
history.push(`/channel/${channel_id}`);
|
||||
} else {
|
||||
history.push(path);
|
||||
history.push("/");
|
||||
}
|
||||
}}>
|
||||
<Message size={24} />
|
||||
</Centred>
|
||||
</IconButton>
|
||||
</Button>
|
||||
<Button active={friendsActive}>
|
||||
<IconButton>
|
||||
<ConditionalLink active={friendsActive} to="/friends">
|
||||
<ConditionalLink active={friendsActive} to="/friends">
|
||||
<IconButton>
|
||||
<Group size={25} />
|
||||
</ConditionalLink>
|
||||
</IconButton>
|
||||
</IconButton>
|
||||
</ConditionalLink>
|
||||
</Button>
|
||||
{/*<Button active={searchActive}>
|
||||
<ConditionalLink active={searchActive} to="/search">
|
||||
@@ -115,23 +106,20 @@ export default observer(() => {
|
||||
</IconButton>
|
||||
</ConditionalLink>
|
||||
</Button>*/}
|
||||
<Button active={discoverActive}>
|
||||
<IconButton>
|
||||
<ConditionalLink
|
||||
active={discoverActive}
|
||||
to="/discover/servers">
|
||||
<Compass size={24} />
|
||||
</ConditionalLink>
|
||||
</IconButton>
|
||||
</Button>
|
||||
<Button active={settingsActive}>
|
||||
<IconButton>
|
||||
<ConditionalLink active={settingsActive} to="/settings">
|
||||
<ConditionalLink active={settingsActive} to="/settings">
|
||||
<IconButton>
|
||||
<UserIcon target={user} size={26} status={true} />
|
||||
</ConditionalLink>
|
||||
</IconButton>
|
||||
</IconButton>
|
||||
</ConditionalLink>
|
||||
</Button>
|
||||
</Navbar>
|
||||
</Base>
|
||||
);
|
||||
});
|
||||
|
||||
export default connectState(BottomNavigation, (state) => {
|
||||
return {
|
||||
lastOpened: state.lastOpened,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1,23 +1,14 @@
|
||||
import { observer } from "mobx-react-lite";
|
||||
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 HomeSidebar from "./left/HomeSidebar";
|
||||
import ServerListSidebar from "./left/ServerListSidebar";
|
||||
import ServerSidebar from "./left/ServerSidebar";
|
||||
import { useSelector } from "react-redux";
|
||||
import { State } from "../../redux";
|
||||
|
||||
export default observer(() => {
|
||||
const layout = useApplicationState().layout;
|
||||
const { pathname } = useLocation();
|
||||
const isOpen =
|
||||
!pathname.startsWith("/discover") &&
|
||||
(isTouchscreenDevice || layout.getSectionState(SIDEBAR_CHANNELS, true));
|
||||
export default function LeftSidebar() {
|
||||
const isOpen = useSelector((state: State) => state.sectionToggle['sidebar_channels'] ?? true)
|
||||
|
||||
return (
|
||||
<SidebarBase>
|
||||
@@ -42,4 +33,4 @@ export default observer(() => {
|
||||
</Switch>
|
||||
</SidebarBase>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import styled, { css } from "styled-components/macro";
|
||||
import styled, { css } from "styled-components";
|
||||
|
||||
import { isTouchscreenDevice } from "../../lib/isTouchscreenDevice";
|
||||
|
||||
@@ -8,13 +8,6 @@ export default styled.div`
|
||||
user-select: none;
|
||||
flex-direction: row;
|
||||
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<{
|
||||
@@ -28,15 +21,10 @@ export const GenericSidebarBase = styled.div<{
|
||||
/*border-end-start-radius: 8px;*/
|
||||
background: var(--secondary-background);
|
||||
|
||||
/*> :nth-child(1) {
|
||||
//border-end-start-radius: 8px;
|
||||
> :nth-child(1) {
|
||||
border-end-start-radius: 8px;
|
||||
}
|
||||
|
||||
> :nth-child(2) {
|
||||
margin-top: 48px;
|
||||
background: red;
|
||||
}*/
|
||||
|
||||
${(props) =>
|
||||
props.mobilePadding &&
|
||||
isTouchscreenDevice &&
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
import { X } from "@styled-icons/boxicons-regular";
|
||||
import { Crown } from "@styled-icons/boxicons-solid";
|
||||
import { X, Crown } from "@styled-icons/boxicons-regular";
|
||||
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 classNames from "classnames";
|
||||
import { useTriggerEvents } from "preact-context-menu";
|
||||
import { attachContextMenu } from "preact-context-menu";
|
||||
import { Localizer, Text } from "preact-i18n";
|
||||
|
||||
import { IconButton } from "@revoltchat/ui";
|
||||
|
||||
import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice";
|
||||
import { stopPropagation } from "../../../lib/stopPropagation";
|
||||
|
||||
@@ -20,6 +19,9 @@ import Tooltip from "../../common/Tooltip";
|
||||
import UserIcon from "../../common/user/UserIcon";
|
||||
import { Username } from "../../common/user/UserShort";
|
||||
import UserStatus from "../../common/user/UserStatus";
|
||||
import IconButton from "../../ui/IconButton";
|
||||
|
||||
import { Children } from "../../../types/Preact";
|
||||
|
||||
type CommonProps = Omit<
|
||||
JSX.HTMLAttributes<HTMLDivElement>,
|
||||
@@ -61,9 +63,9 @@ export const UserButton = observer((props: UserProps) => {
|
||||
data-alert={typeof alert === "string"}
|
||||
data-online={
|
||||
typeof channel !== "undefined" ||
|
||||
(user.online && user.status?.presence !== "Invisible")
|
||||
(user.online && user.status?.presence !== Presence.Invisible)
|
||||
}
|
||||
{...useTriggerEvents("Menu", {
|
||||
onContextMenu={attachContextMenu("Menu", {
|
||||
user: user._id,
|
||||
channel: channel?._id,
|
||||
unread: alert,
|
||||
@@ -86,7 +88,7 @@ export const UserButton = observer((props: UserProps) => {
|
||||
alert ? (
|
||||
channel.last_message.content.slice(0, 32)
|
||||
) : (
|
||||
<UserStatus user={user} tooltip />
|
||||
<UserStatus user={user} />
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
@@ -150,23 +152,24 @@ export const ChannelButton = observer((props: ChannelProps) => {
|
||||
}
|
||||
|
||||
const { openScreen } = useIntermediate();
|
||||
const alerting = alert && !muted && !active;
|
||||
|
||||
return (
|
||||
<div
|
||||
{...divProps}
|
||||
data-active={active}
|
||||
data-alert={alerting}
|
||||
data-alert={typeof alert === "string" && !muted}
|
||||
data-muted={muted}
|
||||
aria-label={channel.name}
|
||||
className={classNames(styles.item, { [styles.compact]: compact })}
|
||||
{...useTriggerEvents("Menu", {
|
||||
onContextMenu={attachContextMenu("Menu", {
|
||||
channel: channel._id,
|
||||
unread: !!alert,
|
||||
})}>
|
||||
<div className={styles.avatar}>
|
||||
<ChannelIcon target={channel} size={compact ? 24 : 32} />
|
||||
</div>
|
||||
<ChannelIcon
|
||||
className={styles.avatar}
|
||||
target={channel}
|
||||
size={compact ? 24 : 32}
|
||||
/>
|
||||
<div className={styles.name}>
|
||||
<div>{channel.name}</div>
|
||||
{channel.channel_type === "Group" && (
|
||||
@@ -186,7 +189,7 @@ export const ChannelButton = observer((props: ChannelProps) => {
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.button}>
|
||||
{alerting && (
|
||||
{alert && !muted && (
|
||||
<div className={styles.alert} data-style={alert}>
|
||||
{alertCount}
|
||||
</div>
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
import { Text } from "preact-i18n";
|
||||
import { useContext } from "preact/hooks";
|
||||
|
||||
import { Banner } from "@revoltchat/ui";
|
||||
|
||||
import {
|
||||
ClientStatus,
|
||||
StatusContext,
|
||||
useClient,
|
||||
} from "../../../context/revoltjs/RevoltClient";
|
||||
|
||||
import Banner from "../../ui/Banner";
|
||||
|
||||
export default function ConnectionStatus() {
|
||||
const status = useContext(StatusContext);
|
||||
const client = useClient();
|
||||
|
||||
if (status === ClientStatus.OFFLINE) {
|
||||
return (
|
||||
@@ -22,10 +20,7 @@ export default function ConnectionStatus() {
|
||||
} else if (status === ClientStatus.DISCONNECTED) {
|
||||
return (
|
||||
<Banner>
|
||||
<Text id="app.special.status.disconnected" /> <br />
|
||||
<a onClick={() => client.websocket.connect()}>
|
||||
<Text id="app.special.status.reconnect" />
|
||||
</a>
|
||||
<Text id="app.special.status.disconnected" />
|
||||
</Banner>
|
||||
);
|
||||
} else if (status === ClientStatus.CONNECTING) {
|
||||
|
||||
@@ -117,7 +117,7 @@
|
||||
}
|
||||
|
||||
&[data-muted="true"] {
|
||||
opacity: 0.4;
|
||||
color: var(--tertiary-foreground);
|
||||
}
|
||||
|
||||
&[data-alert="true"],
|
||||
@@ -158,10 +158,10 @@
|
||||
|
||||
@media (pointer: coarse) {
|
||||
.item {
|
||||
height: 50px;
|
||||
height: 40px;
|
||||
|
||||
&.compact {
|
||||
height: 50px;
|
||||
height: var(--bottom-navigation-height);
|
||||
|
||||
> div {
|
||||
gap: 20px;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { Plus } from "@styled-icons/boxicons-regular";
|
||||
import {
|
||||
Home,
|
||||
UserDetail,
|
||||
@@ -6,77 +5,67 @@ import {
|
||||
Notepad,
|
||||
} from "@styled-icons/boxicons-solid";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { Link, useLocation, useParams } from "react-router-dom";
|
||||
import styled, { css } from "styled-components/macro";
|
||||
import { Link, Redirect, useLocation, useParams } from "react-router-dom";
|
||||
import { RelationshipStatus } from "revolt-api/types/Users";
|
||||
|
||||
import { Text } from "preact-i18n";
|
||||
import { useContext, useEffect } from "preact/hooks";
|
||||
|
||||
import { Category, IconButton } from "@revoltchat/ui";
|
||||
|
||||
import ConditionalLink from "../../../lib/ConditionalLink";
|
||||
import PaintCounter from "../../../lib/PaintCounter";
|
||||
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 { AppContext } from "../../../context/revoltjs/RevoltClient";
|
||||
|
||||
import Category from "../../ui/Category";
|
||||
import placeholderSVG from "../items/placeholder.svg";
|
||||
import { mapChannelWithUnread, useUnreads } from "./common";
|
||||
|
||||
import { GenericSidebarBase, GenericSidebarList } from "../SidebarBase";
|
||||
import ButtonItem, { ChannelButton } from "../items/ButtonItem";
|
||||
import ConnectionStatus from "../items/ConnectionStatus";
|
||||
|
||||
const Navbar = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 14px;
|
||||
font-weight: 600;
|
||||
flex-shrink: 0;
|
||||
height: 48px;
|
||||
type Props = {
|
||||
unreads: Unreads;
|
||||
};
|
||||
|
||||
${() =>
|
||||
isTouchscreenDevice &&
|
||||
css`
|
||||
height: 56px;
|
||||
`}
|
||||
`;
|
||||
|
||||
export default observer(() => {
|
||||
const HomeSidebar = observer((props: Props) => {
|
||||
const { pathname } = useLocation();
|
||||
const client = useContext(AppContext);
|
||||
const state = useApplicationState();
|
||||
const { channel: channel_id } = useParams<{ channel: string }>();
|
||||
const { channel } = useParams<{ channel: string }>();
|
||||
const { openScreen } = useIntermediate();
|
||||
|
||||
const channels = [...client.channels.values()].filter(
|
||||
(x) =>
|
||||
(x.channel_type === "DirectMessage" && x.active) ||
|
||||
x.channel_type === "Group",
|
||||
);
|
||||
const channels = [...client.channels.values()]
|
||||
.filter(
|
||||
(x) =>
|
||||
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
|
||||
// Track what page the user was last on (in home page).
|
||||
useEffect(() => state.layout.setLastHomePath(pathname), [pathname]);
|
||||
useEffect(() => {
|
||||
if (!channel) return;
|
||||
|
||||
channels.sort((b, a) =>
|
||||
a.last_message_id_or_past.localeCompare(b.last_message_id_or_past),
|
||||
);
|
||||
dispatch({
|
||||
type: "LAST_OPENED_SET",
|
||||
parent: "home",
|
||||
child: channel,
|
||||
});
|
||||
}, [channel]);
|
||||
|
||||
// ! FIXME: must be a better way
|
||||
const incoming = [...client.users.values()].filter(
|
||||
(user) => user?.relationship === "Incoming",
|
||||
);
|
||||
channels.sort((b, a) => a.timestamp.localeCompare(b.timestamp));
|
||||
|
||||
return (
|
||||
<GenericSidebarBase mobilePadding>
|
||||
<Navbar>
|
||||
<Text id="app.home.directs" />
|
||||
</Navbar>
|
||||
<ConnectionStatus />
|
||||
<GenericSidebarList>
|
||||
<ConditionalLink active={pathname === "/"} to="/">
|
||||
@@ -95,9 +84,14 @@ export default observer(() => {
|
||||
<ButtonItem
|
||||
active={pathname === "/friends"}
|
||||
alert={
|
||||
incoming.length > 0 ? "mention" : undefined
|
||||
}
|
||||
alertCount={incoming.length}>
|
||||
typeof [...client.users.values()].find(
|
||||
(user) =>
|
||||
user?.relationship ===
|
||||
RelationshipStatus.Incoming,
|
||||
) !== "undefined"
|
||||
? "unread"
|
||||
: undefined
|
||||
}>
|
||||
<UserDetail size={20} />
|
||||
<span>
|
||||
<Text id="app.navigation.tabs.friends" />
|
||||
@@ -107,10 +101,9 @@ export default observer(() => {
|
||||
</>
|
||||
)}
|
||||
<ConditionalLink
|
||||
active={channel?.channel_type === "SavedMessages"}
|
||||
active={obj?.channel_type === "SavedMessages"}
|
||||
to="/open/saved">
|
||||
<ButtonItem
|
||||
active={channel?.channel_type === "SavedMessages"}>
|
||||
<ButtonItem active={obj?.channel_type === "SavedMessages"}>
|
||||
<Notepad size={20} />
|
||||
<span>
|
||||
<Text id="app.navigation.tabs.saved" />
|
||||
@@ -127,52 +120,43 @@ export default observer(() => {
|
||||
</ButtonItem>
|
||||
</Link>
|
||||
)}
|
||||
<Category>
|
||||
<Text id="app.main.categories.conversations" />
|
||||
<IconButton
|
||||
onClick={() =>
|
||||
openScreen({
|
||||
id: "special_input",
|
||||
type: "create_group",
|
||||
})
|
||||
}>
|
||||
<Plus size={16} />
|
||||
</IconButton>
|
||||
</Category>
|
||||
<Category
|
||||
text={<Text id="app.main.categories.conversations" />}
|
||||
action={() =>
|
||||
openScreen({
|
||||
id: "special_input",
|
||||
type: "create_group",
|
||||
})
|
||||
}
|
||||
/>
|
||||
{channels.length === 0 && (
|
||||
<img src={placeholderSVG} loading="eager" />
|
||||
)}
|
||||
{channels.map((channel) => {
|
||||
{channels.map((x) => {
|
||||
let user;
|
||||
if (channel.channel_type === "DirectMessage") {
|
||||
if (!channel.active) return null;
|
||||
user = channel.recipient;
|
||||
if (x.channel.channel_type === "DirectMessage") {
|
||||
if (!x.channel.active) return null;
|
||||
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 (
|
||||
<ConditionalLink
|
||||
key={channel._id}
|
||||
active={channel._id === channel_id}
|
||||
to={`/channel/${channel._id}`}>
|
||||
key={x.channel._id}
|
||||
active={x.channel._id === channel}
|
||||
to={`/channel/${x.channel._id}`}>
|
||||
<ChannelButton
|
||||
user={user}
|
||||
channel={channel}
|
||||
alert={
|
||||
mentionCount > 0
|
||||
? "mention"
|
||||
: isUnread
|
||||
? "unread"
|
||||
: undefined
|
||||
}
|
||||
alertCount={mentionCount}
|
||||
active={channel._id === channel_id}
|
||||
channel={x.channel}
|
||||
alert={x.unread}
|
||||
alertCount={x.alertCount}
|
||||
active={x.channel._id === channel}
|
||||
/>
|
||||
</ConditionalLink>
|
||||
);
|
||||
@@ -182,3 +166,13 @@ export default observer(() => {
|
||||
</GenericSidebarBase>
|
||||
);
|
||||
});
|
||||
|
||||
export default connectState(
|
||||
HomeSidebar,
|
||||
(state) => {
|
||||
return {
|
||||
unreads: state.unreads,
|
||||
};
|
||||
},
|
||||
true,
|
||||
);
|
||||
|
||||
@@ -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 { 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 { useClient } from "../../../context/revoltjs/RevoltClient";
|
||||
|
||||
/**
|
||||
* Server list sidebar shim component
|
||||
*/
|
||||
export default observer(() => {
|
||||
const client = useClient();
|
||||
const state = useApplicationState();
|
||||
const { openScreen } = useIntermediate();
|
||||
const { server: server_id } = useParams<{ server?: string }>();
|
||||
import ServerIcon from "../../common/ServerIcon";
|
||||
import Tooltip from "../../common/Tooltip";
|
||||
import UserHover from "../../common/user/UserHover";
|
||||
import UserIcon from "../../common/user/UserIcon";
|
||||
import IconButton from "../../ui/IconButton";
|
||||
import LineDivider from "../../ui/LineDivider";
|
||||
import { mapChannelWithUnread } from "./common";
|
||||
|
||||
const createServer = useCallback(
|
||||
() =>
|
||||
openScreen({
|
||||
id: "special_input",
|
||||
type: "create_server",
|
||||
}),
|
||||
[],
|
||||
import { Children } from "../../../types/Preact";
|
||||
|
||||
function Icon({
|
||||
children,
|
||||
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 (
|
||||
<ServerList
|
||||
client={client}
|
||||
active={server_id}
|
||||
createServer={createServer}
|
||||
permit={state.notifications}
|
||||
home={state.layout.getLastHomePath}
|
||||
servers={state.ordering.orderedServers}
|
||||
reorder={state.ordering.reorderServer}
|
||||
/>
|
||||
<ServersBase>
|
||||
<ServerList>
|
||||
<ConditionalLink
|
||||
active={homeActive}
|
||||
to={lastOpened.home ? `/channel/${lastOpened.home}` : "/"}>
|
||||
<ServerEntry home active={homeActive}>
|
||||
<Swoosh />
|
||||
<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,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1,26 +1,35 @@
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { Redirect, useParams } from "react-router";
|
||||
import styled, { css } from "styled-components/macro";
|
||||
import styled, { css } from "styled-components";
|
||||
|
||||
import { useTriggerEvents } from "preact-context-menu";
|
||||
import { attachContextMenu } from "preact-context-menu";
|
||||
import { useEffect } from "preact/hooks";
|
||||
|
||||
import { Category } from "@revoltchat/ui";
|
||||
|
||||
import ConditionalLink from "../../../lib/ConditionalLink";
|
||||
import PaintCounter from "../../../lib/PaintCounter";
|
||||
import { internalEmit } from "../../../lib/eventEmitter";
|
||||
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 CollapsibleSection from "../../common/CollapsibleSection";
|
||||
import ServerHeader from "../../common/ServerHeader";
|
||||
import Category from "../../ui/Category";
|
||||
import { mapChannelWithUnread, useUnreads } from "./common";
|
||||
|
||||
import { ChannelButton } from "../items/ButtonItem";
|
||||
import ConnectionStatus from "../items/ConnectionStatus";
|
||||
|
||||
interface Props {
|
||||
unreads: Unreads;
|
||||
notifications: Notifications;
|
||||
}
|
||||
|
||||
const ServerBase = styled.div`
|
||||
height: 100%;
|
||||
width: 232px;
|
||||
@@ -48,9 +57,8 @@ const ServerList = styled.div`
|
||||
}
|
||||
`;
|
||||
|
||||
export default observer(() => {
|
||||
const ServerSidebar = observer((props: Props) => {
|
||||
const client = useClient();
|
||||
const state = useApplicationState();
|
||||
const { server: server_id, channel: channel_id } =
|
||||
useParams<{ server: string; channel?: string }>();
|
||||
|
||||
@@ -59,13 +67,25 @@ export default observer(() => {
|
||||
|
||||
const channel = channel_id ? client.channels.get(channel_id) : undefined;
|
||||
|
||||
// ! FIXME: move this globally
|
||||
// Track which channel the user was last on.
|
||||
// The user selected no channel, let's see if there's a channel available
|
||||
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(() => {
|
||||
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]);
|
||||
|
||||
const uncategorised = new Set(server.channel_ids);
|
||||
@@ -76,8 +96,7 @@ export default observer(() => {
|
||||
if (!entry) return;
|
||||
|
||||
const active = channel?._id === entry._id;
|
||||
const isUnread = entry.isUnread(state.notifications);
|
||||
const mentionCount = entry.getMentions(state.notifications);
|
||||
const muted = props.notifications[id] === "none";
|
||||
|
||||
return (
|
||||
<ConditionalLink
|
||||
@@ -98,15 +117,10 @@ export default observer(() => {
|
||||
<ChannelButton
|
||||
channel={entry}
|
||||
active={active}
|
||||
alert={
|
||||
mentionCount.length > 0
|
||||
? "mention"
|
||||
: isUnread
|
||||
? "unread"
|
||||
: undefined
|
||||
}
|
||||
// ! FIXME: pull it out directly
|
||||
alert={mapChannelWithUnread(entry, props.unreads).unread}
|
||||
compact
|
||||
muted={state.notifications.isMuted(entry)}
|
||||
muted={muted}
|
||||
/>
|
||||
</ConditionalLink>
|
||||
);
|
||||
@@ -124,7 +138,7 @@ export default observer(() => {
|
||||
<CollapsibleSection
|
||||
id={`category_${category.id}`}
|
||||
defaultValue
|
||||
summary={<Category>{category.title}</Category>}>
|
||||
summary={<Category text={category.title} />}>
|
||||
{channels}
|
||||
</CollapsibleSection>,
|
||||
);
|
||||
@@ -140,7 +154,7 @@ export default observer(() => {
|
||||
<ServerHeader server={server} />
|
||||
<ConnectionStatus />
|
||||
<ServerList
|
||||
{...useTriggerEvents("Menu", {
|
||||
onContextMenu={attachContextMenu("Menu", {
|
||||
server_list: server._id,
|
||||
})}>
|
||||
{elements}
|
||||
@@ -149,3 +163,10 @@ export default observer(() => {
|
||||
</ServerBase>
|
||||
);
|
||||
});
|
||||
|
||||
export default connectState(ServerSidebar, (state) => {
|
||||
return {
|
||||
unreads: state.unreads,
|
||||
notifications: state.notifications,
|
||||
};
|
||||
});
|
||||
|
||||
79
src/components/navigation/left/common.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
/* eslint-disable react-hooks/rules-of-hooks */
|
||||
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";
|
||||
|
||||
|
||||
@@ -1,22 +1,21 @@
|
||||
import { Link } from "react-router-dom";
|
||||
import { GroupedVirtuoso } from "react-virtuoso";
|
||||
import { Channel, User } from "revolt.js";
|
||||
import styled, { css } from "styled-components/macro";
|
||||
import { Channel } from "revolt.js/dist/maps/Channels";
|
||||
import { User } from "revolt.js/dist/maps/Users";
|
||||
import styled, { css } from "styled-components";
|
||||
|
||||
import { Text } from "preact-i18n";
|
||||
import { memo } from "preact/compat";
|
||||
|
||||
import { internalEmit } from "../../../lib/eventEmitter";
|
||||
|
||||
import {
|
||||
Screen,
|
||||
useIntermediate,
|
||||
} from "../../../context/intermediate/Intermediate";
|
||||
|
||||
import { UserButton } from "../items/ButtonItem";
|
||||
import { internalEmit } from "../../../lib/eventEmitter";
|
||||
|
||||
export type MemberListGroup = {
|
||||
type: "online" | "offline" | "role" | "no_offline";
|
||||
type: "online" | "offline" | "role";
|
||||
name?: string;
|
||||
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(
|
||||
({
|
||||
item,
|
||||
@@ -69,7 +54,7 @@ const ItemContent = memo(
|
||||
user={item}
|
||||
margin
|
||||
context={context}
|
||||
onClick={(e) => {
|
||||
onClick={e => {
|
||||
if (e.shiftKey) {
|
||||
internalEmit(
|
||||
"MessageBox",
|
||||
@@ -77,13 +62,12 @@ const ItemContent = memo(
|
||||
`<@${item._id}>`,
|
||||
"mention",
|
||||
);
|
||||
} else
|
||||
[
|
||||
openScreen({
|
||||
id: "profile",
|
||||
user_id: item._id,
|
||||
}),
|
||||
];
|
||||
} else[
|
||||
openScreen({
|
||||
id: "profile",
|
||||
user_id: item._id,
|
||||
})
|
||||
]
|
||||
}}
|
||||
/>
|
||||
),
|
||||
@@ -102,22 +86,18 @@ export default function MemberList({
|
||||
<GroupedVirtuoso
|
||||
groupCounts={entries.map((x) => x.users.length)}
|
||||
groupContent={(index) => {
|
||||
const entry = entries[index];
|
||||
const type = entries[index].type;
|
||||
return (
|
||||
<ListCategory first={index === 0}>
|
||||
{entry.type === "role" ? (
|
||||
<>{entry.name}</>
|
||||
) : entry.type === "online" ? (
|
||||
{type === "role" ? (
|
||||
<>{entries[index].name}</>
|
||||
) : type === "online" ? (
|
||||
<Text id="app.status.online" />
|
||||
) : (
|
||||
<Text id="app.status.offline" />
|
||||
)}
|
||||
{entry.type !== "no_offline" && (
|
||||
<>
|
||||
{" - "}
|
||||
{entry.users.length}
|
||||
</>
|
||||
)}
|
||||
{" - "}
|
||||
{entries[index].users.length}
|
||||
</ListCategory>
|
||||
);
|
||||
}}
|
||||
@@ -128,32 +108,7 @@ export default function MemberList({
|
||||
.slice(0, groupIndex)
|
||||
.reduce((a, b) => a + b.users.length, 0);
|
||||
|
||||
const entry = entries[groupIndex];
|
||||
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" rel="noreferrer">
|
||||
#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];
|
||||
const item = entries[groupIndex].users[relativeIndex];
|
||||
if (!item) return null;
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
/* eslint-disable react-hooks/rules-of-hooks */
|
||||
import { autorun } from "mobx";
|
||||
import { observer } from "mobx-react-lite";
|
||||
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 {
|
||||
ClientStatus,
|
||||
@@ -30,15 +33,9 @@ export default function MemberSidebar() {
|
||||
}
|
||||
}
|
||||
|
||||
function useEntries(
|
||||
channel: Channel,
|
||||
generateKeys: () => string[],
|
||||
isServer?: boolean,
|
||||
) {
|
||||
function useEntries(channel: Channel, keys: string[], isServer?: boolean) {
|
||||
const client = channel.client;
|
||||
const [entries, setEntries] = useState<MemberListGroup[]>([]);
|
||||
|
||||
function sort(keys: string[]) {
|
||||
return useMemo(() => {
|
||||
const categories: { [key: string]: [User, string][] } = {
|
||||
online: [],
|
||||
offline: [],
|
||||
@@ -58,7 +55,7 @@ function useEntries(
|
||||
.map((id) => {
|
||||
return [id, roles![id], roles![id].rank ?? 0] as [
|
||||
string,
|
||||
API.Role,
|
||||
Role,
|
||||
number,
|
||||
];
|
||||
})
|
||||
@@ -92,7 +89,7 @@ function useEntries(
|
||||
const sort = member?.nickname ?? u.username;
|
||||
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);
|
||||
} else {
|
||||
if (isServer) {
|
||||
@@ -141,94 +138,48 @@ function useEntries(
|
||||
});
|
||||
}
|
||||
|
||||
// ! FIXME: Temporary performance fix
|
||||
if (shouldSkipOffline(channel.server_id!)) {
|
||||
entries.push({
|
||||
type: "no_offline",
|
||||
users: [null!],
|
||||
});
|
||||
} else if (categories.offline.length > 0) {
|
||||
if (categories.offline.length > 0) {
|
||||
entries.push({
|
||||
type: "offline",
|
||||
users: categories.offline.map((x) => x[0]),
|
||||
});
|
||||
}
|
||||
|
||||
setEntries(entries);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
return autorun(() => sort(generateKeys()));
|
||||
return entries;
|
||||
// eslint-disable-next-line
|
||||
}, [channel]);
|
||||
|
||||
return entries;
|
||||
}, [keys]);
|
||||
}
|
||||
|
||||
export const GroupMemberSidebar = observer(
|
||||
({ channel }: { channel: Channel }) => {
|
||||
const entries = useEntries(channel, () => channel.recipient_ids!);
|
||||
const keys = [...channel.recipient_ids!];
|
||||
const entries = useEntries(channel, keys);
|
||||
|
||||
return (
|
||||
<GenericSidebarBase data-scroll-offset="with-padding">
|
||||
{/*<Container>
|
||||
{isTouchscreenDevice && <div>Group settings go here</div>}
|
||||
</Container>*/}
|
||||
|
||||
<GenericSidebarBase>
|
||||
<MemberList entries={entries} context={channel} />
|
||||
</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(
|
||||
({ channel }: { channel: Channel }) => {
|
||||
const client = useClient();
|
||||
const status = useContext(StatusContext);
|
||||
|
||||
useEffect(() => {
|
||||
const server_id = channel.server_id!;
|
||||
if (status === ClientStatus.ONLINE && !FETCHED.has(server_id)) {
|
||||
FETCHED.add(server_id);
|
||||
channel
|
||||
.server!.syncMembers(shouldSkipOffline(server_id))
|
||||
.catch(() => FETCHED.delete(server_id));
|
||||
if (status === ClientStatus.ONLINE) {
|
||||
channel.server!.fetchMembers();
|
||||
}
|
||||
}, [status, channel]);
|
||||
// eslint-disable-next-line
|
||||
}, [status, channel.server_id]);
|
||||
|
||||
const entries = useEntries(
|
||||
channel,
|
||||
() => [...client.members.keys()],
|
||||
true,
|
||||
);
|
||||
const keys = [...client.members.keys()];
|
||||
const entries = useEntries(channel, keys, true);
|
||||
|
||||
return (
|
||||
<GenericSidebarBase data-scroll-offset="with-padding">
|
||||
{/*<Container>
|
||||
{isTouchscreenDevice && <div>Server settings go here</div>}
|
||||
</Container>*/}
|
||||
<GenericSidebarBase>
|
||||
<MemberList entries={entries} context={channel} />
|
||||
</GenericSidebarBase>
|
||||
);
|
||||
|
||||