initial commit

This commit is contained in:
unknown 2021-07-28 18:45:52 +05:30
commit 026f835a87
176 changed files with 10613 additions and 0 deletions

View file

@ -0,0 +1,46 @@
import React from 'react';
import PropTypes from 'prop-types';
import './ChannelIntro.scss';
import Linkify from 'linkifyjs/react';
import colorMXID from '../../../util/colorMXID';
import Text from '../../atoms/text/Text';
import Avatar from '../../atoms/avatar/Avatar';
function linkifyContent(content) {
return <Linkify options={{ target: { url: '_blank' } }}>{content}</Linkify>;
}
function ChannelIntro({
avatarSrc, name, heading, desc, time,
}) {
return (
<div className="channel-intro">
<Avatar imageSrc={avatarSrc} text={name.slice(0, 1)} bgColor={colorMXID(name)} size="large" />
<div className="channel-intro__content">
<Text className="channel-intro__name" variant="h1">{heading}</Text>
<Text className="channel-intro__desc" variant="b1">{linkifyContent(desc)}</Text>
{ time !== null && <Text className="channel-intro__time" variant="b3">{time}</Text>}
</div>
</div>
);
}
ChannelIntro.defaultProps = {
avatarSrc: false,
time: null,
};
ChannelIntro.propTypes = {
avatarSrc: PropTypes.oneOfType([
PropTypes.string,
PropTypes.bool,
]),
name: PropTypes.string.isRequired,
heading: PropTypes.string.isRequired,
desc: PropTypes.string.isRequired,
time: PropTypes.string,
};
export default ChannelIntro;

View file

@ -0,0 +1,31 @@
.channel-intro {
margin-top: calc(2 * var(--sp-extra-loose));
margin-bottom: var(--sp-extra-loose);
padding-left: calc(var(--sp-normal) + var(--av-small) + var(--sp-tight));
padding-right: var(--sp-extra-tight);
[dir=rtl] & {
padding: {
left: var(--sp-extra-tight);
right: calc(var(--sp-normal) + var(--av-small) + var(--sp-tight));
}
}
.channel-intro__content {
margin-top: var(--sp-extra-loose);
max-width: 640px;
}
&__name {
color: var(--tc-surface-high);
}
&__desc {
color: var(--tc-surface-normal);
margin: var(--sp-tight) 0 var(--sp-extra-tight);
& a {
word-break: break-all;
}
}
&__time {
color: var(--tc-surface-low);
}
}

View file

@ -0,0 +1,73 @@
import React from 'react';
import PropTypes from 'prop-types';
import './ChannelSelector.scss';
import colorMXID from '../../../util/colorMXID';
import Text from '../../atoms/text/Text';
import Avatar from '../../atoms/avatar/Avatar';
import NotificationBadge from '../../atoms/badge/NotificationBadge';
import { blurOnBubbling } from '../../atoms/button/script';
function ChannelSelector({
selected, unread, notificationCount, alert,
iconSrc, imageSrc, roomId, onClick, children,
}) {
return (
<button
className={`channel-selector__button-wrapper${selected ? ' channel-selector--selected' : ''}`}
type="button"
onClick={onClick}
onMouseUp={(e) => blurOnBubbling(e, '.channel-selector__button-wrapper')}
>
<div className="channel-selector">
<div className="channel-selector__icon flex--center">
<Avatar
text={children.slice(0, 1)}
bgColor={colorMXID(roomId)}
imageSrc={imageSrc}
iconSrc={iconSrc}
size="extra-small"
/>
</div>
<div className="channel-selector__text-container">
<Text variant="b1">{children}</Text>
</div>
<div className="channel-selector__badge-container">
{
notificationCount !== 0
? unread && (
<NotificationBadge alert={alert}>
{notificationCount}
</NotificationBadge>
)
: unread && <div className="channel-selector--unread" />
}
</div>
</div>
</button>
);
}
ChannelSelector.defaultProps = {
selected: false,
unread: false,
notificationCount: 0,
alert: false,
iconSrc: null,
imageSrc: null,
};
ChannelSelector.propTypes = {
selected: PropTypes.bool,
unread: PropTypes.bool,
notificationCount: PropTypes.number,
alert: PropTypes.bool,
iconSrc: PropTypes.string,
imageSrc: PropTypes.string,
roomId: PropTypes.string.isRequired,
onClick: PropTypes.func.isRequired,
children: PropTypes.string.isRequired,
};
export default ChannelSelector;

View file

