Compare commits
34
Commits
v2.1.1
...
sidebar-update
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
923d2a2167 | ||
|
|
1deef51df0 | ||
|
|
ddacdbf25f | ||
|
|
7c3a32641c | ||
|
|
1ec7322830 | ||
|
|
30972b8e87 | ||
|
|
e801aa538d | ||
|
|
a5878e9de6 | ||
|
|
8b52c3cfff | ||
|
|
0fc128150b | ||
|
|
db7f1746f6 | ||
|
|
c73c8d7ffa | ||
|
|
ba37b3047e | ||
|
|
010e3cca82 | ||
|
|
74ffd6280f | ||
|
|
6f7934badc | ||
|
|
2f2680be3c | ||
|
|
4cd8f4a94c | ||
|
|
a417980a81 | ||
|
|
af69955801 | ||
|
|
214d49f1d9 | ||
|
|
c4e36a1f97 | ||
|
|
5de6a1bea6 | ||
|
|
74a20a0e14 | ||
|
|
820d08017a | ||
|
|
258afec391 | ||
|
|
0cf5aac591 | ||
|
|
59fd34a4b4 | ||
|
|
1da3d252e8 | ||
|
|
3c1cc59d59 | ||
|
|
1692098d5d | ||
|
|
fbab53af22 | ||
|
|
ce1e263d57 | ||
|
|
9f99320fda |
No files matched your search
@@ -15,7 +15,7 @@ jobs:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3.0.2
|
||||
- name: Build Docker image
|
||||
uses: docker/build-push-action@v3.1.0
|
||||
uses: docker/build-push-action@v3.1.1
|
||||
with:
|
||||
context: .
|
||||
push: false
|
||||
@@ -86,7 +86,7 @@ jobs:
|
||||
with:
|
||||
images: ajbura/cinny
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v3.1.0
|
||||
uses: docker/build-push-action@v3.1.1
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
|
||||
Generated
+23
-23
@@ -1,16 +1,16 @@
|
||||
{
|
||||
"name": "cinny",
|
||||
"version": "2.1.1",
|
||||
"version": "2.1.2",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "cinny",
|
||||
"version": "2.1.1",
|
||||
"version": "2.1.2",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@fontsource/inter": "^4.5.11",
|
||||
"@fontsource/roboto": "^4.5.7",
|
||||
"@fontsource/inter": "^4.5.12",
|
||||
"@fontsource/roboto": "^4.5.8",
|
||||
"@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.12.tgz",
|
||||
"@tippyjs/react": "^4.2.6",
|
||||
"babel-polyfill": "^6.26.0",
|
||||
@@ -70,7 +70,7 @@
|
||||
"html-webpack-plugin": "^5.3.1",
|
||||
"mini-css-extract-plugin": "^2.6.1",
|
||||
"path-browserify": "^1.0.1",
|
||||
"sass": "^1.54.1",
|
||||
"sass": "^1.54.3",
|
||||
"sass-loader": "^13.0.2",
|
||||
"stream-browserify": "^3.0.0",
|
||||
"style-loader": "^3.3.1",
|
||||
@@ -1837,14 +1837,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@fontsource/inter": {
|
||||
"version": "4.5.11",
|
||||
"resolved": "https://registry.npmjs.org/@fontsource/inter/-/inter-4.5.11.tgz",
|
||||
"integrity": "sha512-toizzQkfXL8YJcG/f8j3EYXYGQe4OxiDEItThSigvHU+cYNDw8HPp3wLYQX745hddsnHqOGCM4exitFSBOU8+w=="
|
||||
"version": "4.5.12",
|
||||
"resolved": "https://registry.npmjs.org/@fontsource/inter/-/inter-4.5.12.tgz",
|
||||
"integrity": "sha512-bGKk4/8tube/nCk8hav0ZDBVbzJzc7m0Vt4xF5p15IN4YImwGdtKG38Oq5bU8xHNS+VfvbFFCepgQNj7Pr/Lvg=="
|
||||
},
|
||||
"node_modules/@fontsource/roboto": {
|
||||
"version": "4.5.7",
|
||||
"resolved": "https://registry.npmjs.org/@fontsource/roboto/-/roboto-4.5.7.tgz",
|
||||
"integrity": "sha512-m57UMER23Mk6Drg9OjtHW1Y+0KPGyZfE5XJoPTOsLARLar6013kJj4X2HICt+iFLJqIgTahA/QAvSn9lwF1EEw=="
|
||||
"version": "4.5.8",
|
||||
"resolved": "https://registry.npmjs.org/@fontsource/roboto/-/roboto-4.5.8.tgz",
|
||||
"integrity": "sha512-CnD7zLItIzt86q4Sj3kZUiLcBk1dSk81qcqgMGaZe7SQ1P8hFNxhMl5AZthK1zrDM5m74VVhaOpuMGIL4gagaA=="
|
||||
},
|
||||
"node_modules/@humanwhocodes/config-array": {
|
||||
"version": "0.10.4",
|
||||
@@ -12181,9 +12181,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/sass": {
|
||||
"version": "1.54.1",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.54.1.tgz",
|
||||
"integrity": "sha512-GHJJr31Me32RjjUBagyzx8tzjKBUcDwo5239XANIRBq0adDu5iIG0aFO0i/TBb/4I9oyxkEv44nq/kL1DxdDhA==",
|
||||
"version": "1.54.3",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.54.3.tgz",
|
||||
"integrity": "sha512-fLodey5Qd41Pxp/Tk7Al97sViYwF/TazRc5t6E65O7JOk4XF8pzwIW7CvCxYVOfJFFI/1x5+elDyBIixrp+zrw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"chokidar": ">=3.0.0 <4.0.0",
|
||||
@@ -15526,14 +15526,14 @@
|
||||
}
|
||||
},
|
||||
"@fontsource/inter": {
|
||||
"version": "4.5.11",
|
||||
"resolved": "https://registry.npmjs.org/@fontsource/inter/-/inter-4.5.11.tgz",
|
||||
"integrity": "sha512-toizzQkfXL8YJcG/f8j3EYXYGQe4OxiDEItThSigvHU+cYNDw8HPp3wLYQX745hddsnHqOGCM4exitFSBOU8+w=="
|
||||
"version": "4.5.12",
|
||||
"resolved": "https://registry.npmjs.org/@fontsource/inter/-/inter-4.5.12.tgz",
|
||||
"integrity": "sha512-bGKk4/8tube/nCk8hav0ZDBVbzJzc7m0Vt4xF5p15IN4YImwGdtKG38Oq5bU8xHNS+VfvbFFCepgQNj7Pr/Lvg=="
|
||||
},
|
||||
"@fontsource/roboto": {
|
||||
"version": "4.5.7",
|
||||
"resolved": "https://registry.npmjs.org/@fontsource/roboto/-/roboto-4.5.7.tgz",
|
||||
"integrity": "sha512-m57UMER23Mk6Drg9OjtHW1Y+0KPGyZfE5XJoPTOsLARLar6013kJj4X2HICt+iFLJqIgTahA/QAvSn9lwF1EEw=="
|
||||
"version": "4.5.8",
|
||||
"resolved": "https://registry.npmjs.org/@fontsource/roboto/-/roboto-4.5.8.tgz",
|
||||
"integrity": "sha512-CnD7zLItIzt86q4Sj3kZUiLcBk1dSk81qcqgMGaZe7SQ1P8hFNxhMl5AZthK1zrDM5m74VVhaOpuMGIL4gagaA=="
|
||||
},
|
||||
"@humanwhocodes/config-array": {
|
||||
"version": "0.10.4",
|
||||
@@ -23442,9 +23442,9 @@
|
||||
}
|
||||
},
|
||||
"sass": {
|
||||
"version": "1.54.1",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.54.1.tgz",
|
||||
"integrity": "sha512-GHJJr31Me32RjjUBagyzx8tzjKBUcDwo5239XANIRBq0adDu5iIG0aFO0i/TBb/4I9oyxkEv44nq/kL1DxdDhA==",
|
||||
"version": "1.54.3",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.54.3.tgz",
|
||||
"integrity": "sha512-fLodey5Qd41Pxp/Tk7Al97sViYwF/TazRc5t6E65O7JOk4XF8pzwIW7CvCxYVOfJFFI/1x5+elDyBIixrp+zrw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"chokidar": ">=3.0.0 <4.0.0",
|
||||
|
||||
+4
-4
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "cinny",
|
||||
"version": "2.1.1",
|
||||
"version": "2.1.2",
|
||||
"description": "Yet another matrix client",
|
||||
"main": "index.js",
|
||||
"engines": {
|
||||
@@ -15,8 +15,8 @@
|
||||
"author": "Ajay Bura",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@fontsource/inter": "^4.5.11",
|
||||
"@fontsource/roboto": "^4.5.7",
|
||||
"@fontsource/inter": "^4.5.12",
|
||||
"@fontsource/roboto": "^4.5.8",
|
||||
"@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.12.tgz",
|
||||
"@tippyjs/react": "^4.2.6",
|
||||
"babel-polyfill": "^6.26.0",
|
||||
@@ -76,7 +76,7 @@
|
||||
"html-webpack-plugin": "^5.3.1",
|
||||
"mini-css-extract-plugin": "^2.6.1",
|
||||
"path-browserify": "^1.0.1",
|
||||
"sass": "^1.54.1",
|
||||
"sass": "^1.54.3",
|
||||
"sass-loader": "^13.0.2",
|
||||
"stream-browserify": "^3.0.0",
|
||||
"style-loader": "^3.3.1",
|
||||
|
||||
@@ -1,12 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 18.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 24 24" enable-background="new 0 0 24 24" xml:space="preserve">
|
||||
<g>
|
||||
<polygon points="14,3 14,5 17.8,5 12.9,9.9 14.3,11.3 19,6.6 19,10.2 21,10.2 21,3 "/>
|
||||
<path d="M3,10.2h2V5h5V3H5C3.9,3,3,3.9,3,5V10.2z"/>
|
||||
<path d="M5,14.2H3V19c0,1.1,0.9,2,2,2h5v-2H5V14.2z"/>
|
||||
<path d="M19,19h-5v2h5c1.1,0,2-0.9,2-2v-4.8h-2V19z"/>
|
||||
</g>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14 3V5H17.8L12.9 9.9L14.3 11.3L19 6.6V10.2H21V3H14Z" fill="black"/>
|
||||
<path d="M5 5H10V3H5C3.9 3 3 3.9 3 5V19C3 20.1 3.9 21 5 21H19C20.1 21 21 20.1 21 19V14.2H19V19H5V5Z" fill="black"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 703 B After Width: | Height: | Size: 297 B |
@@ -42,7 +42,6 @@ function RawModal({
|
||||
shouldCloseOnEsc={closeFromOutside}
|
||||
shouldCloseOnOverlayClick={closeFromOutside}
|
||||
shouldReturnFocusAfterClose={false}
|
||||
closeTimeoutMS={300}
|
||||
>
|
||||
{children}
|
||||
</Modal>
|
||||
|
||||
@@ -1,33 +1,9 @@
|
||||
.ReactModal__Overlay {
|
||||
opacity: 0;
|
||||
transition: opacity 200ms var(--fluid-slide-up);
|
||||
}
|
||||
.ReactModal__Overlay--after-open{
|
||||
opacity: 1;
|
||||
}
|
||||
.ReactModal__Overlay--before-close{
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.ReactModal__Content {
|
||||
transform: translateY(100%);
|
||||
transition: transform 200ms var(--fluid-slide-up);
|
||||
}
|
||||
|
||||
.ReactModal__Content--after-open{
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.ReactModal__Content--before-close{
|
||||
transform: translateY(100%);
|
||||
}
|
||||
|
||||
.raw-modal {
|
||||
--small-modal-width: 525px;
|
||||
--medium-modal-width: 712px;
|
||||
--large-modal-width: 1024px;
|
||||
|
||||
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-height: 100%;
|
||||
border-radius: var(--bo-radius);
|
||||
@@ -60,4 +36,31 @@
|
||||
height: 100%;
|
||||
background-color: var(--bg-overlay);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ReactModal__Overlay {
|
||||
animation: raw-modal--overlay 150ms;
|
||||
}
|
||||
|
||||
.ReactModal__Content {
|
||||
animation: raw-modal--content 150ms;
|
||||
}
|
||||
|
||||
@keyframes raw-modal--content {
|
||||
0% {
|
||||
transform: translateY(100px);
|
||||
opacity: .5;
|
||||
}
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
@keyframes raw-modal--overlay {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
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 DownloadSVG from '../../../../public/res/ic/outlined/download.svg';
|
||||
import ExternalSVG from '../../../../public/res/ic/outlined/external.svg';
|
||||
|
||||
function ImageLightbox({
|
||||
url, alt, isOpen, onRequestClose,
|
||||
}) {
|
||||
const handleDownload = () => {
|
||||
FileSaver.saveAs(url, alt);
|
||||
};
|
||||
|
||||
return (
|
||||
<RawModal
|
||||
className="image-lightbox__modal"
|
||||
overlayClassName="image-lightbox__overlay"
|
||||
isOpen={isOpen}
|
||||
onRequestClose={onRequestClose}
|
||||
size="large"
|
||||
>
|
||||
<div className="image-lightbox__header">
|
||||
<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">
|
||||
<img src={url} alt={alt} />
|
||||
</div>
|
||||
</RawModal>
|
||||
);
|
||||
}
|
||||
|
||||
ImageLightbox.propTypes = {
|
||||
url: PropTypes.string.isRequired,
|
||||
alt: PropTypes.string.isRequired,
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onRequestClose: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default ImageLightbox;
|
||||
@@ -0,0 +1,50 @@
|
||||
@use '../../partials/flex';
|
||||
@use '../../partials/text';
|
||||
|
||||
.image-lightbox__modal {
|
||||
box-shadow: none;
|
||||
width: unset;
|
||||
gap: var(--sp-normal);
|
||||
|
||||
border-radius: 0;
|
||||
pointer-events: none;
|
||||
|
||||
& .text {
|
||||
color: white;
|
||||
}
|
||||
& .ic-raw {
|
||||
background-color: white;
|
||||
}
|
||||
}
|
||||
|
||||
.image-lightbox__overlay {
|
||||
background-color: var(--bg-overlay-low);
|
||||
}
|
||||
|
||||
|
||||
.image-lightbox__header > *,
|
||||
.image-lightbox__content > * {
|
||||
pointer-events: all;
|
||||
}
|
||||
.image-lightbox__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
& > .text {
|
||||
@extend .cp-fx__item-one;
|
||||
@extend .cp-txt__ellipsis;
|
||||
}
|
||||
}
|
||||
.image-lightbox__content {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
max-height: 90vh;
|
||||
|
||||
& img {
|
||||
background-color: var(--bg-surface-low);
|
||||
object-fit: contain;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
border-radius: var(--bo-radius);
|
||||
}
|
||||
}
|
||||
@@ -118,7 +118,7 @@ function useImagePackHandles(pack, sendPackContent) {
|
||||
|
||||
const getNewKey = (key) => {
|
||||
if (typeof key !== 'string') return undefined;
|
||||
let newKey = key?.replace(/\s/g, '-');
|
||||
let newKey = key?.replace(/\s/g, '_');
|
||||
if (pack.getImages().get(newKey)) {
|
||||
newKey = suffixRename(
|
||||
newKey,
|
||||
|
||||
@@ -41,11 +41,13 @@ function ImagePackUpload({ onUpload }) {
|
||||
const img = evt.target.files[0];
|
||||
if (!img) return;
|
||||
setImgFile(img);
|
||||
shortcodeRef.current.value = img.name.slice(0, img.name.indexOf('.'));
|
||||
shortcodeRef.current.focus();
|
||||
};
|
||||
const handleRemove = () => {
|
||||
setImgFile(null);
|
||||
inputRef.current.value = null;
|
||||
shortcodeRef.current.value = '';
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -8,47 +8,13 @@ import { BlurhashCanvas } from 'react-blurhash';
|
||||
import Text from '../../atoms/text/Text';
|
||||
import IconButton from '../../atoms/button/IconButton';
|
||||
import Spinner from '../../atoms/spinner/Spinner';
|
||||
import ImageLightbox from '../image-lightbox/ImageLightbox';
|
||||
|
||||
import DownloadSVG from '../../../../public/res/ic/outlined/download.svg';
|
||||
import ExternalSVG from '../../../../public/res/ic/outlined/external.svg';
|
||||
import PlaySVG from '../../../../public/res/ic/outlined/play.svg';
|
||||
|
||||
// https://github.com/matrix-org/matrix-react-sdk/blob/cd15e08fc285da42134817cce50de8011809cd53/src/utils/blobs.ts#L73
|
||||
const ALLOWED_BLOB_MIMETYPES = [
|
||||
'image/jpeg',
|
||||
'image/gif',
|
||||
'image/png',
|
||||
'image/apng',
|
||||
'image/webp',
|
||||
'image/avif',
|
||||
|
||||
'video/mp4',
|
||||
'video/webm',
|
||||
'video/ogg',
|
||||
'video/quicktime',
|
||||
|
||||
'audio/mp4',
|
||||
'audio/webm',
|
||||
'audio/aac',
|
||||
'audio/mpeg',
|
||||
'audio/ogg',
|
||||
'audio/wave',
|
||||
'audio/wav',
|
||||
'audio/x-wav',
|
||||
'audio/x-pn-wav',
|
||||
'audio/flac',
|
||||
'audio/x-flac',
|
||||
];
|
||||
function getBlobSafeMimeType(mimetype) {
|
||||
if (!ALLOWED_BLOB_MIMETYPES.includes(mimetype)) {
|
||||
return 'application/octet-stream';
|
||||
}
|
||||
// Required for Chromium browsers
|
||||
if (mimetype === 'video/quicktime') {
|
||||
return 'video/mp4';
|
||||
}
|
||||
return mimetype;
|
||||
}
|
||||
import { getBlobSafeMimeType } from '../../../util/mimetypes';
|
||||
|
||||
async function getDecryptedBlob(response, type, decryptData) {
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
@@ -159,6 +125,7 @@ function Image({
|
||||
}) {
|
||||
const [url, setUrl] = useState(null);
|
||||
const [blur, setBlur] = useState(true);
|
||||
const [lightbox, setLightbox] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
let unmounted = false;
|
||||
@@ -173,14 +140,42 @@ function Image({
|
||||
};
|
||||
}, []);
|
||||
|
||||
const toggleLightbox = () => {
|
||||
if (!url) return;
|
||||
setLightbox(!lightbox);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="file-container">
|
||||
<FileHeader name={name} link={url || link} type={type} external />
|
||||
<div style={{ height: width !== null ? getNativeHeight(width, height) : 'unset' }} className="image-container">
|
||||
{ blurhash && blur && <BlurhashCanvas hash={blurhash} punch={1} />}
|
||||
{ url !== null && <img onLoad={() => setBlur(false)} src={url || link} alt={name} />}
|
||||
<>
|
||||
<div className="file-container">
|
||||
<div
|
||||
style={{ height: width !== null ? getNativeHeight(width, height) : 'unset' }}
|
||||
className="image-container"
|
||||
role="button"
|
||||
tabIndex="0"
|
||||
onClick={toggleLightbox}
|
||||
onKeyDown={toggleLightbox}
|
||||
>
|
||||
{ blurhash && blur && <BlurhashCanvas hash={blurhash} punch={1} />}
|
||||
{ url !== null && (
|
||||
<img
|
||||
style={{ display: blur ? 'none' : 'unset' }}
|
||||
onLoad={() => setBlur(false)}
|
||||
src={url || link}
|
||||
alt={name}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{url && (
|
||||
<ImageLightbox
|
||||
url={url}
|
||||
alt={name}
|
||||
isOpen={lightbox}
|
||||
onRequestClose={toggleLightbox}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
Image.defaultProps = {
|
||||
@@ -227,11 +222,11 @@ function Sticker({
|
||||
Sticker.defaultProps = {
|
||||
file: null,
|
||||
type: '',
|
||||
width: null,
|
||||
height: null,
|
||||
};
|
||||
Sticker.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
width: null,
|
||||
height: null,
|
||||
width: PropTypes.number,
|
||||
height: PropTypes.number,
|
||||
link: PropTypes.string.isRequired,
|
||||
@@ -289,6 +284,7 @@ function Video({
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [url, setUrl] = useState(null);
|
||||
const [thumbUrl, setThumbUrl] = useState(null);
|
||||
const [blur, setBlur] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
let unmounted = false;
|
||||
@@ -303,16 +299,16 @@ function Video({
|
||||
};
|
||||
}, []);
|
||||
|
||||
async function loadVideo() {
|
||||
const loadVideo = async () => {
|
||||
const myUrl = await getUrl(link, type, file);
|
||||
setUrl(myUrl);
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
function handlePlayVideo() {
|
||||
const handlePlayVideo = () => {
|
||||
setIsLoading(true);
|
||||
loadVideo();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="file-container">
|
||||
@@ -323,15 +319,17 @@ function Video({
|
||||
}}
|
||||
className="video-container"
|
||||
>
|
||||
{ url === null && blurhash && <BlurhashCanvas hash={blurhash} punch={1} />}
|
||||
{ url === null && thumbUrl !== null && (
|
||||
/* eslint-disable-next-line jsx-a11y/alt-text */
|
||||
<img src={thumbUrl} />
|
||||
)}
|
||||
{ url === null && isLoading && <Spinner size="small" /> }
|
||||
{ url === null && !isLoading && <IconButton onClick={handlePlayVideo} tooltip="Play video" src={PlaySVG} />}
|
||||
{ url !== null && (
|
||||
/* eslint-disable-next-line jsx-a11y/media-has-caption */
|
||||
{ url === null ? (
|
||||
<>
|
||||
{ blurhash && blur && <BlurhashCanvas hash={blurhash} punch={1} />}
|
||||
{ thumbUrl !== null && (
|
||||
<img style={{ display: blur ? 'none' : 'unset' }} src={thumbUrl} onLoad={() => setBlur(false)} alt={name} />
|
||||
)}
|
||||
{isLoading && <Spinner size="small" />}
|
||||
{!isLoading && <IconButton onClick={handlePlayVideo} tooltip="Play video" src={PlaySVG} />}
|
||||
</>
|
||||
) : (
|
||||
/* eslint-disable-next-line jsx-a11y/media-has-caption */
|
||||
<video autoPlay controls poster={thumbUrl}>
|
||||
<source src={url} type={getBlobSafeMimeType(type)} />
|
||||
</video>
|
||||
|
||||
@@ -27,14 +27,21 @@
|
||||
white-space: initial;
|
||||
}
|
||||
|
||||
.sticker-container {
|
||||
display: inline-flex;
|
||||
max-width: 128px;
|
||||
width: 100%;
|
||||
& img {
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
|
||||
.image-container,
|
||||
.video-container,
|
||||
.audio-container {
|
||||
font-size: 0;
|
||||
line-height: 0;
|
||||
|
||||
position: relative;
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
@@ -48,7 +55,6 @@
|
||||
.video-container {
|
||||
& img,
|
||||
& canvas {
|
||||
position: absolute;
|
||||
max-width: unset !important;
|
||||
width: 100% !important;
|
||||
height: 100%;
|
||||
@@ -56,19 +62,21 @@
|
||||
margin: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.sticker-container {
|
||||
display: inline-flex;
|
||||
max-width: 128px;
|
||||
width: 100%;
|
||||
& img {
|
||||
width: 100% !important;
|
||||
.image-container {
|
||||
max-height: 460px;
|
||||
img {
|
||||
cursor: pointer;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
.video-container {
|
||||
position: relative;
|
||||
& .ic-btn-surface {
|
||||
background-color: var(--bg-surface-low);
|
||||
}
|
||||
& .ic-btn-surface,
|
||||
& .donut-spinner {
|
||||
position: absolute;
|
||||
}
|
||||
video {
|
||||
|
||||
@@ -37,6 +37,7 @@ import CmdIC from '../../../../public/res/ic/outlined/cmd.svg';
|
||||
import BinIC from '../../../../public/res/ic/outlined/bin.svg';
|
||||
|
||||
import { confirmDialog } from '../confirm-dialog/ConfirmDialog';
|
||||
import { getBlobSafeMimeType } from '../../../util/mimetypes';
|
||||
|
||||
function PlaceholderMessage() {
|
||||
return (
|
||||
@@ -206,7 +207,13 @@ const MessageBody = React.memo(({
|
||||
let content = null;
|
||||
if (isCustomHTML) {
|
||||
try {
|
||||
content = twemojify(sanitizeCustomHtml(body), undefined, true, false, true);
|
||||
content = twemojify(
|
||||
sanitizeCustomHtml(initMatrix.matrixClient, body),
|
||||
undefined,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
);
|
||||
} catch {
|
||||
console.error('Malformed custom html: ', body);
|
||||
content = twemojify(body, undefined);
|
||||
@@ -283,6 +290,11 @@ function MessageEdit({ body, onSave, onCancel }) {
|
||||
}, []);
|
||||
|
||||
const handleKeyDown = (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
onCancel();
|
||||
}
|
||||
|
||||
if (e.key === 'Enter' && e.shiftKey === false) {
|
||||
e.preventDefault();
|
||||
onSave(editInputRef.current.value);
|
||||
@@ -615,7 +627,12 @@ function genMediaContent(mE) {
|
||||
if (typeof mediaMXC === 'undefined' || mediaMXC === '') return <span style={{ color: 'var(--bg-danger)' }}>Malformed event</span>;
|
||||
|
||||
let msgType = mE.getContent()?.msgtype;
|
||||
if (mE.getType() === 'm.sticker') msgType = 'm.sticker';
|
||||
const safeMimetype = getBlobSafeMimeType(mContent.info?.mimetype);
|
||||
if (mE.getType() === 'm.sticker') {
|
||||
msgType = 'm.sticker';
|
||||
} else if (safeMimetype === 'application/octet-stream') {
|
||||
msgType = 'm.file';
|
||||
}
|
||||
|
||||
const blurhash = mContent?.info?.['xyz.amorgan.blurhash'];
|
||||
|
||||
|
||||
@@ -38,10 +38,10 @@ function SpaceOptions({ roomId, afterOptionSelect }) {
|
||||
|
||||
const handleMarkAsRead = () => {
|
||||
const spaceChildren = roomList.getCategorizedSpaces([roomId]);
|
||||
spaceChildren?.forEach((childIds, spaceId) => {
|
||||
spaceChildren?.forEach((childIds) => {
|
||||
childIds?.forEach((childId) => {
|
||||
markAsRead(childId);
|
||||
})
|
||||
});
|
||||
});
|
||||
afterOptionSelect();
|
||||
};
|
||||
|
||||
@@ -252,6 +252,58 @@ function EmojiBoard({ onSelect, searchRef }) {
|
||||
|
||||
return (
|
||||
<div id="emoji-board" className="emoji-board">
|
||||
<ScrollView invisible>
|
||||
<div className="emoji-board__nav">
|
||||
{recentEmojis.length > 0 && (
|
||||
<IconButton
|
||||
onClick={() => openGroup(0)}
|
||||
src={RecentClockIC}
|
||||
tooltip="Recent"
|
||||
tooltipPlacement="left"
|
||||
/>
|
||||
)}
|
||||
<div className="emoji-board__nav-custom">
|
||||
{
|
||||
availableEmojis.map((pack) => {
|
||||
const src = initMatrix.matrixClient
|
||||
.mxcUrlToHttp(pack.avatarUrl ?? pack.getEmojis()[0].mxc);
|
||||
return (
|
||||
<IconButton
|
||||
onClick={() => openGroup(recentOffset + pack.packIndex)}
|
||||
src={src}
|
||||
key={pack.packIndex}
|
||||
tooltip={pack.displayName ?? 'Unknown'}
|
||||
tooltipPlacement="left"
|
||||
isImage
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
<div className="emoji-board__nav-twemoji">
|
||||
{
|
||||
[
|
||||
[0, EmojiIC, 'Smilies'],
|
||||
[1, DogIC, 'Animals'],
|
||||
[2, CupIC, 'Food'],
|
||||
[3, BallIC, 'Activities'],
|
||||
[4, PhotoIC, 'Travel'],
|
||||
[5, BulbIC, 'Objects'],
|
||||
[6, PeaceIC, 'Symbols'],
|
||||
[7, FlagIC, 'Flags'],
|
||||
].map(([indx, ico, name]) => (
|
||||
<IconButton
|
||||
onClick={() => openGroup(recentOffset + availableEmojis.length + indx)}
|
||||
key={indx}
|
||||
src={ico}
|
||||
tooltip={name}
|
||||
tooltipPlacement="left"
|
||||
/>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</ScrollView>
|
||||
<div className="emoji-board__content">
|
||||
<div className="emoji-board__content__search">
|
||||
<RawIcon size="small" src={SearchIC} />
|
||||
@@ -285,58 +337,6 @@ function EmojiBoard({ onSelect, searchRef }) {
|
||||
<Text>:slight_smile:</Text>
|
||||
</div>
|
||||
</div>
|
||||
<ScrollView invisible>
|
||||
<div className="emoji-board__nav">
|
||||
{recentEmojis.length > 0 && (
|
||||
<IconButton
|
||||
onClick={() => openGroup(0)}
|
||||
src={RecentClockIC}
|
||||
tooltip="Recent"
|
||||
tooltipPlacement="right"
|
||||
/>
|
||||
)}
|
||||
<div className="emoji-board__nav-custom">
|
||||
{
|
||||
availableEmojis.map((pack) => {
|
||||
const src = initMatrix.matrixClient
|
||||
.mxcUrlToHttp(pack.avatarUrl ?? pack.getEmojis()[0].mxc);
|
||||
return (
|
||||
<IconButton
|
||||
onClick={() => openGroup(recentOffset + pack.packIndex)}
|
||||
src={src}
|
||||
key={pack.packIndex}
|
||||
tooltip={pack.displayName ?? 'Unknown'}
|
||||
tooltipPlacement="right"
|
||||
isImage
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
<div className="emoji-board__nav-twemoji">
|
||||
{
|
||||
[
|
||||
[0, EmojiIC, 'Smilies'],
|
||||
[1, DogIC, 'Animals'],
|
||||
[2, CupIC, 'Food'],
|
||||
[3, BallIC, 'Activities'],
|
||||
[4, PhotoIC, 'Travel'],
|
||||
[5, BulbIC, 'Objects'],
|
||||
[6, PeaceIC, 'Symbols'],
|
||||
[7, FlagIC, 'Flags'],
|
||||
].map(([indx, ico, name]) => (
|
||||
<IconButton
|
||||
onClick={() => openGroup(recentOffset + availableEmojis.length + indx)}
|
||||
key={indx}
|
||||
src={ico}
|
||||
tooltip={name}
|
||||
tooltipPlacement="right"
|
||||
/>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</ScrollView>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -25,8 +25,7 @@
|
||||
|
||||
min-height: 100%;
|
||||
padding: 4px 6px;
|
||||
background-color: var(--bg-surface-low);
|
||||
@include dir.side(border, 1px solid var(--bg-surface-border), none);
|
||||
@include dir.side(border, none, 1px solid var(--bg-surface-border));
|
||||
|
||||
position: relative;
|
||||
|
||||
@@ -122,8 +121,11 @@
|
||||
@include dir.side(margin, var(--left-margin), var(--right-margin));
|
||||
}
|
||||
& .emoji {
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
max-width: 38px;
|
||||
max-height: 38px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
object-fit: contain;
|
||||
padding: var(--emoji-padding);
|
||||
cursor: pointer;
|
||||
|
||||
@@ -6,7 +6,7 @@ import initMatrix from '../../../client/initMatrix';
|
||||
import cons from '../../../client/state/cons';
|
||||
import navigation from '../../../client/state/navigation';
|
||||
import { join } from '../../../client/action/room';
|
||||
import { selectRoom, selectSpace } from '../../../client/action/navigation';
|
||||
import { selectRoom, selectTab } from '../../../client/action/navigation';
|
||||
|
||||
import Text from '../../atoms/text/Text';
|
||||
import IconButton from '../../atoms/button/IconButton';
|
||||
@@ -32,7 +32,7 @@ function JoinAliasContent({ term, requestClose }) {
|
||||
const openRoom = (roomId) => {
|
||||
const room = mx.getRoom(roomId);
|
||||
if (!room) return;
|
||||
if (room.isSpaceRoom()) selectSpace(roomId);
|
||||
if (room.isSpaceRoom()) selectTab(roomId);
|
||||
else selectRoom(roomId);
|
||||
requestClose();
|
||||
};
|
||||
|
||||
@@ -6,7 +6,7 @@ import { twemojify } from '../../../util/twemojify';
|
||||
|
||||
import initMatrix from '../../../client/initMatrix';
|
||||
import cons from '../../../client/state/cons';
|
||||
import { selectSpace } from '../../../client/action/navigation';
|
||||
import { selectTab, selectSpace } from '../../../client/action/navigation';
|
||||
import navigation from '../../../client/state/navigation';
|
||||
import { abbreviateNumber } from '../../../util/common';
|
||||
|
||||
@@ -107,7 +107,10 @@ function DrawerBreadcrumb({ spaceId }) {
|
||||
{ index !== 0 && <RawIcon size="extra-small" src={ChevronRightIC} />}
|
||||
<Button
|
||||
className={index === spacePath.length - 1 ? 'drawer-breadcrumb__btn--selected' : ''}
|
||||
onClick={() => selectSpace(id)}
|
||||
onClick={() => {
|
||||
if (id === cons.tabs.HOME) selectTab(id);
|
||||
else selectSpace(id);
|
||||
}}
|
||||
>
|
||||
<Text variant="b2">{id === cons.tabs.HOME ? 'Home' : twemojify(mx.getRoom(id).name)}</Text>
|
||||
{ noti !== null && (
|
||||
|
||||
@@ -30,7 +30,7 @@ function Home({ spaceId }) {
|
||||
roomIds = spaceChildIds.filter((roomId) => rooms.has(roomId));
|
||||
directIds = spaceChildIds.filter((roomId) => directs.has(roomId));
|
||||
} else {
|
||||
spaceIds = roomList.getOrphanSpaces();
|
||||
spaceIds = roomList.getOrphanSpaces().filter((id) => !accountData.spaceShortcut.has(id));
|
||||
roomIds = roomList.getOrphanRooms();
|
||||
}
|
||||
|
||||
@@ -80,10 +80,10 @@ function Home({ spaceId }) {
|
||||
<RoomsCategory name="People" roomIds={directIds.sort(roomIdByActivity)} drawerPostie={drawerPostie} />
|
||||
)}
|
||||
|
||||
{ isCategorized && [...categories].map(([catId, childIds]) => {
|
||||
{ isCategorized && [...categories.keys()].sort(roomIdByAtoZ).map((catId) => {
|
||||
const rms = [];
|
||||
const dms = [];
|
||||
childIds.forEach((id) => {
|
||||
categories.get(catId).forEach((id) => {
|
||||
if (directs.has(id)) dms.push(id);
|
||||
else rms.push(id);
|
||||
});
|
||||
|
||||
@@ -70,6 +70,7 @@
|
||||
&__preview > img {
|
||||
max-height: 40px;
|
||||
border-radius: var(--bo-radius);
|
||||
max-width: 150px;
|
||||
}
|
||||
&__icon {
|
||||
padding: var(--sp-extra-tight);
|
||||
|
||||
@@ -164,7 +164,7 @@ function getUsersActionJsx(roomId, userIds, actionStr) {
|
||||
|
||||
const othersCount = userIds.length - MAX_VISIBLE_COUNT;
|
||||
// eslint-disable-next-line react/jsx-one-expression-per-line
|
||||
return <>{u1Jsx}, {u2Jsx}, {u3Jsx} and {othersCount} other are {actionStr}</>;
|
||||
return <>{u1Jsx}, {u2Jsx}, {u3Jsx} and {othersCount} others are {actionStr}</>;
|
||||
}
|
||||
|
||||
function parseTimelineChange(mEvent) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* eslint-disable jsx-a11y/click-events-have-key-events */
|
||||
/* eslint-disable jsx-a11y/no-static-element-interactions */
|
||||
import React from 'react';
|
||||
import React, { useRef } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import './StickerBoard.scss';
|
||||
|
||||
@@ -9,10 +9,12 @@ import { getRelevantPacks } from '../emoji-board/custom-emoji';
|
||||
|
||||
import Text from '../../atoms/text/Text';
|
||||
import ScrollView from '../../atoms/scroll/ScrollView';
|
||||
import IconButton from '../../atoms/button/IconButton';
|
||||
|
||||
function StickerBoard({ roomId, onSelect }) {
|
||||
const mx = initMatrix.matrixClient;
|
||||
const room = mx.getRoom(roomId);
|
||||
const scrollRef = useRef(null);
|
||||
|
||||
const parentIds = initMatrix.roomList.getAllParentSpaces(room.roomId);
|
||||
const parentRooms = [...parentIds].map((id) => mx.getRoom(id));
|
||||
@@ -38,6 +40,11 @@ function StickerBoard({ roomId, onSelect }) {
|
||||
onSelect(stickerData);
|
||||
};
|
||||
|
||||
const openGroup = (groupIndex) => {
|
||||
const scrollContent = scrollRef.current.firstElementChild;
|
||||
scrollContent.children[groupIndex].scrollIntoView();
|
||||
};
|
||||
|
||||
const renderPack = (pack) => (
|
||||
<div className="sticker-board__pack" key={pack.id}>
|
||||
<Text className="sticker-board__pack-header" variant="b2" weight="bold">{pack.displayName ?? 'Unknown'}</Text>
|
||||
@@ -50,6 +57,7 @@ function StickerBoard({ roomId, onSelect }) {
|
||||
alt={sticker.shortcode}
|
||||
title={sticker.body ?? sticker.shortcode}
|
||||
data-mx-sticker={sticker.mxc}
|
||||
loading="lazy"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -58,8 +66,27 @@ function StickerBoard({ roomId, onSelect }) {
|
||||
|
||||
return (
|
||||
<div className="sticker-board">
|
||||
{packs.length > 0 && (
|
||||
<ScrollView invisible>
|
||||
<div className="sticker-board__sidebar">
|
||||
{packs.map((pack, index) => {
|
||||
const src = mx.mxcUrlToHttp(pack.avatarUrl ?? pack.getStickers()[0].mxc);
|
||||
return (
|
||||
<IconButton
|
||||
key={pack.id}
|
||||
onClick={() => openGroup(index)}
|
||||
src={src}
|
||||
tooltip={pack.displayName || 'Unknown'}
|
||||
tooltipPlacement="left"
|
||||
isImage
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</ScrollView>
|
||||
)}
|
||||
<div className="sticker-board__container">
|
||||
<ScrollView autoHide>
|
||||
<ScrollView autoHide ref={scrollRef}>
|
||||
<div
|
||||
onClick={handleOnSelect}
|
||||
className="sticker-board__content"
|
||||
|
||||
@@ -5,6 +5,20 @@
|
||||
--sticker-board-width: 286px;
|
||||
display: flex;
|
||||
height: var(--sticker-board-height);
|
||||
display: flex;
|
||||
|
||||
& > .scrollbar {
|
||||
width: initial;
|
||||
height: var(--sticker-board-height);
|
||||
}
|
||||
|
||||
&__sidebar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100%;
|
||||
padding: 4px 6px;
|
||||
@include dir.side(border, none, 1px solid var(--bg-surface-border));
|
||||
}
|
||||
|
||||
&__container {
|
||||
flex-grow: 1;
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import initMatrix from '../initMatrix';
|
||||
|
||||
function logout() {
|
||||
async function logout() {
|
||||
const mx = initMatrix.matrixClient;
|
||||
mx.stopClient();
|
||||
mx.logout().then(() => {
|
||||
mx.clearStores();
|
||||
window.localStorage.clear();
|
||||
window.location.reload();
|
||||
});
|
||||
try {
|
||||
await mx.logout();
|
||||
} catch {
|
||||
// ignore if failed to logout
|
||||
}
|
||||
mx.clearStores();
|
||||
window.localStorage.clear();
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
export default logout;
|
||||
@@ -4,6 +4,17 @@ import { selectTab, selectSpace, selectRoom } from '../action/navigation';
|
||||
|
||||
function initRoomListListener(roomList) {
|
||||
const listenRoomLeave = (roomId) => {
|
||||
const parents = roomList.roomIdToParents.get(roomId);
|
||||
|
||||
if (parents) {
|
||||
[...parents].forEach((pId) => {
|
||||
const data = navigation.spaceToRoom.get(pId);
|
||||
if (data?.roomId === roomId) {
|
||||
navigation.spaceToRoom.delete(pId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (navigation.selectedRoomId === roomId) {
|
||||
selectRoom(null);
|
||||
}
|
||||
@@ -13,6 +24,8 @@ function initRoomListListener(roomList) {
|
||||
if (idIndex === 0) selectTab(cons.tabs.HOME);
|
||||
else selectSpace(navigation.selectedSpacePath[idIndex - 1]);
|
||||
}
|
||||
|
||||
navigation.removeRecentRoom(roomId);
|
||||
};
|
||||
|
||||
roomList.on(cons.events.roomList.ROOM_LEAVED, listenRoomLeave);
|
||||
|
||||
@@ -8,12 +8,19 @@ import AccountData from './state/AccountData';
|
||||
import RoomsInput from './state/RoomsInput';
|
||||
import Notifications from './state/Notifications';
|
||||
import { cryptoCallbacks } from './state/secretStorageKeys';
|
||||
import navigation from './state/navigation';
|
||||
|
||||
global.Olm = require('@matrix-org/olm');
|
||||
|
||||
// logger.disableAll();
|
||||
|
||||
class InitMatrix extends EventEmitter {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
navigation.initMatrix = this;
|
||||
}
|
||||
|
||||
async init() {
|
||||
await this.startClient();
|
||||
this.setupSync();
|
||||
|
||||
@@ -89,7 +89,7 @@ class RoomList extends EventEmitter {
|
||||
|
||||
child.forEach((childId) => {
|
||||
const room = this.matrixClient.getRoom(childId);
|
||||
if (room === null) return;
|
||||
if (room === null || room.getMyMembership() !== 'join') return;
|
||||
if (room.isSpaceRoom()) categorizeSpace(childId);
|
||||
else mappedChild.add(childId);
|
||||
});
|
||||
|
||||
@@ -6,6 +6,8 @@ import { math } from 'micromark-extension-math';
|
||||
import { encode } from 'blurhash';
|
||||
import { getShortcodeToEmoji } from '../../app/organisms/emoji-board/custom-emoji';
|
||||
import { mathExtensionHtml, spoilerExtension, spoilerExtensionHtml } from '../../util/markdown';
|
||||
import { getBlobSafeMimeType } from '../../util/mimetypes';
|
||||
import { sanitizeText } from '../../util/sanitize';
|
||||
import cons from './cons';
|
||||
import settings from './settings';
|
||||
|
||||
@@ -148,29 +150,25 @@ function findAndReplace(text, regex, filter, replace) {
|
||||
return copyText;
|
||||
}
|
||||
|
||||
function formatAndEmojifyText(mx, roomList, room, text) {
|
||||
function formatUserPill(room, text) {
|
||||
const { userIdsToDisplayNames } = room.currentState;
|
||||
const parentIds = roomList.getAllParentSpaces(room.roomId);
|
||||
const parentRooms = [...parentIds].map((id) => mx.getRoom(id));
|
||||
const allEmoji = getShortcodeToEmoji(mx, [room, ...parentRooms]);
|
||||
|
||||
let formattedText;
|
||||
if (settings.isMarkdown) {
|
||||
formattedText = getFormattedBody(text);
|
||||
} else {
|
||||
formattedText = text;
|
||||
}
|
||||
|
||||
formattedText = findAndReplace(
|
||||
formattedText,
|
||||
return findAndReplace(
|
||||
text,
|
||||
MXID_REGEX,
|
||||
(match) => userIdsToDisplayNames[match[0]],
|
||||
(match) => (
|
||||
`<a href="https://matrix.to/#/${match[0]}">@${userIdsToDisplayNames[match[0]]}</a>`
|
||||
),
|
||||
);
|
||||
formattedText = findAndReplace(
|
||||
formattedText,
|
||||
}
|
||||
|
||||
function formatEmoji(mx, room, roomList, text) {
|
||||
const parentIds = roomList.getAllParentSpaces(room.roomId);
|
||||
const parentRooms = [...parentIds].map((id) => mx.getRoom(id));
|
||||
const allEmoji = getShortcodeToEmoji(mx, [room, ...parentRooms]);
|
||||
|
||||
return findAndReplace(
|
||||
text,
|
||||
SHORTCODE_REGEX,
|
||||
(match) => allEmoji.has(match[1]),
|
||||
(match) => {
|
||||
@@ -191,8 +189,6 @@ function formatAndEmojifyText(mx, roomList, room, text) {
|
||||
return tag;
|
||||
},
|
||||
);
|
||||
|
||||
return formattedText;
|
||||
}
|
||||
|
||||
class RoomsInput extends EventEmitter {
|
||||
@@ -295,25 +291,27 @@ class RoomsInput extends EventEmitter {
|
||||
}
|
||||
|
||||
if (this.getMessage(roomId).trim() !== '') {
|
||||
const rawMessage = input.message;
|
||||
let content = {
|
||||
body: input.message,
|
||||
body: rawMessage,
|
||||
msgtype: 'm.text',
|
||||
};
|
||||
|
||||
// Apply formatting if relevant
|
||||
const formattedBody = formatAndEmojifyText(
|
||||
this.matrixClient,
|
||||
this.roomList,
|
||||
room,
|
||||
input.message,
|
||||
);
|
||||
let formattedBody = settings.isMarkdown
|
||||
? getFormattedBody(rawMessage)
|
||||
: sanitizeText(rawMessage);
|
||||
|
||||
formattedBody = formatUserPill(room, formattedBody);
|
||||
formattedBody = formatEmoji(this.matrixClient, room, this.roomList, formattedBody);
|
||||
|
||||
content.body = findAndReplace(
|
||||
content.body,
|
||||
MXID_REGEX,
|
||||
(match) => room.currentState.userIdsToDisplayNames[match[0]],
|
||||
(match) => `@${room.currentState.userIdsToDisplayNames[match[0]]}`,
|
||||
);
|
||||
if (formattedBody !== input.message) {
|
||||
if (formattedBody !== sanitizeText(rawMessage)) {
|
||||
// Formatting was applied, and we need to switch to custom HTML
|
||||
content.format = 'org.matrix.custom.html';
|
||||
content.formatted_body = formattedBody;
|
||||
@@ -358,7 +356,7 @@ class RoomsInput extends EventEmitter {
|
||||
}
|
||||
|
||||
async sendFile(roomId, file) {
|
||||
const fileType = file.type.slice(0, file.type.indexOf('/'));
|
||||
const fileType = getBlobSafeMimeType(file.type).slice(0, file.type.indexOf('/'));
|
||||
const info = {
|
||||
mimetype: file.type,
|
||||
size: file.size,
|
||||
@@ -481,19 +479,19 @@ class RoomsInput extends EventEmitter {
|
||||
};
|
||||
|
||||
// Apply formatting if relevant
|
||||
const formattedBody = formatAndEmojifyText(
|
||||
this.matrixClient,
|
||||
this.roomList,
|
||||
room,
|
||||
editedBody,
|
||||
);
|
||||
let formattedBody = settings.isMarkdown
|
||||
? getFormattedBody(editedBody)
|
||||
: sanitizeText(editedBody);
|
||||
formattedBody = formatUserPill(room, formattedBody);
|
||||
formattedBody = formatEmoji(this.matrixClient, room, this.roomList, formattedBody);
|
||||
|
||||
content.body = findAndReplace(
|
||||
content.body,
|
||||
MXID_REGEX,
|
||||
(match) => room.currentState.userIdsToDisplayNames[match[0]],
|
||||
(match) => `@${room.currentState.userIdsToDisplayNames[match[0]]}`,
|
||||
);
|
||||
if (formattedBody !== editedBody) {
|
||||
if (formattedBody !== sanitizeText(editedBody)) {
|
||||
content.formatted_body = ` * ${formattedBody}`;
|
||||
content.format = 'org.matrix.custom.html';
|
||||
content['m.new_content'].formatted_body = formattedBody;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const cons = {
|
||||
version: '2.1.1',
|
||||
version: '2.1.2',
|
||||
secretKey: {
|
||||
ACCESS_TOKEN: 'cinny_access_token',
|
||||
DEVICE_ID: 'cinny_device_id',
|
||||
|
||||
+228
-32
@@ -5,6 +5,8 @@ import cons from './cons';
|
||||
class Navigation extends EventEmitter {
|
||||
constructor() {
|
||||
super();
|
||||
// this will attached by initMatrix
|
||||
this.initMatrix = {};
|
||||
|
||||
this.selectedTab = cons.tabs.HOME;
|
||||
this.selectedSpaceId = null;
|
||||
@@ -14,14 +16,20 @@ class Navigation extends EventEmitter {
|
||||
this.isRoomSettings = false;
|
||||
this.recentRooms = [];
|
||||
|
||||
this.spaceToRoom = new Map();
|
||||
|
||||
this.rawModelStack = [];
|
||||
}
|
||||
|
||||
_setSpacePath(roomId) {
|
||||
if (roomId === null || roomId === cons.tabs.HOME) {
|
||||
_addToSpacePath(roomId, asRoot) {
|
||||
if (typeof roomId !== 'string') {
|
||||
this.selectedSpacePath = [cons.tabs.HOME];
|
||||
return;
|
||||
}
|
||||
if (asRoot) {
|
||||
this.selectedSpacePath = [roomId];
|
||||
return;
|
||||
}
|
||||
if (this.selectedSpacePath.includes(roomId)) {
|
||||
const spIndex = this.selectedSpacePath.indexOf(roomId);
|
||||
this.selectedSpacePath = this.selectedSpacePath.slice(0, spIndex + 1);
|
||||
@@ -30,6 +38,215 @@ class Navigation extends EventEmitter {
|
||||
this.selectedSpacePath.push(roomId);
|
||||
}
|
||||
|
||||
_mapRoomToSpace(roomId) {
|
||||
const { roomList } = this.initMatrix;
|
||||
if (
|
||||
this.selectedTab === cons.tabs.HOME
|
||||
&& roomList.rooms.has(roomId)
|
||||
&& !roomList.roomIdToParents.has(roomId)
|
||||
) {
|
||||
this.spaceToRoom.set(cons.tabs.HOME, {
|
||||
roomId,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (this.selectedTab === cons.tabs.DIRECTS && roomList.directs.has(roomId)) {
|
||||
this.spaceToRoom.set(cons.tabs.DIRECTS, {
|
||||
roomId,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const parents = roomList.roomIdToParents.get(roomId);
|
||||
if (!parents) return;
|
||||
|
||||
[...parents].forEach((pId) => {
|
||||
this.spaceToRoom.set(pId, {
|
||||
roomId,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
_selectRoom(roomId, eventId) {
|
||||
const prevSelectedRoomId = this.selectedRoomId;
|
||||
this.selectedRoomId = roomId;
|
||||
if (prevSelectedRoomId !== roomId) this._mapRoomToSpace(roomId);
|
||||
this.removeRecentRoom(prevSelectedRoomId);
|
||||
this.addRecentRoom(prevSelectedRoomId);
|
||||
this.removeRecentRoom(this.selectedRoomId);
|
||||
if (this.isRoomSettings && typeof this.selectedRoomId === 'string') {
|
||||
this.isRoomSettings = !this.isRoomSettings;
|
||||
this.emit(cons.events.navigation.ROOM_SETTINGS_TOGGLED, this.isRoomSettings);
|
||||
}
|
||||
this.emit(
|
||||
cons.events.navigation.ROOM_SELECTED,
|
||||
this.selectedRoomId,
|
||||
prevSelectedRoomId,
|
||||
eventId,
|
||||
);
|
||||
}
|
||||
|
||||
_selectTabWithRoom(roomId) {
|
||||
const { roomList, accountData } = this.initMatrix;
|
||||
const { categorizedSpaces } = accountData;
|
||||
|
||||
if (roomList.isOrphan(roomId)) {
|
||||
if (roomList.directs.has(roomId)) {
|
||||
this._selectSpace(null, true, false);
|
||||
this._selectTab(cons.tabs.DIRECTS, false);
|
||||
return;
|
||||
}
|
||||
this._selectSpace(null, true, false);
|
||||
this._selectTab(cons.tabs.HOME, false);
|
||||
return;
|
||||
}
|
||||
|
||||
const parents = roomList.roomIdToParents.get(roomId);
|
||||
|
||||
if (parents.has(this.selectedSpaceId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (categorizedSpaces.has(this.selectedSpaceId)) {
|
||||
const categories = roomList.getCategorizedSpaces([this.selectedSpaceId]);
|
||||
if ([...parents].find((pId) => categories.has(pId))) {
|
||||
// No need to select tab
|
||||
// As one of parent is child of selected categorized space.
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const spaceInPath = [...this.selectedSpacePath].reverse().find((sId) => parents.has(sId));
|
||||
if (spaceInPath) {
|
||||
this._selectSpace(spaceInPath, false, false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (roomList.directs.has(roomId)) {
|
||||
this._selectSpace(null, true, false);
|
||||
this._selectTab(cons.tabs.DIRECTS, false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (parents.size > 0) {
|
||||
const sortedParents = [...parents].sort((p1, p2) => {
|
||||
const t1 = this.spaceToRoom.get(p1)?.timestamp ?? 0;
|
||||
const t2 = this.spaceToRoom.get(p2)?.timestamp ?? 0;
|
||||
return t2 - t1;
|
||||
});
|
||||
this._selectSpace(sortedParents[0], true, false);
|
||||
this._selectTab(sortedParents[0], false);
|
||||
}
|
||||
}
|
||||
|
||||
_getLatestActiveRoomId(roomIds) {
|
||||
const mx = this.initMatrix.matrixClient;
|
||||
|
||||
let ts = 0;
|
||||
let roomId = null;
|
||||
roomIds.forEach((childId) => {
|
||||
const room = mx.getRoom(childId);
|
||||
if (!room) return;
|
||||
const newTs = room.getLastActiveTimestamp();
|
||||
if (newTs > ts) {
|
||||
ts = newTs;
|
||||
roomId = childId;
|
||||
}
|
||||
});
|
||||
return roomId;
|
||||
}
|
||||
|
||||
_getLatestSelectedRoomId(spaceIds) {
|
||||
let ts = 0;
|
||||
let roomId = null;
|
||||
|
||||
spaceIds.forEach((sId) => {
|
||||
const data = this.spaceToRoom.get(sId);
|
||||
if (!data) return;
|
||||
const newTs = data.timestamp;
|
||||
if (newTs > ts) {
|
||||
ts = newTs;
|
||||
roomId = data.roomId;
|
||||
}
|
||||
});
|
||||
return roomId;
|
||||
}
|
||||
|
||||
_selectTab(tabId, selectRoom = true) {
|
||||
this.selectedTab = tabId;
|
||||
if (selectRoom) this._selectRoomWithTab(this.selectedTab);
|
||||
this.emit(cons.events.navigation.TAB_SELECTED, this.selectedTab);
|
||||
}
|
||||
|
||||
_selectSpace(roomId, asRoot, selectRoom = true) {
|
||||
this._addToSpacePath(roomId, asRoot);
|
||||
this.selectedSpaceId = roomId;
|
||||
if (!asRoot && selectRoom) this._selectRoomWithSpace(this.selectedSpaceId);
|
||||
this.emit(cons.events.navigation.SPACE_SELECTED, this.selectedSpaceId);
|
||||
}
|
||||
|
||||
_selectRoomWithSpace(spaceId) {
|
||||
if (!spaceId) return;
|
||||
const { roomList, accountData, matrixClient } = this.initMatrix;
|
||||
const { categorizedSpaces } = accountData;
|
||||
|
||||
const data = this.spaceToRoom.get(spaceId);
|
||||
if (data && !categorizedSpaces.has(spaceId)) {
|
||||
this._selectRoom(data.roomId);
|
||||
return;
|
||||
}
|
||||
|
||||
const children = [];
|
||||
|
||||
if (categorizedSpaces.has(spaceId)) {
|
||||
const categories = roomList.getCategorizedSpaces([spaceId]);
|
||||
|
||||
const latestSelectedRoom = this._getLatestSelectedRoomId([...categories.keys()]);
|
||||
|
||||
if (latestSelectedRoom) {
|
||||
this._selectRoom(latestSelectedRoom);
|
||||
return;
|
||||
}
|
||||
|
||||
categories?.forEach((categoryId) => {
|
||||
categoryId?.forEach((childId) => {
|
||||
children.push(childId);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
roomList.getSpaceChildren(spaceId).forEach((id) => {
|
||||
if (matrixClient.getRoom(id)?.isSpaceRoom() === false) {
|
||||
children.push(id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!children) {
|
||||
this._selectRoom(null);
|
||||
return;
|
||||
}
|
||||
|
||||
this._selectRoom(this._getLatestActiveRoomId(children));
|
||||
}
|
||||
|
||||
_selectRoomWithTab(tabId) {
|
||||
const { roomList } = this.initMatrix;
|
||||
if (tabId === cons.tabs.HOME || tabId === cons.tabs.DIRECTS) {
|
||||
const data = this.spaceToRoom.get(tabId);
|
||||
if (data) {
|
||||
this._selectRoom(data.roomId);
|
||||
return;
|
||||
}
|
||||
const children = tabId === cons.tabs.HOME ? roomList.getOrphanRooms() : [...roomList.directs];
|
||||
this._selectRoom(this._getLatestActiveRoomId(children));
|
||||
return;
|
||||
}
|
||||
this._selectRoomWithSpace(tabId);
|
||||
}
|
||||
|
||||
removeRecentRoom(roomId) {
|
||||
if (typeof roomId !== 'string') return;
|
||||
const roomIdIndex = this.recentRooms.indexOf(roomId);
|
||||
@@ -59,40 +276,19 @@ class Navigation extends EventEmitter {
|
||||
navigate(action) {
|
||||
const actions = {
|
||||
[cons.actions.navigation.SELECT_TAB]: () => {
|
||||
this.selectedTab = action.tabId;
|
||||
if (this.selectedTab !== cons.tabs.DIRECTS) {
|
||||
if (this.selectedTab === cons.tabs.HOME) {
|
||||
this.selectedSpacePath = [cons.tabs.HOME];
|
||||
this.selectedSpaceId = null;
|
||||
} else {
|
||||
this.selectedSpacePath = [this.selectedTab];
|
||||
this.selectedSpaceId = this.selectedTab;
|
||||
}
|
||||
this.emit(cons.events.navigation.SPACE_SELECTED, this.selectedSpaceId);
|
||||
} else this.selectedSpaceId = null;
|
||||
this.emit(cons.events.navigation.TAB_SELECTED, this.selectedTab);
|
||||
const roomId = (
|
||||
action.tabId !== cons.tabs.HOME && action.tabId !== cons.tabs.DIRECTS
|
||||
) ? action.tabId : null;
|
||||
|
||||
this._selectSpace(roomId, true);
|
||||
this._selectTab(action.tabId);
|
||||
},
|
||||
[cons.actions.navigation.SELECT_SPACE]: () => {
|
||||
this._setSpacePath(action.roomId);
|
||||
this.selectedSpaceId = action.roomId === cons.tabs.HOME ? null : action.roomId;
|
||||
this.emit(cons.events.navigation.SPACE_SELECTED, this.selectedSpaceId);
|
||||
this._selectSpace(action.roomId, false);
|
||||
},
|
||||
[cons.actions.navigation.SELECT_ROOM]: () => {
|
||||
const prevSelectedRoomId = this.selectedRoomId;
|
||||
this.selectedRoomId = action.roomId;
|
||||
this.removeRecentRoom(prevSelectedRoomId);
|
||||
this.addRecentRoom(prevSelectedRoomId);
|
||||
this.removeRecentRoom(this.selectedRoomId);
|
||||
if (this.isRoomSettings && typeof this.selectedRoomId === 'string') {
|
||||
this.isRoomSettings = !this.isRoomSettings;
|
||||
this.emit(cons.events.navigation.ROOM_SETTINGS_TOGGLED, this.isRoomSettings);
|
||||
}
|
||||
this.emit(
|
||||
cons.events.navigation.ROOM_SELECTED,
|
||||
this.selectedRoomId,
|
||||
prevSelectedRoomId,
|
||||
action.eventId,
|
||||
);
|
||||
if (action.roomId) this._selectTabWithRoom(action.roomId);
|
||||
this._selectRoom(action.roomId, action.eventId);
|
||||
},
|
||||
[cons.actions.navigation.OPEN_SPACE_SETTINGS]: () => {
|
||||
this.emit(cons.events.navigation.SPACE_SETTINGS_OPENED, action.roomId, action.tabText);
|
||||
|
||||
+3
-2
@@ -105,7 +105,7 @@
|
||||
|
||||
/* shadow and overlay */
|
||||
--bg-overlay: rgba(0, 0, 0, 20%);
|
||||
--bg-overlay-low: rgba(0, 0, 0, 85%);
|
||||
--bg-overlay-low: rgba(0, 0, 0, 50%);
|
||||
|
||||
--bs-popup: 0 0 16px rgba(0, 0, 0, 10%);
|
||||
|
||||
@@ -264,7 +264,8 @@
|
||||
}
|
||||
|
||||
/* shadow and overlay */
|
||||
--bg-overlay: rgba(0, 0, 0, 50%);
|
||||
--bg-overlay: rgba(0, 0, 0, 60%);
|
||||
--bg-overlay-low: rgba(0, 0, 0, 80%);
|
||||
|
||||
--bs-popup: 0 0 16px rgba(0, 0, 0, 25%);
|
||||
|
||||
|
||||
@@ -166,6 +166,9 @@ export function scaleDownImage(imageFile, width, height) {
|
||||
img.onload = () => {
|
||||
let newWidth = img.width;
|
||||
let newHeight = img.height;
|
||||
if (newHeight <= height && newWidth <= width) {
|
||||
resolve(imageFile);
|
||||
}
|
||||
|
||||
if (newHeight > height) {
|
||||
newWidth = Math.floor(newWidth * (height / newHeight));
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
// https://github.com/matrix-org/matrix-react-sdk/blob/cd15e08fc285da42134817cce50de8011809cd53/src/utils/blobs.ts
|
||||
export const ALLOWED_BLOB_MIMETYPES = [
|
||||
'image/jpeg',
|
||||
'image/gif',
|
||||
'image/png',
|
||||
'image/apng',
|
||||
'image/webp',
|
||||
'image/avif',
|
||||
|
||||
'video/mp4',
|
||||
'video/webm',
|
||||
'video/ogg',
|
||||
'video/quicktime',
|
||||
|
||||
'audio/mp4',
|
||||
'audio/webm',
|
||||
'audio/aac',
|
||||
'audio/mpeg',
|
||||
'audio/ogg',
|
||||
'audio/wave',
|
||||
'audio/wav',
|
||||
'audio/x-wav',
|
||||
'audio/x-pn-wav',
|
||||
'audio/flac',
|
||||
'audio/x-flac',
|
||||
];
|
||||
|
||||
export function getBlobSafeMimeType(mimetype) {
|
||||
if (!ALLOWED_BLOB_MIMETYPES.includes(mimetype)) {
|
||||
return 'application/octet-stream';
|
||||
}
|
||||
// Required for Chromium browsers
|
||||
if (mimetype === 'video/quicktime') {
|
||||
return 'video/mp4';
|
||||
}
|
||||
return mimetype;
|
||||
}
|
||||
+16
-5
@@ -1,7 +1,7 @@
|
||||
import sanitizeHtml from 'sanitize-html';
|
||||
import initMatrix from '../client/initMatrix';
|
||||
|
||||
const MAX_TAG_NESTING = 100;
|
||||
let mx = null;
|
||||
|
||||
const permittedHtmlTags = [
|
||||
'font', 'del', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
|
||||
@@ -54,7 +54,7 @@ function transformATag(tagName, attribs) {
|
||||
'data-mx-pill': userId,
|
||||
},
|
||||
};
|
||||
if (userId === initMatrix.matrixClient.getUserId()) {
|
||||
if (userId === mx?.getUserId()) {
|
||||
pill.attribs['data-mx-ping'] = undefined;
|
||||
}
|
||||
return pill;
|
||||
@@ -76,17 +76,28 @@ function transformATag(tagName, attribs) {
|
||||
|
||||
function transformImgTag(tagName, attribs) {
|
||||
const { src } = attribs;
|
||||
const mx = initMatrix.matrixClient;
|
||||
if (src.startsWith('mxc://') === false) {
|
||||
return {
|
||||
tagName: 'a',
|
||||
attribs: {
|
||||
href: src,
|
||||
rel: 'noopener',
|
||||
target: '_blank',
|
||||
},
|
||||
text: attribs.alt || src,
|
||||
};
|
||||
}
|
||||
return {
|
||||
tagName,
|
||||
attribs: {
|
||||
...attribs,
|
||||
src: src.startsWith('mxc://') ? mx.mxcUrlToHttp(src) : src,
|
||||
src: mx?.mxcUrlToHttp(src),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function sanitizeCustomHtml(body) {
|
||||
export function sanitizeCustomHtml(matrixClient, body) {
|
||||
mx = matrixClient;
|
||||
return sanitizeHtml(body, {
|
||||
allowedTags: permittedHtmlTags,
|
||||
allowedAttributes: permittedTagToAttributes,
|
||||
|
||||
Reference in new issue
Block a user