mirror of
https://github.com/ragestudio/comty.git
synced 2025-06-09 10:34:17 +00:00
fix depecrated deps
This commit is contained in:
parent
79f64eaec2
commit
3cf055c72c
@ -1,17 +1,18 @@
|
||||
import path from "path"
|
||||
|
||||
export default {
|
||||
"@": path.join(__dirname, "src"),
|
||||
"@config": path.join(__dirname, "config"),
|
||||
"@cores": path.join(__dirname, "src/cores"),
|
||||
"@pages": path.join(__dirname, "src/pages"),
|
||||
"@styles": path.join(__dirname, "src/styles"),
|
||||
"@components": path.join(__dirname, "src/components"),
|
||||
"@contexts": path.join(__dirname, "src/contexts"),
|
||||
"@utils": path.join(__dirname, "src/utils"),
|
||||
"@layouts": path.join(__dirname, "src/layouts"),
|
||||
"@hooks": path.join(__dirname, "src/hooks"),
|
||||
"@classes": path.join(__dirname, "src/classes"),
|
||||
"@models": path.join(__dirname, "../../", "comty.js/src/models"),
|
||||
"comty.js": path.join(__dirname, "../../", "comty.js", "src"),
|
||||
}
|
||||
"@": path.join(__dirname, "src"),
|
||||
"@config": path.join(__dirname, "config"),
|
||||
"@cores": path.join(__dirname, "src/cores"),
|
||||
"@pages": path.join(__dirname, "src/pages"),
|
||||
"@styles": path.join(__dirname, "src/styles"),
|
||||
"@components": path.join(__dirname, "src/components"),
|
||||
"@contexts": path.join(__dirname, "src/contexts"),
|
||||
"@utils": path.join(__dirname, "src/utils"),
|
||||
"@layouts": path.join(__dirname, "src/layouts"),
|
||||
"@hooks": path.join(__dirname, "src/hooks"),
|
||||
"@classes": path.join(__dirname, "src/classes"),
|
||||
"@models": path.join(__dirname, "../../", "comty.js/src/models"),
|
||||
"comty.js": path.join(__dirname, "../../", "comty.js", "src"),
|
||||
"@ragestudio/vessel": path.join(__dirname, "../../", "vessel", "src"),
|
||||
}
|
||||
|
@ -3,6 +3,7 @@
|
||||
"version": "1.25.0-a",
|
||||
"license": "ComtyLicense",
|
||||
"main": "electron/main",
|
||||
"type": "module",
|
||||
"author": "RageStudio",
|
||||
"description": "A prototype of a social network.",
|
||||
"scripts": {
|
||||
@ -13,78 +14,61 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^5.4.0",
|
||||
"@capacitor/android": "^5.0.5",
|
||||
"@capacitor/app": "^5.0.3",
|
||||
"@capacitor/assets": "^2.0.4",
|
||||
"@capacitor/cli": "^5.0.5",
|
||||
"@capacitor/core": "^5.0.5",
|
||||
"@capacitor/haptics": "1.1.4",
|
||||
"@capacitor/splash-screen": "^5.0.4",
|
||||
"@capacitor/status-bar": "^5.0.4",
|
||||
"@capacitor/storage": "^1.2.5",
|
||||
"@capgo/capacitor-updater": "^5.0.1",
|
||||
"@dnd-kit/core": "^6.0.8",
|
||||
"@dnd-kit/sortable": "^7.0.2",
|
||||
"@emotion/react": "^11.13.0",
|
||||
"@emotion/styled": "^11.13.0",
|
||||
"@ffmpeg/ffmpeg": "^0.12.10",
|
||||
"@ffmpeg/util": "^0.12.1",
|
||||
"@loadable/component": "5.15.2",
|
||||
"@mui/material": "^5.11.9",
|
||||
"@ragestudio/cordova-nfc": "^1.2.0",
|
||||
"@ragestudio/vessel": "^0.18.1",
|
||||
"@ragestudio/vessel": "^0.19.0",
|
||||
"@sentry/browser": "^7.64.0",
|
||||
"@tauri-apps/api": "^1.5.4",
|
||||
"@tsmx/human-readable": "^1.0.7",
|
||||
"antd": "^5.20.6",
|
||||
"axios": "^1.7.7",
|
||||
"bear-react-carousel": "^4.0.10-alpha.0",
|
||||
"capacitor-music-controls-plugin-v3": "^1.1.0",
|
||||
"classnames": "2.3.1",
|
||||
"dashjs": "^4.7.4",
|
||||
"dompurify": "^3.0.0",
|
||||
"fast-average-color": "^9.2.0",
|
||||
"framer-motion": "^10.12.17",
|
||||
"fuse.js": "6.5.3",
|
||||
"hls.js": "^1.5.17",
|
||||
"howler": "2.2.3",
|
||||
"i18next": "21.6.6",
|
||||
"js-cookie": "3.0.1",
|
||||
"jsmediatags": "^3.9.7",
|
||||
"less": "4.1.2",
|
||||
"lottie-react": "^2.4.0",
|
||||
"luxon": "^3.0.4",
|
||||
"million": "^2.6.4",
|
||||
"mime": "^3.0.0",
|
||||
"moment": "2.29.4",
|
||||
"motion": "^12.4.2",
|
||||
"mpegts.js": "^1.6.10",
|
||||
"nprogress": "^0.2.0",
|
||||
"plyr": "^3.6.12",
|
||||
"plyr-react": "^3.2.1",
|
||||
"plyr": "^3.7.8",
|
||||
"prop-types": "^15.8.1",
|
||||
"react": "^18.2.0",
|
||||
"react": "18.3.1",
|
||||
"react-beautiful-dnd": "^13.1.1",
|
||||
"react-color": "2.19.3",
|
||||
"react-countup": "^6.4.1",
|
||||
"react-dom": "18.2.0",
|
||||
"react-dom": "18.3.1",
|
||||
"react-fast-marquee": "^1.3.5",
|
||||
"react-helmet": "6.1.0",
|
||||
"react-i18next": "11.15.3",
|
||||
"react-icons": "^4.8.0",
|
||||
"react-icons": "^5.4.0",
|
||||
"react-lazy-load-image-component": "^1.5.4",
|
||||
"react-markdown": "^8.0.3",
|
||||
"react-modal-image": "^2.6.0",
|
||||
"react-motion": "0.5.2",
|
||||
"react-rnd": "10.3.5",
|
||||
"react-player": "^2.16.0",
|
||||
"react-rnd": "^10.4.14",
|
||||
"react-router-dom": "^6.26.2",
|
||||
"react-transition-group": "^4.4.5",
|
||||
"react-useanimations": "^2.10.0",
|
||||
"realtime-bpm-analyzer": "^3.2.1",
|
||||
"remark-gfm": "^3.0.1",
|
||||
"rxjs": "^7.5.5",
|
||||
"store": "^2.0.12",
|
||||
"swapy": "^1.0.5",
|
||||
"ua-parser-js": "^1.0.36",
|
||||
"vaul": "^0.9.2",
|
||||
"vaul": "^1.1.2",
|
||||
"vite": "^5.4.4"
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ import "./patches"
|
||||
import config from "@config"
|
||||
|
||||
import React from "react"
|
||||
|
||||
import { Runtime } from "@ragestudio/vessel"
|
||||
import { Helmet } from "react-helmet"
|
||||
import { Translation } from "react-i18next"
|
||||
@ -10,10 +11,6 @@ import { invoke } from "@tauri-apps/api/tauri"
|
||||
import { Lightbox } from "react-modal-image"
|
||||
import * as antd from "antd"
|
||||
|
||||
import { StatusBar, Style } from "@capacitor/status-bar"
|
||||
import { App as CapacitorApp } from "@capacitor/app"
|
||||
import { CapacitorUpdater } from "@capgo/capacitor-updater"
|
||||
|
||||
import AppsMenu from "@components/AppMenu"
|
||||
|
||||
import AuthModel from "@models/auth"
|
||||
@ -41,9 +38,9 @@ import Splash from "./splash"
|
||||
|
||||
import "@styles/index.less"
|
||||
|
||||
if (IS_MOBILE_HOST) {
|
||||
CapacitorUpdater.notifyAppReady()
|
||||
}
|
||||
// if (IS_MOBILE_HOST) {
|
||||
// CapacitorUpdater.notifyAppReady()
|
||||
// }
|
||||
|
||||
class ComtyApp extends React.Component {
|
||||
constructor(props) {
|
||||
@ -66,8 +63,12 @@ class ComtyApp extends React.Component {
|
||||
window.app.message = antd.message
|
||||
window.app.isCapacitor = IS_MOBILE_HOST
|
||||
|
||||
if (window.app.version !== window.localStorage.getItem("last_version")) {
|
||||
app.message.info(`Comty has been updated to version ${window.app.version}!`)
|
||||
if (
|
||||
window.app.version !== window.localStorage.getItem("last_version")
|
||||
) {
|
||||
app.message.info(
|
||||
`Comty has been updated to version ${window.app.version}!`,
|
||||
)
|
||||
window.localStorage.setItem("last_version", window.app.version)
|
||||
}
|
||||
|
||||
@ -113,7 +114,7 @@ class ComtyApp extends React.Component {
|
||||
sessionController: this.sessionController,
|
||||
onDone: () => {
|
||||
app.layout.draggable.destroy("login")
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
},
|
||||
@ -129,19 +130,23 @@ class ComtyApp extends React.Component {
|
||||
props: {
|
||||
bodyStyle: {
|
||||
height: "100%",
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
},
|
||||
// Opens the notification window and sets up the UI for the notification to be displayed
|
||||
openNotifications: () => {
|
||||
window.app.layout.drawer.open("notifications", NotificationsCenter, {
|
||||
props: {
|
||||
width: "fit-content",
|
||||
window.app.layout.drawer.open(
|
||||
"notifications",
|
||||
NotificationsCenter,
|
||||
{
|
||||
props: {
|
||||
width: "fit-content",
|
||||
},
|
||||
allowMultiples: false,
|
||||
escClosable: true,
|
||||
},
|
||||
allowMultiples: false,
|
||||
escClosable: true,
|
||||
})
|
||||
)
|
||||
},
|
||||
openSearcher: (options) => {
|
||||
if (app.isMobile) {
|
||||
@ -150,34 +155,44 @@ class ComtyApp extends React.Component {
|
||||
componentProps: {
|
||||
renderResults: true,
|
||||
autoFocus: true,
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return app.layout.modal.open("searcher", (props) => <Searcher autoFocus renderResults {...props} />, {
|
||||
framed: false
|
||||
})
|
||||
return app.layout.modal.open(
|
||||
"searcher",
|
||||
(props) => <Searcher autoFocus renderResults {...props} />,
|
||||
{
|
||||
framed: false,
|
||||
},
|
||||
)
|
||||
},
|
||||
openMessages: () => {
|
||||
app.location.push("/messages")
|
||||
},
|
||||
openFullImageViewer: (src) => {
|
||||
app.cores.window_mng.render("image_lightbox", <Lightbox
|
||||
small={src}
|
||||
large={src}
|
||||
onClose={() => app.cores.window_mng.close("image_lightbox")}
|
||||
hideDownload
|
||||
showRotate
|
||||
/>)
|
||||
app.cores.window_mng.render(
|
||||
"image_lightbox",
|
||||
<Lightbox
|
||||
small={src}
|
||||
large={src}
|
||||
onClose={() =>
|
||||
app.cores.window_mng.close("image_lightbox")
|
||||
}
|
||||
hideDownload
|
||||
showRotate
|
||||
/>,
|
||||
)
|
||||
},
|
||||
openPostCreator: (params) => {
|
||||
app.layout.modal.open("post_creator", (props) => <PostCreator
|
||||
{...props}
|
||||
{...params}
|
||||
/>, {
|
||||
framed: false
|
||||
})
|
||||
}
|
||||
app.layout.modal.open(
|
||||
"post_creator",
|
||||
(props) => <PostCreator {...props} {...params} />,
|
||||
{
|
||||
framed: false,
|
||||
},
|
||||
)
|
||||
},
|
||||
},
|
||||
navigation: {
|
||||
reload: () => {
|
||||
@ -198,14 +213,16 @@ class ComtyApp extends React.Component {
|
||||
goToSettings: (setting_id) => {
|
||||
return app.location.push(`/settings`, {
|
||||
query: {
|
||||
setting: setting_id
|
||||
}
|
||||
setting: setting_id,
|
||||
},
|
||||
})
|
||||
},
|
||||
goToAccount: (username) => {
|
||||
if (!username) {
|
||||
if (!app.userData) {
|
||||
console.error("Cannot go to account, no username provided and no user logged in")
|
||||
console.error(
|
||||
"Cannot go to account, no username provided and no user logged in",
|
||||
)
|
||||
return false
|
||||
}
|
||||
|
||||
@ -219,27 +236,33 @@ class ComtyApp extends React.Component {
|
||||
},
|
||||
goToPlaylist: (playlist_id) => {
|
||||
return app.location.push(`/play/${playlist_id}`)
|
||||
}
|
||||
},
|
||||
},
|
||||
capacitor: {
|
||||
isAppCapacitor: () => window.navigator.userAgent === "capacitor",
|
||||
setStatusBarStyleDark: async () => {
|
||||
if (!window.app.capacitor.isAppCapacitor()) {
|
||||
console.warn("[App] setStatusBarStyleDark is only available on capacitor")
|
||||
console.warn(
|
||||
"[App] setStatusBarStyleDark is only available on capacitor",
|
||||
)
|
||||
return false
|
||||
}
|
||||
return await StatusBar.setStyle({ style: Style.Dark })
|
||||
},
|
||||
setStatusBarStyleLight: async () => {
|
||||
if (!window.app.capacitor.isAppCapacitor()) {
|
||||
console.warn("[App] setStatusBarStyleLight is not supported on this platform")
|
||||
console.warn(
|
||||
"[App] setStatusBarStyleLight is not supported on this platform",
|
||||
)
|
||||
return false
|
||||
}
|
||||
return await StatusBar.setStyle({ style: Style.Light })
|
||||
},
|
||||
hideStatusBar: async () => {
|
||||
if (!window.app.capacitor.isAppCapacitor()) {
|
||||
console.warn("[App] hideStatusBar is not supported on this platform")
|
||||
console.warn(
|
||||
"[App] hideStatusBar is not supported on this platform",
|
||||
)
|
||||
return false
|
||||
}
|
||||
|
||||
@ -247,7 +270,9 @@ class ComtyApp extends React.Component {
|
||||
},
|
||||
showStatusBar: async () => {
|
||||
if (!window.app.capacitor.isAppCapacitor()) {
|
||||
console.warn("[App] showStatusBar is not supported on this platform")
|
||||
console.warn(
|
||||
"[App] showStatusBar is not supported on this platform",
|
||||
)
|
||||
return false
|
||||
}
|
||||
return await StatusBar.show()
|
||||
@ -257,13 +282,14 @@ class ComtyApp extends React.Component {
|
||||
clearInternalStorage: async () => {
|
||||
antd.Modal.confirm({
|
||||
title: "Clear internal storage",
|
||||
content: "Are you sure you want to clear all internal storage? This will remove all your data from the app, including your session.",
|
||||
content:
|
||||
"Are you sure you want to clear all internal storage? This will remove all your data from the app, including your session.",
|
||||
onOk: async () => {
|
||||
Utils.deleteInternalStorage()
|
||||
}
|
||||
},
|
||||
})
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
static staticRenders = {
|
||||
@ -309,12 +335,10 @@ class ComtyApp extends React.Component {
|
||||
app.navigation.goAuth()
|
||||
|
||||
antd.notification.open({
|
||||
message: <Translation>
|
||||
{(t) => t("Invalid Session")}
|
||||
</Translation>,
|
||||
description: <Translation>
|
||||
{(t) => t(error)}
|
||||
</Translation>,
|
||||
message: (
|
||||
<Translation>{(t) => t("Invalid Session")}</Translation>
|
||||
),
|
||||
description: <Translation>{(t) => t(error)}</Translation>,
|
||||
icon: <Icons.MdOutlineAccessTimeFilled />,
|
||||
})
|
||||
},
|
||||
@ -339,7 +363,7 @@ class ComtyApp extends React.Component {
|
||||
"auth:disabled_account": async () => {
|
||||
await SessionModel.removeToken()
|
||||
app.navigation.goAuth()
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
flushState = async () => {
|
||||
@ -355,13 +379,13 @@ class ComtyApp extends React.Component {
|
||||
|
||||
StatusBar.setOverlaysWebView({ overlay: false })
|
||||
|
||||
CapacitorApp.addListener("backButton", ({ canGoBack }) => {
|
||||
if (!canGoBack) {
|
||||
CapacitorApp.exitApp()
|
||||
} else {
|
||||
app.location.back()
|
||||
}
|
||||
})
|
||||
// CapacitorApp.addListener("backButton", ({ canGoBack }) => {
|
||||
// if (!canGoBack) {
|
||||
// CapacitorApp.exitApp()
|
||||
// } else {
|
||||
// app.location.back()
|
||||
// }
|
||||
// })
|
||||
}
|
||||
|
||||
await this.initialization()
|
||||
@ -389,7 +413,10 @@ class ComtyApp extends React.Component {
|
||||
try {
|
||||
await this.__SessionInit()
|
||||
} catch (error) {
|
||||
console.error(`[App] Error while initializing session`, error)
|
||||
console.error(
|
||||
`[App] Error while initializing session`,
|
||||
error,
|
||||
)
|
||||
|
||||
throw {
|
||||
cause: "Cannot initialize session",
|
||||
@ -436,32 +463,33 @@ class ComtyApp extends React.Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
return <React.Fragment>
|
||||
<Helmet>
|
||||
<title>{config.app.siteName}</title>
|
||||
<meta name="og:description" content={config.app.siteDescription} />
|
||||
<meta property="og:title" content={config.app.siteName} />
|
||||
</Helmet>
|
||||
<Router.InternalRouter>
|
||||
<ThemeProvider>
|
||||
{
|
||||
window.__TAURI__ && <DesktopTopBar />
|
||||
}
|
||||
<Layout
|
||||
user={this.state.user}
|
||||
staticRenders={ComtyApp.staticRenders}
|
||||
bindProps={{
|
||||
user: this.state.user,
|
||||
}}
|
||||
>
|
||||
{
|
||||
this.state.initialized && <Router.PageRender />
|
||||
}
|
||||
</Layout>
|
||||
</ThemeProvider>
|
||||
</Router.InternalRouter>
|
||||
</React.Fragment>
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Helmet>
|
||||
<title>{config.app.siteName}</title>
|
||||
<meta
|
||||
name="og:description"
|
||||
content={config.app.siteDescription}
|
||||
/>
|
||||
<meta property="og:title" content={config.app.siteName} />
|
||||
</Helmet>
|
||||
<Router.InternalRouter>
|
||||
<ThemeProvider>
|
||||
{window.__TAURI__ && <DesktopTopBar />}
|
||||
<Layout
|
||||
user={this.state.user}
|
||||
staticRenders={ComtyApp.staticRenders}
|
||||
bindProps={{
|
||||
user: this.state.user,
|
||||
}}
|
||||
>
|
||||
{this.state.initialized && <Router.PageRender />}
|
||||
</Layout>
|
||||
</ThemeProvider>
|
||||
</Router.InternalRouter>
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default new Runtime(ComtyApp)
|
||||
export default new Runtime(ComtyApp)
|
||||
|
@ -1,19 +1,21 @@
|
||||
import React from "react"
|
||||
import { motion, AnimatePresence } from "framer-motion"
|
||||
import { motion, AnimatePresence } from "motion/react"
|
||||
|
||||
const PageTransition = (props) => {
|
||||
return <AnimatePresence>
|
||||
<motion.div
|
||||
layout
|
||||
initial={{ y: 10, opacity: 0 }}
|
||||
animate={{ y: 0, opacity: 1 }}
|
||||
exit={{ y: -10, opacity: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
{...props}
|
||||
>
|
||||
{props.children}
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
return (
|
||||
<AnimatePresence>
|
||||
<motion.div
|
||||
layout
|
||||
initial={{ y: 10, opacity: 0 }}
|
||||
animate={{ y: 0, opacity: 1 }}
|
||||
exit={{ y: -10, opacity: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
{...props}
|
||||
>
|
||||
{props.children}
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
)
|
||||
}
|
||||
|
||||
export default PageTransition
|
||||
export default PageTransition
|
||||
|
@ -2,7 +2,7 @@ import React from "react"
|
||||
import * as antd from "antd"
|
||||
import classnames from "classnames"
|
||||
import { Icons, createIconRender } from "@components/Icons"
|
||||
import { motion } from "framer-motion"
|
||||
import { motion } from "motion/react"
|
||||
|
||||
import useWsEvents from "@hooks/useWsEvents"
|
||||
|
||||
@ -11,214 +11,220 @@ import PostModel from "@models/post"
|
||||
import "./index.less"
|
||||
|
||||
const PollOption = (props) => {
|
||||
async function onClick() {
|
||||
if (typeof props.onClick === "function") {
|
||||
await props.onClick(props.option.id)
|
||||
}
|
||||
}
|
||||
async function onClick() {
|
||||
if (typeof props.onClick === "function") {
|
||||
await props.onClick(props.option.id)
|
||||
}
|
||||
}
|
||||
|
||||
return <div
|
||||
className={classnames(
|
||||
"poll-option",
|
||||
{
|
||||
["checked"]: props.checked,
|
||||
return (
|
||||
<div
|
||||
className={classnames("poll-option", {
|
||||
["checked"]: props.checked,
|
||||
})}
|
||||
style={{
|
||||
"--percentage": `${props.percentage}%`,
|
||||
}}
|
||||
onClick={onClick}
|
||||
>
|
||||
{props.checked && (
|
||||
<motion.div
|
||||
className="percentage-indicator"
|
||||
animate={{ width: `${props.percentage}%` }}
|
||||
initial={{ width: 0 }}
|
||||
transition={{ ease: "easeOut" }}
|
||||
/>
|
||||
)}
|
||||
|
||||
}
|
||||
)}
|
||||
style={{
|
||||
"--percentage": `${props.percentage}%`
|
||||
}}
|
||||
onClick={onClick}
|
||||
>
|
||||
{
|
||||
props.checked && <motion.div
|
||||
className="percentage-indicator"
|
||||
animate={{ width: `${props.percentage}%` }}
|
||||
initial={{ width: 0 }}
|
||||
transition={{ ease: "easeOut" }}
|
||||
/>
|
||||
}
|
||||
<div className="poll-option-content">
|
||||
{props.checked && createIconRender("FaCheck")}
|
||||
|
||||
<div className="poll-option-content">
|
||||
{
|
||||
props.checked && createIconRender("FaCheck")
|
||||
}
|
||||
{props.showPercentage && (
|
||||
<span>{Math.floor(props.percentage)}%</span>
|
||||
)}
|
||||
|
||||
{
|
||||
props.showPercentage && <span>
|
||||
{Math.floor(props.percentage)}%
|
||||
</span>
|
||||
}
|
||||
|
||||
<span>
|
||||
{props.option.label}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<span>{props.option.label}</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const Poll = (props) => {
|
||||
const { editMode, onClose, formRef } = props
|
||||
const { editMode, onClose, formRef } = props
|
||||
|
||||
const [options, setOptions] = React.useState(props.options ?? [])
|
||||
const [hasVoted, setHasVoted] = React.useState(false)
|
||||
const [totalVotes, setTotalVotes] = React.useState(0)
|
||||
const [options, setOptions] = React.useState(props.options ?? [])
|
||||
const [hasVoted, setHasVoted] = React.useState(false)
|
||||
const [totalVotes, setTotalVotes] = React.useState(0)
|
||||
|
||||
useWsEvents({
|
||||
"post.poll.vote": (data) => {
|
||||
const { post_id, option_id, user_id, previous_option_id } = data
|
||||
useWsEvents(
|
||||
{
|
||||
"post.poll.vote": (data) => {
|
||||
const { post_id, option_id, user_id, previous_option_id } = data
|
||||
|
||||
if (post_id !== props.post_id) {
|
||||
return false
|
||||
}
|
||||
if (post_id !== props.post_id) {
|
||||
return false
|
||||
}
|
||||
|
||||
console.debug(`U[${user_id}] vote to option [${option_id}]`)
|
||||
console.debug(`U[${user_id}] vote to option [${option_id}]`)
|
||||
|
||||
setOptions((prev) => {
|
||||
prev = prev.map((option) => {
|
||||
return option
|
||||
})
|
||||
setOptions((prev) => {
|
||||
prev = prev.map((option) => {
|
||||
return option
|
||||
})
|
||||
|
||||
if (user_id === app.userData._id) {
|
||||
// remove all `voted` properties
|
||||
prev = prev.map((option) => {
|
||||
delete option.voted
|
||||
if (user_id === app.userData._id) {
|
||||
// remove all `voted` properties
|
||||
prev = prev.map((option) => {
|
||||
delete option.voted
|
||||
|
||||
option.voted = option.id === option_id
|
||||
option.voted = option.id === option_id
|
||||
|
||||
return option
|
||||
})
|
||||
}
|
||||
return option
|
||||
})
|
||||
}
|
||||
|
||||
if (previous_option_id) {
|
||||
const previousOptionIndex = prev.findIndex((option) => option.id === previous_option_id)
|
||||
if (previous_option_id) {
|
||||
const previousOptionIndex = prev.findIndex(
|
||||
(option) => option.id === previous_option_id,
|
||||
)
|
||||
|
||||
if (previousOptionIndex !== -1) {
|
||||
prev[previousOptionIndex].count = prev[previousOptionIndex].count - 1
|
||||
}
|
||||
}
|
||||
if (previousOptionIndex !== -1) {
|
||||
prev[previousOptionIndex].count =
|
||||
prev[previousOptionIndex].count - 1
|
||||
}
|
||||
}
|
||||
|
||||
if (option_id) {
|
||||
const newOptionIndex = prev.findIndex((option) => option.id === option_id)
|
||||
if (option_id) {
|
||||
const newOptionIndex = prev.findIndex(
|
||||
(option) => option.id === option_id,
|
||||
)
|
||||
|
||||
if (newOptionIndex !== -1) {
|
||||
prev[newOptionIndex].count += 1
|
||||
}
|
||||
}
|
||||
if (newOptionIndex !== -1) {
|
||||
prev[newOptionIndex].count += 1
|
||||
}
|
||||
}
|
||||
|
||||
return prev
|
||||
})
|
||||
}
|
||||
}, {
|
||||
socketName: "posts"
|
||||
})
|
||||
return prev
|
||||
})
|
||||
},
|
||||
},
|
||||
{
|
||||
socketName: "posts",
|
||||
},
|
||||
)
|
||||
|
||||
async function onVote(id) {
|
||||
console.debug(`Voting poll option`, {
|
||||
option_id: id,
|
||||
post_id: props.post_id,
|
||||
})
|
||||
async function onVote(id) {
|
||||
console.debug(`Voting poll option`, {
|
||||
option_id: id,
|
||||
post_id: props.post_id,
|
||||
})
|
||||
|
||||
await PostModel.votePoll({
|
||||
post_id: props.post_id,
|
||||
option_id: id,
|
||||
})
|
||||
}
|
||||
await PostModel.votePoll({
|
||||
post_id: props.post_id,
|
||||
option_id: id,
|
||||
})
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
if (options) {
|
||||
const totalVotes = options.reduce((sum, option) => {
|
||||
return sum + option.count
|
||||
}, 0)
|
||||
React.useEffect(() => {
|
||||
if (options) {
|
||||
const totalVotes = options.reduce((sum, option) => {
|
||||
return sum + option.count
|
||||
}, 0)
|
||||
|
||||
setTotalVotes(totalVotes)
|
||||
setTotalVotes(totalVotes)
|
||||
|
||||
const hasVoted = options.some((option) => {
|
||||
return option.voted
|
||||
})
|
||||
const hasVoted = options.some((option) => {
|
||||
return option.voted
|
||||
})
|
||||
|
||||
setHasVoted(hasVoted)
|
||||
}
|
||||
}, [options])
|
||||
setHasVoted(hasVoted)
|
||||
}
|
||||
}, [options])
|
||||
|
||||
return <div className="poll">
|
||||
{
|
||||
!editMode && options.map((option, index) => {
|
||||
const percentage = totalVotes > 0 ? (option.count / totalVotes) * 100 : 0
|
||||
return (
|
||||
<div className="poll">
|
||||
{!editMode &&
|
||||
options.map((option, index) => {
|
||||
const percentage =
|
||||
totalVotes > 0 ? (option.count / totalVotes) * 100 : 0
|
||||
|
||||
return <PollOption
|
||||
key={index}
|
||||
option={option}
|
||||
onClick={onVote}
|
||||
checked={option.voted}
|
||||
percentage={percentage}
|
||||
showPercentage={hasVoted}
|
||||
/>
|
||||
})
|
||||
}
|
||||
return (
|
||||
<PollOption
|
||||
key={index}
|
||||
option={option}
|
||||
onClick={onVote}
|
||||
checked={option.voted}
|
||||
percentage={percentage}
|
||||
showPercentage={hasVoted}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
|
||||
{
|
||||
editMode && <antd.Form
|
||||
name="post-poll"
|
||||
className="post-poll-edit"
|
||||
ref={formRef}
|
||||
initialValues={{
|
||||
options: options
|
||||
}}
|
||||
>
|
||||
<antd.Form.List
|
||||
name="options"
|
||||
>
|
||||
{(fields, { add, remove }) => {
|
||||
return <>
|
||||
{
|
||||
fields.map((field, index) => {
|
||||
return <div
|
||||
key={field.key}
|
||||
className="post-poll-edit-option"
|
||||
>
|
||||
<antd.Form.Item
|
||||
{...field}
|
||||
name={[field.name, "label"]}
|
||||
>
|
||||
<antd.Input
|
||||
placeholder="Type a option"
|
||||
/>
|
||||
</antd.Form.Item>
|
||||
{editMode && (
|
||||
<antd.Form
|
||||
name="post-poll"
|
||||
className="post-poll-edit"
|
||||
ref={formRef}
|
||||
initialValues={{
|
||||
options: options,
|
||||
}}
|
||||
>
|
||||
<antd.Form.List name="options">
|
||||
{(fields, { add, remove }) => {
|
||||
return (
|
||||
<>
|
||||
{fields.map((field, index) => {
|
||||
return (
|
||||
<div
|
||||
key={field.key}
|
||||
className="post-poll-edit-option"
|
||||
>
|
||||
<antd.Form.Item
|
||||
{...field}
|
||||
name={[field.name, "label"]}
|
||||
>
|
||||
<antd.Input placeholder="Type a option" />
|
||||
</antd.Form.Item>
|
||||
|
||||
{
|
||||
fields.length > 1 && <antd.Button
|
||||
onClick={() => remove(field.name)}
|
||||
icon={createIconRender("MdRemove")}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
})
|
||||
}
|
||||
{fields.length > 1 && (
|
||||
<antd.Button
|
||||
onClick={() =>
|
||||
remove(field.name)
|
||||
}
|
||||
icon={createIconRender(
|
||||
"MdRemove",
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
|
||||
<antd.Button
|
||||
onClick={() => add()}
|
||||
icon={createIconRender("PlusOutlined")}
|
||||
>
|
||||
Add Option
|
||||
</antd.Button>
|
||||
</>
|
||||
}}
|
||||
</antd.Form.List>
|
||||
</antd.Form>
|
||||
}
|
||||
<antd.Button
|
||||
onClick={() => add()}
|
||||
icon={createIconRender("PlusOutlined")}
|
||||
>
|
||||
Add Option
|
||||
</antd.Button>
|
||||
</>
|
||||
)
|
||||
}}
|
||||
</antd.Form.List>
|
||||
</antd.Form>
|
||||
)}
|
||||
|
||||
{
|
||||
editMode && <div className="poll-edit-actions">
|
||||
<antd.Button
|
||||
onClick={onClose}
|
||||
icon={createIconRender("CloseOutlined")}
|
||||
size="small"
|
||||
type="text"
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
{editMode && (
|
||||
<div className="poll-edit-actions">
|
||||
<antd.Button
|
||||
onClick={onClose}
|
||||
icon={createIconRender("CloseOutlined")}
|
||||
size="small"
|
||||
type="text"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Poll
|
||||
export default Poll
|
||||
|
@ -1,6 +1,5 @@
|
||||
import React from "react"
|
||||
import { Skeleton, Button } from "antd"
|
||||
import Plyr from "plyr-react"
|
||||
import mimetypes from "mime"
|
||||
import BearCarousel from "bear-react-carousel"
|
||||
|
||||
@ -9,167 +8,176 @@ import ImageViewer from "@components/ImageViewer"
|
||||
import ContentFailed from "../contentFailed"
|
||||
|
||||
import "bear-react-carousel/dist/index.css"
|
||||
import "plyr-react/dist/plyr.css"
|
||||
import "./index.less"
|
||||
|
||||
const renderDebug = localStorage.getItem("render_debug") === "true"
|
||||
|
||||
const Attachment = React.memo((props) => {
|
||||
const [loaded, setLoaded] = React.useState(false)
|
||||
const [loaded, setLoaded] = React.useState(false)
|
||||
|
||||
const [mimeType, setMimeType] = React.useState(null)
|
||||
const [mimeType, setMimeType] = React.useState(null)
|
||||
|
||||
try {
|
||||
const { url, id } = props.attachment
|
||||
try {
|
||||
const { url, id } = props.attachment
|
||||
|
||||
const onDoubleClickAttachment = (e) => {
|
||||
if (mimeType.split("/")[0] === "image") {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
const onDoubleClickAttachment = (e) => {
|
||||
if (mimeType.split("/")[0] === "image") {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
|
||||
app.controls.openFullImageViewer(url)
|
||||
}
|
||||
}
|
||||
app.controls.openFullImageViewer(url)
|
||||
}
|
||||
}
|
||||
|
||||
const getMediaType = async () => {
|
||||
let extension = null
|
||||
const getMediaType = async () => {
|
||||
let extension = null
|
||||
|
||||
// get media type by parsing the url
|
||||
const mediaExtname = /\.([a-zA-Z0-9]+)$/.exec(url)
|
||||
// get media type by parsing the url
|
||||
const mediaExtname = /\.([a-zA-Z0-9]+)$/.exec(url)
|
||||
|
||||
if (mediaExtname) {
|
||||
extension = mediaExtname[1]
|
||||
} else {
|
||||
// try to get media by creating requesting the url
|
||||
const response = await fetch(url, {
|
||||
method: "HEAD",
|
||||
})
|
||||
if (mediaExtname) {
|
||||
extension = mediaExtname[1]
|
||||
} else {
|
||||
// try to get media by creating requesting the url
|
||||
const response = await fetch(url, {
|
||||
method: "HEAD",
|
||||
})
|
||||
|
||||
extension = response.headers.get("content-type").split("/")[1]
|
||||
}
|
||||
extension = response.headers.get("content-type").split("/")[1]
|
||||
}
|
||||
|
||||
extension = extension.toLowerCase()
|
||||
extension = extension.toLowerCase()
|
||||
|
||||
if (!extension) {
|
||||
setLoaded(true)
|
||||
if (!extension) {
|
||||
setLoaded(true)
|
||||
|
||||
console.error("Failed to get media type", url, extension)
|
||||
console.error("Failed to get media type", url, extension)
|
||||
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const mimeType = mimetypes.getType(extension)
|
||||
const mimeType = mimetypes.getType(extension)
|
||||
|
||||
setMimeType(mimeType)
|
||||
setMimeType(mimeType)
|
||||
|
||||
setLoaded(true)
|
||||
}
|
||||
setLoaded(true)
|
||||
}
|
||||
|
||||
const renderMedia = () => {
|
||||
if (!mimeType) {
|
||||
return null
|
||||
}
|
||||
const renderMedia = () => {
|
||||
if (!mimeType) {
|
||||
return null
|
||||
}
|
||||
|
||||
switch (mimeType.split("/")[0]) {
|
||||
case "image": {
|
||||
return <ImageViewer src={url} />
|
||||
}
|
||||
case "video": {
|
||||
return <video controls>
|
||||
<source src={url} type={mimeType} />
|
||||
</video>
|
||||
}
|
||||
case "audio": {
|
||||
return <audio controls>
|
||||
<source src={url} type={mimeType} />
|
||||
</audio>
|
||||
}
|
||||
default: {
|
||||
return <h4>
|
||||
Unsupported media type [{mimeType}]
|
||||
</h4>
|
||||
}
|
||||
}
|
||||
}
|
||||
switch (mimeType.split("/")[0]) {
|
||||
case "image": {
|
||||
return <ImageViewer src={url} />
|
||||
}
|
||||
case "video": {
|
||||
return (
|
||||
<video controls>
|
||||
<source src={url} type={mimeType} />
|
||||
</video>
|
||||
)
|
||||
}
|
||||
case "audio": {
|
||||
return (
|
||||
<audio controls>
|
||||
<source src={url} type={mimeType} />
|
||||
</audio>
|
||||
)
|
||||
}
|
||||
default: {
|
||||
return <h4>Unsupported media type [{mimeType}]</h4>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
getMediaType()
|
||||
}, [])
|
||||
React.useEffect(() => {
|
||||
getMediaType()
|
||||
}, [])
|
||||
|
||||
if (!loaded) {
|
||||
return <Skeleton active />
|
||||
}
|
||||
if (!loaded) {
|
||||
return <Skeleton active />
|
||||
}
|
||||
|
||||
if (loaded && !mimeType) {
|
||||
return <ContentFailed />
|
||||
}
|
||||
if (loaded && !mimeType) {
|
||||
return <ContentFailed />
|
||||
}
|
||||
|
||||
return <div
|
||||
key={props.index}
|
||||
id={id}
|
||||
className="attachment"
|
||||
onDoubleClick={onDoubleClickAttachment}
|
||||
>
|
||||
{renderMedia()}
|
||||
</div>
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
return (
|
||||
<div
|
||||
key={props.index}
|
||||
id={id}
|
||||
className="attachment"
|
||||
onDoubleClick={onDoubleClickAttachment}
|
||||
>
|
||||
{renderMedia()}
|
||||
</div>
|
||||
)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
|
||||
return <ContentFailed />
|
||||
}
|
||||
return <ContentFailed />
|
||||
}
|
||||
})
|
||||
|
||||
export default React.memo((props) => {
|
||||
const [controller, setController] = React.useState()
|
||||
const [carouselState, setCarouselState] = React.useState()
|
||||
const [nsfwAccepted, setNsfwAccepted] = React.useState(false)
|
||||
const [controller, setController] = React.useState()
|
||||
const [carouselState, setCarouselState] = React.useState()
|
||||
const [nsfwAccepted, setNsfwAccepted] = React.useState(false)
|
||||
|
||||
React.useEffect(() => {
|
||||
// get attachment index from query string
|
||||
const attachmentIndex = parseInt(new URLSearchParams(window.location.search).get("attachment"))
|
||||
React.useEffect(() => {
|
||||
// get attachment index from query string
|
||||
const attachmentIndex = parseInt(
|
||||
new URLSearchParams(window.location.search).get("attachment"),
|
||||
)
|
||||
|
||||
if (attachmentIndex) {
|
||||
controller?.slideToPage(attachmentIndex)
|
||||
}
|
||||
}, [])
|
||||
if (attachmentIndex) {
|
||||
controller?.slideToPage(attachmentIndex)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return <div className="post_attachments">
|
||||
{
|
||||
props.flags && props.flags.includes("nsfw") && !nsfwAccepted &&
|
||||
<div className="nsfw_alert">
|
||||
<h2>
|
||||
This post may contain sensitive content.
|
||||
</h2>
|
||||
return (
|
||||
<div className="post_attachments">
|
||||
{props.flags && props.flags.includes("nsfw") && !nsfwAccepted && (
|
||||
<div className="nsfw_alert">
|
||||
<h2>This post may contain sensitive content.</h2>
|
||||
|
||||
<Button onClick={() => setNsfwAccepted(true)}>
|
||||
Show anyways
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
<Button onClick={() => setNsfwAccepted(true)}>
|
||||
Show anyways
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{
|
||||
props.attachments?.length > 0 && <BearCarousel
|
||||
data={props.attachments.map((attachment, index) => {
|
||||
if (typeof attachment !== "object") {
|
||||
attachment = {
|
||||
url: attachment,
|
||||
}
|
||||
}
|
||||
{props.attachments?.length > 0 && (
|
||||
<BearCarousel
|
||||
data={props.attachments.map((attachment, index) => {
|
||||
if (typeof attachment !== "object") {
|
||||
attachment = {
|
||||
url: attachment,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
key: index,
|
||||
children: <React.Fragment key={index}>
|
||||
<Attachment index={index} attachment={attachment} />
|
||||
</React.Fragment>
|
||||
}
|
||||
})}
|
||||
isEnableNavButton
|
||||
isEnableMouseMove
|
||||
isEnablePagination
|
||||
setController={setController}
|
||||
onSlideChange={setCarouselState}
|
||||
isDebug={renderDebug}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
})
|
||||
return {
|
||||
key: index,
|
||||
children: (
|
||||
<React.Fragment key={index}>
|
||||
<Attachment
|
||||
index={index}
|
||||
attachment={attachment}
|
||||
/>
|
||||
</React.Fragment>
|
||||
),
|
||||
}
|
||||
})}
|
||||
isEnableNavButton
|
||||
isEnableMouseMove
|
||||
isEnablePagination
|
||||
setController={setController}
|
||||
onSlideChange={setCarouselState}
|
||||
isDebug={renderDebug}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React from "react"
|
||||
import classnames from "classnames"
|
||||
import Plyr from "plyr-react"
|
||||
import { motion } from "framer-motion"
|
||||
import ReactPlayer from "react-player/lazy"
|
||||
import { motion } from "motion/react"
|
||||
|
||||
import Poll from "@components/Poll"
|
||||
import { Icons } from "@components/Icons"
|
||||
@ -14,249 +14,288 @@ import PostAttachments from "./components/attachments"
|
||||
import "./index.less"
|
||||
|
||||
const articleAnimationProps = {
|
||||
layout: true,
|
||||
initial: { y: -100, opacity: 0 },
|
||||
animate: { y: 0, opacity: 1, },
|
||||
exit: { scale: 0, opacity: 0 },
|
||||
transition: { duration: 0.1, },
|
||||
layout: true,
|
||||
initial: { y: -100, opacity: 0 },
|
||||
animate: { y: 0, opacity: 1 },
|
||||
exit: { scale: 0, opacity: 0 },
|
||||
transition: { duration: 0.1 },
|
||||
}
|
||||
|
||||
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.location.push(`/@${result[1].substr(1)}`)}>{result[1]}</a>
|
||||
}
|
||||
},
|
||||
{
|
||||
regex: /#[a-zA-Z0-9_]+/gi,
|
||||
fn: (key, result) => {
|
||||
return <a key={key} onClick={() => window.app.location.push(`/trending/${result[0].substr(1)}`)}>{result[0]}</a>
|
||||
}
|
||||
}
|
||||
{
|
||||
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 <ReactPlayer url={result[1]} controls />
|
||||
},
|
||||
},
|
||||
{
|
||||
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.location.push(`/@${result[1].substr(1)}`)
|
||||
}
|
||||
>
|
||||
{result[1]}
|
||||
</a>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
regex: /#[a-zA-Z0-9_]+/gi,
|
||||
fn: (key, result) => {
|
||||
return (
|
||||
<a
|
||||
key={key}
|
||||
onClick={() =>
|
||||
window.app.location.push(
|
||||
`/trending/${result[0].substr(1)}`,
|
||||
)
|
||||
}
|
||||
>
|
||||
{result[0]}
|
||||
</a>
|
||||
)
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
export default class PostCard extends React.PureComponent {
|
||||
state = {
|
||||
data: this.props.data,
|
||||
state = {
|
||||
data: this.props.data,
|
||||
|
||||
countLikes: this.props.data.countLikes ?? 0,
|
||||
countReplies: this.props.data.countComments ?? 0,
|
||||
countLikes: this.props.data.countLikes ?? 0,
|
||||
countReplies: this.props.data.countComments ?? 0,
|
||||
|
||||
hasLiked: this.props.data.isLiked ?? false,
|
||||
hasSaved: this.props.data.isSaved ?? false,
|
||||
hasReplies: this.props.data.hasReplies ?? false,
|
||||
hasLiked: this.props.data.isLiked ?? false,
|
||||
hasSaved: this.props.data.isSaved ?? false,
|
||||
hasReplies: this.props.data.hasReplies ?? false,
|
||||
|
||||
open: this.props.defaultOpened ?? false,
|
||||
open: this.props.defaultOpened ?? false,
|
||||
|
||||
isNsfw: this.props.data.flags?.includes("nsfw") ?? false,
|
||||
nsfwAccepted: false,
|
||||
}
|
||||
isNsfw: this.props.data.flags?.includes("nsfw") ?? false,
|
||||
nsfwAccepted: false,
|
||||
}
|
||||
|
||||
handleDataUpdate = (data) => {
|
||||
this.setState({
|
||||
data: data,
|
||||
})
|
||||
}
|
||||
handleDataUpdate = (data) => {
|
||||
this.setState({
|
||||
data: data,
|
||||
})
|
||||
}
|
||||
|
||||
onDoubleClick = async () => {
|
||||
if (typeof this.props.events.onDoubleClick !== "function") {
|
||||
console.warn("onDoubleClick event is not a function")
|
||||
return
|
||||
}
|
||||
onDoubleClick = async () => {
|
||||
if (typeof this.props.events.onDoubleClick !== "function") {
|
||||
console.warn("onDoubleClick event is not a function")
|
||||
return
|
||||
}
|
||||
|
||||
return await this.props.events.onDoubleClick(this.state.data)
|
||||
}
|
||||
return await this.props.events.onDoubleClick(this.state.data)
|
||||
}
|
||||
|
||||
onClickDelete = async () => {
|
||||
if (typeof this.props.events.onClickDelete !== "function") {
|
||||
console.warn("onClickDelete event is not a function")
|
||||
return
|
||||
}
|
||||
onClickDelete = async () => {
|
||||
if (typeof this.props.events.onClickDelete !== "function") {
|
||||
console.warn("onClickDelete event is not a function")
|
||||
return
|
||||
}
|
||||
|
||||
return await this.props.events.onClickDelete(this.state.data)
|
||||
}
|
||||
return await this.props.events.onClickDelete(this.state.data)
|
||||
}
|
||||
|
||||
onClickLike = async () => {
|
||||
if (typeof this.props.events.onClickLike !== "function") {
|
||||
console.warn("onClickLike event is not a function")
|
||||
return
|
||||
}
|
||||
onClickLike = async () => {
|
||||
if (typeof this.props.events.onClickLike !== "function") {
|
||||
console.warn("onClickLike event is not a function")
|
||||
return
|
||||
}
|
||||
|
||||
const actionResult = await this.props.events.onClickLike(this.state.data)
|
||||
const actionResult = await this.props.events.onClickLike(
|
||||
this.state.data,
|
||||
)
|
||||
|
||||
if (actionResult) {
|
||||
this.setState({
|
||||
hasLiked: actionResult.liked,
|
||||
countLikes: actionResult.count,
|
||||
})
|
||||
}
|
||||
if (actionResult) {
|
||||
this.setState({
|
||||
hasLiked: actionResult.liked,
|
||||
countLikes: actionResult.count,
|
||||
})
|
||||
}
|
||||
|
||||
return actionResult
|
||||
}
|
||||
return actionResult
|
||||
}
|
||||
|
||||
onClickSave = async () => {
|
||||
if (typeof this.props.events.onClickSave !== "function") {
|
||||
console.warn("onClickSave event is not a function")
|
||||
return
|
||||
}
|
||||
onClickSave = async () => {
|
||||
if (typeof this.props.events.onClickSave !== "function") {
|
||||
console.warn("onClickSave event is not a function")
|
||||
return
|
||||
}
|
||||
|
||||
const actionResult = await this.props.events.onClickSave(this.state.data)
|
||||
const actionResult = await this.props.events.onClickSave(
|
||||
this.state.data,
|
||||
)
|
||||
|
||||
if (actionResult) {
|
||||
this.setState({
|
||||
hasSaved: actionResult.saved,
|
||||
})
|
||||
}
|
||||
if (actionResult) {
|
||||
this.setState({
|
||||
hasSaved: actionResult.saved,
|
||||
})
|
||||
}
|
||||
|
||||
return actionResult
|
||||
}
|
||||
return actionResult
|
||||
}
|
||||
|
||||
onClickEdit = async () => {
|
||||
if (typeof this.props.events.onClickEdit !== "function") {
|
||||
console.warn("onClickEdit event is not a function")
|
||||
return
|
||||
}
|
||||
onClickEdit = async () => {
|
||||
if (typeof this.props.events.onClickEdit !== "function") {
|
||||
console.warn("onClickEdit event is not a function")
|
||||
return
|
||||
}
|
||||
|
||||
return await this.props.events.onClickEdit(this.state.data)
|
||||
}
|
||||
return await this.props.events.onClickEdit(this.state.data)
|
||||
}
|
||||
|
||||
onClickReply = async () => {
|
||||
if (typeof this.props.events.onClickReply !== "function") {
|
||||
console.warn("onClickReply event is not a function")
|
||||
return
|
||||
}
|
||||
onClickReply = async () => {
|
||||
if (typeof this.props.events.onClickReply !== "function") {
|
||||
console.warn("onClickReply event is not a function")
|
||||
return
|
||||
}
|
||||
|
||||
return await this.props.events.onClickReply(this.state.data)
|
||||
}
|
||||
return await this.props.events.onClickReply(this.state.data)
|
||||
}
|
||||
|
||||
componentDidUpdate = (prevProps) => {
|
||||
if (prevProps.data !== this.props.data) {
|
||||
this.setState({
|
||||
data: this.props.data,
|
||||
})
|
||||
}
|
||||
}
|
||||
componentDidUpdate = (prevProps) => {
|
||||
if (prevProps.data !== this.props.data) {
|
||||
this.setState({
|
||||
data: this.props.data,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
componentDidCatch = (error, info) => {
|
||||
console.error(error)
|
||||
componentDidCatch = (error, info) => {
|
||||
console.error(error)
|
||||
|
||||
return <div className="postCard error">
|
||||
<h1>
|
||||
<Icons.FiAlertTriangle />
|
||||
<span>Cannot render this post</span>
|
||||
<span>
|
||||
Maybe this version of the app is outdated or is not supported yet
|
||||
</span>
|
||||
</h1>
|
||||
</div>
|
||||
}
|
||||
return (
|
||||
<div className="postCard error">
|
||||
<h1>
|
||||
<Icons.FiAlertTriangle />
|
||||
<span>Cannot render this post</span>
|
||||
<span>
|
||||
Maybe this version of the app is outdated or is not
|
||||
supported yet
|
||||
</span>
|
||||
</h1>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
componentDidMount = () => {
|
||||
app.cores.api.listenEvent(`post.update.${this.state.data._id}`, this.handleDataUpdate, "posts")
|
||||
}
|
||||
componentDidMount = () => {
|
||||
app.cores.api.listenEvent(
|
||||
`post.update.${this.state.data._id}`,
|
||||
this.handleDataUpdate,
|
||||
"posts",
|
||||
)
|
||||
}
|
||||
|
||||
componentWillUnmount = () => {
|
||||
app.cores.api.unlistenEvent(`post.update.${this.state.data._id}`, this.handleDataUpdate, "posts")
|
||||
}
|
||||
componentWillUnmount = () => {
|
||||
app.cores.api.unlistenEvent(
|
||||
`post.update.${this.state.data._id}`,
|
||||
this.handleDataUpdate,
|
||||
"posts",
|
||||
)
|
||||
}
|
||||
|
||||
render() {
|
||||
return <motion.article
|
||||
className={classnames(
|
||||
"post_card",
|
||||
{
|
||||
["open"]: this.state.open,
|
||||
}
|
||||
)}
|
||||
id={this.state.data._id}
|
||||
style={this.props.style}
|
||||
{...articleAnimationProps}
|
||||
>
|
||||
<div
|
||||
className="post_card_content"
|
||||
context-menu={"post-card"}
|
||||
user-id={this.state.data.user_id}
|
||||
>
|
||||
<PostHeader
|
||||
postData={this.state.data}
|
||||
onDoubleClick={this.onDoubleClick}
|
||||
disableReplyTag={this.props.disableReplyTag}
|
||||
/>
|
||||
render() {
|
||||
return (
|
||||
<motion.article
|
||||
className={classnames("post_card", {
|
||||
["open"]: this.state.open,
|
||||
})}
|
||||
id={this.state.data._id}
|
||||
style={this.props.style}
|
||||
{...articleAnimationProps}
|
||||
>
|
||||
<div
|
||||
className="post_card_content"
|
||||
context-menu={"post-card"}
|
||||
user-id={this.state.data.user_id}
|
||||
>
|
||||
<PostHeader
|
||||
postData={this.state.data}
|
||||
onDoubleClick={this.onDoubleClick}
|
||||
disableReplyTag={this.props.disableReplyTag}
|
||||
/>
|
||||
|
||||
<div
|
||||
id="post_content"
|
||||
className={classnames(
|
||||
"post_content",
|
||||
)}
|
||||
>
|
||||
<div className="message">
|
||||
{
|
||||
processString(messageRegexs)(this.state.data.message ?? "")
|
||||
}
|
||||
</div>
|
||||
<div
|
||||
id="post_content"
|
||||
className={classnames("post_content")}
|
||||
>
|
||||
<div className="message">
|
||||
{processString(messageRegexs)(
|
||||
this.state.data.message ?? "",
|
||||
)}
|
||||
</div>
|
||||
|
||||
{
|
||||
!this.props.disableAttachments && this.state.data.attachments && this.state.data.attachments.length > 0 && <PostAttachments
|
||||
attachments={this.state.data.attachments}
|
||||
flags={this.state.data.flags}
|
||||
/>
|
||||
}
|
||||
{!this.props.disableAttachments &&
|
||||
this.state.data.attachments &&
|
||||
this.state.data.attachments.length > 0 && (
|
||||
<PostAttachments
|
||||
attachments={this.state.data.attachments}
|
||||
flags={this.state.data.flags}
|
||||
/>
|
||||
)}
|
||||
|
||||
{
|
||||
this.state.data.poll_options && <Poll
|
||||
post_id={this.state.data._id}
|
||||
options={this.state.data.poll_options}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
{this.state.data.poll_options && (
|
||||
<Poll
|
||||
post_id={this.state.data._id}
|
||||
options={this.state.data.poll_options}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<PostActions
|
||||
post={this.state.data}
|
||||
user_id={this.state.data.user_id}
|
||||
<PostActions
|
||||
post={this.state.data}
|
||||
user_id={this.state.data.user_id}
|
||||
likesCount={this.state.countLikes}
|
||||
repliesCount={this.state.countReplies}
|
||||
defaultLiked={this.state.hasLiked}
|
||||
defaultSaved={this.state.hasSaved}
|
||||
PP
|
||||
actions={{
|
||||
onClickLike: this.onClickLike,
|
||||
onClickEdit: this.onClickEdit,
|
||||
onClickDelete: this.onClickDelete,
|
||||
onClickSave: this.onClickSave,
|
||||
onClickReply: this.onClickReply,
|
||||
}}
|
||||
/>
|
||||
|
||||
likesCount={this.state.countLikes}
|
||||
repliesCount={this.state.countReplies}
|
||||
|
||||
defaultLiked={this.state.hasLiked}
|
||||
defaultSaved={this.state.hasSaved}
|
||||
PP
|
||||
actions={{
|
||||
onClickLike: this.onClickLike,
|
||||
onClickEdit: this.onClickEdit,
|
||||
onClickDelete: this.onClickDelete,
|
||||
onClickSave: this.onClickSave,
|
||||
onClickReply: this.onClickReply,
|
||||
}}
|
||||
/>
|
||||
|
||||
{
|
||||
!this.props.disableHasReplies && !!this.state.hasReplies && <div
|
||||
className="post-card-has_replies"
|
||||
onClick={() => app.navigation.goToPost(this.state.data._id)}
|
||||
>
|
||||
<span>View {this.state.hasReplies} replies</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
</motion.article>
|
||||
}
|
||||
}
|
||||
{!this.props.disableHasReplies &&
|
||||
!!this.state.hasReplies && (
|
||||
<div
|
||||
className="post-card-has_replies"
|
||||
onClick={() =>
|
||||
app.navigation.goToPost(this.state.data._id)
|
||||
}
|
||||
>
|
||||
<span>
|
||||
View {this.state.hasReplies} replies
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</motion.article>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React from "react"
|
||||
import * as antd from "antd"
|
||||
import { AnimatePresence } from "framer-motion"
|
||||
import { AnimatePresence } from "motion/react"
|
||||
import { Icons } from "@components/Icons"
|
||||
|
||||
import PostCard from "@components/PostCard"
|
||||
@ -11,474 +11,499 @@ import PostModel from "@models/post"
|
||||
import "./index.less"
|
||||
|
||||
const LoadingComponent = () => {
|
||||
return <div className="post_card">
|
||||
<antd.Skeleton
|
||||
avatar
|
||||
style={{
|
||||
width: "100%"
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
return (
|
||||
<div className="post_card">
|
||||
<antd.Skeleton
|
||||
avatar
|
||||
style={{
|
||||
width: "100%",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const NoResultComponent = () => {
|
||||
return <antd.Empty
|
||||
description="No more post here"
|
||||
style={{
|
||||
width: "100%"
|
||||
}}
|
||||
/>
|
||||
return (
|
||||
<antd.Empty
|
||||
description="No more post here"
|
||||
style={{
|
||||
width: "100%",
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const typeToComponent = {
|
||||
"post": (args) => <PostCard {...args} />,
|
||||
//"playlist": (args) => <PlaylistTimelineEntry {...args} />,
|
||||
post: (args) => <PostCard {...args} />,
|
||||
//"playlist": (args) => <PlaylistTimelineEntry {...args} />,
|
||||
}
|
||||
|
||||
const Entry = React.memo((props) => {
|
||||
const { data } = props
|
||||
const { data } = props
|
||||
|
||||
return React.createElement(typeToComponent[data.type ?? "post"] ?? PostCard, {
|
||||
key: data._id,
|
||||
data: data,
|
||||
disableReplyTag: props.disableReplyTag,
|
||||
disableHasReplies: props.disableHasReplies,
|
||||
events: {
|
||||
onClickLike: props.onLikePost,
|
||||
onClickSave: props.onSavePost,
|
||||
onClickDelete: props.onDeletePost,
|
||||
onClickEdit: props.onEditPost,
|
||||
onClickReply: props.onReplyPost,
|
||||
onDoubleClick: props.onDoubleClick,
|
||||
},
|
||||
})
|
||||
return React.createElement(
|
||||
typeToComponent[data.type ?? "post"] ?? PostCard,
|
||||
{
|
||||
key: data._id,
|
||||
data: data,
|
||||
disableReplyTag: props.disableReplyTag,
|
||||
disableHasReplies: props.disableHasReplies,
|
||||
events: {
|
||||
onClickLike: props.onLikePost,
|
||||
onClickSave: props.onSavePost,
|
||||
onClickDelete: props.onDeletePost,
|
||||
onClickEdit: props.onEditPost,
|
||||
onClickReply: props.onReplyPost,
|
||||
onDoubleClick: props.onDoubleClick,
|
||||
},
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
const PostList = React.forwardRef((props, ref) => {
|
||||
return <LoadMore
|
||||
ref={ref}
|
||||
className="post-list"
|
||||
loadingComponent={LoadingComponent}
|
||||
noResultComponent={NoResultComponent}
|
||||
hasMore={props.hasMore}
|
||||
fetching={props.loading}
|
||||
onBottom={props.onLoadMore}
|
||||
>
|
||||
{
|
||||
!props.realtimeUpdates && !app.isMobile && <div className="resume_btn_wrapper">
|
||||
<antd.Button
|
||||
type="primary"
|
||||
shape="round"
|
||||
onClick={props.onResumeRealtimeUpdates}
|
||||
loading={props.resumingLoading}
|
||||
icon={<Icons.FiSyncOutlined />}
|
||||
>
|
||||
Resume
|
||||
</antd.Button>
|
||||
</div>
|
||||
}
|
||||
|
||||
<AnimatePresence>
|
||||
{
|
||||
props.list.map((data) => {
|
||||
return <Entry
|
||||
key={data._id}
|
||||
data={data}
|
||||
{...props}
|
||||
/>
|
||||
})
|
||||
}
|
||||
</AnimatePresence>
|
||||
</LoadMore>
|
||||
return (
|
||||
<LoadMore
|
||||
ref={ref}
|
||||
className="post-list"
|
||||
loadingComponent={LoadingComponent}
|
||||
noResultComponent={NoResultComponent}
|
||||
hasMore={props.hasMore}
|
||||
fetching={props.loading}
|
||||
onBottom={props.onLoadMore}
|
||||
>
|
||||
{!props.realtimeUpdates && !app.isMobile && (
|
||||
<div className="resume_btn_wrapper">
|
||||
<antd.Button
|
||||
type="primary"
|
||||
shape="round"
|
||||
onClick={props.onResumeRealtimeUpdates}
|
||||
loading={props.resumingLoading}
|
||||
icon={<Icons.FiSyncOutlined />}
|
||||
>
|
||||
Resume
|
||||
</antd.Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<AnimatePresence>
|
||||
{props.list.map((data) => {
|
||||
return <Entry key={data._id} data={data} {...props} />
|
||||
})}
|
||||
</AnimatePresence>
|
||||
</LoadMore>
|
||||
)
|
||||
})
|
||||
|
||||
export class PostsListsComponent extends React.Component {
|
||||
state = {
|
||||
openPost: null,
|
||||
|
||||
loading: false,
|
||||
resumingLoading: false,
|
||||
initialLoading: true,
|
||||
scrollingToTop: false,
|
||||
|
||||
topVisible: true,
|
||||
|
||||
realtimeUpdates: true,
|
||||
|
||||
hasMore: true,
|
||||
list: this.props.list ?? [],
|
||||
}
|
||||
|
||||
parentRef = this.props.innerRef
|
||||
listRef = React.createRef()
|
||||
|
||||
timelineWsEvents = {
|
||||
"feed.new": (data) => {
|
||||
console.log("New feed => ", data)
|
||||
|
||||
if (!this.state.realtimeUpdates) {
|
||||
return
|
||||
}
|
||||
|
||||
this.setState({
|
||||
list: [data, ...this.state.list],
|
||||
})
|
||||
},
|
||||
"post.new": (data) => {
|
||||
console.log("New post => ", data)
|
||||
|
||||
if (!this.state.realtimeUpdates) {
|
||||
return
|
||||
}
|
||||
|
||||
this.setState({
|
||||
list: [data, ...this.state.list],
|
||||
})
|
||||
},
|
||||
"post.delete": (id) => {
|
||||
console.log("Deleted post => ", id)
|
||||
|
||||
this.setState({
|
||||
list: this.state.list.filter((post) => {
|
||||
return post._id !== id
|
||||
}),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
handleLoad = async (fn, params = {}) => {
|
||||
this.setState({
|
||||
loading: true,
|
||||
})
|
||||
|
||||
let payload = {
|
||||
trim: this.state.list.length,
|
||||
limit: app.cores.settings.get("feed_max_fetch"),
|
||||
}
|
||||
|
||||
if (this.props.loadFromModelProps) {
|
||||
payload = {
|
||||
...payload,
|
||||
...this.props.loadFromModelProps,
|
||||
}
|
||||
}
|
||||
|
||||
if (params.replace) {
|
||||
payload.trim = 0
|
||||
}
|
||||
|
||||
const result = await fn(payload).catch((err) => {
|
||||
console.error(err)
|
||||
|
||||
app.message.error("Failed to load more posts")
|
||||
|
||||
return null
|
||||
})
|
||||
|
||||
console.log("Loaded posts => ", result)
|
||||
|
||||
if (result) {
|
||||
if (result.length === 0) {
|
||||
return this.setState({
|
||||
hasMore: false,
|
||||
})
|
||||
}
|
||||
|
||||
if (params.replace) {
|
||||
this.setState({
|
||||
list: result,
|
||||
})
|
||||
} else {
|
||||
this.setState({
|
||||
list: [...this.state.list, ...result],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({
|
||||
loading: false,
|
||||
})
|
||||
}
|
||||
|
||||
addPost = (post) => {
|
||||
this.setState({
|
||||
list: [post, ...this.state.list],
|
||||
})
|
||||
}
|
||||
|
||||
removePost = (id) => {
|
||||
this.setState({
|
||||
list: this.state.list.filter((post) => {
|
||||
return post._id !== id
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
_hacks = {
|
||||
addPost: this.addPost,
|
||||
removePost: this.removePost,
|
||||
addRandomPost: () => {
|
||||
const randomId = Math.random().toString(36).substring(7)
|
||||
|
||||
this.addPost({
|
||||
_id: randomId,
|
||||
message: `Random post ${randomId}`,
|
||||
user: {
|
||||
_id: randomId,
|
||||
username: "random user",
|
||||
avatar: `https://api.dicebear.com/7.x/thumbs/svg?seed=${randomId}`,
|
||||
}
|
||||
})
|
||||
},
|
||||
listRef: this.listRef,
|
||||
}
|
||||
|
||||
onResumeRealtimeUpdates = async () => {
|
||||
console.log("Resuming realtime updates")
|
||||
|
||||
this.setState({
|
||||
resumingLoading: true,
|
||||
scrollingToTop: true,
|
||||
})
|
||||
|
||||
this.listRef.current.scrollTo({
|
||||
top: 0,
|
||||
behavior: "smooth",
|
||||
})
|
||||
|
||||
// reload posts
|
||||
await this.handleLoad(this.props.loadFromModel, {
|
||||
replace: true,
|
||||
})
|
||||
|
||||
this.setState({
|
||||
realtimeUpdates: true,
|
||||
resumingLoading: false,
|
||||
})
|
||||
}
|
||||
|
||||
onScrollList = (e) => {
|
||||
const { scrollTop } = e.target
|
||||
|
||||
if (this.state.scrollingToTop && scrollTop === 0) {
|
||||
this.setState({
|
||||
scrollingToTop: false,
|
||||
})
|
||||
}
|
||||
|
||||
if (scrollTop > 200) {
|
||||
if (this.state.topVisible) {
|
||||
this.setState({
|
||||
topVisible: false,
|
||||
})
|
||||
|
||||
if (typeof this.props.onTopVisibility === "function") {
|
||||
this.props.onTopVisibility(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.props.realtime || this.state.resumingLoading || this.state.scrollingToTop) {
|
||||
return null
|
||||
}
|
||||
|
||||
this.setState({
|
||||
realtimeUpdates: false,
|
||||
})
|
||||
} else {
|
||||
if (!this.state.topVisible) {
|
||||
this.setState({
|
||||
topVisible: true,
|
||||
})
|
||||
|
||||
if (typeof this.props.onTopVisibility === "function") {
|
||||
this.props.onTopVisibility(true)
|
||||
}
|
||||
|
||||
// if (this.props.realtime || !this.state.realtimeUpdates && !this.state.resumingLoading && scrollTop < 5) {
|
||||
// this.onResumeRealtimeUpdates()
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount = async () => {
|
||||
if (typeof this.props.loadFromModel === "function") {
|
||||
await this.handleLoad(this.props.loadFromModel)
|
||||
}
|
||||
|
||||
this.setState({
|
||||
initialLoading: false,
|
||||
})
|
||||
|
||||
if (this.props.watchTimeline) {
|
||||
if (!Array.isArray(this.props.watchTimeline)) {
|
||||
console.error("watchTimeline prop must be an array")
|
||||
} else {
|
||||
this.props.watchTimeline.forEach((event) => {
|
||||
if (typeof this.timelineWsEvents[event] !== "function") {
|
||||
console.error(`The event "${event}" is not defined in the timelineWsEvents object`)
|
||||
}
|
||||
|
||||
app.cores.api.listenEvent(event, this.timelineWsEvents[event], "posts")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (this.listRef && this.listRef.current) {
|
||||
this.listRef.current.addEventListener("scroll", this.onScrollList)
|
||||
}
|
||||
|
||||
window._hacks = this._hacks
|
||||
}
|
||||
|
||||
componentWillUnmount = async () => {
|
||||
if (this.props.watchTimeline) {
|
||||
if (!Array.isArray(this.props.watchTimeline)) {
|
||||
console.error("watchTimeline prop must be an array")
|
||||
} else {
|
||||
this.props.watchTimeline.forEach((event) => {
|
||||
if (typeof this.timelineWsEvents[event] !== "function") {
|
||||
console.error(`The event "${event}" is not defined in the timelineWsEvents object`)
|
||||
}
|
||||
|
||||
app.cores.api.unlistenEvent(event, this.timelineWsEvents[event], "posts")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (this.listRef && this.listRef.current) {
|
||||
this.listRef.current.removeEventListener("scroll", this.onScrollList)
|
||||
}
|
||||
|
||||
window._hacks = null
|
||||
}
|
||||
|
||||
componentDidUpdate = async (prevProps, prevState) => {
|
||||
if (prevProps.list !== this.props.list) {
|
||||
this.setState({
|
||||
list: this.props.list,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
onLikePost = async (data) => {
|
||||
let result = await PostModel.toggleLike({ post_id: data._id }).catch(() => {
|
||||
antd.message.error("Failed to like post")
|
||||
|
||||
return false
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
onSavePost = async (data) => {
|
||||
let result = await PostModel.toggleSave({ post_id: data._id }).catch(() => {
|
||||
antd.message.error("Failed to save post")
|
||||
|
||||
return false
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
onEditPost = (data) => {
|
||||
app.controls.openPostCreator({
|
||||
edit_post: data._id,
|
||||
})
|
||||
}
|
||||
|
||||
onReplyPost = (data) => {
|
||||
app.controls.openPostCreator({
|
||||
reply_to: data._id,
|
||||
})
|
||||
}
|
||||
|
||||
onDoubleClickPost = (data) => {
|
||||
app.navigation.goToPost(data._id)
|
||||
}
|
||||
|
||||
onDeletePost = async (data) => {
|
||||
antd.Modal.confirm({
|
||||
title: "Are you sure you want to delete this post?",
|
||||
content: "This action is irreversible",
|
||||
okText: "Yes",
|
||||
okType: "danger",
|
||||
cancelText: "No",
|
||||
onOk: async () => {
|
||||
await PostModel.deletePost({ post_id: data._id }).catch(() => {
|
||||
antd.message.error("Failed to delete post")
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
ontoggleOpen = (to, data) => {
|
||||
if (typeof this.props.onOpenPost === "function") {
|
||||
this.props.onOpenPost(to, data)
|
||||
}
|
||||
}
|
||||
|
||||
onLoadMore = async () => {
|
||||
if (typeof this.props.onLoadMore === "function") {
|
||||
return this.handleLoad(this.props.onLoadMore)
|
||||
} else if (this.props.loadFromModel) {
|
||||
return this.handleLoad(this.props.loadFromModel)
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.initialLoading) {
|
||||
return <antd.Skeleton active />
|
||||
}
|
||||
|
||||
if (this.state.list.length === 0) {
|
||||
if (typeof this.props.emptyListRender === "function") {
|
||||
return React.createElement(this.props.emptyListRender)
|
||||
}
|
||||
|
||||
return <div className="no_more_posts">
|
||||
<antd.Empty />
|
||||
<h1>Whoa, nothing on here...</h1>
|
||||
</div>
|
||||
}
|
||||
|
||||
const PostListProps = {
|
||||
list: this.state.list,
|
||||
|
||||
disableReplyTag: this.props.disableReplyTag,
|
||||
disableHasReplies: this.props.disableHasReplies,
|
||||
|
||||
onLikePost: this.onLikePost,
|
||||
onSavePost: this.onSavePost,
|
||||
onDeletePost: this.onDeletePost,
|
||||
onEditPost: this.onEditPost,
|
||||
onReplyPost: this.onReplyPost,
|
||||
onDoubleClick: this.onDoubleClickPost,
|
||||
|
||||
onLoadMore: this.onLoadMore,
|
||||
hasMore: this.state.hasMore,
|
||||
loading: this.state.loading,
|
||||
|
||||
realtimeUpdates: this.state.realtimeUpdates,
|
||||
resumingLoading: this.state.resumingLoading,
|
||||
onResumeRealtimeUpdates: this.onResumeRealtimeUpdates,
|
||||
}
|
||||
|
||||
if (app.isMobile) {
|
||||
return <PostList
|
||||
ref={this.listRef}
|
||||
{...PostListProps}
|
||||
/>
|
||||
}
|
||||
|
||||
return <div className="post-list_wrapper">
|
||||
<PostList
|
||||
ref={this.listRef}
|
||||
{...PostListProps}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
state = {
|
||||
openPost: null,
|
||||
|
||||
loading: false,
|
||||
resumingLoading: false,
|
||||
initialLoading: true,
|
||||
scrollingToTop: false,
|
||||
|
||||
topVisible: true,
|
||||
|
||||
realtimeUpdates: true,
|
||||
|
||||
hasMore: true,
|
||||
list: this.props.list ?? [],
|
||||
}
|
||||
|
||||
parentRef = this.props.innerRef
|
||||
listRef = React.createRef()
|
||||
|
||||
timelineWsEvents = {
|
||||
"feed.new": (data) => {
|
||||
console.log("New feed => ", data)
|
||||
|
||||
if (!this.state.realtimeUpdates) {
|
||||
return
|
||||
}
|
||||
|
||||
this.setState({
|
||||
list: [data, ...this.state.list],
|
||||
})
|
||||
},
|
||||
"post.new": (data) => {
|
||||
console.log("New post => ", data)
|
||||
|
||||
if (!this.state.realtimeUpdates) {
|
||||
return
|
||||
}
|
||||
|
||||
this.setState({
|
||||
list: [data, ...this.state.list],
|
||||
})
|
||||
},
|
||||
"post.delete": (id) => {
|
||||
console.log("Deleted post => ", id)
|
||||
|
||||
this.setState({
|
||||
list: this.state.list.filter((post) => {
|
||||
return post._id !== id
|
||||
}),
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
handleLoad = async (fn, params = {}) => {
|
||||
this.setState({
|
||||
loading: true,
|
||||
})
|
||||
|
||||
let payload = {
|
||||
trim: this.state.list.length,
|
||||
limit: app.cores.settings.get("feed_max_fetch"),
|
||||
}
|
||||
|
||||
if (this.props.loadFromModelProps) {
|
||||
payload = {
|
||||
...payload,
|
||||
...this.props.loadFromModelProps,
|
||||
}
|
||||
}
|
||||
|
||||
if (params.replace) {
|
||||
payload.trim = 0
|
||||
}
|
||||
|
||||
const result = await fn(payload).catch((err) => {
|
||||
console.error(err)
|
||||
|
||||
app.message.error("Failed to load more posts")
|
||||
|
||||
return null
|
||||
})
|
||||
|
||||
console.log("Loaded posts => ", result)
|
||||
|
||||
if (result) {
|
||||
if (result.length === 0) {
|
||||
return this.setState({
|
||||
hasMore: false,
|
||||
})
|
||||
}
|
||||
|
||||
if (params.replace) {
|
||||
this.setState({
|
||||
list: result,
|
||||
})
|
||||
} else {
|
||||
this.setState({
|
||||
list: [...this.state.list, ...result],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({
|
||||
loading: false,
|
||||
})
|
||||
}
|
||||
|
||||
addPost = (post) => {
|
||||
this.setState({
|
||||
list: [post, ...this.state.list],
|
||||
})
|
||||
}
|
||||
|
||||
removePost = (id) => {
|
||||
this.setState({
|
||||
list: this.state.list.filter((post) => {
|
||||
return post._id !== id
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
_hacks = {
|
||||
addPost: this.addPost,
|
||||
removePost: this.removePost,
|
||||
addRandomPost: () => {
|
||||
const randomId = Math.random().toString(36).substring(7)
|
||||
|
||||
this.addPost({
|
||||
_id: randomId,
|
||||
message: `Random post ${randomId}`,
|
||||
user: {
|
||||
_id: randomId,
|
||||
username: "random user",
|
||||
avatar: `https://api.dicebear.com/7.x/thumbs/svg?seed=${randomId}`,
|
||||
},
|
||||
})
|
||||
},
|
||||
listRef: this.listRef,
|
||||
}
|
||||
|
||||
onResumeRealtimeUpdates = async () => {
|
||||
console.log("Resuming realtime updates")
|
||||
|
||||
this.setState({
|
||||
resumingLoading: true,
|
||||
scrollingToTop: true,
|
||||
})
|
||||
|
||||
this.listRef.current.scrollTo({
|
||||
top: 0,
|
||||
behavior: "smooth",
|
||||
})
|
||||
|
||||
// reload posts
|
||||
await this.handleLoad(this.props.loadFromModel, {
|
||||
replace: true,
|
||||
})
|
||||
|
||||
this.setState({
|
||||
realtimeUpdates: true,
|
||||
resumingLoading: false,
|
||||
})
|
||||
}
|
||||
|
||||
onScrollList = (e) => {
|
||||
const { scrollTop } = e.target
|
||||
|
||||
if (this.state.scrollingToTop && scrollTop === 0) {
|
||||
this.setState({
|
||||
scrollingToTop: false,
|
||||
})
|
||||
}
|
||||
|
||||
if (scrollTop > 200) {
|
||||
if (this.state.topVisible) {
|
||||
this.setState({
|
||||
topVisible: false,
|
||||
})
|
||||
|
||||
if (typeof this.props.onTopVisibility === "function") {
|
||||
this.props.onTopVisibility(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
!this.props.realtime ||
|
||||
this.state.resumingLoading ||
|
||||
this.state.scrollingToTop
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
this.setState({
|
||||
realtimeUpdates: false,
|
||||
})
|
||||
} else {
|
||||
if (!this.state.topVisible) {
|
||||
this.setState({
|
||||
topVisible: true,
|
||||
})
|
||||
|
||||
if (typeof this.props.onTopVisibility === "function") {
|
||||
this.props.onTopVisibility(true)
|
||||
}
|
||||
|
||||
// if (this.props.realtime || !this.state.realtimeUpdates && !this.state.resumingLoading && scrollTop < 5) {
|
||||
// this.onResumeRealtimeUpdates()
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount = async () => {
|
||||
if (typeof this.props.loadFromModel === "function") {
|
||||
await this.handleLoad(this.props.loadFromModel)
|
||||
}
|
||||
|
||||
this.setState({
|
||||
initialLoading: false,
|
||||
})
|
||||
|
||||
if (this.props.watchTimeline) {
|
||||
if (!Array.isArray(this.props.watchTimeline)) {
|
||||
console.error("watchTimeline prop must be an array")
|
||||
} else {
|
||||
this.props.watchTimeline.forEach((event) => {
|
||||
if (typeof this.timelineWsEvents[event] !== "function") {
|
||||
console.error(
|
||||
`The event "${event}" is not defined in the timelineWsEvents object`,
|
||||
)
|
||||
}
|
||||
|
||||
app.cores.api.listenEvent(
|
||||
event,
|
||||
this.timelineWsEvents[event],
|
||||
"posts",
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (this.listRef && this.listRef.current) {
|
||||
this.listRef.current.addEventListener("scroll", this.onScrollList)
|
||||
}
|
||||
|
||||
window._hacks = this._hacks
|
||||
}
|
||||
|
||||
componentWillUnmount = async () => {
|
||||
if (this.props.watchTimeline) {
|
||||
if (!Array.isArray(this.props.watchTimeline)) {
|
||||
console.error("watchTimeline prop must be an array")
|
||||
} else {
|
||||
this.props.watchTimeline.forEach((event) => {
|
||||
if (typeof this.timelineWsEvents[event] !== "function") {
|
||||
console.error(
|
||||
`The event "${event}" is not defined in the timelineWsEvents object`,
|
||||
)
|
||||
}
|
||||
|
||||
app.cores.api.unlistenEvent(
|
||||
event,
|
||||
this.timelineWsEvents[event],
|
||||
"posts",
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (this.listRef && this.listRef.current) {
|
||||
this.listRef.current.removeEventListener(
|
||||
"scroll",
|
||||
this.onScrollList,
|
||||
)
|
||||
}
|
||||
|
||||
window._hacks = null
|
||||
}
|
||||
|
||||
componentDidUpdate = async (prevProps, prevState) => {
|
||||
if (prevProps.list !== this.props.list) {
|
||||
this.setState({
|
||||
list: this.props.list,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
onLikePost = async (data) => {
|
||||
let result = await PostModel.toggleLike({ post_id: data._id }).catch(
|
||||
() => {
|
||||
antd.message.error("Failed to like post")
|
||||
|
||||
return false
|
||||
},
|
||||
)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
onSavePost = async (data) => {
|
||||
let result = await PostModel.toggleSave({ post_id: data._id }).catch(
|
||||
() => {
|
||||
antd.message.error("Failed to save post")
|
||||
|
||||
return false
|
||||
},
|
||||
)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
onEditPost = (data) => {
|
||||
app.controls.openPostCreator({
|
||||
edit_post: data._id,
|
||||
})
|
||||
}
|
||||
|
||||
onReplyPost = (data) => {
|
||||
app.controls.openPostCreator({
|
||||
reply_to: data._id,
|
||||
})
|
||||
}
|
||||
|
||||
onDoubleClickPost = (data) => {
|
||||
app.navigation.goToPost(data._id)
|
||||
}
|
||||
|
||||
onDeletePost = async (data) => {
|
||||
antd.Modal.confirm({
|
||||
title: "Are you sure you want to delete this post?",
|
||||
content: "This action is irreversible",
|
||||
okText: "Yes",
|
||||
okType: "danger",
|
||||
cancelText: "No",
|
||||
onOk: async () => {
|
||||
await PostModel.deletePost({ post_id: data._id }).catch(() => {
|
||||
antd.message.error("Failed to delete post")
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
ontoggleOpen = (to, data) => {
|
||||
if (typeof this.props.onOpenPost === "function") {
|
||||
this.props.onOpenPost(to, data)
|
||||
}
|
||||
}
|
||||
|
||||
onLoadMore = async () => {
|
||||
if (typeof this.props.onLoadMore === "function") {
|
||||
return this.handleLoad(this.props.onLoadMore)
|
||||
} else if (this.props.loadFromModel) {
|
||||
return this.handleLoad(this.props.loadFromModel)
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.initialLoading) {
|
||||
return <antd.Skeleton active />
|
||||
}
|
||||
|
||||
if (this.state.list.length === 0) {
|
||||
if (typeof this.props.emptyListRender === "function") {
|
||||
return React.createElement(this.props.emptyListRender)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="no_more_posts">
|
||||
<antd.Empty />
|
||||
<h1>Whoa, nothing on here...</h1>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const PostListProps = {
|
||||
list: this.state.list,
|
||||
|
||||
disableReplyTag: this.props.disableReplyTag,
|
||||
disableHasReplies: this.props.disableHasReplies,
|
||||
|
||||
onLikePost: this.onLikePost,
|
||||
onSavePost: this.onSavePost,
|
||||
onDeletePost: this.onDeletePost,
|
||||
onEditPost: this.onEditPost,
|
||||
onReplyPost: this.onReplyPost,
|
||||
onDoubleClick: this.onDoubleClickPost,
|
||||
|
||||
onLoadMore: this.onLoadMore,
|
||||
hasMore: this.state.hasMore,
|
||||
loading: this.state.loading,
|
||||
|
||||
realtimeUpdates: this.state.realtimeUpdates,
|
||||
resumingLoading: this.state.resumingLoading,
|
||||
onResumeRealtimeUpdates: this.onResumeRealtimeUpdates,
|
||||
}
|
||||
|
||||
if (app.isMobile) {
|
||||
return <PostList ref={this.listRef} {...PostListProps} />
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="post-list_wrapper">
|
||||
<PostList ref={this.listRef} {...PostListProps} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default React.forwardRef((props, ref) => <PostsListsComponent innerRef={ref} {...props} />)
|
||||
export default React.forwardRef((props, ref) => (
|
||||
<PostsListsComponent innerRef={ref} {...props} />
|
||||
))
|
||||
|
@ -1,86 +1,84 @@
|
||||
import React from "react"
|
||||
|
||||
import { createIconRender } from "@components/Icons"
|
||||
import { AnimatePresence, motion } from "framer-motion"
|
||||
import { AnimatePresence, motion } from "motion/react"
|
||||
|
||||
import "./index.less"
|
||||
|
||||
const ContextMenu = (props) => {
|
||||
const [visible, setVisible] = React.useState(true)
|
||||
const { items = [], cords, clickedComponent, ctx } = props
|
||||
const [visible, setVisible] = React.useState(true)
|
||||
const { items = [], cords, clickedComponent, ctx } = props
|
||||
|
||||
async function onClose() {
|
||||
setVisible(false)
|
||||
props.unregisterOnClose(onClose)
|
||||
}
|
||||
async function onClose() {
|
||||
setVisible(false)
|
||||
props.unregisterOnClose(onClose)
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
props.registerOnClose(onClose)
|
||||
}, [])
|
||||
React.useEffect(() => {
|
||||
props.registerOnClose(onClose)
|
||||
}, [])
|
||||
|
||||
const handleItemClick = async (item) => {
|
||||
if (typeof item.action === "function") {
|
||||
await item.action(clickedComponent, ctx)
|
||||
}
|
||||
}
|
||||
const handleItemClick = async (item) => {
|
||||
if (typeof item.action === "function") {
|
||||
await item.action(clickedComponent, ctx)
|
||||
}
|
||||
}
|
||||
|
||||
const renderItems = () => {
|
||||
if (items.length === 0) {
|
||||
return <div>
|
||||
<p>No items</p>
|
||||
</div>
|
||||
}
|
||||
const renderItems = () => {
|
||||
if (items.length === 0) {
|
||||
return (
|
||||
<div>
|
||||
<p>No items</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return items.map((item, index) => {
|
||||
if (item.type === "separator") {
|
||||
return <div key={index} className="context-menu-separator" />
|
||||
}
|
||||
return items.map((item, index) => {
|
||||
if (item.type === "separator") {
|
||||
return <div key={index} className="context-menu-separator" />
|
||||
}
|
||||
|
||||
return <div
|
||||
key={index}
|
||||
onClick={() => handleItemClick(item)}
|
||||
className="item"
|
||||
>
|
||||
<p className="label">
|
||||
{item.label}
|
||||
</p>
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
onClick={() => handleItemClick(item)}
|
||||
className="item"
|
||||
>
|
||||
<p className="label">{item.label}</p>
|
||||
|
||||
{
|
||||
item.description && <p className="description">
|
||||
{item.description}
|
||||
</p>
|
||||
}
|
||||
{item.description && (
|
||||
<p className="description">{item.description}</p>
|
||||
)}
|
||||
|
||||
{
|
||||
createIconRender(item.icon)
|
||||
}
|
||||
</div>
|
||||
})
|
||||
}
|
||||
{createIconRender(item.icon)}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
return <AnimatePresence>
|
||||
{
|
||||
visible && <div
|
||||
className="context-menu-wrapper"
|
||||
style={{
|
||||
top: cords.y,
|
||||
left: cords.x,
|
||||
}}
|
||||
>
|
||||
<motion.div
|
||||
className="context-menu"
|
||||
initial={{ opacity: 0, scale: 0.8 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.3 }}
|
||||
transition={{ duration: 0.05, ease: "easeInOut" }}
|
||||
>
|
||||
{
|
||||
renderItems()
|
||||
}
|
||||
</motion.div>
|
||||
</div>
|
||||
}
|
||||
</AnimatePresence>
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{visible && (
|
||||
<div
|
||||
className="context-menu-wrapper"
|
||||
style={{
|
||||
top: cords.y,
|
||||
left: cords.x,
|
||||
}}
|
||||
>
|
||||
<motion.div
|
||||
className="context-menu"
|
||||
initial={{ opacity: 0, scale: 0.8 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.3 }}
|
||||
transition={{ duration: 0.05, ease: "easeInOut" }}
|
||||
>
|
||||
{renderItems()}
|
||||
</motion.div>
|
||||
</div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
)
|
||||
}
|
||||
|
||||
export default ContextMenu
|
||||
export default ContextMenu
|
||||
|
@ -1,68 +1,67 @@
|
||||
import { Core } from "@ragestudio/vessel"
|
||||
import { Haptics } from "@capacitor/haptics"
|
||||
// import { Haptics } from "@capacitor/haptics"
|
||||
|
||||
const vibrationPatterns = {
|
||||
light: [10],
|
||||
medium: [50],
|
||||
heavy: [80],
|
||||
error: [100, 30, 100, 30, 100],
|
||||
light: [10],
|
||||
medium: [50],
|
||||
heavy: [80],
|
||||
error: [100, 30, 100, 30, 100],
|
||||
}
|
||||
|
||||
export default class HapticsCore extends Core {
|
||||
static namespace = "haptics"
|
||||
static namespace = "haptics"
|
||||
|
||||
static dependencies = [
|
||||
"settings"
|
||||
]
|
||||
static dependencies = ["settings"]
|
||||
|
||||
static isGlobalDisabled() {
|
||||
return app.cores.settings.is("haptics:enabled", false)
|
||||
}
|
||||
static isGlobalDisabled() {
|
||||
return app.cores.settings.is("haptics:enabled", false)
|
||||
}
|
||||
|
||||
async onInitialize() {
|
||||
if (window.navigator.userAgent === "capacitor") {
|
||||
navigator.vibrate = this.nativeVibrate
|
||||
}
|
||||
async onInitialize() {
|
||||
if (window.navigator.userAgent === "capacitor") {
|
||||
navigator.vibrate = this.nativeVibrate
|
||||
}
|
||||
|
||||
document.addEventListener("click", this.handleClickEvent)
|
||||
}
|
||||
document.addEventListener("click", this.handleClickEvent)
|
||||
}
|
||||
|
||||
public = {
|
||||
isGlobalDisabled: HapticsCore.isGlobalDisabled,
|
||||
vibrate: this.vibrate.bind(this),
|
||||
}
|
||||
public = {
|
||||
//isGlobalDisabled: HapticsCore.isGlobalDisabled,
|
||||
vibrate: this.vibrate.bind(this),
|
||||
}
|
||||
|
||||
nativeVibrate = (pattern) => {
|
||||
if (!Array.isArray(pattern)) {
|
||||
pattern = [pattern]
|
||||
}
|
||||
// nativeVibrate = (pattern) => {
|
||||
// if (!Array.isArray(pattern)) {
|
||||
// pattern = [pattern]
|
||||
// }
|
||||
|
||||
for (let i = 0; i < pattern.length; i++) {
|
||||
Haptics.vibrate({
|
||||
duration: pattern[i],
|
||||
})
|
||||
}
|
||||
}
|
||||
// for (let i = 0; i < pattern.length; i++) {
|
||||
// Haptics.vibrate({
|
||||
// duration: pattern[i],
|
||||
// })
|
||||
// }
|
||||
// }
|
||||
|
||||
handleClickEvent = (event) => {
|
||||
const button = event.target.closest("button") || event.target.closest(".ant-btn")
|
||||
handleClickEvent = (event) => {
|
||||
const button =
|
||||
event.target.closest("button") || event.target.closest(".ant-btn")
|
||||
|
||||
if (button) {
|
||||
this.vibrate("light")
|
||||
}
|
||||
}
|
||||
if (button) {
|
||||
this.vibrate("light")
|
||||
}
|
||||
}
|
||||
|
||||
vibrate(pattern = "light") {
|
||||
const disabled = HapticsCore.isGlobalDisabled()
|
||||
vibrate(pattern = "light") {
|
||||
const disabled = HapticsCore.isGlobalDisabled()
|
||||
|
||||
if (disabled) {
|
||||
return false
|
||||
}
|
||||
if (disabled) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (typeof pattern === "string") {
|
||||
pattern = vibrationPatterns[pattern]
|
||||
}
|
||||
if (typeof pattern === "string") {
|
||||
pattern = vibrationPatterns[pattern]
|
||||
}
|
||||
|
||||
return navigator.vibrate(pattern)
|
||||
}
|
||||
}
|
||||
return navigator.vibrate(pattern)
|
||||
}
|
||||
}
|
||||
|
@ -1,41 +1,48 @@
|
||||
import { Haptics } from "@capacitor/haptics"
|
||||
//import { Haptics } from "@capacitor/haptics"
|
||||
|
||||
const NotfTypeToAudio = {
|
||||
info: "notification",
|
||||
success: "notification",
|
||||
warning: "warn",
|
||||
error: "error",
|
||||
info: "notification",
|
||||
success: "notification",
|
||||
warning: "warn",
|
||||
error: "error",
|
||||
}
|
||||
|
||||
class NotificationFeedback {
|
||||
static getSoundVolume = () => {
|
||||
return (window.app.cores.settings.get("sfx:notifications_volume") ?? 50) / 100
|
||||
}
|
||||
static getSoundVolume = () => {
|
||||
return (
|
||||
(window.app.cores.settings.get("sfx:notifications_volume") ?? 50) /
|
||||
100
|
||||
)
|
||||
}
|
||||
|
||||
static playHaptic = async () => {
|
||||
if (app.cores.settings.get("haptics:notifications_feedback")) {
|
||||
await Haptics.vibrate()
|
||||
}
|
||||
}
|
||||
static playHaptic = async () => {
|
||||
if (app.cores.settings.get("haptics:notifications_feedback")) {
|
||||
//await Haptics.vibrate()
|
||||
//use navigator.vibrate
|
||||
}
|
||||
}
|
||||
|
||||
static playAudio = (type) => {
|
||||
if (app.cores.settings.get("sfx:notifications_feedback")) {
|
||||
if (typeof window.app.cores.sfx?.play === "function") {
|
||||
window.app.cores.sfx.play(NotfTypeToAudio[type] ?? "notification", {
|
||||
volume: NotificationFeedback.getSoundVolume(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
static playAudio = (type) => {
|
||||
if (app.cores.settings.get("sfx:notifications_feedback")) {
|
||||
if (typeof window.app.cores.sfx?.play === "function") {
|
||||
window.app.cores.sfx.play(
|
||||
NotfTypeToAudio[type] ?? "notification",
|
||||
{
|
||||
volume: NotificationFeedback.getSoundVolume(),
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static async feedback({ type = "notification", feedback = true }) {
|
||||
if (!feedback) {
|
||||
return false
|
||||
}
|
||||
static async feedback({ type = "notification", feedback = true }) {
|
||||
if (!feedback) {
|
||||
return false
|
||||
}
|
||||
|
||||
NotificationFeedback.playHaptic(type)
|
||||
NotificationFeedback.playAudio(type)
|
||||
}
|
||||
NotificationFeedback.playHaptic(type)
|
||||
NotificationFeedback.playAudio(type)
|
||||
}
|
||||
}
|
||||
|
||||
export default NotificationFeedback
|
||||
export default NotificationFeedback
|
||||
|
@ -1,131 +0,0 @@
|
||||
import { CapacitorMusicControls } from "capacitor-music-controls-plugin-v3"
|
||||
|
||||
export default class MediaSession {
|
||||
initialize() {
|
||||
CapacitorMusicControls.addListener("controlsNotification", (info) => {
|
||||
console.log(info)
|
||||
|
||||
this.handleControlsEvent(info)
|
||||
})
|
||||
|
||||
document.addEventListener("controlsNotification", (event) => {
|
||||
console.log(event)
|
||||
|
||||
const info = { message: event.message, position: 0 }
|
||||
|
||||
this.handleControlsEvent(info)
|
||||
})
|
||||
}
|
||||
|
||||
update(manifest) {
|
||||
try {
|
||||
if ("mediaSession" in navigator) {
|
||||
return navigator.mediaSession.metadata = new MediaMetadata({
|
||||
title: manifest.title,
|
||||
artist: manifest.artist,
|
||||
album: manifest.album,
|
||||
artwork: [
|
||||
{
|
||||
src: manifest.cover ?? manifest.thumbnail,
|
||||
sizes: "512x512",
|
||||
type: "image/jpeg",
|
||||
}
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
return CapacitorMusicControls.create({
|
||||
track: manifest.title,
|
||||
artist: manifest.artist,
|
||||
album: manifest.album,
|
||||
cover: manifest.cover,
|
||||
|
||||
hasPrev: false,
|
||||
hasNext: false,
|
||||
hasClose: true,
|
||||
|
||||
isPlaying: true,
|
||||
dismissable: false,
|
||||
|
||||
playIcon: "media_play",
|
||||
pauseIcon: "media_pause",
|
||||
prevIcon: "media_prev",
|
||||
nextIcon: "media_next",
|
||||
closeIcon: "media_close",
|
||||
notificationIcon: "notification"
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
updateIsPlaying(to, timeElapsed = 0) {
|
||||
try {
|
||||
if ("mediaSession" in navigator) {
|
||||
return navigator.mediaSession.playbackState = to ? "playing" : "paused"
|
||||
}
|
||||
|
||||
return CapacitorMusicControls.updateIsPlaying({
|
||||
isPlaying: to,
|
||||
elapsed: timeElapsed,
|
||||
})
|
||||
} catch {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
destroy() {
|
||||
if ("mediaSession" in navigator) {
|
||||
navigator.mediaSession.playbackState = "none"
|
||||
}
|
||||
|
||||
this.active = false
|
||||
|
||||
return CapacitorMusicControls.destroy()
|
||||
}
|
||||
|
||||
handleControlsEvent(action) {
|
||||
try {
|
||||
const message = action.message
|
||||
|
||||
switch (message) {
|
||||
case "music-controls-next": {
|
||||
return app.cores.player.playback.next()
|
||||
}
|
||||
case "music-controls-previous": {
|
||||
return app.cores.player.playback.previous()
|
||||
}
|
||||
case "music-controls-pause": {
|
||||
return app.cores.player.playback.pause()
|
||||
}
|
||||
case "music-controls-play": {
|
||||
return app.cores.player.playback.play()
|
||||
}
|
||||
case "music-controls-destroy": {
|
||||
return app.cores.player.playback.stop()
|
||||
}
|
||||
|
||||
// External controls (iOS only)
|
||||
case "music-controls-toggle-play-pause": {
|
||||
return app.cores.player.playback.toggle()
|
||||
}
|
||||
|
||||
// Headset events (Android only)
|
||||
// All media button events are listed below
|
||||
case "music-controls-media-button": {
|
||||
return app.cores.player.playback.toggle()
|
||||
}
|
||||
case "music-controls-headset-unplugged": {
|
||||
return app.cores.player.playback.pause()
|
||||
}
|
||||
case "music-controls-headset-plugged": {
|
||||
return app.cores.player.playback.play()
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,11 +1,8 @@
|
||||
import React from "react"
|
||||
import progressBar from "nprogress"
|
||||
|
||||
import Layouts from "@layouts"
|
||||
|
||||
export default class Layout extends React.PureComponent {
|
||||
progressBar = progressBar.configure({ parent: "html", showSpinner: false })
|
||||
|
||||
state = {
|
||||
layoutType: "default",
|
||||
renderError: null,
|
||||
@ -17,14 +14,18 @@ export default class Layout extends React.PureComponent {
|
||||
},
|
||||
"layout.animations.fadeOut": () => {
|
||||
if (app.cores.settings.get("reduceAnimations")) {
|
||||
console.warn("Skipping fadeIn animation due to `reduceAnimations` setting")
|
||||
console.warn(
|
||||
"Skipping fadeIn animation due to `reduceAnimations` setting",
|
||||
)
|
||||
return false
|
||||
}
|
||||
|
||||
const transitionLayer = document.getElementById("transitionLayer")
|
||||
|
||||
if (!transitionLayer) {
|
||||
console.warn("transitionLayer not found, no animation will be played")
|
||||
console.warn(
|
||||
"transitionLayer not found, no animation will be played",
|
||||
)
|
||||
return false
|
||||
}
|
||||
|
||||
@ -32,19 +33,23 @@ export default class Layout extends React.PureComponent {
|
||||
},
|
||||
"layout.animations.fadeIn": () => {
|
||||
if (app.cores.settings.get("reduceAnimations")) {
|
||||
console.warn("Skipping fadeOut animation due to `reduceAnimations` setting")
|
||||
console.warn(
|
||||
"Skipping fadeOut animation due to `reduceAnimations` setting",
|
||||
)
|
||||
return false
|
||||
}
|
||||
|
||||
const transitionLayer = document.getElementById("transitionLayer")
|
||||
|
||||
if (!transitionLayer) {
|
||||
console.warn("transitionLayer not found, no animation will be played")
|
||||
console.warn(
|
||||
"transitionLayer not found, no animation will be played",
|
||||
)
|
||||
return false
|
||||
}
|
||||
|
||||
transitionLayer.classList.remove("fade-opacity-leave")
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
@ -58,7 +63,10 @@ export default class Layout extends React.PureComponent {
|
||||
}
|
||||
|
||||
if (app.cores.settings.get("reduceAnimations")) {
|
||||
this.layoutInterface.toggleRootContainerClassname("reduce-animations", true)
|
||||
this.layoutInterface.toggleRootContainerClassname(
|
||||
"reduce-animations",
|
||||
true,
|
||||
)
|
||||
}
|
||||
|
||||
this.layoutInterface.toggleCenteredContent(true)
|
||||
@ -75,7 +83,7 @@ export default class Layout extends React.PureComponent {
|
||||
this.setState({ renderError: { info, stack } })
|
||||
}
|
||||
|
||||
layoutInterface = window.app.layout = {
|
||||
layoutInterface = (window.app.layout = {
|
||||
set: (layout) => {
|
||||
if (typeof Layouts[layout] !== "function") {
|
||||
return console.error("Layout not found")
|
||||
@ -88,28 +96,52 @@ export default class Layout extends React.PureComponent {
|
||||
})
|
||||
},
|
||||
toggleCenteredContent: (to) => {
|
||||
return this.layoutInterface.toggleRootContainerClassname("centered-content", to)
|
||||
return this.layoutInterface.toggleRootContainerClassname(
|
||||
"centered-content",
|
||||
to,
|
||||
)
|
||||
},
|
||||
toggleRootScaleEffect: (to) => {
|
||||
return this.layoutInterface.toggleRootContainerClassname("root-scale-effect", to)
|
||||
return this.layoutInterface.toggleRootContainerClassname(
|
||||
"root-scale-effect",
|
||||
to,
|
||||
)
|
||||
},
|
||||
toggleMobileStyle: (to) => {
|
||||
return this.layoutInterface.toggleRootContainerClassname("mobile", to)
|
||||
return this.layoutInterface.toggleRootContainerClassname(
|
||||
"mobile",
|
||||
to,
|
||||
)
|
||||
},
|
||||
toggleReducedAnimations: (to) => {
|
||||
return this.layoutInterface.toggleRootContainerClassname("reduce-animations", to)
|
||||
return this.layoutInterface.toggleRootContainerClassname(
|
||||
"reduce-animations",
|
||||
to,
|
||||
)
|
||||
},
|
||||
toggleTopBarSpacer: (to) => {
|
||||
return this.layoutInterface.toggleRootContainerClassname("top-bar-spacer", to)
|
||||
return this.layoutInterface.toggleRootContainerClassname(
|
||||
"top-bar-spacer",
|
||||
to,
|
||||
)
|
||||
},
|
||||
toggleDisableTopLayoutPadding: (to) => {
|
||||
return this.layoutInterface.toggleRootContainerClassname("disable-top-layout-padding", to)
|
||||
return this.layoutInterface.toggleRootContainerClassname(
|
||||
"disable-top-layout-padding",
|
||||
to,
|
||||
)
|
||||
},
|
||||
togglePagePanelSpacer: (to) => {
|
||||
return this.layoutInterface.toggleRootContainerClassname("page-panel-spacer", to)
|
||||
return this.layoutInterface.toggleRootContainerClassname(
|
||||
"page-panel-spacer",
|
||||
to,
|
||||
)
|
||||
},
|
||||
toggleCompactMode: (to) => {
|
||||
return this.layoutInterface.toggleRootContainerClassname("compact-mode", to)
|
||||
return this.layoutInterface.toggleRootContainerClassname(
|
||||
"compact-mode",
|
||||
to,
|
||||
)
|
||||
},
|
||||
toggleRootContainerClassname: (classname, to) => {
|
||||
const root = document.documentElement
|
||||
@ -119,7 +151,10 @@ export default class Layout extends React.PureComponent {
|
||||
return false
|
||||
}
|
||||
|
||||
to = typeof to === "boolean" ? to : !root.classList.contains(classname)
|
||||
to =
|
||||
typeof to === "boolean"
|
||||
? to
|
||||
: !root.classList.contains(classname)
|
||||
|
||||
if (root.classList.contains(classname) === to) {
|
||||
// ignore
|
||||
@ -154,8 +189,8 @@ export default class Layout extends React.PureComponent {
|
||||
...to,
|
||||
behavior: "smooth",
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
render() {
|
||||
let layoutType = this.state.layoutType
|
||||
@ -167,7 +202,10 @@ export default class Layout extends React.PureComponent {
|
||||
|
||||
if (this.state.renderError) {
|
||||
if (this.props.staticRenders?.RenderError) {
|
||||
return React.createElement(this.props.staticRenders?.RenderError, { error: this.state.renderError })
|
||||
return React.createElement(
|
||||
this.props.staticRenders?.RenderError,
|
||||
{ error: this.state.renderError },
|
||||
)
|
||||
}
|
||||
|
||||
return JSON.stringify(this.state.renderError)
|
||||
@ -176,11 +214,12 @@ export default class Layout extends React.PureComponent {
|
||||
const Layout = Layouts[layoutType]
|
||||
|
||||
if (!Layout) {
|
||||
return app.eventBus.emit("runtime.crash", new Error(`Layout type [${layoutType}] not found`))
|
||||
return app.eventBus.emit(
|
||||
"runtime.crash",
|
||||
new Error(`Layout type [${layoutType}] not found`),
|
||||
)
|
||||
}
|
||||
|
||||
return <Layout {...layoutComponentProps}>
|
||||
{this.props.children}
|
||||
</Layout>
|
||||
return <Layout {...layoutComponentProps}>{this.props.children}</Layout>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,13 +1,16 @@
|
||||
import React from "react"
|
||||
import * as antd from "antd"
|
||||
import classnames from "classnames"
|
||||
import { Motion, spring } from "react-motion"
|
||||
import { motion, AnimatePresence } from "motion/react"
|
||||
|
||||
import { Icons, createIconRender } from "@components/Icons"
|
||||
|
||||
import { WithPlayerContext, Context } from "@contexts/WithPlayerContext"
|
||||
|
||||
import { QuickNavMenuItems, QuickNavMenu } from "@layouts/components/@mobile/quickNav"
|
||||
import {
|
||||
QuickNavMenuItems,
|
||||
QuickNavMenu,
|
||||
} from "@layouts/components/@mobile/quickNav"
|
||||
|
||||
import PlayerView from "@pages/@mobile-views/player"
|
||||
import CreatorView from "@pages/@mobile-views/creator"
|
||||
@ -15,407 +18,434 @@ import CreatorView from "@pages/@mobile-views/creator"
|
||||
import "./index.less"
|
||||
|
||||
const tourSteps = [
|
||||
{
|
||||
title: "Quick nav",
|
||||
description: "Tap & hold on the icon to open the navigation menu.",
|
||||
placement: "top",
|
||||
refName: "navBtnRef",
|
||||
},
|
||||
{
|
||||
title: "Account button",
|
||||
description: "Tap & hold on the account icon to open miscellaneous options.",
|
||||
placement: "top",
|
||||
refName: "accountBtnRef",
|
||||
}
|
||||
{
|
||||
title: "Quick nav",
|
||||
description: "Tap & hold on the icon to open the navigation menu.",
|
||||
placement: "top",
|
||||
refName: "navBtnRef",
|
||||
},
|
||||
{
|
||||
title: "Account button",
|
||||
description:
|
||||
"Tap & hold on the account icon to open miscellaneous options.",
|
||||
placement: "top",
|
||||
refName: "accountBtnRef",
|
||||
},
|
||||
]
|
||||
|
||||
const openPlayerView = () => {
|
||||
app.layout.draggable.open("player", PlayerView)
|
||||
app.layout.draggable.open("player", PlayerView)
|
||||
}
|
||||
const openCreator = () => {
|
||||
app.layout.draggable.open("creator", CreatorView)
|
||||
app.layout.draggable.open("creator", CreatorView)
|
||||
}
|
||||
|
||||
const PlayerButton = (props) => {
|
||||
React.useEffect(() => {
|
||||
openPlayerView()
|
||||
}, [])
|
||||
React.useEffect(() => {
|
||||
openPlayerView()
|
||||
}, [])
|
||||
|
||||
return <div
|
||||
className={classnames(
|
||||
"player_btn",
|
||||
{
|
||||
"bounce": props.playback === "playing"
|
||||
}
|
||||
)}
|
||||
style={{
|
||||
"--average-color": props.colorAnalysis?.rgba,
|
||||
"--color": props.colorAnalysis?.isDark ? "var(--text-color-white)" : "var(--text-color-black)",
|
||||
}}
|
||||
onClick={openPlayerView}
|
||||
>
|
||||
{
|
||||
props.playback === "playing" ? <Icons.MdMusicNote /> : <Icons.MdPause />
|
||||
}
|
||||
</div>
|
||||
return (
|
||||
<div
|
||||
className={classnames("player_btn", {
|
||||
bounce: props.playback === "playing",
|
||||
})}
|
||||
style={{
|
||||
"--average-color": props.colorAnalysis?.rgba,
|
||||
"--color": props.colorAnalysis?.isDark
|
||||
? "var(--text-color-white)"
|
||||
: "var(--text-color-black)",
|
||||
}}
|
||||
onClick={openPlayerView}
|
||||
>
|
||||
{props.playback === "playing" ? (
|
||||
<Icons.MdMusicNote />
|
||||
) : (
|
||||
<Icons.MdPause />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const AccountButton = React.forwardRef((props, ref) => {
|
||||
const user = app.userData
|
||||
const user = app.userData
|
||||
|
||||
const handleClick = () => {
|
||||
if (!user) {
|
||||
return app.navigation.goAuth()
|
||||
}
|
||||
const handleClick = () => {
|
||||
if (!user) {
|
||||
return app.navigation.goAuth()
|
||||
}
|
||||
|
||||
return app.navigation.goToAccount()
|
||||
}
|
||||
return app.navigation.goToAccount()
|
||||
}
|
||||
|
||||
const handleHold = () => {
|
||||
app.layout.draggable.actions({
|
||||
list: [
|
||||
{
|
||||
key: "settings",
|
||||
icon: "FiSettings",
|
||||
label: "Settings",
|
||||
onClick: () => {
|
||||
app.navigation.goToSettings()
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "account",
|
||||
icon: "FiUser",
|
||||
label: "Account",
|
||||
onClick: () => {
|
||||
app.navigation.goToAccount()
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "logout",
|
||||
icon: "FiLogOut",
|
||||
label: "Logout",
|
||||
danger: true,
|
||||
onClick: () => {
|
||||
app.eventBus.emit("app.logout_request")
|
||||
},
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
const handleHold = () => {
|
||||
app.layout.draggable.actions({
|
||||
list: [
|
||||
{
|
||||
key: "settings",
|
||||
icon: "FiSettings",
|
||||
label: "Settings",
|
||||
onClick: () => {
|
||||
app.navigation.goToSettings()
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "account",
|
||||
icon: "FiUser",
|
||||
label: "Account",
|
||||
onClick: () => {
|
||||
app.navigation.goToAccount()
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "logout",
|
||||
icon: "FiLogOut",
|
||||
label: "Logout",
|
||||
danger: true,
|
||||
onClick: () => {
|
||||
app.eventBus.emit("app.logout_request")
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
return <div
|
||||
key="account"
|
||||
id="account"
|
||||
className="item"
|
||||
ref={ref}
|
||||
onClick={handleClick}
|
||||
onContextMenu={handleHold}
|
||||
context-menu="ignore"
|
||||
>
|
||||
<div className="icon">
|
||||
{
|
||||
user ? <antd.Avatar shape="square" src={app.userData.avatar} /> : createIconRender("FiLogin")
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
return (
|
||||
<div
|
||||
key="account"
|
||||
id="account"
|
||||
className="item"
|
||||
ref={ref}
|
||||
onClick={handleClick}
|
||||
onContextMenu={handleHold}
|
||||
context-menu="ignore"
|
||||
>
|
||||
<div className="icon">
|
||||
{user ? (
|
||||
<antd.Avatar shape="square" src={app.userData.avatar} />
|
||||
) : (
|
||||
createIconRender("FiLogin")
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
export class BottomBar extends React.Component {
|
||||
static contextType = Context
|
||||
static contextType = Context
|
||||
|
||||
state = {
|
||||
visible: false,
|
||||
quickNavVisible: false,
|
||||
render: null,
|
||||
tourOpen: false,
|
||||
}
|
||||
state = {
|
||||
visible: false,
|
||||
quickNavVisible: false,
|
||||
render: null,
|
||||
tourOpen: false,
|
||||
}
|
||||
|
||||
busEvents = {
|
||||
"runtime.crash": () => {
|
||||
this.toggleVisibility(false)
|
||||
}
|
||||
}
|
||||
busEvents = {
|
||||
"runtime.crash": () => {
|
||||
this.toggleVisibility(false)
|
||||
},
|
||||
}
|
||||
|
||||
navBtnRef = React.createRef()
|
||||
accountBtnRef = React.createRef()
|
||||
navBtnRef = React.createRef()
|
||||
accountBtnRef = React.createRef()
|
||||
|
||||
componentDidMount = () => {
|
||||
this.interface = app.layout.bottom_bar = {
|
||||
toggleVisible: this.toggleVisibility,
|
||||
isVisible: () => this.state.visible,
|
||||
render: (fragment) => {
|
||||
this.setState({ render: fragment })
|
||||
},
|
||||
clear: () => {
|
||||
this.setState({ render: null })
|
||||
},
|
||||
toggleTour: () => {
|
||||
this.setState({ tourOpen: !this.state.tourOpen })
|
||||
},
|
||||
}
|
||||
componentDidMount = () => {
|
||||
this.interface = app.layout.bottom_bar = {
|
||||
toggleVisible: this.toggleVisibility,
|
||||
isVisible: () => this.state.visible,
|
||||
render: (fragment) => {
|
||||
this.setState({ render: fragment })
|
||||
},
|
||||
clear: () => {
|
||||
this.setState({ render: null })
|
||||
},
|
||||
toggleTour: () => {
|
||||
this.setState({ tourOpen: !this.state.tourOpen })
|
||||
},
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
this.setState({ visible: true })
|
||||
}, 10)
|
||||
setTimeout(() => {
|
||||
this.setState({ visible: true })
|
||||
}, 10)
|
||||
|
||||
// Register bus events
|
||||
Object.keys(this.busEvents).forEach((key) => {
|
||||
app.eventBus.on(key, this.busEvents[key])
|
||||
})
|
||||
// Register bus events
|
||||
Object.keys(this.busEvents).forEach((key) => {
|
||||
app.eventBus.on(key, this.busEvents[key])
|
||||
})
|
||||
|
||||
setTimeout(() => {
|
||||
const isTourFinished = localStorage.getItem("mobile_tour")
|
||||
setTimeout(() => {
|
||||
const isTourFinished = localStorage.getItem("mobile_tour")
|
||||
|
||||
if (!isTourFinished) {
|
||||
this.toggleTour(true)
|
||||
if (!isTourFinished) {
|
||||
this.toggleTour(true)
|
||||
|
||||
localStorage.setItem("mobile_tour", true)
|
||||
}
|
||||
}, 500)
|
||||
}
|
||||
localStorage.setItem("mobile_tour", true)
|
||||
}
|
||||
}, 500)
|
||||
}
|
||||
|
||||
componentWillUnmount = () => {
|
||||
delete window.app.layout.bottom_bar
|
||||
componentWillUnmount = () => {
|
||||
delete window.app.layout.bottom_bar
|
||||
|
||||
// Unregister bus events
|
||||
Object.keys(this.busEvents).forEach((key) => {
|
||||
app.eventBus.off(key, this.busEvents[key])
|
||||
})
|
||||
}
|
||||
// Unregister bus events
|
||||
Object.keys(this.busEvents).forEach((key) => {
|
||||
app.eventBus.off(key, this.busEvents[key])
|
||||
})
|
||||
}
|
||||
|
||||
getTourSteps = () => {
|
||||
return tourSteps.map((step) => {
|
||||
step.target = () => this[step.refName].current
|
||||
getTourSteps = () => {
|
||||
return tourSteps.map((step) => {
|
||||
step.target = () => this[step.refName].current
|
||||
|
||||
return step
|
||||
})
|
||||
}
|
||||
return step
|
||||
})
|
||||
}
|
||||
|
||||
toggleVisibility = (to) => {
|
||||
if (typeof to === "undefined") {
|
||||
to = !this.state.visible
|
||||
}
|
||||
toggleVisibility = (to) => {
|
||||
if (typeof to === "undefined") {
|
||||
to = !this.state.visible
|
||||
}
|
||||
|
||||
this.setState({ visible: to })
|
||||
}
|
||||
this.setState({ visible: to })
|
||||
}
|
||||
|
||||
handleItemClick = (item) => {
|
||||
if (item.dispatchEvent) {
|
||||
app.eventBus.emit(item.dispatchEvent)
|
||||
} else if (item.location) {
|
||||
app.location.push(item.location)
|
||||
}
|
||||
}
|
||||
handleItemClick = (item) => {
|
||||
if (item.dispatchEvent) {
|
||||
app.eventBus.emit(item.dispatchEvent)
|
||||
} else if (item.location) {
|
||||
app.location.push(item.location)
|
||||
}
|
||||
}
|
||||
|
||||
handleNavTouchStart = (e) => {
|
||||
this._navTouchStart = setTimeout(() => {
|
||||
this.setState({ quickNavVisible: true })
|
||||
handleNavTouchStart = (e) => {
|
||||
this._navTouchStart = setTimeout(() => {
|
||||
this.setState({ quickNavVisible: true })
|
||||
|
||||
if (app.cores.haptics?.vibrate) {
|
||||
app.cores.haptics.vibrate(80)
|
||||
}
|
||||
if (app.cores.haptics?.vibrate) {
|
||||
app.cores.haptics.vibrate(80)
|
||||
}
|
||||
|
||||
// remove the timeout
|
||||
this._navTouchStart = null
|
||||
}, 400)
|
||||
}
|
||||
// remove the timeout
|
||||
this._navTouchStart = null
|
||||
}, 400)
|
||||
}
|
||||
|
||||
handleNavTouchEnd = (event) => {
|
||||
if (this._lastHovered) {
|
||||
this._lastHovered.classList.remove("hover")
|
||||
}
|
||||
handleNavTouchEnd = (event) => {
|
||||
if (this._lastHovered) {
|
||||
this._lastHovered.classList.remove("hover")
|
||||
}
|
||||
|
||||
if (this._navTouchStart) {
|
||||
clearTimeout(this._navTouchStart)
|
||||
if (this._navTouchStart) {
|
||||
clearTimeout(this._navTouchStart)
|
||||
|
||||
this._navTouchStart = null
|
||||
this._navTouchStart = null
|
||||
|
||||
return false
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
this.setState({ quickNavVisible: false })
|
||||
this.setState({ quickNavVisible: false })
|
||||
|
||||
// get cords of the touch
|
||||
const x = event.changedTouches[0].clientX
|
||||
const y = event.changedTouches[0].clientY
|
||||
// get cords of the touch
|
||||
const x = event.changedTouches[0].clientX
|
||||
const y = event.changedTouches[0].clientY
|
||||
|
||||
// get the element at the touch
|
||||
const element = document.elementFromPoint(x, y)
|
||||
// get the element at the touch
|
||||
const element = document.elementFromPoint(x, y)
|
||||
|
||||
// get the closest element with the attribute
|
||||
const closest = element.closest(".quick-nav_item")
|
||||
// get the closest element with the attribute
|
||||
const closest = element.closest(".quick-nav_item")
|
||||
|
||||
if (!closest) {
|
||||
return false
|
||||
}
|
||||
if (!closest) {
|
||||
return false
|
||||
}
|
||||
|
||||
const item = QuickNavMenuItems.find((item) => {
|
||||
return item.id === closest.getAttribute("quicknav-item")
|
||||
})
|
||||
const item = QuickNavMenuItems.find((item) => {
|
||||
return item.id === closest.getAttribute("quicknav-item")
|
||||
})
|
||||
|
||||
if (!item) {
|
||||
return false
|
||||
}
|
||||
if (!item) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (item.location) {
|
||||
app.location.push(item.location)
|
||||
if (item.location) {
|
||||
app.location.push(item.location)
|
||||
|
||||
if (app.cores.haptics?.vibrate) {
|
||||
app.cores.haptics.vibrate([40, 80])
|
||||
}
|
||||
}
|
||||
}
|
||||
if (app.cores.haptics?.vibrate) {
|
||||
app.cores.haptics.vibrate([40, 80])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleNavTouchMove = (event) => {
|
||||
// check if the touch is hovering a quicknav item
|
||||
const x = event.changedTouches[0].clientX
|
||||
const y = event.changedTouches[0].clientY
|
||||
handleNavTouchMove = (event) => {
|
||||
// check if the touch is hovering a quicknav item
|
||||
const x = event.changedTouches[0].clientX
|
||||
const y = event.changedTouches[0].clientY
|
||||
|
||||
// get the element at the touch
|
||||
const element = document.elementFromPoint(x, y)
|
||||
// get the element at the touch
|
||||
const element = document.elementFromPoint(x, y)
|
||||
|
||||
// get the closest element with the attribute
|
||||
const closest = element.closest("[quicknav-item]")
|
||||
// get the closest element with the attribute
|
||||
const closest = element.closest("[quicknav-item]")
|
||||
|
||||
if (!closest) {
|
||||
if (this._lastHovered) {
|
||||
this._lastHovered.classList.remove("hover")
|
||||
}
|
||||
if (!closest) {
|
||||
if (this._lastHovered) {
|
||||
this._lastHovered.classList.remove("hover")
|
||||
}
|
||||
|
||||
this._lastHovered = null
|
||||
this._lastHovered = null
|
||||
|
||||
return false
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
if (this._lastHovered !== closest) {
|
||||
if (this._lastHovered) {
|
||||
this._lastHovered.classList.remove("hover")
|
||||
}
|
||||
if (this._lastHovered !== closest) {
|
||||
if (this._lastHovered) {
|
||||
this._lastHovered.classList.remove("hover")
|
||||
}
|
||||
|
||||
this._lastHovered = closest
|
||||
this._lastHovered = closest
|
||||
|
||||
closest.classList.add("hover")
|
||||
closest.classList.add("hover")
|
||||
|
||||
if (app.cores.haptics?.vibrate) {
|
||||
app.cores.haptics.vibrate(40)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (app.cores.haptics?.vibrate) {
|
||||
app.cores.haptics.vibrate(40)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
toggleTour = (to) => {
|
||||
if (typeof to === "undefined") {
|
||||
to = !this.state.tourOpen
|
||||
}
|
||||
toggleTour = (to) => {
|
||||
if (typeof to === "undefined") {
|
||||
to = !this.state.tourOpen
|
||||
}
|
||||
|
||||
this.setState({
|
||||
tourOpen: to
|
||||
})
|
||||
}
|
||||
this.setState({
|
||||
tourOpen: to,
|
||||
})
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.render) {
|
||||
return <div className="bottomBar">
|
||||
{this.state.render}
|
||||
</div>
|
||||
}
|
||||
render() {
|
||||
if (this.state.render) {
|
||||
return <div className="bottomBar">{this.state.render}</div>
|
||||
}
|
||||
|
||||
const heightValue = this.state.visible ? Number(app.cores.style.getDefaultVar("bottom-bar-height").replace("px", "")) : 0
|
||||
const heightValue = Number(
|
||||
app.cores.style
|
||||
.getDefaultVar("bottom-bar-height")
|
||||
.replace("px", ""),
|
||||
)
|
||||
|
||||
return <>
|
||||
{
|
||||
this.state.tourOpen && <antd.Tour
|
||||
open
|
||||
steps={this.getTourSteps()}
|
||||
onClose={() => this.setState({ tourOpen: false })}
|
||||
/>
|
||||
}
|
||||
<QuickNavMenu
|
||||
visible={this.state.quickNavVisible}
|
||||
/>
|
||||
return (
|
||||
<>
|
||||
{this.state.tourOpen && (
|
||||
<antd.Tour
|
||||
open
|
||||
steps={this.getTourSteps()}
|
||||
onClose={() => this.setState({ tourOpen: false })}
|
||||
/>
|
||||
)}
|
||||
<QuickNavMenu visible={this.state.quickNavVisible} />
|
||||
|
||||
<Motion style={{
|
||||
y: spring(this.state.visible ? 0 : 300),
|
||||
height: spring(heightValue)
|
||||
}}>
|
||||
{({ y, height }) =>
|
||||
<div className="bottomBar_wrapper">
|
||||
<div
|
||||
className="bottomBar"
|
||||
style={{
|
||||
WebkitTransform: `translateY(${y}px)`,
|
||||
transform: `translateY(${y}px)`,
|
||||
height: `${height}px`,
|
||||
}}
|
||||
>
|
||||
<div className="items">
|
||||
<div
|
||||
key="creator"
|
||||
id="creator"
|
||||
className={classnames("item", "primary")}
|
||||
onClick={openCreator}
|
||||
>
|
||||
<div className="icon">
|
||||
{createIconRender("FiPlusCircle")}
|
||||
</div>
|
||||
</div>
|
||||
<AnimatePresence>
|
||||
{this.state.visible && (
|
||||
<motion.div
|
||||
className="bottomBar_wrapper"
|
||||
initial={{
|
||||
height: 0,
|
||||
y: 300,
|
||||
}}
|
||||
animate={{
|
||||
height: heightValue,
|
||||
y: 0,
|
||||
}}
|
||||
exit={{
|
||||
height: 0,
|
||||
y: 300,
|
||||
}}
|
||||
transition={{
|
||||
type: "spring",
|
||||
stiffness: 100,
|
||||
damping: 20,
|
||||
}}
|
||||
>
|
||||
<div className="bottomBar">
|
||||
<div className="items">
|
||||
<div
|
||||
key="creator"
|
||||
id="creator"
|
||||
className={classnames(
|
||||
"item",
|
||||
"primary",
|
||||
)}
|
||||
onClick={openCreator}
|
||||
>
|
||||
<div className="icon">
|
||||
{createIconRender("FiPlusCircle")}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{
|
||||
this.context.track_manifest && <div
|
||||
className="item"
|
||||
>
|
||||
<PlayerButton
|
||||
manifest={this.context.track_manifest}
|
||||
playback={this.context.playback_status}
|
||||
colorAnalysis={this.context.track_manifest?.cover_analysis}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
{this.context.track_manifest && (
|
||||
<div className="item">
|
||||
<PlayerButton
|
||||
manifest={
|
||||
this.context.track_manifest
|
||||
}
|
||||
playback={
|
||||
this.context.playback_status
|
||||
}
|
||||
colorAnalysis={
|
||||
this.context.track_manifest
|
||||
?.cover_analysis
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
key="navigator"
|
||||
id="navigator"
|
||||
className="item"
|
||||
ref={this.navBtnRef}
|
||||
onClick={() => app.location.push("/")}
|
||||
onTouchMove={this.handleNavTouchMove}
|
||||
onTouchStart={this.handleNavTouchStart}
|
||||
onTouchEnd={this.handleNavTouchEnd}
|
||||
onTouchCancel={() => {
|
||||
this.setState({ quickNavVisible: false })
|
||||
}}
|
||||
>
|
||||
<div className="icon">
|
||||
{createIconRender("FiHome")}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
key="navigator"
|
||||
id="navigator"
|
||||
className="item"
|
||||
ref={this.navBtnRef}
|
||||
onClick={() => app.location.push("/")}
|
||||
onTouchMove={this.handleNavTouchMove}
|
||||
onTouchStart={this.handleNavTouchStart}
|
||||
onTouchEnd={this.handleNavTouchEnd}
|
||||
onTouchCancel={() => {
|
||||
this.setState({
|
||||
quickNavVisible: false,
|
||||
})
|
||||
}}
|
||||
>
|
||||
<div className="icon">
|
||||
{createIconRender("FiHome")}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
key="searcher"
|
||||
id="searcher"
|
||||
className="item"
|
||||
onClick={app.controls.openSearcher}
|
||||
>
|
||||
<div className="icon">
|
||||
{createIconRender("FiSearch")}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
key="searcher"
|
||||
id="searcher"
|
||||
className="item"
|
||||
onClick={app.controls.openSearcher}
|
||||
>
|
||||
<div className="icon">
|
||||
{createIconRender("FiSearch")}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<AccountButton
|
||||
ref={this.accountBtnRef}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</Motion>
|
||||
</>
|
||||
}
|
||||
<AccountButton ref={this.accountBtnRef} />
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default (props) => {
|
||||
return <WithPlayerContext>
|
||||
<BottomBar
|
||||
{...props}
|
||||
/>
|
||||
</WithPlayerContext>
|
||||
}
|
||||
return (
|
||||
<WithPlayerContext>
|
||||
<BottomBar {...props} />
|
||||
</WithPlayerContext>
|
||||
)
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React from "react"
|
||||
import classnames from "classnames"
|
||||
import { Motion, spring } from "react-motion"
|
||||
import { motion, AnimatePresence } from "motion/react"
|
||||
|
||||
import useLayoutInterface from "@hooks/useLayoutInterface"
|
||||
import useDefaultVisibility from "@hooks/useDefaultVisibility"
|
||||
@ -8,111 +8,125 @@ import useDefaultVisibility from "@hooks/useDefaultVisibility"
|
||||
import "./index.less"
|
||||
|
||||
export default (props) => {
|
||||
const [visible, setVisible] = useDefaultVisibility()
|
||||
const [shouldUseTopBarSpacer, setShouldUseTopBarSpacer] = React.useState(true)
|
||||
const [render, setRender] = React.useState(null)
|
||||
const [visible, setVisible] = useDefaultVisibility()
|
||||
const [shouldUseTopBarSpacer, setShouldUseTopBarSpacer] =
|
||||
React.useState(true)
|
||||
const [render, setRender] = React.useState(null)
|
||||
|
||||
useLayoutInterface("top_bar", {
|
||||
toggleVisibility: (to) => {
|
||||
setVisible((prev) => {
|
||||
if (typeof to === undefined) {
|
||||
to = !prev
|
||||
}
|
||||
useLayoutInterface("top_bar", {
|
||||
toggleVisibility: (to) => {
|
||||
setVisible((prev) => {
|
||||
if (typeof to === undefined) {
|
||||
to = !prev
|
||||
}
|
||||
|
||||
return to
|
||||
})
|
||||
},
|
||||
render: (component, options) => {
|
||||
handleUpdateRender(component, options)
|
||||
},
|
||||
renderDefault: () => {
|
||||
setRender(null)
|
||||
},
|
||||
shouldUseTopBarSpacer: (to) => {
|
||||
app.layout.toggleTopBarSpacer(to)
|
||||
setShouldUseTopBarSpacer(to)
|
||||
}
|
||||
})
|
||||
return to
|
||||
})
|
||||
},
|
||||
render: (component, options) => {
|
||||
handleUpdateRender(component, options)
|
||||
},
|
||||
renderDefault: () => {
|
||||
setRender(null)
|
||||
},
|
||||
shouldUseTopBarSpacer: (to) => {
|
||||
app.layout.toggleTopBarSpacer(to)
|
||||
setShouldUseTopBarSpacer(to)
|
||||
},
|
||||
})
|
||||
|
||||
const handleUpdateRender = (...args) => {
|
||||
if (document.startViewTransition) {
|
||||
return document.startViewTransition(() => {
|
||||
updateRender(...args)
|
||||
})
|
||||
}
|
||||
const handleUpdateRender = (...args) => {
|
||||
if (document.startViewTransition) {
|
||||
return document.startViewTransition(() => {
|
||||
updateRender(...args)
|
||||
})
|
||||
}
|
||||
|
||||
return updateRender(...args)
|
||||
}
|
||||
return updateRender(...args)
|
||||
}
|
||||
|
||||
const updateRender = (component, options = {}) => {
|
||||
setRender({
|
||||
component,
|
||||
options
|
||||
})
|
||||
}
|
||||
const updateRender = (component, options = {}) => {
|
||||
setRender({
|
||||
component,
|
||||
options,
|
||||
})
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!shouldUseTopBarSpacer) {
|
||||
app.layout.togglePagePanelSpacer(true)
|
||||
} else {
|
||||
app.layout.togglePagePanelSpacer(false)
|
||||
}
|
||||
}, [shouldUseTopBarSpacer])
|
||||
React.useEffect(() => {
|
||||
if (!shouldUseTopBarSpacer) {
|
||||
app.layout.togglePagePanelSpacer(true)
|
||||
} else {
|
||||
app.layout.togglePagePanelSpacer(false)
|
||||
}
|
||||
}, [shouldUseTopBarSpacer])
|
||||
|
||||
React.useEffect(() => {
|
||||
if (shouldUseTopBarSpacer) {
|
||||
if (visible) {
|
||||
app.layout.toggleTopBarSpacer(true)
|
||||
} else {
|
||||
app.layout.toggleTopBarSpacer(false)
|
||||
}
|
||||
} else {
|
||||
if (visible) {
|
||||
app.layout.togglePagePanelSpacer(true)
|
||||
} else {
|
||||
app.layout.togglePagePanelSpacer(false)
|
||||
}
|
||||
React.useEffect(() => {
|
||||
if (shouldUseTopBarSpacer) {
|
||||
if (visible) {
|
||||
app.layout.toggleTopBarSpacer(true)
|
||||
} else {
|
||||
app.layout.toggleTopBarSpacer(false)
|
||||
}
|
||||
} else {
|
||||
if (visible) {
|
||||
app.layout.togglePagePanelSpacer(true)
|
||||
} else {
|
||||
app.layout.togglePagePanelSpacer(false)
|
||||
}
|
||||
|
||||
app.layout.toggleTopBarSpacer(false)
|
||||
}
|
||||
}, [visible])
|
||||
app.layout.toggleTopBarSpacer(false)
|
||||
}
|
||||
}, [visible])
|
||||
|
||||
React.useEffect(() => {
|
||||
if (render) {
|
||||
setVisible(true)
|
||||
} else {
|
||||
setVisible(false)
|
||||
}
|
||||
}, [render])
|
||||
React.useEffect(() => {
|
||||
if (render) {
|
||||
setVisible(true)
|
||||
} else {
|
||||
setVisible(false)
|
||||
}
|
||||
}, [render])
|
||||
|
||||
const heightValue = visible ? Number(app.cores.style.getDefaultVar("top-bar-height").replace("px", "")) : 0
|
||||
const heightValue = Number(
|
||||
app.cores.style.getDefaultVar("top-bar-height").replace("px", ""),
|
||||
)
|
||||
|
||||
return <Motion style={{
|
||||
y: spring(visible ? 0 : 300,),
|
||||
height: spring(heightValue,),
|
||||
}}>
|
||||
{({ y, height }) => {
|
||||
return <>
|
||||
<div
|
||||
className="top-bar_wrapper"
|
||||
style={{
|
||||
WebkitTransform: `translateY(-${y}px)`,
|
||||
transform: `translateY(-${y}px)`,
|
||||
height: `${height}px`,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={classnames(
|
||||
"top-bar",
|
||||
render?.options?.className,
|
||||
)}
|
||||
>
|
||||
{
|
||||
render?.component && React.cloneElement(render?.component, render?.options?.props ?? {})
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
}}
|
||||
</Motion>
|
||||
}
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{visible && (
|
||||
<motion.div
|
||||
className="top-bar_wrapper"
|
||||
animate={{
|
||||
y: 0,
|
||||
height: heightValue,
|
||||
}}
|
||||
initial={{
|
||||
y: -300,
|
||||
height: 0,
|
||||
}}
|
||||
exit={{
|
||||
y: -300,
|
||||
height: 0,
|
||||
}}
|
||||
transition={{
|
||||
type: "spring",
|
||||
stiffness: 100,
|
||||
damping: 20,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={classnames(
|
||||
"top-bar",
|
||||
render?.options?.className,
|
||||
)}
|
||||
>
|
||||
{render?.component &&
|
||||
React.cloneElement(
|
||||
render?.component,
|
||||
render?.options?.props ?? {},
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
)
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
import React from "react"
|
||||
import classnames from "classnames"
|
||||
import * as antd from "antd"
|
||||
import { AnimatePresence, motion } from "framer-motion"
|
||||
import { AnimatePresence, motion } from "motion/react"
|
||||
|
||||
import "./index.less"
|
||||
|
||||
@ -30,7 +29,7 @@ export class Drawer extends React.Component {
|
||||
this.toggleVisibility(false)
|
||||
|
||||
this.props.controller.close(this.props.id, {
|
||||
transition: 150
|
||||
transition: 150,
|
||||
})
|
||||
}
|
||||
|
||||
@ -65,31 +64,34 @@ export class Drawer extends React.Component {
|
||||
handleDone: this.handleDone,
|
||||
handleFail: this.handleFail,
|
||||
}
|
||||
return <AnimatePresence>
|
||||
{
|
||||
this.state.visible && <motion.div
|
||||
key={this.props.id}
|
||||
id={this.props.id}
|
||||
className="drawer"
|
||||
style={{
|
||||
...this.options.style,
|
||||
}}
|
||||
transformTemplate={transformTemplate}
|
||||
animate={{
|
||||
x: [-100, 0],
|
||||
opacity: [0, 1],
|
||||
}}
|
||||
exit={{
|
||||
x: [0, -100],
|
||||
opacity: [1, 0],
|
||||
}}
|
||||
>
|
||||
{
|
||||
React.createElement(this.props.children, componentProps)
|
||||
}
|
||||
</motion.div>
|
||||
}
|
||||
</AnimatePresence>
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{this.state.visible && (
|
||||
<motion.div
|
||||
key={this.props.id}
|
||||
id={this.props.id}
|
||||
className="drawer"
|
||||
style={{
|
||||
...this.options.style,
|
||||
}}
|
||||
transformTemplate={transformTemplate}
|
||||
animate={{
|
||||
x: [-100, 0],
|
||||
opacity: [0, 1],
|
||||
}}
|
||||
exit={{
|
||||
x: [0, -100],
|
||||
opacity: [1, 0],
|
||||
}}
|
||||
>
|
||||
{React.createElement(
|
||||
this.props.children,
|
||||
componentProps,
|
||||
)}
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -171,10 +173,12 @@ export default class DrawerController extends React.Component {
|
||||
if (lastDrawer) {
|
||||
if (app.layout.modal && lastDrawer.options.confirmOnOutsideClick) {
|
||||
return app.layout.modal.confirm({
|
||||
descriptionText: lastDrawer.options.confirmOnOutsideClickText ?? "Are you sure you want to close this drawer?",
|
||||
descriptionText:
|
||||
lastDrawer.options.confirmOnOutsideClickText ??
|
||||
"Are you sure you want to close this drawer?",
|
||||
onConfirm: () => {
|
||||
lastDrawer.close()
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@ -202,18 +206,12 @@ export default class DrawerController extends React.Component {
|
||||
}
|
||||
|
||||
if (typeof addresses[id] === "undefined") {
|
||||
drawers.push(<Drawer
|
||||
key={id}
|
||||
{...instance}
|
||||
/>)
|
||||
drawers.push(<Drawer key={id} {...instance} />)
|
||||
|
||||
addresses[id] = drawers.length - 1
|
||||
refs[id] = instance.ref
|
||||
} else {
|
||||
drawers[addresses[id]] = <Drawer
|
||||
key={id}
|
||||
{...instance}
|
||||
/>
|
||||
drawers[addresses[id]] = <Drawer key={id} {...instance} />
|
||||
refs[id] = instance.ref
|
||||
}
|
||||
|
||||
@ -267,37 +265,39 @@ export default class DrawerController extends React.Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
return <>
|
||||
<AnimatePresence>
|
||||
{
|
||||
this.state.maskVisible && <motion.div
|
||||
className="drawers-mask"
|
||||
onClick={() => this.closeLastDrawer()}
|
||||
initial={{
|
||||
opacity: 0,
|
||||
}}
|
||||
animate={{
|
||||
opacity: 1,
|
||||
}}
|
||||
exit={{
|
||||
opacity: 0,
|
||||
}}
|
||||
/>
|
||||
}
|
||||
</AnimatePresence>
|
||||
|
||||
<div
|
||||
className={classnames(
|
||||
"drawers-wrapper",
|
||||
{
|
||||
["hidden"]: !this.state.drawers.length,
|
||||
}
|
||||
)}
|
||||
>
|
||||
return (
|
||||
<>
|
||||
<AnimatePresence>
|
||||
{this.state.drawers}
|
||||
{this.state.maskVisible && (
|
||||
<motion.div
|
||||
className="drawers-mask"
|
||||
onClick={() => this.closeLastDrawer()}
|
||||
initial={{
|
||||
opacity: 0,
|
||||
}}
|
||||
animate={{
|
||||
opacity: 1,
|
||||
}}
|
||||
exit={{
|
||||
opacity: 0,
|
||||
}}
|
||||
transition={{
|
||||
type: "spring",
|
||||
stiffness: 100,
|
||||
damping: 20,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</>
|
||||
|
||||
<div
|
||||
className={classnames("drawers-wrapper", {
|
||||
["hidden"]: !this.state.drawers.length,
|
||||
})}
|
||||
>
|
||||
<AnimatePresence>{this.state.drawers}</AnimatePresence>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,64 +1,64 @@
|
||||
import React from "react"
|
||||
import classnames from "classnames"
|
||||
import { Motion, spring } from "react-motion"
|
||||
import { motion, AnimatePresence } from "motion/react"
|
||||
|
||||
import useLayoutInterface from "@hooks/useLayoutInterface"
|
||||
|
||||
import "./index.less"
|
||||
|
||||
export default (props) => {
|
||||
const [render, setRender] = React.useState(null)
|
||||
const [render, setRender] = React.useState(null)
|
||||
|
||||
useLayoutInterface("header", {
|
||||
render: (component, options) => {
|
||||
if (component === null) {
|
||||
return setRender(null)
|
||||
}
|
||||
useLayoutInterface("header", {
|
||||
render: (component, options) => {
|
||||
if (component === null) {
|
||||
return setRender(null)
|
||||
}
|
||||
|
||||
return setRender({
|
||||
component,
|
||||
options
|
||||
})
|
||||
},
|
||||
})
|
||||
return setRender({
|
||||
component,
|
||||
options,
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
React.useEffect(() => {
|
||||
if (render) {
|
||||
app.layout.toggleDisableTopLayoutPadding(true)
|
||||
} else {
|
||||
app.layout.toggleDisableTopLayoutPadding(false)
|
||||
}
|
||||
}, [render])
|
||||
React.useEffect(() => {
|
||||
if (render) {
|
||||
app.layout.toggleDisableTopLayoutPadding(true)
|
||||
} else {
|
||||
app.layout.toggleDisableTopLayoutPadding(false)
|
||||
}
|
||||
}, [render])
|
||||
|
||||
return <Motion
|
||||
style={{
|
||||
y: spring(render ? 0 : 100,),
|
||||
}}
|
||||
>
|
||||
{({ y, height }) => {
|
||||
return <div
|
||||
className={classnames(
|
||||
"page_header_wrapper",
|
||||
{
|
||||
["hidden"]: !render,
|
||||
}
|
||||
)}
|
||||
style={{
|
||||
WebkitTransform: `translateY(-${y}px)`,
|
||||
transform: `translateY(-${y}px)`,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="page_header"
|
||||
>
|
||||
{
|
||||
render?.component && React.cloneElement(
|
||||
render?.component,
|
||||
render?.options?.props ?? {}
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}}
|
||||
</Motion>
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{render && (
|
||||
<motion.div
|
||||
className="page_header_wrapper"
|
||||
animate={{
|
||||
y: 0,
|
||||
}}
|
||||
initial={{
|
||||
y: -100,
|
||||
}}
|
||||
exit={{
|
||||
y: -100,
|
||||
}}
|
||||
transition={{
|
||||
type: "spring",
|
||||
stiffness: 100,
|
||||
damping: 20,
|
||||
}}
|
||||
>
|
||||
<div className="page_header">
|
||||
{render?.component &&
|
||||
React.cloneElement(
|
||||
render?.component,
|
||||
render?.options?.props ?? {},
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
)
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ import React from "react"
|
||||
import config from "@config"
|
||||
import classnames from "classnames"
|
||||
import { Translation } from "react-i18next"
|
||||
import { motion, AnimatePresence } from "framer-motion"
|
||||
import { motion, AnimatePresence } from "motion/react"
|
||||
import { Menu, Avatar, Dropdown, Tag } from "antd"
|
||||
|
||||
import Drawer from "@layouts/components/drawer"
|
||||
|
@ -1,119 +1,133 @@
|
||||
import React from "react"
|
||||
import classnames from "classnames"
|
||||
import { Motion, spring } from "react-motion"
|
||||
import { motion, AnimatePresence } from "motion/react"
|
||||
|
||||
import WidgetsWrapper from "@components/WidgetsWrapper"
|
||||
|
||||
import "./index.less"
|
||||
|
||||
export default class ToolsBar extends React.Component {
|
||||
state = {
|
||||
visible: false,
|
||||
renders: {
|
||||
top: [],
|
||||
bottom: [],
|
||||
},
|
||||
}
|
||||
state = {
|
||||
visible: false,
|
||||
renders: {
|
||||
top: [],
|
||||
bottom: [],
|
||||
},
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
app.layout.tools_bar = this.interface
|
||||
componentDidMount() {
|
||||
app.layout.tools_bar = this.interface
|
||||
|
||||
setTimeout(() => {
|
||||
this.setState({
|
||||
visible: true,
|
||||
})
|
||||
}, 10)
|
||||
}
|
||||
setTimeout(() => {
|
||||
this.setState({
|
||||
visible: true,
|
||||
})
|
||||
}, 10)
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
delete app.layout.tools_bar
|
||||
}
|
||||
componentWillUnmount() {
|
||||
delete app.layout.tools_bar
|
||||
}
|
||||
|
||||
interface = {
|
||||
toggleVisibility: (to) => {
|
||||
this.setState({
|
||||
visible: to ?? !this.state.visible,
|
||||
})
|
||||
},
|
||||
attachRender: (id, component, props, { position = "bottom" } = {}) => {
|
||||
this.setState((prev) => {
|
||||
prev.renders[position].push({
|
||||
id: id,
|
||||
component: component,
|
||||
props: props,
|
||||
})
|
||||
interface = {
|
||||
toggleVisibility: (to) => {
|
||||
this.setState({
|
||||
visible: to ?? !this.state.visible,
|
||||
})
|
||||
},
|
||||
attachRender: (id, component, props, { position = "bottom" } = {}) => {
|
||||
this.setState((prev) => {
|
||||
prev.renders[position].push({
|
||||
id: id,
|
||||
component: component,
|
||||
props: props,
|
||||
})
|
||||
|
||||
return prev
|
||||
})
|
||||
return prev
|
||||
})
|
||||
|
||||
return component
|
||||
},
|
||||
detachRender: (id) => {
|
||||
this.setState({
|
||||
renders: {
|
||||
top: this.state.renders.top.filter((render) => render.id !== id),
|
||||
bottom: this.state.renders.bottom.filter((render) => render.id !== id),
|
||||
},
|
||||
})
|
||||
return component
|
||||
},
|
||||
detachRender: (id) => {
|
||||
this.setState({
|
||||
renders: {
|
||||
top: this.state.renders.top.filter(
|
||||
(render) => render.id !== id,
|
||||
),
|
||||
bottom: this.state.renders.bottom.filter(
|
||||
(render) => render.id !== id,
|
||||
),
|
||||
},
|
||||
})
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
return true
|
||||
},
|
||||
}
|
||||
|
||||
render() {
|
||||
const hasAnyRenders = this.state.renders.top.length > 0 || this.state.renders.bottom.length > 0
|
||||
render() {
|
||||
const hasAnyRenders =
|
||||
this.state.renders.top.length > 0 ||
|
||||
this.state.renders.bottom.length > 0
|
||||
|
||||
const isVisible = hasAnyRenders && this.state.visible
|
||||
const isVisible = hasAnyRenders && this.state.visible
|
||||
|
||||
return <Motion
|
||||
style={{
|
||||
x: spring(isVisible ? 0 : 100),
|
||||
width: spring(isVisible ? 100 : 0),
|
||||
}}
|
||||
>
|
||||
{({ x, width }) => {
|
||||
return <div
|
||||
style={{
|
||||
width: `${width}%`,
|
||||
transform: `translateX(${x}%)`,
|
||||
}}
|
||||
className={classnames(
|
||||
"tools-bar-wrapper",
|
||||
{
|
||||
visible: isVisible,
|
||||
}
|
||||
)}
|
||||
>
|
||||
<div
|
||||
id="tools_bar"
|
||||
className="tools-bar"
|
||||
>
|
||||
<div className="attached_renders top">
|
||||
{
|
||||
this.state.renders.top.map((render, index) => {
|
||||
return React.createElement(render.component, {
|
||||
...render.props,
|
||||
key: index,
|
||||
})
|
||||
})
|
||||
}
|
||||
</div>
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{isVisible && (
|
||||
<motion.div
|
||||
className={classnames("tools-bar-wrapper", {
|
||||
["visible"]: isVisible,
|
||||
})}
|
||||
animate={{
|
||||
x: 0,
|
||||
width: "100%",
|
||||
}}
|
||||
initial={{
|
||||
x: 100,
|
||||
width: "0%",
|
||||
}}
|
||||
exit={{
|
||||
x: 100,
|
||||
width: "0%",
|
||||
}}
|
||||
transition={{
|
||||
type: "spring",
|
||||
stiffness: 100,
|
||||
damping: 20,
|
||||
}}
|
||||
>
|
||||
<div id="tools_bar" className="tools-bar">
|
||||
<div className="attached_renders top">
|
||||
{this.state.renders.top.map((render, index) => {
|
||||
return React.createElement(
|
||||
render.component,
|
||||
{
|
||||
...render.props,
|
||||
key: index,
|
||||
},
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
<WidgetsWrapper />
|
||||
<WidgetsWrapper />
|
||||
|
||||
<div className="attached_renders bottom">
|
||||
{
|
||||
this.state.renders.bottom.map((render, index) => {
|
||||
return React.createElement(render.component, {
|
||||
...render.props,
|
||||
key: index,
|
||||
})
|
||||
})
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}}
|
||||
</Motion>
|
||||
}
|
||||
}
|
||||
<div className="attached_renders bottom">
|
||||
{this.state.renders.bottom.map(
|
||||
(render, index) => {
|
||||
return React.createElement(
|
||||
render.component,
|
||||
{
|
||||
...render.props,
|
||||
key: index,
|
||||
},
|
||||
)
|
||||
},
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -1,79 +1,78 @@
|
||||
@import "@styles/vars.less";
|
||||
|
||||
.tools-bar-wrapper {
|
||||
position: sticky;
|
||||
position: sticky;
|
||||
|
||||
top: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
|
||||
z-index: 150;
|
||||
z-index: 150;
|
||||
|
||||
height: 100vh;
|
||||
height: 100dvh;
|
||||
max-width: 420px;
|
||||
height: 100vh;
|
||||
height: 100dvh;
|
||||
max-width: 420px;
|
||||
|
||||
padding: 10px;
|
||||
padding: 10px;
|
||||
|
||||
.visible {
|
||||
min-width: 320px;
|
||||
}
|
||||
.visible {
|
||||
min-width: 320px;
|
||||
}
|
||||
|
||||
&:not(.visible) {
|
||||
min-width: 0;
|
||||
padding: 0;
|
||||
|
||||
}
|
||||
&:not(.visible) {
|
||||
min-width: 0;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.tools-bar {
|
||||
position: relative;
|
||||
position: relative;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
border-radius: 12px;
|
||||
border-radius: 12px;
|
||||
|
||||
border-radius: @sidebar_borderRadius;
|
||||
box-shadow: @card-shadow;
|
||||
border-radius: @sidebar_borderRadius;
|
||||
box-shadow: @card-shadow;
|
||||
|
||||
padding: 10px;
|
||||
padding: 10px;
|
||||
|
||||
background-color: var(--background-color-accent);
|
||||
background-color: var(--background-color-accent);
|
||||
|
||||
gap: 20px;
|
||||
gap: 20px;
|
||||
|
||||
flex: 0;
|
||||
flex: 0;
|
||||
|
||||
.attached_renders {
|
||||
position: relative;
|
||||
.attached_renders {
|
||||
position: relative;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
align-items: center;
|
||||
align-items: center;
|
||||
|
||||
width: 100%;
|
||||
height: fit-content;
|
||||
width: 100%;
|
||||
height: fit-content;
|
||||
|
||||
gap: 10px;
|
||||
gap: 10px;
|
||||
|
||||
&.bottom {
|
||||
position: absolute;
|
||||
&.bottom {
|
||||
position: absolute;
|
||||
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
|
||||
padding: 10px;
|
||||
}
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.card {
|
||||
width: 100%;
|
||||
height: fit-content;
|
||||
.card {
|
||||
width: 100%;
|
||||
height: fit-content;
|
||||
|
||||
background-color: var(--background-color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
background-color: var(--background-color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React from "react"
|
||||
import * as antd from "antd"
|
||||
import classnames from "classnames"
|
||||
import { motion, AnimatePresence } from "framer-motion"
|
||||
import { motion, AnimatePresence } from "motion/react"
|
||||
|
||||
import { Icons } from "@components/Icons"
|
||||
import FollowButton from "@components/FollowButton"
|
||||
@ -21,10 +21,10 @@ import FollowersTab from "./tabs/followers"
|
||||
import "./index.less"
|
||||
|
||||
const TabsComponent = {
|
||||
"posts": PostsTab,
|
||||
"followers": FollowersTab,
|
||||
"details": DetailsTab,
|
||||
"music": MusicTab,
|
||||
posts: PostsTab,
|
||||
followers: FollowersTab,
|
||||
details: DetailsTab,
|
||||
music: MusicTab,
|
||||
}
|
||||
|
||||
export default class Account extends React.Component {
|
||||
@ -60,7 +60,7 @@ export default class Account extends React.Component {
|
||||
}
|
||||
|
||||
user = await UserModel.data({
|
||||
username: requestedUser
|
||||
username: requestedUser,
|
||||
}).catch((error) => {
|
||||
console.error(error)
|
||||
|
||||
@ -77,7 +77,9 @@ export default class Account extends React.Component {
|
||||
|
||||
console.log(`Loaded User [${user.username}] >`, user)
|
||||
|
||||
const followersResult = await FollowsModel.getFollowers(user._id).catch(() => false)
|
||||
const followersResult = await FollowsModel.getFollowers(
|
||||
user._id,
|
||||
).catch(() => false)
|
||||
|
||||
if (followersResult) {
|
||||
followersCount = followersResult.count
|
||||
@ -118,7 +120,10 @@ export default class Account extends React.Component {
|
||||
|
||||
handlePageTransition = (key) => {
|
||||
if (typeof key !== "string") {
|
||||
console.error("Cannot handle page transition. Invalid key, only valid passing string", key)
|
||||
console.error(
|
||||
"Cannot handle page transition. Invalid key, only valid passing string",
|
||||
key,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
@ -129,7 +134,7 @@ export default class Account extends React.Component {
|
||||
}
|
||||
|
||||
this.setState({
|
||||
tabActiveKey: key
|
||||
tabActiveKey: key,
|
||||
})
|
||||
}
|
||||
|
||||
@ -137,119 +142,119 @@ export default class Account extends React.Component {
|
||||
const user = this.state.user
|
||||
|
||||
if (this.state.isNotExistent) {
|
||||
return <antd.Result
|
||||
status="404"
|
||||
title="This user does not exist, yet..."
|
||||
>
|
||||
|
||||
</antd.Result>
|
||||
return (
|
||||
<antd.Result
|
||||
status="404"
|
||||
title="This user does not exist, yet..."
|
||||
></antd.Result>
|
||||
)
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
return <antd.Skeleton active />
|
||||
}
|
||||
|
||||
return <div
|
||||
id="profile"
|
||||
className={classnames(
|
||||
"account-profile",
|
||||
{
|
||||
return (
|
||||
<div
|
||||
id="profile"
|
||||
className={classnames("account-profile", {
|
||||
["withCover"]: user.cover,
|
||||
}
|
||||
)}
|
||||
>
|
||||
{
|
||||
user.cover && <div
|
||||
className={classnames("cover", {
|
||||
["expanded"]: this.state.coverExpanded
|
||||
})}
|
||||
style={{ backgroundImage: `url("${user.cover}")` }}
|
||||
onClick={() => this.toggleCoverExpanded()}
|
||||
id="profile-cover"
|
||||
/>
|
||||
}
|
||||
|
||||
<div className="panels">
|
||||
<div className="left-panel">
|
||||
<UserCard
|
||||
user={user}
|
||||
})}
|
||||
>
|
||||
{user.cover && (
|
||||
<div
|
||||
className={classnames("cover", {
|
||||
["expanded"]: this.state.coverExpanded,
|
||||
})}
|
||||
style={{ backgroundImage: `url("${user.cover}")` }}
|
||||
onClick={() => this.toggleCoverExpanded()}
|
||||
id="profile-cover"
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="actions">
|
||||
<FollowButton
|
||||
count={this.state.followersCount}
|
||||
onClick={this.onClickFollow}
|
||||
followed={this.state.following}
|
||||
self={this.state.isSelf}
|
||||
/>
|
||||
<div className="panels">
|
||||
<div className="left-panel">
|
||||
<UserCard user={user} />
|
||||
|
||||
{
|
||||
!this.state.isSelf && <antd.Button
|
||||
icon={<Icons.MdMessage />}
|
||||
onClick={() => app.location.push(`/messages/${user._id}`)}
|
||||
<div className="actions">
|
||||
<FollowButton
|
||||
count={this.state.followersCount}
|
||||
onClick={this.onClickFollow}
|
||||
followed={this.state.following}
|
||||
self={this.state.isSelf}
|
||||
/>
|
||||
}
|
||||
|
||||
{!this.state.isSelf && (
|
||||
<antd.Button
|
||||
icon={<Icons.MdMessage />}
|
||||
onClick={() =>
|
||||
app.location.push(
|
||||
`/messages/${user._id}`,
|
||||
)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="center-panel" ref={this.contentRef}>
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{
|
||||
duration: 0.15,
|
||||
}}
|
||||
key={this.state.tabActiveKey}
|
||||
style={{
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
{React.createElement(
|
||||
TabsComponent[this.state.tabActiveKey],
|
||||
{
|
||||
onTopVisibility:
|
||||
this.onPostListTopVisibility,
|
||||
state: this.state,
|
||||
},
|
||||
)}
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
|
||||
<div className="right-panel">
|
||||
<antd.Menu
|
||||
className="tabMenu"
|
||||
mode={app.isMobile ? "horizontal" : "vertical"}
|
||||
selectedKeys={[this.state.tabActiveKey]}
|
||||
onClick={(e) => this.handlePageTransition(e.key)}
|
||||
items={GenerateMenuItems([
|
||||
{
|
||||
id: "posts",
|
||||
label: "Posts",
|
||||
icon: "FiBookOpen",
|
||||
},
|
||||
{
|
||||
id: "music",
|
||||
label: "Music",
|
||||
icon: "MdAlbum",
|
||||
},
|
||||
{
|
||||
id: "followers",
|
||||
label: "Followers",
|
||||
icon: "FiUsers",
|
||||
},
|
||||
{
|
||||
id: "details",
|
||||
label: "Details",
|
||||
icon: "FiInfo",
|
||||
},
|
||||
])}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="center-panel"
|
||||
ref={this.contentRef}
|
||||
>
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{
|
||||
duration: 0.15,
|
||||
}}
|
||||
key={this.state.tabActiveKey}
|
||||
style={{
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
{
|
||||
React.createElement(TabsComponent[this.state.tabActiveKey], {
|
||||
onTopVisibility: this.onPostListTopVisibility,
|
||||
state: this.state
|
||||
})
|
||||
}
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
|
||||
<div className="right-panel">
|
||||
<antd.Menu
|
||||
className="tabMenu"
|
||||
mode={app.isMobile ? "horizontal" : "vertical"}
|
||||
selectedKeys={[this.state.tabActiveKey]}
|
||||
onClick={(e) => this.handlePageTransition(e.key)}
|
||||
items={GenerateMenuItems([
|
||||
{
|
||||
id: "posts",
|
||||
label: "Posts",
|
||||
icon: "FiBookOpen",
|
||||
},
|
||||
{
|
||||
id: "music",
|
||||
label: "Music",
|
||||
icon: "MdAlbum",
|
||||
},
|
||||
{
|
||||
id: "followers",
|
||||
label: "Followers",
|
||||
icon: "FiUsers",
|
||||
},
|
||||
{
|
||||
id: "details",
|
||||
label: "Details",
|
||||
icon: "FiInfo",
|
||||
}
|
||||
])}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,153 +1,162 @@
|
||||
import React from "react"
|
||||
import classnames from "classnames"
|
||||
import { Motion, spring } from "react-motion"
|
||||
import { motion, AnimatePresence } from "motion/react"
|
||||
|
||||
import { usePlayerStateContext } from "@contexts/WithPlayerContext"
|
||||
|
||||
const LyricsText = React.forwardRef((props, textRef) => {
|
||||
const [playerState] = usePlayerStateContext()
|
||||
const [playerState] = usePlayerStateContext()
|
||||
|
||||
const { lyrics } = props
|
||||
const { lyrics } = props
|
||||
|
||||
const [syncInterval, setSyncInterval] = React.useState(null)
|
||||
const [currentLineIndex, setCurrentLineIndex] = React.useState(0)
|
||||
const [visible, setVisible] = React.useState(false)
|
||||
const [syncInterval, setSyncInterval] = React.useState(null)
|
||||
const [currentLineIndex, setCurrentLineIndex] = React.useState(0)
|
||||
const [visible, setVisible] = React.useState(false)
|
||||
|
||||
function syncPlayback() {
|
||||
const currentTrackTime = app.cores.player.controls.seek() * 1000
|
||||
function syncPlayback() {
|
||||
const currentTrackTime = app.cores.player.controls.seek() * 1000
|
||||
|
||||
const lineIndex = lyrics.synced_lyrics.findIndex((line) => {
|
||||
return currentTrackTime >= line.startTimeMs && currentTrackTime <= line.endTimeMs
|
||||
})
|
||||
const lineIndex = lyrics.synced_lyrics.findIndex((line) => {
|
||||
return (
|
||||
currentTrackTime >= line.startTimeMs &&
|
||||
currentTrackTime <= line.endTimeMs
|
||||
)
|
||||
})
|
||||
|
||||
if (lineIndex === -1) {
|
||||
if (!visible) {
|
||||
setVisible(false)
|
||||
}
|
||||
if (lineIndex === -1) {
|
||||
if (!visible) {
|
||||
setVisible(false)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const line = lyrics.synced_lyrics[lineIndex]
|
||||
const line = lyrics.synced_lyrics[lineIndex]
|
||||
|
||||
setCurrentLineIndex(lineIndex)
|
||||
setCurrentLineIndex(lineIndex)
|
||||
|
||||
if (line.break) {
|
||||
return setVisible(false)
|
||||
}
|
||||
if (line.break) {
|
||||
return setVisible(false)
|
||||
}
|
||||
|
||||
if (line.text) {
|
||||
return setVisible(true)
|
||||
}
|
||||
}
|
||||
if (line.text) {
|
||||
return setVisible(true)
|
||||
}
|
||||
}
|
||||
|
||||
function startSyncInterval() {
|
||||
if (!lyrics || !lyrics.synced_lyrics) {
|
||||
stopSyncInterval()
|
||||
return false
|
||||
}
|
||||
function startSyncInterval() {
|
||||
if (!lyrics || !lyrics.synced_lyrics) {
|
||||
stopSyncInterval()
|
||||
return false
|
||||
}
|
||||
|
||||
if (playerState.playback_status !== "playing") {
|
||||
stopSyncInterval()
|
||||
return false
|
||||
}
|
||||
if (playerState.playback_status !== "playing") {
|
||||
stopSyncInterval()
|
||||
return false
|
||||
}
|
||||
|
||||
if (syncInterval) {
|
||||
stopSyncInterval()
|
||||
}
|
||||
if (syncInterval) {
|
||||
stopSyncInterval()
|
||||
}
|
||||
|
||||
setSyncInterval(setInterval(syncPlayback, 100))
|
||||
}
|
||||
setSyncInterval(setInterval(syncPlayback, 100))
|
||||
}
|
||||
|
||||
function stopSyncInterval() {
|
||||
clearInterval(syncInterval)
|
||||
setSyncInterval(null)
|
||||
}
|
||||
function stopSyncInterval() {
|
||||
clearInterval(syncInterval)
|
||||
setSyncInterval(null)
|
||||
}
|
||||
|
||||
//* Handle when current line index change
|
||||
React.useEffect(() => {
|
||||
if (currentLineIndex === 0) {
|
||||
setVisible(false)
|
||||
} else {
|
||||
setVisible(true)
|
||||
//* Handle when current line index change
|
||||
React.useEffect(() => {
|
||||
if (currentLineIndex === 0) {
|
||||
setVisible(false)
|
||||
} else {
|
||||
setVisible(true)
|
||||
|
||||
// find line element by id
|
||||
const lineElement = textRef.current.querySelector(`#lyrics-line-${currentLineIndex}`)
|
||||
// find line element by id
|
||||
const lineElement = textRef.current.querySelector(
|
||||
`#lyrics-line-${currentLineIndex}`,
|
||||
)
|
||||
|
||||
// center scroll to current line
|
||||
if (lineElement) {
|
||||
lineElement.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "center",
|
||||
})
|
||||
} else {
|
||||
// scroll to top
|
||||
textRef.current.scrollTop = 0
|
||||
}
|
||||
}
|
||||
}, [currentLineIndex])
|
||||
// center scroll to current line
|
||||
if (lineElement) {
|
||||
lineElement.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "center",
|
||||
})
|
||||
} else {
|
||||
// scroll to top
|
||||
textRef.current.scrollTop = 0
|
||||
}
|
||||
}
|
||||
}, [currentLineIndex])
|
||||
|
||||
//* Handle when playback status change
|
||||
React.useEffect(() => {
|
||||
startSyncInterval()
|
||||
}, [playerState.playback_status])
|
||||
//* Handle when playback status change
|
||||
React.useEffect(() => {
|
||||
startSyncInterval()
|
||||
}, [playerState.playback_status])
|
||||
|
||||
//* Handle when manifest object change, reset...
|
||||
React.useEffect(() => {
|
||||
setVisible(false)
|
||||
setCurrentLineIndex(0)
|
||||
}, [playerState.track_manifest])
|
||||
//* Handle when manifest object change, reset...
|
||||
React.useEffect(() => {
|
||||
setVisible(false)
|
||||
setCurrentLineIndex(0)
|
||||
}, [playerState.track_manifest])
|
||||
|
||||
React.useEffect(() => {
|
||||
startSyncInterval()
|
||||
}, [lyrics])
|
||||
React.useEffect(() => {
|
||||
startSyncInterval()
|
||||
}, [lyrics])
|
||||
|
||||
React.useEffect(() => {
|
||||
return () => {
|
||||
clearInterval(syncInterval)
|
||||
}
|
||||
}, [])
|
||||
React.useEffect(() => {
|
||||
return () => {
|
||||
clearInterval(syncInterval)
|
||||
}
|
||||
}, [])
|
||||
|
||||
if (!lyrics?.synced_lyrics) {
|
||||
return null
|
||||
}
|
||||
if (!lyrics?.synced_lyrics) {
|
||||
return null
|
||||
}
|
||||
|
||||
return <div
|
||||
className="lyrics-text-wrapper"
|
||||
>
|
||||
<Motion
|
||||
style={{
|
||||
opacity: spring(visible ? 1 : 0),
|
||||
}}
|
||||
>
|
||||
{({ opacity }) => {
|
||||
return <div
|
||||
ref={textRef}
|
||||
className="lyrics-text"
|
||||
style={{
|
||||
opacity
|
||||
}}
|
||||
>
|
||||
{
|
||||
lyrics.synced_lyrics.map((line, index) => {
|
||||
return <p
|
||||
key={index}
|
||||
id={`lyrics-line-${index}`}
|
||||
className={classnames(
|
||||
"line",
|
||||
{
|
||||
["current"]: currentLineIndex === index
|
||||
}
|
||||
)}
|
||||
>
|
||||
{line.text}
|
||||
</p>
|
||||
})
|
||||
}
|
||||
</div>
|
||||
}}
|
||||
</Motion>
|
||||
</div>
|
||||
return (
|
||||
<div className="lyrics-text-wrapper">
|
||||
<AnimatePresence>
|
||||
{visible && (
|
||||
<motion.div
|
||||
ref={textRef}
|
||||
className="lyrics-text"
|
||||
animate={{
|
||||
opacity: 1,
|
||||
}}
|
||||
initial={{
|
||||
opacity: 0,
|
||||
}}
|
||||
exit={{
|
||||
opacity: 0,
|
||||
}}
|
||||
transition={{
|
||||
type: "spring",
|
||||
stiffness: 100,
|
||||
damping: 20,
|
||||
}}
|
||||
>
|
||||
{lyrics.synced_lyrics.map((line, index) => {
|
||||
return (
|
||||
<p
|
||||
key={index}
|
||||
id={`lyrics-line-${index}`}
|
||||
className={classnames("line", {
|
||||
["current"]: currentLineIndex === index,
|
||||
})}
|
||||
>
|
||||
{line.text}
|
||||
</p>
|
||||
)
|
||||
})}
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
export default LyricsText
|
||||
export default LyricsText
|
||||
|
@ -10,281 +10,299 @@ import config from "@config"
|
||||
import "./index.less"
|
||||
|
||||
const connectionsTooltipStrings = {
|
||||
secure: "This connection is secure",
|
||||
insecure: "This connection is insecure, cause it's not using HTTPS protocol and the server cannot be verified on the trusted certificate authority.",
|
||||
warning: "This connection is secure but the server cannot be verified on the trusted certificate authority.",
|
||||
secure: "This connection is secure",
|
||||
insecure:
|
||||
"This connection is insecure, cause it's not using HTTPS protocol and the server cannot be verified on the trusted certificate authority.",
|
||||
warning:
|
||||
"This connection is secure but the server cannot be verified on the trusted certificate authority.",
|
||||
}
|
||||
|
||||
export default {
|
||||
id: "about",
|
||||
icon: "FiInfo",
|
||||
label: "About",
|
||||
group: "bottom",
|
||||
render: () => {
|
||||
const isProduction = import.meta.env.PROD
|
||||
id: "about",
|
||||
icon: "FiInfo",
|
||||
label: "About",
|
||||
group: "bottom",
|
||||
render: () => {
|
||||
const isProduction = import.meta.env.PROD
|
||||
|
||||
const [serverManifest, setServerManifest] = React.useState(null)
|
||||
const [serverOrigin, setServerOrigin] = React.useState(null)
|
||||
const [secureConnection, setSecureConnection] = React.useState(false)
|
||||
const [capInfo, setCapInfo] = React.useState(null)
|
||||
const [serverManifest, setServerManifest] = React.useState(null)
|
||||
const [serverOrigin, setServerOrigin] = React.useState(null)
|
||||
const [secureConnection, setSecureConnection] = React.useState(false)
|
||||
const [capInfo, setCapInfo] = React.useState(null)
|
||||
|
||||
const setCapacitorInfo = async () => {
|
||||
if (Capacitor.Plugins.App) {
|
||||
const info = await Capacitor.Plugins.App.getInfo()
|
||||
const setCapacitorInfo = async () => {
|
||||
if (Capacitor) {
|
||||
if (Capacitor.Plugins.App) {
|
||||
const info = await Capacitor.Plugins.App.getInfo()
|
||||
|
||||
setCapInfo(info)
|
||||
}
|
||||
}
|
||||
setCapInfo(info)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const checkServerVersion = async () => {
|
||||
const serverManifest = await app.cores.api.customRequest()
|
||||
const checkServerVersion = async () => {
|
||||
const serverManifest = await app.cores.api.customRequest()
|
||||
|
||||
setServerManifest(serverManifest.data)
|
||||
}
|
||||
setServerManifest(serverManifest.data)
|
||||
}
|
||||
|
||||
const checkServerOrigin = async () => {
|
||||
const instance = app.cores.api.client()
|
||||
const checkServerOrigin = async () => {
|
||||
const instance = app.cores.api.client()
|
||||
|
||||
if (instance) {
|
||||
setServerOrigin(instance.mainOrigin)
|
||||
if (instance) {
|
||||
setServerOrigin(instance.mainOrigin)
|
||||
|
||||
if (instance.mainOrigin.startsWith("https")) {
|
||||
setSecureConnection(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (instance.mainOrigin.startsWith("https")) {
|
||||
setSecureConnection(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
checkServerVersion()
|
||||
checkServerOrigin()
|
||||
React.useEffect(() => {
|
||||
checkServerVersion()
|
||||
checkServerOrigin()
|
||||
|
||||
setCapacitorInfo()
|
||||
}, [])
|
||||
setCapacitorInfo()
|
||||
}, [])
|
||||
|
||||
return <div className="about_app">
|
||||
<div className="header">
|
||||
<div className="branding">
|
||||
<div className="logo">
|
||||
<img
|
||||
src={config.logo.alt}
|
||||
alt="Logo"
|
||||
/>
|
||||
</div>
|
||||
<div className="texts">
|
||||
<div className="sitename-text">
|
||||
<h2>{config.app.siteName}</h2>
|
||||
<antd.Tag>Beta</antd.Tag>
|
||||
</div>
|
||||
<span>{config.author}</span>
|
||||
<span>Licensed with {config.package?.license ?? "unlicensed"} </span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="versions">
|
||||
<antd.Tag><Icons.FiTag />v{window.app.version ?? "experimental"}</antd.Tag>
|
||||
<antd.Tag color={isProduction ? "green" : "magenta"}>
|
||||
{isProduction ? <Icons.FiCheckCircle /> : <Icons.FiTriangle />}
|
||||
{String(import.meta.env.MODE)}
|
||||
</antd.Tag>
|
||||
</div>
|
||||
</div>
|
||||
return (
|
||||
<div className="about_app">
|
||||
<div className="header">
|
||||
<div className="branding">
|
||||
<div className="logo">
|
||||
<img src={config.logo.alt} alt="Logo" />
|
||||
</div>
|
||||
<div className="texts">
|
||||
<div className="sitename-text">
|
||||
<h2>{config.app.siteName}</h2>
|
||||
<antd.Tag>Beta</antd.Tag>
|
||||
</div>
|
||||
<span>{config.author}</span>
|
||||
<span>
|
||||
Licensed with{" "}
|
||||
{config.package?.license ?? "unlicensed"}{" "}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="versions">
|
||||
<antd.Tag>
|
||||
<Icons.FiTag />v
|
||||
{window.app.version ?? "experimental"}
|
||||
</antd.Tag>
|
||||
<antd.Tag color={isProduction ? "green" : "magenta"}>
|
||||
{isProduction ? (
|
||||
<Icons.FiCheckCircle />
|
||||
) : (
|
||||
<Icons.FiTriangle />
|
||||
)}
|
||||
{String(import.meta.env.MODE)}
|
||||
</antd.Tag>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="group">
|
||||
<div className="group_header">
|
||||
<h3><Icons.FiInfo />Server information</h3>
|
||||
</div>
|
||||
<div className="group">
|
||||
<div className="group_header">
|
||||
<h3>
|
||||
<Icons.FiInfo />
|
||||
Server information
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div className="field">
|
||||
<div className="field_header">
|
||||
<h3><Icons.MdOutlineStream /> Origin</h3>
|
||||
<div className="field">
|
||||
<div className="field_header">
|
||||
<h3>
|
||||
<Icons.MdOutlineStream /> Origin
|
||||
</h3>
|
||||
|
||||
<antd.Tooltip
|
||||
title={secureConnection ? connectionsTooltipStrings.secure : connectionsTooltipStrings.insecure}
|
||||
>
|
||||
<antd.Tag
|
||||
color={secureConnection ? "green" : "red"}
|
||||
icon={secureConnection ? <Icons.MdHttps /> : <Icons.MdWarning />}
|
||||
>
|
||||
{
|
||||
secureConnection ? " Secure connection" : "Insecure connection"
|
||||
}
|
||||
</antd.Tag>
|
||||
</antd.Tooltip>
|
||||
</div>
|
||||
<antd.Tooltip
|
||||
title={
|
||||
secureConnection
|
||||
? connectionsTooltipStrings.secure
|
||||
: connectionsTooltipStrings.insecure
|
||||
}
|
||||
>
|
||||
<antd.Tag
|
||||
color={secureConnection ? "green" : "red"}
|
||||
icon={
|
||||
secureConnection ? (
|
||||
<Icons.MdHttps />
|
||||
) : (
|
||||
<Icons.MdWarning />
|
||||
)
|
||||
}
|
||||
>
|
||||
{secureConnection
|
||||
? " Secure connection"
|
||||
: "Insecure connection"}
|
||||
</antd.Tag>
|
||||
</antd.Tooltip>
|
||||
</div>
|
||||
|
||||
<div className="field_value">
|
||||
{serverOrigin ?? "Unknown"}
|
||||
</div>
|
||||
</div>
|
||||
<div className="field_value">
|
||||
{serverOrigin ?? "Unknown"}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="field">
|
||||
<div className="field_header">
|
||||
<h3><Icons.MdOutlineMemory /> Instance Performance</h3>
|
||||
</div>
|
||||
<div className="field">
|
||||
<div className="field_header">
|
||||
<h3>
|
||||
<Icons.MdOutlineMemory /> Instance Performance
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div className="field_value">
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "0.5rem",
|
||||
fontSize: "1.4rem",
|
||||
justifyContent: "space-evenly",
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<LatencyIndicator
|
||||
type="http"
|
||||
/>
|
||||
<div className="field_value">
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "0.5rem",
|
||||
fontSize: "1.4rem",
|
||||
justifyContent: "space-evenly",
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<LatencyIndicator type="http" />
|
||||
|
||||
<LatencyIndicator
|
||||
type="ws"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<LatencyIndicator type="ws" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="inline_field">
|
||||
<div className="field_header">
|
||||
<div className="field_icon">
|
||||
<Icons.MdBuild />
|
||||
</div>
|
||||
<div className="inline_field">
|
||||
<div className="field_header">
|
||||
<div className="field_icon">
|
||||
<Icons.MdBuild />
|
||||
</div>
|
||||
|
||||
<p>Version</p>
|
||||
</div>
|
||||
<p>Version</p>
|
||||
</div>
|
||||
|
||||
<div className="field_value">
|
||||
{serverManifest?.version ?? "Unknown"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="field_value">
|
||||
{serverManifest?.version ?? "Unknown"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="group">
|
||||
<h3>Thanks to our sponsors</h3>
|
||||
<SponsorsList />
|
||||
</div>
|
||||
<div className="group">
|
||||
<h3>Thanks to our sponsors</h3>
|
||||
<SponsorsList />
|
||||
</div>
|
||||
|
||||
<div className="group">
|
||||
<div className="inline_field">
|
||||
<div className="field_header">
|
||||
<div className="field_icon">
|
||||
<Icons.MdInfo />
|
||||
</div>
|
||||
<div className="group">
|
||||
<div className="inline_field">
|
||||
<div className="field_header">
|
||||
<div className="field_icon">
|
||||
<Icons.MdInfo />
|
||||
</div>
|
||||
|
||||
<p>Platform</p>
|
||||
</div>
|
||||
<p>Platform</p>
|
||||
</div>
|
||||
|
||||
<div className="field_value">
|
||||
{Capacitor.platform}
|
||||
</div>
|
||||
</div>
|
||||
<div className="field_value">Web App</div>
|
||||
</div>
|
||||
|
||||
<div className="inline_field">
|
||||
<div className="field_header">
|
||||
<div className="field_icon">
|
||||
<Icons.MdInfo />
|
||||
</div>
|
||||
<div className="inline_field">
|
||||
<div className="field_header">
|
||||
<div className="field_icon">
|
||||
<Icons.MdInfo />
|
||||
</div>
|
||||
|
||||
<p>React</p>
|
||||
</div>
|
||||
<p>React</p>
|
||||
</div>
|
||||
|
||||
<div className="field_value">
|
||||
{React.version ?? "Unknown"}
|
||||
</div>
|
||||
</div>
|
||||
<div className="field_value">
|
||||
{React.version ?? "Unknown"}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="inline_field">
|
||||
<div className="field_header">
|
||||
<div className="field_icon">
|
||||
<Icons.MdInfo />
|
||||
</div>
|
||||
<div className="inline_field">
|
||||
<div className="field_header">
|
||||
<div className="field_icon">
|
||||
<Icons.MdInfo />
|
||||
</div>
|
||||
|
||||
<p>Engine</p>
|
||||
</div>
|
||||
<p>Engine</p>
|
||||
</div>
|
||||
|
||||
<div className="field_value">
|
||||
{app.__version ?? "Unknown"}
|
||||
</div>
|
||||
</div>
|
||||
<div className="field_value">
|
||||
{app.__version ?? "Unknown"}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="inline_field">
|
||||
<div className="field_header">
|
||||
<div className="field_icon">
|
||||
<Icons.MdInfo />
|
||||
</div>
|
||||
<div className="inline_field">
|
||||
<div className="field_header">
|
||||
<div className="field_icon">
|
||||
<Icons.MdInfo />
|
||||
</div>
|
||||
|
||||
<p>Comty.js</p>
|
||||
</div>
|
||||
<p>Comty.js</p>
|
||||
</div>
|
||||
|
||||
<div className="field_value">
|
||||
{__comty_shared_state.version ?? "Unknown"}
|
||||
</div>
|
||||
</div>
|
||||
<div className="field_value">
|
||||
{__comty_shared_state.version ?? "Unknown"}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{
|
||||
capInfo && <div className="inline_field">
|
||||
<div className="field_header">
|
||||
<div className="field_icon">
|
||||
<Icons.MdInfo />
|
||||
</div>
|
||||
{capInfo && (
|
||||
<div className="inline_field">
|
||||
<div className="field_header">
|
||||
<div className="field_icon">
|
||||
<Icons.MdInfo />
|
||||
</div>
|
||||
|
||||
<p>App ID</p>
|
||||
</div>
|
||||
<p>App ID</p>
|
||||
</div>
|
||||
|
||||
<div className="field_value">
|
||||
{capInfo.id}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<div className="field_value">{capInfo.id}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{
|
||||
capInfo && <div className="inline_field">
|
||||
<div className="field_header">
|
||||
<div className="field_icon">
|
||||
<Icons.MdInfo />
|
||||
</div>
|
||||
{capInfo && (
|
||||
<div className="inline_field">
|
||||
<div className="field_header">
|
||||
<div className="field_icon">
|
||||
<Icons.MdInfo />
|
||||
</div>
|
||||
|
||||
<p>App Build</p>
|
||||
</div>
|
||||
<p>App Build</p>
|
||||
</div>
|
||||
|
||||
<div className="field_value">
|
||||
{capInfo.build}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<div className="field_value">{capInfo.build}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{
|
||||
capInfo && <div className="inline_field">
|
||||
<div className="field_header">
|
||||
<div className="field_icon">
|
||||
<Icons.MdInfo />
|
||||
</div>
|
||||
{capInfo && (
|
||||
<div className="inline_field">
|
||||
<div className="field_header">
|
||||
<div className="field_icon">
|
||||
<Icons.MdInfo />
|
||||
</div>
|
||||
|
||||
<p>App Version</p>
|
||||
</div>
|
||||
<p>App Version</p>
|
||||
</div>
|
||||
|
||||
<div className="field_value">
|
||||
{capInfo.version}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<div className="field_value">{capInfo.version}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="inline_field">
|
||||
<div className="field_header">
|
||||
<div className="field_icon">
|
||||
<Icons.MdInfo />
|
||||
</div>
|
||||
<div className="inline_field">
|
||||
<div className="field_header">
|
||||
<div className="field_icon">
|
||||
<Icons.MdInfo />
|
||||
</div>
|
||||
|
||||
<p>View Open Source Licenses</p>
|
||||
</div>
|
||||
<p>View Open Source Licenses</p>
|
||||
</div>
|
||||
|
||||
<div className="field_value">
|
||||
<antd.Button
|
||||
icon={<Icons.MdOpenInNew />}
|
||||
onClick={() => app.location.push("/licenses")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
<div className="field_value">
|
||||
<antd.Button
|
||||
icon={<Icons.MdOpenInNew />}
|
||||
onClick={() => app.location.push("/licenses")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
@ -11,70 +11,46 @@ const oneYearInSeconds = 60 * 60 * 24 * 365
|
||||
const sslDirPath = path.join(__dirname, ".ssl")
|
||||
|
||||
const config = {
|
||||
plugins: [
|
||||
react(),
|
||||
],
|
||||
resolve: {
|
||||
alias: aliases,
|
||||
},
|
||||
server: {
|
||||
host: "0.0.0.0",
|
||||
port: 8000,
|
||||
fs: {
|
||||
allow: ["..", "../../"],
|
||||
},
|
||||
headers: {
|
||||
"Strict-Transport-Security": `max-age=${oneYearInSeconds}`
|
||||
},
|
||||
proxy: {
|
||||
"/api": {
|
||||
target: backendUri,
|
||||
rewrite: (path) => path.replace(/^\/api/, ""),
|
||||
hostRewrite: true,
|
||||
changeOrigin: true,
|
||||
xfwd: true,
|
||||
ws: true,
|
||||
toProxy: true,
|
||||
secure: false,
|
||||
}
|
||||
}
|
||||
},
|
||||
css: {
|
||||
preprocessorOptions: {
|
||||
less: {
|
||||
javascriptEnabled: true,
|
||||
}
|
||||
}
|
||||
},
|
||||
optimizeDeps: {
|
||||
esbuildOptions: {
|
||||
target: "esnext"
|
||||
}
|
||||
},
|
||||
build: {
|
||||
target: "esnext",
|
||||
rollupOptions: {
|
||||
output: {
|
||||
manualChunks(id) {
|
||||
if (id.includes("node_modules")) {
|
||||
return id.toString().split("node_modules/")[1].split("/")[0].toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
esbuild: {
|
||||
supported: {
|
||||
"top-level-await": true
|
||||
},
|
||||
}
|
||||
plugins: [react()],
|
||||
resolve: {
|
||||
alias: aliases,
|
||||
},
|
||||
server: {
|
||||
host: "0.0.0.0",
|
||||
port: 8000,
|
||||
fs: {
|
||||
allow: ["..", "../../"],
|
||||
},
|
||||
headers: {
|
||||
"Strict-Transport-Security": `max-age=${oneYearInSeconds}`,
|
||||
},
|
||||
proxy: {
|
||||
"/api": {
|
||||
target: backendUri,
|
||||
rewrite: (path) => path.replace(/^\/api/, ""),
|
||||
hostRewrite: true,
|
||||
changeOrigin: true,
|
||||
xfwd: true,
|
||||
ws: true,
|
||||
toProxy: true,
|
||||
secure: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
css: {
|
||||
preprocessorOptions: {
|
||||
less: {
|
||||
javascriptEnabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if (fs.existsSync(sslDirPath)) {
|
||||
config.server.https = {
|
||||
key: path.join(__dirname, ".ssl", "privkey.pem"),
|
||||
cert: path.join(__dirname, ".ssl", "cert.pem"),
|
||||
}
|
||||
config.server.https = {
|
||||
key: path.join(__dirname, ".ssl", "privkey.pem"),
|
||||
cert: path.join(__dirname, ".ssl", "cert.pem"),
|
||||
}
|
||||
}
|
||||
|
||||
export default defineConfig(config)
|
||||
export default defineConfig(config)
|
||||
|
Loading…
x
Reference in New Issue
Block a user