@ -0,0 +1,66 @@
.channel-selector__button-wrapper {
display: block;
width: calc(100% - var(--sp-extra-tight));
margin-left: auto;
padding: var(--sp-extra-tight) var(--sp-extra-tight);
border: 1px solid transparent;
border-radius: var(--bo-radius);
cursor: pointer;
[dir=rtl] & {
margin: {
left: 0;
right: auto;
}
}
@media (hover: hover) {
&:hover {
background-color: var(--bg-surface-hover);
}
}
&:focus {
outline: none;
background-color: var(--bg-surface-hover);
}
&:active {
background-color: var(--bg-surface-active);
}
}
.channel-selector {
display: flex;
align-items: center;
&__icon {
width: 24px;
height: 24px;
.avatar__border {
box-shadow: none;
}
}
&__text-container {
flex: 1;
min-width: 0;
margin: 0 var(--sp-extra-tight);
& .text {
color: var(--tc-surface-normal);
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
}
}
.channel-selector--unread {
margin: 0 var(--sp-ultra-tight);
height: 8px;
width: 8px;
background-color: var(--tc-surface-low);
border-radius: 50%;
opacity: .4;
}
.channel-selector--selected {
background-color: var(--bg-surface);
border-color: var(--bg-surface-border);
}

View file

@ -0,0 +1,72 @@
import React from 'react';
import PropTypes from 'prop-types';
import './ChannelTile.scss';
import Linkify from 'linkifyjs/react';
import colorMXID from '../../../util/colorMXID';
import Text from '../../atoms/text/Text';
import Avatar from '../../atoms/avatar/Avatar';
function linkifyContent(content) {
return <Linkify options={{ target: { url: '_blank' } }}>{content}</Linkify>;
}
function ChannelTile({
avatarSrc, name, id,
inviterName, memberCount, desc, options,
}) {
return (
<div className="channel-tile">
<div className="channel-tile__avatar">
<Avatar
imageSrc={avatarSrc}
bgColor={colorMXID(id)}
text={name.slice(0, 1)}
/>
</div>
<div className="channel-tile__content">
<Text variant="s1">{name}</Text>
<Text variant="b3">
{
inviterName !== null
? `Invited by ${inviterName} to ${id}${memberCount === null ? '' : `${memberCount} members`}`
: id + (memberCount === null ? '' : `${memberCount} members`)
}
</Text>
{
desc !== null && (typeof desc === 'string')
? <Text className="channel-tile__content__desc" variant="b2">{linkifyContent(desc)}</Text>
: desc
}
</div>
{ options !== null && (
<div className="channel-tile__options">
{options}
</div>
)}
</div>
);
}
ChannelTile.defaultProps = {
avatarSrc: null,
inviterName: null,
options: null,
desc: null,
memberCount: null,
};
ChannelTile.propTypes = {
avatarSrc: PropTypes.string,
name: PropTypes.string.isRequired,
id: PropTypes.string.isRequired,
inviterName: PropTypes.string,
memberCount: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
]),
desc: PropTypes.node,
options: PropTypes.node,
};
export default ChannelTile;

View file

@ -0,0 +1,21 @@
.channel-tile {
display: flex;
&__content {
flex: 1;
min-width: 0;
margin: 0 var(--sp-normal);
&__desc {
white-space: pre-wrap;
& a {
white-space: wrap;
}
}
& .text:not(:first-child) {
margin-top: var(--sp-ultra-tight);
}
}
}

View file

