commit
bdb94d7145
14 changed files with 121 additions and 29 deletions
|
@ -14,6 +14,11 @@ Cinny is a [Matrix](https://matrix.org) client focusing primarily on simple, ele
|
|||
|
||||
## Building and Running
|
||||
|
||||
### Running pre-compiled
|
||||
|
||||
A tarball of pre-compiled version of the app is provided with each [release](https://github.com/ajbura/cinny/releases).
|
||||
You can serve the application with a webserver of your choosing by simply copying `dist/` directory to the webroot.
|
||||
|
||||
### Building from source
|
||||
|
||||
Execute the following commands to compile the app from its source code:
|
||||
|
@ -44,6 +49,8 @@ docker run -p 8080:80 cinny:latest
|
|||
This will forward your `localhost` port 8080 to the container's port 80. You can visit the app in your browser by
|
||||
navigating to `http://localhost:8080`.
|
||||
|
||||
Alternatively you can just pull the [DockerHub image](https://hub.docker.com/r/ajbura/cinny) by `docker pull ajbura/cinny`.
|
||||
|
||||
## License
|
||||
|
||||
Copyright (c) 2021 Ajay Bura (ajbura) and other contributors
|
||||
|
|
2
package-lock.json
generated
2
package-lock.json
generated
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "cinny",
|
||||
"version": "1.3.1",
|
||||
"version": "1.3.2",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "cinny",
|
||||
"version": "1.3.1",
|
||||
"version": "1.3.2",
|
||||
"description": "Yet another matrix client",
|
||||
"main": "index.js",
|
||||
"engines": {
|
||||
|
|
34
src/app/atoms/chip/Chip.jsx
Normal file
34
src/app/atoms/chip/Chip.jsx
Normal file
|
@ -0,0 +1,34 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import './Chip.scss';
|
||||
|
||||
import Text from '../text/Text';
|
||||
import RawIcon from '../system-icons/RawIcon';
|
||||
|
||||
function Chip({
|
||||
iconSrc, iconColor, text, children,
|
||||
}) {
|
||||
return (
|
||||
<div className="chip">
|
||||
{iconSrc != null && <RawIcon src={iconSrc} color={iconColor} size="small" />}
|
||||
{(text != null && text !== '') && <Text variant="b2">{text}</Text>}
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Chip.propTypes = {
|
||||
iconSrc: PropTypes.string,
|
||||
iconColor: PropTypes.string,
|
||||
text: PropTypes.string,
|
||||
children: PropTypes.element,
|
||||
};
|
||||
|
||||
Chip.defaultProps = {
|
||||
iconSrc: null,
|
||||
iconColor: null,
|
||||
text: null,
|
||||
children: null,
|
||||
};
|
||||
|
||||
export default Chip;
|
19
src/app/atoms/chip/Chip.scss
Normal file
19
src/app/atoms/chip/Chip.scss
Normal file
|
@ -0,0 +1,19 @@
|
|||
.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);
|
||||
border: 1px solid var(--bg-surface-border);
|
||||
|
||||
& > .ic-raw {
|
||||
margin-right: var(--sp-extra-tight);
|
||||
[dir=rtl] & {
|
||||
margin-right: 0;
|
||||
margin-left: var(--sp-extra-tight);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -31,6 +31,7 @@ function ContextMenu({
|
|||
interactive
|
||||
arrow={false}
|
||||
maxWidth={maxWidth}
|
||||
duration={200}
|
||||
>
|
||||
{render(isVisible ? hideMenu : showMenu)}
|
||||
</Tippy>
|
||||
|
|
|
@ -139,14 +139,20 @@ function PublicRooms({ isOpen, searchTerm, onRequestClose }) {
|
|||
updateIsViewMore(false);
|
||||
if (totalRooms.length === 0) {
|
||||
updateSearchQuery({
|
||||
error: `No result found for "${inputRoomName}" on ${inputHs}`,
|
||||
error: inputRoomName === ''
|
||||
? `No public rooms on ${inputHs}`
|
||||
: `No result found for "${inputRoomName}" on ${inputHs}`,
|
||||
alias: isInputAlias ? inputRoomName : null,
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
updatePublicRooms([]);
|
||||
let err = 'Something went wrong!';
|
||||
if (e?.httpStatus >= 400 && e?.httpStatus < 500) {
|
||||
err = e.message;
|
||||
}
|
||||
updateSearchQuery({
|
||||
error: 'Something went wrong!',
|
||||
error: err,
|
||||
alias: isInputAlias ? inputRoomName : null,
|
||||
});
|
||||
updateIsSearching(false);
|
||||
|
|
|
@ -327,7 +327,15 @@ function RoomViewInput({
|
|||
if (file !== null) roomsInput.setAttachment(roomId, file);
|
||||
}
|
||||
|
||||
const myPowerlevel = roomTimeline.room.getMember(mx.getUserId()).powerLevel;
|
||||
const canISend = roomTimeline.room.currentState.hasSufficientPowerLevelFor('events_default', myPowerlevel);
|
||||
|
||||
function renderInputs() {
|
||||
if (!canISend) {
|
||||
return (
|
||||
<Text className="room-input__disallowed">You do not have permission to post to this room</Text>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<div className={`room-input__option-container${attachment === null ? '' : ' room-attachment__option'}`}>
|
||||
|
|
|
@ -3,6 +3,11 @@
|
|||
display: flex;
|
||||
min-height: 48px;
|
||||
|
||||
&__disallowed {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&__space {
|
||||
min-width: 0;
|
||||
align-self: center;
|
||||
|
|
|
@ -104,7 +104,7 @@ function AboutSection() {
|
|||
<div>
|
||||
<Text variant="h2">
|
||||
Cinny
|
||||
<span className="text text-b3" style={{ margin: '0 var(--sp-extra-tight)' }}>v1.3.1</span>
|
||||
<span className="text text-b3" style={{ margin: '0 var(--sp-extra-tight)' }}>v1.3.2</span>
|
||||
</Text>
|
||||
<Text>Yet another matrix client</Text>
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@ import {
|
|||
BrowserRouter, Switch, Route, Redirect,
|
||||
} from 'react-router-dom';
|
||||
|
||||
import { isAuthanticated } from '../../client/state/auth';
|
||||
import { isAuthenticated } from '../../client/state/auth';
|
||||
|
||||
import Auth from '../templates/auth/Auth';
|
||||
import Client from '../templates/client/Client';
|
||||
|
@ -13,13 +13,13 @@ function App() {
|
|||
<BrowserRouter>
|
||||
<Switch>
|
||||
<Route exact path="/">
|
||||
{ isAuthanticated() ? <Client /> : <Redirect to="/login" />}
|
||||
{ isAuthenticated() ? <Client /> : <Redirect to="/login" />}
|
||||
</Route>
|
||||
<Route path="/login">
|
||||
{ isAuthanticated() ? <Redirect to="/" /> : <Auth type="login" />}
|
||||
{ isAuthenticated() ? <Redirect to="/" /> : <Auth type="login" />}
|
||||
</Route>
|
||||
<Route path="/register">
|
||||
{ isAuthanticated() ? <Redirect to="/" /> : <Auth type="register" />}
|
||||
{ isAuthenticated() ? <Redirect to="/" /> : <Auth type="register" />}
|
||||
</Route>
|
||||
</Switch>
|
||||
</BrowserRouter>
|
||||
|
|
|
@ -32,6 +32,7 @@ const EMAIL_REGEX = /([a-z0-9]+[_a-z0-9.-][a-z0-9]+)@([a-z0-9-]+(?:.[a-z0-9-]+).
|
|||
const BAD_EMAIL_ERROR = 'Invalid email address';
|
||||
|
||||
function isValidInput(value, regex) {
|
||||
if (typeof regex === 'string') return regex === value;
|
||||
return regex.test(value);
|
||||
}
|
||||
function renderErrorMessage(error) {
|
||||
|
@ -39,24 +40,25 @@ function renderErrorMessage(error) {
|
|||
$error.textContent = error;
|
||||
$error.style.display = 'block';
|
||||
}
|
||||
function showBadInputError($input, error) {
|
||||
function showBadInputError($input, error, stopAutoFocus) {
|
||||
renderErrorMessage(error);
|
||||
$input.focus();
|
||||
if (!stopAutoFocus) $input.focus();
|
||||
const myInput = $input;
|
||||
myInput.style.border = '1px solid var(--bg-danger)';
|
||||
myInput.style.boxShadow = 'none';
|
||||
document.getElementById('auth_submit-btn').disabled = true;
|
||||
}
|
||||
|
||||
function validateOnChange(e, regex, error) {
|
||||
if (!isValidInput(e.target.value, regex) && e.target.value) {
|
||||
showBadInputError(e.target, error);
|
||||
return;
|
||||
function validateOnChange(targetInput, regex, error, stopAutoFocus) {
|
||||
if (!isValidInput(targetInput.value, regex) && targetInput.value) {
|
||||
showBadInputError(targetInput, error, stopAutoFocus);
|
||||
return false;
|
||||
}
|
||||
document.getElementById('auth_error').style.display = 'none';
|
||||
e.target.style.removeProperty('border');
|
||||
e.target.style.removeProperty('box-shadow');
|
||||
targetInput.style.removeProperty('border');
|
||||
targetInput.style.removeProperty('box-shadow');
|
||||
document.getElementById('auth_submit-btn').disabled = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -195,8 +197,8 @@ function Auth({ type }) {
|
|||
<Input
|
||||
forwardRef={usernameRef}
|
||||
onChange={(e) => (type === 'login'
|
||||
? validateOnChange(e, LOCALPART_LOGIN_REGEX, BAD_LOCALPART_ERROR)
|
||||
: validateOnChange(e, LOCALPART_SIGNUP_REGEX, BAD_LOCALPART_ERROR))}
|
||||
? validateOnChange(e.target, LOCALPART_LOGIN_REGEX, BAD_LOCALPART_ERROR)
|
||||
: validateOnChange(e.target, LOCALPART_SIGNUP_REGEX, BAD_LOCALPART_ERROR))}
|
||||
id="auth_username"
|
||||
label="Username"
|
||||
required
|
||||
|
@ -212,7 +214,15 @@ function Auth({ type }) {
|
|||
<div className="password__wrapper">
|
||||
<Input
|
||||
forwardRef={passwordRef}
|
||||
onChange={(e) => validateOnChange(e, ((type === 'login') ? PASSWORD_REGEX : PASSWORD_STRENGHT_REGEX), BAD_PASSWORD_ERROR)}
|
||||
onChange={(e) => {
|
||||
const isValidPass = validateOnChange(e.target, ((type === 'login') ? PASSWORD_REGEX : PASSWORD_STRENGHT_REGEX), BAD_PASSWORD_ERROR);
|
||||
if (type === 'register' && isValidPass) {
|
||||
validateOnChange(
|
||||
confirmPasswordRef.current, passwordRef.current.value,
|
||||
CONFIRM_PASSWORD_ERROR, true,
|
||||
);
|
||||
}
|
||||
}}
|
||||
id="auth_password"
|
||||
type="password"
|
||||
label="Password"
|
||||
|
@ -233,7 +243,9 @@ function Auth({ type }) {
|
|||
<div className="password__wrapper">
|
||||
<Input
|
||||
forwardRef={confirmPasswordRef}
|
||||
onChange={(e) => validateOnChange(e, new RegExp(`^(${passwordRef.current.value})$`), CONFIRM_PASSWORD_ERROR)}
|
||||
onChange={(e) => {
|
||||
validateOnChange(e.target, passwordRef.current.value, CONFIRM_PASSWORD_ERROR);
|
||||
}}
|
||||
id="auth_confirmPassword"
|
||||
type="password"
|
||||
label="Confirm password"
|
||||
|
@ -251,7 +263,7 @@ function Auth({ type }) {
|
|||
</div>
|
||||
<Input
|
||||
forwardRef={emailRef}
|
||||
onChange={(e) => validateOnChange(e, EMAIL_REGEX, BAD_EMAIL_ERROR)}
|
||||
onChange={(e) => validateOnChange(e.target, EMAIL_REGEX, BAD_EMAIL_ERROR)}
|
||||
id="auth_email"
|
||||
type="email"
|
||||
label="Email"
|
||||
|
|
|
@ -4,7 +4,7 @@ function getSecret(key) {
|
|||
return localStorage.getItem(key);
|
||||
}
|
||||
|
||||
const isAuthanticated = () => getSecret(cons.secretKey.ACCESS_TOKEN) !== null;
|
||||
const isAuthenticated = () => getSecret(cons.secretKey.ACCESS_TOKEN) !== null;
|
||||
|
||||
const secret = {
|
||||
accessToken: getSecret(cons.secretKey.ACCESS_TOKEN),
|
||||
|
@ -14,6 +14,6 @@ const secret = {
|
|||
};
|
||||
|
||||
export {
|
||||
isAuthanticated,
|
||||
isAuthenticated,
|
||||
secret,
|
||||
};
|
||||
|
|
|
@ -168,10 +168,10 @@
|
|||
.dark-theme,
|
||||
.butter-theme {
|
||||
/* background color | --bg-[background type]: value */
|
||||
--bg-surface: hsl(208, 8%, 16%);
|
||||
--bg-surface-transparent: hsla(208, 8%, 16%, 0);
|
||||
--bg-surface-low: hsl(208, 8%, 12%);
|
||||
--bg-surface-low-transparent: hsla(208, 8%, 12%, 0);
|
||||
--bg-surface: hsl(208, 8%, 20%);
|
||||
--bg-surface-transparent: hsla(208, 8%, 20%, 0);
|
||||
--bg-surface-low: hsl(208, 8%, 16%);
|
||||
--bg-surface-low-transparent: hsla(208, 8%, 16%, 0);
|
||||
--bg-surface-hover: rgba(255, 255, 255, 3%);
|
||||
--bg-surface-active: rgba(255, 255, 255, 5%);
|
||||
--bg-surface-border: rgba(0, 0, 0, 20%);
|
||||
|
|
Loading…
Reference in a new issue