improve component layout

This commit is contained in:
SrGooglo 2023-03-07 02:10:26 +00:00
parent 38e88b48a7
commit 63ec7daa7a
9 changed files with 288 additions and 239 deletions

View File

@ -1,5 +1,6 @@
import React from "react" import React from "react"
import * as antd from "antd" import * as antd from "antd"
import classnames from "classnames"
import moment from "moment" import moment from "moment"
import { Icons } from "components/Icons" import { Icons } from "components/Icons"
@ -48,11 +49,10 @@ const CommentCard = (props) => {
} }
export default (props) => { export default (props) => {
const [visible, setVisible] = React.useState(props.visible ?? true)
const [comments, setComments] = React.useState(null) const [comments, setComments] = React.useState(null)
const fetchData = async () => { const fetchData = async () => {
setComments(null)
// fetch comments // fetch comments
const commentsResult = await PostModel.getPostComments({ const commentsResult = await PostModel.getPostComments({
post_id: props.post_id post_id: props.post_id
@ -123,9 +123,19 @@ export default (props) => {
} }
React.useEffect(() => { React.useEffect(() => {
fetchData() setVisible(props.visible)
listenEvents() }, [props.visible])
React.useEffect(() => {
if (visible) {
fetchData()
listenEvents()
} else {
unlistenEvents()
}
}, [visible])
React.useEffect(() => {
return () => { return () => {
unlistenEvents() unlistenEvents()
} }
@ -149,17 +159,23 @@ export default (props) => {
}) })
} }
if (!comments) { return <div
return <antd.Skeleton active /> id="comments"
} className={classnames(
"comments",
return <div className="comments"> {
"hidden": !visible
}
)}
>
<div className="header"> <div className="header">
<h3> <h3>
<Icons.MessageSquare /> Comments <Icons.MessageSquare /> Comments
</h3> </h3>
</div> </div>
{renderComments()} {renderComments()}
<div className="commentCreatorWrapper"> <div className="commentCreatorWrapper">
<CommentCreator <CommentCreator
onSubmit={handleCommentSubmit} onSubmit={handleCommentSubmit}

View File

@ -16,6 +16,18 @@
color: var(--text-color); color: var(--text-color);
} }
&.hidden {
height: 0px;
padding: 0px;
}
overflow: hidden;
width: 100%;
height: 100%;
transition: all 150ms ease-in-out;
.comment { .comment {
position: relative; position: relative;
display: flex; display: flex;

View File

@ -11,7 +11,7 @@ export default (props) => {
<Button <Button
type="ghost" type="ghost"
shape="circle" shape="circle"
onClick={props.onClickComments} onClick={props.onClick}
icon={<Icons.MessageCircle />} icon={<Icons.MessageCircle />}
/> />
{ {

View File

@ -8,16 +8,36 @@ import CommentsButton from "./commentsButton"
import "./index.less" import "./index.less"
const SelfActionsItems = [
{
key: "onClickEdit",
label: <>
<Icons.Edit />
<span>Edit</span>
</>,
},
{
key: "onClickDelete",
label: <>
<Icons.Trash />
<span>Delete</span>
</>,
},
{
type: "divider",
},
]
const MoreActionsItems = [ const MoreActionsItems = [
{ {
key: "repost", key: "onClickRepost",
label: <> label: <>
<Icons.Repeat /> <Icons.Repeat />
<span>Repost</span> <span>Repost</span>
</>, </>,
}, },
{ {
key: "share", key: "onClickShare",
label: <> label: <>
<Icons.Share /> <Icons.Share />
<span>Share</span> <span>Share</span>
@ -27,7 +47,7 @@ const MoreActionsItems = [
type: "divider", type: "divider",
}, },
{ {
key: "report", key: "onClickReport",
label: <> label: <>
<Icons.AlertTriangle /> <Icons.AlertTriangle />
<span>Report</span> <span>Report</span>
@ -36,33 +56,68 @@ const MoreActionsItems = [
] ]
export default (props) => { export default (props) => {
const [isSelf, setIsSelf] = React.useState(false)
const {
onClickLike,
onClickSave,
onClickComments,
} = props.actions ?? {}
const genItems = () => {
let items = MoreActionsItems
if (isSelf) {
items = [...SelfActionsItems, ...items]
}
return items
}
const handleDropdownClickItem = (e) => {
if (typeof props.actions[e.key] === "function") {
props.actions[e.key]()
}
}
return <div className="post_actions_wrapper"> return <div className="post_actions_wrapper">
<div className="actions"> <div className="actions">
<div className="action" id="likes"> <div className="action" id="likes">
<LikeButton <LikeButton
defaultLiked={props.defaultLiked} defaultLiked={props.defaultLiked}
onClick={props.onClickLike}
count={props.likesCount} count={props.likesCount}
onClick={onClickLike}
/> />
</div> </div>
<div className="action" id="save"> <div className="action" id="save">
<SaveButton <SaveButton
defaultActive={props.defaultSaved} defaultActive={props.defaultSaved}
onClick={props.onClickSave} onClick={onClickSave}
/> />
</div> </div>
<div className="action" id="comments"> <div className="action" id="comments">
<CommentsButton <CommentsButton
onClickComments={props.onClickComments}
count={props.commentsCount} count={props.commentsCount}
onClick={onClickComments}
/> />
</div> </div>
<div className="action" id="more"> <div className="action" id="more">
<Dropdown <Dropdown
menu={{ menu={{
items: MoreActionsItems items: genItems(),
onClick: handleDropdownClickItem,
}} }}
trigger={["click"]} trigger={["click"]}
onOpenChange={(open) => {
if (open && props.user_id) {
const isSelf = app.cores.permissions.checkUserIdIsSelf(props.user_id)
setIsSelf(isSelf)
}
}}
overlayStyle={{
minWidth: "200px",
}}
> >
<div className="icon"> <div className="icon">
<Icons.MoreHorizontal /> <Icons.MoreHorizontal />

View File

@ -124,7 +124,7 @@ const Attachment = React.memo((props) => {
} }
}) })
export default (props) => { export default React.memo((props) => {
const carouselRef = React.useRef(null) const carouselRef = React.useRef(null)
const [attachmentIndex, setAttachmentIndex] = React.useState(0) const [attachmentIndex, setAttachmentIndex] = React.useState(0)
@ -174,9 +174,11 @@ export default (props) => {
return null return null
} }
return <Attachment index={index} attachment={attachment} /> return <React.Fragment key={index}>
<Attachment index={index} attachment={attachment} />
</React.Fragment>
}) })
} }
</Carousel> </Carousel>
</div> </div>
} })

View File

@ -1,75 +0,0 @@
import React from "react"
import * as antd from "antd"
import Plyr from "plyr-react"
import classnames from "classnames"
import { processString } from "utils"
import "./index.less"
export default (props) => {
let { message } = props.data
const [nsfwAccepted, setNsfwAccepted] = React.useState(false)
// parse message
const regexs = [
{
regex: /https?:\/\/(?:www\.)?youtube\.com\/watch\?v=([a-zA-Z0-9_-]{11})(&[a-zA-Z0-9_-]+=[a-zA-Z0-9_-]+)*/g,
fn: (key, result) => {
return <Plyr source={{
type: "video",
sources: [{
src: result[1],
provider: "youtube",
}],
}} />
}
},
{
regex: /(https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|www\.[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9]+\.[^\s]{2,}|www\.[a-zA-Z0-9]+\.[^\s]{2,})/gi,
fn: (key, result) => {
return <a key={key} href={result[1]} target="_blank" rel="noopener noreferrer">{result[1]}</a>
}
},
{
regex: /(@[a-zA-Z0-9_]+)/gi,
fn: (key, result) => {
return <a key={key} onClick={() => window.app.setLocation(`/@${result[1].substr(1)}`)}>{result[1]}</a>
}
},
]
message = processString(regexs)(message)
return <div
className={
classnames(
"post_content",
{
["nsfw"]: props.nsfw && !nsfwAccepted
}
)
}
>
{props.nsfw && !nsfwAccepted &&
<div className="nsfw_alert">
<h2>
This post may contain sensitive content.
</h2>
<antd.Button onClick={() => setNsfwAccepted(true)}>
Show anyways
</antd.Button>
</div>
}
<div className="message">
{message}
</div>
{
props.children
}
</div>
}

View File

@ -1,81 +0,0 @@
.post_content {
position: relative;
display: inline-flex;
flex-direction: column;
align-items: flex-start;
padding: 0 10px 10px 10px;
border-radius: 8px;
color: rgba(var(--background-color-contrast));
overflow: hidden;
transition: all 0.2s ease-in-out;
z-index: 190;
h1,
h2,
h3,
h4,
h5,
h6,
p,
span {
color: var(--text-color);
}
&.nsfw {
.message {
filter: blur(10px);
}
.post_attachments {
filter: blur(25px);
}
}
.nsfw_alert {
position: absolute;
padding: 10px 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
// -webkit-backdrop-filter: blur(25px);
// backdrop-filter: blur(25px);
//background-color: rgba(0, 0, 0, 0.5);
border-radius: 8px;
z-index: 200;
h2 {
color: #fff;
}
}
.message {
width: 100%;
font-size: 0.9rem;
font-weight: 400;
color: var(--background-color-contrast);
word-break: break-all;
user-select: text;
}
>div {
margin-bottom: 10px;
}
}

View File

@ -1,22 +1,48 @@
import React from "react" import React from "react"
import * as antd from "antd" import * as antd from "antd"
import classnames from "classnames" import classnames from "classnames"
import Plyr from "plyr-react"
import { CommentsCard } from "components" import { CommentsCard } from "components"
import { Icons } from "components/Icons" import { Icons } from "components/Icons"
import { processString } from "utils"
import PostHeader from "./components/header" import PostHeader from "./components/header"
import PostContent from "./components/content"
import PostActions from "./components/actions" import PostActions from "./components/actions"
import PostAttachments from "./components/attachments" import PostAttachments from "./components/attachments"
import "./index.less" import "./index.less"
const messageRegexs = [
{
regex: /https?:\/\/(?:www\.)?youtube\.com\/watch\?v=([a-zA-Z0-9_-]{11})(&[a-zA-Z0-9_-]+=[a-zA-Z0-9_-]+)*/g,
fn: (key, result) => {
return <Plyr source={{
type: "video",
sources: [{
src: result[1],
provider: "youtube",
}],
}} />
}
},
{
regex: /(https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|www\.[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9]+\.[^\s]{2,}|www\.[a-zA-Z0-9]+\.[^\s]{2,})/gi,
fn: (key, result) => {
return <a key={key} href={result[1]} target="_blank" rel="noopener noreferrer">{result[1]}</a>
}
},
{
regex: /(@[a-zA-Z0-9_]+)/gi,
fn: (key, result) => {
return <a key={key} onClick={() => window.app.setLocation(`/@${result[1].substr(1)}`)}>{result[1]}</a>
}
},
]
export default class PostCard extends React.PureComponent { export default class PostCard extends React.PureComponent {
state = { state = {
loading: true,
data: this.props.data ?? {},
countLikes: this.props.data.countLikes ?? 0, countLikes: this.props.data.countLikes ?? 0,
countComments: this.props.data.countComments ?? 0, countComments: this.props.data.countComments ?? 0,
@ -24,6 +50,9 @@ export default class PostCard extends React.PureComponent {
hasSaved: this.props.data.isSaved ?? false, hasSaved: this.props.data.isSaved ?? false,
open: this.props.defaultOpened ?? false, open: this.props.defaultOpened ?? false,
isNsfw: this.props.data.flags?.includes("nsfw") ?? false,
nsfwAccepted: false,
} }
onClickDelete = async () => { onClickDelete = async () => {
@ -32,7 +61,7 @@ export default class PostCard extends React.PureComponent {
return return
} }
return await this.props.events.onClickDelete(this.state.data) return await this.props.events.onClickDelete(this.props.data)
} }
onClickLike = async () => { onClickLike = async () => {
@ -41,7 +70,7 @@ export default class PostCard extends React.PureComponent {
return return
} }
return await this.props.events.onClickLike(this.state.data) return await this.props.events.onClickLike(this.props.data)
} }
onClickSave = async () => { onClickSave = async () => {
@ -50,7 +79,7 @@ export default class PostCard extends React.PureComponent {
return return
} }
return await this.props.events.onClickSave(this.state.data) return await this.props.events.onClickSave(this.props.data)
} }
onClickEdit = async () => { onClickEdit = async () => {
@ -59,7 +88,7 @@ export default class PostCard extends React.PureComponent {
return return
} }
return await this.props.events.onClickEdit(this.state.data) return await this.props.events.onClickEdit(this.props.data)
} }
onDoubleClick = async () => { onDoubleClick = async () => {
@ -76,17 +105,19 @@ export default class PostCard extends React.PureComponent {
} }
if (typeof this.props.events?.onToogleOpen === "function") { if (typeof this.props.events?.onToogleOpen === "function") {
this.props.events?.onToogleOpen(to, this.state.data) this.props.events?.onToogleOpen(to, this.props.data)
} }
this.setState({ this.setState({
open: to, open: to,
}) })
//app.controls.openPostViewer(this.state.data) //app.controls.openPostViewer(this.props.data)
} }
onLikesUpdate = (data) => { onLikesUpdate = (data) => {
console.log("onLikesUpdate", data)
if (data.to) { if (data.to) {
this.setState({ this.setState({
countLikes: this.state.countLikes + 1, countLikes: this.state.countLikes + 1,
@ -99,24 +130,13 @@ export default class PostCard extends React.PureComponent {
} }
componentDidMount = async () => { componentDidMount = async () => {
app.eventBus.on(`post.${this.state.data._id}.delete`, this.onClickDelete)
app.eventBus.on(`post.${this.state.data._id}.update`, this.onClickEdit)
// first listen to post changes // first listen to post changes
app.cores.api.listenEvent(`post.${this.state.data._id}.likes.update`, this.onLikesUpdate) app.cores.api.listenEvent(`post.${this.props.data._id}.likes.update`, this.onLikesUpdate)
this.setState({
isSelf: app.cores.permissions.checkUserIdIsSelf(this.state.data.user_id),
loading: false,
})
} }
componentWillUnmount = () => { componentWillUnmount = () => {
app.eventBus.off(`post.${this.state.data._id}.delete`, this.onClickDelete)
app.eventBus.off(`post.${this.state.data._id}.update`, this.onClickEdit)
// remove the listener // remove the listener
app.cores.api.unlistenEvent(`post.${this.state.data._id}.likes.update`, this.onLikesUpdate) app.cores.api.unlistenEvent(`post.${this.props.data._id}.likes.update`, this.onLikesUpdate)
} }
componentDidCatch = (error, info) => { componentDidCatch = (error, info) => {
@ -133,62 +153,81 @@ export default class PostCard extends React.PureComponent {
</div> </div>
} }
render = () => { render() {
if (this.state.loading) {
return <div
key={this.state.data.key ?? this.state.data._id}
id={this.state.data._id}
className="postCard"
>
<antd.Skeleton active avatar />
</div>
}
return <div return <div
key={this.props.index ?? this.state.data._id} key={this.props.index}
id={this.state.data._id} id={this.props.data._id}
className={classnames( className={classnames(
"postCard", "postCard",
{ {
["open"]: this.state.open, ["open"]: this.state.open,
} }
)} )}
style={this.props.style}
context-menu={"postCard-context"} context-menu={"postCard-context"}
user-id={this.state.data.user_id} user-id={this.props.data.user_id}
self-post={this.state.isSelf.toString()}
> >
<div className="wrapper"> <div className="wrapper">
<PostHeader <PostHeader
postData={this.state.data} postData={this.props.data}
isLiked={this.state.hasLiked}
onDoubleClick={this.onDoubleClick} onDoubleClick={this.onDoubleClick}
/> />
<PostContent <div
data={this.state.data} id="post_content"
nsfw={this.state.data.flags && this.state.data.flags.includes("nsfw")} className={classnames(
onDoubleClick={this.onDoubleClick} "post_content",
{
["nsfw"]: this.state.isNsfw && !this.state.nsfwAccepted,
}
)}
> >
{this.state.data.attachments && this.state.data.attachments.length > 0 && <PostAttachments {
attachments={this.state.data.attachments} this.state.isNsfw && !this.state.nsfwAccepted &&
/>} <div className="nsfw_alert">
</PostContent> <h2>
This post may contain sensitive content.
</h2>
<antd.Button onClick={() => this.setState({ nsfwAccepted: true })}>
Show anyways
</antd.Button>
</div>
}
<div className="message">
{
processString(messageRegexs)(this.props.data.message ?? "")
}
</div>
{
this.props.data.attachments && this.props.data.attachments.length > 0 && <PostAttachments
attachments={this.props.data.attachments}
/>
}
</div>
</div> </div>
<PostActions <PostActions
user_id={this.props.data.user_id}
likesCount={this.state.countLikes} likesCount={this.state.countLikes}
commentsCount={this.state.countComments} commentsCount={this.state.countComments}
defaultLiked={this.state.hasLiked} defaultLiked={this.state.hasLiked}
defaultSaved={this.state.hasSaved} defaultSaved={this.state.hasSaved}
onClickLike={this.onClickLike}
onClickSave={this.onClickSave}
onClickComments={this.onClickComments}
actions={{ actions={{
delete: this.onClickDelete, onClickLike: this.onClickLike,
onClickEdit: this.onClickEdit,
onClickDelete: this.onClickDelete,
onClickSave: this.onClickSave,
onClickComments: this.onClickComments,
}} }}
/> />
{ <CommentsCard
this.state.open && <CommentsCard post_id={this.state.data._id} /> post_id={this.props.data._id}
} visible={this.state.open}
/>
</div> </div>
} }
} }

View File

@ -1,19 +1,22 @@
.postCard { .postCard {
display: inline-flex; display: flex;
flex-direction: column; flex-direction: column;
width: 100%; width: 35vw;
min-width: 300px;
max-width: 600px; max-width: 600px;
background-color: var(--background-color-accent); background-color: var(--background-color-accent);
transition: all 0.2s ease-in-out; transition: all 150ms ease-in-out;
padding: 17px; padding: 17px;
&.open { border-bottom: 2px solid var(--border-color);
height: 100%;
} padding-bottom: 10px;
overflow: hidden;
.wrapper { .wrapper {
display: inline-flex; display: inline-flex;
@ -29,9 +32,87 @@
} }
} }
border-bottom: 2px solid var(--border-color); .post_content {
position: relative;
display: inline-flex;
flex-direction: column;
align-items: flex-start;
padding-bottom: 10px; padding: 0 10px 10px 10px;
border-radius: 8px;
color: rgba(var(--background-color-contrast));
overflow: hidden;
transition: all 0.2s ease-in-out;
z-index: 190;
h1,
h2,
h3,
h4,
h5,
h6,
p,
span {
color: var(--text-color);
}
&.nsfw {
.message {
filter: blur(10px);
}
.post_attachments {
filter: blur(25px);
}
}
.nsfw_alert {
position: absolute;
padding: 10px 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
// -webkit-backdrop-filter: blur(25px);
// backdrop-filter: blur(25px);
//background-color: rgba(0, 0, 0, 0.5);
border-radius: 8px;
z-index: 200;
h2 {
color: #fff;
}
}
.message {
width: 100%;
font-size: 0.9rem;
font-weight: 400;
color: var(--background-color-contrast);
word-break: break-all;
user-select: text;
}
>div {
margin-bottom: 10px;
}
}
&:first-child { &:first-child {
border-top-left-radius: 8px; border-top-left-radius: 8px;