@ -0,0 +1,307 @@
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import './Media.scss';
import encrypt from 'browser-encrypt-attachment';
import Text from '../../atoms/text/Text';
import IconButton from '../../atoms/button/IconButton';
import Spinner from '../../atoms/spinner/Spinner';
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/a9e28db33058d1893d964ec96cd247ecc3d92fc3/src/utils/blobs.ts#L73
const ALLOWED_BLOB_MIMETYPES = [
'image/jpeg',
'image/gif',
'image/png',
'video/mp4',
'video/webm',
'video/ogg',
'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';
}
return mimetype;
}
async function getDecryptedBlob(response, type, decryptData) {
const arrayBuffer = await response.arrayBuffer();
const dataArray = await encrypt.decryptAttachment(arrayBuffer, decryptData);
const blob = new Blob([dataArray], { type: getBlobSafeMimeType(type) });
return blob;
}
async function getUrl(link, type, decryptData) {
try {
const response = await fetch(link, { method: 'GET' });
if (decryptData !== null) {
return URL.createObjectURL(await getDecryptedBlob(response, type, decryptData));
}
const blob = await response.blob();
return URL.createObjectURL(blob);
} catch (e) {
return link;
}
}
function getNativeHeight(width, height) {
const MEDIA_MAX_WIDTH = 296;
const scale = MEDIA_MAX_WIDTH / width;
return scale * height;
}
function FileHeader({
name, link, external,
file, type,
}) {
const [url, setUrl] = useState(null);
async function getFile() {
const myUrl = await getUrl(link, type, file);
setUrl(myUrl);
}
async function handleDownload(e) {
if (file !== null && url === null) {
e.preventDefault();
await getFile();
e.target.click();
}
}
return (
<div className="file-header">
<Text className="file-name" variant="b3">{name}</Text>
{ link !== null && (
<>
{
external && (
<IconButton
size="extra-small"
tooltip="Open in new tab"
src={ExternalSVG}
onClick={() => window.open(url || link)}
/>
)
}
<a href={url || link} download={name} target="_blank" rel="noreferrer">
<IconButton
size="extra-small"
tooltip="Download"
src={DownloadSVG}
onClick={handleDownload}
/>
</a>
</>
)}
</div>
);
}
FileHeader.defaultProps = {
external: false,
file: null,
link: null,
};
FileHeader.propTypes = {
name: PropTypes.string.isRequired,
link: PropTypes.string,
external: PropTypes.bool,
file: PropTypes.shape({}),
type: PropTypes.string.isRequired,
};
function File({
name, link, file, type,
}) {
return (
<div className="file-container">
<FileHeader name={name} link={link} file={file} type={type} />
</div>
);
}
File.defaultProps = {
file: null,
};
File.propTypes = {
name: PropTypes.string.isRequired,
link: PropTypes.string.isRequired,
type: PropTypes.string.isRequired,
file: PropTypes.shape({}),
};
function Image({
name, width, height, link, file, type,
}) {
const [url, setUrl] = useState(null);
useEffect(() => {
let unmounted = false;
async function fetchUrl() {
const myUrl = await getUrl(link, type, file);
if (unmounted) return;
setUrl(myUrl);
}
fetchUrl();
return () => {
unmounted = true;
};
}, []);
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">
{ url !== null && <img src={url || link} alt={name} />}
</div>
</div>
);
}
Image.defaultProps = {
file: null,
width: null,
height: null,
};
Image.propTypes = {
name: PropTypes.string.isRequired,
width: PropTypes.number,
height: PropTypes.number,
link: PropTypes.string.isRequired,
file: PropTypes.shape({}),
type: PropTypes.string.isRequired,
};
function Audio({
name, link, type, file,
}) {
const [isLoading, setIsLoading] = useState(false);
const [url, setUrl] = useState(null);
async function loadAudio() {
const myUrl = await getUrl(link, type, file);
setUrl(myUrl);
setIsLoading(false);
}
function handlePlayAudio() {
setIsLoading(true);
loadAudio();
}
return (
<div className="file-container">
<FileHeader name={name} link={file !== null ? url : url || link} type={type} external />
<div className="audio-container">
{ url === null && isLoading && <Spinner size="small" /> }
{ url === null && !isLoading && <IconButton onClick={handlePlayAudio} tooltip="Play audio" src={PlaySVG} />}
{ url !== null && (
/* eslint-disable-next-line jsx-a11y/media-has-caption */
<audio autoPlay controls>
<source src={url} type={getBlobSafeMimeType(type)} />
</audio>
)}
</div>
</div>
);
}
Audio.defaultProps = {
file: null,
};
Audio.propTypes = {
name: PropTypes.string.isRequired,
link: PropTypes.string.isRequired,
type: PropTypes.string.isRequired,
file: PropTypes.shape({}),
};
function Video({
name, link, thumbnail,
width, height, file, type, thumbnailFile, thumbnailType,
}) {
const [isLoading, setIsLoading] = useState(false);
const [url, setUrl] = useState(null);
const [thumbUrl, setThumbUrl] = useState(null);
useEffect(() => {
let unmounted = false;
async function fetchUrl() {
const myThumbUrl = await getUrl(thumbnail, thumbnailType, thumbnailFile);
if (unmounted) return;
setThumbUrl(myThumbUrl);
}
if (thumbnail !== null) fetchUrl();
return () => {
unmounted = true;
};
}, []);
async function loadVideo() {
const myUrl = await getUrl(link, type, file);
setUrl(myUrl);
setIsLoading(false);
}
function handlePlayVideo() {
setIsLoading(true);
loadVideo();
}
return (
<div className="file-container">
<FileHeader name={name} link={file !== null ? url : url || link} type={type} external />
<div
style={{
height: width !== null ? getNativeHeight(width, height) : 'unset',
backgroundImage: thumbUrl === null ? 'none' : `url(${thumbUrl}`,
}}
className="video-container"
>
{ 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 */
<video autoPlay controls poster={thumbUrl}>
<source src={url} type={getBlobSafeMimeType(type)} />
</video>
)}
</div>
</div>
);
}
Video.defaultProps = {
width: null,
height: null,
file: null,
thumbnail: null,
thumbnailType: null,
thumbnailFile: null,
};
Video.propTypes = {
name: PropTypes.string.isRequired,
link: PropTypes.string.isRequired,
thumbnail: PropTypes.string,
width: PropTypes.number,
height: PropTypes.number,
file: PropTypes.shape({}),
type: PropTypes.string.isRequired,
thumbnailFile: PropTypes.shape({}),
thumbnailType: PropTypes.string,
};
export {
File, Image, Audio, Video,
};

View file

@ -0,0 +1,62 @@
.file-header {
display: flex;
align-items: center;
padding: var(--sp-ultra-tight) var(--sp-tight);
min-height: 42px;
& .file-name {
flex: 1;
color: var(--tc-surface-low);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
.file-container {
--media-max-width: 296px;
background-color: var(--bg-surface-hover);
border-radius: calc(var(--bo-radius) / 2);
overflow: hidden;
max-width: var(--media-max-width);
white-space: initial;
}
.image-container,
.video-container,
.audio-container {
font-size: 0;
line-height: 0;
display: flex;
justify-content: center;
align-items: center;
background-position: center;
background-repeat: no-repeat;
background-size: cover;
}
.image-container {
& img {
max-width: unset !important;
width: 100% !important;
border-radius: 0 !important;
margin: 0 !important;
}
}
.video-container {
& .ic-btn-surface {
background-color: var(--bg-surface-low);
}
video {
width: 100%
}
}
.audio-container {
audio {
width: 100%
}
}

View file

@ -0,0 +1,149 @@
import React from 'react';
import PropTypes from 'prop-types';
import './Message.scss';
import Linkify from 'linkifyjs/react';
import ReactMarkdown from 'react-markdown';
import gfm from 'remark-gfm';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { coy } from 'react-syntax-highlighter/dist/esm/styles/prism';
import Text from '../../atoms/text/Text';
import RawIcon from '../../atoms/system-icons/RawIcon';
import Avatar from '../../atoms/avatar/Avatar';
import ReplyArrowIC from '../../../../public/res/ic/outlined/reply-arrow.svg';
const components = {
code({
// eslint-disable-next-line react/prop-types
inline, className, children,
}) {
const match = /language-(\w+)/.exec(className || '');
return !inline && match ? (
<SyntaxHighlighter
style={coy}
language={match[1]}
PreTag="div"
showLineNumbers
>
{String(children).replace(/\n$/, '')}
</SyntaxHighlighter>
) : (
<code className={className}>{String(children)}</code>
);
},
};
function linkifyContent(content) {
return <Linkify options={{ target: { url: '_blank' } }}>{content}</Linkify>;
}
function genMarkdown(content) {
return <ReactMarkdown remarkPlugins={[gfm]} components={components} linkTarget="_blank">{content}</ReactMarkdown>;
}
function PlaceholderMessage() {
return (
<div className="ph-msg">
<div className="ph-msg__avatar-container">
<div className="ph-msg__avatar" />
</div>
<div className="ph-msg__main-container">
<div className="ph-msg__header" />
<div className="ph-msg__content">
<div />
<div />
<div />
<div />
</div>
</div>
</div>
);
}
function Message({
color, avatarSrc, name, content,
time, markdown, contentOnly, reply,
edited, reactions,
}) {
const msgClass = contentOnly ? 'message--content-only' : 'message--full';
return (
<div className={`message ${msgClass}`}>
<div className="message__avatar-container">
{!contentOnly && <Avatar imageSrc={avatarSrc} text={name.slice(0, 1)} bgColor={color} size="small" />}
</div>
<div className="message__main-container">
{ !contentOnly && (
<div className="message__header">
<div style={{ color }} className="message__profile">
<Text variant="b1">{name}</Text>
</div>
<div className="message__time">
<Text variant="b3">{time}</Text>
</div>
</div>
)}
<div className="message__content">
{ reply !== null && (
<div className="message__reply-content">
<Text variant="b2">
<RawIcon color={reply.color} size="extra-small" src={ReplyArrowIC} />
<span style={{ color: reply.color }}>{reply.to}</span>
<>{` ${reply.content}`}</>
</Text>
</div>
)}
<div className="text text-b1">
{ markdown ? genMarkdown(content) : linkifyContent(content) }
</div>
{ edited && <Text className="message__edited" variant="b3">(edited)</Text>}
{ reactions && (
<div className="message__reactions text text-b3 noselect">
{
reactions.map((reaction) => (
<button key={reaction.id} onClick={() => alert('Sending reactions is yet to be implemented.')} type="button" className={`msg__reaction${reaction.active ? ' msg__reaction--active' : ''}`}>
{`${reaction.key} ${reaction.count}`}
</button>
))
}
</div>
)}
</div>
</div>
</div>
);
}
Message.defaultProps = {
color: 'var(--tc-surface-high)',
avatarSrc: null,
markdown: false,
contentOnly: false,
reply: null,
edited: false,
reactions: null,
};
Message.propTypes = {
color: PropTypes.string,
avatarSrc: PropTypes.string,
name: PropTypes.string.isRequired,
content: PropTypes.node.isRequired,
time: PropTypes.string.isRequired,
markdown: PropTypes.bool,
contentOnly: PropTypes.bool,
reply: PropTypes.shape({
color: PropTypes.string.isRequired,
to: PropTypes.string.isRequired,
content: PropTypes.string.isRequired,
}),
edited: PropTypes.bool,
reactions: PropTypes.arrayOf(PropTypes.exact({
id: PropTypes.string,
key: PropTypes.string,
count: PropTypes.number,
active: PropTypes.bool,
})),
};
export { Message as default, PlaceholderMessage };

View file

@ -0,0 +1,293 @@
@use '../../atoms/scroll/scrollbar';
.message,
.ph-msg {
padding: var(--sp-ultra-tight) var(--sp-normal);
padding-right: var(--sp-extra-tight);
display: flex;
&:hover {
background-color: var(--bg-surface-hover);
}
[dir=rtl] & {
padding: {
left: var(--sp-extra-tight);
right: var(--sp-normal);
}
}
&__avatar-container {
padding-top: 6px;
}
&__avatar-container,
&__profile {
margin-right: var(--sp-tight);
[dir=rtl] & {
margin: {
left: var(--sp-tight);
right: 0;
}
}
}
&__main-container {
flex: 1;
min-width: 0;
}
}
.message {
&--full + &--full,
&--content-only + &--full,
& + .timeline-change,
.timeline-change + & {
margin-top: var(--sp-normal);
}
&__avatar-container {
width: var(--av-small);
}
&__reply-content {
.text {
color: var(--tc-surface-low);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.ic-raw {
width: 16px;
height: 14px;
}
}
&__edited {
color: var(--tc-surface-low);
}
&__reactions {
margin-top: var(--sp-ultra-tight);
}
}
.ph-msg {
&__avatar {
width: var(--av-small);
height: var(--av-small);
background-color: var(--bg-surface-hover);
border-radius: var(--bo-radius);
}
&__header,
&__content > div {
margin: var(--sp-ultra-tight) 0;
margin-right: var(--sp-extra-tight);
height: var(--fs-b1);
width: 100%;
max-width: 100px;
background-color: var(--bg-surface-hover);
border-radius: calc(var(--bo-radius) / 2);
[dir=rtl] & {
margin: {
right: 0;
left: var(--sp-extra-tight);
}
}
}
&__content {
display: flex;
flex-wrap: wrap;
}
&__content > div:nth-child(1n) {
max-width: 10%;
}
&__content > div:nth-child(2n) {
max-width: 50%;
}
}
.message__header {
display: flex;
align-items: baseline;
& .message__profile {
flex: 1;
min-width: 0;
color: var(--tc-surface-high);
& > .text {
color: inherit;
font-weight: 500;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
}
& .message__time {
& > .text {
color: var(--tc-surface-low);
}
}
}
.message__content {
max-width: 640px;
word-break: break-word;
& > .text > * {
white-space: pre-wrap;
}
& a {
word-break: break-all;
}
}
.msg__reaction {
--reaction-height: 24px;
--reaction-padding: 6px;
--reaction-radius: calc(var(--bo-radius) / 2);
display: inline-flex;
align-items: center;
color: var(--tc-surface-normal);
border: 1px solid var(--bg-surface-border);
padding: 0 var(--reaction-padding);
border-radius: var(--reaction-radius);
cursor: pointer;
height: var(--reaction-height);
margin-right: var(--sp-extra-tight);
[dir=rtl] & {
margin: {
right: 0;
left: var(--sp-extra-tight);
}
}
@media (hover: hover) {
&:hover {
background-color: var(--bg-surface-hover);
}
}
&:active {
background-color: var(--bg-surface-active)
}
&--active {
background-color: var(--bg-caution-active);
@media (hover: hover) {
&:hover {
background-color: var(--bg-caution-hover);
}
}
&:active {
background-color: var(--bg-caution-active)
}
}
}
// markdown formating
.message {
& h1,
& h2 {
color: var(--tc-surface-high);
margin: var(--sp-extra-loose) 0 var(--sp-normal);
line-height: var(--lh-h1);
}
& h3,
& h4 {
color: var(--tc-surface-high);
margin: var(--sp-loose) 0 var(--sp-tight);
line-height: var(--lh-h2);
}
& h5,
& h6 {
color: var(--tc-surface-high);
margin: var(--sp-normal) 0 var(--sp-extra-tight);
line-height: var(--lh-s1);
}
& hr {
border-color: var(--bg-surface-border);
}
.text img {
margin: var(--sp-ultra-tight) 0;
max-width: 296px;
border-radius: calc(var(--bo-radius) / 2);
}
& p,
& pre,
& blockquote {
margin: 0;
padding: 0;
}
& pre,
& blockquote {
margin: var(--sp-ultra-tight) 0;
padding: var(--sp-extra-tight);
background-color: var(--bg-surface-hover) !important;
border-radius: calc(var(--bo-radius) / 2);
}
& pre {
div {
background: none !important;
margin: 0 !important;
}
span {
background: none !important;
}
.linenumber {
min-width: 2.25em !important;
}
}
& code {
padding: 0 !important;
color: var(--tc-code) !important;
white-space: pre-wrap;
@include scrollbar.scroll;
@include scrollbar.scroll__h;
@include scrollbar.scroll--auto-hide;
}
& pre code {
color: var(--tc-surface-normal) !important;
}
& blockquote {
padding-left: var(--sp-extra-tight);
border-left: 4px solid var(--bg-surface-active);
white-space: initial !important;
& > * {
white-space: pre-wrap;
}
[dir=rtl] & {
padding: {
left: 0;
right: var(--sp-extra-tight);
}
border: {
left: none;
right: 4px solid var(--bg-surface-active);
}
}
}
& ul,
& ol {
margin: var(--sp-ultra-tight) 0;
padding-left: 24px;
white-space: initial !important;
& > * {
white-space: pre-wrap;
}
[dir=rtl] & {
padding: {
left: 0;
right: 24px;
}
}
}
}

View file

@ -0,0 +1,79 @@
import React from 'react';
import PropTypes from 'prop-types';
import './TimelineChange.scss';
// import Linkify from 'linkifyjs/react';
import Text from '../../atoms/text/Text';
import RawIcon from '../../atoms/system-icons/RawIcon';
import JoinArraowIC from '../../../../public/res/ic/outlined/join-arrow.svg';
import LeaveArraowIC from '../../../../public/res/ic/outlined/leave-arrow.svg';
import InviteArraowIC from '../../../../public/res/ic/outlined/invite-arrow.svg';
import InviteCancelArraowIC from '../../../../public/res/ic/outlined/invite-cancel-arrow.svg';
import UserIC from '../../../../public/res/ic/outlined/user.svg';
import TickMarkIC from '../../../../public/res/ic/outlined/tick-mark.svg';
function TimelineChange({ variant, content, time }) {
let iconSrc;
switch (variant) {
case 'join':
iconSrc = JoinArraowIC;
break;
case 'leave':
iconSrc = LeaveArraowIC;
break;
case 'invite':
iconSrc = InviteArraowIC;
break;
case 'invite-cancel':
iconSrc = InviteCancelArraowIC;
break;
case 'avatar':
iconSrc = UserIC;
break;
case 'follow':
iconSrc = TickMarkIC;
break;
default:
iconSrc = JoinArraowIC;
break;
}
return (
<div className="timeline-change">
<div className="timeline-change__avatar-container">
<RawIcon src={iconSrc} size="extra-small" />
</div>
<div className="timeline-change__content">
<Text variant="b2">
{content}
{/* <Linkify options={{ target: { url: '_blank' } }}>{content}</Linkify> */}
</Text>
</div>
<div className="timeline-change__time">
<Text variant="b3">{time}</Text>
</div>
</div>
);
}
TimelineChange.defaultProps = {
variant: 'other',
};
TimelineChange.propTypes = {
variant: PropTypes.oneOf([
'join', 'leave', 'invite',
'invite-cancel', 'avatar', 'other',
'follow',
]),
content: PropTypes.oneOfType([
PropTypes.string,
PropTypes.node,
]).isRequired,
time: PropTypes.string.isRequired,
};
export default TimelineChange;

View file

@ -0,0 +1,39 @@
.timeline-change {
padding: var(--sp-ultra-tight) var(--sp-normal);
padding-right: var(--sp-extra-tight);
display: flex;
align-items: center;
&:hover {
background-color: var(--bg-surface-hover);
}
[dir=rtl] & {
padding: {
left: var(--sp-extra-tight);
right: var(--sp-normal);
}
}
&__avatar-container {
width: var(--av-small);
display: inline-flex;
justify-content: center;
align-items: center;
opacity: 0.38;
.ic-raw {
background-color: var(--tc-surface-low);
}
}
& .text {
color: var(--tc-surface-low);
}
&__content {
flex: 1;
min-width: 0;
margin: 0 var(--sp-tight);
}
}

View file

@ -0,0 +1,42 @@
import React from 'react';
import PropTypes from 'prop-types';
import './PeopleSelector.scss';
import { blurOnBubbling } from '../../atoms/button/script';
import Text from '../../atoms/text/Text';
import Avatar from '../../atoms/avatar/Avatar';
function PeopleSelector({
avatarSrc, name, color, peopleRole, onClick,
}) {
return (
<div className="people-selector__container">
<button
className="people-selector"
onMouseUp={(e) => blurOnBubbling(e, '.people-selector')}
onClick={onClick}
type="button"
>
<Avatar imageSrc={avatarSrc} text={name.slice(0, 1)} bgColor={color} size="extra-small" />
<Text className="people-selector__name" variant="b1">{name}</Text>
{peopleRole !== null && <Text className="people-selector__role" variant="b3">{peopleRole}</Text>}
</button>
</div>
);
}
PeopleSelector.defaultProps = {
avatarSrc: null,
peopleRole: null,
};
PeopleSelector.propTypes = {
avatarSrc: PropTypes.string,
name: PropTypes.string.isRequired,
color: PropTypes.string.isRequired,
peopleRole: PropTypes.string,
onClick: PropTypes.func.isRequired,
};
export default PeopleSelector;

View file

@ -0,0 +1,40 @@
.people-selector {
width: 100%;
padding: var(--sp-extra-tight);
padding-left: var(--sp-normal);
display: flex;
align-items: center;
cursor: pointer;
[dir=rtl] & {
padding: {
left: var(--sp-extra-tight);
right: var(--sp-normal);
}
}
@media (hover: hover) {
&:hover {
background-color: var(--bg-surface-hover);
}
}
&:focus {
outline: none;
background-color: var(--bg-surface-hover);
}
&:active {
background-color: var(--bg-surface-active);
}
&__name {
flex: 1;
min-width: 0;
margin: 0 var(--sp-tight);
color: var(--tc-surface-normal);
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
&__role {
color: var(--tc-surface-low);
}
}

View file

@ -0,0 +1,123 @@
import React from 'react';
import PropTypes from 'prop-types';
import './PopupWindow.scss';
import Text from '../../atoms/text/Text';
import IconButton from '../../atoms/button/IconButton';
import { MenuItem } from '../../atoms/context-menu/ContextMenu';
import Header, { TitleWrapper } from '../../atoms/header/Header';
import ScrollView from '../../atoms/scroll/ScrollView';
import RawModal from '../../atoms/modal/RawModal';
import ChevronLeftIC from '../../../../public/res/ic/outlined/chevron-left.svg';
function PWContentSelector({
selected, variant, iconSrc,
type, onClick, children,
}) {
const pwcsClass = selected ? ' pw-content-selector--selected' : '';
return (
<div className={`pw-content-selector${pwcsClass}`}>
<MenuItem
variant={variant}
iconSrc={iconSrc}
type={type}
onClick={onClick}
>
{children}
</MenuItem>
</div>
);
}
PWContentSelector.defaultProps = {
selected: false,
variant: 'surface',
iconSrc: 'none',
type: 'button',
};
PWContentSelector.propTypes = {
selected: PropTypes.bool,
variant: PropTypes.oneOf(['surface', 'caution', 'danger']),
iconSrc: PropTypes.string,
type: PropTypes.oneOf(['button', 'submit']),
onClick: PropTypes.func.isRequired,
children: PropTypes.string.isRequired,
};
function PopupWindow({
className, isOpen, title, contentTitle,
drawer, drawerOptions, contentOptions,
onRequestClose, children,
}) {
const haveDrawer = drawer !== null;
return (
<RawModal
className={`${className === null ? '' : `${className} `}pw-model`}
isOpen={isOpen}
onRequestClose={onRequestClose}
size={haveDrawer ? 'large' : 'medium'}
>
<div className="pw">
{haveDrawer && (
<div className="pw__drawer">
<Header>
<IconButton size="small" src={ChevronLeftIC} onClick={onRequestClose} tooltip="Back" />
<TitleWrapper>
<Text variant="s1">{title}</Text>
</TitleWrapper>
{drawerOptions}
</Header>
<div className="pw__drawer__content__wrapper">
<ScrollView invisible>
<div className="pw__drawer__content">
{drawer}
</div>
</ScrollView>
</div>
</div>
)}
<div className="pw__content">
<Header>
<TitleWrapper>
<Text variant="h2">{contentTitle !== null ? contentTitle : title}</Text>
</TitleWrapper>
{contentOptions}
</Header>
<div className="pw__content__wrapper">
<ScrollView autoHide>
<div className="pw__content-container">
{children}
</div>
</ScrollView>
</div>
</div>
</div>
</RawModal>
);
}
PopupWindow.defaultProps = {
className: null,
drawer: null,
contentTitle: null,
drawerOptions: null,
contentOptions: null,
onRequestClose: null,
};
PopupWindow.propTypes = {
className: PropTypes.string,
isOpen: PropTypes.bool.isRequired,
title: PropTypes.string.isRequired,
contentTitle: PropTypes.string,
drawer: PropTypes.node,
drawerOptions: PropTypes.node,
contentOptions: PropTypes.node,
onRequestClose: PropTypes.func,
children: PropTypes.node.isRequired,
};
export { PopupWindow as default, PWContentSelector };

View file

@ -0,0 +1,100 @@
.pw-model {
--modal-height: 656px;
max-height: var(--modal-height) !important;
height: 100%;
}
.pw {
--popup-window-drawer-width: 312px;
width: 100%;
height: 100%;
background-color: var(--bg-surface);
display: flex;
&__drawer {
width: var(--popup-window-drawer-width);
background-color: var(--bg-surface-low);
border-right: 1px solid var(--bg-surface-border);
[dir=rtl] & {
border: {
right: none;
left: 1px solid var(--bg-surface-border);
}
}
}
&__content {
flex: 1;
min-width: 0;
}
&__drawer,
&__content {
display: flex;
flex-direction: column;
}
}
.pw__drawer__content,
.pw__content-container {
padding-top: var(--sp-extra-tight);
padding-bottom: var(--sp-extra-loose);
}
.pw__drawer__content__wrapper,
.pw__content__wrapper {
flex: 1;
min-height: 0;
}
.pw__drawer {
& .header {
padding-left: var(--sp-extra-tight);
& .ic-btn-surface:first-child {
margin-right: var(--sp-ultra-tight);
}
[dir=rtl] & {
padding-right: var(--sp-extra-tight);
& .ic-btn-surface:first-child {
margin-right: 0;
margin-left: var(--sp-ultra-tight);
}
}
}
}
.pw-content-selector {
&--selected {
border: 1px solid var(--bg-surface-border);
border-width: 1px 0;
background-color: var(--bg-surface);
& .context-menu__item > button {
&:hover {
background-color: transparent;
}
}
}
& .context-menu__item > button {
& .text {
color: var(--tc-surface-normal);
}
padding-left: var(--sp-normal);
& .ic-raw {
margin-right: var(--sp-tight);
}
[dir=rtl] & {
padding-right: var(--sp-normal);
& .ic-raw {
margin-right: 0;
margin-left: var(--sp-tight);
}
}
}
}

View file

@ -0,0 +1,32 @@
import React from 'react';
import PropTypes from 'prop-types';
import './SettingTile.scss';
import Text from '../../atoms/text/Text';
function SettingTile({ title, options, content }) {
return (
<div className="setting-tile">
<div className="setting-tile__title__wrapper">
<div className="setting-tile__title">
<Text variant="b1">{title}</Text>
</div>
{options !== null && <div className="setting-tile__options">{options}</div>}
</div>
{content !== null && <div className="setting-tile__content">{content}</div>}
</div>
);
}
SettingTile.defaultProps = {
options: null,
content: null,
};
SettingTile.propTypes = {
title: PropTypes.string.isRequired,
options: PropTypes.node,
content: PropTypes.node,
};
export default SettingTile;

View file

@ -0,0 +1,16 @@
.setting-tile {
&__title__wrapper {
display: flex;
align-items: center;
}
&__title {
flex: 1;
min-width: 0;
margin-right: var(--sp-normal);
[dir=rtl] & {
margin-right: 0;
margin-left: var(--sp-normal);
}
}
}

View file

@ -0,0 +1,72 @@
import React from 'react';
import PropTypes from 'prop-types';
import './SidebarAvatar.scss';
import Tippy from '@tippyjs/react';
import Avatar from '../../atoms/avatar/Avatar';
import Text from '../../atoms/text/Text';
import NotificationBadge from '../../atoms/badge/NotificationBadge';
import { blurOnBubbling } from '../../atoms/button/script';
const SidebarAvatar = React.forwardRef(({
tooltip, text, bgColor, imageSrc,
iconSrc, active, onClick, notifyCount,
}, ref) => {
let activeClass = '';
if (active) activeClass = ' sidebar-avatar--active';
return (
<Tippy
content={<Text variant="b1">{tooltip}</Text>}
className="sidebar-avatar-tippy"
touch="hold"
arrow={false}
placement="right"
maxWidth={200}
delay={[0, 0]}
duration={[100, 0]}
offset={[0, 0]}
>
<button
ref={ref}
className={`sidebar-avatar${activeClass}`}
type="button"
onMouseUp={(e) => blurOnBubbling(e, '.sidebar-avatar')}
onClick={onClick}
>
<Avatar
text={text}
bgColor={bgColor}
imageSrc={imageSrc}
iconSrc={iconSrc}
size="normal"
/>
{ notifyCount !== null && <NotificationBadge alert>{notifyCount}</NotificationBadge> }
</button>
</Tippy>
);
});
SidebarAvatar.defaultProps = {
text: null,
bgColor: 'transparent',
iconSrc: null,
imageSrc: null,
active: false,
onClick: null,
notifyCount: null,
};
SidebarAvatar.propTypes = {
tooltip: PropTypes.string.isRequired,
text: PropTypes.string,
bgColor: PropTypes.string,
imageSrc: PropTypes.string,
iconSrc: PropTypes.string,
active: PropTypes.bool,
onClick: PropTypes.func,
notifyCount: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
]),
};
export default SidebarAvatar;

View file

@ -0,0 +1,63 @@
.sidebar-avatar-tippy {
padding: var(--sp-extra-tight) var(--sp-normal);
background-color: var(--bg-tooltip);
border-radius: var(--bo-radius);
box-shadow: var(--bs-popup);
.text {
color: var(--tc-tooltip);
}
}
.sidebar-avatar {
position: relative;
display: flex;
justify-content: center;
align-items: center;
width: 100%;
cursor: pointer;
& .notification-badge {
position: absolute;
right: var(--sp-extra-tight);
top: calc(-1 * var(--sp-ultra-tight));
box-shadow: 0 0 0 2px var(--bg-surface-low);
}
&:focus {
outline: none;
}
&:active .avatar-container {
box-shadow: var(--bs-surface-outline);
}
&:hover::before,
&:focus::before,
&--active::before {
content: "";
display: block;
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
width: 3px;
height: 12px;
background-color: var(--ic-surface-normal);
border-radius: 0 4px 4px 0;
transition: height 200ms linear;
[dir=rtl] & {
right: 0;
border-radius: 4px 0 0 4px;
}
}
&--active:hover::before,
&--active:focus::before,
&--active::before {
height: 28px;
}
&--active .avatar-container {
background-color: var(--bg-surface);
}
}