refactor: more tailwind
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { FC, ImgHTMLAttributes } from "react";
|
||||
import { FC, ImgHTMLAttributes, useEffect, useState } from "react";
|
||||
import { getInitials } from "../utils";
|
||||
|
||||
interface Props extends ImgHTMLAttributes<HTMLImageElement> {
|
||||
@@ -29,26 +29,34 @@ const Avatar: FC<Props> = ({
|
||||
height,
|
||||
...rest
|
||||
}) => {
|
||||
if (src && src.length !== 0) {
|
||||
return <img src={src} {...rest} />;
|
||||
} else {
|
||||
return (
|
||||
<div
|
||||
className={`rounded-full flex-center ${rest.className || ""}`}
|
||||
style={{
|
||||
width,
|
||||
height,
|
||||
fontSize: getFontSize(width),
|
||||
fontWeight: 400,
|
||||
fontFamily: "'Lato', 'Lato-Regular', 'Helvetica Neue'",
|
||||
background: type === "channel" ? "#EAECF0" : "#4c99e9",
|
||||
color: type === "channel" ? "#475467" : "#FFFFFF"
|
||||
}}
|
||||
>
|
||||
{getInitials(name)}
|
||||
</div>
|
||||
);
|
||||
const [avatarSrc, setAvatarSrc] = useState("");
|
||||
useEffect(() => {
|
||||
if (!src) {
|
||||
setAvatarSrc(src);
|
||||
}
|
||||
}, [src]);
|
||||
const handleError = () => {
|
||||
setAvatarSrc("");
|
||||
};
|
||||
if (avatarSrc) {
|
||||
return <img src={avatarSrc} onError={handleError} {...rest} />;
|
||||
}
|
||||
return (
|
||||
<div
|
||||
className={`rounded-full flex-center ${rest.className || ""}`}
|
||||
style={{
|
||||
width,
|
||||
height,
|
||||
fontSize: getFontSize(width),
|
||||
fontWeight: 400,
|
||||
fontFamily: "'Lato', 'Lato-Regular', 'Helvetica Neue'",
|
||||
background: type === "channel" ? "#EAECF0" : "#4c99e9",
|
||||
color: type === "channel" ? "#475467" : "#FFFFFF"
|
||||
}}
|
||||
>
|
||||
{getInitials(name)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Avatar;
|
||||
|
||||
@@ -139,18 +139,18 @@ const ChannelModal: FC<Props> = ({ personal = false, closeModal }) => {
|
||||
? t("create_private_channel_desc")
|
||||
: t("create_channel_desc")}
|
||||
</p>
|
||||
<div className="w-full flex flex-col justify-start gap-2 mb-8.5">
|
||||
<div className="w-full flex flex-col justify-start gap-2 mb-8">
|
||||
<span className="text-gray-400 text-sm font-normal">{t("channel_name")}</span>
|
||||
<div className="relative">
|
||||
<input className="text-gray-600 rounded p-2 pl-9 border border-solid border-gray-300 w-full" onChange={handleNameInput} value={name} placeholder="new channel" />
|
||||
<ChannelIcon personal={!is_public} className="absolute left-2 top-1/2 -translate-y-1/2" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full flex items-center justify-between mb-12.5">
|
||||
<div className="w-full flex items-center justify-between mb-12">
|
||||
<span className="text-gray-400 text-sm">{t("private_channel")}</span>
|
||||
<StyledToggle
|
||||
data-checked={!is_public}
|
||||
data-disabled={!loginUser?.is_admin}
|
||||
checked={!is_public}
|
||||
disabled={!loginUser?.is_admin}
|
||||
onClick={handleToggle}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { FC, ReactElement } from "react";
|
||||
import StyledMenu from "./styled/Menu";
|
||||
|
||||
export interface Item {
|
||||
title: string;
|
||||
@@ -16,7 +15,7 @@ interface Props {
|
||||
|
||||
const ContextMenu: FC<Props> = ({ items = [], hideMenu = null }) => {
|
||||
return (
|
||||
<StyledMenu>
|
||||
<ul className="context-menu">
|
||||
{items.map((item) => {
|
||||
// if (!item) return null;
|
||||
const {
|
||||
@@ -49,7 +48,7 @@ const ContextMenu: FC<Props> = ({ items = [], hideMenu = null }) => {
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</StyledMenu>
|
||||
</ul>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { FC, ReactElement } from "react";
|
||||
import dayjs from "dayjs";
|
||||
import Styled from "./styled";
|
||||
import clsx from "clsx";
|
||||
import {
|
||||
VideoPreview,
|
||||
AudioPreview,
|
||||
@@ -80,33 +80,33 @@ const FileBox: FC<Props> = ({
|
||||
content
|
||||
}) => {
|
||||
const fromUser = useAppSelector((store) => store.users.byId[from_uid]);
|
||||
const icon = getFileIcon(file_type, name);
|
||||
const icon = getFileIcon(file_type, name, "icon w-9 h-12");
|
||||
if (!content || !fromUser || !name) return null;
|
||||
const previewContent = renderPreview({ file_type, content, name });
|
||||
const withPreview = preview && previewContent;
|
||||
|
||||
return (
|
||||
<Styled
|
||||
className={`file_box ${flex ? "flex" : ""} ${withPreview ? "preview" : ""} ${file_type.startsWith("audio") ? "audio" : ""
|
||||
}`}
|
||||
<div
|
||||
className={clsx(`h-[66px] rounded-md border border-solid border-gray-300 bg-gray-100`, flex ? "w-full" : "w-[370px]", withPreview && "relative overflow-hidden h-[281px]", file_type.startsWith("audio") && "h-[125px]")}
|
||||
>
|
||||
<div className="basic">
|
||||
<div className="w-full p-2 flex items-center justify-between gap-2">
|
||||
{icon}
|
||||
<div className="info">
|
||||
<span className="name">{name}</span>
|
||||
<span className="details">
|
||||
<i className="size">{formatBytes(size)}</i>
|
||||
<i className="time">{dayjs(created_at).fromNow()}</i>
|
||||
<i className="from">
|
||||
by <strong>{fromUser.name}</strong>
|
||||
</i>
|
||||
</span>
|
||||
<div className="flex flex-col gap-1 w-full overflow-hidden">
|
||||
<span className="font-semibold text-sm text-gray-800 whitespace-nowrap text-ellipsis">{name}</span>
|
||||
<em className="text-xs text-gray-500 flex gap-4 not-italic">
|
||||
<span className="size">{formatBytes(size)}</span>
|
||||
<span className="time">{dayjs(created_at).fromNow()}</span>
|
||||
<span>
|
||||
by <strong className="font-bold">{fromUser.name}</strong>
|
||||
</span>
|
||||
</em>
|
||||
</div>
|
||||
<a className="download" download={name} href={`${content}&download=true`}>
|
||||
<a className="whitespace-nowrap" download={name} href={`${content}&download=true`}>
|
||||
<IconDownload className="fill-gray-500" />
|
||||
</a>
|
||||
</div>
|
||||
{withPreview && <div className="preview">{previewContent}</div>}
|
||||
</Styled>
|
||||
{withPreview && <div className="h-[calc(100%_-_64px)] overflow-hidden">{previewContent}</div>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
import styled from "styled-components";
|
||||
|
||||
const Styled = styled.div`
|
||||
background: #f3f4f6;
|
||||
border: 1px solid #d4d4d4;
|
||||
box-sizing: border-box;
|
||||
border-radius: 6px;
|
||||
width: 370px;
|
||||
height: 66px;
|
||||
* {
|
||||
user-select: text;
|
||||
}
|
||||
&.flex {
|
||||
width: 100%;
|
||||
}
|
||||
&.preview {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
height: 281px;
|
||||
&.audio {
|
||||
height: 125px;
|
||||
}
|
||||
}
|
||||
.basic {
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
.icon {
|
||||
width: 36px;
|
||||
height: 48px;
|
||||
}
|
||||
.info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
.name {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
color: #1c1c1e;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.details {
|
||||
font-weight: 400;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
color: #616161;
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
.from strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
.download {
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
.preview {
|
||||
height: calc(100% - 64px);
|
||||
overflow: hidden;
|
||||
/* todo */
|
||||
}
|
||||
`;
|
||||
|
||||
export default Styled;
|
||||
@@ -1,25 +1,26 @@
|
||||
import { useState, MouseEvent, ChangeEvent, FC } from "react";
|
||||
// import toast from "react-hot-toast";
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
import Modal from "../Modal";
|
||||
import Button from "../styled/Button";
|
||||
import Input from "../styled/Input";
|
||||
import Channel from "../Channel";
|
||||
import User from "../User";
|
||||
import Reply from "../Message/Reply";
|
||||
import StyledWrapper from "./styled";
|
||||
import useForwardMessage from "../../hook/useForwardMessage";
|
||||
import useSendMessage from "../../hook/useSendMessage";
|
||||
import useFilteredChannels from "../../hook/useFilteredChannels";
|
||||
import useFilteredUsers from "../../hook/useFilteredUsers";
|
||||
import CloseIcon from "../../../assets/icons/close.circle.svg";
|
||||
import StyledCheckbox from "../styled/Checkbox";
|
||||
import toast from "react-hot-toast";
|
||||
import { useTranslation } from "react-i18next";
|
||||
interface IProps {
|
||||
mids: number[];
|
||||
closeModal: () => void;
|
||||
}
|
||||
|
||||
const ForwardModal: FC<IProps> = ({ mids, closeModal }) => {
|
||||
const { t } = useTranslation();
|
||||
const [appendText, setAppendText] = useState("");
|
||||
const { sendMessages } = useSendMessage();
|
||||
const { forwardMessage, forwarding } = useForwardMessage();
|
||||
@@ -74,16 +75,17 @@ const ForwardModal: FC<IProps> = ({ mids, closeModal }) => {
|
||||
(selectedChannels.length == 0 && selectedMembers.length == 0) || forwarding;
|
||||
return (
|
||||
<Modal>
|
||||
<StyledWrapper>
|
||||
<div className="left">
|
||||
<div className="search">
|
||||
<div className="flex max-h-[514px] min-h-[400px] bg-white drop-shadow rounded-lg overflow-hidden">
|
||||
<div className="w-[271px] shadow-[inset_-1px_0px_0px_rgba(0,_0,_0,_0.1)] overflow-y-scroll bg-inherit">
|
||||
<div className="sticky top-0 bg-inherit z-[90] p-4 w-[calc(100%_-_1px)]">
|
||||
<input
|
||||
className="px-2 py-2.5 text-sm bg-black/10 rounded-lg w-full"
|
||||
value={input}
|
||||
onChange={handleSearchChange}
|
||||
placeholder="Search user or channel"
|
||||
/>
|
||||
</div>
|
||||
<ul className="users">
|
||||
<ul className="flex flex-col pb-5">
|
||||
{channels &&
|
||||
channels.map((c) => {
|
||||
const { gid } = c;
|
||||
@@ -93,7 +95,7 @@ const ForwardModal: FC<IProps> = ({ mids, closeModal }) => {
|
||||
key={gid}
|
||||
data-type="channel"
|
||||
data-id={gid}
|
||||
className="user channel"
|
||||
className="cursor-pointer flex items-center px-4 rounded hover:bg-gray-600/10"
|
||||
onClick={toggleCheck}
|
||||
>
|
||||
<StyledCheckbox readOnly checked={checked} name="cb" id="cb" />
|
||||
@@ -110,7 +112,7 @@ const ForwardModal: FC<IProps> = ({ mids, closeModal }) => {
|
||||
key={uid}
|
||||
data-id={uid}
|
||||
data-type="user"
|
||||
className="user"
|
||||
className="cursor-pointer flex items-center px-4 rounded hover:bg-gray-600/10"
|
||||
onClick={toggleCheck}
|
||||
>
|
||||
<StyledCheckbox readOnly checked={checked} name="cb" id="cb" />
|
||||
@@ -120,15 +122,15 @@ const ForwardModal: FC<IProps> = ({ mids, closeModal }) => {
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
<div className={`right`}>
|
||||
<h3 className="title">Send To {selectedCount}</h3>
|
||||
<ul className="selected">
|
||||
<div className={`flex flex-col items-start p-4 box-border`}>
|
||||
<h3 className="font-semibold text-sm text-gray-700 mb-4">Send To {selectedCount}</h3>
|
||||
<ul className="w-full h-[260px] py-2.5 overflow-y-scroll">
|
||||
{selectedChannels.map((cid) => {
|
||||
return (
|
||||
<li key={cid} className="item">
|
||||
<li key={cid} className="relative">
|
||||
<Channel key={cid} id={cid} interactive={false} />
|
||||
<CloseIcon
|
||||
className="remove"
|
||||
className="cursor-pointer absolute right-1 top-1/2 -translate-y-1/2"
|
||||
onClick={removeSelected.bind(null, cid, "channel")}
|
||||
/>
|
||||
</li>
|
||||
@@ -143,27 +145,27 @@ const ForwardModal: FC<IProps> = ({ mids, closeModal }) => {
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
<div className="msgs">
|
||||
<div className="rounded-lg p-2 max-h-[200px] overflow-auto bg-slate-100 w-[280px] mb-1">
|
||||
{mids.map((mid) => (
|
||||
<Reply key={mid} mid={mid} interactive={false} />
|
||||
))}
|
||||
</div>
|
||||
<Input
|
||||
className="input"
|
||||
className="mb-8"
|
||||
placeholder="Leave a message"
|
||||
value={appendText}
|
||||
onChange={updateAppendText}
|
||||
></Input>
|
||||
<div className="btns">
|
||||
<div className="w-full flex items-center justify-end gap-4">
|
||||
<Button onClick={closeModal} className="normal cancel">
|
||||
Cancel
|
||||
{t('action.cancel')}
|
||||
</Button>
|
||||
<Button className="normal" disabled={sendButtonDisabled} onClick={handleForward}>
|
||||
Send To {selectedCount == 0 ? null : `(${selectedCount})`}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,107 +0,0 @@
|
||||
import styled from "styled-components";
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
display: flex;
|
||||
max-height: 514px;
|
||||
min-height: 400px;
|
||||
background: #fff;
|
||||
box-shadow: 0px 25px 50px rgba(31, 41, 55, 0.25);
|
||||
border-radius: var(--br);
|
||||
transition: all 0.5s ease;
|
||||
overflow: hidden;
|
||||
.left {
|
||||
width: 276px;
|
||||
box-shadow: inset -1px 0px 0px rgba(0, 0, 0, 0.1);
|
||||
overflow-y: scroll;
|
||||
.search {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 99;
|
||||
background: #fff;
|
||||
box-shadow: 0px 1px 0px rgba(0, 0, 0, 0.1);
|
||||
padding: 16px;
|
||||
width: calc(100% - 1px);
|
||||
input {
|
||||
outline: none;
|
||||
width: -webkit-fill-available;
|
||||
padding: 10px 8px;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
background: rgba(0, 0, 0, 0.08);
|
||||
border-radius: var(--br);
|
||||
}
|
||||
}
|
||||
.users {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-bottom: 20px;
|
||||
.user {
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 16px;
|
||||
width: -webkit-fill-available;
|
||||
border-radius: 4px;
|
||||
&:hover {
|
||||
background: rgba(116, 127, 141, 0.1);
|
||||
}
|
||||
> div {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.right {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
padding: 16px;
|
||||
box-sizing: border-box;
|
||||
.title {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
color: #344054;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.selected {
|
||||
width: 100%;
|
||||
height: 260px;
|
||||
padding: 10px 0;
|
||||
overflow: scroll;
|
||||
.item {
|
||||
position: relative;
|
||||
.remove {
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
right: 5px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
}
|
||||
}
|
||||
.msgs {
|
||||
border-radius: var(--br);
|
||||
padding: 8px;
|
||||
max-height: 200px;
|
||||
overflow: auto;
|
||||
background-color: #f4f4f5;
|
||||
width: 280px;
|
||||
margin-bottom: 4px;
|
||||
> .reply {
|
||||
background: none;
|
||||
}
|
||||
}
|
||||
.input {
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
.btns {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 16px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
export default StyledWrapper;
|
||||
@@ -45,7 +45,7 @@ const GithubLoginButton: FC<Props> = ({ type = "login", source = "webapp", clien
|
||||
};
|
||||
|
||||
return (
|
||||
<Button className="flex-center gap-3 ghost !text-gray-600 !border-[#d0d5dd]" onClick={handleGithubLogin}>
|
||||
<Button className="flex-center gap-3 ghost !text-gray-600 dark:!text-gray-100 !border-[#d0d5dd]" onClick={handleGithubLogin}>
|
||||
<IconGithub className="w-6 h-6" />
|
||||
{` ${type === "login" ? t("login.github") : t("reg.github")}`}
|
||||
</Button>
|
||||
|
||||
@@ -4,7 +4,6 @@ import { hideAll } from "tippy.js";
|
||||
import toast from "react-hot-toast";
|
||||
import { useUpdateUserMutation } from "../../../app/services/user";
|
||||
import User from "../User";
|
||||
import StyledMenu from "../styled/Menu";
|
||||
import InviteLink from "../InviteLink";
|
||||
import moreIcon from "../../../assets/icons/more.svg?url";
|
||||
import IconOwner from "../../../assets/icons/owner.svg";
|
||||
@@ -93,7 +92,7 @@ const ManageMembers: FC<Props> = ({ cid }) => {
|
||||
placement="bottom-end"
|
||||
trigger="click"
|
||||
content={
|
||||
<StyledMenu className="menu">
|
||||
<ul className="context-menu">
|
||||
<li
|
||||
className="item sb"
|
||||
onClick={handleToggleRole.bind(null, {
|
||||
@@ -116,7 +115,7 @@ const ManageMembers: FC<Props> = ({ cid }) => {
|
||||
{t("user")}
|
||||
{!is_admin && <IconCheck className="icon" />}
|
||||
</li>
|
||||
</StyledMenu>
|
||||
</ul>
|
||||
}
|
||||
>
|
||||
<IconArrowDown className="cursor-pointer dark:fill-slate-50" />
|
||||
@@ -129,7 +128,7 @@ const ManageMembers: FC<Props> = ({ cid }) => {
|
||||
placement="right-start"
|
||||
trigger="click"
|
||||
content={
|
||||
<StyledMenu className="min-w-30">
|
||||
<ul className="min-w-30 context-menu">
|
||||
{email && (
|
||||
<li className="item" onClick={copyEmail.bind(null, email)}>
|
||||
{ct("action.copy_email")}
|
||||
@@ -145,7 +144,7 @@ const ManageMembers: FC<Props> = ({ cid }) => {
|
||||
{ct("action.remove")}
|
||||
</li>
|
||||
)}
|
||||
</StyledMenu>
|
||||
</ul>
|
||||
}
|
||||
>
|
||||
<div className="relative w-6 h-6">
|
||||
|
||||
@@ -1,51 +1,10 @@
|
||||
import { useState, useRef, useEffect, ChangeEvent, KeyboardEvent, FC } from "react";
|
||||
import styled from "styled-components";
|
||||
import TextareaAutosize from "react-textarea-autosize";
|
||||
import { useKey } from "rooks";
|
||||
import { useEditMessageMutation } from "../../../app/services/message";
|
||||
import { ContentTypes } from "../../../app/config";
|
||||
import { useAppSelector } from "../../../app/store";
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
width: 100%;
|
||||
.input {
|
||||
background: #e5e7eb;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
textarea {
|
||||
outline: none;
|
||||
width: 100%;
|
||||
background: none;
|
||||
resize: unset;
|
||||
user-select: text;
|
||||
color: #374151;
|
||||
font-weight: normal;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
word-break: break-all;
|
||||
white-space: break-spaces;
|
||||
}
|
||||
}
|
||||
.opts {
|
||||
padding: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
.opt {
|
||||
font-weight: normal;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
button {
|
||||
padding: 0 4px;
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
color: #06b6d4;
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
type Props = {
|
||||
mid: number;
|
||||
cancelEdit: () => void;
|
||||
@@ -97,9 +56,10 @@ const EditMessage: FC<Props> = ({ mid, cancelEdit }) => {
|
||||
};
|
||||
if (!msg) return null;
|
||||
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<div className="input">
|
||||
<div className="w-full">
|
||||
<div className="bg-gray-200 rounded-lg p-4">
|
||||
<TextareaAutosize
|
||||
autoFocus
|
||||
onFocus={(e) =>
|
||||
@@ -109,7 +69,7 @@ const EditMessage: FC<Props> = ({ mid, cancelEdit }) => {
|
||||
)
|
||||
}
|
||||
ref={inputRef}
|
||||
className="content"
|
||||
className="content w-full resize-none bg-transparent text-gray-800 text-sm break-all"
|
||||
maxRows={8}
|
||||
minRows={1}
|
||||
onKeyDown={handleInputKeydown}
|
||||
@@ -118,15 +78,15 @@ const EditMessage: FC<Props> = ({ mid, cancelEdit }) => {
|
||||
placeholder={`Edit Message`}
|
||||
/>
|
||||
</div>
|
||||
<div className="opts">
|
||||
<span className="opt">
|
||||
esc to <button onClick={cancelEdit}>cancel</button>
|
||||
<div className="flex items-center p-1 gap-4 text-xs">
|
||||
<span>
|
||||
esc to <button className="text-primary-500 cursor-pointer px-1" onClick={cancelEdit}>cancel</button>
|
||||
</span>
|
||||
<span className="opt">
|
||||
enter to <button onClick={handleSave}>{isEditing ? "saving" : `save`}</button>
|
||||
<span>
|
||||
enter to <button className="text-primary-500 cursor-pointer px-1" onClick={handleSave}>{isEditing ? "saving" : `save`}</button>
|
||||
</span>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default EditMessage;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { useEffect, useState, FC, ReactElement } from "react";
|
||||
import styled from "styled-components";
|
||||
import StyledMsg from "./styled";
|
||||
import renderContent from "./renderContent";
|
||||
import Avatar from "../Avatar";
|
||||
@@ -7,29 +6,6 @@ import IconForward from "../../../assets/icons/forward.svg";
|
||||
import useNormalizeMessage from "../../hook/useNormalizeMessage";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const StyledForward = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-radius: var(--br);
|
||||
background-color: #f4f4f5;
|
||||
> .tip {
|
||||
padding: 8px 8px 0 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
.icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
path {
|
||||
fill: #98a2b3;
|
||||
}
|
||||
}
|
||||
font-weight: 400;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
color: #98a2b3;
|
||||
}
|
||||
`;
|
||||
type Props = {
|
||||
context: "user" | "channel";
|
||||
to: number;
|
||||
@@ -45,15 +21,14 @@ const ForwardedMessage: FC<Props> = ({ context, to, from_uid, id }) => {
|
||||
normalizeMessage(id);
|
||||
}
|
||||
}, [id]);
|
||||
|
||||
useEffect(() => {
|
||||
if (messages) {
|
||||
const forward_mids = messages.map(({ from_mid }) => from_mid) || [];
|
||||
// console.log("fff", messages);
|
||||
setForwards(
|
||||
<StyledForward data-forwarded-mids={forward_mids.join(",")}>
|
||||
<h4 className="tip">
|
||||
<IconForward className="icon" />
|
||||
<div data-forwarded-mids={forward_mids.join(",")} className="flex flex-col rounded-lg bg-gray-100">
|
||||
<h4 className="p-2 pb-0 flex items-center gap-1 text-gray-500 text-xs">
|
||||
<IconForward className="w-4 h-4 fill-gray-500" />
|
||||
{t("action.forward")}
|
||||
</h4>
|
||||
<div className="list">
|
||||
@@ -87,7 +62,7 @@ const ForwardedMessage: FC<Props> = ({ context, to, from_uid, id }) => {
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</StyledForward>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}, [messages, context, to, from_uid]);
|
||||
|
||||
@@ -1,108 +1,13 @@
|
||||
import { FC } from "react";
|
||||
import styled from "styled-components";
|
||||
import Tippy from "@tippyjs/react";
|
||||
import { hideAll } from "tippy.js";
|
||||
import ReactionItem, { Emojis, ReactionMap } from "../ReactionItem";
|
||||
import ReactionPicker from "./ReactionPicker";
|
||||
import Tooltip from "../Tooltip";
|
||||
import { useReactMessageMutation } from "../../../app/services/message";
|
||||
import addEmojiIcon from "../../../assets/icons/add.emoji.svg?url";
|
||||
import IconAddEmoji from "../../../assets/icons/add.emoji.svg";
|
||||
import { useAppSelector } from "../../../app/store";
|
||||
|
||||
const StyledWrapper = styled.span`
|
||||
position: relative;
|
||||
margin-top: 8px;
|
||||
margin-bottom: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
width: fit-content;
|
||||
.reaction {
|
||||
cursor: pointer;
|
||||
background-color: #ecfdff;
|
||||
border-radius: 6px;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 4px;
|
||||
> .emoji {
|
||||
> * {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
background-color: #cff9fe;
|
||||
}
|
||||
&.reacted {
|
||||
box-shadow: inset 0 0 0 1px #06aed4;
|
||||
background-color: #a5f0fc;
|
||||
}
|
||||
|
||||
> .count {
|
||||
font-weight: 400;
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
color: #06aed4;
|
||||
}
|
||||
}
|
||||
> .add {
|
||||
visibility: hidden;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background-color: #ecfdff;
|
||||
border-radius: 6px;
|
||||
border: none;
|
||||
background-image: url(${addEmojiIcon});
|
||||
background-size: 16px;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
&:hover {
|
||||
background-color: #cff9fe;
|
||||
}
|
||||
}
|
||||
&:hover > .add {
|
||||
visibility: visible;
|
||||
}
|
||||
`;
|
||||
const StyledDetails = styled.div`
|
||||
position: relative;
|
||||
background: #ffffff;
|
||||
border-radius: var(--br);
|
||||
box-shadow: 0px 12px 16px -4px rgba(16, 24, 40, 0.08), 0px 4px 6px -2px rgba(16, 24, 40, 0.03);
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
padding: 8px;
|
||||
&:after {
|
||||
content: "";
|
||||
display: block;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
background-color: #fff;
|
||||
border-radius: 1px;
|
||||
position: absolute;
|
||||
bottom: -6px;
|
||||
left: calc(50% - 6px);
|
||||
transform: matrix(0.71, 0.71, -0.71, 0.71, 0, 0);
|
||||
}
|
||||
&.first:after {
|
||||
left: calc(50% - 16px);
|
||||
}
|
||||
.emoji {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
.desc {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 140px;
|
||||
font-weight: 500;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
color: #1d2939;
|
||||
}
|
||||
`;
|
||||
|
||||
const ReactionDetails = ({
|
||||
uids = [],
|
||||
@@ -123,15 +28,15 @@ const ReactionDetails = ({
|
||||
? `${names.join(", ")} and ${names.length - 3} others reacted with`
|
||||
: `${names.join(", ")} reacted with`;
|
||||
return (
|
||||
<StyledDetails className={index == 0 ? "first" : ""}>
|
||||
<div className="emoji">
|
||||
<div className={`relative bg-white rounded-lg shadow flex items-start gap-2 p-2 ${index == 0 ? "first" : ""}`}>
|
||||
<div className="w-8 h-8">
|
||||
<ReactionItem native={emoji} />
|
||||
</div>
|
||||
<div className="desc">
|
||||
<div className="flex flex-col w-[140px] text-xs text-gray-800">
|
||||
<span>{prefixDesc}</span>
|
||||
<span>{ReactionMap[emoji]}</span>
|
||||
</div>
|
||||
</StyledDetails>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
type Props = {
|
||||
@@ -152,15 +57,15 @@ const Reaction: FC<Props> = ({ mid, reactions = null, readOnly = false }) => {
|
||||
reactWithEmoji({ mid, action: emoji });
|
||||
};
|
||||
if (!reactions || Object.entries(reactions).length == 0) return null;
|
||||
|
||||
return (
|
||||
<StyledWrapper className="reactions">
|
||||
<span className="group relative mt-2 mb-1 flex items-center gap-1 w-fit">
|
||||
{Object.entries(reactions).map(([reaction, uids], idx) => {
|
||||
const reacted = uids.findIndex((id: number) => id == currUid) > -1;
|
||||
return uids.length > 0 ? (
|
||||
<span
|
||||
onClick={readOnly ? undefined : handleReact.bind(null, reaction)}
|
||||
className={`reaction ${reacted ? "reacted" : ""}`}
|
||||
// data-count={count > 1 ? count : ""}
|
||||
className={`cursor-pointer rounded-md relative flex items-center gap-1 p-1 hover:bg-[#cff9fe] ${reacted ? "shadow-[inset_0_0_0_1px_#06aed4] bg-[#a5f0fc]" : ""}`}
|
||||
key={reaction}
|
||||
>
|
||||
<Tippy
|
||||
@@ -176,7 +81,7 @@ const Reaction: FC<Props> = ({ mid, reactions = null, readOnly = false }) => {
|
||||
</i>
|
||||
</Tippy>
|
||||
|
||||
{uids.length > 1 ? <em className="count">{`${uids.length}`} </em> : null}
|
||||
{uids.length > 1 ? <em className="text-primary-600 text-xs">{`${uids.length}`} </em> : null}
|
||||
</span>
|
||||
) : null;
|
||||
})}
|
||||
@@ -188,11 +93,13 @@ const Reaction: FC<Props> = ({ mid, reactions = null, readOnly = false }) => {
|
||||
trigger="click"
|
||||
content={<ReactionPicker mid={mid} hidePicker={hideAll} />}
|
||||
>
|
||||
<button className="add"></button>
|
||||
<button className="invisible group-hover:visible w-6 h-6 bg-[#ecfdff] hover:bg-[#cff9fe] rounded-md flex-center">
|
||||
<IconAddEmoji className={'w-4 h-4'} />
|
||||
</button>
|
||||
</Tippy>
|
||||
</Tooltip>
|
||||
)}
|
||||
</StyledWrapper>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
export default Reaction;
|
||||
|
||||
@@ -1,40 +1,9 @@
|
||||
import { FC } from "react";
|
||||
import styled from "styled-components";
|
||||
import { useReactMessageMutation } from "../../../app/services/message";
|
||||
import { Emojis } from "../../../app/config";
|
||||
import Emoji from "../ReactionItem";
|
||||
import { useAppSelector } from "../../../app/store";
|
||||
|
||||
const StyledPicker = styled.div`
|
||||
background: none;
|
||||
z-index: 999;
|
||||
.emojis {
|
||||
padding: 4px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 8px;
|
||||
background: #fff;
|
||||
filter: drop-shadow(0px 25px 50px rgba(31, 41, 55, 0.25));
|
||||
border-radius: 12px;
|
||||
&.reacting {
|
||||
opacity: 0.6;
|
||||
}
|
||||
.wrapper {
|
||||
display: flex;
|
||||
cursor: pointer;
|
||||
border-radius: 8px;
|
||||
padding: 4px;
|
||||
&:hover,
|
||||
&.reacted {
|
||||
background-color: #f5f6f7;
|
||||
}
|
||||
> .emoji {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
type Props = {
|
||||
mid: number;
|
||||
hidePicker: () => void;
|
||||
@@ -52,15 +21,14 @@ const ReactionPicker: FC<Props> = ({ mid, hidePicker }) => {
|
||||
hidePicker();
|
||||
};
|
||||
return (
|
||||
<StyledPicker>
|
||||
<ul className={`emojis ${isLoading ? "reacting" : ""}`}>
|
||||
<div className="z-[999]">
|
||||
<ul className={`p-1 grid grid-cols-[repeat(4,_1fr)] gap-2 bg-white dark:bg-gray-900 drop-shadow-md rounded-xl ${isLoading ? "opacity-60" : ""}`}>
|
||||
{Emojis.map((emoji) => {
|
||||
let reacted =
|
||||
reactionData[emoji] && reactionData[emoji].findIndex((id) => id == currUid) > -1;
|
||||
|
||||
return (
|
||||
<li
|
||||
className={`wrapper ${reacted ? "reacted" : ""}`}
|
||||
className={`flex-center cursor-pointer rounded-lg p-4 hover:bg-gray-50 w-4 h-4 ${reacted ? "bg-gray-50" : ""}`}
|
||||
key={emoji}
|
||||
onClick={handleReact.bind(null, emoji)}
|
||||
>
|
||||
@@ -69,7 +37,7 @@ const ReactionPicker: FC<Props> = ({ mid, hidePicker }) => {
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</StyledPicker>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default ReactionPicker;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import React, { MouseEvent, FC } from "react";
|
||||
import styled from "styled-components";
|
||||
import MarkdownRender from "../MarkdownRender";
|
||||
import { ContentTypes } from "../../../app/config";
|
||||
import { getFileIcon, isImage } from "../../utils";
|
||||
@@ -8,94 +7,6 @@ import LinkifyText from '../LinkifyText';
|
||||
import Avatar from "../Avatar";
|
||||
import { useAppSelector } from "../../../app/store";
|
||||
import { MessagePayload } from "../../../app/slices/message";
|
||||
const Styled = styled.div`
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
padding: 8px;
|
||||
background: #e5e7eb;
|
||||
border-radius: var(--br);
|
||||
gap: 8px;
|
||||
margin-bottom: 4px;
|
||||
|
||||
&.clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.user {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
white-space: nowrap;
|
||||
|
||||
.avatar {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.name {
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
color: #06b6d4;
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
overflow: hidden;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
color: #616161;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.txt {
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 1;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
word-wrap: break-word;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.md {
|
||||
position: relative;
|
||||
max-height: 152px;
|
||||
overflow: hidden;
|
||||
|
||||
&:after {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
content: "";
|
||||
background: linear-gradient(180deg, rgba(255, 255, 255, 0) 63.54%, #e5e7eb 93.09%);
|
||||
}
|
||||
}
|
||||
|
||||
.pic {
|
||||
display: inherit;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 15px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.file_name {
|
||||
margin-left: 5px;
|
||||
font-size: 10px;
|
||||
color: #555;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const renderContent = (data: MessagePayload) => {
|
||||
const { content_type, content, thumbnail, properties } = data;
|
||||
@@ -103,14 +14,14 @@ const renderContent = (data: MessagePayload) => {
|
||||
switch (content_type) {
|
||||
case ContentTypes.text:
|
||||
res = (
|
||||
<span className="txt">
|
||||
<span className="text-ellipsis overflow-hidden break-words break-all text-gray-800">
|
||||
<LinkifyText text={content} url={false} mentionTextOnly={true} mentionPopOver={false} />
|
||||
</span>
|
||||
);
|
||||
break;
|
||||
case ContentTypes.markdown:
|
||||
res = (
|
||||
<div className="md">
|
||||
<div className="max-h-[152px] overflow-hidden">
|
||||
<MarkdownRender content={content} />
|
||||
</div>
|
||||
);
|
||||
@@ -118,14 +29,14 @@ const renderContent = (data: MessagePayload) => {
|
||||
case ContentTypes.file:
|
||||
{
|
||||
const { content_type = "", name, size } = properties || {};
|
||||
const icon = getFileIcon(content_type, name);
|
||||
const icon = getFileIcon(content_type, name, "w-4 h-5");
|
||||
if (isImage(content_type, size)) {
|
||||
res = <img className="pic" src={thumbnail} />;
|
||||
res = <img className="w-10 h-10 object-cover" src={thumbnail} />;
|
||||
} else {
|
||||
res = (
|
||||
<>
|
||||
{icon}
|
||||
<span className="file_name">{name}</span>
|
||||
<span className="ml-1 text-[10px] text-gray-500">{name}</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -161,25 +72,26 @@ const Reply: FC<ReplyProps> = ({ mid, interactive = true }) => {
|
||||
if (!data) return null;
|
||||
const currUser = users[data.from_uid || 0];
|
||||
if (!currUser) return null;
|
||||
|
||||
return (
|
||||
<Styled
|
||||
<div
|
||||
key={mid}
|
||||
data-mid={mid}
|
||||
className={`reply ${interactive ? "clickable" : ""}`}
|
||||
className={`flex items-start p-2 bg-gray-100 rounded-lg gap-2 mb-1 ${interactive ? "cursor-pointer" : "!bg-transparent"}`}
|
||||
onClick={interactive ? handleClick : undefined}
|
||||
>
|
||||
<div className="user">
|
||||
<div className="flex items-center gap-1 whitespace-nowrap">
|
||||
<Avatar
|
||||
width={16}
|
||||
height={16}
|
||||
className="avatar"
|
||||
className="w-4 h-4 rounded-full"
|
||||
src={currUser.avatar}
|
||||
name={currUser.name}
|
||||
/>
|
||||
<span className="name">{currUser.name}</span>
|
||||
<span className="text-sm text-primary-500">{currUser.name}</span>
|
||||
</div>
|
||||
<div className="content">{renderContent(data)}</div>
|
||||
</Styled>
|
||||
<div className="text-sm flex items-center text-gray-400 overflow-hidden">{renderContent(data)}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -188,7 +188,7 @@ const Plugins: FC<Props> = ({
|
||||
|
||||
|
||||
return (
|
||||
<div className="input max-h-[50vh] overflow-auto text-sm text-gray-600 dark:text-white" ref={editableRef}>
|
||||
<div className="input w-full max-h-[50vh] overflow-auto text-sm text-gray-600 dark:text-white" ref={editableRef}>
|
||||
<Plate
|
||||
id={`${TEXT_EDITOR_PREFIX}_${id}`}
|
||||
onChange={handleChange}
|
||||
|
||||
@@ -4,7 +4,6 @@ import Tippy from "@tippyjs/react";
|
||||
import IconMessage from "../../../assets/icons/message.svg";
|
||||
import IconMore from "../../../assets/icons/more.svg";
|
||||
import Avatar from "../Avatar";
|
||||
import StyledMenu from "../styled/Menu";
|
||||
import useUserOperation from "../../hook/useUserOperation";
|
||||
import { useAppSelector } from "../../../app/store";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -76,7 +75,7 @@ const Profile: FC<Props> = ({ uid, type = "embed", cid }) => {
|
||||
trigger="click"
|
||||
hideOnClick={true}
|
||||
content={
|
||||
<StyledMenu>
|
||||
<ul className="context-menu">
|
||||
{canCopyEmail && (
|
||||
<li className="item" onClick={copyEmail.bind(undefined, email)}>
|
||||
{t("copy_email")}
|
||||
@@ -92,7 +91,7 @@ const Profile: FC<Props> = ({ uid, type = "embed", cid }) => {
|
||||
{t("remove")}
|
||||
</li>
|
||||
)}
|
||||
</StyledMenu>
|
||||
</ul>
|
||||
}
|
||||
>
|
||||
<li className={`${iconClass} icon ${hasMore ? "" : "text-gray-500"}`}>
|
||||
|
||||
@@ -29,14 +29,14 @@ export const ReactionMap = {
|
||||
"🚀": ":rocket:",
|
||||
};
|
||||
const emojis: Emojis = {
|
||||
"👍": <EmojiThumbUp className="emoji" />,
|
||||
"👎": <EmojiThumbDown className="emoji" />,
|
||||
"😄": <EmojiSmile className="emoji" />,
|
||||
"👀": <EmojiLook className="emoji" />,
|
||||
"🚀": <EmojiRocket className="emoji" />,
|
||||
"❤️": <EmojiHeart className="emoji" />,
|
||||
"🙁": <EmojiUnhappy className="emoji" />,
|
||||
"🎉": <EmojiCelebrate className="emoji" />
|
||||
"👍": <EmojiThumbUp className="emoji w-full h-full min-w-[16px] min-h-[16px]" />,
|
||||
"👎": <EmojiThumbDown className="emoji w-full h-full min-w-[16px] min-h-[16px]" />,
|
||||
"😄": <EmojiSmile className="emoji w-full h-full min-w-[16px] min-h-[16px]" />,
|
||||
"👀": <EmojiLook className="emoji w-full h-full min-w-[16px] min-h-[16px]" />,
|
||||
"🚀": <EmojiRocket className="emoji w-full h-full min-w-[16px] min-h-[16px]" />,
|
||||
"❤️": <EmojiHeart className="emoji w-full h-full min-w-[16px] min-h-[16px]" />,
|
||||
"🙁": <EmojiUnhappy className="emoji w-full h-full min-w-[16px] min-h-[16px]" />,
|
||||
"🎉": <EmojiCelebrate className="emoji w-full h-full min-w-[16px] min-h-[16px]" />
|
||||
};
|
||||
|
||||
interface Props {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import styled from "styled-components";
|
||||
import { ContentTypes } from "../../../app/config";
|
||||
import MarkdownRender from "../MarkdownRender";
|
||||
import closeIcon from "../../../assets/icons/close.circle.svg?url";
|
||||
@@ -9,73 +8,6 @@ import { useAppSelector } from "../../../app/store";
|
||||
import { MessagePayload } from "../../../app/slices/message";
|
||||
import LinkifyText from "../LinkifyText";
|
||||
|
||||
const Styled = styled.div`
|
||||
background-color: #f3f4f6;
|
||||
z-index: 999;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-start;
|
||||
gap: 16px;
|
||||
border-top-left-radius: 8px;
|
||||
border-top-right-radius: 8px;
|
||||
width: 100%;
|
||||
padding: 12px 16px;
|
||||
.prefix {
|
||||
white-space: nowrap;
|
||||
color: #667085;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
em {
|
||||
font-weight: bold;
|
||||
color: #363f53;
|
||||
}
|
||||
}
|
||||
.content {
|
||||
white-space: normal;
|
||||
color: #616161;
|
||||
overflow: hidden;
|
||||
padding-right: 30px;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
> .pic {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
object-fit: cover;
|
||||
}
|
||||
.md {
|
||||
position: relative;
|
||||
max-height: 100px;
|
||||
overflow: hidden;
|
||||
&:after {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
content: "";
|
||||
background: linear-gradient(180deg, rgba(255, 255, 255, 0) 63.54%, #f3f4f6 93.09%);
|
||||
}
|
||||
}
|
||||
.icon {
|
||||
width: 15px;
|
||||
height: 20px;
|
||||
}
|
||||
.name {
|
||||
margin-left: 5px;
|
||||
font-size: 10px;
|
||||
color: #555;
|
||||
}
|
||||
}
|
||||
.close {
|
||||
background: none;
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
right: 16px;
|
||||
}
|
||||
`;
|
||||
const renderContent = (data: MessagePayload) => {
|
||||
const { content_type, content, thumbnail = "", properties } = data;
|
||||
let res = null;
|
||||
@@ -87,9 +19,10 @@ const renderContent = (data: MessagePayload) => {
|
||||
// return <Mention popover={false} key={idx} uid={+uid} />;
|
||||
// });
|
||||
break;
|
||||
|
||||
case ContentTypes.markdown:
|
||||
res = (
|
||||
<div className="md">
|
||||
<div className="max-h-[100px] overflow-auto">
|
||||
<MarkdownRender content={content} />
|
||||
</div>
|
||||
);
|
||||
@@ -100,13 +33,13 @@ const renderContent = (data: MessagePayload) => {
|
||||
const image = isImage(content_type, size);
|
||||
// console.log("replying data", content_type, size, image);
|
||||
if (image) {
|
||||
res = <img className="pic" src={thumbnail || pictureIcon} />;
|
||||
res = <img className="w-10 h-10 object-cover" src={thumbnail || pictureIcon} />;
|
||||
} else {
|
||||
const icon = getFileIcon(content_type, name);
|
||||
const icon = getFileIcon(content_type, name, "icon w-4 h-5");
|
||||
res = (
|
||||
<>
|
||||
{icon}
|
||||
<span className="name">{name}</span>
|
||||
<span className="ml-1 text-[10px] text-gray-400">{name}</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -118,7 +51,6 @@ const renderContent = (data: MessagePayload) => {
|
||||
// console.log("replying data", data);
|
||||
return res;
|
||||
};
|
||||
|
||||
export default function Replying({
|
||||
context,
|
||||
id,
|
||||
@@ -138,15 +70,16 @@ export default function Replying({
|
||||
if (!msg) return null;
|
||||
const { from_uid = 0 } = msg;
|
||||
const user = usersData[from_uid];
|
||||
|
||||
return (
|
||||
<Styled className="reply">
|
||||
<div className="prefix">
|
||||
Replying to <em>{user?.name}</em>
|
||||
<div className="reply bg-gray-100 z-[999] flex items-start justify-start gap-4 rounded-t-lg w-full px-4 py-3 text-sm">
|
||||
<div className="whitespace-nowrap text-gray-400 ">
|
||||
Replying to <span className="font-bold text-gray-600">{user?.name}</span>
|
||||
</div>
|
||||
<div className="content">{renderContent(msg)}</div>
|
||||
<button className="close" onClick={removeReply}>
|
||||
<div className="text-gray-500 overflow-hidden pr-7 ">{renderContent(msg)}</div>
|
||||
<button className="absolute top-4 right-4 cursor-pointer" onClick={removeReply}>
|
||||
<img src={closeIcon} alt="close icon" />
|
||||
</button>
|
||||
</Styled>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { useEffect, useState, FC } from "react";
|
||||
import clsx from "clsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import useSendMessage from "../../hook/useSendMessage";
|
||||
import useAddLocalFileMessage from "../../hook/useAddLocalFileMessage";
|
||||
import { updateInputMode } from "../../../app/slices/ui";
|
||||
import { ContentTypes, ChatPrefixes } from "../../../app/config";
|
||||
|
||||
import StyledSend from "./styled";
|
||||
// import StyledSend from "./styled";
|
||||
import UploadFileList from "./UploadFileList";
|
||||
import Replying from "./Replying";
|
||||
import Toolbar from "./Toolbar";
|
||||
@@ -15,7 +17,6 @@ import MixedInput, { useMixedEditor } from "../MixedInput";
|
||||
import useDraft from "../../hook/useDraft";
|
||||
import useUploadFile from "../../hook/useUploadFile";
|
||||
import { useAppDispatch, useAppSelector } from "../../../app/store";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const Modes = {
|
||||
text: "text",
|
||||
@@ -135,15 +136,16 @@ const Send: FC<IProps> = ({
|
||||
const placeholder = `${t("send_to")} ${ChatPrefixes[context]}${name} `;
|
||||
const members =
|
||||
context == "channel" ? (channelsData[id]?.is_public ? uids : channelsData[id]?.members) : [];
|
||||
const isMarkdownMode = mode == Modes.markdown;
|
||||
return (
|
||||
<StyledSend
|
||||
className={`send dark:bg-[#4D5761] ${mode} ${markdownFullscreen ? "fullscreen" : ""} ${replying_mid ? "reply" : ""
|
||||
} ${context}`}
|
||||
<div
|
||||
className={clsx(`send relative bg-gray-200 rounded-lg w-full dark:bg-[#4D5761] ${mode} ${markdownFullscreen ? "fullscreen" : ""} ${replying_mid ? "reply" : ""
|
||||
} ${context}`, isMarkdownMode && markdownFullscreen && '-mt-9')}
|
||||
>
|
||||
{replying_mid && <Replying context={context} mid={replying_mid} id={id} />}
|
||||
{mode == Modes.text && <UploadFileList context={context} id={id} />}
|
||||
|
||||
<div className={`send_box ${mode}`}>
|
||||
<div className={clsx(`flex justify-between items-start gap-4 px-4 py-3.5`, isMarkdownMode && `grid grid-cols-[1fr_1fr] grid-rows-[auto_auto] gap-0`)}>
|
||||
<EmojiPicker selectEmoji={insertEmoji} />
|
||||
{mode == Modes.text && (
|
||||
<MixedInput
|
||||
@@ -174,7 +176,7 @@ const Send: FC<IProps> = ({
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</StyledSend>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
import styled from "styled-components";
|
||||
|
||||
const StyledSend = styled.div`
|
||||
position: relative;
|
||||
background: #e5e7eb;
|
||||
border-radius: var(--br);
|
||||
width: 100%;
|
||||
width: -webkit-fill-available;
|
||||
&.markdown.fullscreen {
|
||||
margin-top: -35px;
|
||||
}
|
||||
.send_box {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 15px;
|
||||
padding: 14px 18px;
|
||||
&.markdown {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-template-rows: auto auto;
|
||||
gap: 0;
|
||||
.input {
|
||||
grid-column: span 2;
|
||||
}
|
||||
}
|
||||
&.reply {
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
}
|
||||
.input {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledSend;
|
||||
@@ -1,63 +1,26 @@
|
||||
import styled from "styled-components";
|
||||
|
||||
const StyledButton = styled.button`
|
||||
cursor: pointer;
|
||||
padding: 10px 14px;
|
||||
border: none;
|
||||
box-sizing: border-box;
|
||||
box-shadow: 0 1px 2px rgba(16, 24, 40, 0.05);
|
||||
border-radius: var(--br, 4px);
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
color: #fff;
|
||||
background-color: #22ccee;
|
||||
word-break: keep-all;
|
||||
&.flex {
|
||||
width: 100%;
|
||||
}
|
||||
&:hover,
|
||||
&:active {
|
||||
background-color: #06aed4;
|
||||
}
|
||||
&:focus {
|
||||
background-color: #22ccee;
|
||||
}
|
||||
&:disabled {
|
||||
background-color: #d0d5dd;
|
||||
}
|
||||
&.small {
|
||||
padding: 8px 14px;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
}
|
||||
&.mini {
|
||||
padding: 4px 10px;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
}
|
||||
&.danger {
|
||||
border: none;
|
||||
background-color: #ef4444;
|
||||
color: #fff;
|
||||
&:disabled {
|
||||
background-color: #fecdca;
|
||||
}
|
||||
}
|
||||
&.ghost {
|
||||
border: 1px solid #1fe1f9;
|
||||
background: none;
|
||||
color: #1fe1f9;
|
||||
}
|
||||
&.border_less {
|
||||
box-shadow: none;
|
||||
border: none !important;
|
||||
}
|
||||
&.cancel {
|
||||
border: 1px solid #e5e7eb;
|
||||
background: none;
|
||||
color: #374151;
|
||||
}
|
||||
`;
|
||||
import clsx from "clsx";
|
||||
import { ButtonHTMLAttributes, ReactNode } from "react";
|
||||
|
||||
type Props = ButtonHTMLAttributes<HTMLButtonElement> & { children?: ReactNode }
|
||||
const StyledButton = ({ children, className = '', ...rest }: Props) => {
|
||||
const isGhost = className.includes('ghost');
|
||||
const noBorder = className.includes('border_less');
|
||||
const isCancel = className.includes('cancel');
|
||||
const isDanger = className.includes('danger');
|
||||
const isSmall = className.includes('small');
|
||||
const isMini = className.includes('mini');
|
||||
const isFull = className.includes('flex');
|
||||
return <button className={clsx(`text-sm text-white bg-primary-400 break-keep shadow rounded-lg box-border px-3.5 py-2.5 hover:bg-primary-500 active:bg-primary-500 disabled:bg-gray-300`,
|
||||
isFull && "w-full",
|
||||
isGhost && "text-primary-400 border border-primary-400 !bg-transparent",
|
||||
isCancel && "!bg-transparent text-gray-800 dark:text-gray-50 border border-gray-200",
|
||||
isSmall && "!py-2",
|
||||
noBorder && "!shadow-none border-none",
|
||||
isMini && "!px-2.5 !py-1 !text-xs",
|
||||
isDanger && "bg-red-700 disabled:bg-gray-300 hover:bg-red-700/80",
|
||||
className
|
||||
)} {...rest}>
|
||||
{children}
|
||||
</button>;
|
||||
};
|
||||
export default StyledButton;
|
||||
|
||||
@@ -1,78 +0,0 @@
|
||||
import styled from "styled-components";
|
||||
|
||||
const StyledMenu = styled.ul`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
padding: 4px;
|
||||
background-color: #fff;
|
||||
box-shadow: 0 20px 25px 20px rgba(31, 41, 55, 0.1), 0 10px 10px rgba(31, 41, 55, 0.04);
|
||||
border-radius: 12px;
|
||||
min-width: 200px;
|
||||
.item {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
border-radius: 6px;
|
||||
padding: 6px;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
color: #616161;
|
||||
.icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
path {
|
||||
fill: #475467;
|
||||
}
|
||||
}
|
||||
&.sb {
|
||||
justify-content: space-between;
|
||||
}
|
||||
&:hover {
|
||||
background-color: #22ccee;
|
||||
color: #fff;
|
||||
.icon {
|
||||
path {
|
||||
fill: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
&.bottom_line {
|
||||
margin-bottom: 9px;
|
||||
&:before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
left: 6px;
|
||||
bottom: -4px;
|
||||
display: block;
|
||||
padding: 0 6px;
|
||||
box-sizing: border-box;
|
||||
width: calc(100% - 12px);
|
||||
height: 1px;
|
||||
background-color: #f2f4f7;
|
||||
}
|
||||
}
|
||||
&.danger {
|
||||
color: #a11043;
|
||||
&:hover {
|
||||
background-color: #b42318;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
&[data-disabled="true"] {
|
||||
color: #a4a8b3;
|
||||
.icon {
|
||||
path {
|
||||
fill: #a4a8b3;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledMenu;
|
||||
@@ -1,67 +1,5 @@
|
||||
import { useState, useId, FC } from "react";
|
||||
import styled from "styled-components";
|
||||
|
||||
const StyledForm = styled.form`
|
||||
width: 100%;
|
||||
> .option {
|
||||
&:not(:last-child) {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
> input[type="radio"] {
|
||||
display: none;
|
||||
|
||||
& + .box {
|
||||
background: #ffffff;
|
||||
border: 1px solid #d0d5dd;
|
||||
box-shadow: 0 1px 2px rgba(16, 24, 40, 0.05);
|
||||
border-radius: 8px;
|
||||
transition: all ease-in-out 250ms;
|
||||
|
||||
& > label {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
font-weight: 400;
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
color: #667085;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
transition: all ease-in-out 250ms;
|
||||
|
||||
&:before {
|
||||
content: "";
|
||||
display: inline-block;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 8px;
|
||||
background: #ffffff;
|
||||
box-shadow: inset 0 0 0 4px #ffffff;
|
||||
border: 1px solid #d0d5dd;
|
||||
margin: 14px 8px 14px 14px;
|
||||
transition: all ease-in-out 500ms;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:checked + .box {
|
||||
background: #22ccee;
|
||||
border: 1px solid #d0d5dd;
|
||||
|
||||
& > label {
|
||||
color: #ffffff;
|
||||
|
||||
&:before {
|
||||
background: #ffffff;
|
||||
box-shadow: inset 0 0 0 4px #22ccee;
|
||||
border: 1px solid #ffffff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
type Props = {
|
||||
options: string[];
|
||||
values: (string | number)[];
|
||||
@@ -84,12 +22,12 @@ const Radio: FC<Props> = ({
|
||||
|
||||
const [fallbackValue, setFallbackValue] = useState(defaultValue);
|
||||
const _value = value !== VALUE_NOT_SET ? value : fallbackValue;
|
||||
|
||||
return (
|
||||
<StyledForm>
|
||||
<form className="w-full flex flex-col gap-2">
|
||||
{options.map((item, index) => (
|
||||
<div className="option" key={index}>
|
||||
<div className="relative bg-transparent" key={index}>
|
||||
<input
|
||||
className="absolute top-0 left-0 w-full h-full opacity-0 cursor-pointer peer z-50"
|
||||
type="radio"
|
||||
checked={(values !== VALUES_NOT_SET ? values.indexOf(_value) : _value) === index}
|
||||
onChange={() => {
|
||||
@@ -105,12 +43,16 @@ const Radio: FC<Props> = ({
|
||||
}}
|
||||
id={`${id}-${index}`}
|
||||
/>
|
||||
<div className="box">
|
||||
<label htmlFor={`${id}-${index}`}>{item}</label>
|
||||
<div className="drop-shadow px-2 py-3 border border-solid border-[#d0d5dd] rounded-lg w-full h-full bg-white peer-checked:bg-primary-400 text-sm text-gray-500 peer-checked:text-white">
|
||||
<label className="ml-6" htmlFor={`${id}-${index}`}>{item}</label>
|
||||
</div>
|
||||
<div className="absolute top-1/2 left-3 -translate-y-1/2 w-3.5 h-3.5 rounded-full border border-gray-400 peer-checked:hidden"></div>
|
||||
<div className="absolute top-1/2 left-3 -translate-y-1/2 w-3.5 h-3.5 rounded-full border border-white invisible peer-checked:visible flex-center">
|
||||
<div className="w-2 h-2 bg-white rounded-full"></div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</StyledForm>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
export default Radio;
|
||||
|
||||
@@ -2,7 +2,6 @@ import { FC, useState } from "react";
|
||||
import Tippy from "@tippyjs/react";
|
||||
import IconSelect from "../../../assets/icons/check.sign.svg";
|
||||
import IconArrow from "../../../assets/icons/arrow.down.svg";
|
||||
import Menu from "./Menu";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
|
||||
@@ -42,7 +41,7 @@ const Select: FC<Props> = ({ options = [], updateSelect = null, current = null }
|
||||
placement="bottom"
|
||||
interactive
|
||||
content={
|
||||
<Menu>
|
||||
<ul className="context-menu">
|
||||
{options.map(({ title, value, selected, underline }) => {
|
||||
return (
|
||||
<li
|
||||
@@ -56,7 +55,7 @@ const Select: FC<Props> = ({ options = [], updateSelect = null, current = null }
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</Menu>
|
||||
</ul>
|
||||
}
|
||||
>
|
||||
<div className="select-none border border-solid border-slate-200 p-2 flex items-center gap-2" onClick={toggleVisible}>
|
||||
|
||||
@@ -1,36 +1,12 @@
|
||||
import styled from "styled-components";
|
||||
|
||||
const StyledToggle = styled.div`
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
width: 44px;
|
||||
height: 24px;
|
||||
background-color: #1fe1f9;
|
||||
border-radius: 12px;
|
||||
transition: all 0.2s ease-in;
|
||||
&:after {
|
||||
border-radius: 50%;
|
||||
background-color: #fff;
|
||||
content: "";
|
||||
display: block;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
right: 2px;
|
||||
transition: all 0.4s ease;
|
||||
}
|
||||
&[data-checked="false"] {
|
||||
background-color: #f2f4f7;
|
||||
&:after {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
}
|
||||
&[data-disabled="true"] {
|
||||
cursor: not-allowed;
|
||||
background-color: #ccc;
|
||||
pointer-events: none;
|
||||
}
|
||||
`;
|
||||
|
||||
import clsx from "clsx";
|
||||
import { HTMLAttributes } from "react";
|
||||
const StyledToggle = (props: Pick<HTMLAttributes<HTMLDivElement>, "onClick"> & { checked?: boolean, disabled?: boolean }) => {
|
||||
const { checked = true, disabled = false } = props;
|
||||
return <div
|
||||
{...props}
|
||||
className={clsx(`cursor-pointer relative w-11 h-6 rounded-xl`, checked ? 'bg-primary-400' : 'bg-gray-300', disabled && "cursor-not-allowed bg-gray-400 pointer-events-none")}
|
||||
>
|
||||
<div className={clsx("rounded-full bg-white w-5 h-5 absolute top-0.5 right-0.5 transition-all", !checked && "-translate-x-full")}></div>
|
||||
</div>;
|
||||
};
|
||||
export default StyledToggle;
|
||||
|
||||
Reference in New Issue
Block a user