Compare commits
3 commits
dev
...
settings-s
Author | SHA1 | Date | |
---|---|---|---|
12b5d754fd | |||
973c7dea8f | |||
0bd6b25979 |
287 changed files with 10130 additions and 6921 deletions
22
.eslintrc.js
22
.eslintrc.js
|
@ -9,24 +9,21 @@ module.exports = {
|
|||
"plugin:react-hooks/recommended",
|
||||
"plugin:@typescript-eslint/eslint-recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
'airbnb',
|
||||
'prettier',
|
||||
"airbnb",
|
||||
"prettier",
|
||||
],
|
||||
parser: "@typescript-eslint/parser",
|
||||
parserOptions: {
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
},
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'module',
|
||||
ecmaVersion: "latest",
|
||||
sourceType: "module",
|
||||
},
|
||||
plugins: [
|
||||
'react',
|
||||
'@typescript-eslint'
|
||||
],
|
||||
plugins: ["react", "@typescript-eslint"],
|
||||
rules: {
|
||||
'linebreak-style': 0,
|
||||
'no-underscore-dangle': 0,
|
||||
"linebreak-style": 0,
|
||||
"no-underscore-dangle": 0,
|
||||
|
||||
"import/prefer-default-export": "off",
|
||||
"import/extensions": "off",
|
||||
|
@ -38,10 +35,7 @@ module.exports = {
|
|||
},
|
||||
],
|
||||
|
||||
'react/no-unstable-nested-components': [
|
||||
'error',
|
||||
{ allowAsProps: true },
|
||||
],
|
||||
"react/no-unstable-nested-components": ["error", { allowAsProps: true }],
|
||||
"react/jsx-filename-extension": [
|
||||
"error",
|
||||
{
|
||||
|
|
8
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
8
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
|
@ -42,10 +42,10 @@ body:
|
|||
label: Platform and versions
|
||||
description: "Provide OS, browser and Cinny version with your Homeserver."
|
||||
placeholder: |
|
||||
1. OS: [e.g. Windows 10, MacOS]
|
||||
2. Browser: [e.g. chrome 99.5, firefox 97.2]
|
||||
3. Cinny version: [e.g. 1.8.1 (app.cinny.in)]
|
||||
4. Matrix homeserver: [e.g. matrix.org]
|
||||
1. OS: [e.g. Windows 10, MacOS]
|
||||
2. Browser: [e.g. chrome 99.5, firefox 97.2]
|
||||
3. Cinny version: [e.g. 1.8.1 (app.cinny.in)]
|
||||
4. Matrix homeserver: [e.g. matrix.org]
|
||||
render: shell
|
||||
validations:
|
||||
required: true
|
||||
|
|
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
|
@ -1,8 +1,8 @@
|
|||
<!-- Please read https://github.com/ajbura/cinny/blob/dev/CONTRIBUTING.md before submitting your pull request -->
|
||||
|
||||
### Description
|
||||
<!-- Please include a summary of the change. Please also include relevant motivation and context. List any dependencies that are required for this change. -->
|
||||
|
||||
<!-- Please include a summary of the change. Please also include relevant motivation and context. List any dependencies that are required for this change. -->
|
||||
|
||||
Fixes #
|
||||
|
||||
|
|
2
.github/SECURITY.md
vendored
2
.github/SECURITY.md
vendored
|
@ -1,3 +1,3 @@
|
|||
# Reporting a Vulnerability
|
||||
|
||||
**If you've found a security vulnerability, please report it to cinnyapp@gmail.com**
|
||||
**If you've found a security vulnerability, please report it to cinnyapp@gmail.com**
|
||||
|
|
11
.github/renovate.json
vendored
11
.github/renovate.json
vendored
|
@ -1,15 +1,12 @@
|
|||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": [
|
||||
"config:base",
|
||||
":dependencyDashboardApproval"
|
||||
],
|
||||
"labels": [ "Dependencies" ],
|
||||
"extends": ["config:base", ":dependencyDashboardApproval"],
|
||||
"labels": ["Dependencies"],
|
||||
"packageRules": [
|
||||
{
|
||||
"matchUpdateTypes": [ "lockFileMaintenance" ]
|
||||
"matchUpdateTypes": ["lockFileMaintenance"]
|
||||
}
|
||||
],
|
||||
"lockFileMaintenance": { "enabled": true },
|
||||
"dependencyDashboard": true
|
||||
}
|
||||
}
|
||||
|
|
2
.github/workflows/build-pull-request.yml
vendored
2
.github/workflows/build-pull-request.yml
vendored
|
@ -2,7 +2,7 @@ name: Build pull request
|
|||
|
||||
on:
|
||||
pull_request:
|
||||
types: ['opened', 'synchronize']
|
||||
types: ["opened", "synchronize"]
|
||||
|
||||
jobs:
|
||||
build-pull-request:
|
||||
|
|
10
.github/workflows/cla.yml
vendored
10
.github/workflows/cla.yml
vendored
|
@ -1,4 +1,4 @@
|
|||
name: 'CLA Assistant'
|
||||
name: "CLA Assistant"
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
@ -9,7 +9,7 @@ jobs:
|
|||
CLAssistant:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 'CLA Assistant'
|
||||
- name: "CLA Assistant"
|
||||
if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request_target'
|
||||
# Beta Release
|
||||
uses: cla-assistant/github-action@v2.2.1
|
||||
|
@ -18,10 +18,10 @@ jobs:
|
|||
# the below token should have repo scope and must be manually added by you in the repository's secret
|
||||
PERSONAL_ACCESS_TOKEN: ${{ secrets.CLA_PAT }}
|
||||
with:
|
||||
path-to-signatures: 'signatures.json'
|
||||
path-to-document: 'https://github.com/cinnyapp/cla/blob/main/cla.md' # e.g. a CLA or a DCO document
|
||||
path-to-signatures: "signatures.json"
|
||||
path-to-document: "https://github.com/cinnyapp/cla/blob/main/cla.md" # e.g. a CLA or a DCO document
|
||||
# branch should not be protected
|
||||
branch: 'main'
|
||||
branch: "main"
|
||||
allowlist: ajbura,bot*
|
||||
|
||||
#below are the optional inputs - If the optional inputs are not given, then default values will be taken
|
||||
|
|
8
.github/workflows/deploy-pull-request.yml
vendored
8
.github/workflows/deploy-pull-request.yml
vendored
|
@ -2,8 +2,8 @@ name: Deploy PR to Netlify
|
|||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["Build pull request"]
|
||||
types: [completed]
|
||||
workflows: ["Build pull request"]
|
||||
types: [completed]
|
||||
|
||||
jobs:
|
||||
deploy-pull-request:
|
||||
|
@ -52,5 +52,5 @@ jobs:
|
|||
pr_number: ${{ steps.pr.outputs.id }}
|
||||
comment_tag: ${{ steps.pr.outputs.id }}
|
||||
message: |
|
||||
Preview: ${{ steps.netlify.outputs.deploy-url }}
|
||||
⚠️ Exercise caution. Use test accounts. ⚠️
|
||||
Preview: ${{ steps.netlify.outputs.deploy-url }}
|
||||
⚠️ Exercise caution. Use test accounts. ⚠️
|
||||
|
|
6
.github/workflows/docker-pr.yml
vendored
6
.github/workflows/docker-pr.yml
vendored
|
@ -1,10 +1,10 @@
|
|||
name: 'Docker check'
|
||||
name: "Docker check"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'Dockerfile'
|
||||
- '.github/workflows/docker-pr.yml'
|
||||
- "Dockerfile"
|
||||
- ".github/workflows/docker-pr.yml"
|
||||
|
||||
jobs:
|
||||
docker-build:
|
||||
|
|
4
.github/workflows/lockfile.yml
vendored
4
.github/workflows/lockfile.yml
vendored
|
@ -3,7 +3,7 @@ name: NPM Lockfile Changes
|
|||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'package-lock.json'
|
||||
- "package-lock.json"
|
||||
|
||||
jobs:
|
||||
lockfile_changes:
|
||||
|
@ -23,4 +23,4 @@ jobs:
|
|||
collapsibleThreshold: 25
|
||||
failOnDowngrade: false
|
||||
path: package-lock.json
|
||||
updateComment: true
|
||||
updateComment: true
|
||||
|
|
4
.github/workflows/netlify-dev.yml
vendored
4
.github/workflows/netlify-dev.yml
vendored
|
@ -3,7 +3,7 @@ name: Deploy to Netlify (dev)
|
|||
on:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
- dev
|
||||
|
||||
jobs:
|
||||
deploy-to-netlify:
|
||||
|
@ -32,7 +32,7 @@ jobs:
|
|||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
production-deploy: true
|
||||
github-deployment-environment: nightly
|
||||
github-deployment-description: 'Nightly deployment on each commit to dev branch'
|
||||
github-deployment-description: "Nightly deployment on each commit to dev branch"
|
||||
env:
|
||||
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
||||
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID_DEV }}
|
||||
|
|
2
.github/workflows/prod-deploy.yml
vendored
2
.github/workflows/prod-deploy.yml
vendored
|
@ -31,7 +31,7 @@ jobs:
|
|||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
production-deploy: true
|
||||
github-deployment-environment: stable
|
||||
github-deployment-description: 'Stable deployment on each release'
|
||||
github-deployment-description: "Stable deployment on each release"
|
||||
env:
|
||||
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
||||
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID_APP }}
|
||||
|
|
|
@ -1,4 +1,8 @@
|
|||
{
|
||||
"printWidth": 100,
|
||||
"singleQuote": true
|
||||
"tabWidth": 2,
|
||||
"semi": true,
|
||||
"useTabs": false,
|
||||
"singleQuote": false,
|
||||
"trailingComma": "es5",
|
||||
"printWidth": 80
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ First off, thanks for taking the time to contribute! ❤️
|
|||
All types of contributions are encouraged and valued. Please make sure to read the relevant section before making your contribution. It will make it a lot easier for us maintainers and smooth out the experience for all involved. The community looks forward to your contributions. 🎉
|
||||
|
||||
> And if you like the project, but just don't have time to contribute, that's fine. There are other easy ways to support the project and show your appreciation, which we would also be very happy about:
|
||||
>
|
||||
> - Star the project
|
||||
> - Tweet about it (tag @cinnyapp)
|
||||
> - Refer this project in your project's readme
|
||||
|
@ -18,6 +19,7 @@ Bug reports and feature suggestions must use descriptive and concise titles and
|
|||
## Pull requests
|
||||
|
||||
> ### Legal Notice
|
||||
>
|
||||
> When contributing to this project, you must agree that you have authored 100% of the content, that you have the necessary rights to the content and that the content you contribute may be provided under the project license.
|
||||
|
||||
**NOTE: If you want to add new features, please discuss with maintainers before coding or opening a pull request.** This is to ensure that we are on same track and following our roadmap.
|
||||
|
@ -26,9 +28,9 @@ Bug reports and feature suggestions must use descriptive and concise titles and
|
|||
|
||||
Example:
|
||||
|
||||
|Not ideal|Better|
|
||||
|---|----|
|
||||
|Fixed markAllAsRead in RoomTimeline|Fix read marker when paginating room timeline|
|
||||
| Not ideal | Better |
|
||||
| ----------------------------------- | --------------------------------------------- |
|
||||
| Fixed markAllAsRead in RoomTimeline | Fix read marker when paginating room timeline |
|
||||
|
||||
It is not always possible to phrase every change in such a manner, but it is desired.
|
||||
|
||||
|
@ -39,6 +41,7 @@ Also, we use [ESLint](https://eslint.org/) for clean and stylistically consisten
|
|||
**For any query or design discussion, join our [Matrix room](https://matrix.to/#/#cinny:matrix.org).**
|
||||
|
||||
## Helpful links
|
||||
|
||||
- [BEM methodology](http://getbem.com/introduction/)
|
||||
- [Atomic design](https://bradfrost.com/blog/post/atomic-web-design/)
|
||||
- [Matrix JavaScript SDK documentation](https://matrix-org.github.io/matrix-js-sdk/index.html)
|
||||
|
|
10
index.html
10
index.html
|
@ -18,7 +18,10 @@
|
|||
|
||||
<meta property="og:title" content="Cinny" />
|
||||
<meta property="og:url" content="https://cinny.in" />
|
||||
<meta property="og:image" content="https://cinny.in/assets/favicon-48x48.png" />
|
||||
<meta
|
||||
property="og:image"
|
||||
content="https://cinny.in/assets/favicon-48x48.png"
|
||||
/>
|
||||
<meta
|
||||
property="og:description"
|
||||
content="A Matrix client where you can enjoy the conversation using simple, elegant and secure interface protected by e2ee with the power of open source."
|
||||
|
@ -32,7 +35,10 @@
|
|||
<meta name="application-name" content="Cinny" />
|
||||
<meta name="apple-mobile-web-app-title" content="Cinny" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||
<meta
|
||||
name="apple-mobile-web-app-status-bar-style"
|
||||
content="black-translucent"
|
||||
/>
|
||||
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
|
|
|
@ -1,64 +1,69 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import './Avatar.scss';
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import "./Avatar.scss";
|
||||
|
||||
import { twemojify } from '../../../util/twemojify';
|
||||
import { twemojify } from "../../../util/twemojify";
|
||||
|
||||
import Text from '../text/Text';
|
||||
import RawIcon from '../system-icons/RawIcon';
|
||||
import Text from "../text/Text";
|
||||
import RawIcon from "../system-icons/RawIcon";
|
||||
|
||||
import ImageBrokenSVG from '../../../../public/res/svg/image-broken.svg';
|
||||
import { avatarInitials } from '../../../util/common';
|
||||
import ImageBrokenSVG from "../../../../public/res/svg/image-broken.svg";
|
||||
import { avatarInitials } from "../../../util/common";
|
||||
|
||||
const Avatar = React.forwardRef(({
|
||||
text, bgColor, iconSrc, iconColor, imageSrc, size,
|
||||
}, ref) => {
|
||||
let textSize = 's1';
|
||||
if (size === 'large') textSize = 'h1';
|
||||
if (size === 'small') textSize = 'b1';
|
||||
if (size === 'extra-small') textSize = 'b3';
|
||||
const Avatar = React.forwardRef(
|
||||
({ text, bgColor, iconSrc, iconColor, imageSrc, size }, ref) => {
|
||||
let textSize = "s1";
|
||||
if (size === "large") textSize = "h1";
|
||||
if (size === "small") textSize = "b1";
|
||||
if (size === "extra-small") textSize = "b3";
|
||||
|
||||
return (
|
||||
<div ref={ref} className={`avatar-container avatar-container__${size} noselect`}>
|
||||
{
|
||||
imageSrc !== null
|
||||
? (
|
||||
<img
|
||||
draggable="false"
|
||||
src={imageSrc}
|
||||
onLoad={(e) => { e.target.style.backgroundColor = 'transparent'; }}
|
||||
onError={(e) => { e.target.src = ImageBrokenSVG; }}
|
||||
alt=""
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<span
|
||||
style={{ backgroundColor: iconSrc === null ? bgColor : 'transparent' }}
|
||||
className={`avatar__border${iconSrc !== null ? '--active' : ''}`}
|
||||
>
|
||||
{
|
||||
iconSrc !== null
|
||||
? <RawIcon size={size} src={iconSrc} color={iconColor} />
|
||||
: text !== null && (
|
||||
<Text variant={textSize} primary>
|
||||
{twemojify(avatarInitials(text))}
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={`avatar-container avatar-container__${size} noselect`}
|
||||
>
|
||||
{imageSrc !== null ? (
|
||||
<img
|
||||
draggable="false"
|
||||
src={imageSrc}
|
||||
onLoad={(e) => {
|
||||
e.target.style.backgroundColor = "transparent";
|
||||
}}
|
||||
onError={(e) => {
|
||||
e.target.src = ImageBrokenSVG;
|
||||
}}
|
||||
alt=""
|
||||
/>
|
||||
) : (
|
||||
<span
|
||||
style={{
|
||||
backgroundColor: iconSrc === null ? bgColor : "transparent",
|
||||
}}
|
||||
className={`avatar__border${iconSrc !== null ? "--active" : ""}`}
|
||||
>
|
||||
{iconSrc !== null ? (
|
||||
<RawIcon size={size} src={iconSrc} color={iconColor} />
|
||||
) : (
|
||||
text !== null && (
|
||||
<Text variant={textSize} primary>
|
||||
{twemojify(avatarInitials(text))}
|
||||
</Text>
|
||||
)
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
Avatar.defaultProps = {
|
||||
text: null,
|
||||
bgColor: 'transparent',
|
||||
bgColor: "transparent",
|
||||
iconSrc: null,
|
||||
iconColor: null,
|
||||
imageSrc: null,
|
||||
size: 'normal',
|
||||
size: "normal",
|
||||
};
|
||||
|
||||
Avatar.propTypes = {
|
||||
|
@ -67,7 +72,7 @@ Avatar.propTypes = {
|
|||
iconSrc: PropTypes.string,
|
||||
iconColor: PropTypes.string,
|
||||
imageSrc: PropTypes.string,
|
||||
size: PropTypes.oneOf(['large', 'normal', 'small', 'extra-small']),
|
||||
size: PropTypes.oneOf(["large", "normal", "small", "extra-small"]),
|
||||
};
|
||||
|
||||
export default Avatar;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
@use '../../partials/flex';
|
||||
@use "../../partials/flex";
|
||||
|
||||
.avatar-container {
|
||||
display: inline-flex;
|
||||
|
@ -36,7 +36,7 @@
|
|||
|
||||
.avatar__border {
|
||||
@extend .cp-fx__row--c-c;
|
||||
|
||||
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
|
@ -53,4 +53,4 @@
|
|||
box-shadow: var(--bs-surface-border);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,15 +1,20 @@
|
|||
import { avatarInitials, cssVar } from '../../../util/common';
|
||||
import { avatarInitials, cssVar } from "../../../util/common";
|
||||
|
||||
// renders the avatar and returns it as an URL
|
||||
export default async function renderAvatar({
|
||||
text, bgColor, imageSrc, size, borderRadius, scale,
|
||||
text,
|
||||
bgColor,
|
||||
imageSrc,
|
||||
size,
|
||||
borderRadius,
|
||||
scale,
|
||||
}) {
|
||||
try {
|
||||
const canvas = document.createElement('canvas');
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = size * scale;
|
||||
canvas.height = size * scale;
|
||||
|
||||
const ctx = canvas.getContext('2d');
|
||||
const ctx = canvas.getContext("2d");
|
||||
|
||||
ctx.scale(scale, scale);
|
||||
|
||||
|
@ -27,7 +32,7 @@ export default async function renderAvatar({
|
|||
ctx.clip();
|
||||
|
||||
const img = new Image();
|
||||
img.crossOrigin = 'anonymous';
|
||||
img.crossOrigin = "anonymous";
|
||||
const promise = new Promise((resolve, reject) => {
|
||||
img.onerror = reject;
|
||||
img.onload = resolve;
|
||||
|
@ -42,10 +47,10 @@ export default async function renderAvatar({
|
|||
ctx.fill();
|
||||
|
||||
// centered letter
|
||||
ctx.fillStyle = '#fff';
|
||||
ctx.font = `${cssVar('--fs-s1')} ${cssVar('--font-primary')}`;
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillStyle = "#fff";
|
||||
ctx.font = `${cssVar("--fs-s1")} ${cssVar("--font-primary")}`;
|
||||
ctx.textBaseline = "middle";
|
||||
ctx.textAlign = "center";
|
||||
ctx.fillText(avatarInitials(text), size / 2, size / 2);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,14 +1,18 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import './NotificationBadge.scss';
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import "./NotificationBadge.scss";
|
||||
|
||||
import Text from '../text/Text';
|
||||
import Text from "../text/Text";
|
||||
|
||||
function NotificationBadge({ alert, content }) {
|
||||
const notificationClass = alert ? ' notification-badge--alert' : '';
|
||||
const notificationClass = alert ? " notification-badge--alert" : "";
|
||||
return (
|
||||
<div className={`notification-badge${notificationClass}`}>
|
||||
{content !== null && <Text variant="b3" weight="bold">{content}</Text>}
|
||||
{content !== null && (
|
||||
<Text variant="b3" weight="bold">
|
||||
{content}
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -20,10 +24,7 @@ NotificationBadge.defaultProps = {
|
|||
|
||||
NotificationBadge.propTypes = {
|
||||
alert: PropTypes.bool,
|
||||
content: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.number,
|
||||
]),
|
||||
content: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
};
|
||||
|
||||
export default NotificationBadge;
|
||||
|
|
|
@ -18,4 +18,4 @@
|
|||
min-width: 8px;
|
||||
margin: 0 var(--sp-ultra-tight);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,40 +1,44 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import './Button.scss';
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import "./Button.scss";
|
||||
|
||||
import Text from '../text/Text';
|
||||
import RawIcon from '../system-icons/RawIcon';
|
||||
import { blurOnBubbling } from './script';
|
||||
import Text from "../text/Text";
|
||||
import RawIcon from "../system-icons/RawIcon";
|
||||
import { blurOnBubbling } from "./script";
|
||||
|
||||
const Button = React.forwardRef(({
|
||||
id, className, variant, iconSrc,
|
||||
type, onClick, children, disabled,
|
||||
}, ref) => {
|
||||
const iconClass = (iconSrc === null) ? '' : `btn-${variant}--icon`;
|
||||
return (
|
||||
<button
|
||||
ref={ref}
|
||||
id={id === '' ? undefined : id}
|
||||
className={`${className ? `${className} ` : ''}btn-${variant} ${iconClass} noselect`}
|
||||
onMouseUp={(e) => blurOnBubbling(e, `.btn-${variant}`)}
|
||||
onClick={onClick}
|
||||
// eslint-disable-next-line react/button-has-type
|
||||
type={type}
|
||||
disabled={disabled}
|
||||
>
|
||||
{iconSrc !== null && <RawIcon size="small" src={iconSrc} />}
|
||||
{typeof children === 'string' && <Text variant="b1">{ children }</Text>}
|
||||
{typeof children !== 'string' && children }
|
||||
</button>
|
||||
);
|
||||
});
|
||||
const Button = React.forwardRef(
|
||||
(
|
||||
{ id, className, variant, iconSrc, type, onClick, children, disabled },
|
||||
ref
|
||||
) => {
|
||||
const iconClass = iconSrc === null ? "" : `btn-${variant}--icon`;
|
||||
return (
|
||||
<button
|
||||
ref={ref}
|
||||
id={id === "" ? undefined : id}
|
||||
className={`${
|
||||
className ? `${className} ` : ""
|
||||
}btn-${variant} ${iconClass} noselect`}
|
||||
onMouseUp={(e) => blurOnBubbling(e, `.btn-${variant}`)}
|
||||
onClick={onClick}
|
||||
// eslint-disable-next-line react/button-has-type
|
||||
type={type}
|
||||
disabled={disabled}
|
||||
>
|
||||
{iconSrc !== null && <RawIcon size="small" src={iconSrc} />}
|
||||
{typeof children === "string" && <Text variant="b1">{children}</Text>}
|
||||
{typeof children !== "string" && children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
Button.defaultProps = {
|
||||
id: '',
|
||||
id: "",
|
||||
className: null,
|
||||
variant: 'surface',
|
||||
variant: "surface",
|
||||
iconSrc: null,
|
||||
type: 'button',
|
||||
type: "button",
|
||||
onClick: null,
|
||||
disabled: false,
|
||||
};
|
||||
|
@ -42,9 +46,15 @@ Button.defaultProps = {
|
|||
Button.propTypes = {
|
||||
id: PropTypes.string,
|
||||
className: PropTypes.string,
|
||||
variant: PropTypes.oneOf(['surface', 'primary', 'positive', 'caution', 'danger']),
|
||||
variant: PropTypes.oneOf([
|
||||
"surface",
|
||||
"primary",
|
||||
"positive",
|
||||
"caution",
|
||||
"danger",
|
||||
]),
|
||||
iconSrc: PropTypes.string,
|
||||
type: PropTypes.oneOf(['button', 'submit', 'reset']),
|
||||
type: PropTypes.oneOf(["button", "submit", "reset"]),
|
||||
onClick: PropTypes.func,
|
||||
children: PropTypes.node.isRequired,
|
||||
disabled: PropTypes.bool,
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
@use 'state';
|
||||
@use '../../partials/dir';
|
||||
@use '../../partials/text';
|
||||
@use "state";
|
||||
@use "../../partials/dir";
|
||||
@use "../../partials/text";
|
||||
|
||||
.btn-surface,
|
||||
.btn-primary,
|
||||
|
@ -22,10 +22,9 @@
|
|||
& .text {
|
||||
@extend .cp-txt__ellipsis;
|
||||
}
|
||||
|
||||
|
||||
&--icon {
|
||||
@include dir.side(padding, var(--sp-tight), var(--sp-loose));
|
||||
|
||||
}
|
||||
.ic-raw {
|
||||
@include dir.side(margin, 0, var(--sp-extra-tight));
|
||||
|
@ -42,7 +41,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
.btn-surface {
|
||||
box-shadow: var(--bs-surface-border);
|
||||
@include color(var(--tc-surface-high), var(--ic-surface-normal));
|
||||
|
@ -78,4 +76,4 @@
|
|||
@include state.hover(var(--bg-danger-hover));
|
||||
@include state.focus(var(--bs-danger-outline));
|
||||
@include state.active(var(--bg-danger-active));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import './Checkbox.scss';
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import "./Checkbox.scss";
|
||||
|
||||
function Checkbox({
|
||||
variant, isActive, onToggle,
|
||||
disabled, tabIndex,
|
||||
}) {
|
||||
const className = `checkbox checkbox-${variant}${isActive ? ' checkbox--active' : ''}`;
|
||||
function Checkbox({ variant, isActive, onToggle, disabled, tabIndex }) {
|
||||
const className = `checkbox checkbox-${variant}${
|
||||
isActive ? " checkbox--active" : ""
|
||||
}`;
|
||||
if (onToggle === null) return <span className={className} />;
|
||||
return (
|
||||
// eslint-disable-next-line jsx-a11y/control-has-associated-label
|
||||
|
@ -21,7 +20,7 @@ function Checkbox({
|
|||
}
|
||||
|
||||
Checkbox.defaultProps = {
|
||||
variant: 'primary',
|
||||
variant: "primary",
|
||||
isActive: false,
|
||||
onToggle: null,
|
||||
disabled: false,
|
||||
|
@ -29,7 +28,7 @@ Checkbox.defaultProps = {
|
|||
};
|
||||
|
||||
Checkbox.propTypes = {
|
||||
variant: PropTypes.oneOf(['primary', 'positive', 'caution', 'danger']),
|
||||
variant: PropTypes.oneOf(["primary", "positive", "caution", "danger"]),
|
||||
isActive: PropTypes.bool,
|
||||
onToggle: PropTypes.func,
|
||||
disabled: PropTypes.bool,
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
@use '../../partials/flex';
|
||||
@use './state';
|
||||
@use "../../partials/flex";
|
||||
@use "./state";
|
||||
|
||||
.checkbox {
|
||||
width: 20px;
|
||||
|
@ -36,4 +36,4 @@
|
|||
}
|
||||
.checkbox-danger.checkbox--active {
|
||||
background-color: var(--bg-danger);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,62 +1,80 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import './IconButton.scss';
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import "./IconButton.scss";
|
||||
|
||||
import RawIcon from '../system-icons/RawIcon';
|
||||
import Tooltip from '../tooltip/Tooltip';
|
||||
import { blurOnBubbling } from './script';
|
||||
import Text from '../text/Text';
|
||||
import RawIcon from "../system-icons/RawIcon";
|
||||
import Tooltip from "../tooltip/Tooltip";
|
||||
import { blurOnBubbling } from "./script";
|
||||
import Text from "../text/Text";
|
||||
|
||||
const IconButton = React.forwardRef(({
|
||||
variant, size, type,
|
||||
tooltip, tooltipPlacement, src,
|
||||
onClick, tabIndex, disabled, isImage,
|
||||
className,
|
||||
}, ref) => {
|
||||
const btn = (
|
||||
<button
|
||||
ref={ref}
|
||||
className={`ic-btn ic-btn-${variant} ${className}`}
|
||||
onMouseUp={(e) => blurOnBubbling(e, `.ic-btn-${variant}`)}
|
||||
onClick={onClick}
|
||||
// eslint-disable-next-line react/button-has-type
|
||||
type={type}
|
||||
tabIndex={tabIndex}
|
||||
disabled={disabled}
|
||||
>
|
||||
<RawIcon size={size} src={src} isImage={isImage} />
|
||||
</button>
|
||||
);
|
||||
if (tooltip === null) return btn;
|
||||
return (
|
||||
<Tooltip
|
||||
placement={tooltipPlacement}
|
||||
content={<Text variant="b2">{tooltip}</Text>}
|
||||
>
|
||||
{btn}
|
||||
</Tooltip>
|
||||
);
|
||||
});
|
||||
const IconButton = React.forwardRef(
|
||||
(
|
||||
{
|
||||
variant,
|
||||
size,
|
||||
type,
|
||||
tooltip,
|
||||
tooltipPlacement,
|
||||
src,
|
||||
onClick,
|
||||
tabIndex,
|
||||
disabled,
|
||||
isImage,
|
||||
className,
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const btn = (
|
||||
<button
|
||||
ref={ref}
|
||||
className={`ic-btn ic-btn-${variant} ${className}`}
|
||||
onMouseUp={(e) => blurOnBubbling(e, `.ic-btn-${variant}`)}
|
||||
onClick={onClick}
|
||||
// eslint-disable-next-line react/button-has-type
|
||||
type={type}
|
||||
tabIndex={tabIndex}
|
||||
disabled={disabled}
|
||||
>
|
||||
<RawIcon size={size} src={src} isImage={isImage} />
|
||||
</button>
|
||||
);
|
||||
if (tooltip === null) return btn;
|
||||
return (
|
||||
<Tooltip
|
||||
placement={tooltipPlacement}
|
||||
content={<Text variant="b2">{tooltip}</Text>}
|
||||
>
|
||||
{btn}
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
IconButton.defaultProps = {
|
||||
variant: 'surface',
|
||||
size: 'normal',
|
||||
type: 'button',
|
||||
variant: "surface",
|
||||
size: "normal",
|
||||
type: "button",
|
||||
tooltip: null,
|
||||
tooltipPlacement: 'top',
|
||||
tooltipPlacement: "top",
|
||||
onClick: null,
|
||||
tabIndex: 0,
|
||||
disabled: false,
|
||||
isImage: false,
|
||||
className: '',
|
||||
className: "",
|
||||
};
|
||||
|
||||
IconButton.propTypes = {
|
||||
variant: PropTypes.oneOf(['surface', 'primary', 'positive', 'caution', 'danger']),
|
||||
size: PropTypes.oneOf(['normal', 'small', 'extra-small']),
|
||||
type: PropTypes.oneOf(['button', 'submit', 'reset']),
|
||||
variant: PropTypes.oneOf([
|
||||
"surface",
|
||||
"primary",
|
||||
"positive",
|
||||
"caution",
|
||||
"danger",
|
||||
]),
|
||||
size: PropTypes.oneOf(["normal", "small", "extra-small"]),
|
||||
type: PropTypes.oneOf(["button", "submit", "reset"]),
|
||||
tooltip: PropTypes.string,
|
||||
tooltipPlacement: PropTypes.oneOf(['top', 'right', 'bottom', 'left']),
|
||||
tooltipPlacement: PropTypes.oneOf(["top", "right", "bottom", "left"]),
|
||||
src: PropTypes.string.isRequired,
|
||||
onClick: PropTypes.func,
|
||||
tabIndex: PropTypes.number,
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
@use 'state';
|
||||
@use "state";
|
||||
|
||||
.ic-btn {
|
||||
padding: var(--sp-extra-tight);
|
||||
|
@ -53,4 +53,4 @@
|
|||
@include state.hover(var(--bg-danger-hover));
|
||||
@include focus(var(--bg-danger-hover));
|
||||
@include state.active(var(--bg-danger-active));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,14 +1,17 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import './RadioButton.scss';
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import "./RadioButton.scss";
|
||||
|
||||
function RadioButton({ isActive, onToggle, disabled }) {
|
||||
if (onToggle === null) return <span className={`radio-btn${isActive ? ' radio-btn--active' : ''}`} />;
|
||||
if (onToggle === null)
|
||||
return (
|
||||
<span className={`radio-btn${isActive ? " radio-btn--active" : ""}`} />
|
||||
);
|
||||
return (
|
||||
// eslint-disable-next-line jsx-a11y/control-has-associated-label
|
||||
<button
|
||||
onClick={() => onToggle(!isActive)}
|
||||
className={`radio-btn${isActive ? ' radio-btn--active' : ''}`}
|
||||
className={`radio-btn${isActive ? " radio-btn--active" : ""}`}
|
||||
type="button"
|
||||
disabled={disabled}
|
||||
/>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
@use '../../partials/flex';
|
||||
@use './state';
|
||||
@use "../../partials/flex";
|
||||
@use "./state";
|
||||
|
||||
.radio-btn {
|
||||
@extend .cp-fx__row--c-c;
|
||||
|
@ -12,7 +12,7 @@
|
|||
@include state.disabled;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
content: "";
|
||||
display: inline-block;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
|
@ -25,4 +25,4 @@
|
|||
background-color: var(--bg-positive);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import './Toggle.scss';
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import "./Toggle.scss";
|
||||
|
||||
function Toggle({ isActive, onToggle, disabled }) {
|
||||
const className = `toggle${isActive ? ' toggle--active' : ''}`;
|
||||
const className = `toggle${isActive ? " toggle--active" : ""}`;
|
||||
if (onToggle === null) return <span className={className} />;
|
||||
return (
|
||||
// eslint-disable-next-line jsx-a11y/control-has-associated-label
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
@use '../../partials/dir';
|
||||
@use './state';
|
||||
@use "../../partials/dir";
|
||||
@use "./state";
|
||||
|
||||
.toggle {
|
||||
width: 44px;
|
||||
|
@ -16,14 +16,13 @@
|
|||
transition: background 200ms ease-in-out;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
content: "";
|
||||
display: inline-block;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background-color: var(--tc-surface-low);
|
||||
border-radius: calc(var(--bo-radius) / 2);
|
||||
transition: transform 200ms ease-in-out,
|
||||
opacity 200ms ease-in-out;
|
||||
transition: transform 200ms ease-in-out, opacity 200ms ease-in-out;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
|
@ -40,4 +39,4 @@
|
|||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
@mixin hover($color) {
|
||||
@media (hover: hover) {
|
||||
&:hover {
|
||||
|
@ -22,4 +21,4 @@
|
|||
opacity: 0.4;
|
||||
cursor: no-drop;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,7 +10,10 @@ function blurOnBubbling(e, selector) {
|
|||
|
||||
for (let elIndex = 0; elIndex < bubblingPath.length; elIndex += 1) {
|
||||
if (bubblingPath[elIndex] === document) {
|
||||
console.warn(blurOnBubbling, 'blurOnBubbling: not found selector in bubbling path');
|
||||
console.warn(
|
||||
blurOnBubbling,
|
||||
"blurOnBubbling: not found selector in bubbling path"
|
||||
);
|
||||
break;
|
||||
}
|
||||
if (bubblingPath[elIndex].matches(selector)) {
|
||||
|
|
|
@ -1,24 +1,28 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import './InfoCard.scss';
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import "./InfoCard.scss";
|
||||
|
||||
import Text from '../text/Text';
|
||||
import RawIcon from '../system-icons/RawIcon';
|
||||
import IconButton from '../button/IconButton';
|
||||
import Text from "../text/Text";
|
||||
import RawIcon from "../system-icons/RawIcon";
|
||||
import IconButton from "../button/IconButton";
|
||||
|
||||
import CrossIC from '../../../../public/res/ic/outlined/cross.svg';
|
||||
import CrossIC from "../../../../public/res/ic/outlined/cross.svg";
|
||||
|
||||
function InfoCard({
|
||||
className, style,
|
||||
variant, iconSrc,
|
||||
title, content,
|
||||
rounded, requestClose,
|
||||
className,
|
||||
style,
|
||||
variant,
|
||||
iconSrc,
|
||||
title,
|
||||
content,
|
||||
rounded,
|
||||
requestClose,
|
||||
}) {
|
||||
const classes = [`info-card info-card--${variant}`];
|
||||
if (rounded) classes.push('info-card--rounded');
|
||||
if (rounded) classes.push("info-card--rounded");
|
||||
if (className) classes.push(className);
|
||||
return (
|
||||
<div className={classes.join(' ')} style={style}>
|
||||
<div className={classes.join(" ")} style={style}>
|
||||
{iconSrc && (
|
||||
<div className="info-card__icon">
|
||||
<RawIcon color={`var(--ic-${variant}-high)`} src={iconSrc} />
|
||||
|
@ -38,7 +42,7 @@ function InfoCard({
|
|||
InfoCard.defaultProps = {
|
||||
className: null,
|
||||
style: null,
|
||||
variant: 'surface',
|
||||
variant: "surface",
|
||||
iconSrc: null,
|
||||
content: null,
|
||||
rounded: false,
|
||||
|
@ -48,7 +52,13 @@ InfoCard.defaultProps = {
|
|||
InfoCard.propTypes = {
|
||||
className: PropTypes.string,
|
||||
style: PropTypes.shape({}),
|
||||
variant: PropTypes.oneOf(['surface', 'primary', 'positive', 'caution', 'danger']),
|
||||
variant: PropTypes.oneOf([
|
||||
"surface",
|
||||
"primary",
|
||||
"positive",
|
||||
"caution",
|
||||
"danger",
|
||||
]),
|
||||
iconSrc: PropTypes.string,
|
||||
title: PropTypes.string.isRequired,
|
||||
content: PropTypes.node,
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
@use '.././../partials/flex';
|
||||
@use '.././../partials/dir';
|
||||
@use ".././../partials/flex";
|
||||
@use ".././../partials/dir";
|
||||
|
||||
.info-card {
|
||||
display: flex;
|
||||
|
@ -34,7 +34,6 @@
|
|||
&--surface {
|
||||
border-color: var(--bg-surface-border);
|
||||
background-color: var(--bg-surface-hover);
|
||||
|
||||
}
|
||||
&--primary {
|
||||
border-color: var(--bg-primary);
|
||||
|
@ -76,4 +75,4 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,18 +1,17 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import './Chip.scss';
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import "./Chip.scss";
|
||||
|
||||
import Text from '../text/Text';
|
||||
import RawIcon from '../system-icons/RawIcon';
|
||||
import Text from "../text/Text";
|
||||
import RawIcon from "../system-icons/RawIcon";
|
||||
|
||||
function Chip({
|
||||
iconSrc, iconColor, text, children,
|
||||
onClick,
|
||||
}) {
|
||||
function Chip({ iconSrc, iconColor, text, children, onClick }) {
|
||||
return (
|
||||
<button className="chip" type="button" onClick={onClick}>
|
||||
{iconSrc != null && <RawIcon src={iconSrc} color={iconColor} size="extra-small" />}
|
||||
{(text != null && text !== '') && <Text variant="b3">{text}</Text>}
|
||||
{iconSrc != null && (
|
||||
<RawIcon src={iconSrc} color={iconColor} size="extra-small" />
|
||||
)}
|
||||
{text != null && text !== "" && <Text variant="b3">{text}</Text>}
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
@use '../../partials/dir';
|
||||
@use "../../partials/dir";
|
||||
|
||||
.chip {
|
||||
padding: var(--sp-ultra-tight) var(--sp-extra-tight);
|
||||
|
||||
|
||||
display: inline-flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
|
||||
background: var(--bg-surface-low);
|
||||
border-radius: var(--bo-radius);
|
||||
box-shadow: var(--bs-surface-border);
|
||||
|
@ -28,4 +28,4 @@
|
|||
height: 16px;
|
||||
@include dir.side(margin, 0, var(--sp-ultra-tight));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,17 +1,15 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import './ContextMenu.scss';
|
||||
import React, { useState, useEffect } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import "./ContextMenu.scss";
|
||||
|
||||
import Tippy from '@tippyjs/react';
|
||||
import 'tippy.js/animations/scale-extreme.css';
|
||||
import Tippy from "@tippyjs/react";
|
||||
import "tippy.js/animations/scale-extreme.css";
|
||||
|
||||
import Text from '../text/Text';
|
||||
import Button from '../button/Button';
|
||||
import ScrollView from '../scroll/ScrollView';
|
||||
import Text from "../text/Text";
|
||||
import Button from "../button/Button";
|
||||
import ScrollView from "../scroll/ScrollView";
|
||||
|
||||
function ContextMenu({
|
||||
content, placement, maxWidth, render, afterToggle,
|
||||
}) {
|
||||
function ContextMenu({ content, placement, maxWidth, render, afterToggle }) {
|
||||
const [isVisible, setVisibility] = useState(false);
|
||||
const showMenu = () => setVisibility(true);
|
||||
const hideMenu = () => setVisibility(false);
|
||||
|
@ -26,7 +24,11 @@ function ContextMenu({
|
|||
className="context-menu"
|
||||
visible={isVisible}
|
||||
onClickOutside={hideMenu}
|
||||
content={<ScrollView invisible>{typeof content === 'function' ? content(hideMenu) : content}</ScrollView>}
|
||||
content={
|
||||
<ScrollView invisible>
|
||||
{typeof content === "function" ? content(hideMenu) : content}
|
||||
</ScrollView>
|
||||
}
|
||||
placement={placement}
|
||||
interactive
|
||||
arrow={false}
|
||||
|
@ -39,21 +41,15 @@ function ContextMenu({
|
|||
}
|
||||
|
||||
ContextMenu.defaultProps = {
|
||||
maxWidth: 'unset',
|
||||
placement: 'right',
|
||||
maxWidth: "unset",
|
||||
placement: "right",
|
||||
afterToggle: null,
|
||||
};
|
||||
|
||||
ContextMenu.propTypes = {
|
||||
content: PropTypes.oneOfType([
|
||||
PropTypes.node,
|
||||
PropTypes.func,
|
||||
]).isRequired,
|
||||
placement: PropTypes.oneOf(['top', 'right', 'bottom', 'left']),
|
||||
maxWidth: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.number,
|
||||
]),
|
||||
content: PropTypes.oneOfType([PropTypes.node, PropTypes.func]).isRequired,
|
||||
placement: PropTypes.oneOf(["top", "right", "bottom", "left"]),
|
||||
maxWidth: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
render: PropTypes.func.isRequired,
|
||||
afterToggle: PropTypes.func,
|
||||
};
|
||||
|
@ -61,7 +57,7 @@ ContextMenu.propTypes = {
|
|||
function MenuHeader({ children }) {
|
||||
return (
|
||||
<div className="context-menu__header">
|
||||
<Text variant="b3">{ children }</Text>
|
||||
<Text variant="b3">{children}</Text>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -70,10 +66,7 @@ MenuHeader.propTypes = {
|
|||
children: PropTypes.node.isRequired,
|
||||
};
|
||||
|
||||
function MenuItem({
|
||||
variant, iconSrc, type,
|
||||
onClick, children, disabled,
|
||||
}) {
|
||||
function MenuItem({ variant, iconSrc, type, onClick, children, disabled }) {
|
||||
return (
|
||||
<div className="context-menu__item">
|
||||
<Button
|
||||
|
@ -83,33 +76,33 @@ function MenuItem({
|
|||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
>
|
||||
{ children }
|
||||
{children}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
MenuItem.defaultProps = {
|
||||
variant: 'surface',
|
||||
variant: "surface",
|
||||
iconSrc: null,
|
||||
type: 'button',
|
||||
type: "button",
|
||||
disabled: false,
|
||||
onClick: null,
|
||||
};
|
||||
|
||||
MenuItem.propTypes = {
|
||||
variant: PropTypes.oneOf(['surface', 'positive', 'caution', 'danger']),
|
||||
variant: PropTypes.oneOf(["surface", "positive", "caution", "danger"]),
|
||||
iconSrc: PropTypes.string,
|
||||
type: PropTypes.oneOf(['button', 'submit']),
|
||||
type: PropTypes.oneOf(["button", "submit"]),
|
||||
onClick: PropTypes.func,
|
||||
children: PropTypes.node.isRequired,
|
||||
disabled: PropTypes.bool,
|
||||
};
|
||||
|
||||
function MenuBorder() {
|
||||
return <div style={{ borderBottom: '1px solid var(--bg-surface-border)' }}> </div>;
|
||||
return (
|
||||
<div style={{ borderBottom: "1px solid var(--bg-surface-border)" }}> </div>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
ContextMenu as default, MenuHeader, MenuItem, MenuBorder,
|
||||
};
|
||||
export { ContextMenu as default, MenuHeader, MenuItem, MenuBorder };
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
@use '../../partials/flex';
|
||||
@use '../../partials/text';
|
||||
@use '../../partials/dir';
|
||||
@use "../../partials/flex";
|
||||
@use "../../partials/text";
|
||||
@use "../../partials/dir";
|
||||
|
||||
.context-menu {
|
||||
background-color: var(--bg-surface);
|
||||
|
@ -59,11 +59,7 @@
|
|||
|
||||
// if item doesn't have icon
|
||||
.text:first-child {
|
||||
@include dir.side(
|
||||
margin,
|
||||
calc(var(--ic-small) + var(--sp-tight)),
|
||||
0
|
||||
);
|
||||
@include dir.side(margin, calc(var(--ic-small) + var(--sp-tight)), 0);
|
||||
}
|
||||
}
|
||||
.btn-surface:focus {
|
||||
|
@ -78,4 +74,4 @@
|
|||
.btn-danger:focus {
|
||||
background-color: var(--bg-danger-hover);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
|
||||
import cons from '../../../client/state/cons';
|
||||
import navigation from '../../../client/state/navigation';
|
||||
import cons from "../../../client/state/cons";
|
||||
import navigation from "../../../client/state/navigation";
|
||||
|
||||
import ContextMenu from './ContextMenu';
|
||||
import ContextMenu from "./ContextMenu";
|
||||
|
||||
let key = null;
|
||||
function ReusableContextMenu() {
|
||||
|
@ -29,14 +29,20 @@ function ReusableContextMenu() {
|
|||
return;
|
||||
}
|
||||
setData({
|
||||
placement, cords, render, afterClose,
|
||||
placement,
|
||||
cords,
|
||||
render,
|
||||
afterClose,
|
||||
});
|
||||
};
|
||||
navigation.on(cons.events.navigation.REUSABLE_CONTEXT_MENU_OPENED, handleContextMenuOpen);
|
||||
navigation.on(
|
||||
cons.events.navigation.REUSABLE_CONTEXT_MENU_OPENED,
|
||||
handleContextMenuOpen
|
||||
);
|
||||
return () => {
|
||||
navigation.removeListener(
|
||||
cons.events.navigation.REUSABLE_CONTEXT_MENU_OPENED,
|
||||
handleContextMenuOpen,
|
||||
handleContextMenuOpen
|
||||
);
|
||||
};
|
||||
}, [data]);
|
||||
|
@ -59,24 +65,24 @@ function ReusableContextMenu() {
|
|||
return (
|
||||
<ContextMenu
|
||||
afterToggle={handleAfterToggle}
|
||||
placement={data?.placement || 'right'}
|
||||
content={data?.render(closeMenu) ?? ''}
|
||||
placement={data?.placement || "right"}
|
||||
content={data?.render(closeMenu) ?? ""}
|
||||
render={(toggleMenu) => (
|
||||
<input
|
||||
ref={openerRef}
|
||||
onClick={toggleMenu}
|
||||
type="button"
|
||||
style={{
|
||||
width: '32px',
|
||||
height: '32px',
|
||||
backgroundColor: 'transparent',
|
||||
position: 'fixed',
|
||||
width: "32px",
|
||||
height: "32px",
|
||||
backgroundColor: "transparent",
|
||||
position: "fixed",
|
||||
top: 0,
|
||||
left: 0,
|
||||
padding: 0,
|
||||
border: 'none',
|
||||
visibility: 'hidden',
|
||||
appearance: 'none',
|
||||
border: "none",
|
||||
visibility: "hidden",
|
||||
appearance: "none",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
|
|
@ -1,28 +1,38 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import './Divider.scss';
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import "./Divider.scss";
|
||||
|
||||
import Text from '../text/Text';
|
||||
import Text from "../text/Text";
|
||||
|
||||
function Divider({ text, variant, align }) {
|
||||
const dividerClass = ` divider--${variant} divider--${align}`;
|
||||
return (
|
||||
<div className={`divider${dividerClass}`}>
|
||||
{text !== null && <Text className="divider__text" variant="b3" weight="bold">{text}</Text>}
|
||||
{text !== null && (
|
||||
<Text className="divider__text" variant="b3" weight="bold">
|
||||
{text}
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Divider.defaultProps = {
|
||||
text: null,
|
||||
variant: 'surface',
|
||||
align: 'center',
|
||||
variant: "surface",
|
||||
align: "center",
|
||||
};
|
||||
|
||||
Divider.propTypes = {
|
||||
text: PropTypes.string,
|
||||
variant: PropTypes.oneOf(['surface', 'primary', 'positive', 'caution', 'danger']),
|
||||
align: PropTypes.oneOf(['left', 'center', 'right']),
|
||||
variant: PropTypes.oneOf([
|
||||
"surface",
|
||||
"primary",
|
||||
"positive",
|
||||
"caution",
|
||||
"danger",
|
||||
]),
|
||||
align: PropTypes.oneOf(["left", "center", "right"]),
|
||||
};
|
||||
|
||||
export default Divider;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
.divider-line {
|
||||
content: '';
|
||||
content: "";
|
||||
display: inline-block;
|
||||
flex: 1;
|
||||
border-bottom: 1px solid var(--local-divider-color);
|
||||
|
@ -36,7 +36,7 @@
|
|||
}
|
||||
.divider--primary {
|
||||
--local-divider-color: var(--bg-primary);
|
||||
--local-divider-opacity: .8;
|
||||
--local-divider-opacity: 0.8;
|
||||
.divider__text {
|
||||
color: var(--tc-primary-high);
|
||||
background-color: var(--bg-primary);
|
||||
|
@ -44,7 +44,7 @@
|
|||
}
|
||||
.divider--positive {
|
||||
--local-divider-color: var(--bg-positive);
|
||||
--local-divider-opacity: .8;
|
||||
--local-divider-opacity: 0.8;
|
||||
.divider__text {
|
||||
color: var(--bg-surface);
|
||||
background-color: var(--bg-positive);
|
||||
|
@ -52,7 +52,7 @@
|
|||
}
|
||||
.divider--danger {
|
||||
--local-divider-color: var(--bg-danger);
|
||||
--local-divider-opacity: .8;
|
||||
--local-divider-opacity: 0.8;
|
||||
.divider__text {
|
||||
color: var(--bg-surface);
|
||||
background-color: var(--bg-danger);
|
||||
|
@ -60,7 +60,7 @@
|
|||
}
|
||||
.divider--caution {
|
||||
--local-divider-color: var(--bg-caution);
|
||||
--local-divider-opacity: .8;
|
||||
--local-divider-opacity: 0.8;
|
||||
.divider__text {
|
||||
color: var(--bg-surface);
|
||||
background-color: var(--bg-caution);
|
||||
|
|
|
@ -1,13 +1,9 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import './Header.scss';
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import "./Header.scss";
|
||||
|
||||
function Header({ children }) {
|
||||
return (
|
||||
<div className="header">
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
return <div className="header">{children}</div>;
|
||||
}
|
||||
|
||||
Header.propTypes = {
|
||||
|
@ -15,11 +11,7 @@ Header.propTypes = {
|
|||
};
|
||||
|
||||
function TitleWrapper({ children }) {
|
||||
return (
|
||||
<div className="header__title-wrapper">
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
return <div className="header__title-wrapper">{children}</div>;
|
||||
}
|
||||
|
||||
TitleWrapper.propTypes = {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
@use '../../partials/text';
|
||||
@use '../../partials/dir';
|
||||
@use "../../partials/text";
|
||||
@use "../../partials/dir";
|
||||
|
||||
.header {
|
||||
@include dir.side(padding, var(--sp-normal), var(--sp-extra-tight));
|
||||
|
@ -15,7 +15,7 @@
|
|||
display: flex;
|
||||
align-items: center;
|
||||
margin: 0 var(--sp-tight);
|
||||
|
||||
|
||||
&:first-child {
|
||||
@include dir.side(margin, 0, var(--sp-tight));
|
||||
}
|
||||
|
@ -24,7 +24,7 @@
|
|||
@extend .cp-txt__ellipsis;
|
||||
min-width: 0;
|
||||
}
|
||||
& > .text-b3{
|
||||
& > .text-b3 {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
||||
|
@ -40,4 +40,4 @@
|
|||
display: -webkit-box;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,75 +1,92 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import './Input.scss';
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import "./Input.scss";
|
||||
|
||||
import TextareaAutosize from 'react-autosize-textarea';
|
||||
import TextareaAutosize from "react-autosize-textarea";
|
||||
|
||||
function Input({
|
||||
id, label, name, value, placeholder,
|
||||
required, type, onChange, forwardRef,
|
||||
resizable, minHeight, onResize, state,
|
||||
onKeyDown, disabled, autoFocus,
|
||||
id,
|
||||
label,
|
||||
name,
|
||||
value,
|
||||
placeholder,
|
||||
required,
|
||||
type,
|
||||
onChange,
|
||||
forwardRef,
|
||||
resizable,
|
||||
minHeight,
|
||||
onResize,
|
||||
state,
|
||||
onKeyDown,
|
||||
disabled,
|
||||
autoFocus,
|
||||
}) {
|
||||
return (
|
||||
<div className="input-container">
|
||||
{ label !== '' && <label className="input__label text-b2" htmlFor={id}>{label}</label> }
|
||||
{ resizable
|
||||
? (
|
||||
<TextareaAutosize
|
||||
dir="auto"
|
||||
style={{ minHeight: `${minHeight}px` }}
|
||||
name={name}
|
||||
id={id}
|
||||
className={`input input--resizable${state !== 'normal' ? ` input--${state}` : ''}`}
|
||||
ref={forwardRef}
|
||||
type={type}
|
||||
placeholder={placeholder}
|
||||
required={required}
|
||||
defaultValue={value}
|
||||
autoComplete="off"
|
||||
onChange={onChange}
|
||||
onResize={onResize}
|
||||
onKeyDown={onKeyDown}
|
||||
disabled={disabled}
|
||||
autoFocus={autoFocus}
|
||||
/>
|
||||
) : (
|
||||
<input
|
||||
dir="auto"
|
||||
ref={forwardRef}
|
||||
id={id}
|
||||
name={name}
|
||||
className={`input ${state !== 'normal' ? ` input--${state}` : ''}`}
|
||||
type={type}
|
||||
placeholder={placeholder}
|
||||
required={required}
|
||||
defaultValue={value}
|
||||
autoComplete="off"
|
||||
onChange={onChange}
|
||||
onKeyDown={onKeyDown}
|
||||
disabled={disabled}
|
||||
// eslint-disable-next-line jsx-a11y/no-autofocus
|
||||
autoFocus={autoFocus}
|
||||
/>
|
||||
)}
|
||||
{label !== "" && (
|
||||
<label className="input__label text-b2" htmlFor={id}>
|
||||
{label}
|
||||
</label>
|
||||
)}
|
||||
{resizable ? (
|
||||
<TextareaAutosize
|
||||
dir="auto"
|
||||
style={{ minHeight: `${minHeight}px` }}
|
||||
name={name}
|
||||
id={id}
|
||||
className={`input input--resizable${
|
||||
state !== "normal" ? ` input--${state}` : ""
|
||||
}`}
|
||||
ref={forwardRef}
|
||||
type={type}
|
||||
placeholder={placeholder}
|
||||
required={required}
|
||||
defaultValue={value}
|
||||
autoComplete="off"
|
||||
onChange={onChange}
|
||||
onResize={onResize}
|
||||
onKeyDown={onKeyDown}
|
||||
disabled={disabled}
|
||||
autoFocus={autoFocus}
|
||||
/>
|
||||
) : (
|
||||
<input
|
||||
dir="auto"
|
||||
ref={forwardRef}
|
||||
id={id}
|
||||
name={name}
|
||||
className={`input ${state !== "normal" ? ` input--${state}` : ""}`}
|
||||
type={type}
|
||||
placeholder={placeholder}
|
||||
required={required}
|
||||
defaultValue={value}
|
||||
autoComplete="off"
|
||||
onChange={onChange}
|
||||
onKeyDown={onKeyDown}
|
||||
disabled={disabled}
|
||||
// eslint-disable-next-line jsx-a11y/no-autofocus
|
||||
autoFocus={autoFocus}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Input.defaultProps = {
|
||||
id: null,
|
||||
name: '',
|
||||
label: '',
|
||||
value: '',
|
||||
placeholder: '',
|
||||
type: 'text',
|
||||
name: "",
|
||||
label: "",
|
||||
value: "",
|
||||
placeholder: "",
|
||||
type: "text",
|
||||
required: false,
|
||||
onChange: null,
|
||||
forwardRef: null,
|
||||
resizable: false,
|
||||
minHeight: 46,
|
||||
onResize: null,
|
||||
state: 'normal',
|
||||
state: "normal",
|
||||
onKeyDown: null,
|
||||
disabled: false,
|
||||
autoFocus: false,
|
||||
|
@ -88,7 +105,7 @@ Input.propTypes = {
|
|||
resizable: PropTypes.bool,
|
||||
minHeight: PropTypes.number,
|
||||
onResize: PropTypes.func,
|
||||
state: PropTypes.oneOf(['normal', 'success', 'error']),
|
||||
state: PropTypes.oneOf(["normal", "success", "error"]),
|
||||
onKeyDown: PropTypes.func,
|
||||
disabled: PropTypes.bool,
|
||||
autoFocus: PropTypes.bool,
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
@use '../../atoms/scroll/scrollbar';
|
||||
@use "../../atoms/scroll/scrollbar";
|
||||
|
||||
.input {
|
||||
display: block;
|
||||
|
@ -47,6 +47,6 @@
|
|||
box-shadow: var(--bs-primary-border);
|
||||
}
|
||||
&::placeholder {
|
||||
color: var(--tc-surface-low)
|
||||
color: var(--tc-surface-low);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,23 +1,27 @@
|
|||
import React, { useEffect, useRef } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import './Math.scss';
|
||||
import React, { useEffect, useRef } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import "./Math.scss";
|
||||
|
||||
import katex from 'katex';
|
||||
import 'katex/dist/katex.min.css';
|
||||
import katex from "katex";
|
||||
import "katex/dist/katex.min.css";
|
||||
|
||||
import 'katex/dist/contrib/copy-tex';
|
||||
import "katex/dist/contrib/copy-tex";
|
||||
|
||||
const Math = React.memo(({
|
||||
content, throwOnError, errorColor, displayMode,
|
||||
}) => {
|
||||
const ref = useRef(null);
|
||||
const Math = React.memo(
|
||||
({ content, throwOnError, errorColor, displayMode }) => {
|
||||
const ref = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
katex.render(content, ref.current, { throwOnError, errorColor, displayMode });
|
||||
}, [content, throwOnError, errorColor, displayMode]);
|
||||
useEffect(() => {
|
||||
katex.render(content, ref.current, {
|
||||
throwOnError,
|
||||
errorColor,
|
||||
displayMode,
|
||||
});
|
||||
}, [content, throwOnError, errorColor, displayMode]);
|
||||
|
||||
return <span ref={ref} />;
|
||||
});
|
||||
return <span ref={ref} />;
|
||||
}
|
||||
);
|
||||
Math.defaultProps = {
|
||||
throwOnError: null,
|
||||
errorColor: null,
|
||||
|
|
|
@ -1,36 +1,43 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import './RawModal.scss';
|
||||
import React, { useEffect } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import "./RawModal.scss";
|
||||
|
||||
import Modal from 'react-modal';
|
||||
import Modal from "react-modal";
|
||||
|
||||
import navigation from '../../../client/state/navigation';
|
||||
import navigation from "../../../client/state/navigation";
|
||||
|
||||
Modal.setAppElement('#root');
|
||||
Modal.setAppElement("#root");
|
||||
|
||||
function RawModal({
|
||||
className, overlayClassName,
|
||||
isOpen, size, onAfterOpen, onAfterClose,
|
||||
onRequestClose, closeFromOutside, children,
|
||||
className,
|
||||
overlayClassName,
|
||||
isOpen,
|
||||
size,
|
||||
onAfterOpen,
|
||||
onAfterClose,
|
||||
onRequestClose,
|
||||
closeFromOutside,
|
||||
children,
|
||||
}) {
|
||||
let modalClass = (className !== null) ? `${className} ` : '';
|
||||
let modalClass = className !== null ? `${className} ` : "";
|
||||
switch (size) {
|
||||
case 'large':
|
||||
modalClass += 'raw-modal__large ';
|
||||
case "large":
|
||||
modalClass += "raw-modal__large ";
|
||||
break;
|
||||
case 'medium':
|
||||
modalClass += 'raw-modal__medium ';
|
||||
case "medium":
|
||||
modalClass += "raw-modal__medium ";
|
||||
break;
|
||||
case 'small':
|
||||
case "small":
|
||||
default:
|
||||
modalClass += 'raw-modal__small ';
|
||||
modalClass += "raw-modal__small ";
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
navigation.setIsRawModalVisible(isOpen);
|
||||
}, [isOpen]);
|
||||
|
||||
const modalOverlayClass = (overlayClassName !== null) ? `${overlayClassName} ` : '';
|
||||
const modalOverlayClass =
|
||||
overlayClassName !== null ? `${overlayClassName} ` : "";
|
||||
return (
|
||||
<Modal
|
||||
className={`${modalClass}raw-modal`}
|
||||
|
@ -51,7 +58,7 @@ function RawModal({
|
|||
RawModal.defaultProps = {
|
||||
className: null,
|
||||
overlayClassName: null,
|
||||
size: 'small',
|
||||
size: "small",
|
||||
onAfterOpen: null,
|
||||
onAfterClose: null,
|
||||
onRequestClose: null,
|
||||
|
@ -62,7 +69,7 @@ RawModal.propTypes = {
|
|||
className: PropTypes.string,
|
||||
overlayClassName: PropTypes.string,
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
size: PropTypes.oneOf(['large', 'medium', 'small']),
|
||||
size: PropTypes.oneOf(["large", "medium", "small"]),
|
||||
onAfterOpen: PropTypes.func,
|
||||
onAfterClose: PropTypes.func,
|
||||
onRequestClose: PropTypes.func,
|
||||
|
|
|
@ -49,7 +49,7 @@
|
|||
@keyframes raw-modal--content {
|
||||
0% {
|
||||
transform: translateY(100px);
|
||||
opacity: .5;
|
||||
opacity: 0.5;
|
||||
}
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
|
|
|
@ -1,21 +1,25 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import './ScrollView.scss';
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import "./ScrollView.scss";
|
||||
|
||||
const ScrollView = React.forwardRef(({
|
||||
horizontal, vertical, autoHide, invisible, onScroll, children,
|
||||
}, ref) => {
|
||||
let scrollbarClasses = '';
|
||||
if (horizontal) scrollbarClasses += ' scrollbar__h';
|
||||
if (vertical) scrollbarClasses += ' scrollbar__v';
|
||||
if (autoHide) scrollbarClasses += ' scrollbar--auto-hide';
|
||||
if (invisible) scrollbarClasses += ' scrollbar--invisible';
|
||||
return (
|
||||
<div onScroll={onScroll} ref={ref} className={`scrollbar${scrollbarClasses}`}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
const ScrollView = React.forwardRef(
|
||||
({ horizontal, vertical, autoHide, invisible, onScroll, children }, ref) => {
|
||||
let scrollbarClasses = "";
|
||||
if (horizontal) scrollbarClasses += " scrollbar__h";
|
||||
if (vertical) scrollbarClasses += " scrollbar__v";
|
||||
if (autoHide) scrollbarClasses += " scrollbar--auto-hide";
|
||||
if (invisible) scrollbarClasses += " scrollbar--invisible";
|
||||
return (
|
||||
<div
|
||||
onScroll={onScroll}
|
||||
ref={ref}
|
||||
className={`scrollbar${scrollbarClasses}`}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
ScrollView.defaultProps = {
|
||||
horizontal: false,
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
@use '../../partials/dir';
|
||||
@use '_scrollbar';
|
||||
@use "../../partials/dir";
|
||||
@use "_scrollbar";
|
||||
|
||||
@mixin paddingForSafari($padding) {
|
||||
@media not all and (min-resolution:.001dpcm) {
|
||||
@media not all and (min-resolution: 0.001dpcm) {
|
||||
@include dir.side(padding, 0, $padding);
|
||||
}
|
||||
}
|
||||
|
@ -28,4 +28,4 @@
|
|||
@include scrollbar.scroll--invisible;
|
||||
@include paddingForSafari(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -62,4 +62,4 @@
|
|||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,15 +1,13 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import './SegmentedControls.scss';
|
||||
import React, { useState, useEffect } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import "./SegmentedControls.scss";
|
||||
|
||||
import { blurOnBubbling } from '../button/script';
|
||||
import { blurOnBubbling } from "../button/script";
|
||||
|
||||
import Text from '../text/Text';
|
||||
import RawIcon from '../system-icons/RawIcon';
|
||||
import Text from "../text/Text";
|
||||
import RawIcon from "../system-icons/RawIcon";
|
||||
|
||||
function SegmentedControls({
|
||||
selected, segments, onSelect,
|
||||
}) {
|
||||
function SegmentedControls({ selected, segments, onSelect }) {
|
||||
const [select, setSelect] = useState(selected);
|
||||
|
||||
function selectSegment(segmentIndex) {
|
||||
|
@ -23,32 +21,34 @@ function SegmentedControls({
|
|||
|
||||
return (
|
||||
<div className="segmented-controls">
|
||||
{
|
||||
segments.map((segment, index) => (
|
||||
<button
|
||||
key={Math.random().toString(20).substr(2, 6)}
|
||||
className={`segment-btn${select === index ? ' segment-btn--active' : ''}`}
|
||||
type="button"
|
||||
onClick={() => selectSegment(index)}
|
||||
onMouseUp={(e) => blurOnBubbling(e, '.segment-btn')}
|
||||
>
|
||||
<div className="segment-btn__base">
|
||||
{segment.iconSrc && <RawIcon size="small" src={segment.iconSrc} />}
|
||||
{segment.text && <Text variant="b2">{segment.text}</Text>}
|
||||
</div>
|
||||
</button>
|
||||
))
|
||||
}
|
||||
{segments.map((segment, index) => (
|
||||
<button
|
||||
key={Math.random().toString(20).substr(2, 6)}
|
||||
className={`segment-btn${
|
||||
select === index ? " segment-btn--active" : ""
|
||||
}`}
|
||||
type="button"
|
||||
onClick={() => selectSegment(index)}
|
||||
onMouseUp={(e) => blurOnBubbling(e, ".segment-btn")}
|
||||
>
|
||||
<div className="segment-btn__base">
|
||||
{segment.iconSrc && <RawIcon size="small" src={segment.iconSrc} />}
|
||||
{segment.text && <Text variant="b2">{segment.text}</Text>}
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
SegmentedControls.propTypes = {
|
||||
selected: PropTypes.number.isRequired,
|
||||
segments: PropTypes.arrayOf(PropTypes.shape({
|
||||
iconSrc: PropTypes.string,
|
||||
text: PropTypes.string,
|
||||
})).isRequired,
|
||||
segments: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
iconSrc: PropTypes.string,
|
||||
text: PropTypes.string,
|
||||
})
|
||||
).isRequired,
|
||||
onSelect: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
@use '../button/state';
|
||||
@use '../../partials/dir';
|
||||
@use "../button/state";
|
||||
@use "../../partials/dir";
|
||||
|
||||
.segmented-controls {
|
||||
background-color: var(--bg-surface-low);
|
||||
|
@ -40,18 +40,22 @@
|
|||
& + .segment-btn .segment-btn__base {
|
||||
border: none;
|
||||
}
|
||||
&:first-child{
|
||||
&:first-child {
|
||||
border-left: none;
|
||||
}
|
||||
&:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
[dir=rtl] & {
|
||||
[dir="rtl"] & {
|
||||
border-left: 1px solid var(--bg-surface-border);
|
||||
border-right: 1px solid var(--bg-surface-border);
|
||||
|
||||
&:first-child { border-right: none;}
|
||||
&:last-child { border-left: none;}
|
||||
&:first-child {
|
||||
border-right: none;
|
||||
}
|
||||
&:last-child {
|
||||
border-left: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,19 +1,17 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import './Spinner.scss';
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import "./Spinner.scss";
|
||||
|
||||
function Spinner({ size }) {
|
||||
return (
|
||||
<div className={`donut-spinner donut-spinner--${size}`}> </div>
|
||||
);
|
||||
return <div className={`donut-spinner donut-spinner--${size}`}> </div>;
|
||||
}
|
||||
|
||||
Spinner.defaultProps = {
|
||||
size: 'normal',
|
||||
size: "normal",
|
||||
};
|
||||
|
||||
Spinner.propTypes = {
|
||||
size: PropTypes.oneOf(['normal', 'small']),
|
||||
size: PropTypes.oneOf(["normal", "small"]),
|
||||
};
|
||||
|
||||
export default Spinner;
|
||||
|
|
|
@ -19,4 +19,4 @@
|
|||
to {
|
||||
transform: rotate(1turn);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import './RawIcon.scss';
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import "./RawIcon.scss";
|
||||
|
||||
function RawIcon({ color, size, src, isImage }) {
|
||||
const style = {};
|
||||
if (color !== null) style.backgroundColor = color;
|
||||
if (isImage) {
|
||||
style.backgroundColor = 'transparent';
|
||||
style.backgroundColor = "transparent";
|
||||
style.backgroundImage = `url("${src}")`;
|
||||
} else {
|
||||
style.WebkitMaskImage = `url("${src}")`;
|
||||
|
@ -18,13 +18,13 @@ function RawIcon({ color, size, src, isImage }) {
|
|||
|
||||
RawIcon.defaultProps = {
|
||||
color: null,
|
||||
size: 'normal',
|
||||
size: "normal",
|
||||
isImage: false,
|
||||
};
|
||||
|
||||
RawIcon.propTypes = {
|
||||
color: PropTypes.string,
|
||||
size: PropTypes.oneOf(['large', 'normal', 'small', 'extra-small']),
|
||||
size: PropTypes.oneOf(["large", "normal", "small", "extra-small"]),
|
||||
src: PropTypes.string.isRequired,
|
||||
isImage: PropTypes.bool,
|
||||
};
|
||||
|
|
|
@ -25,4 +25,4 @@
|
|||
}
|
||||
.ic-raw-extra-small {
|
||||
@include icSize(var(--ic-extra-small));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,15 +1,12 @@
|
|||
import React, { useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import './Tabs.scss';
|
||||
import React, { useState } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import "./Tabs.scss";
|
||||
|
||||
import Button from '../button/Button';
|
||||
import ScrollView from '../scroll/ScrollView';
|
||||
import Button from "../button/Button";
|
||||
import ScrollView from "../scroll/ScrollView";
|
||||
|
||||
function TabItem({
|
||||
selected, iconSrc,
|
||||
onClick, children, disabled,
|
||||
}) {
|
||||
const isSelected = selected ? 'tab-item--selected' : '';
|
||||
function TabItem({ selected, iconSrc, onClick, children, disabled }) {
|
||||
const isSelected = selected ? "tab-item--selected" : "";
|
||||
|
||||
return (
|
||||
<Button
|
||||
|
@ -78,7 +75,7 @@ Tabs.propTypes = {
|
|||
iconSrc: PropTypes.string,
|
||||
text: PropTypes.string,
|
||||
disabled: PropTypes.bool,
|
||||
}),
|
||||
})
|
||||
).isRequired,
|
||||
defaultSelected: PropTypes.number,
|
||||
onSelect: PropTypes.func.isRequired,
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
@use '../../partials/dir';
|
||||
@use "../../partials/dir";
|
||||
|
||||
.tabs {
|
||||
height: var(--header-height);
|
||||
|
@ -13,12 +13,12 @@
|
|||
|
||||
.tab-item {
|
||||
flex-shrink: 0;
|
||||
|
||||
|
||||
@include dir.side(padding, var(--sp-normal), 24px);
|
||||
border-radius: 0;
|
||||
height: 100%;
|
||||
box-shadow: none;
|
||||
border-radius: var(--bo-radius) var(--bo-radius) 0 0;
|
||||
border-radius: var(--bo-radius) var(--bo-radius) 0 0;
|
||||
|
||||
&:focus,
|
||||
&:active {
|
||||
|
@ -42,4 +42,4 @@
|
|||
box-shadow: var(--bs-tab-selected);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,30 +1,51 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import './Text.scss';
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import "./Text.scss";
|
||||
|
||||
function Text({
|
||||
className, style, variant, weight,
|
||||
primary, span, children,
|
||||
}) {
|
||||
function Text({ className, style, variant, weight, primary, span, children }) {
|
||||
const classes = [];
|
||||
if (className) classes.push(className);
|
||||
|
||||
classes.push(`text text-${variant} text-${weight}`);
|
||||
if (primary) classes.push('font-primary');
|
||||
if (primary) classes.push("font-primary");
|
||||
|
||||
const textClass = classes.join(' ');
|
||||
if (span) return <span className={textClass} style={style}>{ children }</span>;
|
||||
if (variant === 'h1') return <h1 className={textClass} style={style}>{ children }</h1>;
|
||||
if (variant === 'h2') return <h2 className={textClass} style={style}>{ children }</h2>;
|
||||
if (variant === 's1') return <h4 className={textClass} style={style}>{ children }</h4>;
|
||||
return <p className={textClass} style={style}>{ children }</p>;
|
||||
const textClass = classes.join(" ");
|
||||
if (span)
|
||||
return (
|
||||
<span className={textClass} style={style}>
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
if (variant === "h1")
|
||||
return (
|
||||
<h1 className={textClass} style={style}>
|
||||
{children}
|
||||
</h1>
|
||||
);
|
||||
if (variant === "h2")
|
||||
return (
|
||||
<h2 className={textClass} style={style}>
|
||||
{children}
|
||||
</h2>
|
||||
);
|
||||
if (variant === "s1")
|
||||
return (
|
||||
<h4 className={textClass} style={style}>
|
||||
{children}
|
||||
</h4>
|
||||
);
|
||||
return (
|
||||
<p className={textClass} style={style}>
|
||||
{children}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
Text.defaultProps = {
|
||||
className: null,
|
||||
style: null,
|
||||
variant: 'b1',
|
||||
weight: 'normal',
|
||||
variant: "b1",
|
||||
weight: "normal",
|
||||
primary: false,
|
||||
span: false,
|
||||
};
|
||||
|
@ -32,8 +53,8 @@ Text.defaultProps = {
|
|||
Text.propTypes = {
|
||||
className: PropTypes.string,
|
||||
style: PropTypes.shape({}),
|
||||
variant: PropTypes.oneOf(['h1', 'h2', 's1', 'b1', 'b2', 'b3']),
|
||||
weight: PropTypes.oneOf(['light', 'normal', 'medium', 'bold']),
|
||||
variant: PropTypes.oneOf(["h1", "h2", "s1", "b1", "b2", "b3"]),
|
||||
weight: PropTypes.oneOf(["light", "normal", "medium", "bold"]),
|
||||
primary: PropTypes.bool,
|
||||
span: PropTypes.bool,
|
||||
children: PropTypes.node.isRequired,
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
& img.emoji,
|
||||
& img[data-mx-emoticon] {
|
||||
height: calc(var(--lh-#{$type}) - .25rem);
|
||||
height: calc(var(--lh-#{$type}) - 0.25rem);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -20,7 +20,7 @@
|
|||
margin-right: 2px !important;
|
||||
padding: 0 !important;
|
||||
position: relative;
|
||||
top: -.1rem;
|
||||
top: -0.1rem;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
|
@ -58,4 +58,4 @@
|
|||
.text-b3 {
|
||||
@include font(b3);
|
||||
color: var(--tc-surface-low);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
import dateFormat from 'dateformat';
|
||||
import { isInSameDay } from '../../../util/common';
|
||||
import dateFormat from "dateformat";
|
||||
import { isInSameDay } from "../../../util/common";
|
||||
|
||||
function Time({ timestamp, fullTime }) {
|
||||
const date = new Date(timestamp);
|
||||
|
||||
const formattedFullTime = dateFormat(date, 'dd mmmm yyyy, hh:MM TT');
|
||||
const formattedFullTime = dateFormat(date, "dd mmmm yyyy, hh:MM TT");
|
||||
let formattedDate = formattedFullTime;
|
||||
|
||||
if (!fullTime) {
|
||||
|
@ -16,17 +16,17 @@ function Time({ timestamp, fullTime }) {
|
|||
compareDate.setDate(compareDate.getDate() - 1);
|
||||
const isYesterday = isInSameDay(date, compareDate);
|
||||
|
||||
formattedDate = dateFormat(date, isToday || isYesterday ? 'hh:MM TT' : 'dd/mm/yyyy');
|
||||
formattedDate = dateFormat(
|
||||
date,
|
||||
isToday || isYesterday ? "hh:MM TT" : "dd/mm/yyyy"
|
||||
);
|
||||
if (isYesterday) {
|
||||
formattedDate = `Yesterday, ${formattedDate}`;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<time
|
||||
dateTime={date.toISOString()}
|
||||
title={formattedFullTime}
|
||||
>
|
||||
<time dateTime={date.toISOString()} title={formattedFullTime}>
|
||||
{formattedDate}
|
||||
</time>
|
||||
);
|
||||
|
|
|
@ -1,11 +1,9 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import './Tooltip.scss';
|
||||
import Tippy from '@tippyjs/react';
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import "./Tooltip.scss";
|
||||
import Tippy from "@tippyjs/react";
|
||||
|
||||
function Tooltip({
|
||||
className, placement, content, delay, children,
|
||||
}) {
|
||||
function Tooltip({ className, placement, content, delay, children }) {
|
||||
return (
|
||||
<Tippy
|
||||
content={content}
|
||||
|
@ -23,8 +21,8 @@ function Tooltip({
|
|||
}
|
||||
|
||||
Tooltip.defaultProps = {
|
||||
placement: 'top',
|
||||
className: '',
|
||||
placement: "top",
|
||||
className: "",
|
||||
delay: [200, 0],
|
||||
};
|
||||
|
||||
|
|
|
@ -7,4 +7,4 @@
|
|||
.text {
|
||||
color: var(--tc-tooltip);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
/* eslint-disable import/prefer-default-export */
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
import initMatrix from '../../client/initMatrix';
|
||||
import initMatrix from "../../client/initMatrix";
|
||||
|
||||
export function useAccountData(eventType) {
|
||||
const mx = initMatrix.matrixClient;
|
||||
|
@ -12,9 +12,9 @@ export function useAccountData(eventType) {
|
|||
if (mEvent.getType() !== eventType) return;
|
||||
setEvent(mEvent);
|
||||
};
|
||||
mx.on('accountData', handleChange);
|
||||
mx.on("accountData", handleChange);
|
||||
return () => {
|
||||
mx.removeListener('accountData', handleChange);
|
||||
mx.removeListener("accountData", handleChange);
|
||||
};
|
||||
}, [eventType]);
|
||||
|
||||
|
|
|
@ -1,22 +1,27 @@
|
|||
/* eslint-disable import/prefer-default-export */
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
import initMatrix from '../../client/initMatrix';
|
||||
import cons from '../../client/state/cons';
|
||||
import initMatrix from "../../client/initMatrix";
|
||||
import cons from "../../client/state/cons";
|
||||
|
||||
export function useCategorizedSpaces() {
|
||||
const { accountData } = initMatrix;
|
||||
const [categorizedSpaces, setCategorizedSpaces] = useState([...accountData.categorizedSpaces]);
|
||||
const [categorizedSpaces, setCategorizedSpaces] = useState([
|
||||
...accountData.categorizedSpaces,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleCategorizedSpaces = () => {
|
||||
setCategorizedSpaces([...accountData.categorizedSpaces]);
|
||||
};
|
||||
accountData.on(cons.events.accountData.CATEGORIZE_SPACE_UPDATED, handleCategorizedSpaces);
|
||||
accountData.on(
|
||||
cons.events.accountData.CATEGORIZE_SPACE_UPDATED,
|
||||
handleCategorizedSpaces
|
||||
);
|
||||
return () => {
|
||||
accountData.removeListener(
|
||||
cons.events.accountData.CATEGORIZE_SPACE_UPDATED,
|
||||
handleCategorizedSpaces,
|
||||
handleCategorizedSpaces
|
||||
);
|
||||
};
|
||||
}, []);
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
/* eslint-disable import/prefer-default-export */
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
import initMatrix from '../../client/initMatrix';
|
||||
import { hasCrossSigningAccountData } from '../../util/matrixUtil';
|
||||
import initMatrix from "../../client/initMatrix";
|
||||
import { hasCrossSigningAccountData } from "../../util/matrixUtil";
|
||||
|
||||
export function useCrossSigningStatus() {
|
||||
const mx = initMatrix.matrixClient;
|
||||
|
@ -11,14 +11,14 @@ export function useCrossSigningStatus() {
|
|||
useEffect(() => {
|
||||
if (isCSEnabled) return null;
|
||||
const handleAccountData = (event) => {
|
||||
if (event.getType() === 'm.cross_signing.master') {
|
||||
if (event.getType() === "m.cross_signing.master") {
|
||||
setIsCSEnabled(true);
|
||||
}
|
||||
};
|
||||
|
||||
mx.on('accountData', handleAccountData);
|
||||
mx.on("accountData", handleAccountData);
|
||||
return () => {
|
||||
mx.removeListener('accountData', handleAccountData);
|
||||
mx.removeListener("accountData", handleAccountData);
|
||||
};
|
||||
}, [isCSEnabled === false]);
|
||||
return isCSEnabled;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
/* eslint-disable import/prefer-default-export */
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
import initMatrix from '../../client/initMatrix';
|
||||
import initMatrix from "../../client/initMatrix";
|
||||
|
||||
export function useDeviceList() {
|
||||
const mx = initMatrix.matrixClient;
|
||||
|
@ -10,10 +10,11 @@ export function useDeviceList() {
|
|||
useEffect(() => {
|
||||
let isMounted = true;
|
||||
|
||||
const updateDevices = () => mx.getDevices().then((data) => {
|
||||
if (!isMounted) return;
|
||||
setDeviceList(data.devices || []);
|
||||
});
|
||||
const updateDevices = () =>
|
||||
mx.getDevices().then((data) => {
|
||||
if (!isMounted) return;
|
||||
setDeviceList(data.devices || []);
|
||||
});
|
||||
updateDevices();
|
||||
|
||||
const handleDevicesUpdate = (users) => {
|
||||
|
@ -22,9 +23,9 @@ export function useDeviceList() {
|
|||
}
|
||||
};
|
||||
|
||||
mx.on('crypto.devicesUpdated', handleDevicesUpdate);
|
||||
mx.on("crypto.devicesUpdated", handleDevicesUpdate);
|
||||
return () => {
|
||||
mx.removeListener('crypto.devicesUpdated', handleDevicesUpdate);
|
||||
mx.removeListener("crypto.devicesUpdated", handleDevicesUpdate);
|
||||
isMounted = false;
|
||||
};
|
||||
}, []);
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
/* eslint-disable import/prefer-default-export */
|
||||
import { useState } from 'react';
|
||||
import { useState } from "react";
|
||||
|
||||
export function useForceUpdate() {
|
||||
const [data, setData] = useState(null);
|
||||
|
||||
return [data, function forceUpdateHook() {
|
||||
setData({});
|
||||
}];
|
||||
return [
|
||||
data,
|
||||
function forceUpdateHook() {
|
||||
setData({});
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/* eslint-disable import/prefer-default-export */
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export function usePermission(name, initial) {
|
||||
const [state, setState] = useState(initial);
|
||||
|
@ -15,12 +15,12 @@ export function usePermission(name, initial) {
|
|||
descriptor = _descriptor;
|
||||
|
||||
update();
|
||||
descriptor.addEventListener('change', update);
|
||||
descriptor.addEventListener("change", update);
|
||||
});
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (descriptor) descriptor.removeEventListener('change', update);
|
||||
if (descriptor) descriptor.removeEventListener("change", update);
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
/* eslint-disable import/prefer-default-export */
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
import cons from '../../client/state/cons';
|
||||
import navigation from '../../client/state/navigation';
|
||||
import cons from "../../client/state/cons";
|
||||
import navigation from "../../client/state/navigation";
|
||||
|
||||
export function useSelectedSpace() {
|
||||
const [spaceId, setSpaceId] = useState(navigation.selectedSpaceId);
|
||||
|
@ -13,7 +13,10 @@ export function useSelectedSpace() {
|
|||
};
|
||||
navigation.on(cons.events.navigation.SPACE_SELECTED, onSpaceSelected);
|
||||
return () => {
|
||||
navigation.removeListener(cons.events.navigation.SPACE_SELECTED, onSpaceSelected);
|
||||
navigation.removeListener(
|
||||
cons.events.navigation.SPACE_SELECTED,
|
||||
onSpaceSelected
|
||||
);
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
/* eslint-disable import/prefer-default-export */
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
import cons from '../../client/state/cons';
|
||||
import navigation from '../../client/state/navigation';
|
||||
import cons from "../../client/state/cons";
|
||||
import navigation from "../../client/state/navigation";
|
||||
|
||||
export function useSelectedTab() {
|
||||
const [selectedTab, setSelectedTab] = useState(navigation.selectedTab);
|
||||
|
@ -13,7 +13,10 @@ export function useSelectedTab() {
|
|||
};
|
||||
navigation.on(cons.events.navigation.TAB_SELECTED, onTabSelected);
|
||||
return () => {
|
||||
navigation.removeListener(cons.events.navigation.TAB_SELECTED, onTabSelected);
|
||||
navigation.removeListener(
|
||||
cons.events.navigation.TAB_SELECTED,
|
||||
onTabSelected
|
||||
);
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
|
|
@ -1,22 +1,27 @@
|
|||
/* eslint-disable import/prefer-default-export */
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
import initMatrix from '../../client/initMatrix';
|
||||
import cons from '../../client/state/cons';
|
||||
import initMatrix from "../../client/initMatrix";
|
||||
import cons from "../../client/state/cons";
|
||||
|
||||
export function useSpaceShortcut() {
|
||||
const { accountData } = initMatrix;
|
||||
const [spaceShortcut, setSpaceShortcut] = useState([...accountData.spaceShortcut]);
|
||||
const [spaceShortcut, setSpaceShortcut] = useState([
|
||||
...accountData.spaceShortcut,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
const onSpaceShortcutUpdated = () => {
|
||||
setSpaceShortcut([...accountData.spaceShortcut]);
|
||||
};
|
||||
accountData.on(cons.events.accountData.SPACE_SHORTCUT_UPDATED, onSpaceShortcutUpdated);
|
||||
accountData.on(
|
||||
cons.events.accountData.SPACE_SHORTCUT_UPDATED,
|
||||
onSpaceShortcutUpdated
|
||||
);
|
||||
return () => {
|
||||
accountData.removeListener(
|
||||
cons.events.accountData.SPACE_SHORTCUT_UPDATED,
|
||||
onSpaceShortcutUpdated,
|
||||
onSpaceShortcutUpdated
|
||||
);
|
||||
};
|
||||
}, []);
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/* eslint-disable import/prefer-default-export */
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
export function useStore(...args) {
|
||||
const itemRef = useRef(null);
|
||||
|
|
|
@ -1,20 +1,20 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import './ConfirmDialog.scss';
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import "./ConfirmDialog.scss";
|
||||
|
||||
import { openReusableDialog } from '../../../client/action/navigation';
|
||||
import { openReusableDialog } from "../../../client/action/navigation";
|
||||
|
||||
import Text from '../../atoms/text/Text';
|
||||
import Button from '../../atoms/button/Button';
|
||||
import Text from "../../atoms/text/Text";
|
||||
import Button from "../../atoms/button/Button";
|
||||
|
||||
function ConfirmDialog({
|
||||
desc, actionTitle, actionType, onComplete,
|
||||
}) {
|
||||
function ConfirmDialog({ desc, actionTitle, actionType, onComplete }) {
|
||||
return (
|
||||
<div className="confirm-dialog">
|
||||
<Text>{desc}</Text>
|
||||
<div className="confirm-dialog__btn">
|
||||
<Button variant={actionType} onClick={() => onComplete(true)}>{actionTitle}</Button>
|
||||
<Button variant={actionType} onClick={() => onComplete(true)}>
|
||||
{actionTitle}
|
||||
</Button>
|
||||
<Button onClick={() => onComplete(false)}>Cancel</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -23,7 +23,8 @@ function ConfirmDialog({
|
|||
ConfirmDialog.propTypes = {
|
||||
desc: PropTypes.string.isRequired,
|
||||
actionTitle: PropTypes.string.isRequired,
|
||||
actionType: PropTypes.oneOf(['primary', 'positive', 'danger', 'caution']).isRequired,
|
||||
actionType: PropTypes.oneOf(["primary", "positive", "danger", "caution"])
|
||||
.isRequired,
|
||||
onComplete: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
|
@ -35,24 +36,32 @@ ConfirmDialog.propTypes = {
|
|||
* @return {Promise<boolean>} does it get's confirmed or not
|
||||
*/
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export const confirmDialog = (title, desc, actionTitle, actionType = 'primary') => new Promise((resolve) => {
|
||||
let isCompleted = false;
|
||||
openReusableDialog(
|
||||
<Text variant="s1" weight="medium">{title}</Text>,
|
||||
(requestClose) => (
|
||||
<ConfirmDialog
|
||||
desc={desc}
|
||||
actionTitle={actionTitle}
|
||||
actionType={actionType}
|
||||
onComplete={(isConfirmed) => {
|
||||
isCompleted = true;
|
||||
resolve(isConfirmed);
|
||||
requestClose();
|
||||
}}
|
||||
/>
|
||||
),
|
||||
() => {
|
||||
if (!isCompleted) resolve(false);
|
||||
},
|
||||
);
|
||||
});
|
||||
export const confirmDialog = (
|
||||
title,
|
||||
desc,
|
||||
actionTitle,
|
||||
actionType = "primary"
|
||||
) =>
|
||||
new Promise((resolve) => {
|
||||
let isCompleted = false;
|
||||
openReusableDialog(
|
||||
<Text variant="s1" weight="medium">
|
||||
{title}
|
||||
</Text>,
|
||||
(requestClose) => (
|
||||
<ConfirmDialog
|
||||
desc={desc}
|
||||
actionTitle={actionTitle}
|
||||
actionType={actionType}
|
||||
onComplete={(isConfirmed) => {
|
||||
isCompleted = true;
|
||||
resolve(isConfirmed);
|
||||
requestClose();
|
||||
}}
|
||||
/>
|
||||
),
|
||||
() => {
|
||||
if (!isCompleted) resolve(false);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
|
|
@ -8,4 +8,4 @@
|
|||
display: flex;
|
||||
gap: var(--sp-normal);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,22 +1,29 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import './Dialog.scss';
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import "./Dialog.scss";
|
||||
|
||||
import { twemojify } from '../../../util/twemojify';
|
||||
import { twemojify } from "../../../util/twemojify";
|
||||
|
||||
import Text from '../../atoms/text/Text';
|
||||
import Header, { TitleWrapper } from '../../atoms/header/Header';
|
||||
import ScrollView from '../../atoms/scroll/ScrollView';
|
||||
import RawModal from '../../atoms/modal/RawModal';
|
||||
import Text from "../../atoms/text/Text";
|
||||
import Header, { TitleWrapper } from "../../atoms/header/Header";
|
||||
import ScrollView from "../../atoms/scroll/ScrollView";
|
||||
import RawModal from "../../atoms/modal/RawModal";
|
||||
|
||||
function Dialog({
|
||||
className, isOpen, title, onAfterOpen, onAfterClose,
|
||||
contentOptions, onRequestClose, closeFromOutside, children,
|
||||
className,
|
||||
isOpen,
|
||||
title,
|
||||
onAfterOpen,
|
||||
onAfterClose,
|
||||
contentOptions,
|
||||
onRequestClose,
|
||||
closeFromOutside,
|
||||
children,
|
||||
invisibleScroll,
|
||||
}) {
|
||||
return (
|
||||
<RawModal
|
||||
className={`${className === null ? '' : `${className} `}dialog-modal`}
|
||||
className={`${className === null ? "" : `${className} `}dialog-modal`}
|
||||
isOpen={isOpen}
|
||||
onAfterOpen={onAfterOpen}
|
||||
onAfterClose={onAfterClose}
|
||||
|
@ -28,19 +35,19 @@ function Dialog({
|
|||
<div className="dialog__content">
|
||||
<Header>
|
||||
<TitleWrapper>
|
||||
{
|
||||
typeof title === 'string'
|
||||
? <Text variant="h2" weight="medium" primary>{twemojify(title)}</Text>
|
||||
: title
|
||||
}
|
||||
{typeof title === "string" ? (
|
||||
<Text variant="h2" weight="medium" primary>
|
||||
{twemojify(title)}
|
||||
</Text>
|
||||
) : (
|
||||
title
|
||||
)}
|
||||
</TitleWrapper>
|
||||
{contentOptions}
|
||||
</Header>
|
||||
<div className="dialog__content__wrapper">
|
||||
<ScrollView autoHide={!invisibleScroll} invisible={invisibleScroll}>
|
||||
<div className="dialog__content-container">
|
||||
{children}
|
||||
</div>
|
||||
<div className="dialog__content-container">{children}</div>
|
||||
</ScrollView>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useState, useEffect } from "react";
|
||||
|
||||
import cons from '../../../client/state/cons';
|
||||
import cons from "../../../client/state/cons";
|
||||
|
||||
import navigation from '../../../client/state/navigation';
|
||||
import IconButton from '../../atoms/button/IconButton';
|
||||
import Dialog from './Dialog';
|
||||
import navigation from "../../../client/state/navigation";
|
||||
import IconButton from "../../atoms/button/IconButton";
|
||||
import Dialog from "./Dialog";
|
||||
|
||||
import CrossIC from '../../../../public/res/ic/outlined/cross.svg';
|
||||
import CrossIC from "../../../../public/res/ic/outlined/cross.svg";
|
||||
|
||||
function ReusableDialog() {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
@ -19,7 +19,10 @@ function ReusableDialog() {
|
|||
};
|
||||
navigation.on(cons.events.navigation.REUSABLE_DIALOG_OPENED, handleOpen);
|
||||
return () => {
|
||||
navigation.removeListener(cons.events.navigation.REUSABLE_DIALOG_OPENED, handleOpen);
|
||||
navigation.removeListener(
|
||||
cons.events.navigation.REUSABLE_DIALOG_OPENED,
|
||||
handleOpen
|
||||
);
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
@ -35,10 +38,16 @@ function ReusableDialog() {
|
|||
return (
|
||||
<Dialog
|
||||
isOpen={isOpen}
|
||||
title={data?.title || ''}
|
||||
title={data?.title || ""}
|
||||
onAfterClose={handleAfterClose}
|
||||
onRequestClose={handleRequestClose}
|
||||
contentOptions={<IconButton src={CrossIC} onClick={handleRequestClose} tooltip="Close" />}
|
||||
contentOptions={
|
||||
<IconButton
|
||||
src={CrossIC}
|
||||
onClick={handleRequestClose}
|
||||
tooltip="Close"
|
||||
/>
|
||||
}
|
||||
invisibleScroll
|
||||
>
|
||||
{data?.render(handleRequestClose) || <div />}
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
/* eslint-disable react/prop-types */
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import './FollowingMembers.scss';
|
||||
import React, { useState, useEffect } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import "./FollowingMembers.scss";
|
||||
|
||||
import initMatrix from '../../../client/initMatrix';
|
||||
import cons from '../../../client/state/cons';
|
||||
import { openReadReceipts } from '../../../client/action/navigation';
|
||||
import initMatrix from "../../../client/initMatrix";
|
||||
import cons from "../../../client/state/cons";
|
||||
import { openReadReceipts } from "../../../client/action/navigation";
|
||||
|
||||
import Text from '../../atoms/text/Text';
|
||||
import RawIcon from '../../atoms/system-icons/RawIcon';
|
||||
import TickMarkIC from '../../../../public/res/ic/outlined/tick-mark.svg';
|
||||
import Text from "../../atoms/text/Text";
|
||||
import RawIcon from "../../atoms/system-icons/RawIcon";
|
||||
import TickMarkIC from "../../../../public/res/ic/outlined/tick-mark.svg";
|
||||
|
||||
import { getUsersActionJsx } from '../../organisms/room/common';
|
||||
import { getUsersActionJsx } from "../../organisms/room/common";
|
||||
|
||||
function FollowingMembers({ roomTimeline }) {
|
||||
const [followingMembers, setFollowingMembers] = useState([]);
|
||||
|
@ -27,28 +27,38 @@ function FollowingMembers({ roomTimeline }) {
|
|||
setFollowingMembers(roomTimeline.getLiveReaders());
|
||||
};
|
||||
updateFollowingMembers();
|
||||
roomTimeline.on(cons.events.roomTimeline.LIVE_RECEIPT, updateFollowingMembers);
|
||||
roomTimeline.on(
|
||||
cons.events.roomTimeline.LIVE_RECEIPT,
|
||||
updateFollowingMembers
|
||||
);
|
||||
roomsInput.on(cons.events.roomsInput.MESSAGE_SENT, handleOnMessageSent);
|
||||
return () => {
|
||||
roomTimeline.removeListener(cons.events.roomTimeline.LIVE_RECEIPT, updateFollowingMembers);
|
||||
roomsInput.removeListener(cons.events.roomsInput.MESSAGE_SENT, handleOnMessageSent);
|
||||
roomTimeline.removeListener(
|
||||
cons.events.roomTimeline.LIVE_RECEIPT,
|
||||
updateFollowingMembers
|
||||
);
|
||||
roomsInput.removeListener(
|
||||
cons.events.roomsInput.MESSAGE_SENT,
|
||||
handleOnMessageSent
|
||||
);
|
||||
};
|
||||
}, [roomTimeline]);
|
||||
|
||||
const filteredM = followingMembers.filter((userId) => userId !== myUserId);
|
||||
|
||||
return filteredM.length !== 0 && (
|
||||
<button
|
||||
className="following-members"
|
||||
onClick={() => openReadReceipts(roomId, followingMembers)}
|
||||
type="button"
|
||||
>
|
||||
<RawIcon
|
||||
size="extra-small"
|
||||
src={TickMarkIC}
|
||||
/>
|
||||
<Text variant="b2">{getUsersActionJsx(roomId, filteredM, 'following the conversation.')}</Text>
|
||||
</button>
|
||||
return (
|
||||
filteredM.length !== 0 && (
|
||||
<button
|
||||
className="following-members"
|
||||
onClick={() => openReadReceipts(roomId, followingMembers)}
|
||||
type="button"
|
||||
>
|
||||
<RawIcon size="extra-small" src={TickMarkIC} />
|
||||
<Text variant="b2">
|
||||
{getUsersActionJsx(roomId, filteredM, "following the conversation.")}
|
||||
</Text>
|
||||
</button>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
@use '../../partials/text';
|
||||
@use "../../partials/text";
|
||||
|
||||
.following-members {
|
||||
width: 100%;
|
||||
|
@ -7,7 +7,7 @@
|
|||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
|
||||
|
||||
& .ic-raw {
|
||||
min-width: var(--ic-extra-small);
|
||||
opacity: 0.4;
|
||||
|
@ -28,4 +28,4 @@
|
|||
&:active {
|
||||
background-color: var(--bg-surface-active);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,59 +1,61 @@
|
|||
import React from 'react';
|
||||
import React from "react";
|
||||
|
||||
import initMatrix from '../../../client/initMatrix';
|
||||
import { openReusableContextMenu } from '../../../client/action/navigation';
|
||||
import { getEventCords } from '../../../util/common';
|
||||
import initMatrix from "../../../client/initMatrix";
|
||||
import { openReusableContextMenu } from "../../../client/action/navigation";
|
||||
import { getEventCords } from "../../../util/common";
|
||||
|
||||
import Text from '../../atoms/text/Text';
|
||||
import Button from '../../atoms/button/Button';
|
||||
import { MenuHeader } from '../../atoms/context-menu/ContextMenu';
|
||||
import SettingTile from '../setting-tile/SettingTile';
|
||||
import Text from "../../atoms/text/Text";
|
||||
import Button from "../../atoms/button/Button";
|
||||
import { MenuHeader } from "../../atoms/context-menu/ContextMenu";
|
||||
import SettingTile from "../setting-tile/SettingTile";
|
||||
|
||||
import NotificationSelector from './NotificationSelector';
|
||||
import NotificationSelector from "./NotificationSelector";
|
||||
|
||||
import ChevronBottomIC from '../../../../public/res/ic/outlined/chevron-bottom.svg';
|
||||
import ChevronBottomIC from "../../../../public/res/ic/outlined/chevron-bottom.svg";
|
||||
|
||||
import { useAccountData } from '../../hooks/useAccountData';
|
||||
import { useAccountData } from "../../hooks/useAccountData";
|
||||
|
||||
export const notifType = {
|
||||
ON: 'on',
|
||||
OFF: 'off',
|
||||
NOISY: 'noisy',
|
||||
ON: "on",
|
||||
OFF: "off",
|
||||
NOISY: "noisy",
|
||||
};
|
||||
export const typeToLabel = {
|
||||
[notifType.ON]: 'On',
|
||||
[notifType.OFF]: 'Off',
|
||||
[notifType.NOISY]: 'Noisy',
|
||||
[notifType.ON]: "On",
|
||||
[notifType.OFF]: "Off",
|
||||
[notifType.NOISY]: "Noisy",
|
||||
};
|
||||
Object.freeze(notifType);
|
||||
|
||||
const DM = '.m.rule.room_one_to_one';
|
||||
const ENC_DM = '.m.rule.encrypted_room_one_to_one';
|
||||
const ROOM = '.m.rule.message';
|
||||
const ENC_ROOM = '.m.rule.encrypted';
|
||||
const DM = ".m.rule.room_one_to_one";
|
||||
const ENC_DM = ".m.rule.encrypted_room_one_to_one";
|
||||
const ROOM = ".m.rule.message";
|
||||
const ENC_ROOM = ".m.rule.encrypted";
|
||||
|
||||
export function getActionType(rule) {
|
||||
const { actions } = rule;
|
||||
if (actions.find((action) => action?.set_tweak === 'sound')) return notifType.NOISY;
|
||||
if (actions.find((action) => action?.set_tweak === 'highlight')) return notifType.ON;
|
||||
if (actions.find((action) => action === 'dont_notify')) return notifType.OFF;
|
||||
if (actions.find((action) => action?.set_tweak === "sound"))
|
||||
return notifType.NOISY;
|
||||
if (actions.find((action) => action?.set_tweak === "highlight"))
|
||||
return notifType.ON;
|
||||
if (actions.find((action) => action === "dont_notify")) return notifType.OFF;
|
||||
return notifType.OFF;
|
||||
}
|
||||
|
||||
export function getTypeActions(type, highlightValue = false) {
|
||||
if (type === notifType.OFF) return ['dont_notify'];
|
||||
if (type === notifType.OFF) return ["dont_notify"];
|
||||
|
||||
const highlight = { set_tweak: 'highlight' };
|
||||
if (typeof highlightValue === 'boolean') highlight.value = highlightValue;
|
||||
if (type === notifType.ON) return ['notify', highlight];
|
||||
const highlight = { set_tweak: "highlight" };
|
||||
if (typeof highlightValue === "boolean") highlight.value = highlightValue;
|
||||
if (type === notifType.ON) return ["notify", highlight];
|
||||
|
||||
const sound = { set_tweak: 'sound', value: 'default' };
|
||||
return ['notify', sound, highlight];
|
||||
const sound = { set_tweak: "sound", value: "default" };
|
||||
return ["notify", sound, highlight];
|
||||
}
|
||||
|
||||
function useGlobalNotif() {
|
||||
const mx = initMatrix.matrixClient;
|
||||
const pushRules = useAccountData('m.push_rules')?.getContent();
|
||||
const pushRules = useAccountData("m.push_rules")?.getContent();
|
||||
const underride = pushRules?.global?.underride ?? [];
|
||||
const rulesToType = {
|
||||
[DM]: notifType.ON,
|
||||
|
@ -65,12 +67,14 @@ function useGlobalNotif() {
|
|||
const getRuleCondition = (rule) => {
|
||||
const condition = [];
|
||||
if (rule === DM || rule === ENC_DM) {
|
||||
condition.push({ kind: 'room_member_count', is: '2' });
|
||||
condition.push({ kind: "room_member_count", is: "2" });
|
||||
}
|
||||
condition.push({
|
||||
kind: 'event_match',
|
||||
key: 'type',
|
||||
pattern: [ENC_DM, ENC_ROOM].includes(rule) ? 'm.room.encrypted' : 'm.room.message',
|
||||
kind: "event_match",
|
||||
key: "type",
|
||||
pattern: [ENC_DM, ENC_ROOM].includes(rule)
|
||||
? "m.room.encrypted"
|
||||
: "m.room.message",
|
||||
});
|
||||
return condition;
|
||||
};
|
||||
|
@ -93,7 +97,7 @@ function useGlobalNotif() {
|
|||
}
|
||||
ruleContent.actions = getTypeActions(type);
|
||||
|
||||
mx.setAccountData('m.push_rules', content);
|
||||
mx.setAccountData("m.push_rules", content);
|
||||
};
|
||||
|
||||
const dmRule = underride.find((rule) => rule.rule_id === DM);
|
||||
|
@ -114,8 +118,8 @@ function GlobalNotification() {
|
|||
|
||||
const onSelect = (evt, rule) => {
|
||||
openReusableContextMenu(
|
||||
'bottom',
|
||||
getEventCords(evt, '.btn-surface'),
|
||||
"bottom",
|
||||
getEventCords(evt, ".btn-surface"),
|
||||
(requestClose) => (
|
||||
<NotificationSelector
|
||||
value={rulesToType[rule]}
|
||||
|
@ -124,7 +128,7 @@ function GlobalNotification() {
|
|||
requestClose();
|
||||
}}
|
||||
/>
|
||||
),
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -133,39 +137,67 @@ function GlobalNotification() {
|
|||
<MenuHeader>Global Notifications</MenuHeader>
|
||||
<SettingTile
|
||||
title="Direct messages"
|
||||
options={(
|
||||
<Button onClick={(evt) => onSelect(evt, DM)} iconSrc={ChevronBottomIC}>
|
||||
{ typeToLabel[rulesToType[DM]] }
|
||||
options={
|
||||
<Button
|
||||
onClick={(evt) => onSelect(evt, DM)}
|
||||
iconSrc={ChevronBottomIC}
|
||||
>
|
||||
{typeToLabel[rulesToType[DM]]}
|
||||
</Button>
|
||||
)}
|
||||
content={<Text variant="b3">Default notification settings for all direct message.</Text>}
|
||||
}
|
||||
content={
|
||||
<Text variant="b3">
|
||||
Default notification settings for all direct message.
|
||||
</Text>
|
||||
}
|
||||
/>
|
||||
<SettingTile
|
||||
title="Encrypted direct messages"
|
||||
options={(
|
||||
<Button onClick={(evt) => onSelect(evt, ENC_DM)} iconSrc={ChevronBottomIC}>
|
||||
options={
|
||||
<Button
|
||||
onClick={(evt) => onSelect(evt, ENC_DM)}
|
||||
iconSrc={ChevronBottomIC}
|
||||
>
|
||||
{typeToLabel[rulesToType[ENC_DM]]}
|
||||
</Button>
|
||||
)}
|
||||
content={<Text variant="b3">Default notification settings for all encrypted direct message.</Text>}
|
||||
}
|
||||
content={
|
||||
<Text variant="b3">
|
||||
Default notification settings for all encrypted direct message.
|
||||
</Text>
|
||||
}
|
||||
/>
|
||||
<SettingTile
|
||||
title="Rooms messages"
|
||||
options={(
|
||||
<Button onClick={(evt) => onSelect(evt, ROOM)} iconSrc={ChevronBottomIC}>
|
||||
options={
|
||||
<Button
|
||||
onClick={(evt) => onSelect(evt, ROOM)}
|
||||
iconSrc={ChevronBottomIC}
|
||||
>
|
||||
{typeToLabel[rulesToType[ROOM]]}
|
||||
</Button>
|
||||
)}
|
||||
content={<Text variant="b3">Default notification settings for all room message.</Text>}
|
||||
}
|
||||
content={
|
||||
<Text variant="b3">
|
||||
Default notification settings for all room message.
|
||||
</Text>
|
||||
}
|
||||
/>
|
||||
<SettingTile
|
||||
title="Encrypted rooms messages"
|
||||
options={(
|
||||
<Button onClick={(evt) => onSelect(evt, ENC_ROOM)} iconSrc={ChevronBottomIC}>
|
||||
options={
|
||||
<Button
|
||||
onClick={(evt) => onSelect(evt, ENC_ROOM)}
|
||||
iconSrc={ChevronBottomIC}
|
||||
>
|
||||
{typeToLabel[rulesToType[ENC_ROOM]]}
|
||||
</Button>
|
||||
)}
|
||||
content={<Text variant="b3">Default notification settings for all encrypted room message.</Text>}
|
||||
}
|
||||
content={
|
||||
<Text variant="b3">
|
||||
Default notification settings for all encrypted room message.
|
||||
</Text>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -1,31 +1,31 @@
|
|||
import React from 'react';
|
||||
import './IgnoreUserList.scss';
|
||||
import React from "react";
|
||||
import "./IgnoreUserList.scss";
|
||||
|
||||
import initMatrix from '../../../client/initMatrix';
|
||||
import * as roomActions from '../../../client/action/room';
|
||||
import initMatrix from "../../../client/initMatrix";
|
||||
import * as roomActions from "../../../client/action/room";
|
||||
|
||||
import Text from '../../atoms/text/Text';
|
||||
import Chip from '../../atoms/chip/Chip';
|
||||
import Input from '../../atoms/input/Input';
|
||||
import Button from '../../atoms/button/Button';
|
||||
import { MenuHeader } from '../../atoms/context-menu/ContextMenu';
|
||||
import SettingTile from '../setting-tile/SettingTile';
|
||||
import Text from "../../atoms/text/Text";
|
||||
import Chip from "../../atoms/chip/Chip";
|
||||
import Input from "../../atoms/input/Input";
|
||||
import Button from "../../atoms/button/Button";
|
||||
import { MenuHeader } from "../../atoms/context-menu/ContextMenu";
|
||||
import SettingTile from "../setting-tile/SettingTile";
|
||||
|
||||
import CrossIC from '../../../../public/res/ic/outlined/cross.svg';
|
||||
import CrossIC from "../../../../public/res/ic/outlined/cross.svg";
|
||||
|
||||
import { useAccountData } from '../../hooks/useAccountData';
|
||||
import { useAccountData } from "../../hooks/useAccountData";
|
||||
|
||||
function IgnoreUserList() {
|
||||
useAccountData('m.ignored_user_list');
|
||||
useAccountData("m.ignored_user_list");
|
||||
const ignoredUsers = initMatrix.matrixClient.getIgnoredUsers();
|
||||
|
||||
const handleSubmit = (evt) => {
|
||||
evt.preventDefault();
|
||||
const { ignoreInput } = evt.target.elements;
|
||||
const value = ignoreInput.value.trim();
|
||||
const userIds = value.split(' ').filter((v) => v.match(/^@\S+:\S+$/));
|
||||
const userIds = value.split(" ").filter((v) => v.match(/^@\S+:\S+$/));
|
||||
if (userIds.length === 0) return;
|
||||
ignoreInput.value = '';
|
||||
ignoreInput.value = "";
|
||||
roomActions.ignore(userIds);
|
||||
};
|
||||
|
||||
|
@ -34,12 +34,17 @@ function IgnoreUserList() {
|
|||
<MenuHeader>Ignored users</MenuHeader>
|
||||
<SettingTile
|
||||
title="Ignore user"
|
||||
content={(
|
||||
content={
|
||||
<div className="ignore-user-list__users">
|
||||
<Text variant="b3">Ignore userId if you do not want to receive their messages or invites.</Text>
|
||||
<Text variant="b3">
|
||||
Ignore userId if you do not want to receive their messages or
|
||||
invites.
|
||||
</Text>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<Input name="ignoreInput" required />
|
||||
<Button variant="primary" type="submit">Ignore</Button>
|
||||
<Button variant="primary" type="submit">
|
||||
Ignore
|
||||
</Button>
|
||||
</form>
|
||||
{ignoredUsers.length > 0 && (
|
||||
<div>
|
||||
|
@ -55,7 +60,7 @@ function IgnoreUserList() {
|
|||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -14,4 +14,4 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,35 +1,38 @@
|
|||
import React from 'react';
|
||||
import './KeywordNotification.scss';
|
||||
import React from "react";
|
||||
import "./KeywordNotification.scss";
|
||||
|
||||
import initMatrix from '../../../client/initMatrix';
|
||||
import { openReusableContextMenu } from '../../../client/action/navigation';
|
||||
import { getEventCords } from '../../../util/common';
|
||||
import initMatrix from "../../../client/initMatrix";
|
||||
import { openReusableContextMenu } from "../../../client/action/navigation";
|
||||
import { getEventCords } from "../../../util/common";
|
||||
|
||||
import Text from '../../atoms/text/Text';
|
||||
import Chip from '../../atoms/chip/Chip';
|
||||
import Input from '../../atoms/input/Input';
|
||||
import Button from '../../atoms/button/Button';
|
||||
import { MenuHeader } from '../../atoms/context-menu/ContextMenu';
|
||||
import SettingTile from '../setting-tile/SettingTile';
|
||||
import Text from "../../atoms/text/Text";
|
||||
import Chip from "../../atoms/chip/Chip";
|
||||
import Input from "../../atoms/input/Input";
|
||||
import Button from "../../atoms/button/Button";
|
||||
import { MenuHeader } from "../../atoms/context-menu/ContextMenu";
|
||||
import SettingTile from "../setting-tile/SettingTile";
|
||||
|
||||
import NotificationSelector from './NotificationSelector';
|
||||
import NotificationSelector from "./NotificationSelector";
|
||||
|
||||
import ChevronBottomIC from '../../../../public/res/ic/outlined/chevron-bottom.svg';
|
||||
import CrossIC from '../../../../public/res/ic/outlined/cross.svg';
|
||||
import ChevronBottomIC from "../../../../public/res/ic/outlined/chevron-bottom.svg";
|
||||
import CrossIC from "../../../../public/res/ic/outlined/cross.svg";
|
||||
|
||||
import { useAccountData } from '../../hooks/useAccountData';
|
||||
import { useAccountData } from "../../hooks/useAccountData";
|
||||
import {
|
||||
notifType, typeToLabel, getActionType, getTypeActions,
|
||||
} from './GlobalNotification';
|
||||
notifType,
|
||||
typeToLabel,
|
||||
getActionType,
|
||||
getTypeActions,
|
||||
} from "./GlobalNotification";
|
||||
|
||||
const DISPLAY_NAME = '.m.rule.contains_display_name';
|
||||
const ROOM_PING = '.m.rule.roomnotif';
|
||||
const USERNAME = '.m.rule.contains_user_name';
|
||||
const KEYWORD = 'keyword';
|
||||
const DISPLAY_NAME = ".m.rule.contains_display_name";
|
||||
const ROOM_PING = ".m.rule.roomnotif";
|
||||
const USERNAME = ".m.rule.contains_user_name";
|
||||
const KEYWORD = "keyword";
|
||||
|
||||
function useKeywordNotif() {
|
||||
const mx = initMatrix.matrixClient;
|
||||
const pushRules = useAccountData('m.push_rules')?.getContent();
|
||||
const pushRules = useAccountData("m.push_rules")?.getContent();
|
||||
const override = pushRules?.global?.override ?? [];
|
||||
const content = pushRules?.global?.content ?? [];
|
||||
|
||||
|
@ -60,12 +63,12 @@ function useKeywordNotif() {
|
|||
or.push(orRule);
|
||||
}
|
||||
if (rule === DISPLAY_NAME) {
|
||||
orRule.conditions = [{ kind: 'contains_display_name' }];
|
||||
orRule.conditions = [{ kind: "contains_display_name" }];
|
||||
orRule.actions = getTypeActions(type, true);
|
||||
} else {
|
||||
orRule.conditions = [
|
||||
{ kind: 'event_match', key: 'content.body', pattern: '@room' },
|
||||
{ kind: 'sender_notification_permission', key: 'room' },
|
||||
{ kind: "event_match", key: "content.body", pattern: "@room" },
|
||||
{ kind: "sender_notification_permission", key: "room" },
|
||||
];
|
||||
orRule.actions = getTypeActions(type, true);
|
||||
}
|
||||
|
@ -92,7 +95,7 @@ function useKeywordNotif() {
|
|||
});
|
||||
}
|
||||
|
||||
mx.setAccountData('m.push_rules', evtContent);
|
||||
mx.setAccountData("m.push_rules", evtContent);
|
||||
};
|
||||
|
||||
const addKeyword = (keyword) => {
|
||||
|
@ -104,11 +107,13 @@ function useKeywordNotif() {
|
|||
default: false,
|
||||
actions: getTypeActions(rulesToType[KEYWORD] ?? notifType.NOISY, true),
|
||||
});
|
||||
mx.setAccountData('m.push_rules', pushRules);
|
||||
mx.setAccountData("m.push_rules", pushRules);
|
||||
};
|
||||
const removeKeyword = (rule) => {
|
||||
pushRules.global.content = content.filter((r) => r.rule_id !== rule.rule_id);
|
||||
mx.setAccountData('m.push_rules', pushRules);
|
||||
pushRules.global.content = content.filter(
|
||||
(r) => r.rule_id !== rule.rule_id
|
||||
);
|
||||
mx.setAccountData("m.push_rules", pushRules);
|
||||
};
|
||||
|
||||
const dsRule = override.find((rule) => rule.rule_id === DISPLAY_NAME);
|
||||
|
@ -131,20 +136,16 @@ function useKeywordNotif() {
|
|||
}
|
||||
|
||||
function GlobalNotification() {
|
||||
const {
|
||||
rulesToType,
|
||||
pushRules,
|
||||
setRule,
|
||||
addKeyword,
|
||||
removeKeyword,
|
||||
} = useKeywordNotif();
|
||||
const { rulesToType, pushRules, setRule, addKeyword, removeKeyword } =
|
||||
useKeywordNotif();
|
||||
|
||||
const keywordRules = pushRules?.global?.content.filter((r) => r.rule_id !== USERNAME) ?? [];
|
||||
const keywordRules =
|
||||
pushRules?.global?.content.filter((r) => r.rule_id !== USERNAME) ?? [];
|
||||
|
||||
const onSelect = (evt, rule) => {
|
||||
openReusableContextMenu(
|
||||
'bottom',
|
||||
getEventCords(evt, '.btn-surface'),
|
||||
"bottom",
|
||||
getEventCords(evt, ".btn-surface"),
|
||||
(requestClose) => (
|
||||
<NotificationSelector
|
||||
value={rulesToType[rule]}
|
||||
|
@ -153,7 +154,7 @@ function GlobalNotification() {
|
|||
requestClose();
|
||||
}}
|
||||
/>
|
||||
),
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -161,9 +162,9 @@ function GlobalNotification() {
|
|||
evt.preventDefault();
|
||||
const { keywordInput } = evt.target.elements;
|
||||
const value = keywordInput.value.trim();
|
||||
if (value === '') return;
|
||||
if (value === "") return;
|
||||
addKeyword(value);
|
||||
keywordInput.value = '';
|
||||
keywordInput.value = "";
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -171,50 +172,84 @@ function GlobalNotification() {
|
|||
<MenuHeader>Mentions & keywords</MenuHeader>
|
||||
<SettingTile
|
||||
title="Message containing my display name"
|
||||
options={(
|
||||
<Button onClick={(evt) => onSelect(evt, DISPLAY_NAME)} iconSrc={ChevronBottomIC}>
|
||||
{ typeToLabel[rulesToType[DISPLAY_NAME]] }
|
||||
options={
|
||||
<Button
|
||||
onClick={(evt) => onSelect(evt, DISPLAY_NAME)}
|
||||
iconSrc={ChevronBottomIC}
|
||||
>
|
||||
{typeToLabel[rulesToType[DISPLAY_NAME]]}
|
||||
</Button>
|
||||
)}
|
||||
content={<Text variant="b3">Default notification settings for all message containing your display name.</Text>}
|
||||
}
|
||||
content={
|
||||
<Text variant="b3">
|
||||
Default notification settings for all message containing your
|
||||
display name.
|
||||
</Text>
|
||||
}
|
||||
/>
|
||||
<SettingTile
|
||||
title="Message containing my username"
|
||||
options={(
|
||||
<Button onClick={(evt) => onSelect(evt, USERNAME)} iconSrc={ChevronBottomIC}>
|
||||
{ typeToLabel[rulesToType[USERNAME]] }
|
||||
options={
|
||||
<Button
|
||||
onClick={(evt) => onSelect(evt, USERNAME)}
|
||||
iconSrc={ChevronBottomIC}
|
||||
>
|
||||
{typeToLabel[rulesToType[USERNAME]]}
|
||||
</Button>
|
||||
)}
|
||||
content={<Text variant="b3">Default notification settings for all message containing your username.</Text>}
|
||||
}
|
||||
content={
|
||||
<Text variant="b3">
|
||||
Default notification settings for all message containing your
|
||||
username.
|
||||
</Text>
|
||||
}
|
||||
/>
|
||||
<SettingTile
|
||||
title="Message containing @room"
|
||||
options={(
|
||||
<Button onClick={(evt) => onSelect(evt, ROOM_PING)} iconSrc={ChevronBottomIC}>
|
||||
options={
|
||||
<Button
|
||||
onClick={(evt) => onSelect(evt, ROOM_PING)}
|
||||
iconSrc={ChevronBottomIC}
|
||||
>
|
||||
{typeToLabel[rulesToType[ROOM_PING]]}
|
||||
</Button>
|
||||
)}
|
||||
content={<Text variant="b3">Default notification settings for all messages containing @room.</Text>}
|
||||
}
|
||||
content={
|
||||
<Text variant="b3">
|
||||
Default notification settings for all messages containing @room.
|
||||
</Text>
|
||||
}
|
||||
/>
|
||||
{ rulesToType[KEYWORD] && (
|
||||
{rulesToType[KEYWORD] && (
|
||||
<SettingTile
|
||||
title="Message containing keywords"
|
||||
options={(
|
||||
<Button onClick={(evt) => onSelect(evt, KEYWORD)} iconSrc={ChevronBottomIC}>
|
||||
options={
|
||||
<Button
|
||||
onClick={(evt) => onSelect(evt, KEYWORD)}
|
||||
iconSrc={ChevronBottomIC}
|
||||
>
|
||||
{typeToLabel[rulesToType[KEYWORD]]}
|
||||
</Button>
|
||||
)}
|
||||
content={<Text variant="b3">Default notification settings for all message containing keywords.</Text>}
|
||||
}
|
||||
content={
|
||||
<Text variant="b3">
|
||||
Default notification settings for all message containing keywords.
|
||||
</Text>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<SettingTile
|
||||
title="Keywords"
|
||||
content={(
|
||||
content={
|
||||
<div className="keyword-notification__keyword">
|
||||
<Text variant="b3">Get notification when a message contains keyword.</Text>
|
||||
<Text variant="b3">
|
||||
Get notification when a message contains keyword.
|
||||
</Text>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<Input name="keywordInput" required />
|
||||
<Button variant="primary" type="submit">Add</Button>
|
||||
<Button variant="primary" type="submit">
|
||||
Add
|
||||
</Button>
|
||||
</form>
|
||||
{keywordRules.length > 0 && (
|
||||
<div>
|
||||
|
@ -230,7 +265,7 @@ function GlobalNotification() {
|
|||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -14,4 +14,4 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,25 +1,41 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
import { MenuHeader, MenuItem } from '../../atoms/context-menu/ContextMenu';
|
||||
import { MenuHeader, MenuItem } from "../../atoms/context-menu/ContextMenu";
|
||||
|
||||
import CheckIC from '../../../../public/res/ic/outlined/check.svg';
|
||||
import CheckIC from "../../../../public/res/ic/outlined/check.svg";
|
||||
|
||||
function NotificationSelector({
|
||||
value, onSelect,
|
||||
}) {
|
||||
function NotificationSelector({ value, onSelect }) {
|
||||
return (
|
||||
<div>
|
||||
<MenuHeader>Notification</MenuHeader>
|
||||
<MenuItem iconSrc={value === 'off' ? CheckIC : null} variant={value === 'off' ? 'positive' : 'surface'} onClick={() => onSelect('off')}>Off</MenuItem>
|
||||
<MenuItem iconSrc={value === 'on' ? CheckIC : null} variant={value === 'on' ? 'positive' : 'surface'} onClick={() => onSelect('on')}>On</MenuItem>
|
||||
<MenuItem iconSrc={value === 'noisy' ? CheckIC : null} variant={value === 'noisy' ? 'positive' : 'surface'} onClick={() => onSelect('noisy')}>Noisy</MenuItem>
|
||||
<MenuItem
|
||||
iconSrc={value === "off" ? CheckIC : null}
|
||||
variant={value === "off" ? "positive" : "surface"}
|
||||
onClick={() => onSelect("off")}
|
||||
>
|
||||
Off
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
iconSrc={value === "on" ? CheckIC : null}
|
||||
variant={value === "on" ? "positive" : "surface"}
|
||||
onClick={() => onSelect("on")}
|
||||
>
|
||||
On
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
iconSrc={value === "noisy" ? CheckIC : null}
|
||||
variant={value === "noisy" ? "positive" : "surface"}
|
||||
onClick={() => onSelect("noisy")}
|
||||
>
|
||||
Noisy
|
||||
</MenuItem>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
NotificationSelector.propTypes = {
|
||||
value: PropTypes.oneOf(['off', 'on', 'noisy']).isRequired,
|
||||
value: PropTypes.oneOf(["off", "on", "noisy"]).isRequired,
|
||||
onSelect: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
|
|
|
@ -1,18 +1,16 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import './ImageLightbox.scss';
|
||||
import FileSaver from 'file-saver';
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import "./ImageLightbox.scss";
|
||||
import FileSaver from "file-saver";
|
||||
|
||||
import Text from '../../atoms/text/Text';
|
||||
import RawModal from '../../atoms/modal/RawModal';
|
||||
import IconButton from '../../atoms/button/IconButton';
|
||||
import Text from "../../atoms/text/Text";
|
||||
import RawModal from "../../atoms/modal/RawModal";
|
||||
import IconButton from "../../atoms/button/IconButton";
|
||||
|
||||
import DownloadSVG from '../../../../public/res/ic/outlined/download.svg';
|
||||
import ExternalSVG from '../../../../public/res/ic/outlined/external.svg';
|
||||
import DownloadSVG from "../../../../public/res/ic/outlined/download.svg";
|
||||
import ExternalSVG from "../../../../public/res/ic/outlined/external.svg";
|
||||
|
||||
function ImageLightbox({
|
||||
url, alt, isOpen, onRequestClose,
|
||||
}) {
|
||||
function ImageLightbox({ url, alt, isOpen, onRequestClose }) {
|
||||
const handleDownload = () => {
|
||||
FileSaver.saveAs(url, alt);
|
||||
};
|
||||
|
@ -26,8 +24,14 @@ function ImageLightbox({
|
|||
size="large"
|
||||
>
|
||||
<div className="image-lightbox__header">
|
||||
<Text variant="b2" weight="medium">{alt}</Text>
|
||||
<IconButton onClick={() => window.open(url)} size="small" src={ExternalSVG} />
|
||||
<Text variant="b2" weight="medium">
|
||||
{alt}
|
||||
</Text>
|
||||
<IconButton
|
||||
onClick={() => window.open(url)}
|
||||
size="small"
|
||||
src={ExternalSVG}
|
||||
/>
|
||||
<IconButton onClick={handleDownload} size="small" src={DownloadSVG} />
|
||||
</div>
|
||||
<div className="image-lightbox__content">
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
@use '../../partials/flex';
|
||||
@use '../../partials/text';
|
||||
@use "../../partials/flex";
|
||||
@use "../../partials/text";
|
||||
|
||||
.image-lightbox__modal {
|
||||
box-shadow: none;
|
||||
|
@ -21,7 +21,6 @@
|
|||
background-color: var(--bg-overlay-low);
|
||||
}
|
||||
|
||||
|
||||
.image-lightbox__header > *,
|
||||
.image-lightbox__content > * {
|
||||
pointer-events: all;
|
||||
|
@ -47,4 +46,4 @@
|
|||
max-height: 100%;
|
||||
border-radius: var(--bo-radius);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,75 +1,80 @@
|
|||
import React, {
|
||||
useState, useMemo, useReducer, useEffect,
|
||||
} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import './ImagePack.scss';
|
||||
import React, { useState, useMemo, useReducer, useEffect } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import "./ImagePack.scss";
|
||||
|
||||
import initMatrix from '../../../client/initMatrix';
|
||||
import { openReusableDialog } from '../../../client/action/navigation';
|
||||
import { suffixRename } from '../../../util/common';
|
||||
import initMatrix from "../../../client/initMatrix";
|
||||
import { openReusableDialog } from "../../../client/action/navigation";
|
||||
import { suffixRename } from "../../../util/common";
|
||||
|
||||
import Button from '../../atoms/button/Button';
|
||||
import Text from '../../atoms/text/Text';
|
||||
import Input from '../../atoms/input/Input';
|
||||
import Checkbox from '../../atoms/button/Checkbox';
|
||||
import { MenuHeader } from '../../atoms/context-menu/ContextMenu';
|
||||
import Button from "../../atoms/button/Button";
|
||||
import Text from "../../atoms/text/Text";
|
||||
import Input from "../../atoms/input/Input";
|
||||
import Checkbox from "../../atoms/button/Checkbox";
|
||||
import { MenuHeader } from "../../atoms/context-menu/ContextMenu";
|
||||
|
||||
import { ImagePack as ImagePackBuilder } from '../../organisms/emoji-board/custom-emoji';
|
||||
import { confirmDialog } from '../confirm-dialog/ConfirmDialog';
|
||||
import ImagePackProfile from './ImagePackProfile';
|
||||
import ImagePackItem from './ImagePackItem';
|
||||
import ImagePackUpload from './ImagePackUpload';
|
||||
import { ImagePack as ImagePackBuilder } from "../../organisms/emoji-board/custom-emoji";
|
||||
import { confirmDialog } from "../confirm-dialog/ConfirmDialog";
|
||||
import ImagePackProfile from "./ImagePackProfile";
|
||||
import ImagePackItem from "./ImagePackItem";
|
||||
import ImagePackUpload from "./ImagePackUpload";
|
||||
|
||||
const renameImagePackItem = (shortcode) => new Promise((resolve) => {
|
||||
let isCompleted = false;
|
||||
const renameImagePackItem = (shortcode) =>
|
||||
new Promise((resolve) => {
|
||||
let isCompleted = false;
|
||||
|
||||
openReusableDialog(
|
||||
<Text variant="s1" weight="medium">Rename</Text>,
|
||||
(requestClose) => (
|
||||
<div style={{ padding: 'var(--sp-normal)' }}>
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
const sc = e.target.shortcode.value;
|
||||
if (sc.trim() === '') return;
|
||||
isCompleted = true;
|
||||
resolve(sc.trim());
|
||||
requestClose();
|
||||
}}
|
||||
>
|
||||
<Input
|
||||
value={shortcode}
|
||||
name="shortcode"
|
||||
label="Shortcode"
|
||||
autoFocus
|
||||
required
|
||||
/>
|
||||
<div style={{ height: 'var(--sp-normal)' }} />
|
||||
<Button variant="primary" type="submit">Rename</Button>
|
||||
</form>
|
||||
</div>
|
||||
),
|
||||
() => {
|
||||
if (!isCompleted) resolve(null);
|
||||
},
|
||||
);
|
||||
});
|
||||
openReusableDialog(
|
||||
<Text variant="s1" weight="medium">
|
||||
Rename
|
||||
</Text>,
|
||||
(requestClose) => (
|
||||
<div style={{ padding: "var(--sp-normal)" }}>
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
const sc = e.target.shortcode.value;
|
||||
if (sc.trim() === "") return;
|
||||
isCompleted = true;
|
||||
resolve(sc.trim());
|
||||
requestClose();
|
||||
}}
|
||||
>
|
||||
<Input
|
||||
value={shortcode}
|
||||
name="shortcode"
|
||||
label="Shortcode"
|
||||
autoFocus
|
||||
required
|
||||
/>
|
||||
<div style={{ height: "var(--sp-normal)" }} />
|
||||
<Button variant="primary" type="submit">
|
||||
Rename
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
),
|
||||
() => {
|
||||
if (!isCompleted) resolve(null);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
function getUsage(usage) {
|
||||
if (usage.includes('emoticon') && usage.includes('sticker')) return 'both';
|
||||
if (usage.includes('emoticon')) return 'emoticon';
|
||||
if (usage.includes('sticker')) return 'sticker';
|
||||
if (usage.includes("emoticon") && usage.includes("sticker")) return "both";
|
||||
if (usage.includes("emoticon")) return "emoticon";
|
||||
if (usage.includes("sticker")) return "sticker";
|
||||
|
||||
return 'both';
|
||||
return "both";
|
||||
}
|
||||
|
||||
function isGlobalPack(roomId, stateKey) {
|
||||
const mx = initMatrix.matrixClient;
|
||||
const globalContent = mx.getAccountData('im.ponies.emote_rooms')?.getContent();
|
||||
if (typeof globalContent !== 'object') return false;
|
||||
const globalContent = mx
|
||||
.getAccountData("im.ponies.emote_rooms")
|
||||
?.getContent();
|
||||
if (typeof globalContent !== "object") return false;
|
||||
|
||||
const { rooms } = globalContent;
|
||||
if (typeof rooms !== 'object') return false;
|
||||
if (typeof rooms !== "object") return false;
|
||||
|
||||
return rooms[roomId]?.[stateKey] !== undefined;
|
||||
}
|
||||
|
@ -78,13 +83,17 @@ function useRoomImagePack(roomId, stateKey) {
|
|||
const mx = initMatrix.matrixClient;
|
||||
const room = mx.getRoom(roomId);
|
||||
|
||||
const packEvent = room.currentState.getStateEvents('im.ponies.room_emotes', stateKey);
|
||||
const pack = useMemo(() => (
|
||||
ImagePackBuilder.parsePack(packEvent.getId(), packEvent.getContent())
|
||||
), [room, stateKey]);
|
||||
const packEvent = room.currentState.getStateEvents(
|
||||
"im.ponies.room_emotes",
|
||||
stateKey
|
||||
);
|
||||
const pack = useMemo(
|
||||
() => ImagePackBuilder.parsePack(packEvent.getId(), packEvent.getContent()),
|
||||
[room, stateKey]
|
||||
);
|
||||
|
||||
const sendPackContent = (content) => {
|
||||
mx.sendStateEvent(roomId, 'im.ponies.room_emotes', content, stateKey);
|
||||
mx.sendStateEvent(roomId, "im.ponies.room_emotes", content, stateKey);
|
||||
};
|
||||
|
||||
return {
|
||||
|
@ -95,16 +104,21 @@ function useRoomImagePack(roomId, stateKey) {
|
|||
|
||||
function useUserImagePack() {
|
||||
const mx = initMatrix.matrixClient;
|
||||
const packEvent = mx.getAccountData('im.ponies.user_emotes');
|
||||
const pack = useMemo(() => (
|
||||
ImagePackBuilder.parsePack(mx.getUserId(), packEvent?.getContent() ?? {
|
||||
pack: { display_name: 'Personal' },
|
||||
images: {},
|
||||
})
|
||||
), []);
|
||||
const packEvent = mx.getAccountData("im.ponies.user_emotes");
|
||||
const pack = useMemo(
|
||||
() =>
|
||||
ImagePackBuilder.parsePack(
|
||||
mx.getUserId(),
|
||||
packEvent?.getContent() ?? {
|
||||
pack: { display_name: "Personal" },
|
||||
images: {},
|
||||
}
|
||||
),
|
||||
[]
|
||||
);
|
||||
|
||||
const sendPackContent = (content) => {
|
||||
mx.setAccountData('im.ponies.user_emotes', content);
|
||||
mx.setAccountData("im.ponies.user_emotes", content);
|
||||
};
|
||||
|
||||
return {
|
||||
|
@ -117,12 +131,11 @@ function useImagePackHandles(pack, sendPackContent) {
|
|||
const [, forceUpdate] = useReducer((count) => count + 1, 0);
|
||||
|
||||
const getNewKey = (key) => {
|
||||
if (typeof key !== 'string') return undefined;
|
||||
let newKey = key?.replace(/\s/g, '_');
|
||||
if (typeof key !== "string") return undefined;
|
||||
let newKey = key?.replace(/\s/g, "_");
|
||||
if (pack.getImages().get(newKey)) {
|
||||
newKey = suffixRename(
|
||||
newKey,
|
||||
(suffixedKey) => pack.getImages().get(suffixedKey),
|
||||
newKey = suffixRename(newKey, (suffixedKey) =>
|
||||
pack.getImages().get(suffixedKey)
|
||||
);
|
||||
}
|
||||
return newKey;
|
||||
|
@ -141,10 +154,12 @@ function useImagePackHandles(pack, sendPackContent) {
|
|||
};
|
||||
const handleUsageChange = (newUsage) => {
|
||||
const usage = [];
|
||||
if (newUsage === 'emoticon' || newUsage === 'both') usage.push('emoticon');
|
||||
if (newUsage === 'sticker' || newUsage === 'both') usage.push('sticker');
|
||||
if (newUsage === "emoticon" || newUsage === "both") usage.push("emoticon");
|
||||
if (newUsage === "sticker" || newUsage === "both") usage.push("sticker");
|
||||
pack.setUsage(usage);
|
||||
pack.getImages().forEach((img) => pack.setImageUsage(img.shortcode, undefined));
|
||||
pack
|
||||
.getImages()
|
||||
.forEach((img) => pack.setImageUsage(img.shortcode, undefined));
|
||||
|
||||
sendPackContent(pack.getContent());
|
||||
forceUpdate();
|
||||
|
@ -161,10 +176,10 @@ function useImagePackHandles(pack, sendPackContent) {
|
|||
};
|
||||
const handleDeleteItem = async (key) => {
|
||||
const isConfirmed = await confirmDialog(
|
||||
'Delete',
|
||||
"Delete",
|
||||
`Are you sure that you want to delete "${key}"?`,
|
||||
'Delete',
|
||||
'danger',
|
||||
"Delete",
|
||||
"danger"
|
||||
);
|
||||
if (!isConfirmed) return;
|
||||
pack.removeImage(key);
|
||||
|
@ -174,8 +189,8 @@ function useImagePackHandles(pack, sendPackContent) {
|
|||
};
|
||||
const handleUsageItem = (key, newUsage) => {
|
||||
const usage = [];
|
||||
if (newUsage === 'emoticon' || newUsage === 'both') usage.push('emoticon');
|
||||
if (newUsage === 'sticker' || newUsage === 'both') usage.push('sticker');
|
||||
if (newUsage === "emoticon" || newUsage === "both") usage.push("emoticon");
|
||||
if (newUsage === "sticker" || newUsage === "both") usage.push("sticker");
|
||||
pack.setImageUsage(key, usage);
|
||||
|
||||
sendPackContent(pack.getContent());
|
||||
|
@ -205,21 +220,23 @@ function useImagePackHandles(pack, sendPackContent) {
|
|||
}
|
||||
|
||||
function addGlobalImagePack(mx, roomId, stateKey) {
|
||||
const content = mx.getAccountData('im.ponies.emote_rooms')?.getContent() ?? {};
|
||||
const content =
|
||||
mx.getAccountData("im.ponies.emote_rooms")?.getContent() ?? {};
|
||||
if (!content.rooms) content.rooms = {};
|
||||
if (!content.rooms[roomId]) content.rooms[roomId] = {};
|
||||
content.rooms[roomId][stateKey] = {};
|
||||
return mx.setAccountData('im.ponies.emote_rooms', content);
|
||||
return mx.setAccountData("im.ponies.emote_rooms", content);
|
||||
}
|
||||
function removeGlobalImagePack(mx, roomId, stateKey) {
|
||||
const content = mx.getAccountData('im.ponies.emote_rooms')?.getContent() ?? {};
|
||||
const content =
|
||||
mx.getAccountData("im.ponies.emote_rooms")?.getContent() ?? {};
|
||||
if (!content.rooms) return Promise.resolve();
|
||||
if (!content.rooms[roomId]) return Promise.resolve();
|
||||
delete content.rooms[roomId][stateKey];
|
||||
if (Object.keys(content.rooms[roomId]).length === 0) {
|
||||
delete content.rooms[roomId];
|
||||
}
|
||||
return mx.setAccountData('im.ponies.emote_rooms', content);
|
||||
return mx.setAccountData("im.ponies.emote_rooms", content);
|
||||
}
|
||||
|
||||
function ImagePack({ roomId, stateKey, handlePackDelete }) {
|
||||
|
@ -247,14 +264,17 @@ function ImagePack({ roomId, stateKey, handlePackDelete }) {
|
|||
};
|
||||
|
||||
const myPowerlevel = room.getMember(mx.getUserId())?.powerLevel || 0;
|
||||
const canChange = room.currentState.hasSufficientPowerLevelFor('state_default', myPowerlevel);
|
||||
const canChange = room.currentState.hasSufficientPowerLevelFor(
|
||||
"state_default",
|
||||
myPowerlevel
|
||||
);
|
||||
|
||||
const handleDeletePack = async () => {
|
||||
const isConfirmed = await confirmDialog(
|
||||
'Delete Pack',
|
||||
"Delete Pack",
|
||||
`Are you sure that you want to delete "${pack.displayName}"?`,
|
||||
'Delete',
|
||||
'danger',
|
||||
"Delete",
|
||||
"danger"
|
||||
);
|
||||
if (!isConfirmed) return;
|
||||
handlePackDelete(stateKey);
|
||||
|
@ -265,18 +285,20 @@ function ImagePack({ roomId, stateKey, handlePackDelete }) {
|
|||
return (
|
||||
<div className="image-pack">
|
||||
<ImagePackProfile
|
||||
avatarUrl={pack.avatarUrl ? mx.mxcUrlToHttp(pack.avatarUrl, 42, 42, 'crop') : null}
|
||||
displayName={pack.displayName ?? 'Unknown'}
|
||||
avatarUrl={
|
||||
pack.avatarUrl
|
||||
? mx.mxcUrlToHttp(pack.avatarUrl, 42, 42, "crop")
|
||||
: null
|
||||
}
|
||||
displayName={pack.displayName ?? "Unknown"}
|
||||
attribution={pack.attribution}
|
||||
usage={getUsage(pack.usage)}
|
||||
onUsageChange={canChange ? handleUsageChange : null}
|
||||
onAvatarChange={canChange ? handleAvatarChange : null}
|
||||
onEditProfile={canChange ? handleEditProfile : null}
|
||||
/>
|
||||
{ canChange && (
|
||||
<ImagePackUpload onUpload={handleAddItem} />
|
||||
)}
|
||||
{ images.length === 0 ? null : (
|
||||
{canChange && <ImagePackUpload onUpload={handleAddItem} />}
|
||||
{images.length === 0 ? null : (
|
||||
<div>
|
||||
<div className="image-pack__header">
|
||||
<Text variant="b3">Image</Text>
|
||||
|
@ -300,21 +322,27 @@ function ImagePack({ roomId, stateKey, handlePackDelete }) {
|
|||
<div className="image-pack__footer">
|
||||
{pack.images.size > 2 && (
|
||||
<Button onClick={() => setViewMore(!viewMore)}>
|
||||
{
|
||||
viewMore
|
||||
? 'View less'
|
||||
: `View ${pack.images.size - 2} more`
|
||||
}
|
||||
{viewMore ? "View less" : `View ${pack.images.size - 2} more`}
|
||||
</Button>
|
||||
)}
|
||||
{handlePackDelete && (
|
||||
<Button variant="danger" onClick={handleDeletePack}>
|
||||
Delete Pack
|
||||
</Button>
|
||||
)}
|
||||
{ handlePackDelete && <Button variant="danger" onClick={handleDeletePack}>Delete Pack</Button>}
|
||||
</div>
|
||||
)}
|
||||
<div className="image-pack__global">
|
||||
<Checkbox variant="positive" onToggle={handleGlobalChange} isActive={isGlobal} />
|
||||
<Checkbox
|
||||
variant="positive"
|
||||
onToggle={handleGlobalChange}
|
||||
isActive={isGlobal}
|
||||
/>
|
||||
<div>
|
||||
<Text variant="b2">Use globally</Text>
|
||||
<Text variant="b3">Add this pack to your account to use in all rooms.</Text>
|
||||
<Text variant="b3">
|
||||
Add this pack to your account to use in all rooms.
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -351,8 +379,12 @@ function ImagePackUser() {
|
|||
return (
|
||||
<div className="image-pack">
|
||||
<ImagePackProfile
|
||||
avatarUrl={pack.avatarUrl ? mx.mxcUrlToHttp(pack.avatarUrl, 42, 42, 'crop') : null}
|
||||
displayName={pack.displayName ?? 'Personal'}
|
||||
avatarUrl={
|
||||
pack.avatarUrl
|
||||
? mx.mxcUrlToHttp(pack.avatarUrl, 42, 42, "crop")
|
||||
: null
|
||||
}
|
||||
displayName={pack.displayName ?? "Personal"}
|
||||
attribution={pack.attribution}
|
||||
usage={getUsage(pack.usage)}
|
||||
onUsageChange={handleUsageChange}
|
||||
|
@ -360,7 +392,7 @@ function ImagePackUser() {
|
|||
onEditProfile={handleEditProfile}
|
||||
/>
|
||||
<ImagePackUpload onUpload={handleAddItem} />
|
||||
{ images.length === 0 ? null : (
|
||||
{images.length === 0 ? null : (
|
||||
<div>
|
||||
<div className="image-pack__header">
|
||||
<Text variant="b3">Image</Text>
|
||||
|
@ -380,14 +412,10 @@ function ImagePackUser() {
|
|||
))}
|
||||
</div>
|
||||
)}
|
||||
{(pack.images.size > 2) && (
|
||||
{pack.images.size > 2 && (
|
||||
<div className="image-pack__footer">
|
||||
<Button onClick={() => setViewMore(!viewMore)}>
|
||||
{
|
||||
viewMore
|
||||
? 'View less'
|
||||
: `View ${pack.images.size - 2} more`
|
||||
}
|
||||
{viewMore ? "View less" : `View ${pack.images.size - 2} more`}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
@ -400,11 +428,13 @@ function useGlobalImagePack() {
|
|||
const mx = initMatrix.matrixClient;
|
||||
|
||||
const roomIdToStateKeys = new Map();
|
||||
const globalContent = mx.getAccountData('im.ponies.emote_rooms')?.getContent() ?? { rooms: {} };
|
||||
const globalContent = mx
|
||||
.getAccountData("im.ponies.emote_rooms")
|
||||
?.getContent() ?? { rooms: {} };
|
||||
const { rooms } = globalContent;
|
||||
|
||||
Object.keys(rooms).forEach((roomId) => {
|
||||
if (typeof rooms[roomId] !== 'object') return;
|
||||
if (typeof rooms[roomId] !== "object") return;
|
||||
const room = mx.getRoom(roomId);
|
||||
const stateKeys = Object.keys(rooms[roomId]);
|
||||
if (!room || stateKeys.length === 0) return;
|
||||
|
@ -413,11 +443,11 @@ function useGlobalImagePack() {
|
|||
|
||||
useEffect(() => {
|
||||
const handleEvent = (event) => {
|
||||
if (event.getType() === 'im.ponies.emote_rooms') forceUpdate();
|
||||
if (event.getType() === "im.ponies.emote_rooms") forceUpdate();
|
||||
};
|
||||
mx.addListener('accountData', handleEvent);
|
||||
mx.addListener("accountData", handleEvent);
|
||||
return () => {
|
||||
mx.removeListener('accountData', handleEvent);
|
||||
mx.removeListener("accountData", handleEvent);
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
@ -436,29 +466,39 @@ function ImagePackGlobal() {
|
|||
<div className="image-pack-global">
|
||||
<MenuHeader>Global packs</MenuHeader>
|
||||
<div>
|
||||
{
|
||||
roomIdToStateKeys.size > 0
|
||||
? [...roomIdToStateKeys].map(([roomId, stateKeys]) => {
|
||||
const room = mx.getRoom(roomId);
|
||||
return (
|
||||
stateKeys.map((stateKey) => {
|
||||
const data = room.currentState.getStateEvents('im.ponies.room_emotes', stateKey);
|
||||
const pack = ImagePackBuilder.parsePack(data?.getId(), data?.getContent());
|
||||
if (!pack) return null;
|
||||
return (
|
||||
<div className="image-pack__global" key={pack.id}>
|
||||
<Checkbox variant="positive" onToggle={() => handleChange(roomId, stateKey)} isActive />
|
||||
<div>
|
||||
<Text variant="b2">{pack.displayName ?? 'Unknown'}</Text>
|
||||
<Text variant="b3">{room.name}</Text>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
{roomIdToStateKeys.size > 0 ? (
|
||||
[...roomIdToStateKeys].map(([roomId, stateKeys]) => {
|
||||
const room = mx.getRoom(roomId);
|
||||
return stateKeys.map((stateKey) => {
|
||||
const data = room.currentState.getStateEvents(
|
||||
"im.ponies.room_emotes",
|
||||
stateKey
|
||||
);
|
||||
})
|
||||
: <div className="image-pack-global__empty"><Text>No global packs</Text></div>
|
||||
}
|
||||
const pack = ImagePackBuilder.parsePack(
|
||||
data?.getId(),
|
||||
data?.getContent()
|
||||
);
|
||||
if (!pack) return null;
|
||||
return (
|
||||
<div className="image-pack__global" key={pack.id}>
|
||||
<Checkbox
|
||||
variant="positive"
|
||||
onToggle={() => handleChange(roomId, stateKey)}
|
||||
isActive
|
||||
/>
|
||||
<div>
|
||||
<Text variant="b2">{pack.displayName ?? "Unknown"}</Text>
|
||||
<Text variant="b3">{room.name}</Text>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
})
|
||||
) : (
|
||||
<div className="image-pack-global__empty">
|
||||
<Text>No global packs</Text>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
@use '../../partials/flex';
|
||||
@use "../../partials/flex";
|
||||
|
||||
.image-pack {
|
||||
&-item {
|
||||
|
|
|
@ -1,28 +1,33 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import './ImagePackItem.scss';
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import "./ImagePackItem.scss";
|
||||
|
||||
import { openReusableContextMenu } from '../../../client/action/navigation';
|
||||
import { getEventCords } from '../../../util/common';
|
||||
import { openReusableContextMenu } from "../../../client/action/navigation";
|
||||
import { getEventCords } from "../../../util/common";
|
||||
|
||||
import Avatar from '../../atoms/avatar/Avatar';
|
||||
import Text from '../../atoms/text/Text';
|
||||
import Button from '../../atoms/button/Button';
|
||||
import RawIcon from '../../atoms/system-icons/RawIcon';
|
||||
import IconButton from '../../atoms/button/IconButton';
|
||||
import ImagePackUsageSelector from './ImagePackUsageSelector';
|
||||
import Avatar from "../../atoms/avatar/Avatar";
|
||||
import Text from "../../atoms/text/Text";
|
||||
import Button from "../../atoms/button/Button";
|
||||
import RawIcon from "../../atoms/system-icons/RawIcon";
|
||||
import IconButton from "../../atoms/button/IconButton";
|
||||
import ImagePackUsageSelector from "./ImagePackUsageSelector";
|
||||
|
||||
import ChevronBottomIC from '../../../../public/res/ic/outlined/chevron-bottom.svg';
|
||||
import PencilIC from '../../../../public/res/ic/outlined/pencil.svg';
|
||||
import BinIC from '../../../../public/res/ic/outlined/bin.svg';
|
||||
import ChevronBottomIC from "../../../../public/res/ic/outlined/chevron-bottom.svg";
|
||||
import PencilIC from "../../../../public/res/ic/outlined/pencil.svg";
|
||||
import BinIC from "../../../../public/res/ic/outlined/bin.svg";
|
||||
|
||||
function ImagePackItem({
|
||||
url, shortcode, usage, onUsageChange, onDelete, onRename,
|
||||
url,
|
||||
shortcode,
|
||||
usage,
|
||||
onUsageChange,
|
||||
onDelete,
|
||||
onRename,
|
||||
}) {
|
||||
const handleUsageSelect = (event) => {
|
||||
openReusableContextMenu(
|
||||
'bottom',
|
||||
getEventCords(event, '.btn-surface'),
|
||||
"bottom",
|
||||
getEventCords(event, ".btn-surface"),
|
||||
(closeMenu) => (
|
||||
<ImagePackUsageSelector
|
||||
usage={usage}
|
||||
|
@ -31,27 +36,48 @@ function ImagePackItem({
|
|||
closeMenu();
|
||||
}}
|
||||
/>
|
||||
),
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="image-pack-item">
|
||||
<Avatar imageSrc={url} size="extra-small" text={shortcode} bgColor="black" />
|
||||
<Avatar
|
||||
imageSrc={url}
|
||||
size="extra-small"
|
||||
text={shortcode}
|
||||
bgColor="black"
|
||||
/>
|
||||
<div className="image-pack-item__content">
|
||||
<Text>{shortcode}</Text>
|
||||
</div>
|
||||
<div className="image-pack-item__usage">
|
||||
<div className="image-pack-item__btn">
|
||||
{onRename && <IconButton tooltip="Rename" size="extra-small" src={PencilIC} onClick={() => onRename(shortcode)} />}
|
||||
{onDelete && <IconButton tooltip="Delete" size="extra-small" src={BinIC} onClick={() => onDelete(shortcode)} />}
|
||||
{onRename && (
|
||||
<IconButton
|
||||
tooltip="Rename"
|
||||
size="extra-small"
|
||||
src={PencilIC}
|
||||
onClick={() => onRename(shortcode)}
|
||||
/>
|
||||
)}
|
||||
{onDelete && (
|
||||
<IconButton
|
||||
tooltip="Delete"
|
||||
size="extra-small"
|
||||
src={BinIC}
|
||||
onClick={() => onDelete(shortcode)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<Button onClick={onUsageChange ? handleUsageSelect : undefined}>
|
||||
{onUsageChange && <RawIcon src={ChevronBottomIC} size="extra-small" />}
|
||||
{onUsageChange && (
|
||||
<RawIcon src={ChevronBottomIC} size="extra-small" />
|
||||
)}
|
||||
<Text variant="b2">
|
||||
{usage === 'emoticon' && 'Emoji'}
|
||||
{usage === 'sticker' && 'Sticker'}
|
||||
{usage === 'both' && 'Both'}
|
||||
{usage === "emoticon" && "Emoji"}
|
||||
{usage === "sticker" && "Sticker"}
|
||||
{usage === "both" && "Both"}
|
||||
</Text>
|
||||
</Button>
|
||||
</div>
|
||||
|
@ -67,7 +93,7 @@ ImagePackItem.defaultProps = {
|
|||
ImagePackItem.propTypes = {
|
||||
url: PropTypes.string.isRequired,
|
||||
shortcode: PropTypes.string.isRequired,
|
||||
usage: PropTypes.oneOf(['emoticon', 'sticker', 'both']).isRequired,
|
||||
usage: PropTypes.oneOf(["emoticon", "sticker", "both"]).isRequired,
|
||||
onUsageChange: PropTypes.func,
|
||||
onDelete: PropTypes.func,
|
||||
onRename: PropTypes.func,
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
@use '../../partials/flex';
|
||||
@use '../../partials/dir';
|
||||
@use "../../partials/flex";
|
||||
@use "../../partials/dir";
|
||||
|
||||
.image-pack-item {
|
||||
margin: 0 var(--sp-normal);
|
||||
|
@ -40,4 +40,4 @@
|
|||
gap: var(--sp-ultra-tight);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,24 +1,29 @@
|
|||
import React, { useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import './ImagePackProfile.scss';
|
||||
import React, { useState } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import "./ImagePackProfile.scss";
|
||||
|
||||
import { openReusableContextMenu } from '../../../client/action/navigation';
|
||||
import { getEventCords } from '../../../util/common';
|
||||
import { openReusableContextMenu } from "../../../client/action/navigation";
|
||||
import { getEventCords } from "../../../util/common";
|
||||
|
||||
import Text from '../../atoms/text/Text';
|
||||
import Avatar from '../../atoms/avatar/Avatar';
|
||||
import Button from '../../atoms/button/Button';
|
||||
import IconButton from '../../atoms/button/IconButton';
|
||||
import Input from '../../atoms/input/Input';
|
||||
import ImageUpload from '../image-upload/ImageUpload';
|
||||
import ImagePackUsageSelector from './ImagePackUsageSelector';
|
||||
import Text from "../../atoms/text/Text";
|
||||
import Avatar from "../../atoms/avatar/Avatar";
|
||||
import Button from "../../atoms/button/Button";
|
||||
import IconButton from "../../atoms/button/IconButton";
|
||||
import Input from "../../atoms/input/Input";
|
||||
import ImageUpload from "../image-upload/ImageUpload";
|
||||
import ImagePackUsageSelector from "./ImagePackUsageSelector";
|
||||
|
||||
import ChevronBottomIC from '../../../../public/res/ic/outlined/chevron-bottom.svg';
|
||||
import PencilIC from '../../../../public/res/ic/outlined/pencil.svg';
|
||||
import ChevronBottomIC from "../../../../public/res/ic/outlined/chevron-bottom.svg";
|
||||
import PencilIC from "../../../../public/res/ic/outlined/pencil.svg";
|
||||
|
||||
function ImagePackProfile({
|
||||
avatarUrl, displayName, attribution, usage,
|
||||
onUsageChange, onAvatarChange, onEditProfile,
|
||||
avatarUrl,
|
||||
displayName,
|
||||
attribution,
|
||||
usage,
|
||||
onUsageChange,
|
||||
onAvatarChange,
|
||||
onEditProfile,
|
||||
}) {
|
||||
const [isEdit, setIsEdit] = useState(false);
|
||||
|
||||
|
@ -35,8 +40,8 @@ function ImagePackProfile({
|
|||
|
||||
const handleUsageSelect = (event) => {
|
||||
openReusableContextMenu(
|
||||
'bottom',
|
||||
getEventCords(event, '.btn-surface'),
|
||||
"bottom",
|
||||
getEventCords(event, ".btn-surface"),
|
||||
(closeMenu) => (
|
||||
<ImagePackUsageSelector
|
||||
usage={usage}
|
||||
|
@ -45,48 +50,62 @@ function ImagePackProfile({
|
|||
closeMenu();
|
||||
}}
|
||||
/>
|
||||
),
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="image-pack-profile">
|
||||
{
|
||||
onAvatarChange
|
||||
? (
|
||||
<ImageUpload
|
||||
bgColor="#555"
|
||||
text={displayName}
|
||||
imageSrc={avatarUrl}
|
||||
size="normal"
|
||||
onUpload={onAvatarChange}
|
||||
onRequestRemove={() => onAvatarChange(undefined)}
|
||||
/>
|
||||
)
|
||||
: <Avatar bgColor="#555" text={displayName} imageSrc={avatarUrl} size="normal" />
|
||||
}
|
||||
{onAvatarChange ? (
|
||||
<ImageUpload
|
||||
bgColor="#555"
|
||||
text={displayName}
|
||||
imageSrc={avatarUrl}
|
||||
size="normal"
|
||||
onUpload={onAvatarChange}
|
||||
onRequestRemove={() => onAvatarChange(undefined)}
|
||||
/>
|
||||
) : (
|
||||
<Avatar
|
||||
bgColor="#555"
|
||||
text={displayName}
|
||||
imageSrc={avatarUrl}
|
||||
size="normal"
|
||||
/>
|
||||
)}
|
||||
<div className="image-pack-profile__content">
|
||||
{
|
||||
isEdit
|
||||
? (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<Input name="nameInput" label="Name" value={displayName} required />
|
||||
<Input name="attributionInput" label="Attribution" value={attribution} resizable />
|
||||
<div>
|
||||
<Button variant="primary" type="submit">Save</Button>
|
||||
<Button onClick={() => setIsEdit(false)}>Cancel</Button>
|
||||
</div>
|
||||
</form>
|
||||
) : (
|
||||
<>
|
||||
<div>
|
||||
<Text>{displayName}</Text>
|
||||
{onEditProfile && <IconButton size="extra-small" onClick={() => setIsEdit(true)} src={PencilIC} tooltip="Edit" />}
|
||||
</div>
|
||||
{attribution && <Text variant="b3">{attribution}</Text>}
|
||||
</>
|
||||
)
|
||||
}
|
||||
{isEdit ? (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<Input name="nameInput" label="Name" value={displayName} required />
|
||||
<Input
|
||||
name="attributionInput"
|
||||
label="Attribution"
|
||||
value={attribution}
|
||||
resizable
|
||||
/>
|
||||
<div>
|
||||
<Button variant="primary" type="submit">
|
||||
Save
|
||||
</Button>
|
||||
<Button onClick={() => setIsEdit(false)}>Cancel</Button>
|
||||
</div>
|
||||
</form>
|
||||
) : (
|
||||
<>
|
||||
<div>
|
||||
<Text>{displayName}</Text>
|
||||
{onEditProfile && (
|
||||
<IconButton
|
||||
size="extra-small"
|
||||
onClick={() => setIsEdit(true)}
|
||||
src={PencilIC}
|
||||
tooltip="Edit"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{attribution && <Text variant="b3">{attribution}</Text>}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="image-pack-profile__usage">
|
||||
<Text variant="b3">Pack usage</Text>
|
||||
|
@ -95,9 +114,9 @@ function ImagePackProfile({
|
|||
iconSrc={onUsageChange ? ChevronBottomIC : null}
|
||||
>
|
||||
<Text>
|
||||
{usage === 'emoticon' && 'Emoji'}
|
||||
{usage === 'sticker' && 'Sticker'}
|
||||
{usage === 'both' && 'Both'}
|
||||
{usage === "emoticon" && "Emoji"}
|
||||
{usage === "sticker" && "Sticker"}
|
||||
{usage === "both" && "Both"}
|
||||
</Text>
|
||||
</Button>
|
||||
</div>
|
||||
|
@ -116,7 +135,7 @@ ImagePackProfile.propTypes = {
|
|||
avatarUrl: PropTypes.string,
|
||||
displayName: PropTypes.string.isRequired,
|
||||
attribution: PropTypes.string,
|
||||
usage: PropTypes.oneOf(['emoticon', 'sticker', 'both']).isRequired,
|
||||
usage: PropTypes.oneOf(["emoticon", "sticker", "both"]).isRequired,
|
||||
onUsageChange: PropTypes.func,
|
||||
onAvatarChange: PropTypes.func,
|
||||
onEditProfile: PropTypes.func,
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
@use '../../partials/flex';
|
||||
@use "../../partials/flex";
|
||||
|
||||
.image-pack-profile {
|
||||
padding: var(--sp-normal);
|
||||
|
@ -34,4 +34,4 @@
|
|||
margin-bottom: var(--sp-ultra-tight);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
import React, { useState, useRef } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import './ImagePackUpload.scss';
|
||||
import React, { useState, useRef } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import "./ImagePackUpload.scss";
|
||||
|
||||
import initMatrix from '../../../client/initMatrix';
|
||||
import { scaleDownImage } from '../../../util/common';
|
||||
import initMatrix from "../../../client/initMatrix";
|
||||
import { scaleDownImage } from "../../../util/common";
|
||||
|
||||
import Text from '../../atoms/text/Text';
|
||||
import Button from '../../atoms/button/Button';
|
||||
import Input from '../../atoms/input/Input';
|
||||
import IconButton from '../../atoms/button/IconButton';
|
||||
import CirclePlusIC from '../../../../public/res/ic/outlined/circle-plus.svg';
|
||||
import Text from "../../atoms/text/Text";
|
||||
import Button from "../../atoms/button/Button";
|
||||
import Input from "../../atoms/input/Input";
|
||||
import IconButton from "../../atoms/button/IconButton";
|
||||
import CirclePlusIC from "../../../../public/res/ic/outlined/circle-plus.svg";
|
||||
|
||||
function ImagePackUpload({ onUpload }) {
|
||||
const mx = initMatrix.matrixClient;
|
||||
|
@ -23,7 +23,7 @@ function ImagePackUpload({ onUpload }) {
|
|||
if (!imgFile) return;
|
||||
const { shortcodeInput } = evt.target;
|
||||
const shortcode = shortcodeInput.value.trim();
|
||||
if (shortcode === '') return;
|
||||
if (shortcode === "") return;
|
||||
|
||||
setProgress(true);
|
||||
const image = await scaleDownImage(imgFile, 512, 512);
|
||||
|
@ -32,37 +32,53 @@ function ImagePackUpload({ onUpload }) {
|
|||
onUpload(shortcode, url);
|
||||
setProgress(false);
|
||||
setImgFile(null);
|
||||
shortcodeRef.current.value = '';
|
||||
shortcodeRef.current.value = "";
|
||||
};
|
||||
|
||||
const handleFileChange = (evt) => {
|
||||
const img = evt.target.files[0];
|
||||
if (!img) return;
|
||||
setImgFile(img);
|
||||
shortcodeRef.current.value = img.name.slice(0, img.name.indexOf('.'));
|
||||
shortcodeRef.current.value = img.name.slice(0, img.name.indexOf("."));
|
||||
shortcodeRef.current.focus();
|
||||
};
|
||||
const handleRemove = () => {
|
||||
setImgFile(null);
|
||||
inputRef.current.value = null;
|
||||
shortcodeRef.current.value = '';
|
||||
shortcodeRef.current.value = "";
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="image-pack-upload">
|
||||
<input ref={inputRef} onChange={handleFileChange} style={{ display: 'none' }} type="file" accept=".png, .gif, .webp" required />
|
||||
{
|
||||
imgFile
|
||||
? (
|
||||
<div className="image-pack-upload__file">
|
||||
<IconButton onClick={handleRemove} src={CirclePlusIC} tooltip="Remove file" />
|
||||
<Text>{imgFile.name}</Text>
|
||||
</div>
|
||||
)
|
||||
: <Button onClick={() => inputRef.current.click()}>Import image</Button>
|
||||
}
|
||||
<Input forwardRef={shortcodeRef} name="shortcodeInput" placeholder="shortcode" required />
|
||||
<Button disabled={progress} variant="primary" type="submit">{progress ? 'Uploading...' : 'Upload'}</Button>
|
||||
<input
|
||||
ref={inputRef}
|
||||
onChange={handleFileChange}
|
||||
style={{ display: "none" }}
|
||||
type="file"
|
||||
accept=".png, .gif, .webp"
|
||||
required
|
||||
/>
|
||||
{imgFile ? (
|
||||
<div className="image-pack-upload__file">
|
||||
<IconButton
|
||||
onClick={handleRemove}
|
||||
src={CirclePlusIC}
|
||||
tooltip="Remove file"
|
||||
/>
|
||||
<Text>{imgFile.name}</Text>
|
||||
</div>
|
||||
) : (
|
||||
<Button onClick={() => inputRef.current.click()}>Import image</Button>
|
||||
)}
|
||||
<Input
|
||||
forwardRef={shortcodeRef}
|
||||
name="shortcodeInput"
|
||||
placeholder="shortcode"
|
||||
required
|
||||
/>
|
||||
<Button disabled={progress} variant="primary" type="submit">
|
||||
{progress ? "Uploading..." : "Upload"}
|
||||
</Button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
@use '../../partials/dir';
|
||||
@use '../../partials/text';
|
||||
@use "../../partials/dir";
|
||||
@use "../../partials/text";
|
||||
|
||||
.image-pack-upload {
|
||||
padding: var(--sp-normal);
|
||||
|
@ -19,7 +19,7 @@
|
|||
background: var(--bg-surface-low);
|
||||
border-radius: var(--bo-radius);
|
||||
box-shadow: var(--bs-surface-border);
|
||||
|
||||
|
||||
& button {
|
||||
--parent-height: 40px;
|
||||
width: var(--parent-height);
|
||||
|
@ -28,16 +28,16 @@
|
|||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
|
||||
& .ic-raw {
|
||||
background-color: var(--bg-caution);
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
|
||||
& .text {
|
||||
@extend .cp-txt__ellipsis;
|
||||
@include dir.side(margin, var(--sp-ultra-tight), var(--sp-normal));
|
||||
max-width: 86px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,31 +1,31 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
import { MenuHeader, MenuItem } from '../../atoms/context-menu/ContextMenu';
|
||||
import CheckIC from '../../../../public/res/ic/outlined/check.svg';
|
||||
import { MenuHeader, MenuItem } from "../../atoms/context-menu/ContextMenu";
|
||||
import CheckIC from "../../../../public/res/ic/outlined/check.svg";
|
||||
|
||||
function ImagePackUsageSelector({ usage, onSelect }) {
|
||||
return (
|
||||
<div>
|
||||
<MenuHeader>Usage</MenuHeader>
|
||||
<MenuItem
|
||||
iconSrc={usage === 'emoticon' ? CheckIC : undefined}
|
||||
variant={usage === 'emoticon' ? 'positive' : 'surface'}
|
||||
onClick={() => onSelect('emoticon')}
|
||||
iconSrc={usage === "emoticon" ? CheckIC : undefined}
|
||||
variant={usage === "emoticon" ? "positive" : "surface"}
|
||||
onClick={() => onSelect("emoticon")}
|
||||
>
|
||||
Emoji
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
iconSrc={usage === 'sticker' ? CheckIC : undefined}
|
||||
variant={usage === 'sticker' ? 'positive' : 'surface'}
|
||||
onClick={() => onSelect('sticker')}
|
||||
iconSrc={usage === "sticker" ? CheckIC : undefined}
|
||||
variant={usage === "sticker" ? "positive" : "surface"}
|
||||
onClick={() => onSelect("sticker")}
|
||||
>
|
||||
Sticker
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
iconSrc={usage === 'both' ? CheckIC : undefined}
|
||||
variant={usage === 'both' ? 'positive' : 'surface'}
|
||||
onClick={() => onSelect('both')}
|
||||
iconSrc={usage === "both" ? CheckIC : undefined}
|
||||
variant={usage === "both" ? "positive" : "surface"}
|
||||
onClick={() => onSelect("both")}
|
||||
>
|
||||
Both
|
||||
</MenuItem>
|
||||
|
@ -34,7 +34,7 @@ function ImagePackUsageSelector({ usage, onSelect }) {
|
|||
}
|
||||
|
||||
ImagePackUsageSelector.propTypes = {
|
||||
usage: PropTypes.oneOf(['emoticon', 'sticker', 'both']).isRequired,
|
||||
usage: PropTypes.oneOf(["emoticon", "sticker", "both"]).isRequired,
|
||||
onSelect: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
|
|
|
@ -1,18 +1,22 @@
|
|||
import React, { useState, useRef } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import './ImageUpload.scss';
|
||||
import React, { useState, useRef } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import "./ImageUpload.scss";
|
||||
|
||||
import initMatrix from '../../../client/initMatrix';
|
||||
import initMatrix from "../../../client/initMatrix";
|
||||
|
||||
import Text from '../../atoms/text/Text';
|
||||
import Avatar from '../../atoms/avatar/Avatar';
|
||||
import Spinner from '../../atoms/spinner/Spinner';
|
||||
import RawIcon from '../../atoms/system-icons/RawIcon';
|
||||
import Text from "../../atoms/text/Text";
|
||||
import Avatar from "../../atoms/avatar/Avatar";
|
||||
import Spinner from "../../atoms/spinner/Spinner";
|
||||
import RawIcon from "../../atoms/system-icons/RawIcon";
|
||||
|
||||
import PlusIC from '../../../../public/res/ic/outlined/plus.svg';
|
||||
import PlusIC from "../../../../public/res/ic/outlined/plus.svg";
|
||||
|
||||
function ImageUpload({
|
||||
text, bgColor, imageSrc, onUpload, onRequestRemove,
|
||||
text,
|
||||
bgColor,
|
||||
imageSrc,
|
||||
onUpload,
|
||||
onRequestRemove,
|
||||
size,
|
||||
}) {
|
||||
const [uploadPromise, setUploadPromise] = useState(null);
|
||||
|
@ -26,7 +30,7 @@ function ImageUpload({
|
|||
setUploadPromise(uPromise);
|
||||
|
||||
const res = await uPromise;
|
||||
if (typeof res?.content_uri === 'string') onUpload(res.content_uri);
|
||||
if (typeof res?.content_uri === "string") onUpload(res.content_uri);
|
||||
setUploadPromise(null);
|
||||
} catch {
|
||||
setUploadPromise(null);
|
||||
|
@ -50,40 +54,48 @@ function ImageUpload({
|
|||
uploadImageRef.current.click();
|
||||
}}
|
||||
>
|
||||
<Avatar
|
||||
imageSrc={imageSrc}
|
||||
text={text}
|
||||
bgColor={bgColor}
|
||||
size={size}
|
||||
/>
|
||||
<div className={`img-upload__process ${uploadPromise === null ? ' img-upload__process--stopped' : ''}`}>
|
||||
{uploadPromise === null && (
|
||||
size === 'large'
|
||||
? <Text variant="b3" weight="bold">Upload</Text>
|
||||
: <RawIcon src={PlusIC} color="white" />
|
||||
)}
|
||||
<Avatar imageSrc={imageSrc} text={text} bgColor={bgColor} size={size} />
|
||||
<div
|
||||
className={`img-upload__process ${
|
||||
uploadPromise === null ? " img-upload__process--stopped" : ""
|
||||
}`}
|
||||
>
|
||||
{uploadPromise === null &&
|
||||
(size === "large" ? (
|
||||
<Text variant="b3" weight="bold">
|
||||
Upload
|
||||
</Text>
|
||||
) : (
|
||||
<RawIcon src={PlusIC} color="white" />
|
||||
))}
|
||||
{uploadPromise !== null && <Spinner size="small" />}
|
||||
</div>
|
||||
</button>
|
||||
{ (typeof imageSrc === 'string' || uploadPromise !== null) && (
|
||||
{(typeof imageSrc === "string" || uploadPromise !== null) && (
|
||||
<button
|
||||
className="img-upload__btn-cancel"
|
||||
type="button"
|
||||
onClick={uploadPromise === null ? onRequestRemove : cancelUpload}
|
||||
>
|
||||
<Text variant="b3">{uploadPromise ? 'Cancel' : 'Remove'}</Text>
|
||||
<Text variant="b3">{uploadPromise ? "Cancel" : "Remove"}</Text>
|
||||
</button>
|
||||
)}
|
||||
<input onChange={uploadImage} style={{ display: 'none' }} ref={uploadImageRef} type="file" accept="image/*" />
|
||||
<input
|
||||
onChange={uploadImage}
|
||||
style={{ display: "none" }}
|
||||
ref={uploadImageRef}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ImageUpload.defaultProps = {
|
||||
text: null,
|
||||
bgColor: 'transparent',
|
||||
bgColor: "transparent",
|
||||
imageSrc: null,
|
||||
size: 'large',
|
||||
size: "large",
|
||||
};
|
||||
|
||||
ImageUpload.propTypes = {
|
||||
|
@ -92,7 +104,7 @@ ImageUpload.propTypes = {
|
|||
imageSrc: PropTypes.string,
|
||||
onUpload: PropTypes.func.isRequired,
|
||||
onRequestRemove: PropTypes.func.isRequired,
|
||||
size: PropTypes.oneOf(['large', 'normal']),
|
||||
size: PropTypes.oneOf(["large", "normal"]),
|
||||
};
|
||||
|
||||
export default ImageUpload;
|
||||
|
|
|
@ -1,49 +1,48 @@
|
|||
.img-upload__wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.img-upload {
|
||||
display: flex;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
display: flex;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
|
||||
&__process {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: var(--bo-radius);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: rgba(0, 0, 0, .6);
|
||||
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1;
|
||||
& .text {
|
||||
text-transform: uppercase;
|
||||
color: white;
|
||||
}
|
||||
&--stopped {
|
||||
display: none;
|
||||
}
|
||||
& .donut-spinner {
|
||||
border-color: rgb(255, 255, 255, .3);
|
||||
border-left-color: white;
|
||||
}
|
||||
}
|
||||
&:hover .img-upload__process--stopped {
|
||||
display: flex;
|
||||
}
|
||||
&__process {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: var(--bo-radius);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: rgba(0, 0, 0, 0.6);
|
||||
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1;
|
||||
& .text {
|
||||
text-transform: uppercase;
|
||||
color: white;
|
||||
}
|
||||
&--stopped {
|
||||
display: none;
|
||||
}
|
||||
& .donut-spinner {
|
||||
border-color: rgb(255, 255, 255, 0.3);
|
||||
border-left-color: white;
|
||||
}
|
||||
}
|
||||
&:hover .img-upload__process--stopped {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
&__btn-cancel {
|
||||
margin-top: var(--sp-extra-tight);
|
||||
cursor: pointer;
|
||||
& .text {
|
||||
color: var(--tc-danger-normal)
|
||||
}
|
||||
}
|
||||
&__btn-cancel {
|
||||
margin-top: var(--sp-extra-tight);
|
||||
cursor: pointer;
|
||||
& .text {
|
||||
color: var(--tc-danger-normal);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,18 +1,18 @@
|
|||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import './ExportE2ERoomKeys.scss';
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
import "./ExportE2ERoomKeys.scss";
|
||||
|
||||
import FileSaver from 'file-saver';
|
||||
import FileSaver from "file-saver";
|
||||
|
||||
import initMatrix from '../../../client/initMatrix';
|
||||
import cons from '../../../client/state/cons';
|
||||
import { encryptMegolmKeyFile } from '../../../util/cryptE2ERoomKeys';
|
||||
import initMatrix from "../../../client/initMatrix";
|
||||
import cons from "../../../client/state/cons";
|
||||
import { encryptMegolmKeyFile } from "../../../util/cryptE2ERoomKeys";
|
||||
|
||||
import Text from '../../atoms/text/Text';
|
||||
import Button from '../../atoms/button/Button';
|
||||
import Input from '../../atoms/input/Input';
|
||||
import Spinner from '../../atoms/spinner/Spinner';
|
||||
import Text from "../../atoms/text/Text";
|
||||
import Button from "../../atoms/button/Button";
|
||||
import Input from "../../atoms/input/Input";
|
||||
import Spinner from "../../atoms/spinner/Spinner";
|
||||
|
||||
import { useStore } from '../../hooks/useStore';
|
||||
import { useStore } from "../../hooks/useStore";
|
||||
|
||||
function ExportE2ERoomKeys() {
|
||||
const isMountStore = useStore();
|
||||
|
@ -29,14 +29,14 @@ function ExportE2ERoomKeys() {
|
|||
if (password !== confirmPasswordRef.current.value) {
|
||||
setStatus({
|
||||
isOngoing: false,
|
||||
msg: 'Password does not match.',
|
||||
msg: "Password does not match.",
|
||||
type: cons.status.ERROR,
|
||||
});
|
||||
return;
|
||||
}
|
||||
setStatus({
|
||||
isOngoing: true,
|
||||
msg: 'Getting keys...',
|
||||
msg: "Getting keys...",
|
||||
type: cons.status.IN_FLIGHT,
|
||||
});
|
||||
try {
|
||||
|
@ -44,19 +44,22 @@ function ExportE2ERoomKeys() {
|
|||
if (isMountStore.getItem()) {
|
||||
setStatus({
|
||||
isOngoing: true,
|
||||
msg: 'Encrypting keys...',
|
||||
msg: "Encrypting keys...",
|
||||
type: cons.status.IN_FLIGHT,
|
||||
});
|
||||
}
|
||||
const encKeys = await encryptMegolmKeyFile(JSON.stringify(keys), password);
|
||||
const encKeys = await encryptMegolmKeyFile(
|
||||
JSON.stringify(keys),
|
||||
password
|
||||
);
|
||||
const blob = new Blob([encKeys], {
|
||||
type: 'text/plain;charset=us-ascii',
|
||||
type: "text/plain;charset=us-ascii",
|
||||
});
|
||||
FileSaver.saveAs(blob, 'cinny-keys.txt');
|
||||
FileSaver.saveAs(blob, "cinny-keys.txt");
|
||||
if (isMountStore.getItem()) {
|
||||
setStatus({
|
||||
isOngoing: false,
|
||||
msg: 'Successfully exported all keys.',
|
||||
msg: "Successfully exported all keys.",
|
||||
type: cons.status.SUCCESS,
|
||||
});
|
||||
}
|
||||
|
@ -64,7 +67,7 @@ function ExportE2ERoomKeys() {
|
|||
if (isMountStore.getItem()) {
|
||||
setStatus({
|
||||
isOngoing: false,
|
||||
msg: e.friendlyText || 'Failed to export keys. Please try again.',
|
||||
msg: e.friendlyText || "Failed to export keys. Please try again.",
|
||||
type: cons.status.ERROR,
|
||||
});
|
||||
}
|
||||
|
@ -80,19 +83,45 @@ function ExportE2ERoomKeys() {
|
|||
|
||||
return (
|
||||
<div className="export-e2e-room-keys">
|
||||
<form className="export-e2e-room-keys__form" onSubmit={(e) => { e.preventDefault(); exportE2ERoomKeys(); }}>
|
||||
<Input forwardRef={passwordRef} type="password" placeholder="Password" required />
|
||||
<Input forwardRef={confirmPasswordRef} type="password" placeholder="Confirm password" required />
|
||||
<Button disabled={status.isOngoing} variant="primary" type="submit">Export</Button>
|
||||
<form
|
||||
className="export-e2e-room-keys__form"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
exportE2ERoomKeys();
|
||||
}}
|
||||
>
|
||||
<Input
|
||||
forwardRef={passwordRef}
|
||||
type="password"
|
||||
placeholder="Password"
|
||||
required
|
||||
/>
|
||||
<Input
|
||||
forwardRef={confirmPasswordRef}
|
||||
type="password"
|
||||
placeholder="Confirm password"
|
||||
required
|
||||
/>
|
||||
<Button disabled={status.isOngoing} variant="primary" type="submit">
|
||||
Export
|
||||
</Button>
|
||||
</form>
|
||||
{ status.type === cons.status.IN_FLIGHT && (
|
||||
{status.type === cons.status.IN_FLIGHT && (
|
||||
<div className="import-e2e-room-keys__process">
|
||||
<Spinner size="small" />
|
||||
<Text variant="b2">{status.msg}</Text>
|
||||
</div>
|
||||
)}
|
||||
{status.type === cons.status.SUCCESS && <Text className="import-e2e-room-keys__success" variant="b2">{status.msg}</Text>}
|
||||
{status.type === cons.status.ERROR && <Text className="import-e2e-room-keys__error" variant="b2">{status.msg}</Text>}
|
||||
{status.type === cons.status.SUCCESS && (
|
||||
<Text className="import-e2e-room-keys__success" variant="b2">
|
||||
{status.msg}
|
||||
</Text>
|
||||
)}
|
||||
{status.type === cons.status.ERROR && (
|
||||
<Text className="import-e2e-room-keys__error" variant="b2">
|
||||
{status.msg}
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue